mirror of
https://github.com/esphome/esphome.git
synced 2026-07-01 04:56:09 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb2e8320f0 | |||
| 49ef5f9ad6 | |||
| 707f3f4749 | |||
| ec420d5792 | |||
| 17209df7b5 | |||
| 9cf9b02ba2 | |||
| c90fa2378a | |||
| c04dfa922e | |||
| 668007707d | |||
| ab71f5276f | |||
| d062f62656 | |||
| 03db32d045 | |||
| 8f6d489a9a | |||
| dd07fba943 | |||
| 6f5d642a31 | |||
| 2721f08bcc | |||
| eafc5df3f2 | |||
| 46d0c29be5 | |||
| abdbbf4dd2 | |||
| 4dc0599a7d | |||
| 52c35ec09c | |||
| 76490e45bc | |||
| 0a8130858c |
+1
-1
@@ -1 +1 @@
|
||||
10c432ae818f9ed7fd4a0176a04467b1f2634363f5ec985045a6d72747f60b90
|
||||
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
|
||||
|
||||
MANAGED_LABELS: [
|
||||
'new-component',
|
||||
|
||||
@@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
return { labels, deprecatedInfo };
|
||||
}
|
||||
|
||||
// Strategy: Detect when maintainers cannot modify the PR branch
|
||||
function detectMaintainerAccess(context) {
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Only relevant for cross-repo PRs (forks)
|
||||
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pr.maintainer_can_modify) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOrgFork = pr.head.repo.owner.type === 'Organization';
|
||||
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
|
||||
return { isOrgFork, orgName: pr.head.repo.owner.login };
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels, prFiles, context) {
|
||||
const labels = new Set();
|
||||
@@ -329,5 +347,6 @@ module.exports = {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
};
|
||||
|
||||
@@ -12,9 +12,10 @@ const {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
} = require('./detectors');
|
||||
const { handleReviews } = require('./reviews');
|
||||
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
|
||||
const { applyLabels, removeOldLabels } = require('./labels');
|
||||
|
||||
// Fetch API data
|
||||
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
|
||||
codeOwnerLabels,
|
||||
testLabels,
|
||||
checkboxLabels,
|
||||
deprecatedResult
|
||||
deprecatedResult,
|
||||
maintainerAccess
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(context),
|
||||
detectComponentPlatforms(changedFiles, apiData),
|
||||
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
|
||||
detectCodeOwner(github, context, changedFiles),
|
||||
detectTests(changedFiles),
|
||||
detectPRTemplateCheckboxes(context),
|
||||
detectDeprecatedComponents(github, context, changedFiles)
|
||||
detectDeprecatedComponents(github, context, changedFiles),
|
||||
detectMaintainerAccess(context)
|
||||
]);
|
||||
|
||||
// Extract deprecated component info
|
||||
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
// Handle reviews and org fork comment
|
||||
await Promise.all([
|
||||
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
|
||||
handleMaintainerAccessComment(github, context, maintainerAccess)
|
||||
]);
|
||||
|
||||
// Apply labels
|
||||
await applyLabels(github, context, finalLabels);
|
||||
|
||||
@@ -2,7 +2,8 @@ const {
|
||||
BOT_COMMENT_MARKER,
|
||||
CODEOWNERS_MARKER,
|
||||
TOO_BIG_MARKER,
|
||||
DEPRECATED_COMPONENT_MARKER
|
||||
DEPRECATED_COMPONENT_MARKER,
|
||||
ORG_FORK_MARKER
|
||||
} = require('./constants');
|
||||
|
||||
// Generate review messages
|
||||
@@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
|
||||
}
|
||||
}
|
||||
|
||||
// Handle maintainer access warning comment
|
||||
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
|
||||
if (!maintainerAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Check if we already posted the warning (iterate pages to exit early)
|
||||
let existingComment;
|
||||
for await (const { data: comments } of github.paginate.iterator(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: pr_number }
|
||||
)) {
|
||||
existingComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body && comment.body.includes(ORG_FORK_MARKER)
|
||||
);
|
||||
if (existingComment) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingComment) {
|
||||
console.log('Maintainer access warning comment already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (maintainerAccess.isOrgFork) {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
|
||||
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
|
||||
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
|
||||
} else {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body
|
||||
});
|
||||
console.log('Created maintainer access warning comment');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleReviews
|
||||
handleReviews,
|
||||
handleMaintainerAccessComment
|
||||
};
|
||||
|
||||
@@ -868,7 +868,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -954,7 +955,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
|
||||
@@ -2,30 +2,29 @@ name: Status check labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check ${{ matrix.label }}
|
||||
name: Check blocking labels
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
label:
|
||||
- needs-docs
|
||||
- merge-after-release
|
||||
- chained-pr
|
||||
steps:
|
||||
- name: Check for ${{ matrix.label }} label
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
|
||||
if (hasLabel) {
|
||||
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
|
||||
const labelNames = labels.map(l => l.name);
|
||||
const found = blockingLabels.filter(bl => labelNames.includes(bl));
|
||||
if (found.length > 0) {
|
||||
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.9
|
||||
rev: v0.15.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.4.1
|
||||
PROJECT_NUMBER = 2026.5.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
+1
-8
@@ -750,15 +750,8 @@ def upload_using_esptool(
|
||||
platformio_api.FlashImage(
|
||||
path=idedata.firmware_bin_path, offset=firmware_offset
|
||||
),
|
||||
*idedata.extra_flash_images,
|
||||
]
|
||||
for image in idedata.extra_flash_images:
|
||||
if not image.path.is_file():
|
||||
_LOGGER.warning(
|
||||
"Skipping missing flash image declared by platform: %s",
|
||||
image.path,
|
||||
)
|
||||
continue
|
||||
flash_images.append(image)
|
||||
|
||||
mcu = "esp8266"
|
||||
if CORE.is_esp32:
|
||||
|
||||
+6
-72
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = Path(CORE.config_dir).resolve()
|
||||
self._config_path = Path(CORE.config_path).resolve()
|
||||
self._config_dir = CORE.config_dir
|
||||
self._config_path = CORE.config_path
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
@@ -258,36 +258,21 @@ class ConfigBundleCreator:
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
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:
|
||||
data = yaml_util.load_yaml(self._config_path)
|
||||
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():
|
||||
@@ -623,57 +608,6 @@ 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,11 +2,7 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
require_adc_oneshot_iram,
|
||||
)
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -28,7 +24,6 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
@@ -70,13 +65,6 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def _require_adc_iram(config: ConfigType) -> ConfigType:
|
||||
"""Register ADC oneshot IRAM requirement during config validation."""
|
||||
if CORE.is_esp32:
|
||||
require_adc_oneshot_iram()
|
||||
return config
|
||||
|
||||
|
||||
ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
@@ -107,7 +95,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
validate_config,
|
||||
_require_adc_iram,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ade7953_base {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
|
||||
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
|
||||
|
||||
static const float ADE_POWER_FACTOR = 154.0f;
|
||||
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
|
||||
|
||||
@@ -18,7 +21,12 @@ void ADE7953::setup() {
|
||||
|
||||
// The chip might take up to 100ms to initialise
|
||||
this->set_timeout(100, [this]() {
|
||||
// this->ade_write_8(0x0010, 0x04);
|
||||
// Lock communication interface (SPI or I2C)
|
||||
uint16_t config_v = CONFIG_DEFAULT;
|
||||
this->ade_read_16(CONFIG_16, &config_v);
|
||||
config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit
|
||||
this->ade_write_16(CONFIG_16, config_v);
|
||||
// Configure optimum settings according to datasheet
|
||||
this->ade_write_8(0x00FE, 0xAD);
|
||||
this->ade_write_16(0x0120, 0x0030);
|
||||
// Set gains
|
||||
|
||||
@@ -9,31 +9,35 @@
|
||||
namespace esphome {
|
||||
namespace ade7953_base {
|
||||
|
||||
static const uint8_t PGA_V_8 =
|
||||
static constexpr uint8_t PGA_V_8 =
|
||||
0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IA_8 =
|
||||
static constexpr uint8_t PGA_IA_8 =
|
||||
0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IB_8 =
|
||||
static constexpr uint8_t PGA_IB_8 =
|
||||
0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0])
|
||||
|
||||
static const uint32_t AIGAIN_32 =
|
||||
static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register
|
||||
|
||||
static constexpr uint16_t AIGAIN_32 =
|
||||
0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t AWGAIN_32 =
|
||||
static constexpr uint16_t AVGAIN_32 =
|
||||
0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t AWGAIN_32 =
|
||||
0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVARGAIN_32 =
|
||||
static constexpr uint16_t AVARGAIN_32 =
|
||||
0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVAGAIN_32 =
|
||||
static constexpr uint16_t AVAGAIN_32 =
|
||||
0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit)
|
||||
|
||||
static const uint32_t BIGAIN_32 =
|
||||
static constexpr uint16_t BIGAIN_32 =
|
||||
0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t BWGAIN_32 =
|
||||
static constexpr uint16_t BVGAIN_32 =
|
||||
0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t BWGAIN_32 =
|
||||
0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVARGAIN_32 =
|
||||
static constexpr uint16_t BVARGAIN_32 =
|
||||
0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVAGAIN_32 =
|
||||
static constexpr uint16_t BVAGAIN_32 =
|
||||
0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit)
|
||||
|
||||
class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ade7953_spi {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
|
||||
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
|
||||
|
||||
void AdE7953Spi::setup() {
|
||||
this->spi_setup();
|
||||
ade7953_base::ADE7953::setup();
|
||||
@@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) {
|
||||
this->write_byte16(reg);
|
||||
this->transfer_byte(0);
|
||||
this->write_byte16(value);
|
||||
if (reg == ade7953_base::CONFIG_16) {
|
||||
delayMicroseconds(CONFIG_LOCK_SETTLE_US);
|
||||
}
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace esphome {
|
||||
namespace ade7953_spi {
|
||||
|
||||
class AdE7953Spi : public ade7953_base::ADE7953,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
@@ -671,7 +671,6 @@ message SensorStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SENSOR";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float state = 2;
|
||||
@@ -778,10 +777,9 @@ message SubscribeLogsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (log) = false;
|
||||
option (no_delay) = false;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
LogLevel level = 1 [(force) = true];
|
||||
bytes message = 3 [(force) = true];
|
||||
LogLevel level = 1;
|
||||
bytes message = 3;
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
@@ -1640,7 +1638,6 @@ message BluetoothLERawAdvertisementsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
|
||||
}
|
||||
|
||||
@@ -195,7 +195,10 @@ class APIFrameHelper {
|
||||
}
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
// Check if socket has buffered data ready to read.
|
||||
// Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
|
||||
// or track that they stopped early and retry without this check.
|
||||
// See Socket::ready() for details.
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
|
||||
@@ -23,7 +23,6 @@ extend google.protobuf.MessageOptions {
|
||||
optional bool no_delay = 1040 [default=false];
|
||||
optional string base_class = 1041;
|
||||
optional bool inline_encode = 1042 [default=false];
|
||||
optional bool speed_optimized = 1043 [default=false];
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
|
||||
@@ -745,9 +745,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
|
||||
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
|
||||
@@ -757,9 +755,7 @@ SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) c
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SensorStateResponse::calculate_size() const {
|
||||
uint32_t SensorStateResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 5;
|
||||
size += ProtoSize::calc_float(1, this->state);
|
||||
@@ -916,22 +912,16 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
|
||||
}
|
||||
return true;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
|
||||
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 2;
|
||||
size += ProtoSize::calc_length_force(1, this->message_len_);
|
||||
size += this->level ? 2 : 0;
|
||||
size += ProtoSize::calc_length(1, this->message_len_);
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -2338,9 +2328,7 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
@@ -2362,9 +2350,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
|
||||
@@ -298,15 +298,33 @@ constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
|
||||
class ProtoEncode {
|
||||
public:
|
||||
/// Write a multi-byte varint directly through a pos pointer.
|
||||
/// Unrolled based on the compile-time max varint length for T
|
||||
/// (5 bytes for uint32_t, 10 bytes for uint64_t). The explicit unroll gives
|
||||
/// the compiler straight-line code with early exits instead of a
|
||||
/// data-dependent back-edge branch, which measurably speeds up BLE raw
|
||||
/// advertisement MAC encoding (always 7-byte varints) among other hot paths.
|
||||
///
|
||||
/// Takes/returns pos by value so this function can remain out-of-line without
|
||||
/// forcing the caller to spill pos to memory on every byte write. Callers
|
||||
/// (which are ALWAYS_INLINE) should assign the returned pos back to their
|
||||
/// local. Code size win vs. inlining the whole unroll at every call site.
|
||||
template<typename T>
|
||||
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) {
|
||||
do {
|
||||
static uint8_t *encode_varint_raw_loop(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, T value) {
|
||||
constexpr int MAX_VARINT_BYTES = (sizeof(T) * 8 + 6) / 7; // 5 for u32, 10 for u64
|
||||
#pragma GCC unroll 10
|
||||
for (int i = 0; i < MAX_VARINT_BYTES - 1; i++) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value | 0x80);
|
||||
*pos++ = static_cast<uint8_t>(value) | 0x80;
|
||||
value >>= 7;
|
||||
} while (value > 0x7F);
|
||||
if (value <= 0x7F) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return pos;
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t value) {
|
||||
@@ -315,7 +333,7 @@ class ProtoEncode {
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
@@ -331,7 +349,7 @@ class ProtoEncode {
|
||||
*pos++ = static_cast<uint8_t>(value >> 7);
|
||||
return;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
@@ -340,7 +358,7 @@ class ProtoEncode {
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t type) {
|
||||
@@ -402,7 +420,7 @@ class ProtoEncode {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
|
||||
*pos++ = static_cast<uint8_t>(len);
|
||||
} else {
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
}
|
||||
std::memcpy(pos, string, len);
|
||||
|
||||
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
|
||||
#endif
|
||||
float get_reference_voltage(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
#else
|
||||
return 120.0; // Default voltage
|
||||
#endif
|
||||
}
|
||||
float get_reference_current(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
#else
|
||||
return 5.0f; // Default current
|
||||
#endif
|
||||
|
||||
@@ -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(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
if (!rtc_time.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
||||
await cg.register_parented(var, config[CONF_CANBUS_ID])
|
||||
|
||||
if (can_id := config.get(CONF_CAN_ID)) is not None:
|
||||
can_id = await cg.templatable(can_id, args, cg.uint32)
|
||||
cg.add(var.set_can_id(can_id))
|
||||
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ CC1101Component::CC1101Component() {
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
}
|
||||
|
||||
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
this->cs_->digital_write(true);
|
||||
@@ -150,12 +148,11 @@ void CC1101Component::setup() {
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
|
||||
}
|
||||
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +164,6 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
this->disable_loop();
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
return;
|
||||
@@ -248,7 +244,6 @@ void CC1101Component::begin_tx() {
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
|
||||
@@ -678,12 +673,10 @@ void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
if (value) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
} else {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
}
|
||||
if (value) {
|
||||
this->enable_loop();
|
||||
} else {
|
||||
this->disable_loop();
|
||||
}
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
|
||||
@@ -93,7 +93,6 @@ class CC1101Component : public Component,
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
|
||||
@@ -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(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
if (!rtc_time.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -671,12 +671,11 @@ def _is_framework_url(source: str) -> bool:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 8),
|
||||
"latest": cv.Version(3, 3, 8),
|
||||
"dev": cv.Version(3, 3, 8),
|
||||
"recommended": cv.Version(3, 3, 7),
|
||||
"latest": cv.Version(3, 3, 7),
|
||||
"dev": cv.Version(3, 3, 7),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
@@ -696,7 +695,6 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
|
||||
cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"),
|
||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||
@@ -716,15 +714,17 @@ ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(5, 5, 4),
|
||||
"latest": cv.Version(5, 5, 4),
|
||||
"recommended": cv.Version(5, 5, 3, "1"),
|
||||
"latest": cv.Version(5, 5, 3, "1"),
|
||||
"dev": cv.Version(5, 5, 4),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
6, 0, 0
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(
|
||||
5, 5, 4
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
@@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 38, "1"),
|
||||
"latest": cv.Version(55, 3, 38, "1"),
|
||||
"recommended": cv.Version(55, 3, 37),
|
||||
"latest": cv.Version(55, 3, 37),
|
||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,6 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
||||
|
||||
# VFS requirement tracking
|
||||
# Components that need VFS features can call require_vfs_*() functions
|
||||
@@ -1072,7 +1071,6 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
@@ -1170,17 +1168,6 @@ def require_fatfs() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_adc_oneshot_iram() -> None:
|
||||
"""Mark that ADC oneshot IRAM safety is required by a component.
|
||||
|
||||
Call this from components that use the ADC oneshot driver. When flash cache is
|
||||
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
|
||||
the ADC oneshot read function must be in IRAM to avoid crashes.
|
||||
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
|
||||
|
||||
|
||||
def _parse_idf_component(value: str) -> ConfigType:
|
||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||
# Match operator followed by version-like string (digit or *)
|
||||
@@ -1222,7 +1209,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, string=True
|
||||
*ESP32_CHIP_REVISIONS
|
||||
),
|
||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||
@@ -1281,7 +1268,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
@@ -2082,16 +2068,6 @@ async def to_code(config):
|
||||
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
|
||||
|
||||
# Place ADC oneshot control functions in IRAM for cache safety
|
||||
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
|
||||
# power management, etc.), ADC reads will crash if these functions are in flash.
|
||||
# Components using ADC call require_adc_oneshot_iram() to force this.
|
||||
if (
|
||||
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
|
||||
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
|
||||
|
||||
# Disable FATFS support
|
||||
# Components that need FATFS (SD card, etc.) can call require_fatfs()
|
||||
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
|
||||
|
||||
@@ -1960,10 +1960,6 @@ BOARDS = {
|
||||
"name": "Hornbill ESP32 Minima",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"huidu_hd_wf1": {
|
||||
"name": "Huidu HD-WF1",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"huidu_hd_wf2": {
|
||||
"name": "Huidu HD-WF2",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2032,10 +2028,6 @@ BOARDS = {
|
||||
"name": "LilyGo T-Display-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lilygo-t-energy-s3": {
|
||||
"name": "LilyGo T-Energy-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lilygo-t3-s3": {
|
||||
"name": "LilyGo T3-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2297,18 +2289,10 @@ BOARDS = {
|
||||
"name": "S.ODI Ultra v1",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"seeed_xiao_esp32_s3_plus": {
|
||||
"name": "Seeed Studio XIAO ESP32S3 Plus",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"seeed_xiao_esp32c3": {
|
||||
"name": "Seeed Studio XIAO ESP32C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"seeed_xiao_esp32c5": {
|
||||
"name": "Seeed Studio XIAO ESP32C5",
|
||||
"variant": VARIANT_ESP32C5,
|
||||
},
|
||||
"seeed_xiao_esp32c6": {
|
||||
"name": "Seeed Studio XIAO ESP32C6",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
|
||||
@@ -172,16 +172,10 @@ def validate_gpio_pin(pin):
|
||||
exc,
|
||||
)
|
||||
else:
|
||||
# `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.
|
||||
# Throw an exception if used for a pin that would not have resulted
|
||||
# in a validation error anyway!
|
||||
if ignore_pin_validation_warning:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d has no validation errors to ignore; "
|
||||
"remove `ignore_pin_validation_error: true` from this pin.",
|
||||
pin[CONF_NUMBER],
|
||||
)
|
||||
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
|
||||
|
||||
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_{SPI2_HOST};
|
||||
spi_host_device_t interface_{SPI3_HOST};
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
uint32_t polling_interval_{0};
|
||||
#endif
|
||||
|
||||
@@ -108,13 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
# Use the global's value_type alias as the lambda return type so
|
||||
# TemplatableFn stores a direct function pointer instead of going through
|
||||
# the deprecated converting trampoline when the value expression deduces
|
||||
# to a different type (e.g. int literal assigned to a float global).
|
||||
value_type = cg.RawExpression(f"{full_id.type}::value_type")
|
||||
templ = await cg.templatable(
|
||||
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
|
||||
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
|
||||
)
|
||||
cg.add(var.set_value(templ))
|
||||
return var
|
||||
|
||||
@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
|
||||
)
|
||||
|
||||
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
|
||||
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
|
||||
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
|
||||
|
||||
|
||||
def _validate_esp32_variant(config):
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace ili9xxx {
|
||||
|
||||
|
||||
@@ -229,10 +229,6 @@ 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,6 +28,7 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -675,16 +676,12 @@ def _final_validate(config):
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
config = config.copy()
|
||||
for c in config:
|
||||
if byte_order := c.get(CONF_BYTE_ORDER):
|
||||
if byte_order == "BIG_ENDIAN":
|
||||
_LOGGER.warning(
|
||||
"The image '%s' is configured with big-endian byte order, little-endian is expected",
|
||||
c.get(CONF_FILE),
|
||||
)
|
||||
else:
|
||||
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
|
||||
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"
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -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 + 1), progmem_read_byte(pos));
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
|
||||
@@ -58,12 +58,6 @@ void AddressableLightTransformer::start() {
|
||||
// our transition will handle brightness, disable brightness in correction.
|
||||
this->light_.correction_.set_local_brightness(255);
|
||||
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
|
||||
|
||||
// Uniformity scan is deferred to the first apply() call. start() can run before the underlying
|
||||
// LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
|
||||
// triggering a transition), and reading through ESPColorView would deref a null buffer.
|
||||
this->uniform_start_scanned_ = false;
|
||||
this->uniform_start_is_uniform_ = false;
|
||||
}
|
||||
|
||||
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
|
||||
@@ -103,57 +97,12 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||
// non-linear when applying small deltas.
|
||||
|
||||
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
|
||||
// Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
|
||||
// frame buffer is valid. When every LED already has the same color (the common case: plain
|
||||
// turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
|
||||
// Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
|
||||
// quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
|
||||
// round to stored 0, freezing progress).
|
||||
if (!this->uniform_start_scanned_) {
|
||||
this->uniform_start_scanned_ = true;
|
||||
if (this->light_.size() > 0) {
|
||||
Color first = this->light_[0].get();
|
||||
bool uniform = true;
|
||||
for (int32_t i = 1; i < this->light_.size(); i++) {
|
||||
if (this->light_[i].get() != first) {
|
||||
uniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (uniform) {
|
||||
this->uniform_start_color_ = first;
|
||||
this->uniform_start_is_uniform_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->uniform_start_is_uniform_) {
|
||||
// All LEDs started at the same color: compute the interpolated value once and write it to
|
||||
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
|
||||
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
|
||||
//
|
||||
// Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
|
||||
// overwritten on the next apply() here. The fallback path below would have respected them
|
||||
// via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
|
||||
// support, so this is acceptable.
|
||||
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
|
||||
const Color &start = this->uniform_start_color_;
|
||||
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
|
||||
uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
|
||||
uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
|
||||
uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
|
||||
uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(r, g, b, w);
|
||||
}
|
||||
} else {
|
||||
int32_t scale =
|
||||
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
|
||||
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
|
||||
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
|
||||
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
|
||||
}
|
||||
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
|
||||
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
|
||||
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
|
||||
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
|
||||
}
|
||||
this->last_transition_progress_ = smoothed_progress;
|
||||
this->light_.schedule_show();
|
||||
|
||||
@@ -115,9 +115,6 @@ class AddressableLightTransformer : public LightTransformer {
|
||||
AddressableLight &light_;
|
||||
float last_transition_progress_{0.0f};
|
||||
Color target_color_{};
|
||||
Color uniform_start_color_{};
|
||||
bool uniform_start_scanned_{false};
|
||||
bool uniform_start_is_uniform_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -44,7 +44,6 @@ 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
|
||||
@@ -342,7 +341,7 @@ async def to_code(configs):
|
||||
df.LOGGER.info("LVGL will use hardware rotation via display driver")
|
||||
else:
|
||||
rotation_type = RotationType.ROTATION_SOFTWARE
|
||||
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32P4:
|
||||
if get_esp32_variant() == VARIANT_ESP32P4:
|
||||
df.LOGGER.info("LVGL will use software rotation (PPA accelerated)")
|
||||
else:
|
||||
df.LOGGER.info("LVGL will use software rotation")
|
||||
@@ -452,8 +451,7 @@ 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)
|
||||
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
|
||||
clean_build(clear_pio_cache=False)
|
||||
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
|
||||
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()
|
||||
|
||||
@@ -642,28 +642,26 @@ 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() % width;
|
||||
int32_t col = random_uint32() % this->width_;
|
||||
col = col / this->draw_rounding * this->draw_rounding;
|
||||
int32_t row = random_uint32() % height;
|
||||
int32_t row = random_uint32() % this->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{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1};
|
||||
lv_area_t area{col, row, col + size - 1, row + size - 1};
|
||||
// clip to display bounds just in case
|
||||
if (area.x2 >= width)
|
||||
area.x2 = width - 1;
|
||||
if (area.y2 >= height)
|
||||
area.y2 = height - 1;
|
||||
if (area.x2 >= this->width_)
|
||||
area.x2 = this->width_ - 1;
|
||||
if (area.y2 >= this->height_)
|
||||
area.y2 = this->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++) {
|
||||
reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32();
|
||||
((uint32_t *) (this->draw_buf_))[i] = random_uint32();
|
||||
}
|
||||
this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_));
|
||||
this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,17 +76,16 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
|
||||
#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
|
||||
// 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()); }
|
||||
|
||||
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);
|
||||
}
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -77,11 +77,8 @@ 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"):
|
||||
await w.set_property(
|
||||
"bg_" + prop, await validator.process(config.get(prop))
|
||||
)
|
||||
else:
|
||||
await w.set_property(prop, config, processor=validator)
|
||||
prop = "bg_" + prop
|
||||
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)
|
||||
|
||||
@@ -170,7 +170,7 @@ async def to_code(config):
|
||||
cg.add_library("LEAmDNS", None)
|
||||
|
||||
if CORE.is_esp32:
|
||||
add_idf_component(name="espressif/mdns", ref="1.11.0")
|
||||
add_idf_component(name="espressif/mdns", ref="1.10.0")
|
||||
|
||||
cg.add_define("USE_MDNS")
|
||||
|
||||
|
||||
@@ -451,8 +451,6 @@ async def to_code(config):
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
|
||||
# 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")
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
|
||||
@@ -29,6 +29,14 @@ void VADModel::log_model_config() {
|
||||
bool StreamingModel::load_model_() {
|
||||
RAMAllocator<uint8_t> arena_allocator;
|
||||
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->var_arena_ == nullptr) {
|
||||
this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
if (this->var_arena_ == nullptr) {
|
||||
@@ -45,26 +53,6 @@ bool StreamingModel::load_model_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Probe for the actual required tensor arena size if not yet determined
|
||||
if (!this->tensor_arena_size_probed_) {
|
||||
size_t probed_size = this->probe_arena_size_();
|
||||
if (probed_size > 0) {
|
||||
ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size);
|
||||
this->tensor_arena_size_ = probed_size;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_);
|
||||
}
|
||||
this->tensor_arena_size_probed_ = true;
|
||||
}
|
||||
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->interpreter_ == nullptr) {
|
||||
this->interpreter_ =
|
||||
make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_,
|
||||
@@ -106,70 +94,6 @@ bool StreamingModel::load_model_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t StreamingModel::probe_arena_size_() {
|
||||
RAMAllocator<uint8_t> arena_allocator;
|
||||
|
||||
// Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different
|
||||
// versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct,
|
||||
// and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16
|
||||
// bytes.
|
||||
size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15,
|
||||
(this->tensor_arena_size_ * 2 + 15) & ~15};
|
||||
|
||||
for (size_t attempt_size : attempt_sizes) {
|
||||
uint8_t *probe_arena = arena_allocator.allocate(attempt_size);
|
||||
if (probe_arena == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify the model works at all with this arena size
|
||||
auto probe_interpreter = make_unique<tflite::MicroInterpreter>(
|
||||
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_);
|
||||
|
||||
if (probe_interpreter->AllocateTensors() != kTfLiteOk) {
|
||||
probe_interpreter.reset();
|
||||
arena_allocator.deallocate(probe_arena, attempt_size);
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment).
|
||||
// If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds.
|
||||
size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15;
|
||||
probe_interpreter.reset();
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
|
||||
size_t upper = attempt_size;
|
||||
|
||||
while (lower < upper) {
|
||||
auto test_interpreter = make_unique<tflite::MicroInterpreter>(
|
||||
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_);
|
||||
|
||||
bool ok = test_interpreter->AllocateTensors() == kTfLiteOk;
|
||||
|
||||
test_interpreter.reset();
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
|
||||
if (ok) {
|
||||
// Found a working size smaller than the full arena
|
||||
upper = lower + 16; // Pad by 16 bytes to be safe for future allocations
|
||||
break;
|
||||
}
|
||||
|
||||
// Try the midpoint between current attempt and full size
|
||||
lower = ((lower + upper) / 2 + 15) & ~15;
|
||||
}
|
||||
|
||||
arena_allocator.deallocate(probe_arena, attempt_size);
|
||||
return upper;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void StreamingModel::unload_model() {
|
||||
this->interpreter_.reset();
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@ class StreamingModel {
|
||||
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
|
||||
/// @return True if successful, false otherwise
|
||||
bool load_model_();
|
||||
/// @brief Probes the actual required tensor arena size by trial allocation.
|
||||
/// Tries the manifest size first, then 2x if that fails.
|
||||
/// @return The required arena size rounded up to 16-byte alignment, or 0 on failure.
|
||||
size_t probe_arena_size_();
|
||||
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
|
||||
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
|
||||
|
||||
@@ -74,7 +70,6 @@ class StreamingModel {
|
||||
|
||||
bool loaded_{false};
|
||||
bool enabled_{true};
|
||||
bool tensor_arena_size_probed_{false};
|
||||
bool unprocessed_probability_status_{false};
|
||||
uint8_t current_stride_step_{0};
|
||||
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
|
||||
|
||||
@@ -28,8 +28,7 @@ void AirConditioner::on_status_change() {
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
|
||||
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
|
||||
// Read existing presets (set by codegen), append frost protection, write back
|
||||
auto traits = this->get_traits();
|
||||
const auto &existing = traits.get_supported_custom_presets();
|
||||
const auto &existing = this->get_traits().get_supported_custom_presets();
|
||||
bool found = false;
|
||||
for (const char *p : existing) {
|
||||
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
|
||||
|
||||
@@ -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, 1): power_of_two,
|
||||
model.option(CONF_DRAW_ROUNDING, 2): 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 // 4) / buffer_size
|
||||
fraction = min(20000.0, buffer_size // 16) / buffer_size
|
||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||
(x for x in range(2, 8) if fraction >= 1 / x), 8
|
||||
x for x in range(2, 17) if fraction >= 1 / x
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -234,9 +234,9 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
|
||||
void dump_config() override {
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
|
||||
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
|
||||
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
|
||||
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
HAS_HARDWARE_ROTATION);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ class MipiSpi : public display::Display,
|
||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||
|
||||
// calculate new madctl value from base value adjusted for rotation
|
||||
uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only
|
||||
uint8_t madctl = MADCTL; // lower 8 bits only
|
||||
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
|
||||
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
|
||||
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
|
||||
@@ -546,12 +546,13 @@ 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,
|
||||
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_) {
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
|
||||
this->start_line_ += this->get_height_internal() / FRACTION) {
|
||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||
auto lap = millis();
|
||||
#endif
|
||||
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
|
||||
this->end_line_ =
|
||||
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
}
|
||||
@@ -573,13 +574,12 @@ 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_ = round_buffer(this->x_high_ + 1) - 1;
|
||||
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
|
||||
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 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->x_low_);
|
||||
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = this->get_width_internal();
|
||||
this->y_low_ = this->get_height_internal();
|
||||
|
||||
@@ -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_HEAT_COOL},
|
||||
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
|
||||
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,13 +76,23 @@ void MitsubishiCN105Climate::loop() {
|
||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
climate::ClimateTraits traits;
|
||||
|
||||
for (const auto &p : MODE_MAP) {
|
||||
traits.add_supported_mode(p.second);
|
||||
}
|
||||
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 : FAN_MODE_MAP) {
|
||||
traits.add_supported_fan_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,
|
||||
});
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
|
||||
@@ -36,9 +36,8 @@ bool Nextion::send_command_(const std::string &command) {
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) {
|
||||
ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str());
|
||||
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
|
||||
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
|
||||
return false;
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
@@ -49,16 +48,6 @@ bool Nextion::send_command_(const std::string &command) {
|
||||
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Mark sent immediately after writing to UART. The pacer enforces inter-command
|
||||
// spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_
|
||||
// at zero indefinitely, making can_send() always return true and spacing a no-op.
|
||||
// ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally.
|
||||
if (!this->connection_state_.ignore_is_setup_) {
|
||||
this->command_pacer_.mark_sent(now);
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -264,8 +253,11 @@ bool Nextion::send_command(const char *command) {
|
||||
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
|
||||
return false;
|
||||
|
||||
this->add_no_result_to_queue_with_command_("command", command);
|
||||
return true;
|
||||
if (this->send_command_(command)) {
|
||||
this->add_no_result_to_queue_("command");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Nextion::send_command_printf(const char *format, ...) {
|
||||
@@ -282,8 +274,11 @@ bool Nextion::send_command_printf(const char *format, ...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->add_no_result_to_queue_with_command_("command_printf", buffer);
|
||||
return true;
|
||||
if (this->send_command_(buffer)) {
|
||||
this->add_no_result_to_queue_("command_printf");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef NEXTION_PROTOCOL_LOG
|
||||
@@ -354,43 +349,25 @@ void Nextion::loop() {
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Try to send any pending commands if spacing allows
|
||||
this->process_pending_in_queue_();
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
if (!this->waveform_queue_.empty()) {
|
||||
this->check_pending_waveform_();
|
||||
}
|
||||
#endif // USE_NEXTION_WAVEFORM
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
void Nextion::process_pending_in_queue_() {
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
size_t commands_sent = 0;
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto *item : this->nextion_queue_) {
|
||||
if (item == nullptr || item->pending_command.empty()) {
|
||||
continue; // Already sent, waiting for ACK — skip, don't stop
|
||||
// Check if first item in queue has a pending command
|
||||
auto *front_item = this->nextion_queue_.front();
|
||||
if (front_item && !front_item->pending_command.empty()) {
|
||||
if (this->send_command_(front_item->pending_command)) {
|
||||
// Command sent successfully, clear the pending command
|
||||
front_item->pending_command.clear();
|
||||
ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str());
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
if (++commands_sent > this->max_commands_per_loop_) {
|
||||
ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring");
|
||||
break;
|
||||
}
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->command_pacer_.can_send(now)) {
|
||||
break; // Spacing not elapsed, stop for this loop iteration
|
||||
}
|
||||
|
||||
if (!this->send_command_(item->pending_command)) {
|
||||
break; // Unexpected send failure, stop
|
||||
}
|
||||
item->pending_command.clear();
|
||||
ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str());
|
||||
}
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
@@ -493,6 +470,10 @@ void Nextion::process_nextion_commands_() {
|
||||
this->setup_callback_.call();
|
||||
}
|
||||
}
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent
|
||||
ESP_LOGN(TAG, "Command spacing: marked command sent");
|
||||
#endif
|
||||
break;
|
||||
case 0x02: // invalid Component ID or name was used
|
||||
ESP_LOGW(TAG, "Invalid component ID/name");
|
||||
@@ -1098,18 +1079,10 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a command and enqueue it for response tracking.
|
||||
* @brief
|
||||
*
|
||||
* Callers are responsible for checking is_sleeping() before calling this
|
||||
* method. The sleep guard is deliberately absent here because some callers
|
||||
* (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly
|
||||
* sleep-safe and must bypass it.
|
||||
*
|
||||
* If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready,
|
||||
* the command is saved in the queue entry for retry rather than dropped.
|
||||
*
|
||||
* @param variable_name Name of the variable or component associated with the command.
|
||||
* @param command The raw command string to send.
|
||||
* @param variable_name Variable name for the queue
|
||||
* @param command
|
||||
*/
|
||||
void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
|
||||
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
|
||||
@@ -1290,22 +1263,9 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
|
||||
|
||||
std::string command = "get " + component->get_variable_name_to_send();
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Always enqueue first so the response handler is present when the command
|
||||
// is eventually sent. Store the command for retry if spacing blocked it;
|
||||
// process_pending_in_queue_() will transmit it when the pacer allows.
|
||||
nextion_queue->pending_command = command;
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
if (this->send_command_(command)) {
|
||||
nextion_queue->pending_command.clear();
|
||||
}
|
||||
#else // USE_NEXTION_COMMAND_SPACING
|
||||
if (this->send_command_(command)) {
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
} else {
|
||||
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
@@ -1349,10 +1309,10 @@ void Nextion::check_pending_waveform_() {
|
||||
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
|
||||
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
|
||||
component->get_wave_channel_id(), buffer_to_send);
|
||||
// If spacing or setup state blocks the send, leave the entry at the front
|
||||
// of waveform_queue_ for retry on the next loop iteration via
|
||||
// check_pending_waveform_(). Only pop on a successful send.
|
||||
this->send_command_(command);
|
||||
if (!this->send_command_(command)) {
|
||||
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
this->waveform_queue_.pop();
|
||||
}
|
||||
}
|
||||
#endif // USE_NEXTION_WAVEFORM
|
||||
|
||||
|
||||
@@ -55,20 +55,15 @@ class NextionCommandPacer {
|
||||
uint8_t get_spacing() const { return spacing_ms_; }
|
||||
|
||||
/**
|
||||
* @brief Check if enough time has passed to send the next command.
|
||||
* @param now Current timestamp in milliseconds (use App.get_loop_component_start_time()
|
||||
* for consistency with the rest of the queue timing).
|
||||
* @return true if the spacing interval has elapsed since the last command was sent.
|
||||
* @brief Check if enough time has passed to send next command
|
||||
* @return true if enough time has passed since last command
|
||||
*/
|
||||
bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; }
|
||||
bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; }
|
||||
|
||||
/**
|
||||
* @brief Record the transmit timestamp for the most recently sent command.
|
||||
* @param now Current timestamp in milliseconds, as returned by
|
||||
* App.get_loop_component_start_time(). Must use the same clock
|
||||
* source as can_send() to avoid unsigned underflow.
|
||||
* @brief Mark a command as sent, updating the timing
|
||||
*/
|
||||
void mark_sent(uint32_t now) { last_command_time_ = now; }
|
||||
void mark_sent() { last_command_time_ = millis(); }
|
||||
|
||||
private:
|
||||
uint8_t spacing_ms_;
|
||||
|
||||
@@ -8,11 +8,8 @@ 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
|
||||
@@ -48,18 +45,6 @@ def is_remote_package(package_config: dict) -> bool:
|
||||
return CONF_URL in package_config
|
||||
|
||||
|
||||
def is_package_definition(value: object) -> bool:
|
||||
"""Returns True if the value looks like a package definition rather than a config fragment.
|
||||
|
||||
Package definitions are IncludeFile objects, git URL shorthand strings, or
|
||||
remote package dicts (containing a ``url:`` key). Config fragments are
|
||||
plain dicts that represent component configuration.
|
||||
"""
|
||||
return isinstance(value, (yaml_util.IncludeFile, str)) or (
|
||||
isinstance(value, dict) and is_remote_package(value)
|
||||
)
|
||||
|
||||
|
||||
def valid_package_contents(package_config: dict) -> dict:
|
||||
"""Validate that a package looks like a plausible ESPHome config fragment.
|
||||
|
||||
@@ -324,23 +309,20 @@ def _walk_packages(
|
||||
return config
|
||||
packages = config[CONF_PACKAGES]
|
||||
|
||||
with cv.prepend_path(CONF_PACKAGES):
|
||||
if isinstance(packages, yaml_util.IncludeFile):
|
||||
# If the packages key is an IncludeFile, resolve it first before processing.
|
||||
packages, _ = resolve_include(packages, [], context, strict_undefined=False)
|
||||
if not isinstance(packages, (dict, list)):
|
||||
raise cv.Invalid(
|
||||
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
|
||||
)
|
||||
if not isinstance(packages, (dict, list)):
|
||||
raise cv.Invalid(
|
||||
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
|
||||
)
|
||||
|
||||
with cv.prepend_path(CONF_PACKAGES):
|
||||
if not isinstance(packages, dict):
|
||||
_walk_package_list(packages, callback, context)
|
||||
elif (result := _walk_package_dict(packages, callback, context)) is not None:
|
||||
if not validate_deprecated or any(
|
||||
is_package_definition(v) for v in packages.values()
|
||||
):
|
||||
if not validate_deprecated:
|
||||
raise result
|
||||
# Fallback: treat the dict as a single deprecated package.
|
||||
# Note: this catches *any* cv.Invalid from the callback, which may
|
||||
# mask real validation errors in named package dicts.
|
||||
# This block can be removed once the single-package
|
||||
# deprecation period (2026.7.0) is over.
|
||||
config[CONF_PACKAGES] = [packages]
|
||||
@@ -362,19 +344,12 @@ 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
|
||||
|
||||
|
||||
@@ -486,9 +461,6 @@ class _PackageProcessor:
|
||||
self, package_config: dict | str, context_vars: ContextVars | None
|
||||
) -> dict:
|
||||
"""Resolve a single package and recurse into any nested packages."""
|
||||
from_remote = isinstance(package_config, dict) and is_remote_package(
|
||||
package_config
|
||||
)
|
||||
package_config = self.resolve_package(package_config, context_vars)
|
||||
self.collect_substitutions(package_config)
|
||||
|
||||
@@ -498,18 +470,7 @@ class _PackageProcessor:
|
||||
# Push context from !include vars on the package root and on the packages key
|
||||
context_vars = push_context(package_config, context_vars)
|
||||
context_vars = push_context(package_config[CONF_PACKAGES], context_vars)
|
||||
# Disable the deprecated single-package fallback for remote
|
||||
# packages. _process_remote_package returns dicts with
|
||||
# already-resolved values that is_package_definition cannot
|
||||
# distinguish from config fragments, so the fallback would
|
||||
# always fire and mask real errors with wrong paths
|
||||
# (packages->0 instead of packages-><name>).
|
||||
return _walk_packages(
|
||||
package_config,
|
||||
self.process_package,
|
||||
context_vars,
|
||||
validate_deprecated=not from_remote,
|
||||
)
|
||||
return _walk_packages(package_config, self.process_package, context_vars)
|
||||
|
||||
|
||||
def do_packages_pass(
|
||||
@@ -526,12 +487,7 @@ def do_packages_pass(
|
||||
if CONF_PACKAGES not in config:
|
||||
return config
|
||||
|
||||
with cv.prepend_path(CONF_SUBSTITUTIONS):
|
||||
substitutions = UserDict(
|
||||
resolve_substitutions_block(
|
||||
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
|
||||
)
|
||||
)
|
||||
substitutions = UserDict(config.pop(CONF_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(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
if (!rtc_time.is_valid()) {
|
||||
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(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
if (!rtc_time.is_valid()) {
|
||||
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_VERBOSE) {
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
|
||||
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_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,
|
||||
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,
|
||||
temp, status);
|
||||
|
||||
if (this->x_sensor_ != nullptr)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <WiFi.h>
|
||||
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
|
||||
#elif defined(USE_ETHERNET)
|
||||
#include <lwip_wrap.h> // For LWIPMutex — LwIPLock mirrors its semantics (see below)
|
||||
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
#endif
|
||||
#include <hardware/structs/rosc.h>
|
||||
@@ -43,18 +43,9 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
|
||||
// assertion failures). See esphome#10681.
|
||||
//
|
||||
// WiFi uses cyw43_arch_lwip_begin/end.
|
||||
//
|
||||
// For wired Ethernet, taking only the async_context lock is NOT enough. The
|
||||
// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP`
|
||||
// counter to decide whether to defer packet processing. If we hold the
|
||||
// async_context lock without bumping `__inLWIP`, an interrupt-driven packet
|
||||
// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat`
|
||||
// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's
|
||||
// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the
|
||||
// lock, and on release re-unmask any GPIO IRQs that were deferred while we
|
||||
// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because
|
||||
// pulling lwip_wrap.h there poisons many TUs with lwIP types.
|
||||
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
|
||||
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
|
||||
// firing during critical sections.
|
||||
//
|
||||
// When neither WiFi nor Ethernet is configured, this is a no-op since
|
||||
// there's no network stack and no lwip callbacks to race with.
|
||||
@@ -62,18 +53,8 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
|
||||
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
|
||||
#elif defined(USE_ETHERNET)
|
||||
LwIPLock::LwIPLock() {
|
||||
__inLWIP++;
|
||||
ethernet_arch_lwip_begin();
|
||||
}
|
||||
LwIPLock::~LwIPLock() {
|
||||
ethernet_arch_lwip_end();
|
||||
__inLWIP--;
|
||||
if (__needsIRQEN && !__inLWIP) {
|
||||
__needsIRQEN = false;
|
||||
ethernet_arch_lwip_gpio_unmask();
|
||||
}
|
||||
}
|
||||
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
|
||||
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
|
||||
#else
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
@@ -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.b;
|
||||
this->buffer_[pos + 0] = mapped_color.r;
|
||||
this->buffer_[pos + 1] = mapped_color.g;
|
||||
this->buffer_[pos + 2] = mapped_color.r;
|
||||
this->buffer_[pos + 2] = mapped_color.b;
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 3] = color.w;
|
||||
}
|
||||
|
||||
@@ -32,101 +32,40 @@ void RuntimeStatsCollector::log_stats_() {
|
||||
" Period stats (last %" PRIu32 "ms): %zu active components",
|
||||
this->log_interval_, count);
|
||||
|
||||
// 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;
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
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 total stats since boot (only for active components - idle ones haven't changed)
|
||||
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
|
||||
|
||||
if (count > 0) {
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(sorted, sorted + count, compare_total_time);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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,31 +29,6 @@ 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
|
||||
@@ -62,22 +37,6 @@ 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(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
if (!rtc_time.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,11 +55,13 @@ void SafeModeComponent::dump_config() {
|
||||
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
|
||||
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
|
||||
if (last_invalid != nullptr) {
|
||||
ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label);
|
||||
ESP_LOGW(TAG, "The device reset before the boot was marked successful");
|
||||
ESP_LOGW(TAG,
|
||||
"OTA rollback detected! Rolled back from partition '%s'\n"
|
||||
" The device reset before the boot was marked successful",
|
||||
last_invalid->label);
|
||||
if (esp_reset_reason() == ESP_RST_BROWNOUT) {
|
||||
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!");
|
||||
ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
|
||||
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
|
||||
" See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -86,7 +88,8 @@ void SafeModeComponent::mark_successful() {
|
||||
}
|
||||
|
||||
void SafeModeComponent::loop() {
|
||||
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
|
||||
if (!this->boot_successful_ &&
|
||||
(App.get_loop_component_start_time() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
|
||||
// successful boot, reset counter
|
||||
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
|
||||
this->mark_successful();
|
||||
|
||||
@@ -112,6 +112,8 @@ class BSDSocketImpl {
|
||||
int setblocking(bool blocking);
|
||||
int loop() { return 0; }
|
||||
|
||||
/// Check if the socket has buffered data ready to read.
|
||||
/// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
bool ready() const;
|
||||
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
@@ -96,6 +96,8 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
// Check if the socket has buffered data ready to read.
|
||||
// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
// Intentionally unlocked — this is a polling check called every loop iteration.
|
||||
// A stale read at worst delays processing by one loop tick; the actual I/O in
|
||||
// read() holds the lwip lock and re-checks properly. See esphome#10681.
|
||||
|
||||
@@ -78,6 +78,8 @@ class LwIPSocketImpl {
|
||||
int setblocking(bool blocking);
|
||||
int loop() { return 0; }
|
||||
|
||||
/// Check if the socket has buffered data ready to read.
|
||||
/// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
bool ready() const;
|
||||
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
@@ -53,6 +53,19 @@ bool socket_ready_fd(int fd, bool loop_monitored);
|
||||
|
||||
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
|
||||
// declared above, while the impl headers are included before those declarations.
|
||||
//
|
||||
// Contract (applies to ALL socket implementations — each platform implements
|
||||
// ready() differently, but this contract holds regardless of the mechanism):
|
||||
// ready() checks if the socket has buffered data ready to read. When it returns
|
||||
// true, the caller MUST read until it would block (EAGAIN/EWOULDBLOCK), or until
|
||||
// read() returns 0 to indicate EOF / connection closed, or track that it stopped
|
||||
// early and retry without calling ready(). The next call to ready() will only
|
||||
// report new data correctly if all callers fulfill this contract. Failing to
|
||||
// drain the socket may cause ready() to return false while data remains readable.
|
||||
//
|
||||
// In practice each socket is owned by a single component, so this contract is
|
||||
// straightforward to fulfill — but the owning component must be aware of it,
|
||||
// especially if it limits how many messages it processes per loop iteration.
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
inline bool Socket::ready() const {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
|
||||
@@ -45,8 +45,8 @@ MODELS = {
|
||||
presets={
|
||||
CONF_HEIGHT: 240,
|
||||
CONF_WIDTH: 135,
|
||||
CONF_OFFSET_HEIGHT: 40,
|
||||
CONF_OFFSET_WIDTH: 52,
|
||||
CONF_OFFSET_HEIGHT: 52,
|
||||
CONF_OFFSET_WIDTH: 40,
|
||||
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: 20,
|
||||
CONF_OFFSET_WIDTH: 0,
|
||||
CONF_OFFSET_HEIGHT: 0,
|
||||
CONF_OFFSET_WIDTH: 20,
|
||||
}
|
||||
),
|
||||
"ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec(
|
||||
@@ -77,8 +77,8 @@ MODELS = {
|
||||
presets={
|
||||
CONF_HEIGHT: 240,
|
||||
CONF_WIDTH: 135,
|
||||
CONF_OFFSET_HEIGHT: 40,
|
||||
CONF_OFFSET_WIDTH: 52,
|
||||
CONF_OFFSET_HEIGHT: 52,
|
||||
CONF_OFFSET_WIDTH: 40,
|
||||
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: 0,
|
||||
CONF_OFFSET_WIDTH: 35,
|
||||
CONF_OFFSET_HEIGHT: 35,
|
||||
CONF_OFFSET_WIDTH: 0,
|
||||
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: 0,
|
||||
CONF_OFFSET_WIDTH: 34,
|
||||
CONF_OFFSET_HEIGHT: 34,
|
||||
CONF_OFFSET_WIDTH: 0,
|
||||
CONF_ROTATION: 90,
|
||||
CONF_CS_PIN: "GPIO21",
|
||||
CONF_DC_PIN: "GPIO22",
|
||||
|
||||
@@ -30,56 +30,6 @@ 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)
|
||||
@@ -464,34 +414,6 @@ 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:
|
||||
@@ -507,9 +429,10 @@ 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):
|
||||
substitutions = resolve_substitutions_block(
|
||||
substitutions, command_line_substitutions
|
||||
)
|
||||
if not isinstance(substitutions, dict):
|
||||
raise cv.Invalid(
|
||||
f"Substitutions must be a key to value mapping, got {type(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.int_range(min=0, max=100000)
|
||||
cv.frequency, cv.float_range(min=0, max=100000)
|
||||
),
|
||||
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Required(CONF_FREQUENCY): cv.All(
|
||||
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
|
||||
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
|
||||
),
|
||||
cv.Required(CONF_HW_VERSION): cv.one_of(
|
||||
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
|
||||
|
||||
@@ -104,17 +104,11 @@ void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void SX126x::setup() {
|
||||
// setup pins
|
||||
this->busy_pin_->setup();
|
||||
this->rst_pin_->setup();
|
||||
this->dio1_pin_->setup();
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
static_cast<InternalGPIOPin *>(this->dio1_pin_)
|
||||
->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
|
||||
// start spi
|
||||
this->spi_setup();
|
||||
@@ -354,9 +348,6 @@ void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
|
||||
}
|
||||
|
||||
void SX126x::loop() {
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
if (!this->dio1_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "sx126x_reg.h"
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -101,7 +100,6 @@ class SX126x : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX126x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_packet_params_(uint8_t payload_length);
|
||||
|
||||
@@ -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.int_range(min=0, max=100000)
|
||||
cv.frequency, cv.float_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.int_range(min=int(137e6), max=int(1020e6))
|
||||
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
|
||||
),
|
||||
cv.Required(CONF_MODULATION): cv.enum(MOD),
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
|
||||
@@ -53,8 +53,6 @@ void SX127x::write_fifo_(const std::vector<uint8_t> &packet) {
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void IRAM_ATTR SX127x::gpio_intr(SX127x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void SX127x::setup() {
|
||||
// setup reset
|
||||
this->rst_pin_->setup();
|
||||
@@ -62,7 +60,6 @@ void SX127x::setup() {
|
||||
// setup dio0
|
||||
if (this->dio0_pin_) {
|
||||
this->dio0_pin_->setup();
|
||||
this->dio0_pin_->attach_interrupt(&SX127x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
|
||||
// start spi
|
||||
@@ -316,7 +313,6 @@ void SX127x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo
|
||||
}
|
||||
|
||||
void SX127x::loop() {
|
||||
this->disable_loop();
|
||||
if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
@@ -390,6 +386,11 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mode == MODE_RX && (modulation == MOD_LORA || this->packet_mode_)) {
|
||||
this->enable_loop();
|
||||
} else {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void SX127x::set_mode_rx() {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -87,7 +86,6 @@ class SX127x : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX127x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_mode_(uint8_t modulation, uint8_t mode);
|
||||
|
||||
@@ -315,7 +315,7 @@ void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration
|
||||
my_integration_time_regval = integration_time;
|
||||
this->integration_time_auto_ = false;
|
||||
}
|
||||
this->integration_time_ = (256.f - (float) my_integration_time_regval) * 2.4f;
|
||||
this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
|
||||
ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
|
||||
}
|
||||
void TCS34725Component::set_gain(TCS34725Gain gain) {
|
||||
|
||||
@@ -114,25 +114,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
uint8_t *data, size_t len, bool final) {
|
||||
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
|
||||
|
||||
// First byte of a new upload: index==0 with actual data. (web_server_idf
|
||||
// fires a separate start-marker call with data==nullptr/len==0 before the
|
||||
// first real chunk; gate on len>0 so we only trigger once per upload.)
|
||||
if (index == 0 && len > 0) {
|
||||
// If a previous upload was interrupted (e.g. client closed the tab, TCP
|
||||
// reset) the backend from that session may still be open. Tear it down
|
||||
// so flash state doesn't get concatenated with the new image (which can
|
||||
// produce a technically-valid-sized but corrupted firmware that bricks
|
||||
// the device once it reboots).
|
||||
if (this->ota_backend_) {
|
||||
ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
|
||||
this->ota_backend_->abort();
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Notify listeners that the previous session was aborted before the new one starts.
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
|
||||
#endif
|
||||
this->ota_backend_.reset();
|
||||
}
|
||||
|
||||
if (index == 0 && !this->ota_backend_) {
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
|
||||
|
||||
@@ -943,26 +943,7 @@ def time_period_in_minutes_(value):
|
||||
def update_interval(value):
|
||||
if value == "never":
|
||||
return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN)
|
||||
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
|
||||
return positive_time_period_milliseconds(value)
|
||||
|
||||
|
||||
time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.4.1"
|
||||
__version__ = "2026.5.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -85,12 +85,8 @@ void Application::setup() {
|
||||
if (component->can_proceed())
|
||||
continue;
|
||||
|
||||
// Force the status LED to blink WARNING while we wait for a slow
|
||||
// component to come up. Cleared after setup() finishes if no real
|
||||
// component has warning set.
|
||||
this->app_state_ |= STATUS_LED_WARNING;
|
||||
|
||||
do {
|
||||
uint8_t new_app_state = STATUS_LED_WARNING;
|
||||
uint32_t now = millis();
|
||||
|
||||
// Process pending loop enables to handle GPIO interrupts during setup
|
||||
@@ -100,26 +96,17 @@ void Application::setup() {
|
||||
// Update loop_component_start_time_ right before calling each component
|
||||
this->loop_component_start_time_ = millis();
|
||||
this->components_[j]->call();
|
||||
new_app_state |= this->components_[j]->get_component_state();
|
||||
this->app_state_ |= new_app_state;
|
||||
this->feed_wdt();
|
||||
}
|
||||
|
||||
this->after_loop_tasks_();
|
||||
this->app_state_ = new_app_state;
|
||||
yield();
|
||||
} while (!component->can_proceed() && !component->is_failed());
|
||||
}
|
||||
|
||||
// Setup is complete. Reconcile STATUS_LED_WARNING: the slow-setup path
|
||||
// above may have forced it on, and any status_clear_warning() calls
|
||||
// from components during setup were intentional no-ops (gated by
|
||||
// APP_STATE_SETUP_COMPLETE). Walk components once here to pick up the
|
||||
// real state. STATUS_LED_ERROR is never artificially forced, so its
|
||||
// clear path always works and needs no reconciliation. Finally, set
|
||||
// APP_STATE_SETUP_COMPLETE so subsequent warning clears go through
|
||||
// the normal walk-and-clear path.
|
||||
if (!this->any_component_has_status_flag_(STATUS_LED_WARNING))
|
||||
this->app_state_ &= ~STATUS_LED_WARNING;
|
||||
this->app_state_ |= APP_STATE_SETUP_COMPLETE;
|
||||
|
||||
ESP_LOGI(TAG, "setup() finished successfully!");
|
||||
|
||||
#ifdef USE_SETUP_PRIORITY_OVERRIDE
|
||||
@@ -209,40 +196,21 @@ void Application::process_dump_config_() {
|
||||
this->dump_config_at_++;
|
||||
}
|
||||
|
||||
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().
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
|
||||
this->feed_wdt_slow_(now);
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
// Callers (both feed_wdt() and feed_wdt_with_time()) have already
|
||||
// confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded.
|
||||
arch_feed_wdt();
|
||||
this->last_wdt_feed_ = time;
|
||||
void HOT Application::feed_wdt(uint32_t time) {
|
||||
static uint32_t last_feed = 0;
|
||||
// Use provided time if available, otherwise get current time
|
||||
uint32_t now = time ? time : millis();
|
||||
// Compare in milliseconds (3ms threshold)
|
||||
if (now - last_feed > 3) {
|
||||
arch_feed_wdt();
|
||||
last_feed = now;
|
||||
#ifdef USE_STATUS_LED
|
||||
if (status_led::global_status_led != nullptr) {
|
||||
status_led::global_status_led->call();
|
||||
}
|
||||
if (status_led::global_status_led != nullptr) {
|
||||
status_led::global_status_led->call();
|
||||
}
|
||||
#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
|
||||
// (status_clear_warning_slow_path_ / status_clear_error_slow_path_) on an
|
||||
// actual set→clear transition, so walking O(N) here is paid once per
|
||||
// transition — not once per loop iteration.
|
||||
for (auto *component : this->components_) {
|
||||
if ((component->get_component_state() & flag) != 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Application::reboot() {
|
||||
ESP_LOGI(TAG, "Forcing a reboot");
|
||||
for (auto &component : std::ranges::reverse_view(this->components_)) {
|
||||
@@ -331,7 +299,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
|
||||
|
||||
while (pending_count > 0 && (now - start_time) < timeout_ms) {
|
||||
// Feed watchdog during teardown to prevent triggering
|
||||
this->feed_wdt_with_time(now);
|
||||
this->feed_wdt(now);
|
||||
|
||||
// Process components and compact the array, keeping only those still pending
|
||||
size_t still_pending = 0;
|
||||
|
||||
+17
-82
@@ -385,24 +385,7 @@ 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;
|
||||
|
||||
/// Feed the task watchdog. Cold entry — callers without a millis()
|
||||
/// timestamp in hand. Out of line to keep call sites tiny.
|
||||
void feed_wdt();
|
||||
|
||||
/// 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_.
|
||||
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);
|
||||
}
|
||||
}
|
||||
void feed_wdt(uint32_t time = 0);
|
||||
|
||||
void reboot();
|
||||
|
||||
@@ -418,18 +401,7 @@ class Application {
|
||||
*/
|
||||
void teardown_components(uint32_t timeout_ms);
|
||||
|
||||
/// Return the public app state status bits (STATUS_LED_* only).
|
||||
/// Internal bookkeeping bits like APP_STATE_SETUP_COMPLETE are masked
|
||||
/// out so external readers (status_led components, etc.) never see them.
|
||||
uint8_t get_app_state() const { return this->app_state_ & ~APP_STATE_SETUP_COMPLETE; }
|
||||
|
||||
/// True once Application::setup() has finished walking all components
|
||||
/// and finalized the initial status flags. Before this point, the
|
||||
/// slow-setup busy-wait may be forcing STATUS_LED_WARNING on, and
|
||||
/// status_clear_* intentionally skips its walk-and-clear step so the
|
||||
/// forced bit doesn't get wiped. Stored as a free bit on app_state_
|
||||
/// (bit 6) to avoid costing additional RAM.
|
||||
bool is_setup_complete() const { return (this->app_state_ & APP_STATE_SETUP_COMPLETE) != 0; }
|
||||
uint8_t get_app_state() const { return this->app_state_; }
|
||||
|
||||
// Helper macro for entity getter method declarations
|
||||
#ifdef USE_DEVICES
|
||||
@@ -605,12 +577,6 @@ class Application {
|
||||
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
|
||||
#endif
|
||||
|
||||
/// Walk all registered components looking for any whose component_state_
|
||||
/// has the given flag set. Used by Component::status_clear_*_slow_path_()
|
||||
/// (which is a friend) to decide whether to clear the corresponding bit on
|
||||
/// this->app_state_ (the app-wide "any component has this status" indicator).
|
||||
bool any_component_has_status_flag_(uint8_t flag) const;
|
||||
|
||||
/// Register a component, detecting loop() override at compile time.
|
||||
/// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance.
|
||||
template<typename T> void register_component_(T *comp) {
|
||||
@@ -641,7 +607,7 @@ class Application {
|
||||
void enable_component_loop_(Component *component);
|
||||
void enable_pending_loops_();
|
||||
void activate_looping_component_(uint16_t index);
|
||||
inline uint32_t ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time);
|
||||
inline void 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,10 +615,7 @@ 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.
|
||||
void feed_wdt_slow_(uint32_t time);
|
||||
void feed_wdt_arch_();
|
||||
|
||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||
#ifdef USE_HOST
|
||||
@@ -706,7 +669,6 @@ class Application {
|
||||
// 4-byte members
|
||||
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_HOST
|
||||
int max_fd_{-1}; // Highest file descriptor number for select()
|
||||
@@ -845,15 +807,17 @@ inline void Application::drain_wake_notifications_() {
|
||||
}
|
||||
#endif // USE_HOST
|
||||
|
||||
inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) {
|
||||
inline void 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
|
||||
|
||||
// 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 scheduled tasks
|
||||
this->scheduler.call(loop_start_time);
|
||||
|
||||
// Feed the watchdog timer
|
||||
this->feed_wdt(loop_start_time);
|
||||
|
||||
// Process any pending enable_loop requests from ISRs
|
||||
// This must be done before marking in_loop_ = true to avoid race conditions
|
||||
@@ -871,35 +835,15 @@ inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t l
|
||||
|
||||
// Mark that we're in the loop for safe reentrant modifications
|
||||
this->in_loop_ = true;
|
||||
return last_op_end_time;
|
||||
}
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE 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
|
||||
uint8_t new_app_state = 0;
|
||||
|
||||
// Get the initial loop time at the start
|
||||
uint32_t last_op_end_time = millis();
|
||||
|
||||
// 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
|
||||
this->before_loop_tasks_(last_op_end_time);
|
||||
|
||||
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
|
||||
this->current_loop_index_++) {
|
||||
@@ -915,27 +859,18 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
// Use the finish method to get the current time as the end time
|
||||
last_op_end_time = guard.finish();
|
||||
}
|
||||
this->feed_wdt_with_time(last_op_end_time);
|
||||
new_app_state |= component->get_component_state();
|
||||
this->app_state_ |= new_app_state;
|
||||
this->feed_wdt(last_op_end_time);
|
||||
}
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
uint32_t loop_tail_start_us = micros();
|
||||
#endif
|
||||
this->after_loop_tasks_();
|
||||
this->app_state_ = new_app_state;
|
||||
|
||||
#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
|
||||
|
||||
@@ -62,18 +62,6 @@ 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{}; }
|
||||
|
||||
@@ -205,9 +205,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
} else {
|
||||
// For delays with arguments, capture by value to preserve argument values
|
||||
// Arguments must be copied because original references may be invalid after delay
|
||||
// `mutable` is required so captured copies of non-const reference args (e.g. std::string&)
|
||||
// are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&`
|
||||
auto f = [this, x...]() mutable { this->play_next_(x...); };
|
||||
auto f = [this, x...]() { this->play_next_(x...); };
|
||||
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
|
||||
nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
|
||||
this->delay_.value(x...), std::move(f),
|
||||
|
||||
@@ -411,23 +411,10 @@ void Component::status_set_error(const LogString *message) {
|
||||
}
|
||||
void Component::status_clear_warning_slow_path_() {
|
||||
this->component_state_ &= ~STATUS_LED_WARNING;
|
||||
// Clear the app-wide STATUS_LED_WARNING bit only if setup has finished
|
||||
// AND no other component still has it set. During setup the forced
|
||||
// STATUS_LED_WARNING (from the slow-setup busy-wait) must not be wiped
|
||||
// by a transient component clear — Application::setup() reconciles
|
||||
// the warning bit once at the end before setting APP_STATE_SETUP_COMPLETE.
|
||||
// The set path is unchanged (set_status_flag_ still writes directly).
|
||||
if (App.is_setup_complete() && !App.any_component_has_status_flag_(STATUS_LED_WARNING))
|
||||
App.app_state_ &= ~STATUS_LED_WARNING;
|
||||
ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str()));
|
||||
}
|
||||
void Component::status_clear_error_slow_path_() {
|
||||
this->component_state_ &= ~STATUS_LED_ERROR;
|
||||
// STATUS_LED_ERROR is never artificially forced — it only ever lands
|
||||
// in app_state_ via a real set_status_flag_ call. So the walk-and-clear
|
||||
// path is always safe, including during setup.
|
||||
if (!App.any_component_has_status_flag_(STATUS_LED_ERROR))
|
||||
App.app_state_ &= ~STATUS_LED_ERROR;
|
||||
ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str()));
|
||||
}
|
||||
void Component::status_momentary_warning(const char *name, uint32_t length) {
|
||||
@@ -506,10 +493,6 @@ void PollingComponent::stop_poller() {
|
||||
|
||||
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
uint64_t ComponentRuntimeStats::global_recorded_us = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#endif
|
||||
|
||||
void __attribute__((noinline, cold))
|
||||
WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) {
|
||||
bool should_warn;
|
||||
|
||||
@@ -89,11 +89,6 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08;
|
||||
inline constexpr uint8_t STATUS_LED_ERROR = 0x10;
|
||||
// Component loop override flag uses bit 5 (set at registration time)
|
||||
inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
|
||||
// Bit 6 on Application::app_state_ (ONLY) — set at the end of
|
||||
// Application::setup(). Component::status_clear_*_slow_path_() uses this to
|
||||
// decide whether to propagate clears to App.app_state_. Never set on a
|
||||
// Component's component_state_.
|
||||
inline constexpr uint8_t APP_STATE_SETUP_COMPLETE = 0x40;
|
||||
// Remove before 2026.8.0
|
||||
enum class RetryResult { DONE, RETRY };
|
||||
|
||||
@@ -116,13 +111,6 @@ struct ComponentRuntimeStats {
|
||||
uint64_t total_time_us{0};
|
||||
uint32_t total_max_time_us{0};
|
||||
|
||||
// Cumulative sum of every record_time() duration since boot, across all
|
||||
// components. Used by Application::loop() to snapshot time spent inside
|
||||
// WarnIfComponentBlockingGuard (including guards constructed by the
|
||||
// scheduler at scheduler.cpp) so main-loop overhead accounting can
|
||||
// subtract scheduled-callback time from the before_loop_tasks_ wall time.
|
||||
static uint64_t global_recorded_us; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void record_time(uint32_t duration_us) {
|
||||
this->period_count++;
|
||||
this->period_time_us += duration_us;
|
||||
@@ -132,7 +120,6 @@ struct ComponentRuntimeStats {
|
||||
this->total_time_us += duration_us;
|
||||
if (duration_us > this->total_max_time_us)
|
||||
this->total_max_time_us = duration_us;
|
||||
global_recorded_us += duration_us;
|
||||
}
|
||||
void reset_period() {
|
||||
this->period_count = 0;
|
||||
@@ -601,7 +588,7 @@ class Component {
|
||||
*/
|
||||
class PollingComponent : public Component {
|
||||
public:
|
||||
PollingComponent() : PollingComponent(1) {}
|
||||
PollingComponent() : PollingComponent(0) {}
|
||||
|
||||
/** Initialize this polling component with the given update interval in ms.
|
||||
*
|
||||
|
||||
@@ -144,19 +144,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
return;
|
||||
}
|
||||
|
||||
// An interval of 0 means "fire every tick forever," which is misuse: the
|
||||
// item would always be due, causing Scheduler::call() to spin and starve
|
||||
// the main loop (WDT reset in the field). Coerce to 1ms so existing code
|
||||
// using update_interval=0ms as a pseudo-loop() continues to work at ~1kHz,
|
||||
// and warn so authors can migrate to HighFrequencyLoopRequester which is
|
||||
// the intended mechanism for running fast in the main loop. Zero-delay
|
||||
// timeouts (defer) remain legitimate one-shots and are not affected.
|
||||
if (type == SchedulerItem::INTERVAL && delay == 0) [[unlikely]] {
|
||||
ESP_LOGE(TAG, "[%s] set_interval(0) would spin main loop - coercing to 1ms (use HighFrequencyLoopRequester)",
|
||||
component ? LOG_STR_ARG(component->get_component_log_str()) : LOG_STR_LITERAL("?"));
|
||||
delay = 1;
|
||||
}
|
||||
|
||||
// Take lock early to protect scheduler_item_pool_ access and retry-cancelled check
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
@@ -533,7 +520,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) {
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
void HOT Scheduler::call(uint32_t now) {
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
this->process_defer_queue_(now);
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
@@ -703,9 +690,6 @@ uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
this->debug_verify_no_leak_();
|
||||
}
|
||||
#endif
|
||||
// execute_item_() advances `now` as items fire; return it so the caller
|
||||
// stays monotonic with last_wdt_feed_.
|
||||
return now;
|
||||
}
|
||||
void HOT Scheduler::process_to_add_slow_path_() {
|
||||
LockGuard guard{this->lock_};
|
||||
@@ -755,13 +739,7 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
|
||||
App.set_current_component(item->component);
|
||||
WarnIfComponentBlockingGuard guard{item->component, now};
|
||||
item->callback();
|
||||
uint32_t end = guard.finish();
|
||||
// Feed the watchdog after each scheduled item (both main heap and defer
|
||||
// queue paths go through here). A run of back-to-back callbacks cannot
|
||||
// starve the wdt. The inline fast path is a load + sub + branch — nearly
|
||||
// free when the 3 ms rate limit hasn't elapsed.
|
||||
App.feed_wdt_with_time(end);
|
||||
return end;
|
||||
return guard.finish();
|
||||
}
|
||||
|
||||
// Common implementation for cancel operations - handles locking
|
||||
|
||||
@@ -129,8 +129,7 @@ class Scheduler {
|
||||
|
||||
// Execute all scheduled items that are ready
|
||||
// @param now Fresh timestamp from millis() - must not be stale/cached
|
||||
// @return Timestamp of the last item that ran, or `now` unchanged if none ran.
|
||||
uint32_t call(uint32_t now);
|
||||
void call(uint32_t now);
|
||||
|
||||
// Move items from to_add_ into the main heap.
|
||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||
|
||||
+2
-6
@@ -76,12 +76,8 @@ struct ESPTime {
|
||||
/// @copydoc strftime(const std::string &format)
|
||||
std::string strftime(const char *format);
|
||||
|
||||
/// Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
|
||||
/// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields)
|
||||
/// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields)
|
||||
bool is_valid(bool check_day_of_week = true, bool check_day_of_year = true) const {
|
||||
return this->year >= 2019 && this->fields_in_range(check_day_of_week, check_day_of_year);
|
||||
}
|
||||
/// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019)
|
||||
bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); }
|
||||
|
||||
/// Check if time fields are in range.
|
||||
/// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields)
|
||||
|
||||
@@ -113,8 +113,7 @@ def _generate_source_table_code(
|
||||
entries = ", ".join(var_names)
|
||||
lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};")
|
||||
lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{")
|
||||
cond = "index == 0" if count >= 255 else f"index == 0 || index > {count}"
|
||||
lines.append(f' if ({cond}) return LOG_STR("<unknown>");')
|
||||
lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");')
|
||||
lines.append(" return reinterpret_cast<const LogString *>(")
|
||||
lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));")
|
||||
lines.append("}")
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies:
|
||||
espressif/esp32-camera:
|
||||
version: 2.1.6
|
||||
espressif/mdns:
|
||||
version: 1.11.0
|
||||
version: 1.10.0
|
||||
espressif/esp_wifi_remote:
|
||||
version: 1.4.0
|
||||
rules:
|
||||
|
||||
+128
-4
@@ -5,15 +5,104 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.util import run_external_process
|
||||
from esphome.util import run_external_command, run_external_process
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def patch_structhash():
|
||||
# Patch platformio's structhash to not recompile the entire project when files are
|
||||
# removed/added. This might have unintended consequences, but this improves compile
|
||||
# times greatly when adding/removing components and a simple clean build solves
|
||||
# all issues
|
||||
from platformio.run import cli, helpers
|
||||
|
||||
def patched_clean_build_dir(build_dir, *args):
|
||||
from platformio import fs
|
||||
from platformio.project.helpers import get_project_dir
|
||||
|
||||
platformio_ini = Path(get_project_dir()) / "platformio.ini"
|
||||
|
||||
build_dir = Path(build_dir)
|
||||
|
||||
# if project's config is modified
|
||||
if (
|
||||
build_dir.is_dir()
|
||||
and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime
|
||||
):
|
||||
fs.rmtree(build_dir)
|
||||
|
||||
if not build_dir.is_dir():
|
||||
build_dir.mkdir(parents=True)
|
||||
|
||||
helpers.clean_build_dir = patched_clean_build_dir
|
||||
cli.clean_build_dir = patched_clean_build_dir
|
||||
|
||||
|
||||
def patch_file_downloader():
|
||||
"""Patch PlatformIO's FileDownloader to retry on PackageException errors.
|
||||
|
||||
PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry
|
||||
for 502/503 errors. We add retries with exponential backoff and close the
|
||||
session between attempts to force a fresh TCP connection, which may route
|
||||
to a different CDN edge node.
|
||||
"""
|
||||
from platformio.package.download import FileDownloader
|
||||
from platformio.package.exception import PackageException
|
||||
|
||||
if getattr(FileDownloader.__init__, "_esphome_patched", False):
|
||||
return
|
||||
|
||||
original_init = FileDownloader.__init__
|
||||
|
||||
def patched_init(self, *args: Any, **kwargs: Any) -> None:
|
||||
max_retries = 5
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
original_init(self, *args, **kwargs)
|
||||
return
|
||||
except PackageException as e:
|
||||
if attempt < max_retries - 1:
|
||||
# Exponential backoff: 2, 4, 8, 16 seconds
|
||||
delay = 2 ** (attempt + 1)
|
||||
_LOGGER.warning(
|
||||
"Package download failed: %s. "
|
||||
"Retrying in %d seconds... (attempt %d/%d)",
|
||||
str(e),
|
||||
delay,
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
)
|
||||
# Close the response and session to free resources
|
||||
# and force a new TCP connection on retry, which may
|
||||
# route to a different CDN edge node
|
||||
# pylint: disable=protected-access,broad-except
|
||||
try:
|
||||
if (
|
||||
hasattr(self, "_http_response")
|
||||
and self._http_response is not None
|
||||
):
|
||||
self._http_response.close()
|
||||
if hasattr(self, "_http_session"):
|
||||
self._http_session.close()
|
||||
except Exception:
|
||||
pass
|
||||
# pylint: enable=protected-access,broad-except
|
||||
time.sleep(delay)
|
||||
else:
|
||||
# Final attempt - re-raise
|
||||
raise
|
||||
|
||||
patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access
|
||||
FileDownloader.__init__ = patched_init
|
||||
|
||||
|
||||
IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})"
|
||||
FILTER_PLATFORMIO_LINES = [
|
||||
r"Verbose mode can be enabled via `-v, --verbose` option.*",
|
||||
@@ -53,6 +142,20 @@ FILTER_PLATFORMIO_LINES = [
|
||||
]
|
||||
|
||||
|
||||
class PlatformioLogFilter(logging.Filter):
|
||||
"""Filter to suppress noisy platformio log messages."""
|
||||
|
||||
_PATTERN = re.compile(
|
||||
r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES)
|
||||
)
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
# Only filter messages from platformio-related loggers
|
||||
if "platformio" not in record.name.lower():
|
||||
return True
|
||||
return self._PATTERN.match(record.getMessage()) is None
|
||||
|
||||
|
||||
def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
|
||||
os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute())
|
||||
@@ -63,9 +166,30 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
|
||||
# Increase uv retry count to handle transient network errors (default is 3)
|
||||
os.environ.setdefault("UV_HTTP_RETRIES", "10")
|
||||
cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args)
|
||||
cmd = ["platformio"] + list(args)
|
||||
|
||||
return run_external_process(*cmd, **kwargs)
|
||||
if not CORE.verbose:
|
||||
kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES
|
||||
|
||||
if os.environ.get("ESPHOME_USE_SUBPROCESS") is not None:
|
||||
return run_external_process(*cmd, **kwargs)
|
||||
|
||||
import platformio.__main__
|
||||
|
||||
patch_structhash()
|
||||
patch_file_downloader()
|
||||
|
||||
# Add log filter to suppress noisy platformio messages
|
||||
log_filter = PlatformioLogFilter() if not CORE.verbose else None
|
||||
if log_filter:
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.addFilter(log_filter)
|
||||
try:
|
||||
return run_external_command(platformio.__main__.main, *cmd, **kwargs)
|
||||
finally:
|
||||
if log_filter:
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.removeFilter(log_filter)
|
||||
|
||||
|
||||
def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int:
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
"""Subprocess entry point that applies ESPHome's PlatformIO patches.
|
||||
|
||||
Invoked via ``python -m esphome.platformio_runner`` instead of
|
||||
``python -m platformio`` so that the patches (incremental rebuild
|
||||
preservation, download retries) apply inside the subprocess. Running
|
||||
PlatformIO in a subprocess keeps its ``sys.path`` mutations and other
|
||||
global state from leaking into the ESPHome process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def patch_structhash() -> None:
|
||||
"""Avoid full rebuilds when files are added or removed.
|
||||
|
||||
PlatformIO clears the build dir whenever its structure hash changes.
|
||||
We replace that with an mtime check against ``platformio.ini`` so
|
||||
incremental builds are preserved unless the project config changed.
|
||||
"""
|
||||
from platformio.run import cli, helpers
|
||||
|
||||
def patched_clean_build_dir(build_dir, *_args):
|
||||
from platformio import fs
|
||||
from platformio.project.helpers import get_project_dir
|
||||
|
||||
platformio_ini = Path(get_project_dir()) / "platformio.ini"
|
||||
build_dir = Path(build_dir)
|
||||
|
||||
if (
|
||||
build_dir.is_dir()
|
||||
and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime
|
||||
):
|
||||
fs.rmtree(build_dir)
|
||||
|
||||
if not build_dir.is_dir():
|
||||
build_dir.mkdir(parents=True)
|
||||
|
||||
helpers.clean_build_dir = patched_clean_build_dir
|
||||
cli.clean_build_dir = patched_clean_build_dir
|
||||
|
||||
|
||||
def patch_file_downloader() -> None:
|
||||
"""Retry PlatformIO package downloads with exponential backoff.
|
||||
|
||||
PlatformIO's ``FileDownloader`` uses an ``HTTPSession`` without built-in
|
||||
retry for 502/503 errors. We wrap ``__init__`` to retry on
|
||||
``PackageException`` and close the session between attempts so a new
|
||||
TCP connection can route to a different CDN edge node.
|
||||
"""
|
||||
from platformio.package.download import FileDownloader
|
||||
from platformio.package.exception import PackageException
|
||||
|
||||
if getattr(FileDownloader.__init__, "_esphome_patched", False):
|
||||
return
|
||||
|
||||
original_init = FileDownloader.__init__
|
||||
|
||||
def patched_init(self, *args: Any, **kwargs: Any) -> None:
|
||||
max_retries = 5
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
original_init(self, *args, **kwargs)
|
||||
return
|
||||
except PackageException as e:
|
||||
if attempt < max_retries - 1:
|
||||
delay = 2 ** (attempt + 1)
|
||||
_LOGGER.warning(
|
||||
"Package download failed: %s. "
|
||||
"Retrying in %d seconds... (attempt %d/%d)",
|
||||
str(e),
|
||||
delay,
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
)
|
||||
# pylint: disable=protected-access,broad-except
|
||||
try:
|
||||
if (
|
||||
hasattr(self, "_http_response")
|
||||
and self._http_response is not None
|
||||
):
|
||||
self._http_response.close()
|
||||
if hasattr(self, "_http_session"):
|
||||
self._http_session.close()
|
||||
except Exception:
|
||||
pass
|
||||
# pylint: enable=protected-access,broad-except
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access
|
||||
FileDownloader.__init__ = patched_init
|
||||
|
||||
|
||||
def main() -> int:
|
||||
patch_structhash()
|
||||
patch_file_downloader()
|
||||
|
||||
# Wrap stdout/stderr with RedirectText before PlatformIO runs:
|
||||
#
|
||||
# 1. RedirectText.isatty() unconditionally returns True. Click, tqdm, and
|
||||
# PlatformIO's own progress-bar code check ``stream.isatty()`` to
|
||||
# decide whether to emit TTY-format output (``\r`` cursor moves, ANSI
|
||||
# colors, fancy progress bars). With the wrapper in place they always
|
||||
# emit TTY format, even when our real stdout is a pipe to the parent
|
||||
# process. Downstream consumers (local terminals and the Home
|
||||
# Assistant dashboard log viewer) render the TTY control sequences
|
||||
# correctly, so the user sees real progress bars.
|
||||
#
|
||||
# 2. FILTER_PLATFORMIO_LINES is applied inside RedirectText.write() in
|
||||
# this subprocess, so noisy PlatformIO output is dropped before it
|
||||
# ever leaves the runner. This replaces the parent-side filtering
|
||||
# that was lost when we switched from in-process to subprocess — the
|
||||
# parent's ``subprocess.run`` uses ``.fileno()`` on RedirectText and
|
||||
# bypasses its ``write()`` path entirely.
|
||||
#
|
||||
# Filtering is disabled when the user passed -v / --verbose to
|
||||
# ``esphome compile``, preserving the previous in-process behavior where
|
||||
# verbose mode let all PlatformIO output through unfiltered.
|
||||
from esphome.platformio_api import FILTER_PLATFORMIO_LINES
|
||||
from esphome.util import RedirectText
|
||||
|
||||
is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[1:])
|
||||
filter_lines = None if is_verbose else FILTER_PLATFORMIO_LINES
|
||||
|
||||
sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
|
||||
sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
|
||||
|
||||
import platformio.__main__
|
||||
|
||||
return platformio.__main__.main() or 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+5
-5
@@ -133,10 +133,10 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||
[common:esp32-arduino]
|
||||
extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz
|
||||
|
||||
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||
lib_deps =
|
||||
@@ -169,9 +169,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
[common:esp32-idf]
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz
|
||||
|
||||
framework = espidf
|
||||
lib_deps =
|
||||
|
||||
@@ -20,6 +20,7 @@ classifiers = [
|
||||
"Topic :: Home Automation",
|
||||
]
|
||||
|
||||
# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76
|
||||
requires-python = ">=3.11.0,<3.15"
|
||||
|
||||
dynamic = ["dependencies", "optional-dependencies", "version"]
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.2
|
||||
esphome-dashboard==20260408.1
|
||||
aioesphomeapi==44.16.1
|
||||
aioesphomeapi==44.13.1
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.9 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.10 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -1028,8 +1028,7 @@ class BytesType(TypeInfo):
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
calc_fn = "calc_length_force" if force else "calc_length"
|
||||
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
|
||||
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
|
||||
|
||||
def get_estimated_size(self) -> int:
|
||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
||||
@@ -1110,8 +1109,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
calc_fn = "calc_length_force" if force else "calc_length"
|
||||
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len);"
|
||||
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);"
|
||||
|
||||
|
||||
class PointerToStringBufferType(PointerToBufferTypeBase):
|
||||
@@ -2681,16 +2679,6 @@ def build_message_type(
|
||||
and get_opt(desc, inline_opt, False)
|
||||
)
|
||||
|
||||
# Check if this message wants speed-optimized encode/calculate_size.
|
||||
# When set, __attribute__((optimize("O2"))) is added to the definitions
|
||||
# so GCC inlines the small ProtoEncode helpers even under -Os.
|
||||
is_speed_optimized = get_opt(desc, pb.speed_optimized, False)
|
||||
speed_attr = (
|
||||
'__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n'
|
||||
if is_speed_optimized
|
||||
else ""
|
||||
)
|
||||
|
||||
# Only generate encode method if this message needs encoding and has fields
|
||||
if needs_encode and encode and not is_inline_only:
|
||||
# Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls
|
||||
@@ -2700,7 +2688,7 @@ def build_message_type(
|
||||
)
|
||||
for line in encode
|
||||
]
|
||||
o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
|
||||
o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
|
||||
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
|
||||
o += indent("\n".join(encode_debug)) + "\n"
|
||||
o += " return pos;\n"
|
||||
@@ -2714,7 +2702,7 @@ def build_message_type(
|
||||
|
||||
# Add calculate_size method only if this message needs encoding and has fields
|
||||
if needs_encode and size_calc and not is_inline_only:
|
||||
o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n"
|
||||
o = f"uint32_t {desc.name}::calculate_size() const {{\n"
|
||||
o += " uint32_t size = 0;\n"
|
||||
o += indent("\n".join(size_calc)) + "\n"
|
||||
o += " return size;\n"
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace esphome::benchmarks {
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- random_float() ---
|
||||
// Ported from ol.yaml:148 "Random Float Benchmark"
|
||||
|
||||
static void RandomFloat(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
@@ -38,4 +37,274 @@ static void RandomUint32(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(RandomUint32);
|
||||
|
||||
// --- format_hex_to() - 6 bytes (MAC address sized) ---
|
||||
|
||||
static void FormatHexTo_6Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
|
||||
char buffer[13]; // 6 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_6Bytes);
|
||||
|
||||
// --- format_hex_to() - 16 bytes (UUID sized) ---
|
||||
|
||||
static void FormatHexTo_16Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89,
|
||||
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
|
||||
char buffer[33]; // 16 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 16);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_16Bytes);
|
||||
|
||||
// --- format_hex_to() - 100 bytes (large payload) ---
|
||||
|
||||
static void FormatHexTo_100Bytes(benchmark::State &state) {
|
||||
uint8_t data[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
data[i] = static_cast<uint8_t>(i);
|
||||
}
|
||||
char buffer[201]; // 100 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 100);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_100Bytes);
|
||||
|
||||
// --- format_hex_pretty_to() - 6 bytes with ':' separator ---
|
||||
|
||||
static void FormatHexPrettyTo_6Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
|
||||
char buffer[18]; // 6 * 3
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_pretty_to(buffer, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexPrettyTo_6Bytes);
|
||||
|
||||
// --- format_mac_addr_upper() ---
|
||||
|
||||
static void FormatMacAddrUpper(benchmark::State &state) {
|
||||
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_mac_addr_upper(mac, buffer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatMacAddrUpper);
|
||||
|
||||
// --- fnv1_hash() - short string ---
|
||||
|
||||
static void Fnv1Hash_Short(benchmark::State &state) {
|
||||
const char *str = "sensor.temperature";
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash(str);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1Hash_Short);
|
||||
|
||||
// --- fnv1_hash() - long string ---
|
||||
|
||||
static void Fnv1Hash_Long(benchmark::State &state) {
|
||||
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash(str);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1Hash_Long);
|
||||
|
||||
// --- fnv1a_hash() - short string ---
|
||||
// Use DoNotOptimize on the input pointer to prevent constexpr evaluation
|
||||
|
||||
static void Fnv1aHash_Short(benchmark::State &state) {
|
||||
const char *str = "sensor.temperature";
|
||||
benchmark::DoNotOptimize(str);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1a_hash(str);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1aHash_Short);
|
||||
|
||||
// --- fnv1a_hash() - long string ---
|
||||
|
||||
static void Fnv1aHash_Long(benchmark::State &state) {
|
||||
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
|
||||
benchmark::DoNotOptimize(str);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1a_hash(str);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1aHash_Long);
|
||||
|
||||
// --- fnv1_hash_object_id() - typical entity name ---
|
||||
|
||||
static void Fnv1HashObjectId(benchmark::State &state) {
|
||||
char name[] = "Living Room Temperature Sensor";
|
||||
size_t len = sizeof(name) - 1;
|
||||
benchmark::DoNotOptimize(name);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash_object_id(name, len);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1HashObjectId);
|
||||
|
||||
// --- parse_hex() - 6 bytes from string ---
|
||||
|
||||
static void ParseHex_6Bytes(benchmark::State &state) {
|
||||
const char *hex_str = "ABCDEF012345";
|
||||
uint8_t data[6];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
parse_hex(hex_str, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(data);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ParseHex_6Bytes);
|
||||
|
||||
// --- parse_hex() - 16 bytes from string ---
|
||||
|
||||
static void ParseHex_16Bytes(benchmark::State &state) {
|
||||
const char *hex_str = "ABCDEF0123456789FEDCBA9876543210";
|
||||
uint8_t data[16];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
parse_hex(hex_str, data, 16);
|
||||
}
|
||||
benchmark::DoNotOptimize(data);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ParseHex_16Bytes);
|
||||
|
||||
// --- crc8() - 8 bytes ---
|
||||
|
||||
static void CRC8_8Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
for (auto _ : state) {
|
||||
uint8_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= crc8(data, 8);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CRC8_8Bytes);
|
||||
|
||||
// --- crc16() - 8 bytes ---
|
||||
|
||||
static void CRC16_8Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
for (auto _ : state) {
|
||||
uint16_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= crc16(data, 8);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CRC16_8Bytes);
|
||||
|
||||
// --- value_accuracy_to_buf() - typical sensor value ---
|
||||
|
||||
static void ValueAccuracyToBuf(benchmark::State &state) {
|
||||
char raw_buf[VALUE_ACCURACY_MAX_LEN] = {};
|
||||
std::span<char, VALUE_ACCURACY_MAX_LEN> buf(raw_buf);
|
||||
float value = 23.456f;
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
value_accuracy_to_buf(buf, value, 2);
|
||||
}
|
||||
benchmark::DoNotOptimize(raw_buf);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ValueAccuracyToBuf);
|
||||
|
||||
// --- int8_to_str() ---
|
||||
|
||||
static void Int8ToStr(benchmark::State &state) {
|
||||
char buffer[5] = {};
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
int8_to_str(buffer, static_cast<int8_t>(i & 0xFF));
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Int8ToStr);
|
||||
|
||||
// --- base64_decode() - into pre-allocated buffer ---
|
||||
|
||||
static void Base64Decode_32Bytes(benchmark::State &state) {
|
||||
// 32 bytes encoded = 44 base64 chars
|
||||
const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw==";
|
||||
size_t encoded_len = 44;
|
||||
uint8_t output[32];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
base64_decode(encoded, encoded_len, output, sizeof(output));
|
||||
}
|
||||
benchmark::DoNotOptimize(output);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Base64Decode_32Bytes);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
|
||||
@@ -8,16 +8,10 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import VARIANT_ESP32, VARIANTS
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT
|
||||
from esphome.components.esp32.gpio import validate_gpio_pin
|
||||
from esphome.components.esp32 import VARIANTS
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_IGNORE_PIN_VALIDATION_ERROR,
|
||||
CONF_NUMBER,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.const import CONF_ESPHOME, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
@@ -155,73 +149,6 @@ def test_execute_from_psram_p4_sdkconfig(
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
|
||||
|
||||
def test_ignore_pin_validation_error_on_clean_pin_warns(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A pin that passes validation but sets `ignore_pin_validation_error: true`
|
||||
should log a warning nudging the user to remove the flag, and not raise."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
|
||||
with caplog.at_level("WARNING"):
|
||||
result = validate_gpio_pin(pin)
|
||||
|
||||
assert result[CONF_NUMBER] == 4
|
||||
assert "GPIO4 has no validation errors to ignore" in caplog.text
|
||||
|
||||
|
||||
def test_ignore_pin_validation_error_on_dirty_pin_suppresses(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A pin that fails validation with `ignore_pin_validation_error: true` should
|
||||
log the suppression warning and not raise (existing behavior)."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
# GPIO6 is a flash pin on ESP32 -> pin_validation raises cv.Invalid
|
||||
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
|
||||
with caplog.at_level("WARNING"):
|
||||
result = validate_gpio_pin(pin)
|
||||
|
||||
assert result[CONF_NUMBER] == 6
|
||||
assert "Ignoring validation error on pin 6" in caplog.text
|
||||
|
||||
|
||||
def test_dirty_pin_without_ignore_flag_raises(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A pin that fails validation without the ignore flag should still raise."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
|
||||
with pytest.raises(cv.Invalid, match="flash interface"):
|
||||
validate_gpio_pin(pin)
|
||||
|
||||
|
||||
def test_clean_pin_without_ignore_flag_does_not_warn(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A clean pin without the ignore flag should pass silently."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
|
||||
with caplog.at_level("WARNING"):
|
||||
result = validate_gpio_pin(pin)
|
||||
|
||||
assert result[CONF_NUMBER] == 4
|
||||
assert "has no validation errors to ignore" not in caplog.text
|
||||
|
||||
|
||||
def test_execute_from_psram_disabled_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
"""Tests for the _final_validate buffer size calculation in mipi_spi."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
|
||||
from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
from esphome.const import CONF_BUFFER_SIZE, PlatformFramework
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _validated(config: ConfigType) -> ConfigType:
|
||||
"""Run the component config schema followed by the final validation."""
|
||||
config = CONFIG_SCHEMA(config)
|
||||
FINAL_VALIDATE_SCHEMA(config)
|
||||
return config
|
||||
|
||||
|
||||
def _custom_config(
|
||||
width: int,
|
||||
height: int,
|
||||
color_depth: str | int | None = None,
|
||||
**extra: Any,
|
||||
) -> ConfigType:
|
||||
"""Build a minimal valid custom-model config with the given dimensions."""
|
||||
config: ConfigType = {
|
||||
"model": "custom",
|
||||
"dc_pin": 18,
|
||||
"dimensions": {"width": width, "height": height},
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
}
|
||||
if color_depth is not None:
|
||||
config["color_depth"] = color_depth
|
||||
config.update(extra)
|
||||
return config
|
||||
|
||||
|
||||
# The auto buffer-size selection inside _final_validate targets ~20 kB of
|
||||
# pixel buffer. For a buffer of ``depth_bytes * width * height``, it picks the
|
||||
# smallest integer ``x`` in range(2, 8) such that
|
||||
# ``min(20000, buffer // 4) / buffer >= 1 / x`` (falling back to ``x = 8``).
|
||||
# The test cases below cover the full range of possible outcomes (1/4 .. 1/8).
|
||||
@pytest.mark.parametrize(
|
||||
("width", "height", "color_depth", "expected"),
|
||||
[
|
||||
# 16-bit color depth -- buffer = 2 * width * height
|
||||
# 128*160*2 = 40960 B -> fraction = 10240/40960 = 0.25 -> x = 4
|
||||
pytest.param(128, 160, "16bit", 1.0 / 4, id="16bit_tiny"),
|
||||
# 200*224*2 = 89600 B -> fraction = 20000/89600 ≈ 0.2232 -> x = 5
|
||||
pytest.param(200, 224, "16bit", 1.0 / 5, id="16bit_small"),
|
||||
# 240*224*2 = 107520 B -> fraction ≈ 0.1860 -> x = 6
|
||||
pytest.param(240, 224, "16bit", 1.0 / 6, id="16bit_medium"),
|
||||
# 200*320*2 = 128000 B -> fraction = 0.15625 -> x = 7
|
||||
pytest.param(200, 320, "16bit", 1.0 / 7, id="16bit_large"),
|
||||
# 240*320*2 = 153600 B -> fraction ≈ 0.1302 -> default x = 8
|
||||
pytest.param(240, 320, "16bit", 1.0 / 8, id="16bit_xlarge"),
|
||||
# 320*480*2 = 307200 B -> fraction ≈ 0.0651 -> default x = 8
|
||||
pytest.param(320, 480, "16bit", 1.0 / 8, id="16bit_huge"),
|
||||
# 8-bit color depth -- buffer = width * height
|
||||
# 320*240 = 76800 B -> fraction = 19200/76800 = 0.25 -> x = 4
|
||||
pytest.param(320, 240, "8bit", 1.0 / 4, id="8bit_tiny"),
|
||||
# 400*224 = 89600 B -> fraction ≈ 0.2232 -> x = 5
|
||||
pytest.param(400, 224, "8bit", 1.0 / 5, id="8bit_small"),
|
||||
# 480*224 = 107520 B -> fraction ≈ 0.1860 -> x = 6
|
||||
pytest.param(480, 224, "8bit", 1.0 / 6, id="8bit_medium"),
|
||||
# 400*320 = 128000 B -> fraction = 0.15625 -> x = 7
|
||||
pytest.param(400, 320, "8bit", 1.0 / 7, id="8bit_large"),
|
||||
# 480*320 = 153600 B -> fraction ≈ 0.1302 -> default x = 8
|
||||
pytest.param(480, 320, "8bit", 1.0 / 8, id="8bit_xlarge"),
|
||||
],
|
||||
)
|
||||
def test_buffer_size_auto_selected(
|
||||
width: int,
|
||||
height: int,
|
||||
color_depth: str,
|
||||
expected: float,
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Without PSRAM or an explicit buffer_size, a fraction is chosen from the display size.
|
||||
|
||||
Without any drawing method and without LVGL, final validation also auto-enables
|
||||
``show_test_card``, which in turn makes the component require a buffer and therefore
|
||||
triggers the buffer-size selection path.
|
||||
"""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
|
||||
config = _validated(_custom_config(width, height, color_depth))
|
||||
|
||||
# Sanity check: final validation should have enabled the test card for us,
|
||||
# which is what causes the buffer-size calculation to actually run.
|
||||
assert config.get(CONF_SHOW_TEST_CARD) is True
|
||||
assert config[CONF_BUFFER_SIZE] == pytest.approx(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"buffer_size",
|
||||
[0.125, 0.25, 0.5, 1.0],
|
||||
ids=["one_eighth", "one_quarter", "half", "full"],
|
||||
)
|
||||
def test_explicit_buffer_size_is_preserved(
|
||||
buffer_size: float,
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""An explicitly configured buffer_size is never overridden by final validation."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
|
||||
config = _validated(
|
||||
_custom_config(240, 320, "16bit", buffer_size=buffer_size),
|
||||
)
|
||||
|
||||
assert config[CONF_BUFFER_SIZE] == pytest.approx(buffer_size)
|
||||
|
||||
|
||||
def test_buffer_size_not_set_when_psram_enabled(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config,
|
||||
) -> None:
|
||||
"""When PSRAM is enabled the auto buffer-size selection is skipped."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
# Presence of the psram domain in the full config is what _final_validate checks.
|
||||
set_component_config("psram", True)
|
||||
|
||||
config = _validated(_custom_config(240, 320, "16bit"))
|
||||
|
||||
assert CONF_BUFFER_SIZE not in config
|
||||
|
||||
|
||||
def test_buffer_size_not_set_when_buffer_not_required(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config,
|
||||
) -> None:
|
||||
"""With LVGL present and no drawing methods, no buffer fraction is chosen.
|
||||
|
||||
LVGL suppresses the automatic show_test_card injection, which means
|
||||
``requires_buffer`` is False and the early-return branch fires.
|
||||
"""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("lvgl", [])
|
||||
|
||||
config = _validated(_custom_config(240, 320, "16bit"))
|
||||
|
||||
assert CONF_BUFFER_SIZE not in config
|
||||
# And no test card should have been auto-enabled either.
|
||||
assert not config.get(CONF_SHOW_TEST_CARD)
|
||||
|
||||
|
||||
def test_buffer_size_selected_when_lvgl_with_test_card(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config,
|
||||
) -> None:
|
||||
"""LVGL present + an explicit drawing method still triggers buffer sizing.
|
||||
|
||||
When LVGL is enabled, ``show_test_card`` is not injected automatically,
|
||||
but users can still request it explicitly -- in that case ``requires_buffer``
|
||||
is True and the buffer-size heuristic still runs.
|
||||
"""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("lvgl", [])
|
||||
|
||||
# 128x160 @ 16bit -> expected 1/4 (see test_buffer_size_auto_selected).
|
||||
config = _validated(
|
||||
_custom_config(128, 160, "16bit", show_test_card=True),
|
||||
)
|
||||
|
||||
assert config[CONF_BUFFER_SIZE] == pytest.approx(1.0 / 4)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user