Compare commits

..
Author SHA1 Message Date
J. Nick Koston 5ac2fa8811 [api] Print the dropped action's service with its length
The field can now start as a null StringRef, so the log passes the size and an empty
literal instead of a pointer that may be null.
2026-09-08 04:42:58 +02:00
J. Nick Koston e715696bd7 [core] Bound StringRef's JSON conversion by the view length 2026-09-08 04:15:24 +02:00
J. Nick Koston 82d399400e [core] Convert a null StringRef to an empty JSON string and pin null against null 2026-09-08 03:44:24 +02:00
J. Nick Koston a7bf937001 [core] Keep StringRef::str() inline
The std::string range constructor reads nothing for a zero length, so the guard only
pushed str() out of line; the memcmp guards stay.
2026-09-08 03:22:05 +02:00
J. Nick Koston 63b5331e47 [core] Let StringRef carry a null pointer at zero length
The generated api messages start their encode only string fields that way. starts_with,
compare and str() no longer hand a null pointer to memcmp or the std::string range
constructor when there is nothing to compare or copy, the class comment states the
contract, and gtest cases pin every member on a null, empty view.
2026-09-08 03:14:44 +02:00
J. Nick Koston 4c72948575 [api] Say why the pointer buffer base keeps its constructor 2026-09-08 00:24:14 +02:00
J. Nick Koston 77d850cbe2 [api] Drop the unused array_size on the pointer buffer base 2026-09-08 00:07:54 +02:00
J. Nick Koston a97e4ebc1c [api] Pass needs_decode at both bytes buffer sites and guard the second unconditional copy path 2026-09-07 23:46:15 +02:00
J. Nick Koston d09f642eb2 [api] Name the null default invariant once and thread needs_decode through the pointer buffer base
The dead size parameter goes, the string type reads the inherited flag, the forced short
string path asserts against it, and the generated declarations say why the pointer may be
null.
2026-09-07 23:23:19 +02:00
J. Nick Koston 144cd419ad [api] Test which string fields get the null default 2026-09-07 23:11:47 +02:00
J. Nick Koston 7910c372cd [api] Default response only string fields to a null StringRef
A StringRef field that is only ever encoded is skipped when empty before its pointer is
read, and the dump helper checks empty() first, so pointing it at "" buys nothing while
costing one store per field in every message constructor. Fields that are decoded or force
encoded keep the empty string default.
2026-09-07 23:04:33 +02:00
J. Nick Koston c2a4981e25 [api] Order the wrong wire type assertions on the raw socket
The barrier was a command on the aioesphomeapi connection, which nothing orders against the
raw frames under test; a well formed frame on the raw socket itself now closes each block and
the assertion checks the exact states seen since the marker.
2026-09-07 16:22:42 +02:00
J. Nick Koston 097d487261 [api] Cover truncated bodies in the decode integration test and pin the 64 bit field rejection
Three malformed frames (a tag with a dangling continuation bit, a length prefix past the
payload, a two byte fixed32) must stop the decode loop without taking the connection down;
a generator test records that a double field is rejected before it could reach the loop.
2026-09-07 15:58:28 +02:00
J. Nick Koston 89082c7b2d [api] Cover repeated and sub message fields in the decode case tests 2026-09-07 15:57:14 +02:00
J. Nick Koston fb3befe377 [api] Second cleanup pass over the decode generator
Repeated fields encode their elements through one encode_element() hook instead of two
isinstance ladders, the fixed32 precomputed tag path owns its own guard, the generated
switches drop the dead default case, StringRef takes the byte pointer directly, the
three hand written tag expressions in proto.h go through proto_tag(), and stale comments
about the previous decode design go. The compiled functions are unchanged.
2026-09-07 15:57:14 +02:00
J. Nick Koston 4a3a787756 [api] Return the varint parse result as a struct again
The out parameter form regressed the host, where the 16 byte result already travels in
registers, by about 20 percent on the direct varint parse benchmarks. The void
decode_field and low word bool changes stay.
2026-09-07 15:57:13 +02:00
J. Nick Koston 5aec93d7e8 [api] Trim the decode path: void decode_field, bool from the low word, slow varint out parameter
decode_field() no longer returns a bool that only fed a verbose log; unknown fields are
skipped silently like every other protobuf decoder does, and each message loses the
return value materialisation. Bools read the low 32 bits of the varint, which drops the
second compare on 64 bit varint builds. The multi byte varint path writes its value
through an out parameter instead of returning a 16 byte struct, which takes the spills
out of the decode loop and count_repeated_field.
2026-09-07 15:57:13 +02:00
J. Nick Koston a79cd1550c [api] Collapse the generator's per wire type decode hooks into one
A type now sets a single decode_expr; the wire type it already declares picks the case
label. Drops the unused force_str helper, two dead decode_length overrides, a duplicate
field builder in the generator tests and a needless list copy in the state waiter. The
generated files are unchanged.
2026-09-07 15:57:13 +02:00
J. Nick Koston 6d23e4c842 [api] Key the generated decode switch on the wire tag on every target
The host no longer gets its own switch shape through USE_HOST; every build compiles the
same switch on the field's wire tag.
2026-09-07 15:57:13 +02:00
J. Nick Koston cbdcfe640c [api] Read fixed32 fields with byte loads and test the varint wire type first
ESP-IDF passes -fno-builtin-memcpy, so the four byte memcpy in the decode loop was an
out of line call on every fixed32 field; host compilers fold the byte loads back into one
load. Checking the varint wire type first keeps the common path to one taken branch on
xtensa.
2026-09-07 15:57:13 +02:00
J. Nick Koston 2bc20d8721 [api] Derive decode cases from the type's wire type
decode_case() reads wire_type instead of taking it at every call, the
expression and the store statement are two small hooks that repeated
fields override, and message fields build one body. No generated case
declares a local any more, so the braced form and its test go; the
compiler rejects a jump over a local if one ever appears. The wire
type test now proves a dropped frame with an ordering marker instead
of assuming it, and shares StateWaiter.
2026-09-07 15:57:13 +02:00
J. Nick Koston 2dbb3e055b [api] Add an integration test for decode wire type handling
Hand built frames check that decode_field() takes a field with its
declared wire type, drops the same field sent length delimited or as
fixed32, ignores a varint key, and skips an unknown field with a two
byte tag before decoding the rest. Client commands cover two byte tags
and varints, a two byte length prefix and a negative fixed32 float.
2026-09-07 15:57:13 +02:00
J. Nick Koston 7549309ad3 [api] Trim the decode dispatch comments 2026-09-07 15:57:13 +02:00
J. Nick Koston e600180417 [api] Emit one decode case per field from a single generator property
With one decode_field() switch per message, the three per wire type
content properties only differed in the attribute they read; a single
decode_content built from decode_expr() replaces them, and repeated
fields reuse the element type's expression. Case bodies with several
statements get their block from the body itself instead of a caller
flag, the fixed byte array body copies straight from the payload
instead of through a heap std::string, and the decode comments no
longer restate the switch keying explained next to the macros.
2026-09-07 15:57:13 +02:00
J. Nick Koston 0641dae9d2 [api] Scope generated decode cases that declare locals
A case body with a declaration or several statements now gets its own
block, as the per wire type overrides had, so no jump to a later case
label crosses an initialization.
2026-09-07 15:57:13 +02:00
J. Nick Koston bcf812d62b [api] Keep the decode loop register resident and inline the varint fast path
CodSpeed showed the single virtual costing 7 to 18 percent on the
decode benchmarks. The x86-64 disassembly pointed at the call, not the
switch: passing the field number and wire type alongside the tag plus
a 16 byte union payload kept five values live across the call, so the
compiler spilled this, the end pointer and half of the payload to the
stack and reloaded them for every field.

decode_field() now takes only the tag, the payload pointer (already
the loop cursor) and one scalar that holds the varint or fixed32 value
or the payload length. The generated override wraps them in a
ProtoFieldValue that never exists in memory. On the host the switch
key is the field number derived with one shift and the guard compares
the whole tag against the constant the case declares, which is the
same two instructions the old per wire type dispatch cost.

The loop also handles single byte varints inline instead of going
through the parse result struct, which drops the materialized consumed
count and its add on every tag and small value.
2026-09-07 15:57:13 +02:00
J. Nick Koston b0ce7f58f3 [api] Collapse the three protobuf decode virtuals into one
Every decodable message overrode up to three virtuals, one per wire
type, so each carried a five slot vtable and up to three functions
with their own prologue and return tails. The shared decode loop now
parses the payload for the wire type into a ProtoFieldValue and calls
a single decode_field() virtual with the tag, the field number and the
wire type; the generated override is one switch.

The switch key is chosen per target through PROTO_DECODE_KEY. Embedded
builds compile switches to compare chains (ESP-IDF passes
-fno-jump-tables), so they key on the full wire tag, one compare per
field with no separate wire type check. The host compiler builds a
jump table for the dense field number switch, so there the key is the
field number and PROTO_DECODE_GUARD rejects a mismatched wire type.
Both forms drop a field that arrives with a wire type it does not
declare, exactly as the per wire type virtuals did.

Per decodable message the vtable shrinks from 20 to 12 bytes on
xtensa and the extra decode functions fold into one; the shared loop
shrinks as well. Host instruction counts per decoded field are
unchanged apart from the guard compare, which replaces the prologue of
the separate function it used to call.
2026-09-07 15:57:12 +02:00
J. Nick Koston 455e1d6374 [api] Assert the switch frame count instead of reading for it separately 2026-09-07 15:57:12 +02:00
J. Nick Koston 3af1d50bce [api] Trim the field free message test and a duplicated generator note 2026-09-07 15:57:12 +02:00
J. Nick Koston 842f354a05 [api] Add an integration test for field free messages
Ping, device info, list entities done and disconnect all travel through
the ProtoMessage static entry points now that the no-op thunk is gone.
2026-09-07 15:57:12 +02:00
J. Nick Koston af9b59d4bd [api] Trim the type erased entry point comments 2026-09-07 15:57:12 +02:00
J. Nick Koston 55fc5a10de [api] Tighten the ProtoMessage default entry point comment 2026-09-07 15:57:12 +02:00
J. Nick Koston 79927b918b [api] Clarify which encode entry points forward on ProtoMessage
The base class defaults are independent no-ops; only generated message
classes forward encode() and calculate_size() to their statics.
2026-09-07 15:57:12 +02:00
J. Nick Koston 1b070629bc [api] Make generated encode and size entry points type erased
Every message sent through send_message or the entity paths needed a
proto_encode_msg<T> thunk (17 bytes on xtensa) and, for entity state
and info messages, a calc_size<T> thunk, because the generated encode
and calculate_size were member functions and the connection code wants
plain function pointers over const void *.

The generator now emits the bodies as static encode_msg(const void *)
and calc_size_msg(const void *) functions, so &T::encode_msg is
already a MessageEncodeFn and the thunks disappear. The member
encode() and calculate_size() remain as inline forwarders for direct
callers. ProtoMessage carries the same static defaults for messages
without fields, which also removes the separate no-op encode thunk.
2026-09-07 15:57:12 +02:00
J. Nick Koston c4e1360cdf [api] Check the encoded end against the reserved size under ESPHOME_DEBUG_API
The fixed32 store helper moves to a private section since it neither bounds checks nor
advances the cursor, its comment describes the path each target takes, the generated
file scan flags any ProtoEncode call that does not assign the cursor, and StateWaiter
timeouts can carry a label so gathered waits are told apart.
2026-09-07 15:57:10 +02:00
J. Nick Koston 7c774699d7 [api] Outline the fixed32 writers on ARM cores without unaligned access too
Cortex-M0+ and ARM9 turn the four byte unaligned store into a memcpy call with a stack
temporary at every fixed32 field, and the outlined helper itself became a memcpy call
there, so the helper now spells out the byte stores. Xtensa and host objects are byte for
byte unchanged; on the RP2040 bench config the api object loses 28 bytes and the fixed32
memcpy calls.
2026-09-07 15:24:30 +02:00
J. Nick Koston ea71a24a9b [api] Mark the last two raw varint writers nodiscard and make StateWaiter failures visible
A predicate that raises now fails its wait instead of dying inside the state callback,
and a timeout names the predicate it was waiting for.
2026-09-07 14:01:34 +02:00
J. Nick Koston 709a1e1eb6 [api] Mark the raw encode helpers nodiscard too and drop a duplicate cursor test
The generated file scan already covers every emitted call, so the parametrized copy of
the same assertion goes.
2026-09-07 13:48:53 +02:00
J. Nick Koston 822b701792 [api] Mark the cursor returning encode helpers nodiscard
A call that drops the returned cursor would silently truncate the message, so the
compiler now warns on it and a unit test scans the generated file for the same mistake.
Also corrects the outlining comment for ESP8266, where the inline write is a few byte
stores rather than one, and the RAW_ENCODE_MAP annotation.
2026-09-07 12:37:48 +02:00
J. Nick Koston d2e4d2c46a [api] Outline the fixed32 writers only where memcpy is a call
On the ESP8266 the inline write was already a single store, so the
outlined helper cost a call per fixed32 field: sensor state encode went
from 615 to 864 ns on a d1 mini. ESP32 builds pass -fno-builtin-memcpy,
where the shared copy is both smaller and faster (562 to 328 ns on an
atom), so the gate is now USE_ESP32.
2026-09-07 11:40:01 +02:00
J. Nick Koston adbbda4072 [api] Emit every encode call through one generator helper
_encode_call() owns the cursor assignment and the _force suffix, so
the convention lives in one place instead of at every emission site;
the fixed32 fast path is an arm of the generic encode_content keyed by
a per type value template. write_fixed32_le uses convert_little_endian
instead of its own byte order switch. The integration test shares a
StateWaiter from state_utils and leaves the disconnect to the fixture.
2026-09-07 11:09:07 +02:00
J. Nick Koston 252bf6ea6a [api] Add an integration test for the encode branch boundaries
Covers a zero float that is skipped on the wire, a fixed32 state, a
negative int32, list entity strings and text states whose length
prefix needs two varint bytes, a two byte field tag through the
device info area, and the field free disconnect exchange.
2026-09-07 10:58:31 +02:00
J. Nick Koston 8ec9305688 [api] Trim the encode helper comments 2026-09-07 10:48:18 +02:00
J. Nick Koston 490aca17e6 [api] Share the fixed32 emission between float and fixed32 fields
One helper next to the other precomputed tag paths decides how a
single byte tag fixed32 field is written; the float and fixed32 types
only differ in the value expression. Drop the non forced std::string
encode_string overload, which the generator never emits, and build the
generator tests from one block of field type constants.
2026-09-07 10:33:21 +02:00
J. Nick Koston b77e2441d4 [api] Undefine PROTO_OUTLINE_FOR_SIZE after the encode helpers
The macro only exists for the two fixed32 writers in ProtoEncode, so
drop it once the class is complete instead of leaking it into every
translation unit that includes proto.h.
2026-09-07 10:09:46 +02:00
J. Nick Koston 3b14f4dfc8 [api] Pass the encode cursor by value through the protobuf helpers
The ProtoEncode helpers took the write cursor by reference and a
bool force flag. At -Os the compiler outlines most of them, so every
call site had to keep pos in a stack slot and pass its address, plus
a constant for the flag. The helpers now take the cursor by value and
return the advanced cursor, so consecutive calls chain through the
return register; forced fields call a _force overload instead of
passing a flag.

The fixed32 writers use __builtin_memcpy, which stays a builtin under
ESP-IDF's -fno-builtin-memcpy, and are outlined on embedded targets so
each fixed32 or float field is a short call instead of an inline
memcpy call. Non-forced float and fixed32 fields with a single-byte
tag share the same writer behind a zero check.

Generated encode bodies shrink by 18 percent on an ESP32 IDF proxy
build (2360 to 1932 bytes for 27 messages); entity messages gain the
most, for example ListEntitiesSensorResponse::encode 190 to 134 bytes
and SensorStateResponse::encode 78 to 49 bytes.
2026-09-07 09:21:54 +02:00
194 changed files with 6300 additions and 9944 deletions
+2 -11
View File
@@ -244,20 +244,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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 - name: Run prek
uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
with: with:
prek-version: ${{ steps.prek.outputs.version }} # Keep in sync with requirements_test.txt.
prek-version: "0.4.11"
# This job only runs on pull requests, so nothing ever populates # 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 # 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 # request copy, which is what the old seed-cache job existed to
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
and will be closed if no further activity occurs within 7 days. and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want If you are the author of this PR, please leave a comment if you want
to keep it open. Also, please merge the latest dev branch into your to keep it open. Also, please rebase your PR onto the latest dev
branch to ensure that it's up to date with the latest changes. branch to ensure that it's up to date with the latest changes.
Thank you for your contribution! Thank you for your contribution!
@@ -1,94 +0,0 @@
# Keeps pre-commit hook revs in sync with the requirements files.
#
# Dependabot only bumps the pins in requirements*.txt. Some of those tools
# are pinned again as hook revs in .pre-commit-config.yaml. This workflow
# runs script/sync_dependency_versions.py against the pull request branch
# and pushes a commit with the revs updated.
name: Sync dependency versions
on:
# pull_request_target rather than pull_request so the App secret is
# available on Dependabot pull requests (pull_request runs opened by
# Dependabot only see Dependabot secrets). The job below only touches
# branches in this repository and only ever executes the script from the
# base branch checkout, so fork code never runs with the token.
pull_request_target:
types: [opened, synchronize, reopened]
paths:
- requirements_dev.txt
- requirements_test.txt
- .pre-commit-config.yaml
- script/sync_dependency_versions.py
# The push to the pull request branch uses the App token minted below, so
# the workflow's GITHUB_TOKEN does not need any scopes.
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
sync:
name: Sync pinned versions
runs-on: ubuntu-latest
# Same-repository branches only: a push to a fork is not possible with
# this token, and it keeps untrusted heads out of a privileged job.
if: >-
github.repository == 'esphome/esphome'
&& github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
# A push made with the workflow's own GITHUB_TOKEN would not start
# CI on the new commit; a push with the App token does.
permission-contents: write # git push of the sync commit to the pull request branch
- name: Check out base branch
# Provides the script that runs below. Deliberately the base branch
# so the pull request cannot change what executes here.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Check out pull request branch
# No allow-unsafe-pr-checkout here on purpose: checkout v7 only
# refuses heads that live in a different repository, and the job
# condition above already limits runs to same-repository branches.
# Leaving it off keeps that refusal as a backstop for fork heads.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.ref }}
path: pull-request
token: ${{ steps.generate-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install yamlrocks
# The script edits YAML through yamlrocks. Take the pin from the
# base branch requirements so this workflow has no copy of its own.
run: pip install "$(grep -E '^yamlrocks==' requirements_test.txt | cut -d'#' -f1)"
- name: Sync pinned versions
run: python script/sync_dependency_versions.py --root pull-request
- name: Push changes
working-directory: pull-request
run: |
if git diff --quiet; then
echo "All pinned versions already match the requirements files."
exit 0
fi
git config user.name "esphome[bot]"
git config user.email "115708604+esphome[bot]@users.noreply.github.com"
git commit -am "Sync pinned tool versions with requirements files"
git push
+3 -2
View File
@@ -1,6 +1,7 @@
--- ---
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
ci: ci:
autoupdate_commit_msg: 'pre-commit: autoupdate' autoupdate_commit_msg: 'pre-commit: autoupdate'
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
@@ -10,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.16.6 rev: v0.16.3
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@@ -41,7 +42,7 @@ repos:
- id: pyupgrade - id: pyupgrade
args: [--py312-plus] args: [--py312-plus]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.38.0 rev: v1.37.1
hooks: hooks:
- id: yamllint - id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$ exclude: ^(\.clang-format|\.clang-tidy)$
+1 -1
View File
@@ -840,7 +840,7 @@ file does, and it is the authority when they disagree. The most useful starting
cv.rename_key( cv.rename_key(
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component" 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: For other deprecations, warn manually during validation:
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt -r /requirements.txt
# Install the ESPHome Device Builder dashboard. # Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6 RUN uv pip install --no-cache-dir esphome-device-builder==1.14.4
RUN \ RUN \
platformio settings set enable_telemetry No \ platformio settings set enable_telemetry No \
+3 -1
View File
@@ -23,7 +23,9 @@ from esphome.util import safe_print
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable 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__) _LOGGER = logging.getLogger(__name__)
+56 -51
View File
@@ -13,7 +13,7 @@ void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); }
void Anova::setup() { void Anova::setup() {
this->codec_ = make_unique<AnovaCodec>(); this->codec_ = make_unique<AnovaCodec>();
this->poll_step_ = PollStep::IDLE; this->current_request_ = 0;
} }
void Anova::loop() { void Anova::loop() {
@@ -22,15 +22,6 @@ void Anova::loop() {
this->disable_loop(); this->disable_loop();
} }
void Anova::write_request_(AnovaPacket *pkt) {
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
void Anova::control(const ClimateCall &call) { void Anova::control(const ClimateCall &call) {
auto mode_val = call.get_mode(); auto mode_val = call.get_mode();
if (mode_val.has_value()) { if (mode_val.has_value()) {
@@ -47,11 +38,22 @@ void Anova::control(const ClimateCall &call) {
ESP_LOGW(TAG, "Unsupported mode: %d", mode); ESP_LOGW(TAG, "Unsupported mode: %d", mode);
return; return;
} }
this->write_request_(pkt); auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
} }
auto target_temp = call.get_target_temperature(); auto target_temp = call.get_target_temperature();
if (target_temp.has_value()) { if (target_temp.has_value()) {
this->write_request_(this->codec_->get_set_target_temp_request(*target_temp)); auto *pkt = this->codec_->get_set_target_temp_request(*target_temp);
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
} }
} }
@@ -60,7 +62,6 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_DISCONNECT_EVT: {
this->current_temperature = NAN; this->current_temperature = NAN;
this->target_temperature = NAN; this->target_temperature = NAN;
this->poll_step_ = PollStep::IDLE;
this->publish_state(); this->publish_state();
break; break;
} }
@@ -82,8 +83,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
this->poll_step_ = PollStep::IDLE; this->current_request_ = 0;
this->update(); // begin the first poll cycle immediately this->update();
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_NOTIFY_EVT: {
@@ -100,30 +101,33 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF; this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF;
} }
if (this->codec_->has_unit()) { if (this->codec_->has_unit()) {
ESP_LOGD(TAG, "Anova units is %s", (this->codec_->unit_ == 'f') ? "fahrenheit" : "celsius"); this->fahrenheit_ = (this->codec_->unit_ == 'f');
ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celsius");
this->current_request_++;
} }
this->publish_state(); this->publish_state();
// Advance the poll cycle to its next request based on the reply we got. if (this->current_request_ > 1) {
switch (this->poll_step_) { AnovaPacket *pkt = nullptr;
case PollStep::SET_UNIT: switch (this->current_request_++) {
this->poll_step_ = PollStep::STATUS; case 2:
this->write_request_(this->codec_->get_read_device_status_request()); pkt = this->codec_->get_read_target_temp_request();
break; break;
case PollStep::STATUS: case 3:
this->poll_step_ = PollStep::TARGET; pkt = this->codec_->get_read_current_temp_request();
this->write_request_(this->codec_->get_read_target_temp_request()); break;
break; default:
case PollStep::TARGET: this->current_request_ = 1;
this->poll_step_ = PollStep::CURRENT; break;
this->write_request_(this->codec_->get_read_current_temp_request()); }
break; if (pkt != nullptr) {
case PollStep::CURRENT: auto status =
this->poll_step_ = PollStep::IDLE; // full cycle complete esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
break; pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
default: if (status) {
// A reply to an ad-hoc control() write, outside a managed cycle. ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
break; }
}
} }
break; break;
} }
@@ -132,26 +136,27 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
} }
} }
void Anova::set_unit_of_measurement(const char *unit) { this->want_fahrenheit_ = !strncmp(unit, "f", 1); } void Anova::set_unit_of_measurement(const char *unit) { this->fahrenheit_ = !strncmp(unit, "f", 1); }
void Anova::update() { void Anova::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) if (this->node_state != espbt::ClientState::ESTABLISHED)
return; return;
if (this->poll_step_ != PollStep::IDLE) {
// The previous cycle never finished within a full polling interval -- a if (this->current_request_ < 2) {
// reply was missed or a write failed. Restart the cycle rather than stall; AnovaPacket *pkt;
// the polling interval itself acts as the timeout. A late reply from the if (this->current_request_ == 0) {
// abandoned cycle is harmless: state decoding happens on every notify pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
// regardless of step, and each notify sends at most one follow-up request. } else {
ESP_LOGW(TAG, "[%s] Poll cycle incomplete (step %u); restarting cycle", this->parent_->address_str(), pkt = this->codec_->get_read_device_status_request();
static_cast<uint8_t>(this->poll_step_)); }
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
this->current_request_++;
} }
// Re-assert the configured unit at the start of every poll cycle, then fall
// through the status/temperature reads via the notification handler. Always
// command the configured unit (want_fahrenheit_) -- never the last value the
// device reported, or a drift to 'c' would lock itself in.
this->poll_step_ = PollStep::SET_UNIT;
this->write_request_(this->codec_->get_set_unit_request(this->want_fahrenheit_ ? 'f' : 'c'));
} }
} // namespace esphome::anova } // namespace esphome::anova
+2 -11
View File
@@ -37,20 +37,11 @@ class Anova final : public climate::Climate, public esphome::ble_client::BLEClie
void set_unit_of_measurement(const char *unit); void set_unit_of_measurement(const char *unit);
protected: protected:
// A poll cycle re-asserts the configured unit, then reads device state.
// Re-asserting every cycle prevents the cooker from silently reverting to
// its default (Celsius); previously the unit was only set once on
// connection, so a drift persisted (and corrupted the F/C interpretation of
// subsequent readings) until the BLE link was re-established.
enum class PollStep : uint8_t { SET_UNIT, STATUS, TARGET, CURRENT, IDLE };
void write_request_(AnovaPacket *pkt);
std::unique_ptr<AnovaCodec> codec_; std::unique_ptr<AnovaCodec> codec_;
void control(const climate::ClimateCall &call) override; void control(const climate::ClimateCall &call) override;
uint16_t char_handle_; uint16_t char_handle_;
bool want_fahrenheit_{true}; // configured target unit; never overwritten by device replies uint8_t current_request_;
PollStep poll_step_{PollStep::IDLE}; bool fahrenheit_;
}; };
} // namespace esphome::anova } // namespace esphome::anova
+4 -69
View File
@@ -76,9 +76,7 @@ service APIConnection {
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {} rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {} rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {} rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_usb_info(SerialProxyGetUsbInfoRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {} rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
} }
@@ -229,11 +227,6 @@ enum SerialProxyPortType {
SERIAL_PROXY_PORT_TYPE_TTL = 0; SERIAL_PROXY_PORT_TYPE_TTL = 0;
SERIAL_PROXY_PORT_TYPE_RS232 = 1; SERIAL_PROXY_PORT_TYPE_RS232 = 1;
SERIAL_PROXY_PORT_TYPE_RS485 = 2; SERIAL_PROXY_PORT_TYPE_RS485 = 2;
// A serial device attached through a USB bridge. Set by the device configuration, never
// by the user; identifies ports whose USB identity can be read with
// SerialProxyGetUsbInfoRequest. Deliberately not a USB endpoint type: serial_proxy
// carries serial devices only, whatever bridge chip connects them.
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3;
} }
message SerialProxyInfo { message SerialProxyInfo {
@@ -2733,8 +2726,7 @@ enum SerialProxyParity {
SERIAL_PROXY_PARITY_ODD = 2; SERIAL_PROXY_PARITY_ODD = 2;
} }
// Configure UART parameters for a serial proxy instance. Only the subscribed client may // Configure UART parameters for a serial proxy instance
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
message SerialProxyConfigureRequest { message SerialProxyConfigureRequest {
option (id) = 138; option (id) = 138;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
@@ -2760,8 +2752,7 @@ message SerialProxyDataReceived {
bytes data = 2; // Raw data received from the serial device bytes data = 2; // Raw data received from the serial device
} }
// Write data to a serial device. Only the subscribed client may write; writes from // Write data to a serial device
// others are ignored (since API 1.17).
message SerialProxyWriteRequest { message SerialProxyWriteRequest {
option (id) = 140; option (id) = 140;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
@@ -2772,8 +2763,7 @@ message SerialProxyWriteRequest {
bytes data = 2; // Raw data to write to the serial device bytes data = 2; // Raw data to write to the serial device
} }
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them; // Set modem control pin states (RTS and DTR)
// others are refused with PORT_IN_USE (since API 1.17).
message SerialProxySetModemPinsRequest { message SerialProxySetModemPinsRequest {
option (id) = 141; option (id) = 141;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
@@ -2812,7 +2802,6 @@ enum SerialProxyRequestType {
// error the device answers with INVALID_ARGUMENT. // error the device answers with INVALID_ARGUMENT.
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
} }
enum SerialProxyStatus { enum SerialProxyStatus {
@@ -2825,8 +2814,7 @@ enum SerialProxyStatus {
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
} }
// Generic request message for simple serial proxy operations. FLUSH requires an active // Generic request message for simple serial proxy operations
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
message SerialProxyRequest { message SerialProxyRequest {
option (id) = 144; option (id) = 144;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
@@ -2850,59 +2838,6 @@ message SerialProxyRequestResponse {
string error_message = 4; // Additional detail on failure (optional) string error_message = 4; // Additional detail on failure (optional)
} }
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
// activates the port's protocol-aware tap (if one is configured), letting it observe
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
// speaks is a property of the device configuration, discoverable from the tap
// component's own API surface. A client that is about to flash firmware selects RAW
// first, which definitively disables that injection.
enum SerialProxyMode {
SERIAL_PROXY_MODE_RAW = 0;
SERIAL_PROXY_MODE_PROTOCOL = 1;
}
// Only the subscribed client may change the mode; any other caller -- including one that
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
// when the port has no protocol-aware tap configured.
message SerialProxySetModeRequest {
option (id) = 152;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyMode mode = 2;
}
// Ask for the USB identity of the device behind a USB_SERIAL port. Read-only, so no
// subscription is required -- a client typically uses this to decide which port to
// subscribe to. Answered with NOT_SUPPORTED on ports that are not USB_SERIAL.
message SerialProxyGetUsbInfoRequest {
option (id) = 153;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
}
// The USB identity of the device currently behind a port, read live from the cached
// USB descriptors. Fields are zero/empty while no device is connected.
message SerialProxyGetUsbInfoResponse {
option (id) = 154;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyStatus status = 2; // NOT_SUPPORTED when the port is not USB_SERIAL
bool connected = 3; // True when a USB device is currently attached
uint32 vendor_id = 4;
uint32 product_id = 5;
uint32 bcd_device = 6;
uint32 interface_number = 7; // Channel index on multi-port bridges
string manufacturer = 8;
string product = 9;
string serial_number = 10;
}
// ==================== BLUETOOTH CONNECTION PARAMS ==================== // ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest { message BluetoothSetConnectionParamsRequest {
option (id) = 145; option (id) = 145;
+7 -40
View File
@@ -48,9 +48,6 @@
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h" #include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif #endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_host/usb_host.h"
#endif
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h" #include "esphome/components/water_heater/water_heater.h"
#endif #endif
@@ -1645,27 +1642,6 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
} }
} }
void APIConnection::on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg) {
auto &proxies = App.get_serial_proxies();
SerialProxyGetUsbInfoResponse resp{};
resp.instance = msg.instance;
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
} else {
#ifdef USE_SERIAL_PROXY_USB_INFO
// The response's strings are views into this buffer, which outlives the send below
usb_host::UsbDeviceInfo info;
proxies[msg.instance]->get_usb_info(info, resp);
#else
resp.status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
#endif
}
if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
}
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
auto &proxies = App.get_serial_proxies(); auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) { if (msg.instance >= proxies.size()) {
@@ -1685,7 +1661,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
break; break;
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE: case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS: case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
// Response-only discriminators; never valid in a request // Response-only discriminators; never valid in a request
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type)); ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT; status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
@@ -1698,19 +1673,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
send_serial_proxy_ack(this, msg.instance, msg.type, status); send_serial_proxy_ack(this, msg.instance, msg.type, status);
} }
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return;
}
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
serial_proxy_result_to_status(result));
}
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
if (!this->send_message(msg)) { if (!this->send_message(msg)) {
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full"); ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
@@ -1837,7 +1799,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
resp.api_version_minor = 17; resp.api_version_minor = 16;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise // Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = ESPHOME_VERSION_REF; resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name()); resp.name = StringRef(App.get_name());
@@ -2293,7 +2255,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type,
// Capacity reserved above, cannot fail // Capacity reserved above, cannot fail
(void) shared_buf.resize(write_start + payload_size); (void) shared_buf.resize(write_start + payload_size);
ProtoWriteBuffer buffer{&shared_buf, write_start}; ProtoWriteBuffer buffer{&shared_buf, write_start};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
#ifdef ESPHOME_DEBUG_API
assert(end == shared_buf.data() + shared_buf.size());
#else
(void) end;
#endif
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
} }
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) // encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
+6 -26
View File
@@ -243,9 +243,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg); void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg);
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg); void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg); void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg); void on_serial_proxy_request(const SerialProxyRequest &msg);
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg); void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif #endif
@@ -347,11 +345,7 @@ class APIConnection final : public APIServerConnectionBase {
/// Returns false as soon as the TCP buffer is full. Marked nodiscard so we /// Returns false as soon as the TCP buffer is full. Marked nodiscard so we
/// have no silent failures: every caller must handle (or log) a refusal. /// have no silent failures: every caller must handle (or log) a refusal.
template<typename T> [[nodiscard]] bool send_message(const T &msg) { template<typename T> [[nodiscard]] bool send_message(const T &msg) {
if constexpr (T::ESTIMATED_SIZE == 0) { return this->send_message_(T::calc_size_msg(&msg), T::MESSAGE_TYPE, &T::encode_msg, &msg);
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
} else {
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
}
} }
/// Clear the shared write buffer and reserve space for the first message. /// Clear the shared write buffer and reserve space for the first message.
@@ -407,16 +401,6 @@ class APIConnection final : public APIServerConnectionBase {
void process_state_subscriptions_(); void process_state_subscriptions_();
#endif #endif
// Size thunk — converts void* back to concrete type for direct calculate_size() call
template<typename T> static uint32_t calc_size(const void *msg) {
return static_cast<const T *>(msg)->calculate_size();
}
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
return buf.get_pos();
}
// Non-template buffer management for send_message // Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg); bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
@@ -435,11 +419,7 @@ class APIConnection final : public APIServerConnectionBase {
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead. // Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
// batch_message_type_ is already set by dispatch_message_ before reaching here. // batch_message_type_ is already set by dispatch_message_ before reaching here.
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
if constexpr (T::ESTIMATED_SIZE == 0) { return encode_to_buffer_slow(T::calc_size_msg(&msg), &T::encode_msg, &msg, conn, remaining_size);
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
}
} }
// Non-template core — fills state fields and encodes // Non-template core — fills state fields and encodes
@@ -451,7 +431,7 @@ class APIConnection final : public APIServerConnectionBase {
template<typename T> template<typename T>
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn, static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) { uint32_t remaining_size) {
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); return fill_and_encode_entity_state(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
} }
// Non-template core — fills info fields, allocates buffers, and encodes // Non-template core — fills info fields, allocates buffers, and encodes
@@ -463,7 +443,7 @@ class APIConnection final : public APIServerConnectionBase {
template<typename T> template<typename T>
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn, static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) { uint32_t remaining_size) {
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); return fill_and_encode_entity_info(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
} }
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info // Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
@@ -477,8 +457,8 @@ class APIConnection final : public APIServerConnectionBase {
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg, static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
StringRef &device_class_field, APIConnection *conn, StringRef &device_class_field, APIConnection *conn,
uint32_t remaining_size) { uint32_t remaining_size) {
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>, return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &T::calc_size_msg,
&proto_encode_msg<T>, conn, remaining_size); &T::encode_msg, conn, remaining_size);
} }
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@@ -46,7 +46,13 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
return 0; return 0;
} }
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
#ifdef ESPHOME_DEBUG_API
// A body that writes fewer bytes than calculate_size() promised would ship stale buffer bytes
assert(end == shared_buf.data() + shared_buf.size());
#else
(void) end;
#endif
return total_calculated_size; return total_calculated_size;
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-39
View File
@@ -143,8 +143,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::S
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232"); return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232");
case enums::SERIAL_PROXY_PORT_TYPE_RS485: case enums::SERIAL_PROXY_PORT_TYPE_RS485:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485"); return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485");
case enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_USB_SERIAL");
default: default:
return ESPHOME_PSTR("UNKNOWN"); return ESPHOME_PSTR("UNKNOWN");
} }
@@ -856,8 +854,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE"); return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS: case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS"); return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
default: default:
return ESPHOME_PSTR("UNKNOWN"); return ESPHOME_PSTR("UNKNOWN");
} }
@@ -882,16 +878,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
return ESPHOME_PSTR("UNKNOWN"); return ESPHOME_PSTR("UNKNOWN");
} }
} }
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
switch (value) {
case enums::SERIAL_PROXY_MODE_RAW:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
case enums::SERIAL_PROXY_MODE_PROTOCOL:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#endif #endif
const char *HelloRequest::dump_to(DumpBuffer &out) const { const char *HelloRequest::dump_to(DumpBuffer &out) const {
@@ -2819,31 +2805,6 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
return out.c_str(); return out.c_str();
} }
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
return out.c_str();
}
const char *SerialProxyGetUsbInfoRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
return out.c_str();
}
const char *SerialProxyGetUsbInfoResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoResponse"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
dump_field(out, ESPHOME_PSTR("connected"), this->connected);
dump_field(out, ESPHOME_PSTR("vendor_id"), this->vendor_id);
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer);
dump_field(out, ESPHOME_PSTR("product"), this->product);
dump_field(out, ESPHOME_PSTR("serial_number"), this->serial_number);
return out.c_str();
}
#endif #endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
@@ -712,28 +712,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_device_capabilities_request(); this->on_device_capabilities_request();
break; break;
} }
#ifdef USE_SERIAL_PROXY
case SerialProxySetModeRequest::MESSAGE_TYPE: {
SerialProxySetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
#endif
this->on_serial_proxy_set_mode_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetUsbInfoRequest::MESSAGE_TYPE: {
SerialProxyGetUsbInfoRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_usb_info_request"), msg);
#endif
this->on_serial_proxy_get_usb_info_request(msg);
break;
}
#endif
default: default:
break; break;
} }
-7
View File
@@ -235,13 +235,6 @@ class APIServerConnectionBase {
void on_serial_proxy_request(const SerialProxyRequest &value){}; void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif #endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif #endif
+3 -2
View File
@@ -433,8 +433,9 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// Home Assistant subscribes to actions shortly *after* authenticating, so actions // Home Assistant subscribes to actions shortly *after* authenticating, so actions
// fired right at connection time (on_client_connected, on_time_sync, ...) can // fired right at connection time (on_client_connected, on_time_sync, ...) can
// arrive before the subscription and are lost - warn instead of failing silently. // arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(), call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)") this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected")); : LOG_STR_LITERAL("no client connected"));
} }
+59 -58
View File
@@ -214,73 +214,74 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer; const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length; const uint8_t *end = buffer + length;
while (ptr < end) { // Single-byte varints dominate, so that case advances the cursor inline.
// Parse field header - ptr < end guarantees len >= 1 auto read_varint = [&](proto_varint_value_t &value) ESPHOME_ALWAYS_INLINE {
if (ptr == end)
return false;
if (*ptr < 0x80) [[likely]] {
value = *ptr++;
return true;
}
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) { if (!res.has_value())
return false;
value = res.value;
ptr += res.consumed;
return true;
};
while (ptr < end) {
proto_varint_value_t tag_value;
if (!read_varint(tag_value)) {
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
return; return;
} }
uint32_t tag = static_cast<uint32_t>(res.value); uint32_t tag = static_cast<uint32_t>(tag_value);
uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3; // Length-delimited fields move this past the length prefix
ptr += res.consumed; const uint8_t *data = ptr;
proto_varint_value_t scalar;
switch (field_type) { if (field_type == WIRE_TYPE_VARINT) [[likely]] {
case WIRE_TYPE_VARINT: { // VarInt if (!read_varint(scalar)) {
res = ProtoVarInt::parse(ptr, end - ptr); ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_varint(field_id, res.value)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
static_cast<uint64_t>(res.value));
}
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t val;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian — direct load on LE platforms
memcpy(&val, ptr, 4);
#else
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
#endif
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
}
ptr += 4;
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
return; return;
}
} else {
switch (field_type) {
case WIRE_TYPE_LENGTH_DELIMITED: {
proto_varint_value_t length_value;
if (!read_varint(length_value)) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = static_cast<uint32_t>(length_value);
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
data = ptr;
scalar = field_length;
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: {
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
// Byte loads instead of memcpy: ESP-IDF passes -fno-builtin-memcpy, which made this a call
scalar = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
ptr += 4;
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
return;
}
} }
this->decode_field(tag, data, scalar);
} }
} }
+228 -166
View File
@@ -170,40 +170,43 @@ class ProtoVarInt {
class ProtoMessage; class ProtoMessage;
class ProtoSize; class ProtoSize;
class ProtoLengthDelimited { /// Case label for decode_field(): the wire tag of a field, so a field that arrives with another wire
/// type matches no case.
constexpr uint32_t proto_tag(uint32_t field_id, uint32_t wire_type) { return (field_id << 3) | wire_type; }
/// One decoded field: the payload pointer and a scalar holding the varint or fixed32 value, or the
/// length of a length-delimited field. The wire type in the tag says which applies; accessors do not check.
class ProtoFieldValue {
public: public:
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} ProtoFieldValue(const uint8_t *data, proto_varint_value_t scalar) : data_(data), scalar_(scalar) {}
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
// Direct access to raw data without string allocation proto_varint_value_t as_varint() const { return this->scalar_; }
const uint8_t *data() const { return this->value_; } // A bool is sent as 0 or 1, so the low word is enough and saves a second compare with 64 bit varints
size_t size() const { return this->length_; } bool as_bool() const { return static_cast<uint32_t>(this->scalar_) != 0; }
/// Decode the length-delimited data into a message instance. // Length-delimited accessors
const uint8_t *data() const { return this->data_; }
size_t size() const { return static_cast<size_t>(this->scalar_); }
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->data_), this->size()); }
/// Decode the length-delimited payload into a message instance.
/// Template preserves concrete type so decode() resolves statically. /// Template preserves concrete type so decode() resolves statically.
template<typename T> void decode_to_message(T &msg) const; template<typename T> void decode_to_message(T &msg) const { msg.decode(this->data_, this->size()); }
protected: // Fixed32 accessors
const uint8_t *const value_; uint32_t as_fixed32() const { return static_cast<uint32_t>(this->scalar_); }
const size_t length_; int32_t as_sfixed32() const { return static_cast<int32_t>(this->as_fixed32()); }
};
class Proto32Bit {
public:
explicit Proto32Bit(uint32_t value) : value_(value) {}
uint32_t as_fixed32() const { return this->value_; }
int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
float as_float() const { float as_float() const {
union { union {
uint32_t raw; uint32_t raw;
float value; float value;
} s{}; } s{};
s.raw = this->value_; s.raw = this->as_fixed32();
return s.value; return s.value;
} }
protected: private:
const uint32_t value_; const uint8_t *data_;
proto_varint_value_t scalar_;
}; };
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
@@ -252,7 +255,7 @@ class ProtoWriteBuffer {
* *
* Following https://protobuf.dev/programming-guides/encoding/#structure * Following https://protobuf.dev/programming-guides/encoding/#structure
*/ */
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw(proto_tag(field_id, type)); }
/// Single-pass encode for repeated submessage elements. /// Single-pass encode for repeated submessage elements.
/// Thin template wrapper; all buffer work is in the non-template core. /// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_sub_message(uint32_t field_id, const T &value); template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
@@ -287,19 +290,31 @@ class ProtoWriteBuffer {
uint8_t *pos_; uint8_t *pos_;
}; };
// A four byte unaligned store is a memcpy call on ESP-IDF (-fno-builtin-memcpy) and on ARM cores without
// unaligned access (Cortex-M0+, ARM9), so those targets share one outlined byte store helper per fixed32
// field. Elsewhere the write inlines to a single store, or on ESP8266 to a few stores that measured
// faster than a call, so it stays inline.
#if defined(USE_ESP32) || (defined(__arm__) && !defined(__ARM_FEATURE_UNALIGNED))
#define PROTO_OUTLINE_FOR_SIZE __attribute__((noinline))
#define PROTO_FIXED32_BYTE_STORES true
#else
#define PROTO_OUTLINE_FOR_SIZE inline
#define PROTO_FIXED32_BYTE_STORES false
#endif
// Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize. // Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize.
constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128 constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384 constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
/// Static encode helpers for generated encode() functions. /// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
/// Generated code hoists buffer.pos_ into a local uint8_t *__restrict__ pos, /// returns it advanced, so outlined calls at -Os chain through the return register instead of a
/// then calls these methods which take pos by reference. No struct, no overhead. /// stack slot. Helpers without a _force suffix skip fields holding the proto3 default.
/// For sub-messages, pos is synced back to buffer before the call and reloaded after.
class ProtoEncode { class ProtoEncode {
public: public:
/// Write a multi-byte varint directly through a pos pointer. /// Write a multi-byte varint directly through a pos pointer.
template<typename T> template<typename T>
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) { [[nodiscard]] static inline uint8_t *encode_varint_raw_loop(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
T value) {
do { do {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value | 0x80); *pos++ = static_cast<uint8_t>(value | 0x80);
@@ -307,48 +322,49 @@ class ProtoEncode {
} while (value > 0x7F); } while (value > 0x7F);
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return pos;
} }
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint32_t value) { encode_varint_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] { if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return; return pos;
} }
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). /// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint32_t value) { encode_varint_raw_short(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] { if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return; return pos;
} }
if (value < VARINT_MAX_2_BYTE) [[likely]] { if (value < VARINT_MAX_2_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 2); PROTO_ENCODE_CHECK_BOUNDS(pos, 2);
*pos++ = static_cast<uint8_t>(value | 0x80); *pos++ = static_cast<uint8_t>(value | 0x80);
*pos++ = static_cast<uint8_t>(value >> 7); *pos++ = static_cast<uint8_t>(value >> 7);
return; return pos;
} }
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint64_t value) { encode_varint_raw_64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] { if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value); *pos++ = static_cast<uint8_t>(value);
return; return pos;
} }
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
/// Encode a 48-bit MAC address (stored in a uint64) as varint. /// Encode a 48-bit MAC address (stored in a uint64) as varint.
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the /// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes /// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
/// with no per-byte branch. Falls back to the general loop otherwise. /// with no per-byte branch. Falls back to the general loop otherwise.
/// Caller must guarantee value fits in 48 bits (checked in debug builds). /// Caller must guarantee value fits in 48 bits (checked in debug builds).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint64_t value) { encode_varint_raw_48bit(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits"); assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
#endif #endif
@@ -363,38 +379,39 @@ class ProtoEncode {
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80); pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80); pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
pos[6] = static_cast<uint8_t>(value >> 42); pos[6] = static_cast<uint8_t>(value >> 42);
pos += 7; return pos + 7;
return;
} }
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value); return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint32_t field_id, uint32_t type) { encode_field_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) {
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type); return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, proto_tag(field_id, type));
} }
/// Write a single precomputed tag byte. Tag must be < 128. /// Write a single precomputed tag byte. Tag must be < 128.
static inline void ESPHOME_ALWAYS_INLINE write_raw_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
uint8_t b) { write_raw_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t b) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = b; *pos++ = b;
return pos;
} }
/// Reserve one byte for later backpatch (e.g., sub-message length). /// Reserve one byte for later backpatch (e.g., sub-message length).
/// Advances pos past the reserved byte without writing a value. /// Advances pos past the reserved byte without writing a value.
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) { [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
reserve_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
pos++; return pos + 1;
} }
/// Write raw bytes to the buffer (no tag, no length prefix). /// Write raw bytes to the buffer (no tag, no length prefix).
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
const void *data, size_t len) { encode_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, const void *data, size_t len) {
PROTO_ENCODE_CHECK_BOUNDS(pos, len); PROTO_ENCODE_CHECK_BOUNDS(pos, len);
std::memcpy(pos, data, len); std::memcpy(pos, data, len);
pos += len; return pos + len;
} }
/// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128. /// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128.
/// Tag must be a single-byte varint (< 128). Always encodes (no zero check). /// Tag must be a single-byte varint (< 128). Always encodes (no zero check).
static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, [[nodiscard]] static inline uint8_t *encode_short_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const StringRef &ref) { uint8_t tag, const StringRef &ref) {
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128"); assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128");
#endif #endif
@@ -402,137 +419,191 @@ class ProtoEncode {
pos[0] = tag; pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size()); pos[1] = static_cast<uint8_t>(ref.size());
std::memcpy(pos + 2, ref.c_str(), ref.size()); std::memcpy(pos + 2, ref.c_str(), ref.size());
pos += 2 + ref.size(); return pos + 2 + ref.size();
} }
/// Write a precomputed tag byte + 32-bit value in one operation. /// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
uint8_t tag, uint32_t value) { uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 5); PROTO_ENCODE_CHECK_BOUNDS(pos, 5);
pos[0] = tag; pos[0] = tag;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ write_fixed32_le(pos + 1, value);
std::memcpy(pos + 1, &value, 4); return pos + 5;
#else
pos[1] = static_cast<uint8_t>(value & 0xFF);
pos[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
pos[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
pos[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
#endif
pos += 5;
} }
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const char *string, size_t len, bool force = false) { uint32_t field_id, const char *string, size_t len) {
if (len == 0 && !force) pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
// NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute // NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute
if (len < VARINT_MAX_1_BYTE) [[likely]] { if (len < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len); PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
*pos++ = static_cast<uint8_t>(len); *pos++ = static_cast<uint8_t>(len);
} else { } else {
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len); pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
PROTO_ENCODE_CHECK_BOUNDS(pos, len); PROTO_ENCODE_CHECK_BOUNDS(pos, len);
} }
std::memcpy(pos, string, len); std::memcpy(pos, string, len);
pos += len; return pos + len;
} }
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const std::string &value, bool force = false) { uint32_t field_id, const char *string, size_t len) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size(), force); if (len == 0)
return pos;
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, string, len);
} }
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const StringRef &ref, bool force = false) { uint32_t field_id, const std::string &value) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size(), force); return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size());
} }
static inline void encode_bytes(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
const uint8_t *data, size_t len, bool force = false) { uint32_t field_id, const StringRef &ref) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len, force); return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
} }
static inline void encode_uint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t value, bool force = false) { uint32_t field_id, const StringRef &ref) {
if (value == 0 && !force) return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void encode_uint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_bytes(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint64_t value, bool force = false) { uint32_t field_id, const uint8_t *data, size_t len) {
if (value == 0 && !force) return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
} }
static inline void encode_bool(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, bool value, [[nodiscard]] static inline uint8_t *encode_bytes_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, const uint8_t *data, size_t len) {
if (!value && !force) return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
return; }
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); [[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
}
[[nodiscard]] static inline uint8_t *encode_uint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t value) {
if (value == 0)
return pos;
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
}
[[nodiscard]] static inline uint8_t *encode_uint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint64_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
}
[[nodiscard]] static inline uint8_t *encode_uint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint64_t value) {
if (value == 0)
return pos;
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
}
[[nodiscard]] static inline uint8_t *encode_bool_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, bool value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
PROTO_ENCODE_CHECK_BOUNDS(pos, 1); PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = value ? 0x01 : 0x00; *pos++ = value ? 0x01 : 0x00;
return pos;
} }
static inline void encode_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_bool(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t value, bool force = false) { uint32_t field_id, bool value) {
if (value == 0 && !force) if (!value)
return; return pos;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5); return encode_bool_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
}
/// Tag + fixed32 for multi-byte tags; single-byte tags use write_tag_and_fixed32.
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *encode_fixed32_force(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5);
PROTO_ENCODE_CHECK_BOUNDS(pos, 4); PROTO_ENCODE_CHECK_BOUNDS(pos, 4);
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ write_fixed32_le(pos, value);
std::memcpy(pos, &value, 4); return pos + 4;
pos += 4; }
#else [[nodiscard]] static inline uint8_t *encode_fixed32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
*pos++ = (value >> 0) & 0xFF; uint32_t field_id, uint32_t value) {
*pos++ = (value >> 8) & 0xFF; if (value == 0)
*pos++ = (value >> 16) & 0xFF; return pos;
*pos++ = (value >> 24) & 0xFF; return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
#endif
} }
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
// not supported to reduce overhead on embedded systems. All ESPHome devices are // not supported to reduce overhead on embedded systems. All ESPHome devices are
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
// is needed in the future, the necessary encoding/decoding functions must be added. // is needed in the future, the necessary encoding/decoding functions must be added.
static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value, [[nodiscard]] static inline uint8_t *encode_float(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, float value) {
uint32_t raw = float_to_raw(value); return encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
if (raw == 0 && !force)
return;
encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, raw);
} }
static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value, [[nodiscard]] static inline uint8_t *encode_float_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, float value) {
return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
}
[[nodiscard]] static inline uint8_t *encode_int32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int32_t value) {
if (value < 0) { if (value < 0) {
// negative int32 is always 10 byte long // negative int32 is always 10 byte long
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force); return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
return;
} }
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value), force); return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value));
} }
static inline void encode_int64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int64_t value, [[nodiscard]] static inline uint8_t *encode_int32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
bool force = false) { uint32_t field_id, int32_t value) {
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force); if (value == 0)
return pos;
return encode_int32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
} }
static inline void encode_sint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_int64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
int32_t value, bool force = false) { uint32_t field_id, int64_t value) {
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value), force); return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
} }
static inline void encode_sint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, [[nodiscard]] static inline uint8_t *encode_int64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
int64_t value, bool force = false) { uint32_t field_id, int64_t value) {
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value), force); return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
} }
/// Sub-message encoding: sync pos to buffer, delegate, get pos from return value. [[nodiscard]] static inline uint8_t *encode_sint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int32_t value) {
return encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
}
[[nodiscard]] static inline uint8_t *encode_sint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int32_t value) {
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
}
[[nodiscard]] static inline uint8_t *encode_sint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int64_t value) {
return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
}
[[nodiscard]] static inline uint8_t *encode_sint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, int64_t value) {
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
}
/// Sub-message encoding: sync pos to buffer, delegate, read the cursor back.
template<typename T> template<typename T>
static inline void encode_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, ProtoWriteBuffer &buffer, [[nodiscard]] static inline uint8_t *encode_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const T &value) { ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) {
buffer.set_pos(pos); buffer.set_pos(pos);
buffer.encode_sub_message(field_id, value); buffer.encode_sub_message(field_id, value);
pos = buffer.get_pos(); return buffer.get_pos();
} }
template<typename T> template<typename T>
static inline void encode_optional_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, [[nodiscard]] static inline uint8_t *encode_optional_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) { ProtoWriteBuffer &buffer, uint32_t field_id,
const T &value) {
buffer.set_pos(pos); buffer.set_pos(pos);
buffer.encode_optional_sub_message(field_id, value); buffer.encode_optional_sub_message(field_id, value);
pos = buffer.get_pos(); return buffer.get_pos();
}
private:
/// Unaligned little endian store of four bytes: byte stores where the outlined helper lives (ESP-IDF, ARM
/// without unaligned access), otherwise a memcpy the compiler folds into one store. Callers bounds check
/// and advance the cursor themselves.
static inline void ESPHOME_ALWAYS_INLINE write_fixed32_le(uint8_t *__restrict__ pos, uint32_t value) {
if constexpr (PROTO_FIXED32_BYTE_STORES) {
// Spelled out so the outlined helper does not itself become a memcpy call
pos[0] = static_cast<uint8_t>(value);
pos[1] = static_cast<uint8_t>(value >> 8);
pos[2] = static_cast<uint8_t>(value >> 16);
pos[3] = static_cast<uint8_t>(value >> 24);
} else {
const uint32_t le = convert_little_endian(value);
__builtin_memcpy(pos, &le, 4);
}
} }
}; };
#undef PROTO_OUTLINE_FOR_SIZE
#undef PROTO_FIXED32_BYTE_STORES
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
/** /**
@@ -624,11 +695,12 @@ class DumpBuffer {
class ProtoMessage { class ProtoMessage {
public: public:
// Non-virtual defaults for messages with no fields. // Non-virtual defaults for messages with no fields; generated classes hide all four. The
// Concrete message classes hide these with their own implementations. // static encode_msg/calc_size_msg take const void * so &T::encode_msg needs no thunk.
// All call sites use templates to preserve the concrete type, so virtual static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
// dispatch is not needed. This eliminates per-message vtable entries for return buffer.get_pos();
// encode/calculate_size, saving ~1.3 KB of flash across all message types. }
static uint32_t calc_size_msg(const void *self) { return 0; }
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); } uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); }
uint32_t calculate_size() const { return 0; } uint32_t calculate_size() const { return 0; }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -663,10 +735,10 @@ class ProtoDecodableMessage : public ProtoMessage {
protected: protected:
~ProtoDecodableMessage() = default; ~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; } /// Store one decoded field; \p scalar is the varint or fixed32 value, or the length of the
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } /// length-delimited payload at \p data. An unknown field or wrong wire type matches no case and is skipped.
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } /// Three register arguments keep the decode loop free of spills.
// NOTE: decode_64bit removed - wire type 1 not supported virtual void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {}
}; };
class ProtoSize { class ProtoSize {
@@ -792,7 +864,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the field ID and wire type * @return The number of bytes needed to encode the field ID and wire type
*/ */
static constexpr uint32_t field(uint32_t field_id, uint32_t type) { static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK); uint32_t tag = proto_tag(field_id, type & WIRE_TYPE_MASK);
return varint(tag); return varint(tag);
} }
@@ -876,24 +948,14 @@ class ProtoSize {
// Implementation of methods that depend on ProtoSize being fully defined // Implementation of methods that depend on ProtoSize being fully defined
// Encode thunk — converts void* back to concrete type for direct encode() call
template<typename T> uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
return static_cast<const T *>(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG);
}
// Thin template wrapper; delegates to non-template core in proto.cpp. // Thin template wrapper; delegates to non-template core in proto.cpp.
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) { template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>); this->encode_sub_message(field_id, &value, &T::encode_msg);
} }
// Thin template wrapper; delegates to non-template core. // Thin template wrapper; delegates to non-template core.
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) { template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>); this->encode_optional_sub_message(field_id, T::calc_size_msg(&value), &value, &T::encode_msg);
}
// Template decode_to_message - preserves concrete type so decode() resolves statically
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
msg.decode(this->value_, this->length_);
} }
template<typename T> const char *proto_enum_to_string(T value); template<typename T> const char *proto_enum_to_string(T value);
+156 -208
View File
@@ -9,10 +9,6 @@ namespace esphome::atm90e32 {
static const char *const TAG = "atm90e32"; static const char *const TAG = "atm90e32";
static const LogString *offset_calibration_name(bool power_offsets) {
return power_offsets ? LOG_STR("Power offset") : LOG_STR("Offset");
}
static uint32_t pref_hash(const char *prefix, const char *name_space) { static uint32_t pref_hash(const char *prefix, const char *name_space) {
auto hash = fnv1_hash(prefix); auto hash = fnv1_hash(prefix);
return fnv1_hash_extend(hash, name_space); return fnv1_hash_extend(hash, name_space);
@@ -207,12 +203,13 @@ void ATM90E32Component::setup() {
// Initialize flash storage for power offset calibrations // Initialize flash storage for power offset calibrations
uint32_t po_hash = pref_hash("_power_offset_calibration_", cs); uint32_t po_hash = pref_hash("_power_offset_calibration_", cs);
this->power_offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(po_hash, true); this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
bool migrated_power_offset = false; bool migrated_power_offset = false;
if (has_distinct_legacy_namespace) { if (has_distinct_legacy_namespace) {
uint32_t legacy_po_hash = pref_hash("_power_offset_calibration_", legacy_cs); uint32_t legacy_po_hash = pref_hash("_power_offset_calibration_", legacy_cs);
auto legacy_power_offset_pref = global_preferences->make_preference<OffsetCalibration[3]>(legacy_po_hash, true); auto legacy_power_offset_pref =
OffsetCalibration power_offset_data[3]{}; global_preferences->make_preference<PowerOffsetCalibration[3]>(legacy_po_hash, true);
PowerOffsetCalibration power_offset_data[3]{};
int migration_status = int migration_status =
migrate_legacy_pref_if_needed(this->power_offset_pref_, legacy_power_offset_pref, &power_offset_data); migrate_legacy_pref_if_needed(this->power_offset_pref_, legacy_power_offset_pref, &power_offset_data);
migrated_power_offset = migration_status > 0; migrated_power_offset = migration_status > 0;
@@ -227,20 +224,20 @@ void ATM90E32Component::setup() {
global_preferences->sync(); global_preferences->sync();
} }
this->restore_offset_calibrations_(OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); this->restore_offset_calibrations_();
this->restore_offset_calibrations_(OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); this->restore_power_offset_calibrations_();
} else { } else {
ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
cs); cs);
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(this->voltage_offset_registers[phase], this->write16_(this->voltage_offset_registers[phase],
static_cast<uint16_t>(this->offset_phase_[phase].first_offset)); static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
this->write16_(this->current_offset_registers[phase], this->write16_(this->current_offset_registers[phase],
static_cast<uint16_t>(this->offset_phase_[phase].second_offset)); static_cast<uint16_t>(this->offset_phase_[phase].current_offset_));
this->write16_(this->power_offset_registers[phase], this->write16_(this->power_offset_registers[phase],
static_cast<uint16_t>(this->power_offset_phase_[phase].first_offset)); static_cast<uint16_t>(this->power_offset_phase_[phase].active_power_offset));
this->write16_(this->reactive_power_offset_registers[phase], this->write16_(this->reactive_power_offset_registers[phase],
static_cast<uint16_t>(this->power_offset_phase_[phase].second_offset)); static_cast<uint16_t>(this->power_offset_phase_[phase].reactive_power_offset));
} }
} }
@@ -320,8 +317,8 @@ void ATM90E32Component::log_calibration_status_() {
cs); cs);
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase,
this->config_offset_phase_[phase].first_offset, this->offset_phase_[phase].first_offset, this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_,
this->config_offset_phase_[phase].second_offset, this->offset_phase_[phase].second_offset); this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_);
} }
ESP_LOGW(TAG, ESP_LOGW(TAG,
"[CALIBRATION][%s] ===============================================================================", cs); "[CALIBRATION][%s] ===============================================================================", cs);
@@ -338,8 +335,10 @@ void ATM90E32Component::log_calibration_status_() {
cs); cs);
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase,
this->config_power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].first_offset, this->config_power_offset_phase_[phase].active_power_offset,
this->config_power_offset_phase_[phase].second_offset, this->power_offset_phase_[phase].second_offset); this->power_offset_phase_[phase].active_power_offset,
this->config_power_offset_phase_[phase].reactive_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
} }
ESP_LOGW(TAG, ESP_LOGW(TAG,
"[CALIBRATION][%s] ===============================================================================", cs); "[CALIBRATION][%s] ===============================================================================", cs);
@@ -373,7 +372,7 @@ void ATM90E32Component::log_calibration_status_() {
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->offset_phase_[phase].first_offset, this->offset_phase_[phase].second_offset); this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs);
} }
@@ -386,7 +385,8 @@ void ATM90E32Component::log_calibration_status_() {
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].second_offset); this->power_offset_phase_[phase].active_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
} }
@@ -756,68 +756,36 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
} }
} }
void ATM90E32Component::finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored, void ATM90E32Component::save_offset_calibration_to_memory_() {
bool previous_using_saved, OffsetCalibrationType type) {
const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER;
const char *cs = this->get_calibration_id_(); const char *cs = this->get_calibration_id_();
const LogString *name = offset_calibration_name(power_offsets); bool success = this->offset_pref_.save(&this->offset_phase_);
OffsetCalibration(*offsets)[3] = power_offsets ? &this->power_offset_phase_ : &this->offset_phase_; global_preferences->sync();
ESPPreferenceObject *preference = power_offsets ? &this->power_offset_pref_ : &this->offset_pref_; if (success) {
bool *has_stored =
power_offsets ? &this->has_stored_power_offset_calibration_ : &this->has_stored_offset_calibration_;
bool *restored = power_offsets ? &this->restored_power_offset_calibration_ : &this->restored_offset_calibration_;
bool *mismatches = power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_;
const bool writes_verified = this->verify_offset_writes_(type);
bool saved = false;
bool synced = false;
if (writes_verified) {
saved = preference->save(offsets);
synced = global_preferences->sync();
}
if (writes_verified && saved && synced) {
this->using_saved_calibrations_ = true; this->using_saved_calibrations_ = true;
*has_stored = true; this->restored_offset_calibration_ = true;
*restored = true; for (bool &phase : this->offset_calibration_mismatch_)
for (uint8_t phase = 0; phase < 3; phase++) phase = false;
mismatches[phase] = false; ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration saved to memory. %s calibration completed and verified.", cs, } else {
LOG_STR_ARG(name), LOG_STR_ARG(name)); this->using_saved_calibrations_ = false;
return; ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs);
} }
}
if (writes_verified) { void ATM90E32Component::save_power_offset_calibration_to_memory_() {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save %s calibration to memory!", cs, LOG_STR_ARG(name)); const char *cs = this->get_calibration_id_();
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
global_preferences->sync();
if (success) {
this->using_saved_calibrations_ = true;
this->restored_power_offset_calibration_ = true;
for (bool &phase : this->power_offset_calibration_mismatch_)
phase = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs);
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs);
} }
for (uint8_t phase = 0; phase < 3; phase++) {
this->write_offsets_to_registers_(phase, previous[phase].first_offset, previous[phase].second_offset, type);
}
const bool rollback_verified = this->verify_offset_writes_(type);
bool rollback_persisted = false;
if (writes_verified) {
OffsetCalibration rollback[3]{};
prepare_offset_rollback(previous, previous_restored, rollback);
const bool rollback_saved = preference->save(&rollback);
const bool rollback_synced = global_preferences->sync();
rollback_persisted = rollback_saved && rollback_synced;
if (!rollback_saved || !rollback_synced) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to persist restored %s calibration values!", cs, LOG_STR_ARG(name));
}
}
*restored = previous_restored;
if (rollback_persisted)
*has_stored = previous_restored;
this->using_saved_calibrations_ = previous_using_saved;
if (!rollback_verified) {
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; rollback readback verification failed.", cs,
LOG_STR_ARG(name));
return;
}
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; previous values restored.", cs, LOG_STR_ARG(name));
} }
void ATM90E32Component::run_offset_calibrations() { void ATM90E32Component::run_offset_calibrations() {
@@ -835,16 +803,11 @@ void ATM90E32Component::run_offset_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
OffsetCalibration previous_offsets[3] = {this->offset_phase_[0], this->offset_phase_[1], this->offset_phase_[2]};
const bool previous_restored = this->restored_offset_calibration_;
const bool previous_using_saved = this->using_saved_calibrations_;
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
int16_t voltage_offset = calibrate_offset(phase, true); int16_t voltage_offset = calibrate_offset(phase, true);
int16_t current_offset = calibrate_offset(phase, false); int16_t current_offset = calibrate_offset(phase, false);
this->write_offsets_to_registers_(phase, voltage_offset, current_offset, this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT);
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset,
current_offset); current_offset);
@@ -852,8 +815,7 @@ void ATM90E32Component::run_offset_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs);
this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved, this->save_offset_calibration_to_memory_();
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT);
} }
void ATM90E32Component::run_power_offset_calibrations() { void ATM90E32Component::run_power_offset_calibrations() {
@@ -872,25 +834,18 @@ void ATM90E32Component::run_power_offset_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
OffsetCalibration previous_offsets[3] = {this->power_offset_phase_[0], this->power_offset_phase_[1],
this->power_offset_phase_[2]};
const bool previous_restored = this->restored_power_offset_calibration_;
const bool previous_using_saved = this->using_saved_calibrations_;
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
int16_t active_offset = calibrate_power_offset(phase, false); int16_t active_offset = calibrate_power_offset(phase, false);
int16_t reactive_offset = calibrate_power_offset(phase, true); int16_t reactive_offset = calibrate_power_offset(phase, true);
this->write_offsets_to_registers_(phase, active_offset, reactive_offset, this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER);
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset,
reactive_offset); reactive_offset);
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved, this->save_power_offset_calibration_to_memory_();
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER);
} }
void ATM90E32Component::write_gains_to_registers_() { void ATM90E32Component::write_gains_to_registers_() {
@@ -904,26 +859,35 @@ void ATM90E32Component::write_gains_to_registers_() {
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
} }
void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset, void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) {
OffsetCalibrationType type) { // Save to runtime
const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; this->offset_phase_[phase].voltage_offset_ = voltage_offset;
OffsetCalibration &offsets = power_offsets ? this->power_offset_phase_[phase] : this->offset_phase_[phase]; this->phase_[phase].voltage_offset_ = voltage_offset;
offsets.first_offset = first_offset;
offsets.second_offset = second_offset;
if (power_offsets) {
this->phase_[phase].active_power_offset_ = first_offset;
this->phase_[phase].reactive_power_offset_ = second_offset;
} else {
this->phase_[phase].voltage_offset_ = first_offset;
this->phase_[phase].current_offset_ = second_offset;
}
const uint16_t *first_registers = power_offsets ? this->power_offset_registers : this->voltage_offset_registers; // Save to flash-storable struct
const uint16_t *second_registers = this->offset_phase_[phase].current_offset_ = current_offset;
power_offsets ? this->reactive_power_offset_registers : this->current_offset_registers; this->phase_[phase].current_offset_ = current_offset;
// Write to registers
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
this->write16_(first_registers[phase], static_cast<uint16_t>(first_offset)); this->write16_(voltage_offset_registers[phase], static_cast<uint16_t>(voltage_offset));
this->write16_(second_registers[phase], static_cast<uint16_t>(second_offset)); this->write16_(current_offset_registers[phase], static_cast<uint16_t>(current_offset));
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) {
// Save to runtime
this->phase_[phase].active_power_offset_ = p_offset;
this->phase_[phase].reactive_power_offset_ = q_offset;
// Save to flash-storable struct
this->power_offset_phase_[phase].active_power_offset = p_offset;
this->power_offset_phase_[phase].reactive_power_offset = q_offset;
// Write to registers
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
this->write16_(this->power_offset_registers[phase], static_cast<uint16_t>(p_offset));
this->write16_(this->reactive_power_offset_registers[phase], static_cast<uint16_t>(q_offset));
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
} }
@@ -983,78 +947,89 @@ void ATM90E32Component::restore_gain_calibrations_() {
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs); ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs);
} }
void ATM90E32Component::restore_offset_calibrations_(OffsetCalibrationType type) { void ATM90E32Component::restore_offset_calibrations_() {
const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER;
const char *cs = this->get_calibration_id_(); const char *cs = this->get_calibration_id_();
const LogString *name = power_offsets ? LOG_STR("power offset") : LOG_STR("offset");
OffsetCalibration(*offsets)[3] = power_offsets ? &this->power_offset_phase_ : &this->offset_phase_;
OffsetCalibration(*config_offsets)[3] =
power_offsets ? &this->config_power_offset_phase_ : &this->config_offset_phase_;
ESPPreferenceObject *preference = power_offsets ? &this->power_offset_pref_ : &this->offset_pref_;
bool *has_stored =
power_offsets ? &this->has_stored_power_offset_calibration_ : &this->has_stored_offset_calibration_;
bool *restored = power_offsets ? &this->restored_power_offset_calibration_ : &this->restored_offset_calibration_;
bool *mismatches = power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_;
const bool *has_first = power_offsets ? this->has_config_active_power_offset_ : this->has_config_voltage_offset_;
const bool *has_second = power_offsets ? this->has_config_reactive_power_offset_ : this->has_config_current_offset_;
for (uint8_t i = 0; i < 3; ++i) for (uint8_t i = 0; i < 3; ++i)
(*config_offsets)[i] = (*offsets)[i]; this->config_offset_phase_[i] = this->offset_phase_[i];
bool have_data = this->offset_pref_.load(&this->offset_phase_);
const bool have_data = preference->load(offsets);
bool all_zero = true; bool all_zero = true;
if (have_data) { if (have_data) {
for (const auto &phase : *offsets) { for (auto &phase : this->offset_phase_) {
if (phase.first_offset != 0 || phase.second_offset != 0) { if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) {
all_zero = false; all_zero = false;
break; break;
} }
} }
} }
*has_stored = have_data && !all_zero; if (have_data && !all_zero) {
*restored = false; this->restored_offset_calibration_ = true;
for (uint8_t phase = 0; phase < 3; phase++) {
auto &offset = this->offset_phase_[phase];
bool mismatch = false;
if (this->has_config_voltage_offset_[phase] &&
offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_)
mismatch = true;
if (this->has_config_current_offset_[phase] &&
offset.current_offset_ != this->config_offset_phase_[phase].current_offset_)
mismatch = true;
if (mismatch)
this->offset_calibration_mismatch_[phase] = true;
}
} else {
for (uint8_t phase = 0; phase < 3; phase++)
this->offset_phase_[phase] = this->config_offset_phase_[phase];
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs);
}
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
mismatches[phase] = false; write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_,
if (*has_stored) { this->offset_phase_[phase].current_offset_);
mismatches[phase] = }
(has_first[phase] && (*offsets)[phase].first_offset != (*config_offsets)[phase].first_offset) || }
(has_second[phase] && (*offsets)[phase].second_offset != (*config_offsets)[phase].second_offset);
void ATM90E32Component::restore_power_offset_calibrations_() {
const char *cs = this->get_calibration_id_();
for (uint8_t i = 0; i < 3; ++i)
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_);
bool all_zero = true;
if (have_data) {
for (auto &phase : this->power_offset_phase_) {
if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) {
all_zero = false;
break;
}
} }
} }
if (!*has_stored) { if (have_data && !all_zero) {
for (uint8_t phase = 0; phase < 3; phase++) this->restored_power_offset_calibration_ = true;
(*offsets)[phase] = (*config_offsets)[phase]; for (uint8_t phase = 0; phase < 3; ++phase) {
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored %s calibrations found. Using default values.", cs, LOG_STR_ARG(name)); auto &offset = this->power_offset_phase_[phase];
} bool mismatch = false;
if (this->has_config_active_power_offset_[phase] &&
for (uint8_t phase = 0; phase < 3; phase++) { offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset)
this->write_offsets_to_registers_(phase, (*offsets)[phase].first_offset, (*offsets)[phase].second_offset, type); mismatch = true;
} if (this->has_config_reactive_power_offset_[phase] &&
const bool initial_values_verified = this->verify_offset_writes_(type); offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset)
if (initial_values_verified) { mismatch = true;
const auto state = resolve_offset_restore_state(*has_stored, true, false); if (mismatch)
*restored = state.restored; this->power_offset_calibration_mismatch_[phase] = true;
ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration values verified.", cs, LOG_STR_ARG(name)); }
return;
}
this->using_saved_calibrations_ = false;
for (uint8_t phase = 0; phase < 3; phase++)
mismatches[phase] = false;
for (uint8_t phase = 0; phase < 3; phase++) {
(*offsets)[phase] = (*config_offsets)[phase];
this->write_offsets_to_registers_(phase, (*offsets)[phase].first_offset, (*offsets)[phase].second_offset, type);
}
const auto state = resolve_offset_restore_state(*has_stored, false, this->verify_offset_writes_(type));
*restored = state.restored;
if (state.values_verified) {
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore failed verification; config values verified.", cs,
LOG_STR_ARG(name));
} else { } else {
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore and config fallback both failed verification.", cs, for (uint8_t phase = 0; phase < 3; ++phase)
LOG_STR_ARG(name)); this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase];
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs);
}
for (uint8_t phase = 0; phase < 3; ++phase) {
write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
} }
} }
@@ -1109,14 +1084,14 @@ void ATM90E32Component::clear_gain_calibrations() {
void ATM90E32Component::clear_offset_calibrations() { void ATM90E32Component::clear_offset_calibrations() {
const char *cs = this->get_calibration_id_(); const char *cs = this->get_calibration_id_();
if (!this->has_stored_offset_calibration_) { if (!this->restored_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->offset_phase_[phase].first_offset, this->offset_phase_[phase].second_offset); this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
return; return;
@@ -1129,11 +1104,10 @@ void ATM90E32Component::clear_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
int16_t voltage_offset = int16_t voltage_offset =
this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].first_offset : 0; this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0;
int16_t current_offset = int16_t current_offset =
this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].second_offset : 0; this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0;
this->write_offsets_to_registers_(phase, voltage_offset, current_offset, this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT);
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset,
current_offset); current_offset);
} }
@@ -1143,7 +1117,6 @@ void ATM90E32Component::clear_offset_calibrations() {
this->offset_pref_.save(&zero_offsets); // Clear stored values in flash this->offset_pref_.save(&zero_offsets); // Clear stored values in flash
global_preferences->sync(); global_preferences->sync();
this->has_stored_offset_calibration_ = false;
this->restored_offset_calibration_ = false; this->restored_offset_calibration_ = false;
for (bool &phase : this->offset_calibration_mismatch_) for (bool &phase : this->offset_calibration_mismatch_)
phase = false; phase = false;
@@ -1153,14 +1126,15 @@ void ATM90E32Component::clear_offset_calibrations() {
void ATM90E32Component::clear_power_offset_calibrations() { void ATM90E32Component::clear_power_offset_calibrations() {
const char *cs = this->get_calibration_id_(); const char *cs = this->get_calibration_id_();
if (!this->has_stored_power_offset_calibration_) { if (!this->restored_power_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].second_offset); this->power_offset_phase_[phase].active_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
return; return;
@@ -1173,21 +1147,20 @@ void ATM90E32Component::clear_power_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
int16_t active_offset = int16_t active_offset =
this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].first_offset : 0; this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0;
int16_t reactive_offset = int16_t reactive_offset = this->has_config_reactive_power_offset_[phase]
this->has_config_reactive_power_offset_[phase] ? this->config_power_offset_phase_[phase].second_offset : 0; ? this->config_power_offset_phase_[phase].reactive_power_offset
this->write_offsets_to_registers_(phase, active_offset, reactive_offset, : 0;
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset,
reactive_offset); reactive_offset);
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
OffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
this->power_offset_pref_.save(&zero_power_offsets); this->power_offset_pref_.save(&zero_power_offsets);
global_preferences->sync(); global_preferences->sync();
this->has_stored_power_offset_calibration_ = false;
this->restored_power_offset_calibration_ = false; this->restored_power_offset_calibration_ = false;
for (bool &phase : this->power_offset_calibration_mismatch_) for (bool &phase : this->power_offset_calibration_mismatch_)
phase = false; phase = false;
@@ -1242,31 +1215,6 @@ bool ATM90E32Component::verify_gain_writes_() {
return success; // Return true if all writes were successful, false otherwise return success; // Return true if all writes were successful, false otherwise
} }
bool ATM90E32Component::verify_offset_writes_(OffsetCalibrationType type) {
const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER;
const char *cs = this->get_calibration_id_();
const LogString *name = offset_calibration_name(power_offsets);
const LogString *first_name = power_offsets ? LOG_STR("active") : LOG_STR("voltage");
const LogString *second_name = power_offsets ? LOG_STR("reactive") : LOG_STR("current");
const OffsetCalibration *offsets = power_offsets ? this->power_offset_phase_ : this->offset_phase_;
const uint16_t *first_registers = power_offsets ? this->power_offset_registers : this->voltage_offset_registers;
const uint16_t *second_registers =
power_offsets ? this->reactive_power_offset_registers : this->current_offset_registers;
bool success = true;
for (uint8_t phase = 0; phase < 3; phase++) {
const uint16_t first = this->read16_(first_registers[phase]);
const uint16_t second = this->read16_(second_registers[phase]);
if (!offset_register_value_matches(first, offsets[phase].first_offset) ||
!offset_register_value_matches(second, offsets[phase].second_offset)) {
ESP_LOGE(TAG, "[CALIBRATION][%s] %s readback failed for Phase %s: %s %d/%d, %s %d/%d.", cs, LOG_STR_ARG(name),
phase_labels[phase], LOG_STR_ARG(first_name), static_cast<int16_t>(first), offsets[phase].first_offset,
LOG_STR_ARG(second_name), static_cast<int16_t>(second), offsets[phase].second_offset);
success = false;
}
}
return success;
}
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void ATM90E32Component::check_phase_status() { void ATM90E32Component::check_phase_status() {
uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0); uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0);
+22 -49
View File
@@ -13,40 +13,6 @@
namespace esphome::atm90e32 { namespace esphome::atm90e32 {
inline bool offset_register_value_matches(uint16_t actual, int16_t expected) {
return actual == static_cast<uint16_t>(expected);
}
struct OffsetCalibration {
int16_t first_offset{0};
int16_t second_offset{0};
};
static_assert(sizeof(OffsetCalibration[3]) == 12, "Offset calibration preference layout must remain compatible");
enum class OffsetCalibrationType : uint8_t {
OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT,
OFFSET_CALIBRATION_TYPE_POWER,
};
struct OffsetRestoreState {
bool restored;
bool values_verified;
};
inline OffsetRestoreState resolve_offset_restore_state(bool has_stored_values, bool initial_values_verified,
bool fallback_values_verified) {
if (initial_values_verified)
return {has_stored_values, true};
return {false, fallback_values_verified};
}
inline void prepare_offset_rollback(const OffsetCalibration (&previous)[3], bool had_stored_values,
OffsetCalibration (&rollback)[3]) {
for (uint8_t phase = 0; phase < 3; phase++)
rollback[phase] = had_stored_values ? previous[phase] : OffsetCalibration{};
}
class ATM90E32Component final : public PollingComponent, class ATM90E32Component final : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> { spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
@@ -105,19 +71,19 @@ class ATM90E32Component final : public PollingComponent,
this->has_config_current_gain_[phase] = true; this->has_config_current_gain_[phase] = true;
} }
void set_voltage_offset(uint8_t phase, int16_t offset) { void set_voltage_offset(uint8_t phase, int16_t offset) {
this->offset_phase_[phase].first_offset = offset; this->offset_phase_[phase].voltage_offset_ = offset;
this->has_config_voltage_offset_[phase] = true; this->has_config_voltage_offset_[phase] = true;
} }
void set_current_offset(uint8_t phase, int16_t offset) { void set_current_offset(uint8_t phase, int16_t offset) {
this->offset_phase_[phase].second_offset = offset; this->offset_phase_[phase].current_offset_ = offset;
this->has_config_current_offset_[phase] = true; this->has_config_current_offset_[phase] = true;
} }
void set_active_power_offset(uint8_t phase, int16_t offset) { void set_active_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].first_offset = offset; this->power_offset_phase_[phase].active_power_offset = offset;
this->has_config_active_power_offset_[phase] = true; this->has_config_active_power_offset_[phase] = true;
} }
void set_reactive_power_offset(uint8_t phase, int16_t offset) { void set_reactive_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].second_offset = offset; this->power_offset_phase_[phase].reactive_power_offset = offset;
this->has_config_reactive_power_offset_[phase] = true; this->has_config_reactive_power_offset_[phase] = true;
} }
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
@@ -205,16 +171,16 @@ class ATM90E32Component final : public PollingComponent,
float get_chip_temperature_(); float get_chip_temperature_();
bool get_publish_interval_flag_() { return publish_interval_flag_; }; bool get_publish_interval_flag_() { return publish_interval_flag_; };
void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
void restore_offset_calibrations_(OffsetCalibrationType type); void restore_offset_calibrations_();
void restore_power_offset_calibrations_();
void restore_gain_calibrations_(); void restore_gain_calibrations_();
void save_offset_calibration_to_memory_();
void save_gain_calibration_to_memory_(); void save_gain_calibration_to_memory_();
void finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored, void save_power_offset_calibration_to_memory_();
bool previous_using_saved, OffsetCalibrationType type); void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
void write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset, void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
OffsetCalibrationType type);
void write_gains_to_registers_(); void write_gains_to_registers_();
bool verify_gain_writes_(); bool verify_gain_writes_();
bool verify_offset_writes_(OffsetCalibrationType type);
bool validate_spi_read_(uint16_t expected, const char *context = nullptr); bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
void log_calibration_status_(); void log_calibration_status_();
const char *get_calibration_id_(); const char *get_calibration_id_();
@@ -253,10 +219,19 @@ class ATM90E32Component final : public PollingComponent,
uint32_t cumulative_reverse_active_energy_{0}; uint32_t cumulative_reverse_active_energy_{0};
} phase_[3]; } phase_[3];
OffsetCalibration offset_phase_[3]; struct OffsetCalibration {
int16_t voltage_offset_{0};
int16_t current_offset_{0};
} offset_phase_[3];
OffsetCalibration config_offset_phase_[3]; OffsetCalibration config_offset_phase_[3];
OffsetCalibration power_offset_phase_[3];
OffsetCalibration config_power_offset_phase_[3]; struct PowerOffsetCalibration {
int16_t active_power_offset{0};
int16_t reactive_power_offset{0};
} power_offset_phase_[3];
PowerOffsetCalibration config_power_offset_phase_[3];
struct GainCalibration { struct GainCalibration {
uint16_t voltage_gain{1}; uint16_t voltage_gain{1};
@@ -290,8 +265,6 @@ class ATM90E32Component final : public PollingComponent,
bool enable_offset_calibration_{false}; bool enable_offset_calibration_{false};
bool enable_gain_calibration_{false}; bool enable_gain_calibration_{false};
const char *instance_id_{nullptr}; const char *instance_id_{nullptr};
bool has_stored_offset_calibration_{false};
bool has_stored_power_offset_calibration_{false};
bool restored_offset_calibration_{false}; bool restored_offset_calibration_{false};
bool restored_power_offset_calibration_{false}; bool restored_power_offset_calibration_{false};
bool restored_gain_calibration_{false}; bool restored_gain_calibration_{false};
+3 -4
View File
@@ -313,10 +313,9 @@ FileDecoderState AudioDecoder::decode_mp3_() {
this->output_transfer_buffer_->increase_buffer_length( this->output_transfer_buffer_->increase_buffer_length(
this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); this->audio_stream_info_.value().frames_to_bytes(samples_decoded));
} }
} else if (result == micro_mp3::MP3_STREAM_INFO_READY || result == micro_mp3::MP3_STREAM_INFO_CHANGED) { } else if (result == micro_mp3::MP3_STREAM_INFO_READY) {
// Header parsed: capture stream info and resize the output buffer to fit one full frame. // First successful header parse: capture stream info and resize the output buffer to fit one full frame.
// microMP3 always outputs 16-bit PCM. MP3_STREAM_INFO_CHANGED is handled identically: despite its // microMP3 always outputs 16-bit PCM.
// negative value it is documented as recoverable, so it must not reach the catch-all below.
this->audio_stream_info_ = this->audio_stream_info_ =
audio::AudioStreamInfo(16, this->mp3_decoder_->get_channels(), this->mp3_decoder_->get_sample_rate()); audio::AudioStreamInfo(16, this->mp3_decoder_->get_channels(), this->mp3_decoder_->get_sample_rate());
this->free_buffer_required_ = this->free_buffer_required_ =
@@ -58,9 +58,6 @@ esp_err_t AudioReader::add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &ou
if (current_audio_file_ != nullptr) { if (current_audio_file_ != nullptr) {
// A transfer buffer isn't ncessary for a local file // A transfer buffer isn't ncessary for a local file
this->file_ring_buffer_ = output_ring_buffer.lock(); this->file_ring_buffer_ = output_ring_buffer.lock();
if (this->file_ring_buffer_ == nullptr) {
return ESP_ERR_INVALID_STATE;
}
return ESP_OK; return ESP_OK;
} }
@@ -51,14 +51,14 @@ void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_le
void AudioTransferBuffer::clear_buffered_data() { void AudioTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0; this->buffer_length_ = 0;
if (this->ring_buffer_ != nullptr) { if (this->ring_buffer_.use_count() > 0) {
this->ring_buffer_->reset(); this->ring_buffer_->reset();
} }
} }
void AudioSinkTransferBuffer::clear_buffered_data() { void AudioSinkTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0; this->buffer_length_ = 0;
if (this->ring_buffer_ != nullptr) { if (this->ring_buffer_.use_count() > 0) {
this->ring_buffer_->reset(); this->ring_buffer_->reset();
} }
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
@@ -69,7 +69,7 @@ void AudioSinkTransferBuffer::clear_buffered_data() {
} }
bool AudioTransferBuffer::has_buffered_data() const { bool AudioTransferBuffer::has_buffered_data() const {
if (this->ring_buffer_ != nullptr) { if (this->ring_buffer_.use_count() > 0) {
return ((this->ring_buffer_->available() > 0) || (this->available() > 0)); return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
} }
return (this->available() > 0); return (this->available() > 0);
@@ -144,7 +144,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
size_t bytes_to_read = AudioTransferBuffer::free(); size_t bytes_to_read = AudioTransferBuffer::free();
size_t bytes_read = 0; size_t bytes_read = 0;
if (bytes_to_read > 0) { if (bytes_to_read > 0) {
if (this->ring_buffer_ != nullptr) { if (this->ring_buffer_.use_count() > 0) {
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait); bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
} }
@@ -161,7 +161,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait); bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
} else } else
#endif #endif
if (this->ring_buffer_ != nullptr) { if (this->ring_buffer_.use_count() > 0) {
bytes_written = bytes_written =
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
} else if (this->sink_callback_ != nullptr) { } else if (this->sink_callback_ != nullptr) {
@@ -186,7 +186,7 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
return (this->speaker_->has_buffered_data() || (this->available() > 0)); return (this->speaker_->has_buffered_data() || (this->available() > 0));
} }
#endif #endif
if (this->ring_buffer_ != nullptr) { if (this->ring_buffer_.use_count() > 0) {
return ((this->ring_buffer_->available() > 0) || (this->available() > 0)); return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
} }
return (this->available() > 0); return (this->available() > 0);
+10 -24
View File
@@ -22,23 +22,6 @@ class Automation {
static const char *const TAG; static const char *const TAG;
}; };
// Base for nodes that never read the parent's services.
// The parent releases its services only once every node reports Established, so a node that never
// reports it keeps that memory allocated for the life of the connection.
class BLEClientServicelessNode : public BLEClientNode {
public:
// Final so that Established is always reported on SEARCH_CMPL, before the derived node sees the event.
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) final {
if (event == ESP_GATTC_SEARCH_CMPL_EVT)
this->node_state = espbt::ClientState::ESTABLISHED;
this->on_gattc_event(event, gattc_if, param);
}
protected:
// Derived nodes handle GATT events here rather than by overriding the handler above.
virtual void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {}
};
// implement on_connect automation. // implement on_connect automation.
class BLEClientConnectTrigger final : public Trigger<>, public BLEClientNode { class BLEClientConnectTrigger final : public Trigger<>, public BLEClientNode {
public: public:
@@ -78,7 +61,7 @@ class BLEClientDisconnectTrigger final : public Trigger<>, public BLEClientNode
} }
}; };
class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientServicelessNode { class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientNode {
public: public:
explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {} void loop() override {}
@@ -88,7 +71,7 @@ class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientS
} }
}; };
class BLEClientPasskeyNotificationTrigger final : public Trigger<uint32_t>, public BLEClientServicelessNode { class BLEClientPasskeyNotificationTrigger final : public Trigger<uint32_t>, public BLEClientNode {
public: public:
explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {} void loop() override {}
@@ -99,7 +82,7 @@ class BLEClientPasskeyNotificationTrigger final : public Trigger<uint32_t>, publ
} }
}; };
class BLEClientNumericComparisonRequestTrigger final : public Trigger<uint32_t>, public BLEClientServicelessNode { class BLEClientNumericComparisonRequestTrigger final : public Trigger<uint32_t>, public BLEClientNode {
public: public:
explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {} void loop() override {}
@@ -332,17 +315,19 @@ template<typename... Ts> class BLEClientRemoveBondAction final : public Action<T
BLEClient *parent_{nullptr}; BLEClient *parent_{nullptr};
}; };
template<typename... Ts> class BLEClientConnectAction final : public Action<Ts...>, public BLEClientServicelessNode { template<typename... Ts> class BLEClientConnectAction final : public Action<Ts...>, public BLEClientNode {
public: public:
BLEClientConnectAction(BLEClient *ble_client) { BLEClientConnectAction(BLEClient *ble_client) {
ble_client->register_ble_node(this); ble_client->register_ble_node(this);
ble_client_ = ble_client; ble_client_ = ble_client;
} }
void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
if (this->num_running_ == 0) if (this->num_running_ == 0)
return; return;
switch (event) { switch (event) {
case ESP_GATTC_SEARCH_CMPL_EVT: case ESP_GATTC_SEARCH_CMPL_EVT:
this->node_state = espbt::ClientState::ESTABLISHED;
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
break; break;
// if the connection is closed, terminate the automation chain. // if the connection is closed, terminate the automation chain.
@@ -379,13 +364,14 @@ template<typename... Ts> class BLEClientConnectAction final : public Action<Ts..
std::tuple<Ts...> var_{}; std::tuple<Ts...> var_{};
}; };
template<typename... Ts> class BLEClientDisconnectAction final : public Action<Ts...>, public BLEClientServicelessNode { template<typename... Ts> class BLEClientDisconnectAction final : public Action<Ts...>, public BLEClientNode {
public: public:
BLEClientDisconnectAction(BLEClient *ble_client) { BLEClientDisconnectAction(BLEClient *ble_client) {
ble_client->register_ble_node(this); ble_client->register_ble_node(this);
ble_client_ = ble_client; ble_client_ = ble_client;
} }
void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
if (this->num_running_ == 0) if (this->num_running_ == 0)
return; return;
switch (event) { switch (event) {
@@ -6,7 +6,6 @@ namespace esphome::dallas_temp {
static const char *const TAG = "dallas.temp.sensor"; static const char *const TAG = "dallas.temp.sensor";
static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; static const uint8_t DALLAS_MODEL_DS18S20 = 0x10;
static const uint8_t DALLAS_MODEL_DS18B20 = 0x28;
static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44; static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44;
static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE; static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE;
static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E; static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E;
@@ -155,14 +154,7 @@ float DallasTemperatureSensor::get_temp_c_() {
default: default:
break; break;
} }
// undocumented test for powerup measurement of 85
// https://github.com/cpetrich/counterfeit_DS18B20#solution-to-the-85-c-problem
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18B20) {
if ((temp == 85 * 16) && (this->scratch_pad_[6] == 0xc)) {
ESP_LOGD(TAG, "dropping reading caused by sensor reset");
return NAN;
}
}
return temp / 16.0f; return temp / 16.0f;
} }
+2 -6
View File
@@ -66,15 +66,11 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
unsigned reason = esp_reset_reason(); unsigned reason = esp_reset_reason();
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) { if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
if (reason == ESP_RST_SW || reason == ESP_RST_WDT) { if (reason == ESP_RST_SW) {
// On some ESP32-S3 configurations (e.g. SPIRAM with fetch-instructions/rodata),
// esp_restart() intermittently produces RTCWDT_RTC_RST (ESP_RST_WDT) instead of
// ESP_RST_SW. Check the stored reboot source for both reset reasons so a software
// reboot that ends up as WDT still reports the correct source.
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str())); fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
char reboot_source[REBOOT_MAX_LEN]{}; char reboot_source[REBOOT_MAX_LEN]{};
if (pref.load(&reboot_source) && reboot_source[0] != '\0') { if (pref.load(&reboot_source)) {
reboot_source[REBOOT_MAX_LEN - 1] = '\0'; reboot_source[REBOOT_MAX_LEN - 1] = '\0';
snprintf(buf, size, "Reboot request from %s", reboot_source); snprintf(buf, size, "Reboot request from %s", reboot_source);
} else { } else {
+5 -1
View File
@@ -23,7 +23,11 @@ from esphome.const import (
) )
from esphome.types import ConfigType from esphome.types import ConfigType
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"] DEPENDENCIES = ["debug"]
+5 -1
View File
@@ -9,7 +9,11 @@ from esphome.const import (
) )
from esphome.types import ConfigType from esphome.types import ConfigType
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"] DEPENDENCIES = ["debug"]
@@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning(); this->status_set_warning();
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep..."); ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
} }
this->defer_sleep_(); this->next_enter_deep_sleep_ = true;
return false; return false;
} }
} }
@@ -17,7 +17,6 @@ void DeepSleepComponent::setup() {
void DeepSleepComponent::schedule_sleep_() { void DeepSleepComponent::schedule_sleep_() {
this->next_enter_deep_sleep_ = false; this->next_enter_deep_sleep_ = false;
this->disable_loop();
const optional<uint32_t> run_duration = get_run_duration_(); const optional<uint32_t> run_duration = get_run_duration_();
if (run_duration.has_value()) { if (run_duration.has_value()) {
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration); ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
@@ -46,7 +45,7 @@ void DeepSleepComponent::loop() {
void DeepSleepComponent::begin_sleep(bool manual) { void DeepSleepComponent::begin_sleep(bool manual) {
if (this->prevent_ && !manual) { if (this->prevent_ && !manual) {
this->defer_sleep_(); this->next_enter_deep_sleep_ = true;
return; return;
} }
@@ -190,11 +190,6 @@ class DeepSleepComponent final : public Component {
void schedule_sleep_(); void schedule_sleep_();
bool should_teardown_(); bool should_teardown_();
void defer_sleep_() {
this->next_enter_deep_sleep_ = true;
this->enable_loop();
}
#ifdef USE_BK72XX #ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const; bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); } bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
@@ -100,7 +100,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning(); this->status_set_warning();
ESP_LOGW(TAG, "Waiting for wakeup pin state change"); ESP_LOGW(TAG, "Waiting for wakeup pin state change");
} }
this->defer_sleep_(); this->next_enter_deep_sleep_ = true;
return false; return false;
} }
return true; return true;
+2 -3
View File
@@ -153,14 +153,13 @@ bool ES7210::configure_mic_gain_() {
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
// Configure mic 3 // Configure mic 3
// 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, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
// Configure mic 4 // Configure mic 4
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
+9 -2
View File
@@ -3,11 +3,18 @@ import esphome.codegen as cg
# Re-exported for the many esp32-side users; defined in esphome.const # 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 # and esphome.espidf so the upload/logs fast path can use them without
# importing this package. # importing this package.
from esphome.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION, KEY_VARIANT # noqa: F401 # pylint: disable=unused-import from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
KEY_ESP32,
KEY_FLASH_SIZE,
KEY_IDF_VERSION,
KEY_VARIANT,
)
# Back compat for external components only; in-tree callers import it # Back compat for external components only; in-tree callers import it
# from esphome.espidf directly. # from esphome.espidf directly.
from esphome.espidf import variant_to_idf_target # noqa: F401 # pylint: disable=unused-import from esphome.espidf import ( # noqa: F401 # pylint: disable=unused-import
variant_to_idf_target,
)
KEY_BOARD = "board" KEY_BOARD = "board"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
@@ -41,10 +41,7 @@ const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
#endif #endif
static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
// Milliseconds for data transfer. Covers the lwIP retransmit run seen in static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits
// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000;
// Single-instance pointer — multi-port configs are rejected in final_validate. // Single-instance pointer — multi-port configs are rejected in final_validate.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
@@ -91,14 +91,7 @@ void I2SAudioSpeakerBase::loop() {
this->speaker_task_handle_ = nullptr; this->speaker_task_handle_ = nullptr;
this->stop_i2s_driver_(); this->stop_i2s_driver_();
// ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
// the top of loop(): the audio source's task can raise a start at any point above, including
// during stop_i2s_driver_(), and nothing would ever re-issue it.
const EventBits_t bits_before_clear = xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
if (bits_before_clear & SpeakerEventGroupBits::COMMAND_START) {
ESP_LOGD(TAG, "Start requested while stopping; keeping the request");
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
}
this->status_clear_error(); this->status_clear_error();
this->on_task_stopped(); this->on_task_stopped();
@@ -118,24 +111,21 @@ void I2SAudioSpeakerBase::loop() {
break; break;
} }
// Still starting up or winding down from a previous run
if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) {
break;
}
if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) { if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) {
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
this->status_momentary_error("driver-failure", 1000); this->status_momentary_error("driver-failure", 1000);
break; break;
} }
xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ == nullptr) { if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
this->status_momentary_error("task-failure", 1000); &this->speaker_task_handle_);
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
this->status_momentary_error("task-failure", 1000);
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
}
} }
break; break;
case speaker::STATE_RUNNING: // Intentional fallthrough case speaker::STATE_RUNNING: // Intentional fallthrough
@@ -221,8 +211,8 @@ size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t
} }
bool I2SAudioSpeakerBase::has_buffered_data() const { bool I2SAudioSpeakerBase::has_buffered_data() const {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); if (this->audio_ring_buffer_.use_count() > 0) {
if (temp_ring_buffer != nullptr) { std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
return temp_ring_buffer->available() > 0; return temp_ring_buffer->available() > 0;
} }
return false; return false;
+2 -16
View File
@@ -3,10 +3,8 @@
#include "esphome/components/esp32/crash_handler.h" #include "esphome/components/esp32/crash_handler.h"
#include <esp_log.h> #include <esp_log.h>
#include <esp_idf_version.h>
#include <driver/uart.h> #include <driver/uart.h>
#include <soc/soc_caps.h>
#ifdef USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG #ifdef USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
#include <driver/usb_serial_jtag.h> #include <driver/usb_serial_jtag.h>
@@ -17,10 +15,8 @@
#include <driver/usb_serial_jtag_vfs.h> #include <driver/usb_serial_jtag_vfs.h>
#endif #endif
#endif #endif
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)) #include "esp_idf_version.h"
#include "esp_sleep.h"
#endif
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include <fcntl.h> #include <fcntl.h>
@@ -80,22 +76,12 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
uart_config.parity = UART_PARITY_DISABLE; uart_config.parity = UART_PARITY_DISABLE;
uart_config.stop_bits = UART_STOP_BITS_1; uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
#if SOC_UART_SUPPORT_XTAL_CLK
uart_config.source_clk = UART_SCLK_XTAL;
#else
uart_config.source_clk = UART_SCLK_DEFAULT; uart_config.source_clk = UART_SCLK_DEFAULT;
#endif
uart_param_config(uart_num, &uart_config); uart_param_config(uart_num, &uart_config);
// The logger only writes to UART, never reads, so use the minimum RX buffer. // The logger only writes to UART, never reads, so use the minimum RX buffer.
// ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes). // 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; 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); 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() { void Logger::pre_setup() {
+1 -2
View File
@@ -15,7 +15,6 @@ from ..defines import (
from ..types import LvCompound, LvType from ..types import LvCompound, LvType
from . import Widget, WidgetType, get_widgets from . import Widget, WidgetType, get_widgets
from .buttonmatrix import CONF_BUTTONMATRIX from .buttonmatrix import CONF_BUTTONMATRIX
from .label import CONF_LABEL
from .textarea import CONF_TEXTAREA, lv_textarea_t from .textarea import CONF_TEXTAREA, lv_textarea_t
CONF_KEYBOARD = "keyboard" CONF_KEYBOARD = "keyboard"
@@ -50,7 +49,7 @@ class KeyboardType(WidgetType):
) )
def get_uses(self): def get_uses(self):
return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX, CONF_LABEL return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX
async def to_code(self, w: Widget, config: dict): async def to_code(self, w: Widget, config: dict):
add_lv_use("KEY_LISTENER") add_lv_use("KEY_LISTENER")
+1 -2
View File
@@ -10,7 +10,6 @@ from ..types import lv_obj_t
from . import Widget, WidgetType from . import Widget, WidgetType
from .canvas import CONF_CANVAS from .canvas import CONF_CANVAS
from .img import CONF_IMAGE from .img import CONF_IMAGE
from .label import CONF_LABEL
CONF_QRCODE = "qrcode" CONF_QRCODE = "qrcode"
CONF_DARK_COLOR = "dark_color" CONF_DARK_COLOR = "dark_color"
@@ -42,7 +41,7 @@ class QrCodeType(WidgetType):
) )
def get_uses(self): def get_uses(self):
return CONF_CANVAS, CONF_IMAGE, CONF_LABEL return CONF_CANVAS, CONF_IMAGE
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
await w.set_property( await w.set_property(
+1 -2
View File
@@ -28,7 +28,6 @@ from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
from .button import button_spec from .button import button_spec
from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec
from .label import CONF_LABEL
from .obj import obj_spec from .obj import obj_spec
CONF_TABVIEW = "tabview" CONF_TABVIEW = "tabview"
@@ -75,7 +74,7 @@ class TabviewType(WidgetType):
) )
def get_uses(self): def get_uses(self):
return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON, CONF_LABEL return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON
async def to_code(self, w: Widget, config: dict): async def to_code(self, w: Widget, config: dict):
await w.set_property( await w.set_property(
@@ -129,7 +129,7 @@ void MicroWakeWord::setup() {
return; return;
} }
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) { if (this->ring_buffer_.use_count() > 1) {
// Producer-only write: never touches consumer state. If the buffer is full, ask the inference task // Producer-only write: never touches consumer state. If the buffer is full, ask the inference task
// to drain it - reset() is a consumer operation and must run on the inference task's thread. // to drain it - reset() is a consumer operation and must run on the inference task's thread.
// Disable partial writes so audio chunks are either fully accepted or rejected and handled below. // Disable partial writes so audio chunks are either fully accepted or rejected and handled below.
@@ -446,9 +446,9 @@ void MicroWakeWord::loop() {
xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_STOPPING); xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_STOPPING);
} }
// Retries on a subsequent loop if the task is still running on the other core if ((event_group_bits & EventGroupBits::TASK_STOPPED)) {
if ((event_group_bits & EventGroupBits::TASK_STOPPED) && this->inference_task_.deallocate()) {
ESP_LOGD(TAG, "Inference task is finished, freeing task resources"); ESP_LOGD(TAG, "Inference task is finished, freeing task resources");
this->inference_task_.deallocate();
xEventGroupClearBits(this->event_group_, ALL_BITS); xEventGroupClearBits(this->event_group_, ALL_BITS);
xQueueReset(this->detection_queue_); xQueueReset(this->detection_queue_);
this->set_state_(State::STOPPED); this->set_state_(State::STOPPED);
@@ -48,7 +48,7 @@ class MicrophoneSource final {
template<typename F> void add_data_callback(F &&data_callback) { template<typename F> void add_data_callback(F &&data_callback) {
this->mic_->add_data_callback([this, data_callback](const std::vector<uint8_t> &data) { this->mic_->add_data_callback([this, data_callback](const std::vector<uint8_t> &data) {
if (this->enabled_ || this->passive_) { if (this->enabled_ || this->passive_) {
if (this->processed_samples_ == nullptr) { if (this->processed_samples_.use_count() == 0) {
// Create vector if its unused // Create vector if its unused
this->processed_samples_ = std::make_shared<std::vector<uint8_t>>(); this->processed_samples_ = std::make_shared<std::vector<uint8_t>>();
} }
@@ -218,7 +218,7 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
} }
size_t bytes_written = 0; size_t bytes_written = 0;
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) { if (temp_ring_buffer.use_count() > 0) {
// Only write to the ring buffer if the reference is valid // Only write to the ring buffer if the reference is valid
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
if (bytes_written > 0) { if (bytes_written > 0) {
@@ -250,14 +250,14 @@ esp_err_t SourceSpeaker::start_() {
// avoids unnecessary single-frame splices. // avoids unnecessary single-frame splices.
const size_t ring_buffer_size = const size_t ring_buffer_size =
(this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame; (this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame;
if (this->audio_source_ == nullptr) { if (this->audio_source_.use_count() == 0) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer == nullptr) { if (!temp_ring_buffer) {
temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
this->ring_buffer_ = temp_ring_buffer; this->ring_buffer_ = temp_ring_buffer;
} }
if (temp_ring_buffer == nullptr) { if (!temp_ring_buffer) {
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
@@ -278,7 +278,7 @@ void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); } void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
bool SourceSpeaker::has_buffered_data() const { bool SourceSpeaker::has_buffered_data() const {
return ((this->audio_source_ != nullptr) && this->audio_source_->has_buffered_data()); return ((this->audio_source_.use_count() > 0) && this->audio_source_->has_buffered_data());
} }
void SourceSpeaker::set_mute_state(bool mute_state) { void SourceSpeaker::set_mute_state(bool mute_state) {
@@ -382,8 +382,8 @@ void MixerSpeaker::loop() {
ESP_LOGV(TAG, "Stopping"); ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING); xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
} }
// Retries on a subsequent loop if the task is still running on the other core if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
if ((event_group_bits & MIXER_TASK_STATE_STOPPED) && this->task_.deallocate()) { this->task_.deallocate();
ESP_LOGD(TAG, "Stopped"); ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
this->all_stopped_since_ms_ = 0; this->all_stopped_since_ms_ = 0;
@@ -496,7 +496,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
if (speaker->is_running() && !speaker->get_pause_state()) { if (speaker->is_running() && !speaker->get_pause_state()) {
// Speaker is running and not paused, so it possibly can provide audio data // Speaker is running and not paused, so it possibly can provide audio data
std::shared_ptr<audio::RingBufferAudioSource> audio_source = speaker->get_audio_source().lock(); std::shared_ptr<audio::RingBufferAudioSource> audio_source = speaker->get_audio_source().lock();
if (audio_source == nullptr) { if (audio_source.use_count() == 0) {
// No audio source allocated, so skip processing this speaker // No audio source allocated, so skip processing this speaker
continue; continue;
} }
+42 -135
View File
@@ -26,129 +26,44 @@ static const uint8_t MLX90614_ID4 = 0x3F;
static const char *const TAG = "mlx90614"; 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() { void MLX90614Component::setup() {
if (std::isnan(this->emissivity_)) { if (!this->write_emissivity_()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return; 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_() { bool MLX90614Component::write_emissivity_() {
// Skip the write when the EEPROM already holds the desired value to save write cycles if (std::isnan(this->emissivity_))
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; return true;
} uint16_t value = (uint16_t) (this->emissivity_ * 65535);
if (!this->write_bytes_(MLX90614_EMISSIVITY, 0)) {
return this->write_register_(MLX90614_EMISSIVITY, desired_emissivity);
}
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;
// 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; return false;
} }
// 2. Wait at least 5ms
delay(10); delay(10);
if (!this->write_bytes_(MLX90614_EMISSIVITY, value)) {
// 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; return false;
} }
delay(10);
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; return true;
} }
i2c::ErrorCode MLX90614Component::read_register_(uint8_t reg, uint16_t &data) { bool MLX90614Component::write_bytes_(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[5];
uint8_t buf[6];
buf[0] = this->address_ << 1; buf[0] = this->address_ << 1;
buf[1] = reg; buf[1] = reg;
buf[2] = (this->address_ << 1) | 0x01; buf[2] = data & 0xFF;
buf[3] = data >> 8;
const auto ec = this->read_register(reg, buf + 3, 3); buf[4] = crc8(buf, 4, 0x00, 0x07, true);
if (ec != i2c::ERROR_OK) { return this->write_bytes(reg, buf + 2, 3);
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() { void MLX90614Component::dump_config() {
ESP_LOGCONFIG(TAG, "MLX90614:"); ESP_LOGCONFIG(TAG, "MLX90614:");
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
if (this->emissivity_write_attempts_ != 0) { if (this->is_failed()) {
ESP_LOGW(TAG, " Emissivity not written yet, will retry"); ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
} }
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Ambient", this->ambient_sensor_); LOG_SENSOR(" ", "Ambient", this->ambient_sensor_);
@@ -156,41 +71,33 @@ void MLX90614Component::dump_config() {
} }
void MLX90614Component::update() { void MLX90614Component::update() {
// Temperature reads run regardless of the emissivity state so a failure still shows up as NAN uint8_t emissivity[3];
this->try_write_emissivity_(); if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) {
this->status_set_warning();
// Publishes NAN on a bus or CRC failure so a stuck reading is visible instead of silently stale return;
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 } // namespace esphome::mlx90614
+1 -6
View File
@@ -18,18 +18,13 @@ class MLX90614Component final : public PollingComponent, public i2c::I2CDevice {
void set_emissivity(float emissivity) { emissivity_ = emissivity; } void set_emissivity(float emissivity) { emissivity_ = emissivity; }
protected: protected:
void try_write_emissivity_();
bool write_emissivity_(); bool write_emissivity_();
bool write_register_(uint8_t reg, uint16_t data); bool write_bytes_(uint8_t reg, uint16_t data);
i2c::ErrorCode read_register_(uint8_t reg, uint16_t &data);
sensor::Sensor *ambient_sensor_{nullptr}; sensor::Sensor *ambient_sensor_{nullptr};
sensor::Sensor *object_sensor_{nullptr}; sensor::Sensor *object_sensor_{nullptr};
float emissivity_{NAN}; 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 } // namespace esphome::mlx90614
-3
View File
@@ -67,9 +67,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE))
color_modes.add(ESPHOME_F("rgbww")); color_modes.add(ESPHOME_F("rgbww"));
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
root[ESPHOME_F("brightness")] = true;
if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) {
root[MQTT_MIN_MIREDS] = traits.get_min_mireds(); root[MQTT_MIN_MIREDS] = traits.get_min_mireds();
+6 -10
View File
@@ -26,34 +26,30 @@ namespace esphome::network {
/// Return whether the node is connected to the network (through wifi, eth, ...) /// Return whether the node is connected to the network (through wifi, eth, ...)
ESPHOME_ALWAYS_INLINE inline bool is_connected() { ESPHOME_ALWAYS_INLINE inline bool is_connected() {
// With a single interface enabled the checks below collapse to `if (x) return true; return false;`, which
// clang-tidy wants folded into one return. Keep the per-interface form so every enabled interface is checked.
// NOLINTBEGIN(readability-simplify-boolean-expr)
#ifdef USE_ETHERNET #ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
return true; return true;
#endif #endif
#ifdef USE_MODEM #ifdef USE_MODEM
if (modem::global_modem_component != nullptr && modem::global_modem_component->is_connected()) if (modem::global_modem_component != nullptr)
return true; return modem::global_modem_component->is_connected();
#endif #endif
#ifdef USE_WIFI #ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) if (wifi::global_wifi_component != nullptr)
return true; return wifi::global_wifi_component->is_connected();
#endif #endif
#ifdef USE_OPENTHREAD #ifdef USE_OPENTHREAD
if (openthread::global_openthread_component != nullptr && openthread::global_openthread_component->is_connected()) if (openthread::global_openthread_component != nullptr)
return true; return openthread::global_openthread_component->is_connected();
#endif #endif
#ifdef USE_HOST #ifdef USE_HOST
return true; // Assume it's connected return true; // Assume it's connected
#endif #endif
return false; return false;
// NOLINTEND(readability-simplify-boolean-expr)
} }
/// Return whether the network is disabled: every configured interface with a /// Return whether the network is disabled: every configured interface with a
+6 -1
View File
@@ -14,7 +14,12 @@ from esphome.const import (
) )
from esphome.core import CORE, TimePeriod from esphome.core import CORE, TimePeriod
from . import FILTER_SOURCE_FILES, Nextion, nextion_ns, nextion_ref # noqa: F401 pylint: disable=unused-import from . import ( # noqa: F401 pylint: disable=unused-import
FILTER_SOURCE_FILES,
Nextion,
nextion_ns,
nextion_ref,
)
from .base_component import ( from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH, CONF_AUTO_WAKE_ON_TOUCH,
CONF_COMMAND_SPACING, CONF_COMMAND_SPACING,
+2 -2
View File
@@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType:
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
cg.add_define("USE_NOISE") cg.add_define("USE_NOISE")
cg.add_library("esphome/noise-c", "0.1.26") cg.add_library("esphome/noise-c", "0.1.24")
# noise-c depends on libsodium, but declaring it here too lets the # noise-c depends on libsodium, but declaring it here too lets the
# library manager see the full set up front instead of discovering # library manager see the full set up front instead of discovering
# libsodium only after noise-c has downloaded, so the two can download # libsodium only after noise-c has downloaded, so the two can download
# in parallel. The version must match noise-c's library.json. # in parallel. The version must match noise-c's library.json.
cg.add_library("esphome/libsodium", "1.10021.8") cg.add_library("esphome/libsodium", "1.10021.6")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1") cg.add_build_flag("-DHAVE_INLINE_ASM=1")
@@ -153,8 +153,8 @@ void ResamplerSpeaker::loop() {
ESP_LOGV(TAG, "Stopping"); ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
} }
// Retries on a subsequent loop if the task is still running on the other core if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
if ((event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) && this->task_.deallocate()) { this->task_.deallocate();
ESP_LOGD(TAG, "Stopped"); ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
} }
@@ -235,7 +235,7 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic
bytes_written = this->output_speaker_->play(data, length, ticks_to_wait); bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
} else { } else {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) { if (temp_ring_buffer) {
// Only write to the ring buffer if the reference is valid // Only write to the ring buffer if the reference is valid
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
} else { } else {
@@ -299,7 +299,7 @@ bool ResamplerSpeaker::has_buffered_data() const {
bool has_ring_buffer_data = false; bool has_ring_buffer_data = false;
if (this->requires_resampling_()) { if (this->requires_resampling_()) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) { if (temp_ring_buffer) {
has_ring_buffer_data = (temp_ring_buffer->available() > 0); has_ring_buffer_data = (temp_ring_buffer->available() > 0);
} }
} }
@@ -342,7 +342,7 @@ void ResamplerSpeaker::resample_task(void *params) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create( std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(
this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
if (temp_ring_buffer == nullptr) { if (!temp_ring_buffer) {
err = ESP_ERR_NO_MEM; err = ESP_ERR_NO_MEM;
} else { } else {
this_resampler->ring_buffer_ = temp_ring_buffer; this_resampler->ring_buffer_ = temp_ring_buffer;
+21 -88
View File
@@ -18,16 +18,6 @@ void RFBridgeComponent::ack_() {
} }
bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) {
if (this->bucket_frame_candidate_ && byte == RF_CODE_START) {
// A queued next frame proves the trailing 0x55 really was the bucket
// frame's terminator: Portisch builds pulse entries from alternating
// signal edges, so the two level bits inside one pulse byte are always
// opposite — 0xAA (two high-level nibbles) cannot occur in pulse data.
// Finalize before this byte starts the new frame, so back-to-back
// deliveries are split even when loop() never observed a quiet gap
// between them.
this->finish_bucket_frame_();
}
size_t at = this->rx_buffer_.size(); size_t at = this->rx_buffer_.size();
this->rx_buffer_.push_back(byte); this->rx_buffer_.push_back(byte);
const uint8_t *raw = &this->rx_buffer_[0]; const uint8_t *raw = &this->rx_buffer_[0];
@@ -94,21 +84,26 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) {
break; break;
} }
case RF_CODE_RFIN_BUCKET: { case RF_CODE_RFIN_BUCKET: {
if (at == 2) { if (byte != RF_CODE_STOP) {
// The count byte: Portisch sends at most 7 buckets + sync, so 0 or return true;
// >8 cannot be a genuine capture — reject before it can occupy the
// buffer for a full frame timeout.
return byte != 0 && byte <= B1_MAX_BUCKET_COUNT;
} }
// 0x55 is legal DATA inside a B1 frame: bucket durations are sent
// with only their HIGH byte masked to 7 bits, so a duration such as uint8_t buckets = raw[2] << 1;
// 0x0155 puts a raw 0x55 low byte inside the table — the first 0x55 std::string str;
// must therefore not end the capture. The header declares the table char next_byte[3]; // 2 hex chars + null
// length (raw[2] pairs), so a 0x55 there is always data; one at or
// past the first pulse index is a terminator CANDIDATE, confirmed for (uint32_t i = 0; i <= at; i++) {
// once the UART goes quiet (finish_bucket_frame_ in loop()). buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]);
this->bucket_frame_candidate_ = byte == RF_CODE_STOP && at >= 3 + static_cast<size_t>(raw[2]) * 2; str += next_byte;
return true; if ((i > 3) && buckets) {
buckets--;
}
if ((i < 3) || (buckets % 2) || (i == at - 1)) {
str += " ";
}
}
ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str());
break;
} }
default: default:
ESP_LOGW(TAG, "Unknown action: 0x%02X", action); ESP_LOGW(TAG, "Unknown action: 0x%02X", action);
@@ -124,47 +119,6 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) {
return false; return false;
} }
void RFBridgeComponent::finish_bucket_frame_() {
if (this->rx_buffer_.size() < 4) {
// The candidate flag requires a header + non-empty bucket table, so
// this cannot happen while flag and buffer stay consistent; guard the
// raw[2] / size-1 reads against any future divergence anyway.
this->rx_buffer_.clear();
this->bucket_frame_candidate_ = false;
return;
}
const uint8_t *raw = this->rx_buffer_.data();
const size_t at = this->rx_buffer_.size() - 1;
uint8_t buckets = raw[2] << 1;
std::string str;
char next_byte[3]; // 2 hex chars + null
for (uint32_t i = 0; i <= at; i++) {
buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]);
str += next_byte;
if ((i > 3) && buckets) {
buckets--;
}
if ((i < 3) || (buckets % 2) || (i == at - 1)) {
str += " ";
}
}
ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str());
// Deliberately NOT ACKed: Portisch's B1 command handler leaves its
// last_sniffing_command at the previous mode (RF_CODE_RFIN), and its
// host-ACK handler re-arms sniffing from that stale value — so ACKing a
// bucket delivery silently reverts the radio to standard sniffing and
// ends bucket capture. Its delivery path is fire-and-forget and never
// waits for a host ACK. Stock Itead firmware never sends B1 frames, so
// suppressing this ACK cannot change stock-firmware behavior.
// https://github.com/esphome/esphome/issues/17682
this->rx_buffer_.clear();
this->bucket_frame_candidate_ = false;
}
void RFBridgeComponent::write_byte_str_(const std::string &codes) { void RFBridgeComponent::write_byte_str_(const std::string &codes) {
uint8_t code; uint8_t code;
int size = codes.length(); int size = codes.length();
@@ -176,31 +130,12 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) {
void RFBridgeComponent::loop() { void RFBridgeComponent::loop() {
const uint32_t now = App.get_loop_component_start_time(); const uint32_t now = App.get_loop_component_start_time();
size_t avail = this->available(); if (now - this->last_bridge_byte_ > 50) {
if (avail == 0 && this->bucket_frame_candidate_ && now - this->last_bridge_byte_ > BUCKET_CANDIDATE_QUIET_MS) {
// The trailing 0x55 was followed by UART quiet, so it really was the
// frame terminator and not an interior data byte.
this->finish_bucket_frame_();
this->last_bridge_byte_ = now;
}
const bool receiving_bucket = this->rx_buffer_.size() >= 2 && this->rx_buffer_[1] == RF_CODE_RFIN_BUCKET;
if (receiving_bucket) {
// Never declare an in-progress bucket frame dead while its continuation
// bytes are already queued: a stalled loop() otherwise discards a live
// frame that the UART buffer proves is still arriving.
if (avail == 0 && now - this->last_bridge_byte_ > BUCKET_FRAME_TIMEOUT_MS) {
ESP_LOGD(TAG, "Discarding incomplete RFBridge Bucket frame (%u bytes)",
static_cast<unsigned>(this->rx_buffer_.size()));
this->rx_buffer_.clear();
this->bucket_frame_candidate_ = false;
this->last_bridge_byte_ = now;
}
} else if (now - this->last_bridge_byte_ > 50) {
this->rx_buffer_.clear(); this->rx_buffer_.clear();
this->bucket_frame_candidate_ = false;
this->last_bridge_byte_ = now; this->last_bridge_byte_ = now;
} }
size_t avail = this->available();
while (avail > 0) { while (avail > 0) {
uint8_t buf[64]; uint8_t buf[64];
size_t to_read = std::min(avail, sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
@@ -211,14 +146,12 @@ void RFBridgeComponent::loop() {
for (size_t i = 0; i < to_read; i++) { for (size_t i = 0; i < to_read; i++) {
if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) { if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) {
this->rx_buffer_.clear(); this->rx_buffer_.clear();
this->bucket_frame_candidate_ = false;
} }
if (this->parse_bridge_byte_(buf[i])) { if (this->parse_bridge_byte_(buf[i])) {
ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]); ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]);
this->last_bridge_byte_ = now; this->last_bridge_byte_ = now;
} else { } else {
this->rx_buffer_.clear(); this->rx_buffer_.clear();
this->bucket_frame_candidate_ = false;
} }
} }
} }
-13
View File
@@ -30,17 +30,6 @@ static const uint8_t RF_CODE_BEEP = 0xC0;
static const uint8_t RF_CODE_STOP = 0x55; static const uint8_t RF_CODE_STOP = 0x55;
static const uint8_t RF_DEBOUNCE = 200; static const uint8_t RF_DEBOUNCE = 200;
static const size_t MAX_RX_BUFFER_SIZE = 512; static const size_t MAX_RX_BUFFER_SIZE = 512;
// ~10 byte times at 19200 baud: long enough to prove the UART went quiet
// after a possible bucket-frame terminator, short enough to finish well
// before the next radio capture can be delivered.
static const uint32_t BUCKET_CANDIDATE_QUIET_MS = 5;
// Portisch drains a B1 frame's header, bucket table, and pulse data as
// separate UART writes, so an in-progress bucket frame tolerates a longer
// inter-region gap than the generic 50 ms inter-byte timeout.
static const uint32_t BUCKET_FRAME_TIMEOUT_MS = 250;
// Portisch's uart_put_RF_buckets sends at most 7 buckets plus the sync
// bucket, so a B1 count byte above 8 (or 0) is malformed for any protocol.
static const uint8_t B1_MAX_BUCKET_COUNT = 8;
struct RFBridgeData { struct RFBridgeData {
uint16_t sync; uint16_t sync;
@@ -78,12 +67,10 @@ class RFBridgeComponent final : public uart::UARTDevice, public Component {
void ack_(); void ack_();
void decode_(); void decode_();
bool parse_bridge_byte_(uint8_t byte); bool parse_bridge_byte_(uint8_t byte);
void finish_bucket_frame_();
void write_byte_str_(const std::string &codes); void write_byte_str_(const std::string &codes);
std::vector<uint8_t> rx_buffer_; std::vector<uint8_t> rx_buffer_;
uint32_t last_bridge_byte_{0}; uint32_t last_bridge_byte_{0};
bool bucket_frame_candidate_{false};
CallbackManager<void(RFBridgeData)> data_callback_; CallbackManager<void(RFBridgeData)> data_callback_;
CallbackManager<void(RFBridgeAdvancedData)> advanced_data_callback_; CallbackManager<void(RFBridgeAdvancedData)> advanced_data_callback_;
+7 -51
View File
@@ -6,17 +6,12 @@ from esphome.components import esp32, network, psram, socket, wifi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
CONF_ESPHOME,
CONF_FORMAT, CONF_FORMAT,
CONF_HEIGHT, CONF_HEIGHT,
CONF_ID, CONF_ID,
CONF_MODEL,
CONF_NAME,
CONF_PROJECT,
CONF_SAMPLE_RATE, CONF_SAMPLE_RATE,
CONF_SOURCE, CONF_SOURCE,
CONF_TASK_STACK_IN_PSRAM, CONF_TASK_STACK_IN_PSRAM,
CONF_VERSION,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.core import CORE, ID from esphome.core import CORE, ID
@@ -32,18 +27,9 @@ DOMAIN = "sendspin"
CONF_DISPLAY_OFFSET = "display_offset" CONF_DISPLAY_OFFSET = "display_offset"
CONF_SENDSPIN_ID = "sendspin_id" CONF_SENDSPIN_ID = "sendspin_id"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_MANUFACTURER = "manufacturer"
# An empty device information string would be sent to the server as an empty value rather than
# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps
# the length prefix of a protobuf string field to a single byte, matching `esphome: project:`.
DEVICE_INFO_STRING = cv.All(cv.string_strict, cv.Length(min=1), cv.ByteLength(max=127))
CONF_INITIAL_STATIC_DELAY = "initial_static_delay" CONF_INITIAL_STATIC_DELAY = "initial_static_delay"
CONF_FIXED_DELAY = "fixed_delay" CONF_FIXED_DELAY = "fixed_delay"
CONF_DECODE_MEMORY = "decode_memory" CONF_DECODE_MEMORY = "decode_memory"
CONF_CODECS = "codecs"
# Matches ARTWORK_MAX_SLOTS in sendspin-cpp. # Matches ARTWORK_MAX_SLOTS in sendspin-cpp.
MAX_ARTWORK_SLOTS = 4 MAX_ARTWORK_SLOTS = 4
@@ -58,20 +44,6 @@ CODEC_FORMAT_OPUS = SendspinCodecFormat.enum("OPUS")
CODEC_FORMAT_PCM = SendspinCodecFormat.enum("PCM") CODEC_FORMAT_PCM = SendspinCodecFormat.enum("PCM")
CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED") CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED")
CODEC_FLAC = "flac"
CODEC_OPUS = "opus"
CODEC_PCM = "pcm"
CODECS = {
CODEC_FLAC: CODEC_FORMAT_FLAC,
CODEC_OPUS: CODEC_FORMAT_OPUS,
CODEC_PCM: CODEC_FORMAT_PCM,
}
# Opus only supports 48 kHz audio, so it is left out of the default list at other rates.
DEFAULT_CODECS = [CODEC_FLAC, CODEC_OPUS, CODEC_PCM]
OPUS_SAMPLE_RATE = 48000
SendspinImageFormat = sendspin_library_ns.enum("SendspinImageFormat", is_class=True) SendspinImageFormat = sendspin_library_ns.enum("SendspinImageFormat", is_class=True)
IMAGE_FORMAT_JPEG = SendspinImageFormat.enum("JPEG") IMAGE_FORMAT_JPEG = SendspinImageFormat.enum("JPEG")
IMAGE_FORMAT_PNG = SendspinImageFormat.enum("PNG") IMAGE_FORMAT_PNG = SendspinImageFormat.enum("PNG")
@@ -211,9 +183,6 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(SendspinHub), cv.GenerateID(): cv.declare_id(SendspinHub),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram, cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
} }
), ),
cv.only_on_esp32, cv.only_on_esp32,
@@ -264,22 +233,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_task_stack_in_psram(True)) cg.add(var.set_task_stack_in_psram(True))
psram.request_external_task_stack() psram.request_external_task_stack()
# Device information for the server's client/hello message. Falls back to the project
# information, which is written as `manufacturer.model`. Anything still unset keeps the
# default the hub itself applies: the ESPHome name and version.
project = CORE.config[CONF_ESPHOME].get(CONF_PROJECT, {})
project_manufacturer, _, project_model = project.get(CONF_NAME, "").partition(".")
for value, setter in (
(config.get(CONF_MANUFACTURER) or project_manufacturer, var.set_manufacturer),
(config.get(CONF_MODEL) or project_model, var.set_model),
(
config.get(CONF_FIRMWARE_VERSION) or project.get(CONF_VERSION),
var.set_firmware_version,
),
):
if value:
cg.add(setter(value))
# sendspin-cpp library # sendspin-cpp library
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2") esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2")
@@ -333,13 +286,16 @@ async def to_code(config: ConfigType) -> None:
if data.player_support: if data.player_support:
cg.add_define("USE_SENDSPIN_PLAYER", True) cg.add_define("USE_SENDSPIN_PLAYER", True)
# Configures the player role. Each configured codec is advertised for 16 bits per sample # Configures the player role. We always assume support for 16 bits per sample mono and stereo FLAC, Opus, and PCM at the configured sample rate
# mono and stereo at the configured sample rate. The order is a preference order, both for # (with Opus only supported at 48 kHz since that's the only sample rate it supports). Users can configure the specific formats via the Sendspin server
# the codecs themselves and for stereo over mono.
player_cfg = data.player_config player_cfg = data.player_config
sample_rate = player_cfg[CONF_SAMPLE_RATE] sample_rate = player_cfg[CONF_SAMPLE_RATE]
codecs = [CODECS[codec] for codec in player_cfg[CONF_CODECS]] # OPUS only supports 48 kHz audio
codecs = [CODEC_FORMAT_FLAC]
if sample_rate == 48000:
codecs.append(CODEC_FORMAT_OPUS)
codecs.append(CODEC_FORMAT_PCM)
def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer: def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer:
return cg.StructInitializer( return cg.StructInitializer(
@@ -13,16 +13,11 @@ from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType from esphome.types import ConfigType
from .. import ( from .. import (
CODEC_OPUS,
CODECS,
CONF_CODECS,
CONF_DECODE_MEMORY, CONF_DECODE_MEMORY,
CONF_FIXED_DELAY, CONF_FIXED_DELAY,
CONF_INITIAL_STATIC_DELAY, CONF_INITIAL_STATIC_DELAY,
CONF_SENDSPIN_ID, CONF_SENDSPIN_ID,
DEFAULT_CODECS,
MEMORY_LOCATIONS, MEMORY_LOCATIONS,
OPUS_SAMPLE_RATE,
SendspinHub, SendspinHub,
register_player_config, register_player_config,
request_controller_support, request_controller_support,
@@ -54,32 +49,10 @@ DisableStaticDelayAdjustmentAction = sendspin_ns.class_(
) )
def _resolve_codecs(config: ConfigType) -> ConfigType:
"""Validate the codec preference list, filling in the default when it is not set."""
sample_rate = config[CONF_SAMPLE_RATE]
if (codecs := config.get(CONF_CODECS)) is None:
config[CONF_CODECS] = [
codec
for codec in DEFAULT_CODECS
if codec != CODEC_OPUS or sample_rate == OPUS_SAMPLE_RATE
]
return config
if len(set(codecs)) != len(codecs):
raise cv.Invalid("Each codec may only be listed once", path=[CONF_CODECS])
if CODEC_OPUS in codecs and sample_rate != OPUS_SAMPLE_RATE:
raise cv.Invalid(
f"Codec '{CODEC_OPUS}' requires a {CONF_SAMPLE_RATE} of {OPUS_SAMPLE_RATE}",
path=[CONF_CODECS],
)
return config
def _register(config: ConfigType) -> ConfigType: def _register(config: ConfigType) -> ConfigType:
request_controller_support() request_controller_support()
register_player_config( register_player_config(
{ {
CONF_CODECS: config[CONF_CODECS],
CONF_SAMPLE_RATE: config[CONF_SAMPLE_RATE], CONF_SAMPLE_RATE: config[CONF_SAMPLE_RATE],
CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE],
CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY], CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY],
@@ -112,13 +85,9 @@ CONFIG_SCHEMA = cv.All(
min=16000, max=96000 min=16000, max=96000
), ),
cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True), cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True),
cv.Optional(CONF_CODECS): cv.All(
cv.ensure_list(cv.enum(CODECS, lower=True)), cv.Length(min=1)
),
} }
), ),
cv.only_on_esp32, cv.only_on_esp32,
_resolve_codecs,
_register, _register,
) )
+4 -12
View File
@@ -76,12 +76,8 @@ void SendspinHub::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"Sendspin Hub:\n" "Sendspin Hub:\n"
" Client ID: %s\n" " Client ID: %s\n"
" Manufacturer: %s\n"
" Model: %s\n"
" Firmware version: %s\n"
" Task stack in PSRAM: %s", " Task stack in PSRAM: %s",
get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(), get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
this->firmware_version_, YESNO(this->task_stack_in_psram_));
#ifdef USE_SENDSPIN_ARTWORK #ifdef USE_SENDSPIN_ARTWORK
// Slot indices come from the order the image platform entries were declared, so the log is the // Slot indices come from the order the image platform entries were declared, so the log is the
@@ -131,19 +127,15 @@ const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_P
return get_mac_address_pretty_into_buffer(buf); return get_mac_address_pretty_into_buffer(buf);
} }
const char *SendspinHub::get_product_name_() const {
return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
}
sendspin::SendspinClientConfig SendspinHub::build_client_config_() { sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
sendspin::SendspinClientConfig config; sendspin::SendspinClientConfig config;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf); config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
config.name = App.get_friendly_name(); config.name = App.get_friendly_name();
config.product_name = this->get_product_name_(); config.product_name = App.get_name();
config.manufacturer = this->manufacturer_; config.manufacturer = "ESPHome";
config.software_version = this->firmware_version_; config.software_version = ESPHOME_VERSION;
config.httpd_psram_stack = this->task_stack_in_psram_; config.httpd_psram_stack = this->task_stack_in_psram_;
return config; return config;
@@ -8,7 +8,6 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include "esphome/core/version.h"
#include <sendspin/client.h> #include <sendspin/client.h>
#include <sendspin/config.h> #include <sendspin/config.h>
@@ -126,15 +125,6 @@ class SendspinHub final : public Component,
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
/// @brief Sets the device information reported to the server in the `client/hello` message.
///
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
/// lifetime of the hub. Only called for values the configuration overrides; anything left alone
/// keeps the default described on the member below.
void set_manufacturer(const char *manufacturer) { this->manufacturer_ = manufacturer; }
void set_model(const char *model) { this->model_ = model; }
void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; }
// --- Sendspin role specific methods --- // --- Sendspin role specific methods ---
#ifdef USE_SENDSPIN_ARTWORK #ifdef USE_SENDSPIN_ARTWORK
@@ -197,9 +187,6 @@ class SendspinHub final : public Component,
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
sendspin::SendspinClientConfig build_client_config_(); sendspin::SendspinClientConfig build_client_config_();
/// @brief Returns the product name reported to the server: the configured model, or the device name.
const char *get_product_name_() const;
/// @brief Writes the active network interface's MAC into @p buf and returns its data pointer. /// @brief Writes the active network interface's MAC into @p buf and returns its data pointer.
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi). /// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf); static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
@@ -281,12 +268,6 @@ class SendspinHub final : public Component,
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{}; CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
bool task_stack_in_psram_{false}; bool task_stack_in_psram_{false};
// Device information sent in the `client/hello` message. Defaults apply when neither the
// sendspin configuration nor the project information supplies a value.
const char *manufacturer_{"ESPHome"};
const char *model_{nullptr}; // nullptr reports the device name instead
const char *firmware_version_{ESPHOME_VERSION};
}; };
/// @brief Base class for all sendspin subcomponents. /// @brief Base class for all sendspin subcomponents.
+3 -34
View File
@@ -18,10 +18,9 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import uart from esphome.components import uart
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_UART_ID from esphome.const import CONF_ID, CONF_NAME
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"] CODEOWNERS = ["@kbx81"]
@@ -31,18 +30,14 @@ MULTI_CONF = True
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy") serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice) SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums") api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType") SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
# User-selectable electrical types. USB_SERIAL is deliberately absent: it is derived
# from the uart_id pointing at a usb_uart channel, never set by the user.
SERIAL_PROXY_PORT_TYPES = { SERIAL_PROXY_PORT_TYPES = {
"TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL, "TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL,
"RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232, "RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232,
"RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485, "RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485,
} }
PORT_TYPE_USB_SERIAL = SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_USB_SERIAL
CONF_DTR_PIN = "dtr_pin" CONF_DTR_PIN = "dtr_pin"
CONF_PORT_TYPE = "port_type" CONF_PORT_TYPE = "port_type"
@@ -67,7 +62,7 @@ CONFIG_SCHEMA = (
{ {
cv.GenerateID(): cv.declare_id(SerialProxy), cv.GenerateID(): cv.declare_id(SerialProxy),
cv.Required(CONF_NAME): cv.string_strict, cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True), cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
} }
@@ -77,26 +72,6 @@ CONFIG_SCHEMA = (
) )
def _uses_usb_uart(config: ConfigType, full_config: ConfigType) -> bool:
from esphome.components.usb_uart import is_usb_uart_channel
return is_usb_uart_channel(config[CONF_UART_ID], full_config)
def _final_validate(config: ConfigType) -> ConfigType:
if _uses_usb_uart(config, fv.full_config.get()):
if CONF_PORT_TYPE in config:
raise cv.Invalid(
f"{CONF_PORT_TYPE} is set automatically for USB serial ports"
)
elif CONF_PORT_TYPE not in config:
raise cv.Invalid(f"{CONF_PORT_TYPE} is required")
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.FINAL) @coroutine_with_priority(CoroPriority.FINAL)
async def _add_serial_proxy_count_define() -> None: async def _add_serial_proxy_count_define() -> None:
"""Emit the SERIAL_PROXY_COUNT define once with the final instance count.""" """Emit the SERIAL_PROXY_COUNT define once with the final instance count."""
@@ -111,13 +86,7 @@ async def to_code(config: ConfigType) -> None:
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
cg.add(cg.App.register_serial_proxy(var)) cg.add(cg.App.register_serial_proxy(var))
cg.add(var.set_name(config[CONF_NAME])) cg.add(var.set_name(config[CONF_NAME]))
if _uses_usb_uart(config, CORE.config): cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
cg.add(var.set_port_type(PORT_TYPE_USB_SERIAL))
channel = await cg.get_variable(config[CONF_UART_ID])
cg.add(var.set_usb_channel(channel))
cg.add_define("USE_SERIAL_PROXY_USB_INFO")
else:
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
cg.add_define("USE_SERIAL_PROXY") cg.add_define("USE_SERIAL_PROXY")
# Track instance count for the FINAL priority define # Track instance count for the FINAL priority define
+28 -192
View File
@@ -12,10 +12,6 @@
#include "esphome/components/api/api_server.h" #include "esphome/components/api/api_server.h"
#endif #endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_uart/usb_uart.h"
#endif
namespace esphome::serial_proxy { namespace esphome::serial_proxy {
static const char *const TAG = "serial_proxy"; static const char *const TAG = "serial_proxy";
@@ -33,57 +29,26 @@ void SerialProxy::setup() {
#ifdef USE_API #ifdef USE_API
// instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data // instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
this->outgoing_msg_.instance = this->instance_index_; this->outgoing_msg_.instance = this->instance_index_;
#endif
#ifdef USE_SERIAL_PROXY_TAP
// A tap sets itself up before this runs (its setup priority is higher), so it may
// already be waiting on the port -- a boot-time handshake with the device, say. Leaving
// the loop enabled is what lets that finish; without it the tap would stall until a
// client happened to subscribe.
if (this->tap_ != nullptr && this->tap_->tap_needs_port()) {
return;
}
#endif #endif
// No subscriber at startup; disable loop until a client subscribes // No subscriber at startup; disable loop until a client subscribes
this->disable_loop(); this->disable_loop();
} }
#ifdef USE_SERIAL_PROXY_TAP
void SerialProxy::reset_mode_() {
// The mode belongs to a session, not to the port. Carrying a departed client's choice
// over to the next one would inject protocol bytes into a stream that never asked for
// them -- a firmware upload, or any client built before this request existed and so
// unable to turn it off. Guessing RAW is the safe direction: a client that wanted
// protocol handling and did not ask for it merely sends its own acknowledgements.
if (this->mode_ == api::enums::SERIAL_PROXY_MODE_RAW) {
return;
}
ESP_LOGD(TAG, "Session ended, returning serial proxy [%" PRIu32 "] to RAW mode", this->instance_index_);
this->mode_ = api::enums::SERIAL_PROXY_MODE_RAW;
}
#endif
void SerialProxy::loop() { void SerialProxy::loop() {
#ifdef USE_API #ifdef USE_API
// Detect subscriber disconnect // Safety check — loop should only run when subscribed, but guard against races
if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() ||
!this->api_connection_->is_connection_setup() || !api_is_connected())) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->reset_mode_();
}
// With no subscriber there is normally nothing to do, but a tap may still need the port
// read -- it does its protocol work precisely while nobody else is listening.
if (this->api_connection_ == nullptr) [[unlikely]] { if (this->api_connection_ == nullptr) [[unlikely]] {
#ifdef USE_SERIAL_PROXY_TAP
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
return;
}
#else
this->disable_loop(); this->disable_loop();
return; return;
#endif }
// Detect subscriber disconnect
if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
!api_is_connected()) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->disable_loop();
return;
} }
// Read available data from UART and forward to subscribed client // Read available data from UART and forward to subscribed client
@@ -104,54 +69,11 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
if (!this->read_array(buffer, to_read)) if (!this->read_array(buffer, to_read))
return; return;
#ifdef USE_SERIAL_PROXY_TAP
// Before forwarding, so a tap that answers the device (an acknowledgement, say) is not
// waiting on the network round trip to a subscriber that may not even exist.
if (this->tap_observing_()) {
this->tap_->on_device_rx(buffer, to_read);
}
#endif
if (this->api_connection_ == nullptr) {
return;
}
this->outgoing_msg_.set_data(buffer, to_read); this->outgoing_msg_.set_data(buffer, to_read);
this->api_connection_->send_serial_proxy_data(this->outgoing_msg_); this->api_connection_->send_serial_proxy_data(this->outgoing_msg_);
} }
#endif #endif
#ifdef USE_SERIAL_PROXY_TAP
bool SerialProxy::tap_observing_() const {
if (this->tap_ == nullptr) {
return false;
}
// With no subscriber, a tap doing its own protocol work (the boot-time handshake with
// the device, say) is served regardless of mode -- nobody has chosen one yet. Once a
// subscriber holds the port, the mode alone decides, so RAW stays inert.
if (this->api_connection_ == nullptr && this->tap_->tap_needs_port()) {
return true;
}
// Otherwise the mode decides. RAW must be inert: a client that flips to RAW before
// flashing firmware is entitled to a byte pipe with nothing injecting protocol bytes
// into it, and "the tap turned out not to recognise the stream" is not good enough.
return this->mode_ == api::enums::SERIAL_PROXY_MODE_PROTOCOL;
}
void SerialProxy::tap_pump() {
#ifdef USE_API
// Nothing would consume the bytes; leave them in the FIFO
if (!this->tap_observing_() && this->api_connection_ == nullptr) {
return;
}
const size_t available = this->available();
if (available > 0) {
this->read_and_send_(available);
}
#endif
}
#endif
void SerialProxy::dump_config() { void SerialProxy::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"Serial Proxy [%" PRIu32 "]:\n" "Serial Proxy [%" PRIu32 "]:\n"
@@ -160,10 +82,9 @@ void SerialProxy::dump_config() {
" RTS Pin: %s\n" " RTS Pin: %s\n"
" DTR Pin: %s", " DTR Pin: %s",
this->instance_index_, this->name_ != nullptr ? this->name_ : "", this->instance_index_, this->name_ != nullptr ? this->name_ : "",
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485") this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232") : this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL ? LOG_STR_LITERAL("USB_SERIAL") : LOG_STR_LITERAL("TTL"),
: LOG_STR_LITERAL("TTL"),
this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"), this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured")); this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
} }
@@ -171,9 +92,8 @@ void SerialProxy::dump_config() {
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
uint8_t parity, uint8_t stop_bits, uint8_t data_size) { uint8_t parity, uint8_t stop_bits, uint8_t data_size) {
#ifdef USE_API #ifdef USE_API
if (!this->is_subscriber_(api_connection)) { if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring configure request from client without port subscription [%" PRIu32 "]", ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
} }
#endif #endif
@@ -239,80 +159,24 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
return SerialProxyResult::SERIAL_PROXY_RESULT_OK; return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
} }
SerialProxyResult SerialProxy::set_mode_from_client(api::APIConnection *api_connection,
api::enums::SerialProxyMode mode) {
#ifdef USE_API
// Only the live subscriber may change the mode, so the mode cannot outlive a session
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring mode request from client without port subscription [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
// Values come from a remote client
if (mode != api::enums::SERIAL_PROXY_MODE_RAW && mode != api::enums::SERIAL_PROXY_MODE_PROTOCOL) {
ESP_LOGW(TAG, "Invalid mode: %" PRIu32, static_cast<uint32_t>(mode));
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
}
// PROTOCOL on a port with no tap would be a silent no-op; refuse so the client knows
#ifdef USE_SERIAL_PROXY_TAP
const bool has_tap = this->tap_ != nullptr;
#else
const bool has_tap = false;
#endif
if (mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL && !has_tap) {
ESP_LOGW(TAG, "No tap on serial proxy [%" PRIu32 "]; PROTOCOL mode unavailable", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
}
ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_,
mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL ? LOG_STR_LITERAL("PROTOCOL") : LOG_STR_LITERAL("RAW"));
#ifdef USE_SERIAL_PROXY_TAP
const bool leaving_protocol_mode =
this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW;
this->mode_ = mode;
// Only for an explicit client request, not for reset_mode_() at the end of a session:
// an ordinary disconnect says nothing about the device, whereas a client deliberately
// asking for raw bytes usually precedes changing what the device is.
if (leaving_protocol_mode && this->tap_ != nullptr) {
this->tap_->on_protocol_disabled();
}
#endif
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) { void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
#ifdef USE_API #ifdef USE_API
// Bytes from anyone but the live subscriber would interleave with the subscriber's // Bytes from a client other than the live subscriber would interleave with the
// traffic -- or with an active tap's -- on the wire // subscriber's traffic on the wire
if (!this->is_subscriber_(api_connection)) { if (this->port_claimed_by_other_(api_connection)) {
if (this->api_connection_ != nullptr) { ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_);
ESP_LOGW(TAG, "Ignoring write from client that does not hold serial proxy [%" PRIu32 "]", this->instance_index_);
} else {
// A legacy client streaming writes without subscribing would flood WARN, one per
// request; writes are the only high-rate, unacknowledged operation, so keep this
// visible without drowning the log
ESP_LOGV(TAG, "Ignoring write from client without port subscription [%" PRIu32 "]", this->instance_index_);
}
return; return;
} }
#endif #endif
if (data == nullptr || len == 0) if (data == nullptr || len == 0)
return; return;
this->write_array(data, len); this->write_array(data, len);
#ifdef USE_SERIAL_PROXY_TAP
// After the write, so the tap observes the same ordering the device does
if (this->tap_observing_()) {
this->tap_->on_client_tx(data, len);
}
#endif
} }
SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) { SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
#ifdef USE_API #ifdef USE_API
if (!this->is_subscriber_(api_connection)) { if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring modem pin request from client without port subscription [%" PRIu32 "]", ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
} }
#endif #endif
@@ -338,27 +202,6 @@ SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection
return SerialProxyResult::SERIAL_PROXY_RESULT_OK; return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
} }
#if defined(USE_SERIAL_PROXY_USB_INFO) && defined(USE_API)
void SerialProxy::get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyGetUsbInfoResponse &resp) const {
if (this->usb_channel_ == nullptr) {
resp.status = api::enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
return;
}
resp.interface_number = this->usb_channel_->get_index();
if (!this->usb_channel_->get_parent()->get_device_info(info)) {
// No device attached right now; not an error
return;
}
resp.connected = true;
resp.vendor_id = info.vendor_id;
resp.product_id = info.product_id;
resp.bcd_device = info.bcd_device;
resp.manufacturer = StringRef(info.manufacturer);
resp.product = StringRef(info.product);
resp.serial_number = StringRef(info.serial_number);
}
#endif
uint32_t SerialProxy::get_modem_pins() const { uint32_t SerialProxy::get_modem_pins() const {
return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) | return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
(this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u); (this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
@@ -367,8 +210,8 @@ uint32_t SerialProxy::get_modem_pins() const {
SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) { SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
#ifdef USE_API #ifdef USE_API
// Flushing stalls the port, so it gets the same ownership check as writes // Flushing stalls the port, so it gets the same ownership check as writes
if (!this->is_subscriber_(api_connection)) { if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring flush from client without port subscription [%" PRIu32 "]", this->instance_index_); ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
} }
#endif #endif
@@ -387,6 +230,11 @@ SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
} }
#ifdef USE_API #ifdef USE_API
bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const {
return this->api_connection_ != nullptr && this->api_connection_ != api_connection &&
this->api_connection_->is_connection_setup();
}
SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection, SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection,
api::enums::SerialProxyRequestType type) { api::enums::SerialProxyRequestType type) {
switch (type) { switch (type) {
@@ -404,10 +252,6 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
} }
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription"); ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
// End the dead client's session before starting the new one, so its mode
// cannot leak into a session that never asked for it
this->api_connection_ = nullptr;
this->reset_mode_();
} }
this->api_connection_ = api_connection; this->api_connection_ = api_connection;
this->enable_loop(); this->enable_loop();
@@ -420,15 +264,7 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_OK; return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
} }
this->api_connection_ = nullptr; this->api_connection_ = nullptr;
this->reset_mode_();
#ifdef USE_SERIAL_PROXY_TAP
// Keep the loop alive for a tap that still needs the port (mirrors loop())
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
}
#else
this->disable_loop(); this->disable_loop();
#endif
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_); ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK; return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
default: default:
+3 -125
View File
@@ -20,22 +20,12 @@
#include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_pb2.h"
#endif #endif
#ifdef USE_SERIAL_PROXY_USB_INFO
namespace esphome::usb_uart {
class USBUartChannel;
} // namespace esphome::usb_uart
namespace esphome::usb_host {
struct UsbDeviceInfo;
} // namespace esphome::usb_host
#endif
// Forward-declare types needed outside the USE_API guard. // Forward-declare types needed outside the USE_API guard.
namespace esphome::api { namespace esphome::api {
class APIConnection; class APIConnection;
namespace enums { namespace enums {
enum SerialProxyPortType : uint32_t; enum SerialProxyPortType : uint32_t;
enum SerialProxyRequestType : uint32_t; enum SerialProxyRequestType : uint32_t;
enum SerialProxyMode : uint32_t;
} // namespace enums } // namespace enums
} // namespace esphome::api } // namespace esphome::api
@@ -62,36 +52,6 @@ enum class SerialProxyResult : uint8_t {
/// Maximum bytes to read from UART in a single loop iteration /// Maximum bytes to read from UART in a single loop iteration
inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256; inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
#ifdef USE_SERIAL_PROXY_TAP
/// Observes a port's traffic without owning it, and may inject bytes of its own.
///
/// This exists so protocol-aware behaviour can be layered onto a plain byte pipe without
/// the pipe knowing anything about the protocol: the tap is compiled in only when some
/// component asks for one, so a proxy carrying an RS485 meter pays nothing for it.
///
/// A tap is an observer, never a gatekeeper -- it cannot suppress or alter the bytes
/// flowing in either direction, so a misbehaving tap cannot corrupt the stream.
class SerialProxyTap {
public:
/// Bytes read from the device, before they are forwarded to any subscriber.
virtual void on_device_rx(const uint8_t *data, size_t len) = 0;
/// Bytes a subscriber sent towards the device, after they have been written.
virtual void on_client_tx(const uint8_t *data, size_t len) = 0;
/// True when the port must keep reading even with no subscriber attached, so a tap can
/// do its own protocol work while nobody is listening. Honoured only while no
/// subscriber holds the port; with one attached, the port mode alone decides.
virtual bool tap_needs_port() const = 0;
/// A client explicitly turned protocol handling off for this port. Distinct from the
/// automatic reset when a session ends: this one means a client intends to do something
/// else with the device -- reflash it, most likely -- so anything the tap believes about
/// it should be treated as suspect.
virtual void on_protocol_disabled() = 0;
};
#endif
class SerialProxy final : public uart::UARTDevice, public Component { class SerialProxy final : public uart::UARTDevice, public Component {
public: public:
void setup() override; void setup() override;
@@ -117,9 +77,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Get the port type /// Get the port type
api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; } api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; }
/// Handle a mode change requested by an API client
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode);
/// Configure UART parameters and apply them /// Configure UART parameters and apply them
/// @param api_connection The API connection requesting the change /// @param api_connection The API connection requesting the change
/// @param baudrate Baud rate in bits per second /// @param baudrate Baud rate in bits per second
@@ -164,78 +121,13 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Set the DTR GPIO pin (from YAML configuration) /// Set the DTR GPIO pin (from YAML configuration)
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; } void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
#ifdef USE_SERIAL_PROXY_USB_INFO
/// Attach the USB UART channel behind this port (from code generation)
void set_usb_channel(usb_uart::USBUartChannel *channel) { this->usb_channel_ = channel; }
#ifdef USE_API
/// Fill a USB info response for this port. The response's strings are views into
/// info, so info must outlive the send.
void get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyGetUsbInfoResponse &resp) const;
#endif
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Attach a traffic observer. At most one, set once at setup time.
void set_tap(SerialProxyTap *tap) { this->tap_ = tap; }
/// Write bytes originating from the tap rather than from a client. Bypasses the
/// subscriber ownership check, but only while the tap is being served bytes -- so a
/// port in RAW mode with a subscriber attached stays inert. Returns false when the
/// bytes were dropped for that reason.
bool write_from_tap(const uint8_t *data, size_t len) {
if (!this->tap_observing_()) {
return false;
}
this->write_array(data, len);
return true;
}
/// Whether the tap is currently being served bytes. Can flip false with no callback
/// (a subscriber attaching in RAW mode, say), so a tap should check before starting
/// protocol work and when a reply seems overdue.
bool tap_is_observed() const { return this->tap_observing_(); }
/// Resume reading after a tap's needs change. loop() disables itself when there is
/// neither a subscriber nor a tap that wants the port, so a tap starting fresh work
/// must ask for it back. Must be called from the main loop.
void tap_request_port() { this->enable_loop(); }
/// Whether the underlying device is present. On a USB UART this tracks enumeration, so
/// a tap can notice the device being unplugged and plugged back in.
bool is_device_connected() const { return this->parent_->is_connected(); }
/// Run one read-and-dispatch cycle immediately. Lets a tap make progress before the
/// main loop is running -- during setup, for instance, while a component is still
/// blocking on can_proceed(). Must not be called from on_device_rx() or
/// on_client_tx(): each nested cycle costs a 256-byte stack frame.
void tap_pump();
#endif
protected: protected:
#ifdef USE_API #ifdef USE_API
/// Read from UART, hand the bytes to any tap, and forward them to a subscriber /// Read from UART and send to API client (slow path with 256-byte stack buffer)
/// (slow path with a 256-byte stack buffer)
void read_and_send_(size_t available); void read_and_send_(size_t available);
/// True when the given connection is the live subscriber. Every port operation /// True when a live subscriber other than the given connection holds the port
/// (write, configure, modem pins, flush, mode) requires this, so an unsubscribed bool port_claimed_by_other_(api::APIConnection *api_connection) const;
/// client can never share the wire with the subscriber or an active tap.
bool is_subscriber_(api::APIConnection *api_connection) const { return this->api_connection_ == api_connection; }
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Return the port to RAW when a subscriber goes away, so the mode never outlives it
void reset_mode_();
#else
/// Without a tap, PROTOCOL is refused, so the mode is fixed at RAW and there is
/// nothing to reset
void reset_mode_() {}
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// True when the tap should be shown the traffic passing through this port
bool tap_observing_() const;
#endif #endif
/// Instance index for identifying this proxy in API messages /// Instance index for identifying this proxy in API messages
@@ -255,11 +147,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Port type /// Port type
api::enums::SerialProxyPortType port_type_{}; api::enums::SerialProxyPortType port_type_{};
#ifdef USE_SERIAL_PROXY_TAP
/// How the bytes passing through are treated; zero is SERIAL_PROXY_MODE_RAW
api::enums::SerialProxyMode mode_{};
#endif
/// Optional GPIO pins for modem control /// Optional GPIO pins for modem control
GPIOPin *rts_pin_{nullptr}; GPIOPin *rts_pin_{nullptr};
GPIOPin *dtr_pin_{nullptr}; GPIOPin *dtr_pin_{nullptr};
@@ -267,15 +154,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Current modem pin states /// Current modem pin states
bool rts_state_{false}; bool rts_state_{false};
bool dtr_state_{false}; bool dtr_state_{false};
#ifdef USE_SERIAL_PROXY_TAP
SerialProxyTap *tap_{nullptr};
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// The USB UART channel behind this port; nullptr on non-USB ports
usb_uart::USBUartChannel *usb_channel_{nullptr};
#endif
}; };
} // namespace esphome::serial_proxy } // namespace esphome::serial_proxy
@@ -202,15 +202,8 @@ AudioPipelineState AudioPipeline::process_state() {
if (!this->is_playing_) { if (!this->is_playing_) {
// The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks // The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks
if (this->read_task_.is_created() || this->decode_task_.is_created()) { if (this->read_task_.is_created() || this->decode_task_.is_created()) {
// Both are attempted every time; a task that is still running on the other core is freed by a this->read_task_.deallocate();
// subsequent call, and freeing an already freed task succeeds without doing anything this->decode_task_.deallocate();
bool read_task_freed = this->read_task_.deallocate();
bool decode_task_freed = this->decode_task_.deallocate();
if (!read_task_freed || !decode_task_freed) {
// A task is still running on the other core, so keep the pipeline in its current state and try
// again on the next call
return AudioPipelineState::PLAYING;
}
if (this->hard_stop_) { if (this->hard_stop_) {
// Stop command was sent, so immediately end the playback // Stop command was sent, so immediately end the playback
this->speaker_->stop(); this->speaker_->stop();
@@ -322,17 +315,17 @@ void AudioPipeline::read_task(void *params) {
if (err == ESP_OK) { if (err == ESP_OK) {
size_t file_ring_buffer_size = this_pipeline->buffer_size_; size_t file_ring_buffer_size = this_pipeline->buffer_size_;
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer;
if (temp_ring_buffer == nullptr) { if (!this_pipeline->raw_file_ring_buffer_.use_count()) {
temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size); temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size);
this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer; this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
} }
if (temp_ring_buffer == nullptr) { if (!this_pipeline->raw_file_ring_buffer_.use_count()) {
err = ESP_ERR_NO_MEM; err = ESP_ERR_NO_MEM;
} else { } else {
err = reader->add_sink(temp_ring_buffer); reader->add_sink(this_pipeline->raw_file_ring_buffer_);
} }
} }
@@ -403,9 +396,7 @@ void AudioPipeline::decode_task(void *params) {
make_unique<audio::AudioDecoder>(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_); make_unique<audio::AudioDecoder>(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_);
esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_); esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_);
if (err == ESP_OK) { decoder->add_source(this_pipeline->raw_file_ring_buffer_);
err = decoder->add_source(this_pipeline->raw_file_ring_buffer_);
}
if (err != ESP_OK) { if (err != ESP_OK) {
// Send specific error message // Send specific error message
@@ -2,13 +2,7 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor from esphome.components import binary_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_CONDITION, CONF_ID, CONF_LAMBDA, CONF_STATE
CONF_CONDITION,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_STATE,
)
from esphome.cpp_generator import LambdaExpression from esphome.cpp_generator import LambdaExpression
from .. import template_ns from .. import template_ns
@@ -18,11 +12,7 @@ TemplateBinarySensor = template_ns.class_(
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.with_visibility( binary_sensor.binary_sensor_schema(TemplateBinarySensor)
binary_sensor.binary_sensor_schema(TemplateBinarySensor),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
.extend( .extend(
{ {
cv.Exclusive(CONF_LAMBDA, CONF_CONDITION): cv.returning_lambda, cv.Exclusive(CONF_LAMBDA, CONF_CONDITION): cv.returning_lambda,
@@ -1,14 +1,10 @@
from esphome.components import button from esphome.components import button
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS
from .. import template_ns from .. import template_ns
TemplateButton = template_ns.class_("TemplateButton", button.Button) TemplateButton = template_ns.class_("TemplateButton", button.Button)
CONFIG_SCHEMA = cv.with_visibility( CONFIG_SCHEMA = button.button_schema(TemplateButton)
button.button_schema(TemplateButton), cv.Visibility.UI, CONF_DEVICE_CLASS
)
async def to_code(config): async def to_code(config):
@@ -6,7 +6,6 @@ from esphome.const import (
CONF_ASSUMED_STATE, CONF_ASSUMED_STATE,
CONF_CLOSE_ACTION, CONF_CLOSE_ACTION,
CONF_CURRENT_OPERATION, CONF_CURRENT_OPERATION,
CONF_DEVICE_CLASS,
CONF_ID, CONF_ID,
CONF_LAMBDA, CONF_LAMBDA,
CONF_OPEN_ACTION, CONF_OPEN_ACTION,
@@ -39,11 +38,7 @@ CONF_HAS_POSITION = "has_position"
CONF_TOGGLE_ACTION = "toggle_action" CONF_TOGGLE_ACTION = "toggle_action"
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.with_visibility( cover.cover_schema(TemplateCover)
cover.cover_schema(TemplateCover),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
.extend( .extend(
{ {
cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_LAMBDA): cv.returning_lambda,
@@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import event from esphome.components import event
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS, CONF_EVENT_TYPES from esphome.const import CONF_EVENT_TYPES
from .. import template_ns from .. import template_ns
@@ -9,9 +9,7 @@ CODEOWNERS = ["@nohat"]
TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component) TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component)
CONFIG_SCHEMA = cv.with_visibility( CONFIG_SCHEMA = event.event_schema(TemplateEvent).extend(
event.event_schema(TemplateEvent), cv.Visibility.UI, CONF_DEVICE_CLASS
).extend(
{ {
cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict), cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict),
} }
@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import number from esphome.components import number
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ID, CONF_ID,
CONF_INITIAL_VALUE, CONF_INITIAL_VALUE,
CONF_LAMBDA, CONF_LAMBDA,
@@ -13,7 +12,6 @@ from esphome.const import (
CONF_RESTORE_VALUE, CONF_RESTORE_VALUE,
CONF_SET_ACTION, CONF_SET_ACTION,
CONF_STEP, CONF_STEP,
CONF_UNIT_OF_MEASUREMENT,
) )
from .. import template_ns from .. import template_ns
@@ -48,12 +46,7 @@ def validate(config):
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.with_visibility( number.number_schema(TemplateNumber)
number.number_schema(TemplateNumber),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
)
.extend( .extend(
{ {
cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MAX_VALUE): cv.float_,
+4 -18
View File
@@ -2,16 +2,7 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
CONF_ID,
CONF_LAMBDA,
CONF_STATE,
CONF_STATE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
)
from .. import template_ns from .. import template_ns
@@ -20,14 +11,9 @@ TemplateSensor = template_ns.class_(
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.with_visibility( sensor.sensor_schema(
sensor.sensor_schema(TemplateSensor, accuracy_decimals=1), TemplateSensor,
cv.Visibility.UI, accuracy_decimals=1,
CONF_UNIT_OF_MEASUREMENT,
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_STATE_CLASS,
CONF_FORCE_UPDATE,
) )
.extend( .extend(
{ {
@@ -4,7 +4,6 @@ from esphome.components import switch
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ASSUMED_STATE, CONF_ASSUMED_STATE,
CONF_DEVICE_CLASS,
CONF_ID, CONF_ID,
CONF_LAMBDA, CONF_LAMBDA,
CONF_OPTIMISTIC, CONF_OPTIMISTIC,
@@ -32,11 +31,7 @@ def validate(config):
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.with_visibility( switch.switch_schema(TemplateSwitch)
switch.switch_schema(TemplateSwitch),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
.extend( .extend(
{ {
cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_LAMBDA): cv.returning_lambda,
@@ -3,7 +3,7 @@ import esphome.codegen as cg
from esphome.components import text_sensor from esphome.components import text_sensor
from esphome.components.text_sensor import TextSensorPublishAction from esphome.components.text_sensor import TextSensorPublishAction
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_STATE from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
from .. import template_ns from .. import template_ns
@@ -12,11 +12,7 @@ TemplateTextSensor = template_ns.class_(
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.with_visibility( text_sensor.text_sensor_schema()
text_sensor.text_sensor_schema(),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
.extend( .extend(
{ {
cv.GenerateID(): cv.declare_id(TemplateTextSensor), cv.GenerateID(): cv.declare_id(TemplateTextSensor),
@@ -6,7 +6,6 @@ from esphome.const import (
CONF_ASSUMED_STATE, CONF_ASSUMED_STATE,
CONF_CLOSE_ACTION, CONF_CLOSE_ACTION,
CONF_CURRENT_OPERATION, CONF_CURRENT_OPERATION,
CONF_DEVICE_CLASS,
CONF_ID, CONF_ID,
CONF_LAMBDA, CONF_LAMBDA,
CONF_OPEN_ACTION, CONF_OPEN_ACTION,
@@ -37,11 +36,7 @@ CONF_HAS_POSITION = "has_position"
CONF_TOGGLE_ACTION = "toggle_action" CONF_TOGGLE_ACTION = "toggle_action"
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.with_visibility( valve.valve_schema(TemplateValve)
valve.valve_schema(TemplateValve),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
.extend( .extend(
{ {
cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_LAMBDA): cv.returning_lambda,
+3 -14
View File
@@ -1,13 +1,10 @@
#include "tuya.h" #include "tuya.h"
#include "esphome/components/network/util.h"
#include "esphome/core/gpio.h" #include "esphome/core/gpio.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#ifdef USE_NETWORK
#include "esphome/components/network/util.h"
#endif
#ifdef USE_WIFI #ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h" #include "esphome/components/wifi/wifi_component.h"
#endif #endif
@@ -25,14 +22,6 @@ static const int MAX_RETRIES = 5;
// Max bytes to log for datapoint values (larger values are truncated) // Max bytes to log for datapoint values (larger values are truncated)
static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16; static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16;
static bool network_is_connected() {
#ifdef USE_NETWORK
return network::is_connected();
#else
return false;
#endif
}
void Tuya::setup() { void Tuya::setup() {
this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
if (this->status_pin_ != nullptr) { if (this->status_pin_ != nullptr) {
@@ -565,14 +554,14 @@ void Tuya::send_empty_command_(TuyaCommandType command) {
} }
void Tuya::set_status_pin_() { void Tuya::set_status_pin_() {
bool is_network_ready = network_is_connected() && remote_is_connected(); bool is_network_ready = network::is_connected() && remote_is_connected();
this->status_pin_->digital_write(is_network_ready); this->status_pin_->digital_write(is_network_ready);
} }
uint8_t Tuya::get_wifi_status_code_() { uint8_t Tuya::get_wifi_status_code_() {
uint8_t status = 0x02; uint8_t status = 0x02;
if (network_is_connected()) { if (network::is_connected()) {
status = 0x03; status = 0x03;
// Protocol version 3 also supports specifying when connected to "the cloud" // Protocol version 3 also supports specifying when connected to "the cloud"
+12 -4
View File
@@ -1,4 +1,5 @@
from typing import Any from collections.abc import Callable
from typing import Any, NoReturn
from esphome import automation from esphome import automation
from esphome.automation import Trigger from esphome.automation import Trigger
@@ -47,10 +48,17 @@ UDP_SCHEMA = cv.Schema(
) )
def is_relocated(option: str) -> Callable[[Any], NoReturn]:
def validator(value: Any) -> NoReturn:
raise cv.Invalid(
f"The '{option}' option should now be configured in the 'packet_transport' component"
)
return validator
RELOCATED = { RELOCATED = {
cv.Optional(x): cv.invalid( cv.Optional(x): is_relocated(x)
f"The '{x}' option should now be configured in the 'packet_transport' component"
)
for x in ( for x in (
CONF_PROVIDERS, CONF_PROVIDERS,
CONF_ENCRYPTION, CONF_ENCRYPTION,
+2 -51
View File
@@ -26,8 +26,6 @@ USBClient = usb_host_ns.class_("USBClient", Component)
DOMAIN = "usb_host" DOMAIN = "usb_host"
CONF_VID = "vid" CONF_VID = "vid"
CONF_PID = "pid" CONF_PID = "pid"
CONF_MANUFACTURER = "manufacturer"
CONF_PRODUCT = "product"
CONF_ENABLE_HUBS = "enable_hubs" CONF_ENABLE_HUBS = "enable_hubs"
CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests" CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests"
CONF_MAX_PACKET_SIZE = "max_packet_size" CONF_MAX_PACKET_SIZE = "max_packet_size"
@@ -49,48 +47,7 @@ def usb_device_schema(
schema = schema.extend({cv.Optional(CONF_PID, default=pid): cv.hex_uint16_t}) schema = schema.extend({cv.Optional(CONF_PID, default=pid): cv.hex_uint16_t})
else: else:
schema = schema.extend({cv.Required(CONF_PID): cv.hex_uint16_t}) schema = schema.extend({cv.Required(CONF_PID): cv.hex_uint16_t})
return schema
return schema.extend(
{
cv.Optional(CONF_MANUFACTURER): cv.string_strict,
cv.Optional(CONF_PRODUCT): cv.string_strict,
}
)
_validate_filters_complete = cv.has_none_or_all_keys(CONF_MANUFACTURER, CONF_PRODUCT)
def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
"""Reject invalid USB configuration."""
for config in configs:
_validate_filters_complete(config)
for index, first in enumerate(configs):
# Ensure matching logic does not overlap between entries
for second in configs[index + 1 :]:
if (
not (first[CONF_VID] == 0 and first[CONF_PID] == 0)
and not (second[CONF_VID] == 0 and second[CONF_PID] == 0)
and (
first[CONF_VID] != second[CONF_VID]
or first[CONF_PID] != second[CONF_PID]
)
):
continue
# An unset filter constrains nothing, so only a differing value separates them
if not all(
(a := first.get(key)) is None
or (b := second.get(key)) is None
or a == b
for key in (CONF_MANUFACTURER, CONF_PRODUCT)
):
continue
raise cv.Invalid(
f"USB configs overlap: {first[CONF_ID]!r}, {second[CONF_ID]!r}"
)
return configs
def _set_max_packet_size(config: dict) -> dict: def _set_max_packet_size(config: dict) -> dict:
@@ -115,9 +72,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAX_PACKET_SIZE, default=64): cv.one_of( cv.Optional(CONF_MAX_PACKET_SIZE, default=64): cv.one_of(
64, 128, 256, 512, 1024, int=True 64, 128, 256, 512, 1024, int=True
), ),
cv.Optional(CONF_DEVICES): cv.All( cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
cv.ensure_list(usb_device_schema()), validate_usb_clients
),
} }
), ),
only_on_variant( only_on_variant(
@@ -136,10 +91,6 @@ CONFIG_SCHEMA = cv.All(
async def register_usb_client(config: ConfigType) -> MockObj: async def register_usb_client(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_VID], config[CONF_PID]) var = cg.new_Pvariable(config[CONF_ID], config[CONF_VID], config[CONF_PID])
await cg.register_component(var, config) await cg.register_component(var, config)
if (manufacturer := config.get(CONF_MANUFACTURER)) is not None:
cg.add(var.set_manufacturer_filter(manufacturer))
if (product := config.get(CONF_PRODUCT)) is not None:
cg.add(var.set_product_filter(product))
return var return var
-29
View File
@@ -117,20 +117,6 @@ struct UsbEvent {
// callback function type. // callback function type.
// USB string descriptors hold at most 126 characters; one more for the terminator
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
/// Identity of a connected USB device, copied out of the descriptors the USB host
/// stack caches for the lifetime of the connection
struct UsbDeviceInfo {
uint16_t vendor_id;
uint16_t product_id;
uint16_t bcd_device;
char manufacturer[DESC_STRING_BUF_SIZE];
char product[DESC_STRING_BUF_SIZE];
char serial_number[DESC_STRING_BUF_SIZE];
};
enum ClientState { enum ClientState {
USB_CLIENT_INIT = 0, USB_CLIENT_INIT = 0,
USB_CLIENT_OPEN, USB_CLIENT_OPEN,
@@ -158,15 +144,6 @@ class USBClient : public Component {
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
const std::vector<uint8_t> &data = {}); const std::vector<uint8_t> &data = {});
/// Copy the connected device's identity out of the cached USB descriptors.
/// Returns false when no device is connected.
bool get_device_info(UsbDeviceInfo &info) const;
/// Narrow which device this client claims, beyond the VID/PID it was constructed
/// with, by requiring a descriptor string to match exactly.
void set_manufacturer_filter(const char *manufacturer) { this->manufacturer_filter_ = manufacturer; }
void set_product_filter(const char *product) { this->product_filter_ = product; }
// Lock-free event queue and pool for USB task to main loop communication // Lock-free event queue and pool for USB task to main loop communication
// Must be public for access from static callbacks // Must be public for access from static callbacks
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue; LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
@@ -184,9 +161,6 @@ class USBClient : public Component {
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
virtual void disconnect(); virtual void disconnect();
virtual void on_connected() {} virtual void on_connected() {}
/// Whether the device's descriptor strings satisfy every filter that is set.
bool descriptor_strings_match_(const usb_device_info_t &dev_info) const;
virtual void on_disconnected() { virtual void on_disconnected() {
// Reset all requests to available (all bits to 0) // Reset all requests to available (all bits to 0)
this->trq_in_use_.store(0); this->trq_in_use_.store(0);
@@ -207,9 +181,6 @@ class USBClient : public Component {
// Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available
// Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate)
std::atomic<trq_bitmask_t> trq_in_use_; std::atomic<trq_bitmask_t> trq_in_use_;
// Descriptor strings a device must report to be claimed; nullptr means no constraint
const char *manufacturer_filter_{nullptr};
const char *product_filter_{nullptr};
uint16_t vid_{}; uint16_t vid_{};
uint16_t pid_{}; uint16_t pid_{};
}; };
@@ -143,8 +143,10 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
} while (next_desc != NULL); } while (next_desc != NULL);
} }
#endif #endif
// bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType), // USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
// so character count = (bLength - 2) / 2. // Character count = (bLength - 2) / 2, max 126 chars + null terminator.
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) { static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
if (desc == nullptr || desc->bLength < 2) if (desc == nullptr || desc->bLength < 2)
return "(unspecified)"; return "(unspecified)";
@@ -160,61 +162,6 @@ static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<c
return buffer.data(); return buffer.data();
} }
// A missing descriptor copies as an empty string, unlike the "(unspecified)"
// placeholder the logging helper above uses
static void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
buffer[0] = '\0';
if (desc == nullptr || desc->bLength < 2)
return;
int char_count = (desc->bLength - 2) / 2;
char *p = buffer.data();
char *end = p + buffer.size() - 1;
for (int i = 0; i != char_count && p < end; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
}
// Descriptor strings are UTF-16, so a character above Latin-1 can never match.
static bool descriptor_string_equals(const usb_str_desc_t *desc, const char *expected) {
const int char_count = (desc == nullptr || desc->bLength < 2) ? 0 : (desc->bLength - 2) / 2;
for (int i = 0; i != char_count; i++) {
const uint16_t c = desc->wData[i];
if (c >= 0x100 || expected[i] == '\0' || static_cast<char>(c) != expected[i])
return false;
}
return expected[char_count] == '\0';
}
bool USBClient::descriptor_strings_match_(const usb_device_info_t &dev_info) const {
if (this->manufacturer_filter_ != nullptr &&
!descriptor_string_equals(dev_info.str_desc_manufacturer, this->manufacturer_filter_))
return false;
if (this->product_filter_ != nullptr && !descriptor_string_equals(dev_info.str_desc_product, this->product_filter_))
return false;
return true;
}
bool USBClient::get_device_info(UsbDeviceInfo &info) const {
if (this->state_ != USB_CLIENT_CONNECTED)
return false;
const usb_device_desc_t *desc;
if (usb_host_get_device_descriptor(this->device_handle_, &desc) != ESP_OK)
return false;
info.vendor_id = desc->idVendor;
info.product_id = desc->idProduct;
info.bcd_device = desc->bcdDevice;
usb_device_info_t dev_info;
if (usb_host_device_info(this->device_handle_, &dev_info) != ESP_OK)
return false;
copy_descriptor_string(dev_info.str_desc_manufacturer, info.manufacturer);
copy_descriptor_string(dev_info.str_desc_product, info.product);
copy_descriptor_string(dev_info.str_desc_serial_num, info.serial_number);
return true;
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) // CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) { static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
auto *client = static_cast<USBClient *>(ptr); auto *client = static_cast<USBClient *>(ptr);
@@ -369,16 +316,6 @@ void USBClient::handle_open_state_() {
this->disconnect(); this->disconnect();
return; return;
} }
// Scoped so the buffers do not outlive this cold branch
if (!this->descriptor_strings_match_(dev_info)) {
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
ESP_LOGD(TAG, "Device does not match filter, closing. Manuf: %s; Prod: %s",
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
get_descriptor_string(dev_info.str_desc_product, buf_product));
this->disconnect();
return;
}
this->state_ = USB_CLIENT_CONNECTED; this->state_ = USB_CLIENT_CONNECTED;
char buf_manuf[DESC_STRING_BUF_SIZE]; char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE]; char buf_product[DESC_STRING_BUF_SIZE];
@@ -620,12 +557,6 @@ void USBClient::dump_config() {
" Vendor id %04X\n" " Vendor id %04X\n"
" Product id %04X", " Product id %04X",
this->vid_, this->pid_); this->vid_, this->pid_);
if (this->manufacturer_filter_ != nullptr) {
ESP_LOGCONFIG(TAG, " Manufacturer %s", this->manufacturer_filter_);
}
if (this->product_filter_ != nullptr) {
ESP_LOGCONFIG(TAG, " Product %s", this->product_filter_);
}
} }
// THREAD CONTEXT: Called from both USB task and main loop threads // THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediately after transfer callback completes // - USB task: Immediately after transfer callback completes
+11 -25
View File
@@ -6,7 +6,6 @@ from esphome.components.usb_host import (
get_max_packet_size, get_max_packet_size,
register_usb_client, register_usb_client,
usb_device_schema, usb_device_schema,
validate_usb_clients,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -17,7 +16,7 @@ from esphome.const import (
CONF_DUMMY_RECEIVER, CONF_DUMMY_RECEIVER,
CONF_ID, CONF_ID,
) )
from esphome.core import CORE, ID from esphome.core import CORE
from esphome.cpp_types import Component from esphome.cpp_types import Component
from esphome.types import ConfigType from esphome.types import ConfigType
@@ -28,16 +27,6 @@ usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component) USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component)
USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent) USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent)
def is_usb_uart_channel(uart_id: ID, full_config: ConfigType) -> bool:
"""Return True if the given ID refers to a channel of a configured usb_uart device."""
return any(
channel[CONF_ID] == uart_id
for device in full_config.get("usb_uart") or []
for channel in device[CONF_CHANNELS]
)
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions") UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
UART_PARITY_OPTIONS = { UART_PARITY_OPTIONS = {
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE, "NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
@@ -155,19 +144,16 @@ def channel_schema(type_: "Type") -> cv.Schema:
) )
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.ensure_list(
cv.ensure_list( cv.typed_schema(
cv.typed_schema( {
{ it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
it.name: usb_device_schema(it.cls, it.vid, it.pid).extend( channel_schema(it)
channel_schema(it) )
) for it in uart_types
for it in uart_types },
}, upper=True,
upper=True, )
)
),
validate_usb_clients,
) )
+1 -1
View File
@@ -242,7 +242,7 @@ void USBUartComponent::loop() {
this->chunk_pool_.release(chunk); this->chunk_pool_.release(chunk);
// Invoke the RX callback (if registered) immediately after data lands in the // Invoke the RX callback (if registered) immediately after data lands in the
// ring buffer. This lets consumers such as ZigbeeProxyTap process incoming bytes // ring buffer. This lets consumers such as ZigbeeProxy process incoming bytes
// in the same loop iteration they are delivered, avoiding an extra wakeup cycle. // in the same loop iteration they are delivered, avoiding an extra wakeup cycle.
if (channel->rx_callback_) { if (channel->rx_callback_) {
channel->rx_callback_(); channel->rx_callback_();
+1 -4
View File
@@ -163,13 +163,10 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
/// Register a callback invoked immediately after data is pushed to the input ring buffer. /// Register a callback invoked immediately after data is pushed to the input ring buffer.
/// Called from USBUartComponent::loop() in the main loop context. /// Called from USBUartComponent::loop() in the main loop context.
/// Allows consumers (e.g. ZigbeeProxyTap) to process bytes in the same loop iteration /// Allows consumers (e.g. ZigbeeProxy) to process bytes in the same loop iteration
/// they arrive, eliminating one full main-loop-wakeup cycle of latency. /// they arrive, eliminating one full main-loop-wakeup cycle of latency.
void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); } void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); }
/// Channel index on the bridge (interface number on multi-port bridges)
uint8_t get_index() const { return this->index_; }
protected: protected:
// Not directly instantiable; construct a concrete channel type instead. // Not directly instantiable; construct a concrete channel type instead.
USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {} USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
@@ -1,7 +1,6 @@
#include "zigbee_time_zephyr.h" #include "zigbee_time_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME) #if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME)
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::zigbee { namespace esphome::zigbee {
@@ -48,7 +47,6 @@ void ZigbeeTime::set_epoch_time(uint32_t epoch) {
this->synchronize_epoch_(epoch); this->synchronize_epoch_(epoch);
this->has_time_ = true; this->has_time_ = true;
}); });
App.wake_loop_threadsafe();
} }
void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) { void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) {
+1 -4
View File
@@ -49,8 +49,7 @@ void ZigbeeComponent::factory_reset() {
void ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(ezb_bdb_comm_mode_mask_t mode) { void ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(ezb_bdb_comm_mode_mask_t mode) {
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) { if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
global_zigbee->set_timeout("zb_init", 100, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); }); global_zigbee->set_timeout("zb_init", 10, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); });
App.wake_loop_threadsafe();
return; return;
} }
if (ezb_bdb_start_top_level_commissioning(mode) != EZB_ERR_NONE) { if (ezb_bdb_start_top_level_commissioning(mode) != EZB_ERR_NONE) {
@@ -89,7 +88,6 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
global_zigbee->set_timeout("zb_init", 1000, []() { global_zigbee->set_timeout("zb_init", 1000, []() {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_INITIALIZATION); ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_INITIALIZATION);
}); });
App.wake_loop_threadsafe();
} }
} break; } break;
case EZB_BDB_SIGNAL_STEERING: { case EZB_BDB_SIGNAL_STEERING: {
@@ -115,7 +113,6 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING); ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING);
}); });
} }
App.wake_loop_threadsafe();
} }
} break; } break;
case EZB_ZDO_SIGNAL_LEAVE: { case EZB_ZDO_SIGNAL_LEAVE: {
+2 -4
View File
@@ -1,10 +1,10 @@
#include "zigbee_zephyr.h" #include "zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) #if defined(USE_ZIGBEE) && defined(USE_NRF52)
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <zephyr/settings/settings.h> #include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h> #include <zephyr/storage/flash_map.h>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/wake.h"
extern "C" { extern "C" {
#include <zboss_api.h> #include <zboss_api.h>
@@ -120,7 +120,7 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
/* Set default response value. */ /* Set default response value. */
p_device_cb_param->status = RET_OK; p_device_cb_param->status = RET_OK;
App.wake_loop_threadsafe(); esphome::wake_loop_threadsafe();
// endpoints are enumerated from 1 // endpoints are enumerated from 1
if (global_zigbee->callbacks_.size() >= endpoint) { if (global_zigbee->callbacks_.size() >= endpoint) {
@@ -138,7 +138,6 @@ void ZigbeeComponent::on_join_(bool factory_new) {
ESP_LOGD(TAG, "Joined the network"); ESP_LOGD(TAG, "Joined the network");
this->join_cb_.call(factory_new); this->join_cb_.call(factory_new);
}); });
App.wake_loop_threadsafe();
} }
void ZigbeeComponent::on_start_() { void ZigbeeComponent::on_start_() {
@@ -146,7 +145,6 @@ void ZigbeeComponent::on_start_() {
ESP_LOGD(TAG, "Started zigbee stack"); ESP_LOGD(TAG, "Started zigbee stack");
this->start_cb_.call(); this->start_cb_.call();
}); });
App.wake_loop_threadsafe();
} }
#ifdef USE_ZIGBEE_WIPE_ON_BOOT #ifdef USE_ZIGBEE_WIPE_ON_BOOT
@@ -1,47 +0,0 @@
import esphome.codegen as cg
from esphome.components import serial_proxy
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["serial_proxy"]
CONF_SERIAL_PROXY_ID = "serial_proxy_id"
zigbee_proxy_tap_ns = cg.esphome_ns.namespace("zigbee_proxy_tap")
ZigbeeProxyTap = zigbee_proxy_tap_ns.class_(
"ZigbeeProxyTap", cg.Component, serial_proxy.SerialProxyTap
)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
if (wifi_conf := full_config.get(CONF_WIFI)) and (
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
):
raise cv.Invalid(
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Zigbee proxy"
)
return config
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ZigbeeProxyTap),
cv.Required(CONF_SERIAL_PROXY_ID): cv.use_id(serial_proxy.SerialProxy),
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
sp = await cg.get_variable(config[CONF_SERIAL_PROXY_ID])
var = cg.new_Pvariable(config[CONF_ID], sp)
await cg.register_component(var, config)
cg.add_define("USE_ZIGBEE_PROXY_TAP")
# Compiles the tap interface into serial_proxy; without it the port is a plain byte pipe
cg.add_define("USE_SERIAL_PROXY_TAP")
@@ -1,250 +0,0 @@
#include "ash_detector.h"
#ifdef USE_ZIGBEE_PROXY_TAP
namespace esphome::zigbee_proxy_tap {
// Control byte of an RSTACK, and the only ASH version byte that can follow it
static constexpr uint8_t ASH_RSTACK_CONTROL = 0xC1;
static constexpr uint8_t ASH_PROTOCOL_VERSION = 0x02;
static constexpr size_t ASH_RSTACK_BODY_SIZE = 3; // control, version, reset code
static constexpr size_t ASH_CRC_SIZE = 2;
// Smallest legal frame on the wire: a bare control byte plus its CRC
static constexpr size_t ASH_MIN_FRAME_SIZE = 1 + ASH_CRC_SIZE;
// The opening EZSP version command is a constant: control 0x00 (frmNum 0, ackNum 0)
// followed by [seq=0][frameControl=0][frameId=0] randomized by 0x42 0x21 0xA8. Only the
// trailing requested-version byte varies, so the first four bytes pin the frame exactly.
static constexpr uint8_t EZSP_VERSION_CMD_PREFIX[] = {0x00, 0x42, 0x21, 0xA8};
static constexpr size_t EZSP_VERSION_CMD_SIZE = 5;
// Consecutive frames we could not accept, with neither a good frame nor a retransmission
// in between, before concluding the peer is no longer speaking ASH. A real ASH peer must
// retransmit an unacknowledged frame, so the absence of one is the positive evidence
// here -- garbage on the line is not, since noise proves nothing either way.
static constexpr uint8_t MAX_UNCONFIRMED_REJECTS = 4;
static bool ash_reset_code_is_known(uint8_t code) {
switch (code) {
case 0x00: // RESET_UNKNOWN
case 0x01: // RESET_EXTERNAL
case 0x02: // RESET_POWER_ON
case 0x03: // RESET_WATCHDOG
case 0x06: // RESET_ASSERT
case 0x09: // RESET_BOOTLOADER
case 0x0B: // RESET_SOFTWARE
case 0x51: // ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT
case 0x80: // ERROR_CHIP_SPECIFIC
case 0x81: // RESET_CHIP_SPECIFIC
return true;
default:
return false;
}
}
void AshFrameScanner::begin_frame_() {
this->index_ = 0;
this->crc_ = ASH_CRC_INIT;
this->escaped_ = false;
this->poisoned_ = false;
}
void AshFrameScanner::reset() {
this->begin_frame_();
this->frame_length_ = 0;
this->discarding_ = false;
}
ScanResult AshFrameScanner::feed(uint8_t byte) {
if (byte == ASH_FLAG_BYTE) {
// Snapshot everything the verdict depends on: begin_frame_() clears all of it.
const bool discarding = this->discarding_;
const bool poisoned = this->poisoned_;
const bool escaped = this->escaped_;
const size_t index = this->index_;
const uint16_t crc = this->crc_;
// A FLAG always starts the next frame afresh, whatever preceded it
this->begin_frame_();
this->discarding_ = false;
if (discarding || index == 0) {
// Consecutive delimiters carry no frame at all, so there is nothing to judge
this->frame_length_ = 0;
return ScanResult::NONE;
}
// Running the CRC over the body *and* its trailing CRC bytes leaves zero when
// correct, so validity needs no second pass over the frame.
if (poisoned || escaped || index < ASH_MIN_FRAME_SIZE || crc != 0) {
this->frame_length_ = 0;
return ScanResult::INVALID;
}
this->frame_length_ = index - ASH_CRC_SIZE;
return ScanResult::FRAME;
}
if (this->discarding_) {
return ScanResult::NONE;
}
switch (byte) {
case ASH_CANCEL_BYTE:
// Everything received since the last FLAG is to be ignored
this->begin_frame_();
return ScanResult::NONE;
case ASH_SUBSTITUTE_BYTE:
// A low-level error was flagged; ignore everything up to the next FLAG
this->discarding_ = true;
return ScanResult::NONE;
case ASH_XON_BYTE:
case ASH_XOFF_BYTE:
// Transport flow control, not frame content: skip it without disturbing the frame
return ScanResult::NONE;
case ASH_ESCAPE_BYTE:
this->escaped_ = true;
return ScanResult::NONE;
default:
break;
}
uint8_t value = byte;
if (this->escaped_) {
this->escaped_ = false;
value = byte ^ ASH_XOR_BYTE;
// An escape must decode to a reserved byte; anything else is not ASH framing at all
if (!ash_is_reserved(value)) {
this->poisoned_ = true;
return ScanResult::NONE;
}
}
if (this->index_ >= sizeof(this->buffer_)) {
this->poisoned_ = true;
return ScanResult::NONE;
}
this->buffer_[this->index_++] = value;
this->crc_ = ash_crc16(&value, 1, this->crc_);
return ScanResult::NONE;
}
void AshDetector::reset() {
this->ncp_scanner_.reset();
this->host_scanner_.reset();
this->state_ = AshDetectState::IDLE;
this->rx_sequence_ = 0;
this->ack_owed_ = false;
this->unconfirmed_rejects_ = 0;
}
void AshDetector::from_ncp(uint8_t byte) {
switch (this->ncp_scanner_.feed(byte)) {
case ScanResult::FRAME:
this->handle_ncp_frame_();
break;
case ScanResult::INVALID:
// A delimited chunk that is not a frame. While armed this may be a corrupted ASH
// frame, which the peer will retransmit, or a sign the peer stopped speaking ASH.
// reject_() distinguishes the two by whether a retransmission ever arrives.
this->reject_();
break;
case ScanResult::NONE:
break;
}
}
void AshDetector::handle_ncp_frame_() {
const uint8_t *body = this->ncp_scanner_.frame();
const size_t length = this->ncp_scanner_.length();
const uint8_t control = body[0];
// RSTACK is the only way into the handshake, and the only way back after a firmware
// swap: a Spinel or bootloader NCP never emits one, so those stay unarmed forever.
if (control == ASH_RSTACK_CONTROL) {
if (length == ASH_RSTACK_BODY_SIZE && body[1] == ASH_PROTOCOL_VERSION && ash_reset_code_is_known(body[2])) {
this->state_ = AshDetectState::SAW_RSTACK;
this->rx_sequence_ = 0;
this->ack_owed_ = false;
this->unconfirmed_rejects_ = 0;
}
return;
}
if (this->state_ != AshDetectState::ARMED) {
return;
}
if ((control & 0x80) != 0) {
return; // ACK/NAK/RST/ERROR: nothing is owed for these
}
const uint8_t frame_num = (control >> 4) & ASH_MAX_SEQUENCE;
const bool re_tx = (control & 0x08) != 0;
if (frame_num != this->rx_sequence_) {
// A retransmission still proves the peer is speaking ASH even though we cannot use
// this copy, so it clears the suspicion without being acknowledged.
if (re_tx) {
this->unconfirmed_rejects_ = 0;
} else {
this->reject_();
}
return;
}
this->rx_sequence_ = (this->rx_sequence_ + 1) & ASH_MAX_SEQUENCE;
this->pending_ack_ = this->rx_sequence_;
this->ack_owed_ = true;
this->unconfirmed_rejects_ = 0;
}
void AshDetector::reject_() {
if (this->state_ != AshDetectState::ARMED) {
return;
}
if (++this->unconfirmed_rejects_ >= MAX_UNCONFIRMED_REJECTS) {
this->state_ = AshDetectState::IDLE;
this->unconfirmed_rejects_ = 0;
}
}
void AshDetector::from_host(uint8_t byte) {
if (this->host_scanner_.feed(byte) != ScanResult::FRAME) {
return;
}
if (this->state_ != AshDetectState::SAW_RSTACK) {
return;
}
const uint8_t *body = this->host_scanner_.frame();
if (this->host_scanner_.length() != EZSP_VERSION_CMD_SIZE) {
return;
}
for (size_t i = 0; i < sizeof(EZSP_VERSION_CMD_PREFIX); i++) {
if (body[i] != EZSP_VERSION_CMD_PREFIX[i]) {
return;
}
}
this->state_ = AshDetectState::ARMED;
this->rx_sequence_ = 0;
this->ack_owed_ = false;
this->unconfirmed_rejects_ = 0;
}
bool AshDetector::take_pending_ack(uint8_t &ack_num) {
if (!this->ack_owed_) {
return false;
}
this->ack_owed_ = false;
ack_num = this->pending_ack_;
return true;
}
} // namespace esphome::zigbee_proxy_tap
#endif // USE_ZIGBEE_PROXY_TAP
@@ -1,104 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZIGBEE_PROXY_TAP
#include "ash_protocol.h"
#include <cstddef>
#include <cstdint>
namespace esphome::zigbee_proxy_tap {
// Decides when it is safe to acknowledge NCP frames on a client's behalf.
//
// The client suppresses its own ACKs, so nobody else will send them, and injecting ASH
// bytes into a stream that is not ASH would corrupt it. Detection is therefore one-sided:
// arm only on the session handshake, which is a fixed byte string, and never on frame
// validity, which non-ASH traffic can satisfy by luck.
//
// RSTACK (NCP -> host) c1 02 <reset_code> <crc> 7e
// version (host -> NCP) 00 42 21 a8 <version^0x54> <crc> 7e
//
// Requiring both, in that order, in opposite directions cannot be satisfied by a
// unidirectional byte stream whatever it contains -- which is exactly the situation
// during a firmware upload. Verified against real .gbl images and real Spinel traffic:
// zero false arms, and neither pattern occurs even as a substring.
//
// Getting it wrong in the other direction is cheap: a frame we decline to acknowledge is
// retransmitted by the NCP, so we see a clean copy and lose only the ack timeout. That
// asymmetry is why this errs towards silence everywhere.
enum class AshDetectState : uint8_t {
IDLE, // Not ASH, or not yet proven to be
SAW_RSTACK, // Handshake half-complete; watching for the version command
ARMED, // Session confirmed; acknowledging on the client's behalf
};
enum class ScanResult : uint8_t {
NONE, // Mid-frame, or a delimiter that carried nothing
FRAME, // frame()/length() hold a complete body with a verified CRC
INVALID, // A delimited chunk arrived but was not a well-formed ASH frame
};
// Reassembles one direction of the byte stream into unstuffed, CRC-checked frames.
// Mirrors bellows' AshProtocol.data_received: FLAG ends a frame, CANCEL discards what
// precedes it, SUBSTITUTE poisons everything up to the next FLAG, and XON/XOFF are
// transport flow control removed without disturbing the frame around them.
class AshFrameScanner {
public:
ScanResult feed(uint8_t byte);
void reset();
// Valid only until the next feed() call, which begins overwriting the buffer.
const uint8_t *frame() const { return this->buffer_; }
size_t length() const { return this->frame_length_; }
private:
void begin_frame_();
// Frames are bounded by the ASH maximum, so a stream carrying no delimiters cannot
// grow the buffer without limit; it just keeps failing.
uint8_t buffer_[MAX_ASH_FRAME_SIZE];
size_t index_{0}; // accumulation position for the frame being read
size_t frame_length_{0}; // body length of the last completed frame
uint16_t crc_{ASH_CRC_INIT};
bool escaped_{false};
bool discarding_{false};
bool poisoned_{false};
};
class AshDetector {
public:
void reset();
// Feed observed traffic. Neither call gates forwarding: the detector only watches.
void from_ncp(uint8_t byte);
void from_host(uint8_t byte);
bool armed() const { return this->state_ == AshDetectState::ARMED; }
// True only while the host direction can affect the state machine, i.e. while waiting
// for the version command. Lets the caller skip scanning that direction entirely the
// rest of the time -- it is the one carrying firmware uploads.
bool needs_host_scan() const { return this->state_ == AshDetectState::SAW_RSTACK; }
// An acknowledgement became owed after the last from_ncp() call. Clears the flag.
bool take_pending_ack(uint8_t &ack_num);
protected:
void handle_ncp_frame_();
void reject_();
AshFrameScanner ncp_scanner_;
AshFrameScanner host_scanner_;
AshDetectState state_{AshDetectState::IDLE};
uint8_t rx_sequence_{0};
uint8_t pending_ack_{0};
uint8_t unconfirmed_rejects_{0};
bool ack_owed_{false};
};
} // namespace esphome::zigbee_proxy_tap
#endif // USE_ZIGBEE_PROXY_TAP
@@ -1,41 +0,0 @@
#include "ash_protocol.h"
namespace esphome::zigbee_proxy_tap {
static const uint16_t CRC_NIBBLE_TABLE[16] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF};
uint16_t ash_crc16(const uint8_t *data, size_t length, uint16_t init) {
uint16_t crc = init;
for (size_t i = 0; i < length; i++) {
crc = static_cast<uint16_t>(crc << 4) ^ CRC_NIBBLE_TABLE[(crc >> 12) ^ (data[i] >> 4)];
crc = static_cast<uint16_t>(crc << 4) ^ CRC_NIBBLE_TABLE[(crc >> 12) ^ (data[i] & 0x0F)];
}
return crc;
}
// Appends a byte with ASH stuffing; the caller sized `output` for the worst case.
static void append_byte_stuffed(uint8_t *output, size_t &pos, uint8_t byte) {
if (ash_is_reserved(byte)) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = byte ^ ASH_XOR_BYTE;
} else {
output[pos++] = byte;
}
}
size_t ash_build_ack_frame(uint8_t *output, uint8_t ack_num) {
// ACK control byte: 100nrPPP, where PPP is the next frame number expected
const uint8_t control = 0x80 | (ack_num & ASH_MAX_SEQUENCE);
const uint16_t crc = ash_crc16(&control, 1);
size_t pos = 0;
output[pos++] = ASH_FLAG_BYTE;
append_byte_stuffed(output, pos, control);
append_byte_stuffed(output, pos, (crc >> 8) & 0xFF);
append_byte_stuffed(output, pos, crc & 0xFF);
output[pos++] = ASH_FLAG_BYTE;
return pos;
}
} // namespace esphome::zigbee_proxy_tap
@@ -1,51 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::zigbee_proxy_tap {
// ASH Protocol Constants
static constexpr uint8_t ASH_FLAG_BYTE = 0x7E; // Frame delimiter
static constexpr uint8_t ASH_ESCAPE_BYTE = 0x7D; // Escape/substitution byte
static constexpr uint8_t ASH_XOR_BYTE = 0x20; // XOR mask for escaped bytes
static constexpr uint8_t ASH_SUBSTITUTE_BYTE = 0x18; // Substitution for invalid bytes
static constexpr uint8_t ASH_XON_BYTE = 0x11; // Resume transmission
static constexpr uint8_t ASH_XOFF_BYTE = 0x13; // Pause transmission
static constexpr uint8_t ASH_CANCEL_BYTE = 0x1A; // Discards the partial frame before it
// A reserved byte can never appear literally inside a frame; it is escaped as
// ESCAPE followed by the byte XOR 0x20. Rejecting frames that contain one is what
// eliminates most non-ASH traffic before its CRC is ever computed: real firmware
// images and Spinel payloads are dense in 0x11/0x13/0x18/0x1A.
inline bool ash_is_reserved(uint8_t byte) {
return byte == ASH_FLAG_BYTE || byte == ASH_ESCAPE_BYTE || byte == ASH_XON_BYTE || byte == ASH_XOFF_BYTE ||
byte == ASH_SUBSTITUTE_BYTE || byte == ASH_CANCEL_BYTE;
}
// CRC-CCITT (init 0xFFFF, polynomial 0x1021, transmitted big-endian). Note this is a
// different variant from the Kermit FCS that Spinel/HDLC-lite uses over the same
// 0x7E framing, so Spinel frames systematically fail this check.
uint16_t ash_crc16(const uint8_t *data, size_t length, uint16_t init = 0xFFFF);
// ASH bounds a frame's Data Field at 128 bytes, so the largest body a scanner has to
// hold is that field plus the control byte and the two CRC bytes ahead of the closing
// delimiter. Byte stuffing happens on the wire only and is undone as bytes arrive, so it
// does not enlarge this.
static constexpr size_t ASH_MAX_DATA_FIELD_SIZE = 128;
static constexpr size_t MAX_ASH_FRAME_SIZE = 1 + ASH_MAX_DATA_FIELD_SIZE + 2;
// Protocol limits
static constexpr uint8_t ASH_MAX_SEQUENCE = 7; // 3-bit sequence number (0-7)
static constexpr uint16_t ASH_CRC_INIT = 0xFFFF; // CRC-CCITT initial value
// An ACK is FLAG, control byte, two CRC bytes, FLAG. Every byte but the delimiters may
// need escaping, so the worst case is 2 + 3 * 2 = 8.
static constexpr size_t ASH_ACK_FRAME_MAX_SIZE = 8;
// Writes an ACK frame for `ack_num` into `output`, which must hold at least
// ASH_ACK_FRAME_MAX_SIZE bytes, and returns its length.
size_t ash_build_ack_frame(uint8_t *output, uint8_t ack_num);
} // namespace esphome::zigbee_proxy_tap
@@ -1,61 +0,0 @@
#include "zigbee_proxy_tap.h"
#ifdef USE_ZIGBEE_PROXY_TAP
#include "esphome/core/log.h"
namespace esphome::zigbee_proxy_tap {
static const char *const TAG = "zigbee_proxy_tap";
void ZigbeeProxyTap::setup() { this->parent_->set_tap(this); }
void ZigbeeProxyTap::dump_config() { ESP_LOGCONFIG(TAG, "Zigbee Proxy Tap:\n Port: %s", this->parent_->get_name()); }
void ZigbeeProxyTap::on_device_rx(const uint8_t *data, size_t len) {
for (size_t i = 0; i < len; i++) {
// Observation only: the detector never gates forwarding, so it adds no latency and a
// frame it cannot parse still reaches the client, which judges it for itself.
this->detector_.from_ncp(data[i]);
uint8_t ack_num;
if (this->detector_.take_pending_ack(ack_num)) {
// The client suppresses its own ACKs, so this is the only acknowledgement the NCP
// will see. Only ever sent for a frame that passed CRC and arrived in sequence.
uint8_t frame[ASH_ACK_FRAME_MAX_SIZE];
this->parent_->write_from_tap(frame, ash_build_ack_frame(frame, ack_num));
ESP_LOGV(TAG, "Sent ACK for frame %u", ack_num);
}
}
const bool armed = this->detector_.armed();
if (armed != this->was_armed_) {
this->was_armed_ = armed;
ESP_LOGD(TAG, "ASH session %s",
armed ? LOG_STR_LITERAL("detected, acknowledging frames")
: LOG_STR_LITERAL("lost, no longer acknowledging frames"));
}
}
void ZigbeeProxyTap::on_client_tx(const uint8_t *data, size_t len) {
// Scanning this direction only matters while waiting for the version command that
// completes the handshake. Outside that window it is skipped entirely -- which is what
// makes a firmware upload, all of which flows this way, essentially free.
if (!this->detector_.needs_host_scan()) {
return;
}
for (size_t i = 0; i < len; i++) {
this->detector_.from_host(data[i]);
}
}
void ZigbeeProxyTap::on_protocol_disabled() {
// A client turning protocol handling off is usually about to reflash the radio, so the
// handshake we saw says nothing about what will be on the wire next. Forget it: a real
// ASH session announces itself again with an RSTACK.
this->detector_.reset();
}
} // namespace esphome::zigbee_proxy_tap
#endif // USE_ZIGBEE_PROXY_TAP
@@ -1,50 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZIGBEE_PROXY_TAP
#include "esphome/components/serial_proxy/serial_proxy.h"
#include "esphome/core/component.h"
#include "ash_detector.h"
namespace esphome::zigbee_proxy_tap {
// Acknowledges the ASH frames of an EZSP NCP on behalf of a remote client, so the NCP's
// ack timeout is measured against this device rather than against the network round trip
// to the client. The client suppresses its own acknowledgements, making these the only
// ones the NCP sees.
//
// It never carries client traffic: the serial proxy owns the port and the bytes, and this
// component only observes them. The sole exception is the acknowledgement itself, and it
// is sent only once the handshake has proven the port really is carrying ASH.
class ZigbeeProxyTap : public serial_proxy::SerialProxyTap, public Component {
public:
explicit ZigbeeProxyTap(serial_proxy::SerialProxy *parent) : parent_(parent) {}
void setup() override;
void dump_config() override;
// SerialProxyTap
void on_device_rx(const uint8_t *data, size_t len) override;
void on_client_tx(const uint8_t *data, size_t len) override;
// Acknowledging is only ever useful on a client's behalf, so with nobody subscribed
// there is nothing to do and the port need not be read.
bool tap_needs_port() const override { return false; }
void on_protocol_disabled() override;
protected:
// The port this component observes. Owns the UART and the bytes; every write we make
// goes through it.
serial_proxy::SerialProxy *parent_;
// Decides when acknowledging on the client's behalf is safe. Armed only by the ASH
// session handshake, so a bootloader or Thread NCP never triggers it.
AshDetector detector_;
// Previous armed state, for logging the transitions
bool was_armed_{false};
};
} // namespace esphome::zigbee_proxy_tap
#endif // USE_ZIGBEE_PROXY_TAP
@@ -1,47 +0,0 @@
import esphome.codegen as cg
from esphome.components import serial_proxy
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["serial_proxy"]
CONF_SERIAL_PROXY_ID = "serial_proxy_id"
zwave_proxy_tap_ns = cg.esphome_ns.namespace("zwave_proxy_tap")
ZWaveProxyTap = zwave_proxy_tap_ns.class_(
"ZWaveProxyTap", cg.Component, serial_proxy.SerialProxyTap
)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
if (wifi_conf := full_config.get(CONF_WIFI)) and (
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
):
raise cv.Invalid(
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Z-Wave proxy"
)
return config
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ZWaveProxyTap),
cv.Required(CONF_SERIAL_PROXY_ID): cv.use_id(serial_proxy.SerialProxy),
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
sp = await cg.get_variable(config[CONF_SERIAL_PROXY_ID])
var = cg.new_Pvariable(config[CONF_ID], sp)
await cg.register_component(var, config)
cg.add_define("USE_ZWAVE_PROXY_TAP")
# Compiles the tap interface into serial_proxy; without it the port is a plain byte pipe
cg.add_define("USE_SERIAL_PROXY_TAP")
@@ -1,166 +0,0 @@
#include "zwave_detector.h"
#ifdef USE_ZWAVE_PROXY_TAP
namespace esphome::zwave_proxy_tap {
// Consecutive malformed frames, with no well-formed one in between, before concluding the
// controller is no longer speaking the Serial API. Repeated checksum failures mean our
// idea of where frames begin is wrong, and acknowledging frames we are misreading is
// worse than acknowledging none, so the safe move is to stop and wait to be convinced
// again. A well-formed frame is the evidence that clears the suspicion; garbage is not,
// since noise proves nothing either way.
static constexpr uint8_t MAX_UNCONFIRMED_REJECTS = 4;
// Abandons a frame that stalled part-received, and time-stamps the batch about to be fed.
static void expire_stalled_frame(ZWaveFrameScanner &scanner, uint32_t &frame_start, uint32_t now) {
if (scanner.in_frame()) {
if (now - frame_start <= ZWAVE_FRAME_TIMEOUT_MS) {
return; // Still within its window; keep the start time it already has
}
scanner.reset();
}
frame_start = now;
}
ScanResult ZWaveFrameScanner::feed(uint8_t byte) {
switch (this->state_) {
case ScanState::WAIT_SOF:
// ACK/NAK/CAN and anything else carry no framing, so there is nothing to reassemble
if (byte == ZWAVE_SOF_BYTE) {
this->state_ = ScanState::WAIT_LENGTH;
}
return ScanResult::NONE;
case ScanState::WAIT_LENGTH:
if (byte < ZWAVE_MIN_LENGTH) {
// Not a length the protocol can produce. A 0x01 in this position is far more
// likely to be the real start of a frame than a length, so treat it as one.
this->state_ = byte == ZWAVE_SOF_BYTE ? ScanState::WAIT_LENGTH : ScanState::WAIT_SOF;
return ScanResult::INVALID;
}
this->remaining_ = byte;
this->checksum_ = ZWAVE_CHECKSUM_INIT ^ byte;
this->state_ = ScanState::WAIT_TYPE;
return ScanResult::NONE;
case ScanState::WAIT_TYPE:
this->type_ = byte;
this->checksum_ ^= byte;
this->remaining_--;
this->state_ = ScanState::WAIT_COMMAND;
return ScanResult::NONE;
case ScanState::WAIT_COMMAND:
this->command_ = byte;
this->checksum_ ^= byte;
this->remaining_--;
this->state_ = ScanState::WAIT_BODY;
return ScanResult::NONE;
case ScanState::WAIT_BODY:
break;
}
if (this->remaining_ > 1) {
this->checksum_ ^= byte;
this->remaining_--;
return ScanResult::NONE;
}
// The frame's last byte is its checksum, which the accumulator can be compared against
// directly -- everything it covers has already been folded in.
this->state_ = ScanState::WAIT_SOF;
return byte == this->checksum_ ? ScanResult::FRAME : ScanResult::INVALID;
}
void ZWaveDetector::reset() {
this->device_scanner_.reset();
this->host_scanner_.reset();
this->state_ = ZWaveDetectState::IDLE;
this->pending_command_ = 0;
this->ack_owed_ = false;
this->unconfirmed_rejects_ = 0;
}
void ZWaveDetector::begin_batch(uint32_t now) {
// Both directions, from either caller: a frame stalled in the quiet direction still has
// to expire, and the direction being fed is by definition not stalled.
expire_stalled_frame(this->device_scanner_, this->device_frame_start_, now);
expire_stalled_frame(this->host_scanner_, this->host_frame_start_, now);
}
void ZWaveDetector::from_device(uint8_t byte) {
switch (this->device_scanner_.feed(byte)) {
case ScanResult::FRAME:
this->handle_device_frame_();
break;
case ScanResult::INVALID:
// While armed this may be a corrupted frame, which the controller will retransmit,
// or a sign it stopped speaking the Serial API. reject_() distinguishes the two by
// whether a well-formed frame ever follows.
this->reject_();
break;
case ScanResult::NONE:
break;
}
}
void ZWaveDetector::handle_device_frame_() {
if (this->state_ == ZWaveDetectState::SAW_REQUEST) {
// An unsolicited request from the controller can arrive before the response we are
// waiting for; it is not the other half of the exchange, so it proves nothing.
if (this->device_scanner_.type() != ZWAVE_FRAME_TYPE_RESPONSE ||
this->device_scanner_.command() != this->pending_command_) {
return;
}
this->state_ = ZWaveDetectState::ARMED;
// The host direction stops being scanned from here, so leave nothing part-read behind
this->host_scanner_.reset();
} else if (this->state_ != ZWaveDetectState::ARMED) {
return;
}
// Every well-formed frame is acknowledged, the one that armed us included: the
// controller is already waiting on that one, so answering now saves a retransmit.
this->ack_owed_ = true;
this->unconfirmed_rejects_ = 0;
}
void ZWaveDetector::reject_() {
if (this->state_ != ZWaveDetectState::ARMED) {
return;
}
if (++this->unconfirmed_rejects_ >= MAX_UNCONFIRMED_REJECTS) {
this->state_ = ZWaveDetectState::IDLE;
this->unconfirmed_rejects_ = 0;
this->ack_owed_ = false;
}
}
void ZWaveDetector::from_host(uint8_t byte) {
if (this->host_scanner_.feed(byte) != ScanResult::FRAME) {
return;
}
if (this->state_ == ZWaveDetectState::ARMED) {
return;
}
// Only a request opens an exchange. A later request replaces the one being waited on:
// the host does not repeat a command it has given up on.
if (this->host_scanner_.type() != ZWAVE_FRAME_TYPE_REQUEST) {
return;
}
this->pending_command_ = this->host_scanner_.command();
this->state_ = ZWaveDetectState::SAW_REQUEST;
}
bool ZWaveDetector::take_pending_ack() {
if (!this->ack_owed_) {
return false;
}
this->ack_owed_ = false;
return true;
}
} // namespace esphome::zwave_proxy_tap
#endif // USE_ZWAVE_PROXY_TAP
@@ -1,121 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZWAVE_PROXY_TAP
#include "zwave_protocol.h"
#include <cstdint>
namespace esphome::zwave_proxy_tap {
// Decides when it is safe to acknowledge controller frames on a client's behalf.
//
// The client suppresses its own ACKs, so nobody else will send them, and injecting a
// stray 0x06 into a stream that is not the Serial API would corrupt it. Detection is
// therefore one-sided: arm only on evidence that cannot arise by accident, and never on
// frame validity alone, which other traffic can satisfy by luck.
//
// The Serial API has no fixed opening handshake to key off, but every session is a
// sequence of request/response exchanges, and one of those is evidence enough:
//
// request (host -> ctrl) 01 <len> 00 <cmd> <payload> <chk>
// response (ctrl -> host) 01 <len> 01 <cmd> <payload> <chk>
//
// Requiring a well-formed request and then a well-formed response carrying the same
// command, in opposite directions, cannot be satisfied by a unidirectional byte stream
// whatever it contains -- which is exactly the situation during a firmware upload. It
// also rules out the bootloader, which only ever emits single bytes and menu text and
// never a 0x01-framed multi-byte reply. Keying on the exchange rather than on one
// particular command means it does not matter which command the client opens with.
//
// Getting it wrong in the other direction is cheap: a frame we decline to acknowledge is
// retransmitted by the controller once its ack timeout expires, so we see a clean copy
// and lose only that delay. That asymmetry is why this errs towards silence everywhere,
// including on a bad checksum -- where the zwave_proxy component answers with a NAK, this
// says nothing and lets the timeout do the work.
enum class ZWaveDetectState : uint8_t {
IDLE, // Not the Serial API, or not yet proven to be
SAW_REQUEST, // Exchange half-complete; watching for the matching response
ARMED, // Session confirmed; acknowledging on the client's behalf
};
enum class ScanResult : uint8_t {
NONE, // Mid-frame, or a byte that carried nothing
FRAME, // type()/command() describe a complete frame with a verified checksum
INVALID, // A frame started but was not well formed
};
// Reassembles one direction of the byte stream into checksum-verified frames.
//
// Because the framing is length-prefixed, the length is known before the payload
// arrives, so the checksum can be folded in byte by byte and nothing needs to be
// buffered. Only the two header fields the detector actually reads are kept, which is
// what makes a scanner per direction cost a handful of bytes rather than 257 each.
class ZWaveFrameScanner {
public:
ScanResult feed(uint8_t byte);
void reset() { this->state_ = ScanState::WAIT_SOF; }
/// True while a frame is part-received, so the caller can time it out.
bool in_frame() const { return this->state_ != ScanState::WAIT_SOF; }
// Valid only for the frame the last feed() reported.
uint8_t type() const { return this->type_; }
uint8_t command() const { return this->command_; }
private:
enum class ScanState : uint8_t {
WAIT_SOF,
WAIT_LENGTH,
WAIT_TYPE,
WAIT_COMMAND,
WAIT_BODY, // Payload bytes, then the checksum that ends the frame
};
ScanState state_{ScanState::WAIT_SOF};
uint8_t remaining_{0}; // Bytes of the current frame still to come, checksum included
uint8_t checksum_{ZWAVE_CHECKSUM_INIT};
uint8_t type_{0};
uint8_t command_{0};
};
class ZWaveDetector {
public:
void reset();
/// Time-stamp a batch of observed bytes, before feeding them, so a frame left
/// part-received by an earlier batch is abandoned rather than swallowing this one.
void begin_batch(uint32_t now);
// Feed observed traffic. Neither call gates forwarding: the detector only watches.
void from_device(uint8_t byte);
void from_host(uint8_t byte);
bool armed() const { return this->state_ == ZWaveDetectState::ARMED; }
/// True only while the host direction can still affect the state machine. Lets the
/// caller skip scanning that direction once armed -- it is the busier of the two.
bool needs_host_scan() const { return this->state_ != ZWaveDetectState::ARMED; }
/// An acknowledgement became owed during the last from_device() call. Clears the flag.
bool take_pending_ack();
protected:
void handle_device_frame_();
void reject_();
ZWaveFrameScanner device_scanner_;
ZWaveFrameScanner host_scanner_;
uint32_t device_frame_start_{0};
uint32_t host_frame_start_{0};
ZWaveDetectState state_{ZWaveDetectState::IDLE};
uint8_t pending_command_{0}; // Command of the request awaiting its response
uint8_t unconfirmed_rejects_{0};
bool ack_owed_{false};
};
} // namespace esphome::zwave_proxy_tap
#endif // USE_ZWAVE_PROXY_TAP
@@ -1,33 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::zwave_proxy_tap {
// Z-Wave Serial API framing (INS12350). Unlike ASH, a data frame is length-prefixed
// rather than delimited:
//
// SOF LEN TYPE CMD payload... CHK
//
// LEN counts every byte after itself, the checksum included, so a frame occupies LEN + 2
// bytes on the wire. CHK is the XOR of LEN through the last payload byte, seeded with
// 0xFF. ACK, NAK and CAN stand alone as single bytes and carry no framing of their own.
static constexpr uint8_t ZWAVE_SOF_BYTE = 0x01; // Start of a data frame
static constexpr uint8_t ZWAVE_ACK_BYTE = 0x06; // The only byte this component ever sends
// TYPE field: which half of a request/response exchange the frame is
static constexpr uint8_t ZWAVE_FRAME_TYPE_REQUEST = 0x00;
static constexpr uint8_t ZWAVE_FRAME_TYPE_RESPONSE = 0x01;
// Smallest LEN the protocol can produce: TYPE, CMD and CHK, with no payload
static constexpr uint8_t ZWAVE_MIN_LENGTH = 3;
static constexpr uint8_t ZWAVE_CHECKSUM_INIT = 0xFF;
// The specification requires a receiver to abandon a data frame that has not completed
// this long after its SOF byte. Nothing in the framing marks where a frame ends, so
// without this a truncated frame would swallow the start of the next one.
static constexpr uint32_t ZWAVE_FRAME_TIMEOUT_MS = 1500;
} // namespace esphome::zwave_proxy_tap
@@ -1,62 +0,0 @@
#include "zwave_proxy_tap.h"
#ifdef USE_ZWAVE_PROXY_TAP
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::zwave_proxy_tap {
static const char *const TAG = "zwave_proxy_tap";
void ZWaveProxyTap::setup() { this->parent_->set_tap(this); }
void ZWaveProxyTap::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy Tap:\n Port: %s", this->parent_->get_name()); }
void ZWaveProxyTap::on_device_rx(const uint8_t *data, size_t len) {
this->detector_.begin_batch(App.get_loop_component_start_time());
for (size_t i = 0; i < len; i++) {
// Observation only: the detector never gates forwarding, so it adds no latency and a
// frame it cannot parse still reaches the client, which judges it for itself.
this->detector_.from_device(data[i]);
if (this->detector_.take_pending_ack()) {
// The client suppresses its own ACKs, so this is the only acknowledgement the
// controller will see. Only ever sent for a frame that passed its checksum.
this->parent_->write_from_tap(&ZWAVE_ACK_BYTE, 1);
ESP_LOGV(TAG, "Sent ACK");
}
}
const bool armed = this->detector_.armed();
if (armed != this->was_armed_) {
this->was_armed_ = armed;
ESP_LOGD(TAG, "Serial API session %s",
armed ? LOG_STR_LITERAL("detected, acknowledging frames")
: LOG_STR_LITERAL("lost, no longer acknowledging frames"));
}
}
void ZWaveProxyTap::on_client_tx(const uint8_t *data, size_t len) {
// Scanning this direction only matters until an exchange completes. Once armed it is
// skipped entirely -- which is what makes a firmware upload, all of which flows this
// way, essentially free.
if (!this->detector_.needs_host_scan()) {
return;
}
this->detector_.begin_batch(App.get_loop_component_start_time());
for (size_t i = 0; i < len; i++) {
this->detector_.from_host(data[i]);
}
}
void ZWaveProxyTap::on_protocol_disabled() {
// A client turning protocol handling off is usually about to reflash the controller, so
// the exchange we saw says nothing about what will be on the wire next. Forget it: a
// real session proves itself again with another exchange.
this->detector_.reset();
}
} // namespace esphome::zwave_proxy_tap
#endif // USE_ZWAVE_PROXY_TAP
@@ -1,53 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZWAVE_PROXY_TAP
#include "esphome/components/serial_proxy/serial_proxy.h"
#include "esphome/core/component.h"
#include "zwave_detector.h"
namespace esphome::zwave_proxy_tap {
// Acknowledges the frames of a Z-Wave controller on behalf of a remote client, so the
// controller's ack timeout is measured against this device rather than against the
// network round trip to the client. The client suppresses its own acknowledgements,
// making these the only ones the controller sees.
//
// This is the serial_proxy counterpart of the zwave_proxy component. Where that one owns
// the UART, parses the Serial API in full and carries frames over its own API messages,
// this one only observes: the serial proxy owns the port and the bytes, and carries them
// like those of any other serial device. The sole exception is the acknowledgement
// itself, and it is sent only once a completed request/response exchange has proven the
// port really is carrying the Serial API.
class ZWaveProxyTap : public serial_proxy::SerialProxyTap, public Component {
public:
explicit ZWaveProxyTap(serial_proxy::SerialProxy *parent) : parent_(parent) {}
void setup() override;
void dump_config() override;
// SerialProxyTap
void on_device_rx(const uint8_t *data, size_t len) override;
void on_client_tx(const uint8_t *data, size_t len) override;
// Acknowledging is only ever useful on a client's behalf, so with nobody subscribed
// there is nothing to do and the port need not be read.
bool tap_needs_port() const override { return false; }
void on_protocol_disabled() override;
protected:
// The port this component observes. Owns the UART and the bytes; every write we make
// goes through it.
serial_proxy::SerialProxy *parent_;
// Decides when acknowledging on the client's behalf is safe. Armed only by a completed
// request/response exchange, so a bootloader or a firmware upload never triggers it.
ZWaveDetector detector_;
// Previous armed state, for logging the transitions
bool was_armed_{false};
};
} // namespace esphome::zwave_proxy_tap
#endif // USE_ZWAVE_PROXY_TAP
-32
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
import copy
from datetime import datetime from datetime import datetime
from ipaddress import ( from ipaddress import (
AddressValueError, AddressValueError,
@@ -420,37 +419,6 @@ class Required(vol.Required):
self.visibility: Visibility | None = visibility self.visibility: Visibility | None = visibility
def with_visibility(schema: Schema, visibility: Visibility, *keys: str) -> Schema:
"""Return a copy of ``schema`` with the given ``keys`` re-marked at ``visibility``.
Lets a platform override the editor :class:`Visibility` of fields it
inherits from a shared schema builder without that builder needing a
visibility parameter of its own. The canonical use is a ``template``
platform promoting the value metadata its user is expected to define
(``device_class``, ``unit_of_measurement``, ) onto the main form:
CONFIG_SCHEMA = cv.with_visibility(
sensor.sensor_schema(TemplateSensor),
cv.Visibility.UI,
CONF_DEVICE_CLASS, CONF_UNIT_OF_MEASUREMENT,
)
The original marker's key, default and validator are preserved; only the
visibility changes, and the input ``schema`` is left untouched. Raises if
a requested key is not present so typos fail at schema-build time.
"""
wanted = {str(k) for k in keys}
overrides = {}
for marker, validator in schema.schema.items():
if str(marker) in wanted:
marker = copy.copy(marker)
marker.visibility = visibility
overrides[marker] = validator
if missing := wanted - {str(m) for m in overrides}:
raise ValueError(f"with_visibility: keys not in schema: {sorted(missing)}")
return schema.extend(overrides)
class FinalExternalInvalid(Invalid): class FinalExternalInvalid(Invalid):
"""Represents an invalid value in the final validation phase where the path should not be prepended.""" """Represents an invalid value in the final validation phase where the path should not be prepended."""
-7
View File
@@ -181,7 +181,6 @@
#define USE_SENSOR #define USE_SENSOR
#define USE_SENSOR_FILTER #define USE_SENSOR_FILTER
#define USE_SERIAL_PROXY #define USE_SERIAL_PROXY
#define USE_SERIAL_PROXY_TAP
#define USE_SETUP_PRIORITY_OVERRIDE #define USE_SETUP_PRIORITY_OVERRIDE
#define USE_STATUS_LED #define USE_STATUS_LED
#define USE_STATUS_SENSOR #define USE_STATUS_SENSOR
@@ -199,9 +198,7 @@
#define USE_VALVE #define USE_VALVE
#define USE_WATER_HEATER #define USE_WATER_HEATER
#define USE_WATER_HEATER_VISUAL_OVERRIDES #define USE_WATER_HEATER_VISUAL_OVERRIDES
#define USE_ZIGBEE_PROXY_TAP
#define USE_ZWAVE_PROXY #define USE_ZWAVE_PROXY
#define USE_ZWAVE_PROXY_TAP
// Feature flags which do not work for zephyr // Feature flags which do not work for zephyr
#ifndef USE_ZEPHYR #ifndef USE_ZEPHYR
@@ -400,10 +397,6 @@
#define USB_HOST_MAX_REQUESTS 16 #define USB_HOST_MAX_REQUESTS 16
#define USB_HOST_MAX_PACKET_SIZE 64 #define USB_HOST_MAX_PACKET_SIZE 64
#define USB_UART_OUTPUT_CHUNK_COUNT 5 #define USB_UART_OUTPUT_CHUNK_COUNT 5
// USB identity on serial proxy ports needs the usb_host stack
#ifdef USE_ESP32
#define USE_SERIAL_PROXY_USB_INFO
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7) #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7)
+7 -23
View File
@@ -40,31 +40,16 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size
return true; return true;
} }
bool StaticTask::destroy() { void StaticTask::destroy() {
if (this->handle_ == nullptr) { if (this->handle_ != nullptr) {
return true; TaskHandle_t handle = this->handle_;
this->handle_ = nullptr;
vTaskDelete(handle);
} }
// Suspending takes the task off the ready and event lists, so nothing can schedule it again. It only asks
// the other core to yield though, so the task may still be running on it for a moment.
vTaskSuspend(this->handle_);
if (eTaskGetState(this->handle_) != eSuspended) {
// The task is still running on the other core and using its stack. Deleting it now would only put it on
// the termination list and return, so the caller has to try again once it has been swapped out.
return false;
}
// The task cannot run again, so the delete completes right away instead of being left to the idle task.
TaskHandle_t handle = this->handle_;
this->handle_ = nullptr;
vTaskDelete(handle);
return true;
} }
bool StaticTask::deallocate() { void StaticTask::deallocate() {
if (!this->destroy()) { this->destroy();
return false;
}
if (this->stack_buffer_ != nullptr) { if (this->stack_buffer_ != nullptr) {
RAMAllocator<StackType_t> allocator(this->use_psram_ ? RAMAllocator<StackType_t>::ALLOC_EXTERNAL RAMAllocator<StackType_t> allocator(this->use_psram_ ? RAMAllocator<StackType_t>::ALLOC_EXTERNAL
: RAMAllocator<StackType_t>::ALLOC_INTERNAL); : RAMAllocator<StackType_t>::ALLOC_INTERNAL);
@@ -72,7 +57,6 @@ bool StaticTask::deallocate() {
this->stack_buffer_ = nullptr; this->stack_buffer_ = nullptr;
this->stack_size_ = 0; this->stack_size_ = 0;
} }
return true;
} }
} // namespace esphome } // namespace esphome
+5 -12
View File
@@ -11,7 +11,6 @@ namespace esphome {
/** Helper for FreeRTOS static task management. /** Helper for FreeRTOS static task management.
* Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods. * Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods.
* Call destroy() and deallocate() from another task: a task cannot free the stack it is still running on.
*/ */
class StaticTask { class StaticTask {
public: public:
@@ -24,7 +23,7 @@ class StaticTask {
/// @brief Allocate stack and create task. /// @brief Allocate stack and create task.
/// @param fn Task function /// @param fn Task function
/// @param name Task name (for debug) /// @param name Task name (for debug)
/// @param stack_size Stack size in bytes (StackType_t is a byte on ESP-IDF) /// @param stack_size Stack size in StackType_t words
/// @param param Parameter passed to task function /// @param param Parameter passed to task function
/// @param priority FreeRTOS task priority /// @param priority FreeRTOS task priority
/// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM /// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM
@@ -32,17 +31,11 @@ class StaticTask {
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority,
bool use_psram); bool use_psram);
/// @brief Delete the task, keeping the stack buffer allocated for reuse by a subsequent create() call. /// @brief Delete the task but keep the stack buffer allocated for reuse by a subsequent create() call.
/// The task must have finished its work and parked itself, either suspended or blocked indefinitely: it is void destroy();
/// suspended here so that it cannot be scheduled again, and it is given no chance to clean up.
/// @return true if the task was deleted; false if it is still running on another core, in which case the
/// caller should try again later.
bool destroy();
/// @brief Delete the task (if created) and free the stack buffer. /// @brief Delete the task (if running) and free the stack buffer.
/// @return true if the stack buffer was freed; false if the task is still running on another core, in void deallocate();
/// which case the caller should try again later.
bool deallocate();
protected: protected:
TaskHandle_t handle_{nullptr}; TaskHandle_t handle_{nullptr};

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