mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 22:06:12 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73564dbebd | ||
|
|
4165dbb534 | ||
|
|
3bc9f74f46 | ||
|
|
0dd27c5d08 | ||
|
|
84b0f35b91 | ||
|
|
2aa3677bbf | ||
|
|
c2fbcf0f24 | ||
|
|
afbd2ab7b1 | ||
|
|
3c65c55df0 | ||
|
|
7704ad9384 | ||
|
|
50fdc8e85b | ||
|
|
3e6324445b | ||
|
|
e0e5cbdc27 | ||
|
|
870b80e463 | ||
|
|
899e88be72 |
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
|
||||
- name: Upload compile-test image artifact
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
# The tar is already compressed, so upload it as-is. archive: false
|
||||
# skips the redundant zip and makes the file name the artifact name
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Download image artifact
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: compile-test-image.tar.zst
|
||||
- name: Load image
|
||||
|
||||
+22
-36
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -81,8 +81,8 @@ jobs:
|
||||
- name: Install apt packages (cached)
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
|
||||
determine-jobs:
|
||||
name: Determine which jobs to run
|
||||
@@ -415,8 +415,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
@@ -438,7 +438,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -477,7 +477,7 @@ jobs:
|
||||
# Tests that compare rendered output write the image they actually got here, so a
|
||||
# failure can be looked at without reproducing the whole build locally.
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: integration-test-artifacts-${{ matrix.bucket.name }}
|
||||
path: test_artifacts/
|
||||
@@ -488,7 +488,7 @@ jobs:
|
||||
# script/update_integration_test_durations.py; only full matrix dev
|
||||
# runs produce usable data.
|
||||
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: junit-integration-${{ strategy.job-index }}
|
||||
path: junit-integration.xml
|
||||
@@ -528,7 +528,7 @@ jobs:
|
||||
script/check_import_time.py --check --har importtime.har
|
||||
- name: Upload waterfall HAR
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: import-time-waterfall
|
||||
path: importtime.har
|
||||
@@ -626,7 +626,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
|
||||
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -680,7 +680,7 @@ jobs:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||
pio_cache_key: tidyesp8266
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino
|
||||
@@ -693,16 +693,12 @@ jobs:
|
||||
ignore_errors: false
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for RP2
|
||||
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
|
||||
options: --environment rp2-tidy --grep USE_RP2
|
||||
pio_cache_key: tidyrp2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for host
|
||||
options: --environment host-tidy --grep USE_HOST
|
||||
pio_cache_key: tidyhost
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for LibreTiny
|
||||
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
|
||||
pio_cache_key: tidylibretiny
|
||||
|
||||
steps:
|
||||
@@ -711,16 +707,6 @@ jobs:
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
- name: Install apt packages (cached)
|
||||
if: matrix.pio_cache_key == 'tidyhost'
|
||||
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
|
||||
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
|
||||
# exactly.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
@@ -1003,14 +989,14 @@ jobs:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 S3
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
|
||||
tidy_environment: esp32s3-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 P4
|
||||
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
|
||||
# so their code paths differ -- lint them under the P4 build too.
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
tidy_environment: esp32p4-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 C6
|
||||
@@ -1116,8 +1102,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -1462,7 +1448,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1661,7 +1647,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -1724,7 +1710,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -1754,13 +1740,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -10,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.17.0
|
||||
rev: v0.16.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -36,7 +36,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.22.0
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
|
||||
-11
@@ -183,7 +183,6 @@ esphome/components/es7243e/* @kbx81
|
||||
esphome/components/es8156/* @kbx81
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
esphome/components/es8388/* @P4uLT
|
||||
esphome/components/es9038q2m_katana/* @GioMez
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
|
||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
|
||||
@@ -213,14 +212,12 @@ esphome/components/factory_reset/* @anatoly-savchenkov
|
||||
esphome/components/fastled_base/* @OttoWinter
|
||||
esphome/components/feedback/* @ianchi
|
||||
esphome/components/file/* @esphome/core
|
||||
esphome/components/filter_lifetime/* @tuct
|
||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||
esphome/components/font/* @clydebarrow @esphome/core
|
||||
esphome/components/friedrich/* @rwrozelle
|
||||
esphome/components/fs3000/* @kahrendt
|
||||
esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
esphome/components/fusb302b/* @remcom
|
||||
esphome/components/gcja5/* @gcormier
|
||||
esphome/components/gdk101/* @Szewcson
|
||||
esphome/components/gl_r01_i2c/* @pkejval
|
||||
@@ -242,13 +239,9 @@ esphome/components/gt911/* @clydebarrow @jesserockz
|
||||
esphome/components/haier/* @paveldn
|
||||
esphome/components/haier/binary_sensor/* @paveldn
|
||||
esphome/components/haier/button/* @paveldn
|
||||
esphome/components/haier/select/* @paveldn
|
||||
esphome/components/haier/sensor/* @paveldn
|
||||
esphome/components/haier/switch/* @paveldn
|
||||
esphome/components/haier/text_sensor/* @paveldn
|
||||
esphome/components/haier_tundra/* @fauxpark
|
||||
esphome/components/haier_tundra/button/* @fauxpark
|
||||
esphome/components/haier_tundra/switch/* @fauxpark
|
||||
esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
@@ -257,7 +250,6 @@ esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/hdc2080/* @G-Pereira @jesserockz
|
||||
esphome/components/hdc302x/* @joshuasing
|
||||
esphome/components/hdmi_cec/* @JosVanEijndhoven
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
@@ -294,7 +286,6 @@ esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/i2s_clockless_led_strip/* @j9brown
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/icm20689/* @lboue
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
esphome/components/improv_base/* @esphome/core
|
||||
@@ -389,7 +380,6 @@ esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/midea_portasplit/* @Fexiven
|
||||
esphome/components/mipi_dsi/* @clydebarrow
|
||||
esphome/components/mipi_rgb/* @clydebarrow
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
@@ -444,7 +434,6 @@ esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9505/* @ajfriesen @ssieb
|
||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
|
||||
@@ -2232,6 +2232,12 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def command_rotate_key(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome.cli.rotate_key import command_rotate_key as run
|
||||
|
||||
return run(args, config)
|
||||
|
||||
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome.cli.rename import command_rename as run
|
||||
|
||||
@@ -2258,6 +2264,7 @@ POST_CONFIG_ACTIONS = {
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"rotate-key": command_rotate_key,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
"bundle": command_bundle,
|
||||
@@ -2655,6 +2662,37 @@ def parse_args(argv):
|
||||
)
|
||||
parser_rename.add_argument("name", help="The new name for the device.", type=str)
|
||||
|
||||
parser_rotate_key = subparsers.add_parser(
|
||||
"rotate-key",
|
||||
help=(
|
||||
"Change the OTA encryption key over the air: write a new key to the "
|
||||
"yaml, build with it, upload with the current key, confirm."
|
||||
),
|
||||
parents=[mqtt_options],
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"configuration", help="Your YAML configuration file.", nargs=1
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"--device",
|
||||
action="append",
|
||||
help=ARGUMENT_HELP_DEVICE,
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"--prompt-new-key",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Ask for the new key instead of generating one; read without echo, "
|
||||
"or as one line from stdin when that is not a terminal."
|
||||
),
|
||||
)
|
||||
parser_rotate_key.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
action="store_true",
|
||||
help="Do not ask for confirmation when the api encryption key changes too.",
|
||||
)
|
||||
|
||||
parser_analyze_memory = subparsers.add_parser(
|
||||
"analyze-memory",
|
||||
help="Analyze memory usage by component.",
|
||||
|
||||
+1
-16
@@ -906,21 +906,6 @@ async def while_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
|
||||
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
|
||||
names = {name for _, name in args}
|
||||
renamed: TemplateArgsType = []
|
||||
for type_, name in args:
|
||||
if name == "iteration":
|
||||
depth = 1
|
||||
while f"iteration_{depth}" in names:
|
||||
depth += 1
|
||||
name = f"iteration_{depth}"
|
||||
names.add(name)
|
||||
renamed.append((type_, name))
|
||||
return renamed
|
||||
|
||||
|
||||
@register_action(
|
||||
"repeat",
|
||||
RepeatAction,
|
||||
@@ -944,7 +929,7 @@ async def repeat_action_to_code(
|
||||
actions = await build_action_list(
|
||||
config[CONF_THEN],
|
||||
cg.TemplateArguments(cg.uint32, *template_arg.args),
|
||||
[(cg.uint32, "iteration"), *_shadow_iteration(args)],
|
||||
[(cg.uint32, "iteration"), *args],
|
||||
)
|
||||
for action in actions:
|
||||
cg.add(var.add_then_action(action))
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
"""``esphome rotate-key``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from esphome import espota2
|
||||
from esphome.components.noise import (
|
||||
generate_encryption_key,
|
||||
static_encryption_key,
|
||||
validate_encryption_key,
|
||||
)
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.const import CONF_API, CONF_PASSWORD, CONF_PORT
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.espota2 import ProbeResult
|
||||
from esphome.log import AnsiFore, color
|
||||
from esphome.ota_key_edit import locate_key_edits, old_key_edit, with_sharers
|
||||
from esphome.types import ConfigType
|
||||
from esphome.upload_targets import PortType, get_port_type
|
||||
from esphome.util import (
|
||||
ESPHOME_COMMAND,
|
||||
read_secret_line,
|
||||
run_external_process,
|
||||
safe_input,
|
||||
safe_print,
|
||||
)
|
||||
from esphome.yaml_edit import RestoreError, Snapshot, apply_line_edits, restore_files
|
||||
|
||||
PRECHECK_TIMEOUT = 15.0
|
||||
# What to do when the device does not accept the key in the yaml before anything is written
|
||||
PRECHECK_ADVICE = {
|
||||
ProbeResult.UNREACHABLE: (
|
||||
"The device could not be reached; check that it is online, then try again."
|
||||
),
|
||||
ProbeResult.REJECTED: (
|
||||
"The device runs a different key than the yaml. Install the current "
|
||||
"configuration first, presenting its key with --prompt-ota-key, then rotate."
|
||||
),
|
||||
ProbeResult.NOT_OFFERED: (
|
||||
"The device does not offer encryption: it runs a firmware older than "
|
||||
"ESPHome 2026.9.0 or without a key. Install the current configuration "
|
||||
"first, then rotate."
|
||||
),
|
||||
ProbeResult.DEVICE_ERROR: (
|
||||
"The device answered with an error (see the warning above); fix that first."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def command_rotate_key(args: argparse.Namespace, config: ConfigType) -> int | None:
|
||||
"""Write a new key, build with it, upload with the current one, confirm."""
|
||||
from esphome.__main__ import (
|
||||
Purpose,
|
||||
_esphome_ota_conf,
|
||||
_resolve_network_devices,
|
||||
choose_upload_log_host,
|
||||
)
|
||||
|
||||
def fail(message: str) -> int:
|
||||
safe_print(color(AnsiFore.BOLD_RED, message))
|
||||
return 1
|
||||
|
||||
ota_conf = _esphome_ota_conf(config)
|
||||
if ota_conf is None:
|
||||
return fail("rotate-key needs the esphome OTA platform in the configuration")
|
||||
old_key = static_encryption_key(ota_conf)
|
||||
if old_key is None:
|
||||
return fail(
|
||||
"rotate-key needs 'encryption:' under the esphome OTA platform with a "
|
||||
"key in the yaml; a key provisioned at runtime is rotated from Home "
|
||||
"Assistant"
|
||||
)
|
||||
if CORE.is_host:
|
||||
return fail("rotate-key is for devices updated over the air")
|
||||
old_key = str(old_key)
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device, check_default=None, purpose=Purpose.UPLOADING
|
||||
)
|
||||
if get_port_type(devices[0]) in (PortType.SERIAL, PortType.BOOTSEL):
|
||||
return fail(
|
||||
"rotate-key works over the air; on a serial connection change the "
|
||||
"key in the yaml and run 'esphome run' instead"
|
||||
)
|
||||
network_devices = _resolve_network_devices(devices, config, args)
|
||||
|
||||
if args.prompt_new_key:
|
||||
new_key = read_secret_line("New OTA encryption key: ")
|
||||
else:
|
||||
new_key = generate_encryption_key()
|
||||
try:
|
||||
validate_encryption_key(new_key)
|
||||
except Invalid as err:
|
||||
return fail(f"Invalid new key: {err}")
|
||||
if new_key == old_key:
|
||||
return fail("The new key is the same as the current one")
|
||||
|
||||
try:
|
||||
edits = locate_key_edits(old_key, new_key)
|
||||
# The previous key stays in the config as old_key so an install still
|
||||
# reaches the device if it ends up running either key
|
||||
edits += old_key_edit(old_key)
|
||||
unchecked = with_sharers(edits)
|
||||
except EsphomeError as err:
|
||||
return fail(str(err))
|
||||
|
||||
warnings = []
|
||||
# With the recommended shape the ota inherits the api key, so rotating
|
||||
# it changes what Home Assistant connects with
|
||||
if str(static_encryption_key(config.get(CONF_API) or {})) == old_key:
|
||||
warnings.append(
|
||||
"The api encryption key changes too; Home Assistant will ask for "
|
||||
"the new key after the install."
|
||||
)
|
||||
shared: dict[str, set[str]] = {}
|
||||
for edit in edits:
|
||||
what = f"secret '{edit.secret}'" if edit.secret else edit.path.name
|
||||
for path in edit.shared_with:
|
||||
shared.setdefault(
|
||||
str(path.relative_to(CORE.config_dir.resolve())), set()
|
||||
).add(what)
|
||||
if unchecked:
|
||||
warnings.append(
|
||||
"Could not check every configuration for shared use: "
|
||||
+ "; ".join(unchecked)
|
||||
)
|
||||
if shared:
|
||||
warnings.append(
|
||||
"Other configurations share what is rewritten: "
|
||||
+ "; ".join(
|
||||
f"{p} ({', '.join(sorted(w))})" for p, w in sorted(shared.items())
|
||||
)
|
||||
+ ". Their devices keep the previous key until their next install; "
|
||||
"make sure those configurations carry it as old_key."
|
||||
)
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
safe_print(color(AnsiFore.BOLD_YELLOW, warning))
|
||||
if not args.yes:
|
||||
if not sys.stdin.isatty():
|
||||
return fail("Confirm with --yes when there is no terminal")
|
||||
if safe_input("Continue? [y/N] ").strip().lower() not in ("y", "yes"):
|
||||
return 1
|
||||
|
||||
safe_print("Checking that the device accepts the current key...")
|
||||
found = espota2.probe_ota_key(
|
||||
network_devices,
|
||||
remote_port,
|
||||
old_key,
|
||||
timeout=PRECHECK_TIMEOUT,
|
||||
retry_rejected=False,
|
||||
)
|
||||
if found is not ProbeResult.ACCEPTED:
|
||||
return fail(PRECHECK_ADVICE[found])
|
||||
|
||||
# Both keys are shown only when a file could not be put back
|
||||
keys = f"Previous key: {old_key}\nNew key: {new_key}"
|
||||
try:
|
||||
originals = apply_line_edits(edits)
|
||||
except RestoreError as err:
|
||||
safe_print(keys)
|
||||
return fail(str(err))
|
||||
except EsphomeError as err:
|
||||
return fail(str(err))
|
||||
except KeyboardInterrupt as err:
|
||||
if notes := getattr(err, "__notes__", None): # a rollback that failed too
|
||||
safe_print(keys)
|
||||
return fail("Interrupted; " + "; ".join(notes))
|
||||
return fail("Interrupted before the upload")
|
||||
# From here every exit restores or reports, so nothing sits outside the try
|
||||
uploaded = False
|
||||
try:
|
||||
safe_print(
|
||||
"Wrote the new key to "
|
||||
+ ", ".join(color(AnsiFore.CYAN, str(p)) for p in originals)
|
||||
)
|
||||
# Global options go before the subcommand; the compile parser is strict
|
||||
cli_args = ["--dashboard"] if CORE.dashboard else []
|
||||
if args.toolchain:
|
||||
cli_args += ["--toolchain", str(args.toolchain)]
|
||||
for key, value in args.substitution or []:
|
||||
cli_args += ["-s", key, value]
|
||||
cli_args += ["compile", str(CORE.config_path)]
|
||||
if run_external_process(*ESPHOME_COMMAND, *cli_args) != 0:
|
||||
return fail("Compiling with the new key failed")
|
||||
|
||||
def image_sent() -> None:
|
||||
# The device accepted the session and may commit from here on;
|
||||
# the edited config reaches it on either key
|
||||
nonlocal uploaded
|
||||
uploaded = True
|
||||
|
||||
rc, _ = espota2.run_ota(
|
||||
network_devices,
|
||||
remote_port,
|
||||
ota_conf.get(CONF_PASSWORD),
|
||||
CORE.firmware_bin,
|
||||
noise_psk=old_key,
|
||||
plaintext_fallback=False,
|
||||
on_upload_start=image_sent,
|
||||
)
|
||||
if rc != 0:
|
||||
return fail(
|
||||
"Uploading with the current key failed. The configuration keeps "
|
||||
"the new key with the previous one as old_key, so the next "
|
||||
"install reaches the device whichever key it runs."
|
||||
if uploaded
|
||||
else "The upload did not reach the device."
|
||||
)
|
||||
safe_print("Waiting for the device to come back with the new key...")
|
||||
found = espota2.probe_ota_key(network_devices, remote_port, new_key)
|
||||
if found is not ProbeResult.ACCEPTED:
|
||||
return fail(
|
||||
(
|
||||
"The device still answers with the previous key."
|
||||
if found is ProbeResult.REJECTED
|
||||
else "The device did not come back to confirm the new key."
|
||||
)
|
||||
+ " The configuration keeps the new key, with the previous one as "
|
||||
"old_key, so the next install reaches the device either way."
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
return fail(
|
||||
"Interrupted; the device's key was not confirmed"
|
||||
if uploaded
|
||||
else "Interrupted before the upload"
|
||||
)
|
||||
finally:
|
||||
# Before the upload nothing changed on the device; after it the device
|
||||
# most likely runs the new key, and old_key covers the other case
|
||||
if uploaded:
|
||||
safe_print(f"New OTA encryption key: {color(AnsiFore.CYAN, new_key)}")
|
||||
else:
|
||||
_restore(originals, keys)
|
||||
|
||||
safe_print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
|
||||
return 0
|
||||
|
||||
|
||||
def _restore(originals: dict[Path, Snapshot], keys: str) -> None:
|
||||
"""Put the previous key back, and drop the images a build made with the
|
||||
new key; both keys are shown when a step fails."""
|
||||
files = ", ".join(str(p) for p in originals)
|
||||
try:
|
||||
restore_files(originals)
|
||||
except (EsphomeError, KeyboardInterrupt) as err:
|
||||
# An interrupted restore carries its report as a note
|
||||
why = str(err) or "; ".join(getattr(err, "__notes__", ()))
|
||||
safe_print(color(AnsiFore.BOLD_RED, why or f"Interrupted restoring {files}"))
|
||||
safe_print(keys)
|
||||
else:
|
||||
safe_print(color(AnsiFore.BOLD_YELLOW, f"Restored the previous key in {files}"))
|
||||
build_dir = CORE.firmware_bin.parent
|
||||
failed: list[str] = []
|
||||
images: list[Path] = []
|
||||
try:
|
||||
images = [p for s in (".bin", ".uf2") for p in build_dir.glob(f"*{s}")]
|
||||
for image in images: # every one is tried, whatever the others did
|
||||
try:
|
||||
image.unlink()
|
||||
except OSError as err:
|
||||
failed.append(f"{image}: {err}")
|
||||
except (OSError, KeyboardInterrupt) as err:
|
||||
failed.append(f"{build_dir}: {str(err) or 'interrupted'}")
|
||||
if failed:
|
||||
safe_print(
|
||||
color(
|
||||
AnsiFore.BOLD_RED,
|
||||
"Could not remove images that may hold the new key: "
|
||||
+ "; ".join(failed),
|
||||
)
|
||||
)
|
||||
safe_print(keys)
|
||||
elif images:
|
||||
safe_print(
|
||||
color(
|
||||
AnsiFore.BOLD_YELLOW,
|
||||
f"Removed the images in {build_dir}, which may hold the new key; "
|
||||
"compile again before the next upload.",
|
||||
)
|
||||
)
|
||||
@@ -33,9 +33,35 @@ _CACHE_VERSION = 1
|
||||
_LAMBDA_KEY = "__esphome_lambda__"
|
||||
|
||||
|
||||
_CACHE_SUFFIX = ".validated.json"
|
||||
|
||||
|
||||
def compiled_config_path(config_filename: str) -> Path:
|
||||
"""Path to the cached validated config alongside the storage sidecar."""
|
||||
return CORE.data_dir / "storage" / f"{config_filename}.validated.json"
|
||||
return CORE.data_dir / "storage" / f"{config_filename}{_CACHE_SUFFIX}"
|
||||
|
||||
|
||||
def invalidate_compiled_config() -> None:
|
||||
"""Drop every cache in the storage dir after a change to a file the
|
||||
mtime check cannot see, such as secrets.yaml or an include; other
|
||||
configurations may share that file."""
|
||||
storage = compiled_config_path(CORE.config_filename).parent
|
||||
try:
|
||||
caches = [p for p in storage.iterdir() if p.name.endswith(_CACHE_SUFFIX)]
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError as err:
|
||||
raise EsphomeError(
|
||||
f"Could not list the validated config caches in {storage}: {err}"
|
||||
) from err
|
||||
for path in caches:
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
# A stale cache would present the old key on the next upload
|
||||
raise EsphomeError(
|
||||
f"Could not remove the validated config cache {path}: {err}"
|
||||
) from err
|
||||
|
||||
|
||||
def save_compiled_config(config: ConfigType) -> None:
|
||||
|
||||
@@ -251,6 +251,9 @@ def _validate_supports_response(value: Any) -> str:
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
|
||||
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
|
||||
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
|
||||
|
||||
VARIABLE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
@@ -481,6 +484,29 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
|
||||
return strings
|
||||
|
||||
|
||||
def _action_strings_size(strings: list[str | None]) -> int:
|
||||
"""Bytes needed to copy every string out of flash, each with its terminator."""
|
||||
return sum(
|
||||
len(string.encode("utf-8")) + 1 for string in strings if string is not None
|
||||
)
|
||||
|
||||
|
||||
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
|
||||
if not CORE.is_esp8266:
|
||||
return config
|
||||
actions = config.get(CONF_ACTIONS, [])
|
||||
has_metadata = _has_action_metadata(actions)
|
||||
for conf in actions:
|
||||
size = _action_strings_size(_action_strings(conf, has_metadata))
|
||||
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
|
||||
raise cv.Invalid(
|
||||
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
|
||||
f"description and example text; ESP8266 allows at most "
|
||||
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
|
||||
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
|
||||
if (
|
||||
@@ -505,6 +531,7 @@ def _validate_wizard(config: ConfigType) -> ConfigType:
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
_validate_esp8266_action_strings,
|
||||
_validate_outgoing_host_ipv6,
|
||||
_validate_wizard,
|
||||
)
|
||||
@@ -560,6 +587,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_HOMEASSISTANT_STATES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
|
||||
scratch_size = 0
|
||||
if actions:
|
||||
# Metadata is compiled in for every action once any action declares it, because the
|
||||
# string table layout is fixed by the define rather than per action
|
||||
@@ -611,6 +639,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
func_args.append((native, name))
|
||||
strings = _action_strings(conf, has_metadata)
|
||||
table = _add_action_strings(index, strings)
|
||||
if CORE.is_esp8266:
|
||||
scratch_size = max(scratch_size, _action_strings_size(strings))
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
templ = cg.TemplateArguments(supports_response, *service_template_args)
|
||||
# Key is hashed here because the name is not readable at runtime on ESP8266
|
||||
@@ -637,6 +667,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(auto.add_action(unregister_action))
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
if CORE.is_esp8266 and has_user_actions:
|
||||
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
|
||||
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
|
||||
|
||||
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
|
||||
await wizard.to_code(wizard_config)
|
||||
|
||||
@@ -403,9 +403,6 @@ message WizardCapabilities {
|
||||
message SendspinCapabilities {
|
||||
// Bitmask of the Sendspin features this device supports
|
||||
uint32 feature_flags = 1;
|
||||
// The Sendspin client_id: base64url of the client's static public key, 43 characters.
|
||||
// Empty until the Sendspin client has started once since boot.
|
||||
string client_id = 2 [(max_data_length) = 43];
|
||||
}
|
||||
|
||||
message DeviceCapabilitiesResponse {
|
||||
@@ -1029,8 +1026,8 @@ message SubscribeHomeassistantServicesRequest {
|
||||
}
|
||||
|
||||
message HomeassistantServiceMap {
|
||||
string key = 1 [(progmem) = true];
|
||||
string value = 2 [(progmem) = true];
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message HomeassistantActionRequest {
|
||||
@@ -1039,7 +1036,7 @@ message HomeassistantActionRequest {
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
|
||||
string service = 1 [(progmem) = true];
|
||||
string service = 1;
|
||||
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
|
||||
@@ -1160,21 +1157,21 @@ enum SupportsResponseType {
|
||||
}
|
||||
message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
string name = 1 [(progmem) = true];
|
||||
string name = 1;
|
||||
ServiceArgType type = 2;
|
||||
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
}
|
||||
message ListEntitiesServicesResponse {
|
||||
option (id) = 41;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
string name = 1 [(progmem) = true];
|
||||
string name = 1;
|
||||
fixed32 key = 2 [(force) = true];
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
SupportsResponseType supports_response = 4;
|
||||
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
@@ -3046,15 +3043,12 @@ enum SerialProxyIdentityFlag {
|
||||
// IDs below are empty
|
||||
}
|
||||
|
||||
// The descriptor fields of a USB device as seen by the host stack, and the port's index
|
||||
// among those a vendor driver exposes on the interface
|
||||
// The descriptor fields of a USB device as seen by the host stack
|
||||
message UsbDeviceDescriptor {
|
||||
uint32 vendor_id = 1;
|
||||
uint32 product_id = 2;
|
||||
uint32 bcd_device = 3;
|
||||
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
|
||||
uint32 interface_class = 5; // bInterfaceClass of that interface
|
||||
uint32 port_number = 6;
|
||||
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to
|
||||
}
|
||||
|
||||
// The identity of the device behind a port. Identifies one physical device among others of
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#include "api_connection.h"
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -454,15 +451,6 @@ bool APIConnection::send_disconnect_response_() {
|
||||
DisconnectResponse resp;
|
||||
return this->send_message(resp);
|
||||
}
|
||||
void APIConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
logger::log_crash_to_client();
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::on_disconnect_response() {
|
||||
// Don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
@@ -2131,7 +2119,6 @@ bool APIConnection::send_device_capabilities_response_() {
|
||||
#endif
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
|
||||
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
|
||||
#endif
|
||||
return this->send_message(resp);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
#include "subscribe_state.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
#include "esphome/components/esp32/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
#include "esphome/components/rp2/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
#include "esphome/components/esp8266/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
@@ -289,7 +298,21 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg);
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
esp32::crash_handler_log();
|
||||
esp32::crash_handler_clear();
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
rp2::crash_handler_log();
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
esp8266::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
|
||||
#endif
|
||||
|
||||
@@ -123,7 +123,7 @@ extend google.protobuf.FieldOptions {
|
||||
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
|
||||
optional bool track_presence = 50020 [default=false];
|
||||
|
||||
// progmem: The string field, or pointer_to_buffer bytes field, points into flash (PROGMEM).
|
||||
// progmem: The pointer_to_buffer bytes field points into flash (PROGMEM).
|
||||
// The generated encode and dump copy it with progmem_memcpy, as ESP8266
|
||||
// cannot read flash with ordinary loads.
|
||||
optional bool progmem = 50021 [default=false];
|
||||
|
||||
@@ -310,14 +310,12 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
|
||||
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.client_id);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.feature_flags);
|
||||
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
@@ -1142,8 +1140,8 @@ uint32_t NoiseEncryptionSetKeyResponse::calc_size_msg(const void *self) {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
uint8_t *HomeassistantServiceMap::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const HomeassistantServiceMap *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
|
||||
return pos;
|
||||
}
|
||||
uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
|
||||
@@ -1155,7 +1153,7 @@ uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
|
||||
}
|
||||
uint8_t *HomeassistantActionRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const HomeassistantActionRequest *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
|
||||
for (auto &it : msg.data) {
|
||||
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 2, it);
|
||||
}
|
||||
@@ -1326,13 +1324,13 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
|
||||
uint8_t *ListEntitiesServicesArgument::encode_msg(const void *self,
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const ListEntitiesServicesArgument *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(msg.type));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -1352,14 +1350,14 @@ uint32_t ListEntitiesServicesArgument::calc_size_msg(const void *self) {
|
||||
uint8_t *ListEntitiesServicesResponse::encode_msg(const void *self,
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const ListEntitiesServicesResponse *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, msg.key);
|
||||
for (auto &it : msg.args) {
|
||||
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 3, it);
|
||||
}
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.supports_response));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -4177,8 +4175,6 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
|
||||
return pos;
|
||||
}
|
||||
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
|
||||
@@ -4188,8 +4184,6 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.product_id);
|
||||
size += ProtoSize::calc_uint32(1, msg.bcd_device);
|
||||
size += ProtoSize::calc_uint32(1, msg.interface_number);
|
||||
size += ProtoSize::calc_uint32(1, msg.interface_class);
|
||||
size += ProtoSize::calc_uint32(1, msg.port_number);
|
||||
return size;
|
||||
}
|
||||
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
|
||||
@@ -750,7 +750,6 @@ class WizardCapabilities final : public ProtoMessage {
|
||||
class SendspinCapabilities final : public ProtoMessage {
|
||||
public:
|
||||
uint32_t feature_flags{0};
|
||||
StringRef client_id{nullptr, 0}; // null until set, encode only
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -1389,8 +1388,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
class HomeassistantServiceMap final : public ProtoMessage {
|
||||
public:
|
||||
ProgmemStringRef key{};
|
||||
ProgmemStringRef value{};
|
||||
StringRef key{nullptr, 0}; // null until set, encode only
|
||||
StringRef value{nullptr, 0}; // null until set, encode only
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -1410,7 +1409,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); }
|
||||
#endif
|
||||
ProgmemStringRef service{};
|
||||
StringRef service{nullptr, 0}; // null until set, encode only
|
||||
FixedVector<HomeassistantServiceMap> data{};
|
||||
FixedVector<HomeassistantServiceMap> data_template{};
|
||||
FixedVector<HomeassistantServiceMap> variables{};
|
||||
@@ -1577,13 +1576,13 @@ class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
public:
|
||||
ProgmemStringRef name{};
|
||||
StringRef name{nullptr, 0}; // null until set, encode only
|
||||
enums::ServiceArgType type{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProgmemStringRef description{};
|
||||
StringRef description{nullptr, 0}; // null until set, encode only
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProgmemStringRef example{};
|
||||
StringRef example{nullptr, 0}; // null until set, encode only
|
||||
#endif
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -1604,12 +1603,12 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
|
||||
#endif
|
||||
ProgmemStringRef name{};
|
||||
StringRef name{nullptr, 0}; // null until set, encode only
|
||||
uint32_t key{0};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
enums::SupportsResponseType supports_response{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProgmemStringRef description{};
|
||||
StringRef description{nullptr, 0}; // null until set, encode only
|
||||
#endif
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -4084,8 +4083,6 @@ class UsbDeviceDescriptor final : public ProtoMessage {
|
||||
uint32_t product_id{0};
|
||||
uint32_t bcd_device{0};
|
||||
uint32_t interface_number{0};
|
||||
uint32_t interface_class{0};
|
||||
uint32_t port_number{0};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -12,9 +12,8 @@ namespace esphome::api {
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site
|
||||
void DumpBuffer::append_p_esp8266(const char *str) { this->append_p_esp8266(str, strlen_P(str)); }
|
||||
|
||||
void DumpBuffer::append_p_esp8266(const char *str, size_t len) {
|
||||
void DumpBuffer::append_p_esp8266(const char *str) {
|
||||
size_t len = strlen_P(str);
|
||||
size_t space = CAPACITY - 1 - pos_;
|
||||
if (len > space)
|
||||
len = space;
|
||||
@@ -135,13 +134,6 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
|
||||
progmem_memcpy(data_buf, data, len);
|
||||
dump_bytes_field(out, field_name, data_buf, len, indent);
|
||||
}
|
||||
|
||||
// Helper for string fields in flash
|
||||
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
|
||||
int indent = 2) {
|
||||
append_field_prefix(out, field_name, indent);
|
||||
out.append("'").append_p(value.progmem_ptr(), value.size()).append("'\n");
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
|
||||
@@ -1127,7 +1119,6 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
|
||||
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
|
||||
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
|
||||
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
@@ -1520,13 +1511,13 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("value"), this->value);
|
||||
dump_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
dump_field(out, ESPHOME_PSTR("value"), this->value);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("service"), this->service);
|
||||
dump_field(out, ESPHOME_PSTR("service"), this->service);
|
||||
for (const auto &it : this->data) {
|
||||
out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": ");
|
||||
it.dump_to(out);
|
||||
@@ -1621,19 +1612,19 @@ const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
dump_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("example"), this->example);
|
||||
dump_field(out, ESPHOME_PSTR("example"), this->example);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
for (const auto &it : this->args) {
|
||||
out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": ");
|
||||
@@ -1642,7 +1633,7 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
dump_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -2951,8 +2942,6 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
|
||||
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
|
||||
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
|
||||
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
|
||||
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -547,7 +547,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
||||
// arrive before the subscription and are lost - warn instead of failing silently.
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
|
||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
|
||||
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.progmem_ptr(),
|
||||
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
|
||||
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
||||
: LOG_STR_LITERAL("no client connected"));
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -267,12 +267,12 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.key = ProgmemStringRef(StringRef(it.first));
|
||||
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -289,7 +289,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &event_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(event_name));
|
||||
resp.service = StringRef(event_name);
|
||||
resp.is_event = true;
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -309,13 +309,13 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.is_event = true;
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.key = ProgmemStringRef(StringRef(it.first));
|
||||
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -116,18 +116,42 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
|
||||
const Field *fields = this->fields_;
|
||||
const size_t total = 1 + this->data_count_ + this->data_template_count_ + this->variables_count_;
|
||||
|
||||
// Lambda results must live until the send; the constant strings are encoded from flash where they are
|
||||
// Lambda results, and on ESP8266 the RAM copies of the flash strings, must live until the send
|
||||
size_t lambda_count = 0;
|
||||
for (size_t i = 0; i < total; i++)
|
||||
#ifdef USE_ESP8266
|
||||
size_t flash_len = 0;
|
||||
#endif
|
||||
for (size_t i = 0; i < total; i++) {
|
||||
lambda_count += fields[i].fn != nullptr;
|
||||
#ifdef USE_ESP8266
|
||||
if (fields[i].fn == nullptr)
|
||||
flash_len += ESPHOME_strlen_P(fields[i].value);
|
||||
if (fields[i].key != nullptr)
|
||||
flash_len += ESPHOME_strlen_P(fields[i].key);
|
||||
#endif
|
||||
}
|
||||
FixedVector<std::string> results;
|
||||
results.init(lambda_count);
|
||||
auto string_ref = [](const char *str) { return ProgmemStringRef(str, ESPHOME_strlen_P(str)); };
|
||||
#ifdef USE_ESP8266
|
||||
SmallBufferWithHeapFallback<128, char> flash_copy(flash_len);
|
||||
char *cursor = flash_copy.get();
|
||||
#endif
|
||||
auto string_ref = [&](const char *str) {
|
||||
#ifdef USE_ESP8266
|
||||
size_t len = ESPHOME_strlen_P(str);
|
||||
memcpy_P(cursor, str, len);
|
||||
StringRef ref(cursor, len);
|
||||
cursor += len;
|
||||
return ref;
|
||||
#else
|
||||
return StringRef(str);
|
||||
#endif
|
||||
};
|
||||
auto value_ref = [&](const Field &field) {
|
||||
if (field.fn == nullptr)
|
||||
return string_ref(field.value);
|
||||
results.push_back(field.fn(x...));
|
||||
return ProgmemStringRef(StringRef(results.back()));
|
||||
return StringRef(results.back());
|
||||
};
|
||||
auto fill = [&](FixedVector<HomeassistantServiceMap> &dest, uint8_t count) {
|
||||
dest.init(count);
|
||||
|
||||
@@ -99,7 +99,8 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
|
||||
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
|
||||
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
UserActionScratch scratch;
|
||||
auto resp = service->encode_list_service_response(scratch);
|
||||
if (!this->client_->send_message(resp))
|
||||
return false;
|
||||
// at_ is this service's index
|
||||
|
||||
@@ -418,18 +418,6 @@ class ProtoEncode {
|
||||
std::memcpy(pos + 2, ref.c_str(), ref.size());
|
||||
return pos + 2 + ref.size();
|
||||
}
|
||||
/// encode_short_string_force for a string that may be in flash (PROGMEM).
|
||||
[[nodiscard]] static inline uint8_t *encode_short_progmem_string_force(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, const ProgmemStringRef &ref) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(ref.size() < 128 && "encode_short_progmem_string_force: string exceeds max_data_length < 128");
|
||||
#endif
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
|
||||
pos[0] = tag;
|
||||
pos[1] = static_cast<uint8_t>(ref.size());
|
||||
progmem_memcpy(pos + 2, ref.progmem_ptr(), ref.size());
|
||||
return pos + 2 + ref.size();
|
||||
}
|
||||
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
|
||||
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
|
||||
@@ -493,17 +481,6 @@ class ProtoEncode {
|
||||
progmem_memcpy(pos, data, len);
|
||||
return pos + len;
|
||||
}
|
||||
/// encode_progmem_bytes for a string field that may be in flash.
|
||||
[[nodiscard]] static inline uint8_t *encode_progmem_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const ProgmemStringRef &ref) {
|
||||
return encode_progmem_bytes(pos PROTO_ENCODE_DEBUG_ARG, field_id,
|
||||
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_progmem_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const ProgmemStringRef &ref) {
|
||||
return encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, field_id,
|
||||
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
|
||||
}
|
||||
[[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);
|
||||
@@ -703,20 +680,9 @@ class DumpBuffer {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Append len bytes of a PROGMEM string
|
||||
DumpBuffer &append_p(const char *str, size_t len) {
|
||||
#ifdef USE_ESP8266
|
||||
append_p_esp8266(str, len);
|
||||
#else
|
||||
append_impl_(str, len);
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
|
||||
void append_p_esp8266(const char *str);
|
||||
void append_p_esp8266(const char *str, size_t len);
|
||||
#endif
|
||||
|
||||
const char *c_str() const { return buf_; }
|
||||
|
||||
@@ -1,36 +1,47 @@
|
||||
#include "user_services.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
ProgmemStringRef UserServiceStatic::str_(size_t idx) const {
|
||||
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
|
||||
const char *s = progmem_read_ptr(&this->strings_[idx]);
|
||||
if (s == nullptr)
|
||||
return {};
|
||||
return ProgmemStringRef(s, ESPHOME_strlen_P(s));
|
||||
#ifdef USE_ESP8266
|
||||
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
|
||||
if (scratch.empty())
|
||||
return {};
|
||||
size_t len = strnlen_P(s, scratch.size() - 1);
|
||||
progmem_memcpy(scratch.data(), s, len);
|
||||
scratch[len] = '\0';
|
||||
StringRef ref(scratch.data(), len);
|
||||
scratch = scratch.subspan(len + 1);
|
||||
return ref;
|
||||
#else
|
||||
return StringRef(s);
|
||||
#endif
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
|
||||
std::span<const enums::ServiceArgType> arg_types) const {
|
||||
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = this->str_(0);
|
||||
msg.name = this->str_(0, scratch);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
msg.description = this->str_(1);
|
||||
msg.description = this->str_(1, scratch);
|
||||
#endif
|
||||
msg.args.init(arg_types.size());
|
||||
for (size_t i = 0; i < arg_types.size(); i++) {
|
||||
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.name = this->str_(base);
|
||||
arg.name = this->str_(base, scratch);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
arg.description = this->str_(base + 1);
|
||||
arg.example = this->str_(base + 2);
|
||||
arg.description = this->str_(base + 1, scratch);
|
||||
arg.example = this->str_(base + 2, scratch);
|
||||
#endif
|
||||
}
|
||||
return msg;
|
||||
|
||||
@@ -20,8 +20,9 @@ class APIServer;
|
||||
|
||||
class UserServiceDescriptor {
|
||||
public:
|
||||
/// Build the list-entities message; its strings may point into flash (PROGMEM).
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
||||
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
|
||||
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
|
||||
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -36,6 +37,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
||||
|
||||
template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||
|
||||
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
|
||||
#ifdef USE_ESP8266
|
||||
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
|
||||
#else
|
||||
using UserActionScratch = std::array<char, 0>;
|
||||
#endif
|
||||
|
||||
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
|
||||
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
|
||||
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
|
||||
@@ -55,9 +63,12 @@ class UserServiceStatic : public UserServiceDescriptor {
|
||||
: strings_(strings), key_(key), supports_response_(supports_response) {}
|
||||
|
||||
protected:
|
||||
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types) const;
|
||||
/// Table entry `idx` as it sits in flash; nullptr gives an empty string.
|
||||
ProgmemStringRef str_(size_t idx) const;
|
||||
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
|
||||
std::span<char> scratch) const;
|
||||
/// Reference table entry `idx`; nullptr gives an empty StringRef.
|
||||
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
|
||||
/// is advanced past the copy.
|
||||
StringRef str_(size_t idx, std::span<char> &scratch) const;
|
||||
|
||||
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
|
||||
uint32_t key_;
|
||||
@@ -68,9 +79,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
|
||||
public:
|
||||
using UserServiceStatic::UserServiceStatic;
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
return this->encode_list_service_response_(arg_types);
|
||||
return this->encode_list_service_response_(arg_types, scratch);
|
||||
}
|
||||
|
||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||
@@ -114,9 +125,9 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
this->key_ = fnv1_hash(this->name_.c_str());
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = ProgmemStringRef(StringRef(this->name_));
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
@@ -124,7 +135,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.name = ProgmemStringRef(StringRef(this->arg_names_[i]));
|
||||
arg.name = StringRef(this->arg_names_[i]);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bedjet {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bedjet");
|
||||
static const char *const TAG = "bedjet";
|
||||
|
||||
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
|
||||
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
|
||||
const char *const Automation::TAG = "ble_client.automation";
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ void BluedroidGattClient::set_disconnecting_() {
|
||||
|
||||
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout, const char *param_type) {
|
||||
esp_ble_conn_update_params_t conn_params = {};
|
||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||
conn_params.min_int = min_interval;
|
||||
conn_params.max_int = max_interval;
|
||||
|
||||
@@ -100,8 +100,7 @@ void BM8563::get_date_(ESPTime &time) {
|
||||
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
|
||||
|
||||
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
|
||||
// The chip counts weekdays 0-6 from Sunday (not BCD), ESPTime counts 1-7
|
||||
time.day_of_week = (buf[1] & 0x07) + 1;
|
||||
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
|
||||
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
|
||||
|
||||
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
|
||||
@@ -116,7 +115,7 @@ void BM8563::get_date_(ESPTime &time) {
|
||||
void BM8563::set_date_(const ESPTime &time) {
|
||||
uint8_t buf[4] = {
|
||||
this->byte_to_bcd2_(time.day_of_month),
|
||||
static_cast<uint8_t>(time.day_of_week - 1),
|
||||
this->byte_to_bcd2_(time.day_of_week),
|
||||
this->byte_to_bcd2_(time.month),
|
||||
this->byte_to_bcd2_(time.year % 100),
|
||||
};
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
#include "esphome/components/bme280_base/bme280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bme280_i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bme280_i2c.sensor");
|
||||
static const char *const TAG = "bme280_i2c.sensor";
|
||||
|
||||
class BME280I2CComponent final : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "bmi270_config.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::bmi270 {
|
||||
|
||||
@@ -12,9 +11,6 @@ using namespace bmm150; // NOLINT - sole consumer of the BMM150 aux-device defi
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmi270");
|
||||
|
||||
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±4g", "±8g", "±16g");
|
||||
PROGMEM_STRING_TABLE(GyroRangeStrings, "±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s");
|
||||
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
static constexpr size_t MAX_I2C_BUFFER_SIZE = 32;
|
||||
#else
|
||||
@@ -277,10 +273,11 @@ void BMI270Component::dump_config() {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Accel range : %s",
|
||||
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_, AccelRangeStrings::LAST_INDEX)));
|
||||
ESP_LOGCONFIG(TAG, " Gyro range : %s",
|
||||
LOG_STR_ARG(GyroRangeStrings::get_log_str(this->gyro_range_, GyroRangeStrings::LAST_INDEX)));
|
||||
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
|
||||
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
|
||||
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
|
||||
#ifdef USE_BMI270_BMM150
|
||||
if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) {
|
||||
ESP_LOGCONFIG(TAG, " Aux device : BMM150 magnetometer (%s)",
|
||||
@@ -318,8 +315,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
|
||||
}
|
||||
// Scale factor: LSB/g depends on range
|
||||
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
|
||||
// In PROGMEM; each read is a 32-bit load, which is safe from flash on ESP8266
|
||||
alignas(4) static constexpr float ACCEL_SCALE[] PROGMEM = {
|
||||
static constexpr float ACCEL_SCALE[] = {
|
||||
2.0f / 32768.0f,
|
||||
4.0f / 32768.0f,
|
||||
8.0f / 32768.0f,
|
||||
@@ -333,7 +329,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
|
||||
|
||||
// Gyroscope: registers 0x12–0x17 (6 bytes)
|
||||
// Scale: full-scale range / 2^15
|
||||
alignas(4) static constexpr float GYRO_SCALE[] PROGMEM = {
|
||||
static constexpr float GYRO_SCALE[] = {
|
||||
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
|
||||
};
|
||||
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
#include "esphome/components/bmp280_base/bmp280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp280_i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmp280_i2c.sensor");
|
||||
static const char *const TAG = "bmp280_i2c.sensor";
|
||||
|
||||
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
|
||||
class BMP280I2CComponent final : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
#include "esphome/components/bmp581_base/bmp581_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp581_i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmp581_i2c.sensor");
|
||||
static const char *const TAG = "bmp581_i2c.sensor";
|
||||
|
||||
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
|
||||
class BMP581I2CComponent final : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::chsc6x {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "chsc6x.touchscreen");
|
||||
static const char *const TAG = "chsc6x.touchscreen";
|
||||
|
||||
static const uint8_t CHSC6X_REG_STATUS = 0x00;
|
||||
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
|
||||
|
||||
@@ -8,9 +8,6 @@ namespace esphome::cse7766 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cse7766");
|
||||
|
||||
// The chip only sends the time since the last pulse once a cycle is longer than 1 s (in us)
|
||||
static constexpr uint32_t MIN_HELD_POWER_CYCLE = 1000000;
|
||||
|
||||
void CSE7766Component::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_transmission_ >= 500) {
|
||||
@@ -157,33 +154,18 @@ void CSE7766Component::parse_data_() {
|
||||
if (power_cycle_exceeds_range) {
|
||||
// Datasheet: power cycle exceeding range means active power is 0
|
||||
have_power = true;
|
||||
this->last_power_cycle_ = 0;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
} else if (have_power) {
|
||||
power = power_coeff / float(power_cycle);
|
||||
this->last_power_cycle_ = power_cycle;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
} else if (this->last_power_cycle_ > MIN_HELD_POWER_CYCLE && power_cycle <= 2 * this->last_power_cycle_) {
|
||||
// No new measurement in this frame. With cycles over 1 s the register holds the last
|
||||
// cycle or the time elapsed since the last pulse. While that time is within twice the
|
||||
// last cycle, the next pulse is not overdue, so the last measurement still applies.
|
||||
// Shorter cycles never get here in normal operation and keep the previous behaviour.
|
||||
have_power = true;
|
||||
power = power_coeff / float(this->last_power_cycle_);
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
} else {
|
||||
// The next pulse is overdue: the load has dropped, report 0 W. A load that drops to less
|
||||
// than half also reads 0 W here until its first new pulse arrives.
|
||||
this->last_power_cycle_ = 0;
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
} else if (this->power_sensor_ != nullptr) {
|
||||
// No valid power measurement from chip - publish 0W to avoid stale readings
|
||||
// This typically happens when current is below the measurable threshold (~50mA)
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
|
||||
float current = 0.0f;
|
||||
|
||||
@@ -36,7 +36,6 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
|
||||
|
||||
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
|
||||
uint8_t raw_data_index_{0};
|
||||
uint16_t cf_pulses_last_{0};
|
||||
uint32_t last_transmission_{0};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
@@ -46,8 +45,7 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
|
||||
sensor::Sensor *reactive_power_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_{nullptr};
|
||||
uint32_t cf_pulses_total_{0};
|
||||
// Power cycle of the last frame that carried a new power measurement, 0 if none
|
||||
uint32_t last_power_cycle_{0};
|
||||
uint16_t cf_pulses_last_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::cse7766
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst816 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cst816.touchscreen");
|
||||
static const char *const TAG = "cst816.touchscreen";
|
||||
|
||||
static const uint8_t REG_STATUS = 0x00;
|
||||
static const uint8_t REG_TOUCH_NUM = 0x02;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
|
||||
static const char *const TAG = "cst9220.touchscreen";
|
||||
|
||||
// The CST92xx family uses 16-bit (big-endian) register addresses.
|
||||
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
|
||||
|
||||
@@ -250,42 +250,6 @@ def add_metadata(
|
||||
)
|
||||
|
||||
|
||||
KEY_EXTERNAL_WRITERS = "external_writers"
|
||||
|
||||
|
||||
def _get_external_writer_list() -> list[tuple[ID, str]]:
|
||||
"""Get the raw external writer list. Each entry is (display id, owner)."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
|
||||
|
||||
|
||||
def register_external_writer(display_id: ID, owner: str) -> None:
|
||||
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
|
||||
|
||||
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
|
||||
resolves it in place before final validation.
|
||||
"""
|
||||
_get_external_writer_list().append((display_id, owner))
|
||||
|
||||
|
||||
def get_external_writers(display_id: ID) -> list[str]:
|
||||
"""Get the owners registered for a display. Only valid once IDs are resolved."""
|
||||
return [
|
||||
owner
|
||||
for id_, owner in _get_external_writer_list()
|
||||
if id_.id is not None and id_.id == display_id.id
|
||||
]
|
||||
|
||||
|
||||
def has_external_writer(display_id: ID) -> bool:
|
||||
"""Tell a driver's final validation that something else draws into the display.
|
||||
|
||||
LVGL anywhere in the config also counts, as the drivers assumed before.
|
||||
"""
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
|
||||
|
||||
|
||||
async def register_display(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await setup_display_core_(var, config)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -53,62 +52,37 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
|
||||
// -Os stops inlining it once there are two call sites, which costs a call and
|
||||
// spills per pixel. Without PSRAM there is one call site and it changes
|
||||
// nothing, and draw_pixel_at() stays a virtual call either way.
|
||||
template<ColorBitness BITNESS>
|
||||
static void __attribute__((flatten))
|
||||
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
// BITNESS is a constant here, so to_color()'s three per pixel divisions
|
||||
// become a multiply and shift and its bitness switch folds.
|
||||
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
|
||||
display->draw_pixel_at(
|
||||
x + x_start, y + y_start,
|
||||
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
|
||||
};
|
||||
// The source and frame buffers are row-major, so walking rows keeps the read
|
||||
// and the write sequential. When the display swaps the axes before writing,
|
||||
// walking columns keeps the write sequential instead; with the frame buffer
|
||||
// in PSRAM a strided write costs more than a strided read.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
size_t column_idx = y_offset * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, column_idx++) {
|
||||
size_t source_idx = column_idx;
|
||||
for (int y = 0; y != h; y++, source_idx += line_stride) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
switch (bitness) {
|
||||
case COLOR_BITNESS_565:
|
||||
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
default:
|
||||
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,11 +139,6 @@ enum DisplayRotation {
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
/// Whether a rotation swaps the x and y axes.
|
||||
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
|
||||
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
const int EDGES_TRIGON = 3;
|
||||
const int EDGES_TRIANGLE = 3;
|
||||
const int EDGES_TETRAGON = 4;
|
||||
@@ -722,12 +717,6 @@ class Display : public PollingComponent {
|
||||
|
||||
virtual DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
|
||||
/// neighbours along x land a row apart in the frame buffer. Not the same as
|
||||
/// get_width()/get_height() swapping: a display that rotates in hardware
|
||||
/// reports swapped dimensions but writes unswapped pixels.
|
||||
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::display {
|
||||
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
|
||||
@@ -11,27 +8,6 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
|
||||
|
||||
class ColorUtil {
|
||||
public:
|
||||
/// Reads one packed pixel of the given bitness from a source buffer into the
|
||||
/// value to_color() takes. BITNESS is a template argument so the byte layout
|
||||
/// folds; pass it on to to_color() as a constant too, and its channel scaling
|
||||
/// divides by a constant, which the compiler turns into a multiply and shift.
|
||||
template<ColorBitness BITNESS>
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
|
||||
if constexpr (BITNESS == COLOR_BITNESS_565) {
|
||||
const size_t idx = index * 2;
|
||||
if (big_endian)
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
}
|
||||
}
|
||||
|
||||
static Color to_color(uint32_t colorcode, ColorOrder color_order,
|
||||
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
|
||||
uint8_t first_color, second_color, third_color;
|
||||
@@ -178,54 +154,4 @@ class ColorUtil {
|
||||
return color;
|
||||
}
|
||||
};
|
||||
|
||||
/** Rec.601 luma weights (0.299/0.587/0.114), scaled by 256 */
|
||||
static constexpr uint16_t REC601_LUMA_R = 77;
|
||||
static constexpr uint16_t REC601_LUMA_G = 150;
|
||||
static constexpr uint16_t REC601_LUMA_B = 29;
|
||||
|
||||
/** Rec.601 luma of an RGB color, for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((REC601_LUMA_R * color.r + REC601_LUMA_G * color.g + REC601_LUMA_B * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map an RGB color to a single monochrome bit: 1 = white, 0 = black */
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** color_to_mono() of an RGB565 value as ColorUtil::to_color() reads it, from table lookups instead of a division
|
||||
* per channel.
|
||||
*/
|
||||
class Rgb565ToMono {
|
||||
public:
|
||||
explicit Rgb565ToMono(ColorOrder order) {
|
||||
// Rec.601 weight of the 5-bit, 6-bit and 5-bit fields, by the channel each one holds in this order
|
||||
uint16_t high = REC601_LUMA_R, middle = REC601_LUMA_G, low = REC601_LUMA_B;
|
||||
if (order == COLOR_ORDER_BGR) {
|
||||
high = REC601_LUMA_B;
|
||||
low = REC601_LUMA_R;
|
||||
} else if (order == COLOR_ORDER_GRB) {
|
||||
high = REC601_LUMA_G;
|
||||
middle = REC601_LUMA_R;
|
||||
}
|
||||
// Each field value scaled to 0..255, then weighted
|
||||
for (unsigned i = 0; i != 32; i++) {
|
||||
this->high_[i] = high * esp_scale(i, 31);
|
||||
this->low_[i] = low * esp_scale(i, 31);
|
||||
}
|
||||
for (unsigned i = 0; i != 64; i++)
|
||||
this->middle_[i] = middle * esp_scale(i, 63);
|
||||
}
|
||||
|
||||
uint8_t operator()(uint16_t value) const {
|
||||
const uint32_t weighted = this->high_[value >> 11] + this->middle_[(value >> 5) & 0x3F] + this->low_[value & 0x1F];
|
||||
return ((weighted + 128u) >> 8) >= 128 ? 1 : 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Weighted contribution of each value of each field
|
||||
std::array<uint16_t, 32> high_{};
|
||||
std::array<uint16_t, 64> middle_{};
|
||||
std::array<uint16_t, 32> low_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::display
|
||||
|
||||
@@ -118,8 +118,13 @@ void Ecocomfort2Fan::on_status() {
|
||||
|
||||
int new_speed = this->speed;
|
||||
uint8_t base_speed = raw_speed & SPEED_MASK;
|
||||
if (base_speed >= SPEED_SLEEP && base_speed <= SPEED_VEL3)
|
||||
if (raw_speed & FLAG_BOOST) {
|
||||
new_speed = SPEED_COUNT;
|
||||
} else if (raw_speed & FLAG_NIGHT) {
|
||||
new_speed = SPEED_SLEEP;
|
||||
} else if (base_speed >= SPEED_SLEEP && base_speed <= SPEED_VEL3) {
|
||||
new_speed = base_speed;
|
||||
}
|
||||
const Preset *preset = find_preset(mode, (raw_speed & FLAG_AUTO) != 0);
|
||||
const char *new_preset = preset != nullptr ? preset->name : nullptr;
|
||||
bool same_preset = new_preset == nullptr ? !this->has_preset_mode() : this->get_preset_mode().c_str() == new_preset;
|
||||
|
||||
@@ -205,10 +205,6 @@ void Ecocomfort2Hub::publish_unknown_() {
|
||||
sensor->publish_state(NAN);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->boost_binary_sensor_ != nullptr)
|
||||
this->boost_binary_sensor_->invalidate_state();
|
||||
#endif
|
||||
}
|
||||
|
||||
// C_OPER: mode byte, speed/flags byte
|
||||
@@ -220,6 +216,10 @@ void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) {
|
||||
this->mode_ = data[0];
|
||||
this->speed_ = data[1];
|
||||
ESP_LOGD(TAG, "[%s] C_OPER: mode=0x%02x, speed=0x%02x", this->parent_->address_str(), this->mode_, this->speed_);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->boost_binary_sensor_ != nullptr)
|
||||
this->boost_binary_sensor_->publish_state((this->speed_ & FLAG_BOOST) != 0);
|
||||
#endif
|
||||
#ifdef ECOCOMFORT2_CHILD_COUNT
|
||||
for (auto *child : this->children_) {
|
||||
child->on_status();
|
||||
@@ -227,9 +227,7 @@ void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// C_STATE: bytes 0-1 the mode and speed the unit runs; byte 1 also has the boost flag (0x40) and the advanced
|
||||
// control step (0x20). Byte 2 the air flow direction, bytes 3-4 temperature and 5-6 humidity in hundredths,
|
||||
// 7-8 VOC in ppb
|
||||
// C_STATE: bytes 3-4 temperature and 5-6 humidity in hundredths, 7-8 VOC in ppb, byte 2 the air flow direction
|
||||
void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 9) {
|
||||
ESP_LOGW(TAG, "[%s] C_STATE too short: %u bytes", this->parent_->address_str(), len);
|
||||
@@ -238,12 +236,8 @@ void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) {
|
||||
const float temperature = static_cast<int16_t>(encode_uint16(data[3], data[4])) / 100.0f;
|
||||
const float humidity = encode_uint16(data[5], data[6]) / 100.0f;
|
||||
const uint16_t voc = encode_uint16(data[7], data[8]);
|
||||
ESP_LOGD(TAG, "[%s] C_STATE: mode=0x%02x, speed=0x%02x, %.2f °C, %.2f %%, %u ppb, direction=%u",
|
||||
this->parent_->address_str(), data[0], data[1], temperature, humidity, voc, data[2]);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->boost_binary_sensor_ != nullptr)
|
||||
this->boost_binary_sensor_->publish_state((data[1] & FLAG_BOOST) != 0);
|
||||
#endif
|
||||
ESP_LOGD(TAG, "[%s] C_STATE: %.2f °C, %.2f %%, %u ppb, direction=%u", this->parent_->address_str(), temperature,
|
||||
humidity, voc, data[2]);
|
||||
#ifdef USE_SENSOR
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
@@ -273,7 +267,7 @@ void Ecocomfort2Hub::on_info_(const uint8_t *data, uint16_t len) {
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
// C_CLOCK: two-digit year, month, day, weekday with Monday as 0, hour, minute, second, automatic DST flag
|
||||
// C_CLOCK: hour, minute, second, weekday with Monday as 0, day, month, year as two bytes
|
||||
void Ecocomfort2Hub::sync_clock_() {
|
||||
if (!this->is_ready() || this->char_handle_clock_ == 0)
|
||||
return;
|
||||
@@ -282,11 +276,15 @@ void Ecocomfort2Hub::sync_clock_() {
|
||||
return;
|
||||
// ESPTime counts Sunday=1 .. Saturday=7, the unit Monday=0 .. Sunday=6
|
||||
const uint8_t weekday = (now.day_of_week + 5) % 7;
|
||||
// The time source already applies DST and every sync rewrites the clock, so the unit must not shift it again
|
||||
uint8_t data[8]{
|
||||
static_cast<uint8_t>(now.year % 100), now.month, now.day_of_month, weekday, now.hour, now.minute, now.second, 0};
|
||||
ESP_LOGD(TAG, "[%s] Syncing clock to %04u-%02u-%02u %02u:%02u:%02u", this->parent_->address_str(), now.year,
|
||||
now.month, now.day_of_month, now.hour, now.minute, now.second);
|
||||
uint8_t data[8]{now.hour,
|
||||
now.minute,
|
||||
now.second,
|
||||
weekday,
|
||||
now.day_of_month,
|
||||
now.month,
|
||||
static_cast<uint8_t>(now.year >> 8),
|
||||
static_cast<uint8_t>(now.year)};
|
||||
ESP_LOGD(TAG, "[%s] Syncing clock to %02u:%02u:%02u", this->parent_->address_str(), now.hour, now.minute, now.second);
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_clock_,
|
||||
sizeof(data), data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
|
||||
@@ -32,8 +32,7 @@ static constexpr uint8_t OPER_OUT = 0x02;
|
||||
static constexpr uint8_t OPER_IN_OUT = 0x03;
|
||||
static constexpr uint8_t OPER_SENSOR_OR_AUTO = 0x04;
|
||||
|
||||
// Speed byte: speed in the low nibble, flags in the high nibble.
|
||||
// C_OPER carries FLAG_AUTO; the unit reports FLAG_BOOST in byte 1 of C_STATE.
|
||||
// C_OPER byte 1: speed in the low nibble, flags in the high nibble
|
||||
static constexpr uint8_t SPEED_OFF = 0x00;
|
||||
static constexpr uint8_t SPEED_SLEEP = 0x01;
|
||||
static constexpr uint8_t SPEED_VEL1 = 0x02;
|
||||
@@ -42,6 +41,7 @@ static constexpr uint8_t SPEED_VEL3 = 0x04;
|
||||
static constexpr uint8_t SPEED_MASK = 0x0F;
|
||||
static constexpr uint8_t FLAG_AUTO = 0x10;
|
||||
static constexpr uint8_t FLAG_BOOST = 0x40;
|
||||
static constexpr uint8_t FLAG_NIGHT = 0x80;
|
||||
|
||||
/** Hub component for Ecocomfort 2.0 VMC via BLE. */
|
||||
class Ecocomfort2Hub : public esphome::ble_client::BLEClientNode, public PollingComponent {
|
||||
|
||||
@@ -23,18 +23,11 @@ DEPENDENCIES = ["uart"]
|
||||
|
||||
emontx_ns = cg.esphome_ns.namespace("emontx")
|
||||
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
|
||||
LockOnReboot = emontx_ns.enum("LockOnReboot", is_class=True)
|
||||
|
||||
CONF_EMONTX_ID = "emontx_id"
|
||||
CONF_TAG_NAME = "tag_name"
|
||||
CONF_ON_JSON = "on_json"
|
||||
CONF_LOCK_ON_REBOOT = "lock_on_reboot"
|
||||
LOCK_ON_REBOOT_AUTO = "auto"
|
||||
LOCK_ON_REBOOT_MODES = {
|
||||
LOCK_ON_REBOOT_AUTO: LockOnReboot.LOCK_ON_REBOOT_AUTO,
|
||||
True: LockOnReboot.LOCK_ON_REBOOT_ALWAYS,
|
||||
False: LockOnReboot.LOCK_ON_REBOOT_NEVER,
|
||||
}
|
||||
|
||||
DOMAIN = "emontx"
|
||||
|
||||
@@ -66,9 +59,7 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(): cv.declare_id(EmonTx),
|
||||
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=LOCK_ON_REBOOT_AUTO): cv.Any(
|
||||
cv.boolean, cv.one_of(LOCK_ON_REBOOT_AUTO, lower=True)
|
||||
),
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -144,11 +135,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
await uart.register_uart_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
lock = config[CONF_LOCK_ON_REBOOT]
|
||||
cg.add(var.set_lock_on_reboot(LOCK_ON_REBOOT_MODES[lock]))
|
||||
if lock == LOCK_ON_REBOOT_AUTO:
|
||||
# Locking depends on the firmware version the emonTx reports
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
if config[CONF_LOCK_ON_REBOOT]:
|
||||
cg.add(var.set_lock_on_reboot(True))
|
||||
|
||||
|
||||
# Action: emontx.send_command
|
||||
|
||||
@@ -7,85 +7,30 @@ namespace esphome::emontx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "emontx");
|
||||
|
||||
static constexpr uint32_t UNLOCK_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so the next one waits
|
||||
static constexpr uint32_t COMMAND_GAP_MS = 250;
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 1;
|
||||
// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
|
||||
static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
|
||||
static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so 'v' waits
|
||||
static constexpr uint32_t VERSION_REQUEST_DELAY_MS = 250;
|
||||
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
|
||||
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
|
||||
// Every data line starts with the message counter, in JSON or text format
|
||||
static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
|
||||
static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
|
||||
// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
|
||||
static constexpr uint32_t LOCK_MIN_MAJOR = 1;
|
||||
static constexpr uint32_t LOCK_MIN_MINOR = 2;
|
||||
|
||||
// True if line starts with the PROGMEM prefix and has more after it
|
||||
static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
|
||||
return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
|
||||
}
|
||||
|
||||
// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
|
||||
static bool read_number(const char *&pos, const char *end, uint32_t &value) {
|
||||
const char *start = pos;
|
||||
value = 0;
|
||||
while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
|
||||
value = value * 10 + (*pos++ - '0');
|
||||
return pos != start;
|
||||
}
|
||||
|
||||
// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
|
||||
static bool supports_lock(const char *version, const char *end) {
|
||||
if (version < end && (*version == 'v' || *version == 'V'))
|
||||
version++;
|
||||
uint32_t major;
|
||||
if (!read_number(version, end, major) || version == end || *version != '.')
|
||||
return false;
|
||||
version++;
|
||||
uint32_t minor;
|
||||
if (!read_number(version, end, minor))
|
||||
return false;
|
||||
return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EmonTx::setup() {
|
||||
this->buffer_pos_ = 0;
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
// The line feed ends any boot noise
|
||||
if (this->lock_on_reboot_) {
|
||||
// The line feed ends any boot noise first
|
||||
ESP_LOGI(TAG, "Unlocking emonTx after boot");
|
||||
this->write_byte('\n');
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
this->unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
});
|
||||
return;
|
||||
this->send_command("emonunlock");
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// With AUTO, the reply decides whether to unlock
|
||||
if (this->wants_version_())
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
|
||||
void EmonTx::unlock_() {
|
||||
ESP_LOGI(TAG, "Unlocking emonTx");
|
||||
this->send_command("emonunlock");
|
||||
}
|
||||
|
||||
void EmonTx::on_shutdown() {
|
||||
bool lock = this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS;
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
lock |= this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && this->lock_supported_;
|
||||
#endif
|
||||
if (!lock)
|
||||
if (!this->lock_on_reboot_)
|
||||
return;
|
||||
if (this->is_paused()) {
|
||||
ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
|
||||
@@ -118,7 +63,7 @@ void EmonTx::loop() {
|
||||
this->discarding_line_ = dropped != '\n';
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The emonTx may have been flashed, or a request dropped, during the pause
|
||||
if (this->wants_version_())
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
@@ -151,10 +96,8 @@ void EmonTx::loop() {
|
||||
this->parse_json_(this->buffer_.data(), len);
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_()) {
|
||||
this->parse_msg_number_(this->buffer_.data(), len);
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->parse_version_line_(this->buffer_.data(), len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else if (received == '\r' || this->discarding_line_) {
|
||||
@@ -181,6 +124,16 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
auto msg = root["MSG"];
|
||||
if (this->version_text_sensor_ != nullptr && msg.is<uint32_t>()) {
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
uint32_t msg_number = msg;
|
||||
if (msg_number <= this->last_msg_number_)
|
||||
this->request_version_();
|
||||
this->last_msg_number_ = msg_number;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->json_callbacks_.call(root, StringRef(data, len));
|
||||
return true;
|
||||
@@ -192,50 +145,17 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
void EmonTx::request_version_() {
|
||||
// The firmware may have changed; until the reply, an unknown version gets no lock commands
|
||||
this->lock_supported_ = false;
|
||||
this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
|
||||
this->send_version_request_();
|
||||
}
|
||||
|
||||
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
|
||||
void EmonTx::send_version_request_() {
|
||||
void EmonTx::request_version_() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->write_byte('\n');
|
||||
this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
|
||||
}
|
||||
|
||||
void EmonTx::parse_msg_number_(const char *line, size_t len) {
|
||||
const char *pos;
|
||||
if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_JSON_PREFIX) - 1;
|
||||
} else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
uint32_t msg_number;
|
||||
if (!read_number(pos, line + len, msg_number))
|
||||
return;
|
||||
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
if (msg_number <= this->last_msg_number_) {
|
||||
this->request_version_();
|
||||
} else if (this->version_frames_left_ > 0) {
|
||||
if (--this->version_frames_left_ == 0) {
|
||||
ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
|
||||
} else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
|
||||
this->send_version_request_();
|
||||
}
|
||||
}
|
||||
this->last_msg_number_ = msg_number;
|
||||
this->set_timeout(VERSION_REQUEST_ID, VERSION_REQUEST_DELAY_MS, [this]() { this->send_command("v"); });
|
||||
}
|
||||
|
||||
void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
|
||||
if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
|
||||
if (len <= prefix_len || ESPHOME_strncmp_P(line, VERSION_PREFIX, prefix_len) != 0)
|
||||
return;
|
||||
const char *version = line + prefix_len;
|
||||
const char *end = line + len;
|
||||
@@ -244,23 +164,7 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
if (version == end)
|
||||
return;
|
||||
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
|
||||
this->version_frames_left_ = 0;
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
#endif
|
||||
|
||||
bool supported = supports_lock(version, end);
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
|
||||
// Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->unlock_();
|
||||
this->unlock_sent_ = true;
|
||||
});
|
||||
}
|
||||
this->lock_supported_ = supported;
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -269,13 +173,7 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
*/
|
||||
void EmonTx::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "EmonTx:");
|
||||
const char *lock = LOG_STR_LITERAL("no");
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
lock = LOG_STR_LITERAL("yes");
|
||||
} else if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO) {
|
||||
lock = LOG_STR_LITERAL("auto");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", YESNO(this->lock_on_reboot_));
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
|
||||
@@ -285,7 +183,7 @@ void EmonTx::dump_config() {
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
@@ -22,12 +22,6 @@ namespace esphome::emontx {
|
||||
/// Maximum line length in bytes (plus one byte reserved for null terminator)
|
||||
static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
|
||||
enum class LockOnReboot : uint8_t {
|
||||
LOCK_ON_REBOOT_NEVER,
|
||||
LOCK_ON_REBOOT_ALWAYS,
|
||||
LOCK_ON_REBOOT_AUTO, // Only when the emonTx reports firmware v1.2.0 or later
|
||||
};
|
||||
|
||||
/**
|
||||
* @class EmonTx
|
||||
* @brief Main class for the EmonTx component.
|
||||
@@ -36,7 +30,7 @@ enum class LockOnReboot : uint8_t {
|
||||
* extracts tags and values, and publishes them to registered sensors.
|
||||
*/
|
||||
class EmonTx final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
SUB_TEXT_SENSOR(version)
|
||||
#endif
|
||||
public:
|
||||
@@ -62,8 +56,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
|
||||
|
||||
/// Lock the emonTx ('emonlock') before a reboot, so serial garbage during the reboot is not taken
|
||||
/// as a command, and unlock it after boot. Needs emonTx firmware v1.2.0 or later, which AUTO checks.
|
||||
void set_lock_on_reboot(LockOnReboot lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
/// as a command, and unlock it in setup(). Needs emonTx firmware v1.2.0 or later.
|
||||
void set_lock_on_reboot(bool lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void init_sensors(size_t count) { this->sensors_.init(count); }
|
||||
@@ -72,18 +66,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
|
||||
protected:
|
||||
void parse_json_(const char *data, size_t len);
|
||||
void unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool wants_version_() const {
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
return true;
|
||||
#endif
|
||||
return this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO;
|
||||
}
|
||||
void request_version_();
|
||||
void send_version_request_();
|
||||
void parse_msg_number_(const char *line, size_t len);
|
||||
void parse_version_line_(const char *line, size_t len);
|
||||
#endif
|
||||
|
||||
@@ -95,12 +79,9 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
uint16_t buffer_pos_{0};
|
||||
std::atomic<bool> paused_{false};
|
||||
bool was_paused_{false};
|
||||
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
|
||||
bool lock_on_reboot_{false};
|
||||
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
|
||||
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
|
||||
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
|
||||
uint32_t last_msg_number_{0};
|
||||
#endif
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
|
||||
@@ -21,4 +21,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
|
||||
@@ -14,15 +13,21 @@
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
// The Rec.601 luma helpers and the RGB565 mono lookup live in the shared display utilities, so other
|
||||
// drivers (e.g. it8951 in its 1 bit per pixel mode) can use them too.
|
||||
using display::color_to_mono;
|
||||
using display::rec601_luma;
|
||||
using display::Rgb565ToMono;
|
||||
|
||||
/** Delta for when to regard as gray */
|
||||
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
|
||||
|
||||
/** Rec.601 luma (0.299/0.587/0.114 weights, scaled by 256) for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map RGB color to a single monochrome bit
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
* @return 1 = white, 0 = black
|
||||
*/
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** Map RGB color to one of 4 discrete gray levels (2 bits per pixel)
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
|
||||
@@ -40,6 +40,7 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -197,11 +198,15 @@ def _final_validate(config) -> None:
|
||||
"epaper_spi", require_miso=False, require_mosi=True
|
||||
)(config)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if display.has_external_writer(config[CONF_ID]):
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
|
||||
@@ -131,7 +131,9 @@ void EPaperBase::update() {
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
#endif
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
@@ -154,10 +156,6 @@ void EPaperBase::loop() {
|
||||
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
|
||||
#endif
|
||||
} else {
|
||||
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
|
||||
this->abandon_update_();
|
||||
return;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (now - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
|
||||
@@ -170,20 +168,6 @@ void EPaperBase::loop() {
|
||||
this->process_state_();
|
||||
}
|
||||
|
||||
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
|
||||
void EPaperBase::abandon_update_() {
|
||||
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
|
||||
this->epaper_state_to_string_());
|
||||
this->status_set_warning(LOG_STR("Busy pin timeout"));
|
||||
if (this->state_ == EPaperState::TRANSFER_DATA)
|
||||
this->abort_transfer();
|
||||
// Leave the panel powered down rather than with its high-voltage drivers enabled
|
||||
this->power_off();
|
||||
this->full_update_requested_ = true;
|
||||
this->current_data_index_ = 0;
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the state machine.
|
||||
* Typical state sequence:
|
||||
@@ -258,7 +242,6 @@ void EPaperBase::process_state_() {
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->status_clear_warning();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
|
||||
break;
|
||||
@@ -362,120 +345,6 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// One source row of an RGB565 block, written into a contiguous 1 bit per pixel buffer. Without SwapAxes the row lies
|
||||
// along a buffer row and the bit moves by step; with it the row lies along a bit column and the byte moves by step
|
||||
// rows.
|
||||
template<bool SwapAxes, bool BigEndianSource>
|
||||
void HOT write_rgb565_row(uint8_t *byte, uint8_t mask, int step, size_t row_width, const uint8_t *source, int count,
|
||||
const Rgb565ToMono &to_mono) {
|
||||
const ptrdiff_t byte_step = SwapAxes ? step * static_cast<ptrdiff_t>(row_width) : 0;
|
||||
for (; count != 0; count--, source += 2) {
|
||||
const uint16_t value = BigEndianSource ? (source[0] << 8) | source[1] : source[0] | (source[1] << 8);
|
||||
if (to_mono(value)) {
|
||||
*byte |= mask;
|
||||
} else {
|
||||
*byte &= ~mask;
|
||||
}
|
||||
if constexpr (SwapAxes) {
|
||||
byte += byte_step;
|
||||
} else if (step > 0) {
|
||||
mask >>= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x80;
|
||||
byte++;
|
||||
}
|
||||
} else {
|
||||
mask <<= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x01;
|
||||
byte--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Each source column maps to one buffer coordinate and each source row to the other, each moving by 1 per pixel, so
|
||||
// the visible part of a block and its dirty area are worked out once for the whole block.
|
||||
void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr,
|
||||
ColorOrder order, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const bool swap = this->effective_transform_ & SWAP_XY;
|
||||
const bool mirror_x = this->effective_transform_ & MIRROR_X;
|
||||
const bool mirror_y = this->effective_transform_ & MIRROR_Y;
|
||||
// The buffer axis a source column moves along, and the one a source row moves along
|
||||
const int column_limit = swap ? this->height_ : this->width_;
|
||||
const int row_limit = swap ? this->width_ : this->height_;
|
||||
const bool column_mirrored = swap ? mirror_y : mirror_x;
|
||||
const bool row_mirrored = swap ? mirror_x : mirror_y;
|
||||
const int first_column = std::max(0, -x_start);
|
||||
const int end_column = std::min(w, column_limit - x_start);
|
||||
const int first_row = std::max(0, -y_start);
|
||||
const int end_row = std::min(h, row_limit - y_start);
|
||||
if (first_column >= end_column || first_row >= end_row)
|
||||
return;
|
||||
auto column_to_buffer = [&](int column) {
|
||||
const int u = x_start + column;
|
||||
return column_mirrored ? column_limit - 1 - u : u;
|
||||
};
|
||||
auto row_to_buffer = [&](int row) {
|
||||
const int v = y_start + row;
|
||||
return row_mirrored ? row_limit - 1 - v : v;
|
||||
};
|
||||
|
||||
const int column_low = std::min(column_to_buffer(first_column), column_to_buffer(end_column - 1));
|
||||
const int row_low = std::min(row_to_buffer(first_row), row_to_buffer(end_row - 1));
|
||||
const int columns = end_column - first_column;
|
||||
const int rows = end_row - first_row;
|
||||
const int x_low = swap ? row_low : column_low;
|
||||
const int y_low = swap ? column_low : row_low;
|
||||
const int x_high = x_low + (swap ? rows : columns);
|
||||
const int y_high = y_low + (swap ? columns : rows);
|
||||
|
||||
const Rgb565ToMono to_mono(order);
|
||||
const size_t line_stride = x_offset + w + x_pad;
|
||||
const size_t row_width = this->row_width_;
|
||||
const int step = column_mirrored ? -1 : 1;
|
||||
for (int row = first_row; row != end_row; row++) {
|
||||
const uint8_t *source = ptr + ((y_offset + row) * line_stride + x_offset + first_column) * 2;
|
||||
const int along = column_to_buffer(first_column);
|
||||
const int across = row_to_buffer(row);
|
||||
const int x = swap ? across : along;
|
||||
const int y = swap ? along : across;
|
||||
uint8_t *byte = buffer + y * row_width + x / 8;
|
||||
const uint8_t mask = 0x80u >> (x % 8);
|
||||
if (swap) {
|
||||
if (big_endian) {
|
||||
write_rgb565_row<true, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<true, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
} else if (big_endian) {
|
||||
write_rgb565_row<false, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<false, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
}
|
||||
this->x_low_ = clamp_at_most(this->x_low_, x_low);
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x_high);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y_low);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y_high);
|
||||
}
|
||||
|
||||
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
|
||||
// usual case): the same result as draw_pixel_at() for each pixel. Anything else goes through draw_pixel_at().
|
||||
void HOT EPaperBase::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t contiguous_length;
|
||||
uint8_t *contiguous = this->buffer_.get_span(0, contiguous_length);
|
||||
if (!this->buffer_is_mono() || this->get_clipping().is_set() || bitness != COLOR_BITNESS_565 ||
|
||||
contiguous_length != this->buffer_length_) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
this->draw_rgb565_block_(contiguous, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
void EPaperBase::dump_config() {
|
||||
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -106,10 +106,7 @@ class EPaperBase : public Display,
|
||||
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
// Make the next update a full one. Applied when that update starts, so one in progress is not affected.
|
||||
void request_full_update() { this->full_update_requested_ = true; }
|
||||
// True from the start of an update until the display has refreshed and gone back to sleep.
|
||||
@@ -128,14 +125,9 @@ class EPaperBase : public Display,
|
||||
virtual bool initialise(bool partial);
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
void abandon_update_();
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad);
|
||||
// Whether the buffer holds 1 bit per pixel as draw_pixel_at() writes it, so draw_pixels_at() can write it directly
|
||||
virtual bool buffer_is_mono() const { return false; }
|
||||
|
||||
/**
|
||||
* Methods that must be implemented by concrete classes to control the display
|
||||
@@ -145,8 +137,6 @@ class EPaperBase : public Display,
|
||||
* @return true if done, false if it should be called next loop
|
||||
*/
|
||||
virtual bool transfer_data() = 0;
|
||||
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
|
||||
virtual void abort_transfer() {}
|
||||
/**
|
||||
* Refresh the screen after data transfer
|
||||
*/
|
||||
@@ -197,20 +187,18 @@ class EPaperBase : public Display,
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
|
||||
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t waiting_for_idle_last_print_{};
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#endif
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
uint32_t update_start_time_{};
|
||||
#endif
|
||||
|
||||
// properties with specific initialisers go last
|
||||
uint32_t reset_duration_{10};
|
||||
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
|
||||
uint16_t busy_timeout_seconds_{120};
|
||||
uint8_t full_update_every_{1};
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
uint32_t reset_duration_{10};
|
||||
uint8_t full_update_every_{1};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -15,7 +15,6 @@ class EPaperMono : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override {}
|
||||
void power_off() override{};
|
||||
|
||||
@@ -58,9 +58,7 @@ bool HOT EPaperSSD1677::transfer_data() {
|
||||
// A full update ignores 0x26, and the copy may not hold a real frame yet (first update after
|
||||
// boot): send the new frame to both planes.
|
||||
const bool send_copy = this->plane_ == 0 && this->update_count_ != 0;
|
||||
// A mono buffer already holds the frame as a RAM plane does, so it can be sent as it is; a deeper
|
||||
// buffer (the four-level model) returns false and packs the plane in plane_row().
|
||||
const bool direct = send_copy || this->buffer_is_mono();
|
||||
const bool direct = send_copy || this->buffer_is_plane();
|
||||
const auto &source = send_copy ? this->sent_ : this->buffer_;
|
||||
const size_t row_length = this->plane_row_length_();
|
||||
const size_t plane_length = row_length * this->height_;
|
||||
|
||||
@@ -34,6 +34,9 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
void init_comparison_frame_();
|
||||
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
|
||||
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
|
||||
// Whether the buffer already holds the frame as a RAM plane does, so it can be sent as it is.
|
||||
// A subclass with a deeper buffer returns false and overrides plane_row().
|
||||
virtual bool buffer_is_plane() const { return true; }
|
||||
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
|
||||
virtual void plane_row(size_t y, uint8_t *out);
|
||||
bool reset() override;
|
||||
|
||||
@@ -38,9 +38,9 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 {
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return false; }
|
||||
// A partial update if partial updates are enabled and this is not a full one.
|
||||
bool is_partial_push_() const { return this->update_count_ != 0 && this->sent_.is_valid(); }
|
||||
bool buffer_is_plane() const override { return false; }
|
||||
void plane_row(size_t y, uint8_t *out) override;
|
||||
void refresh_screen(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
|
||||
@@ -364,15 +364,6 @@ bool HOT EPaperT133A01::transfer_data() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperT133A01::abort_transfer() {
|
||||
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
|
||||
if (this->current_data_index_ == 0)
|
||||
return;
|
||||
this->disable();
|
||||
this->cs_pin_->digital_write(true);
|
||||
this->cs1_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
void EPaperT133A01::dump_config() {
|
||||
EPaperBase::dump_config();
|
||||
LOG_PIN(" CS Pin: ", this->cs_pin_);
|
||||
|
||||
@@ -46,7 +46,6 @@ class EPaperT133A01 : public EPaperBase {
|
||||
void deep_sleep() override;
|
||||
|
||||
bool transfer_data() override;
|
||||
void abort_transfer() override;
|
||||
|
||||
/**
|
||||
* Send a command (and optional data) selecting one or both controllers.
|
||||
|
||||
@@ -37,7 +37,6 @@ class EPaperUC8179 final : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
|
||||
@@ -87,38 +87,6 @@ jd79660.extend(
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 2.13-G (V2 hardware/init revision, PCB marked "rev 2.1" or later)
|
||||
#
|
||||
# Vendor init derived from vendor sample code
|
||||
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g_V2.cpp>
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
#
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
|
||||
# Note: panel is 122px wide, but vendor code declares the TRES register as a fixed
|
||||
# 124 on this revision. width=124 is declared here (rather than 122) purely so the
|
||||
# transferred bytes-per-row matches what the vendor init tells the IC to expect; the
|
||||
# extra 2 columns fall outside the visible glass. The model width must equal the
|
||||
# TRES value in initsequence, so overriding dimensions: in YAML brings the shear back.
|
||||
#
|
||||
# fmt: off
|
||||
jd79660.extend(
|
||||
"Waveshare-2.13in-G",
|
||||
width=124,
|
||||
height=250,
|
||||
|
||||
initsequence=(
|
||||
(0x61, 0x00, 0x7C, 0x00, 0xFA,), # TRES: 124x250 fixed (124 = TRES stride value)
|
||||
(0xE9, 0x01,),
|
||||
# Power On (0x04): Must be early part of init seq = Disabled later!
|
||||
(0x04,),
|
||||
),
|
||||
fast_update=(
|
||||
(0xE0, 0x02,),
|
||||
(0xE6, 0x5A,),
|
||||
(0xA5,),
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 7.5-H
|
||||
#
|
||||
# Vendor init derived from vendor sample code
|
||||
@@ -162,7 +130,7 @@ jd79660.extend(
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle.
|
||||
#
|
||||
# fmt: off
|
||||
gdey0213f51 = jd79660.extend(
|
||||
jd79660.extend(
|
||||
"goodisplay-gdey0213f51-2.13",
|
||||
width=128,
|
||||
height=250,
|
||||
@@ -186,13 +154,3 @@ gdey0213f51 = jd79660.extend(
|
||||
(0x04,),
|
||||
),
|
||||
)
|
||||
|
||||
# Waveshare 2.13-G (V1 hardware/init revision, older stock): the same panel and init as the Good
|
||||
# Display entry above, byte for byte, so it only adds the name. Vendor sample:
|
||||
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g.cpp>
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
#
|
||||
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
|
||||
# Note: width=128 is the TRES value the init sequence programs, not the 122 visible columns; overriding
|
||||
# dimensions: in YAML shears the image. Not tested on V1 hardware.
|
||||
gdey0213f51.extend("Waveshare-2.13in-G-V1")
|
||||
|
||||
@@ -24,11 +24,3 @@ WaveshareB(
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="1s",
|
||||
)
|
||||
|
||||
WaveshareB(
|
||||
"waveshare-2.66in-b",
|
||||
width=152,
|
||||
height=296,
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="15s",
|
||||
)
|
||||
|
||||
@@ -111,36 +111,24 @@ void ES8388::setup() {
|
||||
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
|
||||
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0x09));
|
||||
|
||||
#ifdef USE_ES8388_SELECT
|
||||
#ifdef USE_SELECT
|
||||
if (this->dac_output_select_ != nullptr) {
|
||||
if (this->dac_output_initial_.has_value()) {
|
||||
const DacOutputLine line = this->dac_output_initial_.value();
|
||||
ES8388_ERROR_FAILED(this->set_dac_output(line));
|
||||
this->dac_output_select_->publish_state(static_cast<size_t>(line));
|
||||
} else {
|
||||
auto dac_power = this->get_dac_power();
|
||||
if (dac_power.has_value()) {
|
||||
if (this->dac_output_select_->has_index(dac_power.value())) {
|
||||
this->dac_output_select_->publish_state(dac_power.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
|
||||
}
|
||||
auto dac_power = this->get_dac_power();
|
||||
if (dac_power.has_value()) {
|
||||
if (this->dac_output_select_->has_index(dac_power.value())) {
|
||||
this->dac_output_select_->publish_state(dac_power.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->adc_input_mic_select_ != nullptr) {
|
||||
if (this->adc_input_mic_initial_.has_value()) {
|
||||
const AdcInputMicLine line = this->adc_input_mic_initial_.value();
|
||||
ES8388_ERROR_FAILED(this->set_adc_input_mic(line));
|
||||
this->adc_input_mic_select_->publish_state(static_cast<size_t>(line));
|
||||
} else {
|
||||
auto mic_input = this->get_mic_input();
|
||||
if (mic_input.has_value()) {
|
||||
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
|
||||
this->adc_input_mic_select_->publish_state(mic_input.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
|
||||
}
|
||||
auto mic_input = this->get_mic_input();
|
||||
if (mic_input.has_value()) {
|
||||
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
|
||||
this->adc_input_mic_select_->publish_state(mic_input.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +138,7 @@ void ES8388::setup() {
|
||||
void ES8388::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ES8388 Audio Codec:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
#ifdef USE_ES8388_SELECT
|
||||
#ifdef USE_SELECT
|
||||
LOG_SELECT(" ", "DacOutputSelect", this->dac_output_select_);
|
||||
LOG_SELECT(" ", "ADCInputMicSelect", this->adc_input_mic_select_);
|
||||
#endif
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/components/audio_dac/audio_dac.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_ES8388_SELECT
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,7 @@ enum AdcInputMicLine : uint8_t {
|
||||
};
|
||||
|
||||
class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
|
||||
#ifdef USE_ES8388_SELECT
|
||||
#ifdef USE_SELECT
|
||||
SUB_SELECT(dac_output)
|
||||
SUB_SELECT(adc_input_mic)
|
||||
#endif
|
||||
@@ -68,22 +68,11 @@ class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I
|
||||
bool set_dac_output(DacOutputLine line);
|
||||
bool set_adc_input_mic(AdcInputMicLine line);
|
||||
|
||||
#ifdef USE_ES8388_SELECT
|
||||
/// Line to apply in setup() instead of keeping what the codec reports.
|
||||
void set_dac_output_initial(DacOutputLine line) { this->dac_output_initial_ = line; }
|
||||
void set_adc_input_mic_initial(AdcInputMicLine line) { this->adc_input_mic_initial_ = line; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/// @brief Mutes or unmutes the DAC audio out
|
||||
/// @param mute_state True to mute, false to unmute
|
||||
/// @return True if successful and false otherwise
|
||||
bool set_mute_state_(bool mute_state);
|
||||
|
||||
#ifdef USE_ES8388_SELECT
|
||||
optional<DacOutputLine> dac_output_initial_{};
|
||||
optional<AdcInputMicLine> adc_input_mic_initial_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::es8388
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_INITIAL_OPTION,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_CHIP, # noqa: F401
|
||||
)
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_CHIP # noqa: F401
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
|
||||
@@ -13,21 +9,6 @@ from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
|
||||
CONF_DAC_OUTPUT = "dac_output"
|
||||
CONF_ADC_INPUT_MIC = "adc_input_mic"
|
||||
|
||||
DacOutputLine = es8388_ns.enum("DacOutputLine")
|
||||
DAC_OUTPUT_OPTIONS = {
|
||||
"LINE1": DacOutputLine.DAC_OUTPUT_LINE1,
|
||||
"LINE2": DacOutputLine.DAC_OUTPUT_LINE2,
|
||||
"BOTH": DacOutputLine.DAC_OUTPUT_BOTH,
|
||||
}
|
||||
DAC_OUTPUT_OPTION_NAMES = list(DAC_OUTPUT_OPTIONS)
|
||||
AdcInputMicLine = es8388_ns.enum("AdcInputMicLine")
|
||||
ADC_INPUT_MIC_OPTIONS = {
|
||||
"LINE1": AdcInputMicLine.ADC_INPUT_MIC_LINE1,
|
||||
"LINE2": AdcInputMicLine.ADC_INPUT_MIC_LINE2,
|
||||
"DIFFERENCE": AdcInputMicLine.ADC_INPUT_MIC_DIFFERENCE,
|
||||
}
|
||||
ADC_INPUT_MIC_OPTION_NAMES = list(ADC_INPUT_MIC_OPTIONS)
|
||||
|
||||
DacOutputSelect = es8388_ns.class_("DacOutputSelect", select.Select)
|
||||
ADCInputMicSelect = es8388_ns.class_("ADCInputMicSelect", select.Select)
|
||||
|
||||
@@ -38,45 +19,24 @@ CONFIG_SCHEMA = cv.All(
|
||||
DacOutputSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_CHIP,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
|
||||
DAC_OUTPUT_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ADC_INPUT_MIC): select.select_schema(
|
||||
ADCInputMicSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_CHIP,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
|
||||
ADC_INPUT_MIC_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_ES8388_SELECT")
|
||||
hub = await cg.get_variable(config[CONF_ES8388_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
await selects(
|
||||
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=DAC_OUTPUT_OPTION_NAMES
|
||||
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=["LINE1", "LINE2", "BOTH"]
|
||||
)
|
||||
await selects(
|
||||
CONF_ADC_INPUT_MIC,
|
||||
hub.set_adc_input_mic_select,
|
||||
options=ADC_INPUT_MIC_OPTION_NAMES,
|
||||
options=["LINE1", "LINE2", "DIFFERENCE"],
|
||||
)
|
||||
if (
|
||||
initial := config.get(CONF_DAC_OUTPUT, {}).get(CONF_INITIAL_OPTION)
|
||||
) is not None:
|
||||
cg.add(hub.set_dac_output_initial(initial))
|
||||
if (
|
||||
initial := config.get(CONF_ADC_INPUT_MIC, {}).get(CONF_INITIAL_OPTION)
|
||||
) is not None:
|
||||
cg.add(hub.set_adc_input_mic_initial(initial))
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
CODEOWNERS = ["@GioMez"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "es9038q2m_katana"
|
||||
@@ -1,92 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio_dac, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_DEEMPHASIS = "deemphasis"
|
||||
CONF_DOP = "dop"
|
||||
CONF_FILTER_SHAPE = "filter_shape"
|
||||
|
||||
# Fields of the Katana format register: channel layout, word length and rate code.
|
||||
FORMAT_STEREO = 0x80
|
||||
BITS_PER_SAMPLE_CODES = {16: 0x10, 24: 0x20, 32: 0x30}
|
||||
SAMPLE_RATE_CODES = {
|
||||
44100: 0x04,
|
||||
48000: 0x05,
|
||||
88200: 0x06,
|
||||
96000: 0x07,
|
||||
176400: 0x08,
|
||||
192000: 0x09,
|
||||
352800: 0x0A,
|
||||
384000: 0x0B,
|
||||
}
|
||||
|
||||
es9038q2m_katana_ns = cg.esphome_ns.namespace("es9038q2m_katana")
|
||||
ES9038Q2MKatana = es9038q2m_katana_ns.class_(
|
||||
"ES9038Q2MKatana",
|
||||
audio_dac.AudioDac,
|
||||
cg.Component,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
FilterShape = es9038q2m_katana_ns.enum("FilterShape")
|
||||
FILTER_SHAPES = {
|
||||
"linear_phase_fast": FilterShape.FILTER_SHAPE_LINEAR_PHASE_FAST,
|
||||
"linear_phase_slow": FilterShape.FILTER_SHAPE_LINEAR_PHASE_SLOW,
|
||||
"minimum_phase_fast": FilterShape.FILTER_SHAPE_MIN_PHASE_FAST,
|
||||
"minimum_phase_slow": FilterShape.FILTER_SHAPE_MIN_PHASE_SLOW,
|
||||
"apodizing": FilterShape.FILTER_SHAPE_APODIZING,
|
||||
"hybrid": FilterShape.FILTER_SHAPE_HYBRID,
|
||||
"brick_wall": FilterShape.FILTER_SHAPE_BRICK_WALL,
|
||||
}
|
||||
|
||||
DeemphasisMode = es9038q2m_katana_ns.enum("DeemphasisMode")
|
||||
DEEMPHASIS_MODES = {
|
||||
"bypass": DeemphasisMode.DEEMPHASIS_BYPASS,
|
||||
"32khz": DeemphasisMode.DEEMPHASIS_32KHZ,
|
||||
"44_1khz": DeemphasisMode.DEEMPHASIS_44_1KHZ,
|
||||
"48khz": DeemphasisMode.DEEMPHASIS_48KHZ,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ES9038Q2MKatana),
|
||||
cv.Optional(CONF_FILTER_SHAPE, default="apodizing"): cv.enum(
|
||||
FILTER_SHAPES, lower=True
|
||||
),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
|
||||
cv.float_with_unit("bits", "bit"),
|
||||
cv.one_of(*BITS_PER_SAMPLE_CODES, int=True),
|
||||
),
|
||||
cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.one_of(
|
||||
*SAMPLE_RATE_CODES, int=True
|
||||
),
|
||||
cv.Optional(CONF_DEEMPHASIS, default="bypass"): cv.enum(
|
||||
DEEMPHASIS_MODES, lower=True
|
||||
),
|
||||
# Only switches the control logic; the source must already send DoP frames.
|
||||
cv.Optional(CONF_DOP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x30))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_filter_shape(config[CONF_FILTER_SHAPE]))
|
||||
cg.add(
|
||||
var.set_format(
|
||||
FORMAT_STEREO
|
||||
| BITS_PER_SAMPLE_CODES[config[CONF_BITS_PER_SAMPLE]]
|
||||
| SAMPLE_RATE_CODES[config[CONF_SAMPLE_RATE]]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_deemphasis_mode(config[CONF_DEEMPHASIS]))
|
||||
cg.add(var.set_dop_enabled(config[CONF_DOP]))
|
||||
@@ -1,176 +0,0 @@
|
||||
#include "es9038q2m_katana.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::es9038q2m_katana {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "es9038q2m_katana");
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t KATANA_CHIP_ID = 0x30;
|
||||
|
||||
constexpr uint8_t KATANA_REG_CHIP_ID = 0x00;
|
||||
constexpr uint8_t KATANA_REG_RESET = 0x01;
|
||||
constexpr uint8_t KATANA_REG_VOLUME_1 = 0x02;
|
||||
constexpr uint8_t KATANA_REG_VOLUME_2 = 0x03;
|
||||
constexpr uint8_t KATANA_REG_MUTE = 0x04;
|
||||
constexpr uint8_t KATANA_REG_DSP_PROGRAM = 0x05;
|
||||
constexpr uint8_t KATANA_REG_DEEMPHASIS = 0x06;
|
||||
constexpr uint8_t KATANA_REG_DOP = 0x07;
|
||||
constexpr uint8_t KATANA_REG_FORMAT = 0x08;
|
||||
constexpr uint8_t KATANA_REG_COMMAND = 0x09;
|
||||
constexpr uint8_t KATANA_REG_MUTE_STREAM = 0x0A;
|
||||
|
||||
constexpr float KATANA_VOLUME_MIDPOINT = 0.5f;
|
||||
constexpr float KATANA_VOLUME_MIDPOINT_STEPS = 50.0f; // -25 dB at 50% volume.
|
||||
// Cubic amplitude gain: -20 * log10(volume^3) / 0.5 attenuation steps.
|
||||
constexpr float KATANA_VOLUME_CUBIC_SCALE = 120.0f;
|
||||
constexpr uint8_t KATANA_VOLUME_FULL_ATTENUATION = 255;
|
||||
|
||||
// The control logic ignores writes for this long after a reset.
|
||||
constexpr uint32_t RESET_SETTLE_DELAY_MS = 10;
|
||||
|
||||
const LogString *filter_shape_to_str(FilterShape shape) {
|
||||
switch (shape) {
|
||||
case FILTER_SHAPE_LINEAR_PHASE_FAST:
|
||||
return LOG_STR("linear_phase_fast");
|
||||
case FILTER_SHAPE_LINEAR_PHASE_SLOW:
|
||||
return LOG_STR("linear_phase_slow");
|
||||
case FILTER_SHAPE_MIN_PHASE_FAST:
|
||||
return LOG_STR("minimum_phase_fast");
|
||||
case FILTER_SHAPE_MIN_PHASE_SLOW:
|
||||
return LOG_STR("minimum_phase_slow");
|
||||
case FILTER_SHAPE_APODIZING:
|
||||
return LOG_STR("apodizing");
|
||||
case FILTER_SHAPE_HYBRID:
|
||||
return LOG_STR("hybrid");
|
||||
default:
|
||||
return LOG_STR("brick_wall");
|
||||
}
|
||||
}
|
||||
|
||||
const LogString *deemphasis_to_str(DeemphasisMode mode) {
|
||||
switch (mode) {
|
||||
case DEEMPHASIS_32KHZ:
|
||||
return LOG_STR("32khz");
|
||||
case DEEMPHASIS_44_1KHZ:
|
||||
return LOG_STR("44_1khz");
|
||||
case DEEMPHASIS_48KHZ:
|
||||
return LOG_STR("48khz");
|
||||
default:
|
||||
return LOG_STR("bypass");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ES9038Q2MKatana::setup() {
|
||||
uint8_t chip_id = 0;
|
||||
if (!this->read_byte(KATANA_REG_CHIP_ID, &chip_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != KATANA_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Unexpected chip ID 0x%02X", chip_id);
|
||||
this->mark_failed(LOG_STR("Unexpected chip ID"));
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(KATANA_REG_RESET, 0x01)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
delay(RESET_SETTLE_DELAY_MS);
|
||||
|
||||
// MUTE_STREAM is the gate the Linux driver closes between streams; ESPHome has no such
|
||||
// notion, so it stays open and mute is only ever driven through MUTE.
|
||||
if (!this->write_byte(KATANA_REG_FORMAT, this->format_) || !this->write_byte(KATANA_REG_COMMAND, 0x00) ||
|
||||
!this->write_byte(KATANA_REG_DSP_PROGRAM, this->filter_shape_) ||
|
||||
!this->write_byte(KATANA_REG_DEEMPHASIS, this->deemphasis_mode_) ||
|
||||
!this->write_byte(KATANA_REG_DOP, this->dop_enabled_ ? 1 : 0) || !this->write_volume_() ||
|
||||
!this->write_byte(KATANA_REG_MUTE, this->is_muted_ ? 1 : 0) || !this->write_byte(KATANA_REG_MUTE_STREAM, 0x00)) {
|
||||
this->mark_failed(LOG_STR("Failed to configure the control logic"));
|
||||
}
|
||||
}
|
||||
|
||||
void ES9038Q2MKatana::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ES9038Q2M Katana:\n"
|
||||
" Filter shape: %s\n"
|
||||
" De-emphasis: %s\n"
|
||||
" Format register: 0x%02X\n"
|
||||
" DoP: %s",
|
||||
LOG_STR_ARG(filter_shape_to_str(this->filter_shape_)),
|
||||
LOG_STR_ARG(deemphasis_to_str(this->deemphasis_mode_)), this->format_, ONOFF(this->dop_enabled_));
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed())
|
||||
return;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
for (uint8_t reg = KATANA_REG_CHIP_ID; reg <= KATANA_REG_MUTE_STREAM; reg++) {
|
||||
uint8_t value = 0;
|
||||
if (this->read_byte(reg, &value)) {
|
||||
ESP_LOGV(TAG, " Register 0x%02X: 0x%02X", reg, value);
|
||||
} else {
|
||||
ESP_LOGV(TAG, " Register 0x%02X: read failed", reg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Both channel registers follow ESPHome's single master volume.
|
||||
bool ES9038Q2MKatana::write_volume_() {
|
||||
return this->write_byte(KATANA_REG_VOLUME_1, this->volume_reg_) &&
|
||||
this->write_byte(KATANA_REG_VOLUME_2, this->volume_reg_);
|
||||
}
|
||||
|
||||
// Records the outcome of a runtime write; the cached state only changes on success.
|
||||
bool ES9038Q2MKatana::check_write_(bool ok) {
|
||||
if (ok) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ES9038Q2MKatana::set_volume(float volume) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
volume = clamp(volume, 0.0f, 1.0f);
|
||||
const uint8_t previous = this->volume_reg_;
|
||||
float attenuation = KATANA_VOLUME_FULL_ATTENUATION;
|
||||
if (volume >= KATANA_VOLUME_MIDPOINT) {
|
||||
attenuation = KATANA_VOLUME_MIDPOINT_STEPS * (1.0f - volume) / (1.0f - KATANA_VOLUME_MIDPOINT);
|
||||
} else if (volume > 0.0f) {
|
||||
attenuation = KATANA_VOLUME_MIDPOINT_STEPS - KATANA_VOLUME_CUBIC_SCALE * log10f(volume / KATANA_VOLUME_MIDPOINT);
|
||||
}
|
||||
this->volume_reg_ =
|
||||
static_cast<uint8_t>(lroundf(clamp(attenuation, 0.0f, static_cast<float>(KATANA_VOLUME_FULL_ATTENUATION))));
|
||||
if (this->check_write_(this->write_volume_()))
|
||||
return true;
|
||||
this->volume_reg_ = previous;
|
||||
return false;
|
||||
}
|
||||
|
||||
float ES9038Q2MKatana::volume() {
|
||||
if (this->volume_reg_ == KATANA_VOLUME_FULL_ATTENUATION)
|
||||
return 0.0f;
|
||||
if (this->volume_reg_ <= KATANA_VOLUME_MIDPOINT_STEPS)
|
||||
return 1.0f - this->volume_reg_ * (1.0f - KATANA_VOLUME_MIDPOINT) / KATANA_VOLUME_MIDPOINT_STEPS;
|
||||
return KATANA_VOLUME_MIDPOINT *
|
||||
powf(10.0f, (KATANA_VOLUME_MIDPOINT_STEPS - this->volume_reg_) / KATANA_VOLUME_CUBIC_SCALE);
|
||||
}
|
||||
|
||||
bool ES9038Q2MKatana::set_mute_state_(bool mute_state) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
if (!this->check_write_(this->write_byte(KATANA_REG_MUTE, mute_state ? 1 : 0)))
|
||||
return false;
|
||||
this->is_muted_ = mute_state;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::es9038q2m_katana
|
||||
@@ -1,62 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/audio_dac/audio_dac.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::es9038q2m_katana {
|
||||
|
||||
// DSP program numbers of the Katana control interface; there is no program 5.
|
||||
enum FilterShape : uint8_t {
|
||||
FILTER_SHAPE_LINEAR_PHASE_FAST = 0,
|
||||
FILTER_SHAPE_LINEAR_PHASE_SLOW = 1,
|
||||
FILTER_SHAPE_MIN_PHASE_FAST = 2,
|
||||
FILTER_SHAPE_MIN_PHASE_SLOW = 3,
|
||||
FILTER_SHAPE_APODIZING = 4,
|
||||
FILTER_SHAPE_HYBRID = 6,
|
||||
FILTER_SHAPE_BRICK_WALL = 7,
|
||||
};
|
||||
|
||||
enum DeemphasisMode : uint8_t {
|
||||
DEEMPHASIS_BYPASS = 0,
|
||||
DEEMPHASIS_32KHZ = 1,
|
||||
DEEMPHASIS_44_1KHZ = 2,
|
||||
DEEMPHASIS_48KHZ = 3,
|
||||
};
|
||||
|
||||
// Boards that expose an ES9038Q2M through the Allo Katana control logic at I2C address 0x30.
|
||||
// Ported from the Linux driver sound/soc/bcm/allo-katana-codec.c; the DAC's own registers
|
||||
// are never written, the control logic applies the configuration itself.
|
||||
class ES9038Q2MKatana : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
// Cubic below 50%, then 0.5 dB per 1% above, meeting at -25 dB.
|
||||
// Zero selects full attenuation (-127.5 dB).
|
||||
bool set_volume(float volume) override;
|
||||
float volume() override;
|
||||
bool set_mute_off() override { return this->set_mute_state_(false); }
|
||||
bool set_mute_on() override { return this->set_mute_state_(true); }
|
||||
bool is_muted() override { return this->is_muted_; }
|
||||
|
||||
void set_filter_shape(FilterShape shape) { this->filter_shape_ = shape; }
|
||||
void set_format(uint8_t format) { this->format_ = format; }
|
||||
void set_deemphasis_mode(DeemphasisMode mode) { this->deemphasis_mode_ = mode; }
|
||||
void set_dop_enabled(bool enabled) { this->dop_enabled_ = enabled; }
|
||||
|
||||
protected:
|
||||
bool set_mute_state_(bool mute_state);
|
||||
bool write_volume_();
|
||||
bool check_write_(bool ok);
|
||||
|
||||
FilterShape filter_shape_{FILTER_SHAPE_APODIZING};
|
||||
DeemphasisMode deemphasis_mode_{DEEMPHASIS_BYPASS};
|
||||
uint8_t format_{0};
|
||||
// Attenuation in 0.5 dB steps: 0 is loudest, 255 quietest. Starts 20 dB down.
|
||||
uint8_t volume_reg_{0x28};
|
||||
bool dop_enabled_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::es9038q2m_katana
|
||||
@@ -243,7 +243,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
|
||||
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
|
||||
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
||||
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, hdmi_cec, opentherm, Arduino BLE libs
|
||||
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs
|
||||
"esp_driver_i2c", # I2C driver - re-included by i2c; esp32-camera pulls it back itself
|
||||
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
|
||||
"esp_driver_ledc", # LEDC PWM driver - re-included by ledc; esp32-camera pulls it back itself
|
||||
@@ -2920,7 +2920,6 @@ async def _finalize_arduino_aware_flags():
|
||||
if not arduino_linked:
|
||||
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
|
||||
cg.add_define("USE_ESP32_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
elif not CORE.using_arduino:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-c
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG);
|
||||
ESPHOME_LOG_TAG(TAG, "esp32.crash");
|
||||
|
||||
// RAM copy of the build timestamp. The generated constant lives in flash,
|
||||
// which the panic handler must not read (cache may be disabled during
|
||||
@@ -175,8 +175,7 @@ static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME)
|
||||
|
||||
// Validate the NOINIT record. Runs on every has_data() call; re-running is
|
||||
// harmless and the magic is left alone so the record survives an OTA
|
||||
// rollback reboot, crash_handler_clear() drops it once a log client has it
|
||||
// (logger::log_crash_to_client(), for the API and MQTT).
|
||||
// rollback reboot, crash_handler_clear() drops it once an API client has it.
|
||||
static void read_crash_data() {
|
||||
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
|
||||
s_crash_data_valid = true;
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
/// The tag the crash record is logged under, so a client can check the level in force for it
|
||||
inline constexpr const char *CRASH_LOG_TAG = "esp32.crash";
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
|
||||
@@ -131,7 +131,6 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
void ESP32Preferences::open() {
|
||||
// Runs from app_main() before the logger is initialized; any logging here
|
||||
// must be deferred. See s_open_err and make_preference() below.
|
||||
// Components that need NVS check nvs_handle instead of initializing it again.
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
#include <esp_bt.h>
|
||||
@@ -29,6 +28,7 @@ extern "C" {
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
|
||||
@@ -77,9 +77,8 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
|
||||
|
||||
void ESP32BLE::setup() {
|
||||
global_ble = this;
|
||||
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
|
||||
if (global_preferences->nvs_handle == 0) {
|
||||
ESP_LOGE(TAG, "NVS unavailable");
|
||||
if (!ble_pre_setup_()) {
|
||||
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -186,6 +185,15 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ESP32BLE::ble_pre_setup_() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_init_() {
|
||||
if (this->advertising_ != nullptr)
|
||||
|
||||
@@ -185,6 +185,7 @@ class ESP32BLE final : public Component {
|
||||
}
|
||||
this->ble_events_.get_and_reset_dropped_count();
|
||||
}
|
||||
bool ble_pre_setup_();
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void advertising_init_();
|
||||
#endif
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
@@ -300,7 +300,7 @@ void BLEClientBase::log_warning_(const char *message) {
|
||||
|
||||
esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout, const char *param_type) {
|
||||
esp_ble_conn_update_params_t conn_params = {};
|
||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||
conn_params.min_int = min_interval;
|
||||
conn_params.max_int = max_interval;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <nvs_flash.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <esp_bt_main.h>
|
||||
#ifndef CONFIG_BT_CONTROLLER_DISABLED
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <freertos/task.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_OTA
|
||||
|
||||
@@ -6,8 +6,6 @@ import esphome.codegen as cg
|
||||
from esphome.components import canbus
|
||||
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
|
||||
from esphome.components.esp32 import (
|
||||
CONF_MINIMUM_CHIP_REVISION,
|
||||
KEY_ESP32,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
@@ -22,8 +20,6 @@ from esphome.components.esp32 import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_RX_PIN,
|
||||
@@ -31,7 +27,6 @@ from esphome.const import (
|
||||
CONF_TX_PIN,
|
||||
CONF_TX_QUEUE_LEN,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@Sympatron"]
|
||||
@@ -47,7 +42,6 @@ CanMode = esp32_can_ns.enum("CanMode")
|
||||
CAN_MODES = {
|
||||
"NORMAL": CanMode.CAN_MODE_NORMAL,
|
||||
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
|
||||
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
|
||||
}
|
||||
|
||||
# Currently the driver only supports a subset of the bit rates defined in canbus
|
||||
@@ -55,7 +49,6 @@ CAN_MODES = {
|
||||
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
|
||||
|
||||
CAN_SPEEDS_ESP32 = {
|
||||
"20KBPS": CanSpeed.CAN_20KBPS,
|
||||
"25KBPS": CanSpeed.CAN_25KBPS,
|
||||
"50KBPS": CanSpeed.CAN_50KBPS,
|
||||
"100KBPS": CanSpeed.CAN_100KBPS,
|
||||
@@ -121,22 +114,6 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if get_esp32_variant() == VARIANT_ESP32 and config[CONF_BIT_RATE] == "20KBPS":
|
||||
esp32_config = fv.full_config.get()[KEY_ESP32]
|
||||
min_revision = esp32_config[CONF_FRAMEWORK][CONF_ADVANCED].get(
|
||||
CONF_MINIMUM_CHIP_REVISION
|
||||
)
|
||||
if min_revision is None or min_revision < "2.0":
|
||||
raise cv.Invalid(
|
||||
"20KBPS on ESP32 requires esp32.framework.advanced.minimum_chip_revision: '2.0' or higher",
|
||||
path=[CONF_BIT_RATE],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def get_default_tx_enqueue_timeout(bit_rate: str) -> int:
|
||||
bit_rate_numeric = canbus.get_rate(bit_rate)
|
||||
bits_per_packet = 140 # ~max CAN message length
|
||||
|
||||
@@ -33,10 +33,6 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
|
||||
case canbus::CAN_16KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS();
|
||||
return true;
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || (defined(USE_ESP32_VARIANT_ESP32) && CONFIG_ESP32_REV_MIN_FULL >= 200)
|
||||
case canbus::CAN_20KBPS:
|
||||
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS();
|
||||
return true;
|
||||
@@ -79,13 +75,10 @@ bool ESP32Can::setup_internal() {
|
||||
}
|
||||
|
||||
// Select TWAI mode based on configuration
|
||||
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
|
||||
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
|
||||
|
||||
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
|
||||
twai_mode = TWAI_MODE_LISTEN_ONLY;
|
||||
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
|
||||
} else if (this->mode_ == CAN_MODE_NO_ACK) {
|
||||
twai_mode = TWAI_MODE_NO_ACK;
|
||||
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
|
||||
}
|
||||
|
||||
twai_general_config_t g_config =
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace esphome::esp32_can {
|
||||
enum CanMode : uint8_t {
|
||||
CAN_MODE_NORMAL = 0,
|
||||
CAN_MODE_LISTEN_ONLY = 1,
|
||||
CAN_MODE_NO_ACK = 2,
|
||||
};
|
||||
|
||||
class ESP32Can final : public canbus::Canbus {
|
||||
|
||||
@@ -42,7 +42,6 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include "esp_now_hosted_rpc.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -53,8 +52,6 @@ ESPHOME_LOG_TAG(TAG, "esp_now_hosted");
|
||||
// single esp-hosted RPC RX thread. Serializing requests keeps the shared
|
||||
// response slot race-free; a sequence number stops a late/stale response from
|
||||
// being mistaken for ours.
|
||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables): the shim is
|
||||
// C-ABI state shared between the main loop and the esp-hosted RX thread.
|
||||
SemaphoreHandle_t g_req_mutex = nullptr;
|
||||
SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands
|
||||
bool g_setup_done = false; // set only after setup fully succeeds
|
||||
@@ -88,7 +85,6 @@ constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20;
|
||||
uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6];
|
||||
size_t g_peer_count = 0;
|
||||
portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// Caller must hold g_peer_lock.
|
||||
int peer_cache_find_locked(const uint8_t *mac) {
|
||||
@@ -235,14 +231,12 @@ esp_err_t ensure_setup() {
|
||||
g_resp_sem = xSemaphoreCreateBinary();
|
||||
if (g_req_mutex == nullptr || g_resp_sem == nullptr)
|
||||
return ESP_ERR_NO_MEM;
|
||||
esp_err_t err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr);
|
||||
if (err != ESP_OK)
|
||||
esp_err_t err;
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK)
|
||||
return err;
|
||||
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr);
|
||||
if (err != ESP_OK)
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK)
|
||||
return err;
|
||||
err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr);
|
||||
if (err != ESP_OK)
|
||||
if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK)
|
||||
return err;
|
||||
g_setup_done = true;
|
||||
return ESP_OK;
|
||||
@@ -428,11 +422,10 @@ esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len
|
||||
uint8_t hdr[sizeof(esp_now_hosted_send_req_t)];
|
||||
auto *s = reinterpret_cast<esp_now_hosted_send_req_t *>(hdr);
|
||||
s->has_addr = peer_addr != nullptr ? 1 : 0;
|
||||
if (peer_addr != nullptr) {
|
||||
if (peer_addr != nullptr)
|
||||
memcpy(s->peer_addr, peer_addr, 6);
|
||||
} else {
|
||||
else
|
||||
memset(s->peer_addr, 0, 6);
|
||||
}
|
||||
s->data_len = static_cast<uint16_t>(len);
|
||||
// Fire-and-forget (wait=false): native esp_now_send returns once the frame is
|
||||
// queued, with the real TX result delivered later through the send callback.
|
||||
|
||||
@@ -53,14 +53,10 @@ from .const import (
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
KEY_WAVEFORM_REQUIRED,
|
||||
PREFERENCES_RTC_EBOOT_WORDS,
|
||||
PREFERENCES_RTC_NORMAL_WORDS,
|
||||
THROW_STUBS_HEADER,
|
||||
check_preferences_fit,
|
||||
enable_serial,
|
||||
enable_serial1,
|
||||
esp8266_ns,
|
||||
preferences_flash_words,
|
||||
)
|
||||
from .gpio import PinInitialState, add_pin_initial_states_array
|
||||
|
||||
@@ -420,7 +416,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
cg.add_define("USE_ESP8266_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
|
||||
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
|
||||
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
|
||||
@@ -472,12 +467,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Other projects like Tasmota & ESPEasy also use this
|
||||
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
|
||||
|
||||
cg.add_define("ESP8266_PREFERENCES_RTC_NORMAL_WORDS", PREFERENCES_RTC_NORMAL_WORDS)
|
||||
cg.add_define("ESP8266_PREFERENCES_RTC_EBOOT_WORDS", PREFERENCES_RTC_EBOOT_WORDS)
|
||||
cg.add_define(
|
||||
"ESP8266_PREFERENCES_FLASH_WORDS",
|
||||
preferences_flash_words(config[CONF_RESTORE_FROM_FLASH]),
|
||||
)
|
||||
if config[CONF_RESTORE_FROM_FLASH]:
|
||||
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
|
||||
|
||||
@@ -539,7 +528,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
CORE.add_job(add_pin_initial_states_array)
|
||||
CORE.add_job(finalize_waveform_config)
|
||||
CORE.add_job(finalize_serial_config)
|
||||
CORE.add_job(check_preferences_fit)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
|
||||
# Re-exported from the shared definition; here it indexes the BOARDS
|
||||
# metadata dicts, whose entries in boards.py spell the literal.
|
||||
from esphome.const import KEY_FLASH_SIZE # noqa: F401 # pylint: disable=unused-import
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from esphome.core import CORE
|
||||
|
||||
KEY_ESP8266 = "esp8266"
|
||||
KEY_BOARD = "board"
|
||||
@@ -28,87 +24,6 @@ KEY_LDSCRIPT = "ldscript"
|
||||
# Crash-decode tools, resolved once per run by __init__
|
||||
KEY_DECODE_TOOLS = "decode_tools"
|
||||
|
||||
# Saved-settings space in 4-byte words; preferences.cpp gets these as defines. A value cannot span the two
|
||||
# RTC regions, and the flash store is smaller when only explicit flash preferences use it.
|
||||
PREFERENCES_RTC_NORMAL_WORDS = 78
|
||||
PREFERENCES_RTC_EBOOT_WORDS = 32
|
||||
PREFERENCES_RTC_WORDS = PREFERENCES_RTC_NORMAL_WORDS + PREFERENCES_RTC_EBOOT_WORDS
|
||||
PREFERENCES_FLASH_WORDS = 128
|
||||
PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE = 64
|
||||
KEY_PREFERENCES = "preferences"
|
||||
|
||||
|
||||
def preferences_flash_words(restore_from_flash: bool) -> int:
|
||||
"""Size of the flash store for this configuration."""
|
||||
return (
|
||||
PREFERENCES_FLASH_WORDS
|
||||
if restore_from_flash
|
||||
else PREFERENCES_FLASH_WORDS_WITHOUT_RESTORE
|
||||
)
|
||||
|
||||
|
||||
def preference_words(value_bytes: int) -> int:
|
||||
"""Words a saved value takes, checksum word included (rtc_pref_bytes_to_words plus one)."""
|
||||
return (value_bytes + 3) // 4 + 1
|
||||
|
||||
|
||||
def reserve_preferences(
|
||||
count: int, value_bytes: int, in_flash: bool | None = None
|
||||
) -> None:
|
||||
"""Record saved values a component creates; check_preferences_fit() adds them all up.
|
||||
|
||||
in_flash is the value's own storage, as passed to make_preference(); None is the two-argument
|
||||
form, which follows restore_from_flash.
|
||||
"""
|
||||
if in_flash is None:
|
||||
in_flash = CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
|
||||
reservations = CORE.data.setdefault(KEY_ESP8266, {}).setdefault(KEY_PREFERENCES, [])
|
||||
reservations.extend([(preference_words(value_bytes), in_flash)] * count)
|
||||
|
||||
|
||||
def _rtc_values_can_split(values: list[int]) -> bool:
|
||||
"""Whether some order fills the two RTC regions without a value spanning them."""
|
||||
total = sum(values)
|
||||
if total <= PREFERENCES_RTC_NORMAL_WORDS:
|
||||
return True
|
||||
# Bit i of reachable is set when some subset of the values sums to i
|
||||
reachable = 1
|
||||
for words in values:
|
||||
reachable |= reachable << words
|
||||
lowest = max(0, total - PREFERENCES_RTC_EBOOT_WORDS)
|
||||
highest = min(total, PREFERENCES_RTC_NORMAL_WORDS)
|
||||
return any(reachable >> normal & 1 for normal in range(lowest, highest + 1))
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def check_preferences_fit() -> None:
|
||||
"""Fail the build when the reserved saved values cannot fit the space preferences.cpp has."""
|
||||
flash_store = preferences_flash_words(
|
||||
CORE.config[KEY_ESP8266][CONF_RESTORE_FROM_FLASH]
|
||||
)
|
||||
reservations = CORE.data[KEY_ESP8266].get(KEY_PREFERENCES, [])
|
||||
flash_words = sum(w for w, in_flash in reservations if in_flash)
|
||||
rtc_values = [w for w, in_flash in reservations if not in_flash]
|
||||
if flash_words > flash_store:
|
||||
raise EsphomeError(
|
||||
f"Saved values need {flash_words} words but the ESP8266 flash store has {flash_store}; "
|
||||
"use fewer or smaller saved values"
|
||||
)
|
||||
if not _rtc_values_can_split(rtc_values):
|
||||
raise EsphomeError(
|
||||
f"Saved values need {sum(rtc_values)} words but ESP8266 RTC memory has {PREFERENCES_RTC_WORDS} in "
|
||||
f"regions of {PREFERENCES_RTC_NORMAL_WORDS} and {PREFERENCES_RTC_EBOOT_WORDS} that a value cannot "
|
||||
"span; use fewer or smaller saved values, or store them in flash"
|
||||
)
|
||||
if sum(rtc_values) > PREFERENCES_RTC_NORMAL_WORDS:
|
||||
_LOGGER.warning(
|
||||
"Saved values reserve %d of %d words of ESP8266 RTC memory; the fit depends on setup order, and "
|
||||
"saved settings that do not reserve are not counted",
|
||||
sum(rtc_values),
|
||||
PREFERENCES_RTC_WORDS,
|
||||
)
|
||||
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
#include "crash_handler.h"
|
||||
#include "rtc_layout.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
@@ -47,8 +46,11 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
|
||||
// Recover the actual code address from a windowed-ABI return address on the stack.
|
||||
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
|
||||
|
||||
// Crash data lives in the last esphome::esp8266::RTC_CRASH_WORDS of user RTC memory (rtc_layout.h).
|
||||
// RTC user memory layout for crash backtrace data.
|
||||
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
|
||||
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
|
||||
// Store 16 raw candidates, filter to real return addresses at log time.
|
||||
static constexpr uint8_t RTC_CRASH_BASE = 174;
|
||||
static constexpr size_t MAX_BACKTRACE = 16;
|
||||
|
||||
// Magic word packs sentinel, version, and count into one uint32_t:
|
||||
@@ -76,7 +78,7 @@ struct RtcCrashData {
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint32_t epc1; // Fault PC, used to filter duplicates
|
||||
};
|
||||
static_assert(sizeof(RtcCrashData) == esphome::esp8266::RTC_CRASH_WORDS * 4, "RtcCrashData must fill its RTC words");
|
||||
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
@@ -127,7 +129,7 @@ static const LogString *get_reset_reason(uint32_t reason) {
|
||||
// Returns the number of valid backtrace entries (0 if no data found).
|
||||
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
|
||||
RtcCrashData rtc_data;
|
||||
if (!system_rtc_mem_read(esphome::esp8266::RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
|
||||
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
|
||||
return 0;
|
||||
uint32_t magic = rtc_data.magic;
|
||||
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
|
||||
@@ -231,7 +233,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3
|
||||
data.epc1 = epc1;
|
||||
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
|
||||
|
||||
system_rtc_mem_write(esphome::esp8266::RTC_CRASH_BASE, &data, sizeof(data));
|
||||
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
|
||||
}
|
||||
|
||||
#endif // USE_ESP8266_CRASH_HANDLER
|
||||
|
||||
@@ -6,7 +6,6 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include "preferences.h"
|
||||
#include "rtc_layout.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences_rtc.h"
|
||||
@@ -18,26 +17,31 @@ namespace esphome::esp8266 {
|
||||
ESPHOME_LOG_TAG(TAG, "preferences");
|
||||
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = RTC_USER_MEM_WORDS;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
|
||||
|
||||
// RTC memory layout (sizes come from const.py through codegen):
|
||||
// RTC memory layout:
|
||||
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
|
||||
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
|
||||
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
|
||||
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = ESP8266_PREFERENCES_RTC_EBOOT_WORDS;
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = ESP8266_PREFERENCES_RTC_NORMAL_WORDS;
|
||||
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
|
||||
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
|
||||
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
|
||||
static_assert(PREF_TOTAL_WORDS <= RTC_CRASH_BASE - RTC_SYSTEM_USER_BASE,
|
||||
"preference regions overlap the crash handler's RTC words");
|
||||
|
||||
// Maximum preference size in words (limited by uint8_t length_words field)
|
||||
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
||||
|
||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||
|
||||
// Smaller without restore_from_flash, when only explicit flash preferences live here
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = ESP8266_PREFERENCES_FLASH_WORDS;
|
||||
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
|
||||
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
|
||||
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
|
||||
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
|
||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||
#else
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
|
||||
#endif
|
||||
|
||||
static uint32_t
|
||||
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -56,7 +60,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
|
||||
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
|
||||
return false;
|
||||
}
|
||||
if (index < RTC_EBOOT_REGION_WORDS && s_prevent_write) {
|
||||
if (index < 32 && s_prevent_write) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -119,7 +123,7 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
||||
|
||||
// Maximum buffer for any single preference - bounded by storage sizes.
|
||||
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (78) - a single pref can't span both RTC regions.
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
|
||||
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
@@ -176,8 +180,8 @@ ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t
|
||||
} else {
|
||||
uint32_t start = this->current_offset;
|
||||
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
|
||||
// Normal: offset 0-77 maps to RTC offset 32-109
|
||||
// Eboot: offset 78-109 maps to RTC offset 0-31
|
||||
// Normal: offset 0-95 maps to RTC offset 32-127
|
||||
// Eboot: offset 96-127 maps to RTC offset 0-31
|
||||
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
|
||||
// start is in normal but end is not -> switch to Eboot
|
||||
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
// User RTC memory is system blocks 64-191, one 4-byte word each. preferences.cpp owns the first
|
||||
// words, crash_handler.cpp the last; const.py hands preferences.cpp its region sizes.
|
||||
static constexpr uint32_t RTC_SYSTEM_USER_BASE = 64;
|
||||
static constexpr uint32_t RTC_USER_MEM_WORDS = 128;
|
||||
static constexpr uint32_t RTC_CRASH_WORDS = 18;
|
||||
static constexpr uint32_t RTC_CRASH_BASE = RTC_SYSTEM_USER_BASE + RTC_USER_MEM_WORDS - RTC_CRASH_WORDS;
|
||||
|
||||
} // namespace esphome::esp8266
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -44,14 +44,11 @@ const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
|
||||
}
|
||||
#endif
|
||||
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 60000; // milliseconds for initial handshake
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
|
||||
// Milliseconds for data transfer. Covers the lwIP retransmit run seen in
|
||||
// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits
|
||||
// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000;
|
||||
// A handshake this old gives way to a waiting connection: a client retrying over a lossy link otherwise queues
|
||||
// behind its own stalled attempt until both time out
|
||||
static constexpr uint32_t OTA_HANDSHAKE_PREEMPT = 10000;
|
||||
static constexpr uint32_t OTA_PROGRESS_INTERVAL_MS = 1000;
|
||||
static constexpr size_t OTA_SIZE_FIELD_BYTES = 4; // sizes on the wire are 4 bytes MSB first
|
||||
|
||||
@@ -202,9 +199,6 @@ static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_DEFLATE = 0x10;
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_UDP = 0x20;
|
||||
#endif
|
||||
// Noise needs the extended protocol: the prologue binds the 2-byte feature ack
|
||||
static constexpr uint8_t CLIENT_NOISE_FEATURES =
|
||||
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
|
||||
@@ -214,10 +208,6 @@ static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
|
||||
// Raw deflate, window <= OTA_INFLATE_WINDOW_SIZE. Binding once offered: the
|
||||
// client must then send the image size frame and a deflate stream.
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_DEFLATE = 0x08;
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_UDP = 0x10;
|
||||
static constexpr uint8_t UDP_REQUEST_V1 = 0x01; // espota2.UDP_REQUEST
|
||||
#endif
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
inline bool ESPHomeOTAComponent::noise_offered_() const {
|
||||
@@ -276,12 +266,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
this->cleanup_connection_();
|
||||
return;
|
||||
}
|
||||
// Only unauthenticated handshakes reach here; the data phase blocks inside handle_data_
|
||||
if (now - this->client_connect_time_ > OTA_HANDSHAKE_PREEMPT && this->server_->ready()) {
|
||||
ESP_LOGD(TAG, "Newer connection waiting");
|
||||
this->cleanup_connection_();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->ota_state_) {
|
||||
case OTAState::MAGIC_READ: {
|
||||
@@ -345,9 +329,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
|
||||
this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
||||
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_UDP;
|
||||
#endif
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
|
||||
#endif
|
||||
@@ -502,10 +483,6 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
char *sbuf = reinterpret_cast<char *>(buf);
|
||||
size_t image_size;
|
||||
ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP;
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
UdpLink udp_link;
|
||||
this->udp_ = nullptr; // client_connect_time_ is done with
|
||||
#endif
|
||||
|
||||
// Set socket timeouts and blocking mode (see strategy table above)
|
||||
struct timeval tv;
|
||||
@@ -521,38 +498,6 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// Acknowledge auth OK - 1 byte
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
// The server always offers UDP on the extended protocol
|
||||
if (this->extended_proto_() && (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_UDP) != 0) {
|
||||
// Whether the client wants UDP (0: its link looks good), answered with our token
|
||||
if (!this->data_readall_(buf, 1)) {
|
||||
if (this->client_left_before_start_())
|
||||
return;
|
||||
this->log_read_error_(LOG_STR("UDP request"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
// 1 is this UDP format; other values are free for later ones, which we decline (a zero reply keeps TCP)
|
||||
if (buf[0] == UDP_REQUEST_V1) {
|
||||
this->udp_open_(udp_link);
|
||||
if (udp_link.reply[0] == 0) {
|
||||
ESP_LOGW(TAG, "UDP unavailable, using TCP");
|
||||
}
|
||||
}
|
||||
if (!this->data_write_(udp_link.reply, sizeof(udp_link.reply)))
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
if (udp_link.reply[0] != 0) {
|
||||
this->udp_ = &udp_link;
|
||||
if (!this->udp_start_()) {
|
||||
// Nothing started yet: just drop the session, which the client retries as a network failure
|
||||
this->log_read_error_(LOG_STR("UDP start"));
|
||||
this->cleanup_connection_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
if (this->extended_proto_()) {
|
||||
// Read ota type, 1 byte
|
||||
if (!this->data_readall_(buf, 1)) {
|
||||
@@ -653,17 +598,11 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// Acknowledge Update end OK - 1 byte
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
// UDP never reports a close: wait only until the client has our responses, not for its final ack
|
||||
if (this->udp_ != nullptr) {
|
||||
this->udp_linger_();
|
||||
} else
|
||||
#endif
|
||||
// Read ACK
|
||||
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||
this->log_read_error_(LOG_STR("ack"));
|
||||
// do not go to error, this is not fatal
|
||||
}
|
||||
// Read ACK
|
||||
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||
this->log_read_error_(LOG_STR("ack"));
|
||||
// do not go to error, this is not fatal
|
||||
}
|
||||
|
||||
this->cleanup_connection_();
|
||||
delay(10);
|
||||
@@ -684,9 +623,6 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
|
||||
error:
|
||||
this->data_write_byte_(static_cast<uint8_t>(error_code));
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
this->udp_linger_();
|
||||
#endif
|
||||
|
||||
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
|
||||
// Always call abort() unconditionally: backends register external partitions before
|
||||
@@ -715,7 +651,7 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t read = this->transport_read_(buf + at, len - at);
|
||||
ssize_t read = this->client_->read(buf + at, len - at);
|
||||
if (read == -1) {
|
||||
const int err = errno;
|
||||
if (!this->would_block_(err)) {
|
||||
@@ -740,10 +676,6 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
|
||||
return true;
|
||||
}
|
||||
bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
if (this->udp_ != nullptr)
|
||||
return this->udp_send_(buf, len);
|
||||
#endif
|
||||
uint32_t start = millis();
|
||||
uint32_t at = 0;
|
||||
while (len - at > 0) {
|
||||
@@ -796,20 +728,7 @@ void ESPHomeOTAComponent::log_read_error_(const LogString *what) {
|
||||
|
||||
void ESPHomeOTAComponent::log_start_(const LogString *phase) {
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
if (this->client_ == nullptr) {
|
||||
// A committed UDP session has already dropped TCP
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
ipaddr_ntoa_r(&this->udp_->peer_ip, peername, sizeof(peername));
|
||||
#else
|
||||
socket::format_sockaddr_to(reinterpret_cast<const struct sockaddr *>(&this->udp_->peer), this->udp_->peer_len,
|
||||
peername);
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->client_->getpeername_to(peername);
|
||||
}
|
||||
this->client_->getpeername_to(peername);
|
||||
ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername);
|
||||
}
|
||||
|
||||
@@ -921,7 +840,7 @@ ssize_t ESPHomeOTAComponent::receive_data_(uint8_t *buf, DataTransfer &xfer) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
read = this->transport_read_(buf, requested);
|
||||
read = this->client_->read(buf, requested);
|
||||
if (read > 0)
|
||||
break;
|
||||
if (read == 0) {
|
||||
@@ -955,11 +874,6 @@ ssize_t ESPHomeOTAComponent::receive_data_(uint8_t *buf, DataTransfer &xfer) {
|
||||
|
||||
void ESPHomeOTAComponent::send_chunk_acks_(DataTransfer &xfer) {
|
||||
#if USE_OTA_VERSION == 2
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
// UDP acks every message itself
|
||||
if (this->udp_ != nullptr)
|
||||
return;
|
||||
#endif
|
||||
while (xfer.acknowledged + OTA_BLOCK_SIZE <= xfer.total ||
|
||||
(xfer.total == xfer.ota_size && xfer.acknowledged < xfer.ota_size)) {
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||
@@ -969,12 +883,7 @@ void ESPHomeOTAComponent::send_chunk_acks_(DataTransfer &xfer) {
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::cleanup_connection_() {
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
this->udp_ = nullptr; // the link itself lives on handle_data_'s stack
|
||||
#endif
|
||||
// Already gone once a UDP session committed
|
||||
if (this->client_ != nullptr)
|
||||
this->client_->close();
|
||||
this->client_->close();
|
||||
this->client_ = nullptr;
|
||||
this->client_connect_time_ = 0;
|
||||
this->handshake_buf_pos_ = 0;
|
||||
|
||||
@@ -15,25 +15,6 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/hash_base.h"
|
||||
|
||||
// UDP data phase: raw lwIP needs a main loop lock (or a single thread); the host polls a socket instead
|
||||
#ifdef USE_ESP32
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
#if defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || \
|
||||
(defined(USE_ESP32) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING))
|
||||
#define ESPHOME_OTA_UDP
|
||||
#define ESPHOME_OTA_UDP_LWIP
|
||||
#include "lwip/ip_addr.h"
|
||||
#include "lwip/pbuf.h"
|
||||
#include "lwip/udp.h"
|
||||
#if defined(USE_LIBRETINY) && !LWIP_TCPIP_CORE_LOCKING
|
||||
#undef ESPHOME_OTA_UDP
|
||||
#undef ESPHOME_OTA_UDP_LWIP
|
||||
#endif
|
||||
#elif defined(USE_HOST)
|
||||
#define ESPHOME_OTA_UDP
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
|
||||
@@ -84,69 +65,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
protected:
|
||||
void handle_handshake_();
|
||||
void handle_data_();
|
||||
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
// UDP transport for the data phase; wire format in ota_esphome_udp.cpp
|
||||
static constexpr size_t UDP_OUT_SIZE = 64; // our unacked responses: at most three encrypted ones
|
||||
// Messages the device keeps unread: like TCP's 4 segment window, so no more memory than TCP holds
|
||||
static constexpr uint8_t UDP_WINDOW = 4;
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
using UdpSlot = struct pbuf *;
|
||||
#else
|
||||
using UdpSlot = struct OtaUdpDatagram *;
|
||||
#endif
|
||||
// Lives on handle_data_'s stack; the receive callback shares it under the lwIP lock
|
||||
struct UdpLink {
|
||||
~UdpLink();
|
||||
void release(); // closes the socket and frees what is queued
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
struct udp_pcb *pcb{nullptr};
|
||||
ip_addr_t peer_ip;
|
||||
#else
|
||||
std::unique_ptr<socket::Socket> sock;
|
||||
struct sockaddr_storage peer; // the TCP peer; its port is replaced by peer_port
|
||||
socklen_t peer_len{0};
|
||||
#endif
|
||||
uint16_t peer_port{0}; // learned from the client's datagrams
|
||||
// The session token, our answer to the client: top bit clear, so never an error code; all zero declines
|
||||
uint8_t reply[4]{};
|
||||
UdpSlot queue[UDP_WINDOW]{}; // slot seq % UDP_WINDOW for seq in [consumed, consumed + UDP_WINDOW)
|
||||
uint16_t received{0}; // every seq before it is here
|
||||
uint16_t consumed{0}; // the message being read
|
||||
uint16_t read_offset{0}; // payload bytes of it already read
|
||||
uint16_t out_base{0}; // stream offset of out[0]
|
||||
uint8_t out_len{0};
|
||||
bool committed{false}; // the client heard us over UDP
|
||||
uint8_t out[UDP_OUT_SIZE];
|
||||
};
|
||||
// Fills link.reply on success; leaves it all zero (declined) otherwise
|
||||
void udp_open_(UdpLink &link);
|
||||
// Waits for the client to commit to UDP or fall back to TCP; false on failure
|
||||
bool udp_start_();
|
||||
// Like a blocking socket read with SO_RCVTIMEO: up to len bytes, or -1 with EWOULDBLOCK
|
||||
ssize_t udp_read_(uint8_t *buf, size_t len);
|
||||
bool udp_send_(const uint8_t *data, size_t len);
|
||||
// In a UDP session, keeps answering until the client has our responses (an error code), briefly
|
||||
void udp_linger_();
|
||||
// A datagram from the client's IP; caller holds the lwIP lock (or runs in the receive callback)
|
||||
static void udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port);
|
||||
// Caller holds the lwIP lock (or runs in the receive callback)
|
||||
static void udp_send_ack(UdpLink &link, uint16_t prompted_by);
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
static void udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, uint16_t port);
|
||||
void udp_poll_() {}
|
||||
#else
|
||||
void udp_poll_(); // hands what the socket received to udp_on_datagram
|
||||
#endif
|
||||
#endif
|
||||
// Data phase only (udp_ shares storage with the handshake's connect time): TCP or, once committed, the UDP stream
|
||||
inline ssize_t transport_read_(uint8_t *buf, size_t len) {
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
if (this->udp_ != nullptr)
|
||||
return this->udp_read_(buf, len);
|
||||
#endif
|
||||
return this->client_->read(buf, len);
|
||||
}
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
|
||||
bool handle_auth_send_();
|
||||
@@ -157,6 +75,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
#endif // USE_OTA_PASSWORD
|
||||
bool readall_(uint8_t *buf, size_t len);
|
||||
bool writeall_(const uint8_t *buf, size_t len);
|
||||
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// Heap-allocated only while an encrypted OTA session is active.
|
||||
@@ -186,21 +105,18 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
|
||||
bool noise_readall_(uint8_t *buf, size_t len);
|
||||
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
|
||||
static constexpr size_t NOISE_MAX_RESPONSE = 4; // the UDP token
|
||||
bool noise_write_(const uint8_t *data, size_t len);
|
||||
bool noise_write_byte_(uint8_t byte);
|
||||
#endif // USE_OTA_ENCRYPTION
|
||||
|
||||
// Data-phase I/O dispatch: through the noise transport when a session is
|
||||
// active, straight to the socket otherwise.
|
||||
// len is at most NOISE_MAX_RESPONSE
|
||||
inline bool data_write_(const uint8_t *data, size_t len) {
|
||||
inline bool data_write_byte_(uint8_t byte) {
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ != nullptr)
|
||||
return this->noise_write_(data, len);
|
||||
return this->noise_write_byte_(byte);
|
||||
#endif
|
||||
return this->writeall_(data, len);
|
||||
return this->write_byte_(byte);
|
||||
}
|
||||
inline bool data_write_byte_(uint8_t byte) { return this->data_write_(&byte, 1); }
|
||||
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
|
||||
inline bool data_readall_(uint8_t *buf, size_t len) {
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
@@ -284,21 +200,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
std::unique_ptr<socket::Socket> client_;
|
||||
ota::OTABackendPtr backend_;
|
||||
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
union {
|
||||
uint32_t client_connect_time_{0}; // handshake states
|
||||
UdpLink *udp_; // data phase (never both), points into handle_data_'s stack
|
||||
};
|
||||
static_assert(sizeof(UdpLink *) == sizeof(uint32_t), "clearing client_connect_time_ must clear udp_");
|
||||
#else
|
||||
uint32_t client_connect_time_{0};
|
||||
#ifdef ESPHOME_OTA_UDP
|
||||
UdpLink *udp_{nullptr}; // data phase, points into handle_data_'s stack
|
||||
#endif
|
||||
#endif
|
||||
#if defined(ESPHOME_OTA_UDP) && defined(USE_OTA_ENCRYPTION)
|
||||
static_assert(sizeof(UdpLink::reply) <= NOISE_MAX_RESPONSE, "the token goes out through noise_write_");
|
||||
#endif
|
||||
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
|
||||
// Buffer size for OTA data transfer. The upload client derives its maximum
|
||||
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
|
||||
|
||||
@@ -266,15 +266,13 @@ ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
|
||||
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
|
||||
}
|
||||
|
||||
/// Blocking write of a short response (at most NOISE_MAX_RESPONSE bytes) as one encrypted frame.
|
||||
bool ESPHomeOTAComponent::noise_write_(const uint8_t *data, size_t len) {
|
||||
uint8_t frame[noise::FRAME_HEADER_SIZE + NOISE_MAX_RESPONSE + noise::MAC_SIZE];
|
||||
if (len > NOISE_MAX_RESPONSE)
|
||||
return false;
|
||||
memcpy(frame + noise::FRAME_HEADER_SIZE, data, len);
|
||||
/// Blocking write of one response byte as an encrypted frame.
|
||||
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
|
||||
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
|
||||
frame[noise::FRAME_HEADER_SIZE] = byte;
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, len, len + noise::MAC_SIZE);
|
||||
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
|
||||
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Encrypt: %d", err);
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
#include "ota_esphome.h"
|
||||
#if defined(USE_OTA) && defined(ESPHOME_OTA_UDP)
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
#include "lwip/inet.h"
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "lwip/tcpip.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
/*
|
||||
* UDP data phase: the same byte stream as TCP, cut into numbered datagrams.
|
||||
* client -> device [type | COMMITTED][token u32][seq u16][device bytes received u16][payload]
|
||||
* device -> client [ACK][token u32][received u16][seq that prompted it u16][stream offset u16][window u8][our bytes]
|
||||
* The lwIP callback acks each datagram on arrival, so flash writes never look like loss, and keeps messages in its
|
||||
* UDP_WINDOW slots until the OTA loop reads them. "window": free slots in the high nibble, and in the low nibble bit k
|
||||
* set when received + k is already here (like TCP SACK).
|
||||
*/
|
||||
|
||||
namespace esphome {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "esphome.ota");
|
||||
|
||||
// PING (0x02) and PROBE (0x03) only prompt an ACK
|
||||
static constexpr uint8_t UDP_MSG_DATA = 0x01;
|
||||
static constexpr uint8_t UDP_FLAG_COMMITTED = 0x80;
|
||||
static constexpr uint8_t UDP_MSG_ACK = 0x40;
|
||||
static constexpr size_t UDP_HEADER_SIZE = 9;
|
||||
static constexpr size_t UDP_ACK_HEADER_SIZE = 12;
|
||||
// The client's 60 s wait for the token over a lossy TCP link, plus its 15 s probe
|
||||
static constexpr uint32_t UDP_COMMIT_TIMEOUT = 80000;
|
||||
// Like the TCP socket's SO_RCVTIMEO in handle_data_, so readall_ treats both alike
|
||||
static constexpr uint32_t UDP_READ_WAIT_MS = 2000;
|
||||
// How often the commit wait looks at the UDP link between reads of the TCP fallback byte
|
||||
static constexpr uint32_t UDP_COMMIT_POLL_MS = 100;
|
||||
static constexpr uint32_t UDP_LINGER_MS = 3000;
|
||||
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
#ifdef USE_LIBRETINY
|
||||
// LibreTiny's LwIPLock is a no-op; take the core lock directly (the header drops UDP without one)
|
||||
struct UdpLock {
|
||||
UdpLock() { LOCK_TCPIP_CORE(); }
|
||||
~UdpLock() { UNLOCK_TCPIP_CORE(); }
|
||||
};
|
||||
#else
|
||||
using UdpLock = LwIPLock;
|
||||
#endif
|
||||
static uint16_t slot_len(struct pbuf *p) { return p->tot_len; }
|
||||
static uint16_t slot_copy(struct pbuf *p, void *dst, uint16_t len, uint16_t offset) {
|
||||
return pbuf_copy_partial(p, dst, len, offset);
|
||||
}
|
||||
static void slot_free(struct pbuf *p) { pbuf_free(p); }
|
||||
#else
|
||||
// The host has no lwIP thread: the OTA loop drains the socket itself. Empty bodies, not = default, so clang-tidy
|
||||
// does not flag the guards as unused
|
||||
struct UdpLock {
|
||||
UdpLock() {}
|
||||
~UdpLock() {}
|
||||
};
|
||||
struct OtaUdpDatagram {
|
||||
uint16_t len;
|
||||
uint8_t data[1500];
|
||||
};
|
||||
static uint16_t slot_len(OtaUdpDatagram *p) { return p->len; }
|
||||
static uint16_t slot_copy(OtaUdpDatagram *p, void *dst, uint16_t len, uint16_t offset) {
|
||||
const uint16_t n = std::min<uint16_t>(len, p->len - offset);
|
||||
memcpy(dst, p->data + offset, n);
|
||||
return n;
|
||||
}
|
||||
static void slot_free(OtaUdpDatagram *p) { delete p; }
|
||||
#endif
|
||||
|
||||
static void put_u16(uint8_t *at, uint16_t value) {
|
||||
at[0] = value >> 8;
|
||||
at[1] = value & 0xFF;
|
||||
}
|
||||
|
||||
ESPHomeOTAComponent::UdpLink::~UdpLink() { this->release(); }
|
||||
|
||||
void ESPHomeOTAComponent::UdpLink::release() {
|
||||
UdpLock lock;
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
if (this->pcb != nullptr)
|
||||
udp_remove(this->pcb);
|
||||
this->pcb = nullptr;
|
||||
#else
|
||||
this->sock = nullptr;
|
||||
#endif
|
||||
for (auto &p : this->queue) {
|
||||
if (p != nullptr)
|
||||
slot_free(p);
|
||||
p = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::udp_send_ack(UdpLink &link, uint16_t prompted_by) {
|
||||
const uint16_t len = UDP_ACK_HEADER_SIZE + link.out_len;
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
|
||||
if (p == nullptr)
|
||||
return; // Like a lost ACK: the client asks again
|
||||
auto *ack = static_cast<uint8_t *>(p->payload);
|
||||
#else
|
||||
uint8_t ack[UDP_ACK_HEADER_SIZE + UDP_OUT_SIZE];
|
||||
#endif
|
||||
ack[0] = UDP_MSG_ACK;
|
||||
memcpy(ack + 1, link.reply, 4);
|
||||
put_u16(ack + 5, link.received);
|
||||
put_u16(ack + 7, prompted_by);
|
||||
put_u16(ack + 9, link.out_base);
|
||||
uint8_t window = (UDP_WINDOW - static_cast<uint16_t>(link.received - link.consumed)) << 4;
|
||||
for (uint8_t k = 1; k < UDP_WINDOW; k++) {
|
||||
if (link.queue[(link.received + k) % UDP_WINDOW] != nullptr)
|
||||
window |= 1 << k;
|
||||
}
|
||||
ack[11] = window;
|
||||
memcpy(ack + UDP_ACK_HEADER_SIZE, link.out, link.out_len);
|
||||
// Best effort: a lost ACK is repeated for the client's next datagram
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
udp_sendto(link.pcb, p, &link.peer_ip, link.peer_port);
|
||||
pbuf_free(p);
|
||||
#else
|
||||
struct sockaddr_storage to = link.peer;
|
||||
if (to.ss_family == AF_INET6) {
|
||||
reinterpret_cast<sockaddr_in6 *>(&to)->sin6_port = htons(link.peer_port);
|
||||
} else {
|
||||
reinterpret_cast<sockaddr_in *>(&to)->sin_port = htons(link.peer_port);
|
||||
}
|
||||
link.sock->sendto(ack, len, 0, reinterpret_cast<const struct sockaddr *>(&to), link.peer_len);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port) {
|
||||
uint8_t hdr[UDP_HEADER_SIZE];
|
||||
if (slot_copy(p, hdr, UDP_HEADER_SIZE, 0) != UDP_HEADER_SIZE || memcmp(hdr + 1, link.reply, 4) != 0) {
|
||||
slot_free(p);
|
||||
return;
|
||||
}
|
||||
// The token proves the sender; answer the port it arrived from, which NAT may have rewritten
|
||||
link.peer_port = port;
|
||||
link.committed |= (hdr[0] & UDP_FLAG_COMMITTED) != 0;
|
||||
// Drop the responses the client already has
|
||||
const uint16_t acked = encode_uint16(hdr[7], hdr[8]) - link.out_base;
|
||||
if (acked != 0 && acked <= link.out_len) {
|
||||
link.out_len -= acked;
|
||||
memmove(link.out, link.out + acked, link.out_len);
|
||||
link.out_base += acked;
|
||||
}
|
||||
const uint16_t seq = encode_uint16(hdr[5], hdr[6]);
|
||||
UdpSlot &slot = link.queue[seq % UDP_WINDOW];
|
||||
if ((hdr[0] & ~UDP_FLAG_COMMITTED) == UDP_MSG_DATA && slot_len(p) > UDP_HEADER_SIZE &&
|
||||
static_cast<uint16_t>(seq - link.consumed) < UDP_WINDOW && slot == nullptr) {
|
||||
slot = p;
|
||||
while (static_cast<uint16_t>(link.received - link.consumed) < UDP_WINDOW &&
|
||||
link.queue[link.received % UDP_WINDOW] != nullptr)
|
||||
link.received++;
|
||||
} else {
|
||||
slot_free(p);
|
||||
}
|
||||
udp_send_ack(link, seq);
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
// lwIP context: the tcpip thread (ESP32, LibreTiny), SYS (ESP8266) or an IRQ (RP2040)
|
||||
void ESPHomeOTAComponent::udp_recv_cb(void *arg, struct udp_pcb * /*pcb*/, struct pbuf *p, const ip_addr_t *addr,
|
||||
uint16_t port) {
|
||||
auto &link = *static_cast<UdpLink *>(arg);
|
||||
if (!ip_addr_cmp(addr, &link.peer_ip)) {
|
||||
pbuf_free(p);
|
||||
return;
|
||||
}
|
||||
udp_on_datagram(link, p, port);
|
||||
}
|
||||
#else
|
||||
static bool same_ip(const struct sockaddr_storage &a, const struct sockaddr_storage &b) {
|
||||
uint32_t ipv4_a, ipv4_b;
|
||||
const bool v4_a = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&a), &ipv4_a);
|
||||
const bool v4_b = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&b), &ipv4_b);
|
||||
if (v4_a || v4_b)
|
||||
return v4_a && v4_b && ipv4_a == ipv4_b;
|
||||
return a.ss_family == AF_INET6 && b.ss_family == AF_INET6 &&
|
||||
memcmp(&reinterpret_cast<const sockaddr_in6 *>(&a)->sin6_addr,
|
||||
&reinterpret_cast<const sockaddr_in6 *>(&b)->sin6_addr, sizeof(in6_addr)) == 0;
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::udp_poll_() {
|
||||
UdpLink &link = *this->udp_;
|
||||
for (;;) {
|
||||
uint8_t buf[sizeof(OtaUdpDatagram::data)];
|
||||
struct sockaddr_storage from;
|
||||
socklen_t from_len = sizeof(from);
|
||||
const ssize_t len = link.sock->recvfrom(buf, sizeof(buf), reinterpret_cast<struct sockaddr *>(&from), &from_len);
|
||||
if (len < 0)
|
||||
return;
|
||||
if (!same_ip(from, link.peer))
|
||||
continue;
|
||||
auto *datagram = new OtaUdpDatagram; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
datagram->len = len;
|
||||
memcpy(datagram->data, buf, len);
|
||||
const uint16_t port = from.ss_family == AF_INET6 ? ntohs(reinterpret_cast<sockaddr_in6 *>(&from)->sin6_port)
|
||||
: ntohs(reinterpret_cast<sockaddr_in *>(&from)->sin_port);
|
||||
udp_on_datagram(link, datagram, port);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESPHomeOTAComponent::udp_open_(UdpLink &link) {
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t peer_len = sizeof(peer);
|
||||
if (this->client_->getpeername(reinterpret_cast<struct sockaddr *>(&peer), &peer_len) != 0)
|
||||
return;
|
||||
uint8_t token[4];
|
||||
if (!random_bytes(token, sizeof(token)))
|
||||
return;
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
uint32_t ipv4;
|
||||
if (socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&peer), &ipv4)) {
|
||||
ip_addr_set_ip4_u32(&link.peer_ip, ipv4);
|
||||
} else {
|
||||
#if USE_NETWORK_IPV6 && LWIP_IPV6
|
||||
inet6_addr_to_ip6addr(ip_2_ip6(&link.peer_ip), &reinterpret_cast<const sockaddr_in6 *>(&peer)->sin6_addr);
|
||||
IP_SET_TYPE_VAL(link.peer_ip, IPADDR_TYPE_V6);
|
||||
#else
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
UdpLock lock;
|
||||
#if LWIP_IPV6
|
||||
link.pcb = udp_new_ip_type(IPADDR_TYPE_ANY);
|
||||
#else
|
||||
link.pcb = udp_new();
|
||||
#endif
|
||||
// On failure the reply stays all zero and ~UdpLink removes the pcb
|
||||
if (link.pcb == nullptr || udp_bind(link.pcb, IP_ANY_TYPE, this->port_) != ERR_OK)
|
||||
return;
|
||||
udp_recv(link.pcb, udp_recv_cb, &link);
|
||||
#else
|
||||
link.peer = peer;
|
||||
link.peer_len = peer_len;
|
||||
// Same domain as the listener, so the peer address from getpeername fits
|
||||
link.sock = socket::socket_ip(SOCK_DGRAM, IPPROTO_UDP);
|
||||
struct sockaddr_storage local;
|
||||
const socklen_t local_len =
|
||||
socket::set_sockaddr_any(reinterpret_cast<struct sockaddr *>(&local), sizeof(local), this->port_);
|
||||
if (link.sock == nullptr || local_len == 0 ||
|
||||
link.sock->bind(reinterpret_cast<struct sockaddr *>(&local), local_len) != 0 ||
|
||||
link.sock->setblocking(false) != 0)
|
||||
return;
|
||||
#endif
|
||||
// Below the error codes (0x80 and up) the client checks the first byte against, and never all zero
|
||||
token[0] = (token[0] & 0x7F) | 0x01;
|
||||
memcpy(link.reply, token, sizeof(token));
|
||||
}
|
||||
|
||||
bool ESPHomeOTAComponent::udp_start_() {
|
||||
// TCP now only carries a fallback byte; short read timeouts let the UDP link be checked in between
|
||||
struct timeval tv {
|
||||
0, UDP_COMMIT_POLL_MS * 1000
|
||||
};
|
||||
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
const uint32_t start = millis();
|
||||
while (millis() - start < UDP_COMMIT_TIMEOUT) {
|
||||
this->udp_poll_();
|
||||
bool committed;
|
||||
{
|
||||
UdpLock lock;
|
||||
committed = this->udp_->committed;
|
||||
}
|
||||
if (committed) {
|
||||
// The client heard us over UDP, so TCP is done. Outside the lock: closing a socket waits on the tcpip thread
|
||||
this->client_ = nullptr;
|
||||
return true;
|
||||
}
|
||||
// A raw byte outside any Noise frame: the client heard nothing over UDP. Forging it only forces the ordinary
|
||||
// TCP path, which dropping our datagrams forces anyway
|
||||
uint8_t fallback;
|
||||
const ssize_t read = this->client_->read(&fallback, 1);
|
||||
if (read == 1) {
|
||||
this->udp_->release();
|
||||
this->udp_ = nullptr;
|
||||
tv = {UDP_READ_WAIT_MS / 1000, 0};
|
||||
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
return true;
|
||||
}
|
||||
if (read == 0) {
|
||||
this->remote_closed_ = true;
|
||||
return false;
|
||||
}
|
||||
if (!this->would_block_(errno))
|
||||
return false;
|
||||
App.feed_wdt();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t ESPHomeOTAComponent::udp_read_(uint8_t *buf, size_t len) {
|
||||
UdpLink &link = *this->udp_;
|
||||
const uint32_t start = millis();
|
||||
size_t got = 0;
|
||||
for (;;) {
|
||||
this->udp_poll_();
|
||||
UdpSlot p;
|
||||
{
|
||||
UdpLock lock;
|
||||
p = link.consumed != link.received ? link.queue[link.consumed % UDP_WINDOW] : nullptr;
|
||||
}
|
||||
if (p == nullptr) {
|
||||
if (got != 0)
|
||||
return got;
|
||||
if (millis() - start >= UDP_READ_WAIT_MS)
|
||||
break;
|
||||
this->yield_and_feed_watchdog_();
|
||||
continue;
|
||||
}
|
||||
// The callback never touches the slot being read, so the copy runs without the lock
|
||||
const uint16_t offset = UDP_HEADER_SIZE + link.read_offset;
|
||||
const uint16_t n = slot_copy(p, buf + got, std::min<size_t>(len - got, slot_len(p) - offset), offset);
|
||||
got += n;
|
||||
link.read_offset += n;
|
||||
if (UDP_HEADER_SIZE + link.read_offset >= slot_len(p)) {
|
||||
UdpLock lock;
|
||||
slot_free(p);
|
||||
link.queue[link.consumed % UDP_WINDOW] = nullptr;
|
||||
// A full window just opened: tell the client, which is holding back
|
||||
const bool was_full = static_cast<uint16_t>(link.received - link.consumed) == UDP_WINDOW;
|
||||
link.consumed++;
|
||||
link.read_offset = 0;
|
||||
if (was_full)
|
||||
udp_send_ack(link, link.received - 1);
|
||||
}
|
||||
if (got == len)
|
||||
return got;
|
||||
}
|
||||
errno = EWOULDBLOCK;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ESPHomeOTAComponent::udp_send_(const uint8_t *data, size_t len) {
|
||||
UdpLink &link = *this->udp_;
|
||||
UdpLock lock;
|
||||
if (link.out_len + len > UDP_OUT_SIZE)
|
||||
return false;
|
||||
memcpy(link.out + link.out_len, data, len);
|
||||
link.out_len += len;
|
||||
// Prompted by nothing new, so the client takes no round trip sample from it
|
||||
udp_send_ack(link, link.received - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::udp_linger_() {
|
||||
if (this->udp_ == nullptr)
|
||||
return;
|
||||
const uint32_t start = millis();
|
||||
while (millis() - start < UDP_LINGER_MS) {
|
||||
this->udp_poll_();
|
||||
{
|
||||
UdpLock lock;
|
||||
if (this->udp_->out_len == 0)
|
||||
return;
|
||||
}
|
||||
this->yield_and_feed_watchdog_();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
#endif // USE_OTA && ESPHOME_OTA_UDP
|
||||
@@ -3,12 +3,11 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <deque>
|
||||
|
||||
namespace esphome::ezo {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ezo.sensor");
|
||||
static const char *const TAG = "ezo.sensor";
|
||||
|
||||
enum EzoCommandType : uint8_t {
|
||||
EZO_READ = 0,
|
||||
|
||||
@@ -330,9 +330,7 @@ async def write_image(
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(
|
||||
image.resize((width, height)), path
|
||||
).get_flattened_data()
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
CODEOWNERS = ["@tuct"]
|
||||
DOMAIN = "filter_lifetime"
|
||||
@@ -1,112 +0,0 @@
|
||||
#include "filter_lifetime.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::filter_lifetime {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "filter_lifetime");
|
||||
|
||||
static constexpr uint32_t SECONDS_PER_HOUR = 3600;
|
||||
static constexpr float MINUTES_PER_DAY = 24.0f * 60.0f;
|
||||
|
||||
void FilterLifetime::setup() {
|
||||
this->pref_ = this->make_entity_preference<uint32_t>();
|
||||
this->pref_.load(&this->runtime_seconds_);
|
||||
this->last_update_ = millis();
|
||||
this->publish_values_();
|
||||
}
|
||||
|
||||
void FilterLifetime::update() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const uint32_t elapsed_ms = now - this->last_update_;
|
||||
this->last_update_ = now;
|
||||
|
||||
if (this->get_is_on_()) {
|
||||
float speed = this->get_current_speed_();
|
||||
if (std::isnan(speed))
|
||||
speed = 0.0f;
|
||||
speed = std::clamp(speed, 0.0f, 100.0f);
|
||||
const uint32_t prev_hours = this->runtime_seconds_ / SECONDS_PER_HOUR;
|
||||
this->carry_ms_ += static_cast<uint32_t>(elapsed_ms * (speed / 100.0f));
|
||||
this->runtime_seconds_ += this->carry_ms_ / 1000;
|
||||
this->carry_ms_ %= 1000;
|
||||
// Save once per hour of runtime (and in on_shutdown): every save erases a flash sector on ESP8266.
|
||||
if (this->runtime_seconds_ / SECONDS_PER_HOUR != prev_hours)
|
||||
this->pref_.save(&this->runtime_seconds_);
|
||||
ESP_LOGV(TAG, "Elapsed %" PRIu32 " ms at %.0f%%, runtime %" PRIu32 " s", elapsed_ms, speed, this->runtime_seconds_);
|
||||
}
|
||||
this->publish_values_();
|
||||
}
|
||||
|
||||
void FilterLifetime::publish_values_() {
|
||||
const float runtime_minutes = this->runtime_seconds_ / 60.0f;
|
||||
const float max_minutes = this->max_lifetime_minutes_;
|
||||
const float remaining_minutes = std::max(0.0f, max_minutes - runtime_minutes);
|
||||
this->publish_state(100.0f * remaining_minutes / max_minutes);
|
||||
if (this->runtime_hours_sensor_ != nullptr)
|
||||
this->runtime_hours_sensor_->publish_state(runtime_minutes / 60.0f);
|
||||
if (this->remaining_days_sensor_ != nullptr)
|
||||
this->remaining_days_sensor_->publish_state(remaining_minutes / MINUTES_PER_DAY);
|
||||
}
|
||||
|
||||
void FilterLifetime::on_shutdown() { this->pref_.save(&this->runtime_seconds_); }
|
||||
|
||||
void FilterLifetime::reset_filter() {
|
||||
this->runtime_seconds_ = 0;
|
||||
this->carry_ms_ = 0;
|
||||
this->pref_.save(&this->runtime_seconds_);
|
||||
this->publish_values_();
|
||||
ESP_LOGI(TAG, "Filter lifetime reset");
|
||||
}
|
||||
|
||||
bool FilterLifetime::get_is_on_() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// Reads false until the sensor first publishes, so nothing accumulates before then.
|
||||
if (this->is_on_sensor_ != nullptr)
|
||||
return this->is_on_sensor_->state;
|
||||
#endif
|
||||
if (this->is_on_lambda_ != nullptr)
|
||||
return this->is_on_lambda_();
|
||||
return true;
|
||||
}
|
||||
|
||||
float FilterLifetime::get_current_speed_() {
|
||||
if (this->current_speed_sensor_ != nullptr)
|
||||
return this->current_speed_sensor_->state; // NaN before the first publish, treated as 0
|
||||
if (this->current_speed_lambda_ != nullptr)
|
||||
return this->current_speed_lambda_();
|
||||
return 100.0f;
|
||||
}
|
||||
|
||||
void FilterLifetime::dump_config() {
|
||||
LOG_SENSOR("", "Filter Lifetime", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Max lifetime: %" PRIu32 " min (%.1f days)\n"
|
||||
" Runtime: %.1f h",
|
||||
this->max_lifetime_minutes_, this->max_lifetime_minutes_ / MINUTES_PER_DAY,
|
||||
this->runtime_seconds_ / 3600.0f);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->is_on_sensor_ != nullptr) {
|
||||
LOG_BINARY_SENSOR(" ", "Is-on sensor", this->is_on_sensor_);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ESP_LOGCONFIG(TAG, " Is-on: %s",
|
||||
this->is_on_lambda_ != nullptr ? LOG_STR_LITERAL("lambda") : LOG_STR_LITERAL("always"));
|
||||
}
|
||||
if (this->current_speed_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Speed sensor", this->current_speed_sensor_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Speed: %s",
|
||||
this->current_speed_lambda_ != nullptr ? LOG_STR_LITERAL("lambda") : LOG_STR_LITERAL("100%"));
|
||||
}
|
||||
LOG_SENSOR(" ", "Runtime hours", this->runtime_hours_sensor_);
|
||||
LOG_SENSOR(" ", "Remaining days", this->remaining_days_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::filter_lifetime
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user