Merge remote-tracking branch 'origin/dev' into socket-lwip-raw-udp

This commit is contained in:
J. Nick Koston
2026-09-17 08:48:28 -05:00
598 changed files with 12239 additions and 3208 deletions
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+23
View File
@@ -9,6 +9,29 @@ body:
If you have a feature request or enhancement, please [request them here instead][fr].
[fr]: https://github.com/orgs/esphome/discussions
- type: markdown
attributes:
value: |
## Use of AI in bug reports
AI tools are good at carrying out well-defined tasks, but they are not good at troubleshooting.
Please do NOT paste an AI-generated wall of text into the issue template - if the AI hasn't solved
your problem, its wild guesses are not likely to help.
Please DO include your own words and observations, compile/boot logs, and
especially a minimal reproducible example of your YAML configuration that demonstrates the problem.
It is however quite acceptable to use AI to translate your *own* report,
if you aren't a competent English speaker.
If you really think it will be useful to include an AI's analysis, preferably wrap it in a `<details>` block which will be collapsed by default.
If you are using AI to help solve a problem, rather than asking it to speculate about what the problem is,
it can be more useful to ask it to create a step-by-step troubleshooting procedure.
AI is also useful for generating boilerplate code, such as a minimal reproducible example of your YAML
configuration that demonstrates the problem.
Used properly, AI can be a useful tool to help you solve your problem, but don't let it get in the way.
- type: textarea
validations:
required: true
+1 -1
View File
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
+3 -3
View File
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -413,7 +413,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# 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,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want
to keep it open. Also, please 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.
Thank you for your contribution!
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.6
rev: v0.16.7
hooks:
# Run the linter.
- id: ruff
+16 -2
View File
@@ -431,7 +431,17 @@ file does, and it is the authority when they disagree. The most useful starting
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:**
```cpp
@@ -443,7 +453,8 @@ file does, and it is the authority when they disagree. The most useful starting
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`.
@@ -629,6 +640,9 @@ file does, and it is the authority when they disagree. The most useful starting
_request_listener_slot()
cg.add(hub.register_listener(var))
```
When several instances each own a list declared at the same size (one per hub of a
`MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`;
the define is then the largest count any one key requested instead of the total.
```cpp
#ifdef MY_COMPONENT_LISTENER_COUNT
void register_listener(MyComponentListener *listener);
+7 -1
View File
@@ -100,6 +100,7 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bridge/* @kbx81
esphome/components/bthome_mithermometer/* @nagyrobi
esphome/components/button/* @esphome/core
esphome/components/bytebuffer/* @clydebarrow
@@ -111,6 +112,8 @@ esphome/components/captive_portal/* @esphome/core
esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/cdc_acm_uart/* @kbx81
esphome/components/cdc_acm_uart/bridge/* @kbx81
esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/ch423/* @dwmw2
esphome/components/chsc6x/* @kkosik20
@@ -179,7 +182,6 @@ esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
@@ -263,8 +265,10 @@ esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04
esphome/components/icnt86/* @danepowell
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
esphome/components/improv_ble/* @jesserockz
esphome/components/improv_serial/* @esphome/core
esphome/components/ina226/* @latonita @Sergio303
esphome/components/ina260/* @mreditor97
@@ -474,6 +478,7 @@ esphome/components/sendspin/image/* @kahrendt
esphome/components/sendspin/media_player/* @kahrendt
esphome/components/sendspin/media_source/* @kahrendt
esphome/components/sendspin/sensor/* @kahrendt
esphome/components/sendspin/switch/* @kahrendt
esphome/components/sendspin/text_sensor/* @kahrendt
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
@@ -586,6 +591,7 @@ esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/uart_mux/* @kbx81
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9
RUN \
platformio settings set enable_telemetry No \
+99 -28
View File
@@ -102,6 +102,101 @@ def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schem
return CONDITION_REGISTRY.register(name, condition_type, schema)
async def _build_with_parent(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(automation_id, template_arg, parent)
async def _build_without_parent(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
return cg.new_Pvariable(automation_id, template_arg)
async def _build_parented(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(automation_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
def register_simple_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action whose constructor takes the object named by ``config[CONF_ID]``.
Use the ``register_action`` decorator instead when the builder must also set fields.
"""
register_action(name, action_type, schema, synchronous=synchronous)(
_build_with_parent
)
def register_simple_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_simple_action``."""
register_condition(name, condition_type, schema)(_build_with_parent)
def register_bare_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action whose constructor takes no arguments."""
register_action(name, action_type, schema, synchronous=synchronous)(
_build_without_parent
)
def register_bare_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_bare_action``."""
register_condition(name, condition_type, schema)(_build_without_parent)
def register_parented_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action deriving from ``Parented<T>``.
The object is constructed without arguments and ``set_parent()`` receives the object
named by ``config[CONF_ID]``.
"""
register_action(name, action_type, schema, synchronous=synchronous)(_build_parented)
def register_parented_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_parented_action``."""
register_condition(name, condition_type, schema)(_build_parented)
Action = cg.esphome_ns.class_("Action")
Trigger = cg.esphome_ns.class_("Trigger")
ACTION_REGISTRY = Registry()
@@ -534,44 +629,20 @@ async def lambda_action_to_code(
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
@register_action(
register_simple_action(
"component.update",
UpdateComponentAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def component_update_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@register_action(
register_simple_action(
"component.suspend",
SuspendComponentAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def component_suspend_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@register_action(
+8 -5
View File
@@ -90,9 +90,10 @@ def get_project_cmakelists(
"""
idf_target = variant_to_idf_target(get_esp32_variant())
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
# --format=raw because the legacy mode doesn't support it.
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
# total_size, hence the ELF fallback in espidf/size_summary.py; both
# go away together when 1.x support is dropped.
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
# 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})
# 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(
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
${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
+1
View File
@@ -6,5 +6,6 @@ See the component-alias section of esphome/loader.py.
# alias -> (canonical component, removal version or None)
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
"esp32_improv": ("improv_ble", "2027.4.0"),
"rp2040": ("rp2", "2027.7.0"),
}
+1 -1
View File
@@ -94,7 +94,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
/// - SamplingMode::MIN: Use the lowest sample value
/// - SamplingMode::MAX: Use the highest sample value
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
void set_sampling_mode(SamplingMode sampling_mode);
void set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
/// Perform a single ADC sampling operation and return the measured value.
/// 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
@@ -138,11 +138,11 @@ class AlarmControlPanel : public EntityBase {
// in order to store last panel state in flash
ESPPreferenceObject pref_;
// current state
AlarmControlPanelState current_state_;
AlarmControlPanelState current_state_{ACP_STATE_DISARMED};
// the desired (or previous) state
AlarmControlPanelState desired_state_;
AlarmControlPanelState desired_state_{ACP_STATE_DISARMED};
// last time the state was updated
uint32_t last_update_;
uint32_t last_update_{0};
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - passes the new state to listeners
+20 -9
View File
@@ -136,6 +136,12 @@ CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
# Schema defaults that also match the C++ initializers in api_server.h; codegen
# skips the setter when the config equals them.
DEFAULT_PORT = 6053
DEFAULT_REBOOT_TIMEOUT = "15min"
DEFAULT_BATCH_DELAY = "100ms"
def _register_provisioning_source(config: ConfigType) -> ConfigType:
"""Register the API as a provisioning source when encryption is enabled.
@@ -292,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
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
cv.Optional(CONF_PASSWORD): cv.invalid(
"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"
),
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT
): cv.positive_time_period_milliseconds,
cv.Exclusive(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.Optional(CONF_BATCH_DELAY, default=DEFAULT_BATCH_DELAY): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
@@ -350,10 +356,9 @@ CONFIG_SCHEMA = cv.All(
ln882x=5, # Moderate RAM
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Max queued messages per connection, and 2 KB of backlog per slot up
# to 64 KB (a lone message is exempt), before the connection is dropped
# Platform defaults based on available RAM and typical message rates:
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
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_log_listener()
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
# Skip the setters when the config matches the C++ initializers (DEFAULT_*).
if (port := config[CONF_PORT]) != DEFAULT_PORT:
cg.add(var.set_port(port))
if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period(
DEFAULT_REBOOT_TIMEOUT
):
cg.add(var.set_reboot_timeout(reboot_timeout))
if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY):
cg.add(var.set_batch_delay(batch_delay))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
+33 -4
View File
@@ -77,6 +77,7 @@ service APIConnection {
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
}
@@ -2726,7 +2727,8 @@ enum SerialProxyParity {
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 {
option (id) = 138;
option (source) = SOURCE_CLIENT;
@@ -2752,7 +2754,8 @@ message SerialProxyDataReceived {
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 {
option (id) = 140;
option (source) = SOURCE_CLIENT;
@@ -2763,7 +2766,8 @@ message SerialProxyWriteRequest {
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 {
option (id) = 141;
option (source) = SOURCE_CLIENT;
@@ -2802,6 +2806,7 @@ enum SerialProxyRequestType {
// error the device answers with INVALID_ARGUMENT.
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
}
enum SerialProxyStatus {
@@ -2814,7 +2819,8 @@ enum SerialProxyStatus {
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 {
option (id) = 144;
option (source) = SOURCE_CLIENT;
@@ -2838,6 +2844,29 @@ message SerialProxyRequestResponse {
string error_message = 4; // Additional detail on failure (optional)
}
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
// activates the port's protocol-aware tap (if one is configured), letting it observe
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
// speaks is a property of the device configuration, discoverable from the tap
// component's own API surface. A client that is about to flash firmware selects RAW
// first, which definitively disables that injection.
enum SerialProxyMode {
SERIAL_PROXY_MODE_RAW = 0;
SERIAL_PROXY_MODE_PROTOCOL = 1;
}
// Only the subscribed client may change the mode; any other caller -- including one that
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
// when the port has no protocol-aware tap configured.
message SerialProxySetModeRequest {
option (id) = 152;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyMode mode = 2;
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
+26 -9
View File
@@ -1,20 +1,37 @@
#include "api_buffer.h"
#include <new>
#ifdef ESPHOME_DEBUG_API
#include "esphome/core/log.h"
#endif
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) {
// nothrow (no zero-fill) so OOM is reportable; plain new aborts instead
// (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)
if (n > MAX_SIZE)
return false;
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
// realloc extends in place when it can, avoiding the copy
uint8_t *grown = RAMAllocator<uint8_t>().reallocate(this->data_.get(), n);
if (grown == nullptr)
return false;
(void) this->data_.release(); // realloc already freed or reused the old block
this->data_.reset(grown);
this->capacity_ = n;
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
+20 -4
View File
@@ -25,6 +25,7 @@ namespace esphome::api {
/// writes in debug builds.
class APIBuffer {
public:
static constexpr size_t MAX_SIZE = UINT16_MAX; // API frames carry 16 bit lengths
void clear() { this->size_ = 0; }
/// 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); }
@@ -36,9 +37,19 @@ class APIBuffer {
[[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)))
return false;
this->size_ = new_size;
this->size_ = static_cast<uint16_t>(new_size);
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(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
@@ -55,9 +66,14 @@ class APIBuffer {
protected:
bool grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
#ifdef ESPHOME_DEBUG_API
void debug_check_drop_(size_t drop) const;
#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
+20 -8
View File
@@ -364,7 +364,10 @@ void APIConnection::check_keepalive_(uint32_t now) {
ESP_LOGVV(TAG, "Sending keepalive PING");
PingRequest 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),
// schedule it at the front of the batch so it will be sent with priority
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) {
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
if (msg.state) {
a_switch->turn_on();
} else {
a_switch->turn_off();
}
a_switch->control(msg.state);
}
#endif
@@ -1661,6 +1659,7 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
break;
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
// Response-only discriminators; never valid in a request
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
@@ -1673,6 +1672,19 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
send_serial_proxy_ack(this, msg.instance, msg.type, status);
}
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return;
}
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
serial_proxy_result_to_status(result));
}
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
if (!this->send_message(msg)) {
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
@@ -1799,7 +1811,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp;
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
resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name());
+1
View File
@@ -244,6 +244,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg);
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
+1 -1
View File
@@ -171,7 +171,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin
return APIError::OK;
// 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");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
@@ -219,7 +219,10 @@ class APIFrameHelper {
if (this->rx_buf_len_ == 0) {
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:
// Drain backlogged overflow data to the socket and handle errors.
@@ -67,15 +67,15 @@ APIError APINoiseFrameHelper::init() {
}
// init prologue
size_t old_size = prologue_.size();
if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] {
uint8_t *dst = prologue_.append(PROLOGUE_INIT_LEN);
if (dst == nullptr) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
#ifdef USE_ESP8266
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#else
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#endif
state_ = State::CLIENT_HELLO;
@@ -272,17 +272,17 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
// Append 2 size bytes + frame data to the prologue
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;
return APIError::OUT_OF_MEMORY;
}
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
dst[0] = (uint8_t) (rx_size >> 8);
dst[1] = (uint8_t) rx_size;
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;
+57 -64
View File
@@ -1,98 +1,91 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
#include <new>
namespace esphome::api {
APIOverflowBuffer::~APIOverflowBuffer() {
for (auto *entry : this->queue_) {
if (entry != nullptr)
Entry::destroy(entry);
}
}
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
// socket->write() can re-enter this function: a log message emitted from an
// 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.
// Nested call from inside socket->write(); see draining_
if (this->draining_)
return 0;
// RAII so the flag is cleared on every return path
struct DrainGuard {
explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; }
~DrainGuard() { this->flag_ = false; }
bool &flag_;
} guard(this->draining_);
APIOverflowBuffer &owner;
~DrainGuard() { this->owner.draining_ = false; }
} guard{*this};
this->draining_ = true;
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());
if (sent <= 0) {
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
// 0 = nothing sent (treat as no progress)
ssize_t sent = socket->write(msg + LEN_PREFIX, len);
if (sent <= 0)
return sent;
if (static_cast<size_t>(sent) < len) {
// 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;
}
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->head_ += LEN_PREFIX + len;
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)
return false;
uint16_t buffer_size = total_len - skip;
// nothrow: a failed allocation returns nullptr so the connection is dropped
// cleanly instead of plain new's crash or abort on OOM
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *data = new (std::nothrow) uint8_t[buffer_size];
if (data == nullptr)
return false;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new (std::nothrow) Entry{data, buffer_size, 0};
if (entry == nullptr) {
delete[] data;
const size_t new_len = total_len - skip;
const size_t new_bytes = LEN_PREFIX + new_len;
const size_t live = this->buf_.size() - this->head_;
// A lone message is only bound by the buffer; refusing it would just drop the connection
if (live + new_bytes > (this->count_ > 0 ? MAX_BYTES : MAX_LONE_BYTES))
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;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
uint8_t *dst = this->buf_.append(new_bytes);
if (dst == nullptr)
return false;
dst[0] = new_len;
dst[1] = new_len >> 8;
dst += LEN_PREFIX;
for (const struct iovec *end = iov + iovcnt; iov != end; iov++) {
if (skip >= iov->iov_len) {
skip -= iov->iov_len;
} else {
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(entry->data + write_pos, src, len);
write_pos += len;
to_skip = 0;
const size_t len = iov->iov_len - skip;
std::memcpy(dst, static_cast<const uint8_t *>(iov->iov_base) + skip, len);
dst += len;
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_++;
return true;
}
+40 -53
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <sys/types.h>
@@ -8,71 +9,57 @@
#include "esphome/components/socket/headers.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
#include "api_buffer.h"
namespace esphome::api {
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
///
/// Under normal operation this buffer is **never used** — data goes straight
/// from the frame helper to the socket. It only fills when the LWIP TCP
/// send buffer is full (slow client, congested network, heavy logging).
/// The queue drains automatically on subsequent write/loop calls once the
/// 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.
/// TCP send backlog, only used when the socket send buffer is full.
/// One contiguous buffer per connection, allocated on the first stall and
/// kept at its high-water mark so a lossy link does not churn the heap.
/// Messages are stored as a 2 byte length prefix plus payload.
/// API_MAX_SEND_QUEUE bounds queued messages and, at 2 KB per slot, queued
/// bytes; exceeding either fails the connection.
class APIOverflowBuffer {
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.
bool empty() const { return this->count_ == 0; }
/// True when the queue has no room for another entry.
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
/// 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.
/// Drain queued messages to the socket.
/// Returns bytes written, 0 for a re-entrant call, -1 on error (check errno
/// for EWOULDBLOCK); callers only need to act on -1.
ssize_t try_drain(socket::Socket *socket);
/// Enqueue unsent IOV data into the backlog.
/// Copies iov data starting at byte offset `skip` into a new entry.
/// Returns false if the queue is full or allocation fails (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
/// Queue iov data from byte offset `skip` as one message.
/// Returns false when a limit is hit, allocation fails, or storage would move
/// during a drain; the caller should fail the connection.
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:
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
uint8_t head_{0};
uint8_t tail_{0};
static constexpr size_t LEN_PREFIX = 2;
static constexpr size_t BYTES_PER_SLOT = 2048;
// 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};
// Guards against re-entrant drains: socket->write() can re-enter the API
// send path (e.g. a log message emitted from an lwip callback), and a nested
// drain would free the entry the outer drain is still holding.
bool draining_{false};
// socket->write() can re-enter the send path (log from an lwip callback):
// a nested drain makes no progress and a nested enqueue never moves storage
bool draining_ : 1 {false};
bool release_when_drained_ : 1 {false};
};
} // namespace esphome::api
+13
View File
@@ -4253,6 +4253,19 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
size += ProtoSize::calc_length(1, this->error_message.size());
return size;
}
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->instance = value;
break;
case 2:
this->mode = static_cast<enums::SerialProxyMode>(value);
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+21
View File
@@ -356,6 +356,7 @@ enum SerialProxyRequestType : uint32_t {
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5,
};
enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_OK = 0,
@@ -366,6 +367,10 @@ enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
};
enum SerialProxyMode : uint32_t {
SERIAL_PROXY_MODE_RAW = 0,
SERIAL_PROXY_MODE_PROTOCOL = 1,
};
#endif
} // namespace enums
@@ -3403,6 +3408,22 @@ class SerialProxyRequestResponse final : public ProtoMessage {
protected:
};
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 152;
static constexpr uint8_t ESTIMATED_SIZE = 6;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
#endif
uint32_t instance{0};
enums::SerialProxyMode mode{};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
+18
View File
@@ -854,6 +854,8 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -878,6 +880,16 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
return ESPHOME_PSTR("UNKNOWN");
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
switch (value) {
case enums::SERIAL_PROXY_MODE_RAW:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
case enums::SERIAL_PROXY_MODE_PROTOCOL:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const {
@@ -2805,6 +2817,12 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
return out.c_str();
}
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
@@ -712,6 +712,17 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_device_capabilities_request();
break;
}
#ifdef USE_SERIAL_PROXY
case SerialProxySetModeRequest::MESSAGE_TYPE: {
SerialProxySetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
#endif
this->on_serial_proxy_set_mode_request(msg);
break;
}
#endif
default:
break;
}
+3
View File
@@ -235,6 +235,9 @@ class APIServerConnectionBase {
void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
+3 -3
View File
@@ -314,7 +314,7 @@ class APIServer final : public Component,
#endif
// 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};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
@@ -351,8 +351,8 @@ class APIServer final : public Component,
#endif
// Group smaller types together
uint16_t port_{6053};
uint16_t batch_delay_{100};
uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py
uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
uint8_t listen_backlog_{4};
+1 -4
View File
@@ -339,10 +339,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(
name="esphome/esp-audio-libs",
ref="3.2.1",
)
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
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) {
// A transfer buffer isn't ncessary for a local file
this->file_ring_buffer_ = output_ring_buffer.lock();
if (this->file_ring_buffer_ == nullptr) {
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
@@ -51,14 +51,14 @@ void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_le
void AudioTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0;
if (this->ring_buffer_.use_count() > 0) {
if (this->ring_buffer_ != nullptr) {
this->ring_buffer_->reset();
}
}
void AudioSinkTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0;
if (this->ring_buffer_.use_count() > 0) {
if (this->ring_buffer_ != nullptr) {
this->ring_buffer_->reset();
}
#ifdef USE_SPEAKER
@@ -69,7 +69,7 @@ void AudioSinkTransferBuffer::clear_buffered_data() {
}
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->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_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);
}
@@ -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);
} else
#endif
if (this->ring_buffer_.use_count() > 0) {
if (this->ring_buffer_ != nullptr) {
bytes_written =
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
} 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));
}
#endif
if (this->ring_buffer_.use_count() > 0) {
if (this->ring_buffer_ != nullptr) {
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
}
return (this->available() > 0);
@@ -203,16 +203,10 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
this->away_config_ = away_config;
}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void BangBangClimate::dump_config() {
LOG_CLIMATE("", "Bang Bang Climate", this);
ESP_LOGCONFIG(TAG,
@@ -22,10 +22,10 @@ class BangBangClimate final : public climate::Climate, public Component {
void setup() override;
void dump_config() override;
void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
void set_supports_cool(bool supports_cool);
void set_supports_heat(bool supports_heat);
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_normal_config(const BangBangClimateTargetTempConfig &normal_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 {
bool binary;
state->current_values_as_binary(&binary);
if (binary) {
this->output_->turn_on();
} else {
this->output_->turn_off();
}
this->output_->set_state(binary);
}
protected:
+5 -1
View File
@@ -39,6 +39,7 @@ from esphome.const import (
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GARAGE_DOOR,
DEVICE_CLASS_GAS,
DEVICE_CLASS_GLASS_BREAK,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_LIGHT,
DEVICE_CLASS_LOCK,
@@ -81,6 +82,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GARAGE_DOOR,
DEVICE_CLASS_GAS,
DEVICE_CLASS_GLASS_BREAK,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_LIGHT,
DEVICE_CLASS_LOCK,
@@ -452,7 +454,9 @@ _BINARY_SENSOR_SCHEMA = (
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): 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_RELEASE): automation.validate_automation({}),
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> {
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; }
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 {
public:
// User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero.
DelayedOnFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
@@ -63,6 +66,9 @@ class DelayedOnFilter : public Filter {
class DelayedOffFilter : public Filter {
public:
// User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero.
DelayedOffFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
@@ -143,6 +149,8 @@ class StatelessLambdaFilter : public Filter {
class SettleFilter : public Filter {
public:
// User provided, not "= default": `new(p) SettleFilter()` would zero-fill .bss that is already zero.
SettleFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
+10 -1
View File
@@ -23,7 +23,7 @@ public ble_api.h.
import logging
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components import libretiny, wifi
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
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.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_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
@@ -45,15 +45,7 @@ void BluedroidGattClient::setup() {
void BluedroidGattClient::loop() {
if (!esp32_ble::global_ble->is_active()) {
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
// 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);
// ble_before_disabled_event_handler() settles the slot.
return;
}
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);
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_();
} else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
// 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);
}
} else {
// The loop stays on while a link exists (stack-down watch, pre-started
// search flush); it settles only back at IDLE.
// The loop stays on while a link exists (pre-started search flush); it
// settles only back at IDLE.
this->deliver_pending_search_();
if (this->state() == ClientState::IDLE) {
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() {
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
if (this->is_failed()) {
@@ -97,6 +105,11 @@ void BluedroidGattClient::dump_config() {
// ---- contract ops ----
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
// stale CLOSE_EVT then tears down.
if (this->state() != ClientState::IDLE) {
@@ -31,6 +31,9 @@ class BluetoothConnection;
// void disconnect() cannot overload with an int-returning twin.
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
public:
// User provided, not "= default": `new(p) BluedroidGattClient()` would zero-fill .bss that is already zero.
BluedroidGattClient() {}
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
// Lifecycle of one connection attempt's service search.
@@ -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 connect() override;
void disconnect() override;
void ble_before_disabled_event_handler() override;
bool wants_parsed_advertisements() override { return false; }
void on_scan_end() override {}
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 {
public:
// User provided, not "= default": `new(p) BluetoothConnection()` would zero-fill .bss that is already zero.
BluetoothConnection() {}
/// Wire the platform backend. Called from codegen before setup.
void set_backend(ble_device_base::BLEGattConnection *backend) {
this->backend_ = backend;
@@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
// explicit kick the MTU would only be exchanged on the first GATT query,
// which never happens on a V3_WITH_CACHE connection.
// Both registration calls above return void (BTstack 075a078, arduino-pico
// 6.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().
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)
def enable_advertisement_filter() -> None:
"""Compile the advertisement filter hook into bluetooth_proxy.
Called by external filtering components from to_code(). The define behind
this is an implementation detail; do not emit it directly.
Public API for external components. Do not remove.
"""
cg.add_define("USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER")
async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
await _to_code_esp32(config)
@@ -94,6 +94,15 @@ void BluetoothProxy::on_raw_advertisement_(const ble_device_base::RawAdvertiseme
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return;
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
// Ask the filter before the packet is queued, so a dropped advertisement never
// reaches the batch or the network.
if (this->advertisement_filter_.is_set() && !this->advertisement_filter_.should_forward(raw)) {
ESP_LOGVV(TAG, "Filtered packet from %012" PRIX64, raw.address);
return;
}
#endif
auto &adv = this->response_.advertisements[this->response_.advertisements_len];
adv.address = raw.address;
adv.rssi = raw.rssi;
@@ -184,6 +193,9 @@ void BluetoothProxy::dump_config() {
" Adapter MAC: %s",
scan_mode, mac_out);
#endif
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
ESP_LOGCONFIG(TAG, " Advertisement filter: %s", YESNO(this->advertisement_filter_.is_set()));
#endif
}
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
@@ -97,6 +97,29 @@ static_assert(pending_reply_round_trips(0xABCD112233445566ULL, 0x000011223344556
static_assert(PendingReply{}.empty());
#endif
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
/// Predicate slot letting an external component drop advertisements before they
/// are queued for the API. Same shape as
/// ble_device_base::RawAdvertisementCallback. Runs on the advertisement hot
/// path, so it must be cheap and must not block.
///
/// Usage:
/// proxy->set_advertisement_filter({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
/// return static_cast<MyFilter *>(self)->should_forward(adv);
/// }});
///
/// Returning false drops the advertisement. Not called at all while the API is
/// disconnected, which matters to a stateful filter. Compiled in only when an
/// external component calls bluetooth_proxy.enable_advertisement_filter().
struct AdvertisementFilter {
void *instance{nullptr};
bool (*fn)(void *instance, const ble_device_base::RawAdvertisement &adv){nullptr};
/// A default-constructed slot is "no filter"; the proxy guards on this.
bool is_set() const { return this->fn != nullptr; }
bool should_forward(const ble_device_base::RawAdvertisement &adv) const { return this->fn(this->instance, adv); }
};
#endif // USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
class BluetoothProxy final : public Component {
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
// Allow the connection to update connections_free_response_
@@ -162,6 +185,11 @@ class BluetoothProxy final : public Component {
void set_active(bool active) { this->active_ = active; }
bool has_active() { return this->active_; }
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
/// One subscriber; a later call replaces an earlier one.
void set_advertisement_filter(AdvertisementFilter filter) { this->advertisement_filter_ = filter; }
#endif
uint32_t get_legacy_version() const {
if (!this->active_) {
return LEGACY_PASSIVE_ONLY_VERSION;
@@ -330,6 +358,10 @@ class BluetoothProxy final : public Component {
// start on an even word, closing two alignment holes.
uint32_t last_advertisement_flush_time_{0};
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
AdvertisementFilter advertisement_filter_{};
#endif
// BLE advertisement batching
api::BluetoothLERawAdvertisementsResponse response_;
@@ -341,7 +341,6 @@ void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over
void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
this->humidity_oversampling_ = humidity_over_sampling;
}
void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
uint8_t BME280Component::read_u8_(uint8_t a_register) {
uint8_t data = 0;
this->read_byte(a_register, &data);
+1 -1
View File
@@ -69,7 +69,7 @@ class BME280Component : public PollingComponent {
/// Set the oversampling value for the humidity sensor. Default is 16x.
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling);
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
void set_iir_filter(BME280IIRFilter iir_filter);
void set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
-1
View File
@@ -503,7 +503,6 @@ void BME680Component::set_pressure_oversampling(BME680Oversampling pressure_over
void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) {
this->humidity_oversampling_ = humidity_oversampling;
}
void BME680Component::set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) {
this->heater_temperature_ = heater_temperature;
this->heater_duration_ = heater_duration;
+1 -1
View File
@@ -74,7 +74,7 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
/// Set the humidity oversampling value. Defaults to 16X.
void set_humidity_oversampling(BME680Oversampling humidity_oversampling);
/// Set the IIR Filter value. Defaults to no IIR Filter.
void set_iir_filter(BME680IIRFilter iir_filter);
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_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) {
this->pressure_oversampling_ = pressure_over_sampling;
}
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
uint8_t data = 0;
this->bmp_read_byte(a_register, &data);
+1 -1
View File
@@ -59,7 +59,7 @@ class BMP280Component : public PollingComponent {
/// Set the oversampling value for the pressure sensor. Default is 16x.
void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling);
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
void set_iir_filter(BMP280IIRFilter iir_filter);
void set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void setup() override;
void dump_config() override;
+4
View File
@@ -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
+1
View File
@@ -30,6 +30,7 @@ CONF_KEYS = "keys"
CONF_LABEL = "label"
CONF_LIBRETINY = "libretiny"
CONF_LOOP = "loop"
CONF_MANUFACTURER = "manufacturer"
CONF_NOX_INDEX = "nox_index"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive"
+2 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
from esphome.components import climate_ir, remote_base
from esphome.types import ConfigType
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:
remote_base.request_protocol("coolix") # used from C++
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::write_state(bool state) {
if (state) {
source_->turn_on();
} else {
source_->turn_off();
}
}
void CopySwitch::write_state(bool state) { this->source_->control(state); }
} // namespace esphome::copy
@@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning();
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
}
this->next_enter_deep_sleep_ = true;
this->defer_sleep_();
return false;
}
}
@@ -17,6 +17,7 @@ void DeepSleepComponent::setup() {
void DeepSleepComponent::schedule_sleep_() {
this->next_enter_deep_sleep_ = false;
this->disable_loop();
const optional<uint32_t> run_duration = get_run_duration_();
if (run_duration.has_value()) {
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
@@ -45,7 +46,7 @@ void DeepSleepComponent::loop() {
void DeepSleepComponent::begin_sleep(bool manual) {
if (this->prevent_ && !manual) {
this->next_enter_deep_sleep_ = true;
this->defer_sleep_();
return;
}
@@ -190,6 +190,11 @@ class DeepSleepComponent final : public Component {
void schedule_sleep_();
bool should_teardown_();
void defer_sleep_() {
this->next_enter_deep_sleep_ = true;
this->enable_loop();
}
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
@@ -100,7 +100,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning();
ESP_LOGW(TAG, "Waiting for wakeup pin state change");
}
this->next_enter_deep_sleep_ = true;
this->defer_sleep_();
return false;
}
return true;
+4 -1
View File
@@ -3,6 +3,7 @@
#include <utility>
#include <numbers>
#include "display_color_utils.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.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::feed_wdt_pixel_slow_() { App.feed_wdt(); }
bool Display::clip(int x, int y) {
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
return false;
if (!this->get_clipping().inside(x, y))
if (this->is_point_clipped(x, y))
return false;
return true;
}
+19
View File
@@ -758,6 +758,13 @@ class Display : public PollingComponent {
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
/// Whether (x, y) falls outside the active clipping rectangle. Tests the
/// stack top in place: get_clipping() is out of line and returns the Rect
/// by value, which per pixel drawing cannot afford.
bool ESPHOME_ALWAYS_INLINE is_point_clipped(int x, int y) const {
return this->is_clipping() && !this->clipping_rectangle_.back().inside(x, y);
}
/** Check if pixel is within region of display.
*/
bool clip(int x, int y);
@@ -774,6 +781,17 @@ class Display : public PollingComponent {
void do_update_();
void clear_clipping_();
/// Watchdog feed for per pixel loops. App.feed_wdt() is already rate
/// limited, but every call reads the clock; only every 256th pixel makes
/// that call, so the real feeds are unchanged and a pixel costs a counter.
/// At 20 us per pixel on the slowest e-paper path that is about 5 ms
/// between clock reads.
void ESPHOME_ALWAYS_INLINE feed_wdt_per_pixel_() {
if (++this->wdt_pixel_counter_ == 0)
this->feed_wdt_pixel_slow_();
}
void feed_wdt_pixel_slow_();
virtual int get_height_internal() = 0;
virtual int get_width_internal() = 0;
@@ -793,6 +811,7 @@ class Display : public PollingComponent {
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
bool auto_clear_enabled_{true};
std::vector<Rect> clipping_rectangle_;
uint8_t wdt_pixel_counter_{0};
bool show_test_card_{false};
};
@@ -2,7 +2,6 @@
#include <utility>
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::display {
@@ -44,7 +43,7 @@ int DisplayBuffer::get_height() {
}
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
switch (this->rotation_) {
@@ -64,7 +63,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
break;
}
this->draw_absolute_pixel_internal(x, y, color);
App.feed_wdt();
this->feed_wdt_per_pixel_();
}
} // namespace esphome::display
-10
View File
@@ -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);
}
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT
if (!this->is_set()) {
return true;
}
if (absolute) {
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
}
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
}
bool Rect::inside(Rect rect) const {
if (!this->is_set() || !rect.is_set()) {
return true;
+9 -1
View File
@@ -26,7 +26,15 @@ class Rect {
void shrink(Rect rect);
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;
void info(const std::string &prefix = "rect info:");
};
+1 -1
View File
@@ -299,7 +299,7 @@ bool EPaperBase::initialise(bool partial) {
* @return false if the coordinates are out of bounds
*/
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
if (!this->get_clipping().inside(x, y))
if (this->is_point_clipped(x, y))
return false;
if (this->effective_transform_ & SWAP_XY)
std::swap(x, y);
+3 -2
View File
@@ -153,13 +153,14 @@ bool ES7210::configure_mic_gain_() {
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
// 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->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
// Configure mic 4
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 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->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
+176
View File
@@ -111,6 +111,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_FLASH_CHIP = "flash_chip"
CONF_KEY_ID = "key_id"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_NVS_ENCRYPTION = "nvs_encryption"
@@ -189,6 +190,13 @@ PSRAM_XIP_VARIANTS = {
VARIANT_ESP32S31,
}
# Variants whose ROM exports a full-format vsnprintf but no vasprintf
# (esp32c6.rom.newlib-normal.ld). There, the newlib printf engine is only
# linked because esp_http_client calls vasprintf; see vasprintf_stubs.cpp.
# The other variants either export both (classic ESP32, nano-format only) or
# neither, so the engine is already in the image and the wrap saves nothing.
ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6}
# NVS encryption (HMAC peripheral scheme) is only available on variants that
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
@@ -457,6 +465,20 @@ ESP32_CHIP_REVISIONS = {
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
}
# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM
# and only the one matching the flash ID is ever used
ESP32_FLASH_CHIPS = {
"gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP",
"issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP",
"mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP",
"winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP",
"boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP",
"th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP",
"mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
}
FLASH_CHIP_GENERIC = "generic"
FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only
# Socket limit configuration for ESP-IDF
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
@@ -1512,6 +1534,13 @@ def final_validate(config) -> None:
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION],
)
)
if config[CONF_VARIANT] != VARIANT_ESP32S3 and config.get(CONF_FLASH_MODE) == "opi":
errs.append(
cv.Invalid(
f"'{CONF_FLASH_MODE}: opi' is only supported on {VARIANT_ESP32S3}",
path=[CONF_FLASH_MODE],
)
)
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]:
errs.append(
cv.Invalid(
@@ -1519,6 +1548,25 @@ def final_validate(config) -> None:
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
)
)
if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None:
opi = flash_chip == FLASH_CHIP_OPI
if opi and config[CONF_VARIANT] != VARIANT_ESP32S3:
errs.append(
cv.Invalid(
f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
)
)
elif opi != (config.get(CONF_FLASH_MODE) == "opi"):
errs.append(
cv.Invalid(
f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'"
if opi
else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match "
f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
)
)
if (
config[CONF_VARIANT] != VARIANT_ESP32P4
and config.get(CONF_ENGINEERING_SAMPLE) is not None
@@ -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_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_MBEDTLS_TLS_SERVER = "disable_mbedtls_tls_server"
CONF_DISABLE_MBEDTLS_TLS_EXTRAS = "disable_mbedtls_tls_extras"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
@@ -1746,6 +1796,8 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_MBEDTLS_TLS_SERVER_REQUIRED = "mbedtls_tls_server_required"
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED = "mbedtls_tls_extras_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
@@ -1830,6 +1882,30 @@ def require_mbedtls_pkcs7() -> None:
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
def require_mbedtls_tls_server() -> None:
"""Mark that the mbedTLS server-side TLS/DTLS handshake is required.
Call this from components that accept TLS connections (OpenThread's DTLS
commissioner does). This prevents CONFIG_MBEDTLS_TLS_CLIENT_ONLY from
being selected.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] = True
def require_mbedtls_tls_extras(options: Iterable[str] | None = None) -> None:
"""Mark TLS features disabled by ``disable_mbedtls_tls_extras`` as required.
``options`` names the entries of ``MBEDTLS_TLS_EXTRA_OPTIONS`` to keep;
omit it to keep all of them. Call this from components that need AES-CCM,
deterministic ECDSA signing, static RSA/ECDH key exchange, TLS
renegotiation or session tickets, or that run a TLS client against
servers ESPHome cannot vet (wpa_supplicant's EAP client). A user-supplied
sdkconfig_options value is never overridden either.
"""
required = CORE.data[KEY_ESP32].setdefault(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
required.update(MBEDTLS_TLS_EXTRA_OPTIONS if options is None else options)
def require_mbedtls_sha512() -> None:
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
@@ -1929,6 +2005,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
*ESP32_CHIP_REVISIONS, string=True
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
cv.Optional(CONF_FLASH_CHIP): cv.one_of(
FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True
),
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
@@ -1987,6 +2066,8 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_SERVER, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_EXTRAS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
@@ -2302,6 +2383,69 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
# TLS features an HTTPS/MQTT client talking to a modern server never
# negotiates. Static RSA and static ECDH key exchange have no forward secrecy
# and are gone in TLS 1.3, renegotiation is deprecated, esp-tls never enables
# session tickets, AES-CCM ciphersuites are not offered by web servers, and
# deterministic ECDSA only matters when signing with a private key. Together
# they cost ~10 KB of flash whenever TLS is linked (http_request, mqtt).
# wpa_supplicant's EAP client is a second TLS client that talks to RADIUS
# servers ESPHome cannot vet, and a failed EAP handshake leaves the device
# off the network, so the wifi component re-enables all of these when eap is
# configured.
# The EC public key parsing extras stay enabled: they decide whether a peer
# certificate with a compressed point or explicit curve parameters parses,
# which no component can know ahead of time.
MBEDTLS_TLS_EXTRA_OPTIONS = (
"CONFIG_MBEDTLS_KEY_EXCHANGE_RSA",
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA",
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA",
"CONFIG_MBEDTLS_SSL_RENEGOTIATION",
"CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS",
"CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS",
"CONFIG_MBEDTLS_CCM_C",
"CONFIG_MBEDTLS_ECDSA_DETERMINISTIC",
)
# Members of the mbedTLS "TLS Protocol Role" Kconfig choice. Setting one
# member is only valid when the user has not already chosen another.
MBEDTLS_TLS_ROLE_OPTIONS = (
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
"CONFIG_MBEDTLS_TLS_SERVER_ONLY",
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
"CONFIG_MBEDTLS_TLS_DISABLED",
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_mbedtls_tls_sdkconfig(
disable_tls_server: bool, disable_tls_extras: bool
) -> None:
"""Trim mbedTLS to what a TLS client needs unless a component asked otherwise.
Runs at FINAL priority so every require_mbedtls_tls_server() and
require_mbedtls_tls_extras() call has happened. Only the server-side
handshake (~7 KB) is a separate option; nothing in ESPHome accepts TLS
connections, but OpenThread's DTLS commissioner does. A user-supplied
sdkconfig_options value always wins; for the TLS role choice, any member
the user set leaves the whole choice alone so the pair cannot conflict.
"""
data = CORE.data[KEY_ESP32]
sdkconfig = data[KEY_SDKCONFIG_OPTIONS]
if (
disable_tls_server
and not data.get(KEY_MBEDTLS_TLS_SERVER_REQUIRED, False)
and not any(option in sdkconfig for option in MBEDTLS_TLS_ROLE_OPTIONS)
):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_CLIENT_ONLY", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", False)
if disable_tls_extras:
required = data.get(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
for option in MBEDTLS_TLS_EXTRA_OPTIONS:
if option not in required:
set_idf_sdkconfig_default(option, False)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_network_sdkconfig() -> None:
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
@@ -2509,6 +2653,13 @@ async def to_code(config):
# NVS finds stored preferences by key, so preference key migration is possible
cg.add_define("USE_PREFERENCE_KEY_LOOKUP")
cg.add_build_flag("-Wl,-z,noexecstack")
# assert(), HAL_ASSERT and ESP_ERROR_CHECK bake __FILE__ into rodata, and
# IDF's noflash placement puts the flash driver's copies in DRAM. The
# basename keeps the panic output useful at a fraction of the size.
# __FILE_NAME__ is a GCC 12 builtin; IDF 5.0 still ships GCC 11.2.
if idf_version() >= cv.Version(5, 1, 0):
cg.add_build_flag("-D__FILE__=__FILE_NAME__")
cg.add_build_flag("-Wno-builtin-macro-redefined")
# Deferred so KEY_COMPONENTS is fully populated -- see the coroutine.
CORE.add_job(_finalize_arduino_aware_flags)
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
@@ -2566,6 +2717,17 @@ async def to_code(config):
else:
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# esp_http_client calls vasprintf, which on the ESP32-C6 is the only
# reference to newlib's full printf engine (~20 KB: _svfprintf_r,
# _dtoa_r and their helpers); every other caller resolves to the
# ROM. See vasprintf_stubs.cpp. The --undefined flag is needed
# because libsrc.a is scanned before the IDF libraries that
# reference the symbol, so the stub would otherwise never be pulled
# from the archive.
if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS:
cg.add_define("USE_ESP32_VASPRINTF_STUB")
cg.add_build_flag("-Wl,--wrap=vasprintf")
cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf")
else:
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
@@ -2614,6 +2776,8 @@ async def to_code(config):
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
)
# the opi mode choice only exists once octal flash is enabled
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_OCT_FLASH", flash_mode == "opi")
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
@@ -2638,6 +2802,11 @@ async def to_code(config):
add_idf_sdkconfig_option(flag, rev == min_rev)
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
# Keep only the flash vendor driver the board needs; the boot log names it
if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None:
for chip, flag in ESP32_FLASH_CHIPS.items():
add_idf_sdkconfig_option(flag, chip == flash_chip)
# Use SRAM1 region as IRAM on ESP32 (original) variant
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
@@ -2991,6 +3160,13 @@ async def to_code(config):
# FINAL priority: runs after every require_certificate_bundle() call
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
# FINAL priority: runs after every require_mbedtls_tls_*() call
CORE.add_job(
_reconcile_mbedtls_tls_sdkconfig,
advanced[CONF_DISABLE_MBEDTLS_TLS_SERVER],
advanced[CONF_DISABLE_MBEDTLS_TLS_EXTRAS],
)
# FINAL: require_*() calls can come from to_code at or below this priority, so an
# inline read would be iteration-order-dependent; reconcile once after every job ran.
CORE.add_job(
+9 -5
View File
@@ -173,7 +173,10 @@ static const char *const TAG = "esp32.crash";
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
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) {
s_crash_data_valid = true;
// 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;
#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() {
// 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
// and decode each address individually.
void crash_handler_log() {
if (!s_crash_data_valid)
if (!crash_handler_has_data())
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
+1 -6
View File
@@ -4,11 +4,6 @@
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.
void crash_handler_log();
@@ -16,7 +11,7 @@ void crash_handler_log();
/// Call after the data has been delivered to an API client.
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();
} // namespace esphome::esp32
-8
View File
@@ -1,9 +1,6 @@
#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 "crash_handler.h"
#include "esphome/core/hal.h"
#include <esp_clk_tree.h>
@@ -45,11 +42,6 @@ void arch_restart() {
}
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)
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
+22 -13
View File
@@ -83,18 +83,23 @@ void ESP32BLE::setup() {
}
}
void ESP32BLE::enable() {
if (this->state_ != BLE_COMPONENT_STATE_DISABLED)
return;
this->state_ = BLE_COMPONENT_STATE_ENABLE;
}
void ESP32BLE::disable() {
if (this->state_ == BLE_COMPONENT_STATE_DISABLED)
return;
this->state_ = BLE_COMPONENT_STATE_DISABLE;
// Queue the transition for loop(). A pending transition the other way is
// cancelled instead, since nothing was torn down or brought up yet; any other
// state is already there or on its way.
void ESP32BLE::request_state_(bool enable) {
if (enable) {
if (this->state_ == BLE_COMPONENT_STATE_DISABLED) {
this->state_ = BLE_COMPONENT_STATE_ENABLE;
} else if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
}
} else {
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
@@ -580,7 +585,11 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
this->mark_failed();
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) {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
+11 -2
View File
@@ -102,8 +102,8 @@ class ESP32BLE final : public Component {
}
uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; }
void enable();
void disable();
void enable() { this->request_state_(true); }
void disable() { this->request_state_(false); }
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
void setup() override;
void loop() override;
@@ -176,6 +176,15 @@ class ESP32BLE final : public Component {
bool ble_setup_();
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_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
@@ -42,7 +42,7 @@ void BLEClientBase::set_state(espbt::ClientState st) {
void BLEClientBase::loop() {
if (!esp32_ble::global_ble->is_active()) {
this->set_state(espbt::ClientState::INIT);
// ble_before_disabled_event_handler() resets the client.
return;
}
if (this->state() == espbt::ClientState::INIT) {
@@ -72,6 +72,21 @@ void BLEClientBase::loop() {
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() {
ESP_LOGCONFIG(TAG,
" Address: %s\n"
@@ -93,6 +108,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false;
if (this->state() != espbt::ClientState::IDLE)
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");
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
@@ -117,6 +136,15 @@ void BLEClientBase::connect() {
this->connection_index_, this->address_str_);
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_);
this->paired_ = false;
// 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
// Only the cache clean makes the stack's database unsafe to walk.
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
}
@@ -41,6 +41,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void connect() override;
esp_err_t pair();
void disconnect() override;
void ble_before_disabled_event_handler() override;
void unconditional_disconnect();
void release_services();
@@ -114,7 +115,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
#endif
// Group 3: 4-byte types
int gattc_if_;
int gattc_if_{ESP_GATT_IF_NONE};
esp_gatt_status_t status_{ESP_GATT_OK};
// Group 4: Arrays
@@ -139,7 +140,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
uint8_t pending_notify_regs_{0};
bool auto_connect_{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};
// 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 handle_connection_result_(esp_err_t ret);
/// 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)
/// override this to release that state. `reason` is the controller reason code, or
/// ESP_GATT_CONN_TIMEOUT for the safety-timeout path.
/// override this to release that state. `reason` is the controller reason code,
/// 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) {}
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
void set_idle_() {
@@ -3,6 +3,7 @@ import encodings
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32_ble
from esphome.components.const import CONF_MANUFACTURER
from esphome.components.esp32 import request_bluetooth
from esphome.components.esp32_ble import BTLoggers, bt_uuid
import esphome.config_validation as cv
@@ -41,7 +42,6 @@ CONF_DESCRIPTORS = "descriptors"
CONF_ENDIANNESS = "endianness"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_INDICATE = "indicate"
CONF_MANUFACTURER = "manufacturer"
CONF_MANUFACTURER_DATA = "manufacturer_data"
CONF_MAX_CLIENTS = "max_clients"
CONF_ON_WRITE = "on_write"
@@ -597,7 +597,7 @@ async def to_code(config):
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
# Only advertise for the server itself when the configuration gives clients something to
# find. A server that is auto-loaded purely to host a runtime service (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.
cg.add(
var.set_advertising_required(
@@ -40,7 +40,7 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
/** Whether this server needs the device to advertise so clients can find and connect to it.
*
* False for a server that only hosts services created at runtime (e.g. 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.
*/
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_) {
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
} else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) {
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() {
if (!this->parent_->is_active()) {
this->ble_was_disabled_ = true;
return;
} else if (this->ble_was_disabled_) {
}
if (this->ble_was_disabled_) {
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_) {
this->start_scan();
}
@@ -218,7 +223,27 @@ void ESP32BLETracker::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_() {
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 connect() = 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_; }
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; }
// 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:
/// 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_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; }
void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; }
void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; }
void ESP32Camera::set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
/* set exposure parameters */
void ESP32Camera::set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; }
void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
/* set gains parameters */
void ESP32Camera::set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
void ESP32Camera::set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
void ESP32Camera::set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
/* set white balance */
void ESP32Camera::set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
/* set test mode */
void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
/* set fps */
void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) {
this->max_update_interval_ = max_update_interval;
+15 -15
View File
@@ -140,25 +140,25 @@ class ESP32Camera final : public camera::Camera {
void set_pixel_format(ESP32CameraPixelFormat format);
void set_frame_size(ESP32CameraFrameSize size);
void set_jpeg_quality(uint8_t quality);
void set_vertical_flip(bool vertical_flip);
void set_horizontal_mirror(bool horizontal_mirror);
void set_contrast(int contrast);
void set_brightness(int brightness);
void set_saturation(int saturation);
void set_special_effect(ESP32SpecialEffect effect);
void set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
void set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
void set_contrast(int contrast) { this->contrast_ = contrast; }
void set_brightness(int brightness) { this->brightness_ = brightness; }
void set_saturation(int saturation) { this->saturation_ = saturation; }
void set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
/* -- exposure */
void set_aec_mode(ESP32GainControlMode mode);
void set_aec2(bool aec2);
void set_ae_level(int ae_level);
void set_aec_value(uint32_t aec_value);
void set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
void set_aec2(bool aec2) { this->aec2_ = aec2; }
void set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
void set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
/* -- gains */
void set_agc_mode(ESP32GainControlMode mode);
void set_agc_value(uint8_t agc_value);
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling);
void set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
void set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
/* -- white balance */
void set_wb_mode(ESP32WhiteBalanceMode mode);
void set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
/* -- test */
void set_test_pattern(bool test_pattern);
void set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
/* -- framerates */
void set_max_update_interval(uint32_t max_update_interval);
void set_idle_update_interval(uint32_t idle_update_interval);
@@ -169,7 +169,7 @@ void Esp32HostedUpdate::dump_config() {
ESP_LOGCONFIG(TAG,
" Mode: HTTP\n"
" Source URL: %s",
this->source_url_.c_str());
this->source_url_);
#else
ESP_LOGCONFIG(TAG,
" Mode: Embedded\n"
@@ -215,7 +215,7 @@ bool Esp32HostedUpdate::fetch_manifest_() {
auto container = this->http_request_parent_->get(this->source_url_);
if (container == nullptr || container->status_code != 200) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_);
this->status_set_error(LOG_STR("Failed to fetch manifest"));
return false;
}
@@ -25,7 +25,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode setters
void set_source_url(const 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; }
#else
// Embedded mode setters
@@ -38,7 +38,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode members
http_request::HttpRequestComponent *http_request_parent_{nullptr};
std::string source_url_;
const char *source_url_{nullptr}; // literal from codegen
std::string firmware_url_;
// HTTP mode helpers
-8
View File
@@ -363,14 +363,6 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_ENABLE_SERIAL1):
enable_serial1()
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
# a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3,
# which always aborts if exceptions are disabled.
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
cg.add_build_flag("-DNEW_OOM_ABORT")
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
# See throw_stubs.h for details. Must be prepended before <string>, so this
+5 -2
View File
@@ -11,6 +11,9 @@ namespace esphome::esp8266_pwm {
class ESP8266PWM final : public output::FloatOutput, public Component {
public:
// User provided, not "= default": `new(p) ESP8266PWM()` would zero-fill .bss that is already zero.
ESP8266PWM() {}
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
/// Dynamically update frequency
@@ -28,8 +31,8 @@ class ESP8266PWM final : public output::FloatOutput, public Component {
protected:
void write_state(float state) override;
InternalGPIOPin *pin_;
float frequency_{1000.0};
InternalGPIOPin *pin_{nullptr};
float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py
/// Cache last output level for dynamic frequency updating
float last_output_{0.0};
};
+8 -2
View File
@@ -22,6 +22,10 @@ ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Componen
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
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(
output.FLOAT_OUTPUT_SCHEMA.extend(
{
@@ -29,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): cv.All(
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),
cv.require_framework_version(
@@ -48,7 +52,9 @@ async def to_code(config: ConfigType) -> None:
pin = await cg.gpio_pin_expression(config[CONF_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(
+10 -2
View File
@@ -166,9 +166,17 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
CONF_PASSWORD,
)
# web_server and prometheus keep the shared listener up; the captive
# portal's copy only exists on the fallback AP and is the recovery path
# 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 (
(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_ENCRYPTION in conf

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