mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 03:16:37 +00:00
Merge remote-tracking branch 'origin/dev' into socket-lwip-raw-udp
This commit is contained in:
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../.agents/skills
|
||||||
@@ -9,6 +9,29 @@ body:
|
|||||||
If you have a feature request or enhancement, please [request them here instead][fr].
|
If you have a feature request or enhancement, please [request them here instead][fr].
|
||||||
|
|
||||||
[fr]: https://github.com/orgs/esphome/discussions
|
[fr]: https://github.com/orgs/esphome/discussions
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Use of AI in bug reports
|
||||||
|
|
||||||
|
AI tools are good at carrying out well-defined tasks, but they are not good at troubleshooting.
|
||||||
|
Please do NOT paste an AI-generated wall of text into the issue template - if the AI hasn't solved
|
||||||
|
your problem, its wild guesses are not likely to help.
|
||||||
|
|
||||||
|
Please DO include your own words and observations, compile/boot logs, and
|
||||||
|
especially a minimal reproducible example of your YAML configuration that demonstrates the problem.
|
||||||
|
|
||||||
|
It is however quite acceptable to use AI to translate your *own* report,
|
||||||
|
if you aren't a competent English speaker.
|
||||||
|
|
||||||
|
If you really think it will be useful to include an AI's analysis, preferably wrap it in a `<details>` block which will be collapsed by default.
|
||||||
|
|
||||||
|
If you are using AI to help solve a problem, rather than asking it to speculate about what the problem is,
|
||||||
|
it can be more useful to ask it to create a step-by-step troubleshooting procedure.
|
||||||
|
AI is also useful for generating boilerplate code, such as a minimal reproducible example of your YAML
|
||||||
|
configuration that demonstrates the problem.
|
||||||
|
|
||||||
|
Used properly, AI can be a useful tool to help you solve your problem, but don't let it get in the way.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ runs:
|
|||||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||||
# downstream jobs rely on is preserved.
|
# downstream jobs rely on is preserved.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
# Pull request saves land in per-PR scopes nothing else can
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../.agents/skills
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
# ``--system`` (below) installs into the setup-python interpreter;
|
# ``--system`` (below) installs into the setup-python interpreter;
|
||||||
# no venv is created or restored by this workflow.
|
# no venv is created or restored by this workflow.
|
||||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull-request-only workflow: a save could never be shared and
|
# Pull-request-only workflow: a save could never be shared and
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||||
# that ``. venv/bin/activate`` see an identical layout.
|
# that ``. venv/bin/activate`` see an identical layout.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
# Pull request saves land in per-PR scopes nothing else can
|
||||||
@@ -413,7 +413,7 @@ jobs:
|
|||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
# Only needed on cache miss to populate the venv.
|
# Only needed on cache miss to populate the venv.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
# Pull request saves land in per-PR scopes nothing else can
|
||||||
@@ -1274,7 +1274,7 @@ jobs:
|
|||||||
# install step (order-of-magnitude faster on cold boots,
|
# install step (order-of-magnitude faster on cold boots,
|
||||||
# with its own wheel cache). actions/setup-python still
|
# with its own wheel cache). actions/setup-python still
|
||||||
# provides the interpreter.
|
# provides the interpreter.
|
||||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
# Pull request saves land in per-PR scopes nothing else can
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -84,6 +84,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
and will be closed if no further activity occurs within 7 days.
|
and will be closed if no further activity occurs within 7 days.
|
||||||
|
|
||||||
If you are the author of this PR, please leave a comment if you want
|
If you are the author of this PR, please leave a comment if you want
|
||||||
to keep it open. Also, please rebase your PR onto the latest dev
|
to keep it open. Also, please merge the latest dev branch into your
|
||||||
branch to ensure that it's up to date with the latest changes.
|
branch to ensure that it's up to date with the latest changes.
|
||||||
|
|
||||||
Thank you for your contribution!
|
Thank you for your contribution!
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
# setup-python interpreter so subsequent ``prek`` /
|
# setup-python interpreter so subsequent ``prek`` /
|
||||||
# ``script/run-in-env.py`` steps find the deps without a
|
# ``script/run-in-env.py`` steps find the deps without a
|
||||||
# ``uv run`` prefix.
|
# ``uv run`` prefix.
|
||||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.16.6
|
rev: v0.16.7
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
@@ -431,7 +431,17 @@ file does, and it is the authority when they disagree. The most useful starting
|
|||||||
MyComponent *parent_;
|
MyComponent *parent_;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
|
Register it without writing a builder:
|
||||||
|
```python
|
||||||
|
automation.register_simple_action(
|
||||||
|
"my_component.do_something", MyAction, schema, synchronous=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a
|
||||||
|
no-argument constructor, `register_parented_action` for a class deriving from `Parented<T>`, and
|
||||||
|
the `@automation.register_action(...)` decorator only when the builder must also set fields.
|
||||||
|
|
||||||
|
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
|
||||||
|
|
||||||
* **Conditions:**
|
* **Conditions:**
|
||||||
```cpp
|
```cpp
|
||||||
@@ -443,7 +453,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
|||||||
MyComponent *parent_;
|
MyComponent *parent_;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
|
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
|
||||||
|
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
|
||||||
|
|
||||||
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
|
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
|
||||||
|
|
||||||
@@ -629,6 +640,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
|||||||
_request_listener_slot()
|
_request_listener_slot()
|
||||||
cg.add(hub.register_listener(var))
|
cg.add(hub.register_listener(var))
|
||||||
```
|
```
|
||||||
|
When several instances each own a list declared at the same size (one per hub of a
|
||||||
|
`MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`;
|
||||||
|
the define is then the largest count any one key requested instead of the total.
|
||||||
```cpp
|
```cpp
|
||||||
#ifdef MY_COMPONENT_LISTENER_COUNT
|
#ifdef MY_COMPONENT_LISTENER_COUNT
|
||||||
void register_listener(MyComponentListener *listener);
|
void register_listener(MyComponentListener *listener);
|
||||||
|
|||||||
+7
-1
@@ -100,6 +100,7 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
|||||||
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
||||||
esphome/components/bp1658cj/* @Cossid
|
esphome/components/bp1658cj/* @Cossid
|
||||||
esphome/components/bp5758d/* @Cossid
|
esphome/components/bp5758d/* @Cossid
|
||||||
|
esphome/components/bridge/* @kbx81
|
||||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||||
esphome/components/button/* @esphome/core
|
esphome/components/button/* @esphome/core
|
||||||
esphome/components/bytebuffer/* @clydebarrow
|
esphome/components/bytebuffer/* @clydebarrow
|
||||||
@@ -111,6 +112,8 @@ esphome/components/captive_portal/* @esphome/core
|
|||||||
esphome/components/cc1101/* @gabest11 @lygris
|
esphome/components/cc1101/* @gabest11 @lygris
|
||||||
esphome/components/ccs811/* @habbie
|
esphome/components/ccs811/* @habbie
|
||||||
esphome/components/cd74hc4067/* @asoehlke
|
esphome/components/cd74hc4067/* @asoehlke
|
||||||
|
esphome/components/cdc_acm_uart/* @kbx81
|
||||||
|
esphome/components/cdc_acm_uart/bridge/* @kbx81
|
||||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||||
esphome/components/ch423/* @dwmw2
|
esphome/components/ch423/* @dwmw2
|
||||||
esphome/components/chsc6x/* @kkosik20
|
esphome/components/chsc6x/* @kkosik20
|
||||||
@@ -179,7 +182,6 @@ esphome/components/esp32_camera_web_server/* @ayufan
|
|||||||
esphome/components/esp32_can/* @Sympatron
|
esphome/components/esp32_can/* @Sympatron
|
||||||
esphome/components/esp32_hosted/* @swoboda1337
|
esphome/components/esp32_hosted/* @swoboda1337
|
||||||
esphome/components/esp32_hosted/update/* @swoboda1337
|
esphome/components/esp32_hosted/update/* @swoboda1337
|
||||||
esphome/components/esp32_improv/* @jesserockz
|
|
||||||
esphome/components/esp32_rmt/* @jesserockz
|
esphome/components/esp32_rmt/* @jesserockz
|
||||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||||
esphome/components/esp8266/* @esphome/core
|
esphome/components/esp8266/* @esphome/core
|
||||||
@@ -263,8 +265,10 @@ esphome/components/i2s_audio/* @jesserockz
|
|||||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||||
esphome/components/iaqcore/* @yozik04
|
esphome/components/iaqcore/* @yozik04
|
||||||
|
esphome/components/icnt86/* @danepowell
|
||||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||||
esphome/components/improv_base/* @esphome/core
|
esphome/components/improv_base/* @esphome/core
|
||||||
|
esphome/components/improv_ble/* @jesserockz
|
||||||
esphome/components/improv_serial/* @esphome/core
|
esphome/components/improv_serial/* @esphome/core
|
||||||
esphome/components/ina226/* @latonita @Sergio303
|
esphome/components/ina226/* @latonita @Sergio303
|
||||||
esphome/components/ina260/* @mreditor97
|
esphome/components/ina260/* @mreditor97
|
||||||
@@ -474,6 +478,7 @@ esphome/components/sendspin/image/* @kahrendt
|
|||||||
esphome/components/sendspin/media_player/* @kahrendt
|
esphome/components/sendspin/media_player/* @kahrendt
|
||||||
esphome/components/sendspin/media_source/* @kahrendt
|
esphome/components/sendspin/media_source/* @kahrendt
|
||||||
esphome/components/sendspin/sensor/* @kahrendt
|
esphome/components/sendspin/sensor/* @kahrendt
|
||||||
|
esphome/components/sendspin/switch/* @kahrendt
|
||||||
esphome/components/sendspin/text_sensor/* @kahrendt
|
esphome/components/sendspin/text_sensor/* @kahrendt
|
||||||
esphome/components/sensirion_common/* @martgras
|
esphome/components/sensirion_common/* @martgras
|
||||||
esphome/components/sensor/* @esphome/core
|
esphome/components/sensor/* @esphome/core
|
||||||
@@ -586,6 +591,7 @@ esphome/components/uart/* @esphome/core
|
|||||||
esphome/components/uart/button/* @ssieb
|
esphome/components/uart/button/* @ssieb
|
||||||
esphome/components/uart/event/* @eoasmxd
|
esphome/components/uart/event/* @eoasmxd
|
||||||
esphome/components/uart/packet_transport/* @clydebarrow
|
esphome/components/uart/packet_transport/* @clydebarrow
|
||||||
|
esphome/components/uart_mux/* @kbx81
|
||||||
esphome/components/udp/* @clydebarrow
|
esphome/components/udp/* @clydebarrow
|
||||||
esphome/components/ufire_ec/* @pvizeli
|
esphome/components/ufire_ec/* @pvizeli
|
||||||
esphome/components/ufire_ise/* @pvizeli
|
esphome/components/ufire_ise/* @pvizeli
|
||||||
|
|||||||
+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.5
|
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
platformio settings set enable_telemetry No \
|
platformio settings set enable_telemetry No \
|
||||||
|
|||||||
+99
-28
@@ -102,6 +102,101 @@ def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schem
|
|||||||
return CONDITION_REGISTRY.register(name, condition_type, schema)
|
return CONDITION_REGISTRY.register(name, condition_type, schema)
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_with_parent(
|
||||||
|
config: ConfigType,
|
||||||
|
automation_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
|
parent = await cg.get_variable(config[CONF_ID])
|
||||||
|
return cg.new_Pvariable(automation_id, template_arg, parent)
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_without_parent(
|
||||||
|
config: ConfigType,
|
||||||
|
automation_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
|
return cg.new_Pvariable(automation_id, template_arg)
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_parented(
|
||||||
|
config: ConfigType,
|
||||||
|
automation_id: ID,
|
||||||
|
template_arg: cg.TemplateArguments,
|
||||||
|
args: TemplateArgsType,
|
||||||
|
) -> MockObj:
|
||||||
|
var = cg.new_Pvariable(automation_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def register_simple_action(
|
||||||
|
name: str,
|
||||||
|
action_type: MockObjClass,
|
||||||
|
schema: cv.Schema,
|
||||||
|
*,
|
||||||
|
synchronous: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Register an action whose constructor takes the object named by ``config[CONF_ID]``.
|
||||||
|
|
||||||
|
Use the ``register_action`` decorator instead when the builder must also set fields.
|
||||||
|
"""
|
||||||
|
register_action(name, action_type, schema, synchronous=synchronous)(
|
||||||
|
_build_with_parent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_simple_condition(
|
||||||
|
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||||
|
) -> None:
|
||||||
|
"""Condition counterpart of ``register_simple_action``."""
|
||||||
|
register_condition(name, condition_type, schema)(_build_with_parent)
|
||||||
|
|
||||||
|
|
||||||
|
def register_bare_action(
|
||||||
|
name: str,
|
||||||
|
action_type: MockObjClass,
|
||||||
|
schema: cv.Schema,
|
||||||
|
*,
|
||||||
|
synchronous: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Register an action whose constructor takes no arguments."""
|
||||||
|
register_action(name, action_type, schema, synchronous=synchronous)(
|
||||||
|
_build_without_parent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_bare_condition(
|
||||||
|
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||||
|
) -> None:
|
||||||
|
"""Condition counterpart of ``register_bare_action``."""
|
||||||
|
register_condition(name, condition_type, schema)(_build_without_parent)
|
||||||
|
|
||||||
|
|
||||||
|
def register_parented_action(
|
||||||
|
name: str,
|
||||||
|
action_type: MockObjClass,
|
||||||
|
schema: cv.Schema,
|
||||||
|
*,
|
||||||
|
synchronous: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Register an action deriving from ``Parented<T>``.
|
||||||
|
|
||||||
|
The object is constructed without arguments and ``set_parent()`` receives the object
|
||||||
|
named by ``config[CONF_ID]``.
|
||||||
|
"""
|
||||||
|
register_action(name, action_type, schema, synchronous=synchronous)(_build_parented)
|
||||||
|
|
||||||
|
|
||||||
|
def register_parented_condition(
|
||||||
|
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||||
|
) -> None:
|
||||||
|
"""Condition counterpart of ``register_parented_action``."""
|
||||||
|
register_condition(name, condition_type, schema)(_build_parented)
|
||||||
|
|
||||||
|
|
||||||
Action = cg.esphome_ns.class_("Action")
|
Action = cg.esphome_ns.class_("Action")
|
||||||
Trigger = cg.esphome_ns.class_("Trigger")
|
Trigger = cg.esphome_ns.class_("Trigger")
|
||||||
ACTION_REGISTRY = Registry()
|
ACTION_REGISTRY = Registry()
|
||||||
@@ -534,44 +629,20 @@ async def lambda_action_to_code(
|
|||||||
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
||||||
|
|
||||||
|
|
||||||
@register_action(
|
register_simple_action(
|
||||||
"component.update",
|
"component.update",
|
||||||
UpdateComponentAction,
|
UpdateComponentAction,
|
||||||
maybe_simple_id(
|
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||||
{
|
|
||||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
synchronous=True,
|
synchronous=True,
|
||||||
)
|
)
|
||||||
async def component_update_action_to_code(
|
|
||||||
config: ConfigType,
|
|
||||||
action_id: ID,
|
|
||||||
template_arg: cg.TemplateArguments,
|
|
||||||
args: TemplateArgsType,
|
|
||||||
) -> MockObj:
|
|
||||||
comp = await cg.get_variable(config[CONF_ID])
|
|
||||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
|
||||||
|
|
||||||
|
|
||||||
@register_action(
|
register_simple_action(
|
||||||
"component.suspend",
|
"component.suspend",
|
||||||
SuspendComponentAction,
|
SuspendComponentAction,
|
||||||
maybe_simple_id(
|
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||||
{
|
|
||||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
synchronous=True,
|
synchronous=True,
|
||||||
)
|
)
|
||||||
async def component_suspend_action_to_code(
|
|
||||||
config: ConfigType,
|
|
||||||
action_id: ID,
|
|
||||||
template_arg: cg.TemplateArguments,
|
|
||||||
args: TemplateArgsType,
|
|
||||||
) -> MockObj:
|
|
||||||
comp = await cg.get_variable(config[CONF_ID])
|
|
||||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
|
||||||
|
|
||||||
|
|
||||||
@register_action(
|
@register_action(
|
||||||
|
|||||||
@@ -90,9 +90,10 @@ def get_project_cmakelists(
|
|||||||
"""
|
"""
|
||||||
idf_target = variant_to_idf_target(get_esp32_variant())
|
idf_target = variant_to_idf_target(get_esp32_variant())
|
||||||
|
|
||||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
|
||||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
|
||||||
# --format=raw because the legacy mode doesn't support it.
|
# total_size, hence the ELF fallback in espidf/size_summary.py; both
|
||||||
|
# go away together when 1.x support is dropped.
|
||||||
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
||||||
|
|
||||||
# Project-wide compile options: -D defines and -W warning flags (skip
|
# Project-wide compile options: -D defines and -W warning flags (skip
|
||||||
@@ -211,10 +212,12 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
|||||||
|
|
||||||
project({CORE.name})
|
project({CORE.name})
|
||||||
|
|
||||||
# Emit raw JSON size data for ESPHome to read post-build.
|
# Emit per-memory-type JSON size data for ESPHome to read post-build.
|
||||||
|
# json2 stays small; raw dumps every symbol (~2s on a large map) and
|
||||||
|
# this command runs inside the link edge, blocking everything downstream.
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
|
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
|
||||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||||
${{CMAKE_PROJECT_NAME}}.map
|
${{CMAKE_PROJECT_NAME}}.map
|
||||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ See the component-alias section of esphome/loader.py.
|
|||||||
|
|
||||||
# alias -> (canonical component, removal version or None)
|
# alias -> (canonical component, removal version or None)
|
||||||
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
|
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
|
||||||
|
"esp32_improv": ("improv_ble", "2027.4.0"),
|
||||||
"rp2040": ("rp2", "2027.7.0"),
|
"rp2040": ("rp2", "2027.7.0"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
|
|||||||
/// - SamplingMode::MIN: Use the lowest sample value
|
/// - SamplingMode::MIN: Use the lowest sample value
|
||||||
/// - SamplingMode::MAX: Use the highest sample value
|
/// - SamplingMode::MAX: Use the highest sample value
|
||||||
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
|
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
|
||||||
void set_sampling_mode(SamplingMode sampling_mode);
|
void set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
|
||||||
|
|
||||||
/// Perform a single ADC sampling operation and return the measured value.
|
/// Perform a single ADC sampling operation and return the measured value.
|
||||||
/// This function handles raw readings, calibration, and averaging as needed.
|
/// This function handles raw readings, calibration, and averaging as needed.
|
||||||
|
|||||||
@@ -76,6 +76,4 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
|
|
||||||
|
|
||||||
} // namespace esphome::adc
|
} // namespace esphome::adc
|
||||||
|
|||||||
@@ -138,11 +138,11 @@ class AlarmControlPanel : public EntityBase {
|
|||||||
// in order to store last panel state in flash
|
// in order to store last panel state in flash
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
// current state
|
// current state
|
||||||
AlarmControlPanelState current_state_;
|
AlarmControlPanelState current_state_{ACP_STATE_DISARMED};
|
||||||
// the desired (or previous) state
|
// the desired (or previous) state
|
||||||
AlarmControlPanelState desired_state_;
|
AlarmControlPanelState desired_state_{ACP_STATE_DISARMED};
|
||||||
// last time the state was updated
|
// last time the state was updated
|
||||||
uint32_t last_update_;
|
uint32_t last_update_{0};
|
||||||
// the call control function
|
// the call control function
|
||||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||||
// state callback - passes the new state to listeners
|
// state callback - passes the new state to listeners
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ CONF_LISTEN_BACKLOG = "listen_backlog"
|
|||||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||||
|
|
||||||
|
# Schema defaults that also match the C++ initializers in api_server.h; codegen
|
||||||
|
# skips the setter when the config equals them.
|
||||||
|
DEFAULT_PORT = 6053
|
||||||
|
DEFAULT_REBOOT_TIMEOUT = "15min"
|
||||||
|
DEFAULT_BATCH_DELAY = "100ms"
|
||||||
|
|
||||||
|
|
||||||
def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||||
"""Register the API as a provisioning source when encryption is enabled.
|
"""Register the API as a provisioning source when encryption is enabled.
|
||||||
@@ -292,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(APIServer),
|
cv.GenerateID(): cv.declare_id(APIServer),
|
||||||
cv.Optional(CONF_PORT, default=6053): cv.port,
|
cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
# Removed in 2026.1.0 - kept to provide helpful error message
|
# Removed in 2026.1.0 - kept to provide helpful error message
|
||||||
cv.Optional(CONF_PASSWORD): cv.invalid(
|
cv.Optional(CONF_PASSWORD): cv.invalid(
|
||||||
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
|
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
|
||||||
@@ -305,14 +311,14 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
"Or visit https://esphome.io/components/api/#configuration-variables"
|
"Or visit https://esphome.io/components/api/#configuration-variables"
|
||||||
),
|
),
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_REBOOT_TIMEOUT, default="15min"
|
CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT
|
||||||
): cv.positive_time_period_milliseconds,
|
): cv.positive_time_period_milliseconds,
|
||||||
cv.Exclusive(
|
cv.Exclusive(
|
||||||
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
||||||
): 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_BATCH_DELAY, default="100ms"): cv.All(
|
cv.Optional(CONF_BATCH_DELAY, default=DEFAULT_BATCH_DELAY): 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)),
|
||||||
),
|
),
|
||||||
@@ -350,10 +356,9 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
ln882x=5, # Moderate RAM
|
ln882x=5, # Moderate RAM
|
||||||
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
|
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
|
||||||
): cv.int_range(min=1, max=20),
|
): cv.int_range(min=1, max=20),
|
||||||
# Maximum queued send buffers per connection before dropping connection
|
# Max queued messages per connection, and 2 KB of backlog per slot up
|
||||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
# to 64 KB (a lone message is exempt), before the connection is dropped
|
||||||
# Platform defaults based on available RAM and typical message rates:
|
# Platform defaults based on available RAM and typical message rates:
|
||||||
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
|
|
||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
CONF_MAX_SEND_QUEUE,
|
CONF_MAX_SEND_QUEUE,
|
||||||
esp8266=4, # Limited RAM, need to fail fast
|
esp8266=4, # Limited RAM, need to fail fast
|
||||||
@@ -463,9 +468,15 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
# Request a log listener slot for API log streaming
|
# Request a log listener slot for API log streaming
|
||||||
request_log_listener()
|
request_log_listener()
|
||||||
|
|
||||||
cg.add(var.set_port(config[CONF_PORT]))
|
# Skip the setters when the config matches the C++ initializers (DEFAULT_*).
|
||||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
if (port := config[CONF_PORT]) != DEFAULT_PORT:
|
||||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
cg.add(var.set_port(port))
|
||||||
|
if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period(
|
||||||
|
DEFAULT_REBOOT_TIMEOUT
|
||||||
|
):
|
||||||
|
cg.add(var.set_reboot_timeout(reboot_timeout))
|
||||||
|
if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY):
|
||||||
|
cg.add(var.set_batch_delay(batch_delay))
|
||||||
if CONF_LISTEN_BACKLOG in config:
|
if CONF_LISTEN_BACKLOG in config:
|
||||||
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
||||||
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ service APIConnection {
|
|||||||
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
||||||
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
||||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||||
|
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2726,7 +2727,8 @@ enum SerialProxyParity {
|
|||||||
SERIAL_PROXY_PARITY_ODD = 2;
|
SERIAL_PROXY_PARITY_ODD = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure UART parameters for a serial proxy instance
|
// Configure UART parameters for a serial proxy instance. Only the subscribed client may
|
||||||
|
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
|
||||||
message SerialProxyConfigureRequest {
|
message SerialProxyConfigureRequest {
|
||||||
option (id) = 138;
|
option (id) = 138;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
@@ -2752,7 +2754,8 @@ message SerialProxyDataReceived {
|
|||||||
bytes data = 2; // Raw data received from the serial device
|
bytes data = 2; // Raw data received from the serial device
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write data to a serial device
|
// Write data to a serial device. Only the subscribed client may write; writes from
|
||||||
|
// others are ignored (since API 1.17).
|
||||||
message SerialProxyWriteRequest {
|
message SerialProxyWriteRequest {
|
||||||
option (id) = 140;
|
option (id) = 140;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
@@ -2763,7 +2766,8 @@ message SerialProxyWriteRequest {
|
|||||||
bytes data = 2; // Raw data to write to the serial device
|
bytes data = 2; // Raw data to write to the serial device
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set modem control pin states (RTS and DTR)
|
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them;
|
||||||
|
// others are refused with PORT_IN_USE (since API 1.17).
|
||||||
message SerialProxySetModemPinsRequest {
|
message SerialProxySetModemPinsRequest {
|
||||||
option (id) = 141;
|
option (id) = 141;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
@@ -2802,6 +2806,7 @@ enum SerialProxyRequestType {
|
|||||||
// error the device answers with INVALID_ARGUMENT.
|
// error the device answers with INVALID_ARGUMENT.
|
||||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
|
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
|
||||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
|
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
|
||||||
|
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SerialProxyStatus {
|
enum SerialProxyStatus {
|
||||||
@@ -2814,7 +2819,8 @@ enum SerialProxyStatus {
|
|||||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic request message for simple serial proxy operations
|
// Generic request message for simple serial proxy operations. FLUSH requires an active
|
||||||
|
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
|
||||||
message SerialProxyRequest {
|
message SerialProxyRequest {
|
||||||
option (id) = 144;
|
option (id) = 144;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
@@ -2838,6 +2844,29 @@ message SerialProxyRequestResponse {
|
|||||||
string error_message = 4; // Additional detail on failure (optional)
|
string error_message = 4; // Additional detail on failure (optional)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
|
||||||
|
// activates the port's protocol-aware tap (if one is configured), letting it observe
|
||||||
|
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
|
||||||
|
// speaks is a property of the device configuration, discoverable from the tap
|
||||||
|
// component's own API surface. A client that is about to flash firmware selects RAW
|
||||||
|
// first, which definitively disables that injection.
|
||||||
|
enum SerialProxyMode {
|
||||||
|
SERIAL_PROXY_MODE_RAW = 0;
|
||||||
|
SERIAL_PROXY_MODE_PROTOCOL = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the subscribed client may change the mode; any other caller -- including one that
|
||||||
|
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
|
||||||
|
// when the port has no protocol-aware tap configured.
|
||||||
|
message SerialProxySetModeRequest {
|
||||||
|
option (id) = 152;
|
||||||
|
option (source) = SOURCE_CLIENT;
|
||||||
|
option (ifdef) = "USE_SERIAL_PROXY";
|
||||||
|
|
||||||
|
uint32 instance = 1;
|
||||||
|
SerialProxyMode mode = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||||
message BluetoothSetConnectionParamsRequest {
|
message BluetoothSetConnectionParamsRequest {
|
||||||
option (id) = 145;
|
option (id) = 145;
|
||||||
|
|||||||
@@ -1,20 +1,37 @@
|
|||||||
#include "api_buffer.h"
|
#include "api_buffer.h"
|
||||||
#include <new>
|
#ifdef ESPHOME_DEBUG_API
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
|
#ifdef ESPHOME_DEBUG_API
|
||||||
|
void APIBuffer::debug_check_drop_(size_t drop) const {
|
||||||
|
if (drop > this->size_) {
|
||||||
|
ESP_LOGE("api.buffer", "drop_front: drop=%zu size=%u", drop, this->size_);
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
bool APIBuffer::grow_(size_t n) {
|
bool APIBuffer::grow_(size_t n) {
|
||||||
// nothrow (no zero-fill) so OOM is reportable; plain new aborts instead
|
if (n > MAX_SIZE)
|
||||||
// (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF).
|
|
||||||
// RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory.
|
|
||||||
std::unique_ptr<uint8_t[]> new_data(new (std::nothrow) uint8_t[n]);
|
|
||||||
if (new_data == nullptr)
|
|
||||||
return false;
|
return false;
|
||||||
if (this->size_)
|
// realloc extends in place when it can, avoiding the copy
|
||||||
std::memcpy(new_data.get(), this->data_.get(), this->size_);
|
uint8_t *grown = RAMAllocator<uint8_t>().reallocate(this->data_.get(), n);
|
||||||
this->data_ = std::move(new_data);
|
if (grown == nullptr)
|
||||||
|
return false;
|
||||||
|
(void) this->data_.release(); // realloc already freed or reused the old block
|
||||||
|
this->data_.reset(grown);
|
||||||
this->capacity_ = n;
|
this->capacity_ = n;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t *APIBuffer::append(size_t n) {
|
||||||
|
const size_t old_size = this->size_;
|
||||||
|
if (!this->resize(old_size + n))
|
||||||
|
return nullptr;
|
||||||
|
return this->data_.get() + old_size;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace esphome::api {
|
|||||||
/// writes in debug builds.
|
/// writes in debug builds.
|
||||||
class APIBuffer {
|
class APIBuffer {
|
||||||
public:
|
public:
|
||||||
|
static constexpr size_t MAX_SIZE = UINT16_MAX; // API frames carry 16 bit lengths
|
||||||
void clear() { this->size_ = 0; }
|
void clear() { this->size_ = 0; }
|
||||||
/// Returns false if allocation fails; the buffer is left unchanged.
|
/// Returns false if allocation fails; the buffer is left unchanged.
|
||||||
[[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); }
|
[[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); }
|
||||||
@@ -36,9 +37,19 @@ class APIBuffer {
|
|||||||
[[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
|
[[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
|
||||||
if (!this->reserve(std::max(reserve_size, new_size)))
|
if (!this->reserve(std::max(reserve_size, new_size)))
|
||||||
return false;
|
return false;
|
||||||
this->size_ = new_size;
|
this->size_ = static_cast<uint16_t>(new_size);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
/// Grow by n bytes; returns the new bytes, or nullptr on allocation failure.
|
||||||
|
[[nodiscard]] uint8_t *append(size_t n);
|
||||||
|
/// Drop the first `drop` bytes, sliding the rest down. Precondition: drop <= size().
|
||||||
|
void drop_front(size_t drop) {
|
||||||
|
#ifdef ESPHOME_DEBUG_API
|
||||||
|
this->debug_check_drop_(drop);
|
||||||
|
#endif
|
||||||
|
this->size_ -= drop;
|
||||||
|
std::memmove(this->data_.get(), this->data_.get() + drop, this->size_);
|
||||||
|
}
|
||||||
uint8_t *data() { return this->data_.get(); }
|
uint8_t *data() { return this->data_.get(); }
|
||||||
const uint8_t *data() const { return this->data_.get(); }
|
const uint8_t *data() const { return this->data_.get(); }
|
||||||
size_t size() const { return this->size_; }
|
size_t size() const { return this->size_; }
|
||||||
@@ -55,9 +66,14 @@ class APIBuffer {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool grow_(size_t n);
|
bool grow_(size_t n);
|
||||||
std::unique_ptr<uint8_t[]> data_;
|
#ifdef ESPHOME_DEBUG_API
|
||||||
size_t size_{0};
|
void debug_check_drop_(size_t drop) const;
|
||||||
size_t capacity_{0};
|
#endif
|
||||||
|
// RAMAllocator: PSRAM when available, and it reports failure where
|
||||||
|
// new (std::nothrow) still aborts on ESP-IDF without exceptions
|
||||||
|
RAMUniquePtr<uint8_t[]> data_;
|
||||||
|
uint16_t size_{0};
|
||||||
|
uint16_t capacity_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -364,7 +364,10 @@ void APIConnection::check_keepalive_(uint32_t now) {
|
|||||||
ESP_LOGVV(TAG, "Sending keepalive PING");
|
ESP_LOGVV(TAG, "Sending keepalive PING");
|
||||||
PingRequest req;
|
PingRequest req;
|
||||||
this->flags_.sent_ping = this->send_message(req);
|
this->flags_.sent_ping = this->send_message(req);
|
||||||
if (!this->flags_.sent_ping) {
|
if (this->flags_.sent_ping) {
|
||||||
|
// Quiet for a keepalive period and the ping is on its way: a one-off stall's storage can go
|
||||||
|
this->helper_->release_overflow_buffer();
|
||||||
|
} else {
|
||||||
// If we can't send the ping request directly (tx_buffer full),
|
// If we can't send the ping request directly (tx_buffer full),
|
||||||
// schedule it at the front of the batch so it will be sent with priority
|
// schedule it at the front of the batch so it will be sent with priority
|
||||||
ESP_LOGW(TAG, "Buffer full, ping queued");
|
ESP_LOGW(TAG, "Buffer full, ping queued");
|
||||||
@@ -717,12 +720,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
|||||||
}
|
}
|
||||||
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||||
|
a_switch->control(msg.state);
|
||||||
if (msg.state) {
|
|
||||||
a_switch->turn_on();
|
|
||||||
} else {
|
|
||||||
a_switch->turn_off();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -1661,6 +1659,7 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
|||||||
break;
|
break;
|
||||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||||
|
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||||
// Response-only discriminators; never valid in a request
|
// Response-only discriminators; never valid in a request
|
||||||
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||||
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||||
@@ -1673,6 +1672,19 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
|||||||
send_serial_proxy_ack(this, msg.instance, msg.type, status);
|
send_serial_proxy_ack(this, msg.instance, msg.type, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
|
||||||
|
auto &proxies = App.get_serial_proxies();
|
||||||
|
if (msg.instance >= proxies.size()) {
|
||||||
|
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||||
|
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||||
|
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
|
||||||
|
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||||
|
serial_proxy_result_to_status(result));
|
||||||
|
}
|
||||||
|
|
||||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
|
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
|
||||||
if (!this->send_message(msg)) {
|
if (!this->send_message(msg)) {
|
||||||
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
|
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
|
||||||
@@ -1799,7 +1811,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
|||||||
|
|
||||||
HelloResponse resp;
|
HelloResponse resp;
|
||||||
resp.api_version_major = 1;
|
resp.api_version_major = 1;
|
||||||
resp.api_version_minor = 16;
|
resp.api_version_minor = 17;
|
||||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||||
resp.server_info = ESPHOME_VERSION_REF;
|
resp.server_info = ESPHOME_VERSION_REF;
|
||||||
resp.name = StringRef(App.get_name());
|
resp.name = StringRef(App.get_name());
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
|
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
|
||||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
|
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
|
||||||
void on_serial_proxy_request(const SerialProxyRequest &msg);
|
void on_serial_proxy_request(const SerialProxyRequest &msg);
|
||||||
|
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
|
||||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin
|
|||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
|
|
||||||
// Queue unsent data into overflow buffer
|
// Queue unsent data into overflow buffer
|
||||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
|
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, sent)) {
|
||||||
HELPER_LOG("Overflow buffer full or out of memory, dropping connection");
|
HELPER_LOG("Overflow buffer full or out of memory, dropping connection");
|
||||||
this->state_ = State::FAILED;
|
this->state_ = State::FAILED;
|
||||||
return APIError::SOCKET_WRITE_FAILED;
|
return APIError::SOCKET_WRITE_FAILED;
|
||||||
|
|||||||
@@ -219,7 +219,10 @@ class APIFrameHelper {
|
|||||||
if (this->rx_buf_len_ == 0) {
|
if (this->rx_buf_len_ == 0) {
|
||||||
this->rx_buf_.release();
|
this->rx_buf_.release();
|
||||||
}
|
}
|
||||||
|
this->release_overflow_buffer();
|
||||||
}
|
}
|
||||||
|
// Free the send backlog storage once it has drained
|
||||||
|
void release_overflow_buffer() { this->overflow_buf_.release(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Drain backlogged overflow data to the socket and handle errors.
|
// Drain backlogged overflow data to the socket and handle errors.
|
||||||
|
|||||||
@@ -67,15 +67,15 @@ APIError APINoiseFrameHelper::init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// init prologue
|
// init prologue
|
||||||
size_t old_size = prologue_.size();
|
uint8_t *dst = prologue_.append(PROLOGUE_INIT_LEN);
|
||||||
if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] {
|
if (dst == nullptr) [[unlikely]] {
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
return APIError::OUT_OF_MEMORY;
|
return APIError::OUT_OF_MEMORY;
|
||||||
}
|
}
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||||
#else
|
#else
|
||||||
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
state_ = State::CLIENT_HELLO;
|
state_ = State::CLIENT_HELLO;
|
||||||
@@ -272,17 +272,17 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
|
|||||||
return handle_handshake_frame_error_(aerr);
|
return handle_handshake_frame_error_(aerr);
|
||||||
}
|
}
|
||||||
// ignore contents, may be used in future for flags
|
// ignore contents, may be used in future for flags
|
||||||
// Resize for: existing prologue + 2 size bytes + frame data
|
// Append 2 size bytes + frame data to the prologue
|
||||||
size_t old_size = this->prologue_.size();
|
|
||||||
size_t rx_size = this->rx_buf_.size();
|
size_t rx_size = this->rx_buf_.size();
|
||||||
if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] {
|
uint8_t *dst = this->prologue_.append(2 + rx_size);
|
||||||
|
if (dst == nullptr) [[unlikely]] {
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
return APIError::OUT_OF_MEMORY;
|
return APIError::OUT_OF_MEMORY;
|
||||||
}
|
}
|
||||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
dst[0] = (uint8_t) (rx_size >> 8);
|
||||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
dst[1] = (uint8_t) rx_size;
|
||||||
if (rx_size > 0) {
|
if (rx_size > 0) {
|
||||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
std::memcpy(dst + 2, this->rx_buf_.data(), rx_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
state_ = State::SERVER_HELLO;
|
state_ = State::SERVER_HELLO;
|
||||||
|
|||||||
@@ -1,98 +1,91 @@
|
|||||||
#include "api_overflow_buffer.h"
|
#include "api_overflow_buffer.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <new>
|
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
APIOverflowBuffer::~APIOverflowBuffer() {
|
|
||||||
for (auto *entry : this->queue_) {
|
|
||||||
if (entry != nullptr)
|
|
||||||
Entry::destroy(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
|
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
|
||||||
// socket->write() can re-enter this function: a log message emitted from an
|
// Nested call from inside socket->write(); see draining_
|
||||||
// lwip callback during the write goes out over the API and lands back in the
|
|
||||||
// frame helper's write/drain path. If a nested drain ran here it would send
|
|
||||||
// and free the entry the outer drain is still holding, causing a double free.
|
|
||||||
// Report "no progress" instead; the outer drain keeps draining, and the
|
|
||||||
// nested send is enqueued behind the existing backlog.
|
|
||||||
if (this->draining_)
|
if (this->draining_)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
// RAII so the flag is cleared on every return path
|
|
||||||
struct DrainGuard {
|
struct DrainGuard {
|
||||||
explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; }
|
APIOverflowBuffer &owner;
|
||||||
~DrainGuard() { this->flag_ = false; }
|
~DrainGuard() { this->owner.draining_ = false; }
|
||||||
bool &flag_;
|
} guard{*this};
|
||||||
} guard(this->draining_);
|
this->draining_ = true;
|
||||||
|
|
||||||
while (this->count_ > 0) {
|
while (this->count_ > 0) {
|
||||||
Entry *front = this->queue_[this->head_];
|
uint8_t *msg = this->buf_.data() + this->head_;
|
||||||
|
size_t len = msg[0] | (msg[1] << 8);
|
||||||
|
|
||||||
ssize_t sent = socket->write(front->current_data(), front->remaining());
|
ssize_t sent = socket->write(msg + LEN_PREFIX, len);
|
||||||
|
if (sent <= 0)
|
||||||
if (sent <= 0) {
|
return sent;
|
||||||
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
|
if (static_cast<size_t>(sent) < len) {
|
||||||
// 0 = nothing sent (treat as no progress)
|
// Step past the sent bytes and rewrite the prefix there; it lands on bytes already sent
|
||||||
|
this->head_ += sent;
|
||||||
|
len -= sent;
|
||||||
|
msg += sent;
|
||||||
|
msg[0] = len;
|
||||||
|
msg[1] = len >> 8;
|
||||||
return sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
this->head_ += LEN_PREFIX + len;
|
||||||
if (static_cast<uint16_t>(sent) < front->remaining()) {
|
|
||||||
// Partially sent, update offset and stop
|
|
||||||
front->offset += static_cast<uint16_t>(sent);
|
|
||||||
return sent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry fully sent — unlink it before freeing so a freed pointer is never
|
|
||||||
// reachable from the queue
|
|
||||||
this->queue_[this->head_] = nullptr;
|
|
||||||
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
|
|
||||||
this->count_--;
|
this->count_--;
|
||||||
Entry::destroy(front);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0; // All drained
|
this->head_ = 0;
|
||||||
|
if (this->release_when_drained_) {
|
||||||
|
this->release_when_drained_ = false;
|
||||||
|
this->buf_.release();
|
||||||
|
} else {
|
||||||
|
this->buf_.clear();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
|
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip) {
|
||||||
if (this->count_ >= API_MAX_SEND_QUEUE)
|
if (this->count_ >= API_MAX_SEND_QUEUE)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint16_t buffer_size = total_len - skip;
|
const size_t new_len = total_len - skip;
|
||||||
// nothrow: a failed allocation returns nullptr so the connection is dropped
|
const size_t new_bytes = LEN_PREFIX + new_len;
|
||||||
// cleanly instead of plain new's crash or abort on OOM
|
const size_t live = this->buf_.size() - this->head_;
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
// A lone message is only bound by the buffer; refusing it would just drop the connection
|
||||||
auto *data = new (std::nothrow) uint8_t[buffer_size];
|
if (live + new_bytes > (this->count_ > 0 ? MAX_BYTES : MAX_LONE_BYTES))
|
||||||
if (data == nullptr)
|
|
||||||
return false;
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
|
||||||
auto *entry = new (std::nothrow) Entry{data, buffer_size, 0};
|
|
||||||
if (entry == nullptr) {
|
|
||||||
delete[] data;
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (this->buf_.size() + new_bytes > this->buf_.capacity()) {
|
||||||
|
// Storage would move under an outer drain's write()
|
||||||
|
if (this->draining_)
|
||||||
|
return false;
|
||||||
|
if (this->head_ > 0) {
|
||||||
|
// Reclaim the sent prefix before growing
|
||||||
|
this->buf_.drop_front(this->head_);
|
||||||
|
this->head_ = 0;
|
||||||
|
}
|
||||||
|
if (!this->buf_.reserve(reserve_for(live + new_bytes)))
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t to_skip = skip;
|
uint8_t *dst = this->buf_.append(new_bytes);
|
||||||
uint16_t write_pos = 0;
|
if (dst == nullptr)
|
||||||
|
return false;
|
||||||
for (int i = 0; i < iovcnt; i++) {
|
dst[0] = new_len;
|
||||||
if (to_skip >= iov[i].iov_len) {
|
dst[1] = new_len >> 8;
|
||||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
dst += LEN_PREFIX;
|
||||||
|
for (const struct iovec *end = iov + iovcnt; iov != end; iov++) {
|
||||||
|
if (skip >= iov->iov_len) {
|
||||||
|
skip -= iov->iov_len;
|
||||||
} else {
|
} else {
|
||||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
const size_t len = iov->iov_len - skip;
|
||||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
std::memcpy(dst, static_cast<const uint8_t *>(iov->iov_base) + skip, len);
|
||||||
std::memcpy(entry->data + write_pos, src, len);
|
dst += len;
|
||||||
write_pos += len;
|
skip = 0;
|
||||||
to_skip = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish only after the copy completes so a half-built entry is never reachable
|
|
||||||
this->queue_[this->tail_] = entry;
|
|
||||||
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
|
|
||||||
this->count_++;
|
this->count_++;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <array>
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
|
||||||
@@ -8,71 +9,57 @@
|
|||||||
|
|
||||||
#include "esphome/components/socket/headers.h"
|
#include "esphome/components/socket/headers.h"
|
||||||
#include "esphome/components/socket/socket.h"
|
#include "esphome/components/socket/socket.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "api_buffer.h"
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
|
/// TCP send backlog, only used when the socket send buffer is full.
|
||||||
///
|
/// One contiguous buffer per connection, allocated on the first stall and
|
||||||
/// Under normal operation this buffer is **never used** — data goes straight
|
/// kept at its high-water mark so a lossy link does not churn the heap.
|
||||||
/// from the frame helper to the socket. It only fills when the LWIP TCP
|
/// Messages are stored as a 2 byte length prefix plus payload.
|
||||||
/// send buffer is full (slow client, congested network, heavy logging).
|
/// API_MAX_SEND_QUEUE bounds queued messages and, at 2 KB per slot, queued
|
||||||
/// The queue drains automatically on subsequent write/loop calls once the
|
/// bytes; exceeding either fails the connection.
|
||||||
/// socket becomes writable again.
|
|
||||||
///
|
|
||||||
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
|
|
||||||
/// config). If the queue fills completely the connection is marked failed.
|
|
||||||
class APIOverflowBuffer {
|
class APIOverflowBuffer {
|
||||||
public:
|
public:
|
||||||
/// A single heap-allocated send-backlog entry.
|
|
||||||
/// Lifetime is manually managed — see destroy().
|
|
||||||
struct Entry {
|
|
||||||
uint8_t *data;
|
|
||||||
uint16_t size; // Total size of the buffer
|
|
||||||
uint16_t offset; // Current send offset within the buffer
|
|
||||||
|
|
||||||
uint16_t remaining() const { return this->size - this->offset; }
|
|
||||||
const uint8_t *current_data() const { return this->data + this->offset; }
|
|
||||||
|
|
||||||
/// Free this entry and its data buffer.
|
|
||||||
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
|
|
||||||
delete[] entry->data;
|
|
||||||
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
~APIOverflowBuffer();
|
|
||||||
|
|
||||||
/// True when no backlogged data is waiting.
|
/// True when no backlogged data is waiting.
|
||||||
bool empty() const { return this->count_ == 0; }
|
bool empty() const { return this->count_ == 0; }
|
||||||
|
|
||||||
/// True when the queue has no room for another entry.
|
/// Drain queued messages to the socket.
|
||||||
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
|
/// Returns bytes written, 0 for a re-entrant call, -1 on error (check errno
|
||||||
|
/// for EWOULDBLOCK); callers only need to act on -1.
|
||||||
/// Number of entries currently queued.
|
|
||||||
uint8_t count() const { return this->count_; }
|
|
||||||
|
|
||||||
/// Try to drain queued data to the socket.
|
|
||||||
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
|
|
||||||
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
|
|
||||||
/// Callers only need to act on -1; 0 and positive values both mean "no error".
|
|
||||||
/// Frees entries as they are fully sent.
|
|
||||||
ssize_t try_drain(socket::Socket *socket);
|
ssize_t try_drain(socket::Socket *socket);
|
||||||
|
|
||||||
/// Enqueue unsent IOV data into the backlog.
|
/// Queue iov data from byte offset `skip` as one message.
|
||||||
/// Copies iov data starting at byte offset `skip` into a new entry.
|
/// Returns false when a limit is hit, allocation fails, or storage would move
|
||||||
/// Returns false if the queue is full or allocation fails (caller should fail the connection).
|
/// during a drain; the caller should fail the connection.
|
||||||
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
|
bool enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip);
|
||||||
|
|
||||||
|
/// Free the retained storage, now if empty, otherwise once it has drained.
|
||||||
|
void release() {
|
||||||
|
if (this->count_ == 0) {
|
||||||
|
this->buf_.release();
|
||||||
|
} else {
|
||||||
|
this->release_when_drained_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
|
static constexpr size_t LEN_PREFIX = 2;
|
||||||
uint8_t head_{0};
|
static constexpr size_t BYTES_PER_SLOT = 2048;
|
||||||
uint8_t tail_{0};
|
// Reserve in 256 byte steps so a creeping high-water mark settles quickly
|
||||||
|
static constexpr size_t GROW_QUANTUM = 256;
|
||||||
|
// Lone message ceiling, rounded down so reserve_for() never exceeds the buffer limit
|
||||||
|
static constexpr size_t MAX_LONE_BYTES = APIBuffer::MAX_SIZE & ~(GROW_QUANTUM - 1);
|
||||||
|
static constexpr size_t MAX_BYTES = std::min(API_MAX_SEND_QUEUE * BYTES_PER_SLOT, MAX_LONE_BYTES);
|
||||||
|
static constexpr size_t reserve_for(size_t want) { return (want + GROW_QUANTUM - 1) & ~(GROW_QUANTUM - 1); }
|
||||||
|
|
||||||
|
APIBuffer buf_;
|
||||||
|
uint16_t head_{0}; // offset of the front message's length prefix; bytes before it are sent
|
||||||
uint8_t count_{0};
|
uint8_t count_{0};
|
||||||
// Guards against re-entrant drains: socket->write() can re-enter the API
|
// socket->write() can re-enter the send path (log from an lwip callback):
|
||||||
// send path (e.g. a log message emitted from an lwip callback), and a nested
|
// a nested drain makes no progress and a nested enqueue never moves storage
|
||||||
// drain would free the entry the outer drain is still holding.
|
bool draining_ : 1 {false};
|
||||||
bool draining_{false};
|
bool release_when_drained_ : 1 {false};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -4253,6 +4253,19 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
|
|||||||
size += ProtoSize::calc_length(1, this->error_message.size());
|
size += ProtoSize::calc_length(1, this->error_message.size());
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1:
|
||||||
|
this->instance = value;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this->mode = static_cast<enums::SerialProxyMode>(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||||
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ enum SerialProxyRequestType : uint32_t {
|
|||||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
|
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
|
||||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
|
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
|
||||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
|
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
|
||||||
|
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5,
|
||||||
};
|
};
|
||||||
enum SerialProxyStatus : uint32_t {
|
enum SerialProxyStatus : uint32_t {
|
||||||
SERIAL_PROXY_STATUS_OK = 0,
|
SERIAL_PROXY_STATUS_OK = 0,
|
||||||
@@ -366,6 +367,10 @@ enum SerialProxyStatus : uint32_t {
|
|||||||
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
|
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
|
||||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
|
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
|
||||||
};
|
};
|
||||||
|
enum SerialProxyMode : uint32_t {
|
||||||
|
SERIAL_PROXY_MODE_RAW = 0,
|
||||||
|
SERIAL_PROXY_MODE_PROTOCOL = 1,
|
||||||
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace enums
|
} // namespace enums
|
||||||
@@ -3403,6 +3408,22 @@ class SerialProxyRequestResponse final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
|
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
|
||||||
|
public:
|
||||||
|
static constexpr uint16_t MESSAGE_TYPE = 152;
|
||||||
|
static constexpr uint8_t ESTIMATED_SIZE = 6;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
|
||||||
|
#endif
|
||||||
|
uint32_t instance{0};
|
||||||
|
enums::SerialProxyMode mode{};
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
const char *dump_to(DumpBuffer &out) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||||
|
};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||||
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
|
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
|
||||||
|
|||||||
@@ -854,6 +854,8 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
|
|||||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
|
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
|
||||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
|
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
|
||||||
|
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||||
|
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
|
||||||
default:
|
default:
|
||||||
return ESPHOME_PSTR("UNKNOWN");
|
return ESPHOME_PSTR("UNKNOWN");
|
||||||
}
|
}
|
||||||
@@ -878,6 +880,16 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
|||||||
return ESPHOME_PSTR("UNKNOWN");
|
return ESPHOME_PSTR("UNKNOWN");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
|
||||||
|
switch (value) {
|
||||||
|
case enums::SERIAL_PROXY_MODE_RAW:
|
||||||
|
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
|
||||||
|
case enums::SERIAL_PROXY_MODE_PROTOCOL:
|
||||||
|
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
|
||||||
|
default:
|
||||||
|
return ESPHOME_PSTR("UNKNOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||||
@@ -2805,6 +2817,12 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
|||||||
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
|
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
|
||||||
return out.c_str();
|
return out.c_str();
|
||||||
}
|
}
|
||||||
|
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
|
||||||
|
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
|
||||||
|
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||||
|
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
|
||||||
|
return out.c_str();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||||
|
|||||||
@@ -712,6 +712,17 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
|||||||
this->on_device_capabilities_request();
|
this->on_device_capabilities_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#ifdef USE_SERIAL_PROXY
|
||||||
|
case SerialProxySetModeRequest::MESSAGE_TYPE: {
|
||||||
|
SerialProxySetModeRequest msg;
|
||||||
|
msg.decode(msg_data, msg_size);
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
|
||||||
|
#endif
|
||||||
|
this->on_serial_proxy_set_mode_request(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,9 @@ class APIServerConnectionBase {
|
|||||||
void on_serial_proxy_request(const SerialProxyRequest &value){};
|
void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SERIAL_PROXY
|
||||||
|
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
|
||||||
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ class APIServer final : public Component,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 4-byte aligned types
|
// 4-byte aligned types
|
||||||
uint32_t reboot_timeout_{300000};
|
uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py
|
||||||
uint32_t last_connected_{0};
|
uint32_t last_connected_{0};
|
||||||
|
|
||||||
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
||||||
@@ -351,8 +351,8 @@ class APIServer final : public Component,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Group smaller types together
|
// Group smaller types together
|
||||||
uint16_t port_{6053};
|
uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py
|
||||||
uint16_t batch_delay_{100};
|
uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py
|
||||||
// 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};
|
||||||
|
|||||||
@@ -339,10 +339,7 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
# HTTPS streams verify the server against the root certificate bundle
|
# HTTPS streams verify the server against the root certificate bundle
|
||||||
require_certificate_bundle()
|
require_certificate_bundle()
|
||||||
|
|
||||||
add_idf_component(
|
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||||
name="esphome/esp-audio-libs",
|
|
||||||
ref="3.2.1",
|
|
||||||
)
|
|
||||||
|
|
||||||
data = _get_data()
|
data = _get_data()
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ esp_err_t AudioReader::add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &ou
|
|||||||
if (current_audio_file_ != nullptr) {
|
if (current_audio_file_ != nullptr) {
|
||||||
// A transfer buffer isn't ncessary for a local file
|
// A transfer buffer isn't ncessary for a local file
|
||||||
this->file_ring_buffer_ = output_ring_buffer.lock();
|
this->file_ring_buffer_ = output_ring_buffer.lock();
|
||||||
|
if (this->file_ring_buffer_ == nullptr) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_le
|
|||||||
|
|
||||||
void AudioTransferBuffer::clear_buffered_data() {
|
void AudioTransferBuffer::clear_buffered_data() {
|
||||||
this->buffer_length_ = 0;
|
this->buffer_length_ = 0;
|
||||||
if (this->ring_buffer_.use_count() > 0) {
|
if (this->ring_buffer_ != nullptr) {
|
||||||
this->ring_buffer_->reset();
|
this->ring_buffer_->reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioSinkTransferBuffer::clear_buffered_data() {
|
void AudioSinkTransferBuffer::clear_buffered_data() {
|
||||||
this->buffer_length_ = 0;
|
this->buffer_length_ = 0;
|
||||||
if (this->ring_buffer_.use_count() > 0) {
|
if (this->ring_buffer_ != nullptr) {
|
||||||
this->ring_buffer_->reset();
|
this->ring_buffer_->reset();
|
||||||
}
|
}
|
||||||
#ifdef USE_SPEAKER
|
#ifdef USE_SPEAKER
|
||||||
@@ -69,7 +69,7 @@ void AudioSinkTransferBuffer::clear_buffered_data() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool AudioTransferBuffer::has_buffered_data() const {
|
bool AudioTransferBuffer::has_buffered_data() const {
|
||||||
if (this->ring_buffer_.use_count() > 0) {
|
if (this->ring_buffer_ != nullptr) {
|
||||||
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
|
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
|
||||||
}
|
}
|
||||||
return (this->available() > 0);
|
return (this->available() > 0);
|
||||||
@@ -144,7 +144,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
|
|||||||
size_t bytes_to_read = AudioTransferBuffer::free();
|
size_t bytes_to_read = AudioTransferBuffer::free();
|
||||||
size_t bytes_read = 0;
|
size_t bytes_read = 0;
|
||||||
if (bytes_to_read > 0) {
|
if (bytes_to_read > 0) {
|
||||||
if (this->ring_buffer_.use_count() > 0) {
|
if (this->ring_buffer_ != nullptr) {
|
||||||
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
|
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
|
|||||||
bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
|
bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
|
||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
if (this->ring_buffer_.use_count() > 0) {
|
if (this->ring_buffer_ != nullptr) {
|
||||||
bytes_written =
|
bytes_written =
|
||||||
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
|
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
|
||||||
} else if (this->sink_callback_ != nullptr) {
|
} else if (this->sink_callback_ != nullptr) {
|
||||||
@@ -186,7 +186,7 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
|
|||||||
return (this->speaker_->has_buffered_data() || (this->available() > 0));
|
return (this->speaker_->has_buffered_data() || (this->available() > 0));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (this->ring_buffer_.use_count() > 0) {
|
if (this->ring_buffer_ != nullptr) {
|
||||||
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
|
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
|
||||||
}
|
}
|
||||||
return (this->available() > 0);
|
return (this->available() > 0);
|
||||||
|
|||||||
@@ -203,16 +203,10 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
|
|||||||
this->away_config_ = away_config;
|
this->away_config_ = away_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
|
||||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
|
||||||
|
|
||||||
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
|
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
|
||||||
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
|
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
|
||||||
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
|
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
|
||||||
|
|
||||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
|
||||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
|
||||||
|
|
||||||
void BangBangClimate::dump_config() {
|
void BangBangClimate::dump_config() {
|
||||||
LOG_CLIMATE("", "Bang Bang Climate", this);
|
LOG_CLIMATE("", "Bang Bang Climate", this);
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ class BangBangClimate final : public climate::Climate, public Component {
|
|||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
void set_sensor(sensor::Sensor *sensor);
|
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||||
void set_supports_cool(bool supports_cool);
|
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||||
void set_supports_heat(bool supports_heat);
|
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ class BinaryLightOutput final : public light::LightOutput {
|
|||||||
void write_state(light::LightState *state) override {
|
void write_state(light::LightState *state) override {
|
||||||
bool binary;
|
bool binary;
|
||||||
state->current_values_as_binary(&binary);
|
state->current_values_as_binary(&binary);
|
||||||
if (binary) {
|
this->output_->set_state(binary);
|
||||||
this->output_->turn_on();
|
|
||||||
} else {
|
|
||||||
this->output_->turn_off();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
|||||||
DEVICE_CLASS_EMPTY,
|
DEVICE_CLASS_EMPTY,
|
||||||
DEVICE_CLASS_GARAGE_DOOR,
|
DEVICE_CLASS_GARAGE_DOOR,
|
||||||
DEVICE_CLASS_GAS,
|
DEVICE_CLASS_GAS,
|
||||||
|
DEVICE_CLASS_GLASS_BREAK,
|
||||||
DEVICE_CLASS_HEAT,
|
DEVICE_CLASS_HEAT,
|
||||||
DEVICE_CLASS_LIGHT,
|
DEVICE_CLASS_LIGHT,
|
||||||
DEVICE_CLASS_LOCK,
|
DEVICE_CLASS_LOCK,
|
||||||
@@ -81,6 +82,7 @@ DEVICE_CLASSES = [
|
|||||||
DEVICE_CLASS_EMPTY,
|
DEVICE_CLASS_EMPTY,
|
||||||
DEVICE_CLASS_GARAGE_DOOR,
|
DEVICE_CLASS_GARAGE_DOOR,
|
||||||
DEVICE_CLASS_GAS,
|
DEVICE_CLASS_GAS,
|
||||||
|
DEVICE_CLASS_GLASS_BREAK,
|
||||||
DEVICE_CLASS_HEAT,
|
DEVICE_CLASS_HEAT,
|
||||||
DEVICE_CLASS_LIGHT,
|
DEVICE_CLASS_LIGHT,
|
||||||
DEVICE_CLASS_LOCK,
|
DEVICE_CLASS_LOCK,
|
||||||
@@ -452,7 +454,9 @@ _BINARY_SENSOR_SCHEMA = (
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||||
): validate_device_class,
|
): validate_device_class,
|
||||||
cv.Optional(CONF_FILTERS): validate_filters,
|
cv.Optional(
|
||||||
|
CONF_FILTERS, visibility=cv.Visibility.ADVANCED
|
||||||
|
): validate_filters,
|
||||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
||||||
cv.Optional(CONF_ON_CLICK): cv.All(
|
cv.Optional(CONF_ON_CLICK): cv.All(
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
|||||||
*/
|
*/
|
||||||
class BinarySensor : public StatefulEntityBase<bool> {
|
class BinarySensor : public StatefulEntityBase<bool> {
|
||||||
public:
|
public:
|
||||||
explicit BinarySensor() = default;
|
// User provided, not "= default": `new(p) BinarySensor()` would zero-fill .bss that is already zero.
|
||||||
|
explicit BinarySensor() {}
|
||||||
|
|
||||||
const bool &get_state() const override { return this->state; }
|
const bool &get_state() const override { return this->state; }
|
||||||
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
|
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ class DelayedOnOffFilter final : public Filter {
|
|||||||
|
|
||||||
class DelayedOnFilter : public Filter {
|
class DelayedOnFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
|
// User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero.
|
||||||
|
DelayedOnFilter() {}
|
||||||
|
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|
||||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||||
@@ -63,6 +66,9 @@ class DelayedOnFilter : public Filter {
|
|||||||
|
|
||||||
class DelayedOffFilter : public Filter {
|
class DelayedOffFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
|
// User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero.
|
||||||
|
DelayedOffFilter() {}
|
||||||
|
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|
||||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||||
@@ -143,6 +149,8 @@ class StatelessLambdaFilter : public Filter {
|
|||||||
|
|
||||||
class SettleFilter : public Filter {
|
class SettleFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
|
// User provided, not "= default": `new(p) SettleFilter()` would zero-fill .bss that is already zero.
|
||||||
|
SettleFilter() {}
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|
||||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public ble_api.h.
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import libretiny
|
from esphome.components import libretiny, wifi
|
||||||
from esphome.components.libretiny.const import (
|
from esphome.components.libretiny.const import (
|
||||||
FAMILY_BK7231N,
|
FAMILY_BK7231N,
|
||||||
FAMILY_BK7231Q,
|
FAMILY_BK7231Q,
|
||||||
@@ -84,6 +84,15 @@ def _final_validate(config: ConfigType) -> None:
|
|||||||
# which run on a BLE 4.2 board. The hard error is raised at codegen.
|
# which run on a BLE 4.2 board. The hard error is raised at codegen.
|
||||||
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
|
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
|
||||||
_LOGGER.warning("%s (this configuration cannot compile)", msg)
|
_LOGGER.warning("%s (this configuration cannot compile)", msg)
|
||||||
|
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
|
||||||
|
# sleep. With the BLE controller running, that sleep never wakes up once the
|
||||||
|
# station is stopped (adapter restart after failed roams, wifi.disable): the
|
||||||
|
# device is dead until a power cycle (esphome#18592). Keep power save off
|
||||||
|
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
|
||||||
|
wifi.force_power_save_off(
|
||||||
|
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
|
||||||
|
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|||||||
@@ -45,15 +45,7 @@ void BluedroidGattClient::setup() {
|
|||||||
|
|
||||||
void BluedroidGattClient::loop() {
|
void BluedroidGattClient::loop() {
|
||||||
if (!esp32_ble::global_ble->is_active()) {
|
if (!esp32_ble::global_ble->is_active()) {
|
||||||
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
|
// ble_before_disabled_event_handler() settles the slot.
|
||||||
// frees its slot, then re-register the app on the next enable.
|
|
||||||
auto down_st = this->state();
|
|
||||||
if (down_st != ClientState::IDLE && down_st != ClientState::INIT) {
|
|
||||||
this->release_services();
|
|
||||||
this->set_idle_();
|
|
||||||
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
|
|
||||||
}
|
|
||||||
this->set_state(ClientState::INIT);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto st = this->state();
|
auto st = this->state();
|
||||||
@@ -65,7 +57,7 @@ void BluedroidGattClient::loop() {
|
|||||||
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret);
|
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret);
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
}
|
}
|
||||||
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
|
// Do not wait for REG_EVT; connect() rejects until it lands.
|
||||||
this->set_idle_();
|
this->set_idle_();
|
||||||
} else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
|
} else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
|
||||||
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
|
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
|
||||||
@@ -78,8 +70,8 @@ void BluedroidGattClient::loop() {
|
|||||||
this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT);
|
this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The loop stays on while a link exists (stack-down watch, pre-started
|
// The loop stays on while a link exists (pre-started search flush); it
|
||||||
// search flush); it settles only back at IDLE.
|
// settles only back at IDLE.
|
||||||
this->deliver_pending_search_();
|
this->deliver_pending_search_();
|
||||||
if (this->state() == ClientState::IDLE) {
|
if (this->state() == ClientState::IDLE) {
|
||||||
this->disable_loop();
|
this->disable_loop();
|
||||||
@@ -87,6 +79,22 @@ void BluedroidGattClient::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
|
||||||
|
// frees its slot, then register the app again on the next enable.
|
||||||
|
void BluedroidGattClient::ble_before_disabled_event_handler() {
|
||||||
|
auto st = this->state();
|
||||||
|
if (st != ClientState::IDLE && st != ClientState::INIT) {
|
||||||
|
this->release_services();
|
||||||
|
this->set_idle_();
|
||||||
|
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
|
||||||
|
}
|
||||||
|
// The interface belongs to the torn-down stack.
|
||||||
|
this->gattc_if_ = ESP_GATT_IF_NONE;
|
||||||
|
this->set_state(ClientState::INIT);
|
||||||
|
// An idle slot runs no loop; the INIT branch must run to register again.
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
void BluedroidGattClient::dump_config() {
|
void BluedroidGattClient::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
|
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
|
||||||
if (this->is_failed()) {
|
if (this->is_failed()) {
|
||||||
@@ -97,6 +105,11 @@ void BluedroidGattClient::dump_config() {
|
|||||||
// ---- contract ops ----
|
// ---- contract ops ----
|
||||||
|
|
||||||
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
|
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
|
||||||
|
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
|
||||||
|
// Bluedroid drops an open on an unknown interface without any event.
|
||||||
|
ESP_LOGW(TAG, "[%d] Connect rejected, GATT app not registered", this->connection_index_);
|
||||||
|
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||||
|
}
|
||||||
// Only from idle: clobbering DISCONNECTING would open a new link the
|
// Only from idle: clobbering DISCONNECTING would open a new link the
|
||||||
// stale CLOSE_EVT then tears down.
|
// stale CLOSE_EVT then tears down.
|
||||||
if (this->state() != ClientState::IDLE) {
|
if (this->state() != ClientState::IDLE) {
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class BluetoothConnection;
|
|||||||
// void disconnect() cannot overload with an int-returning twin.
|
// void disconnect() cannot overload with an int-returning twin.
|
||||||
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
|
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
|
||||||
public:
|
public:
|
||||||
|
// User provided, not "= default": `new(p) BluedroidGattClient()` would zero-fill .bss that is already zero.
|
||||||
|
BluedroidGattClient() {}
|
||||||
|
|
||||||
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
|
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
|
||||||
|
|
||||||
// Lifecycle of one connection attempt's service search.
|
// Lifecycle of one connection attempt's service search.
|
||||||
@@ -56,6 +59,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
|
|||||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||||
void connect() override;
|
void connect() override;
|
||||||
void disconnect() override;
|
void disconnect() override;
|
||||||
|
void ble_before_disabled_event_handler() override;
|
||||||
bool wants_parsed_advertisements() override { return false; }
|
bool wants_parsed_advertisements() override { return false; }
|
||||||
void on_scan_end() override {}
|
void on_scan_end() override {}
|
||||||
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
|
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ enum class PendingAck : uint8_t {
|
|||||||
|
|
||||||
class BluetoothConnection final : public ble_device_base::GattClientListener {
|
class BluetoothConnection final : public ble_device_base::GattClientListener {
|
||||||
public:
|
public:
|
||||||
|
// User provided, not "= default": `new(p) BluetoothConnection()` would zero-fill .bss that is already zero.
|
||||||
|
BluetoothConnection() {}
|
||||||
|
|
||||||
/// Wire the platform backend. Called from codegen before setup.
|
/// Wire the platform backend. Called from codegen before setup.
|
||||||
void set_backend(ble_device_base::BLEGattConnection *backend) {
|
void set_backend(ble_device_base::BLEGattConnection *backend) {
|
||||||
this->backend_ = backend;
|
this->backend_ = backend;
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
|
|||||||
// explicit kick the MTU would only be exchanged on the first GATT query,
|
// explicit kick the MTU would only be exchanged on the first GATT query,
|
||||||
// which never happens on a V3_WITH_CACHE connection.
|
// which never happens on a V3_WITH_CACHE connection.
|
||||||
// Both registration calls above return void (BTstack 075a078, arduino-pico
|
// Both registration calls above return void (BTstack 075a078, arduino-pico
|
||||||
// 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
// 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||||
// the connect timeout in loop().
|
// the connect timeout in loop().
|
||||||
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
|
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,6 +395,17 @@ async def _to_code_ble_hub(config: ConfigType) -> None:
|
|||||||
await _connections_to_code(var, config)
|
await _connections_to_code(var, config)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_advertisement_filter() -> None:
|
||||||
|
"""Compile the advertisement filter hook into bluetooth_proxy.
|
||||||
|
|
||||||
|
Called by external filtering components from to_code(). The define behind
|
||||||
|
this is an implementation detail; do not emit it directly.
|
||||||
|
|
||||||
|
Public API for external components. Do not remove.
|
||||||
|
"""
|
||||||
|
cg.add_define("USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER")
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
await _to_code_esp32(config)
|
await _to_code_esp32(config)
|
||||||
|
|||||||
@@ -94,6 +94,15 @@ void BluetoothProxy::on_raw_advertisement_(const ble_device_base::RawAdvertiseme
|
|||||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||||
|
// Ask the filter before the packet is queued, so a dropped advertisement never
|
||||||
|
// reaches the batch or the network.
|
||||||
|
if (this->advertisement_filter_.is_set() && !this->advertisement_filter_.should_forward(raw)) {
|
||||||
|
ESP_LOGVV(TAG, "Filtered packet from %012" PRIX64, raw.address);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
auto &adv = this->response_.advertisements[this->response_.advertisements_len];
|
auto &adv = this->response_.advertisements[this->response_.advertisements_len];
|
||||||
adv.address = raw.address;
|
adv.address = raw.address;
|
||||||
adv.rssi = raw.rssi;
|
adv.rssi = raw.rssi;
|
||||||
@@ -184,6 +193,9 @@ void BluetoothProxy::dump_config() {
|
|||||||
" Adapter MAC: %s",
|
" Adapter MAC: %s",
|
||||||
scan_mode, mac_out);
|
scan_mode, mac_out);
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||||
|
ESP_LOGCONFIG(TAG, " Advertisement filter: %s", YESNO(this->advertisement_filter_.is_set()));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||||
|
|||||||
@@ -97,6 +97,29 @@ static_assert(pending_reply_round_trips(0xABCD112233445566ULL, 0x000011223344556
|
|||||||
static_assert(PendingReply{}.empty());
|
static_assert(PendingReply{}.empty());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||||
|
/// Predicate slot letting an external component drop advertisements before they
|
||||||
|
/// are queued for the API. Same shape as
|
||||||
|
/// ble_device_base::RawAdvertisementCallback. Runs on the advertisement hot
|
||||||
|
/// path, so it must be cheap and must not block.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// proxy->set_advertisement_filter({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
|
||||||
|
/// return static_cast<MyFilter *>(self)->should_forward(adv);
|
||||||
|
/// }});
|
||||||
|
///
|
||||||
|
/// Returning false drops the advertisement. Not called at all while the API is
|
||||||
|
/// disconnected, which matters to a stateful filter. Compiled in only when an
|
||||||
|
/// external component calls bluetooth_proxy.enable_advertisement_filter().
|
||||||
|
struct AdvertisementFilter {
|
||||||
|
void *instance{nullptr};
|
||||||
|
bool (*fn)(void *instance, const ble_device_base::RawAdvertisement &adv){nullptr};
|
||||||
|
/// A default-constructed slot is "no filter"; the proxy guards on this.
|
||||||
|
bool is_set() const { return this->fn != nullptr; }
|
||||||
|
bool should_forward(const ble_device_base::RawAdvertisement &adv) const { return this->fn(this->instance, adv); }
|
||||||
|
};
|
||||||
|
#endif // USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||||
|
|
||||||
class BluetoothProxy final : public Component {
|
class BluetoothProxy final : public Component {
|
||||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||||
// Allow the connection to update connections_free_response_
|
// Allow the connection to update connections_free_response_
|
||||||
@@ -162,6 +185,11 @@ class BluetoothProxy final : public Component {
|
|||||||
void set_active(bool active) { this->active_ = active; }
|
void set_active(bool active) { this->active_ = active; }
|
||||||
bool has_active() { return this->active_; }
|
bool has_active() { return this->active_; }
|
||||||
|
|
||||||
|
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||||
|
/// One subscriber; a later call replaces an earlier one.
|
||||||
|
void set_advertisement_filter(AdvertisementFilter filter) { this->advertisement_filter_ = filter; }
|
||||||
|
#endif
|
||||||
|
|
||||||
uint32_t get_legacy_version() const {
|
uint32_t get_legacy_version() const {
|
||||||
if (!this->active_) {
|
if (!this->active_) {
|
||||||
return LEGACY_PASSIVE_ONLY_VERSION;
|
return LEGACY_PASSIVE_ONLY_VERSION;
|
||||||
@@ -330,6 +358,10 @@ class BluetoothProxy final : public Component {
|
|||||||
// start on an even word, closing two alignment holes.
|
// start on an even word, closing two alignment holes.
|
||||||
uint32_t last_advertisement_flush_time_{0};
|
uint32_t last_advertisement_flush_time_{0};
|
||||||
|
|
||||||
|
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||||
|
AdvertisementFilter advertisement_filter_{};
|
||||||
|
#endif
|
||||||
|
|
||||||
// BLE advertisement batching
|
// BLE advertisement batching
|
||||||
api::BluetoothLERawAdvertisementsResponse response_;
|
api::BluetoothLERawAdvertisementsResponse response_;
|
||||||
|
|
||||||
|
|||||||
@@ -341,7 +341,6 @@ void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over
|
|||||||
void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
|
void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
|
||||||
this->humidity_oversampling_ = humidity_over_sampling;
|
this->humidity_oversampling_ = humidity_over_sampling;
|
||||||
}
|
}
|
||||||
void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
|
||||||
uint8_t BME280Component::read_u8_(uint8_t a_register) {
|
uint8_t BME280Component::read_u8_(uint8_t a_register) {
|
||||||
uint8_t data = 0;
|
uint8_t data = 0;
|
||||||
this->read_byte(a_register, &data);
|
this->read_byte(a_register, &data);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class BME280Component : public PollingComponent {
|
|||||||
/// Set the oversampling value for the humidity sensor. Default is 16x.
|
/// Set the oversampling value for the humidity sensor. Default is 16x.
|
||||||
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling);
|
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling);
|
||||||
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
|
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
|
||||||
void set_iir_filter(BME280IIRFilter iir_filter);
|
void set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||||
|
|
||||||
// ========== INTERNAL METHODS ==========
|
// ========== INTERNAL METHODS ==========
|
||||||
// (In most use cases you won't need these)
|
// (In most use cases you won't need these)
|
||||||
|
|||||||
@@ -503,7 +503,6 @@ void BME680Component::set_pressure_oversampling(BME680Oversampling pressure_over
|
|||||||
void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) {
|
void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) {
|
||||||
this->humidity_oversampling_ = humidity_oversampling;
|
this->humidity_oversampling_ = humidity_oversampling;
|
||||||
}
|
}
|
||||||
void BME680Component::set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
|
||||||
void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) {
|
void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) {
|
||||||
this->heater_temperature_ = heater_temperature;
|
this->heater_temperature_ = heater_temperature;
|
||||||
this->heater_duration_ = heater_duration;
|
this->heater_duration_ = heater_duration;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
|
|||||||
/// Set the humidity oversampling value. Defaults to 16X.
|
/// Set the humidity oversampling value. Defaults to 16X.
|
||||||
void set_humidity_oversampling(BME680Oversampling humidity_oversampling);
|
void set_humidity_oversampling(BME680Oversampling humidity_oversampling);
|
||||||
/// Set the IIR Filter value. Defaults to no IIR Filter.
|
/// Set the IIR Filter value. Defaults to no IIR Filter.
|
||||||
void set_iir_filter(BME680IIRFilter iir_filter);
|
void set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||||
|
|
||||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
|
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
|
||||||
|
|||||||
@@ -254,7 +254,6 @@ void BMP280Component::set_temperature_oversampling(BMP280Oversampling temperatur
|
|||||||
void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) {
|
void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) {
|
||||||
this->pressure_oversampling_ = pressure_over_sampling;
|
this->pressure_oversampling_ = pressure_over_sampling;
|
||||||
}
|
}
|
||||||
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
|
||||||
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
|
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
|
||||||
uint8_t data = 0;
|
uint8_t data = 0;
|
||||||
this->bmp_read_byte(a_register, &data);
|
this->bmp_read_byte(a_register, &data);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class BMP280Component : public PollingComponent {
|
|||||||
/// Set the oversampling value for the pressure sensor. Default is 16x.
|
/// Set the oversampling value for the pressure sensor. Default is 16x.
|
||||||
void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling);
|
void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling);
|
||||||
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
|
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
|
||||||
void set_iir_filter(BMP280IIRFilter iir_filter);
|
void set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||||
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
CODEOWNERS = ["@kbx81"]
|
||||||
|
DOMAIN = "bridge"
|
||||||
|
|
||||||
|
IS_PLATFORM_COMPONENT = True
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@kbx81"]
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
from esphome import pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import esp32, uart, usb_cdc_acm
|
||||||
|
from esphome.components.bridge import DOMAIN as BRIDGE_DOMAIN
|
||||||
|
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_DEBUG, CONF_ID, CONF_UART_ID
|
||||||
|
import esphome.final_validate as fv
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
|
CODEOWNERS = ["@kbx81"]
|
||||||
|
DEPENDENCIES = ["tinyusb", "uart", "usb_cdc_acm"]
|
||||||
|
|
||||||
|
CONF_DTR_PIN = "dtr_pin"
|
||||||
|
CONF_RTS_PIN = "rts_pin"
|
||||||
|
CONF_USB_CDC_ACM_ID = "usb_cdc_acm_id"
|
||||||
|
|
||||||
|
cdc_acm_uart_ns = cg.esphome_ns.namespace("cdc_acm_uart")
|
||||||
|
CDCACMUARTBridge = cdc_acm_uart_ns.class_("CDCACMUARTBridge", cg.Component)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(CDCACMUARTBridge),
|
||||||
|
cv.Required(CONF_UART_ID): cv.use_id(uart.IDFUARTComponent),
|
||||||
|
cv.Required(CONF_USB_CDC_ACM_ID): cv.use_id(usb_cdc_acm.USBCDCACMInstance),
|
||||||
|
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
# Narrower than usb_cdc_acm's variant list on purpose: S31/H4 untested on
|
||||||
|
# hardware; extend once verified.
|
||||||
|
esp32.only_on_variant(
|
||||||
|
supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _subtree_references_uart(node: object, uart_id: str) -> bool:
|
||||||
|
"""Return True if any dict in the subtree has a uart_id entry naming this bus."""
|
||||||
|
if isinstance(node, dict):
|
||||||
|
return any(
|
||||||
|
(key == CONF_UART_ID and str(value) == uart_id)
|
||||||
|
or _subtree_references_uart(value, uart_id)
|
||||||
|
for key, value in node.items()
|
||||||
|
)
|
||||||
|
if isinstance(node, list):
|
||||||
|
return any(_subtree_references_uart(item, uart_id) for item in node)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_debug(uart_conf: ConfigType) -> ConfigType:
|
||||||
|
# The worker tasks use the IDF driver directly, so the uart debugger never sees
|
||||||
|
# bridge traffic and its dummy_receiver would drain RX bytes on the main loop.
|
||||||
|
if CONF_DEBUG in uart_conf:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"A bridged UART cannot use 'debug'; the bridge bypasses the UART "
|
||||||
|
"component's read/write path.",
|
||||||
|
[CONF_DEBUG],
|
||||||
|
)
|
||||||
|
return uart_conf
|
||||||
|
|
||||||
|
|
||||||
|
def _final_validate(config: ConfigType) -> ConfigType:
|
||||||
|
full_config = fv.full_config.get()
|
||||||
|
# Bridges of any platform must own their interfaces exclusively; shared ring
|
||||||
|
# buffers and overwritten callbacks would corrupt both streams silently. The
|
||||||
|
# seen-set is keyed on the bridge domain so future platforms share it.
|
||||||
|
# Other components bind either interface through the same uart_id key (the CDC
|
||||||
|
# instance is itself a uart::UARTComponent) and would race the worker tasks.
|
||||||
|
# Bare `id:` references (a uart.write action) cannot be distinguished; not caught.
|
||||||
|
data = full_config.data.setdefault(BRIDGE_DOMAIN, {})
|
||||||
|
for conf_key, label in (
|
||||||
|
(CONF_UART_ID, "UART"),
|
||||||
|
(CONF_USB_CDC_ACM_ID, "USB CDC-ACM interface"),
|
||||||
|
):
|
||||||
|
owned_id = str(config[conf_key])
|
||||||
|
used = data.setdefault(conf_key, set())
|
||||||
|
if owned_id in used:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"The {label} '{owned_id}' is already bridged by another 'bridge' "
|
||||||
|
f"instance; each bridge requires its own {label}.",
|
||||||
|
[conf_key],
|
||||||
|
)
|
||||||
|
used.add(owned_id)
|
||||||
|
for domain, domain_conf in full_config.items():
|
||||||
|
if domain == BRIDGE_DOMAIN:
|
||||||
|
continue
|
||||||
|
if _subtree_references_uart(domain_conf, owned_id):
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"The {label} '{owned_id}' is also used by '{domain}'; a bridge "
|
||||||
|
f"requires exclusive use of its {label}.",
|
||||||
|
[conf_key],
|
||||||
|
)
|
||||||
|
|
||||||
|
fv.id_declaration_match_schema(_reject_debug)(config[CONF_UART_ID])
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config: ConfigType) -> None:
|
||||||
|
uart_component = await cg.get_variable(config[CONF_UART_ID])
|
||||||
|
usb_cdc = await cg.get_variable(config[CONF_USB_CDC_ACM_ID])
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID], uart_component, usb_cdc)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
if dtr_pin_config := config.get(CONF_DTR_PIN):
|
||||||
|
dtr_pin = await cg.gpio_pin_expression(dtr_pin_config)
|
||||||
|
cg.add(var.set_dtr_pin(dtr_pin))
|
||||||
|
if rts_pin_config := config.get(CONF_RTS_PIN):
|
||||||
|
rts_pin = await cg.gpio_pin_expression(rts_pin_config)
|
||||||
|
cg.add(var.set_rts_pin(rts_pin))
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||||
|
#include "cdc_acm_uart_bridge.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/ringbuf.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#include "soc/soc_caps.h"
|
||||||
|
|
||||||
|
namespace esphome::cdc_acm_uart {
|
||||||
|
|
||||||
|
static const char *const TAG = "cdc_acm_uart";
|
||||||
|
|
||||||
|
static constexpr size_t UART_TASK_STACK_SIZE = 4096;
|
||||||
|
static constexpr size_t RINGBUF_RETRY_CHUNK_SIZE = 64;
|
||||||
|
static constexpr uint32_t LOG_THROTTLE_MS = 1000;
|
||||||
|
static constexpr uint32_t UART_RELOAD_SETTLE_MS = 20;
|
||||||
|
// Above the default priority but below the USB/Wi-Fi system tasks.
|
||||||
|
static constexpr UBaseType_t TASK_PRIORITY = 4;
|
||||||
|
|
||||||
|
static bool should_log_now(uint32_t *last_ms, uint32_t interval_ms) {
|
||||||
|
uint32_t now = millis();
|
||||||
|
if ((now - *last_ms) >= interval_ms) {
|
||||||
|
*last_ms = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ringbuf_send_with_retry(RingbufHandle_t ringbuf, const uint8_t *data, size_t len, uint32_t *log_ms) {
|
||||||
|
if (len == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xRingbufferSend(ringbuf, data, len, pdMS_TO_TICKS(1)) == pdTRUE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
while (offset < len) {
|
||||||
|
size_t chunk = std::min(RINGBUF_RETRY_CHUNK_SIZE, len - offset);
|
||||||
|
if (xRingbufferSend(ringbuf, data + offset, chunk, pdMS_TO_TICKS(1)) != pdTRUE) {
|
||||||
|
if (should_log_now(log_ms, LOG_THROTTLE_MS)) {
|
||||||
|
ESP_LOGW(TAG, "USB TX buffer full; some data is lost");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
offset += chunk;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::setup() {
|
||||||
|
// Line state starts deasserted (no host yet); active-low DTR#/RTS# wiring is
|
||||||
|
// handled by configuring the pins inverted, so deasserted idles HIGH.
|
||||||
|
if (this->dtr_pin_ != nullptr) {
|
||||||
|
this->dtr_pin_->setup();
|
||||||
|
this->dtr_pin_->digital_write(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->rts_pin_ != nullptr) {
|
||||||
|
this->rts_pin_->setup();
|
||||||
|
this->rts_pin_->digital_write(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failed UART never assigned its port number, so the worker tasks would run
|
||||||
|
// against an indeterminate port.
|
||||||
|
if (this->uart_parent_->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, "UART parent failed; aborting");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->configured_baud_rate_ = this->uart_parent_->get_baud_rate();
|
||||||
|
this->configured_parity_ = this->uart_parent_->get_parity();
|
||||||
|
this->configured_stop_bits_ = this->uart_parent_->get_stop_bits();
|
||||||
|
this->configured_data_bits_ = this->uart_parent_->get_data_bits();
|
||||||
|
|
||||||
|
// usb_cdc_acm sets up first (priority IO > HARDWARE). Any interface failing marks
|
||||||
|
// the hub failed, and a failed hub no longer runs loop(), so line coding and line
|
||||||
|
// state events would never reach this bridge even if its own interface is healthy.
|
||||||
|
if (this->usb_cdc_parent_->get_parent()->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, "USB CDC ACM failed; aborting");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-instance task names (keyed on the CDC interface number) keep task dumps
|
||||||
|
// unambiguous with multiple bridges.
|
||||||
|
char tx_task_name[] = "cdc_uart_tx_0";
|
||||||
|
char rx_task_name[] = "cdc_uart_rx_0";
|
||||||
|
const char itf_char = format_hex_char(this->usb_cdc_parent_->get_itf());
|
||||||
|
tx_task_name[sizeof(tx_task_name) - 2] = itf_char;
|
||||||
|
rx_task_name[sizeof(rx_task_name) - 2] = itf_char;
|
||||||
|
|
||||||
|
xTaskCreate(uart_tx_task_fn, tx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_tx_task_handle_);
|
||||||
|
if (this->uart_tx_task_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create UART TX task");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xTaskCreate(uart_rx_task_fn, rx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_rx_task_handle_);
|
||||||
|
if (this->uart_rx_task_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create UART RX task");
|
||||||
|
vTaskDelete(this->uart_tx_task_handle_);
|
||||||
|
this->uart_tx_task_handle_ = nullptr;
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only register callbacks once both tasks exist, so a failed setup never drives
|
||||||
|
// DTR/RTS from a dead bridge.
|
||||||
|
this->usb_cdc_parent_->set_line_state_callback([this](bool dtr, bool rts) { this->set_line_state(dtr, rts); });
|
||||||
|
this->usb_cdc_parent_->set_line_coding_callback([this](uint32_t, uint8_t, uint8_t, uint8_t) {
|
||||||
|
this->host_coding_seen_ = true;
|
||||||
|
// Another component owns the UART's framing while paused; resume() re-syncs.
|
||||||
|
if (this->paused_ == 0) {
|
||||||
|
this->set_line_coding();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Release the workers only now: until here a failed setup may still delete the TX
|
||||||
|
// task, which is safe only while it is parked and owns nothing in the driver.
|
||||||
|
xTaskNotifyGive(this->uart_tx_task_handle_);
|
||||||
|
xTaskNotifyGive(this->uart_rx_task_handle_);
|
||||||
|
|
||||||
|
// loop() only services line-coding reloads; stay off the main loop until one is
|
||||||
|
// scheduled.
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG,
|
||||||
|
"CDC-ACM UART Bridge:\n"
|
||||||
|
" UART Bus: %u\n"
|
||||||
|
" USB CDC Interface: %u",
|
||||||
|
this->uart_parent_->get_hw_serial_number(), this->usb_cdc_parent_->get_itf());
|
||||||
|
LOG_PIN(" DTR Pin: ", this->dtr_pin_);
|
||||||
|
LOG_PIN(" RTS Pin: ", this->rts_pin_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::on_shutdown() {
|
||||||
|
// The UART (BUS) shuts down after this component (HARDWARE) and deletes its driver,
|
||||||
|
// freeing the ring buffer and mutexes the worker tasks block on. Suspending the
|
||||||
|
// tasks unlinks them from those objects first.
|
||||||
|
if (this->uart_rx_task_handle_ != nullptr) {
|
||||||
|
vTaskSuspend(this->uart_rx_task_handle_);
|
||||||
|
}
|
||||||
|
if (this->uart_tx_task_handle_ != nullptr) {
|
||||||
|
vTaskSuspend(this->uart_tx_task_handle_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::loop() {
|
||||||
|
switch (this->state_) {
|
||||||
|
case MainState::MAIN_STATE_RELOAD_PENDING:
|
||||||
|
if ((App.get_loop_component_start_time() - this->reload_requested_at_) < UART_RELOAD_SETTLE_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Deliberately not gated on tx_idle_(): a host that re-codes the line mid-stream
|
||||||
|
// wants the new framing now, and its own in-flight bytes are its concern.
|
||||||
|
// apply_settings_live() rewrites the framing registers without reinstalling the
|
||||||
|
// driver, so the worker tasks blocked inside it are undisturbed.
|
||||||
|
this->uart_parent_->apply_settings_live();
|
||||||
|
this->state_ = MainState::MAIN_STATE_RUNNING;
|
||||||
|
break;
|
||||||
|
case MainState::MAIN_STATE_PAUSING:
|
||||||
|
case MainState::MAIN_STATE_RESUMING:
|
||||||
|
// Let a host write that was in flight drain, FIFO included, before a reload
|
||||||
|
// flushes the FIFOs and truncates it.
|
||||||
|
if (!this->tx_idle_()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->state_ == MainState::MAIN_STATE_PAUSING) {
|
||||||
|
this->restore_configured_framing_();
|
||||||
|
this->state_ = MainState::MAIN_STATE_PAUSED;
|
||||||
|
} else {
|
||||||
|
this->finish_resume_();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::set_line_coding() {
|
||||||
|
if (!this->sync_host_framing_()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Coalesce rapid line-coding updates from the host.
|
||||||
|
this->reload_requested_at_ = App.get_loop_component_start_time();
|
||||||
|
this->state_ = MainState::MAIN_STATE_RELOAD_PENDING;
|
||||||
|
// Main-loop context (via USBCDCACMInstance::process_events_).
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CDCACMUARTBridge::sync_host_framing_() {
|
||||||
|
// usb_cdc_acm has already translated the wire coding onto the CDC instance (main
|
||||||
|
// loop); mirror it here so the framing translation has a single source of truth.
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
// Reject 0 (the CDC B0/hang-up encoding; older IDF revisions divide by the rate)
|
||||||
|
// and rates above the SoC ceiling. Anything in between is the driver's call,
|
||||||
|
// matching what a YAML-configured UART accepts.
|
||||||
|
const uint32_t baud = this->usb_cdc_parent_->get_baud_rate();
|
||||||
|
if (baud == 0 || baud > SOC_UART_BITRATE_MAX) {
|
||||||
|
ESP_LOGW(TAG, "Ignoring unsupported baud rate %" PRIu32 " from host; keeping %" PRIu32, baud,
|
||||||
|
this->uart_parent_->get_baud_rate());
|
||||||
|
} else if (this->uart_parent_->get_baud_rate() != baud) {
|
||||||
|
this->uart_parent_->set_baud_rate(baud);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t stop_bits = this->usb_cdc_parent_->get_stop_bits();
|
||||||
|
if (this->uart_parent_->get_stop_bits() != stop_bits) {
|
||||||
|
this->uart_parent_->set_stop_bits(stop_bits);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto parity = this->usb_cdc_parent_->get_parity();
|
||||||
|
if (this->uart_parent_->get_parity() != parity) {
|
||||||
|
this->uart_parent_->set_parity(parity);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// USB CDC permits data-bit counts the UART cannot represent (up to 16).
|
||||||
|
const uint8_t data_bits = this->usb_cdc_parent_->get_data_bits();
|
||||||
|
if (data_bits < 5 || data_bits > 8) {
|
||||||
|
ESP_LOGW(TAG, "Ignoring unsupported data bits %u from host; keeping %u", data_bits,
|
||||||
|
this->uart_parent_->get_data_bits());
|
||||||
|
} else if (this->uart_parent_->get_data_bits() != data_bits) {
|
||||||
|
this->uart_parent_->set_data_bits(data_bits);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
ESP_LOGV(TAG, "Line coding: baud=%" PRIu32 ", data_bits=%u, stop_bits=%u, parity=%u",
|
||||||
|
this->uart_parent_->get_baud_rate(), this->uart_parent_->get_data_bits(),
|
||||||
|
this->uart_parent_->get_stop_bits(), static_cast<uint8_t>(this->uart_parent_->get_parity()));
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::pause() {
|
||||||
|
if (this->state_ == MainState::MAIN_STATE_PAUSING || this->state_ == MainState::MAIN_STATE_PAUSED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->paused_ = 1;
|
||||||
|
// A null RX task means setup() has not completed (or failed): nothing to stop, and
|
||||||
|
// the framing snapshot does not exist yet. Should setup() run later, the RX task
|
||||||
|
// starts parked.
|
||||||
|
if (this->uart_rx_task_handle_ == nullptr) {
|
||||||
|
this->state_ = MainState::MAIN_STATE_PAUSED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Drops a coalesced host reload or a pending resume; loop() restores the framing
|
||||||
|
// once any host write in flight has drained.
|
||||||
|
this->state_ = MainState::MAIN_STATE_PAUSING;
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::resume() {
|
||||||
|
if (this->state_ != MainState::MAIN_STATE_PAUSING && this->state_ != MainState::MAIN_STATE_PAUSED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->uart_rx_task_handle_ == nullptr) {
|
||||||
|
this->paused_ = 0;
|
||||||
|
this->state_ = MainState::MAIN_STATE_RUNNING;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A restore still waiting on the TX side is moot: the host's framing is kept.
|
||||||
|
if (!this->tx_idle_()) {
|
||||||
|
this->state_ = MainState::MAIN_STATE_RESUMING;
|
||||||
|
this->enable_loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->finish_resume_();
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::finish_resume_() {
|
||||||
|
// Take the bus back at a known framing before either task runs again: the host's
|
||||||
|
// if it ever sent one, else the YAML framing (the other owner may have changed it).
|
||||||
|
if (this->host_coding_seen_) {
|
||||||
|
this->sync_host_framing_();
|
||||||
|
this->uart_parent_->apply_settings_live();
|
||||||
|
} else {
|
||||||
|
this->restore_configured_framing_();
|
||||||
|
}
|
||||||
|
this->paused_ = 0;
|
||||||
|
this->state_ = MainState::MAIN_STATE_RUNNING;
|
||||||
|
this->drive_line_state_();
|
||||||
|
xTaskNotifyGive(this->uart_rx_task_handle_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CDCACMUARTBridge::tx_idle_() {
|
||||||
|
const auto uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
|
||||||
|
return this->tx_busy_ == 0 && uart_wait_tx_done(uart_num, 0) == ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::restore_configured_framing_() {
|
||||||
|
// Always applied: the cached settings can lead the hardware by a pending reload,
|
||||||
|
// so they are no proof of what is live.
|
||||||
|
this->uart_parent_->set_baud_rate(this->configured_baud_rate_);
|
||||||
|
this->uart_parent_->set_parity(this->configured_parity_);
|
||||||
|
this->uart_parent_->set_stop_bits(this->configured_stop_bits_);
|
||||||
|
this->uart_parent_->set_data_bits(this->configured_data_bits_);
|
||||||
|
this->uart_parent_->apply_settings_live();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::set_line_state(bool dtr, bool rts) {
|
||||||
|
ESP_LOGV(TAG, "Line state: DTR=%d, RTS=%d", dtr, rts);
|
||||||
|
this->host_dtr_ = dtr;
|
||||||
|
this->host_rts_ = rts;
|
||||||
|
// Frozen while paused: a host opening the port must not reset a peer that another
|
||||||
|
// component is talking to.
|
||||||
|
if (this->paused_ == 0) {
|
||||||
|
this->drive_line_state_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::drive_line_state_() {
|
||||||
|
if (this->dtr_pin_ != nullptr) {
|
||||||
|
this->dtr_pin_->digital_write(this->host_dtr_);
|
||||||
|
}
|
||||||
|
if (this->rts_pin_ != nullptr) {
|
||||||
|
this->rts_pin_->digital_write(this->host_rts_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::uart_rx_task_fn(void *arg) {
|
||||||
|
auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
|
||||||
|
bridge->uart_rx_task_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::uart_tx_task_fn(void *arg) {
|
||||||
|
auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
|
||||||
|
bridge->uart_tx_task_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::uart_rx_task_() {
|
||||||
|
TaskHandle_t usb_tx_handle = this->usb_cdc_parent_->get_tx_task_handle();
|
||||||
|
RingbufHandle_t usb_tx_ringbuf = this->usb_cdc_parent_->get_tx_ringbuf();
|
||||||
|
uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
|
||||||
|
// Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
|
||||||
|
uint32_t tx_full_log_ms = millis() - LOG_THROTTLE_MS;
|
||||||
|
uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
|
||||||
|
|
||||||
|
uint8_t *data = this->uart_rx_buffer_.data();
|
||||||
|
const size_t buf_size = this->uart_rx_buffer_.size();
|
||||||
|
|
||||||
|
// Released by setup() once both tasks exist.
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (this->paused_ != 0) {
|
||||||
|
// Parked until resume() notifies; nothing is read, so the other owner sees
|
||||||
|
// every byte.
|
||||||
|
this->rx_parked_ = 1;
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
this->rx_parked_ = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block until at least one byte is available from UART.
|
||||||
|
int total_rx_size = uart_read_bytes(uart_num, data, 1, pdMS_TO_TICKS(UART_RX_WAIT_MS));
|
||||||
|
if (total_rx_size < 0) {
|
||||||
|
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||||
|
ESP_LOGE(TAG, "UART read failed: %d", total_rx_size);
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (total_rx_size == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// pause() landed during the read: don't forward a byte to a host that is gone.
|
||||||
|
if (this->paused_ != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain the currently buffered burst without waiting.
|
||||||
|
while (true) {
|
||||||
|
int rx_data_size = uart_read_bytes(uart_num, data + total_rx_size, buf_size - total_rx_size, 0);
|
||||||
|
if (rx_data_size < 0) {
|
||||||
|
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||||
|
ESP_LOGE(TAG, "UART read failed: %d", rx_data_size);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (rx_data_size == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGV(TAG, "UART RX: %d bytes", rx_data_size);
|
||||||
|
total_rx_size += rx_data_size;
|
||||||
|
if (total_rx_size >= (int) buf_size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ringbuf_send_with_retry(usb_tx_ringbuf, data, total_rx_size, &tx_full_log_ms);
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "UART RX: waking up USB TX task");
|
||||||
|
xTaskNotifyGive(usb_tx_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDCACMUARTBridge::uart_tx_task_() {
|
||||||
|
RingbufHandle_t usb_rx_ringbuf = this->usb_cdc_parent_->get_rx_ringbuf();
|
||||||
|
uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
|
||||||
|
uint8_t *data_to_uart = this->uart_tx_buffer_.data();
|
||||||
|
const size_t buf_size = this->uart_tx_buffer_.size();
|
||||||
|
size_t rx_size;
|
||||||
|
// Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
|
||||||
|
uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
|
||||||
|
uint32_t drop_log_ms = millis() - LOG_THROTTLE_MS;
|
||||||
|
|
||||||
|
// Released by setup() once both tasks exist.
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
ESP_LOGV(TAG, "Waiting for data to send to UART");
|
||||||
|
esp_err_t ret = usb_cdc_acm::ringbuf_read_bytes(usb_rx_ringbuf, data_to_uart, buf_size, &rx_size, portMAX_DELAY);
|
||||||
|
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||||
|
ESP_LOGE(TAG, "USB RX RingBuf read failed");
|
||||||
|
}
|
||||||
|
// Yield: this task runs above the main loop, so a persistent failure must not
|
||||||
|
// become a tight loop.
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another component owns the UART; host bytes must not interleave with its traffic.
|
||||||
|
// tx_busy_ goes up before the check so is_paused() cannot miss a write in flight.
|
||||||
|
this->tx_busy_ = 1;
|
||||||
|
if (this->paused_ != 0) {
|
||||||
|
this->tx_busy_ = 0;
|
||||||
|
if (should_log_now(&drop_log_ms, LOG_THROTTLE_MS)) {
|
||||||
|
ESP_LOGW(TAG, "Paused; dropping %zu bytes from host", rx_size);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "Sending %zu bytes to UART", rx_size);
|
||||||
|
// Signed: uart_write_bytes() returns -1 on error.
|
||||||
|
int xfer_size = uart_write_bytes(uart_num, data_to_uart, rx_size);
|
||||||
|
this->tx_busy_ = 0;
|
||||||
|
|
||||||
|
if (xfer_size < 0) {
|
||||||
|
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
|
||||||
|
ESP_LOGE(TAG, "UART write failed: %d", xfer_size);
|
||||||
|
}
|
||||||
|
} else if (static_cast<size_t>(xfer_size) != rx_size) {
|
||||||
|
ESP_LOGW(TAG, "UART write incomplete (%d/%zu bytes)", xfer_size, rx_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::cdc_acm_uart
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#pragma once
|
||||||
|
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||||
|
#include "esphome/components/uart/uart_component_esp_idf.h"
|
||||||
|
#include "esphome/components/usb_cdc_acm/usb_cdc_acm.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include "sdkconfig.h"
|
||||||
|
|
||||||
|
namespace esphome::cdc_acm_uart {
|
||||||
|
|
||||||
|
class CDCACMUARTBridge final : public Component {
|
||||||
|
public:
|
||||||
|
// Upper bound on the RX task's blocking read, so pause() takes effect without
|
||||||
|
// aborting the read. Arriving bytes still unblock it immediately.
|
||||||
|
static constexpr uint32_t UART_RX_WAIT_MS = 250;
|
||||||
|
|
||||||
|
CDCACMUARTBridge(uart::IDFUARTComponent *uart_parent, usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent)
|
||||||
|
: uart_parent_(uart_parent), usb_cdc_parent_(usb_cdc_parent) {}
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
void dump_config() override;
|
||||||
|
void on_shutdown() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
void set_dtr_pin(GPIOPin *dtr_pin) { this->dtr_pin_ = dtr_pin; }
|
||||||
|
void set_rts_pin(GPIOPin *rts_pin) { this->rts_pin_ = rts_pin; }
|
||||||
|
|
||||||
|
void set_line_coding();
|
||||||
|
void set_line_state(bool dtr, bool rts);
|
||||||
|
uart::IDFUARTComponent *get_uart_parent() const { return this->uart_parent_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop forwarding in both directions and hand the UART back to its configured
|
||||||
|
* framing, so another component may use the bus. Main-loop only. The RX task parks
|
||||||
|
* within UART_RX_WAIT_MS (a byte it was already reading is discarded). A host write
|
||||||
|
* already in flight is allowed to drain first, which at low baud rates can take
|
||||||
|
* seconds; the framing is restored only after that, so poll is_paused() rather than
|
||||||
|
* waiting a fixed interval. Host bytes not yet written to the UART are discarded.
|
||||||
|
* The DTR/RTS outputs hold their state while paused and follow the host again on
|
||||||
|
* resume().
|
||||||
|
*/
|
||||||
|
void pause();
|
||||||
|
/**
|
||||||
|
* Re-apply the host's line coding and line state, then resume forwarding. Main-loop
|
||||||
|
* only. Deferred until any host write still draining has finished, so the reload
|
||||||
|
* never truncates it.
|
||||||
|
*/
|
||||||
|
void resume();
|
||||||
|
/// True once both worker tasks are off the bus and the configured framing is restored.
|
||||||
|
/// With no RX task (setup() failed or has not run) there is nothing to wait for.
|
||||||
|
bool is_paused() const {
|
||||||
|
return this->state_ == MainState::MAIN_STATE_PAUSED &&
|
||||||
|
(this->uart_rx_task_handle_ == nullptr || this->rx_parked_ != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static void uart_rx_task_fn(void *arg);
|
||||||
|
static void uart_tx_task_fn(void *arg);
|
||||||
|
void uart_rx_task_();
|
||||||
|
void uart_tx_task_();
|
||||||
|
void restore_configured_framing_();
|
||||||
|
// True when the TX task has no write in flight and the UART TX FIFO has drained.
|
||||||
|
bool tx_idle_();
|
||||||
|
void finish_resume_();
|
||||||
|
void drive_line_state_();
|
||||||
|
// Copy the host's line coding onto the UART settings; true if anything changed.
|
||||||
|
bool sync_host_framing_();
|
||||||
|
|
||||||
|
TaskHandle_t uart_rx_task_handle_{nullptr};
|
||||||
|
TaskHandle_t uart_tx_task_handle_{nullptr};
|
||||||
|
|
||||||
|
GPIOPin *dtr_pin_{nullptr};
|
||||||
|
GPIOPin *rts_pin_{nullptr};
|
||||||
|
|
||||||
|
uint32_t reload_requested_at_{0};
|
||||||
|
|
||||||
|
// Worker staging, each sized to the CDC ring buffer it feeds or drains.
|
||||||
|
std::array<uint8_t, CONFIG_TINYUSB_CDC_TX_BUFSIZE> uart_rx_buffer_{};
|
||||||
|
std::array<uint8_t, CONFIG_TINYUSB_CDC_RX_BUFSIZE> uart_tx_buffer_{};
|
||||||
|
|
||||||
|
uart::IDFUARTComponent *uart_parent_;
|
||||||
|
usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent_;
|
||||||
|
|
||||||
|
// YAML framing, captured at setup; the host's line coding overwrites the UART's
|
||||||
|
// settings, so pause() needs the original to restore.
|
||||||
|
uint32_t configured_baud_rate_{0};
|
||||||
|
uart::UARTParityOptions configured_parity_{uart::UART_CONFIG_PARITY_NONE};
|
||||||
|
uint8_t configured_stop_bits_{0};
|
||||||
|
uint8_t configured_data_bits_{0};
|
||||||
|
|
||||||
|
// Written on the main loop, read by both worker tasks. uint8_t rather than bool:
|
||||||
|
// GCC on Xtensa emits an out-of-line call for atomic<bool>.
|
||||||
|
std::atomic<uint8_t> paused_{0};
|
||||||
|
// Raised by the RX task while parked and by the TX task around each UART write, so
|
||||||
|
// the pause hand-off knows when the bus is actually free.
|
||||||
|
std::atomic<uint8_t> rx_parked_{0};
|
||||||
|
std::atomic<uint8_t> tx_busy_{0};
|
||||||
|
// Main-loop state; paused_ mirrors it for the worker tasks.
|
||||||
|
enum class MainState : uint8_t {
|
||||||
|
MAIN_STATE_RUNNING,
|
||||||
|
MAIN_STATE_RELOAD_PENDING, // host line coding debounced, forwarding continues
|
||||||
|
MAIN_STATE_PAUSING, // waiting for TX idle to restore the configured framing
|
||||||
|
MAIN_STATE_PAUSED,
|
||||||
|
MAIN_STATE_RESUMING, // resume() requested while a host write still drains
|
||||||
|
};
|
||||||
|
MainState state_{MainState::MAIN_STATE_RUNNING};
|
||||||
|
// Host line state, recorded even while paused so resume() can re-drive the pins.
|
||||||
|
bool host_dtr_{false};
|
||||||
|
bool host_rts_{false};
|
||||||
|
// True once the host has sent any line coding; resume() then re-syncs to it.
|
||||||
|
bool host_coding_seen_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::cdc_acm_uart
|
||||||
|
#endif
|
||||||
@@ -30,6 +30,7 @@ CONF_KEYS = "keys"
|
|||||||
CONF_LABEL = "label"
|
CONF_LABEL = "label"
|
||||||
CONF_LIBRETINY = "libretiny"
|
CONF_LIBRETINY = "libretiny"
|
||||||
CONF_LOOP = "loop"
|
CONF_LOOP = "loop"
|
||||||
|
CONF_MANUFACTURER = "manufacturer"
|
||||||
CONF_NOX_INDEX = "nox_index"
|
CONF_NOX_INDEX = "nox_index"
|
||||||
CONF_ON_PACKET = "on_packet"
|
CONF_ON_PACKET = "on_packet"
|
||||||
CONF_ON_RECEIVE = "on_receive"
|
CONF_ON_RECEIVE = "on_receive"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import climate_ir
|
from esphome.components import climate_ir, remote_base
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
AUTO_LOAD = ["climate_ir"]
|
AUTO_LOAD = ["climate_ir"]
|
||||||
@@ -12,4 +12,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config: ConfigType) -> None:
|
async def to_code(config: ConfigType) -> None:
|
||||||
|
remote_base.request_protocol("coolix") # used from C++
|
||||||
await climate_ir.new_climate_ir(config)
|
await climate_ir.new_climate_ir(config)
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ void CopySwitch::setup() {
|
|||||||
|
|
||||||
void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); }
|
void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); }
|
||||||
|
|
||||||
void CopySwitch::write_state(bool state) {
|
void CopySwitch::write_state(bool state) { this->source_->control(state); }
|
||||||
if (state) {
|
|
||||||
source_->turn_on();
|
|
||||||
} else {
|
|
||||||
source_->turn_off();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::copy
|
} // namespace esphome::copy
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
|
|||||||
this->status_set_warning();
|
this->status_set_warning();
|
||||||
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
|
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
|
||||||
}
|
}
|
||||||
this->next_enter_deep_sleep_ = true;
|
this->defer_sleep_();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ void DeepSleepComponent::setup() {
|
|||||||
|
|
||||||
void DeepSleepComponent::schedule_sleep_() {
|
void DeepSleepComponent::schedule_sleep_() {
|
||||||
this->next_enter_deep_sleep_ = false;
|
this->next_enter_deep_sleep_ = false;
|
||||||
|
this->disable_loop();
|
||||||
const optional<uint32_t> run_duration = get_run_duration_();
|
const optional<uint32_t> run_duration = get_run_duration_();
|
||||||
if (run_duration.has_value()) {
|
if (run_duration.has_value()) {
|
||||||
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
|
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
|
||||||
@@ -45,7 +46,7 @@ void DeepSleepComponent::loop() {
|
|||||||
|
|
||||||
void DeepSleepComponent::begin_sleep(bool manual) {
|
void DeepSleepComponent::begin_sleep(bool manual) {
|
||||||
if (this->prevent_ && !manual) {
|
if (this->prevent_ && !manual) {
|
||||||
this->next_enter_deep_sleep_ = true;
|
this->defer_sleep_();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ class DeepSleepComponent final : public Component {
|
|||||||
void schedule_sleep_();
|
void schedule_sleep_();
|
||||||
bool should_teardown_();
|
bool should_teardown_();
|
||||||
|
|
||||||
|
void defer_sleep_() {
|
||||||
|
this->next_enter_deep_sleep_ = true;
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_BK72XX
|
#ifdef USE_BK72XX
|
||||||
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
|
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
|
||||||
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
|
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
|
|||||||
this->status_set_warning();
|
this->status_set_warning();
|
||||||
ESP_LOGW(TAG, "Waiting for wakeup pin state change");
|
ESP_LOGW(TAG, "Waiting for wakeup pin state change");
|
||||||
}
|
}
|
||||||
this->next_enter_deep_sleep_ = true;
|
this->defer_sleep_();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <numbers>
|
#include <numbers>
|
||||||
#include "display_color_utils.h"
|
#include "display_color_utils.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
@@ -770,10 +771,12 @@ Rect Display::get_clipping() const {
|
|||||||
|
|
||||||
void Display::clear_clipping_() { this->clipping_rectangle_.clear(); }
|
void Display::clear_clipping_() { this->clipping_rectangle_.clear(); }
|
||||||
|
|
||||||
|
void Display::feed_wdt_pixel_slow_() { App.feed_wdt(); }
|
||||||
|
|
||||||
bool Display::clip(int x, int y) {
|
bool Display::clip(int x, int y) {
|
||||||
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
|
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
|
||||||
return false;
|
return false;
|
||||||
if (!this->get_clipping().inside(x, y))
|
if (this->is_point_clipped(x, y))
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -758,6 +758,13 @@ class Display : public PollingComponent {
|
|||||||
|
|
||||||
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
|
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
|
||||||
|
|
||||||
|
/// Whether (x, y) falls outside the active clipping rectangle. Tests the
|
||||||
|
/// stack top in place: get_clipping() is out of line and returns the Rect
|
||||||
|
/// by value, which per pixel drawing cannot afford.
|
||||||
|
bool ESPHOME_ALWAYS_INLINE is_point_clipped(int x, int y) const {
|
||||||
|
return this->is_clipping() && !this->clipping_rectangle_.back().inside(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
/** Check if pixel is within region of display.
|
/** Check if pixel is within region of display.
|
||||||
*/
|
*/
|
||||||
bool clip(int x, int y);
|
bool clip(int x, int y);
|
||||||
@@ -774,6 +781,17 @@ class Display : public PollingComponent {
|
|||||||
void do_update_();
|
void do_update_();
|
||||||
void clear_clipping_();
|
void clear_clipping_();
|
||||||
|
|
||||||
|
/// Watchdog feed for per pixel loops. App.feed_wdt() is already rate
|
||||||
|
/// limited, but every call reads the clock; only every 256th pixel makes
|
||||||
|
/// that call, so the real feeds are unchanged and a pixel costs a counter.
|
||||||
|
/// At 20 us per pixel on the slowest e-paper path that is about 5 ms
|
||||||
|
/// between clock reads.
|
||||||
|
void ESPHOME_ALWAYS_INLINE feed_wdt_per_pixel_() {
|
||||||
|
if (++this->wdt_pixel_counter_ == 0)
|
||||||
|
this->feed_wdt_pixel_slow_();
|
||||||
|
}
|
||||||
|
void feed_wdt_pixel_slow_();
|
||||||
|
|
||||||
virtual int get_height_internal() = 0;
|
virtual int get_height_internal() = 0;
|
||||||
virtual int get_width_internal() = 0;
|
virtual int get_width_internal() = 0;
|
||||||
|
|
||||||
@@ -793,6 +811,7 @@ class Display : public PollingComponent {
|
|||||||
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
|
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
|
||||||
bool auto_clear_enabled_{true};
|
bool auto_clear_enabled_{true};
|
||||||
std::vector<Rect> clipping_rectangle_;
|
std::vector<Rect> clipping_rectangle_;
|
||||||
|
uint8_t wdt_pixel_counter_{0};
|
||||||
bool show_test_card_{false};
|
bool show_test_card_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "esphome/core/application.h"
|
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome::display {
|
namespace esphome::display {
|
||||||
@@ -44,7 +43,7 @@ int DisplayBuffer::get_height() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||||
if (!this->get_clipping().inside(x, y))
|
if (this->is_point_clipped(x, y))
|
||||||
return; // NOLINT
|
return; // NOLINT
|
||||||
|
|
||||||
switch (this->rotation_) {
|
switch (this->rotation_) {
|
||||||
@@ -64,7 +63,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this->draw_absolute_pixel_internal(x, y, color);
|
this->draw_absolute_pixel_internal(x, y, color);
|
||||||
App.feed_wdt();
|
this->feed_wdt_per_pixel_();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::display
|
} // namespace esphome::display
|
||||||
|
|||||||
@@ -63,16 +63,6 @@ bool Rect::equal(Rect rect) const {
|
|||||||
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
|
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT
|
|
||||||
if (!this->is_set()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (absolute) {
|
|
||||||
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
|
|
||||||
}
|
|
||||||
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Rect::inside(Rect rect) const {
|
bool Rect::inside(Rect rect) const {
|
||||||
if (!this->is_set() || !rect.is_set()) {
|
if (!this->is_set() || !rect.is_set()) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ class Rect {
|
|||||||
void shrink(Rect rect);
|
void shrink(Rect rect);
|
||||||
|
|
||||||
bool inside(Rect rect) const;
|
bool inside(Rect rect) const;
|
||||||
bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const;
|
bool ESPHOME_ALWAYS_INLINE inside(int16_t test_x, int16_t test_y, bool absolute = true) const {
|
||||||
|
if (!this->is_set()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (absolute) {
|
||||||
|
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
|
||||||
|
}
|
||||||
|
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
|
||||||
|
}
|
||||||
bool equal(Rect rect) const;
|
bool equal(Rect rect) const;
|
||||||
void info(const std::string &prefix = "rect info:");
|
void info(const std::string &prefix = "rect info:");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ bool EPaperBase::initialise(bool partial) {
|
|||||||
* @return false if the coordinates are out of bounds
|
* @return false if the coordinates are out of bounds
|
||||||
*/
|
*/
|
||||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||||
if (!this->get_clipping().inside(x, y))
|
if (this->is_point_clipped(x, y))
|
||||||
return false;
|
return false;
|
||||||
if (this->effective_transform_ & SWAP_XY)
|
if (this->effective_transform_ & SWAP_XY)
|
||||||
std::swap(x, y);
|
std::swap(x, y);
|
||||||
|
|||||||
@@ -153,13 +153,14 @@ bool ES7210::configure_mic_gain_() {
|
|||||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
|
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
|
||||||
|
|
||||||
// Configure mic 3
|
// Configure mic 3
|
||||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
|
||||||
|
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
|
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
|
||||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
|
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
|
||||||
|
|
||||||
// Configure mic 4
|
// Configure mic 4
|
||||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
|
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
|
||||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
|
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
|
|||||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||||
|
CONF_FLASH_CHIP = "flash_chip"
|
||||||
CONF_KEY_ID = "key_id"
|
CONF_KEY_ID = "key_id"
|
||||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||||
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
||||||
@@ -189,6 +190,13 @@ PSRAM_XIP_VARIANTS = {
|
|||||||
VARIANT_ESP32S31,
|
VARIANT_ESP32S31,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Variants whose ROM exports a full-format vsnprintf but no vasprintf
|
||||||
|
# (esp32c6.rom.newlib-normal.ld). There, the newlib printf engine is only
|
||||||
|
# linked because esp_http_client calls vasprintf; see vasprintf_stubs.cpp.
|
||||||
|
# The other variants either export both (classic ESP32, nano-format only) or
|
||||||
|
# neither, so the engine is already in the image and the wrap saves nothing.
|
||||||
|
ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6}
|
||||||
|
|
||||||
# NVS encryption (HMAC peripheral scheme) is only available on variants that
|
# 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
|
||||||
@@ -457,6 +465,20 @@ ESP32_CHIP_REVISIONS = {
|
|||||||
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
|
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM
|
||||||
|
# and only the one matching the flash ID is ever used
|
||||||
|
ESP32_FLASH_CHIPS = {
|
||||||
|
"gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP",
|
||||||
|
"issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP",
|
||||||
|
"mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP",
|
||||||
|
"winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP",
|
||||||
|
"boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP",
|
||||||
|
"th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP",
|
||||||
|
"mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
|
||||||
|
}
|
||||||
|
FLASH_CHIP_GENERIC = "generic"
|
||||||
|
FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only
|
||||||
|
|
||||||
# Socket limit configuration for ESP-IDF
|
# Socket limit configuration for ESP-IDF
|
||||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
||||||
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
||||||
@@ -1512,6 +1534,13 @@ def final_validate(config) -> None:
|
|||||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION],
|
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if config[CONF_VARIANT] != VARIANT_ESP32S3 and config.get(CONF_FLASH_MODE) == "opi":
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"'{CONF_FLASH_MODE}: opi' is only supported on {VARIANT_ESP32S3}",
|
||||||
|
path=[CONF_FLASH_MODE],
|
||||||
|
)
|
||||||
|
)
|
||||||
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]:
|
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]:
|
||||||
errs.append(
|
errs.append(
|
||||||
cv.Invalid(
|
cv.Invalid(
|
||||||
@@ -1519,6 +1548,25 @@ def final_validate(config) -> None:
|
|||||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
|
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None:
|
||||||
|
opi = flash_chip == FLASH_CHIP_OPI
|
||||||
|
if opi and config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}",
|
||||||
|
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif opi != (config.get(CONF_FLASH_MODE) == "opi"):
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'"
|
||||||
|
if opi
|
||||||
|
else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match "
|
||||||
|
f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}",
|
||||||
|
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||||
|
)
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
config[CONF_VARIANT] != VARIANT_ESP32P4
|
config[CONF_VARIANT] != VARIANT_ESP32P4
|
||||||
and config.get(CONF_ENGINEERING_SAMPLE) is not None
|
and config.get(CONF_ENGINEERING_SAMPLE) is not None
|
||||||
@@ -1732,6 +1780,8 @@ CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
|
|||||||
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
|
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
|
||||||
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||||
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||||
|
CONF_DISABLE_MBEDTLS_TLS_SERVER = "disable_mbedtls_tls_server"
|
||||||
|
CONF_DISABLE_MBEDTLS_TLS_EXTRAS = "disable_mbedtls_tls_extras"
|
||||||
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||||
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
||||||
@@ -1746,6 +1796,8 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
|
|||||||
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
||||||
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||||
|
KEY_MBEDTLS_TLS_SERVER_REQUIRED = "mbedtls_tls_server_required"
|
||||||
|
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED = "mbedtls_tls_extras_required"
|
||||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||||
@@ -1830,6 +1882,30 @@ def require_mbedtls_pkcs7() -> None:
|
|||||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
|
def require_mbedtls_tls_server() -> None:
|
||||||
|
"""Mark that the mbedTLS server-side TLS/DTLS handshake is required.
|
||||||
|
|
||||||
|
Call this from components that accept TLS connections (OpenThread's DTLS
|
||||||
|
commissioner does). This prevents CONFIG_MBEDTLS_TLS_CLIENT_ONLY from
|
||||||
|
being selected.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
|
def require_mbedtls_tls_extras(options: Iterable[str] | None = None) -> None:
|
||||||
|
"""Mark TLS features disabled by ``disable_mbedtls_tls_extras`` as required.
|
||||||
|
|
||||||
|
``options`` names the entries of ``MBEDTLS_TLS_EXTRA_OPTIONS`` to keep;
|
||||||
|
omit it to keep all of them. Call this from components that need AES-CCM,
|
||||||
|
deterministic ECDSA signing, static RSA/ECDH key exchange, TLS
|
||||||
|
renegotiation or session tickets, or that run a TLS client against
|
||||||
|
servers ESPHome cannot vet (wpa_supplicant's EAP client). A user-supplied
|
||||||
|
sdkconfig_options value is never overridden either.
|
||||||
|
"""
|
||||||
|
required = CORE.data[KEY_ESP32].setdefault(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
|
||||||
|
required.update(MBEDTLS_TLS_EXTRA_OPTIONS if options is None else options)
|
||||||
|
|
||||||
|
|
||||||
def require_mbedtls_sha512() -> None:
|
def require_mbedtls_sha512() -> None:
|
||||||
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
|
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
|
||||||
|
|
||||||
@@ -1929,6 +2005,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
|||||||
*ESP32_CHIP_REVISIONS, string=True
|
*ESP32_CHIP_REVISIONS, string=True
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_FLASH_CHIP): cv.one_of(
|
||||||
|
FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True
|
||||||
|
),
|
||||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||||
# it will handle disabling DHCP server when AP is not configured.
|
# it will handle disabling DHCP server when AP is not configured.
|
||||||
# Default to false (disabled) when WiFi is not used.
|
# Default to false (disabled) when WiFi is not used.
|
||||||
@@ -1987,6 +2066,8 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
|
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_SERVER, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_EXTRAS, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||||
@@ -2302,6 +2383,69 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
|
|||||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
||||||
|
|
||||||
|
|
||||||
|
# TLS features an HTTPS/MQTT client talking to a modern server never
|
||||||
|
# negotiates. Static RSA and static ECDH key exchange have no forward secrecy
|
||||||
|
# and are gone in TLS 1.3, renegotiation is deprecated, esp-tls never enables
|
||||||
|
# session tickets, AES-CCM ciphersuites are not offered by web servers, and
|
||||||
|
# deterministic ECDSA only matters when signing with a private key. Together
|
||||||
|
# they cost ~10 KB of flash whenever TLS is linked (http_request, mqtt).
|
||||||
|
# wpa_supplicant's EAP client is a second TLS client that talks to RADIUS
|
||||||
|
# servers ESPHome cannot vet, and a failed EAP handshake leaves the device
|
||||||
|
# off the network, so the wifi component re-enables all of these when eap is
|
||||||
|
# configured.
|
||||||
|
# The EC public key parsing extras stay enabled: they decide whether a peer
|
||||||
|
# certificate with a compressed point or explicit curve parameters parses,
|
||||||
|
# which no component can know ahead of time.
|
||||||
|
MBEDTLS_TLS_EXTRA_OPTIONS = (
|
||||||
|
"CONFIG_MBEDTLS_KEY_EXCHANGE_RSA",
|
||||||
|
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA",
|
||||||
|
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA",
|
||||||
|
"CONFIG_MBEDTLS_SSL_RENEGOTIATION",
|
||||||
|
"CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS",
|
||||||
|
"CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS",
|
||||||
|
"CONFIG_MBEDTLS_CCM_C",
|
||||||
|
"CONFIG_MBEDTLS_ECDSA_DETERMINISTIC",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Members of the mbedTLS "TLS Protocol Role" Kconfig choice. Setting one
|
||||||
|
# member is only valid when the user has not already chosen another.
|
||||||
|
MBEDTLS_TLS_ROLE_OPTIONS = (
|
||||||
|
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
|
||||||
|
"CONFIG_MBEDTLS_TLS_SERVER_ONLY",
|
||||||
|
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
|
||||||
|
"CONFIG_MBEDTLS_TLS_DISABLED",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
|
async def _reconcile_mbedtls_tls_sdkconfig(
|
||||||
|
disable_tls_server: bool, disable_tls_extras: bool
|
||||||
|
) -> None:
|
||||||
|
"""Trim mbedTLS to what a TLS client needs unless a component asked otherwise.
|
||||||
|
|
||||||
|
Runs at FINAL priority so every require_mbedtls_tls_server() and
|
||||||
|
require_mbedtls_tls_extras() call has happened. Only the server-side
|
||||||
|
handshake (~7 KB) is a separate option; nothing in ESPHome accepts TLS
|
||||||
|
connections, but OpenThread's DTLS commissioner does. A user-supplied
|
||||||
|
sdkconfig_options value always wins; for the TLS role choice, any member
|
||||||
|
the user set leaves the whole choice alone so the pair cannot conflict.
|
||||||
|
"""
|
||||||
|
data = CORE.data[KEY_ESP32]
|
||||||
|
sdkconfig = data[KEY_SDKCONFIG_OPTIONS]
|
||||||
|
if (
|
||||||
|
disable_tls_server
|
||||||
|
and not data.get(KEY_MBEDTLS_TLS_SERVER_REQUIRED, False)
|
||||||
|
and not any(option in sdkconfig for option in MBEDTLS_TLS_ROLE_OPTIONS)
|
||||||
|
):
|
||||||
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_CLIENT_ONLY", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", False)
|
||||||
|
if disable_tls_extras:
|
||||||
|
required = data.get(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
|
||||||
|
for option in MBEDTLS_TLS_EXTRA_OPTIONS:
|
||||||
|
if option not in required:
|
||||||
|
set_idf_sdkconfig_default(option, False)
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.FINAL)
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
async def _reconcile_network_sdkconfig() -> None:
|
async def _reconcile_network_sdkconfig() -> None:
|
||||||
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
|
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
|
||||||
@@ -2509,6 +2653,13 @@ async def to_code(config):
|
|||||||
# NVS finds stored preferences by key, so preference key migration is possible
|
# NVS finds stored preferences by key, so preference key migration is possible
|
||||||
cg.add_define("USE_PREFERENCE_KEY_LOOKUP")
|
cg.add_define("USE_PREFERENCE_KEY_LOOKUP")
|
||||||
cg.add_build_flag("-Wl,-z,noexecstack")
|
cg.add_build_flag("-Wl,-z,noexecstack")
|
||||||
|
# assert(), HAL_ASSERT and ESP_ERROR_CHECK bake __FILE__ into rodata, and
|
||||||
|
# IDF's noflash placement puts the flash driver's copies in DRAM. The
|
||||||
|
# basename keeps the panic output useful at a fraction of the size.
|
||||||
|
# __FILE_NAME__ is a GCC 12 builtin; IDF 5.0 still ships GCC 11.2.
|
||||||
|
if idf_version() >= cv.Version(5, 1, 0):
|
||||||
|
cg.add_build_flag("-D__FILE__=__FILE_NAME__")
|
||||||
|
cg.add_build_flag("-Wno-builtin-macro-redefined")
|
||||||
# Deferred so KEY_COMPONENTS is fully populated -- see the coroutine.
|
# Deferred so KEY_COMPONENTS is fully populated -- see the coroutine.
|
||||||
CORE.add_job(_finalize_arduino_aware_flags)
|
CORE.add_job(_finalize_arduino_aware_flags)
|
||||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
@@ -2566,6 +2717,17 @@ async def to_code(config):
|
|||||||
else:
|
else:
|
||||||
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
|
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
|
||||||
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
||||||
|
# esp_http_client calls vasprintf, which on the ESP32-C6 is the only
|
||||||
|
# reference to newlib's full printf engine (~20 KB: _svfprintf_r,
|
||||||
|
# _dtoa_r and their helpers); every other caller resolves to the
|
||||||
|
# ROM. See vasprintf_stubs.cpp. The --undefined flag is needed
|
||||||
|
# because libsrc.a is scanned before the IDF libraries that
|
||||||
|
# reference the symbol, so the stub would otherwise never be pulled
|
||||||
|
# from the archive.
|
||||||
|
if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS:
|
||||||
|
cg.add_define("USE_ESP32_VASPRINTF_STUB")
|
||||||
|
cg.add_build_flag("-Wl,--wrap=vasprintf")
|
||||||
|
cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf")
|
||||||
else:
|
else:
|
||||||
cg.add_build_flag("-DUSE_ARDUINO")
|
cg.add_build_flag("-DUSE_ARDUINO")
|
||||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||||
@@ -2614,6 +2776,8 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
|
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
|
||||||
)
|
)
|
||||||
|
# the opi mode choice only exists once octal flash is enabled
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_OCT_FLASH", flash_mode == "opi")
|
||||||
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
|
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
|
||||||
@@ -2638,6 +2802,11 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option(flag, rev == min_rev)
|
add_idf_sdkconfig_option(flag, rev == min_rev)
|
||||||
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
|
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
|
||||||
|
|
||||||
|
# Keep only the flash vendor driver the board needs; the boot log names it
|
||||||
|
if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None:
|
||||||
|
for chip, flag in ESP32_FLASH_CHIPS.items():
|
||||||
|
add_idf_sdkconfig_option(flag, chip == flash_chip)
|
||||||
|
|
||||||
# Use SRAM1 region as IRAM on ESP32 (original) variant
|
# Use SRAM1 region as IRAM on ESP32 (original) variant
|
||||||
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
|
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
|
||||||
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
|
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
|
||||||
@@ -2991,6 +3160,13 @@ async def to_code(config):
|
|||||||
# FINAL priority: runs after every require_certificate_bundle() call
|
# FINAL priority: runs after every require_certificate_bundle() call
|
||||||
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
|
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
|
||||||
|
|
||||||
|
# FINAL priority: runs after every require_mbedtls_tls_*() call
|
||||||
|
CORE.add_job(
|
||||||
|
_reconcile_mbedtls_tls_sdkconfig,
|
||||||
|
advanced[CONF_DISABLE_MBEDTLS_TLS_SERVER],
|
||||||
|
advanced[CONF_DISABLE_MBEDTLS_TLS_EXTRAS],
|
||||||
|
)
|
||||||
|
|
||||||
# FINAL: require_*() calls can come from to_code at or below this priority, so an
|
# FINAL: require_*() calls can come from to_code at or below this priority, so an
|
||||||
# inline read would be iteration-order-dependent; reconcile once after every job ran.
|
# inline read would be iteration-order-dependent; reconcile once after every job ran.
|
||||||
CORE.add_job(
|
CORE.add_job(
|
||||||
|
|||||||
@@ -173,7 +173,10 @@ static const char *const TAG = "esp32.crash";
|
|||||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
|
static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
|
||||||
|
|
||||||
void crash_handler_read_and_clear() {
|
// Validate the NOINIT record. Runs on every has_data() call; re-running is
|
||||||
|
// harmless and the magic is left alone so the record survives an OTA
|
||||||
|
// rollback reboot, crash_handler_clear() drops it once an API client has it.
|
||||||
|
static void read_crash_data() {
|
||||||
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
|
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
|
||||||
s_crash_data_valid = true;
|
s_crash_data_valid = true;
|
||||||
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
|
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
|
||||||
@@ -194,11 +197,12 @@ void crash_handler_read_and_clear() {
|
|||||||
s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
|
s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// Don't clear magic here — crash data must survive OTA rollback reboots.
|
|
||||||
// Magic is cleared by crash_handler_clear() after an API client receives the data.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool crash_handler_has_data() { return s_crash_data_valid; }
|
bool crash_handler_has_data() {
|
||||||
|
read_crash_data();
|
||||||
|
return s_crash_data_valid;
|
||||||
|
}
|
||||||
|
|
||||||
void crash_handler_clear() {
|
void crash_handler_clear() {
|
||||||
// Only clear the magic so data doesn't survive the next reboot.
|
// Only clear the magic so data doesn't survive the next reboot.
|
||||||
@@ -426,7 +430,7 @@ static void log_foreign_addresses() {
|
|||||||
// crashes again during boot, and allowing the CLI's process_stacktrace to match
|
// crashes again during boot, and allowing the CLI's process_stacktrace to match
|
||||||
// and decode each address individually.
|
// and decode each address individually.
|
||||||
void crash_handler_log() {
|
void crash_handler_log() {
|
||||||
if (!s_crash_data_valid)
|
if (!crash_handler_has_data())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||||
|
|||||||
@@ -4,11 +4,6 @@
|
|||||||
|
|
||||||
namespace esphome::esp32 {
|
namespace esphome::esp32 {
|
||||||
|
|
||||||
/// Read and validate crash data from NOINIT memory.
|
|
||||||
/// Does not clear the magic marker — call crash_handler_clear() after
|
|
||||||
/// the data has been delivered to an API client so it survives OTA rollback reboots.
|
|
||||||
void crash_handler_read_and_clear();
|
|
||||||
|
|
||||||
/// Log crash data if a crash was detected on previous boot.
|
/// Log crash data if a crash was detected on previous boot.
|
||||||
void crash_handler_log();
|
void crash_handler_log();
|
||||||
|
|
||||||
@@ -16,7 +11,7 @@ void crash_handler_log();
|
|||||||
/// Call after the data has been delivered to an API client.
|
/// Call after the data has been delivered to an API client.
|
||||||
void crash_handler_clear();
|
void crash_handler_clear();
|
||||||
|
|
||||||
/// Returns true if crash data was found this boot.
|
/// Returns true if crash data was found this boot, reading it first if needed.
|
||||||
bool crash_handler_has_data();
|
bool crash_handler_has_data();
|
||||||
|
|
||||||
} // namespace esphome::esp32
|
} // namespace esphome::esp32
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set
|
|
||||||
// before crash_handler.h's #ifdef-guarded namespace block is parsed.
|
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "crash_handler.h"
|
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
|
||||||
#include <esp_clk_tree.h>
|
#include <esp_clk_tree.h>
|
||||||
@@ -45,11 +42,6 @@ void arch_restart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void arch_init() {
|
void arch_init() {
|
||||||
#ifdef USE_ESP32_CRASH_HANDLER
|
|
||||||
// Read crash data from previous boot before anything else
|
|
||||||
esp32::crash_handler_read_and_clear();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Enable the task watchdog only on the loop task (from which we're currently running)
|
// Enable the task watchdog only on the loop task (from which we're currently running)
|
||||||
esp_task_wdt_add(nullptr);
|
esp_task_wdt_add(nullptr);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Linker wrap stub for vasprintf() on variants whose ROM exports a
|
||||||
|
* full-format vsnprintf() but no vasprintf() (ESP32-C6, newlib only).
|
||||||
|
*
|
||||||
|
* On those chips every snprintf/vsnprintf call in the image resolves to
|
||||||
|
* the ROM, so the newlib printf engine (_svfprintf_r, _dtoa_r and their
|
||||||
|
* helpers, ~20 KB) is not linked at all until something references a
|
||||||
|
* printf-family function the ROM lacks. esp_http_client does exactly that
|
||||||
|
* through vasprintf() in its header and auth helpers, so adding
|
||||||
|
* http_request to a build costs the whole engine on top of the HTTP and
|
||||||
|
* TLS code itself.
|
||||||
|
*
|
||||||
|
* This stub reimplements vasprintf() on top of the ROM vsnprintf(), which
|
||||||
|
* keeps the engine out of the image. It is only compiled in when codegen
|
||||||
|
* defines USE_ESP32_VASPRINTF_STUB, which is gated on the variant's ROM
|
||||||
|
* linker script and on the same newlib condition as printf_stubs.cpp.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
|
#if defined(USE_ESP_IDF) && defined(USE_ESP32_VASPRINTF_STUB)
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace esphome::esp32 {}
|
||||||
|
|
||||||
|
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int __wrap_vasprintf(char **strp, const char *fmt, va_list ap) {
|
||||||
|
va_list ap_copy;
|
||||||
|
va_copy(ap_copy, ap);
|
||||||
|
int len = vsnprintf(nullptr, 0, fmt, ap_copy);
|
||||||
|
va_end(ap_copy);
|
||||||
|
if (len < 0) {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
// vasprintf's contract is a malloc'd buffer the caller releases with free()
|
||||||
|
char *buf = static_cast<char *>(malloc(static_cast<size_t>(len) + 1)); // NOLINT(cppcoreguidelines-no-malloc)
|
||||||
|
if (buf == nullptr) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
vsnprintf(buf, static_cast<size_t>(len) + 1, fmt, ap);
|
||||||
|
*strp = buf;
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
|
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||||
|
|
||||||
|
#endif // USE_ESP_IDF && USE_ESP32_VASPRINTF_STUB
|
||||||
@@ -83,18 +83,23 @@ void ESP32BLE::setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::enable() {
|
// Queue the transition for loop(). A pending transition the other way is
|
||||||
if (this->state_ != BLE_COMPONENT_STATE_DISABLED)
|
// cancelled instead, since nothing was torn down or brought up yet; any other
|
||||||
return;
|
// state is already there or on its way.
|
||||||
|
void ESP32BLE::request_state_(bool enable) {
|
||||||
this->state_ = BLE_COMPONENT_STATE_ENABLE;
|
if (enable) {
|
||||||
}
|
if (this->state_ == BLE_COMPONENT_STATE_DISABLED) {
|
||||||
|
this->state_ = BLE_COMPONENT_STATE_ENABLE;
|
||||||
void ESP32BLE::disable() {
|
} else if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
|
||||||
if (this->state_ == BLE_COMPONENT_STATE_DISABLED)
|
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
||||||
return;
|
}
|
||||||
|
} else {
|
||||||
this->state_ = BLE_COMPONENT_STATE_DISABLE;
|
if (this->state_ == BLE_COMPONENT_STATE_ACTIVE) {
|
||||||
|
this->state_ = BLE_COMPONENT_STATE_DISABLE;
|
||||||
|
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
|
||||||
|
this->state_ = BLE_COMPONENT_STATE_DISABLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||||
@@ -580,7 +585,11 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
|
|||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->state_ = BLE_COMPONENT_STATE_DISABLED;
|
this->drain_ble_events_();
|
||||||
|
// A status callback may have asked for BLE back; the stack is down now, so
|
||||||
|
// that request becomes a bring-up.
|
||||||
|
this->state_ =
|
||||||
|
this->state_ == BLE_COMPONENT_STATE_ACTIVE ? BLE_COMPONENT_STATE_ENABLE : BLE_COMPONENT_STATE_DISABLED;
|
||||||
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
|
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
|
||||||
ESP_LOGD(TAG, "Enabling");
|
ESP_LOGD(TAG, "Enabling");
|
||||||
this->state_ = BLE_COMPONENT_STATE_OFF;
|
this->state_ = BLE_COMPONENT_STATE_OFF;
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ class ESP32BLE final : public Component {
|
|||||||
}
|
}
|
||||||
uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; }
|
uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; }
|
||||||
|
|
||||||
void enable();
|
void enable() { this->request_state_(true); }
|
||||||
void disable();
|
void disable() { this->request_state_(false); }
|
||||||
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
|
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
@@ -176,6 +176,15 @@ class ESP32BLE final : public Component {
|
|||||||
|
|
||||||
bool ble_setup_();
|
bool ble_setup_();
|
||||||
bool ble_dismantle_();
|
bool ble_dismantle_();
|
||||||
|
void request_state_(bool enable);
|
||||||
|
// Drop what the old stack queued; the next stack reuses the same interface ids.
|
||||||
|
void drain_ble_events_() {
|
||||||
|
BLEEvent *ble_event;
|
||||||
|
while ((ble_event = this->ble_events_.pop()) != nullptr) {
|
||||||
|
this->ble_event_pool_.release(ble_event);
|
||||||
|
}
|
||||||
|
this->ble_events_.get_and_reset_dropped_count();
|
||||||
|
}
|
||||||
bool ble_pre_setup_();
|
bool ble_pre_setup_();
|
||||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||||
void advertising_init_();
|
void advertising_init_();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ void BLEClientBase::set_state(espbt::ClientState st) {
|
|||||||
|
|
||||||
void BLEClientBase::loop() {
|
void BLEClientBase::loop() {
|
||||||
if (!esp32_ble::global_ble->is_active()) {
|
if (!esp32_ble::global_ble->is_active()) {
|
||||||
this->set_state(espbt::ClientState::INIT);
|
// ble_before_disabled_event_handler() resets the client.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this->state() == espbt::ClientState::INIT) {
|
if (this->state() == espbt::ClientState::INIT) {
|
||||||
@@ -72,6 +72,21 @@ void BLEClientBase::loop() {
|
|||||||
|
|
||||||
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||||
|
|
||||||
|
void BLEClientBase::ble_before_disabled_event_handler() {
|
||||||
|
auto st = this->state();
|
||||||
|
if (st != espbt::ClientState::IDLE && st != espbt::ClientState::INIT) {
|
||||||
|
// No CLOSE_EVT will come: free the services and settle the link.
|
||||||
|
this->release_services();
|
||||||
|
this->set_idle_();
|
||||||
|
this->on_disconnect_complete(ESP_GATT_CONN_TERMINATE_LOCAL_HOST);
|
||||||
|
}
|
||||||
|
// The interface belongs to the torn-down stack.
|
||||||
|
this->gattc_if_ = ESP_GATT_IF_NONE;
|
||||||
|
this->set_state(espbt::ClientState::INIT);
|
||||||
|
// An idle client runs no loop; the INIT branch must run to register again.
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
void BLEClientBase::dump_config() {
|
void BLEClientBase::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" Address: %s\n"
|
" Address: %s\n"
|
||||||
@@ -93,6 +108,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
|||||||
return false;
|
return false;
|
||||||
if (this->state() != espbt::ClientState::IDLE)
|
if (this->state() != espbt::ClientState::IDLE)
|
||||||
return false;
|
return false;
|
||||||
|
// Not registered on this stack yet; promoting now would stop the scan for a
|
||||||
|
// connect that connect() rejects anyway.
|
||||||
|
if (this->gattc_if_ == ESP_GATT_IF_NONE)
|
||||||
|
return false;
|
||||||
|
|
||||||
this->log_event_("Found device");
|
this->log_event_("Found device");
|
||||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
|
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
|
||||||
@@ -117,6 +136,15 @@ void BLEClientBase::connect() {
|
|||||||
this->connection_index_, this->address_str_);
|
this->connection_index_, this->address_str_);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
|
||||||
|
// Bluedroid drops an open on an unknown interface without any event.
|
||||||
|
this->log_warning_("Connect rejected, GATT app not registered");
|
||||||
|
// INIT stays so loop() still registers; only a promoted client goes back.
|
||||||
|
if (this->state() == espbt::ClientState::DISCOVERED) {
|
||||||
|
this->set_state(espbt::ClientState::IDLE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
|
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
|
||||||
this->paired_ = false;
|
this->paired_ = false;
|
||||||
// A registration whose event never arrived must not block this connection's release.
|
// A registration whose event never arrived must not block this connection's release.
|
||||||
@@ -199,7 +227,10 @@ void BLEClientBase::release_services() {
|
|||||||
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
|
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
|
||||||
// Only the cache clean makes the stack's database unsafe to walk.
|
// Only the cache clean makes the stack's database unsafe to walk.
|
||||||
this->services_released_ = true;
|
this->services_released_ = true;
|
||||||
esp_ble_gattc_cache_clean(this->remote_bda_);
|
// A stack on its way down frees its own cache.
|
||||||
|
if (esp32_ble::global_ble->is_active()) {
|
||||||
|
esp_ble_gattc_cache_clean(this->remote_bda_);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
void connect() override;
|
void connect() override;
|
||||||
esp_err_t pair();
|
esp_err_t pair();
|
||||||
void disconnect() override;
|
void disconnect() override;
|
||||||
|
void ble_before_disabled_event_handler() override;
|
||||||
void unconditional_disconnect();
|
void unconditional_disconnect();
|
||||||
void release_services();
|
void release_services();
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Group 3: 4-byte types
|
// Group 3: 4-byte types
|
||||||
int gattc_if_;
|
int gattc_if_{ESP_GATT_IF_NONE};
|
||||||
esp_gatt_status_t status_{ESP_GATT_OK};
|
esp_gatt_status_t status_{ESP_GATT_OK};
|
||||||
|
|
||||||
// Group 4: Arrays
|
// Group 4: Arrays
|
||||||
@@ -139,7 +140,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
uint8_t pending_notify_regs_{0};
|
uint8_t pending_notify_regs_{0};
|
||||||
bool auto_connect_{false};
|
bool auto_connect_{false};
|
||||||
bool paired_{false};
|
bool paired_{false};
|
||||||
// Set only when release_services() cleans the stack's GATT cache, which no API may then walk
|
// Set by release_services() on RAM-cache builds; the stack's GATT database must not be walked after it
|
||||||
bool services_released_{false};
|
bool services_released_{false};
|
||||||
// 8 bytes used, no padding
|
// 8 bytes used, no padding
|
||||||
|
|
||||||
@@ -155,10 +156,11 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
void log_connection_params_(const char *param_type);
|
void log_connection_params_(const char *param_type);
|
||||||
void handle_connection_result_(esp_err_t ret);
|
void handle_connection_result_(esp_err_t ret);
|
||||||
/// Hook called once a connection has been fully torn down (after release_services() and
|
/// Hook called once a connection has been fully torn down (after release_services() and
|
||||||
/// set_idle_()), from both the CLOSE_EVT handler and the DISCONNECTING safety timeout.
|
/// set_idle_()): CLOSE_EVT, the DISCONNECTING safety timeout, or the BLE stack going down.
|
||||||
/// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state)
|
/// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state)
|
||||||
/// override this to release that state. `reason` is the controller reason code, or
|
/// override this to release that state. `reason` is the controller reason code,
|
||||||
/// ESP_GATT_CONN_TIMEOUT for the safety-timeout path.
|
/// ESP_GATT_CONN_TIMEOUT for the safety timeout, or ESP_GATT_CONN_TERMINATE_LOCAL_HOST
|
||||||
|
/// for the stack going down.
|
||||||
virtual void on_disconnect_complete(esp_err_t reason) {}
|
virtual void on_disconnect_complete(esp_err_t reason) {}
|
||||||
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
|
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
|
||||||
void set_idle_() {
|
void set_idle_() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import encodings
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble
|
from esphome.components import esp32_ble
|
||||||
|
from esphome.components.const import CONF_MANUFACTURER
|
||||||
from esphome.components.esp32 import request_bluetooth
|
from esphome.components.esp32 import request_bluetooth
|
||||||
from esphome.components.esp32_ble import BTLoggers, bt_uuid
|
from esphome.components.esp32_ble import BTLoggers, bt_uuid
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
@@ -41,7 +42,6 @@ CONF_DESCRIPTORS = "descriptors"
|
|||||||
CONF_ENDIANNESS = "endianness"
|
CONF_ENDIANNESS = "endianness"
|
||||||
CONF_FIRMWARE_VERSION = "firmware_version"
|
CONF_FIRMWARE_VERSION = "firmware_version"
|
||||||
CONF_INDICATE = "indicate"
|
CONF_INDICATE = "indicate"
|
||||||
CONF_MANUFACTURER = "manufacturer"
|
|
||||||
CONF_MANUFACTURER_DATA = "manufacturer_data"
|
CONF_MANUFACTURER_DATA = "manufacturer_data"
|
||||||
CONF_MAX_CLIENTS = "max_clients"
|
CONF_MAX_CLIENTS = "max_clients"
|
||||||
CONF_ON_WRITE = "on_write"
|
CONF_ON_WRITE = "on_write"
|
||||||
@@ -597,7 +597,7 @@ async def to_code(config):
|
|||||||
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
|
cg.add(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
|
# 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
|
# find. A server that is auto-loaded purely to host a runtime service (improv_ble) stays
|
||||||
# silent until that service asks for advertising.
|
# silent until that service asks for advertising.
|
||||||
cg.add(
|
cg.add(
|
||||||
var.set_advertising_required(
|
var.set_advertising_required(
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
|
|||||||
|
|
||||||
/** Whether this server needs the device to advertise so clients can find and connect to it.
|
/** 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
|
* False for a server that only hosts services created at runtime (e.g. improv_ble), which
|
||||||
* request advertising themselves for as long as they need it.
|
* request advertising themselves for as long as they need it.
|
||||||
*/
|
*/
|
||||||
void set_advertising_required(bool required) { this->advertising_required_ = required; }
|
void set_advertising_required(bool required) { this->advertising_required_ = required; }
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u
|
|||||||
for (auto *client : this->clients_) {
|
for (auto *client : this->clients_) {
|
||||||
client->disconnect();
|
client->disconnect();
|
||||||
}
|
}
|
||||||
|
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||||
|
// The OTA transfer blocks the main loop, so the revert in loop() cannot run. No
|
||||||
|
// active-connection gate here: every client was just told to disconnect.
|
||||||
|
this->update_coex_preference_(false);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
} else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) {
|
} else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) {
|
||||||
this->scan_continuous_before_ota_ = false;
|
this->scan_continuous_before_ota_ = false;
|
||||||
@@ -74,11 +79,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u
|
|||||||
|
|
||||||
void ESP32BLETracker::loop() {
|
void ESP32BLETracker::loop() {
|
||||||
if (!this->parent_->is_active()) {
|
if (!this->parent_->is_active()) {
|
||||||
this->ble_was_disabled_ = true;
|
|
||||||
return;
|
return;
|
||||||
} else if (this->ble_was_disabled_) {
|
}
|
||||||
|
if (this->ble_was_disabled_) {
|
||||||
this->ble_was_disabled_ = false;
|
this->ble_was_disabled_ = false;
|
||||||
// If the BLE stack was disabled, we need to start the scan again.
|
// First start after boot or after the stack came back.
|
||||||
if (this->scan_continuous_) {
|
if (this->scan_continuous_) {
|
||||||
this->start_scan();
|
this->start_scan();
|
||||||
}
|
}
|
||||||
@@ -218,7 +223,27 @@ void ESP32BLETracker::stop_scan() {
|
|||||||
this->stop_scan_();
|
this->stop_scan_();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
|
void ESP32BLETracker::ble_before_disabled_event_handler() {
|
||||||
|
// Tell the controller to stop; a scan still starting has nothing to stop yet.
|
||||||
|
if (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::FAILED) {
|
||||||
|
this->stop_scan_();
|
||||||
|
}
|
||||||
|
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||||
|
for (auto *client : this->clients_) {
|
||||||
|
client->ble_before_disabled_event_handler();
|
||||||
|
}
|
||||||
|
this->skip_next_scan_end_ = false;
|
||||||
|
#endif
|
||||||
|
// The stop above never completes (stack torn down, events dropped); settle
|
||||||
|
// here so start_scan_() sees IDLE once the stack is back.
|
||||||
|
if (this->scanner_state_ != ScannerState::IDLE) {
|
||||||
|
this->cleanup_scan_state_(true);
|
||||||
|
}
|
||||||
|
// A failure latched by the old stack must not be handled against the next.
|
||||||
|
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||||
|
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||||
|
this->ble_was_disabled_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ESP32BLETracker::stop_scan_() {
|
bool ESP32BLETracker::stop_scan_() {
|
||||||
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ class ESPBTClient : public ESPBTDeviceListener {
|
|||||||
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
|
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
|
||||||
virtual void connect() = 0;
|
virtual void connect() = 0;
|
||||||
virtual void disconnect() = 0;
|
virtual void disconnect() = 0;
|
||||||
|
/// Called right before the BLE stack is dismantled. Nothing in flight will
|
||||||
|
/// complete, and the GATT app must register again once the stack is back.
|
||||||
|
virtual void ble_before_disabled_event_handler() {}
|
||||||
bool disconnect_pending() const { return this->want_disconnect_; }
|
bool disconnect_pending() const { return this->want_disconnect_; }
|
||||||
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
|
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
|
||||||
|
|
||||||
@@ -132,7 +135,7 @@ class ESPBTClient : public ESPBTDeviceListener {
|
|||||||
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
|
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
|
||||||
|
|
||||||
// Memory optimized layout
|
// Memory optimized layout
|
||||||
uint8_t app_id; // App IDs are small integers assigned sequentially
|
uint8_t app_id{0}; // App IDs are small integers assigned sequentially
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Set state without IDLE handling - use for direct state transitions.
|
/// Set state without IDLE handling - use for direct state transitions.
|
||||||
|
|||||||
@@ -433,25 +433,10 @@ void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; }
|
void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; }
|
||||||
void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
|
|
||||||
void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
|
|
||||||
void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; }
|
|
||||||
void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; }
|
|
||||||
void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; }
|
|
||||||
void ESP32Camera::set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
|
|
||||||
/* set exposure parameters */
|
/* set exposure parameters */
|
||||||
void ESP32Camera::set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
|
|
||||||
void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; }
|
|
||||||
void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
|
|
||||||
void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
|
|
||||||
/* set gains parameters */
|
/* set gains parameters */
|
||||||
void ESP32Camera::set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
|
|
||||||
void ESP32Camera::set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
|
|
||||||
void ESP32Camera::set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
|
|
||||||
/* set white balance */
|
/* set white balance */
|
||||||
void ESP32Camera::set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
|
|
||||||
/* set test mode */
|
/* set test mode */
|
||||||
void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
|
|
||||||
/* set fps */
|
/* set fps */
|
||||||
void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) {
|
void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) {
|
||||||
this->max_update_interval_ = max_update_interval;
|
this->max_update_interval_ = max_update_interval;
|
||||||
|
|||||||
@@ -140,25 +140,25 @@ class ESP32Camera final : public camera::Camera {
|
|||||||
void set_pixel_format(ESP32CameraPixelFormat format);
|
void set_pixel_format(ESP32CameraPixelFormat format);
|
||||||
void set_frame_size(ESP32CameraFrameSize size);
|
void set_frame_size(ESP32CameraFrameSize size);
|
||||||
void set_jpeg_quality(uint8_t quality);
|
void set_jpeg_quality(uint8_t quality);
|
||||||
void set_vertical_flip(bool vertical_flip);
|
void set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
|
||||||
void set_horizontal_mirror(bool horizontal_mirror);
|
void set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
|
||||||
void set_contrast(int contrast);
|
void set_contrast(int contrast) { this->contrast_ = contrast; }
|
||||||
void set_brightness(int brightness);
|
void set_brightness(int brightness) { this->brightness_ = brightness; }
|
||||||
void set_saturation(int saturation);
|
void set_saturation(int saturation) { this->saturation_ = saturation; }
|
||||||
void set_special_effect(ESP32SpecialEffect effect);
|
void set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
|
||||||
/* -- exposure */
|
/* -- exposure */
|
||||||
void set_aec_mode(ESP32GainControlMode mode);
|
void set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
|
||||||
void set_aec2(bool aec2);
|
void set_aec2(bool aec2) { this->aec2_ = aec2; }
|
||||||
void set_ae_level(int ae_level);
|
void set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
|
||||||
void set_aec_value(uint32_t aec_value);
|
void set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
|
||||||
/* -- gains */
|
/* -- gains */
|
||||||
void set_agc_mode(ESP32GainControlMode mode);
|
void set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
|
||||||
void set_agc_value(uint8_t agc_value);
|
void set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
|
||||||
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling);
|
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
|
||||||
/* -- white balance */
|
/* -- white balance */
|
||||||
void set_wb_mode(ESP32WhiteBalanceMode mode);
|
void set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
|
||||||
/* -- test */
|
/* -- test */
|
||||||
void set_test_pattern(bool test_pattern);
|
void set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
|
||||||
/* -- framerates */
|
/* -- framerates */
|
||||||
void set_max_update_interval(uint32_t max_update_interval);
|
void set_max_update_interval(uint32_t max_update_interval);
|
||||||
void set_idle_update_interval(uint32_t idle_update_interval);
|
void set_idle_update_interval(uint32_t idle_update_interval);
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ void Esp32HostedUpdate::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" Mode: HTTP\n"
|
" Mode: HTTP\n"
|
||||||
" Source URL: %s",
|
" Source URL: %s",
|
||||||
this->source_url_.c_str());
|
this->source_url_);
|
||||||
#else
|
#else
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" Mode: Embedded\n"
|
" Mode: Embedded\n"
|
||||||
@@ -215,7 +215,7 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
|||||||
|
|
||||||
auto container = this->http_request_parent_->get(this->source_url_);
|
auto container = this->http_request_parent_->get(this->source_url_);
|
||||||
if (container == nullptr || container->status_code != 200) {
|
if (container == nullptr || container->status_code != 200) {
|
||||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
|
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_);
|
||||||
this->status_set_error(LOG_STR("Failed to fetch manifest"));
|
this->status_set_error(LOG_STR("Failed to fetch manifest"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
|
|||||||
|
|
||||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||||
// HTTP mode setters
|
// HTTP mode setters
|
||||||
void set_source_url(const std::string &url) { this->source_url_ = url; }
|
void set_source_url(const char *url) { this->source_url_ = url; }
|
||||||
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
|
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
|
||||||
#else
|
#else
|
||||||
// Embedded mode setters
|
// Embedded mode setters
|
||||||
@@ -38,7 +38,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
|
|||||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||||
// HTTP mode members
|
// HTTP mode members
|
||||||
http_request::HttpRequestComponent *http_request_parent_{nullptr};
|
http_request::HttpRequestComponent *http_request_parent_{nullptr};
|
||||||
std::string source_url_;
|
const char *source_url_{nullptr}; // literal from codegen
|
||||||
std::string firmware_url_;
|
std::string firmware_url_;
|
||||||
|
|
||||||
// HTTP mode helpers
|
// HTTP mode helpers
|
||||||
|
|||||||
@@ -363,14 +363,6 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
if config.get(CONF_ENABLE_SERIAL1):
|
if config.get(CONF_ENABLE_SERIAL1):
|
||||||
enable_serial1()
|
enable_serial1()
|
||||||
|
|
||||||
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
|
|
||||||
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
|
|
||||||
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
|
|
||||||
# a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3,
|
|
||||||
# which always aborts if exceptions are disabled.
|
|
||||||
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
|
||||||
cg.add_build_flag("-DNEW_OOM_ABORT")
|
|
||||||
|
|
||||||
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
|
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
|
||||||
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
|
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
|
||||||
# See throw_stubs.h for details. Must be prepended before <string>, so this
|
# See throw_stubs.h for details. Must be prepended before <string>, so this
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ namespace esphome::esp8266_pwm {
|
|||||||
|
|
||||||
class ESP8266PWM final : public output::FloatOutput, public Component {
|
class ESP8266PWM final : public output::FloatOutput, public Component {
|
||||||
public:
|
public:
|
||||||
|
// User provided, not "= default": `new(p) ESP8266PWM()` would zero-fill .bss that is already zero.
|
||||||
|
ESP8266PWM() {}
|
||||||
|
|
||||||
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
|
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
|
||||||
void set_frequency(float frequency) { this->frequency_ = frequency; }
|
void set_frequency(float frequency) { this->frequency_ = frequency; }
|
||||||
/// Dynamically update frequency
|
/// Dynamically update frequency
|
||||||
@@ -28,8 +31,8 @@ class ESP8266PWM final : public output::FloatOutput, public Component {
|
|||||||
protected:
|
protected:
|
||||||
void write_state(float state) override;
|
void write_state(float state) override;
|
||||||
|
|
||||||
InternalGPIOPin *pin_;
|
InternalGPIOPin *pin_{nullptr};
|
||||||
float frequency_{1000.0};
|
float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py
|
||||||
/// Cache last output level for dynamic frequency updating
|
/// Cache last output level for dynamic frequency updating
|
||||||
float last_output_{0.0};
|
float last_output_{0.0};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Componen
|
|||||||
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
|
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
|
||||||
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
|
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
|
||||||
|
|
||||||
|
# Schema default that also matches the C++ initializer in esp8266_pwm.h; codegen
|
||||||
|
# skips the setter when the config equals it.
|
||||||
|
DEFAULT_FREQUENCY = 1000.0
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
output.FLOAT_OUTPUT_SCHEMA.extend(
|
output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
@@ -29,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Required(CONF_PIN): cv.All(
|
cv.Required(CONF_PIN): cv.All(
|
||||||
pins.internal_gpio_output_pin_schema, valid_pwm_pin
|
pins.internal_gpio_output_pin_schema, valid_pwm_pin
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency,
|
cv.Optional(CONF_FREQUENCY, default=DEFAULT_FREQUENCY): validate_frequency,
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.require_framework_version(
|
cv.require_framework_version(
|
||||||
@@ -48,7 +52,9 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
cg.add(var.set_pin(pin))
|
cg.add(var.set_pin(pin))
|
||||||
|
|
||||||
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
|
# Skip the setter when the config matches the C++ initializer (DEFAULT_FREQUENCY).
|
||||||
|
if (frequency := config[CONF_FREQUENCY]) != DEFAULT_FREQUENCY:
|
||||||
|
cg.add(var.set_frequency(frequency))
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
|
|||||||
@@ -166,9 +166,17 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
|||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
)
|
)
|
||||||
# web_server and prometheus keep the shared listener up; the captive
|
# 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
|
# portal's copy only exists on the fallback AP and is the recovery path.
|
||||||
|
# web_server `ota: false` gates /update behind the captive portal on
|
||||||
|
# every listener
|
||||||
|
web_server_conf = full_conf.get(CONF_WEB_SERVER)
|
||||||
|
plaintext_update_reachable = (
|
||||||
|
web_server_conf.get(CONF_OTA) is not False
|
||||||
|
if web_server_conf is not None
|
||||||
|
else "prometheus" in full_conf
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
(CONF_WEB_SERVER in full_conf or "prometheus" in full_conf)
|
plaintext_update_reachable
|
||||||
and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf)
|
and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf)
|
||||||
and any(
|
and any(
|
||||||
CONF_ENCRYPTION in conf
|
CONF_ENCRYPTION in conf
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user