Compare commits

..

4 Commits

Author SHA1 Message Date
J. Nick Koston 1a55b74357 Merge branch 'dev' into web-server-logs 2026-06-30 14:02:44 -05:00
J. Nick Koston bac3de4f93 Merge remote-tracking branch 'origin/dev' into web-server-logs
# Conflicts:
#	esphome/__main__.py
#	tests/unit_tests/test_main.py
2026-06-21 15:03:00 -05:00
J. Nick Koston a97f9e7cda [core] Add esphome logs over web_server HTTP SSE
Stream device logs over the web_server /events Server-Sent Events feed so
'esphome logs' works on devices that have web_server: but no api:. This is
the logging counterpart to web_server OTA. Priority stays API, then MQTT,
then web_server. Reconnects automatically when the stream drops.

Factor the resolve-to-URLs step and the web_server port/auth lookup shared
with web_server OTA into a new web_server_helpers module (resolve_web_server_urls
and get_web_server_connection), with helpers.format_ip_url for IPv4/IPv6 URL
formatting, and broaden the missing-transport log error to suggest web_server:
alongside api:/MQTT/USB.
2026-06-21 10:50:49 -05:00
J. Nick Koston c826293efc [core] Clarify resolve error when a device has no network log/OTA transport
A device with a static IP, mDNS disabled, and no api: component failed
logs with "All specified devices ['OTA'] could not be resolved" and a
hint to set use_address; the hint is misleading since the static IP
already resolves, the real gap is that network logs ride the native API.

Name the missing transport instead: api: for logs, an ota: platform for
uploads. The generic "could not be resolved" message stays for a
genuinely unreachable address.
2026-06-21 09:48:27 -05:00
689 changed files with 5151 additions and 14516 deletions
-2
View File
@@ -1,5 +1,3 @@
# Normalize line endings to LF in the repository
* text eol=lf
*.png binary
*.gif binary
*.apng binary
+2 -2
View File
@@ -42,7 +42,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -67,7 +67,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
-49
View File
@@ -1,49 +0,0 @@
name: Cache nRF Connect SDK
description: >
Resolve the pinned sdk-nrf version and cache the native sdk-nrf install
(west workspace, Zephyr SDK toolchain, python env) at ~/.esphome-sdk-nrf.
Every job that installs sdk-nrf natively (the nrf52 clang-tidy job and,
once the component tests build natively, their batches) shares one cache.
Callers must set env ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf and have
the Python venv already restored.
inputs:
restore-only:
description: >
When "true", only restore -- never save the cache, even on dev. Use from
jobs that may not produce a complete install (e.g. a component batch
that fails mid-install), so a partial install is never written.
default: "false"
runs:
using: composite
steps:
- name: Resolve sdk-nrf and toolchain versions for cache key
# Both versions are pinned in code, not in any file that feeds the
# other cache keys, so resolve them explicitly. Keying on them means
# the cache invalidates when either is bumped (actions/cache never
# overwrites a key).
id: version
shell: bash
run: |
. venv/bin/activate
version=$(python -c '
from esphome.components.nrf52 import RECOMMENDED_SDK_NRF_VERSION
from esphome.components.nrf52.framework import TOOLCHAIN_VERSION
print(f"{RECOMMENDED_SDK_NRF_VERSION}-{TOOLCHAIN_VERSION}")')
echo "version=$version" >> "$GITHUB_OUTPUT"
# Mirror cache-esp-idf: only dev-branch runs write the shared cache (so it
# lives in the default-branch scope readable by all PRs); PRs are
# restore-only and never push multi-GB artifacts into their own scope.
- name: Cache nRF Connect SDK install (write on dev)
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.esphome-sdk-nrf
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
- name: Cache nRF Connect SDK install (restore-only off dev)
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.esphome-sdk-nrf
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
+1 -4
View File
@@ -32,12 +32,9 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
# reuse; dev pushes seed the shared copy instead.
save-cache: ${{ github.event_name != 'pull_request' }}
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
+1 -4
View File
@@ -29,12 +29,9 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
# would only consume quota.
save-cache: "false"
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
+4 -4
View File
@@ -67,7 +67,7 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Determine tag and whether to push
id: tag
@@ -96,7 +96,7 @@ jobs:
- name: Log in to the GitHub container registry
if: steps.tag.outputs.push == 'true'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -151,10 +151,10 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to the GitHub container registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+18 -90
View File
@@ -49,12 +49,9 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
# reuse; dev pushes seed the shared copy instead.
save-cache: ${{ github.event_name != 'pull_request' }}
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
@@ -115,7 +112,7 @@ jobs:
script/build_codeowners.py --check
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2-boards.py --check
script/generate-rp2040-boards.py --check
script/ci_check_duplicate_test_ids.py
import-time:
@@ -174,12 +171,9 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
# reuse; dev pushes seed the shared copy instead.
save-cache: ${{ github.event_name != 'pull_request' }}
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
@@ -378,12 +372,9 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
# reuse; dev pushes seed the shared copy instead.
save-cache: ${{ github.event_name != 'pull_request' }}
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
@@ -465,7 +456,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5
uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1
with:
run: |
. venv/bin/activate
@@ -484,8 +475,6 @@ jobs:
GH_TOKEN: ${{ github.token }}
# esp32-arduino-tidy installs ESP-IDF natively; share the native IDF cache.
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
# nrf52-tidy installs sdk-nrf natively; pin it to a cacheable path.
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
strategy:
fail-fast: false
max-parallel: 2
@@ -502,7 +491,7 @@ jobs:
- id: clang-tidy
name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
cache_sdk_nrf: true
pio_cache_key: tidy-zephyr
ignore_errors: false
steps:
@@ -538,10 +527,6 @@ jobs:
with:
framework: arduino
- name: Cache nRF Connect SDK install
if: matrix.cache_sdk_nrf
uses: ./.github/actions/cache-sdk-nrf
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
@@ -810,7 +795,7 @@ jobs:
if: always()
test-build-components-split:
name: Test components batch (${{ matrix.batch.components }})
name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04
needs:
- common
@@ -820,14 +805,11 @@ jobs:
# esp32 component builds use the native ESP-IDF toolchain (default), so
# share the tidy jobs' install location -- the restore below lands here.
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
# nrf52 component builds install sdk-nrf natively; pin it to the shared
# cacheable path so the restore below lands where the build looks.
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
strategy:
fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix:
batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
steps:
- name: Show disk space
run: |
@@ -835,14 +817,13 @@ jobs:
df -h
- name: List components
run: echo ${{ matrix.batch.components }}
run: echo ${{ matrix.components }}
- name: Install apt packages
# Not cached: this job is pull-request-only, so a cache save could
# never be shared and would only consume quota.
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libsdl2-dev ccache
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: libsdl2-dev ccache
version: 1.1
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -852,21 +833,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install (restore-only)
# Only batches whose test platforms include esp32 need the native
# ESP-IDF install; never save -- just reuse the shared install the
# dev tidy jobs already cached when present.
if: matrix.batch.needs_idf
# A batch may contain no esp32 build, so never save -- just reuse the
# shared install the dev tidy jobs already cached when present.
uses: ./.github/actions/cache-esp-idf
with:
restore-only: true
- name: Cache nRF Connect SDK install (restore-only)
# Only batches whose test platforms include nrf52 need the native
# sdk-nrf install; never save -- just reuse the shared install the
# dev nrf52 tidy job cached when present.
if: matrix.batch.needs_nrf
uses: ./.github/actions/cache-sdk-nrf
with:
restore-only: true
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
@@ -897,7 +868,7 @@ jobs:
fi
# Convert space-separated components to comma-separated for Python script
components_csv=$(echo "${{ matrix.batch.components }}" | tr ' ' ',')
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
@@ -1016,36 +987,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
pre-commit-seed-cache:
name: Seed pre-commit cache
runs-on: ubuntu-latest
needs:
- common
# Saves a dev-scoped pre-commit cache that pull request runs can
# restore, since pre-commit.ci lite itself never runs on dev pushes.
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache pre-commit environments
id: cache-pre-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pre-commit
# Must match the restore key in pre-commit-ci-lite
# yamllint disable-line rule:line-length
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
- name: Install pre-commit hook environments
if: steps.cache-pre-commit.outputs.cache-hit != 'true'
run: |
python -m pip install pre-commit
pre-commit install-hooks
pre-commit-ci-lite:
name: pre-commit.ci lite
runs-on: ubuntu-latest
@@ -1061,22 +1002,9 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
# Inlined from esphome/pre-commit-action with a restore-only cache
# step: the pre-commit-seed-cache job owns saving this cache, so
# pull request runs never write per-PR copies.
- name: Restore pre-commit cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pre-commit
# Must match the key pre-commit-seed-cache saves
# yamllint disable-line rule:line-length
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env:
SKIP: pylint,ci-custom
run: |
python -m pip install pre-commit
pre-commit run --show-diff-on-failure --color=always --all-files
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"
+6 -6
View File
@@ -99,15 +99,15 @@ jobs:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to docker hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -178,17 +178,17 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``pre-commit`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+4 -5
View File
@@ -427,14 +427,13 @@ This document provides essential context for AI models interacting with this pro
When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml` changes, and the component isn't pulled in as a dependency of another changed component), CI skips the compile stage for that component entirely and only runs config validation. This is decided in `script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the `validate_only_components` output that the `test-build-components-split` job consumes.
* **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`.
All includes in test files must go through dict-style `packages:` so that batch grouping works correctly — the grouping scripts only understand dict-style packages. Never use list-style packages (`packages: [- !include ...]`) or top-level merge keys (`<<: !include common.yaml`). Bus packages are keyed by the bus name; the component's `common.yaml` is keyed by the component name (e.g. `cst328: !include common.yaml`):
* **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`:
```yaml
# test.esp32-idf.yaml — everything included via named packages
# test.esp32-idf.yaml — use packages for buses
packages:
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
my_component: !include common.yaml
<<: !include common.yaml
```
```yaml
# common.yaml — component config only, NO bus definitions
+1 -6
View File
@@ -122,7 +122,6 @@ esphome/components/cover/* @esphome/core
esphome/components/cs5460a/* @balrog-kun
esphome/components/cse7761/* @berfenger
esphome/components/cst226/* @clydebarrow
esphome/components/cst328/* @latonita
esphome/components/cst816/* @clydebarrow
esphome/components/cst9220/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
@@ -187,7 +186,6 @@ esphome/components/ezo_pmp/* @carlos-sarmiento
esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/file/* @esphome/core
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/fs3000/* @kahrendt
@@ -209,7 +207,6 @@ esphome/components/gree/switch/* @nagyrobi
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
esphome/components/gsl3670/* @clydebarrow
esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
@@ -406,7 +403,6 @@ esphome/components/pn7160_i2c/* @jesserockz @kbx81
esphome/components/pn7160_spi/* @jesserockz @kbx81
esphome/components/power_supply/* @esphome/core
esphome/components/preferences/* @esphome/core
esphome/components/provisioning/* @esphome/core
esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
esphome/components/pvvx_mithermometer/* @pasiz
@@ -429,7 +425,7 @@ esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/ring_buffer/* @kahrendt
esphome/components/router/speaker/* @kahrendt
esphome/components/rp2/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_ble/* @bdraco
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz
@@ -505,7 +501,6 @@ esphome/components/ssd1331_base/* @kbx81
esphome/components/ssd1331_spi/* @kbx81
esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7123/* @miniskipper
esphome/components/st7567_base/* @latonita
esphome/components/st7567_i2c/* @latonita
esphome/components/st7567_spi/* @latonita
+1 -1
View File
@@ -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.8.0-dev
PROJECT_NUMBER = 2026.7.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
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.3.1
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.22
RUN \
platformio settings set enable_telemetry No \
+1 -2
View File
@@ -21,10 +21,9 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
# Keep the native toolchain installs on the persistent cache root, not the
# Keep the native ESP-IDF install on the persistent cache root, not the
# container's ephemeral user cache dir (re-downloaded on every restart).
export ESPHOME_ESP_IDF_PREFIX="$(dirname "${pio_cache_base}")/idf"
export ESPHOME_SDK_NRF_PREFIX="$(dirname "${pio_cache_base}")/sdk-nrf"
# If /build is mounted, use that as the build path
# otherwise use path in /config (so that builds aren't lost on container restart)
@@ -15,10 +15,9 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
# Keep the native toolchain installs on the persistent /data volume, not the
# Keep the native ESP-IDF install on the persistent /data volume, not the
# container's ephemeral user cache dir (wiped on every add-on update/restart).
export ESPHOME_ESP_IDF_PREFIX=/data/cache/idf
export ESPHOME_SDK_NRF_PREFIX=/data/cache/sdk-nrf
if bashio::config.true 'leave_front_door_open'; then
export DISABLE_HA_AUTHENTICATION=true
+58 -37
View File
@@ -28,13 +28,13 @@ from esphome.const import (
ALLOWED_NAME_CHARS,
ARGUMENT_HELP_DEVICE,
CONF_API,
CONF_AUTH,
CONF_BAUD_RATE,
CONF_BROKER,
CONF_DEASSERT_RTS_DTR,
CONF_DISABLED,
CONF_ESPHOME,
CONF_LEVEL,
CONF_LOG,
CONF_LOG_TOPIC,
CONF_LOGGER,
CONF_MDNS,
@@ -48,7 +48,7 @@ from esphome.const import (
CONF_PORT,
CONF_SUBSTITUTIONS,
CONF_TOPIC,
CONF_USERNAME,
CONF_VERSION,
CONF_WEB_SERVER,
CONF_WIFI,
ENV_NOGITIGNORE,
@@ -225,9 +225,8 @@ def _discover_mac_suffix_devices() -> list[str] | None:
Returns:
- ``None`` when discovery isn't applicable (``name_add_mac_suffix`` off,
mDNS disabled, or ``CORE.address`` isn't a ``.local`` mDNS address).
Callers should then fall back to whatever default OTA address they
normally use.
mDNS disabled, or ``CORE.address`` is already an IP). Callers should
then fall back to whatever default OTA address they normally use.
- ``[]`` when discovery ran but found nothing. Callers should NOT fall
back to the base name: with ``name_add_mac_suffix`` enabled, the base
name by definition doesn't exist on the network.
@@ -237,7 +236,7 @@ def _discover_mac_suffix_devices() -> list[str] | None:
``aioesphomeapi`` via :func:`_resolve_network_devices`) reuses the IPs we
already have without opening a second Zeroconf client.
"""
if not (has_name_add_mac_suffix() and has_mdns() and has_mdns_address()):
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
return None
from esphome.zeroconf import discover_mdns_devices
@@ -276,8 +275,8 @@ def _unresolved_default_error(purpose: Purpose, defaults: list[str]) -> str:
if purpose == Purpose.LOGGING and not has_api():
return (
"Cannot view logs over the network: no 'api:' component is "
"configured. Network log streaming requires the native API; add "
"an 'api:' component, enable MQTT logging, or view logs over USB."
"configured. Add an 'api:' component, enable MQTT logging, add a "
"'web_server:' component, or view logs over USB."
)
if purpose == Purpose.UPLOADING and not has_ota():
return (
@@ -317,9 +316,12 @@ def choose_upload_log_host(
]
resolved.append(choose_prompt(options, purpose=purpose))
elif device == "OTA":
# Logs can stream over a network transport via the native API
# or the web_server HTTP SSE feed.
network_logging = has_api() or has_web_server_logging()
# ensure IP adresses are used first
if is_ip_address(CORE.address) and (
(purpose == Purpose.LOGGING and has_api())
(purpose == Purpose.LOGGING and network_logging)
or (purpose == Purpose.UPLOADING and has_ota())
):
resolved.extend(_resolve_with_cache(CORE.address, purpose))
@@ -331,7 +333,11 @@ def choose_upload_log_host(
if has_mqtt_logging():
resolved.append("MQTT")
if has_api() and has_non_ip_address() and has_resolvable_address():
if (
network_logging
and has_non_ip_address()
and has_resolvable_address()
):
resolved.extend(_ota_hostnames_for_default(purpose))
elif purpose == Purpose.UPLOADING:
@@ -355,7 +361,7 @@ def choose_upload_log_host(
bootsel_permission_error = False
if (
purpose == Purpose.UPLOADING
and CORE.is_rp2
and CORE.is_rp2040
and (picotool := _find_picotool()) is not None
):
bootsel = detect_rp2040_bootsel(picotool)
@@ -393,7 +399,7 @@ def choose_upload_log_host(
mqtt_config = CORE.config[CONF_MQTT]
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if has_api():
if has_api() or has_web_server_logging():
add_ota_options()
elif purpose == Purpose.UPLOADING and has_ota():
@@ -402,7 +408,7 @@ def choose_upload_log_host(
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
if (
purpose == Purpose.UPLOADING
and CORE.is_rp2
and CORE.is_rp2040
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
):
if bootsel_permission_error:
@@ -486,6 +492,21 @@ def has_web_server_ota() -> bool:
)
def has_web_server_logging() -> bool:
"""Check if logs can be streamed over the web_server HTTP SSE endpoint.
The ``web_server`` component exposes a ``/events`` Server-Sent Events
stream that carries ``event: log`` frames. This requires version 2+ (the
v1 UI has no ``/events`` endpoint) and the ``log`` option enabled (default).
"""
web_conf = CORE.config.get(CONF_WEB_SERVER)
if web_conf is None:
return False
if web_conf.get(CONF_VERSION, 2) == 1:
return False
return web_conf.get(CONF_LOG, True)
def has_mqtt_ip_lookup() -> bool:
"""Check if MQTT is available and IP lookup is supported."""
from esphome.components.mqtt import CONF_DISCOVER_IP
@@ -504,22 +525,17 @@ def has_mdns() -> bool:
def has_non_ip_address() -> bool:
"""Check if ``CORE.address`` is set and is not an IP address."""
"""Check if CORE.address is set and is not an IP address."""
return CORE.address is not None and not is_ip_address(CORE.address)
def has_mdns_address() -> bool:
"""Check if ``CORE.address`` is a ``.local`` mDNS hostname."""
return CORE.address is not None and CORE.address.endswith(".local")
def has_ip_address() -> bool:
"""Check if ``CORE.address`` is a valid IP address."""
"""Check if CORE.address is a valid IP address."""
return CORE.address is not None and is_ip_address(CORE.address)
def has_resolvable_address() -> bool:
"""Check if ``CORE.address`` is resolvable (via mDNS, DNS, or is an IP address)."""
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
if CORE.address is None:
@@ -538,7 +554,7 @@ def has_resolvable_address() -> bool:
return True
# .local mDNS hostnames are only resolvable if mDNS is enabled
return not has_mdns_address()
return not CORE.address.endswith(".local")
def has_name_add_mac_suffix() -> bool:
@@ -985,7 +1001,7 @@ def upload_using_platformio(config: ConfigType, port: str) -> int:
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
# the upload target, but 'nobuild' skips the build phase that creates it.
# Create it here so the upload doesn't fail.
if CORE.is_rp2:
if CORE.is_rp2040:
idedata = toolchain.get_idedata(config)
build_dir = Path(idedata.firmware_elf_path).parent
firmware_bin = build_dir / "firmware.bin"
@@ -1173,7 +1189,7 @@ def upload_program(
if CORE.is_esp32 or CORE.is_esp8266:
file = getattr(args, "file", None)
exit_code = upload_using_esptool(config, host, file, args.upload_speed)
elif CORE.is_rp2 or CORE.is_libretiny:
elif CORE.is_rp2040 or CORE.is_libretiny:
exit_code = upload_using_platformio(config, host)
# else: Unknown target platform, exit_code remains 1
@@ -1291,25 +1307,23 @@ def _upload_via_native_api(
def _upload_via_web_server(
config: ConfigType, network_devices: list[str], binary: Path
) -> tuple[int, str | None]:
web_conf = config.get(CONF_WEB_SERVER)
if not web_conf:
raise EsphomeError(
f"Cannot upload via web_server OTA: the {CONF_WEB_SERVER} component "
f"is not configured."
)
remote_port = int(web_conf[CONF_PORT])
auth = web_conf.get(CONF_AUTH) or {}
username = auth.get(CONF_USERNAME)
password = auth.get(CONF_PASSWORD)
from esphome import web_server_ota
from esphome.web_server_helpers import get_web_server_connection
remote_port, username, password = get_web_server_connection(config)
return web_server_ota.run_ota(
network_devices, remote_port, username, password, binary
)
def _show_logs_via_web_server(config: ConfigType, network_devices: list[str]) -> int:
from esphome import web_server_logs
from esphome.web_server_helpers import get_web_server_connection
port, username, password = get_web_server_connection(config)
return web_server_logs.run_logs(network_devices, port, username, password)
# Layout of esp_partition_info_t on flash. Each entry is 32 bytes, leading with a
# 16-bit little-endian magic. ESP-IDF defines ESP_PARTITION_MAGIC = 0x50AA (stored as
# bytes 0xAA, 0x50) for partition entries and ESP_PARTITION_MAGIC_MD5 = 0xEBEB for the
@@ -1438,6 +1452,13 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
config, args.topic, args.username, args.password, args.client_id
)
# Fall back to the web_server HTTP SSE log stream for devices that have
# web_server: but no api: (the logging counterpart to web_server OTA).
if has_web_server_logging() and (
network_devices := _resolve_network_devices(devices, config, args)
):
return _show_logs_via_web_server(config, network_devices)
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
@@ -1647,7 +1668,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
# After BOOTSEL upload, wait for a new serial port to appear
# so it shows up in the log chooser
if successful_device is None and CORE.is_rp2:
if successful_device is None and CORE.is_rp2040:
_wait_for_serial_port(known_ports=pre_upload_ports)
# If exactly one new serial port appeared, use it directly
serial_ports = get_serial_ports()
+10 -34
View File
@@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266.
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from dataclasses import dataclass
import logging
from pathlib import Path
import re
@@ -65,7 +65,6 @@ class RamSymbol:
size: int
section: str
demangled: str = "" # Demangled name, set after batch demangling
aliases: list[str] = field(default_factory=list) # Other names at same address
class RamStringsAnalyzer:
@@ -236,11 +235,6 @@ class RamStringsAnalyzer:
except (subprocess.CalledProcessError, FileNotFoundError):
return
# Track symbols by address so aliases (multiple names for the same
# object, e.g. the newlib __lock___* mutexes that all alias one
# StaticSemaphore_t) are reported once instead of once per name.
symbols_by_addr: dict[int, RamSymbol] = {}
for line in output.split("\n"):
parts = line.split()
if len(parts) < 4:
@@ -259,18 +253,6 @@ class RamStringsAnalyzer:
if sym_type not in DATA_SYMBOL_TYPES:
continue
if (existing := symbols_by_addr.get(addr)) is not None:
# Prefer a global (uppercase type) name as the primary so
# nm output order can't hide it behind a local alias.
if sym_type.isupper() and existing.sym_type.islower():
existing.aliases.append(existing.name)
existing.name = name
existing.sym_type = sym_type
else:
existing.aliases.append(name)
existing.size = max(existing.size, size)
continue
# Check if symbol is in a RAM section
for section_name in self.ram_sections:
if section_name not in self.sections:
@@ -278,15 +260,15 @@ class RamStringsAnalyzer:
section = self.sections[section_name]
if section.address <= addr < section.address + section.size:
symbol = RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
self.ram_symbols.append(
RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
)
)
symbols_by_addr[addr] = symbol
self.ram_symbols.append(symbol)
break
def _demangle_symbols(self) -> None:
@@ -454,13 +436,7 @@ class RamStringsAnalyzer:
for symbol in largest_symbols:
# Use demangled name if available, otherwise raw name
display_name = symbol.demangled or symbol.name
# Truncate the name, not the alias note, so merged aliases stay
# visible even for long demangled C++ names.
alias_note = f" (+{len(symbol.aliases)} aliases)" if symbol.aliases else ""
max_name_len = 49 - len(alias_note)
if len(display_name) > max_name_len:
display_name = display_name[:max_name_len]
name_display = display_name + alias_note
name_display = display_name[:49] if len(display_name) > 49 else display_name
lines.append(
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
)
+1 -15
View File
@@ -6,11 +6,7 @@ from pathlib import Path
from esphome.components.esp32 import get_esp32_variant, idf_version
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.framework_helpers import (
get_project_compile_flags,
get_project_cxx_compile_flags,
get_project_link_flags,
)
from esphome.framework_helpers import get_project_compile_flags, get_project_link_flags
from esphome.helpers import mkdir_p, write_file_if_changed
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
@@ -95,14 +91,6 @@ def get_project_cmakelists(minimal: bool = False) -> str:
for flag in project_compile_opts
)
# Flags registered via cg.add_cxx_build_flag() go on CXX_COMPILE_OPTIONS
# (not COMPILE_OPTIONS) because GCC warns when a C++-only flag such as
# -Wno-volatile is passed on a C compile.
cxx_compile_options = "\n".join(
f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)'
for flag in get_project_cxx_compile_flags()
)
cpp_standard_options = (
CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
if CORE.cpp_standard
@@ -167,8 +155,6 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
{cpp_standard_options}
{cxx_compile_options}
{extra_compile_options}
{managed_components_property}
+3 -2
View File
@@ -108,6 +108,7 @@ Import("env")
def write_cxx_flags_script() -> None:
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
contents = CXX_FLAGS_FILE_CONTENTS
for flag in sorted(CORE.cxx_build_flags):
contents += f'env.Append(CXXFLAGS=["{flag}"])\n'
if not CORE.is_host:
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
contents += "\n"
write_file_if_changed(path, contents)
-1
View File
@@ -25,7 +25,6 @@ from esphome.cpp_generator import ( # noqa: F401
add,
add_build_flag,
add_build_unflag,
add_cxx_build_flag,
add_define,
add_global,
add_library,
-6
View File
@@ -1,6 +0,0 @@
# Importing `esphome.loader` here installs the component-alias
# ``sys.meta_path`` finder before any submodule lookup runs. Without this,
# `from esphome.components import <legacy_alias>` from a fresh interpreter
# can race the finder install and raise ImportError, since the legacy
# alias dir no longer exists on disk.
from esphome import loader as _loader # noqa: F401
+4 -4
View File
@@ -227,12 +227,12 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
def validate_adc_pin(value):
if str(value).upper() == "VCC":
if CORE.is_rp2:
if CORE.is_rp2040:
return pins.internal_gpio_input_pin_schema(29)
return cv.only_on([PLATFORM_ESP8266])("VCC")
if str(value).upper() == "TEMPERATURE":
return cv.only_on_rp2("TEMPERATURE")
return cv.only_on_rp2040("TEMPERATURE")
if CORE.is_esp32:
conf = pins.internal_gpio_input_pin_schema(value)
@@ -261,11 +261,11 @@ def validate_adc_pin(value):
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
return conf
if CORE.is_rp2:
if CORE.is_rp2040:
conf = pins.internal_gpio_input_pin_schema(value)
number = conf[CONF_NUMBER]
if number not in (26, 27, 28, 29):
raise cv.Invalid("RP2: Only pins 26, 27, 28 and 29 support ADC")
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
return conf
if CORE.is_libretiny:
+4 -4
View File
@@ -123,9 +123,9 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32
#ifdef USE_RP2
#ifdef USE_RP2040
void set_is_temperature() { this->is_temperature_ = true; }
#endif // USE_RP2
#endif // USE_RP2040
protected:
uint8_t sample_count_{1};
@@ -152,9 +152,9 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
static adc_oneshot_unit_handle_t shared_adc_handles[2];
#endif // USE_ESP32
#ifdef USE_RP2
#ifdef USE_RP2040
bool is_temperature_{false};
#endif // USE_RP2
#endif // USE_RP2040
#ifdef USE_ZEPHYR
const struct adc_dt_spec *channel_ = nullptr;
@@ -1,4 +1,4 @@
#ifdef USE_RP2
#ifdef USE_RP2040
#include "adc_sensor.h"
#include "esphome/core/log.h"
@@ -17,7 +17,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.rp2";
static const char *const TAG = "adc.rp2040";
void ADCSensor::setup() {
static bool initialized = false;
@@ -102,4 +102,4 @@ float ADCSensor::sample() {
} // namespace esphome::adc
#endif // USE_RP2
#endif // USE_RP2040
+1 -1
View File
@@ -201,7 +201,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_IDF,
},
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"adc_sensor_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"adc_sensor_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
+98 -20
View File
@@ -1,36 +1,114 @@
# ---------------------------------------------------------------------------
# Legacy top-level `animation:` deprecation shim -- REMOVE this whole file after
# 2027.1.0.
#
# Animations are now a platform of the `image:` component (`platform:
# animation`); the real schema, actions and codegen live in `image.py`. This
# module only keeps the deprecated top-level `animation:` key working during the
# deprecation window: it reuses that schema/codegen and adds a one-shot
# deprecation warning (with a pasteable migrated `image:` block) at validation
# time. Deleting this file drops the top-level form entirely.
# ---------------------------------------------------------------------------
import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
import esphome.components.image as espImage
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
from .image import ANIMATION_CONFIG_SCHEMA, setup_animation
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["image", "file"]
AUTO_LOAD = ["image"]
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
DOMAIN = "animation"
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
LEGACY_REMOVAL_VERSION = "2027.1.0"
animation_ns = cg.esphome_ns.namespace("animation")
_capture_legacy_entry, _warn_legacy_animation = (
espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION)
Animation_ = animation_ns.class_("Animation", espImage.Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ANIMATION_CONFIG_SCHEMA)
CONFIG_SCHEMA = cv.All(
espImage.IMAGE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
},
),
espImage.validate_settings,
)
FINAL_VALIDATE_SCHEMA = _warn_legacy_animation
to_code = setup_animation
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (frame := config.get(CONF_FRAME)) is not None:
template_ = await cg.templatable(frame, args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def to_code(config):
(
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await espImage.write_image(config, all_frames=True)
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
height,
frame_count,
image_type,
trans_value,
)
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))
-115
View File
@@ -1,115 +0,0 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
from esphome.components.file.image import image_schema, write_image
from esphome.components.image import Image_, validate_settings
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
from esphome.types import ConfigType
CODEOWNERS = ["@syndlex"]
AUTO_LOAD = ["file"]
DEPENDENCIES = ["display"]
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
animation_ns = cg.esphome_ns.namespace("animation")
Animation_ = animation_ns.class_("Animation", Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
ANIMATION_SCHEMA = image_schema(Animation_).extend(
{
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
},
)
# Shared schema used by both the (deprecated) top-level `animation:` key and the
# `image:` `platform: animation` entry.
ANIMATION_CONFIG_SCHEMA = cv.All(ANIMATION_SCHEMA, validate_settings)
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (frame := config.get(CONF_FRAME)) is not None:
template_ = await cg.templatable(frame, args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def setup_animation(config: ConfigType) -> None:
(
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await write_image(config, all_frames=True)
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
height,
frame_count,
image_type,
trans_value,
)
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))
CONFIG_SCHEMA = ANIMATION_CONFIG_SCHEMA
to_code = setup_animation
+7 -33
View File
@@ -112,23 +112,6 @@ CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
def _register_provisioning_source(config: ConfigType) -> ConfigType:
"""Register the API as a provisioning source when encryption is enabled.
With no ``key`` the device boots unprovisioned and is set up on first
connection; a YAML ``key`` means it is born provisioned. Either way the API
drives the provisioning manager, so it counts as a source for `provisioning:`.
A hardcoded ``key`` is reported so `provisioning:` can warn about it.
"""
if (encryption := config.get(CONF_ENCRYPTION)) is not None:
from esphome.components import provisioning
provisioning.register_source("api")
if CONF_KEY in encryption:
provisioning.report_hardcoded_credentials("api")
return config
def validate_encryption_key(value):
value = cv.string_strict(value)
try:
@@ -317,7 +300,7 @@ CONFIG_SCHEMA = cv.All(
CONF_LISTEN_BACKLOG,
esp8266=1, # Limited RAM (~40KB free), LWIP raw sockets
esp32=4, # More RAM (520KB), BSD sockets
rp2=1, # Limited RAM (264KB), LWIP raw sockets like ESP8266
rp2040=1, # Limited RAM (264KB), LWIP raw sockets like ESP8266
bk72xx=4, # Moderate RAM, BSD-style sockets
rtl87xx=4, # Moderate RAM, BSD-style sockets
host=4, # Abundant resources
@@ -328,7 +311,7 @@ CONFIG_SCHEMA = cv.All(
CONF_MAX_CONNECTIONS,
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
esp32=5, # 520KB RAM available
rp2=4, # 264KB RAM but LWIP constraints
rp2040=4, # 264KB RAM but LWIP constraints
bk72xx=5, # Moderate RAM
rtl87xx=5, # Moderate RAM
host=8, # Abundant resources
@@ -343,7 +326,7 @@ CONFIG_SCHEMA = cv.All(
CONF_MAX_SEND_QUEUE,
esp8266=4, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2=8, # Moderate RAM
rp2040=8, # Moderate RAM
bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
@@ -354,7 +337,6 @@ CONFIG_SCHEMA = cv.All(
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_consume_api_sockets,
_register_provisioning_source,
)
@@ -488,11 +470,8 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
else:
# No key provided, but encryption desired
# Until a key is set, the device accepts both Noise connections
# using the well-known all-zeros PSK (preferred: the key travels
# encrypted, protecting against passive sniffing) and plaintext
# connections (deprecated, remove after 2027.2.0) so a client can
# provide a noise key and the device then switches to noise only.
# This will allow a plaintext client to provide a noise key,
# send it to the device, and then switch to noise.
# The key will be saved in flash and used for future connections
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
@@ -561,20 +540,17 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
)
# synchronous=False: when on_success/on_error is configured, play() stores the
# trigger args until the HomeassistantActionResponse arrives, so non-owning args
# (StringRef into the API receive buffer) must not be used.
@automation.register_action(
"homeassistant.action",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=False,
synchronous=True,
)
@automation.register_action(
"homeassistant.service",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=False,
synchronous=True,
)
async def homeassistant_service_to_code(
config: ConfigType,
@@ -668,8 +644,6 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
)
# synchronous=True is safe here: the event schema has no on_success/on_error,
# so play() never stores the trigger args.
@automation.register_action(
"homeassistant.event",
HomeAssistantServiceCallAction,
-19
View File
@@ -158,16 +158,6 @@ message AuthenticationResponse {
bool invalid_password = 1;
}
// Reason a party is requesting the connection be closed.
enum DisconnectReason {
// No specific reason / not provided (default for older peers).
DISCONNECT_REASON_UNSPECIFIED = 0;
// The device's provisioning window has expired. The device must be reset
// (power-cycled) to reopen the provisioning window before it will accept a
// connection again.
DISCONNECT_REASON_PROVISIONING_CLOSED = 1;
}
// Request to close the connection.
// Can be sent by both the client and server
message DisconnectRequest {
@@ -176,10 +166,6 @@ message DisconnectRequest {
option (no_delay) = true;
// Do not close the connection before the acknowledgement arrives
// Optional reason the connection is being closed. Older peers that do not
// send this field will report DISCONNECT_REASON_UNSPECIFIED (0).
DisconnectReason reason = 1;
}
message DisconnectResponse {
@@ -310,11 +296,6 @@ message DeviceInfoResponse {
// Serial proxy instance metadata
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
// Device is unprovisioned and accepts Noise handshakes with the well-known
// all-zeros PSK, so the api encryption key can be provisioned without being
// sent in plaintext (protects against passive sniffing, not active MITM)
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
}
message ListEntitiesRequest {
+2 -77
View File
@@ -25,9 +25,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/version.h"
#ifdef USE_PROVISIONING
#include "esphome/components/provisioning/provisioning.h"
#endif
#ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h"
@@ -198,29 +195,6 @@ APIConnection::~APIConnection() {
#endif
}
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
void APIConnection::upgrade_helper_to_noise_() {
// The client opened with a Noise hello while this device has no encryption
// key set. Replace the plaintext helper with a Noise helper so the key can
// be provisioned over an encrypted channel: the noise context PSK is all
// zeros when unprovisioned, and NNpsk0 still runs a fresh ephemeral X25519
// exchange, so a passive listener cannot read the session. A publicly known
// PSK authenticates nobody; this protects against sniffing only.
auto *plaintext = static_cast<APIPlaintextFrameHelper *>(this->helper_.get());
uint8_t header[3];
uint8_t header_len = plaintext->get_consumed_header(header);
auto *noise = new APINoiseFrameHelper(plaintext->release_socket_for_switch(), this->parent_->get_noise_ctx());
// Carry over the peername-based client name (Hello has not arrived yet)
const char *name = plaintext->get_client_name();
noise->set_client_name(name, strlen(name));
this->helper_.reset(noise); // destroys the plaintext helper
APIError err = noise->init_from_handoff(header, header_len);
if (err != APIError::OK) {
this->fatal_error_with_log_(LOG_STR("Noise handoff failed"), err);
}
}
#endif // USE_API_NOISE && USE_API_PLAINTEXT
void APIConnection::destroy_active_iterator_() {
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
@@ -279,15 +253,6 @@ void APIConnection::loop() {
// No more data available
break;
} else if (err != APIError::OK) {
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Checked inside the error branch to keep the hot err == OK path
// free of it; this can only fire on the first bytes of a plaintext
// helper on an unprovisioned device
if (err == APIError::PROTOCOL_SWITCH_TO_NOISE) {
this->upgrade_helper_to_noise_();
return;
}
#endif
this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
return;
} else {
@@ -1759,19 +1724,6 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name());
#ifdef USE_PROVISIONING
if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) {
// The provisioning window has closed without the device being provisioned.
// Acknowledge the hello so the client can read the server name, then request
// disconnect with the reason. Authentication is intentionally not completed.
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Provisioning closed; rejecting connection"));
this->send_message(resp);
DisconnectRequest req;
req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
return this->send_message(req);
}
#endif
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0
this->complete_authentication_();
@@ -1807,7 +1759,7 @@ bool APIConnection::send_device_info_response_() {
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
#if defined(USE_ESP8266) || defined(USE_ESP32)
#define ESPHOME_MANUFACTURER "Espressif"
#elif defined(USE_RP2)
#elif defined(USE_RP2040)
#define ESPHOME_MANUFACTURER "Raspberry Pi"
#elif defined(USE_BK72XX)
#define ESPHOME_MANUFACTURER "Beken"
@@ -1892,12 +1844,6 @@ bool APIConnection::send_device_info_response_() {
#endif
#ifdef USE_API_NOISE
resp.api_encryption_supported = true;
#ifndef USE_API_NOISE_PSK_FROM_YAML
// No key from YAML: while no key is set, the key can be provisioned over a
// zero-PSK Noise connection. Gated on the YAML define (not the plaintext
// one) so this advertisement survives the plaintext removal in 2027.2.0.
resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
#endif
#endif
#ifdef USE_DEVICES
size_t device_index = 0;
@@ -1928,8 +1874,7 @@ void APIConnection::on_hello_request(const HelloRequest &msg) {
this->on_fatal_error();
}
}
void APIConnection::on_disconnect_request(const DisconnectRequest & /*msg*/) {
// The reason is informational when a client disconnects us; we always ack and close.
void APIConnection::on_disconnect_request() {
if (!this->send_disconnect_response_()) {
this->on_fatal_error();
}
@@ -2057,15 +2002,6 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
NoiseEncryptionSetKeyResponse resp;
resp.success = false;
#ifdef USE_PROVISIONING
// Refuse to set a key once the provisioning window has closed (defense in depth;
// such connections are already rejected at hello).
if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) {
ESP_LOGW(TAG, "Provisioning closed; rejecting key set");
return this->send_message(resp);
}
#endif
psk_t psk{};
if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) {
@@ -2075,21 +2011,10 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
}
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (APINoiseContext::is_all_zeros(psk)) {
// Accepting the reserved provisioning PSK would report success without
// enabling encryption (or silently clear an existing key)
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
} else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");
} else {
resp.success = true;
#ifdef USE_API_PLAINTEXT
if (this->helper_->frame_footer_size() == 0) {
// Plaintext transport has no frame footer; Noise always has the MAC footer.
// Remove after 2027.2.0 together with plaintext support on keyless devices.
ESP_LOGW(TAG, "Key received over plaintext; deprecated, will be removed in 2027.2.0");
}
#endif
}
return this->send_message(resp);
+5 -10
View File
@@ -18,8 +18,8 @@
#ifdef USE_ESP32_CRASH_HANDLER
#include "esphome/components/esp32/crash_handler.h"
#endif
#ifdef USE_RP2_CRASH_HANDLER
#include "esphome/components/rp2/crash_handler.h"
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
#include "esphome/components/esp8266/crash_handler.h"
@@ -259,7 +259,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_get_time_response(const GetTimeResponse &value);
#endif
void on_hello_request(const HelloRequest &msg);
void on_disconnect_request(const DisconnectRequest &msg);
void on_disconnect_request();
void on_ping_request();
void on_device_info_request();
void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
@@ -279,8 +279,8 @@ class APIConnection final : public APIServerConnectionBase {
esp32::crash_handler_log();
esp32::crash_handler_clear();
#endif
#ifdef USE_RP2_CRASH_HANDLER
rp2::crash_handler_log();
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
esp8266::crash_handler_log();
@@ -626,11 +626,6 @@ class APIConnection final : public APIServerConnectionBase {
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
void finalize_iterator_sync_();
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Swap the plaintext helper for a Noise helper after the client opened
// with a Noise hello on an unprovisioned device (zero-PSK provisioning).
void upgrade_helper_to_noise_();
#endif
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
@@ -97,8 +97,6 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE");
}
#endif
// PROTOCOL_SWITCH_TO_NOISE is intercepted in APIConnection::loop() before
// any logging can happen, so it intentionally has no entry here.
return LOG_STR("UNKNOWN");
}
-11
View File
@@ -88,11 +88,6 @@ enum class APIError : uint16_t {
HANDSHAKESTATE_SPLIT_FAILED = 1020,
BAD_HANDSHAKE_ERROR_BYTE = 1021,
#endif
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Not an error: an unprovisioned device received a Noise client hello on a
// plaintext connection; the caller must hand the socket off to a Noise helper.
PROTOCOL_SWITCH_TO_NOISE = 1023,
#endif
};
const LogString *api_error_to_logstr(APIError err);
@@ -205,12 +200,6 @@ class APIFrameHelper {
// or track that they stopped early and retry without this check.
// See Socket::ready() for details.
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Move the socket out of this helper so a replacement helper can take it
// over (plaintext to Noise handoff on unprovisioned devices). The drained
// helper must be destroyed right after.
std::unique_ptr<socket::Socket> release_socket_for_switch() { return std::move(this->socket_); }
#endif
// Release excess memory from internal buffers after initial sync
void release_buffers() {
// rx_buf_: Safe to clear only if no partial read in progress.
@@ -109,40 +109,6 @@ APIError APINoiseFrameHelper::init() {
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
#ifdef USE_API_PLAINTEXT
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
APIError err = this->init();
if (err != APIError::OK) {
return err;
}
// Seed the header bytes the plaintext helper consumed before detecting the
// Noise indicator; try_read_frame_ resumes from rx_header_buf_len_.
std::memcpy(this->rx_header_buf_, header, header_len);
this->rx_header_buf_len_ = header_len;
// Pump the handshake without gating on socket_->ready(): on LWIP the
// plaintext helper's partial read can drain rcvevent while the rest of the
// client hello sits in the lastdata cache, so ready() may report false even
// though data is available.
return this->pump_handshake_();
}
#endif // USE_API_PLAINTEXT
/// Drive the handshake state machine until DATA, WOULD_BLOCK, or a fatal
/// error. WOULD_BLOCK is not an error: reads stop naturally on EWOULDBLOCK
/// and resume on the next loop().
APIError APINoiseFrameHelper::pump_handshake_() {
while (this->state_ != State::DATA) {
APIError err = this->state_action_();
if (err == APIError::WOULD_BLOCK) {
break;
}
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
}
// Helper for handling handshake frame errors
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
if (aerr == APIError::BAD_INDICATOR) {
@@ -165,13 +131,16 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
// Check ready() once, not per state transition. On ESP8266 LWIP raw TCP,
// ready() returns false once the rx buffer is consumed. Re-checking each
// iteration would block handshake writes that must follow reads,
// deadlocking the handshake. pump_handshake_() stops on WOULD_BLOCK when
// no more data is available to read.
if (state_ != State::DATA && this->socket_->ready()) {
APIError err = this->pump_handshake_();
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
// that must follow reads, deadlocking the handshake. state_action() will return
// WOULD_BLOCK when no more data is available to read.
bool socket_ready = this->socket_->ready();
while (state_ != State::DATA && socket_ready) {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK) {
break;
}
if (err != APIError::OK) {
return err;
}
@@ -22,20 +22,12 @@ class APINoiseFrameHelper final : public APIFrameHelper {
}
~APINoiseFrameHelper() override;
APIError init() override;
#ifdef USE_API_PLAINTEXT
// Take over a connection whose first bytes were consumed by a plaintext
// helper on an unprovisioned device (see APIError::PROTOCOL_SWITCH_TO_NOISE).
// Seeds the already-read header bytes and pumps the handshake state machine
// until it would block.
APIError init_from_handoff(const uint8_t *header, uint8_t header_len);
#endif
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
APIError pump_handshake_();
APIError state_action_();
APIError state_action_client_hello_();
APIError state_action_server_hello_();
@@ -89,17 +89,6 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
// If this was the first read, validate the indicator byte
if (rx_header_buf_pos_ == 0 && received > 0) {
if (rx_header_buf_[0] != 0x00) {
#ifdef USE_API_NOISE
// Dual build (encryption supported but no key set): a 0x01 first byte
// is a Noise client hello. Hand the connection off to a Noise helper
// running the all-zeros provisioning PSK so the encryption key can be
// set without crossing the wire in plaintext. Preserve the bytes we
// already consumed; they are the start of the Noise 3-byte header.
if (rx_header_buf_[0] == 0x01) {
rx_header_buf_pos_ = static_cast<uint8_t>(received);
return APIError::PROTOCOL_SWITCH_TO_NOISE;
}
#endif
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
@@ -23,15 +23,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
#ifdef USE_API_NOISE
// After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the
// header bytes already consumed from the socket (at most 3, the size of the
// Noise fixed header) so the replacement Noise helper can be seeded with them.
uint8_t get_consumed_header(uint8_t out[3]) const {
memcpy(out, this->rx_header_buf_, this->rx_header_buf_pos_);
return this->rx_header_buf_pos_;
}
#endif
protected:
APIError try_read_frame_();
+5 -12
View File
@@ -10,20 +10,13 @@ using psk_t = std::array<uint8_t, 32>;
class APINoiseContext {
public:
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
// doubles as the well-known provisioning PSK that unprovisioned devices
// accept for Noise handshakes (passive-sniffing protection only, no
// authentication). It is never a valid real key.
static bool is_all_zeros(const psk_t &psk) {
uint8_t acc = 0;
for (uint8_t b : psk) {
acc |= b;
}
return acc == 0;
}
void set_psk(psk_t psk) {
this->psk_ = psk;
this->has_psk_ = !is_all_zeros(psk);
bool has_psk = false;
for (auto i : psk) {
has_psk |= i;
}
this->has_psk_ = has_psk;
}
const psk_t &get_psk() const { return this->psk_; }
bool has_psk() const { return this->has_psk_; }
-26
View File
@@ -47,26 +47,6 @@ uint32_t HelloResponse::calculate_size() const {
size += 2 + this->name.size();
return size;
}
bool DisconnectRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->reason = static_cast<enums::DisconnectReason>(value);
break;
default:
return false;
}
return true;
}
uint8_t *DisconnectRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->reason));
return pos;
}
uint32_t DisconnectRequest::calculate_size() const {
uint32_t size = 0;
size += this->reason ? 2 : 0;
return size;
}
#ifdef USE_AREAS
uint8_t *AreaInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
@@ -170,9 +150,6 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
for (const auto &it : this->serial_proxies) {
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 25, it);
}
#endif
#ifdef USE_API_NOISE
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
#endif
return pos;
}
@@ -235,9 +212,6 @@ uint32_t DeviceInfoResponse::calculate_size() const {
for (const auto &it : this->serial_proxies) {
size += ProtoSize::calc_message_force(2, it.calculate_size());
}
#endif
#ifdef USE_API_NOISE
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
#endif
return size;
}
+3 -14
View File
@@ -11,10 +11,6 @@ namespace esphome::api {
namespace enums {
enum DisconnectReason : uint32_t {
DISCONNECT_REASON_UNSPECIFIED = 0,
DISCONNECT_REASON_PROVISIONING_CLOSED = 1,
};
enum SerialProxyPortType : uint32_t {
SERIAL_PROXY_PORT_TYPE_TTL = 0,
SERIAL_PROXY_PORT_TYPE_RS232 = 1,
@@ -431,22 +427,18 @@ class HelloResponse final : public ProtoMessage {
protected:
};
class DisconnectRequest final : public ProtoDecodableMessage {
class DisconnectRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 5;
static constexpr uint8_t ESTIMATED_SIZE = 2;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("disconnect_request"); }
#endif
enums::DisconnectReason reason{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class DisconnectResponse final : public ProtoMessage {
public:
@@ -533,7 +525,7 @@ class SerialProxyInfo final : public ProtoMessage {
class DeviceInfoResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 312;
static constexpr uint16_t ESTIMATED_SIZE = 309;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
#endif
@@ -588,9 +580,6 @@ class DeviceInfoResponse final : public ProtoMessage {
#endif
#ifdef USE_SERIAL_PROXY
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
#endif
#ifdef USE_API_NOISE
bool api_encryption_provisionable{false};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
+1 -15
View File
@@ -125,16 +125,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
}
#pragma GCC diagnostic pop
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
switch (value) {
case enums::DISCONNECT_REASON_UNSPECIFIED:
return ESPHOME_PSTR("DISCONNECT_REASON_UNSPECIFIED");
case enums::DISCONNECT_REASON_PROVISIONING_CLOSED:
return ESPHOME_PSTR("DISCONNECT_REASON_PROVISIONING_CLOSED");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) {
switch (value) {
case enums::SERIAL_PROXY_PORT_TYPE_TTL:
@@ -874,8 +864,7 @@ const char *HelloResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
const char *DisconnectRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("DisconnectRequest"));
dump_field(out, ESPHOME_PSTR("reason"), static_cast<enums::DisconnectReason>(this->reason));
out.append_p(ESPHOME_PSTR("DisconnectRequest {}"));
return out.c_str();
}
const char *DisconnectResponse::dump_to(DumpBuffer &out) const {
@@ -982,9 +971,6 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
it.dump_to(out);
out.append("\n");
}
#endif
#ifdef USE_API_NOISE
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
#endif
return out.c_str();
}
+2 -4
View File
@@ -51,12 +51,10 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
this->log_receive_message_(LOG_STR("on_disconnect_request"));
#endif
this->on_disconnect_request(msg);
this->on_disconnect_request();
break;
}
case DisconnectResponse::MESSAGE_TYPE: {
+1 -1
View File
@@ -21,7 +21,7 @@ class APIServerConnectionBase {
void on_hello_request(const HelloRequest &value){};
void on_disconnect_request(const DisconnectRequest &value){};
void on_disconnect_request(){};
void on_disconnect_response(){};
void on_ping_request(){};
void on_ping_response(){};
+11 -53
View File
@@ -107,30 +107,8 @@ void APIServer::setup() {
// Initialize last_connected_ for reboot timeout tracking
this->last_connected_ = App.get_loop_component_start_time();
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
// Register with the provisioning manager (provisioning:) as a source and
// report our current state (provisioned == an encryption key is set). When the
// window closes, disconnect any client still attempting to provision so it learns
// the reason. The manager owns the timeout, window state and on_timeout automation.
if (provisioning::global_provisioning_manager != nullptr) {
this->provisioning_source_ = provisioning::global_provisioning_manager->register_source();
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_,
this->noise_ctx_.has_psk());
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
for (auto &c : this->active_clients()) {
DisconnectRequest req;
req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
// Best-effort: if the send buffer is full the reason is dropped, but the
// client still learns the window is closed when it reconnects (rejected at
// hello) or via the socket close.
c->send_message(req);
}
});
}
#endif
// Set warning status if reboot timeout is enabled (suppressed while provisioning
// is pending so the device waits to be onboarded instead of rebooting).
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
// Set warning status if reboot timeout is enabled
if (this->reboot_timeout_ != 0) {
this->status_set_warning(LOG_STR("waiting for client connection"));
}
}
@@ -143,10 +121,8 @@ void APIServer::loop() {
if (this->api_connection_count_ == 0) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time).
// Suppressed while a provisioning window is pending so the device waits to be
// onboarded / reset instead of rebooting itself; resumes once provisioned.
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
// (cancelled scheduler items sit in heap memory until their scheduled time)
if (this->reboot_timeout_ != 0) {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No clients; rebooting");
@@ -218,8 +194,7 @@ void APIServer::remove_client_(uint8_t client_index) {
this->clients_[last_index].reset();
// Last client disconnected - set warning and start tracking for reboot timeout
// (suppressed while provisioning is pending - see loop()).
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
this->status_set_warning(LOG_STR("waiting for client connection"));
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -257,7 +232,7 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
conn->start();
// First client connected - clear warning and update timestamp
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -265,13 +240,12 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
}
void APIServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Server:\n"
" Address: %s:%u\n"
" Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
@@ -597,16 +571,8 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
}
SavedNoisePsk new_saved_psk{psk};
bool result = this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
make_active);
#ifdef USE_PROVISIONING
// The device now has a key; report provisioned so the provisioning window is
// satisfied and the reboot timeout resumes normal operation.
if (result && provisioning::global_provisioning_manager != nullptr) {
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, true);
}
#endif
return result;
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
make_active);
#endif
}
bool APIServer::clear_noise_psk(bool make_active) {
@@ -617,16 +583,8 @@ bool APIServer::clear_noise_psk(bool make_active) {
return false;
#else
SavedNoisePsk empty_psk{};
bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
make_active);
#ifdef USE_PROVISIONING
// The key was cleared; report unprovisioned so a subsequent reboot reopens the
// provisioning window.
if (result && provisioning::global_provisioning_manager != nullptr) {
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, false);
}
#endif
return result;
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
make_active);
#endif
}
#endif
+1 -20
View File
@@ -14,9 +14,6 @@
#include "esphome/core/controller.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#ifdef USE_PROVISIONING
#include "esphome/components/provisioning/provisioning.h"
#endif
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
@@ -258,19 +255,6 @@ class APIServer final : public Component,
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
void __attribute__((noinline)) remove_client_(uint8_t client_index);
#ifdef USE_PROVISIONING
// True while a configured provisioning window is still pending (the device is
// unprovisioned). Suppresses the reboot timeout and its warning so the device is
// not auto-rebooted while waiting to be provisioned. False when no provisioning
// window is configured.
bool provisioning_pending_() const {
return provisioning::global_provisioning_manager != nullptr &&
provisioning::global_provisioning_manager->window_pending();
}
#else
bool provisioning_pending_() const { return false; }
#endif
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
bool make_active);
@@ -348,10 +332,7 @@ class APIServer final : public Component,
uint8_t listen_backlog_{4};
bool shutting_down_ = false;
uint8_t api_connection_count_{0};
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
// Index assigned by the provisioning manager for reporting this transport's state.
uint8_t provisioning_source_{0};
#endif
// 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE
APINoiseContext noise_ctx_;
+3 -3
View File
@@ -13,7 +13,7 @@ def AUTO_LOAD() -> list[str]:
if (
not CORE.is_esp32
and not CORE.is_esp8266
and not CORE.is_rp2
and not CORE.is_rp2040
and not CORE.is_libretiny
):
return ["socket"]
@@ -37,7 +37,7 @@ async def to_code(config):
elif CORE.is_esp8266:
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
elif CORE.is_rp2:
elif CORE.is_rp2040:
# https://github.com/ayushsharma82/RPAsyncTCP
# RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better
# ESPAsyncWebServer compatibility
@@ -47,6 +47,6 @@ async def to_code(config):
def FILTER_SOURCE_FILES() -> list[str]:
# Exclude socket implementation for platforms that use AsyncTCP libraries
if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2 or CORE.is_libretiny:
if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny:
return ["async_tcp_socket.cpp"]
return []
+1 -1
View File
@@ -7,7 +7,7 @@
#elif defined(USE_ESP8266)
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
#include <ESPAsyncTCP.h>
#elif defined(USE_RP2)
#elif defined(USE_RP2040)
// Use RPAsyncTCP library for RP2040
#include <RPAsyncTCP.h>
#else
@@ -1,6 +1,6 @@
#include "async_tcp_socket.h"
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2) && !defined(USE_LIBRETINY) && \
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \
(defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS))
#include "esphome/components/network/util.h"
@@ -2,7 +2,7 @@
#include "esphome/core/defines.h"
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2) && !defined(USE_LIBRETINY) && \
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \
(defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS))
#include "esphome/components/socket/socket.h"
+1 -3
View File
@@ -113,9 +113,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
elif file_type in ("mp1", "mp2", "mp3", "mpeg", "mpga"):
# With puremagic >=2.0 this can cause some MP3 (Layer III) files to be labeled as "mp1"/"mp2".
# Treat those labels as MP3 so we still pick the MP3 decoder.
elif file_type in ("mp3", "mpeg", "mpga"):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
elif file_type == "flac":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
@@ -379,17 +379,9 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn
}
void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) {
if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) {
// A previous subscriber still holds the slot. This is almost always a stale
// connection from a client that dropped without a clean disconnect and has
// not yet hit the keepalive timeout; rejecting the new subscriber would
// silently starve it of advertisements until it reconnects, so the newest
// subscriber wins instead.
char old_peername[socket::SOCKADDR_STR_LEN];
char new_peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "Subscription from %s (%s) replaces %s (%s)", api_connection->get_name(),
api_connection->get_peername_to(new_peername), this->api_connection_->get_name(),
this->api_connection_->get_peername_to(old_peername));
if (this->api_connection_ != nullptr) {
ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
return;
}
this->api_connection_ = api_connection;
this->parent_->recalculate_advertisement_parser_types();
@@ -13,7 +13,7 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RP2,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
PlatformFramework,
)
@@ -54,7 +54,7 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RP2,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
]
),
@@ -105,7 +105,7 @@ async def to_code(config):
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):
if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2040):
cg.add_library("DNSServer", None)
-1
View File
@@ -16,7 +16,6 @@ CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
-6
View File
@@ -1,6 +0,0 @@
import esphome.codegen as cg
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["i2c"]
cst328_ns = cg.esphome_ns.namespace("cst328")
@@ -1,28 +0,0 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from .. import cst328_ns
from ..touchscreen import CST328ButtonListener, CST328Touchscreen
CONF_CST328_ID = "cst328_id"
CST328Button = cst328_ns.class_(
"CST328Button",
binary_sensor.BinarySensor,
cg.Component,
CST328ButtonListener,
cg.Parented.template(CST328Touchscreen),
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST328Button).extend(
{
cv.GenerateID(CONF_CST328_ID): cv.use_id(CST328Touchscreen),
}
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_CST328_ID])
@@ -1,16 +0,0 @@
#include "cst328_button.h"
#include "esphome/core/log.h"
namespace esphome::cst328 {
static const char *const TAG = "cst328.binary_sensor";
void CST328Button::setup() {
this->parent_->register_button_listener(this);
this->publish_initial_state(false);
}
void CST328Button::dump_config() { LOG_BINARY_SENSOR("", "CST328 Button", this); }
void CST328Button::update_button(bool state) { this->publish_state(state); }
} // namespace esphome::cst328
@@ -1,20 +0,0 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "../touchscreen/cst328_touchscreen.h"
namespace esphome::cst328 {
class CST328Button : public binary_sensor::BinarySensor,
public Component,
public CST328ButtonListener,
public Parented<CST328Touchscreen> {
public:
void setup() override;
void dump_config() override;
void update_button(bool state) override;
};
} // namespace esphome::cst328
@@ -1,38 +0,0 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from .. import cst328_ns
CST328Touchscreen = cst328_ns.class_(
"CST328Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CST328ButtonListener = cst328_ns.class_("CST328ButtonListener")
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CST328Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x1A))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
if reset_pin := config.get(CONF_RESET_PIN):
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
@@ -1,168 +0,0 @@
#include "cst328_touchscreen.h"
#include "esphome/core/log.h"
namespace esphome::cst328 {
static const char *const TAG = "cst328.touchscreen";
static const uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet
static const uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less
static const uint16_t CST328_FW_CRC = 0xCACA; // Expected firmware CRC value
static const uint8_t CST328_SYNC_BYTE = 0xAB; // Sync byte used in communication
static const uint8_t ZERO_BYTE = 0;
#define I2C_WARN_ON_ERROR(x, log_tag, format, ...) \
do { \
i2c::ErrorCode err_rc_ = (x); \
if (err_rc_ != i2c::ERROR_OK) { \
ESP_LOGW(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
this->status_set_warning(format); \
} \
} while (0)
#define I2C_FAIL_ON_ERROR(x, log_tag, format, ...) \
do { \
i2c::ErrorCode err_rc_ = (x); \
if (err_rc_ != i2c::ERROR_OK) { \
ESP_LOGE(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
this->mark_failed(); \
return; \
} \
} while (0)
void CST328Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up CST328 Touchscreen...");
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
this->set_timeout(CST328_BEFORE_RESET_TIMEOUT, [this] { this->reset_device_(); });
} else {
this->continue_setup_();
}
}
void CST328Touchscreen::reset_device_() {
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
this->set_timeout(CST328_TRANSITION_TIMEOUT, [this] { this->continue_setup_(); });
}
void CST328Touchscreen::continue_setup_() {
ESP_LOGV(TAG, "Continuing CST328 setup...");
uint8_t data_byte{0};
uint8_t buf[24]{};
I2C_FAIL_ON_ERROR(this->write_register16(CST_WM_DEBUG_INFO, buf, 0), TAG, "Failed to enter debug/info mode");
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_CRC_AND_BOOT_TIME, buf, 4), TAG,
"Failed to read FW CRC and boot time");
uint16_t fw_crc = buf[2] + (buf[3] << 8);
if (fw_crc != CST328_FW_CRC) {
ESP_LOGE(TAG, "Error: Firmware CRC mismatch, expected 0x%04X but got 0x%04X", CST328_FW_CRC, fw_crc);
this->mark_failed();
return;
}
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_CHIP_TYPE_AND_PROJECT_ID, buf, 4), TAG,
"Failed to read chip and project ID");
this->chip_id_ = buf[2] + (buf[3] << 8);
this->project_id_ = buf[0] + (buf[1] << 8);
ESP_LOGD(TAG, "Chip ID %X, project ID %X", this->chip_id_, this->project_id_);
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_REVISION, buf, 4), TAG, "Failed to read FW version");
this->fw_ver_major_ = buf[3];
this->fw_ver_minor_ = buf[2];
this->fw_build_ = buf[0] + (buf[1] << 8);
ESP_LOGV(TAG, "FW version %d.%d.%d", this->fw_ver_major_, this->fw_ver_minor_, this->fw_build_);
if (i2c::ERROR_OK == this->read_register16(CST_REG_X_Y_RESOLUTION, buf, 4)) {
this->x_raw_max_ = buf[0] + (buf[1] << 8);
this->y_raw_max_ = buf[2] + (buf[3] << 8);
} else {
this->x_raw_max_ = this->display_->get_native_width();
this->y_raw_max_ = this->display_->get_native_height();
}
I2C_WARN_ON_ERROR(this->write_register16(CST_WM_NORMAL, buf, 0), TAG, "Failed to enter normal mode");
I2C_WARN_ON_ERROR(this->read_register16(CST_REG_TOUCH_INFORMATION, &data_byte, 1), TAG, "Failed to read sync");
I2C_WARN_ON_ERROR(this->write_register16(CST_REG_TOUCH_INFORMATION, &CST328_SYNC_BYTE, 1), TAG,
"Failed to write sync");
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
this->setup_complete_ = true;
ESP_LOGV(TAG, "CST328 setup complete");
}
void CST328Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "CST328 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Chip ID: 0x%04X, Project ID: 0x%04X", this->chip_id_, this->project_id_);
ESP_LOGCONFIG(TAG, " FW version: %d.%d.%d", this->fw_ver_major_, this->fw_ver_minor_, this->fw_build_);
ESP_LOGCONFIG(TAG, " X/Y resolution: %d/%d", this->x_raw_max_, this->y_raw_max_);
}
void CST328Touchscreen::update_button_state_(bool state) {
if (this->button_touched_ == state) {
return;
}
this->button_touched_ = state;
for (auto *listener : this->button_listeners_) {
listener->update_button(state);
}
}
void CST328Touchscreen::update_touches() {
if (!this->setup_complete_) {
this->skip_update_ = true;
return;
}
uint8_t touch_data[CST328_TOUCH_DATA_SIZE];
this->status_clear_warning();
if (i2c::ERROR_OK != this->read_register16(CST_REG_TOUCH_INFORMATION, touch_data, CST328_TOUCH_DATA_SIZE)) {
ESP_LOGW(TAG, "Failed to read touch data");
this->status_set_warning();
this->skip_update_ = true;
return;
}
uint8_t touch_cnt = touch_data[CST_REG_FINGER_COUNT_IDX] & 0x0F;
if (touch_cnt == 0 || touch_cnt > CST328_TOUCH_MAX_POINTS) {
this->update_button_state_(false);
} else {
this->update_button_state_(true);
uint8_t data_idx = 0;
for (uint8_t i = 0; i < touch_cnt; i++) {
uint8_t id = touch_data[data_idx] >> 4;
int16_t x = (touch_data[data_idx + 1] << 4) | ((touch_data[data_idx + 3] >> 4) & 0x0F);
int16_t y = (touch_data[data_idx + 2] << 4) | (touch_data[data_idx + 3] & 0x0F);
int16_t z = touch_data[data_idx + 4];
this->add_raw_touch_position_(id, x, y, z);
data_idx += (i == 0) ? 7 : 5;
}
}
bool cleanup_error = false;
cleanup_error |= (i2c::ERROR_OK != this->write_register16(CST_REG_TOUCH_FINGER_NUMBER, &ZERO_BYTE, 1));
cleanup_error |= (i2c::ERROR_OK != this->write_register16(CST_REG_TOUCH_INFORMATION, &CST328_SYNC_BYTE, 1));
if (cleanup_error) {
ESP_LOGW(TAG, "Failed to clean up touch registers");
}
}
} // namespace esphome::cst328
@@ -1,61 +0,0 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome::cst328 {
static const uint8_t CST328_TOUCH_MAX_POINTS = 5;
static const uint8_t CST328_TOUCH_DATA_SIZE = CST328_TOUCH_MAX_POINTS * 5 + 2;
static const uint16_t CST_REG_TOUCH_INFORMATION = 0xD000;
static const uint16_t CST_REG_TOUCH_FINGER_NUMBER = 0xD005;
static const uint16_t CST_REG_FINGER_COUNT_IDX = CST_REG_TOUCH_FINGER_NUMBER - CST_REG_TOUCH_INFORMATION;
static const uint16_t CST_REG_X_Y_RESOLUTION = 0xD1F8;
static const uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = 0xD1FC;
static const uint16_t CST_REG_CHIP_TYPE_AND_PROJECT_ID = 0xD204;
static const uint16_t CST_REG_FW_REVISION = 0xD208;
static const uint16_t CST_WM_DEBUG_INFO = 0xD101;
static const uint16_t CST_WM_NORMAL = 0xD109;
class CST328ButtonListener {
public:
virtual void update_button(bool state) = 0;
};
class CST328Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void register_button_listener(CST328ButtonListener *listener) { this->button_listeners_.push_back(listener); }
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void update_touches() override;
void reset_device_();
void continue_setup_();
void update_button_state_(bool state);
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
std::vector<CST328ButtonListener *> button_listeners_;
bool button_touched_{};
uint16_t chip_id_{};
uint16_t project_id_{};
uint8_t fw_ver_major_{};
uint8_t fw_ver_minor_{};
uint16_t fw_build_{};
bool setup_complete_{};
};
} // namespace esphome::cst328
+1 -1
View File
@@ -70,7 +70,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
},
"debug_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"debug_host.cpp": {PlatformFramework.HOST_NATIVE},
"debug_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
"debug_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"debug_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
@@ -1,5 +1,5 @@
#include "debug_component.h"
#ifdef USE_RP2
#ifdef USE_RP2040
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <Arduino.h>
@@ -9,8 +9,8 @@
#else
#include <hardware/structs/vreg_and_chip_reset.h>
#endif
#ifdef USE_RP2_CRASH_HANDLER
#include "esphome/components/rp2/crash_handler.h"
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
namespace esphome::debug {
@@ -41,8 +41,8 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
if (watchdog_caused_reboot()) {
bool handled = false;
#ifdef USE_RP2_CRASH_HANDLER
if (rp2::crash_handler_has_data()) {
#ifdef USE_RP2040_CRASH_HANDLER
if (rp2040::crash_handler_has_data()) {
pos = buf_append_str(buf, size, pos, "Crash (HardFault)|");
handled = true;
}
-2
View File
@@ -112,7 +112,6 @@ def model_schema(config):
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
**model.get_config_options(),
}
)
@@ -199,7 +198,6 @@ async def to_code(config):
)
await display.register_display(var, config)
config = await model.to_code(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
@@ -14,9 +14,10 @@ void EPaperMono::refresh_screen(bool partial) {
}
void EPaperMono::deep_sleep() {
// Deep sleep loses RAM so cannot be used with partial update
if (!this->is_using_partial_update_()) {
ESP_LOGV(TAG, "Deep sleep");
ESP_LOGV(TAG, "Deep sleep");
if (this->is_using_partial_update_()) {
this->cmd_data(0x10, {0x00}); // sleep in power on mode
} else {
this->cmd_data(0x10, {0x03}); // deep sleep
}
}
@@ -1,367 +0,0 @@
#include "epaper_spi_t133a01.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.t133a01";
// Color indices used in the 4bpp buffer (sprite-side)
// These MUST match the Arduino GFX TFT_eSPI.h color definitions and
// the remap_color()/COLOR_GET mapping:
// 0x0F=BLACK, 0x00=WHITE, 0x02=GREEN, 0x06=RED, 0x0B=YELLOW, 0x0D=BLUE
static constexpr uint8_t T133A01_BLACK = 0x0F;
static constexpr uint8_t T133A01_WHITE = 0x00;
static constexpr uint8_t T133A01_GREEN = 0x02;
static constexpr uint8_t T133A01_RED = 0x06;
static constexpr uint8_t T133A01_YELLOW = 0x0B;
static constexpr uint8_t T133A01_BLUE = 0x0D;
// T133A01 register addresses
static constexpr uint8_t R00_PSR = 0x00;
static constexpr uint8_t R01_PWR = 0x01;
static constexpr uint8_t R02_POF = 0x02;
static constexpr uint8_t R04_PON = 0x04;
static constexpr uint8_t R05_BTST_N = 0x05;
static constexpr uint8_t R06_BTST_P = 0x06;
static constexpr uint8_t R10_DTM = 0x10;
static constexpr uint8_t R12_DRF = 0x12;
static constexpr uint8_t R50_CDI = 0x50;
static constexpr uint8_t R61_TRES = 0x61;
static constexpr uint8_t RA5_DCDC = 0xA5;
static constexpr uint8_t RE0_CCSET = 0xE0;
static constexpr uint8_t RE3_PWS = 0xE3;
/**
* COLOR_GET remap table from T133A01_Defines.h.
* Translates 4bpp sprite color index to the hardware pixel encoding.
* Sprite: 0x0F=BLACK 0x00=WHITE 0x02=GREEN 0x06=RED 0x0B=YELLOW 0x0D=BLUE
* HW: 0x00=BLACK 0x01=WHITE 0x06=GREEN 0x03=RED 0x02=YELLOW 0x05=BLUE
*/
uint8_t EPaperT133A01::remap_color(uint8_t index) {
switch (index & 0x0F) {
case 0x0F:
return 0x00; // Black
case 0x00:
return 0x01; // White
case 0x02:
return 0x06; // Green
case 0x06:
return 0x03; // Red
case 0x0B:
return 0x02; // Yellow
case 0x0D:
return 0x05; // Blue
default:
return 0x01; // White fallback
}
}
/**
* Map an ESPHome Color to a 4-bit sprite color index.
* Index values match the Arduino GFX TFT_eSPI color definitions:
* 0x00=WHITE, 0x02=GREEN, 0x06=RED, 0x0B=YELLOW, 0x0D=BLUE, 0x0F=BLACK
*/
uint8_t EPaperT133A01::color_to_index(Color color) {
unsigned char max_rgb = std::max({color.r, color.g, color.b});
unsigned char min_rgb = std::min({color.r, color.g, color.b});
// Check for grayscale
if ((max_rgb - min_rgb) < 50) {
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
return T133A01_WHITE;
}
return T133A01_BLACK;
}
bool r_on = (color.r > 128);
bool g_on = (color.g > 128);
bool b_on = (color.b > 128);
if (r_on && g_on && !b_on)
return T133A01_YELLOW;
if (r_on && !g_on && !b_on)
return T133A01_RED;
if (!r_on && g_on && !b_on)
return T133A01_GREEN;
if (!r_on && !g_on && b_on)
return T133A01_BLUE;
// Handle mixed colors: map to nearest primary
if (!r_on && g_on && b_on)
return T133A01_GREEN; // Cyan -> Green
if (r_on && !g_on)
return T133A01_RED; // Magenta -> Red
if (r_on)
return T133A01_WHITE;
return T133A01_BLACK;
}
void EPaperT133A01::setup() {
// Base setup initialises the buffer, the standard pins and the SPI bus.
EPaperBase::setup();
// Both chip-selects are driven directly by this driver (the dual-CS
// protocol needs CS held HIGH while CS1 receives data, which the SPI
// bus cannot do). Start both deselected (HIGH).
this->cs_pin_->setup();
this->cs_pin_->digital_write(true);
this->cs1_pin_->setup();
this->cs1_pin_->digital_write(true);
}
bool EPaperT133A01::reset() {
for (auto *enable_pin : this->enable_pins_) {
enable_pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
if (this->state_ == EPaperState::RESET) {
this->reset_pin_->digital_write(false);
return false;
}
this->reset_pin_->digital_write(true);
}
return true;
}
/**
* Initialise the T133A01 display.
*
* The init sequence uses a mix of CS and CS1 commands as per the Arduino driver.
* The base class init_sequence is NOT used for T133A01 because the dual-CS
* protocol requires per-command routing.
*/
bool EPaperT133A01::initialise(bool partial) {
// Init sequence mirrors the Arduino GFX library's EPD_INIT() macro
// (T133A01_Defines.h). Commands routed to CS only leave CS1 deselected;
// commands routed to both controllers assert CS and CS1 together.
// 0x74 - panel config (CS only)
this->write_command_(0x74, {0x00, 0x0C, 0x0C, 0xD9, 0xDD, 0xDD, 0x15, 0x15, 0x55}, true, false);
delay(10);
// 0xF0 - panel config (CS + CS1)
this->write_command_(0xF0, {0x49, 0x55, 0x13, 0x5D, 0x05, 0x10}, true, true);
delay(10);
// PSR - Panel Setting Register (CS + CS1)
this->write_command_(0x00, {0xDF, 0x69}, true, true);
delay(10);
// DCDC (CS only)
this->write_command_(RA5_DCDC, {0x44, 0x54, 0x00}, true, false);
delay(10);
// CDI (CS + CS1)
this->write_command_(R50_CDI, {0x37}, true, true);
delay(10);
// 0x60 (CS + CS1)
this->write_command_(0x60, {0x03, 0x03}, true, true);
delay(10);
// 0x86 (CS + CS1)
this->write_command_(0x86, {0x10}, true, true);
delay(10);
// PWS - Phase Width Setting (CS + CS1)
this->write_command_(RE3_PWS, {0x22}, true, true);
delay(10);
// TRES - Resolution Setting (CS + CS1).
// With width=1200, height=1600: first word = width = 1200, second word = height/2 = 800.
this->write_command_(R61_TRES,
{(uint8_t) (this->width_ >> 8), (uint8_t) (this->width_ & 0xFF),
(uint8_t) ((this->height_ / 2) >> 8), (uint8_t) ((this->height_ / 2) & 0xFF)},
true, true);
delay(10);
// PWR - Power Setting (CS only)
this->write_command_(R01_PWR, {0x0F, 0x00, 0x28, 0x2C, 0x28, 0x38}, true, false);
delay(10);
// 0xB6 (CS only)
this->write_command_(0xB6, {0x07}, true, false);
delay(10);
// BTST_P (CS only)
this->write_command_(R06_BTST_P, {0xE0, 0x20}, true, false);
delay(10);
// 0xB7 (CS only)
this->write_command_(0xB7, {0x01}, true, false);
delay(10);
// BTST_N (CS only)
this->write_command_(R05_BTST_N, {0xE0, 0x20}, true, false);
delay(10);
// 0xB0 (CS only)
this->write_command_(0xB0, {0x01}, true, false);
delay(10);
// 0xB1 (CS only)
this->write_command_(0xB1, {0x02}, true, false);
delay(10);
return true;
}
void EPaperT133A01::write_command_(uint8_t command, const uint8_t *data, size_t length, bool use_cs, bool use_cs1) {
ESP_LOGV(TAG, "Command: 0x%02X, Length: %u, CS: %d, CS1: %d", command, (unsigned) length, use_cs, use_cs1);
// Chip-selects are active-low: assert the requested controllers.
this->cs_pin_->digital_write(!use_cs);
this->cs1_pin_->digital_write(!use_cs1);
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(command);
if (length > 0) {
this->dc_pin_->digital_write(true);
this->write_array(data, length);
}
this->disable();
this->cs_pin_->digital_write(true);
this->cs1_pin_->digital_write(true);
}
void EPaperT133A01::fill(Color color) {
if (this->get_clipping().is_set()) {
EPaperBase::fill(color);
return;
}
auto pixel_color = color_to_index(color);
this->buffer_.fill(pixel_color + (pixel_color << 4));
}
void EPaperT133A01::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
auto pixel_bits = color_to_index(color);
uint32_t pixel_position = x + y * this->get_width_internal();
uint32_t byte_position = pixel_position / 2;
auto original = this->buffer_[byte_position];
if ((pixel_position & 1) != 0) {
this->buffer_[byte_position] = (original & 0xF0) | pixel_bits;
} else {
this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4);
}
}
void EPaperT133A01::power_on() {
ESP_LOGV(TAG, "Power on");
this->write_command_(R04_PON, true, true);
}
void EPaperT133A01::power_off() {
ESP_LOGV(TAG, "Power off");
this->write_command_(R02_POF, {0x00}, true, true);
}
void EPaperT133A01::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
// Display Refresh
this->write_command_(R12_DRF, {0x01}, true, true);
}
void EPaperT133A01::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->write_command_(0x07, {0xA5}, true, true);
}
bool HOT EPaperT133A01::transfer_data() {
const uint32_t start_time = millis();
const uint16_t bytes_per_half_row = this->width_ / 4;
const uint16_t total_rows = this->height_;
const uint16_t bytes_per_row = this->width_ / 2;
uint8_t line_data[400] = {};
size_t half = this->current_data_index_;
// --- CCSET: select color set before data transfer (CS + CS1) ---
if (half == 0) {
this->write_command_(RE0_CCSET, {0x01}, true, true);
this->wait_for_idle_(true);
delay(10);
}
// --- CS phase: left half of each row via CS ---
// T133A01 requires CS to stay LOW for the ENTIRE DTM data stream.
// Toggling CS between chunks resets the controller's data pointer,
// causing only the last chunk to be retained. Keep CS asserted
// across timeout boundaries by NOT deselecting on yield.
if (half < total_rows) {
if (half == 0) {
this->cs_pin_->digital_write(false); // select CS
this->cs1_pin_->digital_write(true); // deselect CS1
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(R10_DTM);
this->dc_pin_->digital_write(true);
}
while (half < total_rows) {
size_t buf_offset = half * bytes_per_row;
for (uint16_t col = 0; col < bytes_per_half_row; col++) {
uint8_t b = this->buffer_[buf_offset + col];
line_data[col] = (remap_color(b >> 4) << 4) | remap_color(b & 0x0F);
}
this->write_array(line_data, bytes_per_half_row);
half++;
this->current_data_index_ = half;
if (millis() - start_time > MAX_TRANSFER_TIME) {
return false;
}
}
ESP_LOGD(TAG, "CS phase done");
this->disable();
this->cs_pin_->digital_write(true); // deselect CS
}
// --- CS1 phase: right half of each row via CS1 ---
// Same continuous-transaction requirement as the CS phase.
// CS is held HIGH so only CS1 receives the data.
if (half >= total_rows && half < total_rows * 2) {
size_t cs1_row = half - total_rows;
if (cs1_row == 0) {
this->cs_pin_->digital_write(true); // deselect CS
this->cs1_pin_->digital_write(false); // select CS1
this->enable();
this->dc_pin_->digital_write(false);
this->write_byte(R10_DTM);
this->dc_pin_->digital_write(true);
}
while (half < total_rows * 2) {
size_t row = half - total_rows;
size_t buf_offset = row * bytes_per_row + bytes_per_half_row;
for (uint16_t col = 0; col < bytes_per_half_row; col++) {
uint8_t b = this->buffer_[buf_offset + col];
line_data[col] = (remap_color(b >> 4) << 4) | remap_color(b & 0x0F);
}
this->write_array(line_data, bytes_per_half_row);
half++;
this->current_data_index_ = half;
if (millis() - start_time > MAX_TRANSFER_TIME) {
return false;
}
}
ESP_LOGD(TAG, "CS1 phase done");
this->disable();
this->cs1_pin_->digital_write(true); // deselect CS1
}
this->current_data_index_ = 0;
return true;
}
void EPaperT133A01::dump_config() {
EPaperBase::dump_config();
LOG_PIN(" CS Pin: ", this->cs_pin_);
LOG_PIN(" CS1 Pin: ", this->cs1_pin_);
}
} // namespace esphome::epaper_spi
@@ -1,77 +0,0 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* T133A01-based 6-color e-paper display driver.
*
* The T133A01 controller uses a dual-CS SPI architecture:
* - CS (primary): Controls the first half of pixel data transfer
* - CS1 (secondary): Controls panel commands (init, power, refresh) and
* the second half of pixel data transfer
*
* Color depth: 4 bits per pixel, supporting 6 colors:
* White, Green, Red, Yellow, Blue, Black
*
* Buffer layout: 2 pixels per byte (4bpp packed), total buffer size
* is width * height / 2 bytes.
*/
class EPaperT133A01 : public EPaperBase {
public:
EPaperT133A01(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = (size_t) width * height / 2; // 2 pixels per byte at 4bpp
}
void set_cs_pins(GPIOPin *cs, GPIOPin *cs1) {
this->cs_pin_ = cs;
this->cs1_pin_ = cs1;
}
void fill(Color color) override;
void setup() override;
void dump_config() override;
void draw_pixel_at(int x, int y, Color color) override;
protected:
bool reset() override;
bool initialise(bool partial) override;
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
bool transfer_data() override;
/**
* Send a command (and optional data) selecting one or both controllers.
* Both chip-selects are active-low and managed directly by this driver.
* @param command The command byte to send
* @param data Optional pointer to data bytes to send after the command
* @param length Number of data bytes to send after the command
* @param use_cs assert CS (left controller) for this transaction
* @param use_cs1 assert CS1 (right controller) for this transaction
*/
void write_command_(uint8_t command, const uint8_t *data, size_t length, bool use_cs, bool use_cs1);
void write_command_(uint8_t command, std::initializer_list<uint8_t> data, bool use_cs, bool use_cs1) {
this->write_command_(command, data.begin(), data.size(), use_cs, use_cs1);
}
void write_command_(uint8_t command, bool use_cs, bool use_cs1) {
this->write_command_(command, nullptr, 0, use_cs, use_cs1);
}
/// Convert Color to 4-bit T133A01 color index
static uint8_t color_to_index(Color color);
/// Apply COLOR_GET remap table to translate sprite indices to hardware values
static uint8_t remap_color(uint8_t index);
GPIOPin *cs_pin_{nullptr};
GPIOPin *cs1_pin_{nullptr};
};
} // namespace esphome::epaper_spi
@@ -1,146 +0,0 @@
#include "epaper_waveshare_bwr.h"
#include <algorithm>
namespace esphome::epaper_spi {
enum class BwrState : uint8_t {
BWR_BLACK,
BWR_WHITE,
BWR_RED,
};
static BwrState color_to_bwr(Color color) {
if (color.r > color.g + color.b && color.r > 127) {
return BwrState::BWR_RED;
}
if (color.r + color.g + color.b >= 382) {
return BwrState::BWR_WHITE;
}
return BwrState::BWR_BLACK;
}
// UC8179 3-color display buffer layout:
// - 1 bit per pixel, 8 pixels per byte
// - Buffer first half: Black/White plane (1=black, 0=white)
// - Buffer second half: Red plane (1=red, 0=white)
// - Total: row_width * height * 2 bytes
void EPaperWaveshareBWR::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
const uint32_t pos = (x / 8) + (y * this->row_width_);
const uint8_t bit = 0x80 >> (x & 0x07);
const uint32_t red_offset = this->buffer_length_ / 2u;
const auto bwr = color_to_bwr(color);
if (bwr == BwrState::BWR_BLACK) {
this->buffer_[pos] |= bit;
} else {
this->buffer_[pos] &= ~bit;
}
if (bwr == BwrState::BWR_RED) {
this->buffer_[red_offset + pos] |= bit;
} else {
this->buffer_[red_offset + pos] &= ~bit;
}
}
void EPaperWaveshareBWR::fill(Color color) {
const size_t half_buffer = this->buffer_length_ / 2u;
const auto bwr = color_to_bwr(color);
if (bwr == BwrState::BWR_BLACK) {
// Black plane: 0xFF (black), Red plane: 0x00 (no red)
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[i] = 0xFF;
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[half_buffer + i] = 0x00;
} else if (bwr == BwrState::BWR_RED) {
// Black plane: 0x00 (no black), Red plane: 0xFF (red)
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[i] = 0x00;
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[half_buffer + i] = 0xFF;
} else {
// Black plane: 0x00 (no black), Red plane: 0x00 (no red)
this->buffer_.fill(0x00);
}
}
bool HOT EPaperWaveshareBWR::transfer_data() {
const uint32_t start_time = millis();
const size_t buffer_length = this->buffer_length_;
const size_t half_buffer = buffer_length / 2u;
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
// Phase 1: send Black/White plane (first half) via command 0x10 (DTM1)
// UC8179 DTM1 (0x10): inverted to get 0=black, 1=white
if (this->current_data_index_ < half_buffer) {
if (this->current_data_index_ == 0) {
this->command(0x10); // DATA START TRANSMISSION 1 (black channel)
}
this->start_data_();
while (this->current_data_index_ < half_buffer) {
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_);
for (size_t i = 0; i < bytes_to_copy; i++) {
bytes_to_send[i] = ~this->buffer_[this->current_data_index_ + i];
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
this->disable();
return false;
}
}
this->disable();
}
// Phase 2: send Red plane (second half) via command 0x13 (DTM2)
// UC8179 DTM2 (0x13): 1=red, 0=white
if (this->current_data_index_ < buffer_length) {
if (this->current_data_index_ == half_buffer) {
this->command(0x13); // DATA START TRANSMISSION 2 (red channel)
}
this->start_data_();
while (this->current_data_index_ < buffer_length) {
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_);
for (size_t i = 0; i < bytes_to_copy; i++) {
bytes_to_send[i] = this->buffer_[this->current_data_index_ + i];
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
this->disable();
return false;
}
}
this->disable();
}
this->current_data_index_ = 0;
return true;
}
void EPaperWaveshareBWR::power_on() {
this->cmd_data(0x01, {0x07, 0x17, 0x3F, 0x3F}); // POWER SETTING
this->command(0x04); // POWER ON
}
void EPaperWaveshareBWR::refresh_screen(bool /*partial*/) {
this->command(0x12); // DISPLAY REFRESH
}
void EPaperWaveshareBWR::power_off() {
this->command(0x02); // POWER OFF
}
void EPaperWaveshareBWR::deep_sleep() {
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
}
} // namespace esphome::epaper_spi
@@ -1,40 +0,0 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* Waveshare 3-color e-paper displays (UC8179 controller).
* Supports: 7.5" V2 BWR (EDP_7in5b_V2), 800x480 pixels.
*
* Color scheme: Black, White, Red (BWR)
* Buffer layout: 1 bit per pixel, separate planes
* - Buffer first half: Black/White plane (1=black, 0=white)
* - Buffer second half: Red plane (1=red, 0=no red)
* - Total buffer: width * height / 4 bytes (2 * width * height / 8)
*
* The init sequence (INITIALISE state) sends panel configuration only.
* Power-on (0x01 + 0x04) is sent in the POWER_ON state after data transfer;
* the state machine then busy-waits before triggering REFRESH_SCREEN (0x12).
*/
class EPaperWaveshareBWR : public EPaperBase {
public:
EPaperWaveshareBWR(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = this->row_width_ * height * 2;
}
void fill(Color color) override;
protected:
void draw_pixel_at(int x, int y, Color color) override;
bool transfer_data() override;
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
};
} // namespace esphome::epaper_spi
@@ -2,15 +2,11 @@ from typing import Any, Self
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
from esphome.cpp_generator import MockObj
class EpaperModel:
models: dict[str, Self] = {}
# Whether the driver manages chip-select itself instead of via the SPI bus.
manages_cs: bool = False
def __init__(
self,
name: str,
@@ -39,25 +35,6 @@ class EpaperModel:
def get_constructor_args(self, config) -> tuple:
return ()
def get_config_options(self) -> dict:
"""
Return model-specific configuration schema options.
The base implementation adds nothing; specific models override this to
declare extra options without cluttering the shared schema.
:return: A mapping suitable for cv.Schema.extend()
"""
return {}
async def to_code(self, var: MockObj, config: dict) -> dict:
"""
Generate model-specific code for the options added by add_options().
The base implementation does nothing; specific models override this.
The config can be updated in place to add or remove options.
:param var: The component variable
:param config: The validated configuration
"""
return config
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
@@ -1,71 +0,0 @@
"""T133A01-based e-paper displays.
The T133A01 is a 6-color e-paper controller IC that drives large panels
(1200x1600 portrait). It uses a dual-CS SPI architecture where CS
controls one half of the pixel data and CS1 controls the other half,
as well as panel-level commands (power on, refresh, power off).
Supported models:
- Seeed-reTerminal-E1004: 1200x1600 pixels, 6-color (T133A01 panel)
"""
from esphome import pins
import esphome.codegen as cg
from esphome.const import CONF_CS_PIN
from esphome.cpp_generator import MockObj
from . import EpaperModel
CONF_CS1_PIN = "cs1_pin"
class T133A01Model(EpaperModel):
"""EpaperModel subclass for T133A01-based 6-color e-paper displays."""
# The driver drives CS and CS1 directly for the dual-CS protocol.
manages_cs = True
def __init__(self, name, class_name="EPaperT133A01", **defaults):
super().__init__(name, class_name, **defaults)
def get_config_options(self) -> dict:
# CS1 is the second chip-select required by the dual-CS architecture.
# fallback=None makes it required unless the model provides a default.
return {
self.option(CONF_CS1_PIN, fallback=None): pins.gpio_output_pin_schema,
}
async def to_code(self, var: MockObj, config: dict) -> dict:
cs = await cg.gpio_pin_expression(config[CONF_CS_PIN])
cs1 = await cg.gpio_pin_expression(config[CONF_CS1_PIN])
cg.add(var.set_cs_pins(cs, cs1))
# Remove CS and CS1 from the config so that the base class doesn't try to handle them.
return {k: v for k, v in config.items() if k not in (CONF_CS_PIN, CONF_CS1_PIN)}
t133a01_base = T133A01Model(
"t133a01",
minimum_update_interval="30s",
data_rate="10MHz",
)
# Seeed reTerminal E1004 - 13.3" 6-color e-paper (1200x1600, T133A01)
# Portrait orientation (1200 wide × 1600 tall), matching the Arduino
# Setup523 defines TFT_WIDTH=1200, TFT_HEIGHT=1600.
# CS and CS1 each receive half of each row's pixel data
# (300 bytes = 600 pixels per controller, for all 1600 rows).
Seeed_reTerminal_E1004 = t133a01_base.extend(
"Seeed-reTerminal-E1004",
width=1200,
height=1600,
cs_pin=10,
cs1_pin=2,
dc_pin=11,
reset_pin=38,
busy_pin={
"number": 13,
"inverted": True,
"mode": {"input": True},
},
enable_pin=12,
)
@@ -1,56 +0,0 @@
"""Waveshare Black/White/Red e-paper displays using UC8179 controller.
Supported models:
- waveshare-7.5in-bv2-bwr: 800x480 pixels (7.5" BWR display, EDP_7in5b_V2)
These displays use the UC8179 controller. Panel configuration is sent during
the INITIALISE state. Power-on is handled in the POWER_ON state, after data
transfer, so the state machine's built-in busy wait covers the power-on delay.
"""
from . import EpaperModel
class WaveshareBWR(EpaperModel):
"""EpaperModel class for Waveshare Black/White/Red displays using UC8179 controller."""
def __init__(self, name, **defaults):
super().__init__(name, "EPaperWaveshareBWR", **defaults)
def get_init_sequence(self, config):
"""Generate initialization sequence for UC8179 BWR displays.
Panel configuration only power-on is handled separately in power_on()
after data transfer, with the state machine busy-waiting before refresh.
"""
width, height = self.get_dimensions(config)
return (
# PANEL SETTING (KWR mode)
(0x00, 0x0F),
# RESOLUTION SETTING (width x height)
(
0x61,
(width >> 8) & 0xFF,
width & 0xFF,
(height >> 8) & 0xFF,
height & 0xFF,
),
# DUAL SPI MODE (disabled)
(0x15, 0x00),
# VCOM AND DATA INTERVAL SETTING
(0x50, 0x11, 0x07),
# TCON SETTING
(0x60, 0x22),
# RESOLUTION GATE SETTING
(0x65, 0x00, 0x00, 0x00, 0x00),
)
# Model: Waveshare 7.5" V2 BWR (EDP_7in5b_V2) — 800x480, UC8179 controller
WaveshareBWR(
"waveshare-7.5in-bv2-bwr",
width=800,
height=480,
data_rate="10MHz",
minimum_update_interval="30s",
)
+10 -147
View File
@@ -11,7 +11,6 @@ from typing import Any
from esphome import yaml_util
import esphome.codegen as cg
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
@@ -30,7 +29,6 @@ from esphome.const import (
CONF_PATH,
CONF_PLATFORM_VERSION,
CONF_PLATFORMIO_OPTIONS,
CONF_PROJECT,
CONF_REF,
CONF_SAFE_MODE,
CONF_SIZE,
@@ -109,9 +107,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_KEY_ID = "key_id"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_NVS_ENCRYPTION = "nvs_encryption"
CONF_RELEASE = "release"
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
CONF_SIGNING_KEY = "signing_key"
@@ -169,20 +165,6 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
# NVS encryption (HMAC peripheral scheme) is only available on variants that
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
# should be added here.
NVS_ENCRYPTION_HMAC_VARIANTS = {
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
}
COMPILER_OPTIMIZATIONS = {
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
@@ -1116,50 +1098,6 @@ def _detect_variant(value):
return value
def _ota_downgrade_protection_errors(
project_version: str | None, signed_ota_enabled: bool
) -> list[cv.Invalid]:
"""Validate prerequisites for OTA downgrade protection.
Called only when the feature is enabled. Returns a ``cv.Invalid`` for each
unmet requirement: a dotted-numeric project version (the firmware version
compared on-device) and signed OTA (so the embedded version cannot be
forged).
"""
path = [CONF_FRAMEWORK, CONF_ADVANCED, CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]
errs: list[cv.Invalid] = []
if not project_version:
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires a "
f"'{CONF_PROJECT}' with a '{CONF_VERSION}' to be set in the "
f"'{CONF_ESPHOME}' section; this version is the firmware version "
"compared during OTA.",
path=path,
)
)
elif not re.fullmatch(r"\d+(\.\d+)*", project_version):
# The on-device comparison parses dotted-numeric versions only.
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires the "
f"'{CONF_PROJECT}' '{CONF_VERSION}' to be dotted-numeric (such "
f"as '1.2.3'), got '{project_version}'.",
path=path,
)
)
if not signed_ota_enabled:
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires "
f"'{CONF_SIGNED_OTA_VERIFICATION}' to be enabled; without signed "
"OTA the embedded version cannot be trusted.",
path=path,
)
)
return errs
def final_validate(config):
# Imported locally to avoid circular import issues
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
@@ -1365,37 +1303,6 @@ def final_validate(config):
"Binaries will NOT be signed automatically during build. "
"You must sign them externally before flashing."
)
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
variant = config[CONF_VARIANT]
if variant in NVS_ENCRYPTION_HMAC_VARIANTS:
_LOGGER.warning(
"NVS encryption will burn an HMAC key into eFuse key block %d on the "
"first boot of each device. This is PERMANENT and IRREVERSIBLE: "
"the block cannot be erased or reused afterwards. Enabling (or "
"later disabling) encryption also wipes any previously saved "
"preferences once, because the older data can no longer be read.",
nvs_enc[CONF_KEY_ID],
)
else:
supported = ", ".join(
sorted(VARIANT_FRIENDLY[v] for v in NVS_ENCRYPTION_HMAC_VARIANTS)
)
errs.append(
cv.Invalid(
f"NVS encryption (HMAC scheme) is not supported on "
f"{VARIANT_FRIENDLY[variant]} (it has no HMAC peripheral). "
f"Supported variants: {supported}.",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_ENCRYPTION],
)
)
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
project = full_config[CONF_ESPHOME].get(CONF_PROJECT)
errs.extend(
_ota_downgrade_protection_errors(
project[CONF_VERSION] if project else None,
bool(advanced.get(CONF_SIGNED_OTA_VERIFICATION)),
)
)
if errs:
raise cv.MultipleInvalid(errs)
@@ -1578,20 +1485,16 @@ FRAMEWORK_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO),
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_RELEASE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict,
cv.Optional(CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict,
cv.Optional(
CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY
): _parse_pio_platform_version,
cv.Optional(
CONF_SDKCONFIG_OPTIONS, default={}, visibility=cv.Visibility.YAML_ONLY
): {cv.string_strict: cv.string_strict},
cv.Optional(CONF_RELEASE): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_pio_platform_version,
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
cv.string_strict: cv.string_strict
},
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
*LOG_LEVELS_IDF, upper=True
),
cv.Optional(
CONF_ADVANCED, default={}, visibility=cv.Visibility.YAML_ONLY
): cv.Schema(
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
*ASSERTION_LEVELS, upper=True
@@ -1637,9 +1540,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
min=8192, max=32768
),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
cv.Optional(
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, default=False
): cv.boolean,
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
cv.Schema(
{
@@ -1652,15 +1552,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
),
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
),
cv.Optional(CONF_NVS_ENCRYPTION): cv.Schema(
{
# eFuse key block (0-5) that stores the HMAC key from
# which the NVS encryption keys are derived. The block is
# written on first boot if empty -- an irreversible
# operation -- so it must be chosen explicitly.
cv.Required(CONF_KEY_ID): cv.int_range(min=0, max=5),
}
),
cv.Optional(
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
): cv.boolean,
@@ -1681,9 +1572,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
}
),
cv.Optional(
CONF_COMPONENTS, default=[], visibility=cv.Visibility.YAML_ONLY
): cv.ensure_list(
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.All(
cv.Any(
cv.All(cv.string_strict, _parse_idf_component),
@@ -1783,7 +1672,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FLASH_FREQUENCY): cv.one_of(
*FLASH_FREQUENCIES, upper=True
),
cv.Optional(CONF_PARTITIONS, visibility=cv.Visibility.YAML_ONLY): cv.Any(
cv.Optional(CONF_PARTITIONS): cv.Any(
cv.file_,
cv.ensure_list(
cv.All(
@@ -1807,9 +1696,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
cv.Optional(
CONF_TOOLCHAIN, visibility=cv.Visibility.ADVANCED
): _validate_toolchain,
cv.Optional(CONF_TOOLCHAIN): _validate_toolchain,
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)),
@@ -2471,16 +2358,6 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
cg.add_define("USE_OTA_ROLLBACK")
# Enable software OTA downgrade protection. Embed the project version into
# the image's esp_app_desc_t so the OTA backend can compare it against the
# running version (final_validate guarantees a dotted-numeric project
# version and that signed OTA is enabled).
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
project_version = CORE.config[CONF_ESPHOME][CONF_PROJECT][CONF_VERSION]
add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER_FROM_CONFIG", True)
add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER", project_version)
cg.add_define("USE_OTA_DOWNGRADE_PROTECTION")
# Enable signed app verification without hardware secure boot
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True)
@@ -2507,20 +2384,6 @@ async def to_code(config):
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
# Encrypt NVS using the HMAC peripheral scheme. The NVS encryption keys are
# derived at runtime from an HMAC key stored in the configured eFuse block
# (no flash encryption required). The HMAC key is generated and burned into
# the eFuse block on first boot if it is empty. With the scheme selected,
# nvs_sec_provider registers it at startup and the default nvs_flash_init()
# (used in esp32/preferences.cpp) transparently performs the secure init, so
# no C++ changes are needed.
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True)
add_idf_sdkconfig_option("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC", True)
add_idf_sdkconfig_option(
"CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID", nvs_enc[CONF_KEY_ID]
)
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
cg.add_define(
@@ -11,11 +11,8 @@ class ESP32PreferenceBackend final {
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t key{0};
uint32_t nvs_handle{0}; // NVS (flash) path
uint16_t rtc_offset{0}; // RTC path: word offset into the RTC storage region
uint8_t length_words{0}; // RTC path: data length in 32-bit words
bool in_flash{true}; // true: store in NVS (flash); false: store in RTC memory
uint32_t key;
uint32_t nvs_handle;
};
class ESP32Preferences;
-103
View File
@@ -3,10 +3,7 @@
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
#include <esp_attr.h>
#include <nvs_flash.h>
#include <soc/soc_caps.h>
#include <cstring>
#include <vector>
@@ -21,48 +18,6 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// RTC memory backend for preferences requested with in_flash=false. Survives deep sleep and
// software/CPU resets, but not power loss; integrity is guarded by a per-record checksum so
// power-on garbage is detected on load. Keep this small: RTC memory is scarce and shared.
//
// Only compiled in when USE_ESP32_RTC_PREFERENCES_STORAGE is set (see preferences.h): the storage
// buffer reserves RTC memory, so it exists only when some config option actually selected RTC
// storage AND the variant has RTC memory (the ESP32-C2 and -C61 have none, so RTC_NOINIT_ATTR would
// have no section to land in and fail to link). Otherwise in_flash=false transparently falls back
// to NVS (see make_preference below).
//
// On variants with only RTC fast memory (C3/C6/H2/P4/C5/...) RTC_NOINIT_ATTR lands in RTC fast memory.
// This is still safe: the linker reserves .rtc_noinit ahead of any RTC-fast-as-heap pool
// (CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP), and IDF keeps the RTC fast power domain on in deep
// sleep (forced on whether or not it is used as heap), so the data is retained across both resets and
// deep sleep -- only power loss clears it.
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
static constexpr size_t RTC_PREF_SIZE_WORDS = 64; // 256 bytes
static constexpr size_t RTC_PREF_MAX_WORDS = 255; // length_words field is a uint8_t
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static RTC_NOINIT_ATTR uint32_t s_rtc_storage[RTC_PREF_SIZE_WORDS];
static bool save_to_rtc(uint16_t offset, uint32_t key, uint8_t length_words, const uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != length_words)
return false;
const size_t buffer_size = static_cast<size_t>(length_words) + 1;
if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
return false;
rtc_pref_encode(&s_rtc_storage[offset], key, length_words, data, len);
return true;
}
static bool load_from_rtc(uint16_t offset, uint32_t key, uint8_t length_words, uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != length_words)
return false;
const size_t buffer_size = static_cast<size_t>(length_words) + 1;
if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
return false;
return rtc_pref_decode(&s_rtc_storage[offset], key, length_words, data, len);
}
#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
// open() runs from app_main() before the logger is initialized, so any failure
// must be deferred until after global_logger is set. This is emitted from the
// first make_preference() call, which runs from the generated setup() after
@@ -70,10 +25,6 @@ static bool load_from_rtc(uint16_t offset, uint32_t key, uint8_t length_words, u
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!this->in_flash)
return save_to_rtc(this->rtc_offset, this->key, this->length_words, data, len);
#endif
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
@@ -90,10 +41,6 @@ bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
}
bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!this->in_flash)
return load_from_rtc(this->rtc_offset, this->key, this->length_words, data, len);
#endif
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
@@ -147,26 +94,6 @@ void ESP32Preferences::open() {
}
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!in_flash)
return this->make_rtc_preference_(length, type);
#else
if (!in_flash) {
// RTC storage is not compiled in (no config option selected it), so this request
// falls back to NVS -- the historic ESP32 behavior. Warn once so callers explicitly
// asking for RTC storage can discover the fallback.
static bool warned = false;
if (!warned) {
ESP_LOGW(TAG, "RTC preference storage not compiled in; using NVS (enable with 'preferences: rtc_storage: true')");
warned = true;
}
}
#endif
// in_flash, or RTC storage not compiled in: fall back to NVS.
return this->make_preference(length, type);
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
if (s_open_err != ESP_OK) {
if (this->nvs_handle == 0) {
@@ -179,34 +106,10 @@ ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t ty
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
pref->in_flash = true;
return ESPPreferenceObject(pref);
}
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
ESPPreferenceObject ESP32Preferences::make_rtc_preference_(size_t length, uint32_t type) {
const uint32_t length_words = rtc_pref_bytes_to_words(length);
if (length_words > RTC_PREF_MAX_WORDS) {
ESP_LOGE(TAG, "RTC preference too large: %" PRIu32 " words", length_words);
return {};
}
const uint32_t total_words = length_words + 1; // +1 for checksum
if (static_cast<size_t>(this->current_rtc_offset_) + total_words > RTC_PREF_SIZE_WORDS) {
ESP_LOGE(TAG, "RTC preference storage full, cannot allocate %" PRIu32 " words", total_words);
return {};
}
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->key = type;
pref->in_flash = false;
pref->rtc_offset = this->current_rtc_offset_;
pref->length_words = static_cast<uint8_t>(length_words);
this->current_rtc_offset_ += static_cast<uint16_t>(total_words);
return ESPPreferenceObject(pref);
}
#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
bool ESP32Preferences::sync() {
if (s_pending_save.empty())
return true;
@@ -283,12 +186,6 @@ bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save,
bool ESP32Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
// Invalidate RTC-backed preferences too (checksum will no longer match). current_rtc_offset_ is
// deliberately left alone: existing backends keep pointing at their allocated slots, and reset()
// is always followed by a restart (same reason nvs_handle is zeroed below).
memset(s_rtc_storage, 0, sizeof(s_rtc_storage));
#endif
nvs_flash_deinit();
nvs_flash_erase();
+3 -18
View File
@@ -2,15 +2,6 @@
#ifdef USE_ESP32
#include "esphome/core/preference_backend.h"
#include <soc/soc_caps.h>
// RTC-backed preference storage is compiled in only when a config option actually selects it
// (USE_ESP32_RTC_PREFERENCES, emitted during code generation) and the variant has RTC memory
// (SOC_RTC_MEM_SUPPORTED; the ESP32-C2 and -C61 have none). Otherwise in_flash=false falls
// back to NVS and no RTC memory is reserved.
#if defined(USE_ESP32_RTC_PREFERENCES) && SOC_RTC_MEM_SUPPORTED
#define USE_ESP32_RTC_PREFERENCES_STORAGE
#endif
namespace esphome::esp32 {
@@ -20,8 +11,9 @@ class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
public:
using PreferencesMixin<ESP32Preferences>::make_preference;
void open();
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash);
// Two-argument form defaults to NVS (flash) storage, preserving historic ESP32 behavior.
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) {
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type);
bool sync();
bool reset();
@@ -30,13 +22,6 @@ class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str);
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
// RTC-backed storage (in_flash=false).
ESPPreferenceObject make_rtc_preference_(size_t length, uint32_t type);
// Next free word offset in the RTC storage region (bump allocated in make_preference order).
uint16_t current_rtc_offset_{0};
#endif
};
void setup_preferences();
+5 -47
View File
@@ -9,8 +9,6 @@
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#else
#include "esphome/components/watchdog/watchdog.h"
#include <cinttypes>
extern "C" {
#include <esp_hosted.h>
#include <esp_hosted_misc.h>
@@ -35,19 +33,6 @@ namespace esphome::esp32_ble {
static const char *const TAG = "esp32_ble";
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
// Bringing up the remote BT controller issues synchronous RPCs to the
// co-processor with 5 second response timeouts, and the default task watchdog
// is also 5 seconds. If the co-processor firmware does not answer (for example
// factory firmware without Bluetooth support), the watchdog would reboot the
// device before the RPC could return an error, causing a boot loop. Raise the
// watchdog for the duration of the bring-up so failures surface as error
// returns instead. 60 seconds covers the worst case: transport reconnect
// (up to ~20s), version preflight (1s), controller init/enable (5s each) and
// the bluedroid host bring-up over the hosted HCI transport.
static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
#endif
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
#define GAP_SCAN_COMPLETE_EVENTS \
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
@@ -179,9 +164,6 @@ void ESP32BLE::advertising_init_() {
bool ESP32BLE::ble_setup_() {
esp_err_t err;
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
#endif
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller
@@ -210,35 +192,15 @@ bool ESP32BLE::ble_setup_() {
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
if (esp_hosted_connect_to_slave() != ESP_OK) { // NOLINT
ESP_LOGE(TAG, "Co-processor transport failed; BLE disabled");
return false;
}
// Fast preflight (1 second RPC timeout): verifies the co-processor answers
// RPCs at all before the 5 second timeout BT controller RPCs below, and
// before hosted_hci_bluedroid_open(), which aborts if the transport is down.
esp_hosted_coprocessor_fwver_t fw_ver{};
if (esp_hosted_get_coprocessor_fwversion(&fw_ver) != ESP_OK) {
ESP_LOGE(TAG, "Co-processor not responding; BLE disabled. Update its firmware with the esp32_hosted "
"update component");
return false;
}
ESP_LOGD(TAG, "Co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32, fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
esp_hosted_connect_to_slave(); // NOLINT
if (esp_hosted_bt_controller_init() != ESP_OK) {
ESP_LOGE(TAG,
"BT controller init failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
return false;
}
if (esp_hosted_bt_controller_enable() != ESP_OK) {
ESP_LOGE(TAG,
"BT controller enable failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
return false;
}
@@ -370,10 +332,6 @@ bool ESP32BLE::ble_setup_() {
}
bool ESP32BLE::ble_dismantle_() {
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
// Same 5 second RPCs as the bring-up path; see HOSTED_BT_WDT_TIMEOUT_MS
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
#endif
esp_err_t err = esp_bluedroid_disable();
if (err != ESP_OK) {
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
@@ -419,12 +377,12 @@ bool ESP32BLE::ble_dismantle_() {
}
#else
if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGE(TAG, "esp_hosted_bt_controller_disable failed");
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
return false;
}
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
ESP_LOGE(TAG, "esp_hosted_bt_controller_deinit failed");
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
return false;
}
@@ -18,8 +18,6 @@ from esphome.const import (
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
# esp32_ble raises the task watchdog around the remote BT controller bring-up
AUTO_LOAD = ["watchdog"]
CONF_ACTIVE_HIGH = "active_high"
CONF_BUS_WIDTH = "bus_width"
@@ -7,10 +7,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_PROVISIONING
#include "esphome/components/provisioning/provisioning.h"
#endif
#ifdef USE_ESP32
namespace esphome::esp32_improv {
@@ -45,15 +41,6 @@ void ESP32ImprovComponent::setup() {
#endif
global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
#ifdef USE_PROVISIONING
if (provisioning::global_provisioning_manager != nullptr) {
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
ESP_LOGD(TAG, "Provisioning window closed; stopping Improv");
this->stop();
});
}
#endif
// Start with loop disabled - will be enabled by start() when needed
this->disable_loop();
}
@@ -295,15 +282,6 @@ void ESP32ImprovComponent::start() {
if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
return;
#ifdef USE_PROVISIONING
// Don't (re)start advertising once the provisioning window has closed - e.g. when
// wifi tries to restart Improv after the window expired at runtime.
if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) {
ESP_LOGD(TAG, "Provisioning window closed; not starting Improv");
return;
}
#endif
ESP_LOGD(TAG, "Setting Improv to start");
this->should_start_ = true;
this->enable_loop();
@@ -360,15 +338,6 @@ void ESP32ImprovComponent::process_incoming_data_() {
this->incoming_data_.clear();
return;
}
#ifdef USE_PROVISIONING
if (provisioning::global_provisioning_manager != nullptr &&
provisioning::global_provisioning_manager->closed()) {
ESP_LOGW(TAG, "Provisioning window closed; refusing settings");
this->set_error_(improv::ERROR_NOT_AUTHORIZED);
this->incoming_data_.clear();
return;
}
#endif
if (wifi::global_wifi_component->is_disabled()) {
// Wi-Fi is disabled, so we can't provision. Respond immediately
// instead of letting the client wait out its provisioning timeout.
+2 -20
View File
@@ -202,12 +202,8 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(
CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY
): cv.string_strict,
cv.Optional(
CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY
): _parse_platform_version,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
}
),
_arduino_check_versions,
@@ -314,14 +310,6 @@ async def to_code(config):
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
cg.add_build_flag("-DNEW_OOM_ABORT")
# 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.
cg.add_platformio_option(
"build_src_flags", "-include esphome/components/esp8266/throw_stubs.h"
)
# In testing mode, fake larger memory to allow linking grouped component tests
# Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
# we pretend it has much larger memory to test that components compile together
@@ -336,12 +324,6 @@ async def to_code(config):
for symbol in ("vprintf", "printf", "fprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap the lwIP2 glue's do-nothing dhcp_cleanup()/dhcp_release() stubs so the
# linker can drop their "STUB: ..." message strings from DRAM.
# See lwip_glue_stubs.cpp for implementation.
for symbol in ("dhcp_cleanup", "dhcp_release"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
# implementation in the Arduino ESP8266 core.
+1 -1
View File
@@ -12,7 +12,7 @@ namespace esphome {
uint32_t random_uint32() { return os_random(); }
bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2) is set,
// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2040) is set,
// independent of the ESPHOME_THREAD_SINGLE thread model define.
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
@@ -1,35 +0,0 @@
/*
* Linker wrap stubs for the lwIP2 glue's dead DHCP entry points.
*
* The ESP8266 SDK blobs call dhcp_cleanup() and dhcp_release() when the
* station leaves an access point (cnx_sta_leave, wifi_station_dhcpc_stop).
* In the prebuilt lwIP2 glue (liblwip2-*.a, glue-esp/lwip-esp.c) these are
* stubs whose only effect is printing "STUB: dhcp_cleanup" and
* "STUB: dhcp_release"; the real DHCP teardown happens through lwIP2's
* renamed dhcp_cleanup_LWIP2()/dhcp_release_LWIP2() functions.
*
* On ESP8266 .rodata lives in DRAM, so those message strings waste scarce
* RAM. Wrapping the stubs with silent equivalents lets the linker garbage
* collect the glue stub bodies together with their strings.
*
* Saves 38 bytes of RAM and removes the "STUB:" log noise on Wi-Fi
* disconnect. Behavior is otherwise unchanged.
*/
#if defined(USE_ESP8266)
namespace esphome::esp8266 {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// The callers are closed-source SDK blobs; the netif argument is unused.
void __wrap_dhcp_cleanup(void * /*netif*/) {}
// The glue stub returns ERR_ABRT (-8; lwIP 1.4 err_t is a signed char).
signed char __wrap_dhcp_release(void * /*netif*/) { return -8; }
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP8266
+21 -7
View File
@@ -8,7 +8,6 @@ extern "C" {
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
#include <cstring>
@@ -81,6 +80,16 @@ static uint32_t get_esp8266_flash_sector() {
}
static uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; }
static inline size_t bytes_to_words(size_t bytes) { return (bytes + 3) / 4; }
template<class It> uint32_t calculate_crc(It first, It last, uint32_t type) {
uint32_t crc = type;
while (first != last) {
crc ^= (*first++ * 2654435769UL) >> 1;
}
return crc;
}
static bool save_to_flash(size_t offset, const uint32_t *data, size_t len) {
for (uint32_t i = 0; i < len; i++) {
uint32_t j = offset + i;
@@ -128,19 +137,21 @@ static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != this->length_words)
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
rtc_pref_encode(buffer, this->type, this->length_words, data, len);
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
}
bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != this->length_words)
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
@@ -150,7 +161,10 @@ bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
return rtc_pref_decode(buffer, this->type, this->length_words, data, len);
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
}
void ESP8266Preferences::setup() {
@@ -163,13 +177,13 @@ void ESP8266Preferences::setup() {
}
ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
const uint32_t length_words = rtc_pref_bytes_to_words(length);
const uint32_t length_words = bytes_to_words(length);
if (length_words > MAX_PREFERENCE_WORDS) {
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
return {};
}
const uint32_t total_words = length_words + 1; // +1 for checksum
const uint32_t total_words = length_words + 1; // +1 for CRC
uint16_t offset;
if (in_flash) {
-41
View File
@@ -1,41 +0,0 @@
#pragma once
/*
* Inline overrides for std::__throw_* helpers (ESP8266).
*
* ESP8266 Arduino compiles with -fno-exceptions and ships a libstdc++ whose
* std::__throw_* functions already just call abort() -- they never read their
* const char* message argument. But the compiler still emits the message load
* at every throw site (inside header-instantiated std::string / std::vector
* code), so --gc-sections keeps those libstdc++ error strings alive. On
* ESP8266 .rodata lives in DRAM, so each one wastes scarce RAM (e.g.
* "basic_string::_M_construct null not valid", "basic_string::_M_create",
* "cannot create std::vector larger than max_size()", "array::at: ...").
*
* Providing inline definitions here lets GCC see the message argument is
* unused, dead-strip the load, and drop the string entirely -- no LTO needed.
* Behavior is identical to today: a bare abort() (the message was never
* printed). This header MUST be force-included before <string>, so it is
* wired up via build_src_flags "-include ..." in this component's __init__.py.
*
* Note: this defines functions in namespace std (technically UB). It is safe
* here because the definitions match the existing abort() behavior exactly.
*/
#ifdef __cplusplus
// Empty namespace so the CI namespace check is satisfied; the overrides below
// must live in namespace std, so they cannot go in the component namespace.
namespace esphome::esp8266 {} // namespace esphome::esp8266
// NOLINTBEGIN(bugprone-reserved-identifier,bugprone-std-namespace-modification,cert-dcl37-c,cert-dcl51-cpp,cert-dcl58-cpp,readability-identifier-naming)
namespace std {
__attribute__((__noreturn__)) inline void __throw_logic_error(const char *) { __builtin_abort(); }
__attribute__((__noreturn__)) inline void __throw_length_error(const char *) { __builtin_abort(); }
__attribute__((__noreturn__)) inline void __throw_out_of_range(const char *) { __builtin_abort(); }
__attribute__((__noreturn__)) inline void __throw_out_of_range_fmt(const char *, ...) { __builtin_abort(); }
} // namespace std
// NOLINTEND(bugprone-reserved-identifier,bugprone-std-namespace-modification,cert-dcl37-c,cert-dcl51-cpp,cert-dcl58-cpp,readability-identifier-naming)
#endif // __cplusplus
+1 -1
View File
@@ -126,7 +126,7 @@ CONFIG_SCHEMA = cv.All(
CONF_PORT,
esp8266=8266,
esp32=3232,
rp2=2040,
rp2040=2040,
bk72xx=8892,
ln882x=8820,
rtl87xx=8892,
@@ -8,7 +8,7 @@
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_esp8266.h"
#include "esphome/components/ota/ota_backend_arduino_libretiny.h"
#include "esphome/components/ota/ota_backend_arduino_rp2.h"
#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota/ota_backend_esp_idf.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
@@ -94,12 +94,11 @@ void ESPHomeOTAComponent::setup() {
}
void ESPHomeOTAComponent::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Over-The-Air updates:\n"
" Address: %s:%u\n"
" Version: %d",
network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION);
network::get_use_address(), this->port_, USE_OTA_VERSION);
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
ESP_LOGCONFIG(TAG, " Password configured");
+3 -29
View File
@@ -41,7 +41,7 @@ DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action)
ESPNowHandlerTrigger = automation.Trigger.template(
ESPNowRecvInfoConstRef,
cg.uint8.operator("const").operator("ptr"),
cg.uint16,
cg.uint8,
)
OnUnknownPeerTrigger = espnow_ns.class_(
@@ -56,20 +56,6 @@ OnBroadcastTrigger = espnow_ns.class_(
CONF_AUTO_ADD_PEER = "auto_add_peer"
CONF_MAX_PAYLOAD_SIZE = "max_payload_size"
# Payload limits of ESP-NOW v1 and v2 frames. The radio negotiates the
# protocol version per peer on its own; the option only sizes this device's
# packet buffers, whose static RAM cost is proportional to it (~8 KB at 250
# bytes, ~44 KB at 1470).
ESPNOW_PAYLOAD_V1 = 250
ESPNOW_PAYLOAD_V2 = 1470
# Config-time cap for action payloads. The per-device limit is the
# ``max_payload_size`` option, which the action schema cannot see; send()
# enforces it at runtime.
MAX_ESPNOW_PACKET_SIZE = ESPNOW_PAYLOAD_V2
CONF_PEERS = "peers"
CONF_ON_SENT = "on_sent"
CONF_ON_UNKNOWN_PEER = "on_unknown_peer"
@@ -77,15 +63,7 @@ CONF_ON_BROADCAST = "on_broadcast"
CONF_CONTINUE_ON_ERROR = "continue_on_error"
CONF_WAIT_FOR_SENT = "wait_for_sent"
def _validate_max_payload_size(value: int) -> int:
if value > ESPNOW_PAYLOAD_V1:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 0),
esp32_arduino=cv.Version(3, 2, 0),
extra_message="ESP-NOW v2 frames need an ESP-NOW v2 capable framework",
)(value)
return value
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
def validate_channel(value):
@@ -100,9 +78,6 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(ESPNowComponent),
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_MAX_PAYLOAD_SIZE, default=ESPNOW_PAYLOAD_V1): cv.All(
cv.int_range(min=1, max=ESPNOW_PAYLOAD_V2), _validate_max_payload_size
),
cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean,
cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address),
cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation(
@@ -138,7 +113,7 @@ async def _trigger_to_code(config):
[
(ESPNowRecvInfoConstRef, "info"),
(cg.uint8.operator("const").operator("ptr"), "data"),
(cg.uint16, "size"),
(cg.uint8, "size"),
],
config,
)
@@ -150,7 +125,6 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add_define("USE_ESPNOW")
cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE])
if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel))
+6 -6
View File
@@ -119,7 +119,7 @@ template<typename... Ts> class SetChannelAction final : public Action<Ts...>, pu
}
};
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowReceivedPacketHandler {
public:
explicit OnReceiveTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
@@ -128,7 +128,7 @@ class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint
explicit OnReceiveTrigger() {}
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -141,15 +141,15 @@ class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint
bool has_address_{false};
uint8_t address_[ESP_NOW_ETH_ALEN]{};
};
class OnUnknownPeerTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
class OnUnknownPeerTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowUnknownPeerHandler {
public:
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
this->trigger(info, data, size);
return false; // Return false to continue processing other internal handlers
}
};
class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowBroadcastHandler {
public:
explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
@@ -157,7 +157,7 @@ class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const ui
}
explicit OnBroadcastTrigger() {}
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -4,7 +4,6 @@
#include "espnow_err.h"
#include <algorithm>
#include <cinttypes>
#include "esphome/core/application.h"
@@ -97,9 +96,9 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
// Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
// v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
// but the receive buffer only fits v2 frames with ``max_payload_size``; copying a
// larger frame would overflow packet_.receive.data.
if (size < 0 || size > ESPNOW_MAX_DATA_LEN) {
// but our receive buffer is ESP_NOW_MAX_DATA_LEN (250 B); copying a larger
// frame would overflow packet_.receive.data.
if (size < 0 || size > ESP_NOW_MAX_DATA_LEN) {
global_esp_now->receive_packet_queue_.increment_dropped_count();
return;
}
@@ -286,14 +285,11 @@ void ESPNowComponent::loop() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Cap the hex dump at a v1 frame: a full v2 frame would need a
// ~4.4 KB stack buffer.
char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)];
format_mac_addr_upper(info.src_addr, src_buf);
format_mac_addr_upper(info.des_addr, dst_buf);
ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf,
format_hex_pretty_to(hex_buf, packet->packet_.receive.data,
std::min<uint16_t>(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN)));
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcast_handlers_) {
@@ -366,7 +362,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
return ESP_ERR_ESPNOW_PEER_NOT_SET;
} else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
return ESP_ERR_ESPNOW_OWN_ADDRESS;
} else if (size > ESPNOW_MAX_DATA_LEN) {
} else if (size > ESP_NOW_MAX_DATA_LEN) {
return ESP_ERR_ESPNOW_DATA_SIZE;
} else if (!esp_now_is_peer_exist(peer_address)) {
if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
+3 -3
View File
@@ -62,7 +62,7 @@ class ESPNowUnknownPeerHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
/// Handler interface for receiving ESPNow packets
@@ -74,7 +74,7 @@ class ESPNowReceivedPacketHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
/// Handler interface for receiving ESPNow broadcast packets
/// Components should inherit from this class to handle incoming ESPNow data
@@ -85,7 +85,7 @@ class ESPNowBroadcastHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
class ESPNowComponent final : public Component {
+9 -26
View File
@@ -19,23 +19,6 @@ namespace esphome::espnow {
static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
// Maximum payload this component sends and receives, from the
// ``max_payload_size`` option. The radio stack speaks ESP-NOW v2 regardless
// (negotiated per peer); payloads beyond the v1 limit (250 bytes) are opt-in
// because the packet pools are statically sized from this, so their RAM cost
// is proportional (~8 KB at 250 bytes, ~44 KB at the v2 limit of 1470).
#ifndef USE_ESPNOW_MAX_PAYLOAD_SIZE
#define USE_ESPNOW_MAX_PAYLOAD_SIZE ESP_NOW_MAX_DATA_LEN
#endif
static constexpr uint16_t ESPNOW_MAX_DATA_LEN = USE_ESPNOW_MAX_PAYLOAD_SIZE;
#ifdef ESP_NOW_MAX_DATA_LEN_V2
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN_V2,
"espnow max_payload_size cannot exceed the ESP-NOW v2 frame limit");
#else
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN,
"espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)");
#endif
struct WifiPacketRxControl {
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
@@ -95,10 +78,10 @@ class ESPNowPacket {
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct received_data {
ESPNowRecvInfo info; // Information about the received packet
uint8_t data[ESPNOW_MAX_DATA_LEN]; // Data received in the packet
uint16_t size; // Size of the received data
WifiPacketRxControl rx_ctrl; // Status of the received packet
ESPNowRecvInfo info; // Information about the received packet
uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet
uint8_t size; // Size of the received data
WifiPacketRxControl rx_ctrl; // Status of the received packet
} receive;
// NOLINTNEXTLINE(readability-identifier-naming)
@@ -161,15 +144,15 @@ class ESPNowSendPacket {
this->callback_ = nullptr; // Reset callback
}
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
uint8_t data_[ESPNOW_MAX_DATA_LEN]{0}; // Data to send
uint16_t size_{0}; // Size of the data to send, must be <= ESPNOW_MAX_DATA_LEN
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send
uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
private:
void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN);
if (size > ESPNOW_MAX_DATA_LEN) {
if (size > ESP_NOW_MAX_DATA_LEN) {
this->size_ = 0;
return;
}
@@ -42,8 +42,8 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
return;
}
if (buf.size() > ESPNOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %u)", buf.size(), (unsigned) ESPNOW_MAX_DATA_LEN);
if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
return;
}
@@ -55,8 +55,8 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
});
}
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, info.src_addr[0],
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
if (data == nullptr || size == 0) {
@@ -70,9 +70,9 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data
return false; // Allow other handlers to run
}
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size,
info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
if (data == nullptr || size == 0) {
ESP_LOGW(TAG, "Received empty or null broadcast packet");
@@ -24,12 +24,12 @@ class ESPNowTransport final : public packet_transport::PacketTransport,
}
// ESPNow handler interface
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override;
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
size_t get_max_packet_size() override { return ESPNOW_MAX_DATA_LEN; }
size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
bool should_send() override;
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
+15 -17
View File
@@ -4,7 +4,7 @@ import logging
from esphome import automation, pins
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.network import add_use_address, ip_address_literal
from esphome.components.network import ip_address_literal
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -179,11 +179,9 @@ _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"}
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"}
# RP2-supported ethernet types (SPI and PIO QSPI). Applies to the whole
# RP2 family (RP2040 and RP2350); the chip-specific W5100 caveat in the
# comment above is about ESP-IDF driver coverage, not the RP2 platform.
RP2_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"}
_RP2_SPI_LIBRARIES = {
# RP2040-supported ethernet types (SPI and PIO QSPI)
RP2040_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"}
_RP2040_SPI_LIBRARIES = {
"W5100": "lwIP_w5100",
"W5500": "lwIP_w5500",
"ENC28J60": "lwIP_enc28j60",
@@ -363,10 +361,10 @@ def _validate(config):
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
f"on ESP32 classic and ESP32-P4, not {variant}"
)
elif CORE.is_rp2 and config[CONF_TYPE] not in RP2_ETHERNET_TYPES:
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(sorted(RP2_ETHERNET_TYPES))} are supported on the RP2 "
f"platform, not {config[CONF_TYPE]}"
f"Only {', '.join(sorted(RP2040_ETHERNET_TYPES))} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
@@ -461,7 +459,7 @@ SPI_SCHEMA = cv.All(
}
),
),
cv.only_on([Platform.ESP32, Platform.RP2]),
cv.only_on([Platform.ESP32, Platform.RP2040]),
_validate_spi_interface,
)
@@ -475,13 +473,13 @@ CONFIG_SCHEMA = cv.All(
"JL1101": RMII_SCHEMA,
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
"W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W5500": SPI_SCHEMA,
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"ENC28J60": SPI_SCHEMA,
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2])),
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"LAN8670": RMII_SCHEMA,
"GENERIC": GENERIC_SCHEMA,
"YT8531": GENERIC_SCHEMA,
@@ -539,11 +537,11 @@ async def to_code(config):
if CORE.is_esp32:
await _to_code_esp32(var, config)
elif CORE.is_rp2:
elif CORE.is_rp2040:
await _to_code_rp2040(var, config)
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
add_use_address(var, config[CONF_USE_ADDRESS])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))
@@ -672,7 +670,7 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_define("USE_ETHERNET_SPI")
cg.add_library(_RP2_SPI_LIBRARIES[config[CONF_TYPE]], None)
cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None)
def _final_validate_rmii_pins(config: ConfigType) -> None:
@@ -754,7 +752,7 @@ _platform_filter = filter_source_files_from_platform(
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"ethernet_component_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
"ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"esp_eth_phy_jl1101.c": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
@@ -25,7 +25,7 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif
#endif // USE_ESP32
#ifdef USE_RP2
#ifdef USE_RP2040
#if defined(USE_ETHERNET_W5500)
#include <W5500lwIP.h>
#elif defined(USE_ETHERNET_W5100)
@@ -145,8 +145,6 @@ class EthernetComponent final : public Component {
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
void get_eth_mac_address_raw(uint8_t *mac);
@@ -184,14 +182,14 @@ class EthernetComponent final : public Component {
#endif // USE_ETHERNET_SPI
#endif // USE_ESP32
#ifdef USE_RP2
#ifdef USE_RP2040
void set_clk_pin(uint8_t clk_pin);
void set_miso_pin(uint8_t miso_pin);
void set_mosi_pin(uint8_t mosi_pin);
void set_cs_pin(uint8_t cs_pin);
void set_interrupt_pin(int8_t interrupt_pin);
void set_reset_pin(int8_t reset_pin);
#endif // USE_RP2
#endif // USE_RP2040
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
@@ -274,7 +272,7 @@ class EthernetComponent final : public Component {
esp_eth_phy_t *phy_{nullptr};
#endif // USE_ESP32
#ifdef USE_RP2
#ifdef USE_RP2040
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
#if defined(USE_ETHERNET_W5100)
static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time
@@ -303,7 +301,7 @@ class EthernetComponent final : public Component {
uint8_t cs_pin_;
int8_t interrupt_pin_{-1};
int8_t reset_pin_{-1};
#endif // USE_RP2
#endif // USE_RP2040
// Common members
#ifdef USE_ETHERNET_MANUAL_IP
@@ -348,7 +346,7 @@ class EthernetComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{nullptr};
const char *use_address_{""};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
@@ -839,7 +839,7 @@ void EthernetComponent::dump_connect_params_() {
case ETH_SPEED_100M:
link_speed = 100;
break;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 1, 0)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
case ETH_SPEED_1000M:
link_speed = 1000;
break;
@@ -1,12 +1,12 @@
#include "ethernet_component.h"
#if defined(USE_ETHERNET) && defined(USE_RP2)
#if defined(USE_ETHERNET) && defined(USE_RP2040)
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/rp2/gpio.h"
#include "esphome/components/rp2040/gpio.h"
#include <SPI.h>
#include <lwip/dns.h>
@@ -29,7 +29,7 @@ void EthernetComponent::setup() {
// Toggle reset pin if configured
if (this->reset_pin_ >= 0) {
rp2::RP2GPIOPin reset_pin;
rp2040::RP2040GPIOPin reset_pin;
reset_pin.set_pin(this->reset_pin_);
reset_pin.set_flags(gpio::FLAG_OUTPUT);
reset_pin.setup();
@@ -380,4 +380,4 @@ void EthernetComponent::disable() {
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_RP2
#endif // USE_ETHERNET && USE_RP2040
@@ -7,7 +7,7 @@
#include <cinttypes>
#if !defined(USE_RP2) && !defined(USE_HOST)
#if !defined(USE_RP2040) && !defined(USE_HOST)
namespace esphome::factory_reset {
@@ -73,4 +73,4 @@ void FactoryResetComponent::setup() {
} // namespace esphome::factory_reset
#endif // !defined(USE_RP2) && !defined(USE_HOST)
#endif // !defined(USE_RP2040) && !defined(USE_HOST)
@@ -3,7 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/preferences.h"
#if !defined(USE_RP2) && !defined(USE_HOST)
#if !defined(USE_RP2040) && !defined(USE_HOST)
#ifdef USE_ESP32
#include <esp_system.h>
@@ -32,4 +32,4 @@ class FactoryResetComponent final : public Component {
} // namespace esphome::factory_reset
#endif // !defined(USE_RP2) && !defined(USE_HOST)
#endif // !defined(USE_RP2040) && !defined(USE_HOST)
-1
View File
@@ -1 +0,0 @@
CODEOWNERS = ["@esphome/core"]

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