mirror of
https://github.com/esphome/esphome.git
synced 2026-10-28 00:42:11 +00:00
Compare commits
118
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2740312570 | ||
|
|
54b8e2e6dc | ||
|
|
651863323b | ||
|
|
1e627a31f1 | ||
|
|
e5eb577b49 | ||
|
|
c378ea1300 | ||
|
|
cba4f5bc05 | ||
|
|
8109aa96f6 | ||
|
|
4067f572cc | ||
|
|
1e22861d11 | ||
|
|
282be54d1e | ||
|
|
abadfbfd20 | ||
|
|
93b3dc1876 | ||
|
|
02648d3547 | ||
|
|
41e34c19eb | ||
|
|
a0821c225a | ||
|
|
91b1a82a66 | ||
|
|
7801cf4a8a | ||
|
|
e431bfcb38 | ||
|
|
f5afd141de | ||
|
|
e38ee343b4 | ||
|
|
cf398ea8b2 | ||
|
|
bc1841c1b5 | ||
|
|
ed5a570e17 | ||
|
|
16df92212e | ||
|
|
67871bcf55 | ||
|
|
c51020dbaf | ||
|
|
eb1ea4aefe | ||
|
|
b1bfc512ac | ||
|
|
ce8fad1435 | ||
|
|
4f0faac148 | ||
|
|
fc8611a212 | ||
|
|
6b08aa60e6 | ||
|
|
eea66fc32b | ||
|
|
5bbfe12e4f | ||
|
|
533002c41e | ||
|
|
ebb9037ea1 | ||
|
|
eecea15f4f | ||
|
|
ff9b2a1c83 | ||
|
|
9f9df85aa0 | ||
|
|
2b71d5496d | ||
|
|
2578f17dc8 | ||
|
|
37e9b2b7af | ||
|
|
6a21ab4ea7 | ||
|
|
380938177c | ||
|
|
f66ef23256 | ||
|
|
3e3822e554 | ||
|
|
7564f5ff1a | ||
|
|
a807a8f945 | ||
|
|
280fac11e6 | ||
|
|
9924148302 | ||
|
|
66f829c760 | ||
|
|
a88ec7d90b | ||
|
|
54706e869c | ||
|
|
05f7d5e4f1 | ||
|
|
ddbd89dd2a | ||
|
|
5ed59af920 | ||
|
|
4868b498cf | ||
|
|
c66fa81208 | ||
|
|
58ca345684 | ||
|
|
fd598057ef | ||
|
|
ef7279341f | ||
|
|
b947094f45 | ||
|
|
6c5ab89d5f | ||
|
|
8f511a365a | ||
|
|
006f31af93 | ||
|
|
5bb112f407 | ||
|
|
4ab9298ab3 | ||
|
|
3926612281 | ||
|
|
1ce0bed3f6 | ||
|
|
f91486305f | ||
|
|
f191d5e0c3 | ||
|
|
227ca90aad | ||
|
|
1700a40b7c | ||
|
|
28588310e7 | ||
|
|
10a9baff74 | ||
|
|
a23f7bb569 | ||
|
|
53075e4139 | ||
|
|
5722ccba37 | ||
|
|
94e5c3839d | ||
|
|
574762f078 | ||
|
|
d34ffaf392 | ||
|
|
e6aa575f2e | ||
|
|
639ce609bf | ||
|
|
62eafc477d | ||
|
|
56c3361b9a | ||
|
|
50ca381198 | ||
|
|
89a56298c2 | ||
|
|
390742cf9b | ||
|
|
5e37872da2 | ||
|
|
d34d3994e1 | ||
|
|
d58b37faa1 | ||
|
|
8966567be0 | ||
|
|
20c7dcb1dd | ||
|
|
688af60cbf | ||
|
|
9c00f13606 | ||
|
|
833dd0e812 | ||
|
|
8e1044e8ea | ||
|
|
e5200db6fd | ||
|
|
e3dd2f44a4 | ||
|
|
3ef7460fca | ||
|
|
ae187f81f2 | ||
|
|
84f78831f9 | ||
|
|
13dbbcaa32 | ||
|
|
b66822d9bd | ||
|
|
d1829c495d | ||
|
|
ce87bf9b17 | ||
|
|
51ea97deff | ||
|
|
ab800dc09d | ||
|
|
f65ab5629e | ||
|
|
b84532d254 | ||
|
|
6b11636491 | ||
|
|
2bb98f2d64 | ||
|
|
f3c786c784 | ||
|
|
2250430999 | ||
|
|
d1068d582f | ||
|
|
61d2632851 | ||
|
|
e9d940aca4 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -244,11 +244,20 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Read prek version from requirements_test.txt
|
||||
id: prek
|
||||
# requirements_test.txt is the only place the version is pinned, so a
|
||||
# Dependabot bump there is picked up here without a second edit.
|
||||
run: |
|
||||
if ! version=$(sed -nE 's/^prek==([^[:space:]#]+).*/\1/p' requirements_test.txt) || [ -z "$version" ]; then
|
||||
echo "::error::No prek== pin found in requirements_test.txt."
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
|
||||
with:
|
||||
# Keep in sync with requirements_test.txt.
|
||||
prek-version: "0.4.11"
|
||||
prek-version: ${{ steps.prek.outputs.version }}
|
||||
# This job only runs on pull requests, so nothing ever populates
|
||||
# the cache on dev. Every run would miss and then write a per-pull
|
||||
# request copy, which is what the old seed-cache job existed to
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Keeps pre-commit hook revs in sync with the requirements files.
|
||||
#
|
||||
# Dependabot only bumps the pins in requirements*.txt. Some of those tools
|
||||
# are pinned again as hook revs in .pre-commit-config.yaml. This workflow
|
||||
# runs script/sync_dependency_versions.py against the pull request branch
|
||||
# and pushes a commit with the revs updated.
|
||||
|
||||
name: Sync dependency versions
|
||||
|
||||
on:
|
||||
# pull_request_target rather than pull_request so the App secret is
|
||||
# available on Dependabot pull requests (pull_request runs opened by
|
||||
# Dependabot only see Dependabot secrets). The job below only touches
|
||||
# branches in this repository and only ever executes the script from the
|
||||
# base branch checkout, so fork code never runs with the token.
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- requirements_dev.txt
|
||||
- requirements_test.txt
|
||||
- .pre-commit-config.yaml
|
||||
- script/sync_dependency_versions.py
|
||||
|
||||
# The push to the pull request branch uses the App token minted below, so
|
||||
# the workflow's GITHUB_TOKEN does not need any scopes.
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync pinned versions
|
||||
runs-on: ubuntu-latest
|
||||
# Same-repository branches only: a push to a fork is not possible with
|
||||
# this token, and it keeps untrusted heads out of a privileged job.
|
||||
if: >-
|
||||
github.repository == 'esphome/esphome'
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
# A push made with the workflow's own GITHUB_TOKEN would not start
|
||||
# CI on the new commit; a push with the App token does.
|
||||
permission-contents: write # git push of the sync commit to the pull request branch
|
||||
|
||||
- name: Check out base branch
|
||||
# Provides the script that runs below. Deliberately the base branch
|
||||
# so the pull request cannot change what executes here.
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out pull request branch
|
||||
# No allow-unsafe-pr-checkout here on purpose: checkout v7 only
|
||||
# refuses heads that live in a different repository, and the job
|
||||
# condition above already limits runs to same-repository branches.
|
||||
# Leaving it off keeps that refusal as a backstop for fork heads.
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
path: pull-request
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install yamlrocks
|
||||
# The script edits YAML through yamlrocks. Take the pin from the
|
||||
# base branch requirements so this workflow has no copy of its own.
|
||||
run: pip install "$(grep -E '^yamlrocks==' requirements_test.txt | cut -d'#' -f1)"
|
||||
|
||||
- name: Sync pinned versions
|
||||
run: python script/sync_dependency_versions.py --root pull-request
|
||||
|
||||
- name: Push changes
|
||||
working-directory: pull-request
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "All pinned versions already match the requirements files."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "esphome[bot]"
|
||||
git config user.email "115708604+esphome[bot]@users.noreply.github.com"
|
||||
git commit -am "Sync pinned tool versions with requirements files"
|
||||
git push
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
ci:
|
||||
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
||||
@@ -11,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.3
|
||||
rev: v0.16.6
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -42,7 +41,7 @@ repos:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.37.1
|
||||
rev: v1.38.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
exclude: ^(\.clang-format|\.clang-tidy)$
|
||||
|
||||
@@ -553,6 +553,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
4. **Lint:** Run `prek` to ensure code is compliant.
|
||||
5. **Commit:** Commit your changes. There is no strict format for commit messages.
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
|
||||
7. **Comments:** When commenting on GitHub PRs or issues, don't tag contributors, especially bots. Avoid referring to list items (e.g. from reviews) with the form #nn - this will be interpreted by GitHub as a reference to issue or PR nn. Keep comments short and exclude irrelevant details, backstories, restatement of previous comments and anything that is already obvious to the reader.
|
||||
|
||||
* **Documentation Contributions:**
|
||||
* Documentation is hosted in the separate `esphome/esphome.io` repository.
|
||||
@@ -628,6 +629,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);
|
||||
@@ -839,7 +843,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
cv.rename_key(
|
||||
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component"
|
||||
),
|
||||
cv.Schema({ ... }),
|
||||
cv.Schema({...}),
|
||||
)
|
||||
```
|
||||
For other deprecations, warn manually during validation:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.9.0b5
|
||||
PROJECT_NUMBER = 2026.10.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -23,9 +23,7 @@ from esphome.util import safe_print
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from aioesphomeapi.api_pb2 import (
|
||||
SubscribeLogsResponse, # pylint: disable=no-name-in-module
|
||||
)
|
||||
from aioesphomeapi.api_pb2 import SubscribeLogsResponse # pylint: disable=no-name-in-module
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1664,6 +1664,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;
|
||||
@@ -1676,6 +1677,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");
|
||||
@@ -1802,7 +1816,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());
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.0")
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ public ble_api.h.
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import libretiny, wifi
|
||||
from esphome.components import libretiny
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
@@ -84,15 +84,6 @@ 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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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,117 @@
|
||||
#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);
|
||||
|
||||
/**
|
||||
* Stop forwarding in both directions and hand the UART back to its configured
|
||||
* framing, so another component may use the bus. Main-loop only. The RX task parks
|
||||
* within UART_RX_WAIT_MS (a byte it was already reading is discarded). A host write
|
||||
* already in flight is allowed to drain first, which at low baud rates can take
|
||||
* seconds; the framing is restored only after that, so poll is_paused() rather than
|
||||
* waiting a fixed interval. Host bytes not yet written to the UART are discarded.
|
||||
* The DTR/RTS outputs hold their state while paused and follow the host again on
|
||||
* resume().
|
||||
*/
|
||||
void pause();
|
||||
/**
|
||||
* Re-apply the host's line coding and line state, then resume forwarding. Main-loop
|
||||
* only. Deferred until any host write still draining has finished, so the reload
|
||||
* never truncates it.
|
||||
*/
|
||||
void resume();
|
||||
/// True once both worker tasks are off the bus and the configured framing is restored.
|
||||
/// With no RX task (setup() failed or has not run) there is nothing to wait for.
|
||||
bool is_paused() const {
|
||||
return this->state_ == MainState::MAIN_STATE_PAUSED &&
|
||||
(this->uart_rx_task_handle_ == nullptr || this->rx_parked_ != 0);
|
||||
}
|
||||
|
||||
protected:
|
||||
static void uart_rx_task_fn(void *arg);
|
||||
static void uart_tx_task_fn(void *arg);
|
||||
void uart_rx_task_();
|
||||
void uart_tx_task_();
|
||||
void restore_configured_framing_();
|
||||
// True when the TX task has no write in flight and the UART TX FIFO has drained.
|
||||
bool tx_idle_();
|
||||
void finish_resume_();
|
||||
void drive_line_state_();
|
||||
// Copy the host's line coding onto the UART settings; true if anything changed.
|
||||
bool sync_host_framing_();
|
||||
|
||||
TaskHandle_t uart_rx_task_handle_{nullptr};
|
||||
TaskHandle_t uart_tx_task_handle_{nullptr};
|
||||
|
||||
GPIOPin *dtr_pin_{nullptr};
|
||||
GPIOPin *rts_pin_{nullptr};
|
||||
|
||||
uint32_t reload_requested_at_{0};
|
||||
|
||||
// Worker staging, each sized to the CDC ring buffer it feeds or drains.
|
||||
std::array<uint8_t, CONFIG_TINYUSB_CDC_TX_BUFSIZE> uart_rx_buffer_{};
|
||||
std::array<uint8_t, CONFIG_TINYUSB_CDC_RX_BUFSIZE> uart_tx_buffer_{};
|
||||
|
||||
uart::IDFUARTComponent *uart_parent_;
|
||||
usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent_;
|
||||
|
||||
// YAML framing, captured at setup; the host's line coding overwrites the UART's
|
||||
// settings, so pause() needs the original to restore.
|
||||
uint32_t configured_baud_rate_{0};
|
||||
uart::UARTParityOptions configured_parity_{uart::UART_CONFIG_PARITY_NONE};
|
||||
uint8_t configured_stop_bits_{0};
|
||||
uint8_t configured_data_bits_{0};
|
||||
|
||||
// Written on the main loop, read by both worker tasks. uint8_t rather than bool:
|
||||
// GCC on Xtensa emits an out-of-line call for atomic<bool>.
|
||||
std::atomic<uint8_t> paused_{0};
|
||||
// Raised by the RX task while parked and by the TX task around each UART write, so
|
||||
// the pause hand-off knows when the bus is actually free.
|
||||
std::atomic<uint8_t> rx_parked_{0};
|
||||
std::atomic<uint8_t> tx_busy_{0};
|
||||
// Main-loop state; paused_ mirrors it for the worker tasks.
|
||||
enum class MainState : uint8_t {
|
||||
MAIN_STATE_RUNNING,
|
||||
MAIN_STATE_RELOAD_PENDING, // host line coding debounced, forwarding continues
|
||||
MAIN_STATE_PAUSING, // waiting for TX idle to restore the configured framing
|
||||
MAIN_STATE_PAUSED,
|
||||
MAIN_STATE_RESUMING, // resume() requested while a host write still drains
|
||||
};
|
||||
MainState state_{MainState::MAIN_STATE_RUNNING};
|
||||
// Host line state, recorded even while paused so resume() can re-drive the pins.
|
||||
bool host_dtr_{false};
|
||||
bool host_rts_{false};
|
||||
// True once the host has sent any line coding; resume() then re-syncs to it.
|
||||
bool host_coding_seen_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::cdc_acm_uart
|
||||
#endif
|
||||
@@ -30,6 +30,7 @@ CONF_KEYS = "keys"
|
||||
CONF_LABEL = "label"
|
||||
CONF_LIBRETINY = "libretiny"
|
||||
CONF_LOOP = "loop"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
CONF_NOX_INDEX = "nox_index"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
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)
|
||||
@@ -23,11 +23,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
FILTER_SOURCE_FILES,
|
||||
DebugComponent,
|
||||
)
|
||||
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import
|
||||
|
||||
DEPENDENCIES = ["debug"]
|
||||
|
||||
|
||||
@@ -9,11 +9,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
FILTER_SOURCE_FILES,
|
||||
DebugComponent,
|
||||
)
|
||||
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import
|
||||
|
||||
DEPENDENCIES = ["debug"]
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
|
||||
this->status_set_warning();
|
||||
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
|
||||
}
|
||||
this->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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -189,6 +189,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
|
||||
@@ -1732,6 +1739,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 +1755,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 +1841,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.
|
||||
|
||||
@@ -1987,6 +2022,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 +2339,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.
|
||||
@@ -2566,6 +2666,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")
|
||||
@@ -2991,6 +3102,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(
|
||||
|
||||
@@ -3,18 +3,11 @@ import esphome.codegen as cg
|
||||
# Re-exported for the many esp32-side users; defined in esphome.const
|
||||
# and esphome.espidf so the upload/logs fast path can use them without
|
||||
# importing this package.
|
||||
from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
|
||||
KEY_ESP32,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_VARIANT,
|
||||
)
|
||||
from esphome.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION, KEY_VARIANT # noqa: F401 # pylint: disable=unused-import
|
||||
|
||||
# Back compat for external components only; in-tree callers import it
|
||||
# from esphome.espidf directly.
|
||||
from esphome.espidf import ( # noqa: F401 # pylint: disable=unused-import
|
||||
variant_to_idf_target,
|
||||
)
|
||||
from esphome.espidf import variant_to_idf_target # noqa: F401 # pylint: disable=unused-import
|
||||
|
||||
KEY_BOARD = "board"
|
||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -42,7 +42,7 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import HexInt
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.external_files import RemoteFile
|
||||
from esphome.types import ConfigType
|
||||
@@ -76,18 +76,16 @@ def compute_local_image_path(value: str | ConfigType) -> Path:
|
||||
return external_files.compute_local_file_path(DOMAIN, url)
|
||||
|
||||
|
||||
def local_path(value: Path | ConfigType) -> Path:
|
||||
# cv.file_ has already resolved the path against the config dir.
|
||||
return value[CONF_PATH] if isinstance(value, dict) else value
|
||||
def local_path(value: str | ConfigType) -> str:
|
||||
value = value[CONF_PATH] if isinstance(value, dict) else value
|
||||
return str(CORE.relative_config_path(value))
|
||||
|
||||
|
||||
def download_file(url: str, path: Path) -> Path:
|
||||
def download_file(url: str, path: Path) -> str:
|
||||
# The shared NETWORK_TIMEOUT applies; a per-caller timeout would be
|
||||
# silently ignored on a per-run memo hit anyway (memos key by path).
|
||||
external_files.download_content(url, path)
|
||||
# Keep the Path: config-hash normalizes Path values under the data dir,
|
||||
# which a str would dump verbatim and break the CLI/add-on comparison.
|
||||
return path
|
||||
return str(path)
|
||||
|
||||
|
||||
def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
|
||||
@@ -95,13 +93,13 @@ def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
|
||||
return MDI_SOURCES[source] + mdi_id + ".svg", base_dir / f"{mdi_id}.svg"
|
||||
|
||||
|
||||
def download_gh_svg(value: str | ConfigType, source: str) -> Path:
|
||||
def download_gh_svg(value: str | ConfigType, source: str) -> str:
|
||||
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
|
||||
url, path = _gh_svg_url_path(mdi_id, source)
|
||||
return download_file(url, path)
|
||||
|
||||
|
||||
def download_image(value: str | ConfigType) -> Path:
|
||||
def download_image(value: str | ConfigType) -> str:
|
||||
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||
return download_file(value, compute_local_image_path(value))
|
||||
|
||||
@@ -149,7 +147,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None:
|
||||
PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref)
|
||||
|
||||
|
||||
def validate_file_shorthand(value: Any) -> Path:
|
||||
def validate_file_shorthand(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
if (remote := _parse_remote_shorthand(value)) is not None:
|
||||
return download_file(remote.url, remote.path)
|
||||
@@ -167,7 +165,7 @@ LOCAL_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def mdi_schema(source: str) -> cv.All:
|
||||
def validate_mdi(value: ConfigType) -> Path:
|
||||
def validate_mdi(value: ConfigType) -> str:
|
||||
return download_gh_svg(value, source)
|
||||
|
||||
return cv.All(
|
||||
|
||||
@@ -59,11 +59,6 @@ void Infrared::setup() {
|
||||
// Set up traits based on configuration
|
||||
this->traits_.set_supports_transmitter(this->has_transmitter());
|
||||
this->traits_.set_supports_receiver(this->has_receiver());
|
||||
|
||||
// Register as listener for received IR data
|
||||
if (this->receiver_ != nullptr) {
|
||||
this->receiver_->register_listener(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Infrared::dump_config() {
|
||||
|
||||
@@ -119,7 +119,8 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
/// Set the remote receiver component
|
||||
/// Set the remote receiver component; the listener registration happens from codegen, see
|
||||
/// remote_base.attach_receiver
|
||||
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
|
||||
/// Set the remote transmitter component
|
||||
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import infrared, remote_receiver, remote_transmitter
|
||||
from esphome.components import (
|
||||
infrared,
|
||||
remote_base,
|
||||
remote_receiver,
|
||||
remote_transmitter,
|
||||
)
|
||||
from esphome.components.const import CONF_RECEIVER_FREQUENCY
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
|
||||
@@ -82,8 +87,7 @@ async def to_code(config: dict[str, Any]) -> None:
|
||||
|
||||
# Link receiver if specified
|
||||
if CONF_REMOTE_RECEIVER_ID in config:
|
||||
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
|
||||
cg.add(var.set_receiver(receiver))
|
||||
await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID)
|
||||
|
||||
# Set receiver demodulation frequency if specified (metadata only, no hardware effect)
|
||||
if CONF_RECEIVER_FREQUENCY in config:
|
||||
|
||||
@@ -97,10 +97,6 @@ void RfProxy::setup() {
|
||||
|
||||
// remote_transmitter/receiver always uses OOK (on-off keying)
|
||||
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
|
||||
|
||||
if (this->receiver_ != nullptr) {
|
||||
this->receiver_->register_listener(this);
|
||||
}
|
||||
}
|
||||
|
||||
void RfProxy::dump_config() {
|
||||
|
||||
@@ -56,7 +56,8 @@ class RfProxy final : public radio_frequency::RadioFrequency {
|
||||
|
||||
/// Set the remote transmitter component
|
||||
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
|
||||
/// Set the remote receiver component
|
||||
/// Set the remote receiver component; the listener registration happens from codegen, see
|
||||
/// remote_base.attach_receiver
|
||||
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
|
||||
|
||||
/// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import radio_frequency, remote_receiver, remote_transmitter
|
||||
from esphome.components import (
|
||||
radio_frequency,
|
||||
remote_base,
|
||||
remote_receiver,
|
||||
remote_transmitter,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
|
||||
import esphome.final_validate as fv
|
||||
@@ -66,5 +71,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_transmitter(transmitter))
|
||||
|
||||
if CONF_REMOTE_RECEIVER_ID in config:
|
||||
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
|
||||
cg.add(var.set_receiver(receiver))
|
||||
await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID)
|
||||
@@ -64,6 +64,8 @@ static const char *const TAG = "ld2420";
|
||||
|
||||
// Local const's
|
||||
static constexpr uint16_t REFRESH_RATE_MS = 1000;
|
||||
static constexpr uint32_t CMD_ACK_TIMEOUT_MS = 1000;
|
||||
static constexpr uint8_t CMD_MAX_RETRIES = 3;
|
||||
|
||||
// Command sets
|
||||
static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE;
|
||||
@@ -631,37 +633,39 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::write_cmd_frame_(const CmdFrameT &frame) {
|
||||
uint8_t cmd_buffer[MAX_LINE_LENGTH];
|
||||
uint16_t length = 0;
|
||||
const uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame.header, sizeof(frame.header));
|
||||
length += sizeof(frame.header);
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame_data_bytes, sizeof(frame.data_length));
|
||||
length += sizeof(frame.data_length);
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame.command, sizeof(frame.command));
|
||||
length += sizeof(frame.command);
|
||||
|
||||
memcpy(&cmd_buffer[length], frame.data, frame.data_length);
|
||||
length += frame.data_length;
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame.footer, sizeof(frame.footer));
|
||||
length += sizeof(frame.footer);
|
||||
this->write_array(cmd_buffer, length);
|
||||
}
|
||||
|
||||
int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
uint32_t start_millis = millis();
|
||||
uint8_t error = 0;
|
||||
uint8_t ack_buffer[MAX_LINE_LENGTH];
|
||||
uint8_t cmd_buffer[MAX_LINE_LENGTH];
|
||||
this->cmd_reply_.ack = false;
|
||||
if (frame.command != CMD_RESTART) {
|
||||
this->cmd_active_ = true;
|
||||
} // Restart does not reply, thus no ack state required
|
||||
uint8_t retry = 3;
|
||||
uint8_t retry = CMD_MAX_RETRIES;
|
||||
while (retry) {
|
||||
frame.length = 0;
|
||||
uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size
|
||||
|
||||
memcpy(&cmd_buffer[frame.length], &frame.header, sizeof(frame.header));
|
||||
frame.length += sizeof(frame.header);
|
||||
|
||||
memcpy(&cmd_buffer[frame.length], &frame_data_bytes, sizeof(frame.data_length));
|
||||
frame.length += sizeof(frame.data_length);
|
||||
|
||||
memcpy(&cmd_buffer[frame.length], &frame.command, sizeof(frame.command));
|
||||
frame.length += sizeof(frame.command);
|
||||
|
||||
for (uint16_t index = 0; index < frame.data_length; index++) {
|
||||
memcpy(&cmd_buffer[frame.length], &frame.data[index], sizeof(frame.data[index]));
|
||||
frame.length += sizeof(frame.data[index]);
|
||||
}
|
||||
|
||||
memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
|
||||
frame.length += sizeof(frame.footer);
|
||||
this->write_array(cmd_buffer, frame.length);
|
||||
this->write_cmd_frame_(frame);
|
||||
|
||||
error = 0;
|
||||
if (frame.command == CMD_RESTART) {
|
||||
@@ -674,7 +678,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
}
|
||||
delay_microseconds_safe(1450);
|
||||
// Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT.
|
||||
if ((millis() - start_millis) > 1000) {
|
||||
if ((millis() - start_millis) > CMD_ACK_TIMEOUT_MS) {
|
||||
start_millis = millis();
|
||||
error = LD2420_ERROR_TIMEOUT;
|
||||
retry--;
|
||||
@@ -691,16 +695,20 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
return error;
|
||||
}
|
||||
|
||||
void LD2420Component::build_config_mode_frame_(CmdFrameT &frame, bool enable) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
|
||||
if (enable) {
|
||||
memcpy(&frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER));
|
||||
frame.data_length += sizeof(CMD_PROTOCOL_VER);
|
||||
}
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
}
|
||||
|
||||
uint8_t LD2420Component::set_config_mode(bool enable) {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
|
||||
if (enable) {
|
||||
memcpy(&cmd_frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER));
|
||||
cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
|
||||
}
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
this->build_config_mode_frame_(cmd_frame, enable);
|
||||
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"),
|
||||
cmd_frame.command);
|
||||
return this->send_cmd_from_array(cmd_frame);
|
||||
@@ -719,18 +727,6 @@ void LD2420Component::ld2420_restart() {
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
void LD2420Component::get_reg_value_(uint16_t reg) {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_REGISTER;
|
||||
cmd_frame.data[1] = reg;
|
||||
cmd_frame.data_length += 2;
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command);
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
@@ -754,19 +750,22 @@ void LD2420Component::handle_cmd_error(uint16_t error) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::build_gate_threshold_frame_(CmdFrameT &frame, uint8_t gate) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate]));
|
||||
frame.data_length += 2;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate]));
|
||||
frame.data_length += 2;
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, frame.command);
|
||||
}
|
||||
|
||||
int LD2420Component::get_gate_threshold_(uint8_t gate) {
|
||||
uint8_t error;
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate]));
|
||||
cmd_frame.data_length += 2;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate]));
|
||||
cmd_frame.data_length += 2;
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, cmd_frame.command);
|
||||
error = this->send_cmd_from_array(cmd_frame);
|
||||
this->build_gate_threshold_frame_(cmd_frame, gate);
|
||||
uint8_t error = this->send_cmd_from_array(cmd_frame);
|
||||
if (error == 0) {
|
||||
this->current_config.move_thresh[gate] = cmd_reply_.data[0];
|
||||
this->current_config.still_thresh[gate] = cmd_reply_.data[1];
|
||||
@@ -774,24 +773,27 @@ int LD2420Component::get_gate_threshold_(uint8_t gate) {
|
||||
return error;
|
||||
}
|
||||
|
||||
int LD2420Component::get_min_max_distances_timeout_() {
|
||||
uint8_t error;
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG,
|
||||
void LD2420Component::build_min_max_timeout_frame_(CmdFrameT &frame) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_MIN_GATE_REG,
|
||||
sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number
|
||||
cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG,
|
||||
frame.data_length += sizeof(CMD_MIN_GATE_REG);
|
||||
memcpy(&frame.data[frame.data_length], &CMD_MAX_GATE_REG,
|
||||
sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number
|
||||
cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG,
|
||||
frame.data_length += sizeof(CMD_MAX_GATE_REG);
|
||||
memcpy(&frame.data[frame.data_length], &CMD_TIMEOUT_REG,
|
||||
sizeof(CMD_TIMEOUT_REG)); // Register: global delay time
|
||||
cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG);
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command);
|
||||
error = this->send_cmd_from_array(cmd_frame);
|
||||
frame.data_length += sizeof(CMD_TIMEOUT_REG);
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", frame.command);
|
||||
}
|
||||
|
||||
int LD2420Component::get_min_max_distances_timeout_() {
|
||||
CmdFrameT cmd_frame;
|
||||
this->build_min_max_timeout_frame_(cmd_frame);
|
||||
uint8_t error = this->send_cmd_from_array(cmd_frame);
|
||||
if (error == 0) {
|
||||
this->current_config.min_gate = (uint16_t) cmd_reply_.data[0];
|
||||
this->current_config.max_gate = (uint16_t) cmd_reply_.data[1];
|
||||
@@ -800,33 +802,40 @@ int LD2420Component::get_min_max_distances_timeout_() {
|
||||
return error;
|
||||
}
|
||||
|
||||
void LD2420Component::build_system_mode_frame_(CmdFrameT &frame, uint16_t mode) {
|
||||
uint16_t unknown_parm = 0x0000;
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_WRITE_SYS_PARAM;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE));
|
||||
frame.data_length += sizeof(CMD_SYSTEM_MODE);
|
||||
memcpy(&frame.data[frame.data_length], &mode, sizeof(mode));
|
||||
frame.data_length += sizeof(mode);
|
||||
memcpy(&frame.data[frame.data_length], &unknown_parm, sizeof(unknown_parm));
|
||||
frame.data_length += sizeof(unknown_parm);
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending write system mode command: %2X", frame.command);
|
||||
}
|
||||
|
||||
void LD2420Component::set_system_mode(uint16_t mode) {
|
||||
CmdFrameT cmd_frame;
|
||||
uint16_t unknown_parm = 0x0000;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_WRITE_SYS_PARAM;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE));
|
||||
cmd_frame.data_length += sizeof(CMD_SYSTEM_MODE);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &mode, sizeof(mode));
|
||||
cmd_frame.data_length += sizeof(mode);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm));
|
||||
cmd_frame.data_length += sizeof(unknown_parm);
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
|
||||
this->build_system_mode_frame_(cmd_frame, mode);
|
||||
if (this->send_cmd_from_array(cmd_frame) == 0) {
|
||||
this->set_mode_(mode);
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::build_version_frame_(CmdFrameT &frame) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_READ_VERSION;
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read firmware version command: %2X", frame.command);
|
||||
}
|
||||
|
||||
void LD2420Component::get_firmware_version_() {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_VERSION;
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
|
||||
ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command);
|
||||
this->build_version_frame_(cmd_frame);
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
struct CmdFrameT {
|
||||
uint32_t header{0};
|
||||
uint32_t footer{0};
|
||||
uint16_t length{0};
|
||||
uint16_t command{0};
|
||||
uint16_t data_length{0};
|
||||
uint8_t data[18];
|
||||
@@ -155,8 +154,13 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
|
||||
void get_firmware_version_();
|
||||
int get_gate_threshold_(uint8_t gate);
|
||||
void get_reg_value_(uint16_t reg);
|
||||
int get_min_max_distances_timeout_();
|
||||
void write_cmd_frame_(const CmdFrameT &frame);
|
||||
void build_config_mode_frame_(CmdFrameT &frame, bool enable);
|
||||
void build_min_max_timeout_frame_(CmdFrameT &frame);
|
||||
void build_gate_threshold_frame_(CmdFrameT &frame, uint8_t gate);
|
||||
void build_version_frame_(CmdFrameT &frame);
|
||||
void build_system_mode_frame_(CmdFrameT &frame, uint16_t mode);
|
||||
uint16_t get_mode_() { return this->system_mode_; };
|
||||
void set_mode_(uint16_t mode) { this->system_mode_ = mode; };
|
||||
bool get_presence_() { return this->presence_; };
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "esphome/components/esp32/crash_handler.h"
|
||||
#include <esp_log.h>
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#include <driver/uart.h>
|
||||
#include <soc/soc_caps.h>
|
||||
@@ -16,8 +17,10 @@
|
||||
#include <driver/usb_serial_jtag_vfs.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "esp_idf_version.h"
|
||||
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
|
||||
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
|
||||
#include "esp_sleep.h"
|
||||
#endif
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
@@ -87,6 +90,12 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
|
||||
// ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes).
|
||||
const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1;
|
||||
uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0);
|
||||
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
|
||||
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
|
||||
// Always flush before going to light sleep. Could be disabled for devices
|
||||
// without TOP_PD or if source_clk = UART_SCLK_RTC
|
||||
esp_sleep_set_console_uart_handling_mode(ESP_SLEEP_ALWAYS_FLUSH_UART);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Logger::pre_setup() {
|
||||
|
||||
@@ -14,7 +14,7 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
)
|
||||
from esphome.core import Lambda
|
||||
from esphome.cpp_generator import TemplateArguments, get_variable
|
||||
from esphome.cpp_generator import StaticCastExpression, TemplateArguments, get_variable
|
||||
from esphome.cpp_types import nullptr
|
||||
|
||||
from .defines import (
|
||||
@@ -30,7 +30,6 @@ from .defines import (
|
||||
CONF_SHOW_SNOW,
|
||||
CONF_TOP_LAYER,
|
||||
PARTS,
|
||||
StaticCastExpression,
|
||||
add_warning,
|
||||
get_focused_widgets,
|
||||
get_options,
|
||||
|
||||
@@ -10,12 +10,7 @@ from typing import Any
|
||||
from esphome import codegen as cg, config_validation as cv
|
||||
from esphome.const import CONF_ITEMS
|
||||
from esphome.core import CORE, ID, Lambda
|
||||
from esphome.cpp_generator import (
|
||||
CallExpression,
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, StaticCastExpression, call_lambda
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import Expression, SafeExpType
|
||||
|
||||
@@ -157,17 +152,6 @@ def get_refreshed_widgets() -> set:
|
||||
return _get_data(KEY_REFRESHED_WIDGETS, set())
|
||||
|
||||
|
||||
class StaticCastExpression(Expression):
|
||||
__slots__ = ("type", "exp")
|
||||
|
||||
def __init__(self, type: Any, exp: SafeExpType):
|
||||
self.type = str(type)
|
||||
self.exp = cg.safe_exp(exp)
|
||||
|
||||
def __str__(self):
|
||||
return f"static_cast<{self.type}>({self.exp})"
|
||||
|
||||
|
||||
def add_define(macro: str, value="1"):
|
||||
lv_defines = get_defines()
|
||||
value = str(value)
|
||||
@@ -192,31 +176,6 @@ def addr(arg) -> MockObj:
|
||||
return MockObj(f"&{arg}")
|
||||
|
||||
|
||||
def call_lambda(lamb: LambdaExpression) -> Expression:
|
||||
"""
|
||||
Given a lambda, either reduce to a simple expression or call it, possibly with parameters
|
||||
from the surrounding context
|
||||
:param lamb:
|
||||
:return:
|
||||
"""
|
||||
expr = lamb.content.strip()
|
||||
if expr.startswith("return") and expr.endswith(";"):
|
||||
# Convert a lambda returning a simple expression to just that expression
|
||||
expr = cg.RawExpression(expr[6:-1].strip())
|
||||
# Don't cast if the return type is a class
|
||||
if isinstance(lamb.return_type, MockObjClass):
|
||||
return expr
|
||||
return StaticCastExpression(lamb.return_type, expr)
|
||||
# If lambda has parameters, call it with their names
|
||||
# Parameter names come from hardcoded component code (like "x", "it", "event")
|
||||
# not from user input, so they're safe to use directly
|
||||
if lamb.parameters and lamb.parameters.parameters:
|
||||
return CallExpression(
|
||||
lamb, *[MockObj(x.id) for x in lamb.parameters.parameters]
|
||||
)
|
||||
return CallExpression(lamb)
|
||||
|
||||
|
||||
class LValidator:
|
||||
"""
|
||||
A validator for a particular type used in LVGL. Usable in configs as a validator, also
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
)
|
||||
from esphome.core import CORE, ID, Lambda
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_generator import MockObj, StaticCastExpression, call_lambda
|
||||
from esphome.cpp_types import ESPTime, int32, uint32
|
||||
from esphome.helpers import cpp_string_escape
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
@@ -33,9 +33,7 @@ from .defines import (
|
||||
LV_FONTS,
|
||||
LValidator,
|
||||
LvConstant,
|
||||
StaticCastExpression,
|
||||
add_lv_use,
|
||||
call_lambda,
|
||||
get_esphome_fonts_used,
|
||||
get_lv_fonts_used,
|
||||
get_lv_images_used,
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import ID, EsphomeError, TimePeriod
|
||||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_generator import MockObj, call_lambda
|
||||
from esphome.schema_extractors import EnableSchemaExtraction
|
||||
from esphome.types import Expression
|
||||
|
||||
@@ -42,7 +42,6 @@ from ..defines import (
|
||||
STATES,
|
||||
LValidator,
|
||||
add_lv_use,
|
||||
call_lambda,
|
||||
get_styles_used,
|
||||
get_theme_widget_map,
|
||||
get_widget_map,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate, remote_transmitter, sensor, uart
|
||||
from esphome.components import climate, remote_base, remote_transmitter, sensor, uart
|
||||
from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode
|
||||
from esphome.components.remote_base import CONF_TRANSMITTER_ID
|
||||
import esphome.config_validation as cv
|
||||
@@ -280,6 +280,7 @@ async def to_code(config):
|
||||
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
|
||||
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
|
||||
if CONF_TRANSMITTER_ID in config:
|
||||
remote_base.request_protocol("midea") # ir_transmitter.h uses it from C++
|
||||
cg.add_define("USE_REMOTE_TRANSMITTER")
|
||||
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
|
||||
cg.add(var.set_transmitter(transmitter_))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
from esphome.components import climate_ir, remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_USE_FAHRENHEIT
|
||||
from esphome.types import ConfigType
|
||||
@@ -19,5 +19,9 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# midea_ir uses MideaProtocol from C++ and auto-loads coolix, whose coolix.cpp uses
|
||||
# CoolixProtocol even when no coolix climate is configured
|
||||
remote_base.request_protocol("midea")
|
||||
remote_base.request_protocol("coolix")
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))
|
||||
@@ -35,8 +35,8 @@ void MipiDsi::setup() {
|
||||
.bus_id = 0, // index from 0, specify the DSI host to use
|
||||
.num_data_lanes =
|
||||
this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability
|
||||
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, // Clock source for the DPHY
|
||||
.lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps
|
||||
// phy_clk_src left at 0 to enable runtime auto-select.
|
||||
.lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps
|
||||
};
|
||||
auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_);
|
||||
if (err != ESP_OK) {
|
||||
|
||||
@@ -306,9 +306,9 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBuffer
|
||||
|
||||
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
|
||||
if (samples_to_duck > 0) {
|
||||
esp_audio_libs::ducking::apply(audio_source->mutable_data(),
|
||||
static_cast<uint8_t>(this->audio_stream_info_.get_bits_per_sample() / 8),
|
||||
samples_to_duck, this->ducking_state_);
|
||||
this->ducking_ramp_.process(audio_source->mutable_data(),
|
||||
static_cast<uint8_t>(this->audio_stream_info_.get_bits_per_sample() / 8),
|
||||
samples_to_duck);
|
||||
}
|
||||
|
||||
return bytes_read;
|
||||
@@ -316,7 +316,7 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBuffer
|
||||
|
||||
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
|
||||
const uint32_t transition_samples = duration > 0 ? this->audio_stream_info_.ms_to_samples(duration) : 0;
|
||||
esp_audio_libs::ducking::set_target(this->ducking_state_, decibel_reduction, transition_samples);
|
||||
this->ducking_ramp_.set_target_db_reduction_over(decibel_reduction, transition_samples);
|
||||
}
|
||||
|
||||
void SourceSpeaker::enter_stopping_state_() {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/static_task.h"
|
||||
|
||||
#include <ducking.h> // esp-audio-libs
|
||||
#include <gain.h> // esp-audio-libs
|
||||
|
||||
#include <freertos/event_groups.h>
|
||||
|
||||
@@ -108,7 +108,7 @@ class SourceSpeaker final : public speaker::Speaker, public Component {
|
||||
|
||||
bool pause_state_{false};
|
||||
|
||||
esp_audio_libs::ducking::DuckingState ducking_state_{};
|
||||
esp_audio_libs::gain::GainRamp ducking_ramp_;
|
||||
|
||||
std::atomic<uint32_t> pending_playback_frames_{0};
|
||||
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
|
||||
|
||||
@@ -26,44 +26,129 @@ static const uint8_t MLX90614_ID4 = 0x3F;
|
||||
|
||||
static const char *const TAG = "mlx90614";
|
||||
|
||||
// The EEPROM cell has a limited number of write cycles, so stop retrying after a few failures
|
||||
static constexpr uint8_t EMISSIVITY_WRITE_ATTEMPTS = 3;
|
||||
|
||||
// SMBus packet error code: CRC-8 with polynomial 0x07, MSB first
|
||||
static uint8_t crc8_pec(const uint8_t *data, uint8_t len) { return crc8(data, len, 0x00, 0x07, true); }
|
||||
|
||||
void MLX90614Component::setup() {
|
||||
if (!this->write_emissivity_()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
if (std::isnan(this->emissivity_)) {
|
||||
return;
|
||||
}
|
||||
this->emissivity_write_attempts_ = EMISSIVITY_WRITE_ATTEMPTS;
|
||||
this->try_write_emissivity_();
|
||||
if (this->emissivity_write_attempts_ != 0) {
|
||||
this->status_set_warning(LOG_STR("Failed to write emissivity, will retry"));
|
||||
}
|
||||
}
|
||||
|
||||
void MLX90614Component::try_write_emissivity_() {
|
||||
if (this->emissivity_write_attempts_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (this->write_emissivity_()) {
|
||||
this->emissivity_write_attempts_ = 0;
|
||||
return;
|
||||
}
|
||||
if (--this->emissivity_write_attempts_ == 0) {
|
||||
ESP_LOGE(TAG, "Giving up on writing emissivity after %u attempts", EMISSIVITY_WRITE_ATTEMPTS);
|
||||
this->emissivity_write_failed_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool MLX90614Component::write_emissivity_() {
|
||||
if (std::isnan(this->emissivity_))
|
||||
// Skip the write when the EEPROM already holds the desired value to save write cycles
|
||||
uint16_t current_emissivity;
|
||||
if (this->read_register_(MLX90614_EMISSIVITY, current_emissivity) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto desired_emissivity = static_cast<uint16_t>(this->emissivity_ * 0xFFFF);
|
||||
if (current_emissivity == desired_emissivity) {
|
||||
return true;
|
||||
uint16_t value = (uint16_t) (this->emissivity_ * 65535);
|
||||
if (!this->write_bytes_(MLX90614_EMISSIVITY, 0)) {
|
||||
return false;
|
||||
}
|
||||
delay(10);
|
||||
if (!this->write_bytes_(MLX90614_EMISSIVITY, value)) {
|
||||
return false;
|
||||
}
|
||||
delay(10);
|
||||
return true;
|
||||
|
||||
return this->write_register_(MLX90614_EMISSIVITY, desired_emissivity);
|
||||
}
|
||||
|
||||
bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) {
|
||||
bool MLX90614Component::write_register_(uint8_t reg, uint16_t data) {
|
||||
// The PEC covers the whole write transaction: SLA+W, command, data low, data high
|
||||
uint8_t buf[5];
|
||||
buf[0] = this->address_ << 1;
|
||||
buf[1] = reg;
|
||||
buf[2] = data & 0xFF;
|
||||
buf[3] = data >> 8;
|
||||
buf[4] = crc8(buf, 4, 0x00, 0x07, true);
|
||||
return this->write_bytes(reg, buf + 2, 3);
|
||||
|
||||
// See datasheet 8.3.3.1 EEPROM write sequence
|
||||
// 1. Write 0x0000 into the cell of interest (erases the cell)
|
||||
buf[2] = buf[3] = 0;
|
||||
buf[4] = crc8_pec(buf, 4);
|
||||
auto ec = this->write_register(reg, buf + 2, 3);
|
||||
if (ec != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Can't erase register 0x%02X, error %d", reg, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Wait at least 5ms
|
||||
delay(10);
|
||||
|
||||
// 3. Write the new value
|
||||
if (data != 0) {
|
||||
buf[2] = data & 0xFF;
|
||||
buf[3] = data >> 8;
|
||||
buf[4] = crc8_pec(buf, 4);
|
||||
ec = this->write_register(reg, buf + 2, 3);
|
||||
if (ec != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Can't write register 0x%02X, error %d", reg, ec);
|
||||
return false;
|
||||
}
|
||||
// 4. Wait at least 5ms
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// 5. Read back to confirm the value was stored
|
||||
uint16_t read_back;
|
||||
ec = this->read_register_(reg, read_back);
|
||||
if (ec != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Can't check register 0x%02X value, error %d", reg, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (read_back != data) {
|
||||
ESP_LOGW(TAG, "Read back mismatch on register 0x%02X. Expected 0x%04X, got 0x%04X", reg, data, read_back);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
i2c::ErrorCode MLX90614Component::read_register_(uint8_t reg, uint16_t &data) {
|
||||
// The PEC covers the whole read transaction: SLA+W, command, SLA+R, data low, data high
|
||||
uint8_t buf[6];
|
||||
buf[0] = this->address_ << 1;
|
||||
buf[1] = reg;
|
||||
buf[2] = (this->address_ << 1) | 0x01;
|
||||
|
||||
const auto ec = this->read_register(reg, buf + 3, 3);
|
||||
if (ec != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "i2c read error %d", ec);
|
||||
return ec;
|
||||
}
|
||||
|
||||
const auto expected_pec = crc8_pec(buf, 5);
|
||||
if (buf[5] != expected_pec) {
|
||||
ESP_LOGW(TAG, "i2c CRC error. Expected 0x%02X, got 0x%02X", expected_pec, buf[5]);
|
||||
return i2c::ERROR_CRC;
|
||||
}
|
||||
|
||||
data = encode_uint16(buf[4], buf[3]);
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
void MLX90614Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MLX90614:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
if (this->emissivity_write_attempts_ != 0) {
|
||||
ESP_LOGW(TAG, " Emissivity not written yet, will retry");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Ambient", this->ambient_sensor_);
|
||||
@@ -71,33 +156,41 @@ void MLX90614Component::dump_config() {
|
||||
}
|
||||
|
||||
void MLX90614Component::update() {
|
||||
uint8_t emissivity[3];
|
||||
if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
// Temperature reads run regardless of the emissivity state so a failure still shows up as NAN
|
||||
this->try_write_emissivity_();
|
||||
|
||||
// Publishes NAN on a bus or CRC failure so a stuck reading is visible instead of silently stale
|
||||
auto publish_sensor = [this](sensor::Sensor *sensor, uint8_t reg) {
|
||||
if (sensor == nullptr) {
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
uint16_t raw;
|
||||
const auto ec = this->read_register_(reg, raw);
|
||||
if (ec != i2c::ERROR_OK) {
|
||||
sensor->publish_state(NAN);
|
||||
return ec;
|
||||
}
|
||||
|
||||
// Bit 15 set means the device flagged the reading as invalid
|
||||
const float temperature = (raw & 0x8000) ? NAN : raw * 0.02f - 273.15f;
|
||||
ESP_LOGD(TAG, "'%s': Got temperature=%.1f°C", sensor->get_name().c_str(), temperature);
|
||||
sensor->publish_state(temperature);
|
||||
return ec;
|
||||
};
|
||||
|
||||
const auto object_ec = publish_sensor(this->object_sensor_, MLX90614_TEMPERATURE_OBJECT_1);
|
||||
const auto ambient_ec = publish_sensor(this->ambient_sensor_, MLX90614_TEMPERATURE_AMBIENT);
|
||||
|
||||
if (object_ec != i2c::ERROR_OK || ambient_ec != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR("Failed to read some sensors"));
|
||||
} else if (this->emissivity_write_failed_) {
|
||||
this->status_set_warning(LOG_STR("Failed to write emissivity"));
|
||||
} else if (this->emissivity_write_attempts_ != 0) {
|
||||
this->status_set_warning(LOG_STR("Failed to write emissivity, will retry"));
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
uint8_t raw_object[3];
|
||||
if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t raw_ambient[3];
|
||||
if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
float ambient = raw_ambient[1] & 0x80 ? NAN : encode_uint16(raw_ambient[1], raw_ambient[0]) * 0.02f - 273.15f;
|
||||
float object = raw_object[1] & 0x80 ? NAN : encode_uint16(raw_object[1], raw_object[0]) * 0.02f - 273.15f;
|
||||
|
||||
ESP_LOGD(TAG, "Got Temperature=%.1f°C Ambient=%.1f°C", object, ambient);
|
||||
|
||||
if (this->ambient_sensor_ != nullptr && !std::isnan(ambient))
|
||||
this->ambient_sensor_->publish_state(ambient);
|
||||
if (this->object_sensor_ != nullptr && !std::isnan(object))
|
||||
this->object_sensor_->publish_state(object);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
} // namespace esphome::mlx90614
|
||||
@@ -18,13 +18,18 @@ class MLX90614Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_emissivity(float emissivity) { emissivity_ = emissivity; }
|
||||
|
||||
protected:
|
||||
void try_write_emissivity_();
|
||||
bool write_emissivity_();
|
||||
|
||||
bool write_bytes_(uint8_t reg, uint16_t data);
|
||||
bool write_register_(uint8_t reg, uint16_t data);
|
||||
i2c::ErrorCode read_register_(uint8_t reg, uint16_t &data);
|
||||
|
||||
sensor::Sensor *ambient_sensor_{nullptr};
|
||||
sensor::Sensor *object_sensor_{nullptr};
|
||||
|
||||
float emissivity_{NAN};
|
||||
// Remaining attempts to program the emissivity EEPROM cell, bounded to limit cell wear
|
||||
uint8_t emissivity_write_attempts_{0};
|
||||
bool emissivity_write_failed_{false};
|
||||
};
|
||||
} // namespace esphome::mlx90614
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any, Literal, NamedTuple
|
||||
|
||||
@@ -49,8 +48,6 @@ ModbusServerDevice = modbus_ns.class_("ModbusServerDevice")
|
||||
CommandOptions = modbus_ns.struct("CommandOptions")
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ALLOW_BROADCAST_READ = "allow_broadcast_read"
|
||||
CONF_EXPECT_BROADCAST_WRITE_RESPONSE = "expect_broadcast_write_response"
|
||||
CONF_ROLE = "role"
|
||||
CONF_MODBUS_ID = "modbus_id"
|
||||
CONF_SEND_WAIT_TIME = "send_wait_time"
|
||||
@@ -59,28 +56,6 @@ CONF_TURNAROUND_TIME = "turnaround_time"
|
||||
MODBUS_ROLES = ["client", "server"]
|
||||
|
||||
|
||||
# The write (mutating) function codes, matching modbus::helpers::is_function_code_write(). 0x17
|
||||
# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half.
|
||||
_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17})
|
||||
|
||||
# Codes the hub refuses at address 0; keep in sync with modbus::helpers::is_function_code_broadcastable().
|
||||
_NON_BROADCASTABLE_FUNCTION_CODES = frozenset(
|
||||
{0x01, 0x02, 0x03, 0x04, 0x14, 0x15, 0x17, 0x18}
|
||||
)
|
||||
|
||||
|
||||
def is_function_code_write(function_code: int) -> bool:
|
||||
"""True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first,
|
||||
so an exception-flagged code still classifies by its base code (the runtime hub never queues one:
|
||||
queue_pdu() refuses them). Keep in sync with modbus::helpers::is_function_code_write()."""
|
||||
return function_code & 0x7F in _WRITE_FUNCTION_CODES
|
||||
|
||||
|
||||
def is_function_code_broadcastable(function_code: int) -> bool:
|
||||
"""True if the hub accepts the function code at address 0 without allow_broadcast_read."""
|
||||
return function_code & 0x7F not in _NON_BROADCASTABLE_FUNCTION_CODES
|
||||
|
||||
|
||||
class _CommandOption(NamedTuple):
|
||||
"""One per-command option forwarded to the hub (modbus::CommandOptions)."""
|
||||
|
||||
@@ -89,47 +64,14 @@ class _CommandOption(NamedTuple):
|
||||
validator: Any # the static (non-templatable) validator for the key
|
||||
cpp_type: Any # the C++ type the value is generated as
|
||||
default: Any
|
||||
# Function codes the hub honours the option on; it is stripped from any other.
|
||||
applies_to: Callable[[int], bool]
|
||||
requires_broadcast_address: bool = False
|
||||
|
||||
|
||||
def _not_write(function_code: int) -> bool:
|
||||
return not is_function_code_write(function_code)
|
||||
|
||||
|
||||
def _not_broadcastable(function_code: int) -> bool:
|
||||
return not is_function_code_broadcastable(function_code)
|
||||
|
||||
|
||||
# Per-direction command options, single-sourced so the schema, setters and applicability rule cannot
|
||||
# drift; the C++ side adds the matching field per the rules on CommandOptions (modbus.h).
|
||||
# Per-direction command options. Single-sourcing the schema and the setter generation here keeps
|
||||
# them from drifting; the C++ side must add the matching field per the rules documented on
|
||||
# CommandOptions (modbus.h).
|
||||
_COMMAND_OPTIONS: dict[str, list[_CommandOption]] = {
|
||||
"read": [
|
||||
_CommandOption(
|
||||
CONF_CONTINUOUS, "continuous", cv.boolean, bool, False, _not_write
|
||||
),
|
||||
_CommandOption(
|
||||
CONF_ALLOW_BROADCAST_READ,
|
||||
"allow_broadcast_read",
|
||||
cv.boolean,
|
||||
bool,
|
||||
False,
|
||||
_not_broadcastable,
|
||||
requires_broadcast_address=True,
|
||||
),
|
||||
],
|
||||
"write": [
|
||||
_CommandOption(
|
||||
CONF_EXPECT_BROADCAST_WRITE_RESPONSE,
|
||||
"expect_broadcast_write_response",
|
||||
cv.boolean,
|
||||
bool,
|
||||
False,
|
||||
is_function_code_broadcastable,
|
||||
requires_broadcast_address=True,
|
||||
),
|
||||
],
|
||||
"read": [_CommandOption(CONF_CONTINUOUS, "continuous", cv.boolean, bool, False)],
|
||||
"write": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -140,75 +82,32 @@ def _command_options(direction: str) -> list[_CommandOption]:
|
||||
raise ValueError(f"unknown command-options direction {direction!r}") from None
|
||||
|
||||
|
||||
def broadcast_only_option_keys() -> list[str]:
|
||||
return [
|
||||
option.conf_key
|
||||
for options in _COMMAND_OPTIONS.values()
|
||||
for option in options
|
||||
if option.requires_broadcast_address
|
||||
]
|
||||
# The write (mutating) function codes, matching modbus::helpers::is_function_code_write(). 0x17
|
||||
# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half.
|
||||
_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17})
|
||||
|
||||
|
||||
def reject_broadcast_options_for_unicast(
|
||||
address_key: str,
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
"""Reject a broadcast-only option set true on a literal address other than 0."""
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
address = config.get(address_key)
|
||||
if not isinstance(address, int) or address == BROADCAST_ADDRESS:
|
||||
return config
|
||||
for key in broadcast_only_option_keys():
|
||||
if config.get(key) is True:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' only applies to the broadcast address; set '{address_key}: 0' or "
|
||||
f"remove the option.",
|
||||
path=[key],
|
||||
)
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def reject_inapplicable_command_options(
|
||||
pdu_key: str,
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
"""Reject an option set true that the hub would strip from a literal PDU's function code."""
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
pdu = config[pdu_key]
|
||||
if not isinstance(pdu, list):
|
||||
return config
|
||||
for direction in _COMMAND_OPTIONS:
|
||||
for option in _command_options(direction):
|
||||
if config.get(option.conf_key) is True and not option.applies_to(
|
||||
pdu[0]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"'{option.conf_key}: true' does not apply to function code "
|
||||
f"0x{pdu[0]:02X}",
|
||||
path=[option.conf_key],
|
||||
)
|
||||
return config
|
||||
|
||||
return validator
|
||||
def is_function_code_write(function_code: int) -> bool:
|
||||
"""True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first,
|
||||
so an exception-flagged code still classifies by its base code (the runtime hub never queues one:
|
||||
queue_pdu() refuses them). Keep in sync with modbus::helpers::is_function_code_write()."""
|
||||
return function_code & 0x7F in _WRITE_FUNCTION_CODES
|
||||
|
||||
|
||||
def command_options_schema(
|
||||
*,
|
||||
direction: Literal["read", "write"],
|
||||
templatable: bool = False,
|
||||
function_code: int | None = None,
|
||||
*, direction: Literal["read", "write"], templatable: bool = False
|
||||
) -> dict[cv.Optional, Any]:
|
||||
"""Schema fragment for the per-command options of one direction; `function_code` (a typed
|
||||
action's fixed code) leaves out the options that do not apply to it.
|
||||
"""Schema fragment for the per-command options a component forwards to the hub
|
||||
(modbus::CommandOptions). Extend this into any schema that queues commands. Keys are
|
||||
direction-specific so a schema never offers an option the hub would strip (e.g.
|
||||
continuous on a write); the write side has no options yet. For actions (templatable=True the
|
||||
keys also accept lambdas), register the values with register_templatable_command_options().
|
||||
"""
|
||||
return {
|
||||
cv.Optional(option.conf_key, default=option.default): (
|
||||
cv.templatable(option.validator) if templatable else option.validator
|
||||
)
|
||||
for option in _command_options(direction)
|
||||
if function_code is None or option.applies_to(function_code)
|
||||
}
|
||||
|
||||
|
||||
@@ -231,25 +130,6 @@ def command_options_expression(
|
||||
)
|
||||
|
||||
|
||||
def add_command_options(
|
||||
var: MockObj,
|
||||
setter: str,
|
||||
config: ConfigType,
|
||||
*,
|
||||
direction: Literal["read", "write"],
|
||||
) -> None:
|
||||
"""Emit `var.<setter>(<options>)` for a config validated with command_options_schema() of the
|
||||
same direction, skipped when every option is at its C++ default."""
|
||||
if all(
|
||||
config.get(option.conf_key, option.default) == option.default
|
||||
for option in _command_options(direction)
|
||||
):
|
||||
return
|
||||
cg.add(
|
||||
getattr(var, setter)(command_options_expression(config, direction=direction))
|
||||
)
|
||||
|
||||
|
||||
async def register_templatable_command_options(
|
||||
var: MockObj, config: ConfigType, args: TemplateArgsType, direction: str
|
||||
) -> None:
|
||||
|
||||
@@ -832,7 +832,7 @@ void ModbusClientHub::send_next_frame_() {
|
||||
}
|
||||
|
||||
cmd->sent();
|
||||
if (cmd->fire_and_forget()) {
|
||||
if (cmd->frame.address() == BROADCAST_ADDRESS) {
|
||||
// A broadcast (address 0) is never answered (Modbus 4.1), so it is fire-and-forget: on_sent above
|
||||
// reports the transmission, and the entry then retires with no terminal callback instead of
|
||||
// occupying the waiting slot until the send-wait timeout expires. The turnaround delay already
|
||||
@@ -1074,6 +1074,11 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span<const uint8_t> pdu, M
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address == BROADCAST_ADDRESS && !helpers::is_function_code_broadcastable(pdu[0])) {
|
||||
ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize the caller's options in place (the param is a by-value copy) so everything stored or
|
||||
// merged below carries effective options, never the raw request.
|
||||
// continuous is ignored for every mutating code (re-writing a value forever is never intended).
|
||||
@@ -1081,24 +1086,6 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span<const uint8_t> pdu, M
|
||||
ESP_LOGW(TAG, "continuous is ignored for a mutating function (0x%X, address %" PRIu8 ")", pdu[0], address);
|
||||
options.continuous = false;
|
||||
}
|
||||
if (address != BROADCAST_ADDRESS) {
|
||||
options.allow_broadcast_read = false;
|
||||
options.expect_broadcast_write_response = false;
|
||||
} else {
|
||||
const bool broadcastable = helpers::is_function_code_broadcastable(pdu[0]);
|
||||
if (options.allow_broadcast_read && broadcastable) {
|
||||
ESP_LOGV(TAG, "allow_broadcast_read is ignored for function 0x%X: it is broadcastable", pdu[0]);
|
||||
options.allow_broadcast_read = false;
|
||||
}
|
||||
if (options.expect_broadcast_write_response && !broadcastable) {
|
||||
ESP_LOGV(TAG, "expect_broadcast_write_response is ignored for function 0x%X: it is not broadcastable", pdu[0]);
|
||||
options.expect_broadcast_write_response = false;
|
||||
}
|
||||
if (!broadcastable && !options.allow_broadcast_read) {
|
||||
ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// A duplicate of a live entry with the same owner is not queued twice; it resolves against that
|
||||
// entry: anonymous -> dropped; continuous incoming -> convert the entry to a poll; one-shot onto a
|
||||
@@ -1139,7 +1126,6 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span<const uint8_t> pdu, M
|
||||
ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", request absorbed (pending %" PRIu8 ")", address,
|
||||
item.pending);
|
||||
}
|
||||
item.options.expect_broadcast_write_response |= options.expect_broadcast_write_response;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -111,15 +111,11 @@ enum class FrameState : uint8_t {
|
||||
// Per-command send options. Append-only; pass via designated initializers ({.continuous = true}).
|
||||
// A new field reaches the queue with no plumbing but arrives inert until it defines three rules:
|
||||
// normalization in queue_pdu(), a merge rule for duplicate absorption, and teardown in
|
||||
// retire()/silent_retire(). Bit-packed: stored per entry, controller and writer entity, passed by value.
|
||||
// retire()/silent_retire().
|
||||
struct CommandOptions {
|
||||
// A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes.
|
||||
bool continuous : 1 {false};
|
||||
// Wait for the reply to a read sent to address 0, for a device that answers the broadcast address.
|
||||
bool allow_broadcast_read : 1 {false};
|
||||
bool expect_broadcast_write_response : 1 {false};
|
||||
bool continuous{false};
|
||||
};
|
||||
static_assert(sizeof(CommandOptions) == 1, "CommandOptions must stay one byte");
|
||||
|
||||
struct ModbusDeviceCommand {
|
||||
ModbusClientDevice *device;
|
||||
@@ -162,10 +158,6 @@ struct ModbusDeviceCommand {
|
||||
this->pending = 0;
|
||||
this->device = nullptr;
|
||||
}
|
||||
bool fire_and_forget() const {
|
||||
return this->frame.address() == BROADCAST_ADDRESS && !this->options.allow_broadcast_read &&
|
||||
!this->options.expect_broadcast_write_response;
|
||||
}
|
||||
// Fire-and-forget completion for a broadcast (address 0): the frame was transmitted (on_sent already
|
||||
// fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with no terminal callback.
|
||||
void complete_broadcast() {
|
||||
@@ -199,8 +191,7 @@ struct ModbusDeviceCommand {
|
||||
} else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED
|
||||
this->state = FrameState::RETIRED;
|
||||
}
|
||||
// Only continuous ends with the clear; the delivery flags must survive for a granted retry.
|
||||
this->options.continuous = false;
|
||||
this->options = {}; // reset every option
|
||||
}
|
||||
|
||||
// True while the entry is still waiting for a response
|
||||
@@ -543,27 +534,27 @@ class ModbusClientDevice {
|
||||
return this->queue_pdu(
|
||||
helpers::create_read_pdu(FunctionCode::READ_DISCRETE_INPUTS, start_address, number_of_inputs), options);
|
||||
}
|
||||
bool write_single_register(uint16_t start_address, uint16_t value, CommandOptions options = {}) {
|
||||
return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value), options);
|
||||
bool write_single_register(uint16_t start_address, uint16_t value) {
|
||||
return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value));
|
||||
}
|
||||
bool write_single_coil(uint16_t address, bool value, CommandOptions options = {}) {
|
||||
return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value), options);
|
||||
bool write_single_coil(uint16_t address, bool value) {
|
||||
return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value));
|
||||
}
|
||||
bool write_multiple_registers(uint16_t start_address, std::span<const uint16_t> values, CommandOptions options = {}) {
|
||||
bool write_multiple_registers(uint16_t start_address, std::span<const uint16_t> values) {
|
||||
// Empty goes to the full-size builder so the rejection log names this method's limit, not the small one's.
|
||||
if (!values.empty() && values.size() <= helpers::MAX_FEW_REGISTERS)
|
||||
return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values), options);
|
||||
return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values), options);
|
||||
return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values));
|
||||
return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values));
|
||||
}
|
||||
/// Note: std::vector<bool> cannot bind to std::span<const bool>; use a contiguous bool container or the packed
|
||||
/// overload.
|
||||
bool write_multiple_coils(uint16_t start_address, std::span<const bool> values, CommandOptions options = {}) {
|
||||
return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values), options);
|
||||
bool write_multiple_coils(uint16_t start_address, std::span<const bool> values) {
|
||||
return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values));
|
||||
}
|
||||
/// Packed variant: a PackedBits view (the same layout on_read_coils() delivers), so
|
||||
/// read-modify-write needs no unpack/repack.
|
||||
bool write_multiple_coils(uint16_t start_address, PackedBits bits, CommandOptions options = {}) {
|
||||
return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits), options);
|
||||
bool write_multiple_coils(uint16_t start_address, PackedBits bits) {
|
||||
return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits));
|
||||
}
|
||||
/// FC 0x17: the read-back is delivered through on_read_holding_registers(), and a device exception
|
||||
/// (typically a rejected write half) arrives there too via its status - one callback handles both
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.components import modbus
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CONTINUOUS,
|
||||
CONF_COUNT,
|
||||
CONF_ID,
|
||||
CONF_ON_ERROR,
|
||||
@@ -157,6 +158,24 @@ _ACTION_BASE_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _no_continuous_on_write(config: ConfigType) -> ConfigType:
|
||||
"""Reject `continuous: true` on a static write PDU: continuous polling only applies to reads.
|
||||
Only the fully-static case is decidable here; the hub strips the flag from mutating PDUs at
|
||||
runtime, so a templated pdu or continuous falls through to that backstop."""
|
||||
pdu = config[CONF_PDU]
|
||||
if (
|
||||
isinstance(pdu, list)
|
||||
and config.get(CONF_CONTINUOUS) is True
|
||||
and modbus.is_function_code_write(pdu[0])
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_CONTINUOUS}: true' does not apply to a write PDU (function code "
|
||||
f"0x{pdu[0]:02X}); continuous polling only applies to reads",
|
||||
path=[CONF_CONTINUOUS],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
MODBUS_CLIENT_SEND_SCHEMA = cv.All(
|
||||
_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
@@ -167,12 +186,10 @@ MODBUS_CLIENT_SEND_SCHEMA = cv.All(
|
||||
)
|
||||
),
|
||||
**modbus.command_options_schema(direction="read", templatable=True),
|
||||
**modbus.command_options_schema(direction="write", templatable=True),
|
||||
cv.Optional(CONF_ON_RESPONSE): _handler_schema(),
|
||||
}
|
||||
),
|
||||
modbus.reject_inapplicable_command_options(CONF_PDU),
|
||||
modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS),
|
||||
_no_continuous_on_write,
|
||||
)
|
||||
|
||||
|
||||
@@ -244,7 +261,8 @@ async def register_client_action(
|
||||
var.get_not_sent_trigger(), [(_PDU_SPAN, "request")], not_sent_conf
|
||||
)
|
||||
# Wire any command options the action's schema opted into (e.g. continuous on reads). Pass the
|
||||
# matching direction so a write action never generates a read option's setter.
|
||||
# matching direction so a write action never generates a read option's setter; the write side
|
||||
# has no options yet, so this is a no-op there.
|
||||
await modbus.register_templatable_command_options(
|
||||
var, config, args, command_direction
|
||||
)
|
||||
@@ -261,8 +279,6 @@ async def modbus_client_send_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
template_ = await cg.templatable(config[CONF_PDU], args, _PDU_BUFFER)
|
||||
cg.add(var.set_pdu(template_))
|
||||
# The read set is wired by register_client_action() below.
|
||||
await modbus.register_templatable_command_options(var, config, args, "write")
|
||||
return await register_client_action(
|
||||
var,
|
||||
config,
|
||||
@@ -337,7 +353,6 @@ def _read_schema(max_count: int) -> cv.All:
|
||||
}
|
||||
),
|
||||
_no_address_overflow(CONF_COUNT),
|
||||
modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS),
|
||||
)
|
||||
|
||||
|
||||
@@ -349,35 +364,21 @@ def _write_multiple_schema(item: Callable[[Any], Any], max_values: int) -> cv.Al
|
||||
cv.Required(CONF_VALUES): cv.templatable(
|
||||
cv.All(cv.ensure_list(item), cv.Length(min=1, max=max_values))
|
||||
),
|
||||
**modbus.command_options_schema(direction="write", templatable=True),
|
||||
}
|
||||
),
|
||||
_no_address_overflow(CONF_VALUES),
|
||||
modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS),
|
||||
)
|
||||
|
||||
|
||||
_READ_REGISTERS_SCHEMA = _read_schema(modbus.MAX_NUM_OF_REGISTERS_TO_READ)
|
||||
|
||||
_WRITE_SINGLE_REGISTER_SCHEMA = cv.All(
|
||||
_TYPED_ACTION_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.hex_uint16_t),
|
||||
**modbus.command_options_schema(direction="write", templatable=True),
|
||||
}
|
||||
),
|
||||
modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS),
|
||||
_WRITE_SINGLE_REGISTER_SCHEMA = _TYPED_ACTION_SCHEMA.extend(
|
||||
{cv.Required(CONF_VALUE): cv.templatable(cv.hex_uint16_t)}
|
||||
)
|
||||
|
||||
# A coil is one bit, so the value is a boolean - the wire only carries 0x0000 or 0xFF00.
|
||||
_WRITE_SINGLE_COIL_SCHEMA = cv.All(
|
||||
_TYPED_ACTION_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.boolean),
|
||||
**modbus.command_options_schema(direction="write", templatable=True),
|
||||
}
|
||||
),
|
||||
modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS),
|
||||
_WRITE_SINGLE_COIL_SCHEMA = _TYPED_ACTION_SCHEMA.extend(
|
||||
{cv.Required(CONF_VALUE): cv.templatable(cv.boolean)}
|
||||
)
|
||||
|
||||
|
||||
@@ -541,15 +542,10 @@ _READ_WRITE_MULTIPLE_REGISTERS_SCHEMA = cv.All(
|
||||
cv.Length(min=1, max=modbus.MAX_NUM_OF_REGISTERS_TO_WRITE_RW),
|
||||
)
|
||||
),
|
||||
# 0x17 counts as a read at address 0, so it takes allow_broadcast_read only.
|
||||
**modbus.command_options_schema(
|
||||
direction="read", templatable=True, function_code=0x17
|
||||
),
|
||||
}
|
||||
),
|
||||
_no_address_overflow(CONF_READ_COUNT, CONF_READ_ADDRESS),
|
||||
_no_address_overflow(CONF_VALUES, CONF_WRITE_ADDRESS),
|
||||
modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -85,36 +85,18 @@ template<typename... Ts> class ClientActionBase : public Action<Ts...>, public m
|
||||
/// builds its static struct; declaring the values here instead of per action means a new read option
|
||||
/// costs one TEMPLATABLE_VALUE plus one field below, and every read action picks it up.
|
||||
/// The read/write split mirrors _COMMAND_OPTIONS in the modbus component's Python
|
||||
/// (command_options_schema(direction="read") adds exactly these keys); WriteCommandOptions is the twin.
|
||||
/// (command_options_schema(direction="read") adds exactly these keys). When a write-side option
|
||||
/// arrives it gets a WriteCommandOptions twin, so write actions never carry read-only members.
|
||||
template<typename... Ts> class ReadCommandOptions {
|
||||
public:
|
||||
// Poll: re-queue after each success until downgraded (replay with false) or failed. The hub strips
|
||||
// it for mutating function codes at the door (see modbus::CommandOptions).
|
||||
TEMPLATABLE_VALUE(bool, continuous)
|
||||
TEMPLATABLE_VALUE(bool, allow_broadcast_read)
|
||||
|
||||
protected:
|
||||
/// The options for this send, with every templatable value resolved against the action's arguments.
|
||||
modbus::CommandOptions command_options_(const Ts &...x) const {
|
||||
return {.continuous = this->continuous_.value(x...),
|
||||
.allow_broadcast_read = this->allow_broadcast_read_.value(x...)};
|
||||
}
|
||||
};
|
||||
|
||||
/// The write-side per-command options (command_options_schema(direction="write") adds exactly these keys).
|
||||
template<typename... Ts> class WriteCommandOptions {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(bool, expect_broadcast_write_response)
|
||||
|
||||
protected:
|
||||
/// Resolves every write option into `options`, so send's merge of both sets stays exhaustive.
|
||||
void apply_write_command_options_(modbus::CommandOptions &options, const Ts &...x) const {
|
||||
options.expect_broadcast_write_response = this->expect_broadcast_write_response_.value(x...);
|
||||
}
|
||||
modbus::CommandOptions write_command_options_(const Ts &...x) const {
|
||||
modbus::CommandOptions options{};
|
||||
this->apply_write_command_options_(options, x...);
|
||||
return options;
|
||||
return {.continuous = this->continuous_.value(x...)};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,11 +107,8 @@ template<typename... Ts> class WriteCommandOptions {
|
||||
/// modbus::helpers::create_*_pdu() builders and return it directly (smaller builder results convert).
|
||||
/// A PduBuffer drops bytes past modbus::MAX_PDU_SIZE without reporting it (the hub's oversize check
|
||||
/// cannot fire - that limit is the capacity), so an over-long lambda-built PDU is silently truncated.
|
||||
/// A raw PDU may be a read or a write, so this action carries both option sets.
|
||||
template<typename... Ts>
|
||||
class ModbusClientSendAction : public ClientActionBase<Ts...>,
|
||||
public ReadCommandOptions<Ts...>,
|
||||
public WriteCommandOptions<Ts...> {
|
||||
class ModbusClientSendAction : public ClientActionBase<Ts...>, public ReadCommandOptions<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(modbus::helpers::PduBuffer, pdu)
|
||||
|
||||
@@ -137,11 +116,7 @@ class ModbusClientSendAction : public ClientActionBase<Ts...>,
|
||||
return &this->response_trigger_;
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
modbus::CommandOptions options = this->command_options_(x...);
|
||||
this->apply_write_command_options_(options, x...);
|
||||
this->send_or_resolve_(this->pdu_.value(x...), options);
|
||||
}
|
||||
void play(const Ts &...x) override { this->send_or_resolve_(this->pdu_.value(x...), this->command_options_(x...)); }
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
|
||||
this->response_trigger_.trigger(request_pdu, response_pdu);
|
||||
@@ -243,8 +218,7 @@ template<typename... Ts> class ReadBitsAction : public TypedClientActionBase<Ts.
|
||||
|
||||
/// modbus_client.write_single_register: on_response is the acknowledgement (the ack only echoes the
|
||||
/// request, so it carries no arguments).
|
||||
template<typename... Ts>
|
||||
class WriteSingleRegisterAction : public TypedClientActionBase<Ts...>, public WriteCommandOptions<Ts...> {
|
||||
template<typename... Ts> class WriteSingleRegisterAction : public TypedClientActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, start_address)
|
||||
TEMPLATABLE_VALUE(uint16_t, value)
|
||||
@@ -253,8 +227,7 @@ class WriteSingleRegisterAction : public TypedClientActionBase<Ts...>, public Wr
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->send_or_resolve_(
|
||||
modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...)),
|
||||
this->write_command_options_(x...));
|
||||
modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...)));
|
||||
}
|
||||
void on_write_single_register(uint16_t address, uint16_t value, modbus::ResponseStatus status) override {
|
||||
if (modbus::succeeded(status))
|
||||
@@ -267,8 +240,7 @@ class WriteSingleRegisterAction : public TypedClientActionBase<Ts...>, public Wr
|
||||
|
||||
/// modbus_client.write_single_coil: on_response is the acknowledgement (no arguments). A coil holds one
|
||||
/// bit, so the value is a bool - the wire only ever carries 0x0000 or 0xFF00.
|
||||
template<typename... Ts>
|
||||
class WriteSingleCoilAction : public TypedClientActionBase<Ts...>, public WriteCommandOptions<Ts...> {
|
||||
template<typename... Ts> class WriteSingleCoilAction : public TypedClientActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, start_address)
|
||||
TEMPLATABLE_VALUE(bool, value)
|
||||
@@ -277,8 +249,7 @@ class WriteSingleCoilAction : public TypedClientActionBase<Ts...>, public WriteC
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->send_or_resolve_(
|
||||
modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...)),
|
||||
this->write_command_options_(x...));
|
||||
modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...)));
|
||||
}
|
||||
void on_write_single_coil(uint16_t address, bool value, modbus::ResponseStatus status) override {
|
||||
if (modbus::succeeded(status))
|
||||
@@ -293,8 +264,7 @@ class WriteSingleCoilAction : public TypedClientActionBase<Ts...>, public WriteC
|
||||
/// A `values:` list is emitted as a flash array and sent straight from there; only a lambda builds a
|
||||
/// vector, and only when it runs. Same split as canbus's send action, and for the same reason: a static
|
||||
/// list must not allocate on every play().
|
||||
template<typename... Ts>
|
||||
class WriteMultipleRegistersAction : public TypedClientActionBase<Ts...>, public WriteCommandOptions<Ts...> {
|
||||
template<typename... Ts> class WriteMultipleRegistersAction : public TypedClientActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, start_address)
|
||||
|
||||
@@ -318,13 +288,11 @@ class WriteMultipleRegistersAction : public TypedClientActionBase<Ts...>, public
|
||||
// the empty PDU then resolves via on_not_sent like any refused send.
|
||||
if (this->len_ >= 0) {
|
||||
this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(
|
||||
start, std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))),
|
||||
this->write_command_options_(x...));
|
||||
start, std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))));
|
||||
return;
|
||||
}
|
||||
const std::vector<uint16_t> values = this->values_.func(x...);
|
||||
this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(values)),
|
||||
this->write_command_options_(x...));
|
||||
this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(values)));
|
||||
}
|
||||
void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override {
|
||||
@@ -345,8 +313,7 @@ class WriteMultipleRegistersAction : public TypedClientActionBase<Ts...>, public
|
||||
/// A `values:` list is packed into wire layout at code-generation time and stored in flash, so play()
|
||||
/// neither allocates nor packs. A lambda returns std::vector<bool> - already a bit per coil rather than
|
||||
/// a byte - and is packed into a stack buffer on the way to the builder.
|
||||
template<typename... Ts>
|
||||
class WriteMultipleCoilsAction : public TypedClientActionBase<Ts...>, public WriteCommandOptions<Ts...> {
|
||||
template<typename... Ts> class WriteMultipleCoilsAction : public TypedClientActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, start_address)
|
||||
|
||||
@@ -367,16 +334,13 @@ class WriteMultipleCoilsAction : public TypedClientActionBase<Ts...>, public Wri
|
||||
const uint16_t start = this->start_address_.value(x...);
|
||||
if (this->count_ >= 0) {
|
||||
const auto count = static_cast<uint16_t>(this->count_);
|
||||
this->send_or_resolve_(
|
||||
modbus::helpers::create_write_coils_pdu(
|
||||
start, modbus::PackedBits(std::span<const uint8_t>(this->values_.packed, modbus::packed_bit_bytes(count)),
|
||||
count)),
|
||||
this->write_command_options_(x...));
|
||||
this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(
|
||||
start,
|
||||
modbus::PackedBits(std::span<const uint8_t>(this->values_.packed, modbus::packed_bit_bytes(count)), count)));
|
||||
return;
|
||||
}
|
||||
// The builder packs and bound-checks; an over-long set is rejected and logged there.
|
||||
this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...)),
|
||||
this->write_command_options_(x...));
|
||||
this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...)));
|
||||
}
|
||||
void on_write_multiple_coils(uint16_t start_address, modbus::PackedBits bits,
|
||||
modbus::ResponseStatus status) override {
|
||||
@@ -395,8 +359,7 @@ class WriteMultipleCoilsAction : public TypedClientActionBase<Ts...>, public Wri
|
||||
|
||||
/// modbus_client.read_write_multiple_registers (FC 0x17): writes one register block and reads another back in
|
||||
/// one transaction (write first, per Modbus 6.17). on_response delivers the read-back words as `values`.
|
||||
template<typename... Ts>
|
||||
class ReadWriteMultipleRegistersAction : public TypedClientActionBase<Ts...>, public ReadCommandOptions<Ts...> {
|
||||
template<typename... Ts> class ReadWriteMultipleRegistersAction : public TypedClientActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, read_address)
|
||||
TEMPLATABLE_VALUE(uint16_t, read_count)
|
||||
@@ -422,15 +385,13 @@ class ReadWriteMultipleRegistersAction : public TypedClientActionBase<Ts...>, pu
|
||||
// An out-of-range read/write count builds an empty PDU (the builder logs why), resolving via on_not_sent.
|
||||
if (this->len_ >= 0) {
|
||||
this->send_or_resolve_(modbus::helpers::create_read_write_multiple_registers_pdu(
|
||||
read_start, read_count, write_start,
|
||||
std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))),
|
||||
this->command_options_(x...));
|
||||
read_start, read_count, write_start,
|
||||
std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))));
|
||||
return;
|
||||
}
|
||||
const std::vector<uint16_t> values = this->values_.func(x...);
|
||||
this->send_or_resolve_(modbus::helpers::create_read_write_multiple_registers_pdu(
|
||||
read_start, read_count, write_start, std::span<const uint16_t>(values)),
|
||||
this->command_options_(x...));
|
||||
read_start, read_count, write_start, std::span<const uint16_t>(values)));
|
||||
}
|
||||
// The 0x17 response carries only the read block, so the hub dispatch delivers it as a holding-register read.
|
||||
void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
|
||||
|
||||
@@ -103,20 +103,12 @@ def _warn_removed_options(config: ConfigType) -> ConfigType:
|
||||
|
||||
|
||||
def _reject_broadcast_address(config: ConfigType) -> ConfigType:
|
||||
"""Address 0 is rejected unless allow_broadcast_read, which in turn requires address 0."""
|
||||
if config[modbus.CONF_ALLOW_BROADCAST_READ]:
|
||||
if config.get(CONF_ADDRESS) != modbus.BROADCAST_ADDRESS:
|
||||
raise cv.Invalid(
|
||||
f"'{modbus.CONF_ALLOW_BROADCAST_READ}' only applies to the broadcast address; "
|
||||
f"set 'address: 0' or remove the option.",
|
||||
[modbus.CONF_ALLOW_BROADCAST_READ],
|
||||
)
|
||||
return config
|
||||
"""A modbus_controller polls one device, so its address cannot be the broadcast address (0):
|
||||
a broadcast is never answered (Modbus 4.1), so no register could ever read back."""
|
||||
modbus.reject_broadcast_address(
|
||||
config.get(CONF_ADDRESS),
|
||||
"a modbus_controller device address",
|
||||
"Assign the unit address of the device you want to poll, or set allow_broadcast_read if "
|
||||
"it answers address 0.",
|
||||
"Assign the unit address of the device you want to poll.",
|
||||
[CONF_ADDRESS],
|
||||
)
|
||||
return config
|
||||
@@ -354,52 +346,12 @@ def _reject_continuous_write_custom_pdu(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _reject_broadcastable_custom_pdu(config: ConfigType) -> None:
|
||||
"""A broadcastable custom_pdu under an address-0 controller is a real broadcast, never answered."""
|
||||
pdu = config.get(CONF_CUSTOM_PDU)
|
||||
if pdu is None or not modbus.is_function_code_broadcastable(pdu[0]):
|
||||
return
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
if (
|
||||
controller.get(CONF_ADDRESS) == modbus.BROADCAST_ADDRESS
|
||||
and controller.get(modbus.CONF_ALLOW_BROADCAST_READ) is True
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"a '{CONF_CUSTOM_PDU}' with function code 0x{pdu[0] & 0x7F:02X} is a real broadcast at "
|
||||
f"address 0 and is never answered, so it can't be polled through the "
|
||||
f"'{controller[CONF_ID]}' modbus_controller; use a read function code.",
|
||||
[CONF_CUSTOM_PDU],
|
||||
)
|
||||
|
||||
|
||||
def validate_custom_pdu_item(config: ConfigType) -> None:
|
||||
"""Final-validate for the platforms that accept custom_pdu."""
|
||||
"""Final-validate for the read platforms that accept custom_pdu (sensor, binary_sensor,
|
||||
text_sensor): migrate the deprecated custom_command, then reject a write-coded custom_pdu under a
|
||||
continuously-polling controller."""
|
||||
migrate_custom_command(config)
|
||||
_reject_continuous_write_custom_pdu(config)
|
||||
_reject_broadcastable_custom_pdu(config)
|
||||
|
||||
|
||||
def _reject_write_option_off_broadcast(config: ConfigType) -> None:
|
||||
if not any(config.get(key) is True for key in modbus.broadcast_only_option_keys()):
|
||||
return
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
if controller.get(CONF_ADDRESS) != modbus.BROADCAST_ADDRESS:
|
||||
raise cv.Invalid(
|
||||
f"'{modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE}' only applies when the "
|
||||
f"'{controller[CONF_ID]}' modbus_controller is at address 0; remove the option.",
|
||||
[modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE],
|
||||
)
|
||||
|
||||
|
||||
def validate_writer_item(config: ConfigType) -> None:
|
||||
"""Final-validate for the writer platforms (number, output, select, switch)."""
|
||||
if CONF_CUSTOM_PDU in config or CONF_CUSTOM_COMMAND in config:
|
||||
validate_custom_pdu_item(config)
|
||||
_reject_write_option_off_broadcast(config)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
@@ -496,7 +448,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
|
||||
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
||||
modbus.add_command_options(var, "set_read_options", config, direction="read")
|
||||
cg.add(
|
||||
var.set_read_options(
|
||||
modbus.command_options_expression(config, direction="read")
|
||||
)
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ void WriterDevice::warn_write_buffer_deprecated(const LogString *platform, uint1
|
||||
bool WriterDevice::send_raw_frame_deprecated(std::span<const uint8_t> frame) {
|
||||
if (frame.empty())
|
||||
return false;
|
||||
return this->parent_->queue_pdu(frame[0], frame.subspan(1), this, this->write_options_);
|
||||
return this->parent_->queue_pdu(frame[0], frame.subspan(1), this);
|
||||
}
|
||||
|
||||
void ControllerDevice::set_controller(ModbusController *controller) {
|
||||
@@ -234,13 +234,10 @@ void ModbusCommandItem::on_sent(std::span<const uint8_t> request_pdu) {
|
||||
// (frame[0]), which may differ from this controller's. (unqueue_command() is a no-op for a poll.)
|
||||
// A custom polling command sends its PDU to this controller's own address, so only a factory custom
|
||||
// command (a raw frame staged in payload) can carry a different address byte.
|
||||
// An address-0 read with allow_broadcast_read is answered, so it keeps its terminal callback.
|
||||
uint8_t wire_address = this->address_;
|
||||
if (this->function_code_ == FunctionCode::CUSTOM && !this->payload.empty())
|
||||
wire_address = this->payload.data()[0];
|
||||
const bool answered = this->controller_->read_options().allow_broadcast_read &&
|
||||
!modbus::helpers::is_function_code_broadcastable(request_pdu[0]);
|
||||
if (wire_address == modbus::BROADCAST_ADDRESS && !answered)
|
||||
if (wire_address == modbus::BROADCAST_ADDRESS)
|
||||
this->controller_->unqueue_command(this);
|
||||
}
|
||||
|
||||
@@ -288,8 +285,8 @@ void ModbusController::queue_command(ModbusCommandItem command) {
|
||||
this->one_shot_command_items_.push_back(make_unique<ModbusCommandItem>(std::move(command)));
|
||||
// A refused frame gets no terminal callback (see the hub contract), so reclaim the item here.
|
||||
auto &item = this->one_shot_command_items_.back();
|
||||
// One-shots never poll, so only the broadcast flag is passed (the hub strips it from writes).
|
||||
if (!item->send({.allow_broadcast_read = this->read_options_.allow_broadcast_read})) {
|
||||
// We intentionally do not pass read_options_ here, because one-shot commands are usually writes, and are non-polling.
|
||||
if (!item->send()) {
|
||||
// The caller (e.g. a write entity) has usually already published optimistically - surface the loss.
|
||||
ESP_LOGW(TAG, "Command refused by hub: type=0x%X address=0x%X", static_cast<uint8_t>(item->register_type()),
|
||||
item->register_address());
|
||||
@@ -343,7 +340,7 @@ void ModbusController::update() {
|
||||
if (this->can_send()) {
|
||||
for (auto &poll : this->polling_devices_) {
|
||||
ESP_LOGVV(TAG, "Updating range 0x%X", poll.register_address());
|
||||
// read_options_ carries the controller's read-side flags (the offline probe above sends them too).
|
||||
// read_options_ carries the controller's continuous flag (the offline probe above sends it too).
|
||||
// A refusal is already logged by the hub; note the affected range for controller-level diagnostics.
|
||||
if (!poll.queue(this->read_options_)) {
|
||||
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", poll.register_address());
|
||||
|
||||
@@ -280,11 +280,10 @@ class ControllerDevice : protected modbus::ModbusClientDevice {
|
||||
|
||||
void notify_online_(std::span<const uint8_t> request_pdu);
|
||||
|
||||
/// Write-path state for WriterEntity's forwarders, packed into the base's tail padding. The warn flag
|
||||
/// leaves in 2027.3.0.
|
||||
bool dispatched_ : 1 {false};
|
||||
bool write_buffer_deprecated_warned_ : 1 {false};
|
||||
modbus::CommandOptions write_options_{};
|
||||
/// Write-path state owned by WriterEntity's forwarders, stored here so both bools land in the base's
|
||||
/// tail padding instead of adding a word to every writer entity. The warn flag leaves in 2027.3.0.
|
||||
bool dispatched_{false};
|
||||
bool write_buffer_deprecated_warned_{false};
|
||||
ModbusController *controller_{nullptr};
|
||||
};
|
||||
|
||||
@@ -306,8 +305,6 @@ class WriterDevice final : public ControllerDevice {
|
||||
bool dispatched() const { return this->dispatched_; }
|
||||
void set_dispatched() { this->dispatched_ = true; }
|
||||
void clear_dispatched() { this->dispatched_ = false; }
|
||||
modbus::CommandOptions write_options() const { return this->write_options_; }
|
||||
void set_write_options(modbus::CommandOptions options) { this->write_options_ = options; }
|
||||
/// Warn once per entity that filling the write_lambda buffer parameter is deprecated (the entity is now the
|
||||
/// command - call a write helper / queue_pdu() on `item` instead). The buffer parameter is removed in 2027.3.0.
|
||||
void warn_write_buffer_deprecated(const LogString *platform, uint16_t address);
|
||||
@@ -329,29 +326,27 @@ class WriterEntity {
|
||||
/// Whether the lambda called a request helper since the last clear_dispatched_(). Deliberately records
|
||||
/// the call, not the hub's accept/refuse: a refused lambda write must not fall through to the default write.
|
||||
bool dispatched() const { return this->device_.dispatched(); }
|
||||
void set_write_options(modbus::CommandOptions options) { this->device_.set_write_options(options); }
|
||||
bool write_single_register(uint16_t address, uint16_t value) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_single_register(address, value, this->device_.write_options());
|
||||
return this->device_.write_single_register(address, value);
|
||||
}
|
||||
bool write_single_coil(uint16_t address, bool value) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_single_coil(address, value, this->device_.write_options());
|
||||
return this->device_.write_single_coil(address, value);
|
||||
}
|
||||
bool write_multiple_registers(uint16_t address, std::span<const uint16_t> values) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_multiple_registers(address, values, this->device_.write_options());
|
||||
return this->device_.write_multiple_registers(address, values);
|
||||
}
|
||||
bool write_multiple_coils(uint16_t address, std::span<const bool> values) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_multiple_coils(address, values, this->device_.write_options());
|
||||
return this->device_.write_multiple_coils(address, values);
|
||||
}
|
||||
bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_multiple_coils(address, bits, this->device_.write_options());
|
||||
return this->device_.write_multiple_coils(address, bits);
|
||||
}
|
||||
bool queue_pdu(std::span<const uint8_t> pdu) { return this->queue_pdu(pdu, this->device_.write_options()); }
|
||||
bool queue_pdu(std::span<const uint8_t> pdu, modbus::CommandOptions options) {
|
||||
bool queue_pdu(std::span<const uint8_t> pdu, modbus::CommandOptions options = {}) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.queue_pdu(pdu, options);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import modbus, number
|
||||
from esphome.components import number
|
||||
from esphome.components.modbus.helpers import (
|
||||
MODBUS_WRITE_REGISTER_TYPE,
|
||||
SENSOR_VALUE_TYPE,
|
||||
@@ -23,8 +23,8 @@ from .. import (
|
||||
add_modbus_base_properties,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_range_reuse_migration,
|
||||
validate_writer_item,
|
||||
)
|
||||
from ..const import (
|
||||
CONF_BITMASK,
|
||||
@@ -84,7 +84,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_STEP, default=1): cv.positive_float,
|
||||
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
|
||||
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
|
||||
**modbus.command_options_schema(direction="write"),
|
||||
}
|
||||
),
|
||||
validate_min_max,
|
||||
@@ -92,7 +91,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_range_reuse_migration,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_writer_item
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
@@ -123,7 +122,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(parent.add_sensor_item(var))
|
||||
await add_modbus_base_properties(var, config, ModbusNumber)
|
||||
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
||||
modbus.add_command_options(var, "set_write_options", config, direction="write")
|
||||
if CONF_WRITE_LAMBDA in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_WRITE_LAMBDA],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import modbus, output
|
||||
from esphome.components import output
|
||||
from esphome.components.modbus.helpers import (
|
||||
SENSOR_VALUE_TYPE,
|
||||
PduBuffer,
|
||||
@@ -18,7 +18,6 @@ from .. import (
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
reject_odd_holding_write_offset,
|
||||
validate_writer_item,
|
||||
)
|
||||
from ..const import (
|
||||
CONF_CUSTOM_COMMAND,
|
||||
@@ -80,7 +79,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
|
||||
**modbus.command_options_schema(direction="write"),
|
||||
}
|
||||
),
|
||||
"holding": cv.All(
|
||||
@@ -100,7 +98,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
|
||||
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
|
||||
**modbus.command_options_schema(direction="write"),
|
||||
}
|
||||
),
|
||||
reject_odd_holding_write_offset,
|
||||
@@ -114,9 +111,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_writer_item
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
byte_offset = modbus_calc_properties(config)
|
||||
# Binary Output
|
||||
@@ -159,7 +153,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
await output.register_output(var, config)
|
||||
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
|
||||
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
||||
modbus.add_command_options(var, "set_write_options", config, direction="write")
|
||||
cg.add(var.set_parent(parent))
|
||||
if write_template:
|
||||
cg.add(var.set_write_template(write_template))
|
||||
@@ -2,7 +2,7 @@ from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import modbus, select
|
||||
from esphome.components import select
|
||||
from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE, RegisterValues
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
|
||||
@@ -15,7 +15,6 @@ from .. import (
|
||||
modbus_controller_ns,
|
||||
validate_range_reuse_migration,
|
||||
validate_skip_updates_deprecated,
|
||||
validate_writer_item,
|
||||
)
|
||||
from ..const import (
|
||||
CONF_FORCE_NEW_RANGE,
|
||||
@@ -78,7 +77,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_REGISTER_COUNT): cv.positive_int,
|
||||
cv.Required(CONF_OPTIONSMAP): ensure_option_map(),
|
||||
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
|
||||
**modbus.command_options_schema(direction="write"),
|
||||
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
@@ -88,9 +86,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_writer_item
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
options_map = config[CONF_OPTIONSMAP]
|
||||
|
||||
@@ -109,7 +104,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(parent.add_sensor_item(var))
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
||||
modbus.add_command_options(var, "set_write_options", config, direction="write")
|
||||
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import modbus, switch
|
||||
from esphome.components import switch
|
||||
from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE, PduBuffer
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID
|
||||
@@ -13,9 +13,9 @@ from .. import (
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
reject_odd_holding_write_offset,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
validate_range_reuse_migration,
|
||||
validate_writer_item,
|
||||
)
|
||||
from ..const import (
|
||||
CONF_BITMASK,
|
||||
@@ -51,7 +51,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
|
||||
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
|
||||
**modbus.command_options_schema(direction="write"),
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
}
|
||||
),
|
||||
@@ -60,7 +59,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_range_reuse_migration,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_writer_item
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
@@ -79,7 +78,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
|
||||
cg.add(var.set_parent(paren))
|
||||
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
||||
modbus.add_command_options(var, "set_write_options", config, direction="write")
|
||||
assumed_state = config[CONF_ASSUMED_STATE]
|
||||
cg.add(var.set_assumed_state(assumed_state))
|
||||
if not assumed_state:
|
||||
|
||||
@@ -14,12 +14,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, TimePeriod
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
FILTER_SOURCE_FILES,
|
||||
Nextion,
|
||||
nextion_ns,
|
||||
nextion_ref,
|
||||
)
|
||||
from . import FILTER_SOURCE_FILES, Nextion, nextion_ns, nextion_ref # noqa: F401 pylint: disable=unused-import
|
||||
from .base_component import (
|
||||
CONF_AUTO_WAKE_ON_TOUCH,
|
||||
CONF_COMMAND_SPACING,
|
||||
|
||||
@@ -13,6 +13,8 @@ from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
only_on_variant,
|
||||
require_mbedtls_tls_extras,
|
||||
require_mbedtls_tls_server,
|
||||
require_vfs_select,
|
||||
)
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
@@ -109,6 +111,14 @@ def set_sdkconfig_options(config: ConfigType) -> None:
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
|
||||
|
||||
# OpenThread's DTLS commissioner is a TLS server, and its crypto platform
|
||||
# uses AES-CCM and deterministic ECDSA directly. Keep the esp32 component
|
||||
# from trimming them out of mbedTLS.
|
||||
require_mbedtls_tls_server()
|
||||
require_mbedtls_tls_extras(
|
||||
("CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC")
|
||||
)
|
||||
|
||||
if not config.get(CONF_TLV):
|
||||
if pan_id := config.get(CONF_PAN_ID):
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
@@ -40,11 +45,14 @@ from esphome.const import (
|
||||
CONF_ZERO,
|
||||
)
|
||||
from esphome.core import ID, coroutine
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import Registry, SimpleRegistry
|
||||
|
||||
AUTO_LOAD = ["binary_sensor"]
|
||||
|
||||
|
||||
CONF_RECEIVER_ID = "receiver_id"
|
||||
CONF_TRANSMITTER_ID = "transmitter_id"
|
||||
CONF_FIRST = "first"
|
||||
@@ -90,9 +98,42 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def register_listener(var, config):
|
||||
# Listener and dumper lists are StaticVectors sized from these counts, so every registration
|
||||
# must go through add_listener / add_dumper. Every receiver's list gets the same capacity, so
|
||||
# the slots are keyed by receiver and the define is the largest count any one receiver needs.
|
||||
LISTENER_COUNT_DEFINE = "REMOTE_BASE_LISTENER_COUNT"
|
||||
DUMPER_COUNT_DEFINE = "REMOTE_BASE_DUMPER_COUNT"
|
||||
|
||||
|
||||
_request_listener_slot = cg.slot_counter(LISTENER_COUNT_DEFINE)
|
||||
_request_dumper_slot = cg.slot_counter(DUMPER_COUNT_DEFINE)
|
||||
|
||||
|
||||
def add_listener(receiver: MockObj, listener: MockObj) -> None:
|
||||
_request_listener_slot(str(receiver))
|
||||
cg.add(receiver.register_listener(listener))
|
||||
|
||||
|
||||
def add_dumper(receiver: MockObj, dumper: MockObj) -> None:
|
||||
_request_dumper_slot(str(receiver))
|
||||
cg.add(receiver.register_dumper(dumper))
|
||||
|
||||
|
||||
async def register_listener(var: MockObj, config: ConfigType) -> None:
|
||||
receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
|
||||
cg.add(receiver.register_listener(var))
|
||||
add_listener(receiver, var)
|
||||
|
||||
|
||||
async def attach_receiver(
|
||||
var: MockObj, config: ConfigType, key: str = CONF_RECEIVER_ID
|
||||
) -> None:
|
||||
"""Link the configured receiver to an entity and register the entity as its listener.
|
||||
|
||||
The C++ set_receiver() no longer registers the listener; the slot for it is counted here.
|
||||
"""
|
||||
receiver = await cg.get_variable(config[key])
|
||||
cg.add(var.set_receiver(receiver))
|
||||
add_listener(receiver, var)
|
||||
|
||||
|
||||
async def register_transmittable(var, config):
|
||||
@@ -100,8 +141,53 @@ async def register_transmittable(var, config):
|
||||
cg.add(var.set_transmitter(transmitter_))
|
||||
|
||||
|
||||
def register_binary_sensor(name, type, schema):
|
||||
return BINARY_SENSOR_REGISTRY.register(name, type, schema)
|
||||
# Registry names that share a protocol source file
|
||||
def _protocol_stem(name: str) -> str:
|
||||
if name.startswith("rc_switch"):
|
||||
return "rc_switch"
|
||||
if name == "canalsatld":
|
||||
return "canalsat"
|
||||
return name
|
||||
|
||||
|
||||
def protocol_define(name: str) -> str:
|
||||
return f"USE_REMOTE_PROTOCOL_{_protocol_stem(name).upper()}"
|
||||
|
||||
|
||||
_PROTOCOL_STEMS = sorted(
|
||||
path.name.removesuffix("_protocol.cpp")
|
||||
for path in Path(__file__).parent.glob("*_protocol.cpp")
|
||||
)
|
||||
|
||||
|
||||
def request_protocol(name: str) -> None:
|
||||
"""Keep a protocol's source file in the build; components using it from C++ must call this."""
|
||||
if _protocol_stem(name) not in _PROTOCOL_STEMS:
|
||||
raise ValueError(
|
||||
f"Unknown remote protocol {name!r}; expected one of {', '.join(_PROTOCOL_STEMS)}"
|
||||
)
|
||||
cg.add_define(protocol_define(name))
|
||||
|
||||
|
||||
# Only the protocol sources a configuration uses are compiled
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS}
|
||||
)
|
||||
|
||||
|
||||
def register_binary_sensor(
|
||||
name: str, type: MockObj, schema: cv.Schema | dict
|
||||
) -> Callable[[Callable[[MockObj, ConfigType], Any]], Callable]:
|
||||
registerer = BINARY_SENSOR_REGISTRY.register(name, type, schema)
|
||||
|
||||
def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable:
|
||||
async def new_func(var: MockObj, config: ConfigType) -> None:
|
||||
request_protocol(name)
|
||||
await coroutine(func)(var, config)
|
||||
|
||||
return registerer(new_func)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_trigger(name, type, data_type):
|
||||
@@ -114,6 +200,7 @@ def register_trigger(name, type, data_type):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config):
|
||||
request_protocol(name)
|
||||
var = cg.new_Pvariable(config[CONF_TRIGGER_ID])
|
||||
await coroutine(func)(var, config)
|
||||
await automation.build_automation(var, [(data_type, "x")], config)
|
||||
@@ -131,6 +218,7 @@ def register_dumper(name, type, schema=None):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, dumper_id):
|
||||
request_protocol(name)
|
||||
var = cg.new_Pvariable(dumper_id)
|
||||
await coroutine(func)(var, config)
|
||||
return var
|
||||
@@ -171,6 +259,7 @@ def register_action(name, type_, schema):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, action_id, template_arg, args):
|
||||
request_protocol(name)
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await register_transmittable(var, config)
|
||||
if CONF_REPEAT in config:
|
||||
@@ -213,7 +302,13 @@ DUMPER_REGISTRY = Registry()
|
||||
def validate_dumpers(value):
|
||||
if isinstance(value, str) and value.lower() == "all":
|
||||
return validate_dumpers(list(DUMPER_REGISTRY.keys()))
|
||||
return cv.validate_registry("dumper", DUMPER_REGISTRY)(value)
|
||||
entries = cv.validate_registry("dumper", DUMPER_REGISTRY)(value)
|
||||
# a dumper listed twice would register twice; the receiver holds one secondary dumper
|
||||
return list(
|
||||
{
|
||||
next(k for k in entry if k in DUMPER_REGISTRY): entry for entry in entries
|
||||
}.values()
|
||||
)
|
||||
|
||||
|
||||
def validate_triggers(base_schema):
|
||||
@@ -1439,7 +1534,7 @@ def validate_rc_switch_raw_code(value):
|
||||
|
||||
def build_rc_switch_protocol(config):
|
||||
if isinstance(config, int):
|
||||
return rc_switch_protocols[config]
|
||||
return rc_switch_protocol(config)
|
||||
pl = config[CONF_PULSE_LENGTH]
|
||||
return RCSwitchBase(
|
||||
config[CONF_SYNC][0] * pl,
|
||||
@@ -1526,7 +1621,7 @@ RC_SWITCH_TRANSMITTER = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS
|
||||
rc_switch_protocol = ns.rc_switch_protocol
|
||||
RCSwitchData = ns.struct("RCSwitchData")
|
||||
RCSwitchBase = ns.class_("RCSwitchBase")
|
||||
RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger)
|
||||
|
||||
@@ -191,9 +191,9 @@ class ABBWelcomeData {
|
||||
|
||||
class ABBWelcomeProtocol : public RemoteProtocol<ABBWelcomeData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const ABBWelcomeData &src) override;
|
||||
optional<ABBWelcomeData> decode(RemoteReceiveData src) override;
|
||||
void dump(const ABBWelcomeData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const ABBWelcomeData &src);
|
||||
optional<ABBWelcomeData> decode(RemoteReceiveData src);
|
||||
void dump(const ABBWelcomeData &data);
|
||||
|
||||
protected:
|
||||
void encode_byte_(RemoteTransmitData *dst, uint8_t data) const;
|
||||
|
||||
@@ -15,9 +15,9 @@ struct AEHAData {
|
||||
|
||||
class AEHAProtocol : public RemoteProtocol<AEHAData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const AEHAData &data) override;
|
||||
optional<AEHAData> decode(RemoteReceiveData src) override;
|
||||
void dump(const AEHAData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const AEHAData &data);
|
||||
optional<AEHAData> decode(RemoteReceiveData src);
|
||||
void dump(const AEHAData &data);
|
||||
|
||||
private:
|
||||
std::string format_data_(const std::vector<uint8_t> &data);
|
||||
|
||||
@@ -16,9 +16,9 @@ struct Beo4Data {
|
||||
|
||||
class Beo4Protocol : public RemoteProtocol<Beo4Data> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const Beo4Data &data) override;
|
||||
optional<Beo4Data> decode(RemoteReceiveData src) override;
|
||||
void dump(const Beo4Data &data) override;
|
||||
void encode(RemoteTransmitData *dst, const Beo4Data &data);
|
||||
optional<Beo4Data> decode(RemoteReceiveData src);
|
||||
void dump(const Beo4Data &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Beo4)
|
||||
|
||||
@@ -13,9 +13,9 @@ struct BrennenstuhlData {
|
||||
|
||||
class BrennenstuhlProtocol : public RemoteProtocol<BrennenstuhlData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override;
|
||||
optional<BrennenstuhlData> decode(RemoteReceiveData src) override;
|
||||
void dump(const BrennenstuhlData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const BrennenstuhlData &data);
|
||||
optional<BrennenstuhlData> decode(RemoteReceiveData src);
|
||||
void dump(const BrennenstuhlData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Brennenstuhl)
|
||||
|
||||
@@ -21,9 +21,9 @@ struct ByronSXData {
|
||||
|
||||
class ByronSXProtocol : public RemoteProtocol<ByronSXData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const ByronSXData &data) override;
|
||||
optional<ByronSXData> decode(RemoteReceiveData src) override;
|
||||
void dump(const ByronSXData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const ByronSXData &data);
|
||||
optional<ByronSXData> decode(RemoteReceiveData src);
|
||||
void dump(const ByronSXData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(ByronSX)
|
||||
|
||||
@@ -19,9 +19,9 @@ struct CanalSatLDData : public CanalSatData {};
|
||||
|
||||
class CanalSatBaseProtocol : public RemoteProtocol<CanalSatData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const CanalSatData &data) override;
|
||||
optional<CanalSatData> decode(RemoteReceiveData src) override;
|
||||
void dump(const CanalSatData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const CanalSatData &data);
|
||||
optional<CanalSatData> decode(RemoteReceiveData src);
|
||||
void dump(const CanalSatData &data);
|
||||
|
||||
protected:
|
||||
uint16_t frequency_;
|
||||
|
||||
@@ -21,9 +21,9 @@ struct CoolixData {
|
||||
|
||||
class CoolixProtocol : public RemoteProtocol<CoolixData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const CoolixData &data) override;
|
||||
optional<CoolixData> decode(RemoteReceiveData data) override;
|
||||
void dump(const CoolixData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const CoolixData &data);
|
||||
optional<CoolixData> decode(RemoteReceiveData data);
|
||||
void dump(const CoolixData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Coolix)
|
||||
|
||||
@@ -13,9 +13,9 @@ struct DishData {
|
||||
|
||||
class DishProtocol : public RemoteProtocol<DishData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const DishData &data) override;
|
||||
optional<DishData> decode(RemoteReceiveData src) override;
|
||||
void dump(const DishData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const DishData &data);
|
||||
optional<DishData> decode(RemoteReceiveData src);
|
||||
void dump(const DishData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Dish)
|
||||
|
||||
@@ -20,9 +20,9 @@ struct DooyaData {
|
||||
|
||||
class DooyaProtocol : public RemoteProtocol<DooyaData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const DooyaData &data) override;
|
||||
optional<DooyaData> decode(RemoteReceiveData src) override;
|
||||
void dump(const DooyaData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const DooyaData &data);
|
||||
optional<DooyaData> decode(RemoteReceiveData src);
|
||||
void dump(const DooyaData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Dooya)
|
||||
|
||||
@@ -19,9 +19,9 @@ struct DraytonData {
|
||||
|
||||
class DraytonProtocol : public RemoteProtocol<DraytonData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const DraytonData &data) override;
|
||||
optional<DraytonData> decode(RemoteReceiveData src) override;
|
||||
void dump(const DraytonData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const DraytonData &data);
|
||||
optional<DraytonData> decode(RemoteReceiveData src);
|
||||
void dump(const DraytonData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Drayton)
|
||||
|
||||
@@ -21,9 +21,9 @@ struct DysonData {
|
||||
|
||||
class DysonProtocol : public RemoteProtocol<DysonData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const DysonData &data) override;
|
||||
optional<DysonData> decode(RemoteReceiveData src) override;
|
||||
void dump(const DysonData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const DysonData &data);
|
||||
optional<DysonData> decode(RemoteReceiveData src);
|
||||
void dump(const DysonData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Dyson)
|
||||
|
||||
@@ -31,9 +31,9 @@ class GoboxProtocol : public RemoteProtocol<GoboxData> {
|
||||
void dump_timings_(const RawTimings &timings) const;
|
||||
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const GoboxData &data) override;
|
||||
optional<GoboxData> decode(RemoteReceiveData src) override;
|
||||
void dump(const GoboxData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const GoboxData &data);
|
||||
optional<GoboxData> decode(RemoteReceiveData src);
|
||||
void dump(const GoboxData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Gobox)
|
||||
|
||||
@@ -13,9 +13,9 @@ struct HaierData {
|
||||
|
||||
class HaierProtocol : public RemoteProtocol<HaierData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const HaierData &data) override;
|
||||
optional<HaierData> decode(RemoteReceiveData src) override;
|
||||
void dump(const HaierData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const HaierData &data);
|
||||
optional<HaierData> decode(RemoteReceiveData src);
|
||||
void dump(const HaierData &data);
|
||||
|
||||
protected:
|
||||
void encode_byte_(RemoteTransmitData *dst, uint8_t item);
|
||||
|
||||
@@ -14,9 +14,9 @@ struct JVCData {
|
||||
|
||||
class JVCProtocol : public RemoteProtocol<JVCData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const JVCData &data) override;
|
||||
optional<JVCData> decode(RemoteReceiveData src) override;
|
||||
void dump(const JVCData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const JVCData &data);
|
||||
optional<JVCData> decode(RemoteReceiveData src);
|
||||
void dump(const JVCData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(JVC)
|
||||
|
||||
@@ -24,9 +24,9 @@ struct KeeloqData {
|
||||
|
||||
class KeeloqProtocol : public RemoteProtocol<KeeloqData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const KeeloqData &data) override;
|
||||
optional<KeeloqData> decode(RemoteReceiveData src) override;
|
||||
void dump(const KeeloqData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const KeeloqData &data);
|
||||
optional<KeeloqData> decode(RemoteReceiveData src);
|
||||
void dump(const KeeloqData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Keeloq)
|
||||
|
||||
@@ -16,9 +16,9 @@ struct LGData {
|
||||
|
||||
class LGProtocol : public RemoteProtocol<LGData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const LGData &data) override;
|
||||
optional<LGData> decode(RemoteReceiveData src) override;
|
||||
void dump(const LGData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const LGData &data);
|
||||
optional<LGData> decode(RemoteReceiveData src);
|
||||
void dump(const LGData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(LG)
|
||||
|
||||
@@ -27,9 +27,9 @@ struct MagiQuestData {
|
||||
|
||||
class MagiQuestProtocol : public RemoteProtocol<MagiQuestData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const MagiQuestData &data) override;
|
||||
optional<MagiQuestData> decode(RemoteReceiveData src) override;
|
||||
void dump(const MagiQuestData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const MagiQuestData &data);
|
||||
optional<MagiQuestData> decode(RemoteReceiveData src);
|
||||
void dump(const MagiQuestData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(MagiQuest)
|
||||
|
||||
@@ -67,9 +67,9 @@ class MideaData {
|
||||
|
||||
class MideaProtocol : public RemoteProtocol<MideaData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const MideaData &src) override;
|
||||
optional<MideaData> decode(RemoteReceiveData src) override;
|
||||
void dump(const MideaData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const MideaData &src);
|
||||
optional<MideaData> decode(RemoteReceiveData src);
|
||||
void dump(const MideaData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Midea)
|
||||
|
||||
@@ -13,9 +13,9 @@ struct MirageData {
|
||||
|
||||
class MirageProtocol : public RemoteProtocol<MirageData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const MirageData &data) override;
|
||||
optional<MirageData> decode(RemoteReceiveData src) override;
|
||||
void dump(const MirageData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const MirageData &data);
|
||||
optional<MirageData> decode(RemoteReceiveData src);
|
||||
void dump(const MirageData &data);
|
||||
|
||||
protected:
|
||||
void encode_byte_(RemoteTransmitData *dst, uint8_t item);
|
||||
|
||||
@@ -14,9 +14,9 @@ struct NECData {
|
||||
|
||||
class NECProtocol : public RemoteProtocol<NECData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const NECData &data) override;
|
||||
optional<NECData> decode(RemoteReceiveData src) override;
|
||||
void dump(const NECData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const NECData &data);
|
||||
optional<NECData> decode(RemoteReceiveData src);
|
||||
void dump(const NECData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(NEC)
|
||||
|
||||
@@ -24,9 +24,9 @@ class NexaProtocol : public RemoteProtocol<NexaData> {
|
||||
void zero(RemoteTransmitData *dst) const;
|
||||
void sync(RemoteTransmitData *dst) const;
|
||||
|
||||
void encode(RemoteTransmitData *dst, const NexaData &data) override;
|
||||
optional<NexaData> decode(RemoteReceiveData src) override;
|
||||
void dump(const NexaData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const NexaData &data);
|
||||
optional<NexaData> decode(RemoteReceiveData src);
|
||||
void dump(const NexaData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Nexa)
|
||||
|
||||
@@ -16,9 +16,9 @@ struct PanasonicData {
|
||||
|
||||
class PanasonicProtocol : public RemoteProtocol<PanasonicData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const PanasonicData &data) override;
|
||||
optional<PanasonicData> decode(RemoteReceiveData src) override;
|
||||
void dump(const PanasonicData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const PanasonicData &data);
|
||||
optional<PanasonicData> decode(RemoteReceiveData src);
|
||||
void dump(const PanasonicData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Panasonic)
|
||||
|
||||
@@ -13,9 +13,9 @@ struct PioneerData {
|
||||
|
||||
class PioneerProtocol : public RemoteProtocol<PioneerData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const PioneerData &data) override;
|
||||
optional<PioneerData> decode(RemoteReceiveData src) override;
|
||||
void dump(const PioneerData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const PioneerData &data);
|
||||
optional<PioneerData> decode(RemoteReceiveData src);
|
||||
void dump(const PioneerData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Pioneer)
|
||||
|
||||
@@ -30,9 +30,9 @@ class ProntoProtocol : public RemoteProtocol<ProntoData> {
|
||||
std::string compensate_and_dump_sequence_(const RawTimings &data, uint16_t timebase);
|
||||
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const ProntoData &data) override;
|
||||
optional<ProntoData> decode(RemoteReceiveData src) override;
|
||||
void dump(const ProntoData &data) override;
|
||||
void encode(RemoteTransmitData *dst, const ProntoData &data);
|
||||
optional<ProntoData> decode(RemoteReceiveData src);
|
||||
void dump(const ProntoData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Pronto)
|
||||
|
||||
Loaded 100 of 245 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user