mirror of
https://github.com/esphome/esphome.git
synced 2026-09-07 13:36:03 +00:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f9812cef6 | ||
|
|
a684e7e67d | ||
|
|
afc93a4dc8 | ||
|
|
0a74cb4e89 | ||
|
|
9c4a08aaad | ||
|
|
68d5968405 | ||
|
|
30da0e81fa | ||
|
|
900638463a | ||
|
|
1665d509d4 | ||
|
|
29d8f9c7c6 | ||
|
|
18b5277254 | ||
|
|
b110cee973 | ||
|
|
1171fbf892 | ||
|
|
ae549b25bb | ||
|
|
35ab455c10 | ||
|
|
dae9a5d1a6 | ||
|
|
bf380037a6 | ||
|
|
493c7265ab | ||
|
|
238b6bb0b7 | ||
|
|
0211bdf2c0 | ||
|
|
a95bfe0bcb | ||
|
|
3a0bbc4c17 | ||
|
|
cc9dc95cab | ||
|
|
79325fe59a | ||
|
|
5fbe09b68c | ||
|
|
f41ff5aaeb | ||
|
|
7b57fdd35c | ||
|
|
fc671d38e5 | ||
|
|
4e0cda0287 | ||
|
|
676eac7686 |
@@ -374,9 +374,8 @@ jobs:
|
|||||||
- name: Install apt packages (cached)
|
- name: Install apt packages (cached)
|
||||||
# ccache speeds up the host compiles. A cache hit never touches apt
|
# ccache speeds up the host compiles. A cache hit never touches apt
|
||||||
# (mirror outages cannot hang the job); the timeout bounds the cold
|
# (mirror outages cannot hang the job); the timeout bounds the cold
|
||||||
# path. Packages and version must match seed-apt-cache exactly.
|
# path. Packages and version must match seed-apt-cache exactly;
|
||||||
# libsdl2-dev is needed by the headless display tests, which capture
|
# libsdl2-dev is unused here and carried only for cache-key parity.
|
||||||
# screenshots.
|
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||||
with:
|
with:
|
||||||
@@ -439,16 +438,6 @@ jobs:
|
|||||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||||
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
|
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
|
||||||
--junitxml=junit-integration.xml "${test_files[@]}"
|
--junitxml=junit-integration.xml "${test_files[@]}"
|
||||||
- name: Upload test artifacts
|
|
||||||
# Tests that compare rendered output write the image they actually got here, so a
|
|
||||||
# failure can be looked at without reproducing the whole build locally.
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: integration-test-artifacts-${{ matrix.bucket.name }}
|
|
||||||
path: test_artifacts/
|
|
||||||
if-no-files-found: ignore
|
|
||||||
retention-days: 7
|
|
||||||
- name: Upload junit timings
|
- name: Upload junit timings
|
||||||
# Consumed by sync-integration-durations.yml through
|
# Consumed by sync-integration-durations.yml through
|
||||||
# script/update_integration_test_durations.py; only full matrix dev
|
# script/update_integration_test_durations.py; only full matrix dev
|
||||||
|
|||||||
@@ -137,8 +137,6 @@ config/
|
|||||||
!tests/component_tests/**/config/
|
!tests/component_tests/**/config/
|
||||||
tests/build/
|
tests/build/
|
||||||
tests/.esphome/
|
tests/.esphome/
|
||||||
# Output kept by failing tests for inspection; uploaded by CI
|
|
||||||
test_artifacts/
|
|
||||||
/.temp-clang-tidy.cpp
|
/.temp-clang-tidy.cpp
|
||||||
/.temp/
|
/.temp/
|
||||||
.pio/
|
.pio/
|
||||||
|
|||||||
@@ -44,16 +44,6 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
|
|
||||||
## 4. Coding Conventions & Style Guide
|
## 4. Coding Conventions & Style Guide
|
||||||
|
|
||||||
**Read the developer documentation before writing a component.** https://developers.esphome.io covers the
|
|
||||||
component lifecycle, the main loop, and the reasoning behind the rules below in far more depth than this
|
|
||||||
file does, and it is the authority when they disagree. The most useful starting points:
|
|
||||||
|
|
||||||
* https://developers.esphome.io/architecture/components/ - component lifecycle, `setup()`, `loop()`,
|
|
||||||
setup priorities, and how a component is registered.
|
|
||||||
* https://developers.esphome.io/architecture/components/advanced/ - choosing between `loop()`,
|
|
||||||
`set_interval`, `set_timeout` and `defer`; waking the loop from another thread; the RAM cost of each.
|
|
||||||
* https://developers.esphome.io/contributing/code/ - contribution rules, public API and breaking changes.
|
|
||||||
|
|
||||||
* **Formatting:**
|
* **Formatting:**
|
||||||
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
|
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
|
||||||
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
|
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
|
||||||
@@ -152,47 +142,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
|||||||
* **Indentation:** Use spaces (two per indentation level), not tabs
|
* **Indentation:** Use spaces (two per indentation level), not tabs
|
||||||
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
|
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
|
||||||
* **Line length:** Wrap lines at no more than 120 characters
|
* **Line length:** Wrap lines at no more than 120 characters
|
||||||
* **Timing in `loop()`:** Never call `millis()` in a `loop()` body. The current tick's timestamp is
|
|
||||||
already cached - use `App.get_loop_component_start_time()` (from `esphome/core/application.h`).
|
|
||||||
Only reach for `millis()` when you genuinely need sub-tick resolution inside a long operation.
|
|
||||||
* **The main loop runs every 16 ms.** A rate-limit gate shorter than that does nothing: the check
|
|
||||||
passes on essentially every pass of the loop, so it costs a comparison and buys nothing. Pick an
|
|
||||||
interval comfortably coarser than 16 ms, or drop the gate entirely and accept running every loop.
|
|
||||||
```cpp
|
|
||||||
// Bad - a 10ms gate against a 16ms loop never holds anything back
|
|
||||||
static constexpr uint32_t POLL_INTERVAL_MS = 10;
|
|
||||||
const uint32_t now = millis();
|
|
||||||
if (now - this->last_poll_ < POLL_INTERVAL_MS)
|
|
||||||
return;
|
|
||||||
this->last_poll_ = now;
|
|
||||||
```
|
|
||||||
```cpp
|
|
||||||
// Good - an interval that actually rate limits, off the cached timestamp
|
|
||||||
static constexpr uint32_t POLL_INTERVAL_MS = 100;
|
|
||||||
const uint32_t now = App.get_loop_component_start_time();
|
|
||||||
if (now - this->last_poll_ < POLL_INTERVAL_MS)
|
|
||||||
return;
|
|
||||||
this->last_poll_ = now;
|
|
||||||
```
|
|
||||||
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
|
|
||||||
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
|
|
||||||
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
|
|
||||||
* **Don't override a default with the same value:** if a base class method already returns what you
|
|
||||||
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
|
|
||||||
component that wants `DATA` should simply leave it alone.
|
|
||||||
```cpp
|
|
||||||
// Bad - this is exactly what the base class already does
|
|
||||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
|
||||||
```
|
|
||||||
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
|
|
||||||
can be stored in flash rather than RAM.
|
|
||||||
```cpp
|
|
||||||
// Bad
|
|
||||||
ESP_LOGV(TAG, "Key %u %s", key, pressed ? "pressed" : "released");
|
|
||||||
|
|
||||||
// Good
|
|
||||||
ESP_LOGV(TAG, "Key %u %s", key, pressed ? LOG_STR_LITERAL("pressed") : LOG_STR_LITERAL("released"));
|
|
||||||
```
|
|
||||||
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
|
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
|
||||||
(never change after construction) should be constructor parameters rather than set via setter methods.
|
(never change after construction) should be constructor parameters rather than set via setter methods.
|
||||||
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
|
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
|
||||||
@@ -553,7 +502,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
|||||||
4. **Lint:** Run `prek` to ensure code is compliant.
|
4. **Lint:** Run `prek` to ensure code is compliant.
|
||||||
5. **Commit:** Commit your changes. There is no strict format for commit messages.
|
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.
|
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 Contributions:**
|
||||||
* Documentation is hosted in the separate `esphome/esphome.io` repository.
|
* Documentation is hosted in the separate `esphome/esphome.io` repository.
|
||||||
@@ -614,33 +562,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
|||||||
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
|
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
|
||||||
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
|
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
|
||||||
|
|
||||||
**Listener and child-entity registration lists are the most common case, and the most commonly
|
|
||||||
missed.** A `register_*()` method called once per child at code generation time has a count that
|
|
||||||
is known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it
|
|
||||||
returns a function that each consumer calls once per slot it will occupy, and after every
|
|
||||||
`to_code` has run it emits the define with the final count. When nothing registers, no define is
|
|
||||||
emitted and the storage plus its registration method compile out entirely.
|
|
||||||
```python
|
|
||||||
# hub component's __init__.py
|
|
||||||
_request_listener_slot = cg.slot_counter("MY_COMPONENT_LISTENER_COUNT")
|
|
||||||
|
|
||||||
|
|
||||||
async def register_listener(hub: MockObj, var: MockObj) -> None:
|
|
||||||
_request_listener_slot()
|
|
||||||
cg.add(hub.register_listener(var))
|
|
||||||
```
|
|
||||||
```cpp
|
|
||||||
#ifdef MY_COMPONENT_LISTENER_COUNT
|
|
||||||
void register_listener(MyComponentListener *listener);
|
|
||||||
#endif
|
|
||||||
protected:
|
|
||||||
#ifdef MY_COMPONENT_LISTENER_COUNT
|
|
||||||
StaticVector<MyComponentListener *, MY_COMPONENT_LISTENER_COUNT> listeners_;
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL` - a late
|
|
||||||
request raises rather than silently undercounting.
|
|
||||||
|
|
||||||
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
|
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
|
||||||
```cpp
|
```cpp
|
||||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||||
@@ -678,25 +599,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
|||||||
```
|
```
|
||||||
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
|
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
|
||||||
|
|
||||||
5. **Strings set once from configuration:** Use `StringRef` (`esphome/core/string_ref.h`) rather than
|
5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
||||||
`std::string`. Code generation passes a string literal that lives in flash for the life of the
|
|
||||||
program, so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a
|
|
||||||
non-owning pointer plus length; it does not copy, and it must only ever refer to storage that
|
|
||||||
outlives it (a string literal, or a buffer owned elsewhere).
|
|
||||||
```cpp
|
|
||||||
// Bad - heap copy of a literal that is already in flash
|
|
||||||
void set_keys(std::string keys) { this->keys_ = std::move(keys); }
|
|
||||||
std::string keys_;
|
|
||||||
```
|
|
||||||
```cpp
|
|
||||||
// Good - no allocation
|
|
||||||
void set_keys(const char *keys) { this->keys_ = StringRef(keys); }
|
|
||||||
StringRef keys_;
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
6. **Detection:** Look for these patterns in compiler output:
|
||||||
|
|
||||||
7. **Detection:** Look for these patterns in compiler output:
|
|
||||||
- Large code sections with STL symbols (vector, map, set)
|
- Large code sections with STL symbols (vector, map, set)
|
||||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ esphome/components/cst816/* @clydebarrow
|
|||||||
esphome/components/cst9220/* @clydebarrow
|
esphome/components/cst9220/* @clydebarrow
|
||||||
esphome/components/ct_clamp/* @jesserockz
|
esphome/components/ct_clamp/* @jesserockz
|
||||||
esphome/components/current_based/* @djwmarcx
|
esphome/components/current_based/* @djwmarcx
|
||||||
esphome/components/d01/* @ch604
|
|
||||||
esphome/components/dac7678/* @NickB1
|
esphome/components/dac7678/* @NickB1
|
||||||
esphome/components/daikin_arc/* @MagicBear
|
esphome/components/daikin_arc/* @MagicBear
|
||||||
esphome/components/daikin_brc/* @hagak
|
esphome/components/daikin_brc/* @hagak
|
||||||
@@ -149,7 +148,6 @@ esphome/components/display_menu_base/* @numo68
|
|||||||
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
||||||
esphome/components/dps310/* @kbx81
|
esphome/components/dps310/* @kbx81
|
||||||
esphome/components/ds1307/* @badbadc0ffee
|
esphome/components/ds1307/* @badbadc0ffee
|
||||||
esphome/components/ds1603l/* @JakeLC15
|
|
||||||
esphome/components/ds2484/* @mrk-its
|
esphome/components/ds2484/* @mrk-its
|
||||||
esphome/components/ds248x/* @tomwellnitz
|
esphome/components/ds248x/* @tomwellnitz
|
||||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||||
@@ -496,7 +494,6 @@ esphome/components/sm2335/* @Cossid
|
|||||||
esphome/components/sml/* @alengwenus
|
esphome/components/sml/* @alengwenus
|
||||||
esphome/components/smt100/* @piechade
|
esphome/components/smt100/* @piechade
|
||||||
esphome/components/sn74hc165/* @jesserockz
|
esphome/components/sn74hc165/* @jesserockz
|
||||||
esphome/components/snapshot/* @clydebarrow
|
|
||||||
esphome/components/socket/* @esphome/core
|
esphome/components/socket/* @esphome/core
|
||||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||||
esphome/components/sound_level/* @kahrendt
|
esphome/components/sound_level/* @kahrendt
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2026.10.0-dev
|
PROJECT_NUMBER = 2026.9.0-dev
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
+3
-52
@@ -23,8 +23,7 @@ For this repository there are two trusted inputs by design:
|
|||||||
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
|
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
|
||||||
(see below).
|
(see below).
|
||||||
2. **Authenticated peers of a running device** — clients holding the device's
|
2. **Authenticated peers of a running device** — clients holding the device's
|
||||||
API/OTA encryption key, API password, OTA password, or web server
|
API encryption key / password, OTA password, or web server credentials.
|
||||||
credentials.
|
|
||||||
|
|
||||||
The security boundary is therefore **unauthenticated network traffic vs. those
|
The security boundary is therefore **unauthenticated network traffic vs. those
|
||||||
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
|
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
|
||||||
@@ -77,8 +76,8 @@ These *are* security bugs in this repo, and we want to hear about them privately
|
|||||||
captive portal, etc.) **without** valid credentials.
|
captive portal, etc.) **without** valid credentials.
|
||||||
- Authentication or encryption bypass on the device — reaching API calls, OTA
|
- Authentication or encryption bypass on the device — reaching API calls, OTA
|
||||||
updates, or the web server without the configured key/password.
|
updates, or the web server without the configured key/password.
|
||||||
- Flaws that weaken the device's API or OTA encryption (Noise), OTA auth, or
|
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
|
||||||
web server auth below their documented guarantees.
|
below their documented guarantees.
|
||||||
|
|
||||||
## The web server is an open HTTP API by design
|
## The web server is an open HTTP API by design
|
||||||
|
|
||||||
@@ -122,54 +121,6 @@ and any memory-safety or protocol bug in the server reachable without credential
|
|||||||
This section documents the current design and scope; it is not a judgment that the
|
This section documents the current design and scope; it is not a judgment that the
|
||||||
design is optimal or that it will not change.
|
design is optimal or that it will not change.
|
||||||
|
|
||||||
## OTA update encryption
|
|
||||||
|
|
||||||
The `esphome` OTA platform optionally encrypts updates with the same Noise
|
|
||||||
`NNpsk0` pattern the native API uses; one key protects the device. A device
|
|
||||||
whose `api:` block has an encryption key, static in the YAML or provisioned at
|
|
||||||
runtime, compiles in the transport and offers it on every OTA connection once
|
|
||||||
it holds a key, so an uploader presenting that key gets the guarantees below
|
|
||||||
even without an `ota: encryption:` block; only that block makes the device
|
|
||||||
require encryption. The guarantees are: the firmware image is confidential in
|
|
||||||
transit, the uploader is authenticated by the pre-shared key, and the plaintext
|
|
||||||
negotiation preceding the handshake is bound into the handshake prologue, so
|
|
||||||
stripping or tampering with it fails the first MAC. With `ota: encryption:`
|
|
||||||
configured both ends fail closed with no override: the device refuses
|
|
||||||
plaintext uploads, and the CLI refuses to send plaintext when a key is
|
|
||||||
configured. Without that block the CLI tries a static api key when the device
|
|
||||||
offers and, until 2027.3.0, falls back to plaintext with a warning when the
|
|
||||||
offer is missing or the handshake fails; a runtime provisioned key never
|
|
||||||
reaches the CLI, so those uploads stay plaintext.
|
|
||||||
|
|
||||||
Defeating any of that without the key is in scope: a device that requires
|
|
||||||
encryption accepting a plaintext or downgraded upload, getting past the MAC,
|
|
||||||
or recovering image contents from captured traffic.
|
|
||||||
|
|
||||||
The following are **not** vulnerabilities, by design:
|
|
||||||
|
|
||||||
- Plaintext OTA on a device with no `ota: encryption:` block, including one
|
|
||||||
that offers encryption because it has an api key. That is the documented
|
|
||||||
default, authenticated (if at all) by the OTA password. An uploader that
|
|
||||||
takes the offer skips the password; the key authenticates it. With a
|
|
||||||
runtime provisioned key and no `provisioning:` window, whoever provisions
|
|
||||||
the key gains that upload path too; validation warns about the pair.
|
|
||||||
- The CLI plaintext fallback until 2027.3.0: without `ota: encryption:` an
|
|
||||||
active attacker who strips the offer or breaks the handshake can make a
|
|
||||||
keyed CLI upload plaintext, with the pre-existing plaintext exposure. A
|
|
||||||
device that requires encryption still refuses that upload.
|
|
||||||
- The enablement window: firmware built with a static api key already offers
|
|
||||||
encryption, so turning on `ota: encryption:` is itself an encrypted upload.
|
|
||||||
Older firmware needs one last plaintext upload of an offering build, with
|
|
||||||
the pre-existing plaintext exposure.
|
|
||||||
- The web OTA `/update` endpoint alongside encryption. With the `web_server`
|
|
||||||
or `prometheus` component the shared listener is always up, so the endpoint
|
|
||||||
stays reachable and validation warns about that combination;
|
|
||||||
`captive_portal:` alone brings the listener up only for the fallback AP
|
|
||||||
window, which is the intended recovery path, so that is not warned about.
|
|
||||||
- CLI retry behavior on transport or MAC failures; every attempt renegotiates
|
|
||||||
a fresh handshake with fresh ephemerals, so retrying does not weaken
|
|
||||||
authentication.
|
|
||||||
|
|
||||||
## Explicitly out of scope
|
## Explicitly out of scope
|
||||||
|
|
||||||
- Local attackers who already have shell access on the host that runs `esphome`.
|
- Local attackers who already have shell access on the host that runs `esphome`.
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
|||||||
-r /requirements.txt
|
-r /requirements.txt
|
||||||
|
|
||||||
# Install the ESPHome Device Builder dashboard.
|
# Install the ESPHome Device Builder dashboard.
|
||||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.4
|
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
platformio settings set enable_telemetry No \
|
platformio settings set enable_telemetry No \
|
||||||
|
|||||||
+1
-40
@@ -26,9 +26,7 @@ from esphome.const import (
|
|||||||
CONF_DEASSERT_RTS_DTR,
|
CONF_DEASSERT_RTS_DTR,
|
||||||
CONF_DISABLED,
|
CONF_DISABLED,
|
||||||
CONF_DISCOVER_IP,
|
CONF_DISCOVER_IP,
|
||||||
CONF_ENCRYPTION,
|
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
CONF_KEY,
|
|
||||||
CONF_LEVEL,
|
CONF_LEVEL,
|
||||||
CONF_LOG,
|
CONF_LOG,
|
||||||
CONF_LOG_TOPIC,
|
CONF_LOG_TOPIC,
|
||||||
@@ -1335,28 +1333,9 @@ def _upload_via_native_api(
|
|||||||
break
|
break
|
||||||
|
|
||||||
from esphome import espota2
|
from esphome import espota2
|
||||||
from esphome.components.noise import static_encryption_key
|
|
||||||
|
|
||||||
remote_port = int(ota_conf[CONF_PORT])
|
remote_port = int(ota_conf[CONF_PORT])
|
||||||
password = ota_conf.get(CONF_PASSWORD)
|
password = ota_conf.get(CONF_PASSWORD)
|
||||||
# Fail closed: an encryption block whose key did not resolve must never
|
|
||||||
# fall back to a plaintext upload
|
|
||||||
noise_psk = None
|
|
||||||
plaintext_fallback = False
|
|
||||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
|
||||||
noise_psk = encryption_conf.get(CONF_KEY)
|
|
||||||
if not noise_psk:
|
|
||||||
raise EsphomeError(
|
|
||||||
"OTA encryption is configured but no key was resolved; "
|
|
||||||
"set the key under 'ota: encryption:' or 'api: encryption:'"
|
|
||||||
)
|
|
||||||
# Ensure the key is a string, as required by the underlying OTA implementation.
|
|
||||||
# It arrives here as a SensitiveStr which aioesphomeapi rejects.
|
|
||||||
noise_psk = str(noise_psk)
|
|
||||||
elif api_key := static_encryption_key(config.get(CONF_API) or {}):
|
|
||||||
# Remove before 2027.3.0: the api key is tried, falling back to plaintext
|
|
||||||
noise_psk = str(api_key)
|
|
||||||
plaintext_fallback = True
|
|
||||||
|
|
||||||
def check_partition_access(option_string: str) -> None:
|
def check_partition_access(option_string: str) -> None:
|
||||||
if not ota_conf.get("allow_partition_access"):
|
if not ota_conf.get("allow_partition_access"):
|
||||||
@@ -1387,15 +1366,7 @@ def _upload_via_native_api(
|
|||||||
if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER:
|
if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER:
|
||||||
_validate_bootloader_binary(binary)
|
_validate_bootloader_binary(binary)
|
||||||
|
|
||||||
return espota2.run_ota(
|
return espota2.run_ota(network_devices, remote_port, password, binary, ota_type)
|
||||||
network_devices,
|
|
||||||
remote_port,
|
|
||||||
password,
|
|
||||||
binary,
|
|
||||||
ota_type,
|
|
||||||
noise_psk,
|
|
||||||
plaintext_fallback=plaintext_fallback,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _upload_via_web_server(
|
def _upload_via_web_server(
|
||||||
@@ -1404,16 +1375,6 @@ def _upload_via_web_server(
|
|||||||
from esphome import web_server_ota
|
from esphome import web_server_ota
|
||||||
from esphome.web_server_helpers import get_web_server_connection
|
from esphome.web_server_helpers import get_web_server_connection
|
||||||
|
|
||||||
if any(
|
|
||||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
|
||||||
and ota_item.get(CONF_ENCRYPTION) is not None
|
|
||||||
for ota_item in config.get(CONF_OTA, [])
|
|
||||||
):
|
|
||||||
_LOGGER.warning(
|
|
||||||
"This config has OTA encryption, but the web_server OTA path sends "
|
|
||||||
"the image over plaintext HTTP; use the esphome OTA platform to "
|
|
||||||
"keep it confidential"
|
|
||||||
)
|
|
||||||
remote_port, username, password = get_web_server_connection(config)
|
remote_port, username, password = get_web_server_connection(config)
|
||||||
return web_server_ota.run_ota(
|
return web_server_ota.run_ota(
|
||||||
network_devices, remote_port, username, password, binary
|
network_devices, remote_port, username, password, binary
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ def get_arduino8266_tools_path() -> Path:
|
|||||||
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
|
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
|
||||||
|
|
||||||
|
|
||||||
# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0
|
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
|
||||||
|
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
|
||||||
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
|
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,16 +53,20 @@ def framework_package_version(ver: Version) -> str:
|
|||||||
"""Map an Arduino core version to its registry package version (3.1.2 ->
|
"""Map an Arduino core version to its registry package version (3.1.2 ->
|
||||||
3.30102.0; the leading 3 is the package major).
|
3.30102.0; the leading 3 is the package major).
|
||||||
|
|
||||||
Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION.
|
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
|
||||||
|
at MIN_FRAMEWORK_VERSION.
|
||||||
"""
|
"""
|
||||||
if ver.major > 3:
|
if ver.major > 3:
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
f"Arduino core {ver} is not supported yet; "
|
f"Arduino core {ver} is not supported yet; "
|
||||||
"the newest known core series is 3.x"
|
"the newest known core series is 3.x"
|
||||||
)
|
)
|
||||||
if ver.major < 3:
|
if ver <= Version(2, 6, 2):
|
||||||
|
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
|
||||||
|
# boundary as _format_framework_arduino_version's era guard)
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
f"Arduino core {ver} is not supported; ESPHome requires core 3.x"
|
f"Arduino core {ver} uses an older package encoding than this "
|
||||||
|
"helper implements (newer than 2.6.2)"
|
||||||
)
|
)
|
||||||
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||||
|
|
||||||
|
|||||||
@@ -162,9 +162,8 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
|
|||||||
auto status =
|
auto status =
|
||||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
||||||
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||||
if (status) {
|
if (status)
|
||||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Alpha3::update() {
|
void Alpha3::update() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.automation import Condition
|
from esphome.automation import Condition
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_DESCRIPTION
|
from esphome.components.const import CONF_DESCRIPTION, CONF_HOST
|
||||||
from esphome.components.logger import request_log_listener
|
from esphome.components.logger import request_log_listener
|
||||||
|
|
||||||
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
|
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
|
||||||
@@ -14,7 +14,6 @@ from esphome.components.noise import ( # noqa: F401
|
|||||||
ENCRYPTION_SCHEMA,
|
ENCRYPTION_SCHEMA,
|
||||||
decode_encryption_key,
|
decode_encryption_key,
|
||||||
encryption_schema,
|
encryption_schema,
|
||||||
new_psk_progmem,
|
|
||||||
validate_encryption_key,
|
validate_encryption_key,
|
||||||
)
|
)
|
||||||
from esphome.config_helpers import filter_source_files_from_defines, get_logger_level
|
from esphome.config_helpers import filter_source_files_from_defines, get_logger_level
|
||||||
@@ -25,6 +24,8 @@ from esphome.const import (
|
|||||||
CONF_CAPTURE_RESPONSE,
|
CONF_CAPTURE_RESPONSE,
|
||||||
CONF_DATA,
|
CONF_DATA,
|
||||||
CONF_DATA_TEMPLATE,
|
CONF_DATA_TEMPLATE,
|
||||||
|
CONF_DELAY,
|
||||||
|
CONF_ENABLE_IPV6,
|
||||||
CONF_ENCRYPTION,
|
CONF_ENCRYPTION,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
@@ -48,6 +49,7 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
|
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
|
||||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||||
|
import esphome.final_validate as fv
|
||||||
from esphome.helpers import fnv1_hash
|
from esphome.helpers import fnv1_hash
|
||||||
from esphome.types import ConfigFragmentType, ConfigType
|
from esphome.types import ConfigFragmentType, ConfigType
|
||||||
|
|
||||||
@@ -134,6 +136,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
|||||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||||
|
CONF_OUTGOING_CONNECTION = "outgoing_connection"
|
||||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||||
|
|
||||||
|
|
||||||
@@ -285,9 +288,44 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
|||||||
# (not max_connections, which is the upper limit rarely reached)
|
# (not max_connections, which is the upper limit rarely reached)
|
||||||
socket.consume_sockets(3, "api")(config)
|
socket.consume_sockets(3, "api")(config)
|
||||||
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
|
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
|
||||||
|
if CONF_OUTGOING_CONNECTION in config:
|
||||||
|
socket.consume_sockets(1, "api_outgoing_connection")(config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_outgoing_connection(config: ConfigType) -> ConfigType:
|
||||||
|
if CONF_OUTGOING_CONNECTION not in config:
|
||||||
|
return config
|
||||||
|
if CONF_ENCRYPTION not in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"outgoing_connection requires 'encryption' so the peer is verified by key",
|
||||||
|
path=[CONF_OUTGOING_CONNECTION],
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
_OUTGOING_CONNECTION_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_HOST): cv.ipaddress,
|
||||||
|
cv.Optional(CONF_PORT, default=6054): cv.port,
|
||||||
|
# Bounded to half the device's uint32 millisecond range so the wait
|
||||||
|
# always elapses under a wrapping clock
|
||||||
|
cv.Optional(CONF_DELAY, default="60s"): cv.All(
|
||||||
|
cv.positive_time_period_milliseconds,
|
||||||
|
cv.Range(max=cv.TimePeriod(milliseconds=2147483647)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _outgoing_connection_schema(config: ConfigType | None) -> ConfigType:
|
||||||
|
# A bare `outgoing_connection:` block is valid; without a host the device
|
||||||
|
# dials the remembered last dial-back client
|
||||||
|
if config is None:
|
||||||
|
config = {}
|
||||||
|
return _OUTGOING_CONNECTION_SCHEMA(config)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -312,6 +350,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
): ACTIONS_SCHEMA,
|
): ACTIONS_SCHEMA,
|
||||||
cv.Exclusive(CONF_ACTIONS, 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_ENCRYPTION): encryption_schema,
|
||||||
|
cv.Optional(CONF_OUTGOING_CONNECTION): _outgoing_connection_schema,
|
||||||
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
||||||
cv.positive_time_period_milliseconds,
|
cv.positive_time_period_milliseconds,
|
||||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||||
@@ -368,6 +407,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||||
|
_validate_outgoing_connection,
|
||||||
_consume_api_sockets,
|
_consume_api_sockets,
|
||||||
_register_provisioning_source,
|
_register_provisioning_source,
|
||||||
)
|
)
|
||||||
@@ -424,7 +464,52 @@ def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings
|
def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType:
|
||||||
|
"""Reject the raw lwip_tcp socket, the only option on ESP8266 and RP2040.
|
||||||
|
|
||||||
|
Checked against the resolved implementation so an explicit selection on
|
||||||
|
another platform is caught the same way as the platform default.
|
||||||
|
"""
|
||||||
|
if CONF_OUTGOING_CONNECTION not in config:
|
||||||
|
return config
|
||||||
|
from esphome.components import socket
|
||||||
|
|
||||||
|
socket_conf = fv.full_config.get().get("socket") or {}
|
||||||
|
if (
|
||||||
|
impl := socket_conf.get(socket.CONF_IMPLEMENTATION)
|
||||||
|
) in socket.IMPLEMENTATIONS_WITHOUT_CONNECT:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"outgoing_connection is not supported with the {impl} socket "
|
||||||
|
"implementation (the only one on ESP8266 and RP2040) because it "
|
||||||
|
"cannot make outgoing connections",
|
||||||
|
path=[CONF_OUTGOING_CONNECTION],
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
|
||||||
|
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
|
||||||
|
if (
|
||||||
|
(outgoing := config.get(CONF_OUTGOING_CONNECTION)) is None
|
||||||
|
or (host := outgoing.get(CONF_HOST)) is None
|
||||||
|
or host.version != 6
|
||||||
|
):
|
||||||
|
return config
|
||||||
|
network_conf = fv.full_config.get().get("network") or {}
|
||||||
|
if not network_conf.get(CONF_ENABLE_IPV6):
|
||||||
|
raise cv.Invalid(
|
||||||
|
"outgoing_connection host is an IPv6 address but IPv6 is not "
|
||||||
|
"enabled; set 'network: enable_ipv6: true'",
|
||||||
|
path=[CONF_OUTGOING_CONNECTION, CONF_HOST],
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||||
|
_validate_esp8266_action_strings,
|
||||||
|
_validate_outgoing_socket_implementation,
|
||||||
|
_validate_outgoing_host_ipv6,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _add_action_strings(
|
def _add_action_strings(
|
||||||
@@ -590,7 +675,8 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
|
|
||||||
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
||||||
if key := encryption_config.get(CONF_KEY):
|
if key := encryption_config.get(CONF_KEY):
|
||||||
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key)))
|
decoded = decode_encryption_key(key)
|
||||||
|
cg.add(var.set_noise_psk(list(decoded)))
|
||||||
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
|
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
|
||||||
else:
|
else:
|
||||||
# No key provided, but encryption desired
|
# No key provided, but encryption desired
|
||||||
@@ -606,6 +692,13 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
else:
|
else:
|
||||||
cg.add_define("USE_API_PLAINTEXT")
|
cg.add_define("USE_API_PLAINTEXT")
|
||||||
|
|
||||||
|
if (outgoing := config.get(CONF_OUTGOING_CONNECTION)) is not None:
|
||||||
|
cg.add_define("USE_API_OUTGOING_CONNECTION")
|
||||||
|
if (host := outgoing.get(CONF_HOST)) is not None:
|
||||||
|
cg.add_define("API_OUTGOING_CONNECTION_HOST", str(host))
|
||||||
|
cg.add_define("API_OUTGOING_CONNECTION_PORT", outgoing[CONF_PORT])
|
||||||
|
cg.add_define("API_OUTGOING_CONNECTION_DELAY", outgoing[CONF_DELAY])
|
||||||
|
|
||||||
cg.add_define("USE_API")
|
cg.add_define("USE_API")
|
||||||
cg.add_global(api_ns.using)
|
cg.add_global(api_ns.using)
|
||||||
|
|
||||||
@@ -992,6 +1085,7 @@ _define_filter = filter_source_files_from_defines(
|
|||||||
"user_services.cpp": "USE_API_USER_DEFINED_ACTIONS",
|
"user_services.cpp": "USE_API_USER_DEFINED_ACTIONS",
|
||||||
"api_frame_helper_noise.cpp": "USE_API_NOISE",
|
"api_frame_helper_noise.cpp": "USE_API_NOISE",
|
||||||
"api_frame_helper_plaintext.cpp": "USE_API_PLAINTEXT",
|
"api_frame_helper_plaintext.cpp": "USE_API_PLAINTEXT",
|
||||||
|
"api_outgoing_connection.cpp": "USE_API_OUTGOING_CONNECTION",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ message HelloRequest {
|
|||||||
string client_info = 1;
|
string client_info = 1;
|
||||||
uint32 api_version_major = 2;
|
uint32 api_version_major = 2;
|
||||||
uint32 api_version_minor = 3;
|
uint32 api_version_minor = 3;
|
||||||
|
|
||||||
|
// Set by clients that can accept connections the device opens to them
|
||||||
|
// (see api: outgoing_connection:). The device remembers this client's
|
||||||
|
// address as the target to dial when no such client is connected.
|
||||||
|
bool outgoing_connection_target = 4 [(field_ifdef) = "USE_API_OUTGOING_CONNECTION"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirmation of successful connection request.
|
// Confirmation of successful connection request.
|
||||||
@@ -331,6 +336,10 @@ message DeviceInfoResponse {
|
|||||||
// all-zeros PSK, so the api encryption key can be provisioned without being
|
// all-zeros PSK, so the api encryption key can be provisioned without being
|
||||||
// sent in plaintext (protects against passive sniffing, not active MITM)
|
// sent in plaintext (protects against passive sniffing, not active MITM)
|
||||||
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
|
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
|
||||||
|
|
||||||
|
// Device is built with the api outgoing_connection option and can open
|
||||||
|
// the TCP connection to a dial-back target itself
|
||||||
|
bool api_outgoing_connection_supported = 27 [(field_ifdef) = "USE_API_OUTGOING_CONNECTION"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== DEVICE CAPABILITIES ====================
|
// ==================== DEVICE CAPABILITIES ====================
|
||||||
|
|||||||
@@ -1822,6 +1822,19 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
|||||||
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0
|
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0
|
||||||
this->complete_authentication_();
|
this->complete_authentication_();
|
||||||
|
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
// With a PSK set only key-verified transports reach hello: plaintext and
|
||||||
|
// zero-PSK are rejected, and pre-activation sessions are force-closed
|
||||||
|
if (msg.outgoing_connection_target && !this->flags_.outgoing_connection_target) {
|
||||||
|
if (this->parent_->get_noise_ctx().has_psk()) {
|
||||||
|
this->flags_.outgoing_connection_target = true;
|
||||||
|
this->parent_->on_outgoing_target_client(this);
|
||||||
|
} else {
|
||||||
|
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Dial-back target refused; no key active"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return this->send_message(resp);
|
return this->send_message(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1944,6 +1957,9 @@ bool APIConnection::send_device_info_response_() {
|
|||||||
// one) so this advertisement survives the plaintext removal in 2027.2.0.
|
// one) so this advertisement survives the plaintext removal in 2027.2.0.
|
||||||
resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
|
resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
resp.api_outgoing_connection_supported = true;
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
size_t device_index = 0;
|
size_t device_index = 0;
|
||||||
@@ -2161,10 +2177,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe
|
|||||||
bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) {
|
bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) {
|
||||||
NoiseEncryptionSetKeyResponse resp;
|
NoiseEncryptionSetKeyResponse resp;
|
||||||
resp.success = false;
|
resp.success = false;
|
||||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
// A yaml key cannot be changed at runtime, so no decode or save path is built
|
|
||||||
ESP_LOGW(TAG, "Key set in YAML");
|
|
||||||
#else
|
|
||||||
#ifdef USE_PROVISIONING
|
#ifdef USE_PROVISIONING
|
||||||
// Refuse to set a key once the provisioning window has closed (defense in depth;
|
// Refuse to set a key once the provisioning window has closed (defense in depth;
|
||||||
// such connections are already rejected at hello).
|
// such connections are already rejected at hello).
|
||||||
@@ -2199,7 +2212,6 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif // USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
|
|
||||||
return this->send_message(resp);
|
return this->send_message(resp);
|
||||||
}
|
}
|
||||||
@@ -2395,9 +2407,8 @@ void APIConnection::process_batch_() {
|
|||||||
} else if (payload_size == 0) {
|
} else if (payload_size == 0) {
|
||||||
// payload_size == 0 with remove set means encoding hit OOM and the
|
// payload_size == 0 with remove set means encoding hit OOM and the
|
||||||
// connection is being dropped; warn only for a genuinely oversized message
|
// connection is being dropped; warn only for a genuinely oversized message
|
||||||
if (!this->flags_.remove) {
|
if (!this->flags_.remove)
|
||||||
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
|
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
|
||||||
}
|
|
||||||
this->clear_batch_();
|
this->clear_batch_();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -375,6 +375,21 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
return this->helper_->get_peername_to(buf);
|
return this->helper_->get_peername_to(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
/// Outgoing connection: send our server hello immediately so the peer can
|
||||||
|
/// pick the matching key. Outgoing connections are only dialed when a PSK
|
||||||
|
/// is set, so the helper is always the noise helper. Call after start().
|
||||||
|
void mark_outgoing() {
|
||||||
|
if (this->flags_.remove) {
|
||||||
|
return; // start() failed; the connection is already being torn down
|
||||||
|
}
|
||||||
|
APIError err = static_cast<APINoiseFrameHelper *>(this->helper_.get())->send_server_hello_first();
|
||||||
|
if (err != APIError::OK) {
|
||||||
|
this->fatal_error_with_log_(LOG_STR("Server hello failed"), err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool try_to_clear_buffer_slow_(bool log_out_of_space);
|
bool try_to_clear_buffer_slow_(bool log_out_of_space);
|
||||||
|
|
||||||
@@ -745,6 +760,9 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
uint8_t batch_first_message : 1; // For batch buffer allocation
|
uint8_t batch_first_message : 1; // For batch buffer allocation
|
||||||
uint8_t should_try_send_immediately : 1; // True after initial states are sent
|
uint8_t should_try_send_immediately : 1; // True after initial states are sent
|
||||||
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
|
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
uint8_t outgoing_connection_target : 1; // Client declared itself a dial-back target in its hello
|
||||||
|
#endif
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
uint8_t log_only_mode : 1;
|
uint8_t log_only_mode : 1;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ class APIFrameHelper {
|
|||||||
CLOSED = 6,
|
CLOSED = 6,
|
||||||
FAILED = 7,
|
FAILED = 7,
|
||||||
EXPLICIT_REJECT = 8, // Noise only
|
EXPLICIT_REJECT = 8, // Noise only
|
||||||
|
CLIENT_HELLO_OUTGOING = 9, // Noise only: like CLIENT_HELLO but the server hello already went out (outgoing conn)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fast inline state check for read_packet/write_protobuf_messages hot path.
|
// Fast inline state check for read_packet/write_protobuf_messages hot path.
|
||||||
|
|||||||
@@ -81,6 +81,13 @@ APIError APINoiseFrameHelper::init() {
|
|||||||
state_ = State::CLIENT_HELLO;
|
state_ = State::CLIENT_HELLO;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
APIError APINoiseFrameHelper::send_server_hello_first() {
|
||||||
|
// The peer needs our name and MAC to pick the key before its first message
|
||||||
|
this->state_ = State::CLIENT_HELLO_OUTGOING;
|
||||||
|
return this->send_server_hello_frame_();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_API_PLAINTEXT
|
#ifdef USE_API_PLAINTEXT
|
||||||
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
|
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
|
||||||
APIError err = this->init();
|
APIError err = this->init();
|
||||||
@@ -253,6 +260,9 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
HELPER_LOG("Bad state for method: %d", (int) this->state_);
|
HELPER_LOG("Bad state for method: %d", (int) this->state_);
|
||||||
return APIError::BAD_STATE;
|
return APIError::BAD_STATE;
|
||||||
case State::CLIENT_HELLO:
|
case State::CLIENT_HELLO:
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
case State::CLIENT_HELLO_OUTGOING:
|
||||||
|
#endif
|
||||||
return this->state_action_client_hello_();
|
return this->state_action_client_hello_();
|
||||||
case State::SERVER_HELLO:
|
case State::SERVER_HELLO:
|
||||||
return this->state_action_server_hello_();
|
return this->state_action_server_hello_();
|
||||||
@@ -285,11 +295,16 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
|
|||||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
if (this->state_ == State::CLIENT_HELLO_OUTGOING) {
|
||||||
|
// Server hello already went out at handoff
|
||||||
|
return this->start_handshake_();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
state_ = State::SERVER_HELLO;
|
state_ = State::SERVER_HELLO;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
APIError APINoiseFrameHelper::state_action_server_hello_() {
|
APIError APINoiseFrameHelper::send_server_hello_frame_() {
|
||||||
// send server hello
|
|
||||||
const auto &name = App.get_name();
|
const auto &name = App.get_name();
|
||||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||||
get_mac_address_into_buffer(mac);
|
get_mac_address_into_buffer(mac);
|
||||||
@@ -313,15 +328,18 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
|
|||||||
// node mac, terminated by null byte
|
// node mac, terminated by null byte
|
||||||
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||||
|
|
||||||
APIError aerr = write_frame_(msg, total_size);
|
return write_frame_(msg, total_size);
|
||||||
|
}
|
||||||
|
APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||||
|
APIError aerr = this->send_server_hello_frame_();
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
return this->start_handshake_();
|
||||||
// start handshake
|
}
|
||||||
aerr = init_handshake_();
|
APIError APINoiseFrameHelper::start_handshake_() {
|
||||||
|
APIError aerr = init_handshake_();
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|
||||||
state_ = State::HANDSHAKE;
|
state_ = State::HANDSHAKE;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
@@ -548,7 +566,7 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
|||||||
* @return 0 on success, -1 on error (check errno)
|
* @return 0 on success, -1 on error (check errno)
|
||||||
*/
|
*/
|
||||||
APIError APINoiseFrameHelper::init_handshake_() {
|
APIError APINoiseFrameHelper::init_handshake_() {
|
||||||
int err = this->handshake_.init(this->ctx_, prologue_.data(), prologue_.size());
|
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
|
||||||
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
|||||||
// Seeds the already-read header bytes and pumps the handshake state machine
|
// Seeds the already-read header bytes and pumps the handshake state machine
|
||||||
// until it would block.
|
// until it would block.
|
||||||
APIError init_from_handoff(const uint8_t *header, uint8_t header_len);
|
APIError init_from_handoff(const uint8_t *header, uint8_t header_len);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
// Send the server hello immediately so the peer can pick the key before
|
||||||
|
// its PSK-mixed message. Call after init(); the mode is tracked in state_
|
||||||
|
// so the helper does not grow.
|
||||||
|
APIError send_server_hello_first();
|
||||||
#endif
|
#endif
|
||||||
APIError loop() override;
|
APIError loop() override;
|
||||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||||
@@ -39,6 +45,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
|||||||
APIError state_action_();
|
APIError state_action_();
|
||||||
APIError state_action_client_hello_();
|
APIError state_action_client_hello_();
|
||||||
APIError state_action_server_hello_();
|
APIError state_action_server_hello_();
|
||||||
|
APIError send_server_hello_frame_();
|
||||||
|
APIError start_handshake_();
|
||||||
APIError state_action_handshake_();
|
APIError state_action_handshake_();
|
||||||
APIError state_action_handshake_read_();
|
APIError state_action_handshake_read_();
|
||||||
APIError state_action_handshake_write_();
|
APIError state_action_handshake_write_();
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
#include "api_outgoing_connection.h"
|
||||||
|
#if defined(USE_API) && defined(USE_API_OUTGOING_CONNECTION)
|
||||||
|
|
||||||
|
#include "api_connection.h"
|
||||||
|
#include "api_server.h"
|
||||||
|
#include "esphome/components/network/util.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace esphome::api {
|
||||||
|
|
||||||
|
static const char *const TAG = "api.outgoing";
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::setup() {
|
||||||
|
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||||
|
this->target_pref_ = global_preferences->make_preference<SavedOutgoingTarget>(629847102UL, true);
|
||||||
|
if (this->target_pref_.load(&this->saved_)) {
|
||||||
|
// Defend against a corrupt or truncated blob before the first read
|
||||||
|
this->saved_.host[sizeof(this->saved_.host) - 1] = '\0';
|
||||||
|
this->host_persisted_ = true;
|
||||||
|
ESP_LOGD(TAG, "Loaded target %s", this->saved_.host);
|
||||||
|
} else {
|
||||||
|
// Never saved, or the blob failed its size/CRC check
|
||||||
|
ESP_LOGD(TAG, "No saved target");
|
||||||
|
this->saved_ = {};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::loop(APIServer *server) {
|
||||||
|
if (server->has_outgoing_target_client_()) {
|
||||||
|
return; // on_target_client() already reset the dial state
|
||||||
|
}
|
||||||
|
if (this->dialed_conn_ != nullptr) {
|
||||||
|
// A live dialed session (flagged or not, e.g. a host: peer) is the
|
||||||
|
// target; a silent one dies on the handshake timeout
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint32_t now = App.get_loop_component_start_time();
|
||||||
|
switch (this->state_) {
|
||||||
|
case DialState::DIAL_STATE_IDLE:
|
||||||
|
#ifdef USE_DEEP_SLEEP
|
||||||
|
// A deep sleep wake window is too short to spend on the delay
|
||||||
|
this->schedule_wait_(now, BACKOFF_MIN_MS);
|
||||||
|
#else
|
||||||
|
// Target went away; give it the configured delay to reconnect first
|
||||||
|
this->schedule_wait_(now, API_OUTGOING_CONNECTION_DELAY);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
case DialState::DIAL_STATE_WAITING:
|
||||||
|
if (now - this->state_ts_ >= this->wait_) {
|
||||||
|
this->try_dial_(server, now);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DialState::DIAL_STATE_CONNECTING:
|
||||||
|
this->poll_connect_(server, now);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
|
||||||
|
if (!network::is_connected()) {
|
||||||
|
// Flips within seconds of boot; recheck fast so a deep sleep wake
|
||||||
|
// window is not spent waiting
|
||||||
|
this->schedule_wait_(now, NETWORK_RETRY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *host = this->target_host_();
|
||||||
|
if (host == nullptr) {
|
||||||
|
// The steady state until a dial-back client has ever connected
|
||||||
|
ESP_LOGV(TAG, "Not dialing: no target");
|
||||||
|
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool at_limit = server->at_client_limit_();
|
||||||
|
if (at_limit || !server->noise_ctx_.has_psk()) {
|
||||||
|
ESP_LOGD(TAG, "Not dialing: %s", at_limit ? "max connections" : "no key");
|
||||||
|
// Not a dial failure; retry without escalating the backoff
|
||||||
|
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct sockaddr_storage addr;
|
||||||
|
socklen_t addr_len =
|
||||||
|
socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, API_OUTGOING_CONNECTION_PORT);
|
||||||
|
if (addr_len == 0) {
|
||||||
|
ESP_LOGW(TAG, "Invalid target %s", host);
|
||||||
|
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||||
|
// A corrupt remembered value can never become dialable; forget it
|
||||||
|
// (covers an IPv6 literal left by an earlier enable_ipv6 build too)
|
||||||
|
this->saved_ = {};
|
||||||
|
if (!this->persist_target_()) {
|
||||||
|
ESP_LOGW(TAG, "Failed to clear target");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
this->schedule_retry_(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->dial_socket_ = socket::socket_loop_monitored(((struct sockaddr *) &addr)->sa_family, SOCK_STREAM, IPPROTO_TCP);
|
||||||
|
if (!this->dial_socket_ || this->dial_socket_->setblocking(false) != 0) {
|
||||||
|
ESP_LOGW(TAG, "Socket %s failed: errno %d", this->dial_socket_ ? "setblocking" : "create", errno);
|
||||||
|
this->schedule_retry_(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Dialing %s:%u", host, API_OUTGOING_CONNECTION_PORT);
|
||||||
|
int err = this->dial_socket_->connect((struct sockaddr *) &addr, addr_len);
|
||||||
|
if (err == 0) {
|
||||||
|
// Immediate success (possible for localhost)
|
||||||
|
this->handoff_(server, now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errno != EINPROGRESS) {
|
||||||
|
ESP_LOGW(TAG, "Connect failed: errno %d", errno);
|
||||||
|
this->schedule_retry_(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->state_ = DialState::DIAL_STATE_CONNECTING;
|
||||||
|
this->state_ts_ = now;
|
||||||
|
this->last_poll_ = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
|
||||||
|
if (now - this->state_ts_ >= CONNECT_TIMEOUT_MS) {
|
||||||
|
ESP_LOGW(TAG, "Connect timeout");
|
||||||
|
this->schedule_retry_(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (now - this->last_poll_ < CONNECT_POLL_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->last_poll_ = now;
|
||||||
|
int err = 0;
|
||||||
|
switch (socket::poll_connect(*this->dial_socket_, err)) {
|
||||||
|
case socket::ConnectPollResult::CONNECT_POLL_PENDING:
|
||||||
|
break;
|
||||||
|
case socket::ConnectPollResult::CONNECT_POLL_CONNECTED:
|
||||||
|
this->handoff_(server, now);
|
||||||
|
break;
|
||||||
|
case socket::ConnectPollResult::CONNECT_POLL_ERROR:
|
||||||
|
ESP_LOGW(TAG, "Connect failed: %d", err);
|
||||||
|
this->schedule_retry_(now);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::handoff_(APIServer *server, uint32_t now) {
|
||||||
|
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
|
||||||
|
if (this->dialed_conn_ == nullptr) {
|
||||||
|
// Only preconditions (slot limit, key cleared) refuse the handoff; the
|
||||||
|
// peer is reachable, so do not escalate the backoff
|
||||||
|
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Connected; dialed_conn_ gates further dialing until the session settles
|
||||||
|
this->state_ = DialState::DIAL_STATE_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::schedule_wait_(uint32_t now, uint32_t wait) {
|
||||||
|
this->dial_socket_.reset(); // no-op when the socket was handed off
|
||||||
|
this->state_ = DialState::DIAL_STATE_WAITING;
|
||||||
|
this->state_ts_ = now;
|
||||||
|
this->wait_ = wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::schedule_retry_(uint32_t now) {
|
||||||
|
// +/-20% jitter so a fleet of devices does not retry one server in lockstep
|
||||||
|
const uint32_t jitter_span = this->backoff_ / 5;
|
||||||
|
this->schedule_wait_(now, this->backoff_ - jitter_span + (random_uint32() % (2 * jitter_span + 1)));
|
||||||
|
this->backoff_ = std::min(this->backoff_ * 2, BACKOFF_MAX_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::on_client_removed(APIConnection *conn, bool was_authenticated) {
|
||||||
|
if (conn != this->dialed_conn_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->dialed_conn_ = nullptr;
|
||||||
|
if (was_authenticated) {
|
||||||
|
// A working peer (e.g. a host: target that never sends the flag)
|
||||||
|
// disconnected normally; state is IDLE, so loop() applies the delay
|
||||||
|
this->backoff_ = BACKOFF_MIN_MS;
|
||||||
|
} else {
|
||||||
|
this->schedule_retry_(App.get_loop_component_start_time());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::on_target_client(APIConnection *conn) {
|
||||||
|
// The target is connected; stop any dial in flight and reset the backoff.
|
||||||
|
// A dialed connection stays tracked unless it is this one: an inbound
|
||||||
|
// target must not orphan a still-open dial.
|
||||||
|
this->dial_socket_.reset();
|
||||||
|
if (conn == this->dialed_conn_) {
|
||||||
|
this->dialed_conn_ = nullptr;
|
||||||
|
}
|
||||||
|
this->state_ = DialState::DIAL_STATE_IDLE;
|
||||||
|
this->backoff_ = BACKOFF_MIN_MS;
|
||||||
|
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||||
|
SavedOutgoingTarget target{};
|
||||||
|
conn->get_peername_to(target.host);
|
||||||
|
if (target.host[0] == '\0') {
|
||||||
|
ESP_LOGW(TAG, "Could not read peer address; not remembering target");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->host_persisted_ && strcmp(target.host, this->saved_.host) == 0) {
|
||||||
|
return; // unchanged and already on flash; avoid flash wear
|
||||||
|
}
|
||||||
|
// Use the fresh address this boot even if the flash write fails; a failed
|
||||||
|
// write is retried on the next flagged hello via host_persisted_
|
||||||
|
this->saved_ = target;
|
||||||
|
if (!this->persist_target_()) {
|
||||||
|
ESP_LOGW(TAG, "Failed to save target");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Saved %s as outgoing connection target", this->saved_.host);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutgoingConnectionManager::dump_config() const {
|
||||||
|
const char *host = this->target_host_();
|
||||||
|
if (host == nullptr) {
|
||||||
|
host = "none remembered yet";
|
||||||
|
}
|
||||||
|
// The boot delay differs from delay: on deep sleep builds, so print the
|
||||||
|
// value that actually applies
|
||||||
|
ESP_LOGCONFIG(TAG,
|
||||||
|
" Outgoing connection port: %u\n"
|
||||||
|
" Outgoing connection host: %s\n"
|
||||||
|
" Outgoing connection boot delay: %" PRIu32 "ms",
|
||||||
|
API_OUTGOING_CONNECTION_PORT, host, BOOT_WAIT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::api
|
||||||
|
#endif // USE_API && USE_API_OUTGOING_CONNECTION
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#if defined(USE_API) && defined(USE_API_OUTGOING_CONNECTION)
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||||
|
#error "api outgoing_connection needs a socket implementation that can make outgoing connections"
|
||||||
|
#endif
|
||||||
|
#ifndef USE_API_NOISE
|
||||||
|
#error "api outgoing_connection needs noise encryption so the peer is verified by key"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "esphome/components/socket/socket.h"
|
||||||
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace esphome::api {
|
||||||
|
|
||||||
|
class APIServer;
|
||||||
|
class APIConnection;
|
||||||
|
|
||||||
|
// Follows the build's address family (ifdef'd in socket/headers.h): toggling
|
||||||
|
// enable_ipv6 changes the blob size, load() rejects the old blob, and the
|
||||||
|
// target is simply relearned
|
||||||
|
static constexpr size_t SAVED_TARGET_HOST_LEN = socket::SOCKADDR_STR_LEN;
|
||||||
|
|
||||||
|
struct SavedOutgoingTarget {
|
||||||
|
// IP as text so the socket component's v4-mapped-IPv6 normalization is
|
||||||
|
// reused on both ends; empty = none remembered
|
||||||
|
char host[SAVED_TARGET_HOST_LEN];
|
||||||
|
} PACKED; // NOLINT
|
||||||
|
|
||||||
|
/// Dials out when no dial-back target client is connected. Only the TCP
|
||||||
|
/// direction flips: the device stays the Noise responder, so both sides
|
||||||
|
/// still verify by key. Targets the YAML host or the last remembered client.
|
||||||
|
class OutgoingConnectionManager {
|
||||||
|
public:
|
||||||
|
void setup();
|
||||||
|
void loop(APIServer *server);
|
||||||
|
/// A key-verified client declared itself a dial-back target; last one wins
|
||||||
|
void on_target_client(APIConnection *conn);
|
||||||
|
/// Clears the dialed-connection gate; dying unauthenticated escalates the backoff
|
||||||
|
void on_client_removed(APIConnection *conn, bool was_authenticated);
|
||||||
|
void on_shutdown() { this->dial_socket_.reset(); }
|
||||||
|
void dump_config() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
enum class DialState : uint8_t {
|
||||||
|
DIAL_STATE_IDLE,
|
||||||
|
DIAL_STATE_WAITING,
|
||||||
|
DIAL_STATE_CONNECTING,
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr uint32_t BACKOFF_MIN_MS = 5000;
|
||||||
|
static constexpr uint32_t BACKOFF_MAX_MS = 300000;
|
||||||
|
static constexpr uint32_t CONNECT_TIMEOUT_MS = 10000;
|
||||||
|
static constexpr uint32_t CONNECT_POLL_INTERVAL_MS = 250;
|
||||||
|
static constexpr uint32_t NETWORK_RETRY_MS = 500;
|
||||||
|
static constexpr uint32_t PRECONDITION_RETRY_MS = 5000;
|
||||||
|
// Boot waits for the client to connect in first; a deep sleep wake window
|
||||||
|
// is short, so connecting out immediately is the wake state
|
||||||
|
#ifdef USE_DEEP_SLEEP
|
||||||
|
static constexpr uint32_t BOOT_WAIT_MS = 0;
|
||||||
|
#else
|
||||||
|
static constexpr uint32_t BOOT_WAIT_MS = API_OUTGOING_CONNECTION_DELAY;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void try_dial_(APIServer *server, uint32_t now);
|
||||||
|
void poll_connect_(APIServer *server, uint32_t now);
|
||||||
|
// Hand the connected socket to the server and gate on the new connection
|
||||||
|
void handoff_(APIServer *server, uint32_t now);
|
||||||
|
// Close any half-open dial and wait a jittered backoff before retrying
|
||||||
|
void schedule_retry_(uint32_t now);
|
||||||
|
// Wait without escalating the backoff (used for unmet preconditions)
|
||||||
|
void schedule_wait_(uint32_t now, uint32_t wait);
|
||||||
|
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||||
|
// Write saved_ to flash, tracking success in host_persisted_
|
||||||
|
bool persist_target_() {
|
||||||
|
this->host_persisted_ = this->target_pref_.save(&this->saved_) && global_preferences->sync();
|
||||||
|
return this->host_persisted_;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
const char *target_host_() const {
|
||||||
|
#ifdef API_OUTGOING_CONNECTION_HOST
|
||||||
|
return API_OUTGOING_CONNECTION_HOST;
|
||||||
|
#else
|
||||||
|
return this->saved_.host[0] != '\0' ? this->saved_.host : nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointers first (4 bytes each on 32-bit)
|
||||||
|
std::unique_ptr<socket::Socket> dial_socket_;
|
||||||
|
// Compared only, never dereferenced
|
||||||
|
APIConnection *dialed_conn_{nullptr};
|
||||||
|
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||||
|
ESPPreferenceObject target_pref_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 4-byte types
|
||||||
|
uint32_t backoff_{BACKOFF_MIN_MS};
|
||||||
|
uint32_t wait_{BOOT_WAIT_MS};
|
||||||
|
uint32_t state_ts_{0};
|
||||||
|
uint32_t last_poll_{0};
|
||||||
|
|
||||||
|
// Byte-aligned types last
|
||||||
|
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||||
|
SavedOutgoingTarget saved_{};
|
||||||
|
// False while saved_ holds a value the flash write failed for; retried on
|
||||||
|
// the next flagged hello
|
||||||
|
bool host_persisted_{false};
|
||||||
|
#endif
|
||||||
|
DialState state_{DialState::DIAL_STATE_WAITING};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::api
|
||||||
|
#endif // USE_API && USE_API_OUTGOING_CONNECTION
|
||||||
@@ -15,6 +15,11 @@ bool HelloRequest::decode_varint(uint32_t field_id, proto_varint_value_t value)
|
|||||||
case 3:
|
case 3:
|
||||||
this->api_version_minor = value;
|
this->api_version_minor = value;
|
||||||
break;
|
break;
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
case 4:
|
||||||
|
this->outgoing_connection_target = value != 0;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -175,6 +180,9 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
|
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 27, this->api_outgoing_connection_supported);
|
||||||
#endif
|
#endif
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
@@ -240,6 +248,9 @@ uint32_t DeviceInfoResponse::calculate_size() const {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
|
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
size += ProtoSize::calc_bool(2, this->api_outgoing_connection_supported);
|
||||||
#endif
|
#endif
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,13 +412,16 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
|||||||
class HelloRequest final : public ProtoDecodableMessage {
|
class HelloRequest final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint16_t MESSAGE_TYPE = 1;
|
static constexpr uint16_t MESSAGE_TYPE = 1;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const LogString *message_name() const override { return LOG_STR("hello_request"); }
|
const LogString *message_name() const override { return LOG_STR("hello_request"); }
|
||||||
#endif
|
#endif
|
||||||
StringRef client_info{};
|
StringRef client_info{};
|
||||||
uint32_t api_version_major{0};
|
uint32_t api_version_major{0};
|
||||||
uint32_t api_version_minor{0};
|
uint32_t api_version_minor{0};
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
bool outgoing_connection_target{false};
|
||||||
|
#endif
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *dump_to(DumpBuffer &out) const override;
|
const char *dump_to(DumpBuffer &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -549,7 +552,7 @@ class SerialProxyInfo final : public ProtoMessage {
|
|||||||
class DeviceInfoResponse final : public ProtoMessage {
|
class DeviceInfoResponse final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint16_t MESSAGE_TYPE = 10;
|
static constexpr uint16_t MESSAGE_TYPE = 10;
|
||||||
static constexpr uint16_t ESTIMATED_SIZE = 312;
|
static constexpr uint16_t ESTIMATED_SIZE = 315;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
|
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
|
||||||
#endif
|
#endif
|
||||||
@@ -607,6 +610,9 @@ class DeviceInfoResponse final : public ProtoMessage {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
bool api_encryption_provisionable{false};
|
bool api_encryption_provisionable{false};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
bool api_outgoing_connection_supported{false};
|
||||||
#endif
|
#endif
|
||||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||||
uint32_t calculate_size() const;
|
uint32_t calculate_size() const;
|
||||||
|
|||||||
@@ -885,6 +885,9 @@ const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
|||||||
dump_field(out, ESPHOME_PSTR("client_info"), this->client_info);
|
dump_field(out, ESPHOME_PSTR("client_info"), this->client_info);
|
||||||
dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major);
|
dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major);
|
||||||
dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor);
|
dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor);
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
dump_field(out, ESPHOME_PSTR("outgoing_connection_target"), this->outgoing_connection_target);
|
||||||
|
#endif
|
||||||
return out.c_str();
|
return out.c_str();
|
||||||
}
|
}
|
||||||
const char *HelloResponse::dump_to(DumpBuffer &out) const {
|
const char *HelloResponse::dump_to(DumpBuffer &out) const {
|
||||||
@@ -1008,6 +1011,9 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
|
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
dump_field(out, ESPHOME_PSTR("api_outgoing_connection_supported"), this->api_outgoing_connection_supported);
|
||||||
#endif
|
#endif
|
||||||
return out.c_str();
|
return out.c_str();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,29 +34,13 @@ APIServer::APIServer() { global_api_server = this; }
|
|||||||
void APIServer::socket_failed_(const LogString *msg) {
|
void APIServer::socket_failed_(const LogString *msg) {
|
||||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||||
this->destroy_socket_();
|
this->destroy_socket_();
|
||||||
this->mark_failed();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIServer::setup() {
|
bool APIServer::create_listen_socket_() {
|
||||||
ControllerRegistry::register_controller(this);
|
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
|
||||||
// Always reserve the slot: flash preferences are positional on esp8266, so
|
|
||||||
// a yaml key build must keep the layout of a runtime key build
|
|
||||||
uint32_t hash = 88491486UL;
|
|
||||||
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
|
|
||||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
// A cleared record loads fine but holds no key
|
|
||||||
if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) {
|
|
||||||
ESP_LOGD(TAG, "Loaded saved Noise PSK");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
|
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
|
||||||
if (this->socket_ == nullptr) {
|
if (this->socket_ == nullptr) {
|
||||||
this->socket_failed_(LOG_STR("creation"));
|
this->socket_failed_(LOG_STR("creation"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
int enable = 1;
|
int enable = 1;
|
||||||
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
||||||
@@ -67,7 +51,7 @@ void APIServer::setup() {
|
|||||||
err = this->socket_->setblocking(false);
|
err = this->socket_->setblocking(false);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
this->socket_failed_(LOG_STR("nonblocking"));
|
this->socket_failed_(LOG_STR("nonblocking"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sockaddr_storage server;
|
struct sockaddr_storage server;
|
||||||
@@ -75,19 +59,47 @@ void APIServer::setup() {
|
|||||||
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
|
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
|
||||||
if (sl == 0) {
|
if (sl == 0) {
|
||||||
this->socket_failed_(LOG_STR("set sockaddr"));
|
this->socket_failed_(LOG_STR("set sockaddr"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = this->socket_->bind((struct sockaddr *) &server, sl);
|
err = this->socket_->bind((struct sockaddr *) &server, sl);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
this->socket_failed_(LOG_STR("bind"));
|
this->socket_failed_(LOG_STR("bind"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = this->socket_->listen(this->listen_backlog_);
|
err = this->socket_->listen(this->listen_backlog_);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
this->socket_failed_(LOG_STR("listen"));
|
this->socket_failed_(LOG_STR("listen"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APIServer::setup() {
|
||||||
|
ControllerRegistry::register_controller(this);
|
||||||
|
|
||||||
|
#ifdef USE_API_NOISE
|
||||||
|
uint32_t hash = 88491486UL;
|
||||||
|
|
||||||
|
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
|
||||||
|
|
||||||
|
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
||||||
|
// Only load saved PSK if not set from YAML
|
||||||
|
if (this->load_and_apply_noise_psk_()) {
|
||||||
|
ESP_LOGD(TAG, "Loaded saved Noise PSK");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!this->create_listen_socket_()) {
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
// Dial-out needs no listener; degrade instead of stopping the component
|
||||||
|
this->status_set_error(LOG_STR("listen socket failed"));
|
||||||
|
#else
|
||||||
|
this->mark_failed();
|
||||||
return;
|
return;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
@@ -135,6 +147,9 @@ void APIServer::setup() {
|
|||||||
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
this->outgoing_conn_.setup();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIServer::loop() {
|
void APIServer::loop() {
|
||||||
@@ -143,6 +158,12 @@ void APIServer::loop() {
|
|||||||
this->accept_new_connections_();
|
this->accept_new_connections_();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
if (!this->shutting_down_) {
|
||||||
|
this->outgoing_conn_.loop(this);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (this->api_connection_count_ == 0) {
|
if (this->api_connection_count_ == 0) {
|
||||||
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
||||||
// (cancelled scheduler items sit in heap memory until their scheduled time).
|
// (cancelled scheduler items sit in heap memory until their scheduled time).
|
||||||
@@ -151,7 +172,12 @@ void APIServer::loop() {
|
|||||||
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||||
const uint32_t now = App.get_loop_component_start_time();
|
const uint32_t now = App.get_loop_component_start_time();
|
||||||
if (now - this->last_connected_ > this->reboot_timeout_) {
|
if (now - this->last_connected_ > this->reboot_timeout_) {
|
||||||
|
// Distinguish a wrong-key peer from nothing connecting at all
|
||||||
|
if (this->saw_unauthenticated_client_) {
|
||||||
|
ESP_LOGE(TAG, "Clients connected but none authenticated; rebooting");
|
||||||
|
} else {
|
||||||
ESP_LOGE(TAG, "No clients; rebooting");
|
ESP_LOGE(TAG, "No clients; rebooting");
|
||||||
|
}
|
||||||
App.reboot();
|
App.reboot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +229,15 @@ void APIServer::remove_client_(uint8_t client_index) {
|
|||||||
std::string client_peername(client->get_peername_to(peername_buf));
|
std::string client_peername(client->get_peername_to(peername_buf));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Read before the swap-and-reset below destroys the connection
|
||||||
|
const bool was_authenticated = client->is_authenticated();
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
if (client->flags_.outgoing_connection_target) {
|
||||||
|
this->outgoing_target_count_--;
|
||||||
|
}
|
||||||
|
this->outgoing_conn_.on_client_removed(client.get(), was_authenticated);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||||
client->helper_->close();
|
client->helper_->close();
|
||||||
|
|
||||||
@@ -221,9 +256,18 @@ void APIServer::remove_client_(uint8_t client_index) {
|
|||||||
|
|
||||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||||
// (suppressed while provisioning is pending - see loop()).
|
// (suppressed while provisioning is pending - see loop()).
|
||||||
|
// Refresh on every authenticated removal, not just the last one, so an
|
||||||
|
// unauthenticated straggler removed later (e.g. a port scan, or a dial to
|
||||||
|
// a host that accepts TCP but never speaks the API) cannot discard a
|
||||||
|
// healthy session's timestamp and trigger a spurious reboot
|
||||||
|
if (was_authenticated) {
|
||||||
|
this->last_connected_ = App.get_loop_component_start_time();
|
||||||
|
this->saw_unauthenticated_client_ = false;
|
||||||
|
} else {
|
||||||
|
this->saw_unauthenticated_client_ = true;
|
||||||
|
}
|
||||||
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||||
this->last_connected_ = App.get_loop_component_start_time();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
@@ -245,7 +289,7 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
|||||||
sock->getpeername_to(peername);
|
sock->getpeername_to(peername);
|
||||||
|
|
||||||
// Check if we're at the connection limit
|
// Check if we're at the connection limit
|
||||||
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
|
if (this->at_client_limit_()) {
|
||||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
|
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
|
||||||
// Immediately close - socket destructor will handle cleanup
|
// Immediately close - socket destructor will handle cleanup
|
||||||
sock.reset();
|
sock.reset();
|
||||||
@@ -254,18 +298,54 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
|||||||
|
|
||||||
ESP_LOGD(TAG, "Accept %s", peername);
|
ESP_LOGD(TAG, "Accept %s", peername);
|
||||||
|
|
||||||
auto *conn = new APIConnection(std::move(sock), this);
|
this->add_client_(new APIConnection(std::move(sock), this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool APIServer::add_client_(APIConnection *conn) {
|
||||||
|
if (this->at_client_limit_()) {
|
||||||
|
// The accept path checks first to skip the allocation; the outgoing
|
||||||
|
// handoff relies on this check
|
||||||
|
ESP_LOGW(TAG, "Max connections (%d), dropping client", MAX_API_CONNECTIONS);
|
||||||
|
delete conn;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this->clients_[this->api_connection_count_++].reset(conn);
|
this->clients_[this->api_connection_count_++].reset(conn);
|
||||||
conn->start();
|
conn->start();
|
||||||
|
|
||||||
// First client connected - clear warning and update timestamp
|
// First client connected - clear warning. The reboot watchdog timestamp is
|
||||||
|
// refreshed when an authenticated client is removed (see remove_client_),
|
||||||
|
// never on bare TCP connects.
|
||||||
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||||
this->status_clear_warning();
|
this->status_clear_warning();
|
||||||
this->last_connected_ = App.get_loop_component_start_time();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
APIConnection *APIServer::add_outgoing_client_(std::unique_ptr<socket::Socket> sock) {
|
||||||
|
// Re-check at the handoff: the PSK may have been cleared since the dial
|
||||||
|
// started (mark_outgoing() needs the noise helper); add_client_ re-checks
|
||||||
|
// the slot limit
|
||||||
|
if (!this->noise_ctx_.has_psk()) {
|
||||||
|
ESP_LOGW(TAG, "Dropping outgoing connection (no key)");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto *conn = new APIConnection(std::move(sock), this);
|
||||||
|
if (!this->add_client_(conn)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// After start(): sends our server hello first so the peer can pick the key
|
||||||
|
conn->mark_outgoing();
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APIServer::on_outgoing_target_client(APIConnection *conn) {
|
||||||
|
this->outgoing_target_count_++;
|
||||||
|
this->outgoing_conn_.on_target_client(conn);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void APIServer::dump_config() {
|
void APIServer::dump_config() {
|
||||||
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
|
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
@@ -282,6 +362,9 @@ void APIServer::dump_config() {
|
|||||||
#else
|
#else
|
||||||
ESP_LOGCONFIG(TAG, " Noise encryption: NO");
|
ESP_LOGCONFIG(TAG, " Noise encryption: NO");
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
this->outgoing_conn_.dump_config();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIServer::handle_disconnect(APIConnection *conn) {}
|
void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||||
@@ -550,7 +633,6 @@ const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_sta
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||||
const LogString *fail_log_msg, bool make_active) {
|
const LogString *fail_log_msg, bool make_active) {
|
||||||
if (!this->noise_pref_.save(&new_psk)) {
|
if (!this->noise_pref_.save(&new_psk)) {
|
||||||
@@ -577,6 +659,8 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
|
|||||||
if (!c->send_message(req)) {
|
if (!c->send_message(req)) {
|
||||||
API_LOG_MSG_DROPPED(TAG, "Disconnect request");
|
API_LOG_MSG_DROPPED(TAG, "Disconnect request");
|
||||||
}
|
}
|
||||||
|
// Force it: a session from before the key was active must not survive
|
||||||
|
c->flags_.next_close = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -584,19 +668,22 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool APIServer::load_and_apply_noise_psk_() {
|
bool APIServer::load_and_apply_noise_psk_() {
|
||||||
// Load into a temp so a failed read cannot disturb the key in use
|
SavedNoisePsk saved{};
|
||||||
SavedNoisePsk loaded{};
|
if (!this->noise_pref_.load(&saved))
|
||||||
if (!this->noise_pref_.load(&loaded))
|
|
||||||
return false;
|
return false;
|
||||||
this->saved_psk_ = loaded;
|
this->set_noise_psk(saved.psk);
|
||||||
// An unprovisioned device stores the reserved all-zeros key, which is no key
|
|
||||||
const bool has_key = !noise::NoiseContext::is_all_zeros(this->saved_psk_.psk);
|
|
||||||
this->noise_ctx_.set_psk(has_key ? this->saved_psk_.psk.data() : nullptr);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
|
bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
|
||||||
if (this->saved_psk_.psk == psk) {
|
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||||
|
// When PSK is set from YAML, this function should never be called
|
||||||
|
// but if it is, reject the change
|
||||||
|
ESP_LOGW(TAG, "Key set in YAML");
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
auto &old_psk = this->noise_ctx_.get_psk();
|
||||||
|
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
|
||||||
ESP_LOGW(TAG, "New PSK matches old");
|
ESP_LOGW(TAG, "New PSK matches old");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -612,8 +699,15 @@ bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return result;
|
return result;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
bool APIServer::clear_noise_psk(bool make_active) {
|
bool APIServer::clear_noise_psk(bool make_active) {
|
||||||
|
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||||
|
// When PSK is set from YAML, this function should never be called
|
||||||
|
// but if it is, reject the change
|
||||||
|
ESP_LOGW(TAG, "Key set in YAML");
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
SavedNoisePsk empty_psk{};
|
SavedNoisePsk empty_psk{};
|
||||||
bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
|
bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
|
||||||
make_active);
|
make_active);
|
||||||
@@ -625,8 +719,8 @@ bool APIServer::clear_noise_psk(bool make_active) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return result;
|
return result;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif // USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
@@ -678,6 +772,9 @@ void APIServer::on_shutdown() {
|
|||||||
|
|
||||||
// Close the listening socket to prevent new connections
|
// Close the listening socket to prevent new connections
|
||||||
this->destroy_socket_();
|
this->destroy_socket_();
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
this->outgoing_conn_.on_shutdown();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Change batch delay to 5ms for quick flushing during shutdown
|
// Change batch delay to 5ms for quick flushing during shutdown
|
||||||
this->batch_delay_ = 5;
|
this->batch_delay_ = 5;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
#include "api_pb2_service.h"
|
#include "api_pb2_service.h"
|
||||||
|
#include "api_outgoing_connection.h"
|
||||||
#include "esphome/components/socket/socket.h"
|
#include "esphome/components/socket/socket.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
@@ -76,16 +77,15 @@ class APIServer final : public Component,
|
|||||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
// Runtime key changes exist for the provisioning path only (not lambdas);
|
|
||||||
// with a yaml key they compile out
|
|
||||||
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
|
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
|
||||||
bool clear_noise_psk(bool make_active = true);
|
bool clear_noise_psk(bool make_active = true);
|
||||||
#endif
|
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||||
/// psk points at 32 bytes that live in flash for the life of the program
|
|
||||||
void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); }
|
|
||||||
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||||
#endif // USE_API_NOISE
|
#endif // USE_API_NOISE
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
// Called by APIConnection when a client declares itself a dial-back target in its hello
|
||||||
|
void on_outgoing_target_client(APIConnection *conn);
|
||||||
|
#endif
|
||||||
|
|
||||||
void handle_disconnect(APIConnection *conn);
|
void handle_disconnect(APIConnection *conn);
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
@@ -263,6 +263,16 @@ class APIServer final : public Component,
|
|||||||
protected:
|
protected:
|
||||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||||
void __attribute__((noinline)) accept_new_connections_();
|
void __attribute__((noinline)) accept_new_connections_();
|
||||||
|
// Insert a constructed connection into the client slots and start it.
|
||||||
|
// Takes ownership; deletes the connection and returns false at the limit
|
||||||
|
bool add_client_(APIConnection *conn);
|
||||||
|
bool at_client_limit_() const { return this->api_connection_count_ >= MAX_API_CONNECTIONS; }
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
// Returns the new connection, or nullptr (socket dropped) when at the limit
|
||||||
|
APIConnection *add_outgoing_client_(std::unique_ptr<socket::Socket> sock);
|
||||||
|
bool has_outgoing_target_client_() const { return this->outgoing_target_count_ != 0; }
|
||||||
|
friend class OutgoingConnectionManager;
|
||||||
|
#endif
|
||||||
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
|
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
|
||||||
void __attribute__((noinline)) remove_client_(uint8_t client_index);
|
void __attribute__((noinline)) remove_client_(uint8_t client_index);
|
||||||
|
|
||||||
@@ -280,12 +290,10 @@ class APIServer final : public Component,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||||
bool make_active);
|
bool make_active);
|
||||||
// Load saved PSK from preferences and apply it. Returns true on success.
|
// Load saved PSK from preferences and apply it. Returns true on success.
|
||||||
bool load_and_apply_noise_psk_();
|
bool load_and_apply_noise_psk_();
|
||||||
#endif // USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
#endif // USE_API_NOISE
|
#endif // USE_API_NOISE
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
// Helper methods to reduce code duplication
|
// Helper methods to reduce code duplication
|
||||||
@@ -304,6 +312,7 @@ class APIServer final : public Component,
|
|||||||
this->socket_ = nullptr;
|
this->socket_ = nullptr;
|
||||||
}
|
}
|
||||||
void socket_failed_(const LogString *msg);
|
void socket_failed_(const LogString *msg);
|
||||||
|
bool create_listen_socket_();
|
||||||
// Pointers and pointer-like types first (4 bytes each)
|
// Pointers and pointer-like types first (4 bytes each)
|
||||||
socket::ListenSocket *socket_{nullptr};
|
socket::ListenSocket *socket_{nullptr};
|
||||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||||
@@ -356,8 +365,16 @@ class APIServer final : public Component,
|
|||||||
// Connection limits - these defaults will be overridden by config values
|
// Connection limits - these defaults will be overridden by config values
|
||||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
|
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
|
||||||
uint8_t listen_backlog_{4};
|
uint8_t listen_backlog_{4};
|
||||||
bool shutting_down_ = false;
|
// Bit-packed so the two flags share one byte
|
||||||
|
bool shutting_down_ : 1 = false;
|
||||||
|
// For the reboot log: whether any removal since the last watchdog refresh
|
||||||
|
// was an unauthenticated session (e.g. a wrong-key peer)
|
||||||
|
bool saw_unauthenticated_client_ : 1 = false;
|
||||||
uint8_t api_connection_count_{0};
|
uint8_t api_connection_count_{0};
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
// Connected clients whose hello declared them a dial-back target
|
||||||
|
uint8_t outgoing_target_count_{0};
|
||||||
|
#endif
|
||||||
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
|
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
|
||||||
// Index assigned by the provisioning manager for reporting this transport's state.
|
// Index assigned by the provisioning manager for reporting this transport's state.
|
||||||
uint8_t provisioning_source_{0};
|
uint8_t provisioning_source_{0};
|
||||||
@@ -365,11 +382,11 @@ class APIServer final : public Component,
|
|||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
noise::NoiseContext noise_ctx_;
|
noise::NoiseContext noise_ctx_;
|
||||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
|
||||||
SavedNoisePsk saved_psk_{}; // backs noise_ctx_ for a runtime provisioned key
|
|
||||||
#endif
|
|
||||||
ESPPreferenceObject noise_pref_;
|
ESPPreferenceObject noise_pref_;
|
||||||
#endif // USE_API_NOISE
|
#endif // USE_API_NOISE
|
||||||
|
#ifdef USE_API_OUTGOING_CONNECTION
|
||||||
|
OutgoingConnectionManager outgoing_conn_;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|||||||
@@ -42,7 +42,16 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket_->setblocking(false);
|
if (socket_->setblocking(false) != 0) {
|
||||||
|
// Capture before the log and reset() below can clobber errno; a blocking
|
||||||
|
// connect()/read() would otherwise stall the whole loop
|
||||||
|
const int saved_errno = errno;
|
||||||
|
ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno);
|
||||||
|
socket_.reset();
|
||||||
|
if (error_cb_)
|
||||||
|
error_cb_(error_arg_, this, saved_errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
@@ -97,45 +106,22 @@ void AsyncClient::loop() {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (connecting_) {
|
if (connecting_) {
|
||||||
// For connecting, we need to check writability, not readability
|
int err = 0;
|
||||||
// The Application's select() only monitors read FDs, so we do our own check here
|
switch (socket::poll_connect(*socket_, err)) {
|
||||||
// For ESP platforms lwip_select() might be faster, but this code isn't used
|
case socket::ConnectPollResult::CONNECT_POLL_PENDING:
|
||||||
// on those platforms anyway. If it was, we'd fix the Application select()
|
break;
|
||||||
// to report writability instead of doing it this way.
|
case socket::ConnectPollResult::CONNECT_POLL_CONNECTED:
|
||||||
int fd = socket_->get_fd();
|
|
||||||
if (fd < 0) {
|
|
||||||
ESP_LOGW(TAG, "Invalid socket fd");
|
|
||||||
close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fd_set writefds;
|
|
||||||
FD_ZERO(&writefds);
|
|
||||||
FD_SET(fd, &writefds);
|
|
||||||
|
|
||||||
struct timeval tv = {0, 0};
|
|
||||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
|
||||||
|
|
||||||
if (ret > 0 && FD_ISSET(fd, &writefds)) {
|
|
||||||
int error = 0;
|
|
||||||
socklen_t len = sizeof(error);
|
|
||||||
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
|
|
||||||
connecting_ = false;
|
connecting_ = false;
|
||||||
connected_ = true;
|
connected_ = true;
|
||||||
if (connect_cb_)
|
if (connect_cb_)
|
||||||
connect_cb_(connect_arg_, this);
|
connect_cb_(connect_arg_, this);
|
||||||
} else {
|
break;
|
||||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
case socket::ConnectPollResult::CONNECT_POLL_ERROR:
|
||||||
close();
|
ESP_LOGW(TAG, "Connection failed: %d", err);
|
||||||
if (error_cb_)
|
|
||||||
error_cb_(error_arg_, this, error);
|
|
||||||
}
|
|
||||||
} else if (ret < 0) {
|
|
||||||
const int err = errno;
|
|
||||||
ESP_LOGE(TAG, "Select error: %d", err);
|
|
||||||
close();
|
close();
|
||||||
if (error_cb_)
|
if (error_cb_)
|
||||||
error_cb_(error_arg_, this, err);
|
error_cb_(error_arg_, this, err);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else if (connected_) {
|
} else if (connected_) {
|
||||||
// For connected sockets, use the Application's select() results
|
// For connected sockets, use the Application's select() results
|
||||||
|
|||||||
@@ -62,9 +62,8 @@ BdkActivityState bdk_scan_state(uint8_t activity_idx) {
|
|||||||
|
|
||||||
uint8_t bdk_scan_acquire_activity() {
|
uint8_t bdk_scan_acquire_activity() {
|
||||||
uint8_t idx = app_ble_get_idle_actv_idx_handle(SCAN_ACTV);
|
uint8_t idx = app_ble_get_idle_actv_idx_handle(SCAN_ACTV);
|
||||||
if (idx == INVALID_ACTIVITY_IDX) {
|
if (idx == INVALID_ACTIVITY_IDX)
|
||||||
ESP_LOGE(TAG, "Scan start failed: no idle activity handle");
|
ESP_LOGE(TAG, "Scan start failed: no idle activity handle");
|
||||||
}
|
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,9 +181,8 @@ void BK72xxBLE::enable() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!bdaddr_live) {
|
if (!bdaddr_live)
|
||||||
ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
|
ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this->state_ = BLEComponentState::ACTIVE;
|
this->state_ = BLEComponentState::ACTIVE;
|
||||||
@@ -211,10 +210,9 @@ void BK72xxBLE::loop() {
|
|||||||
// Re-check a settled scan; scan_start() refills the bring-up budget.
|
// Re-check a settled scan; scan_start() refills the bring-up budget.
|
||||||
// WARN: the only report of a drop that recovers inside its budget.
|
// WARN: the only report of a drop that recovers inside its budget.
|
||||||
if (this->scan_start(this->requested_.interval, this->requested_.window, this->requested_.active) !=
|
if (this->scan_start(this->requested_.interval, this->requested_.window, this->requested_.active) !=
|
||||||
ScanOpResult::SETTLED) {
|
ScanOpResult::SETTLED)
|
||||||
ESP_LOGW(TAG, "Controller dropped the scan; restarting");
|
ESP_LOGW(TAG, "Controller dropped the scan; restarting");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Drain the lock-free ring filled by the BLE task; all per-report work runs
|
// Drain the lock-free ring filled by the BLE task; all per-report work runs
|
||||||
// here on the main task, then the report returns to the pool.
|
// here on the main task, then the report returns to the pool.
|
||||||
@@ -232,9 +230,8 @@ void BK72xxBLE::loop() {
|
|||||||
// Log dropped reports — only reachable when reports were processed; drops can
|
// Log dropped reports — only reachable when reports were processed; drops can
|
||||||
// only occur while the queue is full, and only this loop drains it.
|
// only occur while the queue is full, and only this loop drains it.
|
||||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||||
if (dropped > 0) {
|
if (dropped > 0)
|
||||||
ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
|
ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BK72xxBLE::get_mac_lsb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
|
void BK72xxBLE::get_mac_lsb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
|
||||||
@@ -452,9 +449,8 @@ ScanOpResult BK72xxBLE::advance_stop_(BdkActivityState state, bool ready) {
|
|||||||
if (!ready) {
|
if (!ready) {
|
||||||
// Acting mid-operation could delete an activity whose start lands
|
// Acting mid-operation could delete an activity whose start lands
|
||||||
// afterwards, leaking the slot with the radio on; wait.
|
// afterwards, leaking the slot with the radio on; wait.
|
||||||
if (this->last_result_ == ScanOpResult::SETTLED) {
|
if (this->last_result_ == ScanOpResult::SETTLED)
|
||||||
ESP_LOGD(TAG, "Scan stop deferred (controller busy)");
|
ESP_LOGD(TAG, "Scan stop deferred (controller busy)");
|
||||||
}
|
|
||||||
return ScanOpResult::PENDING;
|
return ScanOpResult::PENDING;
|
||||||
}
|
}
|
||||||
// Settled, so CREATED unambiguously means "never started".
|
// Settled, so CREATED unambiguously means "never started".
|
||||||
@@ -478,9 +474,8 @@ ScanOpResult BK72xxBLE::advance_start_(BdkActivityState state, bool ready) {
|
|||||||
return ScanOpResult::PENDING;
|
return ScanOpResult::PENDING;
|
||||||
}
|
}
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
if (this->last_result_ == ScanOpResult::SETTLED) {
|
if (this->last_result_ == ScanOpResult::SETTLED)
|
||||||
ESP_LOGD(TAG, "Scan start deferred (controller busy)");
|
ESP_LOGD(TAG, "Scan start deferred (controller busy)");
|
||||||
}
|
|
||||||
return ScanOpResult::PENDING;
|
return ScanOpResult::PENDING;
|
||||||
}
|
}
|
||||||
if (state == BdkActivityState::CREATED) {
|
if (state == BdkActivityState::CREATED) {
|
||||||
|
|||||||
@@ -69,9 +69,8 @@ void BK72xxBLETracker::on_ota_global_state(ota::OTAState state, float progress,
|
|||||||
this->stop_scan();
|
this->stop_scan();
|
||||||
// The transfer starves the loop; a deferred stop would leave the radio
|
// The transfer starves the loop; a deferred stop would leave the radio
|
||||||
// scanning for the whole update, so drain it here, bounded.
|
// scanning for the whole update, so drain it here, bounded.
|
||||||
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS)) {
|
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS))
|
||||||
ESP_LOGE(TAG, "Scan still stopping at OTA start; the radio may contend with the update");
|
ESP_LOGE(TAG, "Scan still stopping at OTA start; the radio may contend with the update");
|
||||||
}
|
|
||||||
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
|
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
|
||||||
// On success the device reboots, so restore only on a failed/aborted update;
|
// On success the device reboots, so restore only on a failed/aborted update;
|
||||||
// loop() restarts the scan on its next iteration (continuous idle branch).
|
// loop() restarts the scan on its next iteration (continuous idle branch).
|
||||||
|
|||||||
@@ -80,9 +80,8 @@ void BLEBinaryOutput::write_state(bool state) {
|
|||||||
esp_err_t err =
|
esp_err_t err =
|
||||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
|
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
|
||||||
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||||
if (err != ESP_GATT_OK) {
|
if (err != ESP_GATT_OK)
|
||||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err);
|
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::ble_client
|
} // namespace esphome::ble_client
|
||||||
|
|||||||
@@ -327,12 +327,10 @@ void BME680Component::read_data_() {
|
|||||||
|
|
||||||
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure,
|
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure,
|
||||||
humidity, gas_resistance);
|
humidity, gas_resistance);
|
||||||
if (!gas_valid) {
|
if (!gas_valid)
|
||||||
ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!");
|
ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!");
|
||||||
}
|
if (!heat_stable)
|
||||||
if (!heat_stable) {
|
|
||||||
ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)");
|
ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)");
|
||||||
}
|
|
||||||
|
|
||||||
if (this->temperature_sensor_ != nullptr)
|
if (this->temperature_sensor_ != nullptr)
|
||||||
this->temperature_sensor_->publish_state(temperature);
|
this->temperature_sensor_->publish_state(temperature);
|
||||||
|
|||||||
@@ -125,19 +125,6 @@ CLIMATE_SWING_MODES = {
|
|||||||
|
|
||||||
validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True)
|
validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True)
|
||||||
|
|
||||||
ClimateAction = climate_ns.enum("ClimateAction")
|
|
||||||
CLIMATE_ACTIONS = {
|
|
||||||
"OFF": ClimateAction.CLIMATE_ACTION_OFF,
|
|
||||||
"COOLING": ClimateAction.CLIMATE_ACTION_COOLING,
|
|
||||||
"HEATING": ClimateAction.CLIMATE_ACTION_HEATING,
|
|
||||||
"IDLE": ClimateAction.CLIMATE_ACTION_IDLE,
|
|
||||||
"DRYING": ClimateAction.CLIMATE_ACTION_DRYING,
|
|
||||||
"FAN": ClimateAction.CLIMATE_ACTION_FAN,
|
|
||||||
"DEFROSTING": ClimateAction.CLIMATE_ACTION_DEFROSTING,
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_climate_action = cv.enum(CLIMATE_ACTIONS, upper=True)
|
|
||||||
|
|
||||||
CONF_MIN_HUMIDITY = "min_humidity"
|
CONF_MIN_HUMIDITY = "min_humidity"
|
||||||
CONF_MAX_HUMIDITY = "max_humidity"
|
CONF_MAX_HUMIDITY = "max_humidity"
|
||||||
CONF_TARGET_HUMIDITY = "target_humidity"
|
CONF_TARGET_HUMIDITY = "target_humidity"
|
||||||
|
|||||||
@@ -368,8 +368,8 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Climate::save_state_(const ClimateTraits &traits) {
|
void Climate::save_state_(const ClimateTraits &traits) {
|
||||||
#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
|
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
|
||||||
#pragma GCC diagnostic push
|
!defined(CLANG_TIDY)
|
||||||
#pragma GCC diagnostic ignored "-Wclass-memaccess"
|
#pragma GCC diagnostic ignored "-Wclass-memaccess"
|
||||||
#define TEMP_IGNORE_MEMACCESS
|
#define TEMP_IGNORE_MEMACCESS
|
||||||
#endif
|
#endif
|
||||||
@@ -749,40 +749,34 @@ void Climate::dump_traits_(const char *tag) {
|
|||||||
}
|
}
|
||||||
if (!traits.get_supported_modes().empty()) {
|
if (!traits.get_supported_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported modes:");
|
ESP_LOGCONFIG(tag, " Supported modes:");
|
||||||
for (ClimateMode m : traits.get_supported_modes()) {
|
for (ClimateMode m : traits.get_supported_modes())
|
||||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!traits.get_supported_fan_modes().empty()) {
|
if (!traits.get_supported_fan_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported fan modes:");
|
ESP_LOGCONFIG(tag, " Supported fan modes:");
|
||||||
for (ClimateFanMode m : traits.get_supported_fan_modes()) {
|
for (ClimateFanMode m : traits.get_supported_fan_modes())
|
||||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||||
for (const char *s : traits.get_supported_custom_fan_modes()) {
|
for (const char *s : traits.get_supported_custom_fan_modes())
|
||||||
ESP_LOGCONFIG(tag, " - %s", s);
|
ESP_LOGCONFIG(tag, " - %s", s);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!traits.get_supported_presets().empty()) {
|
if (!traits.get_supported_presets().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported presets:");
|
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||||
for (ClimatePreset p : traits.get_supported_presets()) {
|
for (ClimatePreset p : traits.get_supported_presets())
|
||||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!traits.get_supported_custom_presets().empty()) {
|
if (!traits.get_supported_custom_presets().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||||
for (const char *s : traits.get_supported_custom_presets()) {
|
for (const char *s : traits.get_supported_custom_presets())
|
||||||
ESP_LOGCONFIG(tag, " - %s", s);
|
ESP_LOGCONFIG(tag, " - %s", s);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!traits.get_supported_swing_modes().empty()) {
|
if (!traits.get_supported_swing_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||||
for (ClimateSwingMode m : traits.get_supported_swing_modes()) {
|
for (ClimateSwingMode m : traits.get_supported_swing_modes())
|
||||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::climate
|
} // namespace esphome::climate
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
|
|||||||
void CM1106Component::dump_config() {
|
void CM1106Component::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "CM1106:");
|
ESP_LOGCONFIG(TAG, "CM1106:");
|
||||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||||
|
this->check_uart_settings(9600);
|
||||||
if (this->is_failed()) {
|
if (this->is_failed()) {
|
||||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,14 +46,6 @@ CONFIG_SCHEMA = (
|
|||||||
.extend(uart.UART_DEVICE_SCHEMA)
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|
||||||
"cm1106",
|
|
||||||
baud_rate=9600,
|
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
"""Code generation entry point."""
|
"""Code generation entry point."""
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ CONF_CHANNEL_COLORS = "channel_colors"
|
|||||||
CONF_CLIMATE_ID = "climate_id"
|
CONF_CLIMATE_ID = "climate_id"
|
||||||
CONF_CO2_EQUIVALENT = "co2_equivalent"
|
CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||||
CONF_COLOR_DEPTH = "color_depth"
|
CONF_COLOR_DEPTH = "color_depth"
|
||||||
CONF_COLUMNS = "columns"
|
|
||||||
CONF_CRC_ENABLE = "crc_enable"
|
CONF_CRC_ENABLE = "crc_enable"
|
||||||
CONF_DATA_BITS = "data_bits"
|
CONF_DATA_BITS = "data_bits"
|
||||||
CONF_DESCRIPTION = "description"
|
CONF_DESCRIPTION = "description"
|
||||||
@@ -23,10 +22,10 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
|||||||
CONF_ENABLED = "enabled"
|
CONF_ENABLED = "enabled"
|
||||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||||
|
CONF_HOST = "host"
|
||||||
CONF_IAQ = "iaq"
|
CONF_IAQ = "iaq"
|
||||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||||
CONF_IS_WRGB = "is_wrgb"
|
CONF_IS_WRGB = "is_wrgb"
|
||||||
CONF_KEYS = "keys"
|
|
||||||
CONF_LABEL = "label"
|
CONF_LABEL = "label"
|
||||||
CONF_LIBRETINY = "libretiny"
|
CONF_LIBRETINY = "libretiny"
|
||||||
CONF_LOOP = "loop"
|
CONF_LOOP = "loop"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ void CSE7761Component::dump_config() {
|
|||||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
}
|
}
|
||||||
LOG_UPDATE_INTERVAL(this);
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSE7761Component::update() {
|
void CSE7761Component::update() {
|
||||||
|
|||||||
@@ -68,13 +68,7 @@ CONFIG_SCHEMA = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||||
"cse7761",
|
"cse7761", baud_rate=38400, require_rx=True, require_tx=True
|
||||||
baud_rate=38400,
|
|
||||||
require_rx=True,
|
|
||||||
require_tx=True,
|
|
||||||
data_bits=8,
|
|
||||||
parity="EVEN",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ void CSE7766Component::dump_config() {
|
|||||||
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
|
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
|
||||||
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
|
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
|
||||||
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
|
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
|
||||||
|
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::cse7766
|
} // namespace esphome::cse7766
|
||||||
|
|||||||
@@ -84,12 +84,7 @@ CONFIG_SCHEMA = (
|
|||||||
.extend(cv.COMPONENT_SCHEMA)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
)
|
)
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||||
"cse7766",
|
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True
|
||||||
baud_rate=4800,
|
|
||||||
require_rx=True,
|
|
||||||
data_bits=8,
|
|
||||||
parity="EVEN",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
#include "d01.h"
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
|
|
||||||
// uart specification for d01 sensor from https://manuals.plus/ae/1005006417362019:
|
|
||||||
//
|
|
||||||
// A frame of serial output data includes 4 bytes, formatted as follows:
|
|
||||||
// __Characteristic byte: Fixed value 0xA5.
|
|
||||||
// __Data byte: DATAH is the high 7 bits of the concentration value, and DATAL is the low 7 bits of the concentration
|
|
||||||
// value.
|
|
||||||
// __Check byte: The low 7 bits of the sum of all bytes before the check byte.
|
|
||||||
//
|
|
||||||
// If the serial output is 4 bytes of data: 0*A5 0*01 0*2C 0*52, then DATAH = 0*01 = 1, DATAL = 0*2C = 44.
|
|
||||||
// Concentration value = 1*128 + 44 = 172 µg/m³.
|
|
||||||
//
|
|
||||||
// The PM2.5 dust concentration value obtained from the dust sensor needs to be calibrated with a K value coefficient
|
|
||||||
// based on the TSI instrument's photometric method. It is generally recommended to use 0.4.
|
|
||||||
|
|
||||||
namespace esphome::d01 {
|
|
||||||
|
|
||||||
static const char *const TAG = "d01";
|
|
||||||
|
|
||||||
static const uint8_t D01_FRAME_HEADER = 0xA5;
|
|
||||||
|
|
||||||
void D01SensorComponent::dump_config() { LOG_SENSOR(" ", "D01 PM2.5", this); }
|
|
||||||
|
|
||||||
void D01SensorComponent::loop() {
|
|
||||||
uint8_t buf[4];
|
|
||||||
while (this->available() >= 4) {
|
|
||||||
if (this->peek() != D01_FRAME_HEADER) {
|
|
||||||
this->read();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this->read_array(buf, 4);
|
|
||||||
uint8_t sum = (buf[0] + buf[1] + buf[2]) & 0x7F;
|
|
||||||
if (sum != buf[3]) {
|
|
||||||
ESP_LOGW(TAG, "checksum mismatch");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
uint16_t latest_concentration = (buf[1] & 0x7F) * 128 + (buf[2] & 0x7F);
|
|
||||||
ESP_LOGV(TAG, "Unadjusted PM2.5 Concentration: %d µg/m³", latest_concentration);
|
|
||||||
this->publish_state(latest_concentration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::d01
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "esphome/components/sensor/sensor.h"
|
|
||||||
#include "esphome/components/uart/uart.h"
|
|
||||||
|
|
||||||
namespace esphome::d01 {
|
|
||||||
|
|
||||||
class D01SensorComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
|
|
||||||
public:
|
|
||||||
void dump_config() override;
|
|
||||||
void loop() override;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace esphome::d01
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
from esphome.components import sensor, uart
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.const import (
|
|
||||||
DEVICE_CLASS_PM25,
|
|
||||||
ICON_BLUR,
|
|
||||||
STATE_CLASS_MEASUREMENT,
|
|
||||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
)
|
|
||||||
from esphome.types import ConfigType
|
|
||||||
|
|
||||||
CODEOWNERS = ["@ch604"]
|
|
||||||
DEPENDENCIES = ["uart"]
|
|
||||||
|
|
||||||
d01_ns = cg.esphome_ns.namespace("d01")
|
|
||||||
D01SensorComponent = d01_ns.class_(
|
|
||||||
"D01SensorComponent", sensor.Sensor, uart.UARTDevice, cg.Component
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
|
||||||
sensor.sensor_schema(
|
|
||||||
D01SensorComponent,
|
|
||||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
icon=ICON_BLUR,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_PM25,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
)
|
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
|
||||||
.extend(uart.UART_DEVICE_SCHEMA)
|
|
||||||
)
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|
||||||
"d01",
|
|
||||||
baud_rate=9600,
|
|
||||||
require_rx=True,
|
|
||||||
require_tx=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
|
||||||
var = await sensor.new_sensor(config)
|
|
||||||
await cg.register_component(var, config)
|
|
||||||
await uart.register_uart_device(var, config)
|
|
||||||
@@ -26,14 +26,6 @@ CONFIG_SCHEMA = (
|
|||||||
.extend(cv.polling_component_schema("30s"))
|
.extend(cv.polling_component_schema("30s"))
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|
||||||
"daly_bms",
|
|
||||||
baud_rate=9600,
|
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
|
|||||||
|
|
||||||
void DalyBmsComponent::setup() { this->next_request_ = 1; }
|
void DalyBmsComponent::setup() { this->next_request_ = 1; }
|
||||||
|
|
||||||
void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); }
|
void DalyBmsComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Daly BMS:");
|
||||||
|
this->check_uart_settings(9600);
|
||||||
|
}
|
||||||
|
|
||||||
void DalyBmsComponent::update() {
|
void DalyBmsComponent::update() {
|
||||||
this->trigger_next_ = true;
|
this->trigger_next_ = true;
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ void DebugComponent::dump_config() {
|
|||||||
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
|
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
|
||||||
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
|
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
|
||||||
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
|
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
|
||||||
#ifdef USE_ESP8266
|
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||||
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
|
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
|
||||||
#endif // USE_ESP8266
|
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||||
#endif // USE_SENSOR
|
#endif // USE_SENSOR
|
||||||
|
|
||||||
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
|
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent {
|
|||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
|
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
|
||||||
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
|
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
|
||||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||||
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
|
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||||
@@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent {
|
|||||||
|
|
||||||
sensor::Sensor *free_sensor_{nullptr};
|
sensor::Sensor *free_sensor_{nullptr};
|
||||||
sensor::Sensor *block_sensor_{nullptr};
|
sensor::Sensor *block_sensor_{nullptr};
|
||||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||||
sensor::Sensor *fragmentation_sensor_{nullptr};
|
sensor::Sensor *fragmentation_sensor_{nullptr};
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||||
|
|||||||
@@ -159,10 +159,12 @@ void DebugComponent::update_platform_() {
|
|||||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||||
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
|
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
|
||||||
}
|
}
|
||||||
|
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||||
if (this->fragmentation_sensor_ != nullptr) {
|
if (this->fragmentation_sensor_ != nullptr) {
|
||||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||||
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
|
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,12 @@ CONFIG_SCHEMA = {
|
|||||||
),
|
),
|
||||||
cv.Optional(CONF_FRAGMENTATION): cv.All(
|
cv.Optional(CONF_FRAGMENTATION): cv.All(
|
||||||
cv.Any(
|
cv.Any(
|
||||||
|
cv.All(
|
||||||
cv.only_on_esp8266,
|
cv.only_on_esp8266,
|
||||||
|
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
|
||||||
|
),
|
||||||
cv.only_on_esp32,
|
cv.only_on_esp32,
|
||||||
msg="This feature is only available on ESP8266 and ESP32",
|
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
|
||||||
),
|
),
|
||||||
sensor.sensor_schema(
|
sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_PERCENT,
|
unit_of_measurement=UNIT_PERCENT,
|
||||||
|
|||||||
@@ -60,12 +60,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
).extend(uart.UART_DEVICE_SCHEMA)
|
).extend(uart.UART_DEVICE_SCHEMA)
|
||||||
)
|
)
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||||
"dfplayer",
|
"dfplayer", baud_rate=9600, require_tx=True
|
||||||
baud_rate=9600,
|
|
||||||
require_tx=True,
|
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,9 @@ void DFPlayer::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
|
void DFPlayer::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "DFPlayer:");
|
||||||
|
this->check_uart_settings(9600);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::dfplayer
|
} // namespace esphome::dfplayer
|
||||||
|
|||||||
@@ -154,9 +154,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (error_code != 0) {
|
if (error_code != 0) {
|
||||||
if (report_errors) {
|
if (report_errors)
|
||||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
#include "ds1603l.h"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include "esphome/core/helpers.h"
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
|
|
||||||
namespace esphome::ds1603l {
|
|
||||||
|
|
||||||
static const char *const TAG = "ds1603l.sensor";
|
|
||||||
|
|
||||||
void DS1603L::loop() {
|
|
||||||
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
|
|
||||||
uint8_t byte;
|
|
||||||
while (this->available() > 0 && this->read_byte(&byte)) {
|
|
||||||
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
|
|
||||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->rx_buffer_[this->rx_count_++] = byte;
|
|
||||||
if (this->rx_count_ < FRAME_SIZE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->parse_data_()) {
|
|
||||||
this->rx_count_ = 0;
|
|
||||||
} else {
|
|
||||||
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
|
|
||||||
this->resync_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
|
|
||||||
|
|
||||||
bool DS1603L::parse_data_() {
|
|
||||||
uint8_t header = this->rx_buffer_[0];
|
|
||||||
uint8_t data_h = this->rx_buffer_[1];
|
|
||||||
uint8_t data_l = this->rx_buffer_[2];
|
|
||||||
uint8_t checksum = this->rx_buffer_[3];
|
|
||||||
|
|
||||||
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
|
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Data: Header=0x%02X, Data_H=0x%02X, Data_L=0x%02X, Checksum=0x%02X", header, data_h, data_l, checksum);
|
|
||||||
|
|
||||||
if (checksum != computed_checksum) {
|
|
||||||
ESP_LOGW(TAG, "Checksum mismatch: received 0x%02X, expected 0x%02X", checksum, computed_checksum);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->publish_state(encode_uint16(data_h, data_l));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DS1603L::resync_() {
|
|
||||||
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
|
|
||||||
size_t start = 1;
|
|
||||||
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
|
|
||||||
start++;
|
|
||||||
}
|
|
||||||
this->rx_count_ -= start;
|
|
||||||
if (this->rx_count_ > 0) {
|
|
||||||
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::ds1603l
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
#include "esphome/components/sensor/sensor.h"
|
|
||||||
#include "esphome/components/uart/uart.h"
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
|
|
||||||
namespace esphome::ds1603l {
|
|
||||||
|
|
||||||
class DS1603L final : public sensor::Sensor, public Component, public uart::UARTDevice {
|
|
||||||
public:
|
|
||||||
void loop() override;
|
|
||||||
void dump_config() override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
static constexpr uint8_t HEADER_BYTE = 0xFF;
|
|
||||||
static constexpr size_t FRAME_SIZE = 4;
|
|
||||||
|
|
||||||
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
|
|
||||||
bool parse_data_();
|
|
||||||
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
|
|
||||||
void resync_();
|
|
||||||
|
|
||||||
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
|
|
||||||
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace esphome::ds1603l
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
from esphome.components import sensor, uart
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.const import (
|
|
||||||
DEVICE_CLASS_DISTANCE,
|
|
||||||
STATE_CLASS_MEASUREMENT,
|
|
||||||
UNIT_MILLIMETER,
|
|
||||||
)
|
|
||||||
from esphome.types import ConfigType
|
|
||||||
|
|
||||||
CODEOWNERS = ["@JakeLC15"]
|
|
||||||
DEPENDENCIES = ["uart"]
|
|
||||||
|
|
||||||
ds1603l_ns = cg.esphome_ns.namespace("ds1603l")
|
|
||||||
DS1603L = ds1603l_ns.class_("DS1603L", sensor.Sensor, cg.Component, uart.UARTDevice)
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
|
||||||
sensor.sensor_schema(
|
|
||||||
DS1603L,
|
|
||||||
unit_of_measurement=UNIT_MILLIMETER,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_DISTANCE,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
)
|
|
||||||
.extend(uart.UART_DEVICE_SCHEMA)
|
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
|
||||||
)
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|
||||||
"ds1603l",
|
|
||||||
baud_rate=9600,
|
|
||||||
require_tx=False,
|
|
||||||
require_rx=True,
|
|
||||||
data_bits=8,
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
|
||||||
var = await sensor.new_sensor(config)
|
|
||||||
await cg.register_component(var, config)
|
|
||||||
await uart.register_uart_device(var, config)
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
#include "epaper_spi_uc8179.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
|
|
||||||
namespace esphome::epaper_spi {
|
|
||||||
|
|
||||||
static constexpr const char *const TAG = "epaper_spi.uc8179";
|
|
||||||
|
|
||||||
bool EPaperUC8179::initialise(bool partial) {
|
|
||||||
EPaperBase::initialise(partial); // send the model init sequence
|
|
||||||
this->partial_ = partial;
|
|
||||||
ESP_LOGV(TAG, "Power on");
|
|
||||||
// POWER ON must precede the waveform/mode registers and the data transfer
|
|
||||||
// (the original driver powers on and busy-waits before writing them).
|
|
||||||
// The state machine busy-waits before entering TRANSFER_DATA.
|
|
||||||
this->command(0x04);
|
|
||||||
// Give the busy line time to assert before the state machine polls it
|
|
||||||
this->next_delay_ = 100;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the refresh mode. Must be called after power-on has completed.
|
|
||||||
void EPaperUC8179::set_refresh_mode_() {
|
|
||||||
if (!this->is_using_partial_update_()) {
|
|
||||||
return; // plain full refresh uses the mode set by the init sequence
|
|
||||||
}
|
|
||||||
// Fast and partial refresh use flipped data polarity and a floating border
|
|
||||||
this->cmd_data(0x50, {0xA9, 0x07});
|
|
||||||
// Force the waveform via the temperature registers: 0x5A selects the fast
|
|
||||||
// full-refresh waveform, 0x6E the partial-refresh waveform
|
|
||||||
this->cmd_data(0xE0, {0x02});
|
|
||||||
if (this->partial_) {
|
|
||||||
this->cmd_data(0xE5, {0x6E});
|
|
||||||
this->command(0x91); // enter partial mode
|
|
||||||
// Set the partial window to the full screen
|
|
||||||
const uint16_t x_end = this->width_ - 1;
|
|
||||||
const uint16_t y_end = this->height_ - 1;
|
|
||||||
this->cmd_data(0x90, {0x00, 0x00, static_cast<uint8_t>(x_end >> 8), static_cast<uint8_t>(x_end & 0xFF), 0x00, 0x00,
|
|
||||||
static_cast<uint8_t>(y_end >> 8), static_cast<uint8_t>(y_end & 0xFF), 0x01});
|
|
||||||
} else {
|
|
||||||
this->cmd_data(0xE5, {0x5A});
|
|
||||||
this->command(0x92); // exit partial mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HOT EPaperUC8179::transfer_data() {
|
|
||||||
const uint32_t start_time = millis();
|
|
||||||
const size_t buffer_length = this->buffer_length_;
|
|
||||||
if (this->current_data_index_ == 0) {
|
|
||||||
this->set_refresh_mode_();
|
|
||||||
}
|
|
||||||
// Fast full refresh sends the previous-image plane as well, so that every pixel transitions
|
|
||||||
const bool two_pass = this->is_using_partial_update_() && !this->partial_;
|
|
||||||
// Plain full refresh sends inverted data (buffer is 1=white, the wire wants 0=white);
|
|
||||||
// in fast/partial mode the data polarity is flipped via the VCOM/data-interval
|
|
||||||
// register instead, so the new-image plane is sent unmodified
|
|
||||||
const bool invert_new_data = !this->is_using_partial_update_();
|
|
||||||
|
|
||||||
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
|
|
||||||
|
|
||||||
// Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image
|
|
||||||
if (two_pass && this->current_data_index_ < buffer_length) {
|
|
||||||
if (this->current_data_index_ == 0) {
|
|
||||||
this->command(0x10); // DATA START TRANSMISSION 1 (previous image)
|
|
||||||
}
|
|
||||||
this->start_data_();
|
|
||||||
while (this->current_data_index_ < buffer_length) {
|
|
||||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_);
|
|
||||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
|
||||||
bytes_to_send[i] = ~this->buffer_[this->current_data_index_ + i];
|
|
||||||
}
|
|
||||||
this->write_array(bytes_to_send, bytes_to_copy);
|
|
||||||
this->current_data_index_ += bytes_to_copy;
|
|
||||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
|
||||||
this->disable();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: new image via 0x13 (DTM2)
|
|
||||||
const size_t offset = two_pass ? buffer_length : 0;
|
|
||||||
const size_t total = offset + buffer_length;
|
|
||||||
if (this->current_data_index_ < total) {
|
|
||||||
if (this->current_data_index_ == offset) {
|
|
||||||
this->command(0x13); // DATA START TRANSMISSION 2 (new image)
|
|
||||||
}
|
|
||||||
this->start_data_();
|
|
||||||
while (this->current_data_index_ < total) {
|
|
||||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, total - this->current_data_index_);
|
|
||||||
const size_t data_idx = this->current_data_index_ - offset;
|
|
||||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
|
||||||
const uint8_t byte = this->buffer_[data_idx + i];
|
|
||||||
bytes_to_send[i] = invert_new_data ? ~byte : byte;
|
|
||||||
}
|
|
||||||
this->write_array(bytes_to_send, bytes_to_copy);
|
|
||||||
this->current_data_index_ += bytes_to_copy;
|
|
||||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
|
||||||
this->disable();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
this->current_data_index_ = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EPaperUC8179::power_on() {
|
|
||||||
// Power-on is sent at the end of initialise() instead, because the
|
|
||||||
// waveform/mode registers and the data transfer must follow it
|
|
||||||
}
|
|
||||||
|
|
||||||
void EPaperUC8179::refresh_screen(bool /*partial*/) {
|
|
||||||
ESP_LOGV(TAG, "Refresh");
|
|
||||||
this->command(0x12); // DISPLAY REFRESH
|
|
||||||
// Delay the next busy poll: the busy line takes a short time to assert after
|
|
||||||
// the refresh command, and polling too early would read it as already idle
|
|
||||||
this->next_delay_ = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EPaperUC8179::power_off() {
|
|
||||||
ESP_LOGV(TAG, "Power off");
|
|
||||||
this->command(0x02); // POWER OFF
|
|
||||||
}
|
|
||||||
|
|
||||||
void EPaperUC8179::deep_sleep() {
|
|
||||||
// Deep sleep loses the previous-image RAM that partial refresh compares against
|
|
||||||
if (!this->is_using_partial_update_()) {
|
|
||||||
ESP_LOGV(TAG, "Deep sleep");
|
|
||||||
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::epaper_spi
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "epaper_spi.h"
|
|
||||||
|
|
||||||
namespace esphome::epaper_spi {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Monochrome e-paper displays using the UC8179 controller.
|
|
||||||
* Supports: 7.5" V2 (EPD_7in5_V2), 800x480 pixels, as used by the
|
|
||||||
* Waveshare 7.5" V2 HAT and the Seeed reTerminal E1001.
|
|
||||||
*
|
|
||||||
* Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default).
|
|
||||||
*
|
|
||||||
* The INITIALISE state sends the panel configuration followed by power-on
|
|
||||||
* (0x04); the state machine busy-waits for power-on to complete before
|
|
||||||
* TRANSFER_DATA, which first writes the waveform/mode registers (these are
|
|
||||||
* only accepted while powered) and then the image data. The state machine
|
|
||||||
* busy-waits again before triggering REFRESH_SCREEN (0x12).
|
|
||||||
*
|
|
||||||
* Three refresh modes are used, following the Waveshare EPD_7in5_V2 examples:
|
|
||||||
* - full_update_every == 1: plain full refresh. The new image is sent
|
|
||||||
* inverted to DTM2 (0x13) and the controller uses its normal waveform.
|
|
||||||
* - full_update_every > 1, full update: fast full refresh. The data polarity
|
|
||||||
* is flipped via the VCOM/data-interval register, a fast waveform is forced
|
|
||||||
* via the temperature registers, and the image is sent to both DTM1 (0x10,
|
|
||||||
* inverted) and DTM2 (0x13) so that every pixel transitions.
|
|
||||||
* - full_update_every > 1, partial update: partial refresh. A partial-update
|
|
||||||
* waveform is forced, partial mode is entered with a full-screen window and
|
|
||||||
* only DTM2 is sent; the controller compares against its previous-image RAM.
|
|
||||||
*/
|
|
||||||
class EPaperUC8179 final : public EPaperBase {
|
|
||||||
public:
|
|
||||||
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
|
||||||
size_t init_sequence_length)
|
|
||||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
|
|
||||||
this->buffer_length_ = this->row_width_ * height;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool initialise(bool partial) override;
|
|
||||||
bool transfer_data() override;
|
|
||||||
void refresh_screen(bool partial) override;
|
|
||||||
void power_on() override;
|
|
||||||
void power_off() override;
|
|
||||||
void deep_sleep() override;
|
|
||||||
void set_refresh_mode_();
|
|
||||||
|
|
||||||
// Set by initialise() so transfer_data() knows which planes to send
|
|
||||||
bool partial_{};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace esphome::epaper_spi
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
"""Monochrome e-paper displays using the UC8179 controller.
|
|
||||||
|
|
||||||
Supported models:
|
|
||||||
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
|
|
||||||
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
|
|
||||||
7.5" 800x480 panel on an integrated ESP32-S3 board
|
|
||||||
|
|
||||||
Panel configuration and power-on (0x04) are both sent during the INITIALISE
|
|
||||||
state; the state machine's built-in busy wait then covers the power-on delay
|
|
||||||
before the waveform/mode registers and image data are transferred.
|
|
||||||
|
|
||||||
These displays support fast full and partial refresh: set ``full_update_every``
|
|
||||||
greater than 1 to enable it. Every ``full_update_every``-th update is a fast
|
|
||||||
full refresh, with partial refreshes in between.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from esphome.const import CONF_DATA_RATE
|
|
||||||
|
|
||||||
from . import EpaperModel
|
|
||||||
|
|
||||||
|
|
||||||
class UC8179(EpaperModel):
|
|
||||||
"""EpaperModel class for monochrome displays using the UC8179 controller."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
class_name: str = "EPaperUC8179",
|
|
||||||
data_rate: str = "10MHz",
|
|
||||||
**defaults: Any,
|
|
||||||
) -> None:
|
|
||||||
defaults.setdefault(CONF_DATA_RATE, data_rate)
|
|
||||||
super().__init__(name, class_name, **defaults)
|
|
||||||
|
|
||||||
def get_init_sequence(self, config: dict) -> tuple:
|
|
||||||
"""Generate the initialization sequence for UC8179 mono displays.
|
|
||||||
|
|
||||||
Panel configuration only — the driver appends power-on (0x04) at the
|
|
||||||
end of the INITIALISE state, and the state machine busy-waits for it
|
|
||||||
to complete before the data transfer starts.
|
|
||||||
"""
|
|
||||||
width, height = self.get_dimensions(config)
|
|
||||||
return (
|
|
||||||
# POWER SETTING
|
|
||||||
(0x01, 0x07, 0x07, 0x3F, 0x3F),
|
|
||||||
# BOOSTER SOFT START
|
|
||||||
(0x06, 0x17, 0x17, 0x28, 0x17),
|
|
||||||
# PANEL SETTING (black/white mode, LUT from OTP)
|
|
||||||
(0x00, 0x1F),
|
|
||||||
# RESOLUTION SETTING (width x height)
|
|
||||||
(
|
|
||||||
0x61,
|
|
||||||
(width >> 8) & 0xFF,
|
|
||||||
width & 0xFF,
|
|
||||||
(height >> 8) & 0xFF,
|
|
||||||
height & 0xFF,
|
|
||||||
),
|
|
||||||
# DUAL SPI MODE (disabled)
|
|
||||||
(0x15, 0x00),
|
|
||||||
# VCOM AND DATA INTERVAL SETTING
|
|
||||||
(0x50, 0x10, 0x07),
|
|
||||||
# TCON SETTING
|
|
||||||
(0x60, 0x22),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
uc8179 = UC8179("uc8179")
|
|
||||||
|
|
||||||
# Waveshare 7.5" V2 mono (EPD_7in5_V2) — 800x480, UC8179 controller
|
|
||||||
waveshare_7_5_v2 = uc8179.extend(
|
|
||||||
"waveshare-7.5in-v2",
|
|
||||||
width=800,
|
|
||||||
height=480,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Seeed reTerminal E1001 — 7.5" mono e-paper (800x480), same panel as the
|
|
||||||
# Waveshare 7.5" V2, driven by an integrated ESP32-S3 board
|
|
||||||
waveshare_7_5_v2.extend(
|
|
||||||
"seeed-reterminal-e1001",
|
|
||||||
cs_pin=10,
|
|
||||||
dc_pin=11,
|
|
||||||
reset_pin=12,
|
|
||||||
busy_pin={
|
|
||||||
"number": 13,
|
|
||||||
"inverted": True,
|
|
||||||
"mode": {
|
|
||||||
"input": True,
|
|
||||||
"pullup": True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -182,13 +182,6 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
|
|||||||
VARIANT_ESP32,
|
VARIANT_ESP32,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Variants that support execution from PSRAM
|
|
||||||
PSRAM_XIP_VARIANTS = {
|
|
||||||
VARIANT_ESP32S3,
|
|
||||||
VARIANT_ESP32P4,
|
|
||||||
VARIANT_ESP32S31,
|
|
||||||
}
|
|
||||||
|
|
||||||
# NVS encryption (HMAC peripheral scheme) is only available on variants that
|
# NVS encryption (HMAC peripheral scheme) is only available on variants that
|
||||||
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
|
# 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
|
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
|
||||||
@@ -1530,7 +1523,7 @@ def final_validate(config) -> None:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||||
if config[CONF_VARIANT] not in PSRAM_XIP_VARIANTS:
|
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
|
||||||
errs.append(
|
errs.append(
|
||||||
cv.Invalid(
|
cv.Invalid(
|
||||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
|
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
|
||||||
@@ -2734,7 +2727,13 @@ async def to_code(config):
|
|||||||
_configure_lwip_max_sockets(conf)
|
_configure_lwip_max_sockets(conf)
|
||||||
|
|
||||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||||
|
if variant == VARIANT_ESP32S3:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||||
|
elif variant == VARIANT_ESP32P4:
|
||||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
|
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unhandled ESP32 variant")
|
||||||
|
|
||||||
# Apply LWIP core locking for better socket performance
|
# Apply LWIP core locking for better socket performance
|
||||||
# This is already enabled by default in Arduino framework, where it provides
|
# This is already enabled by default in Arduino framework, where it provides
|
||||||
|
|||||||
@@ -100,38 +100,21 @@ void ESP32BLE::disable() {
|
|||||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||||
void ESP32BLE::advertising_start() {
|
void ESP32BLE::advertising_start() {
|
||||||
this->advertising_init_();
|
this->advertising_init_();
|
||||||
this->advertising_ref_count_++;
|
if (!this->is_active())
|
||||||
this->advertising_refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ESP32BLE::advertising_stop() {
|
|
||||||
if (this->advertising_ref_count_ == 0)
|
|
||||||
return;
|
return;
|
||||||
this->advertising_ref_count_--;
|
|
||||||
this->advertising_refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ESP32BLE::advertising_refresh() {
|
|
||||||
if (this->advertising_ == nullptr || !this->is_active())
|
|
||||||
return;
|
|
||||||
// Advertise while any component still needs it, otherwise stop
|
|
||||||
if (this->advertising_ref_count_ == 0) {
|
|
||||||
this->advertising_->stop();
|
|
||||||
} else {
|
|
||||||
this->advertising_->start();
|
this->advertising_->start();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||||
this->advertising_init_();
|
this->advertising_init_();
|
||||||
this->advertising_->set_service_data(data);
|
this->advertising_->set_service_data(data);
|
||||||
this->advertising_refresh();
|
this->advertising_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||||
this->advertising_init_();
|
this->advertising_init_();
|
||||||
this->advertising_->set_manufacturer_data(data);
|
this->advertising_->set_manufacturer_data(data);
|
||||||
this->advertising_refresh();
|
this->advertising_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name) {
|
void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name) {
|
||||||
@@ -153,7 +136,7 @@ void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> da
|
|||||||
this->advertising_->set_service_data(data);
|
this->advertising_->set_service_data(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->advertising_refresh();
|
this->advertising_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
|
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
|
||||||
@@ -164,13 +147,13 @@ void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<voi
|
|||||||
void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
|
void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
|
||||||
this->advertising_init_();
|
this->advertising_init_();
|
||||||
this->advertising_->add_service_uuid(uuid);
|
this->advertising_->add_service_uuid(uuid);
|
||||||
this->advertising_refresh();
|
this->advertising_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
||||||
this->advertising_init_();
|
this->advertising_init_();
|
||||||
this->advertising_->remove_service_uuid(uuid);
|
this->advertising_->remove_service_uuid(uuid);
|
||||||
this->advertising_refresh();
|
this->advertising_start();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -592,10 +575,6 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
||||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
|
||||||
// Requests made before the stack was up (or before it was re-enabled) take effect now
|
|
||||||
this->advertising_refresh();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,17 +114,7 @@ class ESP32BLE final : public Component {
|
|||||||
void set_name(const char *name) { this->name_ = name; }
|
void set_name(const char *name) { this->name_ = name; }
|
||||||
|
|
||||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||||
/** Request advertising on behalf of a component.
|
|
||||||
*
|
|
||||||
* Requests are reference counted: advertising runs until every component that called
|
|
||||||
* advertising_start() has released it again with advertising_stop(). Each component must
|
|
||||||
* pair its calls, so nothing advertises until something actually asks for it.
|
|
||||||
*/
|
|
||||||
void advertising_start();
|
void advertising_start();
|
||||||
/// Release a request made with advertising_start(); advertising stops at the last release.
|
|
||||||
void advertising_stop();
|
|
||||||
/// Apply the current payload and request count: advertise while requested, otherwise stop.
|
|
||||||
void advertising_refresh();
|
|
||||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||||
@@ -236,9 +226,6 @@ class ESP32BLE final : public Component {
|
|||||||
// 1-byte aligned members (grouped together to minimize padding)
|
// 1-byte aligned members (grouped together to minimize padding)
|
||||||
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
|
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
|
||||||
bool enable_on_boot_{}; // 1 byte
|
bool enable_on_boot_{}; // 1 byte
|
||||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
|
||||||
uint8_t advertising_ref_count_{0}; // 1 byte, number of components requesting advertising
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||||
optional<esp_ble_auth_req_t> auth_req_mode_;
|
optional<esp_ble_auth_req_t> auth_req_mode_;
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ void ESP32BLEBeacon::setup() {
|
|||||||
this->on_advertise_();
|
this->on_advertise_();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// A beacon always needs the device to advertise, and never releases the request
|
|
||||||
global_ble->advertising_start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLEBeacon::on_advertise_() {
|
void ESP32BLEBeacon::on_advertise_() {
|
||||||
|
|||||||
@@ -596,18 +596,6 @@ async def to_code(config):
|
|||||||
cg.add(var.set_parent(parent))
|
cg.add(var.set_parent(parent))
|
||||||
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
|
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
|
||||||
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
|
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 (esp32_improv) stays
|
|
||||||
# silent until that service asks for advertising.
|
|
||||||
cg.add(
|
|
||||||
var.set_advertising_required(
|
|
||||||
CONF_MANUFACTURER_DATA in config
|
|
||||||
or any(
|
|
||||||
not uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID)
|
|
||||||
for service_config in config[CONF_SERVICES]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if CONF_MANUFACTURER_DATA in config:
|
if CONF_MANUFACTURER_DATA in config:
|
||||||
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
|
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
|
||||||
for service_config in config[CONF_SERVICES]:
|
for service_config in config[CONF_SERVICES]:
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ void BLEServer::loop() {
|
|||||||
if (this->device_information_service_->is_running()) {
|
if (this->device_information_service_->is_running()) {
|
||||||
this->state_ = RUNNING;
|
this->state_ = RUNNING;
|
||||||
this->restart_advertising_();
|
this->restart_advertising_();
|
||||||
this->request_advertising_();
|
|
||||||
ESP_LOGD(TAG, "BLE server setup successfully");
|
ESP_LOGD(TAG, "BLE server setup successfully");
|
||||||
} else if (this->device_information_service_->is_created()) {
|
} else if (this->device_information_service_->is_created()) {
|
||||||
this->device_information_service_->start();
|
this->device_information_service_->start();
|
||||||
@@ -99,20 +98,6 @@ void BLEServer::restart_advertising_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BLEServer::request_advertising_() {
|
|
||||||
if (!this->advertising_required_ || this->advertising_requested_)
|
|
||||||
return;
|
|
||||||
this->advertising_requested_ = true;
|
|
||||||
this->parent_->advertising_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLEServer::release_advertising_() {
|
|
||||||
if (!this->advertising_requested_)
|
|
||||||
return;
|
|
||||||
this->advertising_requested_ = false;
|
|
||||||
this->parent_->advertising_stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
|
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||||
@@ -185,7 +170,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
|
|||||||
this->add_client_(param->connect.conn_id);
|
this->add_client_(param->connect.conn_id);
|
||||||
// Resume advertising so additional clients can discover and connect
|
// Resume advertising so additional clients can discover and connect
|
||||||
if (this->client_count_ < this->max_clients_) {
|
if (this->client_count_ < this->max_clients_) {
|
||||||
this->parent_->advertising_refresh();
|
this->parent_->advertising_start();
|
||||||
}
|
}
|
||||||
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
|
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
|
||||||
break;
|
break;
|
||||||
@@ -193,7 +178,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
|
|||||||
case ESP_GATTS_DISCONNECT_EVT: {
|
case ESP_GATTS_DISCONNECT_EVT: {
|
||||||
ESP_LOGD(TAG, "BLE Client disconnected");
|
ESP_LOGD(TAG, "BLE Client disconnected");
|
||||||
this->remove_client_(param->disconnect.conn_id);
|
this->remove_client_(param->disconnect.conn_id);
|
||||||
this->parent_->advertising_refresh();
|
this->parent_->advertising_start();
|
||||||
this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
|
this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -241,8 +226,6 @@ void BLEServer::remove_client_(uint16_t conn_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BLEServer::ble_before_disabled_event_handler() {
|
void BLEServer::ble_before_disabled_event_handler() {
|
||||||
// Advertising is re-requested once the server is running again after BLE is re-enabled
|
|
||||||
this->release_advertising_();
|
|
||||||
// Delete all clients
|
// Delete all clients
|
||||||
this->client_count_ = 0;
|
this->client_count_ = 0;
|
||||||
// Delete all services
|
// Delete all services
|
||||||
|
|||||||
@@ -38,13 +38,6 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
|||||||
this->restart_advertising_();
|
this->restart_advertising_();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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. esp32_improv), which
|
|
||||||
* request advertising themselves for as long as they need it.
|
|
||||||
*/
|
|
||||||
void set_advertising_required(bool required) { this->advertising_required_ = required; }
|
|
||||||
|
|
||||||
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
|
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
|
||||||
uint8_t get_max_clients() const { return this->max_clients_; }
|
uint8_t get_max_clients() const { return this->max_clients_; }
|
||||||
|
|
||||||
@@ -89,8 +82,6 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void restart_advertising_();
|
void restart_advertising_();
|
||||||
void request_advertising_();
|
|
||||||
void release_advertising_();
|
|
||||||
|
|
||||||
int8_t find_client_index_(uint16_t conn_id) const;
|
int8_t find_client_index_(uint16_t conn_id) const;
|
||||||
void add_client_(uint16_t conn_id);
|
void add_client_(uint16_t conn_id);
|
||||||
@@ -102,8 +93,6 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
|||||||
std::vector<uint8_t> manufacturer_data_{};
|
std::vector<uint8_t> manufacturer_data_{};
|
||||||
esp_gatt_if_t gatts_if_{0};
|
esp_gatt_if_t gatts_if_{0};
|
||||||
bool registered_{false};
|
bool registered_{false};
|
||||||
bool advertising_required_{true};
|
|
||||||
bool advertising_requested_{false};
|
|
||||||
|
|
||||||
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
|
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
|
||||||
uint8_t client_count_{0};
|
uint8_t client_count_{0};
|
||||||
|
|||||||
@@ -210,9 +210,8 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
|
|||||||
if (!image) {
|
if (!image) {
|
||||||
// A shutdown is not a lost frame: wait_for_image_() returns empty as soon
|
// A shutdown is not a lost frame: wait_for_image_() returns empty as soon
|
||||||
// as running_ clears, and the loop condition below ends the stream anyway.
|
// as running_ clears, and the loop condition below ends the stream anyway.
|
||||||
if (this->running_) {
|
if (this->running_)
|
||||||
ESP_LOGW(TAG, "STREAM: failed to acquire frame");
|
ESP_LOGW(TAG, "STREAM: failed to acquire frame");
|
||||||
}
|
|
||||||
res = ESP_FAIL;
|
res = ESP_FAIL;
|
||||||
}
|
}
|
||||||
if (res == ESP_OK) {
|
if (res == ESP_OK) {
|
||||||
|
|||||||
@@ -37,25 +37,6 @@ CONF_HANDSHAKE_PIN = "handshake_pin"
|
|||||||
CONF_SDIO_FREQUENCY = "sdio_frequency"
|
CONF_SDIO_FREQUENCY = "sdio_frequency"
|
||||||
CONF_SPI_MODE = "spi_mode"
|
CONF_SPI_MODE = "spi_mode"
|
||||||
|
|
||||||
# ESP-NOW-over-hosted shim (esp_now_hosted.cpp). esp-hosted proxies esp_wifi.h
|
|
||||||
# but not esp_now.h (espressif/esp-hosted-mcu#19), and esp_wifi_remote injects
|
|
||||||
# the esp_now.h header on the ESP32-P4 host with no implementation, leaving the
|
|
||||||
# esp_now_* symbols undefined at link. On a P4 host, esp_now_hosted.cpp DEFINES
|
|
||||||
# those symbols and forwards each call to the co-processor over esp-hosted's
|
|
||||||
# CustomRpc "peer data transfer" channel, so ESPHome's `espnow` component links
|
|
||||||
# and runs unchanged (proven on a Tab5, 2026-07-20). The .cpp is guarded to
|
|
||||||
# CONFIG_IDF_TARGET_ESP32P4 so it compiles to nothing on hosts with a native
|
|
||||||
# ESP-NOW stack. CustomRpc needs these two host-side Kconfig options. Host
|
|
||||||
# registers 3 handlers (RESP, RECV, SEND); the coprocessor registers 1 (REQ);
|
|
||||||
# we ask for 8 to leave room for other CustomRpc extensions alongside.
|
|
||||||
#
|
|
||||||
# The coprocessor must run the matching custom firmware (a parallel effort in
|
|
||||||
# esphome/esp-hosted-firmware). esp_now_hosted_rpc.h here is the canonical copy
|
|
||||||
# of the wire contract and MUST stay byte-identical to the copy that coprocessor
|
|
||||||
# firmware uses — the packed structs are the on-wire layout, so any divergence
|
|
||||||
# silently corrupts every ESP-NOW frame.
|
|
||||||
_MAX_CUSTOM_MSG_HANDLERS = 8
|
|
||||||
|
|
||||||
# Shared fields for both transport modes
|
# Shared fields for both transport modes
|
||||||
BASE_SCHEMA = cv.Schema(
|
BASE_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -281,23 +262,6 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
else:
|
else:
|
||||||
_configure_spi(config)
|
_configure_spi(config)
|
||||||
|
|
||||||
# ESP-NOW-over-hosted shim: only the radio-less ESP32-P4 host needs it (see
|
|
||||||
# the note by _MAX_CUSTOM_MSG_HANDLERS). Enabled for every P4 host, not
|
|
||||||
# gated on the `espnow` component being present: the shim is tiny and the
|
|
||||||
# esp_now_* symbols/CustomRpc calls it defines require these Kconfig options
|
|
||||||
# to link whenever esp_now_hosted.cpp compiles (which is on any P4 host), so
|
|
||||||
# coupling the two keeps the build consistent. When `espnow` is absent the
|
|
||||||
# symbols are simply unused and never register a callback at runtime.
|
|
||||||
if esp32.get_esp32_variant() == esp32.VARIANT_ESP32P4:
|
|
||||||
add_define("USE_ESP_NOW_HOSTED")
|
|
||||||
# esp-hosted's CustomRpc ("peer data transfer") path — off by default.
|
|
||||||
esp32.add_idf_sdkconfig_option(
|
|
||||||
"CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER", True
|
|
||||||
)
|
|
||||||
esp32.add_idf_sdkconfig_option(
|
|
||||||
"CONFIG_ESP_HOSTED_MAX_CUSTOM_MSG_HANDLERS", _MAX_CUSTOM_MSG_HANDLERS
|
|
||||||
)
|
|
||||||
|
|
||||||
# Place the transport mempool in PSRAM. Required on memory-tight host
|
# Place the transport mempool in PSRAM. Required on memory-tight host
|
||||||
# configurations (e.g. P4 with a large LVGL UI) where the internal-RAM
|
# configurations (e.g. P4 with a large LVGL UI) where the internal-RAM
|
||||||
# mempool allocation fails at boot with `sdio_mempool_create` assert.
|
# mempool allocation fails at boot with `sdio_mempool_create` assert.
|
||||||
|
|||||||
@@ -1,467 +0,0 @@
|
|||||||
/*
|
|
||||||
* esp_now_hosted — host-side shim implementing <esp_now.h> over esp-hosted
|
|
||||||
* CustomRpc, so ESPHome's `espnow` component can run on a radio-less host
|
|
||||||
* (e.g. the ESP32-P4) whose radio lives on an esp-hosted co-processor.
|
|
||||||
*
|
|
||||||
* A radio-less host has no native ESP-NOW. esp_wifi_remote INJECTS the full
|
|
||||||
* esp_now.h header (types + declarations) but ships NO implementation, so every
|
|
||||||
* esp_now_* symbol is an undefined reference at link time. This translation
|
|
||||||
* unit provides those definitions; each forwards to the co-processor over
|
|
||||||
* CustomRpc (see esphome/esp-hosted-firmware for the matching coprocessor
|
|
||||||
* handlers). No esp-hosted or esp_wifi_remote source is patched, and there is no
|
|
||||||
* duplicate-symbol clash because nothing else defines these symbols here.
|
|
||||||
*
|
|
||||||
* See esp_now_hosted_rpc.h for the wire protocol.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "sdkconfig.h"
|
|
||||||
|
|
||||||
// Only build the shim on the radio-less host. On chips with a native ESP-NOW
|
|
||||||
// stack (S3, C6, …) the real symbols exist and this file must stay empty to
|
|
||||||
// avoid duplicate definitions.
|
|
||||||
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/semphr.h"
|
|
||||||
|
|
||||||
#include "esp_idf_version.h"
|
|
||||||
#include "esp_log.h"
|
|
||||||
#include "esp_timer.h"
|
|
||||||
|
|
||||||
#include <esp_now.h> // injected declarations we are now DEFINING
|
|
||||||
#include <esp_wifi_types.h> // wifi_pkt_rx_ctrl_t, wifi_tx_info_t
|
|
||||||
|
|
||||||
// esp_hosted_misc.h (host) ships WITHOUT an extern "C" guard, so including it
|
|
||||||
// from C++ would give its declarations C++ linkage and the real C symbols in
|
|
||||||
// libesp_hosted would go unresolved at link. Wrap it. (Verified vs
|
|
||||||
// esp_hosted 2.12.9.)
|
|
||||||
extern "C" {
|
|
||||||
#include "esp_hosted_misc.h" // esp_hosted_{send_custom_data,register_custom_callback}
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "esp_now_hosted_rpc.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
const char *const TAG = "esp_now_hosted";
|
|
||||||
|
|
||||||
// One outstanding request at a time. ESPHome drives esp_now_* from the main
|
|
||||||
// loop; the matching response and the async RECV/SEND events all arrive on the
|
|
||||||
// single esp-hosted RPC RX thread. Serializing requests keeps the shared
|
|
||||||
// response slot race-free; a sequence number stops a late/stale response from
|
|
||||||
// being mistaken for ours.
|
|
||||||
SemaphoreHandle_t g_req_mutex = nullptr;
|
|
||||||
SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands
|
|
||||||
bool g_setup_done = false; // set only after setup fully succeeds
|
|
||||||
uint8_t g_seq = 0;
|
|
||||||
volatile uint8_t g_expect_seq = 0;
|
|
||||||
volatile int32_t g_resp_status = 0;
|
|
||||||
uint8_t g_resp_ret[16];
|
|
||||||
volatile uint16_t g_resp_ret_len = 0;
|
|
||||||
|
|
||||||
// Written from the main loop (register/unregister/deinit), read from the
|
|
||||||
// esp-hosted RX thread (on_recv/on_send). volatile for the same reason the
|
|
||||||
// g_resp_* globals are: force the RX thread to observe an updated pointer
|
|
||||||
// (e.g. a nulling by esp_now_deinit) rather than a cached one.
|
|
||||||
volatile esp_now_recv_cb_t g_recv_cb = nullptr;
|
|
||||||
volatile esp_now_send_cb_t g_send_cb = nullptr;
|
|
||||||
|
|
||||||
// Local mirror of the co-processor's peer table. ESPHome's espnow component
|
|
||||||
// calls esp_now_is_peer_exist() on the main loop for every received frame
|
|
||||||
// (twice) and every send; forwarding each as a blocking RPC round-trip stalls
|
|
||||||
// the loop. The shim is the only path that mutates the co-processor peer table
|
|
||||||
// (add/del/deinit all go through here), so this mirror is authoritative and
|
|
||||||
// esp_now_is_peer_exist() can answer from it with no round-trip.
|
|
||||||
//
|
|
||||||
// esp_now_* are public C symbols: any component or user lambda may call them,
|
|
||||||
// and although ESPHome's espnow touches peers only from the main loop today
|
|
||||||
// (its RX/TX callbacks merely enqueue), the shim cannot rely on that. A short
|
|
||||||
// spinlock keeps the mirror consistent from any task/core, matching native
|
|
||||||
// esp_now_*'s own internal thread-safety. The critical sections are a bounded
|
|
||||||
// (<=20-entry) scan, so they stay tiny. ESP_NOW_MAX_TOTAL_PEER_NUM is 20.
|
|
||||||
constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20;
|
|
||||||
uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6];
|
|
||||||
size_t g_peer_count = 0;
|
|
||||||
portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED;
|
|
||||||
|
|
||||||
// Caller must hold g_peer_lock.
|
|
||||||
int peer_cache_find_locked(const uint8_t *mac) {
|
|
||||||
for (size_t i = 0; i < g_peer_count; i++) {
|
|
||||||
if (memcmp(g_peer_cache[i], mac, 6) == 0)
|
|
||||||
return static_cast<int>(i);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peer_cache_contains(const uint8_t *mac) {
|
|
||||||
portENTER_CRITICAL(&g_peer_lock);
|
|
||||||
const bool found = peer_cache_find_locked(mac) >= 0;
|
|
||||||
portEXIT_CRITICAL(&g_peer_lock);
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
void peer_cache_add(const uint8_t *mac) {
|
|
||||||
portENTER_CRITICAL(&g_peer_lock);
|
|
||||||
if (peer_cache_find_locked(mac) < 0 && g_peer_count < ESP_NOW_HOSTED_MAX_PEERS)
|
|
||||||
memcpy(g_peer_cache[g_peer_count++], mac, 6);
|
|
||||||
portEXIT_CRITICAL(&g_peer_lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
void peer_cache_remove(const uint8_t *mac) {
|
|
||||||
portENTER_CRITICAL(&g_peer_lock);
|
|
||||||
const int idx = peer_cache_find_locked(mac);
|
|
||||||
if (idx >= 0) {
|
|
||||||
g_peer_count--;
|
|
||||||
if (static_cast<size_t>(idx) != g_peer_count) // move the last entry into the gap
|
|
||||||
memcpy(g_peer_cache[idx], g_peer_cache[g_peer_count], 6);
|
|
||||||
}
|
|
||||||
portEXIT_CRITICAL(&g_peer_lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
void peer_cache_clear() {
|
|
||||||
portENTER_CRITICAL(&g_peer_lock);
|
|
||||||
g_peer_count = 0;
|
|
||||||
portEXIT_CRITICAL(&g_peer_lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── CustomRpc event handlers (run on the esp-hosted RPC RX thread) ──────────
|
|
||||||
// Keep them short and non-blocking. In particular they MUST NOT call back into
|
|
||||||
// any esp_now_* shim function: that would try to take g_req_mutex / wait on the
|
|
||||||
// RX thread that delivers the response, and deadlock.
|
|
||||||
|
|
||||||
void on_resp(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) {
|
|
||||||
if (len < sizeof(esp_now_hosted_resp_t)) {
|
|
||||||
ESP_LOGW(TAG, "RESP too short: %u bytes", static_cast<unsigned>(len));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto *r = reinterpret_cast<const esp_now_hosted_resp_t *>(data);
|
|
||||||
if (r->seq != g_expect_seq) { // late response from a timed-out request (expected)
|
|
||||||
ESP_LOGV(TAG, "dropping stale RESP seq %u (want %u)", r->seq, g_expect_seq);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
g_resp_status = r->status;
|
|
||||||
uint16_t rl = r->ret_len;
|
|
||||||
if (rl > sizeof(g_resp_ret)) {
|
|
||||||
// Larger than any real opcode return — a likely wire-format drift signal.
|
|
||||||
ESP_LOGW(TAG, "RESP ret_len %u exceeds buffer, clamping (wire drift?)", rl);
|
|
||||||
rl = sizeof(g_resp_ret);
|
|
||||||
}
|
|
||||||
if (len >= sizeof(esp_now_hosted_resp_t) + rl) {
|
|
||||||
memcpy(g_resp_ret, r->ret, rl);
|
|
||||||
} else {
|
|
||||||
// Truncated frame: fail closed. Never hand the caller stale bytes left in
|
|
||||||
// g_resp_ret by a previous response, and don't let request() report a
|
|
||||||
// zeroed payload as success — override the status to an error.
|
|
||||||
ESP_LOGW(TAG, "RESP truncated: claims %u ret bytes, frame too short", rl);
|
|
||||||
rl = 0;
|
|
||||||
g_resp_status = ESP_ERR_INVALID_RESPONSE;
|
|
||||||
}
|
|
||||||
g_resp_ret_len = rl;
|
|
||||||
xSemaphoreGive(g_resp_sem);
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_recv(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) {
|
|
||||||
// Read the volatile pointer once: esp_now_unregister_recv_cb()/deinit() (via
|
|
||||||
// the espnow component's disable()) can null it on the main loop between the
|
|
||||||
// guard and the call, which would otherwise turn the call into a null-deref.
|
|
||||||
const esp_now_recv_cb_t cb = g_recv_cb;
|
|
||||||
if (cb == nullptr)
|
|
||||||
return;
|
|
||||||
if (len < sizeof(esp_now_hosted_recv_evt_t)) {
|
|
||||||
ESP_LOGW(TAG, "RECV too short: %u bytes", static_cast<unsigned>(len));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto *e = reinterpret_cast<const esp_now_hosted_recv_evt_t *>(data);
|
|
||||||
if (len < sizeof(esp_now_hosted_recv_evt_t) + e->data_len) {
|
|
||||||
ESP_LOGW(TAG, "RECV data_len %u exceeds frame", e->data_len);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ESPHome dereferences info->rx_ctrl->{rssi,timestamp}; give it a real one.
|
|
||||||
wifi_pkt_rx_ctrl_t rx_ctrl;
|
|
||||||
memset(&rx_ctrl, 0, sizeof(rx_ctrl));
|
|
||||||
rx_ctrl.rssi = e->rssi;
|
|
||||||
rx_ctrl.channel = e->channel;
|
|
||||||
rx_ctrl.timestamp = static_cast<uint32_t>(esp_timer_get_time());
|
|
||||||
|
|
||||||
esp_now_recv_info_t info;
|
|
||||||
info.src_addr = const_cast<uint8_t *>(e->src_addr);
|
|
||||||
info.des_addr = const_cast<uint8_t *>(e->des_addr);
|
|
||||||
info.rx_ctrl = &rx_ctrl;
|
|
||||||
cb(&info, e->data, static_cast<int>(e->data_len));
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_send(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) {
|
|
||||||
// Read the volatile pointer once (see on_recv): disable()/deinit() can null it
|
|
||||||
// on the main loop concurrently with this RX-thread callback.
|
|
||||||
const esp_now_send_cb_t cb = g_send_cb;
|
|
||||||
if (cb == nullptr)
|
|
||||||
return;
|
|
||||||
if (len < sizeof(esp_now_hosted_send_evt_t)) {
|
|
||||||
ESP_LOGW(TAG, "SEND evt too short: %u bytes", static_cast<unsigned>(len));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto *e = reinterpret_cast<const esp_now_hosted_send_evt_t *>(data);
|
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
|
||||||
// IDF >= 5.5: esp_now_send_cb_t takes esp_now_send_info_t (== wifi_tx_info_t),
|
|
||||||
// whose des_addr is a POINTER (not an inline array). Point it at the event's
|
|
||||||
// MAC (valid for this callback) — do NOT memcpy into it (that writes NULL and
|
|
||||||
// faults). ESPHome reads only info->des_addr.
|
|
||||||
esp_now_send_info_t si;
|
|
||||||
memset(&si, 0, sizeof(si));
|
|
||||||
si.des_addr = const_cast<uint8_t *>(e->des_addr);
|
|
||||||
cb(&si, static_cast<esp_now_send_status_t>(e->status));
|
|
||||||
#else
|
|
||||||
cb(e->des_addr, static_cast<esp_now_send_status_t>(e->status));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t ensure_setup() {
|
|
||||||
// Gate on g_setup_done, not on g_req_mutex: a failure part-way through (a
|
|
||||||
// semaphore that did not allocate, a callback that did not register) must not
|
|
||||||
// leave a later call thinking setup completed. Semaphore creation is guarded
|
|
||||||
// so a retry after a partial failure does not leak the earlier handles.
|
|
||||||
if (g_setup_done)
|
|
||||||
return ESP_OK;
|
|
||||||
if (g_req_mutex == nullptr)
|
|
||||||
g_req_mutex = xSemaphoreCreateMutex();
|
|
||||||
if (g_resp_sem == nullptr)
|
|
||||||
g_resp_sem = xSemaphoreCreateBinary();
|
|
||||||
if (g_req_mutex == nullptr || g_resp_sem == nullptr)
|
|
||||||
return ESP_ERR_NO_MEM;
|
|
||||||
esp_err_t err;
|
|
||||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK)
|
|
||||||
return err;
|
|
||||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK)
|
|
||||||
return err;
|
|
||||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK)
|
|
||||||
return err;
|
|
||||||
g_setup_done = true;
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send one request envelope. With wait=true (default) block until the matching
|
|
||||||
// response (or timeout); with wait=false return as soon as the frame is handed
|
|
||||||
// to the transport (fire-and-forget, used by esp_now_send).
|
|
||||||
//
|
|
||||||
// `tail` is an optional second chunk written straight after `payload`. Callers
|
|
||||||
// with a fixed header plus a bulk body (esp_now_send) pass the two separately
|
|
||||||
// so they never need a build buffer of their own: both chunks are laid into the
|
|
||||||
// request buffer here, under g_req_mutex, which keeps concurrent callers from
|
|
||||||
// racing and saves a full copy of the body on every transmit.
|
|
||||||
esp_err_t request(uint8_t opcode, const void *payload, uint16_t plen, void *ret, uint16_t ret_cap, uint16_t *ret_len,
|
|
||||||
bool wait = true, const void *tail = nullptr, uint16_t tail_len = 0) {
|
|
||||||
esp_err_t err = ensure_setup();
|
|
||||||
if (err != ESP_OK)
|
|
||||||
return err;
|
|
||||||
if (plen > ESP_NOW_HOSTED_MAX_PAYLOAD || tail_len > ESP_NOW_HOSTED_MAX_PAYLOAD - plen)
|
|
||||||
return ESP_ERR_INVALID_SIZE;
|
|
||||||
const uint16_t total_len = static_cast<uint16_t>(plen + tail_len);
|
|
||||||
|
|
||||||
if (xSemaphoreTake(g_req_mutex, portMAX_DELAY) != pdTRUE)
|
|
||||||
return ESP_FAIL;
|
|
||||||
|
|
||||||
static uint8_t buf[sizeof(esp_now_hosted_req_t) + ESP_NOW_HOSTED_MAX_PAYLOAD]; // guarded by g_req_mutex
|
|
||||||
auto *req = reinterpret_cast<esp_now_hosted_req_t *>(buf);
|
|
||||||
req->opcode = opcode;
|
|
||||||
req->seq = ++g_seq;
|
|
||||||
req->payload_len = total_len;
|
|
||||||
if (plen != 0)
|
|
||||||
memcpy(req->payload, payload, plen);
|
|
||||||
if (tail_len != 0)
|
|
||||||
memcpy(req->payload + plen, tail, tail_len);
|
|
||||||
g_expect_seq = req->seq;
|
|
||||||
|
|
||||||
xSemaphoreTake(g_resp_sem, 0); // drain any stale signal before sending
|
|
||||||
err = esp_hosted_send_custom_data(ESP_NOW_HOSTED_MSG_REQ, buf, sizeof(esp_now_hosted_req_t) + total_len);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
xSemaphoreGive(g_req_mutex);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
if (!wait) {
|
|
||||||
// Fire-and-forget (esp_now_send): the co-processor enqueues the frame and
|
|
||||||
// reports the real TX result later via the async SEND event, exactly like
|
|
||||||
// native esp_now_send. Returning here keeps the main loop off the ~100 ms+
|
|
||||||
// RPC round-trip. The matching RESP is ignored (seq won't match the next
|
|
||||||
// waited request, so on_resp drops it).
|
|
||||||
xSemaphoreGive(g_req_mutex);
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
if (xSemaphoreTake(g_resp_sem, pdMS_TO_TICKS(ESP_NOW_HOSTED_TIMEOUT_MS)) != pdTRUE) {
|
|
||||||
ESP_LOGW(TAG, "opcode %u timed out", opcode);
|
|
||||||
xSemaphoreGive(g_req_mutex);
|
|
||||||
return ESP_ERR_TIMEOUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int32_t status = g_resp_status;
|
|
||||||
if (ret != nullptr && ret_cap != 0) {
|
|
||||||
uint16_t n = g_resp_ret_len < ret_cap ? g_resp_ret_len : ret_cap;
|
|
||||||
memcpy(ret, const_cast<const uint8_t *>(g_resp_ret), n);
|
|
||||||
if (ret_len != nullptr)
|
|
||||||
*ret_len = n;
|
|
||||||
}
|
|
||||||
xSemaphoreGive(g_req_mutex);
|
|
||||||
return static_cast<esp_err_t>(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// ── The <esp_now.h> surface, defined for the radio-less host ────────────────
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
esp_err_t esp_now_init(void) { return request(ESP_NOW_HOSTED_OP_INIT, nullptr, 0, nullptr, 0, nullptr); }
|
|
||||||
|
|
||||||
esp_err_t esp_now_deinit(void) {
|
|
||||||
g_recv_cb = nullptr;
|
|
||||||
g_send_cb = nullptr;
|
|
||||||
peer_cache_clear(); // the co-processor drops all peers on deinit
|
|
||||||
return request(ESP_NOW_HOSTED_OP_DEINIT, nullptr, 0, nullptr, 0, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t esp_now_get_version(uint32_t *version) {
|
|
||||||
uint32_t v = 0;
|
|
||||||
uint16_t rl = 0;
|
|
||||||
esp_err_t err = request(ESP_NOW_HOSTED_OP_GET_VERSION, nullptr, 0, &v, sizeof(v), &rl);
|
|
||||||
if (version != nullptr)
|
|
||||||
*version = v;
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t esp_now_register_recv_cb(esp_now_recv_cb_t cb) {
|
|
||||||
// Only arm the callback once the CustomRpc handlers are actually registered,
|
|
||||||
// so a failed setup leaves g_recv_cb null rather than falsely "registered".
|
|
||||||
esp_err_t err = ensure_setup();
|
|
||||||
if (err != ESP_OK)
|
|
||||||
return err;
|
|
||||||
g_recv_cb = cb;
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
esp_err_t esp_now_unregister_recv_cb(void) {
|
|
||||||
g_recv_cb = nullptr;
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
esp_err_t esp_now_register_send_cb(esp_now_send_cb_t cb) {
|
|
||||||
esp_err_t err = ensure_setup();
|
|
||||||
if (err != ESP_OK)
|
|
||||||
return err;
|
|
||||||
g_send_cb = cb;
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
esp_err_t esp_now_unregister_send_cb(void) {
|
|
||||||
g_send_cb = nullptr;
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t add_or_mod_peer(uint8_t opcode, const esp_now_peer_info_t *peer, bool wait) {
|
|
||||||
if (peer == nullptr)
|
|
||||||
return ESP_ERR_ESPNOW_ARG;
|
|
||||||
esp_now_hosted_peer_t p;
|
|
||||||
memset(&p, 0, sizeof(p));
|
|
||||||
memcpy(p.peer_addr, peer->peer_addr, 6);
|
|
||||||
memcpy(p.lmk, peer->lmk, 16);
|
|
||||||
p.channel = peer->channel;
|
|
||||||
p.ifidx = static_cast<uint8_t>(peer->ifidx);
|
|
||||||
p.encrypt = peer->encrypt ? 1 : 0;
|
|
||||||
return request(opcode, &p, sizeof(p), nullptr, 0, nullptr, wait);
|
|
||||||
}
|
|
||||||
esp_err_t esp_now_add_peer(const esp_now_peer_info_t *peer) {
|
|
||||||
// Fire-and-forget (wait=false): adding a peer is a blocking RPC round-trip,
|
|
||||||
// and ESPHome's espnow calls it on the main loop when a device joins the mesh
|
|
||||||
// — under co-processor load that stalls the UI (peer-churn stutter). Issue it
|
|
||||||
// without waiting and mirror it locally. Safe against a following
|
|
||||||
// esp_now_send to the same peer: both ride the same in-order CustomRpc
|
|
||||||
// channel (mutex-serialized on the host) and the co-processor processes REQs
|
|
||||||
// FIFO, so ADD_PEER is applied before the SEND. Trade-off: a co-processor-side
|
|
||||||
// failure (e.g. peer table full) is no longer reported synchronously — the
|
|
||||||
// same limitation as esp_now_send — but ESPHome only adds peers it validated.
|
|
||||||
esp_err_t err = add_or_mod_peer(ESP_NOW_HOSTED_OP_ADD_PEER, peer, /*wait=*/false);
|
|
||||||
if (err == ESP_OK)
|
|
||||||
peer_cache_add(peer->peer_addr); // keep the local mirror in sync
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
esp_err_t esp_now_mod_peer(const esp_now_peer_info_t *peer) {
|
|
||||||
// mod_peer changes a peer's parameters, not its existence, so the cache is
|
|
||||||
// unaffected. Kept synchronous — it is not on any hot path (espnow never
|
|
||||||
// calls it), so the extra round-trip does not matter and the status is useful.
|
|
||||||
return add_or_mod_peer(ESP_NOW_HOSTED_OP_MOD_PEER, peer, /*wait=*/true);
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t esp_now_del_peer(const uint8_t *peer_addr) {
|
|
||||||
if (peer_addr == nullptr)
|
|
||||||
return ESP_ERR_ESPNOW_ARG;
|
|
||||||
// Fire-and-forget for the same reason as add_peer (peer churn on the main
|
|
||||||
// loop). Removal is order-independent, so this is strictly safe.
|
|
||||||
esp_err_t err = request(ESP_NOW_HOSTED_OP_DEL_PEER, peer_addr, 6, nullptr, 0, nullptr, /*wait=*/false);
|
|
||||||
if (err == ESP_OK)
|
|
||||||
peer_cache_remove(peer_addr); // keep the local mirror in sync
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool esp_now_is_peer_exist(const uint8_t *peer_addr) {
|
|
||||||
if (peer_addr == nullptr)
|
|
||||||
return false;
|
|
||||||
// Answered from the local mirror — no RPC round-trip. ESPHome's espnow calls
|
|
||||||
// this on the main loop for every received frame and every send, so a
|
|
||||||
// blocking round-trip here would stall rendering under mesh traffic.
|
|
||||||
return peer_cache_contains(peer_addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len) {
|
|
||||||
if (len > ESP_NOW_HOSTED_MAX_FRAME)
|
|
||||||
return ESP_ERR_ESPNOW_ARG;
|
|
||||||
if (data == nullptr && len != 0) // native esp_now_send treats this as an arg error
|
|
||||||
return ESP_ERR_ESPNOW_ARG;
|
|
||||||
// Only the small fixed header is built here; the caller's frame goes over as
|
|
||||||
// the request tail, so request() lays both into its own buffer under
|
|
||||||
// g_req_mutex. esp_now_send is a public C symbol and may be called from any
|
|
||||||
// task, and a shared build buffer here would let two callers corrupt each
|
|
||||||
// other's frame. Passing the body through also drops a full-frame copy per
|
|
||||||
// transmit, on the path this shim exists to keep quick.
|
|
||||||
uint8_t hdr[sizeof(esp_now_hosted_send_req_t)];
|
|
||||||
auto *s = reinterpret_cast<esp_now_hosted_send_req_t *>(hdr);
|
|
||||||
s->has_addr = peer_addr != nullptr ? 1 : 0;
|
|
||||||
if (peer_addr != nullptr)
|
|
||||||
memcpy(s->peer_addr, peer_addr, 6);
|
|
||||||
else
|
|
||||||
memset(s->peer_addr, 0, 6);
|
|
||||||
s->data_len = static_cast<uint16_t>(len);
|
|
||||||
// Fire-and-forget (wait=false): native esp_now_send returns once the frame is
|
|
||||||
// queued, with the real TX result delivered later through the send callback.
|
|
||||||
// The co-processor mirrors that — it acks enqueue immediately and reports the
|
|
||||||
// outcome via the async SEND event (on_send -> on_send_report). Waiting for
|
|
||||||
// the RPC RESP here would block the main loop for the full round-trip on
|
|
||||||
// every transmit.
|
|
||||||
return request(ESP_NOW_HOSTED_OP_SEND, hdr, sizeof(hdr), nullptr, 0, nullptr, /*wait=*/false, data,
|
|
||||||
static_cast<uint16_t>(len));
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t esp_now_set_pmk(const uint8_t *pmk) {
|
|
||||||
if (pmk == nullptr)
|
|
||||||
return ESP_ERR_ESPNOW_ARG;
|
|
||||||
return request(ESP_NOW_HOSTED_OP_SET_PMK, pmk, 16, nullptr, 0, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remainder of the <esp_now.h> surface. Not used by ESPHome's espnow component
|
|
||||||
// today; provided so the whole header links and future callers get a defined
|
|
||||||
// (if unimplemented) symbol rather than a link error. Wire them through
|
|
||||||
// CustomRpc if a use case appears.
|
|
||||||
esp_err_t esp_now_get_peer(const uint8_t * /*peer_addr*/, esp_now_peer_info_t * /*peer*/) {
|
|
||||||
return ESP_ERR_NOT_SUPPORTED;
|
|
||||||
}
|
|
||||||
esp_err_t esp_now_fetch_peer(bool /*from_head*/, esp_now_peer_info_t * /*peer*/) { return ESP_ERR_NOT_SUPPORTED; }
|
|
||||||
esp_err_t esp_now_get_peer_num(esp_now_peer_num_t * /*num*/) { return ESP_ERR_NOT_SUPPORTED; }
|
|
||||||
esp_err_t esp_now_set_wake_window(uint16_t /*window*/) {
|
|
||||||
return ESP_ERR_NOT_SUPPORTED; // power-save wake window is not forwarded; don't claim success
|
|
||||||
}
|
|
||||||
esp_err_t esp_now_set_peer_rate_config(const uint8_t * /*peer_addr*/, esp_now_rate_config_t * /*cfg*/) {
|
|
||||||
return ESP_ERR_NOT_SUPPORTED;
|
|
||||||
}
|
|
||||||
esp_err_t esp_wifi_config_espnow_rate(wifi_interface_t /*ifx*/, wifi_phy_rate_t /*rate*/) {
|
|
||||||
return ESP_ERR_NOT_SUPPORTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // extern "C"
|
|
||||||
|
|
||||||
#endif // CONFIG_IDF_TARGET_ESP32P4
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* esp_now_hosted — ESP-NOW-over-CustomRpc wire protocol.
|
|
||||||
*
|
|
||||||
* Shared, byte-for-byte-identical contract between:
|
|
||||||
* - the host shim (esphome/components/esp32_hosted/esp_now_hosted.cpp)
|
|
||||||
* - the coprocessor firmware (esphome/esp-hosted-firmware)
|
|
||||||
*
|
|
||||||
* It rides esp-hosted's CustomRpc channel (RPC ID 388, "peer data transfer",
|
|
||||||
* available since esp-hosted v2.8.1), teaching the radio-less host <-> radio
|
|
||||||
* co-processor link to carry esp_now.h, which esp-hosted itself does not proxy
|
|
||||||
* (Espressif issue espressif/esp-hosted-mcu#19).
|
|
||||||
*
|
|
||||||
* KEEP THE TWO COPIES IN SYNC. The canonical copy lives here; the coprocessor
|
|
||||||
* firmware uses a verbatim copy. Both sides are little-endian, so these packed
|
|
||||||
* structs are wire-compatible with no byte-swapping.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef ESP_NOW_HOSTED_RPC_H
|
|
||||||
#define ESP_NOW_HOSTED_RPC_H
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
#include <cstdint>
|
|
||||||
#else
|
|
||||||
#include <stdint.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ── CustomRpc message IDs (any uint32_t except 0xFFFFFFFF) ──────────────────
|
|
||||||
* One REQ handler slot on the device; three event handler slots on the host.
|
|
||||||
* The bytes spell "now" + index, a private range unlikely to clash with other
|
|
||||||
* CustomRpc users (e.g. the stock peer_data_transfer example's 1..6). */
|
|
||||||
#define ESP_NOW_HOSTED_MSG_REQ 0x6E6F7701u /* host -> device : request envelope */
|
|
||||||
#define ESP_NOW_HOSTED_MSG_RESP 0x6E6F7702u /* device -> host : reply to a REQ */
|
|
||||||
#define ESP_NOW_HOSTED_MSG_RECV 0x6E6F7703u /* device -> host : async RX frame */
|
|
||||||
#define ESP_NOW_HOSTED_MSG_SEND 0x6E6F7704u /* device -> host : async TX status */
|
|
||||||
|
|
||||||
/* ── Request opcodes ────────────────────────────────────────────────────── */
|
|
||||||
enum {
|
|
||||||
ESP_NOW_HOSTED_OP_INIT = 1, /* esp_now_init + register device recv/send cbs */
|
|
||||||
ESP_NOW_HOSTED_OP_DEINIT = 2, /* unregister cbs + esp_now_deinit */
|
|
||||||
ESP_NOW_HOSTED_OP_ADD_PEER = 3, /* payload: esp_now_hosted_peer_t */
|
|
||||||
ESP_NOW_HOSTED_OP_DEL_PEER = 4, /* payload: 6-byte peer MAC */
|
|
||||||
ESP_NOW_HOSTED_OP_IS_PEER_EXIST = 5, /* payload: 6-byte MAC; ret: 1 byte bool */
|
|
||||||
ESP_NOW_HOSTED_OP_SEND = 6, /* payload: esp_now_hosted_send_req_t */
|
|
||||||
ESP_NOW_HOSTED_OP_GET_VERSION = 7, /* ret: uint32 version */
|
|
||||||
ESP_NOW_HOSTED_OP_SET_PMK = 8, /* payload: 16-byte PMK */
|
|
||||||
ESP_NOW_HOSTED_OP_MOD_PEER = 9, /* payload: esp_now_hosted_peer_t */
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Largest ESP-NOW payload we forward. ESP-NOW v2 (IDF >= 5.4) is 1470 B; well
|
|
||||||
* under esp-hosted's 8166 B CustomRpc cap, so the shim never truncates. */
|
|
||||||
#define ESP_NOW_HOSTED_MAX_FRAME 1470u
|
|
||||||
/* Envelope slack for the largest opcode payload (a SEND req wrapping a frame). */
|
|
||||||
#define ESP_NOW_HOSTED_MAX_PAYLOAD (ESP_NOW_HOSTED_MAX_FRAME + 16u)
|
|
||||||
/* Host request/response round-trip timeout over the transport. Generous:
|
|
||||||
* normal RTT is sub-millisecond, but Wi-Fi/BLE contention on the co-processor
|
|
||||||
* can stall the RX thread. */
|
|
||||||
#define ESP_NOW_HOSTED_TIMEOUT_MS 2000
|
|
||||||
|
|
||||||
/* ── Envelopes ──────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
/* These payloads are shared verbatim with the C co-processor firmware, so they
|
|
||||||
* use C's `typedef struct {...} name;` idiom rather than C++ `using` aliases,
|
|
||||||
* which would not compile there. Silence clang-tidy's modernize-use-using for
|
|
||||||
* the shared struct block. */
|
|
||||||
// NOLINTBEGIN(modernize-use-using)
|
|
||||||
typedef struct {
|
|
||||||
uint8_t opcode; /* one of ESP_NOW_HOSTED_OP_* */
|
|
||||||
uint8_t seq; /* wraps 0..255; echoed in the response for matching */
|
|
||||||
uint16_t payload_len; /* bytes of opcode-specific payload that follow */
|
|
||||||
uint8_t payload[]; /* flexible */
|
|
||||||
} __attribute__((packed)) esp_now_hosted_req_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t opcode; /* echoes the request opcode */
|
|
||||||
uint8_t seq; /* echoes the request seq */
|
|
||||||
int32_t status; /* esp_err_t from the native call on the co-processor */
|
|
||||||
uint16_t ret_len; /* bytes of return payload that follow */
|
|
||||||
uint8_t ret[]; /* flexible (e.g. version u32, is_peer_exist bool) */
|
|
||||||
} __attribute__((packed)) esp_now_hosted_resp_t;
|
|
||||||
|
|
||||||
/* ── Opcode payloads ────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
/* esp_now_peer_info_t minus the host-only `priv` pointer, which is meaningless
|
|
||||||
* across the transport and never set by ESPHome's espnow component. */
|
|
||||||
typedef struct {
|
|
||||||
uint8_t peer_addr[6];
|
|
||||||
uint8_t lmk[16];
|
|
||||||
uint8_t channel; /* 0 = current channel */
|
|
||||||
uint8_t ifidx; /* wifi_interface_t (0=STA, 1=AP) */
|
|
||||||
uint8_t encrypt; /* bool */
|
|
||||||
} __attribute__((packed)) esp_now_hosted_peer_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t has_addr; /* 0 => peer_addr is NULL (broadcast to all peers) */
|
|
||||||
uint8_t peer_addr[6];
|
|
||||||
uint16_t data_len;
|
|
||||||
uint8_t data[]; /* flexible, up to ESP_NOW_HOSTED_MAX_FRAME */
|
|
||||||
} __attribute__((packed)) esp_now_hosted_send_req_t;
|
|
||||||
|
|
||||||
/* ── Async events (device -> host) ──────────────────────────────────────── */
|
|
||||||
|
|
||||||
/* Reconstructed on the host into an esp_now_recv_info_t + a minimal
|
|
||||||
* wifi_pkt_rx_ctrl_t. ESPHome's espnow reads info->src_addr, info->des_addr,
|
|
||||||
* info->rx_ctrl->rssi and info->rx_ctrl->timestamp. */
|
|
||||||
typedef struct {
|
|
||||||
uint8_t src_addr[6];
|
|
||||||
uint8_t des_addr[6];
|
|
||||||
int8_t rssi;
|
|
||||||
uint8_t channel;
|
|
||||||
uint16_t data_len;
|
|
||||||
uint8_t data[]; /* flexible */
|
|
||||||
} __attribute__((packed)) esp_now_hosted_recv_evt_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t des_addr[6];
|
|
||||||
uint8_t status; /* esp_now_send_status_t (0 = success) */
|
|
||||||
} __attribute__((packed)) esp_now_hosted_send_evt_t;
|
|
||||||
// NOLINTEND(modernize-use-using)
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* ESP_NOW_HOSTED_RPC_H */
|
|
||||||
@@ -112,7 +112,6 @@ void ESP32ImprovComponent::loop() {
|
|||||||
this->state_callback_.call(this->state_, this->error_state_);
|
this->state_callback_.call(this->state_, this->error_state_);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
this->release_advertising_();
|
|
||||||
this->incoming_data_.clear();
|
this->incoming_data_.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,9 +143,8 @@ void ESP32ImprovComponent::loop() {
|
|||||||
ESP_LOGV(TAG, "Starting with device name advertising");
|
ESP_LOGV(TAG, "Starting with device name advertising");
|
||||||
this->advertising_device_name_ = true;
|
this->advertising_device_name_ = true;
|
||||||
this->last_name_adv_time_ = App.get_loop_component_start_time();
|
this->last_name_adv_time_ = App.get_loop_component_start_time();
|
||||||
// Set the payload before requesting, so advertising starts exactly once
|
|
||||||
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
|
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
|
||||||
this->request_advertising_();
|
esp32_ble::global_ble->advertising_start();
|
||||||
|
|
||||||
// Set initial state based on whether we have an authorizer
|
// Set initial state based on whether we have an authorizer
|
||||||
this->set_state_(this->get_initial_state_(), false);
|
this->set_state_(this->get_initial_state_(), false);
|
||||||
@@ -328,8 +326,6 @@ void ESP32ImprovComponent::stop() {
|
|||||||
this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
|
this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
|
||||||
if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
|
if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
|
||||||
return;
|
return;
|
||||||
// Release first so removing the service UUID does not restart advertising on the way out
|
|
||||||
this->release_advertising_();
|
|
||||||
this->service_->stop();
|
this->service_->stop();
|
||||||
this->set_state_(improv::STATE_STOPPED);
|
this->set_state_(improv::STATE_STOPPED);
|
||||||
});
|
});
|
||||||
@@ -524,20 +520,6 @@ void ESP32ImprovComponent::update_advertising_type_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32ImprovComponent::request_advertising_() {
|
|
||||||
if (this->advertising_requested_)
|
|
||||||
return;
|
|
||||||
this->advertising_requested_ = true;
|
|
||||||
esp32_ble::global_ble->advertising_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ESP32ImprovComponent::release_advertising_() {
|
|
||||||
if (!this->advertising_requested_)
|
|
||||||
return;
|
|
||||||
this->advertising_requested_ = false;
|
|
||||||
esp32_ble::global_ble->advertising_stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
improv::State ESP32ImprovComponent::get_initial_state_() const {
|
improv::State ESP32ImprovComponent::get_initial_state_() const {
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
// If we have an authorizer, start in awaiting authorization state
|
// If we have an authorizer, start in awaiting authorization state
|
||||||
|
|||||||
@@ -104,11 +104,8 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
|
|||||||
bool status_indicator_state_{false};
|
bool status_indicator_state_{false};
|
||||||
uint32_t last_name_adv_time_{0};
|
uint32_t last_name_adv_time_{0};
|
||||||
bool advertising_device_name_{false};
|
bool advertising_device_name_{false};
|
||||||
bool advertising_requested_{false};
|
|
||||||
void set_status_indicator_state_(bool state);
|
void set_status_indicator_state_(bool state);
|
||||||
void update_advertising_type_();
|
void update_advertising_type_();
|
||||||
void request_advertising_();
|
|
||||||
void release_advertising_();
|
|
||||||
|
|
||||||
void set_state_(improv::State state, bool update_advertising = true);
|
void set_state_(improv::State state, bool update_advertising = true);
|
||||||
void set_error_(improv::Error error);
|
void set_error_(improv::Error error);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script
|
|||||||
from esphome.storage_json import StorageJSON
|
from esphome.storage_json import StorageJSON
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
from .boards import BOARDS, board_ld_script
|
from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_EARLY_PIN_INIT,
|
CONF_EARLY_PIN_INIT,
|
||||||
CONF_ENABLE_SERIAL,
|
CONF_ENABLE_SERIAL,
|
||||||
@@ -43,6 +43,8 @@ from .const import (
|
|||||||
CONF_RESTORE_FROM_FLASH,
|
CONF_RESTORE_FROM_FLASH,
|
||||||
KEY_BOARD,
|
KEY_BOARD,
|
||||||
KEY_ESP8266,
|
KEY_ESP8266,
|
||||||
|
KEY_FLASH_SIZE,
|
||||||
|
KEY_LDSCRIPT,
|
||||||
KEY_PIN_INITIAL_STATES,
|
KEY_PIN_INITIAL_STATES,
|
||||||
KEY_SERIAL1_REQUIRED,
|
KEY_SERIAL1_REQUIRED,
|
||||||
KEY_SERIAL_REQUIRED,
|
KEY_SERIAL_REQUIRED,
|
||||||
@@ -131,6 +133,10 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
|||||||
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to
|
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to
|
||||||
# a PIO platformio/framework-arduinoespressif8266 value
|
# a PIO platformio/framework-arduinoespressif8266 value
|
||||||
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
||||||
|
if ver <= cv.Version(2, 4, 1):
|
||||||
|
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||||
|
if ver <= cv.Version(2, 6, 2):
|
||||||
|
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||||
# Same encoding the native toolchain uses for its package download, so a
|
# Same encoding the native toolchain uses for its package download, so a
|
||||||
# version bump cannot drift between the two paths.
|
# version bump cannot drift between the two paths.
|
||||||
from esphome.arduino8266.framework import framework_package_version
|
from esphome.arduino8266.framework import framework_package_version
|
||||||
@@ -153,9 +159,11 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
|||||||
# - https://github.com/esp8266/Arduino/releases
|
# - https://github.com/esp8266/Arduino/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
||||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
|
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
|
||||||
# The platformio/espressif8266 version to use for arduino 3 framework versions
|
# The platformio/espressif8266 version to use for arduino 2 framework versions
|
||||||
# - https://github.com/platformio/platform-espressif8266/releases
|
# - https://github.com/platformio/platform-espressif8266/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
|
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
|
||||||
|
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
|
||||||
|
# for arduino 3 framework versions
|
||||||
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
|
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
|
||||||
# for arduino 4 framework versions
|
# for arduino 4 framework versions
|
||||||
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
|
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
|
||||||
@@ -180,14 +188,6 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
|||||||
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
||||||
source = value.get(CONF_SOURCE, None)
|
source = value.get(CONF_SOURCE, None)
|
||||||
|
|
||||||
if version < cv.Version(3, 0, 0):
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"Arduino framework {version} is no longer supported; ESPHome requires "
|
|
||||||
f"C++20, which needs Arduino core 3.x. Use the recommended version "
|
|
||||||
f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).",
|
|
||||||
path=[CONF_VERSION],
|
|
||||||
)
|
|
||||||
|
|
||||||
value[CONF_VERSION] = str(version)
|
value[CONF_VERSION] = str(version)
|
||||||
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
|
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
|
||||||
|
|
||||||
@@ -195,8 +195,12 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
|||||||
if platform_version is None:
|
if platform_version is None:
|
||||||
if version >= cv.Version(3, 1, 0):
|
if version >= cv.Version(3, 1, 0):
|
||||||
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
|
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
|
||||||
else:
|
elif version >= cv.Version(3, 0, 0):
|
||||||
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
|
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
|
||||||
|
elif version >= cv.Version(2, 5, 0):
|
||||||
|
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))
|
||||||
|
else:
|
||||||
|
platform_version = _parse_platform_version(str(cv.Version(1, 8, 0)))
|
||||||
value[CONF_PLATFORM_VERSION] = platform_version
|
value[CONF_PLATFORM_VERSION] = platform_version
|
||||||
|
|
||||||
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
|
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
|
||||||
@@ -285,11 +289,29 @@ def check_rosetta() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _choose_ld_script(board: str) -> str:
|
def _choose_ld_script(board: str, ver: cv.Version) -> str | None:
|
||||||
"""The flash ld to pin for this board."""
|
"""The flash ld to pin for this board and core, or None for cores
|
||||||
|
without ld-script support."""
|
||||||
|
board_data = BOARDS[board]
|
||||||
|
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
|
||||||
|
if ver <= cv.Version(2, 3, 0):
|
||||||
|
# No ld script support
|
||||||
|
return None
|
||||||
|
if ver <= cv.Version(2, 4, 2):
|
||||||
|
# Old ld script path; the modern per-board override names do not
|
||||||
|
# exist in this core's SDK, so the override cannot be honored.
|
||||||
|
# Substituting the size default would move _FS_end and the
|
||||||
|
# preferences sector, wiping flash-backed state on flash.
|
||||||
|
if KEY_LDSCRIPT in board_data:
|
||||||
|
raise EsphomeError(
|
||||||
|
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
|
||||||
|
f"flash layout, which Arduino core {ver} cannot honor; "
|
||||||
|
"use a core newer than 2.4.2"
|
||||||
|
)
|
||||||
|
return ld_scripts[0]
|
||||||
# A per-board override preserves a layout the board shipped with
|
# A per-board override preserves a layout the board shipped with
|
||||||
# (see d1_wroom_02 in boards.py)
|
# (see d1_wroom_02 in boards.py)
|
||||||
return board_ld_script(BOARDS[board])
|
return board_ld_script(board_data)
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||||
@@ -413,9 +435,10 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if config[CONF_BOARD] in BOARDS:
|
if config[CONF_BOARD] in BOARDS:
|
||||||
cg.add_platformio_option(
|
ld_script = _choose_ld_script(config[CONF_BOARD], ver)
|
||||||
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
|
|
||||||
)
|
if ld_script is not None:
|
||||||
|
cg.add_platformio_option("board_build.ldscript", ld_script)
|
||||||
|
|
||||||
CORE.add_job(add_pin_initial_states_array)
|
CORE.add_job(add_pin_initial_states_array)
|
||||||
CORE.add_job(finalize_waveform_config)
|
CORE.add_job(finalize_waveform_config)
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.noise import (
|
|
||||||
encryption_schema,
|
|
||||||
new_psk_progmem,
|
|
||||||
static_encryption_key,
|
|
||||||
)
|
|
||||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
||||||
from esphome.config_helpers import filter_source_files_from_defines, merge_config
|
from esphome.config_helpers import merge_config
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_API,
|
|
||||||
CONF_ENCRYPTION,
|
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_KEY,
|
|
||||||
CONF_NUM_ATTEMPTS,
|
CONF_NUM_ATTEMPTS,
|
||||||
CONF_OTA,
|
CONF_OTA,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
@@ -23,7 +15,6 @@ from esphome.const import (
|
|||||||
CONF_REBOOT_TIMEOUT,
|
CONF_REBOOT_TIMEOUT,
|
||||||
CONF_SAFE_MODE,
|
CONF_SAFE_MODE,
|
||||||
CONF_VERSION,
|
CONF_VERSION,
|
||||||
CONF_WEB_SERVER,
|
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.coroutine import CoroPriority
|
from esphome.coroutine import CoroPriority
|
||||||
@@ -39,14 +30,7 @@ CODEOWNERS = ["@esphome/core"]
|
|||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
|
|
||||||
|
|
||||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
AUTO_LOAD = ["sha256", "socket"]
|
||||||
"""Auto-load noise only when encryption is configured; the api key offer
|
|
||||||
inherits it from the api component."""
|
|
||||||
base = ["sha256", "socket"]
|
|
||||||
# A falsy config is a tooling probe for the maximal set
|
|
||||||
if not config or CONF_ENCRYPTION in config:
|
|
||||||
return base + ["noise"]
|
|
||||||
return base
|
|
||||||
|
|
||||||
|
|
||||||
esphome = cg.esphome_ns.namespace("esphome")
|
esphome = cg.esphome_ns.namespace("esphome")
|
||||||
@@ -83,24 +67,11 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
|||||||
CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port]
|
CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port]
|
||||||
and CONF_PASSWORD in ota_conf
|
and CONF_PASSWORD in ota_conf
|
||||||
and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD]
|
and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD]
|
||||||
!= ota_conf[CONF_PASSWORD]
|
!= ota_conf.get(CONF_PASSWORD)
|
||||||
):
|
):
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Found multiple configurations but {CONF_PASSWORD} is inconsistent"
|
f"Found multiple configurations but {CONF_PASSWORD} is inconsistent"
|
||||||
)
|
)
|
||||||
# Encryption blocks conflict only when both pin a key; a bare
|
|
||||||
# `encryption:` (a package/device split) is compatible with a
|
|
||||||
# keyed one, and merge_config yields the keyed result
|
|
||||||
merged_key = (
|
|
||||||
merged_ota_esphome_configs_by_port[conf_port]
|
|
||||||
.get(CONF_ENCRYPTION, {})
|
|
||||||
.get(CONF_KEY)
|
|
||||||
)
|
|
||||||
other_key = ota_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
|
|
||||||
if merged_key and other_key and merged_key != other_key:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"Found multiple configurations but {CONF_ENCRYPTION} is inconsistent"
|
|
||||||
)
|
|
||||||
|
|
||||||
ports_with_merged_configs.append(conf_port)
|
ports_with_merged_configs.append(conf_port)
|
||||||
merged_ota_esphome_configs_by_port[conf_port] = merge_config(
|
merged_ota_esphome_configs_by_port[conf_port] = merge_config(
|
||||||
@@ -123,64 +94,6 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
|||||||
|
|
||||||
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
|
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
|
||||||
|
|
||||||
api_conf = full_conf.get(CONF_API) or {}
|
|
||||||
for ota_conf in merged_ota_esphome_configs_by_port.values():
|
|
||||||
# Merging same-port blocks can combine a password from one block with
|
|
||||||
# encryption from another; re-check the exclusion on the merged result.
|
|
||||||
_validate_no_password_with_encryption(ota_conf)
|
|
||||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
|
||||||
_resolve_encryption_key(encryption_conf, api_conf)
|
|
||||||
elif CONF_PASSWORD in ota_conf and static_encryption_key(api_conf) is not None:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"'%s' %s wastes significant flash and RAM (about 3.5 KB and 60 "
|
|
||||||
"bytes plus the password on the heap): the device already offers "
|
|
||||||
"encryption with the '%s' %s %s, which authenticates any uploader "
|
|
||||||
"that takes it, and a password only matters for uploaders without "
|
|
||||||
"encryption support; remove '%s' and add '%s' under '%s' so "
|
|
||||||
"uploads use the key and encryption is required",
|
|
||||||
CONF_OTA,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_API,
|
|
||||||
CONF_ENCRYPTION,
|
|
||||||
CONF_KEY,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_ENCRYPTION,
|
|
||||||
CONF_OTA,
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
CONF_PASSWORD in ota_conf
|
|
||||||
and CONF_ENCRYPTION in api_conf
|
|
||||||
and not api_conf[CONF_ENCRYPTION].get(CONF_KEY)
|
|
||||||
):
|
|
||||||
# The CLI still needs the password; whoever provisions the key skips it
|
|
||||||
_LOGGER.warning(
|
|
||||||
"The '%s' %s %s provisioned at runtime also authenticates OTA "
|
|
||||||
"uploads once provisioned; '%s' %s then only guards plaintext "
|
|
||||||
"uploads. Whoever provisions the key can upload firmware "
|
|
||||||
"without the password, so add a 'provisioning:' block to limit "
|
|
||||||
"when that is possible",
|
|
||||||
CONF_API,
|
|
||||||
CONF_ENCRYPTION,
|
|
||||||
CONF_KEY,
|
|
||||||
CONF_OTA,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
)
|
|
||||||
# web_server and prometheus keep the shared listener up; the captive
|
|
||||||
# portal's copy only exists on the fallback AP and is the recovery path
|
|
||||||
if (
|
|
||||||
(CONF_WEB_SERVER in full_conf or "prometheus" in full_conf)
|
|
||||||
and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf)
|
|
||||||
and any(
|
|
||||||
CONF_ENCRYPTION in conf
|
|
||||||
for conf in merged_ota_esphome_configs_by_port.values()
|
|
||||||
)
|
|
||||||
):
|
|
||||||
_LOGGER.warning(
|
|
||||||
"OTA encryption does not cover the %s OTA platform; its "
|
|
||||||
"plaintext /update endpoint accepts the same image",
|
|
||||||
CONF_WEB_SERVER,
|
|
||||||
)
|
|
||||||
|
|
||||||
full_conf[CONF_OTA] = new_ota_conf
|
full_conf[CONF_OTA] = new_ota_conf
|
||||||
fv.full_config.set(full_conf)
|
fv.full_config.set(full_conf)
|
||||||
|
|
||||||
@@ -194,46 +107,6 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -> None:
|
|
||||||
"""Resolve the one encryption key per device into the ota block.
|
|
||||||
|
|
||||||
An explicit ota key must match the api key, a bare block inherits it,
|
|
||||||
a runtime provisioned api key cannot be inherited.
|
|
||||||
"""
|
|
||||||
api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
|
|
||||||
if ota_key := encryption_conf.get(CONF_KEY):
|
|
||||||
if api_key and ota_key != api_key:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY} must match the "
|
|
||||||
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY}; omit the "
|
|
||||||
f"'{CONF_OTA}' {CONF_KEY} to use the '{CONF_API}' one"
|
|
||||||
)
|
|
||||||
elif not api_key:
|
|
||||||
if CONF_ENCRYPTION in api_conf:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"the '{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} is provisioned at "
|
|
||||||
f"runtime and cannot be inherited at build time; set an explicit "
|
|
||||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY}"
|
|
||||||
)
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} has no {CONF_KEY} and there is no "
|
|
||||||
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} to inherit; set one of them"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
encryption_conf[CONF_KEY] = api_key
|
|
||||||
|
|
||||||
|
|
||||||
# Also called on merged same-port configs in final validate, where schemas
|
|
||||||
# do not run
|
|
||||||
def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType:
|
|
||||||
if CONF_PASSWORD in config and CONF_ENCRYPTION in config:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the "
|
|
||||||
f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}'"
|
|
||||||
)
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
||||||
"""Register socket needs for OTA component."""
|
"""Register socket needs for OTA component."""
|
||||||
from esphome.components import socket
|
from esphome.components import socket
|
||||||
@@ -261,7 +134,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
): cv.port,
|
): cv.port,
|
||||||
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
|
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_PASSWORD): cv.sensitive(),
|
cv.Optional(CONF_PASSWORD): cv.sensitive(),
|
||||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
|
||||||
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
|
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
|
||||||
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
|
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
|
||||||
),
|
),
|
||||||
@@ -275,18 +147,12 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
)
|
)
|
||||||
.extend(BASE_OTA_SCHEMA)
|
.extend(BASE_OTA_SCHEMA)
|
||||||
.extend(cv.COMPONENT_SCHEMA),
|
.extend(cv.COMPONENT_SCHEMA),
|
||||||
_validate_no_password_with_encryption,
|
|
||||||
_consume_ota_sockets,
|
_consume_ota_sockets,
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
||||||
|
|
||||||
|
|
||||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
|
||||||
{"ota_esphome_noise.cpp": "USE_OTA_ENCRYPTION"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
|
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
@@ -305,25 +171,6 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
if config.get(CONF_ALLOW_PARTITION_ACCESS):
|
if config.get(CONF_ALLOW_PARTITION_ACCESS):
|
||||||
cg.add_define("USE_OTA_PARTITIONS")
|
cg.add_define("USE_OTA_PARTITIONS")
|
||||||
|
|
||||||
# One key per device: an api encryption block supplies it (static or
|
|
||||||
# runtime) and offers; the ota block only adds the requirement
|
|
||||||
api_conf = CORE.config.get(CONF_API) or {}
|
|
||||||
encryption_conf = config.get(CONF_ENCRYPTION)
|
|
||||||
own_key = None
|
|
||||||
if encryption_conf is not None and static_encryption_key(api_conf) is None:
|
|
||||||
own_key = encryption_conf[CONF_KEY]
|
|
||||||
if own_key is not None:
|
|
||||||
cg.add_define("USE_OTA_ENCRYPTION")
|
|
||||||
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], own_key)))
|
|
||||||
elif CONF_ENCRYPTION in api_conf:
|
|
||||||
cg.add_define("USE_OTA_ENCRYPTION")
|
|
||||||
cg.add_define("USE_OTA_ENCRYPTION_FROM_API")
|
|
||||||
if static_encryption_key(api_conf) is None:
|
|
||||||
# The key arrives at runtime, so the offer has to look for it
|
|
||||||
cg.add_define("USE_OTA_ENCRYPTION_PROVISIONED")
|
|
||||||
if encryption_conf is not None:
|
|
||||||
cg.add_define("USE_OTA_ENCRYPTION_REQUIRED")
|
|
||||||
|
|
||||||
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
|
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
|
||||||
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
|
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
#include "ota_esphome.h"
|
#include "ota_esphome.h"
|
||||||
#ifdef USE_OTA_ENCRYPTION_FROM_API
|
|
||||||
#include "esphome/components/api/api_server.h"
|
|
||||||
#endif
|
|
||||||
#ifdef USE_OTA
|
#ifdef USE_OTA
|
||||||
#ifdef USE_OTA_PASSWORD
|
#ifdef USE_OTA_PASSWORD
|
||||||
#include "esphome/components/sha256/sha256.h"
|
#include "esphome/components/sha256/sha256.h"
|
||||||
@@ -29,17 +26,8 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
static const char *const TAG = "esphome.ota";
|
static const char *const TAG = "esphome.ota";
|
||||||
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
|
|
||||||
#ifdef USE_OTA_ENCRYPTION_FROM_API
|
|
||||||
return api::global_api_server->get_noise_ctx();
|
|
||||||
#else
|
|
||||||
return this->noise_ctx_;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
|
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
|
||||||
|
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
|
||||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
|
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
|
||||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
|
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
|
||||||
|
|
||||||
@@ -110,25 +98,8 @@ void ESPHomeOTAComponent::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
"Over-The-Air updates:\n"
|
"Over-The-Air updates:\n"
|
||||||
" Address: %s:%u\n"
|
" Address: %s:%u\n"
|
||||||
" Version: %d"
|
" Version: %d",
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION);
|
||||||
"\n Encryption: %s"
|
|
||||||
#endif
|
|
||||||
,
|
|
||||||
network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION
|
|
||||||
#ifdef USE_OTA_ENCRYPTION_REQUIRED
|
|
||||||
,
|
|
||||||
LOG_STR_LITERAL("required")
|
|
||||||
#elif defined(USE_OTA_ENCRYPTION_PROVISIONED)
|
|
||||||
// A runtime provisioned key may not exist yet
|
|
||||||
,
|
|
||||||
this->noise_context_().has_psk() ? LOG_STR_LITERAL("offered, plaintext accepted")
|
|
||||||
: LOG_STR_LITERAL("offered once the api key is provisioned")
|
|
||||||
#elif defined(USE_OTA_ENCRYPTION)
|
|
||||||
,
|
|
||||||
LOG_STR_LITERAL("offered, plaintext accepted")
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
#ifdef USE_OTA_PASSWORD
|
#ifdef USE_OTA_PASSWORD
|
||||||
if (!this->password_.empty()) {
|
if (!this->password_.empty()) {
|
||||||
ESP_LOGCONFIG(TAG, " Password configured");
|
ESP_LOGCONFIG(TAG, " Password configured");
|
||||||
@@ -178,22 +149,8 @@ void ESPHomeOTAComponent::loop() {
|
|||||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
||||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
|
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
|
||||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
|
|
||||||
// Noise needs the extended protocol: the prologue binds the 2-byte feature ack
|
|
||||||
static constexpr uint8_t CLIENT_NOISE_FEATURES =
|
|
||||||
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
|
|
||||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
|
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
|
||||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
|
|
||||||
|
|
||||||
inline bool ESPHomeOTAComponent::extended_proto_() const {
|
|
||||||
#ifdef USE_OTA_ENCRYPTION_REQUIRED
|
|
||||||
// FEATURE_READ already refused every client without the extended protocol
|
|
||||||
return true;
|
|
||||||
#else
|
|
||||||
return (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void ESPHomeOTAComponent::handle_handshake_() {
|
void ESPHomeOTAComponent::handle_handshake_() {
|
||||||
/// Handle the OTA handshake and authentication.
|
/// Handle the OTA handshake and authentication.
|
||||||
@@ -245,7 +202,8 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate magic bytes
|
// Validate magic bytes
|
||||||
if (memcmp(this->handshake_buf_, MAGIC_BYTES, sizeof(MAGIC_BYTES)) != 0) {
|
static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
|
||||||
|
if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
|
||||||
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
|
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
|
||||||
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
|
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
|
||||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
|
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
|
||||||
@@ -277,16 +235,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
|||||||
}
|
}
|
||||||
this->ota_features_ = this->handshake_buf_[0];
|
this->ota_features_ = this->handshake_buf_[0];
|
||||||
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
|
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
|
||||||
|
|
||||||
#ifdef USE_OTA_ENCRYPTION_REQUIRED
|
|
||||||
// `ota: encryption:` requires the client to negotiate encryption
|
|
||||||
if ((this->ota_features_ & CLIENT_NOISE_FEATURES) != CLIENT_NOISE_FEATURES) {
|
|
||||||
ESP_LOGW(TAG, "Client does not support encryption");
|
|
||||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
this->transition_ota_state_(OTAState::FEATURE_ACK);
|
this->transition_ota_state_(OTAState::FEATURE_ACK);
|
||||||
|
|
||||||
const bool supports_compression =
|
const bool supports_compression =
|
||||||
@@ -295,21 +243,13 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
|||||||
// Compose the feature-ack response. When the client negotiates the extended protocol we emit
|
// Compose the feature-ack response. When the client negotiates the extended protocol we emit
|
||||||
// a 2-byte response (marker + server feature flags); otherwise we emit the single-byte
|
// a 2-byte response (marker + server feature flags); otherwise we emit the single-byte
|
||||||
// legacy response.
|
// legacy response.
|
||||||
if (this->extended_proto_()) {
|
this->extended_proto_ = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0;
|
||||||
|
if (this->extended_proto_) {
|
||||||
static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
|
static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
|
||||||
this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
||||||
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
|
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
|
||||||
#ifdef USE_OTA_PARTITIONS
|
#ifdef USE_OTA_PARTITIONS
|
||||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
|
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
|
||||||
#endif
|
|
||||||
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
|
|
||||||
// A runtime provisioned key may not exist yet
|
|
||||||
if (this->noise_context_().has_psk()) {
|
|
||||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
|
|
||||||
}
|
|
||||||
#elif defined(USE_OTA_ENCRYPTION)
|
|
||||||
// A yaml key always exists: validation rejects the all-zeros key
|
|
||||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
|
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
this->handshake_buf_[0] =
|
this->handshake_buf_[0] =
|
||||||
@@ -321,24 +261,10 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
|||||||
case OTAState::FEATURE_ACK: {
|
case OTAState::FEATURE_ACK: {
|
||||||
static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1;
|
static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1;
|
||||||
static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2;
|
static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2;
|
||||||
const size_t ack_size = this->extended_proto_() ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE;
|
const size_t ack_size = this->extended_proto_ ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE;
|
||||||
if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
|
if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
// Latch the offer actually sent: a key activating between the two
|
|
||||||
// states must not start a session the client never expects
|
|
||||||
if ((this->handshake_buf_[1] & SERVER_FEATURE_SUPPORTS_NOISE) != 0 &&
|
|
||||||
(this->ota_features_ & CLIENT_NOISE_FEATURES) == CLIENT_NOISE_FEATURES) {
|
|
||||||
// handshake_buf_ still holds the feature ack composed above; a
|
|
||||||
// would-block re-entry lands here without rebuilding it
|
|
||||||
if (!this->noise_start_session_(this->handshake_buf_[1])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this->transition_ota_state_(OTAState::NOISE_HANDSHAKE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#ifdef USE_OTA_PASSWORD
|
#ifdef USE_OTA_PASSWORD
|
||||||
// If password is set, move to auth phase
|
// If password is set, move to auth phase
|
||||||
if (!this->password_.empty()) {
|
if (!this->password_.empty()) {
|
||||||
@@ -376,16 +302,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
|||||||
this->handle_data_();
|
this->handle_data_();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
case OTAState::NOISE_HANDSHAKE:
|
|
||||||
if (!this->handle_noise_handshake_()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this->transition_ota_state_(OTAState::DATA);
|
|
||||||
this->handle_data_();
|
|
||||||
return;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -424,8 +340,6 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
/// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses
|
/// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses
|
||||||
/// wakeable_delay() in read();
|
/// wakeable_delay() in read();
|
||||||
/// write() always returns immediately
|
/// write() always returns immediately
|
||||||
// Backend calls overwrite this with OK; reset to UNKNOWN before any
|
|
||||||
// goto error that follows a successful begin()/write()
|
|
||||||
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||||
size_t total = 0;
|
size_t total = 0;
|
||||||
uint32_t last_progress = 0;
|
uint32_t last_progress = 0;
|
||||||
@@ -444,14 +358,17 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
tv.tv_usec = 0;
|
tv.tv_usec = 0;
|
||||||
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||||
this->client_->setblocking(true);
|
if (this->client_->setblocking(true) != 0) {
|
||||||
|
this->log_socket_error_(LOG_STR("blocking"));
|
||||||
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
|
}
|
||||||
|
|
||||||
// Acknowledge auth OK - 1 byte
|
// Acknowledge auth OK - 1 byte
|
||||||
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||||
|
|
||||||
if (this->extended_proto_()) {
|
if (this->extended_proto_) {
|
||||||
// Read ota type, 1 byte
|
// Read ota type, 1 byte
|
||||||
if (!this->data_readall_(buf, 1)) {
|
if (!this->readall_(buf, 1)) {
|
||||||
this->log_read_error_(LOG_STR("OTA type"));
|
this->log_read_error_(LOG_STR("OTA type"));
|
||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
}
|
}
|
||||||
@@ -460,7 +377,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
|
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
|
||||||
|
|
||||||
// Read size, 4 bytes MSB first
|
// Read size, 4 bytes MSB first
|
||||||
if (!this->data_readall_(buf, 4)) {
|
if (!this->readall_(buf, 4)) {
|
||||||
this->log_read_error_(LOG_STR("size"));
|
this->log_read_error_(LOG_STR("size"));
|
||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
}
|
}
|
||||||
@@ -491,12 +408,11 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
|
|
||||||
// Acknowledge prepare OK - 1 byte
|
// Acknowledge prepare OK - 1 byte
|
||||||
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
||||||
|
|
||||||
// Read binary MD5, 32 bytes
|
// Read binary MD5, 32 bytes
|
||||||
if (!this->data_readall_(buf, 32)) {
|
if (!this->readall_(buf, 32)) {
|
||||||
this->log_read_error_(LOG_STR("MD5 checksum"));
|
this->log_read_error_(LOG_STR("MD5 checksum"));
|
||||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
|
||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
}
|
}
|
||||||
sbuf[32] = '\0';
|
sbuf[32] = '\0';
|
||||||
@@ -504,7 +420,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
this->backend_->set_update_md5(sbuf);
|
this->backend_->set_update_md5(sbuf);
|
||||||
|
|
||||||
// Acknowledge MD5 OK - 1 byte
|
// Acknowledge MD5 OK - 1 byte
|
||||||
this->data_write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||||
|
|
||||||
// Track when we last received data so a silently-vanished peer (no FIN/RST
|
// Track when we last received data so a silently-vanished peer (no FIN/RST
|
||||||
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
|
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
|
||||||
@@ -520,20 +436,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
}
|
}
|
||||||
size_t remaining = ota_size - total;
|
size_t remaining = ota_size - total;
|
||||||
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
||||||
ssize_t read;
|
ssize_t read = this->client_->read(buf, requested);
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
if (this->noise_ != nullptr) {
|
|
||||||
// One frame per call; noise_read_data_ waits internally (readall_), so
|
|
||||||
// there is no would-block retry here and failures are already logged.
|
|
||||||
read = this->noise_read_data_(buf, requested);
|
|
||||||
if (read <= 0) {
|
|
||||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
|
||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
read = this->client_->read(buf, requested);
|
|
||||||
if (read == -1) {
|
if (read == -1) {
|
||||||
const int err = errno;
|
const int err = errno;
|
||||||
if (this->would_block_(err)) {
|
if (this->would_block_(err)) {
|
||||||
@@ -542,14 +445,11 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ESP_LOGW(TAG, "Read err %d", err);
|
ESP_LOGW(TAG, "Read err %d", err);
|
||||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
|
||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
} else if (read == 0) {
|
} else if (read == 0) {
|
||||||
ESP_LOGW(TAG, "Remote closed");
|
ESP_LOGW(TAG, "Remote closed");
|
||||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
|
||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
last_data_ms = millis();
|
last_data_ms = millis();
|
||||||
error_code = this->backend_->write(buf, read);
|
error_code = this->backend_->write(buf, read);
|
||||||
@@ -560,7 +460,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
total += read;
|
total += read;
|
||||||
#if USE_OTA_VERSION == 2
|
#if USE_OTA_VERSION == 2
|
||||||
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
||||||
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||||
size_acknowledged += OTA_BLOCK_SIZE;
|
size_acknowledged += OTA_BLOCK_SIZE;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -579,7 +479,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Acknowledge receive OK - 1 byte
|
// Acknowledge receive OK - 1 byte
|
||||||
this->data_write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
||||||
|
|
||||||
error_code = this->backend_->end();
|
error_code = this->backend_->end();
|
||||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||||
@@ -588,10 +488,10 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Acknowledge Update end OK - 1 byte
|
// Acknowledge Update end OK - 1 byte
|
||||||
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||||
|
|
||||||
// Read ACK
|
// Read ACK
|
||||||
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||||
this->log_read_error_(LOG_STR("ack"));
|
this->log_read_error_(LOG_STR("ack"));
|
||||||
// do not go to error, this is not fatal
|
// do not go to error, this is not fatal
|
||||||
}
|
}
|
||||||
@@ -614,7 +514,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
App.safe_reboot();
|
App.safe_reboot();
|
||||||
|
|
||||||
error:
|
error:
|
||||||
this->data_write_byte_(static_cast<uint8_t>(error_code));
|
this->write_byte_(static_cast<uint8_t>(error_code));
|
||||||
|
|
||||||
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
|
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
|
||||||
// Always call abort() unconditionally: backends register external partitions before
|
// Always call abort() unconditionally: backends register external partitions before
|
||||||
@@ -781,9 +681,6 @@ void ESPHomeOTAComponent::cleanup_connection_() {
|
|||||||
this->backend_ = nullptr;
|
this->backend_ = nullptr;
|
||||||
#ifdef USE_OTA_PASSWORD
|
#ifdef USE_OTA_PASSWORD
|
||||||
this->cleanup_auth_();
|
this->cleanup_auth_();
|
||||||
#endif
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
this->noise_ = nullptr;
|
|
||||||
#endif
|
#endif
|
||||||
// Intentionally no disable_loop() — letting loop() run one more iteration catches
|
// Intentionally no disable_loop() — letting loop() run one more iteration catches
|
||||||
// any connection that queued on the listener mid-session (otherwise the wake flag,
|
// any connection that queued on the listener mid-session (otherwise the wake flag,
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
#ifdef USE_OTA
|
#ifdef USE_OTA
|
||||||
#include "esphome/components/ota/ota_backend_factory.h"
|
#include "esphome/components/ota/ota_backend_factory.h"
|
||||||
#include "esphome/components/socket/socket.h"
|
#include "esphome/components/socket/socket.h"
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
#include "esphome/components/noise/noise_handshake.h"
|
|
||||||
#endif
|
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
@@ -27,9 +24,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
|||||||
AUTH_SEND, // Sending authentication request
|
AUTH_SEND, // Sending authentication request
|
||||||
AUTH_READ, // Reading authentication data
|
AUTH_READ, // Reading authentication data
|
||||||
#endif // USE_OTA_PASSWORD
|
#endif // USE_OTA_PASSWORD
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
NOISE_HANDSHAKE, // Exchanging Noise handshake frames
|
|
||||||
#endif
|
|
||||||
DATA, // BLOCKING! Processing OTA data (update, etc.)
|
DATA, // BLOCKING! Processing OTA data (update, etc.)
|
||||||
};
|
};
|
||||||
#ifdef USE_OTA_PASSWORD
|
#ifdef USE_OTA_PASSWORD
|
||||||
@@ -44,11 +38,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
|||||||
}
|
}
|
||||||
#endif // USE_OTA_PASSWORD
|
#endif // USE_OTA_PASSWORD
|
||||||
|
|
||||||
#if defined(USE_OTA_ENCRYPTION) && !defined(USE_OTA_ENCRYPTION_FROM_API)
|
|
||||||
/// psk points at 32 bytes that live in flash for the life of the program
|
|
||||||
void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// Manually set the port OTA should listen on
|
/// Manually set the port OTA should listen on
|
||||||
void set_port(uint16_t port) { this->port_ = port; }
|
void set_port(uint16_t port) { this->port_ = port; }
|
||||||
|
|
||||||
@@ -74,51 +63,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
|||||||
bool writeall_(const uint8_t *buf, size_t len);
|
bool writeall_(const uint8_t *buf, size_t len);
|
||||||
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
|
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
|
||||||
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
// Heap-allocated only while an encrypted OTA session is active.
|
|
||||||
struct NoiseSession {
|
|
||||||
~NoiseSession();
|
|
||||||
noise::NoiseResponderHandshake handshake;
|
|
||||||
NoiseCipherState *send_cipher{nullptr};
|
|
||||||
NoiseCipherState *recv_cipher{nullptr};
|
|
||||||
uint16_t frame_len{0}; // total frame size once the header is parsed, 0 until then
|
|
||||||
uint16_t frame_pos{0}; // bytes read or written so far
|
|
||||||
bool writing{false}; // a produced handshake frame is still being flushed
|
|
||||||
uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
|
|
||||||
};
|
|
||||||
// The api server's live context when the api has encryption, else our own
|
|
||||||
const noise::NoiseContext &noise_context_() const;
|
|
||||||
bool noise_start_session_(uint8_t server_feature_flags);
|
|
||||||
bool handle_noise_handshake_();
|
|
||||||
bool noise_try_read_frame_();
|
|
||||||
size_t noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len);
|
|
||||||
bool noise_try_write_frame_();
|
|
||||||
void noise_send_reject_(const LogString *reason);
|
|
||||||
ssize_t noise_decrypt_(uint8_t *buf, size_t len);
|
|
||||||
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
|
|
||||||
bool noise_readall_(uint8_t *buf, size_t len);
|
|
||||||
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
|
|
||||||
bool noise_write_byte_(uint8_t byte);
|
|
||||||
#endif // USE_OTA_ENCRYPTION
|
|
||||||
|
|
||||||
// Data-phase I/O dispatch: through the noise transport when a session is
|
|
||||||
// active, straight to the socket otherwise.
|
|
||||||
inline bool data_write_byte_(uint8_t byte) {
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
if (this->noise_ != nullptr)
|
|
||||||
return this->noise_write_byte_(byte);
|
|
||||||
#endif
|
|
||||||
return this->write_byte_(byte);
|
|
||||||
}
|
|
||||||
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
|
|
||||||
inline bool data_readall_(uint8_t *buf, size_t len) {
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
if (this->noise_ != nullptr)
|
|
||||||
return this->noise_readall_(buf, len);
|
|
||||||
#endif
|
|
||||||
return this->readall_(buf, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool try_read_(size_t to_read, const LogString *desc);
|
bool try_read_(size_t to_read, const LogString *desc);
|
||||||
bool try_write_(size_t to_write, const LogString *desc);
|
bool try_write_(size_t to_write, const LogString *desc);
|
||||||
|
|
||||||
@@ -147,12 +91,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
|||||||
std::string password_;
|
std::string password_;
|
||||||
std::unique_ptr<uint8_t[]> auth_buf_;
|
std::unique_ptr<uint8_t[]> auth_buf_;
|
||||||
#endif // USE_OTA_PASSWORD
|
#endif // USE_OTA_PASSWORD
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
#ifndef USE_OTA_ENCRYPTION_FROM_API
|
|
||||||
noise::NoiseContext noise_ctx_;
|
|
||||||
#endif
|
|
||||||
std::unique_ptr<NoiseSession> noise_;
|
|
||||||
#endif // USE_OTA_ENCRYPTION
|
|
||||||
|
|
||||||
socket::ListenSocket *server_{nullptr};
|
socket::ListenSocket *server_{nullptr};
|
||||||
std::unique_ptr<socket::Socket> client_;
|
std::unique_ptr<socket::Socket> client_;
|
||||||
@@ -160,20 +98,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
|||||||
|
|
||||||
uint32_t client_connect_time_{0};
|
uint32_t client_connect_time_{0};
|
||||||
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
|
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
|
||||||
// Buffer size for OTA data transfer. The upload client derives its maximum
|
|
||||||
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
|
|
||||||
// minus the 16-byte MAC); both must change together.
|
|
||||||
static constexpr size_t OTA_BUFFER_SIZE = 1040;
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
// espota2.NOISE_MAX_PLAINTEXT; shrinking the buffer would reject every
|
|
||||||
// frame a current CLI sends
|
|
||||||
static constexpr size_t NOISE_CLIENT_MAX_PLAINTEXT = 1024;
|
|
||||||
static_assert(OTA_BUFFER_SIZE >= NOISE_CLIENT_MAX_PLAINTEXT + noise::MAC_SIZE,
|
|
||||||
"OTA_BUFFER_SIZE must fit a full encrypted data frame");
|
|
||||||
#endif
|
|
||||||
static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
|
|
||||||
// Derived from the feature byte; storing it would pad the trailing bytes
|
|
||||||
bool extended_proto_() const;
|
|
||||||
#ifdef USE_OTA_PARTITIONS
|
#ifdef USE_OTA_PARTITIONS
|
||||||
uint32_t running_app_offset_{0};
|
uint32_t running_app_offset_{0};
|
||||||
size_t running_app_size_{0};
|
size_t running_app_size_{0};
|
||||||
@@ -187,6 +111,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
|||||||
uint8_t auth_buf_pos_{0};
|
uint8_t auth_buf_pos_{0};
|
||||||
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
|
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
|
||||||
#endif // USE_OTA_PASSWORD
|
#endif // USE_OTA_PASSWORD
|
||||||
|
bool extended_proto_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
#include "ota_esphome.h"
|
|
||||||
#ifdef USE_OTA
|
|
||||||
#ifdef USE_OTA_ENCRYPTION
|
|
||||||
#include "esphome/components/noise/noise.h"
|
|
||||||
#include "esphome/components/ota/ota_backend.h"
|
|
||||||
#include "esphome/core/hal.h"
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <new>
|
|
||||||
|
|
||||||
#ifdef USE_ESP8266
|
|
||||||
#include <pgmspace.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
|
|
||||||
static const char *const TAG = "esphome.ota";
|
|
||||||
|
|
||||||
#ifdef USE_ESP8266
|
|
||||||
static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit";
|
|
||||||
#else
|
|
||||||
static constexpr char OTA_NOISE_PROLOGUE_INIT[] = "NoiseOTAInit";
|
|
||||||
#endif
|
|
||||||
static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = sizeof(OTA_NOISE_PROLOGUE_INIT) - 1;
|
|
||||||
|
|
||||||
ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
|
|
||||||
if (this->send_cipher != nullptr) {
|
|
||||||
noise_cipherstate_free(this->send_cipher);
|
|
||||||
}
|
|
||||||
if (this->recv_cipher != nullptr) {
|
|
||||||
noise_cipherstate_free(this->recv_cipher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Allocate the session and start the responder handshake.
|
|
||||||
*
|
|
||||||
* The prologue binds the whole plaintext preamble, so any tampering with the
|
|
||||||
* negotiation (a stripped feature flag, a changed version) breaks the first
|
|
||||||
* handshake MAC on either side:
|
|
||||||
* "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags
|
|
||||||
*/
|
|
||||||
bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
|
|
||||||
// A provisioned key cleared between the offer and here is not guarded: the
|
|
||||||
// session runs on the zero key load_psk fills in and fails the client's MAC.
|
|
||||||
// Default-init: the frame buffer is written before it is read
|
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession);
|
|
||||||
static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
|
|
||||||
static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
|
|
||||||
static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
|
|
||||||
uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN +
|
|
||||||
PROLOGUE_FEATURE_ACK_LEN];
|
|
||||||
progmem_memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
|
|
||||||
uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
|
|
||||||
// Magic bytes, already validated in MAGIC_READ
|
|
||||||
std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
|
|
||||||
p += sizeof(MAGIC_BYTES);
|
|
||||||
// Our magic ack
|
|
||||||
*p++ = ota::OTA_RESPONSE_OK;
|
|
||||||
*p++ = USE_OTA_VERSION;
|
|
||||||
// The feature byte the client sent
|
|
||||||
*p++ = this->ota_features_;
|
|
||||||
// The feature ack we sent (noise requires the extended protocol)
|
|
||||||
*p++ = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
|
||||||
*p++ = server_feature_flags;
|
|
||||||
|
|
||||||
// The caller only starts a session when the context holds a key
|
|
||||||
int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY
|
|
||||||
: this->noise_->handshake.init(this->noise_context_(), prologue, sizeof(prologue));
|
|
||||||
if (err != 0) {
|
|
||||||
// Raw noise codes throughout: the name table would cost flash in builds
|
|
||||||
// where only the OTA uses noise
|
|
||||||
ESP_LOGW(TAG, "Session init: %d", err);
|
|
||||||
this->cleanup_connection_();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Drive the non-blocking handshake from loop(); returns true once the
|
|
||||||
* transport ciphers are ready. A would-block returns false and the next
|
|
||||||
* loop() resumes from the NoiseSession cursors; on failure the connection
|
|
||||||
* is cleaned up.
|
|
||||||
*/
|
|
||||||
bool ESPHomeOTAComponent::handle_noise_handshake_() {
|
|
||||||
NoiseSession &s = *this->noise_;
|
|
||||||
while (true) {
|
|
||||||
if (s.writing) {
|
|
||||||
if (!this->noise_try_write_frame_()) {
|
|
||||||
return false; // would block, or errored and cleaned up
|
|
||||||
}
|
|
||||||
s.writing = false;
|
|
||||||
s.frame_pos = 0;
|
|
||||||
s.frame_len = 0;
|
|
||||||
}
|
|
||||||
switch (s.handshake.action()) {
|
|
||||||
case noise::NoiseResponderHandshake::Action::ACTION_READ: {
|
|
||||||
if (!this->noise_try_read_frame_()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
|
|
||||||
s.frame_pos = 0;
|
|
||||||
s.frame_len = 0;
|
|
||||||
if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
|
|
||||||
ESP_LOGW(TAG, "Client rejected the handshake: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
|
|
||||||
this->cleanup_connection_();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
|
|
||||||
if (err != 0) {
|
|
||||||
// A MAC failure here almost always means the uploader has a different key
|
|
||||||
const LogString *reason = noise::reject_reason_for(err);
|
|
||||||
ESP_LOGW(TAG, "Handshake read: %s (%d)", LOG_STR_ARG(reason), err);
|
|
||||||
this->noise_send_reject_(reason);
|
|
||||||
this->cleanup_connection_();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case noise::NoiseResponderHandshake::Action::ACTION_WRITE: {
|
|
||||||
size_t msg_len = 0;
|
|
||||||
int err =
|
|
||||||
s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
|
|
||||||
if (err != 0) {
|
|
||||||
ESP_LOGW(TAG, "Handshake write: %d", err);
|
|
||||||
this->cleanup_connection_();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const uint16_t payload_len = msg_len + 1;
|
|
||||||
noise::write_frame_header(s.frame_buf, payload_len);
|
|
||||||
s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
|
|
||||||
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
|
|
||||||
s.frame_pos = 0;
|
|
||||||
s.writing = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: {
|
|
||||||
int err = s.handshake.split(s.send_cipher, s.recv_cipher);
|
|
||||||
if (err != 0) {
|
|
||||||
ESP_LOGW(TAG, "Handshake split: %d", err);
|
|
||||||
this->cleanup_connection_();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ESP_LOGD(TAG, "Noise handshake complete");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
ESP_LOGW(TAG, "Bad handshake state");
|
|
||||||
this->cleanup_connection_();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Payload length from a frame header, or 0 (logged) when the indicator or
|
|
||||||
/// the length is out of range. Callers pass min_len >= 1 so 0 is never valid.
|
|
||||||
size_t ESPHomeOTAComponent::noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len) {
|
|
||||||
const size_t payload_len = encode_uint16(header[1], header[2]);
|
|
||||||
if (header[0] != noise::FRAME_INDICATOR || payload_len < min_len || payload_len > max_len) {
|
|
||||||
ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], payload_len);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return payload_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Non-blocking read of one handshake frame into the session buffer.
|
|
||||||
bool ESPHomeOTAComponent::noise_try_read_frame_() {
|
|
||||||
NoiseSession &s = *this->noise_;
|
|
||||||
while (true) {
|
|
||||||
// The header first, then the body once the header says how long it is
|
|
||||||
const uint16_t want = s.frame_len == 0 ? noise::FRAME_HEADER_SIZE : s.frame_len;
|
|
||||||
if (s.frame_pos < want) {
|
|
||||||
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, want - s.frame_pos);
|
|
||||||
if (!this->handle_read_error_(read, LOG_STR("read noise"))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
s.frame_pos += read;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (s.frame_len != 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const size_t payload_len = this->noise_frame_payload_len_(s.frame_buf, 1, 1 + noise::MAX_HANDSHAKE_SIZE);
|
|
||||||
if (payload_len == 0) {
|
|
||||||
this->cleanup_connection_();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Non-blocking write of the pending session-buffer frame.
|
|
||||||
bool ESPHomeOTAComponent::noise_try_write_frame_() {
|
|
||||||
NoiseSession &s = *this->noise_;
|
|
||||||
while (s.frame_pos < s.frame_len) {
|
|
||||||
ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
|
|
||||||
if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
s.frame_pos += written;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Best-effort explicit reject frame so the client can log a readable reason.
|
|
||||||
void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
|
|
||||||
// Every reason here comes from noise::reject_reason_for(), so the exported
|
|
||||||
// floor is the exact capacity needed
|
|
||||||
uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE];
|
|
||||||
const size_t payload_len =
|
|
||||||
noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
|
|
||||||
noise::write_frame_header(data, payload_len);
|
|
||||||
this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt a ciphertext in place; returns the plaintext size or -1.
|
|
||||||
ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) {
|
|
||||||
NoiseBuffer mbuf;
|
|
||||||
noise_buffer_init(mbuf);
|
|
||||||
noise_buffer_set_inout(mbuf, buf, len, len);
|
|
||||||
int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
|
|
||||||
if (err != 0) {
|
|
||||||
ESP_LOGW(TAG, "Decrypt: %d", err);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return mbuf.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Blocking read of one frame whose ciphertext size must be within the given
|
|
||||||
* bounds, decrypted in place; returns the plaintext size, or -1 on error.
|
|
||||||
* buf needs max_ciphertext capacity.
|
|
||||||
*/
|
|
||||||
ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
|
|
||||||
uint8_t header[noise::FRAME_HEADER_SIZE];
|
|
||||||
if (!this->readall_(header, sizeof(header))) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
const size_t ciphertext_len = this->noise_frame_payload_len_(header, min_ciphertext, max_ciphertext);
|
|
||||||
if (ciphertext_len == 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!this->readall_(buf, ciphertext_len)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return this->noise_decrypt_(buf, ciphertext_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Blocking read of one frame whose plaintext must be exactly len bytes
|
|
||||||
* (control units are one unit per frame). buf needs len + noise::MAC_SIZE
|
|
||||||
* capacity; the plaintext lands at buf[0..len).
|
|
||||||
*/
|
|
||||||
bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
|
|
||||||
return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Blocking read of one data-phase frame, decrypted in place; returns the
|
|
||||||
* plaintext size, or -1 on error. buf is the OTA_BUFFER_SIZE data buffer.
|
|
||||||
* The ciphertext must fit that buffer and its plaintext must fit what the
|
|
||||||
* caller accepts (the remaining image bytes).
|
|
||||||
*/
|
|
||||||
ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
|
|
||||||
const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
|
|
||||||
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blocking write of one response byte as an encrypted frame.
|
|
||||||
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
|
|
||||||
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
|
|
||||||
frame[noise::FRAME_HEADER_SIZE] = byte;
|
|
||||||
NoiseBuffer mbuf;
|
|
||||||
noise_buffer_init(mbuf);
|
|
||||||
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
|
|
||||||
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
|
|
||||||
if (err != 0) {
|
|
||||||
ESP_LOGW(TAG, "Encrypt: %d", err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
noise::write_frame_header(frame, mbuf.size);
|
|
||||||
return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome
|
|
||||||
#endif // USE_OTA_ENCRYPTION
|
|
||||||
#endif // USE_OTA
|
|
||||||
@@ -3,7 +3,6 @@ from typing import Any
|
|||||||
from esphome import automation, core
|
from esphome import automation, core
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import wifi
|
from esphome.components import wifi
|
||||||
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
|
|
||||||
from esphome.components.udp import CONF_ON_RECEIVE
|
from esphome.components.udp import CONF_ON_RECEIVE
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -18,7 +17,6 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core import CORE, HexInt
|
from esphome.core import CORE, HexInt
|
||||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||||
import esphome.final_validate as fv
|
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
@@ -134,24 +132,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_variant(config: ConfigType) -> ConfigType:
|
|
||||||
# ESP-NOW rides the Wi-Fi PHY. Radio-less esp32 variants have no native
|
|
||||||
# ESP-NOW; only the ESP32-P4 has a path, via the esp32_hosted shim that
|
|
||||||
# supplies the esp_now_* symbols. Fail here with a clear message instead of
|
|
||||||
# letting the build reach an "undefined reference to esp_now_*" link error.
|
|
||||||
variant = get_esp32_variant()
|
|
||||||
if wifi.variant_has_wifi(variant):
|
|
||||||
return config
|
|
||||||
if variant != VARIANT_ESP32P4:
|
|
||||||
raise cv.Invalid(f"ESP-NOW is not supported on {variant} (no Wi-Fi radio)")
|
|
||||||
if "esp32_hosted" not in fv.full_config.get():
|
|
||||||
raise cv.Invalid(f"ESP-NOW on {variant} requires the esp32_hosted component")
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _validate_variant
|
|
||||||
|
|
||||||
|
|
||||||
async def _trigger_to_code(config: ConfigType) -> MockObj:
|
async def _trigger_to_code(config: ConfigType) -> MockObj:
|
||||||
if address := config.get(CONF_ADDRESS):
|
if address := config.get(CONF_ADDRESS):
|
||||||
address = address.parts
|
address = address.parts
|
||||||
|
|||||||
@@ -334,10 +334,9 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
|
|||||||
}
|
}
|
||||||
if (traits.supports_preset_modes()) {
|
if (traits.supports_preset_modes()) {
|
||||||
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
||||||
for (const char *s : traits.supported_preset_modes()) {
|
for (const char *s : traits.supported_preset_modes())
|
||||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::fan
|
} // namespace esphome::fan
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ void HBridgeSwitch::dump_config() {
|
|||||||
LOG_PIN(" On Pin: ", this->on_pin_);
|
LOG_PIN(" On Pin: ", this->on_pin_);
|
||||||
LOG_PIN(" Off Pin: ", this->off_pin_);
|
LOG_PIN(" Off Pin: ", this->off_pin_);
|
||||||
ESP_LOGCONFIG(TAG, " Pulse length: %" PRId32 " ms", this->pulse_length_);
|
ESP_LOGCONFIG(TAG, " Pulse length: %" PRId32 " ms", this->pulse_length_);
|
||||||
if (this->wait_time_) {
|
if (this->wait_time_)
|
||||||
ESP_LOGCONFIG(TAG, " Wait time %" PRId32 " ms", this->wait_time_);
|
ESP_LOGCONFIG(TAG, " Wait time %" PRId32 " ms", this->wait_time_);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HBridgeSwitch::write_state(bool state) {
|
void HBridgeSwitch::write_state(bool state) {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ void HC8Component::dump_config() {
|
|||||||
" Warmup time: %" PRIu32 " s",
|
" Warmup time: %" PRIu32 " s",
|
||||||
this->warmup_seconds_);
|
this->warmup_seconds_);
|
||||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||||
|
this->check_uart_settings(9600);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::hc8
|
} // namespace esphome::hc8
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|||||||
baud_rate=9600,
|
baud_rate=9600,
|
||||||
require_rx=True,
|
require_rx=True,
|
||||||
require_tx=True,
|
require_tx=True,
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ CoverTraits HE60rCover::get_traits() {
|
|||||||
|
|
||||||
void HE60rCover::dump_config() {
|
void HE60rCover::dump_config() {
|
||||||
LOG_COVER("", "HE60R Cover", this);
|
LOG_COVER("", "HE60R Cover", this);
|
||||||
|
this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" Open Duration: %.1fs\n"
|
" Open Duration: %.1fs\n"
|
||||||
" Close Duration: %.1fs",
|
" Close Duration: %.1fs",
|
||||||
this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
|
this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
|
||||||
auto restore = this->restore_state_();
|
auto restore = this->restore_state_();
|
||||||
if (restore.has_value()) {
|
if (restore.has_value())
|
||||||
ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
|
ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HE60rCover::endstop_reached_(CoverOperation operation) {
|
void HE60rCover::endstop_reached_(CoverOperation operation) {
|
||||||
@@ -77,9 +77,8 @@ void HE60rCover::process_rx_(uint8_t data) {
|
|||||||
ESP_LOGV(TAG, "Process RX data %X", data);
|
ESP_LOGV(TAG, "Process RX data %X", data);
|
||||||
if (!this->query_seen_) {
|
if (!this->query_seen_) {
|
||||||
this->query_seen_ = data == QUERY_BYTE;
|
this->query_seen_ = data == QUERY_BYTE;
|
||||||
if (!this->query_seen_) {
|
if (!this->query_seen_)
|
||||||
ESP_LOGD(TAG, "RX Byte %02X", data);
|
ESP_LOGD(TAG, "RX Byte %02X", data);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (data) {
|
switch (data) {
|
||||||
|
|||||||
@@ -257,9 +257,8 @@ void HoermannHcp::on_state_reg_(uint16_t value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The low byte can change on its own, so only report a state we cannot decode once.
|
// The low byte can change on its own, so only report a state we cannot decode once.
|
||||||
if (state != (previous >> 8)) {
|
if (state != (previous >> 8))
|
||||||
ESP_LOGW(TAG, "Unknown door state 0x%02X", state);
|
ESP_LOGW(TAG, "Unknown door state 0x%02X", state);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Low byte of register 6: bit 0x10 is the lamp, bit 0x04 the relay. The reference implementation records
|
// Low byte of register 6: bit 0x10 is the lamp, bit 0x04 the relay. The reference implementation records
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
|
|||||||
void HrxlMaxsonarWrComponent::dump_config() {
|
void HrxlMaxsonarWrComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:");
|
ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:");
|
||||||
LOG_SENSOR(" ", "Distance", this);
|
LOG_SENSOR(" ", "Distance", this);
|
||||||
|
// As specified in the sensor's data sheet
|
||||||
|
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::hrxl_maxsonar_wr
|
} // namespace esphome::hrxl_maxsonar_wr
|
||||||
|
|||||||
@@ -23,14 +23,6 @@ CONFIG_SCHEMA = sensor.sensor_schema(
|
|||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
).extend(uart.UART_DEVICE_SCHEMA)
|
).extend(uart.UART_DEVICE_SCHEMA)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|
||||||
"hrxl_maxsonar_wr",
|
|
||||||
baud_rate=9600,
|
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
var = await sensor.new_sensor(config)
|
var = await sensor.new_sensor(config)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREO
|
|||||||
static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)};
|
static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)};
|
||||||
|
|
||||||
void HydreonRGxxComponent::dump_config() {
|
void HydreonRGxxComponent::dump_config() {
|
||||||
|
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||||
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
|
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
|
||||||
if (this->is_failed()) {
|
if (this->is_failed()) {
|
||||||
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
|
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
|
||||||
|
|||||||
@@ -130,14 +130,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
_validate,
|
_validate,
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|
||||||
"hydreon_rgxx",
|
|
||||||
baud_rate=9600,
|
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ void KamstrupKMPComponent::dump_config() {
|
|||||||
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
|
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
|
||||||
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
|
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KamstrupKMPComponent::update() {
|
void KamstrupKMPComponent::update() {
|
||||||
|
|||||||
@@ -102,13 +102,7 @@ CONFIG_SCHEMA = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||||
"kamstrup_kmp",
|
"kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True
|
||||||
baud_rate=1200,
|
|
||||||
require_rx=True,
|
|
||||||
require_tx=True,
|
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=2,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,33 +16,26 @@ void KeyCollector::loop() {
|
|||||||
void KeyCollector::dump_config() {
|
void KeyCollector::dump_config() {
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_CONFIG
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_CONFIG
|
||||||
ESP_LOGCONFIG(TAG, "Key Collector:");
|
ESP_LOGCONFIG(TAG, "Key Collector:");
|
||||||
if (this->min_length_ > 0) {
|
if (this->min_length_ > 0)
|
||||||
ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_);
|
ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_);
|
||||||
}
|
if (this->max_length_ > 0)
|
||||||
if (this->max_length_ > 0) {
|
|
||||||
ESP_LOGCONFIG(TAG, " max length: %d", this->max_length_);
|
ESP_LOGCONFIG(TAG, " max length: %d", this->max_length_);
|
||||||
}
|
if (!this->back_keys_.empty())
|
||||||
if (!this->back_keys_.empty()) {
|
|
||||||
ESP_LOGCONFIG(TAG, " erase keys '%s'", this->back_keys_.c_str());
|
ESP_LOGCONFIG(TAG, " erase keys '%s'", this->back_keys_.c_str());
|
||||||
}
|
if (!this->clear_keys_.empty())
|
||||||
if (!this->clear_keys_.empty()) {
|
|
||||||
ESP_LOGCONFIG(TAG, " clear keys '%s'", this->clear_keys_.c_str());
|
ESP_LOGCONFIG(TAG, " clear keys '%s'", this->clear_keys_.c_str());
|
||||||
}
|
if (!this->start_keys_.empty())
|
||||||
if (!this->start_keys_.empty()) {
|
|
||||||
ESP_LOGCONFIG(TAG, " start keys '%s'", this->start_keys_.c_str());
|
ESP_LOGCONFIG(TAG, " start keys '%s'", this->start_keys_.c_str());
|
||||||
}
|
|
||||||
if (!this->end_keys_.empty()) {
|
if (!this->end_keys_.empty()) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" end keys '%s'\n"
|
" end keys '%s'\n"
|
||||||
" end key is required: %s",
|
" end key is required: %s",
|
||||||
this->end_keys_.c_str(), ONOFF(this->end_key_required_));
|
this->end_keys_.c_str(), ONOFF(this->end_key_required_));
|
||||||
}
|
}
|
||||||
if (!this->allowed_keys_.empty()) {
|
if (!this->allowed_keys_.empty())
|
||||||
ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str());
|
ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str());
|
||||||
}
|
if (this->timeout_ > 0)
|
||||||
if (this->timeout_ > 0) {
|
|
||||||
ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0);
|
ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0);
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -333,9 +333,8 @@ void LN882HBLE::loop() {
|
|||||||
// the queue empty — from the very first report on. Checking here keeps that
|
// the queue empty — from the very first report on. Checking here keeps that
|
||||||
// failure visible instead of producing a scanner that is silently dead.
|
// failure visible instead of producing a scanner that is silently dead.
|
||||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||||
if (dropped > 0) {
|
if (dropped > 0)
|
||||||
ESP_LOGW(TAG, "Dropped %u scan reports (queue full or out of memory for a report slot)", dropped);
|
ESP_LOGW(TAG, "Dropped %u scan reports (queue full or out of memory for a report slot)", dropped);
|
||||||
}
|
|
||||||
// Drain the lock-free ring filled by the rw task; all per-report work runs
|
// Drain the lock-free ring filled by the rw task; all per-report work runs
|
||||||
// here on the main task, then the report returns to the pool.
|
// here on the main task, then the report returns to the pool.
|
||||||
BLEScanReport *report = this->report_queue_.pop();
|
BLEScanReport *report = this->report_queue_.pop();
|
||||||
|
|||||||
@@ -1059,9 +1059,8 @@ static void *lv_alloc_draw_buf(size_t size, bool internal) {
|
|||||||
void *buffer;
|
void *buffer;
|
||||||
size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN);
|
size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN);
|
||||||
buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
|
buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
|
||||||
if (buffer == nullptr) {
|
if (buffer == nullptr)
|
||||||
ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
|
ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
|
||||||
}
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from contextlib import ExitStack
|
|||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_COLUMNS, CONF_ROWS
|
from esphome.components.const import CONF_ROWS
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROW, CONF_TEXT, CONF_WIDTH
|
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROW, CONF_TEXT, CONF_WIDTH
|
||||||
from esphome.core import ID
|
from esphome.core import ID
|
||||||
@@ -20,6 +20,7 @@ from .label import CONF_LABEL
|
|||||||
|
|
||||||
CONF_TABLE = "table"
|
CONF_TABLE = "table"
|
||||||
CONF_CELLS = "cells"
|
CONF_CELLS = "cells"
|
||||||
|
CONF_COLUMNS = "columns"
|
||||||
CONF_ROW_COUNT = "row_count"
|
CONF_ROW_COUNT = "row_count"
|
||||||
CONF_COLUMN_COUNT = "column_count"
|
CONF_COLUMN_COUNT = "column_count"
|
||||||
CONF_MERGE_RIGHT = "merge_right"
|
CONF_MERGE_RIGHT = "merge_right"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from esphome import automation, pins
|
from esphome import automation, pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import key_provider
|
from esphome.components import key_provider
|
||||||
from esphome.components.const import CONF_COLUMNS, CONF_KEYS, CONF_ROWS
|
from esphome.components.const import CONF_ROWS
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
|
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
@@ -21,6 +21,8 @@ MatrixKeyTrigger = matrix_keypad_ns.class_(
|
|||||||
)
|
)
|
||||||
|
|
||||||
CONF_KEYPAD_ID = "keypad_id"
|
CONF_KEYPAD_ID = "keypad_id"
|
||||||
|
CONF_COLUMNS = "columns"
|
||||||
|
CONF_KEYS = "keys"
|
||||||
CONF_DEBOUNCE_TIME = "debounce_time"
|
CONF_DEBOUNCE_TIME = "debounce_time"
|
||||||
CONF_HAS_DIODES = "has_diodes"
|
CONF_HAS_DIODES = "has_diodes"
|
||||||
CONF_HAS_PULLDOWNS = "has_pulldowns"
|
CONF_HAS_PULLDOWNS = "has_pulldowns"
|
||||||
|
|||||||
@@ -192,8 +192,6 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
if CORE.using_arduino:
|
if CORE.using_arduino:
|
||||||
if CORE.is_esp8266:
|
if CORE.is_esp8266:
|
||||||
cg.add_library("ESP8266mDNS", None)
|
cg.add_library("ESP8266mDNS", None)
|
||||||
# No MDNS global in the build; mdns_esp8266.cpp owns a guarded MDNSResponder
|
|
||||||
cg.add_build_flag("-DNO_GLOBAL_MDNS")
|
|
||||||
elif CORE.is_rp2:
|
elif CORE.is_rp2:
|
||||||
cg.add_library("LEAmDNS", None)
|
cg.add_library("LEAmDNS", None)
|
||||||
|
|
||||||
|
|||||||
@@ -13,47 +13,8 @@
|
|||||||
|
|
||||||
namespace esphome::mdns {
|
namespace esphome::mdns {
|
||||||
|
|
||||||
// Main-loop calls into LEAmDNS that send (update() and close(); begin(), addService() and
|
|
||||||
// the scheduled restart never reach a send) can yield inside UdpContext::sendTimeout(); a
|
|
||||||
// packet arriving then re-enters LEAmDNS from lwIP on the same UdpContext and both sides
|
|
||||||
// free the same tx pbufs (#18760). Received packets stay queued during such a call and are
|
|
||||||
// processed from the main loop afterwards.
|
|
||||||
class GuardedMDNSResponder : public ::esp8266::MDNSImplementation::MDNSResponder {
|
|
||||||
public:
|
|
||||||
void update_guarded() { this->run_guarded_(&GuardedMDNSResponder::update); }
|
|
||||||
void close_guarded() { this->run_guarded_(&GuardedMDNSResponder::close); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void run_guarded_(bool (GuardedMDNSResponder::*fn)()) {
|
|
||||||
UdpContext *ctx = this->m_pUDPContext;
|
|
||||||
if (ctx == nullptr) {
|
|
||||||
(this->*fn)();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Set every time: a restart replaces the context together with its stock handler. Only
|
|
||||||
// begin() and the scheduled netif callback restart, never update() or close(), so the
|
|
||||||
// context cannot change underneath this call.
|
|
||||||
ctx->onRx([this]() {
|
|
||||||
if (!this->in_loop_call_) {
|
|
||||||
this->_callProcess();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this->in_loop_call_ = true;
|
|
||||||
(this->*fn)();
|
|
||||||
// close() releases the context; a yield in here queues further packets for this loop too
|
|
||||||
while (this->m_pUDPContext != nullptr && this->m_pUDPContext->next()) {
|
|
||||||
this->_parseMessage();
|
|
||||||
}
|
|
||||||
this->in_loop_call_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
volatile bool in_loop_call_{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
static GuardedMDNSResponder mdns_responder; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
|
|
||||||
static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
|
static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
|
||||||
mdns_responder.begin(App.get_name().c_str());
|
MDNS.begin(App.get_name().c_str());
|
||||||
|
|
||||||
for (const auto &service : services) {
|
for (const auto &service : services) {
|
||||||
// Strip the leading underscore from the proto and service_type. While it is
|
// Strip the leading underscore from the proto and service_type. While it is
|
||||||
@@ -69,9 +30,9 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
|
|||||||
service_type++;
|
service_type++;
|
||||||
}
|
}
|
||||||
uint16_t port = service.port.value();
|
uint16_t port = service.port.value();
|
||||||
mdns_responder.addService(FPSTR(service_type), FPSTR(proto), port);
|
MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
|
||||||
for (const auto &record : service.txt_records) {
|
for (const auto &record : service.txt_records) {
|
||||||
mdns_responder.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
|
MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
|
||||||
FPSTR(MDNS_STR_ARG(record.value)));
|
FPSTR(MDNS_STR_ARG(record.value)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +52,7 @@ void MDNSComponent::start_polling_window_() {
|
|||||||
if (wifi->is_roaming() || (!wifi->is_connected() && !wifi->is_ap_active()))
|
if (wifi->is_roaming() || (!wifi->is_connected() && !wifi->is_ap_active()))
|
||||||
return;
|
return;
|
||||||
#endif
|
#endif
|
||||||
mdns_responder.update_guarded();
|
MDNS.update();
|
||||||
});
|
});
|
||||||
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
|
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
|
||||||
}
|
}
|
||||||
@@ -120,7 +81,7 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void MDNSComponent::on_shutdown() {
|
void MDNSComponent::on_shutdown() {
|
||||||
mdns_responder.close_guarded();
|
MDNS.close();
|
||||||
delay(10);
|
delay(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ void MHZ19Component::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, "MH-Z19:");
|
ESP_LOGCONFIG(TAG, "MH-Z19:");
|
||||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||||
|
this->check_uart_settings(9600);
|
||||||
|
|
||||||
if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
|
if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
|
||||||
ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot");
|
ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot");
|
||||||
} else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
|
} else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
|
||||||
|
|||||||
@@ -80,14 +80,6 @@ CONFIG_SCHEMA = (
|
|||||||
.extend(uart.UART_DEVICE_SCHEMA)
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|
||||||
"mhz19",
|
|
||||||
baud_rate=9600,
|
|
||||||
data_bits=8,
|
|
||||||
parity="NONE",
|
|
||||||
stop_bits=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ void MipiDsi::setup() {
|
|||||||
.bus_id = 0, // index from 0, specify the DSI host to use
|
.bus_id = 0, // index from 0, specify the DSI host to use
|
||||||
.num_data_lanes =
|
.num_data_lanes =
|
||||||
this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability
|
this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability
|
||||||
// phy_clk_src left at 0 to enable runtime auto-select.
|
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, // Clock source for the DPHY
|
||||||
.lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps
|
.lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps
|
||||||
};
|
};
|
||||||
auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_);
|
auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_);
|
||||||
@@ -237,9 +237,8 @@ void MipiDsi::write_to_display_(int x_start, int y_start, int w, int h, const ui
|
|||||||
xSemaphoreTake(this->io_lock_, portMAX_DELAY);
|
xSemaphoreTake(this->io_lock_, portMAX_DELAY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK)
|
||||||
ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MipiDsi::check_buffer_() {
|
bool MipiDsi::check_buffer_() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user