mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 00:12:11 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b2b16e664 | ||
|
|
7a4ffa98f6 | ||
|
|
eca4c2d0a5 | ||
|
|
43b6b41d16 | ||
|
|
b6d70d4e34 | ||
|
|
4af2db7e8a | ||
|
|
92059abf6c | ||
|
|
e6d124d229 | ||
|
|
0575eb14f2 | ||
|
|
c732903390 | ||
|
|
eaee1cdd86 | ||
|
|
964a54f7a7 | ||
|
|
3e5797d153 | ||
|
|
7e99301644 | ||
|
|
08387ad289 |
No files matched your search
@@ -1,116 +0,0 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Review guidance for ESPHome pull requests. Use this when reviewing a pull request that changes ESPHome Python, C++, or component code, to check it against the project's coding conventions, embedded-systems memory rules, testing requirements, and breaking-change policy.
|
||||
---
|
||||
|
||||
# Reviewing ESPHome pull requests
|
||||
|
||||
ESPHome parses YAML into C++ firmware for memory-constrained microcontrollers
|
||||
(ESP32, ESP8266, RP2040, LibreTiny). Review changes with that in mind: RAM and
|
||||
flash are scarce, and code runs unattended for months.
|
||||
|
||||
`AGENTS.md` in the repository root is the full contributor guide and the
|
||||
authority when it disagrees with this summary. The developer documentation at
|
||||
https://developers.esphome.io explains the component lifecycle and the reasoning
|
||||
behind these rules. This skill lists the concrete things worth flagging in a
|
||||
review; read `AGENTS.md` for the detail behind any item.
|
||||
|
||||
Only raise findings that the diff actually introduces or changes. Do not ask for
|
||||
drive-by cleanup of pre-existing code the PR did not touch.
|
||||
|
||||
## Memory and embedded constraints (highest value)
|
||||
|
||||
Heap allocation after `setup()` is treated as a reliability bug, not a
|
||||
performance nit, because it fragments a small shared heap. Flag:
|
||||
|
||||
- New heap allocation on a hot path or after setup that could be avoided.
|
||||
- `std::vector` where the size is known at compile time (use `std::array`, or
|
||||
`StaticVector<T, N>` when a `push_back` API is needed) or fixed at runtime
|
||||
init (use `FixedVector<T>`).
|
||||
- Listener / child-entity registration lists stored as `std::vector`; these have
|
||||
a compile-time-known count and should use `cg.slot_counter()` plus
|
||||
`StaticVector`.
|
||||
- `std::vector<uint8_t>` for a byte buffer that never grows: prefer
|
||||
`std::unique_ptr<uint8_t[]>` or `std::array`.
|
||||
- `std::map` / `std::set` / `std::unordered_map` for small datasets (1-16
|
||||
elements): a `std::vector` of a small struct with linear search is lighter.
|
||||
- `std::deque` anywhere: it allocates 512-byte blocks and should be avoided.
|
||||
- `std::string` storing a value set once from config: prefer `StringRef` (the
|
||||
literal already lives in flash).
|
||||
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
|
||||
a buffer or view API exists.
|
||||
|
||||
## C++ conventions
|
||||
|
||||
- Include what you use: a file referencing a symbol must include the header that
|
||||
declares it, even if it currently arrives transitively. New or changed uses of
|
||||
a symbol need the matching include.
|
||||
- Prefix all member access with `this->`.
|
||||
- Naming: `lower_snake_case` for functions/methods/variables, `UpperCamelCase`
|
||||
for classes/structs/enums, `UPPER_SNAKE_CASE` for namespace-scope constants,
|
||||
trailing underscore on protected/private fields.
|
||||
- `enum class` values must be prefixed with the enum name in `UPPER_SNAKE_CASE`
|
||||
(e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Bare names like `SUCCESS`,
|
||||
`FAIL`, or `OK` collide with SDK macros on some platforms and break the build.
|
||||
- Prefer `const`/`enum` over `#define`; `#define` is only for conditional
|
||||
compilation and code-generation sizes.
|
||||
- Never call `millis()` in a `loop()` body; use
|
||||
`App.get_loop_component_start_time()`. A rate-limit gate below ~16 ms (the loop
|
||||
period) does nothing.
|
||||
- Pick the timing primitive by cadence: gated `loop()` under 250 ms,
|
||||
`set_interval` at 500 ms and above.
|
||||
- Do not override a base method to return the value it already returns (e.g.
|
||||
`get_setup_priority()` returning `setup_priority::DATA`).
|
||||
- Wrap string literals passed as printf `%s` args in `LOG_STR_LITERAL()`.
|
||||
- Required, invariant dependencies should be constructor parameters, not setters.
|
||||
- Callback registration methods must be templated (`template<typename F>`), not
|
||||
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
|
||||
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
|
||||
|
||||
## Python conventions
|
||||
|
||||
- Type-annotate every new function signature (params and return), new dataclass
|
||||
fields, and new module-level variables. Import `ConfigType` from
|
||||
`esphome.types`.
|
||||
- Use the walrus operator to avoid a double lookup, e.g.
|
||||
`if (blah := config.get(CONF_BLAH)) is not None:`.
|
||||
- Reuse existing validators from `config_validation.py` (`cv.rename_key`,
|
||||
`cv.has_exactly_one_key`, etc.) via `cv.All(...)` instead of hand-rolling.
|
||||
- `esphome/const.py` is frozen: no new `CONF_` constants there. Define them in
|
||||
the component's own `.py`, or in `esphome/components/const/__init__.py` when
|
||||
shared. The same constant defined in three or more component files fails CI.
|
||||
- State that must persist during code generation goes in `CORE.data` namespaced
|
||||
under the component `DOMAIN` (a `@dataclass`), not module-level mutable globals.
|
||||
- Prefer callback-based triggers via `build_callback_automation()`; only use a
|
||||
`Trigger<Ts...>` subclass when the forwarder needs mutable state.
|
||||
|
||||
## Testing and coverage
|
||||
|
||||
- New and changed lines and branches need test coverage, including defensive
|
||||
early-returns, error paths, and no-op guards. A mocked-out function is not
|
||||
covered; exercise the real call path too.
|
||||
- Component YAML tests live in `tests/components/<component>/`. Never define
|
||||
buses (uart, i2c, spi, modbus) directly in a test file: pull them from
|
||||
`tests/test_build_components/common/` through dict-style `packages:` so CI can
|
||||
group builds. List-style packages or top-level merge keys block grouping.
|
||||
- Config-only checks use the `validate.*.yaml` prefix; compiled checks use
|
||||
`test.*.yaml`.
|
||||
|
||||
## Breaking changes and public API
|
||||
|
||||
- Base classes under `esphome/core/` and documented config options are public
|
||||
API. Undocumented `public` members of a component are internal.
|
||||
- A breaking change needs justification, a migration path in the PR description,
|
||||
and a deprecation window where feasible (`ESPDEPRECATED` in C++,
|
||||
`cv.rename_key(..., removed_in=...)` in Python). Changing a codegen-injected
|
||||
lambda signature is not a breaking change.
|
||||
|
||||
## Process and PR hygiene
|
||||
|
||||
- PR titles start with a `[tag]` prefix: the component name (e.g. `[uart] ...`)
|
||||
or `[core]` for shared code.
|
||||
- Prose in docs, comments, and commit messages should be plain English. Keep
|
||||
inline comments short and only where the code is not self-explanatory; do not
|
||||
restate what the code says.
|
||||
- Verify the PR fills out `.github/PULL_REQUEST_TEMPLATE.md` and adds
|
||||
`CODEOWNERS` entries for a new component.
|
||||
@@ -1 +0,0 @@
|
||||
../.agents/skills
|
||||
File renamed without changes.
@@ -9,29 +9,6 @@ body:
|
||||
If you have a feature request or enhancement, please [request them here instead][fr].
|
||||
|
||||
[fr]: https://github.com/orgs/esphome/discussions
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Use of AI in bug reports
|
||||
|
||||
AI tools are good at carrying out well-defined tasks, but they are not good at troubleshooting.
|
||||
Please do NOT paste an AI-generated wall of text into the issue template - if the AI hasn't solved
|
||||
your problem, its wild guesses are not likely to help.
|
||||
|
||||
Please DO include your own words and observations, compile/boot logs, and
|
||||
especially a minimal reproducible example of your YAML configuration that demonstrates the problem.
|
||||
|
||||
It is however quite acceptable to use AI to translate your *own* report,
|
||||
if you aren't a competent English speaker.
|
||||
|
||||
If you really think it will be useful to include an AI's analysis, preferably wrap it in a `<details>` block which will be collapsed by default.
|
||||
|
||||
If you are using AI to help solve a problem, rather than asking it to speculate about what the problem is,
|
||||
it can be more useful to ask it to create a step-by-step troubleshooting procedure.
|
||||
AI is also useful for generating boilerplate code, such as a minimal reproducible example of your YAML
|
||||
configuration that demonstrates the problem.
|
||||
|
||||
Used properly, AI can be a useful tool to help you solve your problem, but don't let it get in the way.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -42,7 +42,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
@@ -32,7 +32,7 @@ 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@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../.agents/skills
|
||||
@@ -29,7 +29,7 @@ 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@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Determine tag and whether to push
|
||||
id: tag
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
|
||||
@@ -49,7 +49,7 @@ 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@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -244,20 +244,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Read prek version from requirements_test.txt
|
||||
id: prek
|
||||
# requirements_test.txt is the only place the version is pinned, so a
|
||||
# Dependabot bump there is picked up here without a second edit.
|
||||
run: |
|
||||
if ! version=$(sed -nE 's/^prek==([^[:space:]#]+).*/\1/p' requirements_test.txt) || [ -z "$version" ]; then
|
||||
echo "::error::No prek== pin found in requirements_test.txt."
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
|
||||
with:
|
||||
prek-version: ${{ steps.prek.outputs.version }}
|
||||
# Keep in sync with requirements_test.txt.
|
||||
prek-version: "0.4.11"
|
||||
# This job only runs on pull requests, so nothing ever populates
|
||||
# the cache on dev. Every run would miss and then write a per-pull
|
||||
# request copy, which is what the old seed-cache job existed to
|
||||
@@ -321,7 +312,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -350,7 +341,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Report empty upload to Codecov
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
run_command: empty-upload
|
||||
force: true
|
||||
@@ -413,7 +404,7 @@ 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@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1274,7 +1265,7 @@ 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@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
permissions:
|
||||
issues: write # issues.lock on closed issues
|
||||
pull-requests: write # issues.lock on closed pull requests
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # 2026.9.0
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@0fdd5e311b7e744069166696072a1a9cbc5fbeb6 # 2026.8.1
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
|
||||
# GitHub App token so the labels, comments and closures come from
|
||||
# esphome[bot] instead of github-actions[bot].
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 # main
|
||||
secrets:
|
||||
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
and will be closed if no further activity occurs within 7 days.
|
||||
|
||||
If you are the author of this PR, please leave a comment if you want
|
||||
to keep it open. Also, please merge the latest dev branch into your
|
||||
to keep it open. Also, please rebase your PR onto the latest dev
|
||||
branch to ensure that it's up to date with the latest changes.
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Keeps pre-commit hook revs in sync with the requirements files.
|
||||
#
|
||||
# Dependabot only bumps the pins in requirements*.txt. Some of those tools
|
||||
# are pinned again as hook revs in .pre-commit-config.yaml. This workflow
|
||||
# runs script/sync_dependency_versions.py against the pull request branch
|
||||
# and pushes a commit with the revs updated.
|
||||
|
||||
name: Sync dependency versions
|
||||
|
||||
on:
|
||||
# pull_request_target rather than pull_request so the App secret is
|
||||
# available on Dependabot pull requests (pull_request runs opened by
|
||||
# Dependabot only see Dependabot secrets). The job below only touches
|
||||
# branches in this repository and only ever executes the script from the
|
||||
# base branch checkout, so fork code never runs with the token.
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- requirements_dev.txt
|
||||
- requirements_test.txt
|
||||
- .pre-commit-config.yaml
|
||||
- script/sync_dependency_versions.py
|
||||
|
||||
# The push to the pull request branch uses the App token minted below, so
|
||||
# the workflow's GITHUB_TOKEN does not need any scopes.
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync pinned versions
|
||||
runs-on: ubuntu-latest
|
||||
# Same-repository branches only: a push to a fork is not possible with
|
||||
# this token, and it keeps untrusted heads out of a privileged job.
|
||||
if: >-
|
||||
github.repository == 'esphome/esphome'
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
# A push made with the workflow's own GITHUB_TOKEN would not start
|
||||
# CI on the new commit; a push with the App token does.
|
||||
permission-contents: write # git push of the sync commit to the pull request branch
|
||||
|
||||
- name: Check out base branch
|
||||
# Provides the script that runs below. Deliberately the base branch
|
||||
# so the pull request cannot change what executes here.
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out pull request branch
|
||||
# No allow-unsafe-pr-checkout here on purpose: checkout v7 only
|
||||
# refuses heads that live in a different repository, and the job
|
||||
# condition above already limits runs to same-repository branches.
|
||||
# Leaving it off keeps that refusal as a backstop for fork heads.
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
path: pull-request
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install yamlrocks
|
||||
# The script edits YAML through yamlrocks. Take the pin from the
|
||||
# base branch requirements so this workflow has no copy of its own.
|
||||
run: pip install "$(grep -E '^yamlrocks==' requirements_test.txt | cut -d'#' -f1)"
|
||||
|
||||
- name: Sync pinned versions
|
||||
run: python script/sync_dependency_versions.py --root pull-request
|
||||
|
||||
- name: Push changes
|
||||
working-directory: pull-request
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "All pinned versions already match the requirements files."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "esphome[bot]"
|
||||
git config user.email "115708604+esphome[bot]@users.noreply.github.com"
|
||||
git commit -am "Sync pinned tool versions with requirements files"
|
||||
git push
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
ci:
|
||||
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
||||
@@ -10,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.8
|
||||
rev: v0.16.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -41,7 +42,7 @@ repos:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.38.0
|
||||
rev: v1.37.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
exclude: ^(\.clang-format|\.clang-tidy)$
|
||||
|
||||
@@ -431,17 +431,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
MyComponent *parent_;
|
||||
};
|
||||
```
|
||||
Register it without writing a builder:
|
||||
```python
|
||||
automation.register_simple_action(
|
||||
"my_component.do_something", MyAction, schema, synchronous=True
|
||||
)
|
||||
```
|
||||
The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a
|
||||
no-argument constructor, `register_parented_action` for a class deriving from `Parented<T>`, and
|
||||
the `@automation.register_action(...)` decorator only when the builder must also set fields.
|
||||
|
||||
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
|
||||
Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
|
||||
|
||||
* **Conditions:**
|
||||
```cpp
|
||||
@@ -453,8 +443,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
MyComponent *parent_;
|
||||
};
|
||||
```
|
||||
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
|
||||
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
|
||||
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
|
||||
|
||||
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
|
||||
|
||||
@@ -564,7 +553,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
4. **Lint:** Run `prek` to ensure code is compliant.
|
||||
5. **Commit:** Commit your changes. There is no strict format for commit messages.
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
|
||||
7. **Comments:** When commenting on GitHub PRs or issues, don't tag contributors, especially bots. Avoid referring to list items (e.g. from reviews) with the form #nn - this will be interpreted by GitHub as a reference to issue or PR nn. Keep comments short and exclude irrelevant details, backstories, restatement of previous comments and anything that is already obvious to the reader.
|
||||
|
||||
* **Documentation Contributions:**
|
||||
* Documentation is hosted in the separate `esphome/esphome.io` repository.
|
||||
@@ -640,9 +628,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
_request_listener_slot()
|
||||
cg.add(hub.register_listener(var))
|
||||
```
|
||||
When several instances each own a list declared at the same size (one per hub of a
|
||||
`MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`;
|
||||
the define is then the largest count any one key requested instead of the total.
|
||||
```cpp
|
||||
#ifdef MY_COMPONENT_LISTENER_COUNT
|
||||
void register_listener(MyComponentListener *listener);
|
||||
@@ -854,7 +839,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
cv.rename_key(
|
||||
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component"
|
||||
),
|
||||
cv.Schema({...}),
|
||||
cv.Schema({ ... }),
|
||||
)
|
||||
```
|
||||
For other deprecations, warn manually during validation:
|
||||
|
||||
+1
-7
@@ -100,7 +100,6 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bridge/* @kbx81
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
esphome/components/button/* @esphome/core
|
||||
esphome/components/bytebuffer/* @clydebarrow
|
||||
@@ -112,8 +111,6 @@ esphome/components/captive_portal/* @esphome/core
|
||||
esphome/components/cc1101/* @gabest11 @lygris
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/cdc_acm_uart/* @kbx81
|
||||
esphome/components/cdc_acm_uart/bridge/* @kbx81
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
esphome/components/ch423/* @dwmw2
|
||||
esphome/components/chsc6x/* @kkosik20
|
||||
@@ -182,6 +179,7 @@ esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
esphome/components/esp32_hosted/update/* @swoboda1337
|
||||
esphome/components/esp32_improv/* @jesserockz
|
||||
esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
@@ -265,10 +263,8 @@ esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
esphome/components/improv_base/* @esphome/core
|
||||
esphome/components/improv_ble/* @jesserockz
|
||||
esphome/components/improv_serial/* @esphome/core
|
||||
esphome/components/ina226/* @latonita @Sergio303
|
||||
esphome/components/ina260/* @mreditor97
|
||||
@@ -478,7 +474,6 @@ esphome/components/sendspin/image/* @kahrendt
|
||||
esphome/components/sendspin/media_player/* @kahrendt
|
||||
esphome/components/sendspin/media_source/* @kahrendt
|
||||
esphome/components/sendspin/sensor/* @kahrendt
|
||||
esphome/components/sendspin/switch/* @kahrendt
|
||||
esphome/components/sendspin/text_sensor/* @kahrendt
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
@@ -591,7 +586,6 @@ esphome/components/uart/* @esphome/core
|
||||
esphome/components/uart/button/* @ssieb
|
||||
esphome/components/uart/event/* @eoasmxd
|
||||
esphome/components/uart/packet_transport/* @clydebarrow
|
||||
esphome/components/uart_mux/* @kbx81
|
||||
esphome/components/udp/* @clydebarrow
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.10.0-dev
|
||||
PROJECT_NUMBER = 2026.9.1
|
||||
|
||||
# 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
|
||||
|
||||
@@ -23,7 +23,9 @@ from esphome.util import safe_print
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from aioesphomeapi.api_pb2 import SubscribeLogsResponse # pylint: disable=no-name-in-module
|
||||
from aioesphomeapi.api_pb2 import (
|
||||
SubscribeLogsResponse, # pylint: disable=no-name-in-module
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
+28
-99
@@ -102,101 +102,6 @@ def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schem
|
||||
return CONDITION_REGISTRY.register(name, condition_type, schema)
|
||||
|
||||
|
||||
async def _build_with_parent(
|
||||
config: ConfigType,
|
||||
automation_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(automation_id, template_arg, parent)
|
||||
|
||||
|
||||
async def _build_without_parent(
|
||||
config: ConfigType,
|
||||
automation_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
return cg.new_Pvariable(automation_id, template_arg)
|
||||
|
||||
|
||||
async def _build_parented(
|
||||
config: ConfigType,
|
||||
automation_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(automation_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
def register_simple_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool,
|
||||
) -> None:
|
||||
"""Register an action whose constructor takes the object named by ``config[CONF_ID]``.
|
||||
|
||||
Use the ``register_action`` decorator instead when the builder must also set fields.
|
||||
"""
|
||||
register_action(name, action_type, schema, synchronous=synchronous)(
|
||||
_build_with_parent
|
||||
)
|
||||
|
||||
|
||||
def register_simple_condition(
|
||||
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||
) -> None:
|
||||
"""Condition counterpart of ``register_simple_action``."""
|
||||
register_condition(name, condition_type, schema)(_build_with_parent)
|
||||
|
||||
|
||||
def register_bare_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool,
|
||||
) -> None:
|
||||
"""Register an action whose constructor takes no arguments."""
|
||||
register_action(name, action_type, schema, synchronous=synchronous)(
|
||||
_build_without_parent
|
||||
)
|
||||
|
||||
|
||||
def register_bare_condition(
|
||||
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||
) -> None:
|
||||
"""Condition counterpart of ``register_bare_action``."""
|
||||
register_condition(name, condition_type, schema)(_build_without_parent)
|
||||
|
||||
|
||||
def register_parented_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool,
|
||||
) -> None:
|
||||
"""Register an action deriving from ``Parented<T>``.
|
||||
|
||||
The object is constructed without arguments and ``set_parent()`` receives the object
|
||||
named by ``config[CONF_ID]``.
|
||||
"""
|
||||
register_action(name, action_type, schema, synchronous=synchronous)(_build_parented)
|
||||
|
||||
|
||||
def register_parented_condition(
|
||||
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||
) -> None:
|
||||
"""Condition counterpart of ``register_parented_action``."""
|
||||
register_condition(name, condition_type, schema)(_build_parented)
|
||||
|
||||
|
||||
Action = cg.esphome_ns.class_("Action")
|
||||
Trigger = cg.esphome_ns.class_("Trigger")
|
||||
ACTION_REGISTRY = Registry()
|
||||
@@ -629,20 +534,44 @@ async def lambda_action_to_code(
|
||||
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
||||
|
||||
|
||||
register_simple_action(
|
||||
@register_action(
|
||||
"component.update",
|
||||
UpdateComponentAction,
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_update_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
||||
|
||||
|
||||
register_simple_action(
|
||||
@register_action(
|
||||
"component.suspend",
|
||||
SuspendComponentAction,
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_suspend_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
||||
|
||||
|
||||
@register_action(
|
||||
|
||||
@@ -90,10 +90,9 @@ def get_project_cmakelists(
|
||||
"""
|
||||
idf_target = variant_to_idf_target(get_esp32_variant())
|
||||
|
||||
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
|
||||
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
|
||||
# total_size, hence the ELF fallback in espidf/size_summary.py; both
|
||||
# go away together when 1.x support is dropped.
|
||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
||||
# --format=raw because the legacy mode doesn't support it.
|
||||
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
||||
|
||||
# Project-wide compile options: -D defines and -W warning flags (skip
|
||||
@@ -212,12 +211,10 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
project({CORE.name})
|
||||
|
||||
# Emit per-memory-type JSON size data for ESPHome to read post-build.
|
||||
# json2 stays small; raw dumps every symbol (~2s on a large map) and
|
||||
# this command runs inside the link edge, blocking everything downstream.
|
||||
# Emit raw JSON size data for ESPHome to read post-build.
|
||||
add_custom_command(
|
||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
|
||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||
${{CMAKE_PROJECT_NAME}}.map
|
||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||
|
||||
@@ -6,6 +6,5 @@ See the component-alias section of esphome/loader.py.
|
||||
|
||||
# alias -> (canonical component, removal version or None)
|
||||
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
|
||||
"esp32_improv": ("improv_ble", "2027.4.0"),
|
||||
"rp2040": ("rp2", "2027.7.0"),
|
||||
}
|
||||
@@ -94,7 +94,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
|
||||
/// - SamplingMode::MIN: Use the lowest sample value
|
||||
/// - SamplingMode::MAX: Use the highest sample value
|
||||
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
|
||||
void set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
|
||||
void set_sampling_mode(SamplingMode sampling_mode);
|
||||
|
||||
/// Perform a single ADC sampling operation and return the measured value.
|
||||
/// This function handles raw readings, calibration, and averaging as needed.
|
||||
|
||||
@@ -76,4 +76,6 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
|
||||
}
|
||||
}
|
||||
|
||||
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
|
||||
|
||||
} // namespace esphome::adc
|
||||
@@ -138,11 +138,11 @@ class AlarmControlPanel : public EntityBase {
|
||||
// in order to store last panel state in flash
|
||||
ESPPreferenceObject pref_;
|
||||
// current state
|
||||
AlarmControlPanelState current_state_{ACP_STATE_DISARMED};
|
||||
AlarmControlPanelState current_state_;
|
||||
// the desired (or previous) state
|
||||
AlarmControlPanelState desired_state_{ACP_STATE_DISARMED};
|
||||
AlarmControlPanelState desired_state_;
|
||||
// last time the state was updated
|
||||
uint32_t last_update_{0};
|
||||
uint32_t last_update_;
|
||||
// the call control function
|
||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||
// state callback - passes the new state to listeners
|
||||
|
||||
@@ -136,12 +136,6 @@ CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||
|
||||
# Schema defaults that also match the C++ initializers in api_server.h; codegen
|
||||
# skips the setter when the config equals them.
|
||||
DEFAULT_PORT = 6053
|
||||
DEFAULT_REBOOT_TIMEOUT = "15min"
|
||||
DEFAULT_BATCH_DELAY = "100ms"
|
||||
|
||||
|
||||
def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
"""Register the API as a provisioning source when encryption is enabled.
|
||||
@@ -298,7 +292,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(APIServer),
|
||||
cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
cv.Optional(CONF_PORT, default=6053): cv.port,
|
||||
# Removed in 2026.1.0 - kept to provide helpful error message
|
||||
cv.Optional(CONF_PASSWORD): cv.invalid(
|
||||
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
|
||||
@@ -311,14 +305,14 @@ CONFIG_SCHEMA = cv.All(
|
||||
"Or visit https://esphome.io/components/api/#configuration-variables"
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT
|
||||
CONF_REBOOT_TIMEOUT, default="15min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Exclusive(
|
||||
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
||||
): ACTIONS_SCHEMA,
|
||||
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
|
||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
||||
cv.Optional(CONF_BATCH_DELAY, default=DEFAULT_BATCH_DELAY): cv.All(
|
||||
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
),
|
||||
@@ -468,15 +462,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Request a log listener slot for API log streaming
|
||||
request_log_listener()
|
||||
|
||||
# Skip the setters when the config matches the C++ initializers (DEFAULT_*).
|
||||
if (port := config[CONF_PORT]) != DEFAULT_PORT:
|
||||
cg.add(var.set_port(port))
|
||||
if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period(
|
||||
DEFAULT_REBOOT_TIMEOUT
|
||||
):
|
||||
cg.add(var.set_reboot_timeout(reboot_timeout))
|
||||
if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY):
|
||||
cg.add(var.set_batch_delay(batch_delay))
|
||||
cg.add(var.set_port(config[CONF_PORT]))
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||
if CONF_LISTEN_BACKLOG in config:
|
||||
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
||||
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
||||
|
||||
@@ -77,7 +77,6 @@ service APIConnection {
|
||||
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -2727,8 +2726,7 @@ enum SerialProxyParity {
|
||||
SERIAL_PROXY_PARITY_ODD = 2;
|
||||
}
|
||||
|
||||
// Configure UART parameters for a serial proxy instance. Only the subscribed client may
|
||||
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
|
||||
// Configure UART parameters for a serial proxy instance
|
||||
message SerialProxyConfigureRequest {
|
||||
option (id) = 138;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2754,8 +2752,7 @@ message SerialProxyDataReceived {
|
||||
bytes data = 2; // Raw data received from the serial device
|
||||
}
|
||||
|
||||
// Write data to a serial device. Only the subscribed client may write; writes from
|
||||
// others are ignored (since API 1.17).
|
||||
// Write data to a serial device
|
||||
message SerialProxyWriteRequest {
|
||||
option (id) = 140;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2766,8 +2763,7 @@ message SerialProxyWriteRequest {
|
||||
bytes data = 2; // Raw data to write to the serial device
|
||||
}
|
||||
|
||||
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them;
|
||||
// others are refused with PORT_IN_USE (since API 1.17).
|
||||
// Set modem control pin states (RTS and DTR)
|
||||
message SerialProxySetModemPinsRequest {
|
||||
option (id) = 141;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2806,7 +2802,6 @@ enum SerialProxyRequestType {
|
||||
// error the device answers with INVALID_ARGUMENT.
|
||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
|
||||
}
|
||||
|
||||
enum SerialProxyStatus {
|
||||
@@ -2819,8 +2814,7 @@ enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations. FLUSH requires an active
|
||||
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
|
||||
// Generic request message for simple serial proxy operations
|
||||
message SerialProxyRequest {
|
||||
option (id) = 144;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2844,29 +2838,6 @@ message SerialProxyRequestResponse {
|
||||
string error_message = 4; // Additional detail on failure (optional)
|
||||
}
|
||||
|
||||
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
|
||||
// activates the port's protocol-aware tap (if one is configured), letting it observe
|
||||
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
|
||||
// speaks is a property of the device configuration, discoverable from the tap
|
||||
// component's own API surface. A client that is about to flash firmware selects RAW
|
||||
// first, which definitively disables that injection.
|
||||
enum SerialProxyMode {
|
||||
SERIAL_PROXY_MODE_RAW = 0;
|
||||
SERIAL_PROXY_MODE_PROTOCOL = 1;
|
||||
}
|
||||
|
||||
// Only the subscribed client may change the mode; any other caller -- including one that
|
||||
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
|
||||
// when the port has no protocol-aware tap configured.
|
||||
message SerialProxySetModeRequest {
|
||||
option (id) = 152;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1;
|
||||
SerialProxyMode mode = 2;
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
|
||||
@@ -720,7 +720,12 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
}
|
||||
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||
a_switch->control(msg.state);
|
||||
|
||||
if (msg.state) {
|
||||
a_switch->turn_on();
|
||||
} else {
|
||||
a_switch->turn_off();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1659,7 +1664,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||
// Response-only discriminators; never valid in a request
|
||||
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
@@ -1672,19 +1676,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
send_serial_proxy_ack(this, msg.instance, msg.type, status);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||
serial_proxy_result_to_status(result));
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
|
||||
if (!this->send_message(msg)) {
|
||||
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
|
||||
@@ -1811,7 +1802,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 17;
|
||||
resp.api_version_minor = 16;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
|
||||
@@ -244,7 +244,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
|
||||
void on_serial_proxy_request(const SerialProxyRequest &msg);
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
|
||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -4253,19 +4253,6 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
|
||||
size += ProtoSize::calc_length(1, this->error_message.size());
|
||||
return size;
|
||||
}
|
||||
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->instance = value;
|
||||
break;
|
||||
case 2:
|
||||
this->mode = static_cast<enums::SerialProxyMode>(value);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
|
||||
@@ -356,7 +356,6 @@ enum SerialProxyRequestType : uint32_t {
|
||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
|
||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5,
|
||||
};
|
||||
enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_OK = 0,
|
||||
@@ -367,10 +366,6 @@ enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
|
||||
};
|
||||
enum SerialProxyMode : uint32_t {
|
||||
SERIAL_PROXY_MODE_RAW = 0,
|
||||
SERIAL_PROXY_MODE_PROTOCOL = 1,
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace enums
|
||||
@@ -3408,22 +3403,6 @@ class SerialProxyRequestResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 152;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 6;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
enums::SerialProxyMode mode{};
|
||||
#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;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
|
||||
|
||||
@@ -854,8 +854,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -880,16 +878,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_MODE_RAW:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
|
||||
case enums::SERIAL_PROXY_MODE_PROTOCOL:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -2817,12 +2805,6 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -712,17 +712,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
this->on_device_capabilities_request();
|
||||
break;
|
||||
}
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxySetModeRequest::MESSAGE_TYPE: {
|
||||
SerialProxySetModeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_set_mode_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -235,9 +235,6 @@ class APIServerConnectionBase {
|
||||
void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -319,7 +319,7 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
// 4-byte aligned types
|
||||
uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
||||
@@ -356,8 +356,8 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py
|
||||
uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py
|
||||
uint16_t port_{6053};
|
||||
uint16_t batch_delay_{100};
|
||||
// Connection limits - these defaults will be overridden by config values
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
|
||||
uint8_t listen_backlog_{4};
|
||||
|
||||
@@ -181,11 +181,11 @@ async def at581x_settings_to_code(
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
# Radar configuration
|
||||
if (frontend_reset := config.get(CONF_HW_FRONTEND_RESET)) is not None:
|
||||
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
|
||||
template_ = await cg.templatable(frontend_reset, args, cg.int8)
|
||||
cg.add(var.set_hw_frontend_reset(template_))
|
||||
|
||||
if (freq := config.get(CONF_FREQUENCY)) is not None:
|
||||
if freq := config.get(CONF_FREQUENCY):
|
||||
if not cg.is_template(freq):
|
||||
freq = int(freq / 1000000)
|
||||
template_ = await cg.templatable(freq, args, cg.int_)
|
||||
@@ -195,19 +195,19 @@ async def at581x_settings_to_code(
|
||||
template_ = await cg.templatable(sens_dist, args, cg.int_)
|
||||
cg.add(var.set_sensing_distance(template_))
|
||||
|
||||
if (selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME)) is not None:
|
||||
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int_)
|
||||
cg.add(var.set_poweron_selfcheck_time(template_))
|
||||
|
||||
if (protect := config.get(CONF_PROTECT_TIME)) is not None:
|
||||
if protect := config.get(CONF_PROTECT_TIME):
|
||||
template_ = await cg.templatable(protect, args, cg.int_)
|
||||
cg.add(var.set_protect_time(template_))
|
||||
|
||||
if (trig_base := config.get(CONF_TRIGGER_BASE)) is not None:
|
||||
if trig_base := config.get(CONF_TRIGGER_BASE):
|
||||
template_ = await cg.templatable(trig_base, args, cg.int_)
|
||||
cg.add(var.set_trigger_base(template_))
|
||||
|
||||
if (trig_keep := config.get(CONF_TRIGGER_KEEP)) is not None:
|
||||
if trig_keep := config.get(CONF_TRIGGER_KEEP):
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int_)
|
||||
cg.add(var.set_trigger_keep(template_))
|
||||
|
||||
@@ -215,7 +215,7 @@ async def at581x_settings_to_code(
|
||||
template_ = await cg.templatable(stage_gain, args, cg.int_)
|
||||
cg.add(var.set_stage_gain(template_))
|
||||
|
||||
if (power := config.get(CONF_POWER_CONSUMPTION)) is not None:
|
||||
if power := config.get(CONF_POWER_CONSUMPTION):
|
||||
if not cg.is_template(power):
|
||||
power = int(power * 1000000)
|
||||
template_ = await cg.templatable(power, args, cg.int_)
|
||||
|
||||
@@ -339,7 +339,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||
add_idf_component(
|
||||
name="esphome/esp-audio-libs",
|
||||
ref="3.2.1",
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -203,10 +203,16 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
|
||||
this->away_config_ = away_config;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
|
||||
|
||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
|
||||
void BangBangClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Bang Bang Climate", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -22,10 +22,10 @@ class BangBangClimate final : public climate::Climate, public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
void set_sensor(sensor::Sensor *sensor);
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
||||
void set_supports_cool(bool supports_cool);
|
||||
void set_supports_heat(bool supports_heat);
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ class BinaryLightOutput final : public light::LightOutput {
|
||||
void write_state(light::LightState *state) override {
|
||||
bool binary;
|
||||
state->current_values_as_binary(&binary);
|
||||
this->output_->set_state(binary);
|
||||
if (binary) {
|
||||
this->output_->turn_on();
|
||||
} else {
|
||||
this->output_->turn_off();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -39,7 +39,6 @@ from esphome.const import (
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_GARAGE_DOOR,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_GLASS_BREAK,
|
||||
DEVICE_CLASS_HEAT,
|
||||
DEVICE_CLASS_LIGHT,
|
||||
DEVICE_CLASS_LOCK,
|
||||
@@ -82,7 +81,6 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_GARAGE_DOOR,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_GLASS_BREAK,
|
||||
DEVICE_CLASS_HEAT,
|
||||
DEVICE_CLASS_LIGHT,
|
||||
DEVICE_CLASS_LOCK,
|
||||
|
||||
@@ -32,8 +32,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
||||
*/
|
||||
class BinarySensor : public StatefulEntityBase<bool> {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) BinarySensor()` would zero-fill .bss that is already zero.
|
||||
explicit BinarySensor() {}
|
||||
explicit BinarySensor() = default;
|
||||
|
||||
const bool &get_state() const override { return this->state; }
|
||||
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
|
||||
|
||||
@@ -53,9 +53,6 @@ class DelayedOnOffFilter final : public Filter {
|
||||
|
||||
class DelayedOnFilter : public Filter {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero.
|
||||
DelayedOnFilter() {}
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
@@ -66,9 +63,6 @@ class DelayedOnFilter : public Filter {
|
||||
|
||||
class DelayedOffFilter : public Filter {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero.
|
||||
DelayedOffFilter() {}
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
@@ -149,8 +143,6 @@ class StatelessLambdaFilter : public Filter {
|
||||
|
||||
class SettleFilter : public Filter {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) SettleFilter()` would zero-fill .bss that is already zero.
|
||||
SettleFilter() {}
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
@@ -31,9 +31,6 @@ class BluetoothConnection;
|
||||
// void disconnect() cannot overload with an int-returning twin.
|
||||
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) BluedroidGattClient()` would zero-fill .bss that is already zero.
|
||||
BluedroidGattClient() {}
|
||||
|
||||
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
|
||||
|
||||
// Lifecycle of one connection attempt's service search.
|
||||
|
||||
@@ -37,9 +37,6 @@ enum class PendingAck : uint8_t {
|
||||
|
||||
class BluetoothConnection final : public ble_device_base::GattClientListener {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) BluetoothConnection()` would zero-fill .bss that is already zero.
|
||||
BluetoothConnection() {}
|
||||
|
||||
/// Wire the platform backend. Called from codegen before setup.
|
||||
void set_backend(ble_device_base::BLEGattConnection *backend) {
|
||||
this->backend_ = backend;
|
||||
|
||||
@@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
|
||||
// explicit kick the MTU would only be exchanged on the first GATT query,
|
||||
// which never happens on a V3_WITH_CACHE connection.
|
||||
// Both registration calls above return void (BTstack 075a078, arduino-pico
|
||||
// 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// the connect timeout in loop().
|
||||
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
|
||||
}
|
||||
|
||||
@@ -395,17 +395,6 @@ async def _to_code_ble_hub(config: ConfigType) -> None:
|
||||
await _connections_to_code(var, config)
|
||||
|
||||
|
||||
def enable_advertisement_filter() -> None:
|
||||
"""Compile the advertisement filter hook into bluetooth_proxy.
|
||||
|
||||
Called by external filtering components from to_code(). The define behind
|
||||
this is an implementation detail; do not emit it directly.
|
||||
|
||||
Public API for external components. Do not remove.
|
||||
"""
|
||||
cg.add_define("USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER")
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.is_esp32:
|
||||
await _to_code_esp32(config)
|
||||
|
||||
@@ -94,15 +94,6 @@ void BluetoothProxy::on_raw_advertisement_(const ble_device_base::RawAdvertiseme
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
return;
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
// Ask the filter before the packet is queued, so a dropped advertisement never
|
||||
// reaches the batch or the network.
|
||||
if (this->advertisement_filter_.is_set() && !this->advertisement_filter_.should_forward(raw)) {
|
||||
ESP_LOGVV(TAG, "Filtered packet from %012" PRIX64, raw.address);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto &adv = this->response_.advertisements[this->response_.advertisements_len];
|
||||
adv.address = raw.address;
|
||||
adv.rssi = raw.rssi;
|
||||
@@ -193,9 +184,6 @@ void BluetoothProxy::dump_config() {
|
||||
" Adapter MAC: %s",
|
||||
scan_mode, mac_out);
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
ESP_LOGCONFIG(TAG, " Advertisement filter: %s", YESNO(this->advertisement_filter_.is_set()));
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
|
||||
@@ -97,29 +97,6 @@ static_assert(pending_reply_round_trips(0xABCD112233445566ULL, 0x000011223344556
|
||||
static_assert(PendingReply{}.empty());
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
/// Predicate slot letting an external component drop advertisements before they
|
||||
/// are queued for the API. Same shape as
|
||||
/// ble_device_base::RawAdvertisementCallback. Runs on the advertisement hot
|
||||
/// path, so it must be cheap and must not block.
|
||||
///
|
||||
/// Usage:
|
||||
/// proxy->set_advertisement_filter({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
|
||||
/// return static_cast<MyFilter *>(self)->should_forward(adv);
|
||||
/// }});
|
||||
///
|
||||
/// Returning false drops the advertisement. Not called at all while the API is
|
||||
/// disconnected, which matters to a stateful filter. Compiled in only when an
|
||||
/// external component calls bluetooth_proxy.enable_advertisement_filter().
|
||||
struct AdvertisementFilter {
|
||||
void *instance{nullptr};
|
||||
bool (*fn)(void *instance, const ble_device_base::RawAdvertisement &adv){nullptr};
|
||||
/// A default-constructed slot is "no filter"; the proxy guards on this.
|
||||
bool is_set() const { return this->fn != nullptr; }
|
||||
bool should_forward(const ble_device_base::RawAdvertisement &adv) const { return this->fn(this->instance, adv); }
|
||||
};
|
||||
#endif // USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
|
||||
class BluetoothProxy final : public Component {
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
// Allow the connection to update connections_free_response_
|
||||
@@ -185,11 +162,6 @@ class BluetoothProxy final : public Component {
|
||||
void set_active(bool active) { this->active_ = active; }
|
||||
bool has_active() { return this->active_; }
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
/// One subscriber; a later call replaces an earlier one.
|
||||
void set_advertisement_filter(AdvertisementFilter filter) { this->advertisement_filter_ = filter; }
|
||||
#endif
|
||||
|
||||
uint32_t get_legacy_version() const {
|
||||
if (!this->active_) {
|
||||
return LEGACY_PASSIVE_ONLY_VERSION;
|
||||
@@ -358,10 +330,6 @@ class BluetoothProxy final : public Component {
|
||||
// start on an even word, closing two alignment holes.
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
AdvertisementFilter advertisement_filter_{};
|
||||
#endif
|
||||
|
||||
// BLE advertisement batching
|
||||
api::BluetoothLERawAdvertisementsResponse response_;
|
||||
|
||||
|
||||
@@ -341,6 +341,7 @@ void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over
|
||||
void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
|
||||
this->humidity_oversampling_ = humidity_over_sampling;
|
||||
}
|
||||
void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
uint8_t BME280Component::read_u8_(uint8_t a_register) {
|
||||
uint8_t data = 0;
|
||||
this->read_byte(a_register, &data);
|
||||
|
||||
@@ -69,7 +69,7 @@ class BME280Component : public PollingComponent {
|
||||
/// Set the oversampling value for the humidity sensor. Default is 16x.
|
||||
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling);
|
||||
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
|
||||
void set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
void set_iir_filter(BME280IIRFilter iir_filter);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
|
||||
@@ -503,6 +503,7 @@ void BME680Component::set_pressure_oversampling(BME680Oversampling pressure_over
|
||||
void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) {
|
||||
this->humidity_oversampling_ = humidity_oversampling;
|
||||
}
|
||||
void BME680Component::set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) {
|
||||
this->heater_temperature_ = heater_temperature;
|
||||
this->heater_duration_ = heater_duration;
|
||||
|
||||
@@ -74,7 +74,7 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
/// Set the humidity oversampling value. Defaults to 16X.
|
||||
void set_humidity_oversampling(BME680Oversampling humidity_oversampling);
|
||||
/// Set the IIR Filter value. Defaults to no IIR Filter.
|
||||
void set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
void set_iir_filter(BME680IIRFilter iir_filter);
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
|
||||
|
||||
@@ -254,6 +254,7 @@ void BMP280Component::set_temperature_oversampling(BMP280Oversampling temperatur
|
||||
void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) {
|
||||
this->pressure_oversampling_ = pressure_over_sampling;
|
||||
}
|
||||
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
|
||||
uint8_t data = 0;
|
||||
this->bmp_read_byte(a_register, &data);
|
||||
|
||||
@@ -59,7 +59,7 @@ class BMP280Component : public PollingComponent {
|
||||
/// Set the oversampling value for the pressure sensor. Default is 16x.
|
||||
void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling);
|
||||
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
|
||||
void set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
void set_iir_filter(BMP280IIRFilter iir_filter);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DOMAIN = "bridge"
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -1,114 +0,0 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, uart, usb_cdc_acm
|
||||
from esphome.components.bridge import DOMAIN as BRIDGE_DOMAIN
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DEBUG, CONF_ID, CONF_UART_ID
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["tinyusb", "uart", "usb_cdc_acm"]
|
||||
|
||||
CONF_DTR_PIN = "dtr_pin"
|
||||
CONF_RTS_PIN = "rts_pin"
|
||||
CONF_USB_CDC_ACM_ID = "usb_cdc_acm_id"
|
||||
|
||||
cdc_acm_uart_ns = cg.esphome_ns.namespace("cdc_acm_uart")
|
||||
CDCACMUARTBridge = cdc_acm_uart_ns.class_("CDCACMUARTBridge", cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CDCACMUARTBridge),
|
||||
cv.Required(CONF_UART_ID): cv.use_id(uart.IDFUARTComponent),
|
||||
cv.Required(CONF_USB_CDC_ACM_ID): cv.use_id(usb_cdc_acm.USBCDCACMInstance),
|
||||
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
# Narrower than usb_cdc_acm's variant list on purpose: S31/H4 untested on
|
||||
# hardware; extend once verified.
|
||||
esp32.only_on_variant(
|
||||
supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _subtree_references_uart(node: object, uart_id: str) -> bool:
|
||||
"""Return True if any dict in the subtree has a uart_id entry naming this bus."""
|
||||
if isinstance(node, dict):
|
||||
return any(
|
||||
(key == CONF_UART_ID and str(value) == uart_id)
|
||||
or _subtree_references_uart(value, uart_id)
|
||||
for key, value in node.items()
|
||||
)
|
||||
if isinstance(node, list):
|
||||
return any(_subtree_references_uart(item, uart_id) for item in node)
|
||||
return False
|
||||
|
||||
|
||||
def _reject_debug(uart_conf: ConfigType) -> ConfigType:
|
||||
# The worker tasks use the IDF driver directly, so the uart debugger never sees
|
||||
# bridge traffic and its dummy_receiver would drain RX bytes on the main loop.
|
||||
if CONF_DEBUG in uart_conf:
|
||||
raise cv.Invalid(
|
||||
"A bridged UART cannot use 'debug'; the bridge bypasses the UART "
|
||||
"component's read/write path.",
|
||||
[CONF_DEBUG],
|
||||
)
|
||||
return uart_conf
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
full_config = fv.full_config.get()
|
||||
# Bridges of any platform must own their interfaces exclusively; shared ring
|
||||
# buffers and overwritten callbacks would corrupt both streams silently. The
|
||||
# seen-set is keyed on the bridge domain so future platforms share it.
|
||||
# Other components bind either interface through the same uart_id key (the CDC
|
||||
# instance is itself a uart::UARTComponent) and would race the worker tasks.
|
||||
# Bare `id:` references (a uart.write action) cannot be distinguished; not caught.
|
||||
data = full_config.data.setdefault(BRIDGE_DOMAIN, {})
|
||||
for conf_key, label in (
|
||||
(CONF_UART_ID, "UART"),
|
||||
(CONF_USB_CDC_ACM_ID, "USB CDC-ACM interface"),
|
||||
):
|
||||
owned_id = str(config[conf_key])
|
||||
used = data.setdefault(conf_key, set())
|
||||
if owned_id in used:
|
||||
raise cv.Invalid(
|
||||
f"The {label} '{owned_id}' is already bridged by another 'bridge' "
|
||||
f"instance; each bridge requires its own {label}.",
|
||||
[conf_key],
|
||||
)
|
||||
used.add(owned_id)
|
||||
for domain, domain_conf in full_config.items():
|
||||
if domain == BRIDGE_DOMAIN:
|
||||
continue
|
||||
if _subtree_references_uart(domain_conf, owned_id):
|
||||
raise cv.Invalid(
|
||||
f"The {label} '{owned_id}' is also used by '{domain}'; a bridge "
|
||||
f"requires exclusive use of its {label}.",
|
||||
[conf_key],
|
||||
)
|
||||
|
||||
fv.id_declaration_match_schema(_reject_debug)(config[CONF_UART_ID])
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
uart_component = await cg.get_variable(config[CONF_UART_ID])
|
||||
usb_cdc = await cg.get_variable(config[CONF_USB_CDC_ACM_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID], uart_component, usb_cdc)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if dtr_pin_config := config.get(CONF_DTR_PIN):
|
||||
dtr_pin = await cg.gpio_pin_expression(dtr_pin_config)
|
||||
cg.add(var.set_dtr_pin(dtr_pin))
|
||||
if rts_pin_config := config.get(CONF_RTS_PIN):
|
||||
rts_pin = await cg.gpio_pin_expression(rts_pin_config)
|
||||
cg.add(var.set_rts_pin(rts_pin))
|
||||
@@ -1,468 +0,0 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#include "cdc_acm_uart_bridge.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/ringbuf.h"
|
||||
#include "driver/uart.h"
|
||||
#include "soc/soc_caps.h"
|
||||
|
||||
namespace esphome::cdc_acm_uart {
|
||||
|
||||
static const char *const TAG = "cdc_acm_uart";
|
||||
|
||||
static constexpr size_t UART_TASK_STACK_SIZE = 4096;
|
||||
static constexpr size_t RINGBUF_RETRY_CHUNK_SIZE = 64;
|
||||
static constexpr uint32_t LOG_THROTTLE_MS = 1000;
|
||||
static constexpr uint32_t UART_RELOAD_SETTLE_MS = 20;
|
||||
// Above the default priority but below the USB/Wi-Fi system tasks.
|
||||
static constexpr UBaseType_t TASK_PRIORITY = 4;
|
||||
|
||||
static bool should_log_now(uint32_t *last_ms, uint32_t interval_ms) {
|
||||
uint32_t now = millis();
|
||||
if ((now - *last_ms) >= interval_ms) {
|
||||
*last_ms = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ringbuf_send_with_retry(RingbufHandle_t ringbuf, const uint8_t *data, size_t len, uint32_t *log_ms) {
|
||||
if (len == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (xRingbufferSend(ringbuf, data, len, pdMS_TO_TICKS(1)) == pdTRUE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
while (offset < len) {
|
||||
size_t chunk = std::min(RINGBUF_RETRY_CHUNK_SIZE, len - offset);
|
||||
if (xRingbufferSend(ringbuf, data + offset, chunk, pdMS_TO_TICKS(1)) != pdTRUE) {
|
||||
if (should_log_now(log_ms, LOG_THROTTLE_MS)) {
|
||||
ESP_LOGW(TAG, "USB TX buffer full; some data is lost");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
offset += chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::setup() {
|
||||
// Line state starts deasserted (no host yet); active-low DTR#/RTS# wiring is
|
||||
// handled by configuring the pins inverted, so deasserted idles HIGH.
|
||||
if (this->dtr_pin_ != nullptr) {
|
||||
this->dtr_pin_->setup();
|
||||
this->dtr_pin_->digital_write(false);
|
||||
}
|
||||
|
||||
if (this->rts_pin_ != nullptr) {
|
||||
this->rts_pin_->setup();
|
||||
this->rts_pin_->digital_write(false);
|
||||
}
|
||||
|
||||
// A failed UART never assigned its port number, so the worker tasks would run
|
||||
// against an indeterminate port.
|
||||
if (this->uart_parent_->is_failed()) {
|
||||
ESP_LOGE(TAG, "UART parent failed; aborting");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->configured_baud_rate_ = this->uart_parent_->get_baud_rate();
|
||||
this->configured_parity_ = this->uart_parent_->get_parity();
|
||||
this->configured_stop_bits_ = this->uart_parent_->get_stop_bits();
|
||||
this->configured_data_bits_ = this->uart_parent_->get_data_bits();
|
||||
|
||||
// usb_cdc_acm sets up first (priority IO > HARDWARE). Any interface failing marks
|
||||
// the hub failed, and a failed hub no longer runs loop(), so line coding and line
|
||||
// state events would never reach this bridge even if its own interface is healthy.
|
||||
if (this->usb_cdc_parent_->get_parent()->is_failed()) {
|
||||
ESP_LOGE(TAG, "USB CDC ACM failed; aborting");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-instance task names (keyed on the CDC interface number) keep task dumps
|
||||
// unambiguous with multiple bridges.
|
||||
char tx_task_name[] = "cdc_uart_tx_0";
|
||||
char rx_task_name[] = "cdc_uart_rx_0";
|
||||
const char itf_char = format_hex_char(this->usb_cdc_parent_->get_itf());
|
||||
tx_task_name[sizeof(tx_task_name) - 2] = itf_char;
|
||||
rx_task_name[sizeof(rx_task_name) - 2] = itf_char;
|
||||
|
||||
xTaskCreate(uart_tx_task_fn, tx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_tx_task_handle_);
|
||||
if (this->uart_tx_task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create UART TX task");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
xTaskCreate(uart_rx_task_fn, rx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_rx_task_handle_);
|
||||
if (this->uart_rx_task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create UART RX task");
|
||||
vTaskDelete(this->uart_tx_task_handle_);
|
||||
this->uart_tx_task_handle_ = nullptr;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only register callbacks once both tasks exist, so a failed setup never drives
|
||||
// DTR/RTS from a dead bridge.
|
||||
this->usb_cdc_parent_->set_line_state_callback([this](bool dtr, bool rts) { this->set_line_state(dtr, rts); });
|
||||
this->usb_cdc_parent_->set_line_coding_callback([this](uint32_t, uint8_t, uint8_t, uint8_t) {
|
||||
this->host_coding_seen_ = true;
|
||||
// Another component owns the UART's framing while paused; resume() re-syncs.
|
||||
if (this->paused_ == 0) {
|
||||
this->set_line_coding();
|
||||
}
|
||||
});
|
||||
|
||||
// Release the workers only now: until here a failed setup may still delete the TX
|
||||
// task, which is safe only while it is parked and owns nothing in the driver.
|
||||
xTaskNotifyGive(this->uart_tx_task_handle_);
|
||||
xTaskNotifyGive(this->uart_rx_task_handle_);
|
||||
|
||||
// loop() only services line-coding reloads; stay off the main loop until one is
|
||||
// scheduled.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CDC-ACM UART Bridge:\n"
|
||||
" UART Bus: %u\n"
|
||||
" USB CDC Interface: %u",
|
||||
this->uart_parent_->get_hw_serial_number(), this->usb_cdc_parent_->get_itf());
|
||||
LOG_PIN(" DTR Pin: ", this->dtr_pin_);
|
||||
LOG_PIN(" RTS Pin: ", this->rts_pin_);
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::on_shutdown() {
|
||||
// The UART (BUS) shuts down after this component (HARDWARE) and deletes its driver,
|
||||
// freeing the ring buffer and mutexes the worker tasks block on. Suspending the
|
||||
// tasks unlinks them from those objects first.
|
||||
if (this->uart_rx_task_handle_ != nullptr) {
|
||||
vTaskSuspend(this->uart_rx_task_handle_);
|
||||
}
|
||||
if (this->uart_tx_task_handle_ != nullptr) {
|
||||
vTaskSuspend(this->uart_tx_task_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::loop() {
|
||||
switch (this->state_) {
|
||||
case MainState::MAIN_STATE_RELOAD_PENDING:
|
||||
if ((App.get_loop_component_start_time() - this->reload_requested_at_) < UART_RELOAD_SETTLE_MS) {
|
||||
return;
|
||||
}
|
||||
// Deliberately not gated on tx_idle_(): a host that re-codes the line mid-stream
|
||||
// wants the new framing now, and its own in-flight bytes are its concern.
|
||||
// apply_settings_live() rewrites the framing registers without reinstalling the
|
||||
// driver, so the worker tasks blocked inside it are undisturbed.
|
||||
this->uart_parent_->apply_settings_live();
|
||||
this->state_ = MainState::MAIN_STATE_RUNNING;
|
||||
break;
|
||||
case MainState::MAIN_STATE_PAUSING:
|
||||
case MainState::MAIN_STATE_RESUMING:
|
||||
// Let a host write that was in flight drain, FIFO included, before a reload
|
||||
// flushes the FIFOs and truncates it.
|
||||
if (!this->tx_idle_()) {
|
||||
return;
|
||||
}
|
||||
if (this->state_ == MainState::MAIN_STATE_PAUSING) {
|
||||
this->restore_configured_framing_();
|
||||
this->state_ = MainState::MAIN_STATE_PAUSED;
|
||||
} else {
|
||||
this->finish_resume_();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::set_line_coding() {
|
||||
if (!this->sync_host_framing_()) {
|
||||
return;
|
||||
}
|
||||
// Coalesce rapid line-coding updates from the host.
|
||||
this->reload_requested_at_ = App.get_loop_component_start_time();
|
||||
this->state_ = MainState::MAIN_STATE_RELOAD_PENDING;
|
||||
// Main-loop context (via USBCDCACMInstance::process_events_).
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
bool CDCACMUARTBridge::sync_host_framing_() {
|
||||
// usb_cdc_acm has already translated the wire coding onto the CDC instance (main
|
||||
// loop); mirror it here so the framing translation has a single source of truth.
|
||||
bool changed = false;
|
||||
|
||||
// Reject 0 (the CDC B0/hang-up encoding; older IDF revisions divide by the rate)
|
||||
// and rates above the SoC ceiling. Anything in between is the driver's call,
|
||||
// matching what a YAML-configured UART accepts.
|
||||
const uint32_t baud = this->usb_cdc_parent_->get_baud_rate();
|
||||
if (baud == 0 || baud > SOC_UART_BITRATE_MAX) {
|
||||
ESP_LOGW(TAG, "Ignoring unsupported baud rate %" PRIu32 " from host; keeping %" PRIu32, baud,
|
||||
this->uart_parent_->get_baud_rate());
|
||||
} else if (this->uart_parent_->get_baud_rate() != baud) {
|
||||
this->uart_parent_->set_baud_rate(baud);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const uint8_t stop_bits = this->usb_cdc_parent_->get_stop_bits();
|
||||
if (this->uart_parent_->get_stop_bits() != stop_bits) {
|
||||
this->uart_parent_->set_stop_bits(stop_bits);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const auto parity = this->usb_cdc_parent_->get_parity();
|
||||
if (this->uart_parent_->get_parity() != parity) {
|
||||
this->uart_parent_->set_parity(parity);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// USB CDC permits data-bit counts the UART cannot represent (up to 16).
|
||||
const uint8_t data_bits = this->usb_cdc_parent_->get_data_bits();
|
||||
if (data_bits < 5 || data_bits > 8) {
|
||||
ESP_LOGW(TAG, "Ignoring unsupported data bits %u from host; keeping %u", data_bits,
|
||||
this->uart_parent_->get_data_bits());
|
||||
} else if (this->uart_parent_->get_data_bits() != data_bits) {
|
||||
this->uart_parent_->set_data_bits(data_bits);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ESP_LOGV(TAG, "Line coding: baud=%" PRIu32 ", data_bits=%u, stop_bits=%u, parity=%u",
|
||||
this->uart_parent_->get_baud_rate(), this->uart_parent_->get_data_bits(),
|
||||
this->uart_parent_->get_stop_bits(), static_cast<uint8_t>(this->uart_parent_->get_parity()));
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::pause() {
|
||||
if (this->state_ == MainState::MAIN_STATE_PAUSING || this->state_ == MainState::MAIN_STATE_PAUSED) {
|
||||
return;
|
||||
}
|
||||
this->paused_ = 1;
|
||||
// A null RX task means setup() has not completed (or failed): nothing to stop, and
|
||||
// the framing snapshot does not exist yet. Should setup() run later, the RX task
|
||||
// starts parked.
|
||||
if (this->uart_rx_task_handle_ == nullptr) {
|
||||
this->state_ = MainState::MAIN_STATE_PAUSED;
|
||||
return;
|
||||
}
|
||||
// Drops a coalesced host reload or a pending resume; loop() restores the framing
|
||||
// once any host write in flight has drained.
|
||||
this->state_ = MainState::MAIN_STATE_PAUSING;
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::resume() {
|
||||
if (this->state_ != MainState::MAIN_STATE_PAUSING && this->state_ != MainState::MAIN_STATE_PAUSED) {
|
||||
return;
|
||||
}
|
||||
if (this->uart_rx_task_handle_ == nullptr) {
|
||||
this->paused_ = 0;
|
||||
this->state_ = MainState::MAIN_STATE_RUNNING;
|
||||
return;
|
||||
}
|
||||
// A restore still waiting on the TX side is moot: the host's framing is kept.
|
||||
if (!this->tx_idle_()) {
|
||||
this->state_ = MainState::MAIN_STATE_RESUMING;
|
||||
this->enable_loop();
|
||||
return;
|
||||
}
|
||||
this->finish_resume_();
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::finish_resume_() {
|
||||
// Take the bus back at a known framing before either task runs again: the host's
|
||||
// if it ever sent one, else the YAML framing (the other owner may have changed it).
|
||||
if (this->host_coding_seen_) {
|
||||
this->sync_host_framing_();
|
||||
this->uart_parent_->apply_settings_live();
|
||||
} else {
|
||||
this->restore_configured_framing_();
|
||||
}
|
||||
this->paused_ = 0;
|
||||
this->state_ = MainState::MAIN_STATE_RUNNING;
|
||||
this->drive_line_state_();
|
||||
xTaskNotifyGive(this->uart_rx_task_handle_);
|
||||
}
|
||||
|
||||
bool CDCACMUARTBridge::tx_idle_() {
|
||||
const auto uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
|
||||
return this->tx_busy_ == 0 && uart_wait_tx_done(uart_num, 0) == ESP_OK;
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::restore_configured_framing_() {
|
||||
// Always applied: the cached settings can lead the hardware by a pending reload,
|
||||
// so they are no proof of what is live.
|
||||
this->uart_parent_->set_baud_rate(this->configured_baud_rate_);
|
||||
this->uart_parent_->set_parity(this->configured_parity_);
|
||||
this->uart_parent_->set_stop_bits(this->configured_stop_bits_);
|
||||
this->uart_parent_->set_data_bits(this->configured_data_bits_);
|
||||
this->uart_parent_->apply_settings_live();
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::set_line_state(bool dtr, bool rts) {
|
||||
ESP_LOGV(TAG, "Line state: DTR=%d, RTS=%d", dtr, rts);
|
||||
this->host_dtr_ = dtr;
|
||||
this->host_rts_ = rts;
|
||||
// Frozen while paused: a host opening the port must not reset a peer that another
|
||||
// component is talking to.
|
||||
if (this->paused_ == 0) {
|
||||
this->drive_line_state_();
|
||||
}
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::drive_line_state_() {
|
||||
if (this->dtr_pin_ != nullptr) {
|
||||
this->dtr_pin_->digital_write(this->host_dtr_);
|
||||
}
|
||||
if (this->rts_pin_ != nullptr) {
|
||||
this->rts_pin_->digital_write(this->host_rts_);
|
||||
}
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::uart_rx_task_fn(void *arg) {
|
||||
auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
|
||||
bridge->uart_rx_task_();
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::uart_tx_task_fn(void *arg) {
|
||||
auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
|
||||
bridge->uart_tx_task_();
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::uart_rx_task_() {
|
||||
TaskHandle_t usb_tx_handle = this->usb_cdc_parent_->get_tx_task_handle();
|
||||
RingbufHandle_t usb_tx_ringbuf = this->usb_cdc_parent_->get_tx_ringbuf();
|
||||
uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
|
||||
// Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
|
||||
uint32_t tx_full_log_ms = millis() - LOG_THROTTLE_MS;
|
||||
uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
|
||||
|
||||
uint8_t *data = this->uart_rx_buffer_.data();
|
||||
const size_t buf_size = this->uart_rx_buffer_.size();
|
||||
|
||||
// Released by setup() once both tasks exist.
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
while (true) {
|
||||
if (this->paused_ != 0) {
|
||||
// Parked until resume() notifies; nothing is read, so the other owner sees
|
||||
// every byte.
|
||||
this->rx_parked_ = 1;
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
this->rx_parked_ = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Block until at least one byte is available from UART.
|
||||
int total_rx_size = uart_read_bytes(uart_num, data, 1, pdMS_TO_TICKS(UART_RX_WAIT_MS));
|
||||
if (total_rx_size < 0) {
|
||||
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||
ESP_LOGE(TAG, "UART read failed: %d", total_rx_size);
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
continue;
|
||||
}
|
||||
if (total_rx_size == 0) {
|
||||
continue;
|
||||
}
|
||||
// pause() landed during the read: don't forward a byte to a host that is gone.
|
||||
if (this->paused_ != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drain the currently buffered burst without waiting.
|
||||
while (true) {
|
||||
int rx_data_size = uart_read_bytes(uart_num, data + total_rx_size, buf_size - total_rx_size, 0);
|
||||
if (rx_data_size < 0) {
|
||||
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||
ESP_LOGE(TAG, "UART read failed: %d", rx_data_size);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (rx_data_size == 0) {
|
||||
break;
|
||||
}
|
||||
ESP_LOGV(TAG, "UART RX: %d bytes", rx_data_size);
|
||||
total_rx_size += rx_data_size;
|
||||
if (total_rx_size >= (int) buf_size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ringbuf_send_with_retry(usb_tx_ringbuf, data, total_rx_size, &tx_full_log_ms);
|
||||
|
||||
ESP_LOGV(TAG, "UART RX: waking up USB TX task");
|
||||
xTaskNotifyGive(usb_tx_handle);
|
||||
}
|
||||
}
|
||||
|
||||
void CDCACMUARTBridge::uart_tx_task_() {
|
||||
RingbufHandle_t usb_rx_ringbuf = this->usb_cdc_parent_->get_rx_ringbuf();
|
||||
uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
|
||||
uint8_t *data_to_uart = this->uart_tx_buffer_.data();
|
||||
const size_t buf_size = this->uart_tx_buffer_.size();
|
||||
size_t rx_size;
|
||||
// Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
|
||||
uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
|
||||
uint32_t drop_log_ms = millis() - LOG_THROTTLE_MS;
|
||||
|
||||
// Released by setup() once both tasks exist.
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
while (true) {
|
||||
ESP_LOGV(TAG, "Waiting for data to send to UART");
|
||||
esp_err_t ret = usb_cdc_acm::ringbuf_read_bytes(usb_rx_ringbuf, data_to_uart, buf_size, &rx_size, portMAX_DELAY);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||
ESP_LOGE(TAG, "USB RX RingBuf read failed");
|
||||
}
|
||||
// Yield: this task runs above the main loop, so a persistent failure must not
|
||||
// become a tight loop.
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Another component owns the UART; host bytes must not interleave with its traffic.
|
||||
// tx_busy_ goes up before the check so is_paused() cannot miss a write in flight.
|
||||
this->tx_busy_ = 1;
|
||||
if (this->paused_ != 0) {
|
||||
this->tx_busy_ = 0;
|
||||
if (should_log_now(&drop_log_ms, LOG_THROTTLE_MS)) {
|
||||
ESP_LOGW(TAG, "Paused; dropping %zu bytes from host", rx_size);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Sending %zu bytes to UART", rx_size);
|
||||
// Signed: uart_write_bytes() returns -1 on error.
|
||||
int xfer_size = uart_write_bytes(uart_num, data_to_uart, rx_size);
|
||||
this->tx_busy_ = 0;
|
||||
|
||||
if (xfer_size < 0) {
|
||||
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||
ESP_LOGE(TAG, "UART write failed: %d", xfer_size);
|
||||
}
|
||||
} else if (static_cast<size_t>(xfer_size) != rx_size) {
|
||||
ESP_LOGW(TAG, "UART write incomplete (%d/%zu bytes)", xfer_size, rx_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::cdc_acm_uart
|
||||
#endif
|
||||
@@ -1,118 +0,0 @@
|
||||
#pragma once
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#include "esphome/components/uart/uart_component_esp_idf.h"
|
||||
#include "esphome/components/usb_cdc_acm/usb_cdc_acm.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include "sdkconfig.h"
|
||||
|
||||
namespace esphome::cdc_acm_uart {
|
||||
|
||||
class CDCACMUARTBridge final : public Component {
|
||||
public:
|
||||
// Upper bound on the RX task's blocking read, so pause() takes effect without
|
||||
// aborting the read. Arriving bytes still unblock it immediately.
|
||||
static constexpr uint32_t UART_RX_WAIT_MS = 250;
|
||||
|
||||
CDCACMUARTBridge(uart::IDFUARTComponent *uart_parent, usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent)
|
||||
: uart_parent_(uart_parent), usb_cdc_parent_(usb_cdc_parent) {}
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void set_dtr_pin(GPIOPin *dtr_pin) { this->dtr_pin_ = dtr_pin; }
|
||||
void set_rts_pin(GPIOPin *rts_pin) { this->rts_pin_ = rts_pin; }
|
||||
|
||||
void set_line_coding();
|
||||
void set_line_state(bool dtr, bool rts);
|
||||
uart::IDFUARTComponent *get_uart_parent() const { return this->uart_parent_; }
|
||||
|
||||
/**
|
||||
* Stop forwarding in both directions and hand the UART back to its configured
|
||||
* framing, so another component may use the bus. Main-loop only. The RX task parks
|
||||
* within UART_RX_WAIT_MS (a byte it was already reading is discarded). A host write
|
||||
* already in flight is allowed to drain first, which at low baud rates can take
|
||||
* seconds; the framing is restored only after that, so poll is_paused() rather than
|
||||
* waiting a fixed interval. Host bytes not yet written to the UART are discarded.
|
||||
* The DTR/RTS outputs hold their state while paused and follow the host again on
|
||||
* resume().
|
||||
*/
|
||||
void pause();
|
||||
/**
|
||||
* Re-apply the host's line coding and line state, then resume forwarding. Main-loop
|
||||
* only. Deferred until any host write still draining has finished, so the reload
|
||||
* never truncates it.
|
||||
*/
|
||||
void resume();
|
||||
/// True once both worker tasks are off the bus and the configured framing is restored.
|
||||
/// With no RX task (setup() failed or has not run) there is nothing to wait for.
|
||||
bool is_paused() const {
|
||||
return this->state_ == MainState::MAIN_STATE_PAUSED &&
|
||||
(this->uart_rx_task_handle_ == nullptr || this->rx_parked_ != 0);
|
||||
}
|
||||
|
||||
protected:
|
||||
static void uart_rx_task_fn(void *arg);
|
||||
static void uart_tx_task_fn(void *arg);
|
||||
void uart_rx_task_();
|
||||
void uart_tx_task_();
|
||||
void restore_configured_framing_();
|
||||
// True when the TX task has no write in flight and the UART TX FIFO has drained.
|
||||
bool tx_idle_();
|
||||
void finish_resume_();
|
||||
void drive_line_state_();
|
||||
// Copy the host's line coding onto the UART settings; true if anything changed.
|
||||
bool sync_host_framing_();
|
||||
|
||||
TaskHandle_t uart_rx_task_handle_{nullptr};
|
||||
TaskHandle_t uart_tx_task_handle_{nullptr};
|
||||
|
||||
GPIOPin *dtr_pin_{nullptr};
|
||||
GPIOPin *rts_pin_{nullptr};
|
||||
|
||||
uint32_t reload_requested_at_{0};
|
||||
|
||||
// Worker staging, each sized to the CDC ring buffer it feeds or drains.
|
||||
std::array<uint8_t, CONFIG_TINYUSB_CDC_TX_BUFSIZE> uart_rx_buffer_{};
|
||||
std::array<uint8_t, CONFIG_TINYUSB_CDC_RX_BUFSIZE> uart_tx_buffer_{};
|
||||
|
||||
uart::IDFUARTComponent *uart_parent_;
|
||||
usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent_;
|
||||
|
||||
// YAML framing, captured at setup; the host's line coding overwrites the UART's
|
||||
// settings, so pause() needs the original to restore.
|
||||
uint32_t configured_baud_rate_{0};
|
||||
uart::UARTParityOptions configured_parity_{uart::UART_CONFIG_PARITY_NONE};
|
||||
uint8_t configured_stop_bits_{0};
|
||||
uint8_t configured_data_bits_{0};
|
||||
|
||||
// Written on the main loop, read by both worker tasks. uint8_t rather than bool:
|
||||
// GCC on Xtensa emits an out-of-line call for atomic<bool>.
|
||||
std::atomic<uint8_t> paused_{0};
|
||||
// Raised by the RX task while parked and by the TX task around each UART write, so
|
||||
// the pause hand-off knows when the bus is actually free.
|
||||
std::atomic<uint8_t> rx_parked_{0};
|
||||
std::atomic<uint8_t> tx_busy_{0};
|
||||
// Main-loop state; paused_ mirrors it for the worker tasks.
|
||||
enum class MainState : uint8_t {
|
||||
MAIN_STATE_RUNNING,
|
||||
MAIN_STATE_RELOAD_PENDING, // host line coding debounced, forwarding continues
|
||||
MAIN_STATE_PAUSING, // waiting for TX idle to restore the configured framing
|
||||
MAIN_STATE_PAUSED,
|
||||
MAIN_STATE_RESUMING, // resume() requested while a host write still drains
|
||||
};
|
||||
MainState state_{MainState::MAIN_STATE_RUNNING};
|
||||
// Host line state, recorded even while paused so resume() can re-drive the pins.
|
||||
bool host_dtr_{false};
|
||||
bool host_rts_{false};
|
||||
// True once the host has sent any line coding; resume() then re-syncs to it.
|
||||
bool host_coding_seen_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::cdc_acm_uart
|
||||
#endif
|
||||
@@ -30,7 +30,6 @@ CONF_KEYS = "keys"
|
||||
CONF_LABEL = "label"
|
||||
CONF_LIBRETINY = "libretiny"
|
||||
CONF_LOOP = "loop"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
CONF_NOX_INDEX = "nox_index"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir, remote_base
|
||||
from esphome.components import climate_ir
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
@@ -12,5 +12,4 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
remote_base.request_protocol("coolix") # used from C++
|
||||
await climate_ir.new_climate_ir(config)
|
||||
@@ -13,6 +13,12 @@ void CopySwitch::setup() {
|
||||
|
||||
void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); }
|
||||
|
||||
void CopySwitch::write_state(bool state) { this->source_->control(state); }
|
||||
void CopySwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
source_->turn_on();
|
||||
} else {
|
||||
source_->turn_off();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -23,7 +23,11 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
FILTER_SOURCE_FILES,
|
||||
DebugComponent,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["debug"]
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
FILTER_SOURCE_FILES,
|
||||
DebugComponent,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["debug"]
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
|
||||
this->status_set_warning();
|
||||
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
|
||||
}
|
||||
this->defer_sleep_();
|
||||
this->next_enter_deep_sleep_ = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ void DeepSleepComponent::setup() {
|
||||
|
||||
void DeepSleepComponent::schedule_sleep_() {
|
||||
this->next_enter_deep_sleep_ = false;
|
||||
this->disable_loop();
|
||||
const optional<uint32_t> run_duration = get_run_duration_();
|
||||
if (run_duration.has_value()) {
|
||||
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
|
||||
@@ -46,7 +45,7 @@ void DeepSleepComponent::loop() {
|
||||
|
||||
void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->prevent_ && !manual) {
|
||||
this->defer_sleep_();
|
||||
this->next_enter_deep_sleep_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -190,11 +190,6 @@ class DeepSleepComponent final : public Component {
|
||||
void schedule_sleep_();
|
||||
bool should_teardown_();
|
||||
|
||||
void defer_sleep_() {
|
||||
this->next_enter_deep_sleep_ = true;
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
|
||||
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
|
||||
|
||||
@@ -100,7 +100,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Waiting for wakeup pin state change");
|
||||
}
|
||||
this->defer_sleep_();
|
||||
this->next_enter_deep_sleep_ = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <utility>
|
||||
#include <numbers>
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -771,12 +770,10 @@ Rect Display::get_clipping() const {
|
||||
|
||||
void Display::clear_clipping_() { this->clipping_rectangle_.clear(); }
|
||||
|
||||
void Display::feed_wdt_pixel_slow_() { App.feed_wdt(); }
|
||||
|
||||
bool Display::clip(int x, int y) {
|
||||
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
|
||||
return false;
|
||||
if (this->is_point_clipped(x, y))
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -758,13 +758,6 @@ class Display : public PollingComponent {
|
||||
|
||||
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
|
||||
|
||||
/// Whether (x, y) falls outside the active clipping rectangle. Tests the
|
||||
/// stack top in place: get_clipping() is out of line and returns the Rect
|
||||
/// by value, which per pixel drawing cannot afford.
|
||||
bool ESPHOME_ALWAYS_INLINE is_point_clipped(int x, int y) const {
|
||||
return this->is_clipping() && !this->clipping_rectangle_.back().inside(x, y);
|
||||
}
|
||||
|
||||
/** Check if pixel is within region of display.
|
||||
*/
|
||||
bool clip(int x, int y);
|
||||
@@ -781,17 +774,6 @@ class Display : public PollingComponent {
|
||||
void do_update_();
|
||||
void clear_clipping_();
|
||||
|
||||
/// Watchdog feed for per pixel loops. App.feed_wdt() is already rate
|
||||
/// limited, but every call reads the clock; only every 256th pixel makes
|
||||
/// that call, so the real feeds are unchanged and a pixel costs a counter.
|
||||
/// At 20 us per pixel on the slowest e-paper path that is about 5 ms
|
||||
/// between clock reads.
|
||||
void ESPHOME_ALWAYS_INLINE feed_wdt_per_pixel_() {
|
||||
if (++this->wdt_pixel_counter_ == 0)
|
||||
this->feed_wdt_pixel_slow_();
|
||||
}
|
||||
void feed_wdt_pixel_slow_();
|
||||
|
||||
virtual int get_height_internal() = 0;
|
||||
virtual int get_width_internal() = 0;
|
||||
|
||||
@@ -811,7 +793,6 @@ class Display : public PollingComponent {
|
||||
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
|
||||
bool auto_clear_enabled_{true};
|
||||
std::vector<Rect> clipping_rectangle_;
|
||||
uint8_t wdt_pixel_counter_{0};
|
||||
bool show_test_card_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -43,7 +44,7 @@ int DisplayBuffer::get_height() {
|
||||
}
|
||||
|
||||
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||
if (this->is_point_clipped(x, y))
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return; // NOLINT
|
||||
|
||||
switch (this->rotation_) {
|
||||
@@ -63,7 +64,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||
break;
|
||||
}
|
||||
this->draw_absolute_pixel_internal(x, y, color);
|
||||
this->feed_wdt_per_pixel_();
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
} // namespace esphome::display
|
||||
@@ -63,6 +63,16 @@ bool Rect::equal(Rect rect) const {
|
||||
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
|
||||
}
|
||||
|
||||
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT
|
||||
if (!this->is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
|
||||
}
|
||||
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
|
||||
}
|
||||
|
||||
bool Rect::inside(Rect rect) const {
|
||||
if (!this->is_set() || !rect.is_set()) {
|
||||
return true;
|
||||
|
||||
@@ -26,15 +26,7 @@ class Rect {
|
||||
void shrink(Rect rect);
|
||||
|
||||
bool inside(Rect rect) const;
|
||||
bool ESPHOME_ALWAYS_INLINE inside(int16_t test_x, int16_t test_y, bool absolute = true) const {
|
||||
if (!this->is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
|
||||
}
|
||||
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
|
||||
}
|
||||
bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const;
|
||||
bool equal(Rect rect) const;
|
||||
void info(const std::string &prefix = "rect info:");
|
||||
};
|
||||
|
||||
@@ -299,7 +299,7 @@ bool EPaperBase::initialise(bool partial) {
|
||||
* @return false if the coordinates are out of bounds
|
||||
*/
|
||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
if (this->is_point_clipped(x, y))
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return false;
|
||||
if (this->effective_transform_ & SWAP_XY)
|
||||
std::swap(x, y);
|
||||
|
||||
@@ -153,14 +153,13 @@ bool ES7210::configure_mic_gain_() {
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
|
||||
|
||||
// Configure mic 3
|
||||
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
|
||||
|
||||
// Configure mic 4
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
|
||||
|
||||
@@ -111,7 +111,6 @@ 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_FLASH_CHIP = "flash_chip"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
||||
@@ -190,13 +189,6 @@ PSRAM_XIP_VARIANTS = {
|
||||
VARIANT_ESP32S31,
|
||||
}
|
||||
|
||||
# Variants whose ROM exports a full-format vsnprintf but no vasprintf
|
||||
# (esp32c6.rom.newlib-normal.ld). There, the newlib printf engine is only
|
||||
# linked because esp_http_client calls vasprintf; see vasprintf_stubs.cpp.
|
||||
# The other variants either export both (classic ESP32, nano-format only) or
|
||||
# neither, so the engine is already in the image and the wrap saves nothing.
|
||||
ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6}
|
||||
|
||||
# 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
|
||||
@@ -465,20 +457,6 @@ ESP32_CHIP_REVISIONS = {
|
||||
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
|
||||
}
|
||||
|
||||
# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM
|
||||
# and only the one matching the flash ID is ever used
|
||||
ESP32_FLASH_CHIPS = {
|
||||
"gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP",
|
||||
"issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP",
|
||||
"mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP",
|
||||
"winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP",
|
||||
"boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP",
|
||||
"th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP",
|
||||
"mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
|
||||
}
|
||||
FLASH_CHIP_GENERIC = "generic"
|
||||
FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only
|
||||
|
||||
# Socket limit configuration for ESP-IDF
|
||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
||||
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
||||
@@ -1534,13 +1512,6 @@ def final_validate(config) -> None:
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION],
|
||||
)
|
||||
)
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32S3 and config.get(CONF_FLASH_MODE) == "opi":
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_MODE}: opi' is only supported on {VARIANT_ESP32S3}",
|
||||
path=[CONF_FLASH_MODE],
|
||||
)
|
||||
)
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
@@ -1548,25 +1519,6 @@ def final_validate(config) -> None:
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
|
||||
)
|
||||
)
|
||||
if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None:
|
||||
opi = flash_chip == FLASH_CHIP_OPI
|
||||
if opi and config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||
)
|
||||
)
|
||||
elif opi != (config.get(CONF_FLASH_MODE) == "opi"):
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'"
|
||||
if opi
|
||||
else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match "
|
||||
f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||
)
|
||||
)
|
||||
if (
|
||||
config[CONF_VARIANT] != VARIANT_ESP32P4
|
||||
and config.get(CONF_ENGINEERING_SAMPLE) is not None
|
||||
@@ -1780,8 +1732,6 @@ CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
|
||||
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
|
||||
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||
CONF_DISABLE_MBEDTLS_TLS_SERVER = "disable_mbedtls_tls_server"
|
||||
CONF_DISABLE_MBEDTLS_TLS_EXTRAS = "disable_mbedtls_tls_extras"
|
||||
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
||||
@@ -1796,8 +1746,6 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
|
||||
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
||||
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_MBEDTLS_TLS_SERVER_REQUIRED = "mbedtls_tls_server_required"
|
||||
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED = "mbedtls_tls_extras_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||
@@ -1882,30 +1830,6 @@ def require_mbedtls_pkcs7() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_tls_server() -> None:
|
||||
"""Mark that the mbedTLS server-side TLS/DTLS handshake is required.
|
||||
|
||||
Call this from components that accept TLS connections (OpenThread's DTLS
|
||||
commissioner does). This prevents CONFIG_MBEDTLS_TLS_CLIENT_ONLY from
|
||||
being selected.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_tls_extras(options: Iterable[str] | None = None) -> None:
|
||||
"""Mark TLS features disabled by ``disable_mbedtls_tls_extras`` as required.
|
||||
|
||||
``options`` names the entries of ``MBEDTLS_TLS_EXTRA_OPTIONS`` to keep;
|
||||
omit it to keep all of them. Call this from components that need AES-CCM,
|
||||
deterministic ECDSA signing, static RSA/ECDH key exchange, TLS
|
||||
renegotiation or session tickets, or that run a TLS client against
|
||||
servers ESPHome cannot vet (wpa_supplicant's EAP client). A user-supplied
|
||||
sdkconfig_options value is never overridden either.
|
||||
"""
|
||||
required = CORE.data[KEY_ESP32].setdefault(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
|
||||
required.update(MBEDTLS_TLS_EXTRA_OPTIONS if options is None else options)
|
||||
|
||||
|
||||
def require_mbedtls_sha512() -> None:
|
||||
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
|
||||
|
||||
@@ -2005,9 +1929,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
*ESP32_CHIP_REVISIONS, string=True
|
||||
),
|
||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FLASH_CHIP): cv.one_of(
|
||||
FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True
|
||||
),
|
||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||
# it will handle disabling DHCP server when AP is not configured.
|
||||
# Default to false (disabled) when WiFi is not used.
|
||||
@@ -2066,8 +1987,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_SERVER, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_EXTRAS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||
@@ -2383,69 +2302,6 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
||||
|
||||
|
||||
# TLS features an HTTPS/MQTT client talking to a modern server never
|
||||
# negotiates. Static RSA and static ECDH key exchange have no forward secrecy
|
||||
# and are gone in TLS 1.3, renegotiation is deprecated, esp-tls never enables
|
||||
# session tickets, AES-CCM ciphersuites are not offered by web servers, and
|
||||
# deterministic ECDSA only matters when signing with a private key. Together
|
||||
# they cost ~10 KB of flash whenever TLS is linked (http_request, mqtt).
|
||||
# wpa_supplicant's EAP client is a second TLS client that talks to RADIUS
|
||||
# servers ESPHome cannot vet, and a failed EAP handshake leaves the device
|
||||
# off the network, so the wifi component re-enables all of these when eap is
|
||||
# configured.
|
||||
# The EC public key parsing extras stay enabled: they decide whether a peer
|
||||
# certificate with a compressed point or explicit curve parameters parses,
|
||||
# which no component can know ahead of time.
|
||||
MBEDTLS_TLS_EXTRA_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_KEY_EXCHANGE_RSA",
|
||||
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA",
|
||||
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA",
|
||||
"CONFIG_MBEDTLS_SSL_RENEGOTIATION",
|
||||
"CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS",
|
||||
"CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS",
|
||||
"CONFIG_MBEDTLS_CCM_C",
|
||||
"CONFIG_MBEDTLS_ECDSA_DETERMINISTIC",
|
||||
)
|
||||
|
||||
# Members of the mbedTLS "TLS Protocol Role" Kconfig choice. Setting one
|
||||
# member is only valid when the user has not already chosen another.
|
||||
MBEDTLS_TLS_ROLE_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
|
||||
"CONFIG_MBEDTLS_TLS_SERVER_ONLY",
|
||||
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
|
||||
"CONFIG_MBEDTLS_TLS_DISABLED",
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_mbedtls_tls_sdkconfig(
|
||||
disable_tls_server: bool, disable_tls_extras: bool
|
||||
) -> None:
|
||||
"""Trim mbedTLS to what a TLS client needs unless a component asked otherwise.
|
||||
|
||||
Runs at FINAL priority so every require_mbedtls_tls_server() and
|
||||
require_mbedtls_tls_extras() call has happened. Only the server-side
|
||||
handshake (~7 KB) is a separate option; nothing in ESPHome accepts TLS
|
||||
connections, but OpenThread's DTLS commissioner does. A user-supplied
|
||||
sdkconfig_options value always wins; for the TLS role choice, any member
|
||||
the user set leaves the whole choice alone so the pair cannot conflict.
|
||||
"""
|
||||
data = CORE.data[KEY_ESP32]
|
||||
sdkconfig = data[KEY_SDKCONFIG_OPTIONS]
|
||||
if (
|
||||
disable_tls_server
|
||||
and not data.get(KEY_MBEDTLS_TLS_SERVER_REQUIRED, False)
|
||||
and not any(option in sdkconfig for option in MBEDTLS_TLS_ROLE_OPTIONS)
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_CLIENT_ONLY", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", False)
|
||||
if disable_tls_extras:
|
||||
required = data.get(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
|
||||
for option in MBEDTLS_TLS_EXTRA_OPTIONS:
|
||||
if option not in required:
|
||||
set_idf_sdkconfig_default(option, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_network_sdkconfig() -> None:
|
||||
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
|
||||
@@ -2653,13 +2509,6 @@ async def to_code(config):
|
||||
# NVS finds stored preferences by key, so preference key migration is possible
|
||||
cg.add_define("USE_PREFERENCE_KEY_LOOKUP")
|
||||
cg.add_build_flag("-Wl,-z,noexecstack")
|
||||
# assert(), HAL_ASSERT and ESP_ERROR_CHECK bake __FILE__ into rodata, and
|
||||
# IDF's noflash placement puts the flash driver's copies in DRAM. The
|
||||
# basename keeps the panic output useful at a fraction of the size.
|
||||
# __FILE_NAME__ is a GCC 12 builtin; IDF 5.0 still ships GCC 11.2.
|
||||
if idf_version() >= cv.Version(5, 1, 0):
|
||||
cg.add_build_flag("-D__FILE__=__FILE_NAME__")
|
||||
cg.add_build_flag("-Wno-builtin-macro-redefined")
|
||||
# Deferred so KEY_COMPONENTS is fully populated -- see the coroutine.
|
||||
CORE.add_job(_finalize_arduino_aware_flags)
|
||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
@@ -2717,17 +2566,6 @@ async def to_code(config):
|
||||
else:
|
||||
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
|
||||
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
||||
# esp_http_client calls vasprintf, which on the ESP32-C6 is the only
|
||||
# reference to newlib's full printf engine (~20 KB: _svfprintf_r,
|
||||
# _dtoa_r and their helpers); every other caller resolves to the
|
||||
# ROM. See vasprintf_stubs.cpp. The --undefined flag is needed
|
||||
# because libsrc.a is scanned before the IDF libraries that
|
||||
# reference the symbol, so the stub would otherwise never be pulled
|
||||
# from the archive.
|
||||
if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS:
|
||||
cg.add_define("USE_ESP32_VASPRINTF_STUB")
|
||||
cg.add_build_flag("-Wl,--wrap=vasprintf")
|
||||
cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf")
|
||||
else:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||
@@ -2776,8 +2614,6 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
|
||||
)
|
||||
# the opi mode choice only exists once octal flash is enabled
|
||||
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_OCT_FLASH", flash_mode == "opi")
|
||||
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
|
||||
@@ -2807,11 +2643,6 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(flag, rev == min_rev)
|
||||
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
|
||||
|
||||
# Keep only the flash vendor driver the board needs; the boot log names it
|
||||
if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None:
|
||||
for chip, flag in ESP32_FLASH_CHIPS.items():
|
||||
add_idf_sdkconfig_option(flag, chip == flash_chip)
|
||||
|
||||
# Use SRAM1 region as IRAM on ESP32 (original) variant
|
||||
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
|
||||
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
|
||||
@@ -3165,13 +2996,6 @@ async def to_code(config):
|
||||
# FINAL priority: runs after every require_certificate_bundle() call
|
||||
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
|
||||
|
||||
# FINAL priority: runs after every require_mbedtls_tls_*() call
|
||||
CORE.add_job(
|
||||
_reconcile_mbedtls_tls_sdkconfig,
|
||||
advanced[CONF_DISABLE_MBEDTLS_TLS_SERVER],
|
||||
advanced[CONF_DISABLE_MBEDTLS_TLS_EXTRAS],
|
||||
)
|
||||
|
||||
# FINAL: require_*() calls can come from to_code at or below this priority, so an
|
||||
# inline read would be iteration-order-dependent; reconcile once after every job ran.
|
||||
CORE.add_job(
|
||||
|
||||
@@ -3,11 +3,18 @@ import esphome.codegen as cg
|
||||
# Re-exported for the many esp32-side users; defined in esphome.const
|
||||
# and esphome.espidf so the upload/logs fast path can use them without
|
||||
# importing this package.
|
||||
from esphome.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION, KEY_VARIANT # noqa: F401 # pylint: disable=unused-import
|
||||
from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
|
||||
KEY_ESP32,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_VARIANT,
|
||||
)
|
||||
|
||||
# Back compat for external components only; in-tree callers import it
|
||||
# from esphome.espidf directly.
|
||||
from esphome.espidf import variant_to_idf_target # noqa: F401 # pylint: disable=unused-import
|
||||
from esphome.espidf import ( # noqa: F401 # pylint: disable=unused-import
|
||||
variant_to_idf_target,
|
||||
)
|
||||
|
||||
KEY_BOARD = "board"
|
||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Linker wrap stub for vasprintf() on variants whose ROM exports a
|
||||
* full-format vsnprintf() but no vasprintf() (ESP32-C6, newlib only).
|
||||
*
|
||||
* On those chips every snprintf/vsnprintf call in the image resolves to
|
||||
* the ROM, so the newlib printf engine (_svfprintf_r, _dtoa_r and their
|
||||
* helpers, ~20 KB) is not linked at all until something references a
|
||||
* printf-family function the ROM lacks. esp_http_client does exactly that
|
||||
* through vasprintf() in its header and auth helpers, so adding
|
||||
* http_request to a build costs the whole engine on top of the HTTP and
|
||||
* TLS code itself.
|
||||
*
|
||||
* This stub reimplements vasprintf() on top of the ROM vsnprintf(), which
|
||||
* keeps the engine out of the image. It is only compiled in when codegen
|
||||
* defines USE_ESP32_VASPRINTF_STUB, which is gated on the variant's ROM
|
||||
* linker script and on the same newlib condition as printf_stubs.cpp.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP_IDF) && defined(USE_ESP32_VASPRINTF_STUB)
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
|
||||
int __wrap_vasprintf(char **strp, const char *fmt, va_list ap) {
|
||||
va_list ap_copy;
|
||||
va_copy(ap_copy, ap);
|
||||
int len = vsnprintf(nullptr, 0, fmt, ap_copy);
|
||||
va_end(ap_copy);
|
||||
if (len < 0) {
|
||||
return len;
|
||||
}
|
||||
// vasprintf's contract is a malloc'd buffer the caller releases with free()
|
||||
char *buf = static_cast<char *>(malloc(static_cast<size_t>(len) + 1)); // NOLINT(cppcoreguidelines-no-malloc)
|
||||
if (buf == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
vsnprintf(buf, static_cast<size_t>(len) + 1, fmt, ap);
|
||||
*strp = buf;
|
||||
return len;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_ESP_IDF && USE_ESP32_VASPRINTF_STUB
|
||||
@@ -3,7 +3,6 @@ import encodings
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
from esphome.components.esp32 import request_bluetooth
|
||||
from esphome.components.esp32_ble import BTLoggers, bt_uuid
|
||||
import esphome.config_validation as cv
|
||||
@@ -42,6 +41,7 @@ CONF_DESCRIPTORS = "descriptors"
|
||||
CONF_ENDIANNESS = "endianness"
|
||||
CONF_FIRMWARE_VERSION = "firmware_version"
|
||||
CONF_INDICATE = "indicate"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
CONF_MANUFACTURER_DATA = "manufacturer_data"
|
||||
CONF_MAX_CLIENTS = "max_clients"
|
||||
CONF_ON_WRITE = "on_write"
|
||||
@@ -597,7 +597,7 @@ async def to_code(config):
|
||||
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
|
||||
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
|
||||
# Only advertise for the server itself when the configuration gives clients something to
|
||||
# find. A server that is auto-loaded purely to host a runtime service (improv_ble) stays
|
||||
# find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays
|
||||
# silent until that service asks for advertising.
|
||||
cg.add(
|
||||
var.set_advertising_required(
|
||||
|
||||
@@ -40,7 +40,7 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
||||
|
||||
/** Whether this server needs the device to advertise so clients can find and connect to it.
|
||||
*
|
||||
* False for a server that only hosts services created at runtime (e.g. improv_ble), which
|
||||
* False for a server that only hosts services created at runtime (e.g. esp32_improv), which
|
||||
* request advertising themselves for as long as they need it.
|
||||
*/
|
||||
void set_advertising_required(bool required) { this->advertising_required_ = required; }
|
||||
|
||||
@@ -135,7 +135,7 @@ class ESPBTClient : public ESPBTDeviceListener {
|
||||
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
|
||||
|
||||
// Memory optimized layout
|
||||
uint8_t app_id{0}; // App IDs are small integers assigned sequentially
|
||||
uint8_t app_id; // App IDs are small integers assigned sequentially
|
||||
|
||||
protected:
|
||||
/// Set state without IDLE handling - use for direct state transitions.
|
||||
|
||||
@@ -433,10 +433,25 @@ void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) {
|
||||
}
|
||||
}
|
||||
void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; }
|
||||
void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
|
||||
void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
|
||||
void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; }
|
||||
void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; }
|
||||
void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; }
|
||||
void ESP32Camera::set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
|
||||
/* set exposure parameters */
|
||||
void ESP32Camera::set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
|
||||
void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; }
|
||||
void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
|
||||
void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
|
||||
/* set gains parameters */
|
||||
void ESP32Camera::set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
|
||||
void ESP32Camera::set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
|
||||
void ESP32Camera::set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
|
||||
/* set white balance */
|
||||
void ESP32Camera::set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
|
||||
/* set test mode */
|
||||
void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
|
||||
/* set fps */
|
||||
void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) {
|
||||
this->max_update_interval_ = max_update_interval;
|
||||
|
||||
@@ -140,25 +140,25 @@ class ESP32Camera final : public camera::Camera {
|
||||
void set_pixel_format(ESP32CameraPixelFormat format);
|
||||
void set_frame_size(ESP32CameraFrameSize size);
|
||||
void set_jpeg_quality(uint8_t quality);
|
||||
void set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
|
||||
void set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
|
||||
void set_contrast(int contrast) { this->contrast_ = contrast; }
|
||||
void set_brightness(int brightness) { this->brightness_ = brightness; }
|
||||
void set_saturation(int saturation) { this->saturation_ = saturation; }
|
||||
void set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
|
||||
void set_vertical_flip(bool vertical_flip);
|
||||
void set_horizontal_mirror(bool horizontal_mirror);
|
||||
void set_contrast(int contrast);
|
||||
void set_brightness(int brightness);
|
||||
void set_saturation(int saturation);
|
||||
void set_special_effect(ESP32SpecialEffect effect);
|
||||
/* -- exposure */
|
||||
void set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
|
||||
void set_aec2(bool aec2) { this->aec2_ = aec2; }
|
||||
void set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
|
||||
void set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
|
||||
void set_aec_mode(ESP32GainControlMode mode);
|
||||
void set_aec2(bool aec2);
|
||||
void set_ae_level(int ae_level);
|
||||
void set_aec_value(uint32_t aec_value);
|
||||
/* -- gains */
|
||||
void set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
|
||||
void set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
|
||||
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
|
||||
void set_agc_mode(ESP32GainControlMode mode);
|
||||
void set_agc_value(uint8_t agc_value);
|
||||
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling);
|
||||
/* -- white balance */
|
||||
void set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
|
||||
void set_wb_mode(ESP32WhiteBalanceMode mode);
|
||||
/* -- test */
|
||||
void set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
|
||||
void set_test_pattern(bool test_pattern);
|
||||
/* -- framerates */
|
||||
void set_max_update_interval(uint32_t max_update_interval);
|
||||
void set_idle_update_interval(uint32_t idle_update_interval);
|
||||
|
||||
@@ -169,7 +169,7 @@ void Esp32HostedUpdate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: HTTP\n"
|
||||
" Source URL: %s",
|
||||
this->source_url_);
|
||||
this->source_url_.c_str());
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: Embedded\n"
|
||||
@@ -215,7 +215,7 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
|
||||
auto container = this->http_request_parent_->get(this->source_url_);
|
||||
if (container == nullptr || container->status_code != 200) {
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_);
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
|
||||
this->status_set_error(LOG_STR("Failed to fetch manifest"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode setters
|
||||
void set_source_url(const char *url) { this->source_url_ = url; }
|
||||
void set_source_url(const std::string &url) { this->source_url_ = url; }
|
||||
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
|
||||
#else
|
||||
// Embedded mode setters
|
||||
@@ -38,7 +38,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode members
|
||||
http_request::HttpRequestComponent *http_request_parent_{nullptr};
|
||||
const char *source_url_{nullptr}; // literal from codegen
|
||||
std::string source_url_;
|
||||
std::string firmware_url_;
|
||||
|
||||
// HTTP mode helpers
|
||||
|
||||
+29
-61
@@ -1,41 +1,14 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, improv_base, output
|
||||
from esphome.components import binary_sensor, esp32_ble, improv_base, output
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_ON_START,
|
||||
CONF_ON_STATE,
|
||||
CONF_TRIGGER_ID,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# The BLE GATT server component that hosts the Improv service, per target
|
||||
# platform. improv_ble itself is platform neutral; supporting another chip
|
||||
# means adding its BLE server component here and the matching backend in
|
||||
# improv_ble_component.cpp. Doubles as the platform gate below, so an
|
||||
# unsupported chip is rejected in validation rather than at link time.
|
||||
BLE_SERVER_BACKENDS: dict[str, str] = {
|
||||
PLATFORM_ESP32: "esp32_ble_server",
|
||||
}
|
||||
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
auto_load = ["improv_base"]
|
||||
if backend := BLE_SERVER_BACKENDS.get(CORE.target_platform):
|
||||
auto_load.append(backend)
|
||||
return auto_load
|
||||
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
DEPENDENCIES = ["wifi"]
|
||||
|
||||
# Legacy top-level YAML key that routes here; esphome/loader.py and
|
||||
# esphome/config.py handle the warning and the key rename.
|
||||
ALIASES = ["esp32_improv"]
|
||||
ALIAS_REMOVAL_VERSION = "2027.4.0"
|
||||
DEPENDENCIES = ["wifi", "esp32"]
|
||||
|
||||
CONF_AUTHORIZED_DURATION = "authorized_duration"
|
||||
CONF_AUTHORIZER = "authorizer"
|
||||
@@ -56,29 +29,29 @@ improv_ns = cg.esphome_ns.namespace("improv")
|
||||
Error = improv_ns.enum("Error")
|
||||
State = improv_ns.enum("State")
|
||||
|
||||
improv_ble_ns = cg.esphome_ns.namespace("improv_ble")
|
||||
ImprovBLEComponent = improv_ble_ns.class_("ImprovBLEComponent", cg.Component)
|
||||
ImprovBLEProvisionedTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEProvisionedTrigger", automation.Trigger.template()
|
||||
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
|
||||
ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component)
|
||||
ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_(
|
||||
"ESP32ImprovProvisionedTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEProvisioningTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEProvisioningTrigger", automation.Trigger.template()
|
||||
ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_(
|
||||
"ESP32ImprovProvisioningTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStartTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStartTrigger", automation.Trigger.template()
|
||||
ESP32ImprovStartTrigger = esp32_improv_ns.class_(
|
||||
"ESP32ImprovStartTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStateTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStateTrigger", automation.Trigger.template()
|
||||
ESP32ImprovStateTrigger = esp32_improv_ns.class_(
|
||||
"ESP32ImprovStateTrigger", automation.Trigger.template()
|
||||
)
|
||||
ImprovBLEStoppedTrigger = improv_ble_ns.class_(
|
||||
"ImprovBLEStoppedTrigger", automation.Trigger.template()
|
||||
ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
|
||||
"ESP32ImprovStoppedTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ImprovBLEComponent),
|
||||
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
||||
cv.Required(CONF_AUTHORIZER): cv.Any(
|
||||
cv.none, cv.use_id(binary_sensor.BinarySensor)
|
||||
),
|
||||
@@ -95,60 +68,55 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEProvisionedTrigger
|
||||
ESP32ImprovProvisionedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEProvisioningTrigger
|
||||
ESP32ImprovProvisioningTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStartTrigger
|
||||
ESP32ImprovStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStateTrigger
|
||||
ESP32ImprovStateTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ImprovBLEStoppedTrigger
|
||||
ESP32ImprovStoppedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(improv_base.IMPROV_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on(list(BLE_SERVER_BACKENDS)),
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# ESP32 backend setup: the platform gate above means this is the only backend
|
||||
# that can reach to_code. Make it conditional when a second one is added.
|
||||
from esphome.components import esp32_ble
|
||||
|
||||
# Register the loggers this component needs
|
||||
esp32_ble.register_bt_logger(esp32_ble.BTLoggers.GATT, esp32_ble.BTLoggers.SMP)
|
||||
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add_define("USE_IMPROV_BLE")
|
||||
cg.add_define("USE_IMPROV")
|
||||
|
||||
await improv_base.setup_improv_core(var, config)
|
||||
await improv_base.setup_improv_core(var, config, "esp32_improv")
|
||||
|
||||
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
|
||||
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
|
||||
@@ -187,4 +155,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
use_state_callback = True
|
||||
if use_state_callback:
|
||||
cg.add_define("USE_IMPROV_BLE_STATE_CALLBACK")
|
||||
cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK")
|
||||
+19
-19
@@ -1,17 +1,17 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#include "improv_ble_component.h"
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
#include "esp32_improv_component.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <improv.h>
|
||||
|
||||
namespace esphome::improv_ble {
|
||||
namespace esphome::esp32_improv {
|
||||
|
||||
class ImprovBLEProvisionedTrigger final : public Trigger<> {
|
||||
class ESP32ImprovProvisionedTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEProvisionedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
@@ -20,12 +20,12 @@ class ImprovBLEProvisionedTrigger final : public Trigger<> {
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEProvisioningTrigger final : public Trigger<> {
|
||||
class ESP32ImprovProvisioningTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEProvisioningTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
@@ -34,12 +34,12 @@ class ImprovBLEProvisioningTrigger final : public Trigger<> {
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStartTrigger final : public Trigger<> {
|
||||
class ESP32ImprovStartTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEStartTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
|
||||
!this->parent_->is_failed()) {
|
||||
@@ -49,12 +49,12 @@ class ImprovBLEStartTrigger final : public Trigger<> {
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStateTrigger final : public Trigger<improv::State, improv::Error> {
|
||||
class ESP32ImprovStateTrigger final : public Trigger<improv::State, improv::Error> {
|
||||
public:
|
||||
explicit ImprovBLEStateTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (!this->parent_->is_failed()) {
|
||||
this->trigger(state, error);
|
||||
@@ -63,12 +63,12 @@ class ImprovBLEStateTrigger final : public Trigger<improv::State, improv::Error>
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ImprovBLEStoppedTrigger final : public Trigger<> {
|
||||
class ESP32ImprovStoppedTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ImprovBLEStoppedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
|
||||
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
@@ -77,10 +77,10 @@ class ImprovBLEStoppedTrigger final : public Trigger<> {
|
||||
}
|
||||
|
||||
protected:
|
||||
ImprovBLEComponent *parent_;
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::improv_ble
|
||||
} // namespace esphome::esp32_improv
|
||||
|
||||
#endif
|
||||
#endif
|
||||
+39
-36
@@ -1,7 +1,10 @@
|
||||
#include "improv_ble_component.h"
|
||||
#include "esp32_improv_component.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -12,15 +15,11 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
|
||||
|
||||
namespace esphome::improv_ble {
|
||||
namespace esphome::esp32_improv {
|
||||
|
||||
using namespace bytebuffer;
|
||||
|
||||
static const char *const TAG = "improv_ble.component";
|
||||
static const char *const TAG = "esp32_improv.component";
|
||||
static constexpr size_t IMPROV_MAX_LOG_BYTES = 128;
|
||||
static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
|
||||
// command + data length + trailing byte
|
||||
@@ -39,9 +38,9 @@ static constexpr uint8_t IMPROV_SERVICE_DATA_SIZE = 8;
|
||||
static constexpr uint8_t IMPROV_PROTOCOL_ID_1 = 0x77; // 'P' << 1 | 'R' >> 7
|
||||
static constexpr uint8_t IMPROV_PROTOCOL_ID_2 = 0x46; // 'I' << 1 | 'M' >> 7
|
||||
|
||||
ImprovBLEComponent::ImprovBLEComponent() { global_improv_component = this; }
|
||||
ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; }
|
||||
|
||||
void ImprovBLEComponent::setup() {
|
||||
void ESP32ImprovComponent::setup() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->authorizer_ != nullptr) {
|
||||
this->authorizer_->add_on_state_callback([this](bool state) {
|
||||
@@ -67,7 +66,7 @@ void ImprovBLEComponent::setup() {
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::setup_characteristics() {
|
||||
void ESP32ImprovComponent::setup_characteristics() {
|
||||
this->status_ = this->service_->create_characteristic(
|
||||
improv::STATUS_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
BLEDescriptor *status_descriptor = new BLE2902();
|
||||
@@ -105,11 +104,11 @@ void ImprovBLEComponent::setup_characteristics() {
|
||||
this->setup_complete_ = true;
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::loop() {
|
||||
void ESP32ImprovComponent::loop() {
|
||||
if (!global_ble_server->is_running()) {
|
||||
if (this->state_ != improv::STATE_STOPPED) {
|
||||
this->state_ = improv::STATE_STOPPED;
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
this->state_callback_.call(this->state_, this->error_state_);
|
||||
#endif
|
||||
}
|
||||
@@ -201,19 +200,23 @@ void ImprovBLEComponent::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::set_status_indicator_state_(bool state) {
|
||||
void ESP32ImprovComponent::set_status_indicator_state_(bool state) {
|
||||
#ifdef USE_OUTPUT
|
||||
if (this->status_indicator_ == nullptr)
|
||||
return;
|
||||
if (this->status_indicator_state_ == state)
|
||||
return;
|
||||
this->status_indicator_state_ = state;
|
||||
this->status_indicator_->set_state(state);
|
||||
if (state) {
|
||||
this->status_indicator_->turn_on();
|
||||
} else {
|
||||
this->status_indicator_->turn_off();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
const char *ImprovBLEComponent::state_to_string_(improv::State state) {
|
||||
const char *ESP32ImprovComponent::state_to_string_(improv::State state) {
|
||||
switch (state) {
|
||||
case improv::STATE_STOPPED:
|
||||
return "STOPPED";
|
||||
@@ -231,7 +234,7 @@ const char *ImprovBLEComponent::state_to_string_(improv::State state) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ImprovBLEComponent::check_identify_() {
|
||||
bool ESP32ImprovComponent::check_identify_() {
|
||||
uint32_t now = millis();
|
||||
|
||||
bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
|
||||
@@ -243,7 +246,7 @@ bool ImprovBLEComponent::check_identify_() {
|
||||
return identify;
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::set_state_(improv::State state, bool update_advertising) {
|
||||
void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) {
|
||||
// Skip if state hasn't changed
|
||||
if (this->state_ == state) {
|
||||
return;
|
||||
@@ -271,12 +274,12 @@ void ImprovBLEComponent::set_state_(improv::State state, bool update_advertising
|
||||
// Advertise the new state via service data
|
||||
this->advertise_service_data_();
|
||||
}
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
this->state_callback_.call(this->state_, this->error_state_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::set_error_(improv::Error error) {
|
||||
void ESP32ImprovComponent::set_error_(improv::Error error) {
|
||||
if (error != improv::ERROR_NONE) {
|
||||
ESP_LOGE(TAG, "Error: %d", error);
|
||||
}
|
||||
@@ -292,14 +295,14 @@ void ImprovBLEComponent::set_error_(improv::Error error) {
|
||||
}
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::send_response_(std::span<const uint8_t> response) {
|
||||
void ESP32ImprovComponent::send_response_(std::span<const uint8_t> response) {
|
||||
// The BLE characteristic owns its value, so one exact-size copy is required here
|
||||
this->rpc_response_->set_value(std::vector<uint8_t>(response.begin(), response.end()));
|
||||
if (this->state_ != improv::STATE_STOPPED)
|
||||
this->rpc_response_->notify();
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::start() {
|
||||
void ESP32ImprovComponent::start() {
|
||||
if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
|
||||
return;
|
||||
|
||||
@@ -317,7 +320,7 @@ void ImprovBLEComponent::start() {
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::stop() {
|
||||
void ESP32ImprovComponent::stop() {
|
||||
this->should_start_ = false;
|
||||
// Wait before stopping the service to ensure all BLE clients see the state change.
|
||||
// This prevents clients from repeatedly reconnecting and wasting resources by allowing
|
||||
@@ -332,10 +335,10 @@ void ImprovBLEComponent::stop() {
|
||||
});
|
||||
}
|
||||
|
||||
float ImprovBLEComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||
float ESP32ImprovComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||
|
||||
void ImprovBLEComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Improv BLE:");
|
||||
void ESP32ImprovComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ESP32 Improv:");
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_);
|
||||
#endif
|
||||
@@ -344,7 +347,7 @@ void ImprovBLEComponent::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::process_incoming_data_() {
|
||||
void ESP32ImprovComponent::process_incoming_data_() {
|
||||
if (this->incoming_data_.size() < 3)
|
||||
return;
|
||||
uint8_t length = this->incoming_data_[1];
|
||||
@@ -419,7 +422,7 @@ void ImprovBLEComponent::process_incoming_data_() {
|
||||
}
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::on_wifi_connect_timeout_() {
|
||||
void ESP32ImprovComponent::on_wifi_connect_timeout_() {
|
||||
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
||||
this->set_state_(improv::STATE_AUTHORIZED);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -430,7 +433,7 @@ void ImprovBLEComponent::on_wifi_connect_timeout_() {
|
||||
wifi::global_wifi_component->clear_sta();
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::check_wifi_connection_() {
|
||||
void ESP32ImprovComponent::check_wifi_connection_() {
|
||||
if (!wifi::global_wifi_component->is_connected()) {
|
||||
return;
|
||||
}
|
||||
@@ -444,7 +447,7 @@ void ImprovBLEComponent::check_wifi_connection_() {
|
||||
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
|
||||
improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS);
|
||||
|
||||
#ifdef USE_IMPROV_NEXT_URL
|
||||
#ifdef USE_ESP32_IMPROV_NEXT_URL
|
||||
// Add next_url if configured (should be first per Improv BLE spec)
|
||||
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
|
||||
#endif
|
||||
@@ -477,7 +480,7 @@ void ImprovBLEComponent::check_wifi_connection_() {
|
||||
this->stop();
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::advertise_service_data_() {
|
||||
void ESP32ImprovComponent::advertise_service_data_() {
|
||||
uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
|
||||
service_data[0] = IMPROV_PROTOCOL_ID_1; // PR
|
||||
service_data[1] = IMPROV_PROTOCOL_ID_2; // IM
|
||||
@@ -496,7 +499,7 @@ void ImprovBLEComponent::advertise_service_data_() {
|
||||
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>(service_data), false);
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::update_advertising_type_() {
|
||||
void ESP32ImprovComponent::update_advertising_type_() {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data
|
||||
@@ -521,21 +524,21 @@ void ImprovBLEComponent::update_advertising_type_() {
|
||||
}
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::request_advertising_() {
|
||||
void ESP32ImprovComponent::request_advertising_() {
|
||||
if (this->advertising_requested_)
|
||||
return;
|
||||
this->advertising_requested_ = true;
|
||||
esp32_ble::global_ble->advertising_start();
|
||||
}
|
||||
|
||||
void ImprovBLEComponent::release_advertising_() {
|
||||
void ESP32ImprovComponent::release_advertising_() {
|
||||
if (!this->advertising_requested_)
|
||||
return;
|
||||
this->advertising_requested_ = false;
|
||||
esp32_ble::global_ble->advertising_stop();
|
||||
}
|
||||
|
||||
improv::State ImprovBLEComponent::get_initial_state_() const {
|
||||
improv::State ESP32ImprovComponent::get_initial_state_() const {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// If we have an authorizer, start in awaiting authorization state
|
||||
return this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION;
|
||||
@@ -545,8 +548,8 @@ improv::State ImprovBLEComponent::get_initial_state_() const {
|
||||
#endif
|
||||
}
|
||||
|
||||
ImprovBLEComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::improv_ble
|
||||
} // namespace esphome::esp32_improv
|
||||
|
||||
#endif
|
||||
+10
-14
@@ -5,10 +5,12 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_server.h"
|
||||
#include "esphome/components/improv_base/improv_base.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
#include "esphome/core/automation.h"
|
||||
#endif
|
||||
|
||||
@@ -23,23 +25,17 @@
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
// ESP-IDF is currently the only target platform with a BLE GATT server, so it is
|
||||
// the only backend this component has. The Python side keeps the platform table
|
||||
// (BLE_SERVER_BACKENDS in __init__.py); a second backend adds another arm here.
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_server.h"
|
||||
|
||||
#include <improv.h>
|
||||
|
||||
namespace esphome::improv_ble {
|
||||
namespace esphome::esp32_improv {
|
||||
|
||||
using namespace esp32_ble_server;
|
||||
|
||||
class ImprovBLEComponent final : public Component, public improv_base::ImprovBase {
|
||||
class ESP32ImprovComponent final : public Component, public improv_base::ImprovBase {
|
||||
public:
|
||||
ImprovBLEComponent();
|
||||
ESP32ImprovComponent();
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
@@ -51,7 +47,7 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas
|
||||
bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
|
||||
bool should_start() const { return this->should_start_; }
|
||||
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
@@ -101,7 +97,7 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas
|
||||
|
||||
improv::State state_{improv::STATE_STOPPED};
|
||||
improv::Error error_state_{improv::ERROR_NONE};
|
||||
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
CallbackManager<void(improv::State, improv::Error)> state_callback_{};
|
||||
#endif
|
||||
|
||||
@@ -129,8 +125,8 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern ImprovBLEComponent *global_improv_component;
|
||||
extern ESP32ImprovComponent *global_improv_component;
|
||||
|
||||
} // namespace esphome::improv_ble
|
||||
} // namespace esphome::esp32_improv
|
||||
|
||||
#endif
|
||||
@@ -363,6 +363,14 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config.get(CONF_ENABLE_SERIAL1):
|
||||
enable_serial1()
|
||||
|
||||
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
|
||||
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
|
||||
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
|
||||
# a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3,
|
||||
# which always aborts if exceptions are disabled.
|
||||
# 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
|
||||
|
||||
@@ -11,9 +11,6 @@ namespace esphome::esp8266_pwm {
|
||||
|
||||
class ESP8266PWM final : public output::FloatOutput, public Component {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) ESP8266PWM()` would zero-fill .bss that is already zero.
|
||||
ESP8266PWM() {}
|
||||
|
||||
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
|
||||
void set_frequency(float frequency) { this->frequency_ = frequency; }
|
||||
/// Dynamically update frequency
|
||||
@@ -31,7 +28,7 @@ class ESP8266PWM final : public output::FloatOutput, public Component {
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
|
||||
InternalGPIOPin *pin_{nullptr};
|
||||
InternalGPIOPin *pin_;
|
||||
float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py
|
||||
/// Cache last output level for dynamic frequency updating
|
||||
float last_output_{0.0};
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include <lwip/dns.h>
|
||||
#include <cinttypes>
|
||||
#include "esp_event.h"
|
||||
#ifdef USE_PSRAM
|
||||
#include <esp_psram.h>
|
||||
#endif
|
||||
|
||||
// IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry;
|
||||
// they are no longer included via esp_eth.h and need explicit includes.
|
||||
@@ -71,6 +74,32 @@ static const char *const TAG = "ethernet";
|
||||
// PHY register size for hex logging
|
||||
static constexpr size_t PHY_REG_SIZE = 2;
|
||||
|
||||
// Dual wifi + ethernet SPI builds: the one place internal RAM is short and lwip's other buffers are
|
||||
// already in PSRAM. Not with L2 TAP, whose filter lives in the glue's input path this replaces.
|
||||
#if defined(USE_PSRAM) && defined(USE_ETHERNET_SPI) && defined(USE_WIFI) && !defined(CONFIG_ESP_NETIF_L2_TAP)
|
||||
#define USE_ETHERNET_RX_PSRAM
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - reported by dump_config()
|
||||
static bool rx_psram_installed = false;
|
||||
|
||||
// ESP-IDF ethernet drivers malloc() every received frame in internal RAM, where it stays until lwIP
|
||||
// hands it to the application. Move it to PSRAM; if that fails the frame is passed on where it is.
|
||||
static esp_err_t eth_input_to_psram(esp_eth_handle_t handle, uint8_t *buffer, uint32_t length, void *priv) {
|
||||
auto *copy = static_cast<uint8_t *>(heap_caps_malloc(length, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
if (copy != nullptr) {
|
||||
memcpy(copy, buffer, length);
|
||||
free(buffer); // NOLINT(cppcoreguidelines-no-malloc) - allocated by the driver with malloc()
|
||||
buffer = copy;
|
||||
} else {
|
||||
static bool warned = false; // once, this runs per frame in the driver's task
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
ESP_LOGW(TAG, "PSRAM allocation failed, frame kept in internal RAM (reported once)");
|
||||
}
|
||||
}
|
||||
return esp_netif_receive(static_cast<esp_netif_t *>(priv), buffer, length, nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
|
||||
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
@@ -453,6 +482,16 @@ void EthernetComponent::ethernet_lazy_init_() {
|
||||
/* attach Ethernet driver to TCP/IP stack */
|
||||
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
|
||||
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
|
||||
#ifdef USE_ETHERNET_RX_PSRAM
|
||||
// The glue frees every receive buffer with free(), so the replacement buffer must come from the heap
|
||||
if (esp_psram_is_initialized()) {
|
||||
err = esp_eth_update_input_path(this->eth_handle_, eth_input_to_psram, this->eth_netif_);
|
||||
rx_psram_installed = err == ESP_OK;
|
||||
if (!rx_psram_installed) {
|
||||
ESP_LOGW(TAG, "PSRAM RX path not installed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Register user defined event handers
|
||||
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
|
||||
@@ -630,6 +669,13 @@ void EthernetComponent::dump_config() {
|
||||
this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_);
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
|
||||
#ifdef USE_ETHERNET_RX_PSRAM
|
||||
// Only known once the driver is up; with enable_on_boot: false that is after this dump
|
||||
if (this->ethernet_initialized_) {
|
||||
ESP_LOGCONFIG(TAG, " RX frames: %s",
|
||||
rx_psram_installed ? LOG_STR_LITERAL("PSRAM") : LOG_STR_LITERAL("internal RAM"));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
|
||||
@@ -13,9 +13,6 @@ class IPAddressEthernetInfo final : public Component,
|
||||
public text_sensor::TextSensor,
|
||||
public ethernet::EthernetIPStateListener {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) IPAddressEthernetInfo()` would zero-fill .bss that is already zero.
|
||||
IPAddressEthernetInfo() {}
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
|
||||
|
||||
@@ -183,7 +183,7 @@ class Fan : public EntityBase {
|
||||
|
||||
LazyCallbackManager<void()> state_callback_{};
|
||||
ESPPreferenceObject rtc_;
|
||||
FanRestoreMode restore_mode_{FanRestoreMode::NO_RESTORE};
|
||||
FanRestoreMode restore_mode_;
|
||||
|
||||
private:
|
||||
/// Lazy-allocate preset modes vector (never freed — entity lives forever).
|
||||
|
||||
@@ -47,9 +47,6 @@ class GPIOBinarySensorStore {
|
||||
|
||||
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) GPIOBinarySensor()` would zero-fill .bss that is already zero.
|
||||
GPIOBinarySensor() {}
|
||||
|
||||
// No destructor needed: ESPHome components are created at boot and live forever.
|
||||
// Interrupts are only detached on reboot when memory is cleared anyway.
|
||||
|
||||
@@ -73,7 +70,7 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
GPIOPin *pin_{nullptr};
|
||||
GPIOPin *pin_;
|
||||
GPIOBinarySensorStore store_;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,10 +13,18 @@ void GPIOSwitch::setup() {
|
||||
bool initial_state = this->get_initial_state_with_restore_mode().value_or(false);
|
||||
|
||||
// write state before setup
|
||||
this->control(initial_state);
|
||||
if (initial_state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
this->pin_->setup();
|
||||
// write after setup again for other IOs
|
||||
this->control(initial_state);
|
||||
if (initial_state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
}
|
||||
void GPIOSwitch::dump_config() {
|
||||
LOG_SWITCH("", "GPIO Switch", this);
|
||||
|
||||
@@ -9,9 +9,6 @@ namespace esphome::gpio {
|
||||
|
||||
class GPIOSwitch final : public switch_::Switch, public Component {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) GPIOSwitch()` would zero-fill .bss that is already zero.
|
||||
GPIOSwitch() {}
|
||||
|
||||
void set_pin(GPIOPin *pin) { pin_ = pin; }
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
@@ -28,7 +25,7 @@ class GPIOSwitch final : public switch_::Switch, public Component {
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
GPIOPin *pin_{nullptr};
|
||||
GPIOPin *pin_;
|
||||
#ifdef USE_GPIO_SWITCH_INTERLOCK
|
||||
FixedVector<Switch *> interlock_;
|
||||
uint32_t interlock_wait_time_{0};
|
||||
|
||||
Loaded 100 of 528 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user