mirror of
https://github.com/esphome/esphome.git
synced 2026-09-04 03:56:04 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f280abdbd | ||
|
|
8a88a3f576 | ||
|
|
82431641cc | ||
|
|
f79e9f9cb1 | ||
|
|
f17fd9fcb3 | ||
|
|
07f091059c | ||
|
|
2807a6932b | ||
|
|
ec09ce636c | ||
|
|
7f437aa680 | ||
|
|
f8a60d0259 | ||
|
|
50f50aa7e0 | ||
|
|
676ecae469 | ||
|
|
78258dc554 | ||
|
|
55180938ca | ||
|
|
d88a95c3f6 | ||
|
|
4dad932cd8 | ||
|
|
3b8b90b2cd | ||
|
|
9df8505c8d | ||
|
|
427b83dc0a | ||
|
|
deb641d413 | ||
|
|
0b3e399588 | ||
|
|
33d9adbef1 | ||
|
|
88d6ded5f3 |
@@ -1,39 +0,0 @@
|
||||
name: Cache Arduino ESP8266
|
||||
description: >
|
||||
Resolve the pinned Arduino core and xtensa toolchain versions and cache the
|
||||
native ESP8266 install (~110 MB framework + toolchain; no ccache store, the
|
||||
seed job saves before any compile runs). Exports
|
||||
ESPHOME_ARDUINO8266_PREFIX to the job so every later step installs into
|
||||
the cached path; the Python venv must already be restored. Mirrors
|
||||
cache-esp-idf: only dev-branch pushes write the shared cache, everything
|
||||
else restores.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Resolve the native toolchain cache key
|
||||
# Versions are pinned in code, not a hashable file; resolve them so a
|
||||
# bump changes the cache key. Assignment form so errexit catches a
|
||||
# resolver failure.
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
# One owner for the install prefix: exported here and referenced by
|
||||
# the cache steps below via env, so the caller's install and the
|
||||
# cached path cannot diverge.
|
||||
echo "ESPHOME_ARDUINO8266_PREFIX=$HOME/.esphome-arduino8266" >> "$GITHUB_ENV"
|
||||
. venv/bin/activate
|
||||
key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")')
|
||||
[ -n "$key" ] || exit 1
|
||||
echo "key=$key" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache the native toolchain (write on dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
|
||||
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
|
||||
- name: Restore the native toolchain (off dev)
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
|
||||
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
|
||||
@@ -102,8 +102,6 @@ jobs:
|
||||
device-builder: ${{ steps.determine.outputs.device-builder }}
|
||||
esp32-platformio: ${{ steps.determine.outputs.esp32-platformio }}
|
||||
esp32-platformio-components: ${{ steps.determine.outputs.esp32-platformio-components }}
|
||||
esp8266-native: ${{ steps.determine.outputs.esp8266-native }}
|
||||
esp8266-native-components: ${{ steps.determine.outputs.esp8266-native-components }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||
@@ -167,8 +165,6 @@ jobs:
|
||||
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
|
||||
echo "esp32-platformio=$(echo "$output" | jq -r '.esp32_platformio')" >> $GITHUB_OUTPUT
|
||||
echo "esp32-platformio-components=$(echo "$output" | jq -r '.esp32_platformio_components')" >> $GITHUB_OUTPUT
|
||||
echo "esp8266-native=$(echo "$output" | jq -r '.esp8266_native')" >> $GITHUB_OUTPUT
|
||||
echo "esp8266-native-components=$(echo "$output" | jq -r '.esp8266_native_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
@@ -187,32 +183,6 @@ jobs:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
|
||||
seed-esp8266-native-cache:
|
||||
name: Seed the esp8266 native toolchain cache
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
# PR-branch cache saves are invisible to other PRs, so dev pushes seed
|
||||
# the shared entry test-esp8266-native restores. Only dev: the composite
|
||||
# action saves nowhere else, so a beta/release push would download the
|
||||
# toolchain and discard it.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache the native toolchain
|
||||
uses: ./.github/actions/cache-arduino8266
|
||||
- name: Install the native toolchain
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python -c "from esphome.arduino8266.framework import check_and_install; from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION; check_and_install(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION)"
|
||||
|
||||
ci-custom:
|
||||
name: Run script/ci-custom
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -404,9 +374,8 @@ jobs:
|
||||
- name: Install apt packages (cached)
|
||||
# ccache speeds up the host compiles. A cache hit never touches apt
|
||||
# (mirror outages cannot hang the job); the timeout bounds the cold
|
||||
# path. Packages and version must match seed-apt-cache exactly.
|
||||
# libsdl2-dev is needed by the headless display tests, which capture
|
||||
# screenshots.
|
||||
# path. Packages and version must match seed-apt-cache exactly;
|
||||
# libsdl2-dev is unused here and carried only for cache-key parity.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
@@ -469,16 +438,6 @@ jobs:
|
||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
|
||||
--junitxml=junit-integration.xml "${test_files[@]}"
|
||||
- name: Upload test artifacts
|
||||
# Tests that compare rendered output write the image they actually got here, so a
|
||||
# failure can be looked at without reproducing the whole build locally.
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: integration-test-artifacts-${{ matrix.bucket.name }}
|
||||
path: test_artifacts/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
- name: Upload junit timings
|
||||
# Consumed by sync-integration-durations.yml through
|
||||
# script/update_integration_test_durations.py; only full matrix dev
|
||||
@@ -1258,7 +1217,7 @@ jobs:
|
||||
|
||||
# compile validates config first, so a separate config pass is
|
||||
# redundant for this smoke test. ESP-IDF framework via PlatformIO:
|
||||
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio --fail-on-no-tests
|
||||
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio
|
||||
|
||||
echo ""
|
||||
echo "ESP-IDF-via-PlatformIO build passed! Starting Arduino smoke test..."
|
||||
@@ -1267,40 +1226,6 @@ jobs:
|
||||
# Arduino framework via PlatformIO (only components with an esp32-ard test are built):
|
||||
python3 script/test_build_components.py -e compile -t esp32-ard -c "$TEST_COMPONENTS" -f --toolchain platformio
|
||||
|
||||
test-esp8266-native:
|
||||
name: Test esp8266 components with the native toolchain
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
|
||||
env:
|
||||
# Computed by script/determine-jobs.py (ESP8266_NATIVE_TEST_COMPONENTS)
|
||||
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache the native toolchain
|
||||
uses: ./.github/actions/cache-arduino8266
|
||||
|
||||
- name: Run native toolchain compile test
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
echo "Testing components: $TEST_COMPONENTS"
|
||||
echo ""
|
||||
|
||||
# ESP8266 Arduino built directly (no PlatformIO); compile validates
|
||||
# config first, so a separate config pass is redundant.
|
||||
python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
|
||||
|
||||
device-builder:
|
||||
name: Test downstream esphome/device-builder
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -1663,7 +1588,6 @@ jobs:
|
||||
needs:
|
||||
- common
|
||||
- seed-apt-cache
|
||||
- seed-esp8266-native-cache
|
||||
- determine-jobs
|
||||
- ci-custom
|
||||
- pylint
|
||||
@@ -1679,7 +1603,6 @@ jobs:
|
||||
- clang-tidy-esp32-variants
|
||||
- test-build-components-split
|
||||
- test-esp32-platformio
|
||||
- test-esp8266-native
|
||||
- device-builder
|
||||
- memory-impact-target-branch
|
||||
- memory-impact-pr-branch
|
||||
|
||||
@@ -137,8 +137,6 @@ config/
|
||||
!tests/component_tests/**/config/
|
||||
tests/build/
|
||||
tests/.esphome/
|
||||
# Output kept by failing tests for inspection; uploaded by CI
|
||||
test_artifacts/
|
||||
/.temp-clang-tidy.cpp
|
||||
/.temp/
|
||||
.pio/
|
||||
|
||||
@@ -496,7 +496,6 @@ esphome/components/sm2335/* @Cossid
|
||||
esphome/components/sml/* @alengwenus
|
||||
esphome/components/smt100/* @piechade
|
||||
esphome/components/sn74hc165/* @jesserockz
|
||||
esphome/components/snapshot/* @clydebarrow
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/sound_level/* @kahrendt
|
||||
|
||||
@@ -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.10.0-dev
|
||||
PROJECT_NUMBER = 2026.9.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
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+31
-83
@@ -817,9 +817,7 @@ def write_cpp_file() -> int:
|
||||
from esphome.build_gen import espidf
|
||||
|
||||
espidf.write_project()
|
||||
elif not CORE.using_native_toolchain:
|
||||
# Other native builds generate their project at compile time;
|
||||
# never write a platformio.ini for them
|
||||
else:
|
||||
from esphome.build_gen import platformio
|
||||
|
||||
platformio.write_project()
|
||||
@@ -861,14 +859,20 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
toolchain.create_elf_copy()
|
||||
from esphome.build_helpers.idedata import warn_if_idedata_missing
|
||||
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
|
||||
|
||||
warn_if_idedata_missing(toolchain.get_idedata)
|
||||
elif CORE.using_native_toolchain:
|
||||
raise EsphomeError(
|
||||
f"Toolchain '{CORE.toolchain.value}' resolved but no platform "
|
||||
"backend claimed the build"
|
||||
)
|
||||
try:
|
||||
if toolchain.get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
# The firmware already built; an idedata failure must not fail
|
||||
# a successful build.
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -971,15 +975,12 @@ def upload_using_esptool(
|
||||
|
||||
if file is not None:
|
||||
flash_images = [FlashImage(path=file, offset="0x0")]
|
||||
elif (native := _native_toolchain_module()) is not None:
|
||||
# Every native backend supplies its own 0x0 flash image (bootloader
|
||||
# and partitions included where the target needs them)
|
||||
image = native.get_factory_firmware_path()
|
||||
if not image.is_file():
|
||||
raise EsphomeError(
|
||||
f"{image} does not exist; compile the configuration first"
|
||||
)
|
||||
flash_images = [FlashImage(path=image, offset="0x0")]
|
||||
elif CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
flash_images = [
|
||||
FlashImage(path=toolchain.get_factory_firmware_path(), offset="0x0")
|
||||
]
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -1949,39 +1950,15 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
return run_multiple_configs(files, build_command)
|
||||
|
||||
|
||||
# Native build backend per (target platform, toolchain). Keyed here rather
|
||||
# than through a platform hook so the serial upload/logs fast path never
|
||||
# imports the platform component package (see the esp32 variant comment in
|
||||
# upload_using_esptool); the platform half comes from CORE.data the same way.
|
||||
_NATIVE_TOOLCHAIN_MODULES = {
|
||||
("esp32", Toolchain.ESP_IDF): "esphome.espidf.toolchain",
|
||||
("esp8266", Toolchain.ARDUINO): "esphome.arduino8266.toolchain",
|
||||
}
|
||||
|
||||
|
||||
def _native_toolchain_module():
|
||||
"""The native build backend module for the resolved toolchain."""
|
||||
if not CORE.using_native_toolchain:
|
||||
return None
|
||||
key = (CORE.target_platform, CORE.toolchain)
|
||||
if (module_path := _NATIVE_TOOLCHAIN_MODULES.get(key)) is None:
|
||||
# Degrading to the PlatformIO path would build with the wrong backend
|
||||
raise EsphomeError(
|
||||
f"Toolchain '{CORE.toolchain.value}' has no native build backend "
|
||||
f"module for platform {CORE.target_platform}"
|
||||
)
|
||||
return importlib.import_module(module_path)
|
||||
|
||||
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
native_toolchain = _native_toolchain_module()
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
# Native ESP-IDF derives idedata from the build's compile_commands.json,
|
||||
# so the configuration must already be compiled.
|
||||
from esphome.espidf import toolchain as espidf_toolchain
|
||||
|
||||
if native_toolchain is not None:
|
||||
# Native toolchains derive idedata from the build's
|
||||
# compile_commands.json, so the configuration must already be compiled.
|
||||
idedata = native_toolchain.get_idedata()
|
||||
idedata = espidf_toolchain.get_idedata()
|
||||
if idedata is None:
|
||||
_LOGGER.error(
|
||||
"No idedata available; compile the configuration first",
|
||||
@@ -2020,17 +1997,6 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
|
||||
|
||||
# Refuse an unsupported toolchain before paying for a full compile
|
||||
native_toolchain = _native_toolchain_module()
|
||||
if native_toolchain is None and not CORE.using_toolchain_platformio:
|
||||
_LOGGER.error(
|
||||
"analyze-memory is not supported with the '%s' toolchain on %s; "
|
||||
"re-run with --toolchain platformio",
|
||||
CORE.toolchain.value if CORE.toolchain else "unresolved",
|
||||
CORE.target_platform,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Always compile to ensure fresh data (fast if no changes - just relinks)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
@@ -2042,31 +2008,13 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
|
||||
# Get idedata for analysis
|
||||
idedata = None
|
||||
if native_toolchain is not None:
|
||||
objdump = native_toolchain.get_objdump_path()
|
||||
readelf = native_toolchain.get_readelf_path()
|
||||
for tool in (objdump, readelf):
|
||||
if not tool.is_file():
|
||||
# The analyzer would silently fall back to host binutils,
|
||||
# which cannot read the target ELF. clean-all is heavy for
|
||||
# ESP-IDF, so suggest a recompile first.
|
||||
_LOGGER.error(
|
||||
"%s is missing; the toolchain install may be incomplete "
|
||||
"(recompile, or run 'esphome clean-all' if it persists)",
|
||||
tool,
|
||||
)
|
||||
return 1
|
||||
objdump_path = str(objdump)
|
||||
readelf_path = str(readelf)
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
firmware_elf = native_toolchain.get_elf_path()
|
||||
if not firmware_elf.is_file():
|
||||
# The analyzer swallows tool failures, so a missing ELF would
|
||||
# produce an exit-0 zeroed report
|
||||
_LOGGER.error(
|
||||
"%s is missing; compile the configuration first", firmware_elf
|
||||
)
|
||||
return 1
|
||||
objdump_path = str(toolchain.get_objdump_path())
|
||||
readelf_path = str(toolchain.get_readelf_path())
|
||||
|
||||
firmware_elf = toolchain.get_elf_path()
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ from typing import NamedTuple
|
||||
|
||||
from esphome.build_helpers.ccache import ccache_defaults_env
|
||||
from esphome.build_helpers.ninja import find_ninja
|
||||
from esphome.build_helpers.pch import ccache_pch_env
|
||||
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
|
||||
from esphome.core import EsphomeError, Version
|
||||
from esphome.framework_helpers import str_to_lst_of_str
|
||||
@@ -45,7 +44,8 @@ def get_arduino8266_tools_path() -> Path:
|
||||
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
|
||||
|
||||
|
||||
# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0
|
||||
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
|
||||
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
|
||||
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
|
||||
|
||||
|
||||
@@ -53,16 +53,20 @@ def framework_package_version(ver: Version) -> str:
|
||||
"""Map an Arduino core version to its registry package version (3.1.2 ->
|
||||
3.30102.0; the leading 3 is the package major).
|
||||
|
||||
Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION.
|
||||
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
|
||||
at MIN_FRAMEWORK_VERSION.
|
||||
"""
|
||||
if ver.major > 3:
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} is not supported yet; "
|
||||
"the newest known core series is 3.x"
|
||||
)
|
||||
if ver.major < 3:
|
||||
if ver <= Version(2, 6, 2):
|
||||
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
|
||||
# boundary as _format_framework_arduino_version's era guard)
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} is not supported; ESPHome requires core 3.x"
|
||||
f"Arduino core {ver} uses an older package encoding than this "
|
||||
"helper implements (newer than 2.6.2)"
|
||||
)
|
||||
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
|
||||
@@ -157,6 +161,4 @@ def ccache_env(ccache: str | None) -> dict[str, str]:
|
||||
"""
|
||||
if ccache is None:
|
||||
return {}
|
||||
env = ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
|
||||
env.update(ccache_pch_env())
|
||||
return env
|
||||
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
"""Native Arduino ESP8266 build driver (the PlatformIO ``run`` equivalent)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from esphome.arduino8266 import framework
|
||||
from esphome.build_helpers.ccache import resolve_ccache_path
|
||||
from esphome.const import (
|
||||
CONF_COMPILE_PROCESS_LIMIT,
|
||||
CONF_ESPHOME,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# ESP8266 user RAM (matches upload.maximum_ram_size in every board manifest)
|
||||
_MAX_RAM_SIZE = 81920
|
||||
|
||||
|
||||
def _warn_ignored_platformio_options() -> None:
|
||||
"""Warn for component-added platformio options the native build drops.
|
||||
|
||||
The consumed set is exported by core/config.py next to the routing that
|
||||
stores these options, so the two cannot drift; YAML upload_speed never
|
||||
reaches CORE.platformio_options here.
|
||||
"""
|
||||
from esphome.core.config import NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
|
||||
|
||||
consumed = NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
|
||||
for key in sorted(CORE.platformio_options or {}):
|
||||
if key not in consumed:
|
||||
_LOGGER.warning(
|
||||
"platformio_options->%s is ignored when building with the "
|
||||
"native 'arduino' toolchain",
|
||||
key,
|
||||
)
|
||||
|
||||
|
||||
_RAM_SECTIONS = (".data", ".rodata", ".bss")
|
||||
_FLASH_SECTIONS = (".irom0.text", ".text", ".text1", ".data", ".rodata")
|
||||
|
||||
|
||||
def get_build_dir() -> Path:
|
||||
return CORE.relative_pioenvs_path(CORE.name)
|
||||
|
||||
|
||||
def get_elf_path() -> Path:
|
||||
return get_build_dir() / "firmware.elf"
|
||||
|
||||
|
||||
def _toolchain_tool(name: str) -> Path:
|
||||
return framework.toolchain_tool(framework.get_toolchain_path(), name)
|
||||
|
||||
|
||||
def get_factory_firmware_path() -> Path:
|
||||
"""The image to serial-flash at 0x0 (same bytes as firmware.bin: the
|
||||
8266 factory copy exists for artifact-contract parity, not content)."""
|
||||
return get_build_dir() / "firmware.factory.bin"
|
||||
|
||||
|
||||
def get_addr2line_path() -> Path:
|
||||
return _toolchain_tool("addr2line")
|
||||
|
||||
|
||||
def get_objdump_path() -> Path:
|
||||
return _toolchain_tool("objdump")
|
||||
|
||||
|
||||
def get_readelf_path() -> Path:
|
||||
return _toolchain_tool("readelf")
|
||||
|
||||
|
||||
def run_compile(config: ConfigType, verbose: bool) -> int:
|
||||
from esphome.build_gen import arduino8266 as build_gen
|
||||
|
||||
_warn_ignored_platformio_options()
|
||||
paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
|
||||
# Resolved once per build: the resolution probes PATH and spawns the
|
||||
# runnability check, and three consumers need the same answer
|
||||
ccache = resolve_ccache_path()
|
||||
ninja_changed = build_gen.write_project(paths, ccache)
|
||||
|
||||
build_dir = get_build_dir()
|
||||
env = framework.get_build_env(paths.toolchain, ccache)
|
||||
|
||||
# Regenerate the compile DB before the build (a pure function of
|
||||
# build.ninja); skip only when it is at least as fresh as build.ninja
|
||||
# (an interrupted previous run may have rewritten the manifest without
|
||||
# regenerating the DB).
|
||||
compdb = build_dir / "compile_commands.json"
|
||||
compdb_stamp = build_dir / ".compile_commands.stamp"
|
||||
ninja_file = build_dir / "build.ninja"
|
||||
# Freshness rides a stamp: the DB itself is written through
|
||||
# write_file_if_changed (its mtime feeds get_idedata's cache), so a
|
||||
# regeneration with identical content would stay "stale" forever
|
||||
if (
|
||||
ninja_changed
|
||||
or not compdb.is_file()
|
||||
or not compdb_stamp.is_file()
|
||||
or compdb_stamp.stat().st_mtime < ninja_file.stat().st_mtime
|
||||
):
|
||||
_write_compile_commands(paths.ninja, build_dir, env)
|
||||
compdb_stamp.touch()
|
||||
|
||||
cmd = [str(paths.ninja)]
|
||||
if verbose:
|
||||
cmd.append("-v")
|
||||
if jobs := config[CONF_ESPHOME].get(CONF_COMPILE_PROCESS_LIMIT):
|
||||
cmd += ["-j", str(jobs)]
|
||||
# Explicit targets, not the default statement: a generator defect that
|
||||
# drops them fails loudly with "unknown target" instead of a green
|
||||
# no-op run that leaves stale artifacts in place
|
||||
targets = ["firmware.factory.bin", "firmware.ota.bin"]
|
||||
cmd += targets
|
||||
|
||||
# A dry-run probe keeps a no-op rebuild quiet: ninja would only print
|
||||
# "no work to do". A freshly rewritten manifest all but guarantees work,
|
||||
# so skip the probe (and its full stat pass) on that path. cwd instead
|
||||
# of -C also drops the "Entering directory" banner on real builds.
|
||||
skip_build = False
|
||||
if not ninja_changed:
|
||||
probe = subprocess.run(
|
||||
[str(paths.ninja), "-n", *targets],
|
||||
cwd=build_dir,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
if probe.stderr.strip():
|
||||
# A load-time diagnostic (e.g. "multiple rules generate X")
|
||||
# flags a generator bug; the skip branch would otherwise
|
||||
# swallow it forever
|
||||
_LOGGER.warning("ninja: %s", probe.stderr.strip())
|
||||
if probe.returncode != 0:
|
||||
# An unknown target here is the defective-manifest case; fall
|
||||
# through to the real build so the error prints attributably
|
||||
_LOGGER.debug("ninja probe failed; running the full build")
|
||||
skip_build = probe.returncode == 0 and "no work to do" in probe.stdout
|
||||
if skip_build:
|
||||
_LOGGER.debug("ninja: nothing to rebuild")
|
||||
else:
|
||||
_LOGGER.debug("Running: %s", " ".join(cmd))
|
||||
rc = subprocess.run(
|
||||
cmd, cwd=build_dir, env=env, check=False, close_fds=False
|
||||
).returncode
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
# ninja already refused a manifest missing the explicit targets above;
|
||||
# existence covers the remaining hole (a rule that ran but wrote
|
||||
# elsewhere). The factory/ota copies are what upload and OTA consume.
|
||||
build_dir_artifacts = (
|
||||
get_elf_path(),
|
||||
build_dir / "firmware.bin",
|
||||
get_factory_firmware_path(),
|
||||
build_dir / "firmware.ota.bin",
|
||||
)
|
||||
for artifact in build_dir_artifacts:
|
||||
if not artifact.is_file():
|
||||
_LOGGER.error("Build produced no %s", artifact)
|
||||
return 1
|
||||
|
||||
if not _print_size_summary(build_dir, paths):
|
||||
# The cause was already warned; name the consequence so a build
|
||||
# contributing no RAM/Flash metric is visible to CI harnesses
|
||||
_LOGGER.warning("Firmware size summary unavailable for this build")
|
||||
from esphome.build_helpers.idedata import warn_if_idedata_missing
|
||||
|
||||
warn_if_idedata_missing(lambda: get_idedata(ccache))
|
||||
return 0
|
||||
|
||||
|
||||
def _write_compile_commands(
|
||||
ninja_path: Path, build_dir: Path, env: dict[str, str]
|
||||
) -> None:
|
||||
compdb = build_dir / "compile_commands.json"
|
||||
result = subprocess.run(
|
||||
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", "c", "cxx", "asm"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Drop any stale database so consumers (IDE integration, clang-tidy,
|
||||
# the memory analyzer) can't silently read outdated data.
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(f"Could not generate compile_commands.json: {result.stderr}")
|
||||
try:
|
||||
entries = json.loads(result.stdout)
|
||||
except ValueError as err:
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(
|
||||
f"ninja produced an unparsable compile database: {err} "
|
||||
f"(output starts {result.stdout[:120]!r})"
|
||||
) from err
|
||||
if not entries:
|
||||
# compdb exits 0 with [] for unknown rule names; a renamed compile
|
||||
# rule must fail the build, not silently strand every consumer
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(
|
||||
"ninja produced an empty compile database; the generator's rule "
|
||||
"names no longer match"
|
||||
)
|
||||
# write_file_if_changed keeps the mtime stable on no-op builds so the
|
||||
# idedata cache in get_idedata() stays valid.
|
||||
write_file_if_changed(compdb, result.stdout)
|
||||
|
||||
|
||||
def _parse_app_size(build_dir: Path, paths: framework.InstalledPaths) -> int | None:
|
||||
"""Read the app flash budget (irom0_0_seg length) from the linker script."""
|
||||
from esphome.build_gen.arduino8266 import get_flash_ld_path
|
||||
from esphome.components.esp8266.build_surgery import segment_length
|
||||
|
||||
# Warnings, not debug: without the app size the Flash summary line is
|
||||
# dropped and CI's memory-impact extraction loses its flash metric.
|
||||
ld_path = get_flash_ld_path(build_dir, paths)
|
||||
try:
|
||||
ld_text = ld_path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
# UnicodeDecodeError: a truncated/corrupt script must degrade to
|
||||
# the same warning, never abort an already-linked build
|
||||
_LOGGER.warning("Cannot read linker script for the Flash summary: %s", err)
|
||||
return None
|
||||
app_size = segment_length(ld_text, "irom0_0_seg")
|
||||
if app_size is None:
|
||||
_LOGGER.warning("irom0_0_seg not found in %s; skipping Flash summary", ld_path)
|
||||
return None
|
||||
if app_size == 0:
|
||||
_LOGGER.warning(
|
||||
"irom0_0_seg has zero length in %s; skipping Flash summary", ld_path
|
||||
)
|
||||
return None
|
||||
return app_size
|
||||
|
||||
|
||||
def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> bool:
|
||||
"""Print the PlatformIO-shaped RAM/Flash lines; False when skipped.
|
||||
|
||||
The exact shape (including the bar) is parsed by
|
||||
``script/ci_memory_impact_extract.py``; ``print_size_line`` matches it.
|
||||
"""
|
||||
from esphome.build_helpers.size_summary import print_size_line
|
||||
|
||||
size_tool = _toolchain_tool("size")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(size_tool), "-A", "-d", str(get_elf_path())],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
except OSError as err:
|
||||
# The summary is a bonus artifact like idedata; a truncated
|
||||
# toolchain extraction must not discard an already-linked build
|
||||
_LOGGER.warning("Could not summarize firmware size: %s", err)
|
||||
return False
|
||||
if result.returncode != 0:
|
||||
_LOGGER.warning("Could not summarize firmware size: %s", result.stderr)
|
||||
return False
|
||||
sections: dict[str, int] = {}
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0].startswith("."):
|
||||
try:
|
||||
sections[parts[0]] = int(parts[1])
|
||||
except ValueError:
|
||||
# An unparsed RAM/Flash section trips the missing-sections
|
||||
# guard below, so no total is built on a dropped value
|
||||
_LOGGER.warning("Unparsable size output for section %s", parts[0])
|
||||
if missing := set(_RAM_SECTIONS + _FLASH_SECTIONS) - set(sections):
|
||||
# A defaulted 0 would print a confidently wrong total for CI's metric
|
||||
_LOGGER.warning(
|
||||
"Size output is missing section(s) %s; skipping the size summary",
|
||||
", ".join(sorted(missing)),
|
||||
)
|
||||
return False
|
||||
# Resolve the flash budget before printing anything: a RAM line without
|
||||
# its Flash line would let CI's memory-impact extraction sum the two
|
||||
# metrics over different build counts (_parse_app_size already warned).
|
||||
app_size = _parse_app_size(build_dir, paths)
|
||||
if not app_size:
|
||||
return False
|
||||
ram = sum(sections[s] for s in _RAM_SECTIONS)
|
||||
flash = sum(sections[s] for s in _FLASH_SECTIONS)
|
||||
print_size_line("RAM", ram, _MAX_RAM_SIZE)
|
||||
print_size_line("Flash", flash, app_size)
|
||||
return True
|
||||
|
||||
|
||||
# Sentinel: "resolve for me"; None is a real value meaning disabled.
|
||||
_CCACHE_UNRESOLVED: Any = object()
|
||||
|
||||
|
||||
def get_idedata(ccache: str | None = _CCACHE_UNRESOLVED) -> dict | None:
|
||||
"""Derive idedata from the build's compile_commands.json.
|
||||
|
||||
Same contract as ``espidf.toolchain.get_idedata``: the fields IDE
|
||||
integrations, clang-tidy, and the memory analyzer expect.
|
||||
"""
|
||||
from esphome.build_helpers.idedata import load_or_build_idedata
|
||||
|
||||
if ccache is _CCACHE_UNRESOLVED:
|
||||
# Deliberately uncached: env/PATH can change between builds in a
|
||||
# long-lived host process
|
||||
ccache = resolve_ccache_path()
|
||||
return load_or_build_idedata(
|
||||
get_build_dir() / "compile_commands.json",
|
||||
get_elf_path(),
|
||||
# Suffixed so a platformio->arduino->platformio round trip on one
|
||||
# config never serves the other toolchain's cache shape
|
||||
CORE.relative_internal_path("idedata", f"{CORE.name}.arduino.json"),
|
||||
# The compile DB's commands carry the same ccache prefix the ninja
|
||||
# rules were generated with
|
||||
launcher=str(ccache) if ccache else None,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,6 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.build_helpers import pch
|
||||
from esphome.build_helpers.pch import (
|
||||
PCH_DEFAULT_HEADERS,
|
||||
PCH_HEADER_NAME,
|
||||
mark_pch_emitted,
|
||||
pch_enabled,
|
||||
pch_header_text,
|
||||
)
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
get_excluded_builtin_components,
|
||||
@@ -287,67 +279,9 @@ idf_component_register(
|
||||
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
{link_opts_str}
|
||||
)
|
||||
{_pch_cmake()}"""
|
||||
|
||||
|
||||
def _pch_cmake() -> str:
|
||||
"""The src component's precompiled-header block (C++ TUs only).
|
||||
|
||||
The -include stays relative (resolved from the compiler cwd, the build
|
||||
dir); an absolute path would poison ccache keys.
|
||||
"""
|
||||
if not pch_enabled():
|
||||
return ""
|
||||
return f"""
|
||||
# ESPHome precompiled header (see esphome/build_helpers/pch.py).
|
||||
# OBJECT_DEPENDS is on the header, not the .gch: pch-baked headers drop
|
||||
# out of TU depfiles, and prepare_pch() touches the header on rebuild.
|
||||
target_compile_options(${{COMPONENT_LIB}} PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-Winvalid-pch>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-include>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:{PCH_HEADER_NAME}>"
|
||||
)
|
||||
set_source_files_properties(${{app_sources}} PROPERTIES
|
||||
OBJECT_DEPENDS "${{CMAKE_BINARY_DIR}}/{PCH_HEADER_NAME}")
|
||||
"""
|
||||
|
||||
|
||||
def discard_pch() -> None:
|
||||
"""Drop the pch sidecars in the IDF build dir."""
|
||||
pch.discard_pch(CORE.relative_build_path("build"))
|
||||
|
||||
|
||||
def prepare_pch() -> None:
|
||||
"""Build the .gch right before ninja, after every reconfigure, so the
|
||||
compile_commands.json flags and the sdkconfig are the settled ones."""
|
||||
if not pch_enabled():
|
||||
# Self-cleaning escape hatch: drop any previously built .gch
|
||||
pch.discard_pch(CORE.relative_build_path("build"))
|
||||
return
|
||||
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||
try:
|
||||
sdkconfig = sdkconfig_path.read_text(encoding="utf-8")
|
||||
except OSError as err:
|
||||
# Fail closed: the sdkconfig is the .sum's only config identity for
|
||||
# sdkconfig.h-only options; a stand-in marker would collide
|
||||
_LOGGER.warning(
|
||||
"Could not read %s; compiling without the pch: %s", sdkconfig_path, err
|
||||
)
|
||||
pch.discard_pch(CORE.relative_build_path("build"))
|
||||
return
|
||||
pch.prepare_pch(
|
||||
CORE.relative_build_path("build"),
|
||||
PCH_DEFAULT_HEADERS,
|
||||
(
|
||||
str(idf_version()),
|
||||
CORE.cpp_standard or "",
|
||||
sdkconfig,
|
||||
*get_project_compile_flags(),
|
||||
*get_project_cxx_compile_flags(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def write_project(
|
||||
minimal: bool = False, builtin_components: list[str] | None = None
|
||||
) -> None:
|
||||
@@ -367,14 +301,6 @@ def write_project(
|
||||
get_component_cmakelists(),
|
||||
)
|
||||
|
||||
if pch_enabled():
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("build", PCH_HEADER_NAME),
|
||||
pch_header_text(PCH_DEFAULT_HEADERS),
|
||||
)
|
||||
# Consumers carry the -include; gate the ccache relaxation on it
|
||||
mark_pch_emitted()
|
||||
|
||||
# Snapshot the exclusion set so has_outdated_files() can trigger a
|
||||
# discovery reconfigure when it changes. Excluded components never
|
||||
# register in project_description.json, so re-including one (e.g. a
|
||||
|
||||
@@ -87,19 +87,6 @@ def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
|
||||
"CCACHE_DIR": str(cache_dir),
|
||||
"CCACHE_NOHASHDIR": "true",
|
||||
"CCACHE_DEPEND": "1",
|
||||
# A user value wins via the filter below
|
||||
"CCACHE_BASEDIR": effective_ccache_basedir(),
|
||||
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
|
||||
}
|
||||
return {k: v for k, v in defaults.items() if k not in os.environ}
|
||||
|
||||
|
||||
def effective_ccache_basedir() -> str:
|
||||
"""The prefix ccache rewrites out of hashed paths: a user CCACHE_BASEDIR
|
||||
wins, else the resolved build path (matching ccache_defaults_env)."""
|
||||
from esphome.core import CORE
|
||||
|
||||
raw = os.environ.get("CCACHE_BASEDIR")
|
||||
if raw is not None and Path(raw).is_absolute() and len(Path(raw).parts) > 1:
|
||||
return raw
|
||||
# Unset or degenerate ("", "/", relative): fall back to the build path
|
||||
return str(Path(CORE.build_path).resolve())
|
||||
|
||||
@@ -11,7 +11,6 @@ consumers (IDE integration, clang-tidy) expect:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -22,8 +21,6 @@ import subprocess
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import write_file
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Everything idedata generation may raise after a successful link; idedata
|
||||
# is a bonus artifact, so consumers warn instead of failing the build
|
||||
IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
@@ -34,36 +31,13 @@ IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
ValueError,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def warn_if_idedata_missing(get_idedata: Callable[[], dict | None]) -> None:
|
||||
"""Run an idedata generator, downgrading any failure to a warning.
|
||||
|
||||
Shared by the native backends: the firmware already built, so a missing
|
||||
or broken idedata must not fail a successful build.
|
||||
"""
|
||||
try:
|
||||
if get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
if isinstance(err, (EsphomeError, OSError)):
|
||||
# Routine environmental failures keep the detail at debug
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
else:
|
||||
# LookupError/ValueError/RuntimeError smell like a parsing bug;
|
||||
# a permanently masked traceback would hide it on every build
|
||||
_LOGGER.warning("Idedata failure detail", exc_info=True)
|
||||
|
||||
|
||||
# C++ translation-unit suffixes.
|
||||
CXX_SOURCE_SUFFIXES = (".cpp", ".cc", ".cxx")
|
||||
# C++ translation-unit suffixes used to identify ESPHome source files.
|
||||
_CXX_SUFFIXES = (".cpp", ".cc")
|
||||
# Suffixes of input/output files that appear bare on the command line (and so
|
||||
# must not be mistaken for compiler flags).
|
||||
_INPUT_FILE_SUFFIXES = (*CXX_SOURCE_SUFFIXES, ".c", ".o", ".S", ".s")
|
||||
_INPUT_FILE_SUFFIXES = (*_CXX_SUFFIXES, ".c", ".o", ".S", ".s")
|
||||
# Path marker identifying an ESPHome source translation unit.
|
||||
_ESPHOME_SRC_MARKER = "/src/esphome/"
|
||||
|
||||
@@ -72,11 +46,11 @@ def _is_esphome_src(file: str) -> bool:
|
||||
"""Whether ``file`` is an ESPHome C++ translation unit; normalized to
|
||||
``/`` first since Windows compile DBs use backslashes."""
|
||||
return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith(
|
||||
CXX_SOURCE_SUFFIXES
|
||||
_CXX_SUFFIXES
|
||||
)
|
||||
|
||||
|
||||
def split_command(command: str) -> list[str]:
|
||||
def _split_command(command: str) -> list[str]:
|
||||
r"""Tokenize a compile_commands.json / response-file command string.
|
||||
|
||||
On Windows, tokenize per Windows ``argv`` rules via ``CommandLineToArgvW``.
|
||||
@@ -112,7 +86,7 @@ def split_command(command: str) -> list[str]:
|
||||
ctypes.windll.kernel32.LocalFree(argv)
|
||||
|
||||
|
||||
def expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
"""Inline any ``@response-file`` arguments (paths relative to ``directory``).
|
||||
|
||||
GCC response files embed flags that must be expanded so GCC-only flags
|
||||
@@ -127,8 +101,8 @@ def expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
rf = directory / rf
|
||||
try:
|
||||
out.extend(
|
||||
expand_response_files(
|
||||
split_command(rf.read_text(encoding="utf-8")), directory
|
||||
_expand_response_files(
|
||||
_split_command(rf.read_text(encoding="utf-8")), directory
|
||||
)
|
||||
)
|
||||
continue
|
||||
@@ -147,7 +121,7 @@ def _pick_entry(entries: list[dict]) -> dict:
|
||||
if _is_esphome_src(entry["file"]):
|
||||
return entry
|
||||
for entry in entries:
|
||||
if entry["file"].endswith(CXX_SOURCE_SUFFIXES):
|
||||
if entry["file"].endswith(_CXX_SUFFIXES):
|
||||
return entry
|
||||
raise ValueError("no C++ translation unit found in compile_commands.json")
|
||||
|
||||
@@ -157,25 +131,16 @@ def _pick_entry(entries: list[dict]) -> dict:
|
||||
_LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"})
|
||||
|
||||
|
||||
def is_launcher(token: str) -> bool:
|
||||
def _is_launcher(token: str) -> bool:
|
||||
return Path(token).stem.lower() in _LAUNCHER_STEMS
|
||||
|
||||
|
||||
def is_joined_include(tok: str) -> bool:
|
||||
"""The joined ``-includefoo.h`` spelling; excludes clang's -include-pch."""
|
||||
return (
|
||||
tok.startswith("-include")
|
||||
and tok != "-include"
|
||||
and not tok.startswith("-include-")
|
||||
)
|
||||
|
||||
|
||||
def parse_entry(
|
||||
entry: dict, launcher: str | None = None
|
||||
) -> tuple[str, list[str], list[str], list[str]]:
|
||||
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
|
||||
directory = Path(entry["directory"])
|
||||
tokens = expand_response_files(split_command(entry["command"]), directory)
|
||||
tokens = _expand_response_files(_split_command(entry["command"]), directory)
|
||||
|
||||
def _include(raw: str) -> str:
|
||||
# Resolve against the entry's ``directory`` so cached idedata works
|
||||
@@ -191,7 +156,7 @@ def parse_entry(
|
||||
if not tokens:
|
||||
# An empty command, or one that was only the launcher; fail by name
|
||||
raise ValueError(f"empty compile command for {entry.get('file')}")
|
||||
if is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
|
||||
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
|
||||
# Stale DB built with a launcher this run no longer configures; the
|
||||
# real compiler is the next token
|
||||
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
|
||||
@@ -204,23 +169,11 @@ def parse_entry(
|
||||
defines: list[str] = []
|
||||
includes: list[str] = []
|
||||
cxx_flags: list[str] = []
|
||||
unresolved_force_includes: list[str] = []
|
||||
|
||||
it = iter(tokens[1:])
|
||||
for tok in it:
|
||||
if tok in ("-c", "-o"):
|
||||
next(it, None) # drop the flag and its argument (input/output)
|
||||
elif tok == "-include" or is_joined_include(tok):
|
||||
# Re-anchor only names next to the compile (the pch); a name
|
||||
# meant for the -I chain must stay untouched
|
||||
raw = next(it, "") if tok == "-include" else tok[len("-include") :]
|
||||
if not raw:
|
||||
_LOGGER.warning("Dropping -include with no argument")
|
||||
elif Path(resolved := _include(raw)).is_file():
|
||||
cxx_flags.extend(("-include", resolved))
|
||||
else:
|
||||
unresolved_force_includes.append(raw)
|
||||
cxx_flags.extend(("-include", raw))
|
||||
elif tok.startswith("-D"):
|
||||
# ``.strip()`` handles tokens like ``-D CONFIGURED=1`` (a single
|
||||
# quoted arg with a space after -D) that some flags arrive as.
|
||||
@@ -239,14 +192,6 @@ def parse_entry(
|
||||
pass # input/output files
|
||||
else:
|
||||
cxx_flags.append(tok)
|
||||
for raw in unresolved_force_includes:
|
||||
# A deleted build artifact would otherwise surface only downstream
|
||||
if not any((Path(inc) / raw).is_file() for inc in includes):
|
||||
_LOGGER.warning(
|
||||
"-include %s found neither next to the compile nor on the "
|
||||
"include path; cached idedata may not resolve it",
|
||||
raw,
|
||||
)
|
||||
return cxx_path, defines, includes, cxx_flags
|
||||
|
||||
|
||||
@@ -311,7 +256,7 @@ def _cache_usable(cached: object) -> bool:
|
||||
if not isinstance(cached, dict) or "cc_path" not in cached:
|
||||
return False
|
||||
cxx_path = cached.get("cxx_path")
|
||||
if not isinstance(cxx_path, str) or is_launcher(cxx_path):
|
||||
if not isinstance(cxx_path, str) or _is_launcher(cxx_path):
|
||||
return False
|
||||
includes = cached.get("includes")
|
||||
return isinstance(includes, dict) and isinstance(includes.get("build"), list)
|
||||
@@ -359,7 +304,7 @@ def load_or_build_idedata(
|
||||
def reject_launcher_compiler(cxx_path: str) -> None:
|
||||
"""Reject a compile DB naming a launcher (ccache) as the compiler; it
|
||||
must never be probed, cached, or consumed."""
|
||||
if is_launcher(cxx_path):
|
||||
if _is_launcher(cxx_path):
|
||||
raise EsphomeError(
|
||||
f"compile_commands.json names the launcher {cxx_path} as the "
|
||||
"compiler; the compile database is unusable"
|
||||
|
||||
@@ -1,423 +0,0 @@
|
||||
"""Shared precompiled-header policy for the build backends.
|
||||
|
||||
The prefix either mirrors the TUs' own force-includes (ESP8266) or is a
|
||||
curated core-header set (ESP-IDF). ``esphome: includes:`` sources receive
|
||||
it too; Arduino.h visibility there is intended (esphome#8693).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import posixpath
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
|
||||
from esphome.build_helpers.idedata import (
|
||||
CXX_SOURCE_SUFFIXES,
|
||||
expand_response_files,
|
||||
is_launcher,
|
||||
split_command,
|
||||
)
|
||||
|
||||
_DOMAIN = "pch"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _PCHData:
|
||||
emitted: bool = False
|
||||
|
||||
|
||||
def _pch_data() -> _PCHData:
|
||||
from esphome.core import CORE
|
||||
|
||||
if _DOMAIN not in CORE.data:
|
||||
CORE.data[_DOMAIN] = _PCHData()
|
||||
return CORE.data[_DOMAIN]
|
||||
|
||||
|
||||
def mark_pch_emitted() -> None:
|
||||
"""Record that this build's consumers reference the pch."""
|
||||
_pch_data().emitted = True
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The header and its .gch/.sum sidecars live in the build directory.
|
||||
PCH_HEADER_NAME = "esphome_pch.h"
|
||||
|
||||
# Every artifact the pch machinery can leave behind, for cleanup.
|
||||
PCH_ARTIFACT_NAMES = (
|
||||
PCH_HEADER_NAME,
|
||||
f"{PCH_HEADER_NAME}.gch",
|
||||
f"{PCH_HEADER_NAME}.gch.sum",
|
||||
f"{PCH_HEADER_NAME}.gch.failed",
|
||||
)
|
||||
|
||||
# The core defines header every backend anchors its prefix on.
|
||||
PCH_CORE_HEADER = "esphome/core/defines.h"
|
||||
|
||||
# Prefix-header contents for backends that inject a curated set (rather
|
||||
# than mirroring the TUs' own force-includes), defines.h first so USE_*
|
||||
# macros exist for the rest. Deliberately hard-coded: frequency-derived
|
||||
# sets measured no better and kept selecting headers that cannot compile
|
||||
# standalone (X-macro, platform-variant). Every entry must be safe to
|
||||
# include first in an empty TU. Caveat: application.h/automation.h become
|
||||
# ambiently visible, so a TU missing those #includes still builds on such
|
||||
# backends; ESPHOME_PCH_ENABLE=0 restores the strict view.
|
||||
PCH_DEFAULT_HEADERS = (
|
||||
PCH_CORE_HEADER,
|
||||
"esphome/core/component.h",
|
||||
"esphome/core/helpers.h",
|
||||
"esphome/core/log.h",
|
||||
"esphome/core/application.h",
|
||||
"esphome/core/automation.h",
|
||||
)
|
||||
|
||||
# ccache cannot hash through a .gch; CCACHE_PCH_EXTSUM makes it hash the
|
||||
# .sum sidecar instead of the .gch bytes, which are not reproducible.
|
||||
# Keep in sync with the literals in platformio/pch.py.script.
|
||||
_CCACHE_PCH_ENV = {
|
||||
"CCACHE_SLOPPINESS": "pch_defines,time_macros",
|
||||
"CCACHE_PCH_EXTSUM": "true",
|
||||
}
|
||||
|
||||
# Both include forms: an angle include resolving under src/ must enter the
|
||||
# digest too; ones that do not resolve simply end the walk
|
||||
# Compiler failures that clear on their own must not latch the .failed marker
|
||||
_TRANSIENT_ERRORS = ("No space left", "Cannot allocate", "Resource temporarily")
|
||||
|
||||
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
|
||||
|
||||
|
||||
def pch_enabled() -> bool:
|
||||
"""Precompiled-header knob: default on, ``ESPHOME_PCH_ENABLE=0`` opts out."""
|
||||
return parse_enable_env("ESPHOME_PCH_ENABLE") is not False
|
||||
|
||||
|
||||
def ccache_pch_env() -> dict[str, str]:
|
||||
"""Settings ccache needs to cache compiles that consume the .gch;
|
||||
empty unless this build actually emitted one. User-set values win.
|
||||
Native backends export these process-wide; only time_macros affects
|
||||
non-pch TUs."""
|
||||
if not (pch_enabled() and _pch_data().emitted):
|
||||
return {}
|
||||
extsum = os.environ.get("CCACHE_PCH_EXTSUM")
|
||||
if extsum is not None and extsum.strip().lower() not in ("1", "true", "yes", "on"):
|
||||
# ccache then hashes the non-reproducible .gch bytes: permanent misses
|
||||
_LOGGER.warning("CCACHE_PCH_EXTSUM=%s disables pch caching", extsum)
|
||||
env = {k: v for k, v in _CCACHE_PCH_ENV.items() if k not in os.environ}
|
||||
user_sloppiness = os.environ.get("CCACHE_SLOPPINESS")
|
||||
if user_sloppiness is not None and (
|
||||
missing := [
|
||||
t
|
||||
for t in ("pch_defines", "time_macros")
|
||||
if t not in {tok.strip() for tok in user_sloppiness.split(",")}
|
||||
]
|
||||
):
|
||||
# Without these ccache declines every pch-consuming compile
|
||||
env["CCACHE_SLOPPINESS"] = ",".join((user_sloppiness, *missing))
|
||||
_LOGGER.warning(
|
||||
"Adding %s to CCACHE_SLOPPINESS so ccache can cache compiles "
|
||||
"that use the precompiled header",
|
||||
",".join(missing),
|
||||
)
|
||||
return env
|
||||
|
||||
|
||||
def pch_extra_scripts() -> list[str]:
|
||||
"""The extra_scripts entries a PlatformIO platform registers for the
|
||||
pch; empty when disabled (the script itself has no enable check)."""
|
||||
return ["post:pch.py"] if pch_enabled() else []
|
||||
|
||||
|
||||
def pch_header_text(include_headers: Iterable[str]) -> str:
|
||||
"""The prefix-header source: exactly these includes, in order."""
|
||||
return "".join(f'#include "{name}"\n' for name in include_headers)
|
||||
|
||||
|
||||
def _resolves(path: Path) -> bool:
|
||||
"""False when missing; other stat failures propagate (identity unknown,
|
||||
unlike is_file(), which would silently drop the header)."""
|
||||
try:
|
||||
return stat.S_ISREG(path.stat().st_mode)
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
return False
|
||||
|
||||
|
||||
def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]:
|
||||
"""Include closure of ``roots``: src-relative name -> contents.
|
||||
|
||||
Resolution mirrors the compiler (includer's dir, then src root); names
|
||||
outside ``src_dir`` end the walk and are versioned by the caller. No
|
||||
#ifdef evaluation: over-approximating is the safe direction.
|
||||
"""
|
||||
seen: dict[str, bytes] = {}
|
||||
stack: list[tuple[str, str]] = [(name, "") for name in roots]
|
||||
while stack:
|
||||
name, from_dir = stack.pop()
|
||||
for candidate in (f"{from_dir}/{name}" if from_dir else name, name):
|
||||
rel = posixpath.normpath(candidate)
|
||||
if not rel.startswith("..") and _resolves(src_dir / rel):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if rel in seen:
|
||||
continue
|
||||
try:
|
||||
data = (src_dir / rel).read_bytes()
|
||||
except OSError as err:
|
||||
# A marker would truncate the transitive walk; fail closed
|
||||
_LOGGER.warning("Could not read %s for the pch checksum: %s", rel, err)
|
||||
raise
|
||||
seen[rel] = data
|
||||
parent = posixpath.dirname(rel)
|
||||
stack.extend(
|
||||
# surrogateescape: a non-UTF-8 name just fails to resolve
|
||||
(inc.decode(errors="surrogateescape"), parent)
|
||||
for inc in _INCLUDE_RE.findall(data)
|
||||
)
|
||||
return seen
|
||||
|
||||
|
||||
def pch_checksum(
|
||||
src_dir: Path, include_headers: Iterable[str], extra: Iterable[str]
|
||||
) -> str:
|
||||
"""Digest standing in for the .gch in ccache's hash: the include closure
|
||||
of the prefix header plus caller-supplied identity strings (versioned
|
||||
install paths, flags). Raises OSError when a header's identity cannot
|
||||
be established at all; callers must then compile without a pch."""
|
||||
digest = hashlib.sha256()
|
||||
closure = _include_closure(src_dir, include_headers)
|
||||
for name in sorted(closure):
|
||||
digest.update(name.encode(errors="surrogateescape"))
|
||||
digest.update(closure[name])
|
||||
digest.update(b"\0")
|
||||
for item in extra:
|
||||
digest.update(item.encode(errors="surrogateescape"))
|
||||
digest.update(b"\0")
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
# Tokens dropped when retargeting a TU's flags at the prefix header
|
||||
# (the pch compile must not touch depfiles)
|
||||
_PCH_STRIP_FLAGS_WITH_ARG = frozenset({"-o", "-c", "-MT", "-MF", "-MQ"})
|
||||
_PCH_STRIP_FLAGS = frozenset({"-MD", "-MMD", "-MP", "-MM", "-M"})
|
||||
|
||||
|
||||
def pch_compile_command(
|
||||
build_dir: Path, header: Path, gch: Path
|
||||
) -> tuple[list[str], Path] | None:
|
||||
"""The exact src C++ flags from compile_commands.json retargeted at the
|
||||
header, with the directory they resolve against (relative -I paths must
|
||||
be expanded and executed from the same root); None (logged) when no
|
||||
configured C++ TU is available yet."""
|
||||
from esphome.core import CORE
|
||||
|
||||
try:
|
||||
entries = json.loads(
|
||||
(build_dir / "compile_commands.json").read_text(encoding="utf-8")
|
||||
)
|
||||
except (OSError, json.JSONDecodeError) as err:
|
||||
# Configure already succeeded, so an unusable DB is a real anomaly
|
||||
_LOGGER.warning("No usable compile database, skipping pch: %s", err)
|
||||
return None
|
||||
if not isinstance(entries, list):
|
||||
_LOGGER.warning("Malformed compile database, skipping pch")
|
||||
return None
|
||||
# CMake may spell paths through a symlink differently than CORE does
|
||||
# (macOS /tmp vs /private/tmp), so compare resolved paths
|
||||
src_root = Path(CORE.relative_src_path()).resolve()
|
||||
entry = next(
|
||||
(
|
||||
e
|
||||
for e in entries
|
||||
if isinstance(e, dict)
|
||||
and isinstance(e.get("file"), str)
|
||||
and e["file"].endswith(CXX_SOURCE_SUFFIXES)
|
||||
and Path(e["file"]).resolve().is_relative_to(src_root)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if entry is None:
|
||||
_LOGGER.warning("No src C++ entry in the compile database, skipping pch")
|
||||
return None
|
||||
directory = entry.get("directory")
|
||||
cmd_dir = Path(directory) if isinstance(directory, str) and directory else build_dir
|
||||
command = entry.get("command")
|
||||
tokens = expand_response_files(
|
||||
split_command(command if isinstance(command, str) else ""), cmd_dir
|
||||
)
|
||||
# A DB recorded with ccache enabled prefixes the compiler with the
|
||||
# launcher; the .gch must be compiled directly
|
||||
if tokens and is_launcher(tokens[0]):
|
||||
tokens = tokens[1:]
|
||||
if not tokens:
|
||||
# "arguments"-style or empty entries must skip, not spawn "-x ..."
|
||||
_LOGGER.warning("Compile database entry has no usable command, skipping pch")
|
||||
return None
|
||||
args: list[str] = []
|
||||
arg_it = iter(tokens)
|
||||
for tok in arg_it:
|
||||
if tok in _PCH_STRIP_FLAGS_WITH_ARG:
|
||||
next(arg_it, None)
|
||||
continue
|
||||
if tok in _PCH_STRIP_FLAGS:
|
||||
continue
|
||||
if tok == "-include":
|
||||
# Drop only the injected prefix; user force-includes must reach
|
||||
# the .gch compile or GCC rejects it over the macro mismatch
|
||||
inc = next(arg_it, "")
|
||||
if not inc.endswith(PCH_HEADER_NAME):
|
||||
args.extend(("-include", inc))
|
||||
continue
|
||||
args.append(tok)
|
||||
return [*args, "-x", "c++-header", "-c", str(header), "-o", str(gch)], cmd_dir
|
||||
|
||||
|
||||
def _log_pch_in_use() -> None:
|
||||
# The only place a user can discover the knob; emitted only once a
|
||||
# .gch is actually fresh or being built
|
||||
_LOGGER.info(
|
||||
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
|
||||
)
|
||||
|
||||
|
||||
def _read_stamp(path: Path) -> str:
|
||||
"""A corrupt sidecar must read as stale, not kill the pch forever."""
|
||||
try:
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return ""
|
||||
|
||||
|
||||
def discard_pch(build_dir: Path) -> None:
|
||||
"""Remove the pch sidecars so a stale .gch is never consumed.
|
||||
|
||||
Bumps the header only when a .gch was actually removed: TUs compiled
|
||||
against it have incomplete depfiles, while a repeat failure with no
|
||||
.gch must not force a full rebuild every build.
|
||||
"""
|
||||
header = build_dir / PCH_HEADER_NAME
|
||||
gch = Path(f"{header}.gch")
|
||||
had_gch = gch.is_file()
|
||||
gch.unlink(missing_ok=True)
|
||||
Path(f"{gch}.sum").unlink(missing_ok=True)
|
||||
if had_gch and header.is_file():
|
||||
os.utime(header)
|
||||
|
||||
|
||||
def prepare_pch(
|
||||
build_dir: Path, include_headers: tuple[str, ...], extra: Iterable[str]
|
||||
) -> None:
|
||||
"""Compile ``build_dir``'s .gch from compile_commands.json flags and
|
||||
write its ccache .sum.
|
||||
|
||||
The .sum doubles as the freshness stamp and folds in the compile
|
||||
command, so a flag-only change rebuilds the .gch; ``extra`` carries
|
||||
backend identity (framework version, sdkconfig, ...). A failed
|
||||
compile falls back to the plain header include.
|
||||
"""
|
||||
from esphome.core import CORE
|
||||
|
||||
header = build_dir / PCH_HEADER_NAME
|
||||
gch = Path(f"{header}.gch")
|
||||
sum_path = Path(f"{gch}.sum")
|
||||
cmd_and_dir = pch_compile_command(build_dir, header, gch)
|
||||
if cmd_and_dir is None:
|
||||
# Freshness cannot be validated; a leftover .gch must not be consumed
|
||||
discard_pch(build_dir)
|
||||
return
|
||||
cmd, cmd_dir = cmd_and_dir
|
||||
# Strip like ccache's rewriting (user CCACHE_BASEDIR wins); the raw
|
||||
# build path covers unresolved (symlinked) spellings
|
||||
cmd_id = (
|
||||
" ".join(cmd)
|
||||
.replace(effective_ccache_basedir(), "")
|
||||
.replace(str(CORE.build_path), "")
|
||||
)
|
||||
try:
|
||||
checksum = pch_checksum(
|
||||
CORE.relative_src_path(),
|
||||
include_headers,
|
||||
(
|
||||
# The closure is sorted, so root order only enters via the text
|
||||
pch_header_text(include_headers),
|
||||
*extra,
|
||||
cmd_id,
|
||||
),
|
||||
)
|
||||
except (OSError, UnicodeError) as err:
|
||||
# Identity unknown: a stale cache entry must never be served
|
||||
_LOGGER.warning(
|
||||
"Could not establish the pch identity; compiling without it: %s", err
|
||||
)
|
||||
discard_pch(build_dir)
|
||||
return
|
||||
if gch.is_file() and _read_stamp(sum_path) == checksum:
|
||||
_log_pch_in_use()
|
||||
return
|
||||
failed_marker = Path(f"{gch}.failed")
|
||||
if _read_stamp(failed_marker) == checksum:
|
||||
_LOGGER.info(
|
||||
"Precompiled header disabled after an earlier failure; delete %s to retry",
|
||||
failed_marker,
|
||||
)
|
||||
return
|
||||
_log_pch_in_use()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cmd_dir,
|
||||
# C locale keeps diagnostics matchable by _TRANSIENT_ERRORS
|
||||
env={**os.environ, "LC_ALL": "C"},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=300,
|
||||
)
|
||||
error = None
|
||||
if result.returncode < 0:
|
||||
# Killed by a signal (OOM, ^C): environmental, do not latch
|
||||
_LOGGER.warning(
|
||||
"Precompiled header compile was killed (signal %d); retrying "
|
||||
"next build",
|
||||
-result.returncode,
|
||||
)
|
||||
discard_pch(build_dir)
|
||||
return
|
||||
if result.returncode != 0:
|
||||
error = result.stderr.strip() or f"exit code {result.returncode}"
|
||||
elif not gch.is_file():
|
||||
error = "compiler produced no .gch"
|
||||
except (OSError, subprocess.SubprocessError) as err:
|
||||
# Transient (timeout, spawn/IO): warn and retry next build, no marker
|
||||
_LOGGER.warning("Precompiled header compile did not run: %s", err)
|
||||
discard_pch(build_dir)
|
||||
return
|
||||
if error is not None:
|
||||
_LOGGER.warning(
|
||||
"Precompiled header failed; compiling without it: %s", error[:400]
|
||||
)
|
||||
# This path latches, so keep the full compiler output recoverable
|
||||
_LOGGER.debug("Full pch compile output: %s", error)
|
||||
discard_pch(build_dir)
|
||||
if any(m in error for m in _TRANSIENT_ERRORS):
|
||||
# Resource exhaustion clears on its own; retry next build
|
||||
return
|
||||
# Skip retries until a header/flag/backend-identity/command change
|
||||
failed_marker.write_text(checksum + "\n", encoding="utf-8")
|
||||
os.utime(header)
|
||||
return
|
||||
failed_marker.unlink(missing_ok=True)
|
||||
sum_path.write_text(checksum + "\n", encoding="utf-8")
|
||||
# Consumers depend on the header (depfiles cannot see through a .gch);
|
||||
# bump it so users of the previous .gch recompile
|
||||
os.utime(header)
|
||||
@@ -3,7 +3,6 @@ import logging
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import web_server_base, wifi
|
||||
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
@@ -15,7 +14,6 @@ from esphome.const import (
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2,
|
||||
PLATFORM_RTL87XX,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
@@ -76,17 +74,7 @@ def _final_validate(config: ConfigType) -> None:
|
||||
"Add 'ap:' to your WiFi configuration to enable the captive portal."
|
||||
)
|
||||
|
||||
# Register socket needs for DNS server and additional HTTP connections
|
||||
# - 1 UDP socket for DNS server
|
||||
# - 3 TCP sockets for captive portal detection probes + configuration requests
|
||||
# OS captive portal detection makes multiple probe requests that stay in TIME_WAIT.
|
||||
# Need headroom for actual user configuration requests.
|
||||
# LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts.
|
||||
# The listening socket is registered by web_server_base (shared HTTP server).
|
||||
from esphome.components import socket
|
||||
|
||||
socket.consume_sockets(3, "captive_portal")(config)
|
||||
socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config)
|
||||
web_server_base.consume_captive_dns_sockets(config, "captive_portal")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
@@ -106,16 +94,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_COMPRESSION] == "gzip":
|
||||
cg.add_define("USE_CAPTIVE_PORTAL_GZIP")
|
||||
|
||||
if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2):
|
||||
cg.add_library("DNSServer", None)
|
||||
|
||||
|
||||
# Only compile the ESP-IDF DNS server when using ESP-IDF framework
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"dns_server_esp32_idf.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
}
|
||||
)
|
||||
web_server_base.add_captive_dns_library()
|
||||
|
||||
@@ -102,17 +102,7 @@ void CaptivePortal::start() {
|
||||
this->base_->add_handler_without_auth(this);
|
||||
}
|
||||
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
// Create DNS server instance for ESP-IDF
|
||||
this->dns_server_ = make_unique<DNSServer>();
|
||||
this->dns_server_->start(ip);
|
||||
#elif defined(USE_ARDUINO)
|
||||
this->dns_server_ = make_unique<DNSServer>();
|
||||
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
this->dns_server_->start(53, ESPHOME_F("*"), ip);
|
||||
#endif
|
||||
this->dns_.start(wifi::global_wifi_component->wifi_soft_ap_ip());
|
||||
|
||||
this->initialized_ = true;
|
||||
this->active_ = true;
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
#include <memory>
|
||||
#if defined(USE_ESP32)
|
||||
#include "dns_server_esp32_idf.h"
|
||||
#elif defined(USE_ARDUINO)
|
||||
#include <DNSServer.h>
|
||||
#endif
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
#include "esphome/components/web_server_base/captive_dns.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
@@ -19,17 +14,7 @@ class CaptivePortal final : public AsyncWebHandler, public Component {
|
||||
CaptivePortal(web_server_base::WebServerBase *base);
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override {
|
||||
#if defined(USE_ESP32)
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->process_next_request();
|
||||
}
|
||||
#elif defined(USE_ARDUINO)
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->processNextRequest();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void loop() override { this->dns_.loop(); }
|
||||
float get_setup_priority() const override;
|
||||
void start();
|
||||
bool is_active() const { return this->active_; }
|
||||
@@ -37,10 +22,7 @@ class CaptivePortal final : public AsyncWebHandler, public Component {
|
||||
this->active_ = false;
|
||||
this->disable_loop(); // Stop processing DNS requests
|
||||
this->base_->deinit();
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->stop();
|
||||
this->dns_server_ = nullptr;
|
||||
}
|
||||
this->dns_.stop();
|
||||
}
|
||||
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
@@ -60,9 +42,7 @@ class CaptivePortal final : public AsyncWebHandler, public Component {
|
||||
web_server_base::WebServerBase *base_;
|
||||
bool initialized_{false};
|
||||
bool active_{false};
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
std::unique_ptr<DNSServer> dns_server_{nullptr};
|
||||
#endif
|
||||
web_server_base::CaptiveDNS dns_;
|
||||
};
|
||||
|
||||
extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -368,8 +368,8 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
||||
}
|
||||
|
||||
void Climate::save_state_(const ClimateTraits &traits) {
|
||||
#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
|
||||
#pragma GCC diagnostic push
|
||||
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
|
||||
!defined(CLANG_TIDY)
|
||||
#pragma GCC diagnostic ignored "-Wclass-memaccess"
|
||||
#define TEMP_IGNORE_MEMACCESS
|
||||
#endif
|
||||
|
||||
@@ -100,6 +100,7 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
|
||||
void CM1106Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CM1106:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
this->check_uart_settings(9600);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
|
||||
@@ -46,14 +46,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"cm1106",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
"""Code generation entry point."""
|
||||
|
||||
@@ -58,6 +58,7 @@ void CSE7761Component::dump_config() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
}
|
||||
|
||||
void CSE7761Component::update() {
|
||||
|
||||
@@ -68,13 +68,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"cse7761",
|
||||
baud_rate=38400,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
"cse7761", baud_rate=38400, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -255,6 +255,7 @@ void CSE7766Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
|
||||
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
|
||||
}
|
||||
|
||||
} // namespace esphome::cse7766
|
||||
|
||||
@@ -84,12 +84,7 @@ CONFIG_SCHEMA = (
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"cse7766",
|
||||
baud_rate=4800,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,14 +26,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(cv.polling_component_schema("30s"))
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"daly_bms",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -22,7 +22,10 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
|
||||
|
||||
void DalyBmsComponent::setup() { this->next_request_ = 1; }
|
||||
|
||||
void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); }
|
||||
void DalyBmsComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Daly BMS:");
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
void DalyBmsComponent::update() {
|
||||
this->trigger_next_ = true;
|
||||
|
||||
@@ -22,9 +22,9 @@ void DebugComponent::dump_config() {
|
||||
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
|
||||
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
|
||||
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
|
||||
#ifdef USE_ESP8266
|
||||
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
|
||||
#endif // USE_ESP8266
|
||||
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
#endif // USE_SENSOR
|
||||
|
||||
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
|
||||
|
||||
@@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent {
|
||||
#ifdef USE_SENSOR
|
||||
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
|
||||
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
@@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent {
|
||||
|
||||
sensor::Sensor *free_sensor_{nullptr};
|
||||
sensor::Sensor *block_sensor_{nullptr};
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||
sensor::Sensor *fragmentation_sensor_{nullptr};
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
|
||||
@@ -159,10 +159,12 @@ void DebugComponent::update_platform_() {
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
|
||||
}
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
if (this->fragmentation_sensor_ != nullptr) {
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -52,9 +52,12 @@ CONFIG_SCHEMA = {
|
||||
),
|
||||
cv.Optional(CONF_FRAGMENTATION): cv.All(
|
||||
cv.Any(
|
||||
cv.only_on_esp8266,
|
||||
cv.All(
|
||||
cv.only_on_esp8266,
|
||||
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
msg="This feature is only available on ESP8266 and ESP32",
|
||||
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
|
||||
),
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@@ -60,12 +60,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"dfplayer",
|
||||
baud_rate=9600,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"dfplayer", baud_rate=9600, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -277,6 +277,9 @@ void DFPlayer::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
|
||||
void DFPlayer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DFPlayer:");
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
} // namespace esphome::dfplayer
|
||||
|
||||
@@ -3,10 +3,8 @@ from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from esphome.build_helpers.pch import pch_extra_scripts
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -16,7 +14,6 @@ from esphome.const import (
|
||||
CONF_FRAMEWORK,
|
||||
CONF_PLATFORM_VERSION,
|
||||
CONF_SOURCE,
|
||||
CONF_TOOLCHAIN,
|
||||
CONF_VERSION,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
@@ -24,7 +21,6 @@ from esphome.const import (
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP8266,
|
||||
ThreadModel,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
@@ -35,21 +31,21 @@ from esphome.core import (
|
||||
)
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import IS_MACOS, copy_file_if_changed
|
||||
from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, ESP8266_BOARD_BUILD, board_ld_script
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script
|
||||
from .const import (
|
||||
BUILD_FLASH_MODES,
|
||||
CONF_EARLY_PIN_INIT,
|
||||
CONF_ENABLE_SERIAL,
|
||||
CONF_ENABLE_SERIAL1,
|
||||
CONF_RESTORE_FROM_FLASH,
|
||||
KEY_BOARD,
|
||||
KEY_ESP8266,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_LDSCRIPT,
|
||||
KEY_PIN_INITIAL_STATES,
|
||||
KEY_SCANF_FLOAT,
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
KEY_WAVEFORM_REQUIRED,
|
||||
@@ -109,53 +105,6 @@ def set_core_data(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.ARDUINO)
|
||||
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
|
||||
_resolve_toolchain = cv.resolve_toolchain("ESP8266", _TOOLCHAINS, Toolchain.PLATFORMIO)
|
||||
|
||||
|
||||
def _validate_native_toolchain(config: ConfigType) -> ConfigType:
|
||||
"""Constraints of the native (non-PlatformIO) Arduino toolchain."""
|
||||
if not CORE.using_toolchain_arduino:
|
||||
return config
|
||||
from esphome.arduino8266.framework import MIN_FRAMEWORK_VERSION
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
version = cv.Version.parse(conf[CONF_VERSION])
|
||||
if version < MIN_FRAMEWORK_VERSION:
|
||||
raise cv.Invalid(
|
||||
"'toolchain: arduino' requires framework version "
|
||||
f"{MIN_FRAMEWORK_VERSION} or newer"
|
||||
)
|
||||
# platform_version is a PlatformIO concept; drop it (as esp32's native
|
||||
# toolchain does), warning when a custom pin is discarded. The floor
|
||||
# above guarantees the schema-derived default is the ARDUINO_4 spec.
|
||||
if (
|
||||
conf.pop(CONF_PLATFORM_VERSION, _ARDUINO_4_PLATFORM_SPEC)
|
||||
!= _ARDUINO_4_PLATFORM_SPEC
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"'platform_version' is ignored by 'toolchain: arduino'; the native "
|
||||
"toolchain downloads the framework and compiler directly"
|
||||
)
|
||||
if conf[CONF_SOURCE] != _format_framework_arduino_version(version):
|
||||
raise cv.Invalid(
|
||||
"'toolchain: arduino' does not support a custom framework source; "
|
||||
"use 'toolchain: platformio'"
|
||||
)
|
||||
# BOARDS is a subset of ESP8266_BOARD_BUILD today; the second clause is
|
||||
# a drift guard for the independently regenerated tables
|
||||
if (
|
||||
config[CONF_BOARD] not in BOARDS
|
||||
or config[CONF_BOARD] not in ESP8266_BOARD_BUILD
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Board '{config[CONF_BOARD]}' is not supported by "
|
||||
"'toolchain: arduino'; use 'toolchain: platformio'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
|
||||
"""Binary-download entries for a built ESP8266 firmware.
|
||||
|
||||
@@ -184,8 +133,12 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to
|
||||
# a PIO platformio/framework-arduinoespressif8266 value
|
||||
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
||||
if ver <= cv.Version(2, 4, 1):
|
||||
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
if ver <= cv.Version(2, 6, 2):
|
||||
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
# Same encoding the native toolchain uses for its package download, so a
|
||||
# custom-source check against this value cannot drift from what it fetches.
|
||||
# version bump cannot drift between the two paths.
|
||||
from esphome.arduino8266.framework import framework_package_version
|
||||
|
||||
try:
|
||||
@@ -206,9 +159,11 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
# - https://github.com/esp8266/Arduino/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
|
||||
# The platformio/espressif8266 version to use for arduino 3 framework versions
|
||||
# The platformio/espressif8266 version to use for arduino 2 framework versions
|
||||
# - https://github.com/platformio/platform-espressif8266/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
|
||||
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
|
||||
# for arduino 3 framework versions
|
||||
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
|
||||
# for arduino 4 framework versions
|
||||
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
|
||||
@@ -233,23 +188,19 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
||||
source = value.get(CONF_SOURCE, None)
|
||||
|
||||
if version < cv.Version(3, 0, 0):
|
||||
raise cv.Invalid(
|
||||
f"Arduino framework {version} is no longer supported; ESPHome requires "
|
||||
f"C++20, which needs Arduino core 3.x. Use the recommended version "
|
||||
f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).",
|
||||
path=[CONF_VERSION],
|
||||
)
|
||||
|
||||
value[CONF_VERSION] = str(version)
|
||||
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
|
||||
|
||||
platform_version = value.get(CONF_PLATFORM_VERSION)
|
||||
if platform_version is None:
|
||||
if version >= cv.Version(3, 1, 0):
|
||||
platform_version = _ARDUINO_4_PLATFORM_SPEC
|
||||
else:
|
||||
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
|
||||
elif version >= cv.Version(3, 0, 0):
|
||||
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
|
||||
elif version >= cv.Version(2, 5, 0):
|
||||
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))
|
||||
else:
|
||||
platform_version = _parse_platform_version(str(cv.Version(1, 8, 0)))
|
||||
value[CONF_PLATFORM_VERSION] = platform_version
|
||||
|
||||
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
|
||||
@@ -270,10 +221,6 @@ def _parse_platform_version(value: Any) -> str:
|
||||
return value
|
||||
|
||||
|
||||
# The platform_version derived for every core >= 3.1.0 config
|
||||
_ARDUINO_4_PLATFORM_SPEC = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
|
||||
|
||||
|
||||
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -290,6 +237,7 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"]
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -306,30 +254,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_TOOLCHAIN, visibility=cv.Visibility.ADVANCED
|
||||
): _validate_toolchain,
|
||||
}
|
||||
),
|
||||
_resolve_toolchain,
|
||||
_validate_native_toolchain,
|
||||
# Until the native toolchain lands, PlatformIO is the only backend;
|
||||
# reject a --toolchain this platform cannot serve yet.
|
||||
cv.require_platformio_toolchain("ESP8266"),
|
||||
set_core_data,
|
||||
)
|
||||
|
||||
|
||||
def native_toolchain_module():
|
||||
"""The native build backend for the resolved toolchain, if any.
|
||||
|
||||
``__main__`` dispatches from its own toolchain-keyed table; this helper
|
||||
serves the component's internal callers.
|
||||
"""
|
||||
if not CORE.using_toolchain_arduino:
|
||||
return None
|
||||
from esphome.arduino8266 import toolchain
|
||||
|
||||
return toolchain
|
||||
|
||||
|
||||
def check_rosetta() -> None:
|
||||
"""Fail fast when the x86_64 ESP8266 toolchain cannot run on this Mac.
|
||||
|
||||
@@ -356,22 +289,39 @@ def check_rosetta() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _choose_ld_script(board: str) -> str:
|
||||
"""The flash ld to pin for this board."""
|
||||
def _choose_ld_script(board: str, ver: cv.Version) -> str | None:
|
||||
"""The flash ld to pin for this board and core, or None for cores
|
||||
without ld-script support."""
|
||||
board_data = BOARDS[board]
|
||||
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
|
||||
if ver <= cv.Version(2, 3, 0):
|
||||
# No ld script support
|
||||
return None
|
||||
if ver <= cv.Version(2, 4, 2):
|
||||
# Old ld script path; the modern per-board override names do not
|
||||
# exist in this core's SDK, so the override cannot be honored.
|
||||
# Substituting the size default would move _FS_end and the
|
||||
# preferences sector, wiping flash-backed state on flash.
|
||||
if KEY_LDSCRIPT in board_data:
|
||||
raise EsphomeError(
|
||||
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
|
||||
f"flash layout, which Arduino core {ver} cannot honor; "
|
||||
"use a core newer than 2.4.2"
|
||||
)
|
||||
return ld_scripts[0]
|
||||
# A per-board override preserves a layout the board shipped with
|
||||
# (see d1_wroom_02 in boards.py)
|
||||
return board_ld_script(BOARDS[board])
|
||||
return board_ld_script(board_data)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
use_platformio = CORE.using_toolchain_platformio
|
||||
cg.add(esp8266_ns.setup_preferences())
|
||||
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_build_flag("-DUSE_ESP8266")
|
||||
cg.set_cpp_standard("gnu++20")
|
||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
@@ -387,33 +337,28 @@ async def to_code(config: ConfigType) -> None:
|
||||
"enabling scanf float support (~8KB flash)"
|
||||
)
|
||||
|
||||
# The native generator reads the same decision (KEY_SCANF_FLOAT)
|
||||
CORE.data[KEY_ESP8266][KEY_SCANF_FLOAT] = bool(enable_scanf_float)
|
||||
if use_platformio:
|
||||
extra_scripts = [
|
||||
"pre:ccache.py",
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
"pre:relocate_ratetable.py",
|
||||
]
|
||||
if not enable_scanf_float:
|
||||
extra_scripts.append("pre:remove_float_scanf.py")
|
||||
extra_scripts.extend(pch_extra_scripts())
|
||||
extra_scripts.append("post:post_build.py")
|
||||
cg.add_platformio_option("extra_scripts", extra_scripts)
|
||||
extra_scripts = [
|
||||
"pre:ccache.py",
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
"pre:relocate_ratetable.py",
|
||||
]
|
||||
if not enable_scanf_float:
|
||||
extra_scripts.append("pre:remove_float_scanf.py")
|
||||
extra_scripts.append("post:post_build.py")
|
||||
cg.add_platformio_option("extra_scripts", extra_scripts)
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO")
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"],
|
||||
)
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"],
|
||||
)
|
||||
|
||||
# Default for platformio is LWIP2_LOW_MEMORY with:
|
||||
# - MSS=536
|
||||
@@ -451,8 +396,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
|
||||
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
|
||||
# See throw_stubs.h for details. Must be prepended before <string>, so this
|
||||
# uses build_src_flags with -include. Unconditional: the native build
|
||||
# generator reads the same option, keeping one source of truth.
|
||||
# uses build_src_flags with -include.
|
||||
cg.add_platformio_option(
|
||||
"build_src_flags", "-include esphome/components/esp8266/throw_stubs.h"
|
||||
)
|
||||
@@ -482,8 +426,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
# implementation in the Arduino ESP8266 core.
|
||||
cg.add_build_flag("-Wl,--wrap=millis")
|
||||
|
||||
# Unconditional: the native build generator reads the same option,
|
||||
# keeping one source of truth
|
||||
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
|
||||
|
||||
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
@@ -492,10 +434,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"),
|
||||
)
|
||||
|
||||
if use_platformio and config[CONF_BOARD] in BOARDS:
|
||||
cg.add_platformio_option(
|
||||
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
|
||||
)
|
||||
if config[CONF_BOARD] in BOARDS:
|
||||
ld_script = _choose_ld_script(config[CONF_BOARD], ver)
|
||||
|
||||
if ld_script is not None:
|
||||
cg.add_platformio_option("board_build.ldscript", ld_script)
|
||||
|
||||
CORE.add_job(add_pin_initial_states_array)
|
||||
CORE.add_job(finalize_waveform_config)
|
||||
@@ -533,24 +476,8 @@ async def finalize_serial_config() -> None:
|
||||
cg.add_build_flag("-DNO_GLOBAL_SERIAL1")
|
||||
|
||||
|
||||
# Called by __main__.compile_program; returning False falls through to the
|
||||
# PlatformIO toolchain.
|
||||
def run_compile(args, config: ConfigType) -> bool:
|
||||
# Positive check: the native backend only runs when explicitly resolved
|
||||
toolchain = native_toolchain_module()
|
||||
if toolchain is None:
|
||||
return False
|
||||
if toolchain.run_compile(config, CORE.verbose) != 0:
|
||||
raise EsphomeError("ESP8266 native build failed")
|
||||
return True
|
||||
|
||||
|
||||
# Called by writer.py
|
||||
def copy_files() -> None:
|
||||
# Native builds skip the PlatformIO extra scripts; the build generator
|
||||
# carries their logic
|
||||
if CORE.using_toolchain_arduino:
|
||||
return
|
||||
dir = Path(__file__).parent
|
||||
for script in (
|
||||
"post_build",
|
||||
@@ -565,7 +492,6 @@ def copy_files() -> None:
|
||||
CORE.relative_build_path(f"{script}.py"),
|
||||
)
|
||||
copy_ccache_script()
|
||||
copy_pch_script()
|
||||
|
||||
|
||||
# ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder
|
||||
@@ -608,75 +534,22 @@ ESP8266_EXCEPTION_CODES = {
|
||||
}
|
||||
|
||||
|
||||
_DECODE_WARNED_AT: dict[str, float] = {}
|
||||
def _decode_pc(config: ConfigType, addr: str) -> None:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
|
||||
def _warn_decode_problem(key: str, message: str, *args) -> bool:
|
||||
"""Warn, deduplicated briefly so a burst of stack-dump addresses warns
|
||||
once but a later dump warns again; returns whether it warned so the
|
||||
caller can mark suppressed addresses individually."""
|
||||
now = time.monotonic()
|
||||
last = _DECODE_WARNED_AT.get(key)
|
||||
if last is not None and now - last < 30:
|
||||
return False
|
||||
_DECODE_WARNED_AT[key] = now
|
||||
_LOGGER.warning(message, *args)
|
||||
return True
|
||||
|
||||
|
||||
def _decode_pc(config: ConfigType, addr: str, *, bulk: bool = False) -> None:
|
||||
"""Decode one crash address. ``bulk``: the caller is scanning every
|
||||
8-hex stack word, most of which are not code addresses -- unmappable
|
||||
ones log at debug so real frames are not buried."""
|
||||
if (native_toolchain := native_toolchain_module()) is not None:
|
||||
addr2line = native_toolchain.get_addr2line_path()
|
||||
elf = native_toolchain.get_elf_path()
|
||||
for path in (addr2line, elf):
|
||||
if not path.is_file():
|
||||
_warn_decode_problem(
|
||||
str(path), "Cannot decode crash addresses: %s missing", path
|
||||
)
|
||||
# The detailed warning names no address; mark named
|
||||
# registers, but bulk stack words at debug (~150 per dump)
|
||||
log = _LOGGER.debug if bulk else _LOGGER.warning
|
||||
log("Not decoded %s (toolchain file missing)", addr)
|
||||
return
|
||||
addr2line, elf = str(addr2line), str(elf)
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
idedata = toolchain.get_idedata(config)
|
||||
if not idedata.addr2line_path or not idedata.firmware_elf_path:
|
||||
_warn_decode_problem(
|
||||
"no-addr2line",
|
||||
"Cannot decode crash addresses: no addr2line or ELF in idedata",
|
||||
)
|
||||
log = _LOGGER.debug if bulk else _LOGGER.warning
|
||||
log("Not decoded %s (no addr2line or ELF)", addr)
|
||||
return
|
||||
addr2line, elf = idedata.addr2line_path, idedata.firmware_elf_path
|
||||
command = [addr2line, "-pfiaC", "-e", elf, addr]
|
||||
idedata = toolchain.get_idedata(config)
|
||||
if not idedata.addr2line_path or not idedata.firmware_elf_path:
|
||||
_LOGGER.debug("decode_pc no addr2line")
|
||||
return
|
||||
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
|
||||
try:
|
||||
translation = subprocess.check_output(command, close_fds=False).decode().strip()
|
||||
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
|
||||
# Warn, not debug: a failing addr2line must be visible. The warning
|
||||
# is rate-limited across a dump, so mark every undecoded address
|
||||
# inline or the rest read as merely unmappable
|
||||
if not _warn_decode_problem(
|
||||
"addr2line-failed", "Could not decode crash address %s (%s)", addr, err
|
||||
):
|
||||
# The detailed warning already named this address; mark only
|
||||
# the rate-limited ones, and bulk stack words at debug
|
||||
log = _LOGGER.debug if bulk else _LOGGER.warning
|
||||
log("Not decoded %s (addr2line failed)", addr)
|
||||
except Exception: # noqa: BLE001 # pylint: disable=broad-except
|
||||
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||
return
|
||||
|
||||
if "?? ??:0" in translation:
|
||||
# A named register that fails to decode is confusing silence; a
|
||||
# bulk stack word failing is the expected common case
|
||||
log = _LOGGER.debug if bulk else _LOGGER.warning
|
||||
log("Not decoded %s (address not in %s)", addr, elf)
|
||||
# Nothing useful
|
||||
return
|
||||
translation = translation.replace(" at ??:?", "").replace(":?", "")
|
||||
_LOGGER.warning("Decoded %s", translation)
|
||||
@@ -746,6 +619,6 @@ def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) ->
|
||||
|
||||
if backtrace_state:
|
||||
for addr in re.finditer(STACKTRACE_ESP8266_BACKTRACE_PC_RE, line):
|
||||
_decode_pc(config, addr.group(), bulk=True)
|
||||
_decode_pc(config, addr.group())
|
||||
|
||||
return backtrace_state
|
||||
|
||||
@@ -16,6 +16,7 @@ KEY_WAVEFORM_REQUIRED = "waveform_required"
|
||||
KEY_SERIAL_REQUIRED = "serial_required"
|
||||
KEY_SERIAL1_REQUIRED = "serial1_required"
|
||||
# Set for the native (non-PlatformIO) toolchain's build generator
|
||||
KEY_FLASH_MODE = "flash_mode"
|
||||
KEY_SCANF_FLOAT = "scanf_float"
|
||||
# Per-board flash-layout override consumed by board_ld_script()
|
||||
KEY_LDSCRIPT = "ldscript"
|
||||
@@ -72,6 +73,3 @@ def enable_serial1() -> None:
|
||||
enable_serial1()
|
||||
"""
|
||||
CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL1_REQUIRED] = True
|
||||
|
||||
|
||||
BUILD_FLASH_MODES = ("qio", "qout", "dio", "dout")
|
||||
|
||||
@@ -96,6 +96,7 @@ void HC8Component::dump_config() {
|
||||
" Warmup time: %" PRIu32 " s",
|
||||
this->warmup_seconds_);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
} // namespace esphome::hc8
|
||||
|
||||
@@ -47,9 +47,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ CoverTraits HE60rCover::get_traits() {
|
||||
|
||||
void HE60rCover::dump_config() {
|
||||
LOG_COVER("", "HE60R Cover", this);
|
||||
this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Open Duration: %.1fs\n"
|
||||
" Close Duration: %.1fs",
|
||||
|
||||
@@ -68,6 +68,8 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
|
||||
void HrxlMaxsonarWrComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:");
|
||||
LOG_SENSOR(" ", "Distance", this);
|
||||
// As specified in the sensor's data sheet
|
||||
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
}
|
||||
|
||||
} // namespace esphome::hrxl_maxsonar_wr
|
||||
|
||||
@@ -23,14 +23,6 @@ CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(uart.UART_DEVICE_SCHEMA)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"hrxl_maxsonar_wr",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
|
||||
@@ -11,6 +11,7 @@ static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREO
|
||||
static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)};
|
||||
|
||||
void HydreonRGxxComponent::dump_config() {
|
||||
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
|
||||
|
||||
@@ -130,14 +130,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
_validate,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"hydreon_rgxx",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -26,6 +26,8 @@ void KamstrupKMPComponent::dump_config() {
|
||||
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
|
||||
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
|
||||
}
|
||||
|
||||
this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::update() {
|
||||
|
||||
@@ -102,13 +102,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"kamstrup_kmp",
|
||||
baud_rate=1200,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=2,
|
||||
"kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.build_helpers.pch import pch_extra_scripts
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -27,7 +26,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
|
||||
from . import gpio # noqa: F401
|
||||
@@ -514,7 +513,7 @@ async def component_to_code(config):
|
||||
# it for project source files only. GCC uses the last -O flag.
|
||||
build_src_flags += " -Os"
|
||||
cg.add_platformio_option("build_src_flags", build_src_flags)
|
||||
cg.add_platformio_option("extra_scripts", ["pre:ccache.py", *pch_extra_scripts()])
|
||||
cg.add_platformio_option("extra_scripts", ["pre:ccache.py"])
|
||||
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
|
||||
# On other families, patch_linker.py routes .sram.text into the right
|
||||
# RAM-executable output section and prints a post-link placement summary.
|
||||
@@ -620,4 +619,3 @@ def copy_files() -> None:
|
||||
CORE.relative_build_path("patch_linker.py"),
|
||||
)
|
||||
copy_ccache_script()
|
||||
copy_pch_script()
|
||||
|
||||
@@ -143,6 +143,8 @@ void MHZ19Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MH-Z19:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
this->check_uart_settings(9600);
|
||||
|
||||
if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
|
||||
ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot");
|
||||
} else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
|
||||
|
||||
@@ -80,14 +80,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"mhz19",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -163,7 +163,10 @@ void Mk2PVRouter::publish_value_(const char *tag, const char *val) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void Mk2PVRouter::dump_config() { ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); }
|
||||
void Mk2PVRouter::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Mk2PVRouter:");
|
||||
this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
|
||||
}
|
||||
|
||||
#ifdef MK2PVROUTER_LISTENER_COUNT
|
||||
void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) {
|
||||
|
||||
@@ -43,6 +43,7 @@ class Mk2PVRouter final : public Component, public uart::UARTDevice {
|
||||
|
||||
protected:
|
||||
static constexpr size_t CRC_SUFFIX_LEN = 1;
|
||||
static constexpr uint32_t BAUD_RATE = 9600;
|
||||
|
||||
enum class State : uint8_t {
|
||||
WAITING_FOR_START,
|
||||
|
||||
@@ -209,8 +209,14 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
|
||||
http_client.setTimeout(this->tft_upload_http_timeout_);
|
||||
|
||||
bool begin_status = false;
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
|
||||
http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
|
||||
http_client.setFollowRedirects(true);
|
||||
#endif
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
|
||||
http_client.setRedirectLimit(3);
|
||||
#endif
|
||||
begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
|
||||
if (!begin_status) {
|
||||
this->connection_state_.is_updating_ = false;
|
||||
|
||||
@@ -16,6 +16,7 @@ void PM1006Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PM1006:");
|
||||
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
void PM1006Component::update() {
|
||||
|
||||
@@ -48,9 +48,6 @@ def validate_interval_uart(config: ConfigType) -> None:
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)(config)
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ void PMSX003Component::dump_config() {
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
|
||||
}
|
||||
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
void PMSX003Component::loop() {
|
||||
|
||||
@@ -302,13 +302,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
def final_validate(config: ConfigType) -> None:
|
||||
require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
|
||||
schema = uart.final_validate_device_schema(
|
||||
"pmsx003",
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=require_tx,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx
|
||||
)
|
||||
schema(config)
|
||||
|
||||
|
||||
@@ -41,14 +41,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"pylontech",
|
||||
baud_rate=115200,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -33,6 +33,7 @@ static const uint8_t ASCII_LF = 0x0A;
|
||||
PylontechComponent::PylontechComponent() {}
|
||||
|
||||
void PylontechComponent::dump_config() {
|
||||
this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
ESP_LOGCONFIG(TAG, "pylontech:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Connection with pylontech failed!");
|
||||
|
||||
@@ -4,7 +4,11 @@ from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, esp32_rmt, remote_base
|
||||
from esphome.components.libretiny import get_libretiny_family
|
||||
from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_RTL8720C,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -45,9 +49,7 @@ DigitalWriteAction = remote_transmitter_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
# Keep in sync with the USE_LIBRETINY_VARIANT_RTL8720C / REMOTE_TRANSMITTER_BK_PWM gates in
|
||||
# remote_transmitter.h, which decide where set_non_blocking() is declared
|
||||
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7238)
|
||||
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7231N, FAMILY_BK7238)
|
||||
|
||||
|
||||
def _validate_non_blocking_platform(value: bool) -> bool:
|
||||
@@ -57,7 +59,9 @@ def _validate_non_blocking_platform(value: bool) -> bool:
|
||||
return cv.boolean(value)
|
||||
if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES:
|
||||
return cv.boolean(value)
|
||||
raise cv.Invalid("non_blocking is only supported on ESP32, RTL8720C and BK7238")
|
||||
raise cv.Invalid(
|
||||
"non_blocking is only supported on ESP32, RTL8720C, BK7231N and BK7238"
|
||||
)
|
||||
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
#endif // SOC_RMT_SUPPORTED
|
||||
#endif // USE_ESP32
|
||||
|
||||
// Enables the ISR-driven transmitter on Beken. Gated on BK7238 alone: the shadow-load PWM
|
||||
// block is shared with BK7231N, but LibreTiny builds that family against an older BDK whose
|
||||
// PWM driver has no pwm_init_param()/pwm_start(). See remote_transmitter_bk72xx.cpp.
|
||||
// Keep in sync with _NON_BLOCKING_LIBRETINY_FAMILIES in __init__.py.
|
||||
#ifdef USE_LIBRETINY_VARIANT_BK7238
|
||||
// The BK7231N-style PWM block (hardware shadow-load duty updates) enables the ISR-driven
|
||||
// transmitter on these families; family-level proxy for the SDK's CFG_SOC_NAME gate.
|
||||
// See remote_transmitter_bk72xx.cpp.
|
||||
#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238)
|
||||
#define REMOTE_TRANSMITTER_BK_PWM
|
||||
#endif
|
||||
|
||||
|
||||
@@ -9,13 +9,10 @@
|
||||
// with the core's fixes for type-name collisions between the two
|
||||
#include <ArduinoPrivate.h>
|
||||
|
||||
// Needs the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
|
||||
// for glitch-free per-edge duty updates, and an SDK exposing pwm_init_param()/pwm_start().
|
||||
// BK7231N has the block but LibreTiny builds it against an older BDK offering only the
|
||||
// sddev_control API (CMD_PWM_INIT_PARAM), so it stays on the generic bit-bang path until
|
||||
// someone can add and validate that path on real hardware. Every other Beken SoC lacks the
|
||||
// block. REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h; when it is
|
||||
// unset this file compiles to nothing and remote_transmitter.cpp is used instead.
|
||||
// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
|
||||
// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang
|
||||
// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing.
|
||||
// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h.
|
||||
|
||||
namespace esphome::remote_transmitter {
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware timer
|
||||
// interrupt: RTL8720C (gtimer) and BK7238 (BKTIMER1). Everything platform-specific sits behind
|
||||
// five hooks implemented in the per-family files -- carrier setup, duty writes, one-shot arming
|
||||
// and timer stop. Families without a usable timer keep the generic bit-bang implementation and
|
||||
// compile none of this.
|
||||
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware
|
||||
// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything
|
||||
// platform-specific sits behind five hooks implemented in the per-family files -- carrier
|
||||
// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep
|
||||
// the generic bit-bang implementation and compile none of this.
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
|
||||
|
||||
namespace esphome::remote_transmitter {
|
||||
|
||||
@@ -6,7 +6,6 @@ from string import ascii_letters, digits
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from esphome.build_helpers.pch import pch_extra_scripts
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -34,7 +33,7 @@ from esphome.core import (
|
||||
)
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed
|
||||
from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -341,10 +340,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
|
||||
cg.add_platformio_option(
|
||||
"extra_scripts",
|
||||
["pre:ccache.py", *pch_extra_scripts(), "post:post_build.py"],
|
||||
)
|
||||
cg.add_platformio_option("extra_scripts", ["pre:ccache.py", "post:post_build.py"])
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
@@ -648,7 +644,6 @@ def copy_files() -> None:
|
||||
CORE.relative_build_path("inject_lwip_include.py"),
|
||||
)
|
||||
copy_ccache_script()
|
||||
copy_pch_script()
|
||||
_generate_lwipopts_h()
|
||||
if generate_pio_files():
|
||||
path = CORE.relative_src_path("esphome.h")
|
||||
|
||||
@@ -1,254 +1 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
|
||||
|
||||
SDL_KEYS = (
|
||||
"SDLK_UNKNOWN",
|
||||
"SDLK_RETURN",
|
||||
"SDLK_ESCAPE",
|
||||
"SDLK_BACKSPACE",
|
||||
"SDLK_TAB",
|
||||
"SDLK_SPACE",
|
||||
"SDLK_EXCLAIM",
|
||||
"SDLK_QUOTEDBL",
|
||||
"SDLK_HASH",
|
||||
"SDLK_PERCENT",
|
||||
"SDLK_DOLLAR",
|
||||
"SDLK_AMPERSAND",
|
||||
"SDLK_QUOTE",
|
||||
"SDLK_LEFTPAREN",
|
||||
"SDLK_RIGHTPAREN",
|
||||
"SDLK_ASTERISK",
|
||||
"SDLK_PLUS",
|
||||
"SDLK_COMMA",
|
||||
"SDLK_MINUS",
|
||||
"SDLK_PERIOD",
|
||||
"SDLK_SLASH",
|
||||
"SDLK_0",
|
||||
"SDLK_1",
|
||||
"SDLK_2",
|
||||
"SDLK_3",
|
||||
"SDLK_4",
|
||||
"SDLK_5",
|
||||
"SDLK_6",
|
||||
"SDLK_7",
|
||||
"SDLK_8",
|
||||
"SDLK_9",
|
||||
"SDLK_COLON",
|
||||
"SDLK_SEMICOLON",
|
||||
"SDLK_LESS",
|
||||
"SDLK_EQUALS",
|
||||
"SDLK_GREATER",
|
||||
"SDLK_QUESTION",
|
||||
"SDLK_AT",
|
||||
"SDLK_LEFTBRACKET",
|
||||
"SDLK_BACKSLASH",
|
||||
"SDLK_RIGHTBRACKET",
|
||||
"SDLK_CARET",
|
||||
"SDLK_UNDERSCORE",
|
||||
"SDLK_BACKQUOTE",
|
||||
"SDLK_a",
|
||||
"SDLK_b",
|
||||
"SDLK_c",
|
||||
"SDLK_d",
|
||||
"SDLK_e",
|
||||
"SDLK_f",
|
||||
"SDLK_g",
|
||||
"SDLK_h",
|
||||
"SDLK_i",
|
||||
"SDLK_j",
|
||||
"SDLK_k",
|
||||
"SDLK_l",
|
||||
"SDLK_m",
|
||||
"SDLK_n",
|
||||
"SDLK_o",
|
||||
"SDLK_p",
|
||||
"SDLK_q",
|
||||
"SDLK_r",
|
||||
"SDLK_s",
|
||||
"SDLK_t",
|
||||
"SDLK_u",
|
||||
"SDLK_v",
|
||||
"SDLK_w",
|
||||
"SDLK_x",
|
||||
"SDLK_y",
|
||||
"SDLK_z",
|
||||
"SDLK_CAPSLOCK",
|
||||
"SDLK_F1",
|
||||
"SDLK_F2",
|
||||
"SDLK_F3",
|
||||
"SDLK_F4",
|
||||
"SDLK_F5",
|
||||
"SDLK_F6",
|
||||
"SDLK_F7",
|
||||
"SDLK_F8",
|
||||
"SDLK_F9",
|
||||
"SDLK_F10",
|
||||
"SDLK_F11",
|
||||
"SDLK_F12",
|
||||
"SDLK_PRINTSCREEN",
|
||||
"SDLK_SCROLLLOCK",
|
||||
"SDLK_PAUSE",
|
||||
"SDLK_INSERT",
|
||||
"SDLK_HOME",
|
||||
"SDLK_PAGEUP",
|
||||
"SDLK_DELETE",
|
||||
"SDLK_END",
|
||||
"SDLK_PAGEDOWN",
|
||||
"SDLK_RIGHT",
|
||||
"SDLK_LEFT",
|
||||
"SDLK_DOWN",
|
||||
"SDLK_UP",
|
||||
"SDLK_NUMLOCKCLEAR",
|
||||
"SDLK_KP_DIVIDE",
|
||||
"SDLK_KP_MULTIPLY",
|
||||
"SDLK_KP_MINUS",
|
||||
"SDLK_KP_PLUS",
|
||||
"SDLK_KP_ENTER",
|
||||
"SDLK_KP_1",
|
||||
"SDLK_KP_2",
|
||||
"SDLK_KP_3",
|
||||
"SDLK_KP_4",
|
||||
"SDLK_KP_5",
|
||||
"SDLK_KP_6",
|
||||
"SDLK_KP_7",
|
||||
"SDLK_KP_8",
|
||||
"SDLK_KP_9",
|
||||
"SDLK_KP_0",
|
||||
"SDLK_KP_PERIOD",
|
||||
"SDLK_APPLICATION",
|
||||
"SDLK_POWER",
|
||||
"SDLK_KP_EQUALS",
|
||||
"SDLK_F13",
|
||||
"SDLK_F14",
|
||||
"SDLK_F15",
|
||||
"SDLK_F16",
|
||||
"SDLK_F17",
|
||||
"SDLK_F18",
|
||||
"SDLK_F19",
|
||||
"SDLK_F20",
|
||||
"SDLK_F21",
|
||||
"SDLK_F22",
|
||||
"SDLK_F23",
|
||||
"SDLK_F24",
|
||||
"SDLK_EXECUTE",
|
||||
"SDLK_HELP",
|
||||
"SDLK_MENU",
|
||||
"SDLK_SELECT",
|
||||
"SDLK_STOP",
|
||||
"SDLK_AGAIN",
|
||||
"SDLK_UNDO",
|
||||
"SDLK_CUT",
|
||||
"SDLK_COPY",
|
||||
"SDLK_PASTE",
|
||||
"SDLK_FIND",
|
||||
"SDLK_MUTE",
|
||||
"SDLK_VOLUMEUP",
|
||||
"SDLK_VOLUMEDOWN",
|
||||
"SDLK_KP_COMMA",
|
||||
"SDLK_KP_EQUALSAS400",
|
||||
"SDLK_ALTERASE",
|
||||
"SDLK_SYSREQ",
|
||||
"SDLK_CANCEL",
|
||||
"SDLK_CLEAR",
|
||||
"SDLK_PRIOR",
|
||||
"SDLK_RETURN2",
|
||||
"SDLK_SEPARATOR",
|
||||
"SDLK_OUT",
|
||||
"SDLK_OPER",
|
||||
"SDLK_CLEARAGAIN",
|
||||
"SDLK_CRSEL",
|
||||
"SDLK_EXSEL",
|
||||
"SDLK_KP_00",
|
||||
"SDLK_KP_000",
|
||||
"SDLK_THOUSANDSSEPARATOR",
|
||||
"SDLK_DECIMALSEPARATOR",
|
||||
"SDLK_CURRENCYUNIT",
|
||||
"SDLK_CURRENCYSUBUNIT",
|
||||
"SDLK_KP_LEFTPAREN",
|
||||
"SDLK_KP_RIGHTPAREN",
|
||||
"SDLK_KP_LEFTBRACE",
|
||||
"SDLK_KP_RIGHTBRACE",
|
||||
"SDLK_KP_TAB",
|
||||
"SDLK_KP_BACKSPACE",
|
||||
"SDLK_KP_A",
|
||||
"SDLK_KP_B",
|
||||
"SDLK_KP_C",
|
||||
"SDLK_KP_D",
|
||||
"SDLK_KP_E",
|
||||
"SDLK_KP_F",
|
||||
"SDLK_KP_XOR",
|
||||
"SDLK_KP_POWER",
|
||||
"SDLK_KP_PERCENT",
|
||||
"SDLK_KP_LESS",
|
||||
"SDLK_KP_GREATER",
|
||||
"SDLK_KP_AMPERSAND",
|
||||
"SDLK_KP_DBLAMPERSAND",
|
||||
"SDLK_KP_VERTICALBAR",
|
||||
"SDLK_KP_DBLVERTICALBAR",
|
||||
"SDLK_KP_COLON",
|
||||
"SDLK_KP_HASH",
|
||||
"SDLK_KP_SPACE",
|
||||
"SDLK_KP_AT",
|
||||
"SDLK_KP_EXCLAM",
|
||||
"SDLK_KP_MEMSTORE",
|
||||
"SDLK_KP_MEMRECALL",
|
||||
"SDLK_KP_MEMCLEAR",
|
||||
"SDLK_KP_MEMADD",
|
||||
"SDLK_KP_MEMSUBTRACT",
|
||||
"SDLK_KP_MEMMULTIPLY",
|
||||
"SDLK_KP_MEMDIVIDE",
|
||||
"SDLK_KP_PLUSMINUS",
|
||||
"SDLK_KP_CLEAR",
|
||||
"SDLK_KP_CLEARENTRY",
|
||||
"SDLK_KP_BINARY",
|
||||
"SDLK_KP_OCTAL",
|
||||
"SDLK_KP_DECIMAL",
|
||||
"SDLK_KP_HEXADECIMAL",
|
||||
"SDLK_LCTRL",
|
||||
"SDLK_LSHIFT",
|
||||
"SDLK_LALT",
|
||||
"SDLK_LGUI",
|
||||
"SDLK_RCTRL",
|
||||
"SDLK_RSHIFT",
|
||||
"SDLK_RALT",
|
||||
"SDLK_RGUI",
|
||||
"SDLK_MODE",
|
||||
"SDLK_AUDIONEXT",
|
||||
"SDLK_AUDIOPREV",
|
||||
"SDLK_AUDIOSTOP",
|
||||
"SDLK_AUDIOPLAY",
|
||||
"SDLK_AUDIOMUTE",
|
||||
"SDLK_MEDIASELECT",
|
||||
"SDLK_WWW",
|
||||
"SDLK_MAIL",
|
||||
"SDLK_CALCULATOR",
|
||||
"SDLK_COMPUTER",
|
||||
"SDLK_AC_SEARCH",
|
||||
"SDLK_AC_HOME",
|
||||
"SDLK_AC_BACK",
|
||||
"SDLK_AC_FORWARD",
|
||||
"SDLK_AC_STOP",
|
||||
"SDLK_AC_REFRESH",
|
||||
"SDLK_AC_BOOKMARKS",
|
||||
"SDLK_BRIGHTNESSDOWN",
|
||||
"SDLK_BRIGHTNESSUP",
|
||||
"SDLK_DISPLAYSWITCH",
|
||||
"SDLK_KBDILLUMTOGGLE",
|
||||
"SDLK_KBDILLUMDOWN",
|
||||
"SDLK_KBDILLUMUP",
|
||||
"SDLK_EJECT",
|
||||
"SDLK_SLEEP",
|
||||
"SDLK_APP1",
|
||||
"SDLK_APP2",
|
||||
"SDLK_AUDIOREWIND",
|
||||
"SDLK_AUDIOFASTFORWARD",
|
||||
"SDLK_SOFTLEFT",
|
||||
"SDLK_SOFTRIGHT",
|
||||
"SDLK_CALL",
|
||||
"SDLK_ENDCALL",
|
||||
)
|
||||
|
||||
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
|
||||
|
||||
@@ -7,15 +7,262 @@ from esphome.core import Lambda
|
||||
from esphome.cpp_generator import ExpressionStatement, RawExpression
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import SDL_KEYMAP
|
||||
from .display import CONF_SDL_ID, Sdl, headless_final_validate
|
||||
from .display import CONF_SDL_ID, Sdl
|
||||
|
||||
CODEOWNERS = ["@bdm310"]
|
||||
|
||||
STATE_ARG = "state"
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = headless_final_validate("binary_sensor")
|
||||
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
|
||||
|
||||
SDL_KEYS = (
|
||||
"SDLK_UNKNOWN",
|
||||
"SDLK_RETURN",
|
||||
"SDLK_ESCAPE",
|
||||
"SDLK_BACKSPACE",
|
||||
"SDLK_TAB",
|
||||
"SDLK_SPACE",
|
||||
"SDLK_EXCLAIM",
|
||||
"SDLK_QUOTEDBL",
|
||||
"SDLK_HASH",
|
||||
"SDLK_PERCENT",
|
||||
"SDLK_DOLLAR",
|
||||
"SDLK_AMPERSAND",
|
||||
"SDLK_QUOTE",
|
||||
"SDLK_LEFTPAREN",
|
||||
"SDLK_RIGHTPAREN",
|
||||
"SDLK_ASTERISK",
|
||||
"SDLK_PLUS",
|
||||
"SDLK_COMMA",
|
||||
"SDLK_MINUS",
|
||||
"SDLK_PERIOD",
|
||||
"SDLK_SLASH",
|
||||
"SDLK_0",
|
||||
"SDLK_1",
|
||||
"SDLK_2",
|
||||
"SDLK_3",
|
||||
"SDLK_4",
|
||||
"SDLK_5",
|
||||
"SDLK_6",
|
||||
"SDLK_7",
|
||||
"SDLK_8",
|
||||
"SDLK_9",
|
||||
"SDLK_COLON",
|
||||
"SDLK_SEMICOLON",
|
||||
"SDLK_LESS",
|
||||
"SDLK_EQUALS",
|
||||
"SDLK_GREATER",
|
||||
"SDLK_QUESTION",
|
||||
"SDLK_AT",
|
||||
"SDLK_LEFTBRACKET",
|
||||
"SDLK_BACKSLASH",
|
||||
"SDLK_RIGHTBRACKET",
|
||||
"SDLK_CARET",
|
||||
"SDLK_UNDERSCORE",
|
||||
"SDLK_BACKQUOTE",
|
||||
"SDLK_a",
|
||||
"SDLK_b",
|
||||
"SDLK_c",
|
||||
"SDLK_d",
|
||||
"SDLK_e",
|
||||
"SDLK_f",
|
||||
"SDLK_g",
|
||||
"SDLK_h",
|
||||
"SDLK_i",
|
||||
"SDLK_j",
|
||||
"SDLK_k",
|
||||
"SDLK_l",
|
||||
"SDLK_m",
|
||||
"SDLK_n",
|
||||
"SDLK_o",
|
||||
"SDLK_p",
|
||||
"SDLK_q",
|
||||
"SDLK_r",
|
||||
"SDLK_s",
|
||||
"SDLK_t",
|
||||
"SDLK_u",
|
||||
"SDLK_v",
|
||||
"SDLK_w",
|
||||
"SDLK_x",
|
||||
"SDLK_y",
|
||||
"SDLK_z",
|
||||
"SDLK_CAPSLOCK",
|
||||
"SDLK_F1",
|
||||
"SDLK_F2",
|
||||
"SDLK_F3",
|
||||
"SDLK_F4",
|
||||
"SDLK_F5",
|
||||
"SDLK_F6",
|
||||
"SDLK_F7",
|
||||
"SDLK_F8",
|
||||
"SDLK_F9",
|
||||
"SDLK_F10",
|
||||
"SDLK_F11",
|
||||
"SDLK_F12",
|
||||
"SDLK_PRINTSCREEN",
|
||||
"SDLK_SCROLLLOCK",
|
||||
"SDLK_PAUSE",
|
||||
"SDLK_INSERT",
|
||||
"SDLK_HOME",
|
||||
"SDLK_PAGEUP",
|
||||
"SDLK_DELETE",
|
||||
"SDLK_END",
|
||||
"SDLK_PAGEDOWN",
|
||||
"SDLK_RIGHT",
|
||||
"SDLK_LEFT",
|
||||
"SDLK_DOWN",
|
||||
"SDLK_UP",
|
||||
"SDLK_NUMLOCKCLEAR",
|
||||
"SDLK_KP_DIVIDE",
|
||||
"SDLK_KP_MULTIPLY",
|
||||
"SDLK_KP_MINUS",
|
||||
"SDLK_KP_PLUS",
|
||||
"SDLK_KP_ENTER",
|
||||
"SDLK_KP_1",
|
||||
"SDLK_KP_2",
|
||||
"SDLK_KP_3",
|
||||
"SDLK_KP_4",
|
||||
"SDLK_KP_5",
|
||||
"SDLK_KP_6",
|
||||
"SDLK_KP_7",
|
||||
"SDLK_KP_8",
|
||||
"SDLK_KP_9",
|
||||
"SDLK_KP_0",
|
||||
"SDLK_KP_PERIOD",
|
||||
"SDLK_APPLICATION",
|
||||
"SDLK_POWER",
|
||||
"SDLK_KP_EQUALS",
|
||||
"SDLK_F13",
|
||||
"SDLK_F14",
|
||||
"SDLK_F15",
|
||||
"SDLK_F16",
|
||||
"SDLK_F17",
|
||||
"SDLK_F18",
|
||||
"SDLK_F19",
|
||||
"SDLK_F20",
|
||||
"SDLK_F21",
|
||||
"SDLK_F22",
|
||||
"SDLK_F23",
|
||||
"SDLK_F24",
|
||||
"SDLK_EXECUTE",
|
||||
"SDLK_HELP",
|
||||
"SDLK_MENU",
|
||||
"SDLK_SELECT",
|
||||
"SDLK_STOP",
|
||||
"SDLK_AGAIN",
|
||||
"SDLK_UNDO",
|
||||
"SDLK_CUT",
|
||||
"SDLK_COPY",
|
||||
"SDLK_PASTE",
|
||||
"SDLK_FIND",
|
||||
"SDLK_MUTE",
|
||||
"SDLK_VOLUMEUP",
|
||||
"SDLK_VOLUMEDOWN",
|
||||
"SDLK_KP_COMMA",
|
||||
"SDLK_KP_EQUALSAS400",
|
||||
"SDLK_ALTERASE",
|
||||
"SDLK_SYSREQ",
|
||||
"SDLK_CANCEL",
|
||||
"SDLK_CLEAR",
|
||||
"SDLK_PRIOR",
|
||||
"SDLK_RETURN2",
|
||||
"SDLK_SEPARATOR",
|
||||
"SDLK_OUT",
|
||||
"SDLK_OPER",
|
||||
"SDLK_CLEARAGAIN",
|
||||
"SDLK_CRSEL",
|
||||
"SDLK_EXSEL",
|
||||
"SDLK_KP_00",
|
||||
"SDLK_KP_000",
|
||||
"SDLK_THOUSANDSSEPARATOR",
|
||||
"SDLK_DECIMALSEPARATOR",
|
||||
"SDLK_CURRENCYUNIT",
|
||||
"SDLK_CURRENCYSUBUNIT",
|
||||
"SDLK_KP_LEFTPAREN",
|
||||
"SDLK_KP_RIGHTPAREN",
|
||||
"SDLK_KP_LEFTBRACE",
|
||||
"SDLK_KP_RIGHTBRACE",
|
||||
"SDLK_KP_TAB",
|
||||
"SDLK_KP_BACKSPACE",
|
||||
"SDLK_KP_A",
|
||||
"SDLK_KP_B",
|
||||
"SDLK_KP_C",
|
||||
"SDLK_KP_D",
|
||||
"SDLK_KP_E",
|
||||
"SDLK_KP_F",
|
||||
"SDLK_KP_XOR",
|
||||
"SDLK_KP_POWER",
|
||||
"SDLK_KP_PERCENT",
|
||||
"SDLK_KP_LESS",
|
||||
"SDLK_KP_GREATER",
|
||||
"SDLK_KP_AMPERSAND",
|
||||
"SDLK_KP_DBLAMPERSAND",
|
||||
"SDLK_KP_VERTICALBAR",
|
||||
"SDLK_KP_DBLVERTICALBAR",
|
||||
"SDLK_KP_COLON",
|
||||
"SDLK_KP_HASH",
|
||||
"SDLK_KP_SPACE",
|
||||
"SDLK_KP_AT",
|
||||
"SDLK_KP_EXCLAM",
|
||||
"SDLK_KP_MEMSTORE",
|
||||
"SDLK_KP_MEMRECALL",
|
||||
"SDLK_KP_MEMCLEAR",
|
||||
"SDLK_KP_MEMADD",
|
||||
"SDLK_KP_MEMSUBTRACT",
|
||||
"SDLK_KP_MEMMULTIPLY",
|
||||
"SDLK_KP_MEMDIVIDE",
|
||||
"SDLK_KP_PLUSMINUS",
|
||||
"SDLK_KP_CLEAR",
|
||||
"SDLK_KP_CLEARENTRY",
|
||||
"SDLK_KP_BINARY",
|
||||
"SDLK_KP_OCTAL",
|
||||
"SDLK_KP_DECIMAL",
|
||||
"SDLK_KP_HEXADECIMAL",
|
||||
"SDLK_LCTRL",
|
||||
"SDLK_LSHIFT",
|
||||
"SDLK_LALT",
|
||||
"SDLK_LGUI",
|
||||
"SDLK_RCTRL",
|
||||
"SDLK_RSHIFT",
|
||||
"SDLK_RALT",
|
||||
"SDLK_RGUI",
|
||||
"SDLK_MODE",
|
||||
"SDLK_AUDIONEXT",
|
||||
"SDLK_AUDIOPREV",
|
||||
"SDLK_AUDIOSTOP",
|
||||
"SDLK_AUDIOPLAY",
|
||||
"SDLK_AUDIOMUTE",
|
||||
"SDLK_MEDIASELECT",
|
||||
"SDLK_WWW",
|
||||
"SDLK_MAIL",
|
||||
"SDLK_CALCULATOR",
|
||||
"SDLK_COMPUTER",
|
||||
"SDLK_AC_SEARCH",
|
||||
"SDLK_AC_HOME",
|
||||
"SDLK_AC_BACK",
|
||||
"SDLK_AC_FORWARD",
|
||||
"SDLK_AC_STOP",
|
||||
"SDLK_AC_REFRESH",
|
||||
"SDLK_AC_BOOKMARKS",
|
||||
"SDLK_BRIGHTNESSDOWN",
|
||||
"SDLK_BRIGHTNESSUP",
|
||||
"SDLK_DISPLAYSWITCH",
|
||||
"SDLK_KBDILLUMTOGGLE",
|
||||
"SDLK_KBDILLUMDOWN",
|
||||
"SDLK_KBDILLUMUP",
|
||||
"SDLK_EJECT",
|
||||
"SDLK_SLEEP",
|
||||
"SDLK_APP1",
|
||||
"SDLK_APP2",
|
||||
"SDLK_AUDIOREWIND",
|
||||
"SDLK_AUDIOFASTFORWARD",
|
||||
"SDLK_SOFTLEFT",
|
||||
"SDLK_SOFTRIGHT",
|
||||
"SDLK_CALL",
|
||||
"SDLK_ENDCALL",
|
||||
)
|
||||
|
||||
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
binary_sensor.binary_sensor_schema(BinarySensor)
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display
|
||||
from esphome.components.snapshot import Snapshot, register_snapshot
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIMENSIONS,
|
||||
@@ -17,21 +16,14 @@ from esphome.const import (
|
||||
CONF_Y,
|
||||
PLATFORM_HOST,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import SDL_KEYMAP
|
||||
|
||||
AUTO_LOAD = ["snapshot"]
|
||||
|
||||
sdl_ns = cg.esphome_ns.namespace("sdl")
|
||||
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component, Snapshot)
|
||||
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component)
|
||||
sdl_window_flags = cg.global_ns.enum("SDL_WindowFlags")
|
||||
|
||||
|
||||
CONF_CENTERED_ON_DISPLAY = "centered_on_display"
|
||||
CONF_HEADLESS = "headless"
|
||||
CONF_SNAPSHOT_KEY = "snapshot_key"
|
||||
CONF_SDL_OPTIONS = "sdl_options"
|
||||
CONF_SDL_ID = "sdl_id"
|
||||
CONF_WINDOW_OPTIONS = "window_options"
|
||||
@@ -75,29 +67,12 @@ def _validate_position(config: dict) -> dict:
|
||||
raise cv.Invalid("Must specify either 'x' and 'y' or 'centered_on_display'")
|
||||
|
||||
|
||||
def _validate_headless(config: ConfigType) -> ConfigType:
|
||||
if not config[CONF_HEADLESS]:
|
||||
return config
|
||||
if CONF_WINDOW_OPTIONS in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_WINDOW_OPTIONS}' has no effect when '{CONF_HEADLESS}' is set - there is no window"
|
||||
)
|
||||
if CONF_SNAPSHOT_KEY in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_SNAPSHOT_KEY}' cannot be used when '{CONF_HEADLESS}' is set - "
|
||||
f"there is no keyboard. Use the 'snapshot.take' action instead"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Sdl),
|
||||
cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options,
|
||||
cv.Optional(CONF_HEADLESS, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SNAPSHOT_KEY): cv.enum(SDL_KEYMAP),
|
||||
cv.Required(CONF_DIMENSIONS): cv.Any(
|
||||
cv.dimensions,
|
||||
cv.Schema(
|
||||
@@ -124,42 +99,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
),
|
||||
_validate_headless,
|
||||
cv.only_on(PLATFORM_HOST),
|
||||
)
|
||||
|
||||
|
||||
def headless_final_validate(platform: str) -> cv.Schema:
|
||||
"""Build a FINAL_VALIDATE_SCHEMA rejecting a platform whose sdl display is headless.
|
||||
|
||||
Mouse and keyboard platforms are driven by window events, so under a headless display they
|
||||
would never report anything.
|
||||
"""
|
||||
|
||||
def validate_display(display_config: ConfigType) -> ConfigType:
|
||||
if display_config.get(CONF_HEADLESS):
|
||||
raise cv.Invalid(
|
||||
f"The sdl {platform} platform needs a window, but its display has "
|
||||
f"'{CONF_HEADLESS}' set"
|
||||
)
|
||||
return display_config
|
||||
|
||||
return cv.Schema(
|
||||
{cv.Required(CONF_SDL_ID): fv.id_declaration_match_schema(validate_display)},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
for option in config[CONF_SDL_OPTIONS].split():
|
||||
cg.add_build_flag(option)
|
||||
cg.add_build_flag("-DSDL_BYTEORDER=4321")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await register_snapshot(var, config)
|
||||
cg.add(var.set_headless(config[CONF_HEADLESS]))
|
||||
if (key := config.get(CONF_SNAPSHOT_KEY)) is not None:
|
||||
cg.add(var.set_snapshot_key(key))
|
||||
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
|
||||
@@ -2,17 +2,8 @@
|
||||
#include "sdl_esphome.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace esphome::sdl {
|
||||
|
||||
namespace {
|
||||
|
||||
// Key under which each window keeps a pointer back to its Sdl instance.
|
||||
constexpr const char *const WINDOW_DATA_KEY = "esphome_sdl";
|
||||
|
||||
} // namespace
|
||||
|
||||
int Sdl::get_width() {
|
||||
switch (this->rotation_) {
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
@@ -37,96 +28,17 @@ int Sdl::get_height() {
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl::destroy_renderer_() {
|
||||
// Reverse order of creation: the renderer refers to the window or surface it was made from.
|
||||
if (this->shot_target_ != nullptr) {
|
||||
SDL_DestroyTexture(this->shot_target_);
|
||||
this->shot_target_ = nullptr;
|
||||
}
|
||||
if (this->texture_ != nullptr) {
|
||||
SDL_DestroyTexture(this->texture_);
|
||||
this->texture_ = nullptr;
|
||||
}
|
||||
if (this->renderer_ != nullptr) {
|
||||
SDL_DestroyRenderer(this->renderer_);
|
||||
this->renderer_ = nullptr;
|
||||
}
|
||||
if (this->window_ != nullptr) {
|
||||
SDL_DestroyWindow(this->window_);
|
||||
this->window_ = nullptr;
|
||||
}
|
||||
if (this->surface_ != nullptr) {
|
||||
SDL_FreeSurface(this->surface_);
|
||||
this->surface_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Sdl::setup_failed_(const char *what) {
|
||||
ESP_LOGE(TAG, "%s: %s", what, SDL_GetError());
|
||||
// Give back whatever was created before the failure. Without this a half set up display leaves an
|
||||
// empty window on screen for the life of the process, still registered as an event target.
|
||||
this->destroy_renderer_();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Sdl::setup_renderer_() {
|
||||
SDL_SetMainReady();
|
||||
if (this->headless_) {
|
||||
// SDL_INIT_VIDEO is deliberately not requested: a software renderer bound to a surface needs no
|
||||
// video device, so this works on a machine with no display server at all.
|
||||
if (SDL_Init(0) != 0)
|
||||
return this->setup_failed_("SDL_Init failed");
|
||||
this->surface_ = SDL_CreateRGBSurfaceWithFormat(0, this->width_, this->height_, 16, SDL_PIXELFORMAT_RGB565);
|
||||
if (this->surface_ == nullptr)
|
||||
return this->setup_failed_("Could not create offscreen surface");
|
||||
this->renderer_ = SDL_CreateSoftwareRenderer(this->surface_);
|
||||
} else {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0)
|
||||
return this->setup_failed_("SDL_Init failed");
|
||||
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
|
||||
this->window_options_);
|
||||
if (this->window_ == nullptr)
|
||||
return this->setup_failed_("Could not create window");
|
||||
// Lets loop() find the display an event belongs to, so one display does not act on another's
|
||||
// input when several windows are open.
|
||||
SDL_SetWindowData(this->window_, WINDOW_DATA_KEY, this);
|
||||
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
|
||||
}
|
||||
if (this->renderer_ == nullptr)
|
||||
return this->setup_failed_("Could not create renderer");
|
||||
if (SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_) != 0)
|
||||
return this->setup_failed_("Could not set renderer logical size");
|
||||
void Sdl::setup() {
|
||||
SDL_Init(SDL_INIT_VIDEO);
|
||||
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
|
||||
this->window_options_);
|
||||
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
|
||||
SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_);
|
||||
this->texture_ =
|
||||
SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_);
|
||||
if (this->texture_ == nullptr)
|
||||
return this->setup_failed_("Could not create texture");
|
||||
// The texture has no alpha channel, so blending is pointless. Headless it would also force a
|
||||
// different software blit path onto the 16 bit target surface.
|
||||
if (SDL_SetTextureBlendMode(this->texture_, this->headless_ ? SDL_BLENDMODE_NONE : SDL_BLENDMODE_BLEND) != 0)
|
||||
return this->setup_failed_("Could not set texture blend mode");
|
||||
return true;
|
||||
SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND);
|
||||
}
|
||||
|
||||
void Sdl::setup() {
|
||||
if (!this->setup_renderer_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (this->headless_) {
|
||||
// Nothing generates events, so there is nothing for loop() to do.
|
||||
this->disable_loop();
|
||||
} else if (this->snapshot_key_ != 0) {
|
||||
this->add_key_listener(this->snapshot_key_, [this](bool down) {
|
||||
if (down && !this->take_snapshot(nullptr)) {
|
||||
ESP_LOGW(TAG, "snapshot key did not write a file");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl::update() {
|
||||
if (this->texture_ == nullptr)
|
||||
return;
|
||||
this->do_update_();
|
||||
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_))
|
||||
return;
|
||||
@@ -139,19 +51,12 @@ void Sdl::update() {
|
||||
}
|
||||
|
||||
void Sdl::redraw_(SDL_Rect &rect) {
|
||||
// Nothing to present when headless - a snapshot blits the whole texture when it needs it, so
|
||||
// doing it here as well would just burn CPU. draw_pixels_at() calls this on every partial
|
||||
// update, so it is worth skipping.
|
||||
if (this->headless_)
|
||||
return;
|
||||
SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
|
||||
SDL_RenderPresent(this->renderer_);
|
||||
}
|
||||
|
||||
void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (this->texture_ == nullptr)
|
||||
return;
|
||||
SDL_Rect rect{x_start, y_start, w, h};
|
||||
if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
@@ -164,7 +69,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
|
||||
}
|
||||
|
||||
void Sdl::draw_pixel_at(int x, int y, Color color) {
|
||||
if (this->texture_ == nullptr || !this->get_clipping().inside(x, y))
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
@@ -199,148 +104,61 @@ void Sdl::process_key(uint32_t keycode, bool down) {
|
||||
callback->second(down);
|
||||
}
|
||||
|
||||
Sdl *Sdl::instance_for_window_(uint32_t window_id) {
|
||||
SDL_Window *window = SDL_GetWindowFromID(window_id);
|
||||
if (window == nullptr)
|
||||
return nullptr;
|
||||
return static_cast<Sdl *>(SDL_GetWindowData(window, WINDOW_DATA_KEY));
|
||||
}
|
||||
|
||||
void Sdl::handle_event_(const SDL_Event &event) {
|
||||
switch (event.type) {
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (event.button.button == 1) {
|
||||
this->mouse_x = event.button.x;
|
||||
this->mouse_y = event.button.y;
|
||||
this->mouse_down = event.button.state != 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
if (event.motion.state & 1) {
|
||||
this->mouse_x = event.motion.x;
|
||||
this->mouse_y = event.motion.y;
|
||||
this->mouse_down = true;
|
||||
} else {
|
||||
this->mouse_down = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_KEYDOWN:
|
||||
// Ignore auto-repeat, otherwise holding a key floods the listeners.
|
||||
if (event.key.repeat != 0)
|
||||
break;
|
||||
ESP_LOGD(TAG, "keydown %d", event.key.keysym.sym);
|
||||
this->process_key(event.key.keysym.sym, true);
|
||||
break;
|
||||
|
||||
case SDL_KEYUP:
|
||||
ESP_LOGD(TAG, "keyup %d", event.key.keysym.sym);
|
||||
this->process_key(event.key.keysym.sym, false);
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
switch (event.window.event) {
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
case SDL_WINDOWEVENT_RESIZED: {
|
||||
SDL_Rect rect{0, 0, this->width_, this->height_};
|
||||
this->redraw_(rect);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl::loop() {
|
||||
SDL_Event e;
|
||||
// Take everything that is waiting, not one event per loop. A touch drag produces a burst of
|
||||
// motion events, and consuming them one at a time lets the queue grow without bound, so the
|
||||
// pointer ends up acting on input from further and further in the past. Draining collapses a
|
||||
// burst to the position it ended at, which is the one the user is asking for anyway.
|
||||
while (SDL_PollEvent(&e)) {
|
||||
if (e.type == SDL_QUIT)
|
||||
exit(0);
|
||||
|
||||
// Events carry the window they happened in, so send each one to the display that owns it.
|
||||
uint32_t window_id;
|
||||
if (SDL_PollEvent(&e)) {
|
||||
switch (e.type) {
|
||||
case SDL_QUIT:
|
||||
exit(0);
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
window_id = e.button.windowID;
|
||||
if (e.button.button == 1) {
|
||||
this->mouse_x = e.button.x;
|
||||
this->mouse_y = e.button.y;
|
||||
this->mouse_down = e.button.state != 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
window_id = e.motion.windowID;
|
||||
if (e.motion.state & 1) {
|
||||
this->mouse_x = e.button.x;
|
||||
this->mouse_y = e.button.y;
|
||||
this->mouse_down = true;
|
||||
} else {
|
||||
this->mouse_down = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_KEYDOWN:
|
||||
ESP_LOGD(TAG, "keydown %d", e.key.keysym.sym);
|
||||
this->process_key(e.key.keysym.sym, true);
|
||||
break;
|
||||
|
||||
case SDL_KEYUP:
|
||||
window_id = e.key.windowID;
|
||||
ESP_LOGD(TAG, "keyup %d", e.key.keysym.sym);
|
||||
this->process_key(e.key.keysym.sym, false);
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
window_id = e.window.windowID;
|
||||
switch (e.window.event) {
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
case SDL_WINDOWEVENT_RESIZED: {
|
||||
SDL_Rect rect{0, 0, this->width_, this->height_};
|
||||
this->redraw_(rect);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Anything else, including the touch events SDL reports alongside the mouse events it
|
||||
// synthesises from them, is not used here.
|
||||
ESP_LOGV(TAG, "Event %d", e.type);
|
||||
continue;
|
||||
}
|
||||
|
||||
Sdl *target = instance_for_window_(window_id);
|
||||
if (target == nullptr) {
|
||||
// Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
|
||||
// input that stops working leaves no trace at all.
|
||||
ESP_LOGV(TAG, "Event %d for unknown window %u", e.type, window_id);
|
||||
continue;
|
||||
}
|
||||
target->handle_event_(e);
|
||||
}
|
||||
}
|
||||
|
||||
bool Sdl::capture_bgr(uint8_t *dest, size_t row_stride) {
|
||||
if (this->texture_ == nullptr || this->renderer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Snapshot requested but SDL is not set up");
|
||||
return false;
|
||||
}
|
||||
if (this->shot_target_ == nullptr) {
|
||||
this->shot_target_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_TARGET,
|
||||
this->width_, this->height_);
|
||||
if (this->shot_target_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not create capture texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
SDL_SetTextureBlendMode(this->shot_target_, SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
// Render into an offscreen target first. SDL_RenderReadPixels works in physical output pixels and
|
||||
// ignores the logical size, so reading straight off a resizable window would read more pixels than
|
||||
// there is room for.
|
||||
// Every step is checked: a failed clear or copy would otherwise be read back as a blank or stale
|
||||
// picture, written out, and reported as a snapshot that worked.
|
||||
bool ok = false;
|
||||
if (SDL_SetRenderTarget(this->renderer_, this->shot_target_) == 0) {
|
||||
ok = SDL_SetRenderDrawColor(this->renderer_, 0, 0, 0, SDL_ALPHA_OPAQUE) == 0 &&
|
||||
SDL_RenderClear(this->renderer_) == 0 &&
|
||||
SDL_RenderCopy(this->renderer_, this->texture_, nullptr, nullptr) == 0 &&
|
||||
SDL_RenderReadPixels(this->renderer_, nullptr, SDL_PIXELFORMAT_BGR24, dest, static_cast<int>(row_stride)) == 0;
|
||||
if (SDL_SetRenderTarget(this->renderer_, nullptr) != 0) {
|
||||
// Stuck rendering into shot_target_ from here on, so there's no point continuing.
|
||||
ESP_LOGE(TAG, "Could not restore the render target: %s", SDL_GetError());
|
||||
this->mark_failed();
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
ESP_LOGE(TAG, "Could not capture the screen: %s", SDL_GetError());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace esphome::sdl
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/snapshot/snapshot.h"
|
||||
#define SDL_MAIN_HANDLED
|
||||
#include "SDL.h"
|
||||
#include <map>
|
||||
@@ -15,7 +13,7 @@ namespace esphome::sdl {
|
||||
|
||||
constexpr static const char *const TAG = "sdl";
|
||||
|
||||
class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
class Sdl final : public display::Display {
|
||||
public:
|
||||
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
|
||||
void update() override;
|
||||
@@ -34,9 +32,6 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
this->pos_x_ = pos_x;
|
||||
this->pos_y_ = pos_y;
|
||||
}
|
||||
void set_headless(bool headless) { this->headless_ = headless; }
|
||||
void set_snapshot_key(int32_t keycode) { this->snapshot_key_ = keycode; }
|
||||
|
||||
int get_width() override;
|
||||
int get_height() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
@@ -56,40 +51,20 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
void redraw_(SDL_Rect &rect);
|
||||
bool setup_renderer_();
|
||||
/// Release the window, surface, renderer and textures, and forget them.
|
||||
void destroy_renderer_();
|
||||
/// Log an SDL failure during setup, release anything already created, and return false.
|
||||
bool setup_failed_(const char *what);
|
||||
int snapshot_width() override { return this->width_; }
|
||||
int snapshot_height() override { return this->height_; }
|
||||
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
|
||||
void handle_event_(const SDL_Event &event);
|
||||
/// The display owning the given window, or nullptr if it is not one of ours.
|
||||
static Sdl *instance_for_window_(uint32_t window_id);
|
||||
SDL_Renderer *renderer_{};
|
||||
SDL_Window *window_{};
|
||||
SDL_Texture *texture_{};
|
||||
// Offscreen render target used when headless. SDL_CreateSoftwareRenderer only borrows the
|
||||
// surface, and the renderer goes back to using it as its output whenever the capture target is
|
||||
// released, so it has to stay alive as long as the renderer does.
|
||||
SDL_Surface *surface_{};
|
||||
// Capture target, created on first snapshot.
|
||||
SDL_Texture *shot_target_{};
|
||||
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{};
|
||||
int width_{};
|
||||
int height_{};
|
||||
uint32_t window_options_{0};
|
||||
int32_t pos_x_{SDL_WINDOWPOS_UNDEFINED};
|
||||
int32_t pos_y_{SDL_WINDOWPOS_UNDEFINED};
|
||||
int32_t snapshot_key_{0};
|
||||
SDL_Renderer *renderer_{};
|
||||
SDL_Window *window_{};
|
||||
SDL_Texture *texture_{};
|
||||
uint16_t x_low_{0};
|
||||
uint16_t y_low_{0};
|
||||
uint16_t x_high_{0};
|
||||
uint16_t y_high_{0};
|
||||
bool headless_{false};
|
||||
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::sdl
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,12 +4,10 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..display import CONF_SDL_ID, Sdl, headless_final_validate, sdl_ns
|
||||
from ..display import CONF_SDL_ID, Sdl, sdl_ns
|
||||
|
||||
SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = headless_final_validate("touchscreen")
|
||||
|
||||
|
||||
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
|
||||
{
|
||||
|
||||
@@ -31,7 +31,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
baud_rate=115200,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
@@ -33,6 +33,8 @@ void MR60FDA2Component::dump_config() {
|
||||
|
||||
// Initialisation functions
|
||||
void MR60FDA2Component::setup() {
|
||||
this->check_uart_settings(115200);
|
||||
|
||||
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
|
||||
this->current_frame_id_ = 0;
|
||||
this->current_frame_len_ = 0;
|
||||
|
||||
@@ -130,26 +130,17 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
|
||||
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
|
||||
// Some bridges watch line-coding changes as a signalling channel (a magic baud
|
||||
// sequence to enter a bootloader, say), so redundant applies are not harmless.
|
||||
static const uart::UARTParityOptions PARITY_MAP[] = {
|
||||
uart::UART_CONFIG_PARITY_NONE,
|
||||
uart::UART_CONFIG_PARITY_EVEN,
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
|
||||
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
|
||||
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
// Apply validated parameters
|
||||
uart_comp->set_baud_rate(baudrate);
|
||||
uart_comp->set_stop_bits(stop_bits);
|
||||
uart_comp->set_data_bits(data_size);
|
||||
|
||||
// Map parity value to UARTParityOptions
|
||||
static const uart::UARTParityOptions PARITY_MAP[] = {
|
||||
uart::UART_CONFIG_PARITY_NONE,
|
||||
uart::UART_CONFIG_PARITY_EVEN,
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
uart_comp->set_parity(PARITY_MAP[parity]);
|
||||
|
||||
// load_settings() is available on ESP8266 and ESP32 platforms
|
||||
|
||||
@@ -68,13 +68,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"smt100",
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"smt100", baud_rate=9600, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ void SMT100Component::dump_config() {
|
||||
LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
int SMT100Component::readline_(int readch, char *buffer, int len) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Shared support for writing what a display is showing out to an image file.
|
||||
|
||||
The component itself has no configuration. It provides the ``snapshot.take`` action and the C++
|
||||
base class behind it, so any display that can hand over its pixels - the in memory display in this
|
||||
component, or an SDL window - saves files the same way, under the same directory, with the same
|
||||
rules about names.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
DOMAIN = "snapshot"
|
||||
|
||||
CONF_FILENAME = "filename"
|
||||
|
||||
snapshot_ns = cg.esphome_ns.namespace("snapshot")
|
||||
Snapshot = snapshot_ns.class_("Snapshot")
|
||||
SnapshotAction = snapshot_ns.class_("SnapshotAction", automation.Action)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"snapshot.take",
|
||||
SnapshotAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Snapshot),
|
||||
cv.Optional(CONF_FILENAME): cv.templatable(cv.string),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def snapshot_take_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
if (filename := config.get(CONF_FILENAME)) is not None:
|
||||
cg.add(var.set_filename(await cg.templatable(filename, args, cg.std_string)))
|
||||
return var
|
||||
|
||||
|
||||
@dataclass
|
||||
class SnapshotData:
|
||||
directory_defined: bool = False
|
||||
|
||||
|
||||
def _get_data() -> SnapshotData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = SnapshotData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
async def register_snapshot(var: MockObj, config: ConfigType) -> None:
|
||||
"""Set up a component so that the snapshot action can write its picture to a file."""
|
||||
data = _get_data()
|
||||
# Only once, however many displays there are: two defines that say the same thing do not
|
||||
# compare equal, so asking for this per display repeats the line in defines.h.
|
||||
if not data.directory_defined:
|
||||
data.directory_defined = True
|
||||
cg.add_define(
|
||||
"ESPHOME_SNAPSHOT_DIR",
|
||||
(CORE.data_dir / "snapshots" / CORE.name).as_posix(),
|
||||
)
|
||||
cg.add(var.set_snapshot_prefix(str(config[CONF_ID])))
|
||||
@@ -1,61 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIMENSIONS,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_WIDTH,
|
||||
PLATFORM_HOST,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import Snapshot, register_snapshot, snapshot_ns
|
||||
|
||||
# The base class and the file writing live in the parent component, which nothing else in a
|
||||
# configuration using only this platform would pull in.
|
||||
AUTO_LOAD = ["snapshot"]
|
||||
|
||||
SnapshotDisplay = snapshot_ns.class_(
|
||||
"SnapshotDisplay", display.DisplayBuffer, cg.Component, Snapshot
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SnapshotDisplay),
|
||||
cv.Required(CONF_DIMENSIONS): cv.Any(
|
||||
cv.dimensions,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): cv.positive_not_null_int,
|
||||
cv.Required(CONF_HEIGHT): cv.positive_not_null_int,
|
||||
}
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.only_on(PLATFORM_HOST),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await register_snapshot(var, config)
|
||||
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
|
||||
else:
|
||||
(width, height) = dimensions
|
||||
cg.add(var.set_dimensions(width, height))
|
||||
|
||||
if lamb := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lamb, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
@@ -1,80 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
#include "snapshot_display.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
static const char *const TAG = "snapshot.display";
|
||||
|
||||
namespace {
|
||||
|
||||
/// Spread a channel that only goes up to `max` over the whole 0 to 255 range, so that the
|
||||
/// brightest value stays the brightest. This is the same arithmetic SDL uses, which is what makes
|
||||
/// a picture taken here come out identical to the same picture taken from an SDL window.
|
||||
constexpr uint8_t expand_channel(uint16_t value, uint16_t max) { return static_cast<uint8_t>(value * 255 / max); }
|
||||
|
||||
constexpr uint16_t RED_MAX = 0x1F;
|
||||
constexpr uint16_t GREEN_MAX = 0x3F;
|
||||
constexpr uint16_t BLUE_MAX = 0x1F;
|
||||
|
||||
} // namespace
|
||||
|
||||
void SnapshotDisplay::setup() {
|
||||
this->init_internal_(static_cast<uint32_t>(this->width_) * this->height_ * 2);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Could not allocate display buffer"));
|
||||
}
|
||||
}
|
||||
|
||||
void SnapshotDisplay::dump_config() { LOG_DISPLAY("", "Snapshot", this); }
|
||||
|
||||
void SnapshotDisplay::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (this->buffer_ == nullptr || x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return;
|
||||
this->pixels_()[y * this->width_ + x] = display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB);
|
||||
}
|
||||
|
||||
void SnapshotDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr,
|
||||
display::ColorOrder order, display::ColorBitness bitness, bool big_endian,
|
||||
int x_offset, int y_offset, int x_pad) {
|
||||
if (this->buffer_ == nullptr)
|
||||
return;
|
||||
// Anything that is not already laid out the way the buffer is, or that would reach outside it,
|
||||
// goes through the base class, which turns it into one call per pixel with the bounds checked.
|
||||
const bool copyable = this->rotation_ == display::DISPLAY_ROTATION_0_DEGREES &&
|
||||
bitness == display::COLOR_BITNESS_565 && !big_endian && x_start >= 0 && y_start >= 0 &&
|
||||
x_start + w <= this->width_ && y_start + h <= this->height_;
|
||||
if (!copyable) {
|
||||
DisplayBuffer::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
const size_t stride = static_cast<size_t>(x_offset) + w + x_pad;
|
||||
const uint8_t *src = ptr + (stride * y_offset + x_offset) * 2;
|
||||
for (int y = 0; y != h; y++) {
|
||||
memcpy(&this->pixels_()[(y_start + y) * this->width_ + x_start], src + y * stride * 2, w * 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool SnapshotDisplay::capture_bgr(uint8_t *dest, size_t row_stride) {
|
||||
if (this->buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Snapshot requested but there is no buffer to read");
|
||||
return false;
|
||||
}
|
||||
const uint16_t *src = this->pixels_();
|
||||
for (int y = 0; y != this->height_; y++) {
|
||||
uint8_t *out = dest + y * row_stride;
|
||||
for (int x = 0; x != this->width_; x++) {
|
||||
const uint16_t pixel = *src++;
|
||||
*out++ = expand_channel(pixel & BLUE_MAX, BLUE_MAX);
|
||||
*out++ = expand_channel((pixel >> 5) & GREEN_MAX, GREEN_MAX);
|
||||
*out++ = expand_channel(pixel >> 11, RED_MAX);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
#endif
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/snapshot/snapshot.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
/// A display with nowhere to show anything: it keeps the picture in memory, where the snapshot
|
||||
/// action can pick it up. That makes it a way to see what a configuration draws on a machine with
|
||||
/// no screen, and to check the result in a test.
|
||||
class SnapshotDisplay final : public display::DisplayBuffer, public Snapshot {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override { this->do_update_(); }
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
void set_dimensions(uint16_t width, uint16_t height) {
|
||||
this->width_ = width;
|
||||
this->height_ = height;
|
||||
}
|
||||
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
int snapshot_width() override { return this->width_; }
|
||||
int snapshot_height() override { return this->height_; }
|
||||
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
|
||||
|
||||
/// The picture, one 16 bit RGB565 value per pixel, topmost row first. Owned by DisplayBuffer as
|
||||
/// a byte pointer; this is the same memory seen as what is actually stored in it.
|
||||
uint16_t *pixels_() { return reinterpret_cast<uint16_t *>(this->buffer_); }
|
||||
|
||||
int width_{};
|
||||
int height_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
|
||||
#endif
|
||||
@@ -1,248 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
#include "snapshot.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *const TAG = "snapshot";
|
||||
|
||||
// Longest name we will build a path from. NAME_MAX is 255 and we may append a collision suffix.
|
||||
constexpr size_t MAX_NAME_LENGTH = 200;
|
||||
// Give up rather than spin forever if every candidate name is taken.
|
||||
constexpr unsigned MAX_NAME_ATTEMPTS = 1000;
|
||||
// A BMP file header followed by a BITMAPINFOHEADER, which is where the pixels start.
|
||||
constexpr size_t BMP_HEADER_SIZE = 54;
|
||||
constexpr size_t BMP_INFO_HEADER_SIZE = 40;
|
||||
constexpr int BMP_BITS_PER_PIXEL = 24;
|
||||
|
||||
/// True if the name already ends in ".bmp". The comparison ignores case, so "shot.BMP" is left
|
||||
/// alone rather than turned into "shot.BMP.bmp".
|
||||
bool has_bmp_suffix(const std::string &name) {
|
||||
return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, ".bmp") == 0;
|
||||
}
|
||||
|
||||
/// Reduce a user supplied name to a single safe path component. Everything outside the allowed set
|
||||
/// is replaced, so "..", "/" and absolute paths cannot escape the snapshot directory.
|
||||
/// Returns an empty string if nothing usable is left.
|
||||
std::string sanitise_filename(const char *const name, bool *name_changed) {
|
||||
std::string result;
|
||||
bool all_dots = true;
|
||||
bool changed = false;
|
||||
for (const char *p = name; *p != '\0'; p++) {
|
||||
if (result.size() >= MAX_NAME_LENGTH) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
char c = *p;
|
||||
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
|
||||
c = '_';
|
||||
changed = true;
|
||||
}
|
||||
if (c != '.')
|
||||
all_dots = false;
|
||||
result.push_back(c);
|
||||
}
|
||||
if (all_dots) {
|
||||
*name_changed = true;
|
||||
return "";
|
||||
}
|
||||
if (!has_bmp_suffix(result))
|
||||
result += ".bmp";
|
||||
*name_changed = changed;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Insert "-<attempt>" before the file extension, e.g. "shot.bmp" -> "shot-1.bmp".
|
||||
std::string add_suffix(const std::string &name, unsigned attempt) {
|
||||
char suffix[12];
|
||||
snprintf(suffix, sizeof(suffix), "-%u", attempt);
|
||||
auto dot = name.rfind('.');
|
||||
if (dot == std::string::npos)
|
||||
return name + suffix;
|
||||
return name.substr(0, dot) + suffix + name.substr(dot);
|
||||
}
|
||||
|
||||
/// Directory snapshots are written to. The environment variable lets a test redirect output
|
||||
/// without rebuilding, matching how the host platform handles ESPHOME_PREFDIR.
|
||||
const char *snapshot_dir() {
|
||||
const char *dir = getenv("ESPHOME_SNAPSHOT_DIR"); // NOLINT(concurrency-mt-unsafe)
|
||||
return dir != nullptr && dir[0] != '\0' ? dir : ESPHOME_SNAPSHOT_DIR;
|
||||
}
|
||||
|
||||
/// Store a value in as many bytes, least significant first, and step the pointer past it.
|
||||
/// BMP is a little endian format whatever the machine writing it uses.
|
||||
void put_le(uint8_t *&dest, uint32_t value, size_t bytes) {
|
||||
for (size_t i = 0; i != bytes; i++)
|
||||
*dest++ = static_cast<uint8_t>(value >> (8 * i));
|
||||
}
|
||||
|
||||
/// The number of bytes one row of `width` pixels takes up in the file. Rows are padded out to a
|
||||
/// multiple of four bytes.
|
||||
size_t bmp_row_size(int width) { return (static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
|
||||
|
||||
/// Write pixels out as a 24 bit BMP. The rows given start with the topmost and are `row_stride`
|
||||
/// bytes apart, which must leave room for a whole padded row; a BMP holds its rows the other way
|
||||
/// up, so they go out last first.
|
||||
bool write_bmp(FILE *file, const uint8_t *pixels, int width, int height, size_t row_stride) {
|
||||
const size_t row_size = bmp_row_size(width);
|
||||
const size_t pixel_bytes = row_size * height;
|
||||
|
||||
uint8_t header[BMP_HEADER_SIZE];
|
||||
uint8_t *pos = header;
|
||||
*pos++ = 'B';
|
||||
*pos++ = 'M';
|
||||
put_le(pos, static_cast<uint32_t>(BMP_HEADER_SIZE + pixel_bytes), 4);
|
||||
put_le(pos, 0, 4); // reserved
|
||||
put_le(pos, BMP_HEADER_SIZE, 4);
|
||||
put_le(pos, BMP_INFO_HEADER_SIZE, 4);
|
||||
put_le(pos, static_cast<uint32_t>(width), 4);
|
||||
put_le(pos, static_cast<uint32_t>(height), 4);
|
||||
put_le(pos, 1, 2); // one plane
|
||||
put_le(pos, BMP_BITS_PER_PIXEL, 2);
|
||||
put_le(pos, 0, 4); // not compressed
|
||||
put_le(pos, static_cast<uint32_t>(pixel_bytes), 4);
|
||||
put_le(pos, 0, 4); // pixels per metre across, unspecified
|
||||
put_le(pos, 0, 4); // pixels per metre down, unspecified
|
||||
put_le(pos, 0, 4); // no palette
|
||||
put_le(pos, 0, 4); // so no palette entry matters more than another
|
||||
|
||||
if (fwrite(header, 1, sizeof(header), file) != sizeof(header))
|
||||
return false;
|
||||
for (int y = height - 1; y >= 0; y--) {
|
||||
if (fwrite(pixels + static_cast<size_t>(y) * row_stride, 1, row_size, file) != row_size)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Reserve a name in the snapshot directory and write the picture to it.
|
||||
/// With `exact` set the given name is the only one tried; otherwise a number is added on
|
||||
/// collision. Returns true if a file was written.
|
||||
bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name,
|
||||
bool exact) {
|
||||
const std::string dir = snapshot_dir();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) {
|
||||
ESP_LOGE(TAG, "Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// O_EXCL guarantees we never write over a file that is already there.
|
||||
std::string path;
|
||||
int fd = -1;
|
||||
for (unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
|
||||
path = dir + "/" + (attempt == 0 ? name : add_suffix(name, attempt));
|
||||
fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644);
|
||||
if (fd >= 0)
|
||||
break;
|
||||
if (errno != EEXIST) {
|
||||
ESP_LOGE(TAG, "Could not create %s: %s", path.c_str(), strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (exact) {
|
||||
// The caller asked for this exact name, so silently writing somewhere else would be worse
|
||||
// than failing - a test asserting on the path would pick up a stale file.
|
||||
ESP_LOGE(TAG, "Snapshot %s already exists, not overwriting", path.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (fd < 0) {
|
||||
ESP_LOGE(TAG, "Could not find an unused name for %s in %s", name.c_str(), dir.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE *file = fdopen(fd, "wb");
|
||||
if (file == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not open %s: %s", path.c_str(), strerror(errno));
|
||||
::close(fd);
|
||||
::unlink(path.c_str());
|
||||
return false;
|
||||
}
|
||||
bool ok = write_bmp(file, pixels, width, height, row_stride);
|
||||
int saved_errno = ok ? 0 : errno;
|
||||
// Closing can fail in its own right - the last of the data is still on its way out.
|
||||
if (fclose(file) != 0) {
|
||||
if (ok)
|
||||
saved_errno = errno;
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) {
|
||||
ESP_LOGE(TAG, "Could not write %s: %s", path.c_str(), strerror(saved_errno));
|
||||
// Leave no truncated file behind - it would block a retry under the same name.
|
||||
::unlink(path.c_str());
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG, "Snapshot written to %s", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// helper function since ESP_LOGW is disallowed in a header file
|
||||
void Snapshot::log_action_failed() { ESP_LOGW(TAG, "snapshot.take did not write a file"); }
|
||||
|
||||
bool Snapshot::take_snapshot(const char *filename) {
|
||||
const int width = this->snapshot_width();
|
||||
const int height = this->snapshot_height();
|
||||
if (width <= 0 || height <= 0) {
|
||||
ESP_LOGE(TAG, "Snapshot requested but the display is %dx%d", width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
bool exact = false;
|
||||
if (filename != nullptr) {
|
||||
bool name_changed = false;
|
||||
name = sanitise_filename(filename, &name_changed);
|
||||
exact = !name.empty();
|
||||
if (name_changed) {
|
||||
ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
|
||||
name.empty() ? "a name made from the time" : name.c_str());
|
||||
}
|
||||
}
|
||||
if (name.empty()) {
|
||||
struct timespec now {};
|
||||
if (clock_gettime(CLOCK_REALTIME, &now) != 0)
|
||||
now = {};
|
||||
struct tm tm_buf {};
|
||||
if (localtime_r(&now.tv_sec, &tm_buf) == nullptr)
|
||||
tm_buf = {};
|
||||
char stamp[32]{};
|
||||
// ::strftime to be sure of the one from <ctime>; display has an unrelated member of that name
|
||||
if (::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm_buf) == 0)
|
||||
snprintf(stamp, sizeof(stamp), "unknown-time");
|
||||
char buffer[MAX_NAME_LENGTH];
|
||||
int written =
|
||||
snprintf(buffer, sizeof(buffer), "%s-%s-%03ld.bmp", this->snapshot_prefix_, stamp, now.tv_nsec / 1000000);
|
||||
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
|
||||
ESP_LOGW(TAG, "Could not build a timestamped snapshot name, using a fallback");
|
||||
snprintf(buffer, sizeof(buffer), "snapshot.bmp");
|
||||
}
|
||||
name = buffer;
|
||||
}
|
||||
|
||||
// Rows are padded out to a multiple of four bytes, as the file wants them, so each one can be
|
||||
// written straight from the buffer. Zeroed on allocation, which is what the padding must be.
|
||||
const size_t row_stride = bmp_row_size(width);
|
||||
auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
|
||||
if (!this->capture_bgr(pixels.get(), row_stride))
|
||||
return false;
|
||||
return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
|
||||
}
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
#endif
|
||||
@@ -1,72 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Directory snapshots are written to. Normally set by codegen to a folder under .esphome; the
|
||||
// fallback keeps the component compiling for static analysis, where no defines.h is generated.
|
||||
#ifndef ESPHOME_SNAPSHOT_DIR
|
||||
#define ESPHOME_SNAPSHOT_DIR "."
|
||||
#endif
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
/// Base for anything that can hand over the picture it is showing so it can be written to a file.
|
||||
///
|
||||
/// A subclass says how big the picture is and fills in the pixels. Everything else - picking a
|
||||
/// name, staying inside the snapshot directory, not writing over anything, and encoding the file -
|
||||
/// is done here, so every component that can take a snapshot behaves the same way.
|
||||
class Snapshot {
|
||||
public:
|
||||
virtual ~Snapshot() = default;
|
||||
|
||||
/// Set the word generated names start with. Codegen passes the component id, so with more than
|
||||
/// one display in a device it is clear which one a file came from.
|
||||
void set_snapshot_prefix(const char *prefix) { this->snapshot_prefix_ = prefix; }
|
||||
|
||||
/// Write the current picture to a BMP file in the snapshot directory.
|
||||
///
|
||||
/// Pass nullptr to have a name made up from the prefix and the current time. A file that is
|
||||
/// already there is never written over. Returns true if a file was written.
|
||||
bool take_snapshot(const char *filename);
|
||||
|
||||
/// Log that an action-triggered snapshot did not write a file.
|
||||
static void log_action_failed();
|
||||
|
||||
protected:
|
||||
/// Width of the picture in pixels.
|
||||
virtual int snapshot_width() = 0;
|
||||
/// Height of the picture in pixels.
|
||||
virtual int snapshot_height() = 0;
|
||||
/// Fill in the picture: three bytes per pixel in blue, green, red order, topmost row first, with
|
||||
/// `row_stride` bytes from the start of one row to the start of the next. Returns false, having
|
||||
/// logged why, if the picture could not be read.
|
||||
virtual bool capture_bgr(uint8_t *dest, size_t row_stride) = 0;
|
||||
|
||||
const char *snapshot_prefix_{"snapshot"};
|
||||
};
|
||||
|
||||
template<typename... Ts> class SnapshotAction final : public Action<Ts...>, public Parented<Snapshot> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::string, filename)
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override {
|
||||
bool ok;
|
||||
if (this->filename_.has_value()) {
|
||||
ok = this->parent_->take_snapshot(this->filename_.value(x...).c_str());
|
||||
} else {
|
||||
ok = this->parent_->take_snapshot(nullptr);
|
||||
}
|
||||
if (!ok)
|
||||
this->parent_->log_action_failed();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
|
||||
#endif
|
||||
@@ -33,13 +33,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"t6615",
|
||||
baud_rate=19200,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"t6615", baud_rate=19200, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ void T6615Component::query_ppm_() {
|
||||
void T6615Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "T6615:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
this->check_uart_settings(19200);
|
||||
}
|
||||
|
||||
} // namespace esphome::t6615
|
||||
|
||||
@@ -35,22 +35,6 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
# Historical mode runs at 1200 baud, standard mode at 9600 baud.
|
||||
baud_rate = 1200 if config[CONF_HISTORICAL_MODE] else 9600
|
||||
uart.final_validate_device_schema(
|
||||
"teleinfo",
|
||||
baud_rate=baud_rate,
|
||||
data_bits=7,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
)(config)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -184,7 +184,10 @@ void TeleInfo::publish_value_(const std::string &tag, const std::string &val) {
|
||||
element->publish_val(val);
|
||||
}
|
||||
}
|
||||
void TeleInfo::dump_config() { ESP_LOGCONFIG(TAG, "TeleInfo:"); }
|
||||
void TeleInfo::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "TeleInfo:");
|
||||
this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
|
||||
}
|
||||
TeleInfo::TeleInfo(bool historical_mode) {
|
||||
if (historical_mode) {
|
||||
/*
|
||||
@@ -192,9 +195,11 @@ TeleInfo::TeleInfo(bool historical_mode) {
|
||||
*/
|
||||
checksum_area_end_ = 2;
|
||||
separator_ = 0x20;
|
||||
baud_rate_ = 1200;
|
||||
} else {
|
||||
checksum_area_end_ = 1;
|
||||
separator_ = 0x9;
|
||||
baud_rate_ = 9600;
|
||||
}
|
||||
}
|
||||
void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); }
|
||||
|
||||
@@ -31,6 +31,7 @@ class TeleInfo final : public PollingComponent, public uart::UARTDevice {
|
||||
std::vector<TeleInfoListener *> teleinfo_listeners_{};
|
||||
|
||||
protected:
|
||||
uint32_t baud_rate_;
|
||||
int checksum_area_end_;
|
||||
int separator_;
|
||||
char buf_[MAX_BUF_SIZE];
|
||||
|
||||
@@ -36,6 +36,8 @@ cover::CoverTraits Tormatic::get_traits() {
|
||||
|
||||
void Tormatic::dump_config() {
|
||||
LOG_COVER("", "Tormatic Cover", this);
|
||||
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Open Duration: %.1fs\n"
|
||||
" Close Duration: %.1fs",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <vector>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "uart_component.h"
|
||||
|
||||
@@ -67,7 +66,6 @@ class UARTDevice {
|
||||
}
|
||||
|
||||
/// Check that the configuration of the UART bus matches the provided values and otherwise print a warning
|
||||
ESPDEPRECATED("Use uart.final_validate_device_schema() in Python instead. Removed in 2027.3.0", "2026.9.0")
|
||||
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1,
|
||||
UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8);
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
baud_rate=2400,
|
||||
data_bits=8,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
@@ -213,6 +213,7 @@ void UFM01Component::dump_config() {
|
||||
LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
|
||||
#endif
|
||||
this->check_uart_settings(2400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
}
|
||||
|
||||
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) {
|
||||
|
||||
@@ -50,7 +50,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
parity=None,
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ void UponorSmatrixComponent::dump_config() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->check_uart_settings(19200);
|
||||
|
||||
if (!this->unknown_devices_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
|
||||
for (auto device_address : this->unknown_devices_) {
|
||||
|
||||
@@ -29,14 +29,6 @@ CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"vbus",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -11,7 +11,10 @@ static const char *const TAG = "vbus";
|
||||
// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical)
|
||||
static constexpr size_t VBUS_MAX_LOG_BYTES = 64;
|
||||
|
||||
void VBus::dump_config() { ESP_LOGCONFIG(TAG, "VBus:"); }
|
||||
void VBus::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "VBus:");
|
||||
check_uart_settings(9600);
|
||||
}
|
||||
|
||||
static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) {
|
||||
for (int i = 0; i < count; i++, septet >>= 1) {
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.components.logger import request_log_listener
|
||||
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
CONF_AUTH,
|
||||
CONF_COMPRESSION,
|
||||
CONF_CSS_INCLUDE,
|
||||
@@ -23,15 +24,19 @@ from esphome.const import (
|
||||
CONF_JS_URL,
|
||||
CONF_LOCAL,
|
||||
CONF_LOG,
|
||||
CONF_MANUAL_IP,
|
||||
CONF_NAME,
|
||||
CONF_NETWORKS,
|
||||
CONF_OTA,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_STATIC_IP,
|
||||
CONF_TYPE,
|
||||
CONF_USERNAME,
|
||||
CONF_VERSION,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WEB_SERVER_ID,
|
||||
CONF_WIFI,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
@@ -46,7 +51,23 @@ from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["json", "web_server_base"]
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
# No config parameter on purpose: that would make this a late (dynamic) auto-load and
|
||||
# ota.web_server's dependency on web_server_base would not be satisfied in time.
|
||||
auto_load = ["json", "web_server_base"]
|
||||
# The AP mode DNS server (web_server_base/dns_server_esp32_idf) uses socket; only
|
||||
# configs with a WiFi access point can end up in AP mode. CORE.raw_config is set
|
||||
# after package merging, so a wifi block from a package is visible here.
|
||||
wifi = CORE.raw_config.get(CONF_WIFI) if CORE.raw_config else None
|
||||
if (
|
||||
CORE.is_esp32
|
||||
and wifi is not None
|
||||
and (not isinstance(wifi, dict) or CONF_AP in wifi)
|
||||
):
|
||||
auto_load.append("socket")
|
||||
return auto_load
|
||||
|
||||
|
||||
AUTH_TYPE_BASIC = "basic"
|
||||
AUTH_TYPE_DIGEST = "digest"
|
||||
@@ -205,9 +226,6 @@ def _final_validate_sorting(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate_sorting
|
||||
|
||||
|
||||
def _consume_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for web_server component."""
|
||||
from esphome.components import socket
|
||||
@@ -334,6 +352,95 @@ async def add_entity_config(entity: MockObj, config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
|
||||
def wifi_is_ap_only(wifi_config: ConfigType | None) -> bool:
|
||||
"""AP only: an access point and no network to join, so the device is only ever reached
|
||||
through its own AP."""
|
||||
return (
|
||||
wifi_config is not None
|
||||
and CONF_AP in wifi_config
|
||||
and not wifi_config.get(CONF_NETWORKS)
|
||||
)
|
||||
|
||||
|
||||
def serve_local(config: ConfigType, wifi_config: ConfigType | None) -> bool:
|
||||
"""Embed the interface unless ``local:`` says otherwise; AP only WiFi has no internet
|
||||
for the hosted page. Version 1 has no local mode."""
|
||||
if (local := config.get(CONF_LOCAL)) is not None:
|
||||
return local
|
||||
return config[CONF_VERSION] != 1 and wifi_is_ap_only(wifi_config)
|
||||
|
||||
|
||||
def serve_captive(config: ConfigType, full_config: ConfigType) -> bool:
|
||||
"""web_server runs its own captive portal while the AP is up: embedded interface plus
|
||||
an access point, unless captive_portal (which owns that role) is configured. Only on
|
||||
port 80: the OS captive portal probes and the DHCP portal URI always use port 80, so
|
||||
a portal on another port could never be discovered."""
|
||||
wifi_config = full_config.get(CONF_WIFI)
|
||||
return (
|
||||
"captive_portal" not in full_config
|
||||
and config[CONF_PORT] == 80
|
||||
and wifi_config is not None
|
||||
and CONF_AP in wifi_config
|
||||
and serve_local(config, wifi_config)
|
||||
)
|
||||
|
||||
|
||||
def _final_validate_ap_mode(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()
|
||||
wifi_config = full_config.get(CONF_WIFI)
|
||||
captive = serve_captive(config, full_config)
|
||||
local = serve_local(config, wifi_config)
|
||||
if captive:
|
||||
web_server_base.consume_captive_dns_sockets(config, "web_server")
|
||||
# Surface behavior that the config does not spell out.
|
||||
if local and CONF_LOCAL not in config:
|
||||
_LOGGER.info(
|
||||
"WiFi is AP only: embedding the web interface in the firmware "
|
||||
"(local: true, roughly 13 KB of flash for version 2, 78 KB for version 3)%s. "
|
||||
"Set 'local: false' to load it from the internet instead.",
|
||||
" and serving it as a captive portal on the access point"
|
||||
if captive
|
||||
else "",
|
||||
)
|
||||
elif captive:
|
||||
_LOGGER.info(
|
||||
"web_server will act as a captive portal while the %saccess point is active.",
|
||||
"" if wifi_is_ap_only(wifi_config) else "fallback ",
|
||||
)
|
||||
if not wifi_is_ap_only(wifi_config):
|
||||
return
|
||||
if not local:
|
||||
_LOGGER.warning(
|
||||
"WiFi is AP only and the web_server interface is loaded from the internet, "
|
||||
"which browsers on the access point usually cannot reach; the page stays "
|
||||
"blank. %s so the interface is embedded in the firmware.",
|
||||
"Remove 'local: false'"
|
||||
if config.get(CONF_LOCAL) is False
|
||||
else "Migrate to version 2 or 3",
|
||||
)
|
||||
elif config[CONF_PORT] != 80:
|
||||
ap_ip = "192.168.4.1"
|
||||
if (manual_ip := wifi_config[CONF_AP].get(CONF_MANUAL_IP)) is not None:
|
||||
ap_ip = str(manual_ip[CONF_STATIC_IP])
|
||||
_LOGGER.warning(
|
||||
"WiFi is AP only and web_server uses port %d. The interface cannot open "
|
||||
"automatically on the access point (captive portal detection only works on "
|
||||
"port 80); open http://%s:%d/ manually, or remove 'port:' to use 80.",
|
||||
config[CONF_PORT],
|
||||
ap_ip,
|
||||
config[CONF_PORT],
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
# Called one after the other rather than via cv.All: these return None.
|
||||
_final_validate_sorting(config)
|
||||
_final_validate_ap_mode(config)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def build_index_html(config: ConfigType) -> str:
|
||||
html = "<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>"
|
||||
css_include = config.get(CONF_CSS_INCLUDE)
|
||||
@@ -434,8 +541,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
with path.open(encoding="utf-8") as js_file:
|
||||
add_resource_as_progmem("JS_INCLUDE", js_file.read())
|
||||
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))
|
||||
if CONF_LOCAL in config and config[CONF_LOCAL]:
|
||||
if serve_local(config, CORE.config.get(CONF_WIFI)):
|
||||
cg.add_define("USE_WEBSERVER_LOCAL")
|
||||
if serve_captive(config, CORE.config):
|
||||
# AP mode: DNS server plus redirect of unknown URLs so phones open the interface
|
||||
cg.add_define("USE_WEBSERVER_CAPTIVE")
|
||||
web_server_base.add_captive_dns_library()
|
||||
if config[CONF_COMPRESSION] == "gzip":
|
||||
cg.add_define("USE_WEBSERVER_GZIP")
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
#include "esphome/components/radio_frequency/radio_frequency.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER_LOCAL
|
||||
#if USE_WEBSERVER_VERSION == 2
|
||||
#include "server_index_v2.h"
|
||||
@@ -334,7 +338,15 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou
|
||||
}
|
||||
#endif
|
||||
|
||||
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {}
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
WebServer *global_web_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#endif
|
||||
|
||||
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
global_web_server = this;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||
void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
|
||||
@@ -380,6 +392,11 @@ void WebServer::setup() {
|
||||
this->base_->add_handler(&this->events_);
|
||||
#endif
|
||||
this->base_->add_handler(this);
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
// Not-found fallback (outside the auth middleware): the OS captive portal probes hit
|
||||
// arbitrary URLs and must get the redirect without credentials.
|
||||
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *request) { this->handle_not_found_(request); });
|
||||
#endif
|
||||
|
||||
// OTA is now handled by the web_server OTA platform
|
||||
|
||||
@@ -395,16 +412,52 @@ void WebServer::setup() {
|
||||
});
|
||||
}
|
||||
void WebServer::loop() {
|
||||
// No SSE clients connected; stop looping until a new client connects via
|
||||
bool keep_looping = this->events_.loop();
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
this->dns_.loop();
|
||||
keep_looping |= this->dns_.is_running();
|
||||
#endif
|
||||
// No SSE clients connected (and no captive DNS to serve); stop looping until a new client connects via
|
||||
// enable_loop_soon_any_context(). This is safe because:
|
||||
// - set_interval/set_timeout/defer run via the Scheduler, independent of loop()
|
||||
// - deferrable_send_state early-outs when no clients are connected
|
||||
// - try_send_nodefer (log, ping) iterates sessions which are empty
|
||||
// - REST API handlers use defer() which runs via the Scheduler
|
||||
if (!this->events_.loop())
|
||||
if (!keep_looping)
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
void WebServer::start_captive() {
|
||||
// CaptiveDNS::start() no-ops too; this guard just avoids repeating the log and enable_loop
|
||||
if (this->dns_.is_running())
|
||||
return;
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
this->dns_.start(ip);
|
||||
this->enable_loop();
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGI(TAG, "AP mode: serving the web interface as captive portal at http://%s/", ip.str_to(ip_buf));
|
||||
}
|
||||
|
||||
void WebServer::end_captive() { this->dns_.stop(); }
|
||||
|
||||
void WebServer::handle_not_found_(AsyncWebServerRequest *request) {
|
||||
// OS captive portal probe (or any other unknown page) while the AP is up: send the browser
|
||||
// to the real page. A redirect rather than the page itself, because the interface resolves
|
||||
// its /events and REST paths relative to the page URL.
|
||||
if (this->dns_.is_running() && request->method() == HTTP_GET) {
|
||||
// Captive mode requires port 80 (enforced at validation), so no port suffix is needed.
|
||||
char location[7 + network::IP_ADDRESS_BUFFER_SIZE + 1];
|
||||
size_t pos = buf_append_str(location, sizeof(location), 0, "http://");
|
||||
wifi::global_wifi_component->wifi_soft_ap_ip().str_to(location + pos);
|
||||
buf_append_str(location, sizeof(location), strlen(location), "/");
|
||||
request->redirect(location);
|
||||
return;
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
(void) level;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
#include "esphome/components/web_server_base/captive_dns.h"
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/controller.h"
|
||||
@@ -276,6 +279,18 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
|
||||
/// Handle an index request under '/'.
|
||||
void handle_index_request(AsyncWebServerRequest *request);
|
||||
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
/** AP mode: run a DNS server that answers every name with the AP address and redirect any
|
||||
* unknown URL to the interface, so a phone joining the AP opens it through the OS captive
|
||||
* portal check. Started and ended by the wifi component with the access point. start may run
|
||||
* before setup() (wifi sets up first): safe because enable_loop() is a no-op before setup;
|
||||
* nothing but the DNS server may be touched, in particular not base_ or the handlers.
|
||||
*/
|
||||
void start_captive();
|
||||
void end_captive();
|
||||
bool is_captive() const { return this->dns_.is_running(); }
|
||||
#endif
|
||||
|
||||
/// Return the webserver configuration as JSON.
|
||||
json::SerializationBuffer<> get_config_json();
|
||||
|
||||
@@ -597,6 +612,10 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
|
||||
#elif USE_ARDUINO
|
||||
DeferredUpdateEventSourceList events_;
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
void handle_not_found_(AsyncWebServerRequest *request);
|
||||
web_server_base::CaptiveDNS dns_;
|
||||
#endif
|
||||
|
||||
#if USE_WEBSERVER_VERSION == 1
|
||||
const char *css_url_{nullptr};
|
||||
@@ -696,5 +715,9 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
extern WebServer *global_web_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#endif
|
||||
|
||||
} // namespace esphome::web_server
|
||||
#endif
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.const import CONF_ID, PlatformFramework
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
@@ -26,6 +27,22 @@ WebServerBase = web_server_base_ns.class_("WebServerBase")
|
||||
CONF_WEB_SERVER_BASE_ID = "web_server_base_id"
|
||||
|
||||
|
||||
def consume_captive_dns_sockets(config: ConfigType, name: str) -> None:
|
||||
"""Register the sockets a captive portal needs on top of the shared HTTP server:
|
||||
1 UDP socket for the DNS server and 3 TCP sockets for the OS captive portal probes,
|
||||
which make several requests that linger in TIME_WAIT."""
|
||||
from esphome.components import socket
|
||||
|
||||
socket.consume_sockets(3, name)(config)
|
||||
socket.consume_sockets(1, name, socket.SocketType.UDP)(config)
|
||||
|
||||
|
||||
def add_captive_dns_library() -> None:
|
||||
"""Pull in the Arduino DNSServer library used by CaptiveDNS off ESP32."""
|
||||
if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2):
|
||||
cg.add_library("DNSServer", None)
|
||||
|
||||
|
||||
def _consume_web_server_base_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register the shared listening socket for the HTTP server.
|
||||
|
||||
@@ -81,3 +98,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_platformio_option("extra_scripts", ["pre:fix_rp2040_hash.py"])
|
||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
|
||||
|
||||
# The DNS server used for captive portals on ESP32; other platforms use the Arduino
|
||||
# DNSServer library. Its source is also guarded by USE_CAPTIVE_PORTAL / USE_WEBSERVER_CAPTIVE.
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"dns_server_esp32_idf.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
// DNS server that answers every name with the access point address, so a phone joining the
|
||||
// AP runs its captive portal check against the device. Shared by captive_portal and the
|
||||
// web_server AP mode; hides the ESP32 (own implementation) vs Arduino (DNSServer library) split.
|
||||
#if defined(USE_CAPTIVE_PORTAL) || defined(USE_WEBSERVER_CAPTIVE)
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#if defined(USE_ESP32)
|
||||
#include "dns_server_esp32_idf.h"
|
||||
#elif defined(USE_ARDUINO)
|
||||
#include <DNSServer.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::web_server_base {
|
||||
|
||||
// The server object only exists while running, so an idle owner (AP not up) pays one pointer.
|
||||
class CaptiveDNS {
|
||||
public:
|
||||
void start(const network::IPAddress &ip) {
|
||||
if (this->dns_server_ != nullptr)
|
||||
return;
|
||||
this->dns_server_ = make_unique<DNSServer>();
|
||||
#if defined(USE_ESP32)
|
||||
this->dns_server_->start(ip);
|
||||
#elif defined(USE_ARDUINO)
|
||||
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
this->dns_server_->start(53, ESPHOME_F("*"), ip);
|
||||
#endif
|
||||
}
|
||||
void stop() {
|
||||
if (this->dns_server_ == nullptr)
|
||||
return;
|
||||
this->dns_server_->stop();
|
||||
this->dns_server_ = nullptr;
|
||||
}
|
||||
/// Answer one pending query; call from the owner's loop() while running.
|
||||
void loop() {
|
||||
if (this->dns_server_ == nullptr)
|
||||
return;
|
||||
#if defined(USE_ESP32)
|
||||
this->dns_server_->process_next_request();
|
||||
#elif defined(USE_ARDUINO)
|
||||
this->dns_server_->processNextRequest();
|
||||
#endif
|
||||
}
|
||||
bool is_running() const { return this->dns_server_ != nullptr; }
|
||||
|
||||
protected:
|
||||
// ESP32: web_server_base::DNSServer from dns_server_esp32_idf.h; Arduino: the library class.
|
||||
std::unique_ptr<DNSServer> dns_server_;
|
||||
};
|
||||
|
||||
} // namespace esphome::web_server_base
|
||||
#endif // USE_CAPTIVE_PORTAL || USE_WEBSERVER_CAPTIVE
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
#include "dns_server_esp32_idf.h"
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32) && (defined(USE_CAPTIVE_PORTAL) || defined(USE_WEBSERVER_CAPTIVE))
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -7,9 +7,9 @@
|
||||
#include <lwip/sockets.h>
|
||||
#include <lwip/inet.h>
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
namespace esphome::web_server_base {
|
||||
|
||||
static const char *const TAG = "captive_portal.dns";
|
||||
static const char *const TAG = "web_server_base.dns";
|
||||
|
||||
// DNS constants
|
||||
static constexpr uint16_t DNS_PORT = 53;
|
||||
@@ -202,6 +202,6 @@ void DNSServer::process_next_request() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::captive_portal
|
||||
} // namespace esphome::web_server_base
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ESP32 && (USE_CAPTIVE_PORTAL || USE_WEBSERVER_CAPTIVE)
|
||||
+8
-4
@@ -1,11 +1,15 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/core/defines.h"
|
||||
// Small DNS server that answers every query with the access point address, so a
|
||||
// phone joining the AP opens the captive portal or web_server page on its own.
|
||||
// Shared by captive_portal and the web_server AP mode.
|
||||
#if defined(USE_ESP32) && (defined(USE_CAPTIVE_PORTAL) || defined(USE_WEBSERVER_CAPTIVE))
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
namespace esphome::web_server_base {
|
||||
|
||||
class DNSServer {
|
||||
public:
|
||||
@@ -27,6 +31,6 @@ class DNSServer {
|
||||
uint8_t buffer_[DNS_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
} // namespace esphome::captive_portal
|
||||
} // namespace esphome::web_server_base
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ESP32 && (USE_CAPTIVE_PORTAL || USE_WEBSERVER_CAPTIVE)
|
||||
@@ -325,9 +325,9 @@ StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) co
|
||||
return StringRef(buffer.data(), decoded_len);
|
||||
}
|
||||
|
||||
void AsyncWebServerRequest::redirect(const std::string &url) {
|
||||
void AsyncWebServerRequest::redirect(const char *url) {
|
||||
httpd_resp_set_status(*this, "302 Found");
|
||||
httpd_resp_set_hdr(*this, "Location", url.c_str());
|
||||
httpd_resp_set_hdr(*this, "Location", url);
|
||||
httpd_resp_set_hdr(*this, "Connection", "close");
|
||||
httpd_resp_send(*this, nullptr, 0);
|
||||
}
|
||||
|
||||
@@ -126,7 +126,8 @@ class AsyncWebServerRequest {
|
||||
void requestAuthentication() const;
|
||||
#endif
|
||||
|
||||
void redirect(const std::string &url);
|
||||
void redirect(const char *url);
|
||||
void redirect(const std::string &url) { this->redirect(url.c_str()); }
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE send(AsyncWebServerResponse *response) {
|
||||
httpd_resp_send(*this, response->get_content_data(), response->get_content_size());
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
#include "esphome/components/captive_portal/captive_portal.h"
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
#include "esphome/components/web_server/web_server.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV
|
||||
#include "esphome/components/esp32_improv/esp32_improv_component.h"
|
||||
@@ -741,9 +744,9 @@ void WiFiComponent::start() {
|
||||
if (captive_portal::global_captive_portal != nullptr) {
|
||||
this->wifi_sta_pre_setup_();
|
||||
this->start_scanning();
|
||||
captive_portal::global_captive_portal->start();
|
||||
}
|
||||
#endif
|
||||
this->start_ap_portal_();
|
||||
#endif // USE_WIFI_AP
|
||||
}
|
||||
#ifdef USE_IMPROV
|
||||
@@ -804,8 +807,8 @@ void WiFiComponent::loop() {
|
||||
this->check_connecting_finished(now);
|
||||
break;
|
||||
}
|
||||
// Use longer cooldown when captive portal/improv is active to avoid disrupting user config
|
||||
bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
|
||||
// Use longer cooldown when a portal/improv is active to avoid disrupting a user on the AP
|
||||
bool portal_active = this->is_ap_portal_active_() || this->is_esp32_improv_active_();
|
||||
uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
|
||||
if (now - this->action_started_ > cooldown_duration) {
|
||||
// After cooldown we either restarted the adapter because of
|
||||
@@ -882,13 +885,11 @@ void WiFiComponent::loop() {
|
||||
ESP_LOGI(TAG, "Starting fallback AP");
|
||||
this->setup_ap_config_();
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (captive_portal::global_captive_portal != nullptr) {
|
||||
// Reset so we force one full scan after captive portal starts
|
||||
// (previous scans were filtered because captive portal wasn't active yet)
|
||||
this->has_completed_scan_after_captive_portal_start_ = false;
|
||||
captive_portal::global_captive_portal->start();
|
||||
}
|
||||
// Reset so we force one full scan after captive portal starts
|
||||
// (previous scans were filtered because captive portal wasn't active yet)
|
||||
this->has_completed_scan_after_captive_portal_start_ = false;
|
||||
#endif
|
||||
this->start_ap_portal_();
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
@@ -1635,10 +1636,8 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
||||
this->num_retried_ = 0;
|
||||
if (this->has_ap()) {
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (this->is_captive_portal_active_()) {
|
||||
captive_portal::global_captive_portal->end();
|
||||
}
|
||||
#ifdef USE_WIFI_AP
|
||||
this->end_ap_portal_();
|
||||
#endif
|
||||
ESP_LOGD(TAG, "Disabling AP");
|
||||
this->wifi_mode_({}, false);
|
||||
@@ -1965,10 +1964,10 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
// Skip actual adapter restart if captive portal/improv is active
|
||||
// Skip actual adapter restart if a portal/improv is active
|
||||
// This allows state machine to reset num_retried_ and trigger fresh scan
|
||||
// without disrupting the captive portal/improv connection
|
||||
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
|
||||
// without disrupting the portal/improv connection
|
||||
if (!this->is_ap_portal_active_() && !this->is_esp32_improv_active_()) {
|
||||
this->restart_adapter();
|
||||
} else {
|
||||
// Even when skipping full restart, disconnect to clear driver state
|
||||
@@ -2227,6 +2226,38 @@ bool WiFiComponent::is_captive_portal_active_() {
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WiFiComponent::is_ap_portal_active_() {
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
if (web_server::global_web_server->is_captive())
|
||||
return true;
|
||||
#endif
|
||||
return this->is_captive_portal_active_();
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
// global_web_server needs no null check: codegen always instantiates WebServer when
|
||||
// USE_WEBSERVER_CAPTIVE is defined, and the constructor assigns the global.
|
||||
void WiFiComponent::start_ap_portal_() {
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (captive_portal::global_captive_portal != nullptr)
|
||||
captive_portal::global_captive_portal->start();
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
web_server::global_web_server->start_captive();
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiComponent::end_ap_portal_() {
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (this->is_captive_portal_active_())
|
||||
captive_portal::global_captive_portal->end();
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_CAPTIVE
|
||||
web_server::global_web_server->end_captive();
|
||||
#endif
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
bool WiFiComponent::is_esp32_improv_active_() {
|
||||
#ifdef USE_IMPROV
|
||||
return esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user