Compare commits

..
Author SHA1 Message Date
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
69 changed files with 4631 additions and 4999 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,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.5 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
+6 -1
View File
@@ -2255,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 -24
View File
@@ -345,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.
@@ -405,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);
@@ -433,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
@@ -449,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
@@ -461,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
@@ -475,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
+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_ =
+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"]
+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"
@@ -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();
@@ -5,7 +5,6 @@
#include <esp_log.h> #include <esp_log.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>
@@ -77,11 +76,7 @@ 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).
+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(
-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 -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,
+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_;
+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,
+4 -1
View File
@@ -69,7 +69,10 @@ def _make_create_connection() -> Callable[..., socket.socket]:
from aiohappyeyeballs import start_connection from aiohappyeyeballs import start_connection
from urllib3.exceptions import LocationParseError from urllib3.exceptions import LocationParseError
from urllib3.util.connection import _set_socket_options, allowed_gai_family # noqa: PLC2701 from urllib3.util.connection import ( # noqa: PLC2701
_set_socket_options,
allowed_gai_family,
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT # noqa: PLC2701 from urllib3.util.timeout import _DEFAULT_TIMEOUT # noqa: PLC2701
from esphome import async_thread from esphome import async_thread
+1 -5
View File
@@ -616,15 +616,11 @@ def _make_registry_client() -> Any:
elsewhere, not by the PlatformIO registry. elsewhere, not by the PlatformIO registry.
""" """
from platformio.package.manager._registry import PackageManagerRegistryMixin from platformio.package.manager._registry import PackageManagerRegistryMixin
from platformio.registry.client import RegistryClient
class _Registry(PackageManagerRegistryMixin): class _Registry(PackageManagerRegistryMixin):
def __init__(self) -> None: def __init__(self) -> None:
self._registry_client = None
self.pkg_type = "library" self.pkg_type = "library"
self._registry_client = RegistryClient()
# The probe sleeps ~500 ms per lookup (see runner.patch_registry_private_packages);
# instance-level so the ESPHome process never patches PlatformIO's class
self._registry_client.allowed_private_packages = lambda: False
@staticmethod @staticmethod
def is_system_compatible(value: Any, custom_system: Any = None) -> bool: def is_system_compatible(value: Any, custom_system: Any = None) -> bool:
+9 -34
View File
@@ -371,27 +371,14 @@ def _registry_jobs(
return jobs, failed, installable return jobs, failed, installable
def _is_vcs_spec_uri(url: str) -> bool:
"""Whether pio's ``install_from_uri`` would clone this URI rather than
copy or download it (PackageSpec normalizes git URLs to ``git+``)."""
return not url.startswith(("file://", "symlink://", "http://", "https://"))
def _spec_name(spec: Any, url: str) -> str:
"""The spec's name; the URL basename fallback is defensive only
(PackageSpec derives a name from the URI itself)."""
return spec.name or url.split("#", 1)[0].rsplit("/", 1)[-1]
def _uri_jobs( def _uri_jobs(
manager: Any, specs: list[Any], seen: set[str] manager: Any, specs: list[Any], seen: set[str]
) -> tuple[list[tuple[str, int, Any]], int, list[tuple[str, Any]]]: ) -> tuple[list[tuple[str, int, Any]], int, list[tuple[str, Any]]]:
"""Jobs for direct-URL specs; a HEAD sizes each for the combined bar. """Jobs for direct-URL specs; a HEAD sizes each for the combined bar.
Also returns how many HEAD probes errored (an absent length is not an Also returns how many HEAD probes errored (an absent length is not an
error) and the ``(name, spec)`` pairs to pre-install: downloaded error) and the ``(name, spec)`` pairs whose archives will be
archives, plus VCS specs, which have no archive -- the pre-install installable.
itself clones them, in parallel instead of one at a time in pio run.
""" """
from esphome.net_retry import fetch_with_retry, http_request from esphome.net_retry import fetch_with_retry, http_request
@@ -399,17 +386,13 @@ def _uri_jobs(
installable: list[tuple[str, Any]] = [] installable: list[tuple[str, Any]] = []
for spec in specs: for spec in specs:
url = spec.uri url = spec.uri
if not url: if not url or not url.startswith(("http://", "https://")):
continue continue # git+/file specs are cloned/copied, not downloaded
is_vcs = _is_vcs_spec_uri(url) if url.split("#", 1)[0].endswith(".git"):
if not is_vcs and not url.startswith(("http://", "https://")): continue # bare-URL VCS spec; PlatformIO clones it
continue # file/symlink specs are copied in place by pio run
if manager.get_package(spec): if manager.get_package(spec):
continue continue
name = _spec_name(spec, url) name = spec.name or url.rsplit("/", 1)[-1]
if is_vcs:
installable.append((name, spec))
continue
# PlatformIO downloads URL specs with no checksum # PlatformIO downloads URL specs with no checksum
dl_path = Path(manager.compute_download_path(url, "")) dl_path = Path(manager.compute_download_path(url, ""))
if dl_path.is_file(): if dl_path.is_file():
@@ -423,7 +406,7 @@ def _uri_jobs(
if str(dl_path) in seen: if str(dl_path) in seen:
continue # another spec already claimed this .part continue # another spec already claimed this .part
seen.add(str(dl_path)) seen.add(str(dl_path))
candidates.append((name, url, dl_path, spec)) candidates.append((spec.name, url, dl_path, spec))
errors: list[str] = [] errors: list[str] = []
@@ -932,14 +915,8 @@ def _prefetch(build_dir: Path, env: str) -> None:
if name not in failed_names if name not in failed_names
} }
if to_install: if to_install:
# Clones first: they wait on the network, so they must not
# queue behind CPU-bound archive extractions
ordered = sorted(
to_install.values(),
key=lambda entry: not ((url := entry[1].uri) and _is_vcs_spec_uri(url)),
)
try: try:
_preinstall(mgr, ordered) _preinstall(mgr, list(to_install.values()))
if is_platform: if is_platform:
platform_packages_installed = True platform_packages_installed = True
except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught
@@ -974,10 +951,8 @@ def main(argv: list[str]) -> int:
"""Subprocess entry point: ``prefetch <build_dir> <env_name>``.""" """Subprocess entry point: ``prefetch <build_dir> <env_name>``."""
from esphome.core import CORE from esphome.core import CORE
from esphome.log import setup_log from esphome.log import setup_log
from esphome.platformio.runner import patch_registry_private_packages
signal.signal(signal.SIGTERM, _sigterm) signal.signal(signal.SIGTERM, _sigterm)
patch_registry_private_packages()
raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL") raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL")
try: try:
level = int(raw_level) if raw_level is not None else logging.INFO level = int(raw_level) if raw_level is not None else logging.INFO
+1 -13
View File
@@ -2,8 +2,7 @@
Invoked via ``python -m esphome.platformio.runner`` instead of Invoked via ``python -m esphome.platformio.runner`` instead of
``python -m platformio`` so that the patches (incremental rebuild ``python -m platformio`` so that the patches (incremental rebuild
preservation, download retries, skipping the private-package probe) apply preservation, download retries) apply inside the subprocess. Running
inside the subprocess. Running
PlatformIO in a subprocess keeps its ``sys.path`` mutations and other PlatformIO in a subprocess keeps its ``sys.path`` mutations and other
global state from leaking into the ESPHome process. global state from leaking into the ESPHome process.
""" """
@@ -106,16 +105,6 @@ def patch_file_downloader() -> None:
FileDownloader.__init__ = patched_init FileDownloader.__init__ = patched_init
def patch_registry_private_packages() -> None:
"""Skip PlatformIO's private-package probe; it sleeps ~500 ms per lookup.
ESPHome never uses private packages, so the answer is always False.
"""
from platformio.registry.client import RegistryClient
RegistryClient.allowed_private_packages = staticmethod(lambda: False) # type: ignore[method-assign]
_IGNORE_LIB_WARNINGS = "(?:Hash|Update)" _IGNORE_LIB_WARNINGS = "(?:Hash|Update)"
# Regex patterns matched against each line of PlatformIO output. Lines that # Regex patterns matched against each line of PlatformIO output. Lines that
# match are dropped by RedirectText before they reach the parent process. # match are dropped by RedirectText before they reach the parent process.
@@ -163,7 +152,6 @@ FILTER_PLATFORMIO_LINES = [
def main() -> int: def main() -> int:
patch_structhash() patch_structhash()
patch_file_downloader() patch_file_downloader()
patch_registry_private_packages()
# Wrap stdout/stderr with RedirectText before PlatformIO runs: # Wrap stdout/stderr with RedirectText before PlatformIO runs:
# #
+2 -2
View File
@@ -1,4 +1,4 @@
# Useful stuff when working in a development environment # Useful stuff when working in a development environment
clang-format==13.0.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating
clang-tidy==22.1.8 clang-tidy==22.1.8
yamllint==1.38.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating
+4 -5
View File
@@ -1,9 +1,8 @@
pylint==4.0.8 pylint==4.0.8
flake8==7.3.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.16.6 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py ruff==0.16.5 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
prek==0.5.2 # .github/workflows/ci.yml reads this pin prek==0.5.1 # also change in .github/workflows/ci.yml when updating
yamlrocks==0.6.1 # used by script/sync_dependency_versions.py
# Unit tests # Unit tests
pytest==9.1.1 pytest==9.1.1
+213 -292
View File
@@ -28,6 +28,11 @@ class WireType(IntEnum):
END_GROUP = 4 # groups (deprecated) END_GROUP = 4 # groups (deprecated)
FIXED32 = 5 # fixed32, sfixed32, float FIXED32 = 5 # fixed32, sfixed32, float
@property
def cpp_name(self) -> str:
"""The matching constant in proto.h."""
return f"WIRE_TYPE_{self.name}"
# Generate with # Generate with
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto # protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
@@ -126,9 +131,10 @@ def camel_to_snake(name: str) -> str:
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def force_str(force: bool) -> str: def _encode_call(func: str, *args: str, force: bool = False) -> str:
"""Convert a boolean force value to string format for C++ code.""" """Emit one ProtoEncode call; every helper takes the cursor and returns it advanced."""
return str(force).lower() suffix = "_force" if force else ""
return f"pos = ProtoEncode::{func}{suffix}({', '.join(('pos', *args))});"
class TypeInfo(ABC): class TypeInfo(ABC):
@@ -223,55 +229,39 @@ class TypeInfo(ABC):
def class_member(self) -> str: def class_member(self) -> str:
return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};" return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};"
@property def decode_case(self, body: str) -> str:
def decode_varint_content(self) -> str: """Emit one decode_field() case, keyed on the field's wire tag."""
content = self.decode_varint return f"case proto_tag({self.number}, {self.wire_type.cpp_name}):\n" + indent(
if content is None: f"{body}\nbreak;"
return None )
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_varint = None # Expression that reads this field from `value`; None when the type is never decoded.
decode_expr: str | None = None
def _decode_store(self, expr: str) -> str:
return f"this->{self.field_name} = {expr};"
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str | None:
content = self.decode_length """The decode_field() case for this field, or None when it is never decoded."""
if content is None: expr = self.decode_expr
return None return None if expr is None else self.decode_case(self._decode_store(expr))
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_length = None
@property
def decode_32bit_content(self) -> str:
content = self.decode_32bit
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_32bit = None
@property
def decode_64bit_content(self) -> str:
content = self.decode_64bit
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_64bit = None
# Mapping from encode_func to raw encode expression template. # Mapping from encode_func to raw encode expression template.
# When a forced field has a single-byte tag, the code generator emits # When a forced field has a single-byte tag, the code generator emits
# write_raw_byte(tag) + raw encode instead of the full encode_* method, # write_raw_byte(tag) + raw encode instead of the full encode_* method,
# eliminating the zero-check branch and encode_field_raw indirection. # eliminating the zero-check branch and encode_field_raw indirection.
# {value} is replaced with the actual field expression. # {value} is replaced with the actual field expression.
RAW_ENCODE_MAP: dict[str, str] = { RAW_ENCODE_MAP: dict[str, tuple[str, str]] = {
"encode_uint32": "ProtoEncode::encode_varint_raw(pos, {value});", "encode_uint32": ("encode_varint_raw", "{value}"),
"encode_uint64": "ProtoEncode::encode_varint_raw_64(pos, {value});", "encode_uint64": ("encode_varint_raw_64", "{value}"),
"encode_sint32": "ProtoEncode::encode_varint_raw_short(pos, encode_zigzag32({value}));", "encode_sint32": ("encode_varint_raw_short", "encode_zigzag32({value})"),
"encode_sint64": "ProtoEncode::encode_varint_raw_64(pos, encode_zigzag64({value}));", "encode_sint64": ("encode_varint_raw_64", "encode_zigzag64({value})"),
"encode_int64": "ProtoEncode::encode_varint_raw_64(pos, static_cast<uint64_t>({value}));", "encode_int64": ("encode_varint_raw_64", "static_cast<uint64_t>({value})"),
"encode_bool": "ProtoEncode::write_raw_byte(pos, {value} ? 0x01 : 0x00);", "encode_bool": ("write_raw_byte", "{value} ? 0x01 : 0x00"),
} }
# Fixed32 value expression for the shared tag+fixed32 writer; None for other wire types
fixed32_value_template: str | None = None
def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: def _encode_with_precomputed_tag(self, value_expr: str) -> str | None:
"""Try to emit a precomputed-tag encode for a field. """Try to emit a precomputed-tag encode for a field.
@@ -288,12 +278,17 @@ class TypeInfo(ABC):
return None return None
max_val = self.max_value max_val = self.max_value
# Only use RAW_ENCODE_MAP for forced fields or fields with max_value # Only use RAW_ENCODE_MAP for forced fields or fields with max_value
raw_expr = None raw = None
if self.force or max_val is not None: if self.force or max_val is not None:
raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) raw = self.RAW_ENCODE_MAP.get(self.encode_func)
if raw_expr is None: if raw is None:
return None return None
body = f"ProtoEncode::write_raw_byte(pos, {tag});\n{raw_expr.format(value=value_expr)}" func, arg = raw
body = (
_encode_call("write_raw_byte", str(tag))
+ "\n"
+ _encode_call(func, arg.format(value=value_expr))
)
if self.force: if self.force:
return body return body
# Non-forced with max_value: inline zero-check + raw encode # Non-forced with max_value: inline zero-check + raw encode
@@ -314,23 +309,44 @@ class TypeInfo(ABC):
return None return None
# When max_len < 128, length varint is always 1 byte # When max_len < 128, length varint is always 1 byte
len_encode = ( len_encode = (
f"ProtoEncode::write_raw_byte(pos, static_cast<uint8_t>({len_expr}));" _encode_call("write_raw_byte", f"static_cast<uint8_t>({len_expr})")
if max_len is not None and max_len < 128 if max_len is not None and max_len < 128
else f"ProtoEncode::encode_varint_raw(pos, {len_expr});" else _encode_call("encode_varint_raw", len_expr)
) )
return "\n".join(
(
_encode_call("write_raw_byte", str(tag)),
len_encode,
_encode_call("encode_raw", data_expr, len_expr),
)
)
def _encode_fixed32_with_precomputed_tag(self, value: str) -> str | None:
"""Single-byte tag fixed32 write, or None for other types and multi-byte tags."""
tag = self.calculate_tag()
if self.fixed32_value_template is None or tag >= 128:
return None
value_expr = self.fixed32_value_template.format(value=value)
if self.force:
return _encode_call("write_tag_and_fixed32", str(tag), value_expr)
return ( return (
f"ProtoEncode::write_raw_byte(pos, {tag});\n" f"if (uint32_t raw = {value_expr}; raw != 0) [[likely]] {{\n"
f"{len_encode}\n" f" {_encode_call('write_tag_and_fixed32', str(tag), 'raw')}\n"
f"ProtoEncode::encode_raw(pos, {data_expr}, {len_expr});" "}"
) )
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"): value = f"this->{self.field_name}"
if result := self._encode_with_precomputed_tag(value):
return result return result
if self.force: if result := self._encode_fixed32_with_precomputed_tag(value):
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);" return result
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});" return _encode_call(self.encode_func, str(self.number), value, force=self.force)
def encode_element(self, number: int, element: str) -> str:
"""Encode one element of a repeated field; elements are always written."""
return _encode_call(self.encode_func, str(number), element, force=True)
encode_func = None encode_func = None
@@ -605,7 +621,6 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
# Unsupported but defined for completeness # Unsupported but defined for completeness
cpp_type = "double" cpp_type = "double"
default_value = "0.0" default_value = "0.0"
decode_64bit = "value.as_double()"
encode_func = "encode_double" encode_func = "encode_double"
wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec
@@ -631,10 +646,12 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
class FloatType(FixedSizeTypeMixin, TypeInfo): class FloatType(FixedSizeTypeMixin, TypeInfo):
cpp_type = "float" cpp_type = "float"
default_value = "0.0f" default_value = "0.0f"
decode_32bit = "value.as_float()" decode_expr = "value.as_float()"
encode_func = "encode_float" encode_func = "encode_float"
wire_type = WireType.FIXED32 # Uses wire type 5 wire_type = WireType.FIXED32 # Uses wire type 5
fixed32_value_template = "float_to_raw({value})"
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n' o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
o += "out.append(buffer);" o += "out.append(buffer);"
@@ -658,7 +675,7 @@ class Int64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t" cpp_type = "int64_t"
_varint_max_bits = 64 _varint_max_bits = 64
default_value = "0" default_value = "0"
decode_varint = "static_cast<int64_t>(value)" decode_expr = "static_cast<int64_t>(value.as_varint())"
encode_func = "encode_int64" encode_func = "encode_int64"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -679,7 +696,7 @@ class UInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint64_t" cpp_type = "uint64_t"
_varint_max_bits = 64 _varint_max_bits = 64
default_value = "0" default_value = "0"
decode_varint = "value" decode_expr = "value.as_varint()"
encode_func = "encode_uint64" encode_func = "encode_uint64"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -697,11 +714,11 @@ class UInt64Type(VarintTypeMixin, TypeInfo):
return self._get_simple_size_calculation(name, force, "uint64") return self._get_simple_size_calculation(name, force, "uint64")
@property @property
def RAW_ENCODE_MAP(self) -> dict[str, str]: # noqa: N802 def RAW_ENCODE_MAP(self) -> dict[str, tuple[str, str]]: # noqa: N802
if self.mac_address: if self.mac_address:
return { return {
**TypeInfo.RAW_ENCODE_MAP, **TypeInfo.RAW_ENCODE_MAP,
"encode_uint64": "ProtoEncode::encode_varint_raw_48bit(pos, {value});", "encode_uint64": ("encode_varint_raw_48bit", "{value}"),
} }
return TypeInfo.RAW_ENCODE_MAP return TypeInfo.RAW_ENCODE_MAP
@@ -714,7 +731,7 @@ class Int32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t" cpp_type = "int32_t"
_varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf _varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf
default_value = "0" default_value = "0"
decode_varint = "static_cast<int32_t>(value)" decode_expr = "static_cast<int32_t>(value.as_varint())"
encode_func = "encode_int32" encode_func = "encode_int32"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -734,7 +751,6 @@ class Int32Type(VarintTypeMixin, TypeInfo):
class Fixed64Type(FixedSizeTypeMixin, TypeInfo): class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint64_t" cpp_type = "uint64_t"
default_value = "0" default_value = "0"
decode_64bit = "value.as_fixed64()"
encode_func = "encode_fixed64" encode_func = "encode_fixed64"
wire_type = WireType.FIXED64 # Uses wire type 1 wire_type = WireType.FIXED64 # Uses wire type 1
@@ -760,7 +776,7 @@ class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
class Fixed32Type(FixedSizeTypeMixin, TypeInfo): class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint32_t" cpp_type = "uint32_t"
default_value = "0" default_value = "0"
decode_32bit = "value.as_fixed32()" decode_expr = "value.as_fixed32()"
encode_func = "encode_fixed32" encode_func = "encode_fixed32"
wire_type = WireType.FIXED32 # Uses wire type 5 wire_type = WireType.FIXED32 # Uses wire type 5
@@ -769,15 +785,7 @@ class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
o += "out.append(buffer);" o += "out.append(buffer);"
return o return o
@property fixed32_value_template = "{value}"
def encode_content(self) -> str:
tag = self.calculate_tag()
if self.force and tag < 128:
# Emit combined tag+value write: precomputed tag + direct memcpy
return f"ProtoEncode::write_tag_and_fixed32(pos, {tag}, this->{self.field_name});"
if self.force:
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);"
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});"
def get_size_calculation(self, name: str, force: bool = False) -> str: def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size() field_id_size = self.calculate_field_id_size()
@@ -797,7 +805,7 @@ class BoolType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 1 _varint_max_bits = 1
cpp_type = "bool" cpp_type = "bool"
default_value = "false" default_value = "false"
decode_varint = "value != 0" decode_expr = "value.as_bool()"
encode_func = "encode_bool" encode_func = "encode_bool"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -817,7 +825,7 @@ class StringType(TypeInfo):
default_value = "" default_value = ""
reference_type = "std::string &" reference_type = "std::string &"
const_reference_type = "const std::string &" const_reference_type = "const std::string &"
decode_length = "value.as_string()" decode_expr = "value.as_string()"
encode_func = "encode_string" encode_func = "encode_string"
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
@@ -851,9 +859,12 @@ class StringType(TypeInfo):
f"this->{self.field_name}_ref_.size()", f"this->{self.field_name}_ref_.size()",
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_, true);" "encode_string",
return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_);" str(self.number),
f"this->{self.field_name}_ref_",
force=self.force,
)
def dump(self, name): def dump(self, name):
# If name is 'it', this is a repeated field element - always use string # If name is 'it', this is a repeated field element - always use string
@@ -929,6 +940,9 @@ class MessageType(TypeInfo):
def can_use_dump_field(cls) -> bool: def can_use_dump_field(cls) -> bool:
return False return False
def encode_element(self, number: int, element: str) -> str:
return _encode_call("encode_sub_message", "buffer", str(number), element)
@property @property
def cpp_type(self) -> str: def cpp_type(self) -> str:
return self._field.type_name[1:] return self._field.type_name[1:]
@@ -951,15 +965,9 @@ class MessageType(TypeInfo):
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
# Sub-message encoding needs buffer for backpatch/sync # Sub-message encoding needs buffer for backpatch/sync
return f"ProtoEncode::{self.encode_func}(pos, buffer, {self.number}, this->{self.field_name});" return _encode_call(
self.encode_func, "buffer", str(self.number), f"this->{self.field_name}"
@property )
def decode_length(self) -> str:
# Override to return None for message types because we can't use template-based
# decoding when the specific message type isn't known at compile time.
# Instead, we use the non-template decode_to_message() method which allows
# runtime polymorphism through virtual function calls.
return None
@property @property
def public_content(self) -> list[str]: def public_content(self) -> list[str]:
@@ -976,19 +984,14 @@ class MessageType(TypeInfo):
) )
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str:
# Custom decode that doesn't use templates body = f"value.decode_to_message(this->{self.field_name});"
if self._track_presence: if self._track_presence:
# decode_to_message() cannot report failure, so setting the flag # decode_to_message() cannot report failure, so setting the flag
# afterwards only documents intent; a status-returning decode could # afterwards only documents intent; a status-returning decode could
# gate it for real without touching callers. # gate it for real without touching callers.
return ( body += f"\nthis->has_{self.name} = true;"
f"case {self.number}:\n" return self.decode_case(body)
f" value.decode_to_message(this->{self.field_name});\n"
f" this->has_{self.name} = true;\n"
f" break;"
)
return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;"
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return f"{name}.dump_to(out);" return f"{name}.dump_to(out);"
@@ -1027,7 +1030,7 @@ class BytesType(TypeInfo):
reference_type = "std::string &" reference_type = "std::string &"
const_reference_type = "const std::string &" const_reference_type = "const std::string &"
encode_func = "encode_bytes" encode_func = "encode_bytes"
decode_length = "value.as_string()" decode_expr = "value.as_string()"
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
@property @property
@@ -1058,9 +1061,13 @@ class BytesType(TypeInfo):
f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_" f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_"
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" "encode_bytes",
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" str(self.number),
f"this->{self.field_name}_ptr_",
f"this->{self.field_name}_len_",
force=self.force,
)
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)" ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)"
@@ -1133,11 +1140,6 @@ class PointerToBufferTypeBase(TypeInfo):
super().__init__(field) super().__init__(field)
self.array_size = 0 self.array_size = 0
@property
def decode_length(self) -> str | None:
# This is handled in decode_length_content
return None
@property @property
def wire_type(self) -> WireType: def wire_type(self) -> WireType:
"""Get the wire type for this field.""" """Get the wire type for this field."""
@@ -1170,17 +1172,20 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
f"this->{self.field_name}", f"this->{self.field_name}_len" f"this->{self.field_name}", f"this->{self.field_name}_len"
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" "encode_bytes",
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" str(self.number),
f"this->{self.field_name}",
f"this->{self.field_name}_len",
force=self.force,
)
@property @property
def decode_length_content(self) -> str | None: def decode_content(self) -> str:
return f"""case {self.number}: {{ return self.decode_case(
this->{self.field_name} = value.data(); f"this->{self.field_name} = value.data();\n"
this->{self.field_name}_len = value.size(); f"this->{self.field_name}_len = value.size();",
break; )
}}"""
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return ( return (
@@ -1224,24 +1229,26 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
if max_len is not None and max_len < 128 and self.force: if max_len is not None and max_len < 128 and self.force:
tag = self.calculate_tag() tag = self.calculate_tag()
if tag < 128: if tag < 128:
return f"ProtoEncode::encode_short_string_force(pos, {tag}, this->{self.field_name});" return _encode_call(
"encode_short_string_force", str(tag), f"this->{self.field_name}"
)
if result := self._encode_bytes_with_precomputed_tag( if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}.c_str()", f"this->{self.field_name}.c_str()",
f"this->{self.field_name}.size()", f"this->{self.field_name}.size()",
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}, true);" "encode_string",
return ( str(self.number),
f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name});" f"this->{self.field_name}",
force=self.force,
) )
@property @property
def decode_length_content(self) -> str | None: def decode_content(self) -> str:
return f"""case {self.number}: {{ return self.decode_case(
this->{self.field_name} = StringRef(reinterpret_cast<const char *>(value.data()), value.size()); f"this->{self.field_name} = StringRef(value.data(), value.size());",
break; )
}}"""
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
# Not used since we use dump_field, but required by abstract base class # Not used since we use dump_field, but required by abstract base class
@@ -1310,14 +1317,13 @@ class PackedBufferTypeInfo(TypeInfo):
] ]
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str:
"""Store pointer to buffer and calculate count of packed varints.""" """Store pointer to buffer and calculate count of packed varints."""
return f"""case {self.number}: {{ return self.decode_case(
this->{self.field_name}_data_ = value.data(); f"this->{self.field_name}_data_ = value.data();\n"
this->{self.field_name}_length_ = value.size(); f"this->{self.field_name}_length_ = value.size();\n"
this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size()); f"this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size());",
break; )
}}"""
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
@@ -1402,17 +1408,11 @@ class FixedArrayBytesType(TypeInfo):
] ]
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str:
o = f"case {self.number}: {{\n" return self.decode_case(
o += " const std::string &data_str = value.as_string();\n" f"this->{self.field_name}_len = std::min<size_t>(value.size(), {self.array_size});\n"
o += f" this->{self.field_name}_len = data_str.size();\n" f"memcpy(this->{self.field_name}, value.data(), this->{self.field_name}_len);",
o += f" if (this->{self.field_name}_len > {self.array_size}) {{\n" )
o += f" this->{self.field_name}_len = {self.array_size};\n"
o += " }\n"
o += f" memcpy(this->{self.field_name}, data_str.data(), this->{self.field_name}_len);\n"
o += " break;\n"
o += "}"
return o
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
@@ -1421,9 +1421,13 @@ class FixedArrayBytesType(TypeInfo):
f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len
): ):
return result return result
if self.force: return _encode_call(
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" "encode_bytes",
return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" str(self.number),
f"this->{self.field_name}",
f"this->{self.field_name}_len",
force=self.force,
)
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return f"out.append(format_hex_pretty({name}, {name}_len));" return f"out.append(format_hex_pretty({name}, {name}_len));"
@@ -1471,7 +1475,7 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint32_t" cpp_type = "uint32_t"
_varint_max_bits = 32 _varint_max_bits = 32
default_value = "0" default_value = "0"
decode_varint = "value" decode_expr = "value.as_varint()"
encode_func = "encode_uint32" encode_func = "encode_uint32"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1494,13 +1498,21 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
class EnumType(VarintTypeMixin, TypeInfo): class EnumType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 32 _varint_max_bits = 32
def encode_element(self, number: int, element: str) -> str:
return _encode_call(
self.encode_func,
str(number),
f"static_cast<uint32_t>({element})",
force=True,
)
@property @property
def cpp_type(self) -> str: def cpp_type(self) -> str:
return f"enums::{self._field.type_name[1:]}" return f"enums::{self._field.type_name[1:]}"
@property @property
def decode_varint(self) -> str: def decode_expr(self) -> str:
return f"static_cast<{self.cpp_type}>(value)" return f"static_cast<{self.cpp_type}>(value.as_varint())"
default_value = "" default_value = ""
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1520,9 +1532,9 @@ class EnumType(VarintTypeMixin, TypeInfo):
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
value_expr = f"static_cast<uint32_t>(this->{self.field_name})" value_expr = f"static_cast<uint32_t>(this->{self.field_name})"
if self.force: return _encode_call(
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr}, true);" self.encode_func, str(self.number), value_expr, force=self.force
return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr});" )
def dump(self, name: str) -> str: def dump(self, name: str) -> str:
return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));"
@@ -1547,7 +1559,7 @@ class EnumType(VarintTypeMixin, TypeInfo):
class SFixed32Type(FixedSizeTypeMixin, TypeInfo): class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int32_t" cpp_type = "int32_t"
default_value = "0" default_value = "0"
decode_32bit = "value.as_sfixed32()" decode_expr = "value.as_sfixed32()"
encode_func = "encode_sfixed32" encode_func = "encode_sfixed32"
wire_type = WireType.FIXED32 # Uses wire type 5 wire_type = WireType.FIXED32 # Uses wire type 5
@@ -1573,7 +1585,6 @@ class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
class SFixed64Type(FixedSizeTypeMixin, TypeInfo): class SFixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int64_t" cpp_type = "int64_t"
default_value = "0" default_value = "0"
decode_64bit = "value.as_sfixed64()"
encode_func = "encode_sfixed64" encode_func = "encode_sfixed64"
wire_type = WireType.FIXED64 # Uses wire type 1 wire_type = WireType.FIXED64 # Uses wire type 1
@@ -1600,7 +1611,7 @@ class SInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t" cpp_type = "int32_t"
_varint_max_bits = 32 # zigzag encoding keeps it 32-bit _varint_max_bits = 32 # zigzag encoding keeps it 32-bit
default_value = "0" default_value = "0"
decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))" decode_expr = "decode_zigzag32(static_cast<uint32_t>(value.as_varint()))"
encode_func = "encode_sint32" encode_func = "encode_sint32"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1621,7 +1632,7 @@ class SInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t" cpp_type = "int64_t"
_varint_max_bits = 64 _varint_max_bits = 64
default_value = "0" default_value = "0"
decode_varint = "decode_zigzag64(value)" decode_expr = "decode_zigzag64(value.as_varint())"
encode_func = "encode_sint64" encode_func = "encode_sint64"
wire_type = WireType.VARINT # Uses wire type 0 wire_type = WireType.VARINT # Uses wire type 0
@@ -1701,9 +1712,9 @@ def _generate_inline_encode_block(
lines = [] lines = []
lines.append(f"auto &sub_msg = {element};") lines.append(f"auto &sub_msg = {element};")
lines.append(f"ProtoEncode::write_raw_byte(pos, {tag});") lines.append(_encode_call("write_raw_byte", str(tag)))
lines.append("uint8_t *len_pos = pos;") lines.append("uint8_t *len_pos = pos;")
lines.append("ProtoEncode::reserve_byte(pos);") lines.append(_encode_call("reserve_byte"))
# Generate inline field encoding for each sub-message field # Generate inline field encoding for each sub-message field
for field in sub_desc.field: for field in sub_desc.field:
@@ -1774,18 +1785,11 @@ class FixedArrayRepeatedType(TypeInfo):
def _encode_element(self, element: str) -> str: def _encode_element(self, element: str) -> str:
"""Helper to generate encode statement for a single element.""" """Helper to generate encode statement for a single element."""
if isinstance(self._ti, EnumType): if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type):
return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);" return _generate_inline_encode_block(
# Repeated message elements use encode_sub_message (force=true is default) self.number, self._ti.cpp_type, element
if isinstance(self._ti, MessageType): )
if _is_inline_encode(self._ti.cpp_type): return self._ti.encode_element(self.number, element)
return _generate_inline_encode_block(
self.number, self._ti.cpp_type, element
)
return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});"
return (
f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);"
)
@property @property
def cpp_type(self) -> str: def cpp_type(self) -> str:
@@ -2079,55 +2083,23 @@ class RepeatedTypeInfo(TypeInfo):
return self._ti.wire_type return self._ti.wire_type
@property @property
def decode_varint_content(self) -> str: def decode_expr(self) -> str | None:
# Pointer fields don't support decoding return self._ti.decode_expr
if self._use_pointer:
return None def _decode_store(self, expr: str) -> str:
content = self._ti.decode_varint return f"this->{self.field_name}.push_back({expr});"
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property @property
def decode_length_content(self) -> str: def decode_content(self) -> str | None:
# Pointer fields don't support decoding # Pointer fields don't support decoding
if self._use_pointer: if self._use_pointer:
return None return None
content = self._ti.decode_length if isinstance(self._ti, MessageType):
if content is None and isinstance(self._ti, MessageType): return self.decode_case(
# Special handling for non-template message decoding f"this->{self.field_name}.emplace_back();\n"
return f"case {self.number}: this->{self.field_name}.emplace_back(); value.decode_to_message(this->{self.field_name}.back()); break;" f"value.decode_to_message(this->{self.field_name}.back());"
if content is None: )
return None return super().decode_content
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property
def decode_32bit_content(self) -> str:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_32bit
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property
def decode_64bit_content(self) -> str:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_64bit
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property @property
def _ti_is_bool(self) -> bool: def _ti_is_bool(self) -> bool:
@@ -2135,15 +2107,7 @@ class RepeatedTypeInfo(TypeInfo):
return isinstance(self._ti, BoolType) return isinstance(self._ti, BoolType)
def _encode_element_call(self, element: str) -> str: def _encode_element_call(self, element: str) -> str:
"""Helper to generate encode call for a single element.""" return self._ti.encode_element(self.number, element)
if isinstance(self._ti, EnumType):
return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);"
# Repeated message elements use encode_sub_message (force=true is default)
if isinstance(self._ti, MessageType):
return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});"
return (
f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);"
)
@property @property
def encode_content(self) -> str: def encode_content(self) -> str:
@@ -2152,7 +2116,7 @@ class RepeatedTypeInfo(TypeInfo):
# Special handling for const char* elements (when container_no_template contains "const char") # Special handling for const char* elements (when container_no_template contains "const char")
if "const char" in self._container_no_template: if "const char" in self._container_no_template:
o = f"for (const char *it : *this->{self.field_name}) {{\n" o = f"for (const char *it : *this->{self.field_name}) {{\n"
o += f" ProtoEncode::{self._ti.encode_func}(pos, {self.number}, it, strlen(it), true);\n" o += f" {_encode_call(self._ti.encode_func, str(self.number), 'it', 'strlen(it)', force=True)}\n"
else: else:
o = f"for (const auto &it : *this->{self.field_name}) {{\n" o = f"for (const auto &it : *this->{self.field_name}) {{\n"
o += f" {self._encode_element_call('it')}\n" o += f" {self._encode_element_call('it')}\n"
@@ -2538,10 +2502,7 @@ def build_message_type(
) -> tuple[str, str, str]: ) -> tuple[str, str, str]:
public_content: list[str] = [] public_content: list[str] = []
protected_content: list[str] = [] protected_content: list[str] = []
decode_varint: list[str] = [] decode: list[str] = []
decode_length: list[str] = []
decode_32bit: list[str] = []
decode_64bit: list[str] = []
encode: list[str] = [] encode: list[str] = []
dump: list[str] = [] dump: list[str] = []
size_calc: list[str] = [] size_calc: list[str] = []
@@ -2670,22 +2631,8 @@ def build_message_type(
if field.options.HasExtension(pb.field_ifdef): if field.options.HasExtension(pb.field_ifdef):
field_ifdef = field.options.Extensions[pb.field_ifdef] field_ifdef = field.options.Extensions[pb.field_ifdef]
if ti.decode_varint_content: if case := ti.decode_content:
decode_varint.extend( decode.extend(wrap_with_ifdef(case, field_ifdef))
wrap_with_ifdef(ti.decode_varint_content, field_ifdef)
)
if ti.decode_length_content:
decode_length.extend(
wrap_with_ifdef(ti.decode_length_content, field_ifdef)
)
if ti.decode_32bit_content:
decode_32bit.extend(
wrap_with_ifdef(ti.decode_32bit_content, field_ifdef)
)
if ti.decode_64bit_content:
decode_64bit.extend(
wrap_with_ifdef(ti.decode_64bit_content, field_ifdef)
)
if ti.dump_content: if ti.dump_content:
# Check for field_ifdef option for dump as well # Check for field_ifdef option for dump as well
field_ifdef = None field_ifdef = None
@@ -2695,49 +2642,15 @@ def build_message_type(
dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef)) dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef))
cpp = "" cpp = ""
if decode_varint: if decode:
o = f"bool {desc.name}::decode_varint(uint32_t field_id, proto_varint_value_t value) {{\n" o = f"void {desc.name}::decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {{\n"
o += " switch (field_id) {\n" o += " const ProtoFieldValue value(data, scalar);\n"
o += indent("\n".join(decode_varint), " ") + "\n" o += " switch (tag) {\n"
o += " default: return false;\n" o += indent("\n".join(decode), " ") + "\n"
o += " }\n" o += " }\n"
o += " return true;\n"
o += "}\n" o += "}\n"
cpp += o cpp += o
prot = "bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;" prot = "void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;"
protected_content.insert(0, prot)
if decode_length:
o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_length), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;"
protected_content.insert(0, prot)
if decode_32bit:
o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_32bit), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;"
protected_content.insert(0, prot)
if decode_64bit:
o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_64bit), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
protected_content.insert(0, prot) protected_content.insert(0, prot)
# Generate custom decode() override for messages with FixedVector fields # Generate custom decode() override for messages with FixedVector fields
@@ -2784,28 +2697,36 @@ def build_message_type(
) )
for line in encode for line in encode
] ]
o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" o = f"{speed_attr}uint8_t *{desc.name}::encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {{\n"
o += f" const auto &msg = *static_cast<const {desc.name} *>(self);\n"
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
o += indent("\n".join(encode_debug)) + "\n" o += indent("\n".join(encode_debug)).replace("this->", "msg.") + "\n"
o += " return pos;\n" o += " return pos;\n"
o += "}\n" o += "}\n"
cpp += o cpp += o
prot = ( public_content.append(
"uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;" "static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);"
)
public_content.append(
"uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {\n"
" return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);\n"
"}"
) )
public_content.append(prot)
# If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used
# Add calculate_size method only if this message needs encoding and has fields # Add calculate_size method only if this message needs encoding and has fields
if needs_encode and size_calc and not is_inline_only: if needs_encode and size_calc and not is_inline_only:
o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n" o = f"{speed_attr}uint32_t {desc.name}::calc_size_msg(const void *self) {{\n"
o += f" const auto &msg = *static_cast<const {desc.name} *>(self);\n"
o += " uint32_t size = 0;\n" o += " uint32_t size = 0;\n"
o += indent("\n".join(size_calc)) + "\n" o += indent("\n".join(size_calc)).replace("this->", "msg.") + "\n"
o += " return size;\n" o += " return size;\n"
o += "}\n" o += "}\n"
cpp += o cpp += o
prot = "uint32_t calculate_size() const;" public_content.append("static uint32_t calc_size_msg(const void *self);")
public_content.append(prot) public_content.append(
"uint32_t calculate_size() const { return calc_size_msg(this); }"
)
# If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used
# dump_to method declaration in header # dump_to method declaration in header
+1 -25
View File
@@ -14,31 +14,7 @@ top=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
[ -x "$top/venv/bin/python" ] && exit 0 [ -x "$top/venv/bin/python" ] && exit 0
[ -x "$top/script/setup" ] || exit 0 [ -x "$top/script/setup" ] || exit 0
# Every worktree shares the hooks directory of the checkout it was created
# from, and the script/setup run below is the one from whichever branch was just
# checked out. Older branches install their own pre-commit hook without checking
# for a worktree: that moves the shared hook aside as pre-commit.legacy and
# replaces it with one tied to this worktree's virtual environment, so commits
# break in every checkout. To rule that out, the hooks directory is copied
# before script/setup runs and put back exactly as it was afterwards, including
# removing any file script/setup added.
hooks=$(git rev-parse --path-format=absolute --git-path hooks 2>/dev/null) || exit 0
snap=$(mktemp -d "$hooks/.post-checkout.XXXXXX") || exit 0
cp -p "$hooks"/* "$snap"/ 2>/dev/null
# Clear VIRTUAL_ENV so a checkout made from a shell with an environment already # Clear VIRTUAL_ENV so a checkout made from a shell with an environment already
# activated still gets its own, rather than having the active one repointed at # activated still gets its own, rather than having the active one repointed at
# this working tree. # this working tree.
env -u VIRTUAL_ENV "$top/script/setup" exec env -u VIRTUAL_ENV "$top/script/setup"
status=$?
for f in "$hooks"/*; do
[ -e "$snap/${f##*/}" ] || rm -f "$f"
done
# Files are moved rather than copied so a hook that is still running, such as
# this one, is swapped out atomically instead of being rewritten in place.
for f in "$snap"/*; do
cmp -s "$f" "$hooks/${f##*/}" 2>/dev/null || mv -f "$f" "$hooks/${f##*/}"
done
rm -rf "$snap"
exit $status
-164
View File
@@ -1,164 +0,0 @@
#!/usr/bin/env python3
"""Keep pre-commit hook revs in sync with the requirements files.
Dependabot only bumps the ``package==version`` pins in ``requirements*.txt``.
Some of those tools are pinned a second time as hook ``rev`` values in
``.pre-commit-config.yaml``. This script treats the requirements files as
the source of truth and rewrites the revs to match, editing the config
through yamlrocks so comments and layout survive.
Run without arguments to apply the changes in place, or with ``--check`` to
only report drift (exit status 1 when anything is out of sync).
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass
from pathlib import Path
import re
import sys
from typing import Any
import yamlrocks
REPO_ROOT = Path(__file__).resolve().parent.parent
PRECOMMIT_CONFIG = ".pre-commit-config.yaml"
class SyncError(Exception):
"""A pin could not be located in a requirements file or the config."""
@dataclass(frozen=True)
class SyncTarget:
"""A requirements pin and the pre-commit repo whose rev mirrors it."""
package: str
requirements_file: str
repo: str
SYNC_TARGETS: tuple[SyncTarget, ...] = (
SyncTarget(
"ruff", "requirements_test.txt", "https://github.com/astral-sh/ruff-pre-commit"
),
SyncTarget("flake8", "requirements_test.txt", "https://github.com/PyCQA/flake8"),
SyncTarget(
"pyupgrade", "requirements_test.txt", "https://github.com/asottile/pyupgrade"
),
SyncTarget(
"clang-format",
"requirements_dev.txt",
"https://github.com/pre-commit/mirrors-clang-format",
),
SyncTarget(
"yamllint",
"requirements_dev.txt",
"https://github.com/adrienverge/yamllint.git",
),
)
def read_requirement_version(requirements: str, package: str) -> str | None:
"""Return the ``==`` pin for ``package`` or None when it is not pinned."""
pattern = re.compile(
rf"^{re.escape(package)}==(?P<version>[^\s#]+)",
re.MULTILINE | re.IGNORECASE,
)
match = pattern.search(requirements)
return match.group("version") if match else None
def find_repo_entry(doc: Any, repo: str) -> Any:
"""Return the single ``- repo:`` block for ``repo`` in a pre-commit doc."""
try:
entries = [entry for entry in doc["repos"] if entry["repo"] == repo]
except KeyError as err:
raise SyncError(f"malformed pre-commit config, missing key {err}") from None
if len(entries) != 1:
raise SyncError(
f"expected exactly one block for repo {repo}, found {len(entries)}"
)
return entries[0]
def current_rev(entry: Any, repo: str) -> tuple[str, str]:
"""Split the block's rev into its tag prefix (``v`` or empty) and version."""
if "rev" not in entry:
raise SyncError(f"repo {repo} has no rev")
rev = entry["rev"]
if not isinstance(rev, str):
# A rev such as ``1.0`` parses as a number and cannot be compared or
# rewritten safely; quote it in the config instead.
raise SyncError(f"rev of repo {repo} is not a string: {rev!r}")
prefix = "v" if rev.startswith("v") else ""
return prefix, rev.removeprefix("v")
def sync(root: Path, *, write: bool) -> list[str]:
"""Bring every hook rev in line with its requirements pin.
Returns one description per rev that was (or, when ``write`` is False,
would be) changed. Raises SyncError when a pin cannot be found, which
means SYNC_TARGETS has gone stale and needs updating by hand.
"""
config_path = root / PRECOMMIT_CONFIG
doc = yamlrocks.loads(config_path.read_bytes(), option=yamlrocks.OPT_ROUND_TRIP)
requirements: dict[str, str] = {}
changes: list[str] = []
for target in SYNC_TARGETS:
if target.requirements_file not in requirements:
requirements[target.requirements_file] = (
root / target.requirements_file
).read_text()
version = read_requirement_version(
requirements[target.requirements_file], target.package
)
if version is None:
raise SyncError(
f"{target.requirements_file}: no '{target.package}==' pin found"
)
entry = find_repo_entry(doc, target.repo)
prefix, current = current_rev(entry, target.repo)
if current == version:
continue
changes.append(f"{target.package}: {current} -> {version}")
entry["rev"] = f"{prefix}{version}"
if changes and write:
config_path.write_bytes(doc.to_yaml())
return changes
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"--check",
action="store_true",
help="report drift without modifying any file; exit 1 if out of sync",
)
parser.add_argument(
"--root",
type=Path,
default=REPO_ROOT,
help="repository checkout to operate on (default: this checkout)",
)
args = parser.parse_args(argv)
try:
changes = sync(args.root, write=not args.check)
except SyncError as err:
print(f"error: {err}", file=sys.stderr)
return 1
for change in changes:
print(change)
if args.check and changes:
return 1
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())
@@ -249,7 +249,7 @@ static APIBuffer build_infrared_rf_transmit_wire() {
std::memcpy(bytes + len, packed, packed_len); std::memcpy(bytes + len, packed, packed_len);
len += packed_len; len += packed_len;
// field 6: modulation = 1 (non-zero so it's actually emitted and exercises // field 6: modulation = 1 (non-zero so it's actually emitted and exercises
// decode_varint for this field, matching the documented layout above). // decode_field for this field, matching the documented layout above).
put_byte(0x30); put_byte(0x30);
put_varint(1); put_varint(1);
@@ -1,32 +0,0 @@
esphome:
name: test-keyboard-no-label
esp32:
board: esp32dev
framework:
type: esp-idf
spi:
- id: spi_bus
clk_pin: GPIO18
mosi_pin: GPIO23
display:
- platform: mipi_spi
spi_id: spi_bus
model: st7789v
id: tft_display
dimensions:
width: 240
height: 320
cs_pin: GPIO22
dc_pin: GPIO21
auto_clear_enabled: false
invert_colors: false
update_interval: never
lvgl:
displays: tft_display
widgets:
- keyboard:
id: keyboard_widget
@@ -1,34 +0,0 @@
esphome:
name: test-qrcode-no-label
esp32:
board: esp32dev
framework:
type: esp-idf
spi:
- id: spi_bus
clk_pin: GPIO18
mosi_pin: GPIO23
display:
- platform: mipi_spi
spi_id: spi_bus
model: st7789v
id: tft_display
dimensions:
width: 240
height: 320
cs_pin: GPIO22
dc_pin: GPIO21
auto_clear_enabled: false
invert_colors: false
update_interval: never
lvgl:
displays: tft_display
widgets:
- qrcode:
id: qr_widget
size: 100
text: "esphome.io"
@@ -1,35 +0,0 @@
esphome:
name: test-tabview-no-label
esp32:
board: esp32dev
framework:
type: esp-idf
spi:
- id: spi_bus
clk_pin: GPIO18
mosi_pin: GPIO23
display:
- platform: mipi_spi
spi_id: spi_bus
model: st7789v
id: tft_display
dimensions:
width: 240
height: 320
cs_pin: GPIO22
dc_pin: GPIO21
auto_clear_enabled: false
invert_colors: false
update_interval: never
lvgl:
displays: tft_display
widgets:
- tabview:
id: tabview_widget
tabs:
- name: "Tab 1"
id: tab_1
@@ -1,32 +0,0 @@
"""Widgets whose LVGL C implementation creates or references labels
internally (tab titles, key legends, the QR canvas fallback) must declare
the label dependency in ``get_uses()``. Otherwise a config that contains
no ``label`` widget of its own compiles LVGL without ``LV_USE_LABEL`` and
fails at C compile time with undefined ``lv_label_*`` symbols.
"""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.lvgl import defines as df
@pytest.mark.parametrize(
"yaml_file",
[
"qrcode_no_label.yaml",
"keyboard_no_label.yaml",
"tabview_no_label.yaml",
],
)
def test_label_less_config_enables_lv_use_label(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
yaml_file: str,
) -> None:
generate_main(component_config_path(yaml_file))
assert "LV_USE_LABEL" in df.get_defines()
@@ -59,7 +59,7 @@ static void verify_mac(uint64_t mac, size_t expected_bytes) {
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size(); uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size();
#endif #endif
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac); pos = ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac);
size_t new_len = pos - api_buf.data(); size_t new_len = pos - api_buf.data();
EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec; EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec;
-5
View File
@@ -1,5 +0,0 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.dependencies = manifest.dependencies + ["sensor", "spi"]
@@ -1,62 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/components/atm90e32/atm90e32.h"
namespace esphome::atm90e32::testing {
TEST(ATM90E32OffsetRegisterVerification, AcceptsExactSignedReadback) {
EXPECT_TRUE(offset_register_value_matches(0x007B, 123));
EXPECT_TRUE(offset_register_value_matches(0xFF85, -123));
}
TEST(ATM90E32OffsetRegisterVerification, RejectsMismatchedReadback) {
EXPECT_FALSE(offset_register_value_matches(0x007C, 123));
EXPECT_FALSE(offset_register_value_matches(0xFF84, -123));
}
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedStoredValuesAsRestored) {
const auto state = resolve_offset_restore_state(true, true, false);
EXPECT_TRUE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedConfigFallbackAsNotRestored) {
const auto state = resolve_offset_restore_state(true, false, true);
EXPECT_FALSE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsFailedConfigFallbackAsUnverified) {
const auto state = resolve_offset_restore_state(true, false, false);
EXPECT_FALSE(state.restored);
EXPECT_FALSE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsConfigWithoutStoredValuesAsNotRestored) {
const auto state = resolve_offset_restore_state(false, true, false);
EXPECT_FALSE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetPersistence, RollsBackStoredValuesOrZeroSentinel) {
const OffsetCalibration previous[3]{{1, -1}, {2, -2}, {3, -3}};
OffsetCalibration rollback[3]{};
prepare_offset_rollback(previous, true, rollback);
for (uint8_t phase = 0; phase < 3; phase++) {
EXPECT_EQ(rollback[phase].first_offset, previous[phase].first_offset);
EXPECT_EQ(rollback[phase].second_offset, previous[phase].second_offset);
}
prepare_offset_rollback(previous, false, rollback);
for (const auto &phase : rollback) {
EXPECT_EQ(phase.first_offset, 0);
EXPECT_EQ(phase.second_offset, 0);
}
}
} // namespace esphome::atm90e32::testing
@@ -1,29 +0,0 @@
# Tuya without any network component (no wifi/ethernet/api), as used on
# serial-only or BLE-only Tuya MCU boards. Regression test for
# https://github.com/esphome/esphome/issues/18942
substitutions:
status_pin: P6
packages:
uart: !include ../../test_build_components/common/uart/bk72xx-ard.yaml
tuya:
status_pin: ${status_pin}
binary_sensor:
- platform: tuya
id: tuya_presence
sensor_datapoint: 101
sensor:
- platform: tuya
id: tuya_light_intensity
sensor_datapoint: 103
number:
- platform: tuya
id: tuya_far_detection
number_datapoint: 109
min_value: 0
max_value: 600
step: 1
@@ -0,0 +1,43 @@
esphome:
name: api-decode-wire-types-test
host:
api:
logger:
level: DEBUG
switch:
- platform: template
name: "Wire Switch"
optimistic: true
output:
- platform: template
id: wire_dim
type: float
write_action:
- lambda: ""
light:
- platform: monochromatic
name: "Wire Light"
output: wire_dim
default_transition_length: 0s
effects:
- pulse:
name: Pulse
text:
- platform: template
name: "Wire Text"
optimistic: true
mode: text
min_length: 0
max_length: 255
number:
- platform: template
name: "Wire Number"
optimistic: true
min_value: -1000
max_value: 1000
step: 0.5
@@ -0,0 +1,11 @@
esphome:
name: api-empty-message-test
host:
api:
logger:
level: DEBUG
switch:
- platform: template
name: "Empty Message Switch"
optimistic: true
@@ -0,0 +1,58 @@
esphome:
name: api-encode-boundaries-test
# Top-level area fills DeviceInfoResponse.suggested_area (field 16, a two-byte tag)
area:
id: kitchen_area
name: Kitchen
on_boot:
- sensor.template.publish:
id: zero_then_value
state: 0.0
host:
api:
logger:
level: DEBUG
sensor:
- platform: template
name: "Zero Then Value"
id: zero_then_value
# Negative int32 takes the ten byte varint path
accuracy_decimals: -2
update_interval: never
text_sensor:
- platform: template
name: "Long Text"
id: long_text
update_interval: never
number:
- platform: template
name: "Negative Number"
optimistic: true
min_value: -1000
max_value: 1000
step: 0.5
initial_value: -123.5
select:
- platform: template
name: "Long Option Select"
optimistic: true
options:
- short
- "option-with-a-name-long-enough-that-its-length-prefix-needs-two-varint-bytes-when-the-list-entities-response-is-encoded-xxxxxxxxxx"
initial_option: short
button:
- platform: template
name: "Publish Values"
on_press:
- sensor.template.publish:
id: zero_then_value
state: 12.5
- text_sensor.template.publish:
id: long_text
state: !lambda return std::string(200, 'y');
+5 -4
View File
@@ -125,11 +125,12 @@ class RawApiClient:
await self.read_until_frame(MESSAGE_TYPE_OF[api_pb2.HelloResponse]) await self.read_until_frame(MESSAGE_TYPE_OF[api_pb2.HelloResponse])
async def send_message(self, msg: message.Message) -> None: async def send_message(self, msg: message.Message) -> None:
await self.send_raw(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString())
async def send_raw(self, msg_type: int, payload: bytes) -> None:
"""Send a frame with a hand built payload, for shapes protobuf will not serialize."""
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
await loop.sock_sendall( await loop.sock_sendall(self._sock, encode_frame(msg_type, payload))
self._sock,
encode_frame(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString()),
)
async def read_until_frame(self, msg_type: int, timeout: float = 10.0) -> None: async def read_until_frame(self, msg_type: int, timeout: float = 10.0) -> None:
"""Read until at least one frame of msg_type has been received.""" """Read until at least one frame of msg_type has been received."""
+40
View File
@@ -57,6 +57,46 @@ async def wait_for_state(
return await asyncio.wait_for(future, timeout=timeout) return await asyncio.wait_for(future, timeout=timeout)
class StateWaiter:
"""Route one state subscription to any number of predicate waits."""
def __init__(self) -> None:
self._waiters: list[
tuple[Callable[[EntityState], bool], asyncio.Future[EntityState]]
] = []
def on_state(self, state: EntityState) -> None:
for predicate, future in self._waiters:
if future.done():
continue
try:
matched = predicate(state)
except Exception as exc: # noqa: BLE001 the wait re-raises it, the callback must not die
future.set_exception(exc)
continue
if matched:
future.set_result(state)
async def expect(
self,
predicate: Callable[[EntityState], bool],
timeout: float = 5.0,
label: str | None = None,
) -> EntityState:
"""Wait for the next state matching ``predicate``; states seen before this call do not count."""
entry = (predicate, asyncio.get_running_loop().create_future())
self._waiters.append(entry)
try:
async with asyncio.timeout(timeout):
return await entry[1]
except TimeoutError:
raise TimeoutError(
f"no state matched {label or predicate} within {timeout}s"
) from None
finally:
self._waiters.remove(entry)
def find_entity[T: EntityInfo]( def find_entity[T: EntityInfo](
entities: list[EntityInfo], entities: list[EntityInfo],
object_id_substring: str, object_id_substring: str,
@@ -0,0 +1,142 @@
"""decode_field() must take fields that match their declared wire type, drop the ones that do
not, skip unknown fields, and handle two byte tags, varints and length prefixes."""
from __future__ import annotations
from collections.abc import Callable
import struct
from aioesphomeapi import (
EntityState,
LightState,
NumberState,
SwitchState,
TextState,
api_pb2,
)
import pytest
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient, encode_varint
from .state_utils import InitialStateHelper, StateWaiter, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
SWITCH_COMMAND = MESSAGE_TYPE_OF[api_pb2.SwitchCommandRequest]
WIRE_VARINT, WIRE_LENGTH, WIRE_FIXED32 = 0, 2, 5
def tag(field: int, wire_type: int) -> bytes:
return encode_varint((field << 3) | wire_type)
@pytest.mark.asyncio
async def test_api_decode_wire_types(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
unused_tcp_port: int,
) -> None:
async with (
run_compiled(yaml_config),
api_client_connected() as client,
RawApiClient(unused_tcp_port) as raw,
):
entities, _ = await client.list_entities_services()
switch = require_entity(entities, "wire_switch")
light = require_entity(entities, "wire_light")
text = require_entity(entities, "wire_text")
number = require_entity(entities, "wire_number")
key = tag(1, WIRE_FIXED32) + struct.pack("<I", switch.key)
on, off = tag(2, WIRE_VARINT) + b"\x01", tag(2, WIRE_VARINT) + b"\x00"
switch_states: list[bool] = []
waiter = StateWaiter()
def on_state(state: EntityState) -> None:
if isinstance(state, SwitchState) and state.key == switch.key:
switch_states.append(state.state)
waiter.on_state(state)
def switch_is(value: bool) -> Callable[[EntityState], bool]:
return lambda s: (
isinstance(s, SwitchState) and s.key == switch.key and s.state is value
)
def number_is(value: float) -> Callable[[EntityState], bool]:
return lambda s: (
isinstance(s, NumberState) and s.key == number.key and s.state == value
)
initial = InitialStateHelper(entities)
client.subscribe_states(initial.on_state_wrapper(on_state))
await initial.wait_for_initial_states()
await raw.connect()
# A well formed command: fixed32 key, varint state
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True))
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# The same field with the wrong wire type is dropped, and a varint key never matches an
# entity; each of these would turn the switch on if the payload were read as a varint
seen = len(switch_states)
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x01\x01")
await raw.send_raw(
SWITCH_COMMAND, key + tag(2, WIRE_FIXED32) + b"\x01\x00\x00\x00"
)
await raw.send_raw(
SWITCH_COMMAND, tag(1, WIRE_VARINT) + encode_varint(switch.key) + on
)
# Ordered on the raw socket itself: this frame cannot be parsed before the bad ones, so
# the only switch state since the marker must be the one it produces
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True), label="switch on after wrong wire types")
assert switch_states[seen:] == [True]
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# Truncated bodies stop the decode loop without taking the connection down: a tag with its
# continuation bit set and nothing after it, a length prefix past the end of the payload,
# and a fixed32 with two of its four bytes
seen = len(switch_states)
await raw.send_raw(SWITCH_COMMAND, key + b"\x80")
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x7f" + b"ab")
await raw.send_raw(SWITCH_COMMAND, tag(1, WIRE_FIXED32) + b"\x01\x02")
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True), label="switch on after truncated frames")
assert switch_states[seen:] == [True]
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# A negative number goes through the fixed32 float path of a normal client
client.number_command(number.key, -77.5)
await waiter.expect(number_is(-77.5))
# An unknown field ahead of the known ones is skipped; field 200 needs a two byte tag
await raw.send_raw(
SWITCH_COMMAND, tag(200, WIRE_VARINT) + encode_varint(300) + key + on
)
await waiter.expect(switch_is(True))
# Two byte tags (effect fields 18 and 19) and a two byte varint (300 ms transition)
client.light_command(
light.key, state=True, brightness=0.5, transition_length=0.3, effect="Pulse"
)
await waiter.expect(
lambda s: (
isinstance(s, LightState) and s.key == light.key and s.effect == "Pulse"
)
)
client.light_command(light.key, effect="None", state=False)
await waiter.expect(
lambda s: isinstance(s, LightState) and s.key == light.key and not s.state
)
# A string whose length prefix needs two varint bytes
long_text = "w" * 200
client.text_command(text.key, long_text)
await waiter.expect(
lambda s: (
isinstance(s, TextState) and s.key == text.key and s.state == long_text
)
)
@@ -0,0 +1,37 @@
"""Messages without fields go through the shared ProtoMessage entry points on both directions."""
from __future__ import annotations
from aioesphomeapi import api_pb2
import pytest
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient
from .types import RunCompiledFunction
@pytest.mark.asyncio
async def test_api_empty_message_roundtrip(
yaml_config: str,
run_compiled: RunCompiledFunction,
unused_tcp_port: int,
) -> None:
async with run_compiled(yaml_config), RawApiClient(unused_tcp_port) as client:
await client.connect()
# Field free request and reply on the plain send path
await client.send_message(api_pb2.PingRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.PingResponse])
# Field free request answered by a message with fields, and a list that ends with
# the field free ListEntitiesDoneResponse through the batching path
await client.send_message(api_pb2.DeviceInfoRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.DeviceInfoResponse])
await client.send_message(api_pb2.ListEntitiesRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.ListEntitiesDoneResponse])
assert (
client.frame_counts[MESSAGE_TYPE_OF[api_pb2.ListEntitiesSwitchResponse]]
== 1
)
await client.send_message(api_pb2.DisconnectRequest())
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.DisconnectResponse])
@@ -0,0 +1,78 @@
"""Encode paths at their branch boundaries: zero skipped float, fixed32 state, negative int32,
length prefixes of two varint bytes and two byte field tags."""
from __future__ import annotations
import asyncio
from aioesphomeapi import (
NumberState,
SelectInfo,
SensorInfo,
SensorState,
TextSensorState,
)
import pytest
from .state_utils import InitialStateHelper, StateWaiter, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
LONG_OPTION = (
"option-with-a-name-long-enough-that-its-length-prefix-needs-two-varint-bytes-"
"when-the-list-entities-response-is-encoded-xxxxxxxxxx"
)
@pytest.mark.asyncio
async def test_api_encode_boundaries(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
async with run_compiled(yaml_config), api_client_connected() as client:
device_info, (entities, _) = await asyncio.gather(
client.device_info(), client.list_entities_services()
)
assert device_info.suggested_area == "Kitchen"
sensor = require_entity(entities, "zero_then_value", SensorInfo)
assert sensor.accuracy_decimals == -2
select = require_entity(entities, "long_option_select", SelectInfo)
assert len(LONG_OPTION) >= 128
assert select.options == ["short", LONG_OPTION]
text = require_entity(entities, "long_text")
number = require_entity(entities, "negative_number")
button = require_entity(entities, "publish_values")
initial = InitialStateHelper(entities)
waiter = StateWaiter()
client.subscribe_states(initial.on_state_wrapper(waiter.on_state))
await initial.wait_for_initial_states()
# A float of exactly zero is skipped on the wire and must still read as 0.0, not missing
first = initial.initial_states[sensor.key]
assert isinstance(first, SensorState)
assert first.state == 0.0 and not first.missing_state
first_number = initial.initial_states[number.key]
assert isinstance(first_number, NumberState)
assert first_number.state == -123.5
client.button_command(button.key)
await asyncio.gather(
waiter.expect(
lambda s: (
isinstance(s, SensorState)
and s.key == sensor.key
and s.state == 12.5
),
label="sensor 12.5",
),
waiter.expect(
lambda s: (
isinstance(s, TextSensorState)
and s.key == text.key
and s.state == "y" * 200
),
label="text 200 x y",
),
)
@@ -1,219 +0,0 @@
"""Unit tests for script/sync_dependency_versions.py."""
from pathlib import Path
import subprocess
import sys
import pytest
import yamlrocks
sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve()))
import sync_dependency_versions as sync_mod # noqa: E402
PRECOMMIT = """\
# See https://pre-commit.com for more information
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.0
hooks:
- id: ruff
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v3.0.0
hooks:
- id: pyupgrade
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v13.0.1
hooks:
- id: clang-format
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.0.0
hooks:
- id: yamllint
- repo: local
hooks:
- id: pylint
"""
REQ_TEST = """\
pylint==4.0.8
flake8==7.1.0
ruff==0.2.0 # comment
pyupgrade==3.0.0
"""
REQ_DEV = """\
clang-format==13.0.1
yamllint==1.0.0
"""
RUFF_REPO = "https://github.com/astral-sh/ruff-pre-commit"
DUPLICATE_RUFF_BLOCK = f" - repo: {RUFF_REPO}\n rev: v0.3.0\n hooks: []\n"
EXPECTED_DRIFT = ["ruff: 0.1.0 -> 0.2.0", "flake8: 7.0.0 -> 7.1.0"]
EXPECTED_PRECOMMIT = PRECOMMIT.replace("rev: v0.1.0", "rev: v0.2.0").replace(
"rev: 7.0.0", "rev: 7.1.0"
)
@pytest.fixture
def root(tmp_path: Path) -> Path:
"""A fake checkout where ruff (v-prefixed) and flake8 (bare) have drifted."""
(tmp_path / ".pre-commit-config.yaml").write_text(PRECOMMIT)
(tmp_path / "requirements_test.txt").write_text(REQ_TEST)
(tmp_path / "requirements_dev.txt").write_text(REQ_DEV)
return tmp_path
def _load(text: str) -> object:
return yamlrocks.loads(text.encode(), option=yamlrocks.OPT_ROUND_TRIP)
@pytest.mark.parametrize(
("requirements", "expected"),
[
("prek==0.5.1 # comment\n", "0.5.1"),
("Prek==0.5.1\n", "0.5.1"),
("other==1.0\nprek==0.5.1\n", "0.5.1"),
("prek>=0.5.1\n", None),
("prek-extra==0.5.1\n", None),
("", None),
],
)
def test_read_requirement_version(requirements: str, expected: str | None) -> None:
assert sync_mod.read_requirement_version(requirements, "prek") == expected
def test_find_repo_entry() -> None:
entry = sync_mod.find_repo_entry(_load(PRECOMMIT), RUFF_REPO)
assert entry["rev"] == "v0.1.0"
@pytest.mark.parametrize(
("text", "message"),
[
("hooks: []\n", "missing key 'repos'"),
("repos:\n - rev: 1.0.0\n", "missing key 'repo'"),
(PRECOMMIT + DUPLICATE_RUFF_BLOCK, "found 2"),
("repos:\n - repo: other\n rev: 1.0.0\n", "found 0"),
],
)
def test_find_repo_entry_errors(text: str, message: str) -> None:
with pytest.raises(sync_mod.SyncError, match=message):
sync_mod.find_repo_entry(_load(text), RUFF_REPO)
@pytest.mark.parametrize(
("rev", "expected"),
[("v0.1.0", ("v", "0.1.0")), ("7.0.0", ("", "7.0.0")), ("'1.0'", ("", "1.0"))],
)
def test_current_rev(rev: str, expected: tuple[str, str]) -> None:
doc = _load(f"repos:\n - repo: {RUFF_REPO}\n rev: {rev}\n")
assert sync_mod.current_rev(doc["repos"][0], RUFF_REPO) == expected
@pytest.mark.parametrize(
("block", "message"),
[(" hooks: []\n", "has no rev"), (" rev: 1.0\n", "not a string: 1.0")],
)
def test_current_rev_errors(block: str, message: str) -> None:
doc = _load(f"repos:\n - repo: {RUFF_REPO}\n{block}")
with pytest.raises(sync_mod.SyncError, match=message):
sync_mod.current_rev(doc["repos"][0], RUFF_REPO)
def test_sync_reports_without_writing(root: Path) -> None:
assert sync_mod.sync(root, write=False) == EXPECTED_DRIFT
assert (root / ".pre-commit-config.yaml").read_text() == PRECOMMIT
def test_sync_writes_keeps_layout_and_is_idempotent(root: Path) -> None:
assert sync_mod.sync(root, write=True) == EXPECTED_DRIFT
assert (root / ".pre-commit-config.yaml").read_text() == EXPECTED_PRECOMMIT
assert sync_mod.sync(root, write=True) == []
def test_sync_does_not_touch_a_config_that_matches(root: Path) -> None:
(root / ".pre-commit-config.yaml").write_text(EXPECTED_PRECOMMIT)
before = (root / ".pre-commit-config.yaml").stat().st_mtime_ns
assert sync_mod.sync(root, write=True) == []
assert (root / ".pre-commit-config.yaml").stat().st_mtime_ns == before
def test_sync_missing_requirement_pin(root: Path) -> None:
(root / "requirements_dev.txt").write_text("")
with pytest.raises(sync_mod.SyncError, match="no 'clang-format==' pin"):
sync_mod.sync(root, write=True)
def test_sync_propagates_config_errors(root: Path) -> None:
(root / ".pre-commit-config.yaml").write_text(PRECOMMIT + DUPLICATE_RUFF_BLOCK)
with pytest.raises(sync_mod.SyncError, match="found 2"):
sync_mod.sync(root, write=True)
def test_main_check_reports_drift(
root: Path, capsys: pytest.CaptureFixture[str]
) -> None:
assert sync_mod.main(["--check", "--root", str(root)]) == 1
assert capsys.readouterr().out.splitlines() == EXPECTED_DRIFT
assert (root / ".pre-commit-config.yaml").read_text() == PRECOMMIT
def test_main_writes_then_check_is_clean(
root: Path, capsys: pytest.CaptureFixture[str]
) -> None:
assert sync_mod.main(["--root", str(root)]) == 0
assert capsys.readouterr().out.splitlines() == EXPECTED_DRIFT
assert sync_mod.main(["--check", "--root", str(root)]) == 0
assert capsys.readouterr().out == ""
def test_main_reports_sync_error(
root: Path, capsys: pytest.CaptureFixture[str]
) -> None:
(root / "requirements_dev.txt").write_text("")
assert sync_mod.main(["--root", str(root)]) == 1
assert (
"error: requirements_dev.txt: no 'clang-format==' pin"
in capsys.readouterr().err
)
def test_main_defaults_to_repo_root(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, object] = {}
def fake_sync(root: Path, *, write: bool) -> list[str]:
seen["root"] = root
seen["write"] = write
return []
monkeypatch.setattr(sync_mod, "sync", fake_sync)
assert sync_mod.main([]) == 0
assert seen == {"root": sync_mod.REPO_ROOT, "write": True}
def test_repository_is_in_sync() -> None:
"""The real checkout must match; a failure here means a rev has drifted.
Also proves every SYNC_TARGETS entry still resolves in the real files.
"""
assert sync_mod.sync(sync_mod.REPO_ROOT, write=False) == []
def test_cli_entry_point(root: Path) -> None:
"""Run the script the way the workflow does, as a subprocess."""
script = Path(sync_mod.__file__)
result = subprocess.run(
[sys.executable, str(script), "--check", "--root", str(root)],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 1
assert result.stdout.splitlines() == EXPECTED_DRIFT
@@ -194,17 +194,17 @@ def test_superseded_device_info_fields_still_declared_in_header() -> None:
def test_superseded_device_info_fields_still_encoded_and_sized() -> None: def test_superseded_device_info_fields_still_encoded_and_sized() -> None:
"""Each superseded field must still be touched by DeviceInfoResponse's """Each superseded field must still be touched by DeviceInfoResponse's
generated encode() and calculate_size(), i.e. it is still put on the wire. generated encode_msg() and calc_size_msg(), i.e. it is still put on the wire.
""" """
encode_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::encode") encode_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::encode_msg")
size_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::calculate_size") size_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::calc_size_msg")
for field_name in SUPERSEDED_FIELDS: for field_name in SUPERSEDED_FIELDS:
assert f"this->{field_name}" in encode_body, ( assert f"msg.{field_name}" in encode_body, (
f"DeviceInfoResponse::encode() no longer references {field_name}. " f"DeviceInfoResponse::encode_msg() no longer references {field_name}. "
f"{DEPRECATED_FIELD_TRAP}" f"{DEPRECATED_FIELD_TRAP}"
) )
assert f"this->{field_name}" in size_body, ( assert f"msg.{field_name}" in size_body, (
f"DeviceInfoResponse::calculate_size() no longer references " f"DeviceInfoResponse::calc_size_msg() no longer references "
f"{field_name}. {DEPRECATED_FIELD_TRAP}" f"{field_name}. {DEPRECATED_FIELD_TRAP}"
) )
@@ -380,3 +380,13 @@ def test_api_version_minor_is_at_least_15() -> None:
"clients to see api_version >= 1.15 in HelloResponse before they will " "clients to see api_version >= 1.15 in HelloResponse before they will "
"ever request it." "ever request it."
) )
def test_generated_encode_calls_keep_the_cursor() -> None:
"""No generated ProtoEncode call may drop the returned cursor."""
dropped = [
line
for line in CPP_TEXT.splitlines()
if "ProtoEncode::" in line and "pos = ProtoEncode::" not in line
]
assert not dropped, dropped[:5]
@@ -15,9 +15,13 @@ import pytest
sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf")) sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
import aioesphomeapi.api_options_pb2 as pb # noqa: E402
from api_protobuf import ( # noqa: E402 from api_protobuf import ( # noqa: E402
MAX_MESSAGE_ID, MAX_MESSAGE_ID,
SOURCE_CLIENT,
_make_ifdef_line, _make_ifdef_line,
build_message_type,
create_field_type_info,
get_varint64_ifdef, get_varint64_ifdef,
validate_message_id, validate_message_id,
) )
@@ -34,16 +38,26 @@ def _file_with_messages(
file_desc = descriptor_pb2.FileDescriptorProto(name="test.proto") file_desc = descriptor_pb2.FileDescriptorProto(name="test.proto")
for name, field_type, deprecated in messages: for name, field_type, deprecated in messages:
msg = file_desc.message_type.add(name=name) msg = file_desc.message_type.add(name=name)
field = msg.field.add(name="value", number=1, type=field_type) field = msg.field.add()
field.CopyFrom(_field(field_type))
field.options.deprecated = deprecated field.options.deprecated = deprecated
return file_desc return file_desc
UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64 UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64
MESSAGE = descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE
DOUBLE = descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE
INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64 INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64
SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64 SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64
UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32 UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32
INT32 = descriptor_pb2.FieldDescriptorProto.TYPE_INT32
SINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT32
FIXED64 = descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64 FIXED64 = descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64
FIXED32 = descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32
FLOAT = descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT
BOOL = descriptor_pb2.FieldDescriptorProto.TYPE_BOOL
STRING = descriptor_pb2.FieldDescriptorProto.TYPE_STRING
BYTES = descriptor_pb2.FieldDescriptorProto.TYPE_BYTES
def test_no_varint64_fields() -> None: def test_no_varint64_fields() -> None:
@@ -107,3 +121,172 @@ def test_message_id_at_maximum_is_accepted() -> None:
def test_message_id_above_maximum_is_rejected() -> None: def test_message_id_above_maximum_is_rejected() -> None:
with pytest.raises(ValueError, match="exceeds the plaintext"): with pytest.raises(ValueError, match="exceeds the plaintext"):
validate_message_id(MAX_MESSAGE_ID + 1, "TooBigMessage") validate_message_id(MAX_MESSAGE_ID + 1, "TooBigMessage")
def _field(
field_type: int, number: int = 1, *, force: bool = False, repeated: bool = False
) -> descriptor_pb2.FieldDescriptorProto:
field = descriptor_pb2.FieldDescriptorProto(
name="value", number=number, type=field_type
)
if repeated:
field.label = descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED
if force:
field.options.Extensions[pb.force] = True
return field
def _encode_field(
field_type: int, number: int = 1, force: bool = False, repeated: bool = False
) -> str:
"""Return the encode statement the generator emits for one encode-only field."""
field = _field(field_type, number, force=force, repeated=repeated)
return create_field_type_info(
field, needs_decode=False, needs_encode=True
).encode_content
SCALAR_TYPES = [
BOOL,
UINT32,
INT32,
UINT64,
INT64,
SINT32,
FLOAT,
FIXED32,
STRING,
BYTES,
]
@pytest.mark.parametrize("field_type", SCALAR_TYPES)
def test_forced_fields_use_the_force_overload_or_raw_writes(field_type: int) -> None:
content = _encode_field(field_type, force=True)
assert (
"_force(" in content
or "write_raw_byte(" in content
or "write_tag_and_fixed32(" in content
), content
@pytest.mark.parametrize("field_type", [FLOAT, FIXED32])
def test_single_byte_tag_fixed32_shares_the_outlined_writer(field_type: int) -> None:
unconditional = _encode_field(field_type, force=True)
assert unconditional.count("write_tag_and_fixed32(pos, 13,") == 1, unconditional
guarded = _encode_field(field_type, force=False)
assert guarded.startswith("if ("), guarded
assert "[[likely]]" in guarded
assert "write_tag_and_fixed32(pos, 13," in guarded
@pytest.mark.parametrize("field_type", [FLOAT, FIXED32])
def test_multi_byte_tag_fixed32_falls_back_to_the_generic_helper(
field_type: int,
) -> None:
content = _encode_field(field_type, number=16)
assert "write_tag_and_fixed32" not in content, content
assert content.startswith("pos = ProtoEncode::encode_"), content
def _decode_case(field_type: int, number: int, *, repeated: bool = False) -> str:
"""Return the decode_field() case the generator emits for one decoded field."""
field = _field(field_type, number, repeated=repeated)
if field_type == MESSAGE:
field.type_name = ".Sub"
return create_field_type_info(
field, needs_decode=True, needs_encode=False
).decode_content
@pytest.mark.parametrize(
("field_type", "number", "wire_type", "accessor"),
[
(UINT32, 2, "WIRE_TYPE_VARINT", "value.as_varint()"),
(BOOL, 3, "WIRE_TYPE_VARINT", "value.as_bool()"),
(STRING, 1, "WIRE_TYPE_LENGTH_DELIMITED", "value.data()"),
(FLOAT, 4, "WIRE_TYPE_FIXED32", "value.as_float()"),
(FIXED32, 5, "WIRE_TYPE_FIXED32", "value.as_fixed32()"),
],
)
def test_decode_cases_carry_field_number_and_wire_type(
field_type: int, number: int, wire_type: str, accessor: str
) -> None:
"""Each decoded field yields one case keyed on its number and declared wire type."""
case = _decode_case(field_type, number)
lines = case.splitlines()
assert lines[0] == f"case proto_tag({number}, {wire_type}):", case
assert accessor in lines[1], case
assert lines[-1].strip() == "break;", case
@pytest.mark.parametrize(
("field_type", "repeated", "wire_type", "store"),
[
(UINT32, True, "WIRE_TYPE_VARINT", "this->value.push_back(value.as_varint());"),
(
STRING,
True,
"WIRE_TYPE_LENGTH_DELIMITED",
"this->value.push_back(value.as_string());",
),
(
MESSAGE,
False,
"WIRE_TYPE_LENGTH_DELIMITED",
"value.decode_to_message(this->value);",
),
(
MESSAGE,
True,
"WIRE_TYPE_LENGTH_DELIMITED",
"value.decode_to_message(this->value.back());",
),
],
)
def test_repeated_and_message_fields_decode_through_the_same_case_shape(
field_type: int, repeated: bool, wire_type: str, store: str
) -> None:
"""Repeated and sub message fields land in the one switch with their own store."""
case = _decode_case(field_type, 7, repeated=repeated)
lines = case.splitlines()
assert lines[0] == f"case proto_tag(7, {wire_type}):", case
assert store in case, case
if field_type == MESSAGE and repeated:
assert "this->value.emplace_back();" in case, case
assert lines[-1].strip() == "break;", case
def test_a_fixed64_field_fails_at_generation_time() -> None:
"""The decode loop has no 64 bit wire type path, so such a field must never reach it silently."""
desc = descriptor_pb2.DescriptorProto(name="Wide")
desc.field.add(name="ratio", number=1, type=DOUBLE)
with pytest.raises(
ValueError, match="64-bit type 'double' .*ratio.* not supported"
):
build_message_type(desc, {}, {"Wide": SOURCE_CLIENT})
def test_message_gets_a_single_decode_field_override() -> None:
"""All wire types of a decoded message land in one decode_field() switch."""
desc = descriptor_pb2.DescriptorProto(name="Mixed")
desc.field.add(name="name", number=1, type=STRING)
desc.field.add(name="count", number=2, type=UINT32)
desc.field.add(name="level", number=3, type=FLOAT)
header, cpp, _ = build_message_type(desc, {}, {"Mixed": SOURCE_CLIENT})
decl = "void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;"
assert header.count(decl) == 1
assert (
cpp.count(
"void Mixed::decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {"
)
== 1
)
assert "switch (tag) {" in cpp
assert "const ProtoFieldValue value(data, scalar);" in cpp
for number, wire_type in (
(1, "WIRE_TYPE_LENGTH_DELIMITED"),
(2, "WIRE_TYPE_VARINT"),
(3, "WIRE_TYPE_FIXED32"),
):
assert f"case proto_tag({number}, {wire_type}):" in cpp, cpp
@@ -1,37 +0,0 @@
"""Tests for the udp component configuration schema."""
from __future__ import annotations
import pytest
from esphome.components import udp
from esphome.components.packet_transport import (
CONF_BINARY_SENSORS,
CONF_ENCRYPTION,
CONF_PING_PONG_ENABLE,
CONF_PROVIDERS,
CONF_ROLLING_CODE_ENABLE,
CONF_SENSORS,
)
import esphome.config_validation as cv
@pytest.mark.parametrize(
"option",
[
CONF_PROVIDERS,
CONF_ENCRYPTION,
CONF_PING_PONG_ENABLE,
CONF_ROLLING_CODE_ENABLE,
CONF_SENSORS,
CONF_BINARY_SENSORS,
],
)
def test_relocated_option_rejected(option: str) -> None:
"""Options that moved to packet_transport raise a pointing error."""
with pytest.raises(cv.Invalid) as exc_info:
udp.CONFIG_SCHEMA({option: True})
assert (
f"The '{option}' option should now be configured in the 'packet_transport' component"
in str(exc_info.value)
)
@@ -7,7 +7,6 @@ exercised in their own test modules)."""
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from unittest.mock import Mock
import pytest import pytest
@@ -229,24 +228,6 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch):
_resolve_registry_version("owner", "pkg", set()) _resolve_registry_version("owner", "pkg", set())
def test_make_registry_client_skips_private_package_probe(monkeypatch):
"""Our client answers the probe locally without patching PlatformIO's class."""
from platformio.account.client import AccountClient
from platformio.registry.client import RegistryClient
pio_probe = RegistryClient.__dict__["allowed_private_packages"]
monkeypatch.setattr(
AccountClient,
"get_account_info",
Mock(side_effect=AssertionError("account probe must not run")),
)
client = lib._make_registry_client().get_registry_client_instance()
assert client.allowed_private_packages() is False
assert RegistryClient.__dict__["allowed_private_packages"] is pio_probe
def _patch_registry_resolve(monkeypatch: pytest.MonkeyPatch) -> None: def _patch_registry_resolve(monkeypatch: pytest.MonkeyPatch) -> None:
"""Stub the registry lookup so tests never touch the network.""" """Stub the registry lookup so tests never touch the network."""
monkeypatch.setattr( monkeypatch.setattr(
+3 -52
View File
@@ -661,8 +661,7 @@ def test_registry_jobs_one_bad_spec_keeps_the_rest(tmp_path: Path) -> None:
def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None: def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None:
"""HEAD sizes direct-URL specs; VCS specs skip the download but are """HEAD sizes direct-URL specs; git and unreachable URLs are skipped."""
still installable (the pre-install clones them in parallel)."""
m = _fake_manager(tmp_path) m = _fake_manager(tmp_path)
resp = MagicMock() resp = MagicMock()
resp.headers = {"content-length": "2222"} resp.headers = {"content-length": "2222"}
@@ -672,13 +671,14 @@ def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None:
[ [
_FakeSpec(uri="https://x/big.zip", name="big", custom_name=True), _FakeSpec(uri="https://x/big.zip", name="big", custom_name=True),
_FakeSpec(uri="git+https://x/repo.git", name="repo"), _FakeSpec(uri="git+https://x/repo.git", name="repo"),
_FakeSpec(uri="https://x/repo.git#v1", name="barevcs"),
_FakeSpec(name="registry"), _FakeSpec(name="registry"),
], ],
set(), set(),
) )
assert failed == 0 assert failed == 0
assert [(n, s) for n, s, _ in jobs] == [("big", 2222)] assert [(n, s) for n, s, _ in jobs] == [("big", 2222)]
assert [n for n, _ in installable] == ["repo", "big"] assert [n for n, _ in installable] == ["big"]
# a successful HEAD with no Content-Length is a clean skip # a successful HEAD with no Content-Length is a clean skip
resp.headers = {} resp.headers = {}
with patch("esphome.net_retry.http_request", return_value=resp): with patch("esphome.net_retry.http_request", return_value=resp):
@@ -687,35 +687,6 @@ def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None:
) == ([], 0, []) ) == ([], 0, [])
def test_uri_jobs_vcs_specs_installable_without_probe(tmp_path: Path) -> None:
"""VCS specs never probe the network here (there is no archive); an
uninstalled one is handed to the pre-install, an installed one and
file/symlink specs are skipped."""
m = _fake_manager(tmp_path)
with patch("esphome.net_retry.http_request") as mock_head:
jobs, failed, installable = pf._uri_jobs(
m,
[
_FakeSpec(uri="git+https://x/tool.git#1.0", name="tool"),
_FakeSpec(uri="hg+https://x/old", name="mercurial"),
# Name falls back to the URL basename, fragment excluded
_FakeSpec(uri="git+https://x/noname#v2", name=None),
_FakeSpec(uri="file:///local/dir", name="local"),
_FakeSpec(uri="symlink:///local/dir", name="link"),
],
set(),
)
mock_head.assert_not_called()
assert (jobs, failed) == ([], 0)
assert [n for n, _ in installable] == ["tool", "mercurial", "noname"]
m.get_package.return_value = object() # already installed: warm and silent
with patch("esphome.net_retry.http_request"):
assert pf._uri_jobs(
m, [_FakeSpec(uri="git+https://x/tool.git#1.0", name="tool")], set()
) == ([], 0, [])
def test_uri_jobs_head_failure_counts_as_unresolved( def test_uri_jobs_head_failure_counts_as_unresolved(
tmp_path: Path, caplog: pytest.LogCaptureFixture tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
@@ -1254,20 +1225,6 @@ def test_main_runs_prefetch(tmp_path: Path) -> None:
mock_prefetch.assert_called_once_with(tmp_path, "testenv") mock_prefetch.assert_called_once_with(tmp_path, "testenv")
def test_main_skips_private_package_probe_before_prefetch(tmp_path: Path) -> None:
"""The registry probe patch is applied before any package manager runs."""
order: list[str] = []
with (
patch.object(pf, "_prefetch", side_effect=lambda *_: order.append("prefetch")),
patch(
"esphome.platformio.runner.patch_registry_private_packages",
side_effect=lambda: order.append("patch"),
),
):
assert pf.main([str(tmp_path), "testenv"]) == 0
assert order == ["patch", "prefetch"]
def test_main_bad_argv_is_a_distinct_exit( def test_main_bad_argv_is_a_distinct_exit(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
@@ -1919,9 +1876,3 @@ def test_platformio_private_api_contract() -> None:
derived = PackageSpec("https://x/y/archive/master.zip") derived = PackageSpec("https://x/y/archive/master.zip")
assert derived.name and not derived.has_custom_name() assert derived.name and not derived.has_custom_name()
assert PackageSpec("Foo=https://x/y/archive/master.zip").has_custom_name() assert PackageSpec("Foo=https://x/y/archive/master.zip").has_custom_name()
# _is_vcs_spec_uri relies on bare .git URLs normalizing to git+, on
# both parse paths (raw string, and requirements= for platform tools)
assert PackageSpec("https://github.com/x/y.git#v1").uri.startswith("git+")
assert PackageSpec(
owner="o", name="tool-x", requirements="https://github.com/x/y.git"
).uri.startswith("git+")
@@ -6,9 +6,7 @@ from collections.abc import Callable
import io import io
import sys import sys
from types import ModuleType from types import ModuleType
from unittest.mock import Mock
from platformio.registry.client import RegistryClient
import pytest import pytest
from esphome.platformio import runner from esphome.platformio import runner
@@ -32,7 +30,6 @@ def _prepare_main(
monkeypatch.setattr(sys, "stderr", stream) monkeypatch.setattr(sys, "stderr", stream)
monkeypatch.setattr(runner, "patch_structhash", lambda: None) monkeypatch.setattr(runner, "patch_structhash", lambda: None)
monkeypatch.setattr(runner, "patch_file_downloader", lambda: None) monkeypatch.setattr(runner, "patch_file_downloader", lambda: None)
monkeypatch.setattr(runner, "patch_registry_private_packages", lambda: None)
platformio = ModuleType("platformio") platformio = ModuleType("platformio")
platformio_main = ModuleType("platformio.__main__") platformio_main = ModuleType("platformio.__main__")
@@ -94,40 +91,3 @@ def test_main_still_filters_a_drained_partial_line(
assert runner.main() == 0 assert runner.main() == 0
assert buf.getvalue() == b"" assert buf.getvalue() == b""
def test_main_applies_registry_private_packages_patch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The probe is patched before PlatformIO runs."""
order: list[str] = []
_prepare_main(monkeypatch, lambda: order.append("pio") or 0)
monkeypatch.setattr(
runner, "patch_registry_private_packages", lambda: order.append("patch")
)
assert runner.main() == 0
assert order == ["patch", "pio"]
# Snapshot PlatformIO's own probe at import, before any test can patch it
_PIO_PROBE = RegistryClient.__dict__["allowed_private_packages"]
def test_patch_registry_private_packages_skips_account_probe(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Answers False without touching the account client."""
from platformio.account.client import AccountClient
monkeypatch.setattr(RegistryClient, "allowed_private_packages", _PIO_PROBE)
monkeypatch.setattr(
AccountClient,
"get_account_info",
Mock(side_effect=AssertionError("account probe must not run")),
)
runner.patch_registry_private_packages()
assert RegistryClient.allowed_private_packages() is False
assert RegistryClient().allowed_private_packages() is False