Compare commits

...

73 Commits

Author SHA1 Message Date
Jonathan Swoboda 359c6a7265 [libretiny] Update LibreTiny to v1.13.0 (#17288) 2026-06-29 22:34:32 -04:00
J. Nick Koston a031da351d Merge pull request #17294 from esphome/bump-device-builder-1.0.22 2026-06-29 19:29:08 -07:00
esphome[bot] 091b6a0ba0 Bump bundled esphome-device-builder to 1.0.22 2026-06-30 02:07:06 +00:00
Jesse Hills cf9d97d5ae [pixoo] Add Divoom Pixoo display component (#16974) 2026-06-30 11:20:48 +12:00
Clyde Stubbs 3e1a6b4e11 [cst9220] Add CST9220 and CST9217 touchscreen support (#16888) 2026-06-30 11:18:01 +12:00
Clyde Stubbs 5c7245dfcd [qmi8658] Motion platform for QMI8658 IMU (#16889) 2026-06-30 11:04:25 +12:00
Clyde Stubbs 1611345c55 [agents] Add English language AI guidelines for documentation (#17290) 2026-06-29 22:37:17 +00:00
Jonathan Swoboda b36e20d60b Revert "Bump awalsh128/cache-apt-pkgs-action from 1.6.0 to 1.6.2 (#17286)" (#17289) 2026-06-29 18:25:16 -04:00
Ardumine 93eb6f78e0 [network] Enlarge Zephyr net buffer pool and TCP windows on nRF52/Zephyr plataform (#17278) 2026-06-29 17:32:57 -04:00
dependabot[bot] e308075e3f Bump puremagic from 1.30 to 2.2.0 (#17285)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 16:12:23 -04:00
dependabot[bot] 405607e9d2 Bump esptool from 5.3.0 to 5.3.1 (#17284)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 16:06:36 -04:00
dependabot[bot] 797ed23765 Bump tzlocal from 5.4.3 to 5.4.4 (#17283)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 16:02:11 -04:00
dependabot[bot] 8780c7e0ac Bump awalsh128/cache-apt-pkgs-action from 1.6.0 to 1.6.2 (#17286)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-29 16:01:41 -04:00
Jonathan Swoboda 136e343988 [ethernet] Generic and YT8531 PHY over RGMII (gigabit) for ESP32-S31 (#17277) 2026-06-29 14:13:11 -04:00
Jonathan Swoboda b8690c8e31 [core] Drop Python 3.11 support (#17280) 2026-06-29 12:32:28 -04:00
Kevin Ahrendt 2778c62d07 [audio] Bump microMP3 to v0.4.0 (#17279) 2026-06-29 11:33:56 -04:00
Jesse Hills 7984349c36 Merge branch 'release' into dev 2026-06-29 22:32:34 +12:00
Jesse Hills 0cbbd64577 Merge pull request #17223 from esphome/bump-2026.6.3
2026.6.3
2026-06-29 22:31:44 +12:00
Jesse Hills a618ee11b4 Bump version to 2026.6.3 2026-06-29 20:30:24 +12:00
Tom 6251c26cc6 [espnow] Fix espnow crash when send() is called without a callback (#17266) 2026-06-29 20:30:24 +12:00
Jonathan Swoboda 4fbe0d87ec [wifi] Fix crash when WiFi is enabled late alongside ESP-NOW (#17239) 2026-06-29 20:30:24 +12:00
esphome[bot] 24d8e99c50 Bump bundled esphome-device-builder to 1.0.21 (#17257)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-06-29 20:30:24 +12:00
Jonathan Swoboda 14b6a0ede1 [espnow] Don't throttle ESP-NOW RX when deep_sleep is present (#17240) 2026-06-29 20:30:24 +12:00
Jonathan Swoboda 1793ca5eac [core] Suppress unactionable legacy-redaction warning for substitutions (#17242) 2026-06-29 20:30:24 +12:00
esphome[bot] 62e19bcb27 Bump bundled esphome-device-builder to 1.0.20 (#17244) 2026-06-29 20:30:24 +12:00
Franck Nijhof 84d1c34c28 [core] Fix area saved as null in storage.json (#17219)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-29 20:30:24 +12:00
esphome[bot] f78cbf9200 Bump bundled esphome-device-builder to 1.0.19 (#17217)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-06-29 20:30:23 +12:00
esphome[bot] eb711381d3 Bump bundled esphome-device-builder to 1.0.18 (#17212)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-06-29 20:30:23 +12:00
Jonathan Swoboda 9a1daa5247 [hbridge] Fix light stuck on one polarity (#17162) 2026-06-29 20:30:18 +12:00
Clyde Stubbs f3d61ca3e1 [mipi][mipi_spi] Swap native dimensions for swap_xy hardware transform (#17201)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:29:44 +12:00
Jonathan Swoboda 29dfd820c6 [wifi] Report STA IP, not SoftAP IP, in wifi_info on ESP8266 (#17185) 2026-06-29 20:29:44 +12:00
Jonathan Swoboda 8bc5b97298 [network] Set IPv4 type tag on all lwIP platforms, not just esp32 (#17200) 2026-06-29 20:29:44 +12:00
Jonathan Swoboda 7a64163c4f [esp32] Accept '#' as ESP-IDF source ref separator (#17193) 2026-06-29 20:29:44 +12:00
esphome[bot] dfe14f9c3a Bump bundled esphome-device-builder to 1.0.17 (#17199) 2026-06-29 20:29:44 +12:00
esphome[bot] 26cf373ae7 Bump bundled esphome-device-builder to 1.0.16 (#17182) 2026-06-29 20:29:44 +12:00
Geoffrey Frogeye 94ccddf176 [opentherm] Support power scaling disabled (#17183)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-06-29 20:29:44 +12:00
Clyde Stubbs 2ec24505d0 [mipi_spi] Suppress sequence errors when page selection used (#17176) 2026-06-29 20:29:44 +12:00
esphome[bot] 4f7faa7712 Bump bundled esphome-device-builder to 1.0.15 (#17170) 2026-06-29 20:29:44 +12:00
Clyde Stubbs b3dcaac262 [mipi_spi] Warn on MODE3 default for display without CS pin (#17153) 2026-06-29 20:29:44 +12:00
mnewton25 ee118d384a [esp32] Use POSIX path for secure-boot signing/verification keys Fixes #17164 (#17166)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-29 20:29:44 +12:00
Jonathan Swoboda 8d36167e11 [esp32_ble_server] Fix set_value action with by-reference triggers (#17156) 2026-06-29 20:29:44 +12:00
esphome[bot] 6d559a32df Bump bundled esphome-device-builder to 1.0.14 (#17139) 2026-06-29 20:29:37 +12:00
Jonathan Swoboda bf0d31b3ab [espidf] Don't fail framework check on broken unrelated PATH tools (#17053) 2026-06-29 20:23:23 +12:00
dependabot[bot] d8ffb732b7 Bump zeroconf from 0.149.16 to 0.150.0 (#17137)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 20:23:23 +12:00
Tom 9e8261056c [espnow] Fix espnow crash when send() is called without a callback (#17266) 2026-06-28 23:06:12 -04:00
Clyde Stubbs 5f311d281e [esphome] Warn when a YAML merge (<<:) drops a key (#17246)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-29 07:17:01 +10:00
J. Nick Koston a336ad6732 [mcp4725] Use constexpr bit shift instead of powf for full-scale value (#17261) 2026-06-28 17:07:51 -04:00
Bonne Eggleston 8434d54cc7 [modbus] Reinstate turnaround delay after broadcasts (Revert #17209) (#17263)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-28 17:07:25 -04:00
Jonathan Swoboda b62f7a41c9 [multiple] Single-precision float math, avoid double promotion (stragglers) (#17260) 2026-06-28 16:15:38 -04:00
Bonne Eggleston 4ebecf514a [modbus_server] Simplify server response handling (#12376)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-06-28 19:41:47 +00:00
Jonathan Swoboda 2f32c88ae5 [wifi] Fix crash when WiFi is enabled late alongside ESP-NOW (#17239) 2026-06-28 15:16:42 -04:00
Jonathan Swoboda 95449068e7 [multiple] Single-precision float math, avoid double promotion (batch 4/4) (#17256) 2026-06-28 14:18:41 -04:00
Jonathan Swoboda 556def78aa [multiple] Single-precision float math, avoid double promotion (batch 3/4) (#17255) 2026-06-28 14:18:30 -04:00
Jonathan Swoboda b7803cf9b5 [multiple] Single-precision float math, avoid double promotion (batch 2/4) (#17254) 2026-06-28 14:18:20 -04:00
Jonathan Swoboda 40820287f1 [multiple] Single-precision float math, avoid double promotion (batch 1/4) (#17253) 2026-06-28 14:18:13 -04:00
Jonathan Swoboda 6210dfb4d0 [core] Use single-precision float math to avoid double promotion (#17252) 2026-06-28 14:18:01 -04:00
esphome[bot] 45c712b17b Bump bundled esphome-device-builder to 1.0.21 (#17257)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-06-28 09:41:49 -07:00
alorente 8e23065b86 [it8951] Add IT8951 e-paper controller support to epaper_spi (#15346)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Citric Li <37475446+limengdu@users.noreply.github.com>
Co-authored-by: koosoli <koosoli@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:14:05 +10:00
Jonathan Swoboda d3892b8399 [platformio] Extract toolchain-agnostic PlatformIO library converter (#17243) 2026-06-28 07:03:09 -04:00
Jonathan Swoboda bda789052d [espnow] Don't throttle ESP-NOW RX when deep_sleep is present (#17240) 2026-06-27 22:17:09 +00:00
Jonathan Swoboda 0fb100f2d1 [core] Suppress unactionable legacy-redaction warning for substitutions (#17242) 2026-06-27 18:05:00 -04:00
esphome[bot] fd7fc6b8e8 Bump bundled esphome-device-builder to 1.0.20 (#17244) 2026-06-27 15:00:23 -07:00
tomaszduda23 690e8c3fb9 [nrf52] add upload for native build (#17100)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-27 15:50:28 -04:00
Ardumine a0742a9535 [api] Add nRF52 support (#17226)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-27 14:22:34 -04:00
Jonathan Swoboda ccc57475b7 [deep_sleep] Add ESP32-C5 support (#17237) 2026-06-27 12:04:45 -04:00
Franck Nijhof 24ec65e68e [esp32] Only warn about S3 PSRAM pins (GPIO33-37) in octal mode (#17222)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-27 11:26:47 -04:00
dependabot[bot] 436938b931 Bump actions/cache/restore from 6.0.0 to 6.1.0 (#17231)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 10:33:13 -04:00
dependabot[bot] 063c4371de Bump actions/cache from 6.0.0 to 6.1.0 (#17230)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 10:33:01 -04:00
dependabot[bot] 7ad4cbf46f Bump actions/cache/save from 6.0.0 to 6.1.0 (#17229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 10:32:50 -04:00
dependabot[bot] 88875daf52 Bump actions/cache/restore from 6.0.0 to 6.1.0 in /.github/actions/restore-python (#17228)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 10:32:39 -04:00
Franck Nijhof 7811781a96 [es8388] Fix DAC unable to unmute once muted (#17221)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-27 10:21:36 -04:00
Franck Nijhof da5e11d196 [core] Fix area saved as null in storage.json (#17219)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-27 09:49:56 -04:00
Ardumine 75cdabee3d [socket] Add BSD socket support for nRF52 (#16699)
Co-authored-by: tomaszduda23 <tomaszduda23@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-26 09:30:07 -04:00
195 changed files with 9304 additions and 3957 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
# yamllint disable-line rule:line-length
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
python-version: "3.12"
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
+2 -2
View File
@@ -65,7 +65,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
@@ -149,7 +149,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
@@ -60,7 +60,7 @@ jobs:
if: steps.pr.outputs.skip != 'true'
uses: ./.github/actions/restore-python
with:
python-version: "3.11"
python-version: "3.12"
cache-key: ${{ hashFiles('.cache-key') }}
- name: Download memory analysis artifacts
+15 -15
View File
@@ -12,8 +12,8 @@ permissions:
contents: read # actions/checkout for all jobs; individual jobs add their own scopes when they need to write
env:
DEFAULT_PYTHON: "3.11"
PYUPGRADE_TARGET: "--py311-plus"
DEFAULT_PYTHON: "3.12"
PYUPGRADE_TARGET: "--py312-plus"
concurrency:
# yamllint disable-line rule:line-length
@@ -39,7 +39,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -203,7 +203,7 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
os:
@@ -250,7 +250,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -295,7 +295,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -339,7 +339,7 @@ jobs:
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -365,7 +365,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -509,14 +509,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -820,7 +820,7 @@ jobs:
run: echo ${{ matrix.components }}
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: libsdl2-dev ccache
version: 1.1
@@ -1098,7 +1098,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -1122,7 +1122,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -1164,7 +1164,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -1211,7 +1211,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
+1 -1
View File
@@ -96,7 +96,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
+1 -1
View File
@@ -40,7 +40,7 @@ repos:
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py311-plus]
args: [--py312-plus]
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
hooks:
+7 -1
View File
@@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro
## 2. Core Technologies & Stack
* **Languages:** Python (>=3.11), C++ (gnu++20)
* **Languages:** Python (>=3.12), C++ (gnu++20)
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
* **Configuration:** YAML.
@@ -709,3 +709,9 @@ This document provides essential context for AI models interacting with this pro
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```
## 9. English Language
The project uses English for non-code content. When drafting documentation, code comments, commit messages,
PR descriptions, and similar text, avoid technical jargon. Instead, express concepts in plain English,
using standard technical terms only when required. Ensure the text is readily comprehensible to a wide
audience, including non-native English speakers.
+4
View File
@@ -123,6 +123,7 @@ esphome/components/cs5460a/* @balrog-kun
esphome/components/cse7761/* @berfenger
esphome/components/cst226/* @clydebarrow
esphome/components/cst816/* @clydebarrow
esphome/components/cst9220/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
@@ -266,6 +267,7 @@ esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/ir_rf_proxy/* @kbx81
esphome/components/it8951/* @koosoli @limengdu @Passific
esphome/components/jsn_sr04t/* @Mafus1
esphome/components/json/* @esphome/core
esphome/components/kamstrup_kmp/* @cfeenstra1024
@@ -385,6 +387,7 @@ esphome/components/pcm5122/* @remcom
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
esphome/components/pixoo/* @jesserockz
esphome/components/pm1006/* @habbie
esphome/components/pm2005/* @andrewjswan
esphome/components/pmsa003i/* @sjtrny
@@ -404,6 +407,7 @@ esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmi8658/* @clydebarrow
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_dbi/* @clydebarrow
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.19
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.22
RUN \
platformio settings set enable_telemetry No \
+1 -1
View File
@@ -2,6 +2,6 @@ esphome:
name: docker-test-ln882x-arduino
ln882x:
board: generic-ln882hki
board: generic-ln882h
logger:
+22 -5
View File
@@ -1488,12 +1488,29 @@ _LEGACY_REDACTION_REMOVAL = "2026.12.0"
def _redact_with_legacy_fallback(output: str) -> str:
unmarked: set[str] = set()
# Track the top-level ``substitutions:`` block. Its keys are arbitrary
# user-chosen names with no schema validator, so the ``cv.sensitive(...)``
# migration named in the warning can't be applied to them. Their values are
# still redacted, but emitting the (unactionable) deprecation warning would
# only confuse users.
in_substitutions = False
def _replace(m: re.Match[str]) -> str:
unmarked.add(m.group("key"))
return f"{m.group('key')}: \\033[8m{m.group('val')}\\033[28m"
output = _LEGACY_REDACTION_RE.sub(_replace, output)
lines = output.split("\n")
for i, line in enumerate(lines):
# A non-indented, non-blank line is a top-level key that opens or
# closes the substitutions block.
if line and not line[0].isspace():
in_substitutions = line.startswith(f"{CONF_SUBSTITUTIONS}:")
m = _LEGACY_REDACTION_RE.search(line)
if m is None:
continue
if not in_substitutions:
unmarked.add(m.group("key"))
lines[i] = (
f"{line[: m.start()]}{m.group('key')}: "
f"\\033[8m{m.group('val')}\\033[28m{line[m.end() :]}"
)
output = "\n".join(lines)
for key in sorted(unmarked):
_LOGGER.warning(
"Field '%s' is being redacted by a legacy substring heuristic. "
+3 -6
View File
@@ -12,12 +12,9 @@ from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
import threading
from typing import Generic, TypeVar
_T = TypeVar("_T")
class AsyncThreadRunner(threading.Thread, Generic[_T]):
class AsyncThreadRunner[T](threading.Thread):
"""Run an async coroutine in a daemon thread and expose its result.
The runner catches all exceptions from the coroutine and stores them in
@@ -35,10 +32,10 @@ class AsyncThreadRunner(threading.Thread, Generic[_T]):
result = runner.result
"""
def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None:
def __init__(self, coro_factory: Callable[[], Awaitable[T]]) -> None:
super().__init__(daemon=True)
self._coro_factory = coro_factory
self.result: _T | None = None
self.result: T | None = None
self.exception: BaseException | None = None
self.event = threading.Event()
+1 -1
View File
@@ -25,7 +25,7 @@ void A01nyubComponent::check_buffer_() {
if (this->buffer_[3] == checksum) {
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
if (distance > 280) {
float meters = distance / 1000.0;
float meters = distance / 1000.0f;
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
this->publish_state(meters);
} else {
+1 -1
View File
@@ -216,7 +216,7 @@ void AcDimmer::setup() {
}
void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
state = std::acos(1 - (2 * state)) / std::numbers::pi_v<float>; // RMS power compensation
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
if (new_value != 0 && this->store_.value == 0)
this->store_.init_cycle = this->init_with_half_cycle_;
+5 -5
View File
@@ -114,13 +114,13 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->decoder_->decode(param->notify.value, param->notify.value_len);
if (this->decoder_->has_position()) {
this->position = ((float) this->decoder_->position_ / 100.0);
this->position = ((float) this->decoder_->position_ / 100.0f);
if (!this->invert_position_)
this->position = 1 - this->position;
if (this->position > 0.97)
this->position = 1.0;
if (this->position < 0.02)
this->position = 0.0;
if (this->position > 0.97f)
this->position = 1.0f;
if (this->position < 0.02f)
this->position = 0.0f;
this->publish_state();
}
+2 -2
View File
@@ -6,9 +6,9 @@
namespace esphome::anova {
float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); }
float ftoc(float f) { return (f - 32.0f) * (5.0f / 9.0f); }
float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; }
float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0f; }
AnovaPacket *AnovaCodec::clean_packet_() {
this->packet_.length = strlen((char *) this->packet_.data);
+2
View File
@@ -305,6 +305,7 @@ CONFIG_SCHEMA = cv.All(
rtl87xx=4, # Moderate RAM, BSD-style sockets
host=4, # Abundant resources
ln882x=4, # Moderate RAM
nrf52=4, # ~256KB RAM, BSD sockets
): cv.int_range(min=1, max=10),
cv.SplitDefault(
CONF_MAX_CONNECTIONS,
@@ -315,6 +316,7 @@ CONFIG_SCHEMA = cv.All(
rtl87xx=5, # Moderate RAM
host=8, # Abundant resources
ln882x=5, # Moderate RAM
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
@@ -31,6 +31,13 @@
#include <vector>
#include <string>
#if defined(LOG_LEVEL_NONE)
// Zephyr defines LOG_LEVEL_NONE as a logging macro that collides with the LogLevel enum value of
// the same name in the generated api_pb2.h. Undefine it for the rest of this translation unit so
// the enum parses; nothing below needs Zephyr's logging macro.
#undef LOG_LEVEL_NONE
#endif
namespace esphome::api {
// This file only provides includes, no actual code
+1 -1
View File
@@ -395,7 +395,7 @@ async def to_code(config):
)
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
add_idf_component(name="esphome/micro-mp3", ref="0.3.0")
add_idf_component(name="esphome/micro-mp3", ref="0.4.0")
_emit_memory_pair(
data.mp3.buffer_memory,
"CONFIG_MICRO_MP3_PREFER_PSRAM",
@@ -112,7 +112,7 @@ float BinarySensorMap::bayesian_predicate_(bool sensor_state, float prior, float
prob_state_source_false = 1 - prob_given_false;
}
return prob_state_source_true / (prior * prob_state_source_true + (1.0 - prior) * prob_state_source_false);
return prob_state_source_true / (prior * prob_state_source_true + (1.0f - prior) * prob_state_source_false);
}
void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -205,7 +205,7 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se
// Chip temperature
if (reference == BL0906_TREF) {
value = (float) to_int32_t(data_s24);
value = (value - 64) * 12.5 / 59 - 40;
value = (value - 64) * 12.5f / 59 - 40;
}
sensor->publish_state(value);
}
+1 -1
View File
@@ -120,7 +120,7 @@ float BL0940::calculate_power_reference_() {
float BL0940::calculate_energy_reference_() {
// formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2)
// or: power_reference_ * 3600000 / (1638.4 * 256)
return this->power_reference_cal_ * 3600000 / (1638.4 * 256);
return this->power_reference_cal_ * 3600000 / (1638.4f * 256);
}
float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; }
+2 -2
View File
@@ -124,14 +124,14 @@ void BL0942::setup() {
// If either current or voltage references are set explicitly by the user,
// calculate the power reference from it unless that is also explicitly set.
if ((this->current_reference_set_ || this->voltage_reference_set_) && !this->power_reference_set_) {
this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0 / 305978.0) / 73989.0;
this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0f / 305978.0f) / 73989.0f;
this->power_reference_set_ = true;
}
// Similarly for energy reference, if the power reference was set by the user
// either implicitly or explicitly.
if (this->power_reference_set_ && !this->energy_reference_set_) {
this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4;
this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4f;
this->energy_reference_set_ = true;
}
@@ -204,7 +204,7 @@ void MedianCombinationComponent::handle_new_value(float value) {
median = sensor_states[sensor_states_size / 2];
} else {
// Even number of measurements, use the average of the two middle measurements
median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0;
median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0f;
}
}
+6
View File
@@ -0,0 +1,6 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c"]
cst9220_ns = cg.esphome_ns.namespace("cst9220")
@@ -0,0 +1,36 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from .. import cst9220_ns
CST9220Touchscreen = cst9220_ns.class_(
"CST9220Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CST9220Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x5A))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
if reset_pin := config.get(CONF_RESET_PIN):
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
@@ -0,0 +1,141 @@
#include "cst9220_touchscreen.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
namespace esphome::cst9220 {
void CST9220Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
// Wait for the controller to leave its bootloader before talking to it.
this->set_timeout(30, [this] { this->continue_setup_(); });
}
void CST9220Touchscreen::continue_setup_() {
uint8_t buffer[4];
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
// Enter command mode so the configuration registers can be read.
if (this->write_register16(REG_CMD_MODE, buffer, 0) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to enter command mode"));
this->mark_failed();
return;
}
delay(10);
// The firmware check code confirms that valid firmware is loaded.
if (this->read_register16(REG_CHECKCODE, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read check code"));
this->mark_failed();
return;
}
uint32_t checkcode = encode_uint32(buffer[3], buffer[2], buffer[1], buffer[0]);
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
ESP_LOGE(TAG, "Invalid firmware check code: 0x%08" PRIX32, checkcode);
this->status_set_error(LOG_STR("Invalid firmware check code"));
this->mark_failed();
return;
}
// Read the panel resolution unless the user supplied calibration values.
if (this->read_register16(REG_RESOLUTION, buffer, 4) == i2c::ERROR_OK) {
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = encode_uint16(buffer[1], buffer[0]);
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = encode_uint16(buffer[3], buffer[2]);
}
// Read the chip type and project id and validate the controller.
if (this->read_register16(REG_CHIP_INFO, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read chip ID"));
this->mark_failed();
return;
}
this->chip_id_ = encode_uint16(buffer[3], buffer[2]);
this->project_id_ = encode_uint16(buffer[1], buffer[0]);
if (this->chip_id_ != CST9220_CHIP_ID && this->chip_id_ != CST9217_CHIP_ID) {
ESP_LOGE(TAG, "Unknown chip ID: 0x%04X", this->chip_id_);
this->status_set_error(LOG_STR("Unknown chip ID"));
this->mark_failed();
return;
}
// Fall back to the display dimensions if the resolution read failed.
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = this->display_->get_native_width();
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = this->display_->get_native_height();
this->setup_complete_ = true;
}
void CST9220Touchscreen::update_touches() {
if (!this->setup_complete_)
return;
uint8_t data[CST9220_DATA_LENGTH];
// Only an actual I2C failure should skip the update; a successful read with no
// touches is a real "all fingers lifted" state that must flow through so the
// base class can generate the release event.
if (this->read_register16(REG_TOUCH_DATA, data, sizeof(data)) != i2c::ERROR_OK) {
this->status_set_warning();
this->skip_update_ = true;
return;
}
this->status_clear_warning();
// Acknowledge the report so the controller can prepare the next one.
uint8_t ack = TOUCH_ACK;
this->write_register16(REG_TOUCH_DATA, &ack, 1);
// A valid report carries the ACK marker at offset 6; offset 0 holds the first
// point and must be neither the ACK marker nor empty. Anything else means no
// valid touch data this cycle, which we report as zero touches (not a skip).
if (data[0] == TOUCH_ACK || data[0] == 0x00 || data[6] != TOUCH_ACK)
return;
uint8_t num_touches = data[5] & 0x7F;
if (num_touches > CST9220_MAX_TOUCHES)
num_touches = CST9220_MAX_TOUCHES;
for (uint8_t i = 0; i < num_touches; i++) {
// The first point starts at offset 0; subsequent points are offset by the
// two status bytes that follow it.
const uint8_t *p = data + i * 5 + (i == 0 ? 0 : 2);
uint8_t id = p[0] >> 4;
uint8_t event = p[0] & 0x0F;
if (event != TOUCH_EVENT_DOWN)
continue;
// p[3] is shared: high nibble holds the X LSBs, low nibble the Y LSBs.
uint16_t x = (p[1] << 4) | (p[3] >> 4);
uint16_t y = (p[2] << 4) | (p[3] & 0x0F);
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
this->add_raw_touch_position_(id, x, y);
}
}
void CST9220Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG,
"CST9220 Touchscreen:\n"
" Chip ID: 0x%04X\n"
" Project ID: 0x%04X\n"
" X Raw Min: %d, X Raw Max: %d\n"
" Y Raw Min: %d, Y Raw Max: %d",
this->chip_id_, this->project_id_, this->x_raw_min_, this->x_raw_max_, this->y_raw_min_,
this->y_raw_max_);
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
}
} // namespace esphome::cst9220
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::cst9220 {
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
static const uint16_t REG_CMD_MODE = 0xD101; // enter command mode
static const uint16_t REG_CHECKCODE = 0xD1FC; // firmware check code
static const uint16_t REG_RESOLUTION = 0xD1F8; // panel resolution
static const uint16_t REG_CHIP_INFO = 0xD204; // chip type + project id
static const uint8_t TOUCH_ACK = 0xAB;
static const uint8_t TOUCH_EVENT_DOWN = 0x06;
static const uint16_t CST9220_CHIP_ID = 0x9220;
static const uint16_t CST9217_CHIP_ID = 0x9217;
// Maximum simultaneous touch points reported by the family.
static const uint8_t CST9220_MAX_TOUCHES = 5;
// Report layout: 5 bytes per touch point plus 5 bytes of status/ack overhead.
static const size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
class CST9220Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void update_touches() override;
void continue_setup_();
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
uint16_t chip_id_{};
uint16_t project_id_{};
bool setup_complete_{};
};
} // namespace esphome::cst9220
@@ -39,7 +39,7 @@ void CurrentBasedCover::control(const CoverCall &call) {
auto opt_pos = call.get_position();
if (opt_pos.has_value()) {
auto pos = *opt_pos;
if (fabsf(this->position - pos) < 0.01) {
if (fabsf(this->position - pos) < 0.01f) {
// already at target
} else {
auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
+1 -1
View File
@@ -151,7 +151,7 @@ uint8_t DaikinBrcClimate::temperature_() {
// Temperature in remote is in F
if (this->fahrenheit_) {
temperature = (uint8_t) roundf(
clamp<float>(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
clamp<float>(((this->target_temperature * 1.8f) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
} else {
temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1;
}
@@ -138,7 +138,7 @@ float DallasTemperatureSensor::get_temp_c_() {
if (this->scratch_pad_[7] == 0) {
return NAN;
}
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25f;
}
switch (this->resolution_) {
case 9:
@@ -96,7 +96,8 @@ class DeepSleepComponent final : public Component {
#endif
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
void set_touch_wakeup(bool touch_wakeup);
#endif
@@ -16,7 +16,7 @@ namespace esphome::deep_sleep {
// | ESP32-S3 | ✓ | ✓ | ✓ | |
// | ESP32-C2 | | | | ✓ |
// | ESP32-C3 | | | | ✓ |
// | ESP32-C5 | | (✓) | | (✓) |
// | ESP32-C5 | | | | |
// | ESP32-C6 | | ✓ | | ✓ |
// | ESP32-C61 | | ✓ | | ✓ |
// | ESP32-H2 | | ✓ | | |
@@ -56,7 +56,8 @@ void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wa
#endif
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
#endif
@@ -99,7 +100,8 @@ void DeepSleepComponent::deep_sleep_() {
// Single pin wakeup (ext0) - ESP32, S2, S3 only
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
@@ -122,9 +124,9 @@ void DeepSleepComponent::deep_sleep_() {
}
#endif
// GPIO wakeup - C2, C3, C6, C61 only
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
defined(USE_ESP32_VARIANT_ESP32C61)
// GPIO wakeup - C2, C3, C5, C6, C61 only
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61)
if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
// Make sure GPIO is in input mode, not all RTC GPIO pins are input by default
@@ -154,7 +156,8 @@ void DeepSleepComponent::deep_sleep_() {
// Touch wakeup - ESP32, S2, S3 only
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
!defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
esp_sleep_enable_touchpad_wakeup();
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
+1 -1
View File
@@ -15,7 +15,7 @@ class DemoSensor final : public sensor::Sensor, public PollingComponent {
float base = std::isnan(this->state) ? 0.0f : this->state;
this->publish_state(base + val * 10);
} else {
if (val < 0.1) {
if (val < 0.1f) {
this->publish_state(NAN);
} else {
this->publish_state(val * 100);
+1 -1
View File
@@ -9,7 +9,7 @@ namespace esphome::demo {
class DemoSwitch final : public switch_::Switch, public Component {
public:
void setup() override {
bool initial = random_float() < 0.5;
bool initial = random_float() < 0.5f;
this->publish_state(initial);
}
+2 -2
View File
@@ -10,9 +10,9 @@ class DemoTextSensor final : public text_sensor::TextSensor, public PollingCompo
public:
void update() override {
float val = random_float();
if (val < 0.33) {
if (val < 0.33f) {
this->publish_state("foo");
} else if (val < 0.66) {
} else if (val < 0.66f) {
this->publish_state("bar");
} else {
this->publish_state("foobar");
+27 -27
View File
@@ -121,51 +121,51 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->cmd_ = "detRangeCfg -1 0 0";
} else if (min2 < 0 || max2 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15f, max1 / 0.15f);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = round(min3 / 0.15) * 0.15;
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15, min3 / 0.15, max3 / 0.15);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f, min3 / 0.15f, max3 / 0.15f);
this->cmd_ = buf;
} else {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
this->min2_ = min2 = round(min2 / 0.15) * 0.15;
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = round(min3 / 0.15) * 0.15;
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min4_ = min4 = round(min4 / 0.15) * 0.15;
this->max4_ = max4 = round(max4 / 0.15) * 0.15;
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
this->min4_ = min4 = roundf(min4 / 0.15f) * 0.15f;
this->max4_ = max4 = roundf(max4 / 0.15f) * 0.15f;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15,
min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f,
min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f);
this->cmd_ = buf;
}
+8 -8
View File
@@ -42,10 +42,10 @@ void Display::line_at_angle(int x, int y, int angle, int length, Color color) {
void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color) {
// Calculate start and end points
int x1 = (start_radius * cos(angle * M_PI / 180)) + x;
int y1 = (start_radius * sin(angle * M_PI / 180)) + y;
int x2 = (stop_radius * cos(angle * M_PI / 180)) + x;
int y2 = (stop_radius * sin(angle * M_PI / 180)) + y;
int x1 = (start_radius * std::cos(angle * std::numbers::pi_v<float> / 180)) + x;
int y1 = (start_radius * std::sin(angle * std::numbers::pi_v<float> / 180)) + y;
int x2 = (stop_radius * std::cos(angle * std::numbers::pi_v<float> / 180)) + x;
int y2 = (stop_radius * std::sin(angle * std::numbers::pi_v<float> / 180)) + y;
// Draw line
this->line(x1, y1, x2, y2, color);
@@ -228,7 +228,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
int e2max, e2min;
progress = std::max(0, std::min(progress, 100)); // 0..100
int draw_progress = progress > 50 ? (100 - progress) : progress;
float tan_a = (progress == 50) ? 65535 : tan(float(draw_progress) * M_PI / 100); // slope
float tan_a = (progress == 50) ? 65535 : tanf(float(draw_progress) * std::numbers::pi_v<float> / 100); // slope
do {
// outer dots
@@ -444,15 +444,15 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *
// hence we rotate the shape by 270° to orient the polygon up.
rotation_degrees += ROTATION_270_DEGREES;
// Convert the rotation to radians, easier to use in trigonometrical calculations
float rotation_radians = rotation_degrees * std::numbers::pi / 180;
float rotation_radians = rotation_degrees * std::numbers::pi_v<float> / 180;
// A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no
// additional rotation of the shape.
// A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal,
// this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the
// left side of the first horizontal edge.
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0;
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi_v<float> / edges : 0.0f;
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians;
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi_v<float> + rotation_radians;
*vertex_x = (int) std::round(std::cos(vertex_angle) * radius) + center_x;
*vertex_y = (int) std::round(std::sin(vertex_angle) * radius) + center_y;
}
+1 -1
View File
@@ -12,7 +12,7 @@ class DS2484OneWireBus final : public one_wire::OneWireBus, public i2c::I2CDevic
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS - 1.0; }
float get_setup_priority() const override { return setup_priority::BUS - 1.0f; }
bool reset_device();
int reset_int() override;
+4 -4
View File
@@ -169,14 +169,14 @@ bool ES7210::configure_mic_gain_() {
uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) {
// reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB
mic_gain += 0.5;
if (mic_gain <= 33.0) {
mic_gain += 0.5f;
if (mic_gain <= 33.0f) {
return (uint8_t) (mic_gain / 3);
}
if (mic_gain < 36.0) {
if (mic_gain < 36.0f) {
return 12;
}
if (mic_gain < 37.0) {
if (mic_gain < 37.0f) {
return 13;
}
return 14;
+4 -4
View File
@@ -105,14 +105,14 @@ bool ES7243E::configure_mic_gain_() {
uint8_t ES7243E::es7243e_gain_reg_value_(float mic_gain) {
// reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB
mic_gain += 0.5;
if (mic_gain <= 33.0) {
mic_gain += 0.5f;
if (mic_gain <= 33.0f) {
return (uint8_t) mic_gain / 3;
}
if (mic_gain < 36.0) {
if (mic_gain < 36.0f) {
return 12;
}
if (mic_gain < 37.0) {
if (mic_gain < 37.0f) {
return 13;
}
return 14;
+7 -1
View File
@@ -173,8 +173,14 @@ bool ES8388::set_mute_state_(bool mute_state) {
ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL3, &value));
ESP_LOGV(TAG, "Read ES8388_DACCONTROL3: 0x%02X", value);
// Only toggle the DACMute bit; the other bits of this register hold unrelated
// DAC settings that must be preserved. Previously muting overwrote the whole
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
// could not be unmuted again.
if (mute_state) {
value = 0x3C;
value |= ES8388_DACCONTROL3_DAC_MUTE;
} else {
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
}
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
+1
View File
@@ -38,6 +38,7 @@ static const uint8_t ES8388_ADCCONTROL14 = 0x16;
static const uint8_t ES8388_DACCONTROL1 = 0x17;
static const uint8_t ES8388_DACCONTROL2 = 0x18;
static const uint8_t ES8388_DACCONTROL3 = 0x19;
static const uint8_t ES8388_DACCONTROL3_DAC_MUTE = 0x04; // DACMute, bit 2 of DACCONTROL3
static const uint8_t ES8388_DACCONTROL4 = 0x1a;
static const uint8_t ES8388_DACCONTROL5 = 0x1b;
static const uint8_t ES8388_DACCONTROL6 = 0x1c;
+4
View File
@@ -1102,6 +1102,8 @@ def final_validate(config):
# Imported locally to avoid circular import issues
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
from .gpio import final_validate_pins
errs = []
conf_fw = config[CONF_FRAMEWORK]
advanced = conf_fw[CONF_ADVANCED]
@@ -1185,6 +1187,8 @@ def final_validate(config):
)
)
final_validate_pins(full_config)
if (
config[CONF_FLASH_SIZE] == "32MB"
and "ota" in full_config
+15 -1
View File
@@ -18,6 +18,7 @@ from esphome.const import (
PLATFORM_ESP32,
)
from esphome.core import CORE
from esphome.types import ConfigType
from . import boards
from .const import (
@@ -50,7 +51,11 @@ from .gpio_esp32_h4 import esp32_h4_validate_gpio_pin, esp32_h4_validate_support
from .gpio_esp32_h21 import esp32_h21_validate_gpio_pin, esp32_h21_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
from .gpio_esp32_s3 import (
esp32_s3_final_validate_pins,
esp32_s3_validate_gpio_pin,
esp32_s3_validate_supports,
)
from .gpio_esp32_s31 import esp32_s31_validate_gpio_pin, esp32_s31_validate_supports
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
@@ -96,6 +101,7 @@ def _translate_pin(value):
class ESP32ValidationFunctions:
pin_validation: Callable[[int], int]
usage_validation: Callable[[dict[str, Any]], dict[str, Any]]
final_validate: Callable[[ConfigType], None] | None = None
_esp32_validations = {
@@ -145,6 +151,7 @@ _esp32_validations = {
VARIANT_ESP32S3: ESP32ValidationFunctions(
pin_validation=esp32_s3_validate_gpio_pin,
usage_validation=esp32_s3_validate_supports,
final_validate=esp32_s3_final_validate_pins,
),
VARIANT_ESP32S31: ESP32ValidationFunctions(
pin_validation=esp32_s31_validate_gpio_pin,
@@ -261,3 +268,10 @@ async def esp32_pin_to_code(config):
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var
def final_validate_pins(full_config: ConfigType) -> None:
"""Run the active variant's pin final-validation, if it defines one."""
funcs = _esp32_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])
if funcs is not None and funcs.final_validate is not None:
funcs.final_validate(full_config)
+38 -7
View File
@@ -2,8 +2,15 @@ import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from esphome.const import (
CONF_DISABLED,
CONF_INPUT,
CONF_MODE,
CONF_NUMBER,
PLATFORM_ESP32,
)
from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin
from esphome.types import ConfigType
_ESP32S3_SPI_PSRAM_PINS = {
26: "SPICS1",
@@ -38,11 +45,9 @@ def esp32_s3_validate_gpio_pin(value: int) -> int:
raise cv.Invalid(
f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})"
)
if value in _ESP32S3R8_PSRAM_PINS:
_LOGGER.warning(
"GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models",
value,
)
# GPIO33-37 (_ESP32S3R8_PSRAM_PINS) are only taken by the PSRAM interface in
# octal mode -- whether that applies isn't known here, so the warning is
# deferred to final_validate_pins() in gpio.py once the PSRAM mode is resolved.
if value in (22, 23, 24, 25):
# These pins are not exposed in GPIO mux (reason unknown)
@@ -71,3 +76,29 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER)
return value
def esp32_s3_final_validate_pins(full_config: ConfigType) -> None:
"""Warn about GPIO33-37 usage, but only when octal PSRAM (which uses them) is set.
These pins are only taken by the PSRAM interface in octal mode (ESP32-S3R8 /
S3R8V); on quad-PSRAM variants -- or when the psram block is disabled, so the
octal interface is never configured -- they are free. The per-pin validator
can't know the PSRAM mode, so the check is deferred here, where
PIN_SCHEMA_REGISTRY.pins_used already lists every used pin.
"""
# Imported locally to avoid circular import issues
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN, TYPE_OCTAL
psram_config = full_config.get(PSRAM_DOMAIN, {})
if psram_config.get(CONF_DISABLED) or psram_config.get(CONF_MODE) != TYPE_OCTAL:
return
for number in sorted(
number
for key, _client_id, number in PIN_SCHEMA_REGISTRY.pins_used
if key == PLATFORM_ESP32 and number in _ESP32S3R8_PSRAM_PINS
):
_LOGGER.warning(
"GPIO%d is used by the PSRAM interface in octal mode and should be avoided",
number,
)
@@ -28,9 +28,6 @@ namespace esphome::espnow {
static constexpr const char *TAG = "espnow";
static const esp_err_t CONFIG_ESPNOW_WAKE_WINDOW = 50;
static const esp_err_t CONFIG_ESPNOW_WAKE_INTERVAL = 100;
ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static const LogString *espnow_error_to_str(esp_err_t error) {
@@ -204,11 +201,6 @@ void ESPNowComponent::enable_() {
esp_wifi_get_mac(WIFI_IF_STA, this->own_address_);
#ifdef USE_DEEP_SLEEP
esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW);
esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL);
#endif
this->state_ = ESPNOW_STATE_ENABLED;
for (auto peer : this->peers_) {
@@ -311,7 +303,9 @@ void ESPNowComponent::loop() {
ESP_LOGV(TAG, ">>> [%s] %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status)));
#endif
if (this->current_send_packet_ != nullptr) {
this->current_send_packet_->callback_(packet->packet_.sent.status);
if (this->current_send_packet_->callback_ != nullptr) {
this->current_send_packet_->callback_(packet->packet_.sent.status);
}
this->send_packet_pool_.release(this->current_send_packet_);
this->current_send_packet_ = nullptr; // Reset current packet after sending
}
+55
View File
@@ -126,6 +126,8 @@ ETHERNET_TYPES = {
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
"W6100": EthernetType.ETHERNET_TYPE_W6100,
"W6300": EthernetType.ETHERNET_TYPE_W6300,
"GENERIC": EthernetType.ETHERNET_TYPE_GENERIC,
"YT8531": EthernetType.ETHERNET_TYPE_YT8531,
}
# PHY types that need compile-time defines for conditional compilation
@@ -145,6 +147,8 @@ _PHY_TYPE_TO_DEFINE = {
"ENC28J60": "USE_ETHERNET_ENC28J60",
"W6100": "USE_ETHERNET_W6100",
"W6300": "USE_ETHERNET_W6300",
"GENERIC": "USE_ETHERNET_GENERIC",
"YT8531": "USE_ETHERNET_YT8531",
}
@@ -309,6 +313,24 @@ def _validate(config):
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]."
)
elif config[CONF_TYPE] in ("GENERIC", "YT8531"):
from esphome.components.esp32 import (
VARIANT_ESP32S31,
get_esp32_variant,
idf_version,
)
eth_type = config[CONF_TYPE]
variant = get_esp32_variant()
if variant != VARIANT_ESP32S31:
raise cv.Invalid(
f"The '{eth_type}' (RGMII) PHY is only supported on gigabit-capable "
f"variants (ESP32-S31), not {variant}"
)
if idf_version() < cv.Version(6, 0, 0):
raise cv.Invalid(
f"The '{eth_type}' (RGMII) PHY requires ESP-IDF 6.0 or newer."
)
elif config[CONF_TYPE] != "OPENETH":
from esphome.components.esp32 import (
VARIANT_ESP32,
@@ -392,6 +414,23 @@ RMII_SCHEMA = cv.All(
cv.only_on([Platform.ESP32]),
)
# Generic IEEE 802.3 PHY over the internal EMAC RGMII interface (e.g. ESP32-S31).
# RGMII data pins come from the IDF per-target default config.
GENERIC_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA),
}
)
),
cv.only_on([Platform.ESP32]),
)
SPI_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
@@ -442,6 +481,8 @@ CONFIG_SCHEMA = cv.All(
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"LAN8670": RMII_SCHEMA,
"GENERIC": GENERIC_SCHEMA,
"YT8531": GENERIC_SCHEMA,
},
upper=True,
),
@@ -571,6 +612,20 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
elif config[CONF_TYPE] == "OPENETH":
cg.add_define("USE_ETHERNET_OPENETH")
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
elif config[CONF_TYPE] in ("GENERIC", "YT8531"):
# RGMII data pins come from the IDF default config; set MDC/MDIO + PHY addr.
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN]))
if CONF_POWER_PIN in config:
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
for register_value in config.get(CONF_PHY_REGISTERS, []):
reg = phy_register(
register_value.get(CONF_ADDRESS),
register_value.get(CONF_VALUE),
register_value.get(CONF_PAGE_ID),
)
cg.add(var.add_phy_register(reg))
else:
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
@@ -86,6 +86,8 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_ENC28J60,
ETHERNET_TYPE_W6100,
ETHERNET_TYPE_W6300,
ETHERNET_TYPE_GENERIC,
ETHERNET_TYPE_YT8531,
};
struct ManualIP {
@@ -229,6 +231,11 @@ class EthernetComponent final : public Component {
#ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
#endif
#ifdef USE_ETHERNET_YT8531
/// @brief Apply YT8531-specific config: re-enable auto-negotiation (disabled on
/// reset) and set the RGMII Tx/Rx clock delays needed for reliable data sampling.
void yt8531_phy_init_();
#endif
/// @brief Set arbitratry PHY registers from config.
void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data);
@@ -254,9 +254,14 @@ void EthernetComponent::ethernet_lazy_init_() {
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
#endif
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio =
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
// The RGMII types (GENERIC, YT8531) use the RGMII interface and default GPIO map from
// eth_esp32_emac_default_config(); writing the RMII clock config would clobber that
// union, so skip the RMII clock override for them.
if (this->type_ != ETHERNET_TYPE_GENERIC && this->type_ != ETHERNET_TYPE_YT8531) {
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio =
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
}
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
#endif
@@ -319,6 +324,20 @@ void EthernetComponent::ethernet_lazy_init_() {
break;
}
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
// GENERIC and YT8531 both use the built-in generic 802.3 PHY driver; YT8531 gets
// extra chip-specific tuning applied later in ethernet_lazy_init_().
#ifdef USE_ETHERNET_GENERIC
case ETHERNET_TYPE_GENERIC:
#endif
#ifdef USE_ETHERNET_YT8531
case ETHERNET_TYPE_YT8531:
#endif
#if defined(USE_ETHERNET_GENERIC) || defined(USE_ETHERNET_YT8531)
this->phy_ = esp_eth_phy_new_generic(&phy_config);
break;
#endif
#endif
#endif
#ifdef USE_ETHERNET_SPI
#if defined(USE_ETHERNET_W5500)
@@ -363,7 +382,30 @@ void EthernetComponent::ethernet_lazy_init_() {
for (const auto &phy_register : this->phy_registers_) {
this->write_phy_register_(mac, phy_register);
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#ifdef USE_ETHERNET_GENERIC
// The generic 802.3 PHY driver only resets the PHY in its init; it never enables
// auto-negotiation. A PHY that resets into a forced-speed mode (BMCR auto-nego bit
// clear) therefore stays there, and esp_eth_start() skips negotiation because the
// driver cached auto_nego_en=false at install time. Force auto-negotiation on here
// (which also updates that cached state) so esp_eth_start() restarts a proper
// negotiation. (YT8531 does this as part of its own chip-specific init below.)
if (this->type_ == ETHERNET_TYPE_GENERIC) {
bool autoneg_enable = true;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_AUTONEGO, &autoneg_enable);
ESPHL_ERROR_CHECK(err, "Enable auto-negotiation failed");
}
#endif
#ifdef USE_ETHERNET_YT8531
if (this->type_ == ETHERNET_TYPE_YT8531) {
this->yt8531_phy_init_();
if (this->is_failed())
return;
}
#endif
#endif // ESP_IDF_VERSION >= 6.0.0
#endif // !USE_ETHERNET_SPI
// use ESP internal eth mac
uint8_t mac_addr[6];
@@ -486,6 +528,16 @@ void EthernetComponent::dump_config() {
eth_type = "LAN8670";
break;
#endif
#ifdef USE_ETHERNET_GENERIC
case ETHERNET_TYPE_GENERIC:
eth_type = "Generic (RGMII)";
break;
#endif
#ifdef USE_ETHERNET_YT8531
case ETHERNET_TYPE_YT8531:
eth_type = "YT8531 (RGMII)";
break;
#endif
default:
eth_type = "Unknown";
@@ -782,6 +834,19 @@ void EthernetComponent::dump_connect_params_() {
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
uint16_t link_speed = 10;
switch (this->get_link_speed()) {
case ETH_SPEED_100M:
link_speed = 100;
break;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
case ETH_SPEED_1000M:
link_speed = 1000;
break;
#endif
default:
break;
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
@@ -796,7 +861,7 @@ void EthernetComponent::dump_connect_params_() {
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), link_speed);
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
@@ -958,6 +1023,50 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
#endif
}
#ifdef USE_ETHERNET_YT8531
void EthernetComponent::yt8531_phy_init_() {
esp_err_t err;
// The YT8531 disables auto-negotiation on hardware reset (undocumented behavior), and the
// generic 802.3 driver only resets the PHY, so re-enable it (this also updates the driver's
// cached auto-nego state used by esp_eth_start()).
bool autoneg_enable = true;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_AUTONEGO, &autoneg_enable);
ESPHL_ERROR_CHECK(err, "YT8531 enable auto-negotiation failed");
// RGMII needs ~2 ns Tx and Rx clock delays for reliable data sampling. These are set through
// the YT8531 extended-register interface: write the ext-register address to 0x1E, then
// read/modify/write its value via 0x1F.
esp_eth_phy_reg_rw_data_t phy_reg;
uint32_t reg_val;
phy_reg.reg_value_p = &reg_val;
// RX ~2 ns coarse delay: EXT_CHIP_CONFIG (0xA001), set rxc_dly_en (bit 8).
reg_val = 0xA001;
phy_reg.reg_addr = 0x1E;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 select Chip_Config failed");
phy_reg.reg_addr = 0x1F;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_READ_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 read Chip_Config failed");
reg_val |= (1U << 8);
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 write Chip_Config failed");
// TX ~2 ns delay: EXT_RGMII_CONFIG1 (0xA003), tx_delay_sel[3:0] and tx_delay_sel_fe[7:4] = 13.
reg_val = 0xA003;
phy_reg.reg_addr = 0x1E;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 select RGMII_Config1 failed");
phy_reg.reg_addr = 0x1F;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_READ_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 read RGMII_Config1 failed");
reg_val = (reg_val & ~0x00FFU) | (13U << 4) | (13U << 0);
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_WRITE_PHY_REG, &phy_reg);
ESPHL_ERROR_CHECK(err, "YT8531 write RGMII_Config1 failed");
}
#endif
#endif
} // namespace esphome::ethernet
+2 -2
View File
@@ -139,7 +139,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
/// Draw grid
if (!std::isnan(this->gridspacing_y_)) {
for (int y = yn; y <= ym; y++) {
int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn)));
int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0f - (float) (y - yn) / (ym - yn)));
for (uint32_t x = 0; x < this->width_; x += 2) {
buff->draw_pixel_at(x_offset + x, y_offset + py, color);
}
@@ -177,7 +177,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
bool b = (trace->get_line_type() & bit) == bit;
if (b) {
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0f - v)) - thick / 2 + y_offset;
auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
if (y >= y_offset && static_cast<uint32_t>(y) < y_offset + this->height_)
buff->draw_pixel_at(x, y, c);
@@ -122,7 +122,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps,
rpm = clamp<uint16_t>(rpm, 1, 300);
ms_per_step = (uint16_t) (3000.0 / (float) rpm);
ms_per_step = (uint16_t) (3000.0f / (float) rpm);
buffer_[0] = mode;
buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw
buffer_[2] = steps;
@@ -153,7 +153,7 @@ void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t
uint16_t ms_per_step = 0;
rpm = clamp<uint16_t>(rpm, 1, 300);
ms_per_step = (uint16_t) (3000.0 / (float) rpm);
ms_per_step = (uint16_t) (3000.0f / (float) rpm);
buffer_[0] = mode;
buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw
+1 -1
View File
@@ -607,7 +607,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
if (climate_control.target_temperature.has_value()) {
float target_temp = climate_control.target_temperature.value();
out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49f) ? 1 : 0;
}
if (out_data->ac_power == 0) {
// If AC is off - no presets allowed
@@ -341,7 +341,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
if (climate_control.target_temperature.has_value()) {
float target_temp = climate_control.target_temperature.value();
out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49f) ? 1 : 0;
}
if (out_data->ac_power == 0) {
// If AC is off - no presets allowed
+3 -1
View File
@@ -2,6 +2,8 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <numbers>
namespace esphome::hmc5883l {
static const char *const TAG = "hmc5883l";
@@ -126,7 +128,7 @@ void HMC5883LComponent::update() {
const float y = int16_t(raw_y) * mg_per_bit * 0.1f;
const float z = int16_t(raw_z) * mg_per_bit * 0.1f;
float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v<float>;
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading);
if (this->x_sensor_ != nullptr)
@@ -55,7 +55,9 @@ float HONEYWELLABPSensor::countstopressure_(const int counts, const float min_pr
// Converts a digital temperature measurement in counts to temperature in C
// This will be invalid if sensore daoes not have temperature measurement capability
float HONEYWELLABPSensor::countstotemperatures_(const int counts) { return (((float) counts / 2047.0) * 200.0) - 50.0; }
float HONEYWELLABPSensor::countstotemperatures_(const int counts) {
return (((float) counts / 2047.0f) * 200.0f) - 50.0f;
}
// Pressure value from the most recent reading in units
float HONEYWELLABPSensor::read_pressure_() {
@@ -69,9 +71,9 @@ void HONEYWELLABPSensor::update() {
ESP_LOGV(TAG, "Update Honeywell ABP Sensor");
if (readsensor_() == 0) {
if (this->pressure_sensor_ != nullptr)
this->pressure_sensor_->publish_state(read_pressure_() * 1.0);
this->pressure_sensor_->publish_state(read_pressure_() * 1.0f);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(read_temperature_() * 1.0);
this->temperature_sensor_->publish_state(read_temperature_() * 1.0f);
}
}
@@ -55,7 +55,7 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
millimeters = millimeters * 10;
}
float meters = float(millimeters) / 1000.0;
float meters = float(millimeters) / 1000.0f;
ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters);
this->publish_state(meters);
} else {
@@ -139,7 +139,7 @@ void I2SAudioSpeakerBase::set_volume(float volume) {
this->volume_ = volume;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_ != nullptr) {
if (volume > 0.0) {
if (volume > 0.0f) {
this->audio_dac_->set_mute_off();
}
this->audio_dac_->set_volume(volume);
+1 -1
View File
@@ -119,7 +119,7 @@ void INA219Component::setup() {
}
this->calibration_lsb_ = lsb;
auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_));
auto calibration = uint32_t(0.04096f / (0.000001f * lsb * this->shunt_resistance_ohm_));
ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration);
if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) {
this->mark_failed();
+1 -1
View File
@@ -70,7 +70,7 @@ void INA226Component::setup() {
this->calibration_lsb_ = lsb;
auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f));
auto calibration = uint32_t(0.00512f / (lsb * this->shunt_resistance_ohm_ / 1000000.0f));
ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration);
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@Passific", "@koosoli", "@limengdu"]
+433
View File
@@ -0,0 +1,433 @@
"""
ESPHome configuration for the IT8951 e-paper controller.
"""
from esphome import automation, core, pins
import esphome.codegen as cg
from esphome.components import display, spi
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
import esphome.config_validation as cv
from esphome.config_validation import update_interval
from esphome.const import (
CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DATA_RATE,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_FULL_UPDATE_EVERY,
CONF_HEIGHT,
CONF_ID,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODE,
CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SLEEP_WHEN_DONE,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"]
CONF_VCOM = "vcom"
CONF_VCOM_REGISTER = "vcom_register"
CONF_FORCE_TEMPERATURE = "force_temperature"
CONF_GRAYSCALE = "grayscale"
CONF_DITHERING = "dithering"
CONF_UPDATE_MODE = "update_mode"
CONF_USE_LEGACY_DPY_AREA = "use_legacy_dpy_area"
# VCOM SET sub-command selectors. The IT8951 firmware accepts different
# values across panels; most respond to 0x0001, but a few — e.g. the Seeed
# reTerminal E1003 — only respond to 0x0002 and silently drop 0x0001.
VCOM_REGISTER_DEFAULT = 0x0001
VCOM_REGISTER_ALT = 0x0002
VCOM_REGISTER_OPTIONS = (VCOM_REGISTER_DEFAULT, VCOM_REGISTER_ALT)
it8951_ns = cg.esphome_ns.namespace("it8951")
IT8951Display = it8951_ns.class_("IT8951Display", display.Display, spi.SPIDevice)
IT8951UpdateAction = it8951_ns.class_("IT8951UpdateAction", automation.Action)
# Hardware waveform modes exposed to YAML. Strings are mapped to the C++
# UpdateMode enum so the runtime can store the mode as a uint16_t rather
# than a std::string (avoiding a heap-resident member; see ESPHome
# CLAUDE.md "STL Container Guidelines"). "fast" and "full" are
# convenience aliases for DU and GC16 respectively.
UpdateMode = it8951_ns.enum("UpdateMode")
UPDATE_MODE_OPTIONS = {
"INIT": UpdateMode.UPDATE_MODE_INIT,
"DU": UpdateMode.UPDATE_MODE_DU,
"GC16": UpdateMode.UPDATE_MODE_GC16,
"GL16": UpdateMode.UPDATE_MODE_GL16,
"GLR16": UpdateMode.UPDATE_MODE_GLR16,
"GLD16": UpdateMode.UPDATE_MODE_GLD16,
"DU4": UpdateMode.UPDATE_MODE_DU4,
"A2": UpdateMode.UPDATE_MODE_A2,
"FAST": UpdateMode.UPDATE_MODE_DU,
"FULL": UpdateMode.UPDATE_MODE_GC16,
}
# Maps the YAML mode string directly to the C++ UpdateMode enum value, so the
# config option and the it8951.update action share one validator.
update_mode = cv.enum(UPDATE_MODE_OPTIONS, upper=True)
# Transform flag values mirror the C++ TRANSFORM_* constants.
_TRANSFORM_NONE = 0
_TRANSFORM_MIRROR_X = 1
_TRANSFORM_MIRROR_Y = 2
_TRANSFORM_SWAP_XY = 4
_TRANSFORM_FLAGS = {
CONF_MIRROR_X: _TRANSFORM_MIRROR_X,
CONF_MIRROR_Y: _TRANSFORM_MIRROR_Y,
CONF_SWAP_XY: _TRANSFORM_SWAP_XY,
}
class IT8951Model:
"""A specific board / panel preset for the IT8951 controller."""
models: dict[str, "IT8951Model"] = {}
def __init__(self, name: str, **defaults):
name = name.upper()
self.name = name
self.defaults = defaults
IT8951Model.models[name] = self
def get_default(self, key, fallback=None):
return self.defaults.get(key, fallback)
def get_dimensions(self, config) -> tuple[int, int]:
# If dimensions are in config, use them; otherwise fall back to model defaults.
if CONF_DIMENSIONS in config:
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
return dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]
return tuple(dimensions)
# Model must have defaults if dimensions not in config.
return self.get_default(CONF_WIDTH), self.get_default(CONF_HEIGHT)
# --- Model presets ----------------------------------------------------------
# The generic model leaves dimensions and pin choices up to the user.
IT8951Model("it8951", vcom=2300, sleep_when_done=True, data_rate=12_000_000)
IT8951Model(
"m5stack-m5paper",
width=960,
height=540,
busy_pin=27,
reset_pin=23,
cs_pin=15,
vcom=2300,
sleep_when_done=True,
data_rate=20_000_000,
)
IT8951Model(
"seeed-reterminal-e1003",
width=1872,
height=1404,
busy_pin=13,
reset_pin=12,
cs_pin=10,
# Board power-enable rails: 1.8V logic supply (GPIO21) and the EPD supply
# (GPIO11). Driven high during setup so no separate power_supply is needed.
enable_pin=[21, 11],
vcom=1400,
# reTerminal E1003 panel firmware only accepts the 0x0002 VCOM SET
# selector; using the default 0x0001 leaves VCOM unchanged and breaks
# grayscale waveforms (GC16/GL16) — INIT still works because it does
# not depend on VCOM accuracy.
vcom_register=VCOM_REGISTER_ALT,
# The reTerminal E1003 ships with on-die temperature sensing disabled,
# so the host must declare an operating temperature; otherwise the
# waveform LUT defaults to a value that produces no visible change
# for grayscale modes.
force_temperature=25,
sleep_when_done=False,
data_rate=20_000_000,
mirror_x=True,
)
IT8951Model(
"seeed-ee03",
width=1872,
height=1404,
busy_pin=4,
reset_pin=38,
cs_pin=44,
vcom=1400,
sleep_when_done=False,
data_rate=4_000_000,
)
# ---------------------------------------------------------------------------
DIMENSION_SCHEMA = cv.Schema(
{
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
}
)
def _model_pin_option(model, key, schema):
default = model.get_default(key)
if default is None:
return cv.Required(key), schema
return cv.Optional(key, default=default), schema
def _model_schema(config):
model = IT8951Model.models[config[CONF_MODEL]]
has_default_dimensions = (
model.get_default(CONF_WIDTH) is not None
and model.get_default(CONF_HEIGHT) is not None
)
dimensions_key = (
cv.Optional(
CONF_DIMENSIONS,
default={
CONF_WIDTH: model.get_default(CONF_WIDTH),
CONF_HEIGHT: model.get_default(CONF_HEIGHT),
},
)
if has_default_dimensions
else cv.Required(CONF_DIMENSIONS)
)
schema = display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
)
).extend(
{
cv.GenerateID(): cv.declare_id(IT8951Display),
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True, space="-"),
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): update_interval,
cv.Optional(CONF_FULL_UPDATE_EVERY, default=30): cv.int_range(1, 255),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
}
),
cv.Optional(
CONF_INVERT_COLORS, default=model.get_default(CONF_INVERT_COLORS, False)
): cv.boolean,
cv.Optional(
CONF_SLEEP_WHEN_DONE,
default=model.get_default(CONF_SLEEP_WHEN_DONE, False),
): cv.boolean,
# Pixel format: true = 4bpp grayscale, false = packed 1bpp
# monochrome. Monochrome halves the framebuffer and enables fast DU
# partial refreshes; grayscale gives 16 levels but always uses GC16.
cv.Optional(
CONF_GRAYSCALE, default=model.get_default(CONF_GRAYSCALE, True)
): cv.boolean,
# Monochrome only: ordered-dither pale colours so they render as
# visible stipple. Disable for a crisp hard black/white threshold
# (better for purely black/white text). No effect in grayscale mode.
cv.Optional(
CONF_DITHERING, default=model.get_default(CONF_DITHERING, True)
): cv.boolean,
cv.Optional(
CONF_VCOM, default=model.get_default(CONF_VCOM, 2300)
): cv.int_range(0, 5000),
cv.Optional(
CONF_VCOM_REGISTER,
default=model.get_default(CONF_VCOM_REGISTER, VCOM_REGISTER_DEFAULT),
): cv.one_of(*VCOM_REGISTER_OPTIONS, int=True),
**(
{
cv.Optional(
CONF_FORCE_TEMPERATURE,
default=model.get_default(CONF_FORCE_TEMPERATURE),
): cv.int_range(min=-40, max=85)
}
if model.get_default(CONF_FORCE_TEMPERATURE) is not None
else {}
),
cv.Optional(
CONF_USE_LEGACY_DPY_AREA,
default=model.get_default(CONF_USE_LEGACY_DPY_AREA, False),
): cv.boolean,
cv.Optional(CONF_UPDATE_MODE): update_mode,
# One or more GPIOs driven high during setup to power on the panel
# (e.g. board power-enable rails), before reset and init.
cv.Optional(
CONF_ENABLE_PIN, default=model.get_default(CONF_ENABLE_PIN, [])
): cv.ensure_list(pins.gpio_output_pin_schema),
cv.Optional(CONF_RESET_DURATION): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
dimensions_key: DIMENSION_SCHEMA,
}
)
# Pin options: required if the model doesn't supply a default.
pin_specs = (
(CONF_BUSY_PIN, pins.gpio_input_pin_schema),
(CONF_RESET_PIN, pins.gpio_output_pin_schema),
(CONF_CS_PIN, pins.gpio_output_pin_schema),
)
pin_extra = {}
for key, schema_value in pin_specs:
opt, sv = _model_pin_option(model, key, schema_value)
pin_extra[opt] = sv
return schema.extend(pin_extra)
def _customise_schema(config):
config = cv.Schema(
{
cv.Required(CONF_MODEL): cv.one_of(
*IT8951Model.models, upper=True, space="-"
)
},
extra=cv.ALLOW_EXTRA,
)(config)
model_config = _model_schema(config)(config)
model = IT8951Model.models[config[CONF_MODEL].upper()]
width, height = model.get_dimensions(model_config)
display.add_metadata(
model_config[CONF_ID],
width,
height,
# Rotation is applied per-pixel in draw_pixel_at at no extra cost, so we
# advertise hardware rotation: LVGL routes its rotation to the driver via
# set_rotation rather than rotating the framebuffer in software.
has_hardware_rotation=True,
has_writer=any(
model_config.get(key)
for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
),
# Report the configured rotation so LVGL can detect (and reject) a
# rotation set in the display config instead of the LVGL config.
rotation=model_config.get(CONF_ROTATION, 0),
# The IT8951 snaps partial display refreshes to a 32-pixel X boundary
# (see prepare_update_region_), so have LVGL round its redraw areas to
# 32px too — this keeps flush rectangles aligned with what the panel
# actually refreshes and avoids redundant re-rounding/over-draw.
draw_rounding=32,
)
return model_config
CONFIG_SCHEMA = _customise_schema
def _final_validate(config):
# IT8951 reads from SPI (DevInfo, VCOM, register reads) so MISO is required.
spi.final_validate_device_schema("it8951", require_miso=True, 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 LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
config[CONF_SHOW_TEST_CARD] = True
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
model = IT8951Model.models[config[CONF_MODEL]]
width, height = model.get_dimensions(config)
var = cg.new_Pvariable(config[CONF_ID], model.name, width, height)
await display.register_display(var, config)
await spi.register_spi_device(var, config, write_only=False)
if lambda_config := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
if reset_pin := config.get(CONF_RESET_PIN):
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
if busy_pin := config.get(CONF_BUSY_PIN):
cg.add(var.set_busy_pin(await cg.gpio_pin_expression(busy_pin)))
if enable_pins := config.get(CONF_ENABLE_PIN):
cg.add(
var.set_enable_pins(
[await cg.gpio_pin_expression(pin) for pin in enable_pins]
)
)
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
if (reset_duration := config.get(CONF_RESET_DURATION)) is not None:
cg.add(var.set_reset_duration(reset_duration))
if config.get(CONF_INVERT_COLORS):
cg.add(var.set_invert_colors(True))
if config.get(CONF_SLEEP_WHEN_DONE):
cg.add(var.set_sleep_when_done(True))
cg.add(var.set_vcom(config[CONF_VCOM]))
cg.add(var.set_vcom_register(config[CONF_VCOM_REGISTER]))
if CONF_FORCE_TEMPERATURE in config:
cg.add(var.set_force_temperature(config[CONF_FORCE_TEMPERATURE]))
if config.get(CONF_USE_LEGACY_DPY_AREA):
cg.add(var.set_use_legacy_dpy_area(True))
cg.add(var.set_grayscale(config[CONF_GRAYSCALE]))
cg.add(var.set_dithering(config[CONF_DITHERING]))
if (mode := config.get(CONF_UPDATE_MODE)) is not None:
cg.add(var.set_update_mode(mode))
transform = config.get(
CONF_TRANSFORM,
{
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X),
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y),
},
)
transform_value = sum(
flag for key, flag in _TRANSFORM_FLAGS.items() if transform.get(key)
)
if transform_value:
cg.add(var.set_transform(RawExpression(str(transform_value))))
@automation.register_action(
"it8951.update",
IT8951UpdateAction,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(IT8951Display),
cv.Optional(CONF_MODE): cv.templatable(update_mode),
}
),
synchronous=True,
)
async def it8951_update_action_to_code(config, action_id, template_arg, args):
display_var = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, display_var)
if mode := config.get(CONF_MODE):
mode = await cg.templatable(mode, args, UpdateMode)
cg.add(var.set_mode(mode))
return var
File diff suppressed because it is too large Load Diff
+373
View File
@@ -0,0 +1,373 @@
#pragma once
#include <cstddef>
#include <utility>
#include <vector>
#include "esphome/components/display/display.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "it8951_defs.h"
namespace esphome::it8951 {
using namespace display;
// --- Bounded op queue --------------------------------------------------------
// Fixed-capacity ring buffer used by the loop scheduler. Replaces std::deque
// to comply with ESPHome's STL container guidelines (std::deque allocates in
// 512-byte blocks regardless of element size). Size analysis: the deepest
// observed scenario is UPDATE_REFRESH (10 enqueued ops) + CHECK_LUT_IDLE's
// 5 push_front rescheduling = 14 simultaneous entries. We use 32 for a
// comfortable margin while keeping RAM cost low (~192 bytes per instance vs
// 512+ bytes for std::deque).
template<typename T, size_t N> class StaticOpQueue {
public:
bool empty() const { return this->count_ == 0; }
size_t size() const { return this->count_; }
static constexpr size_t capacity() { return N; }
bool push_back(const T &value) {
if (this->count_ >= N)
return false;
this->data_[(this->head_ + this->count_) % N] = value;
++this->count_;
return true;
}
bool push_front(const T &value) {
if (this->count_ >= N)
return false;
this->head_ = (this->head_ + N - 1) % N;
this->data_[this->head_] = value;
++this->count_;
return true;
}
void pop_front() {
if (this->count_ == 0)
return;
this->head_ = (this->head_ + 1) % N;
--this->count_;
}
const T &front() const { return this->data_[this->head_]; }
T &front() { return this->data_[this->head_]; }
void clear() {
this->head_ = 0;
this->count_ = 0;
}
private:
T data_[N]{};
size_t head_{0};
size_t count_{0};
};
// Op queue capacity. See StaticOpQueue comment for sizing analysis.
static constexpr size_t OP_QUEUE_SIZE = 32;
// --- Op queue ---------------------------------------------------------------
// Each Op is a single CS-asserted SPI transaction (or a tiny bookkeeping
// step). The loop processes one Op per iteration after gating on HW_RDY, so
// the natural ESPHome loop cadence (~8-16 ms) provides inter-op pacing
// without any blocking waits.
//
// Compound Ops (READ_DEV_INFO, XFER_*, DPY_BUF_AREA, ENABLE_1BPP, ...) are
// short self-contained methods that do all their SPI work inside a single
// CS cycle (or a small handful of cycles) and complete well under 2ms, so
// they don't break the no-blocking budget.
//
// Each write-type op is a SINGLE CS-asserted transaction. The loop-level
// HW_RDY gate ensures the controller is ready before dispatching any op, so
// no blocking waits are needed within write ops.
//
// Read ops are decomposed: the command/address that triggers data preparation
// is sent as write ops (CMD, WRITE_W), then a separate read op runs only
// after the loop confirms HW_RDY is back HIGH (data ready). No blocking.
enum class OpType : uint8_t {
CMD, // single CS: CMD preamble + command word (a)
WRITE_W, // single CS: WRITE preamble + data word (a)
WRITE_REG, // single CS: WRITE preamble + addr(a) + value(b)
// (caller must enqueue CMD(TCON_REG_WR) before this)
READ_DEV_INFO, // single CS: READ preamble + dummy + read DevInfo struct
// (caller enqueues CMD(GET_DEV_INFO) first; loop HW_RDY gate
// ensures data is ready before this op runs)
READ_WORD, // single CS: READ preamble + dummy + read one 16-bit word
// into read_result_. Loop HW_RDY gate ensures data ready.
CHECK_LUT_IDLE, // checks read_result_; if non-zero, re-enqueues read sequence
SET_1BPP, // uses read_result_ to set UP1SR bit 2, enqueues writes
XFER_LISAR, // set image-buffer target address (2× reg write: 4 CS transactions)
XFER_AREA_CMD, // single CS: CMD preamble + TCON_LD_IMG_AREA
XFER_AREA_ARGS, // single CS: WRITE preamble + 5 area-parameter words
XFER_ROWS, // single CS: WRITE preamble + row pixel data (time-sliced)
XFER_AREA_END, // single CS: CMD preamble + TCON_LD_IMG_END
DPY_BUF_CMD, // single CS: CMD preamble + I80_CMD_DPY_BUF_AREA
DPY_BUF_ARGS, // single CS: WRITE preamble + 7 display-area words
GPIO_RESET_LOW, // drive RESET pin low
GPIO_RESET_HIGH, // drive RESET pin high
DELAY_MS, // park `delay_until_` for a few ms (no SPI)
};
struct Op {
OpType type;
uint16_t a{0};
uint16_t b{0};
};
// High-level controller phases. Each phase enqueues a sequence of Ops; when
// the queue drains, advance_phase_() runs the next phase.
// This separation keeps per-Op work tiny and predictable.
enum class Phase : uint8_t {
IDLE,
// Initialisation
INIT_RESET, // reset pulse + wake controller + packed-write enable
INIT_DEV_INFO, // GET_DEV_INFO and validate
INIT_VCOM, // write configured VCOM
INIT_TEMP, // force temperature for waveform LUT selection
INIT_DONE, // allocate framebuffer; transition to IDLE
// Update flow
UPDATE_PREPARE, // do_update_, compute dirty region, decide 4bpp/1bpp
UPDATE_TRANSFER, // one LD_IMG_AREA, time-sliced row streaming, one LD_IMG_END
UPDATE_REFRESH, // wait LUT idle, optionally enable 1bpp, send DPY_BUF_AREA
UPDATE_SLEEP, // optional deep sleep
};
class IT8951Display : public Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> {
public:
IT8951Display(const char *name, uint16_t width, uint16_t height) : name_(name), width_(width), height_(height) {
this->row_width_ = this->compute_row_width_();
this->buffer_length_ = static_cast<size_t>(this->row_width_) * static_cast<size_t>(height);
}
// --- Component lifecycle ---
void setup() override;
void loop() override;
void dump_config() override;
void on_safe_shutdown() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
// --- Config setters (called from generated code) ---
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
void set_busy_pin(GPIOPin *pin) { this->busy_pin_ = pin; }
void set_enable_pins(std::vector<GPIOPin *> pins) { this->enable_pins_ = std::move(pins); }
void set_reset_duration(uint32_t ms) { this->reset_duration_ = ms; }
void set_full_update_every(uint8_t n) {
this->full_update_every_ = n;
// Seed the counter so the very first update trips the full-update branch in
// prepare_update_region_, giving a freshly-booted panel a clean GC16 refresh
// before any partial (fast-waveform) updates begin.
this->partial_update_count_ = n;
}
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
void set_sleep_when_done(bool s) { this->sleep_when_done_ = s; }
void set_vcom(uint16_t vcom_mv) { this->vcom_ = vcom_mv; }
void set_vcom_register(uint16_t selector) { this->vcom_register_ = selector; }
void set_force_temperature(int16_t celsius) {
this->force_temperature_ = celsius;
this->force_temperature_set_ = true;
}
void set_use_legacy_dpy_area(bool use) { this->use_legacy_dpy_area_ = use; }
// Pixel format: true = 4bpp grayscale framebuffer, false = packed 1bpp
// monochrome framebuffer. Chosen at config time; the framebuffer is stored
// in this native format and every update uses the matching transfer path.
void set_grayscale(bool g) { this->grayscale_ = g; }
// Monochrome only: ordered-dither pale colours (true) vs a hard 50% threshold.
void set_dithering(bool d) { this->dithering_ = d; }
void set_update_mode(uint16_t m) { this->default_update_mode_ = static_cast<UpdateMode>(m); }
void set_transform(uint8_t t) {
this->transform_ = t;
this->update_effective_transform_();
}
void set_rotation(DisplayRotation rotation) override {
Display::set_rotation(rotation);
this->update_effective_transform_();
}
// --- Display API ---
void update() override;
void update_mode(UpdateMode mode);
DisplayType get_display_type() override { return this->grayscale_ ? DISPLAY_TYPE_GRAYSCALE : DISPLAY_TYPE_BINARY; }
void fill(Color color) override;
void clear() override { this->fill(Color::WHITE); }
void draw_pixel_at(int x, int y, Color color) override;
// Bulk pixel blit (used by LVGL and image rendering). Overridden to write
// straight into the framebuffer, avoiding the base class's per-pixel
// draw_pixel_at overhead (watchdog feed, clipping test, dirty-box clamps).
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;
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
protected:
int get_height_internal() override { return this->height_; }
int get_width_internal() override { return this->width_; }
// --- Coord transform / dirty region ---
void update_effective_transform_();
// Map display (logical) coordinates to native framebuffer coordinates by
// applying effective_transform_ (swap/mirror). Shared by rotate_coordinates_
// and the bulk draw_pixels_at path.
void apply_transform_(int &x, int &y) const;
bool rotate_coordinates_(int &x, int &y);
void reset_dirty_region_();
// --- Framebuffer geometry / monochrome packing ---
// Bytes per row for the configured pixel format: 4bpp grayscale packs two
// pixels per byte; monochrome packs eight bits per byte, rounded up to a
// whole 16-pixel group (matching the controller's 8bpp-load / 1bpp trick).
uint16_t compute_row_width_() const {
return this->grayscale_ ? static_cast<uint16_t>((static_cast<uint32_t>(this->width_) + 1) / 2)
: static_cast<uint16_t>(((static_cast<uint32_t>(this->width_) + 15) / 16) * 2);
}
void set_mono_pixel_(uint16_t x, uint16_t y, bool value) const;
// Write a 4bpp grayscale nibble into the framebuffer (two pixels per byte).
void set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const;
// Convert a color and write it at native framebuffer coordinates: a 4bpp
// nibble in grayscale mode, or an ordered-dithered bit in monochrome mode.
void write_pixel_native_(uint16_t x, uint16_t y, const Color &color) const;
// --- Op queue / loop machinery ---
void enqueue_(OpType type, uint16_t a = 0, uint16_t b = 0);
void prepend_(OpType type, uint16_t a = 0, uint16_t b = 0);
bool is_busy_() const;
void process_op_(const Op &op);
void advance_phase_();
void set_phase_(Phase next);
void start_update_(UpdateMode mode);
// --- SPI primitives (each is one CS-asserted burst, fully non-blocking) ---
void spi_cmd_(uint16_t cmd);
void spi_write_word_(uint16_t value);
void spi_write_reg_(uint16_t addr, uint16_t value);
void spi_write_args_(const uint16_t *args, uint16_t count);
uint16_t spi_read_word_(); // non-blocking: HW_RDY confirmed by loop gate
void spi_read_dev_info_(); // non-blocking: HW_RDY confirmed by loop gate
// --- Compound Ops (small bounded helpers) ---
void op_xfer_lisar_();
void op_xfer_area_args_();
void op_xfer_area_end_();
bool op_xfer_rows_(); // returns true when current update area fully sent
void op_dpy_buf_args_();
void op_check_lut_idle_();
void op_set_1bpp_();
// --- Phase enqueuers ---
void enqueue_init_reset_();
void enqueue_init_dev_info_();
void enqueue_init_vcom_();
void enqueue_init_temp_();
void enqueue_update_transfer_();
void enqueue_update_refresh_();
void enqueue_update_sleep_();
bool prepare_update_region_(UpdateMode &mode);
// --- Recovery ---
void recover_();
// --- State ---
static constexpr uint32_t BUSY_TIMEOUT_MS = 5000;
StaticOpQueue<Op, OP_QUEUE_SIZE> queue_;
Phase phase_{Phase::IDLE};
uint32_t delay_until_{0};
uint32_t phase_started_at_{0};
// Requests a continuous (non-throttled) main loop while streaming image data
// so 20ms transfer slices aren't separated by the ~16ms default loop interval.
HighFrequencyLoopRequester high_freq_;
// Pending update bookkeeping
bool update_pending_{false};
UpdateMode pending_update_mode_{UPDATE_MODE_NONE};
UpdateMode active_mode_{UPDATE_MODE_NONE};
uint16_t area_x_{0}, area_y_{0}, area_w_{0}, area_h_{0};
uint16_t transfer_row_{0};
bool initialised_{false};
// True once TCON_SLEEP has been sent and the controller has not been woken
// since. The next update must issue TCON_SYS_RUN before any SPI op.
bool asleep_{false};
uint32_t partial_update_count_{0};
uint32_t update_started_at_{0};
// Read result storage for decomposed read-modify-write op sequences
uint16_t read_result_{0};
// Device info
DevInfo dev_info_{};
uint16_t img_buf_addr_l_{0};
uint16_t img_buf_addr_h_{0};
// Configured properties
const char *name_;
uint16_t width_;
uint16_t height_;
uint16_t row_width_;
size_t buffer_length_{};
uint8_t *buffer_{};
uint8_t transform_{0};
uint8_t effective_transform_{0};
uint8_t full_update_every_{1};
uint32_t reset_duration_{10};
uint16_t vcom_{2300};
uint16_t vcom_register_{I80_CMD_VCOM_WRITE};
int16_t force_temperature_{DEFAULT_FORCE_TEMP_C};
bool force_temperature_set_{false};
bool use_legacy_dpy_area_{false};
bool invert_colors_{false};
bool sleep_when_done_{false};
// Pixel format selector (see set_grayscale): true = 4bpp grayscale,
// false = packed 1bpp monochrome.
bool grayscale_{true};
// Monochrome dithering (see set_dithering): true = ordered dither.
bool dithering_{true};
UpdateMode default_update_mode_{UPDATE_MODE_NONE};
GPIOPin *reset_pin_{nullptr};
GPIOPin *busy_pin_{nullptr};
// GPIOs driven high during setup to power on the panel (empty if unused).
std::vector<GPIOPin *> enable_pins_;
// Dirty region (pixel coordinates of bounding box of changes since last update)
uint16_t x_low_{0}, y_low_{0}, x_high_{0}, y_high_{0};
// Saved data rate so we can probe slow then run fast
uint32_t configured_data_rate_{0};
// Consecutive recovery attempts; used to give up rather than infinite-loop
// when the controller is unresponsive (e.g. wiring issue).
uint8_t recovery_attempts_{0};
// DevInfo read retry counter (controller often returns garbage on the first
// read after reset; the original driver retried up to 3 times with 100ms
// between attempts).
uint8_t dev_info_attempts_{0};
};
// --- Automation action ---
template<typename... Ts> class IT8951UpdateAction : public Action<Ts...> {
public:
explicit IT8951UpdateAction(IT8951Display *display) : display_(display) {}
TEMPLATABLE_VALUE(UpdateMode, mode)
protected:
void play(const Ts &...x) override {
if (!this->display_->is_ready())
return;
if (this->mode_.has_value()) {
this->display_->update_mode(this->mode_.value(x...));
} else {
this->display_->update();
}
}
IT8951Display *display_;
};
} // namespace esphome::it8951
+168
View File
@@ -0,0 +1,168 @@
#pragma once
#include <cstdint>
namespace esphome::it8951 {
struct DevInfo {
uint16_t panel_width{0};
uint16_t panel_height{0};
uint16_t img_buf_addr_l{0};
uint16_t img_buf_addr_h{0};
uint16_t fw_version[8]{};
uint16_t lut_version[8]{};
};
// --- IT8951 SPI packet preambles ---
static constexpr uint16_t PACKET_TYPE_CMD = 0x6000;
static constexpr uint16_t PACKET_TYPE_WRITE = 0x0000;
static constexpr uint16_t PACKET_TYPE_READ = 0x1000;
// --- Built-in I80 commands ---
static constexpr uint16_t TCON_SYS_RUN = 0x0001;
static constexpr uint16_t TCON_STANDBY = 0x0002;
static constexpr uint16_t TCON_SLEEP = 0x0003;
static constexpr uint16_t TCON_REG_RD = 0x0010;
static constexpr uint16_t TCON_REG_WR = 0x0011;
static constexpr uint16_t TCON_LD_IMG = 0x0020;
static constexpr uint16_t TCON_LD_IMG_AREA = 0x0021;
static constexpr uint16_t TCON_LD_IMG_END = 0x0022;
// --- I80 user-defined commands ---
static constexpr uint16_t I80_CMD_DPY_AREA = 0x0034;
static constexpr uint16_t I80_CMD_GET_DEV_INFO = 0x0302;
static constexpr uint16_t I80_CMD_DPY_BUF_AREA = 0x0037;
static constexpr uint16_t I80_CMD_VCOM = 0x0039;
static constexpr uint16_t I80_CMD_VCOM_READ = 0x0000;
// VCOM write selectors. Different IT8951-driven panels accept different
// selector values for the VCOM SET sub-command. Most panels (m5stack-m5paper,
// generic dev kits) accept 0x0001. Some panels — notably the Seeed
// reTerminal E1003 — only respond to selector 0x0002 and silently ignore
// 0x0001, leaving VCOM at its default and making grayscale waveforms
// (GC16/GL16) ineffective even though INIT still works.
static constexpr uint16_t I80_CMD_VCOM_WRITE = 0x0001;
static constexpr uint16_t I80_CMD_VCOM_WRITE_ALT = 0x0002;
// Force temperature command. The IT8951 selects waveform LUTs based on
// panel temperature; if it is left at the controller default, panels with
// auto-temperature disabled (notably the Seeed reTerminal E1003) will
// run waveforms against a mismatched LUT, leaving pixels visually
// unchanged even though the LUT engine completes a full cycle. The
// selector word selects the operation (0x0001 = write); the value word
// is the temperature in degrees Celsius.
static constexpr uint16_t I80_CMD_FORCE_TEMP = 0x0040;
static constexpr uint16_t I80_CMD_FORCE_TEMP_WRITE = 0x0001;
static constexpr int16_t DEFAULT_FORCE_TEMP_C = 25;
// --- Pixel mode (bits per pixel encoding) ---
static constexpr uint8_t PIXEL_2BPP = 0;
static constexpr uint8_t PIXEL_3BPP = 1;
static constexpr uint8_t PIXEL_4BPP = 2;
static constexpr uint8_t PIXEL_8BPP = 3;
// --- Endian flags for LD_IMG_AREA ---
static constexpr uint8_t LDIMG_L_ENDIAN = 0;
static constexpr uint8_t LDIMG_B_ENDIAN = 1;
// --- SPI probe frequency used for initial controller handshake ---
static constexpr uint32_t SPI_PROBE_FREQUENCY = 1'000'000;
// --- Refresh modes ---
/*
INIT The initialization (INIT) mode is
used to completely erase the display and leave it in the white state. It is
useful for situations where the display information in memory is not a faithful
representation of the optical state of the display, for example, after the
device receives power after it has been fully powered down. This waveform
switches the display several times and leaves it in the white state.
DU
The direct update (DU) is a very fast, non-flashy update. This mode supports
transitions from any graytone to black or white only. It cannot be used to
update to any graytone other than black or white. The fast update time for this
mode makes it useful for response to touch sensor or pen input or menu selection
indictors.
GC16
The grayscale clearing (GC16) mode is used to update the full display and
provide a high image quality. When GC16 is used with Full Display Update the
entire display will update as the new image is written. If a Partial Update
command is used the only pixels with changing graytone values will update. The
GC16 mode has 16 unique gray levels.
GL16
The GL16 waveform is primarily used to update sparse content on a white
background, such as a page of anti-aliased text, with reduced flash. The
GL16 waveform has 16 unique gray levels.
GLR16
The GLR16 mode is used in conjunction with an image preprocessing algorithm to
update sparse content on a white background with reduced flash and reduced image
artifacts. The GLR16 mode supports 16 graytones. If only the even pixel states
are used (0, 2, 4, 30), the mode will behave exactly as a traditional GL16
waveform mode. If a separately-supplied image preprocessing algorithm is used,
the transitions invoked by the pixel states 29 and 31 are used to improve
display quality. For the AF waveform, it is assured that the GLR16 waveform data
will point to the same voltage lists as the GL16 data and does not need to be
stored in a separate memory.
GLD16
The GLD16 mode is used in conjunction with an image preprocessing algorithm to
update sparse content on a white background with reduced flash and reduced image
artifacts. It is recommended to be used only with the full display update. The
GLD16 mode supports 16 graytones. If only the even pixel states are used (0, 2,
4, 30), the mode will behave exactly as a traditional GL16 waveform mode. If a
separately-supplied image preprocessing algorithm is used, the transitions
invoked by the pixel states 29 and 31 are used to refresh the background with a
lighter flash compared to GC16 mode following a predetermined pixel map as
encoded in the waveform file, and reduce image artifacts even more compared to
the GLR16 mode. For the AF waveform, it is assured that the GLD16 waveform data
will point to the same voltage lists as the GL16 data and does not need to be
stored in a separate memory.
DU4
The DU4 is a fast update time (similar to DU), non-flashy waveform. This mode
supports transitions from any gray tone to gray tones 1,6,11,16 represented by
pixel states [0 10 20 30]. The combination of fast update time and four gray
tones make it useful for anti-aliased text in menus. There is a moderate
increase in ghosting compared with GC16.
A2
The A2 mode is a fast, non-flash update mode designed for fast paging turning or
simple black/white animation. This mode supports transitions from and to black
or white only. It cannot be used to update to any graytone other than black or
white. The recommended update sequence to transition into repeated A2 updates is
shown in Figure 1. The use of a white image in the transition from 4-bit to
1-bit images will reduce ghosting and improve image quality for A2 updates.
*/
enum UpdateMode : uint16_t {
UPDATE_MODE_INIT = 0,
UPDATE_MODE_DU = 1,
UPDATE_MODE_GC16 = 2,
UPDATE_MODE_GL16 = 3,
UPDATE_MODE_GLR16 = 4,
UPDATE_MODE_GLD16 = 5,
UPDATE_MODE_DU4 = 6,
UPDATE_MODE_A2 = 7,
UPDATE_MODE_NONE = 8,
};
// --- Registers ---
static constexpr uint16_t DISPLAY_REG_BASE = 0x1000;
static constexpr uint16_t UP1SR = DISPLAY_REG_BASE + 0x138;
static constexpr uint16_t LUTAFSR = DISPLAY_REG_BASE + 0x224;
static constexpr uint16_t BGVR = DISPLAY_REG_BASE + 0x250;
static constexpr uint16_t I80CPCR = 0x0004;
static constexpr uint16_t MCSR_BASE_ADDR = 0x0200;
static constexpr uint16_t LISAR = MCSR_BASE_ADDR + 0x0008;
// Display orientation flags
static constexpr uint8_t TRANSFORM_NONE = 0;
static constexpr uint8_t TRANSFORM_MIRROR_X = 1;
static constexpr uint8_t TRANSFORM_MIRROR_Y = 2;
static constexpr uint8_t TRANSFORM_SWAP_XY = 4;
} // namespace esphome::it8951
+5 -5
View File
@@ -211,14 +211,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"dev": (cv.Version(1, 13, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
),
"recommended": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
),
}
@@ -359,7 +359,9 @@ if __name__ == "__main__":
check_base_code(BASE_CODE_INIT)
# list all boards from ltchiptool
components_dir = Path(__file__).parent.parent
boards = [Board(b) for b in Board.get_list()]
# Board.get_list() returns glob (filesystem) order, which is non-deterministic
# and produces noisy diffs on regeneration; sort by board id for stable output.
boards = sorted((Board(b) for b in Board.get_list()), key=lambda b: b.name)
# keep track of all supported root- and chip-families
components = set()
families = {}
@@ -315,14 +315,14 @@ class LightColorValues {
if (this->color_temperature_ <= 0) {
return this->color_temperature_;
}
return 1000000.0 / this->color_temperature_;
return 1000000.0f / this->color_temperature_;
}
/// Set the color temperature property of these light color values in kelvin.
void set_color_temperature_kelvin(float color_temperature) {
if (color_temperature <= 0) {
return;
}
this->color_temperature_ = 1000000.0 / color_temperature;
this->color_temperature_ = 1000000.0f / color_temperature;
}
/// Get the cold white property of these light color values. In range 0.0 to 1.0.
+1 -1
View File
@@ -47,7 +47,7 @@ class LightTransitionTransformer : public LightTransformer {
LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_;
LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->end_values_;
if (this->changing_color_mode_)
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
p = p < 0.5f ? p * 2 : (p - 0.5f) * 2;
float v = LightTransformer::smoothed_progress(p);
return LightColorValues::lerp(start, end, v);
+412 -85
View File
@@ -15,26 +15,38 @@ Any manual changes WILL BE LOST on regeneration.
from esphome.components.libretiny.const import FAMILY_LN882H
LN882X_BOARDS = {
"generic-ln882hki": {
"name": "Generic - LN882HKI",
"generic-ln882h": {
"name": "Generic - LN882H",
"family": FAMILY_LN882H,
},
"wb02a": {
"name": "WB02A Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2s": {
"name": "WL2S Wi-Fi/BLE Module",
"generic-ln882h-tuya": {
"name": "Generic - LN882H (Tuya)",
"family": FAMILY_LN882H,
},
"ln-02": {
"name": "LN-02 Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"ln-cb3s-v1.0": {
"name": "LN-CB3S V1.0",
"family": FAMILY_LN882H,
},
"wb02a": {
"name": "WB02A Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2h-u": {
"name": "WL2H-U Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2s": {
"name": "WL2S Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
}
LN882X_BOARD_PINS = {
"generic-ln882hki": {
"generic-ln882h": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
@@ -153,27 +165,292 @@ LN882X_BOARD_PINS = {
"A6": 20,
"A7": 21,
},
"generic-ln882h-tuya": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 7,
"WIRE0_SCL_8": 8,
"WIRE0_SCL_9": 9,
"WIRE0_SCL_10": 10,
"WIRE0_SCL_11": 11,
"WIRE0_SCL_12": 12,
"WIRE0_SCL_13": 19,
"WIRE0_SCL_14": 20,
"WIRE0_SCL_15": 21,
"WIRE0_SCL_16": 22,
"WIRE0_SCL_17": 23,
"WIRE0_SCL_18": 24,
"WIRE0_SCL_19": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 7,
"WIRE0_SDA_8": 8,
"WIRE0_SDA_9": 9,
"WIRE0_SDA_10": 10,
"WIRE0_SDA_11": 11,
"WIRE0_SDA_12": 12,
"WIRE0_SDA_13": 19,
"WIRE0_SDA_14": 20,
"WIRE0_SDA_15": 21,
"WIRE0_SDA_16": 22,
"WIRE0_SDA_17": 23,
"WIRE0_SDA_18": 24,
"WIRE0_SDA_19": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC5": 19,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PB03": 19,
"PB3": 19,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB07": 23,
"PB7": 23,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"TX0": 2,
"TX1": 25,
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 5,
"D6": 6,
"D7": 7,
"D8": 8,
"D9": 9,
"D10": 10,
"D11": 11,
"D12": 12,
"D13": 19,
"D14": 20,
"D15": 21,
"D16": 22,
"D17": 23,
"D18": 24,
"D19": 25,
"A2": 0,
"A3": 1,
"A4": 4,
"A5": 19,
"A6": 20,
"A7": 21,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC5": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB03": 19,
"PB3": 19,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"SCL0": 9,
"SDA0": 9,
"TX0": 2,
"TX1": 25,
"D0": 11,
"D1": 19,
"D2": 3,
"D3": 24,
"D4": 2,
"D5": 25,
"D6": 1,
"D7": 0,
"D8": 9,
"A0": 19,
"A1": 1,
"A2": 0,
},
"ln-cb3s-v1.0": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 9,
"WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 20,
"WIRE0_SCL_10": 21,
"WIRE0_SCL_11": 22,
"WIRE0_SCL_12": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 9,
"WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 20,
"WIRE0_SDA_10": 21,
"WIRE0_SDA_11": 22,
"WIRE0_SDA_12": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"TX0": 2,
"TX1": 25,
"D0": 0,
"D1": 1,
"D2": 4,
"D3": 5,
"D4": 6,
"D5": 20,
"D6": 25,
"D7": 9,
"D8": 21,
"D9": 22,
"D10": 3,
"D11": 2,
"D12": 11,
"A0": 0,
"A1": 1,
"A2": 4,
"A3": 20,
"A4": 21,
},
"wb02a": {
"WIRE0_SCL_0": 1,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 4,
"WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 24,
"WIRE0_SCL_9": 25,
"WIRE0_SCL_5": 6,
"WIRE0_SCL_6": 7,
"WIRE0_SCL_7": 9,
"WIRE0_SCL_8": 10,
"WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 25,
"WIRE0_SDA_0": 1,
"WIRE0_SDA_1": 2,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 4,
"WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 24,
"WIRE0_SDA_9": 25,
"WIRE0_SDA_5": 6,
"WIRE0_SDA_6": 7,
"WIRE0_SDA_7": 9,
"WIRE0_SDA_8": 10,
"WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
@@ -190,6 +467,8 @@ LN882X_BOARD_PINS = {
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA09": 9,
@@ -206,18 +485,128 @@ LN882X_BOARD_PINS = {
"TX0": 2,
"TX1": 25,
"D0": 7,
"D1": 5,
"D1": 6,
"D2": 3,
"D3": 10,
"D4": 2,
"D5": 1,
"D6": 4,
"D7": 9,
"D8": 24,
"D9": 25,
"D7": 5,
"D8": 9,
"D9": 24,
"D10": 25,
"A0": 1,
"A1": 4,
},
"wl2h-u": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 7,
"WIRE0_SCL_8": 10,
"WIRE0_SCL_9": 11,
"WIRE0_SCL_10": 12,
"WIRE0_SCL_11": 19,
"WIRE0_SCL_12": 20,
"WIRE0_SCL_13": 21,
"WIRE0_SCL_14": 22,
"WIRE0_SCL_15": 23,
"WIRE0_SCL_16": 24,
"WIRE0_SCL_17": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 7,
"WIRE0_SDA_8": 10,
"WIRE0_SDA_9": 11,
"WIRE0_SDA_10": 12,
"WIRE0_SDA_11": 19,
"WIRE0_SDA_12": 20,
"WIRE0_SDA_13": 21,
"WIRE0_SDA_14": 22,
"WIRE0_SDA_15": 23,
"WIRE0_SDA_16": 24,
"WIRE0_SDA_17": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC5": 19,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PB03": 19,
"PB3": 19,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB07": 23,
"PB7": 23,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"TX0": 2,
"TX1": 25,
"D0": 5,
"D1": 6,
"D2": 4,
"D3": 1,
"D4": 0,
"D5": 24,
"D6": 25,
"D7": 7,
"D8": 10,
"D9": 11,
"D10": 12,
"D11": 19,
"D12": 2,
"D13": 3,
"D14": 20,
"D15": 21,
"D16": 22,
"D17": 23,
"A0": 4,
"A1": 1,
"A2": 0,
"A3": 19,
"A4": 20,
"A5": 21,
},
"wl2s": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
@@ -298,68 +687,6 @@ LN882X_BOARD_PINS = {
"A1": 19,
"A2": 1,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC5": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB03": 19,
"PB3": 19,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"SCL0": 9,
"SDA0": 9,
"TX0": 2,
"TX1": 25,
"D0": 11,
"D1": 19,
"D2": 3,
"D3": 24,
"D4": 2,
"D5": 25,
"D6": 1,
"D7": 0,
"D8": 9,
"A0": 19,
"A1": 1,
"A2": 0,
},
}
BOARDS = LN882X_BOARDS
+1 -1
View File
@@ -75,7 +75,7 @@ void LTR390Component::read_als_() {
uint32_t als = *val;
if (this->light_sensor_ != nullptr) {
float lux = ((0.6 * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
float lux = ((0.6f * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_;
this->light_sensor_->publish_state(lux);
}
+6 -6
View File
@@ -500,12 +500,12 @@ void LTRAlsPs501Component::apply_lux_calculation_(AlsReadings &data) {
// method from
// https://github.com/fards/Ainol_fire_kernel/blob/83832cf8a3082fd8e963230f4b1984479d1f1a84/customer/drivers/lightsensor/ltr501als.c#L295
if (ratio < 0.45) {
lux = 1.7743 * ch0 + 1.1059 * ch1;
} else if (ratio < 0.64) {
lux = 3.7725 * ch0 - 1.3363 * ch1;
} else if (ratio < 0.85) {
lux = 1.6903 * ch0 - 0.1693 * ch1;
if (ratio < 0.45f) {
lux = 1.7743f * ch0 + 1.1059f * ch1;
} else if (ratio < 0.64f) {
lux = 3.7725f * ch0 - 1.3363f * ch1;
} else if (ratio < 0.85f) {
lux = 1.6903f * ch0 - 0.1693f * ch1;
} else {
ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
lux = 0.0f;
+6 -6
View File
@@ -480,12 +480,12 @@ void LTRAlsPsComponent::apply_lux_calculation_(AlsReadings &data) {
float inv_pfactor = this->glass_attenuation_factor_;
float lux = 0.0f;
if (ratio < 0.45) {
lux = (1.7743 * ch0 + 1.1059 * ch1);
} else if (ratio < 0.64 && ratio >= 0.45) {
lux = (4.2785 * ch0 - 1.9548 * ch1);
} else if (ratio < 0.85 && ratio >= 0.64) {
lux = (0.5926 * ch0 + 0.1185 * ch1);
if (ratio < 0.45f) {
lux = (1.7743f * ch0 + 1.1059f * ch1);
} else if (ratio < 0.64f && ratio >= 0.45f) {
lux = (4.2785f * ch0 - 1.9548f * ch1);
} else if (ratio < 0.85f && ratio >= 0.64f) {
lux = (0.5926f * ch0 + 0.1185f * ch1);
} else {
ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
lux = 0.0f;
+1 -1
View File
@@ -23,7 +23,7 @@ void MAX17043Component::update() {
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL"));
} else {
float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0;
float voltage = (1.25f * (float) (raw_voltage >> 4)) / 1000.0f;
this->voltage_sensor_->publish_state(voltage);
this->status_clear_warning();
}
+1 -1
View File
@@ -31,7 +31,7 @@ float MCP3204::read_data(uint8_t pin, bool differential) {
this->disable();
uint16_t digital_value = encode_uint16(b0, b1) >> 4;
return float(digital_value) / 4096.000 * this->reference_voltage_; // in V
return float(digital_value) / 4096.000f * this->reference_voltage_; // in V
}
} // namespace esphome::mcp3204
@@ -29,7 +29,9 @@ void Mcp4461Wiper::write_state(float state) {
}
}
float Mcp4461Wiper::read_state() { return (static_cast<float>(this->parent_->get_wiper_level_(this->wiper_)) / 256.0); }
float Mcp4461Wiper::read_state() {
return (static_cast<float>(this->parent_->get_wiper_level_(this->wiper_)) / 256.0f);
}
float Mcp4461Wiper::update_state() {
this->state_ = this->read_state();
+2 -1
View File
@@ -24,7 +24,8 @@ void MCP4725::dump_config() {
// https://learn.sparkfun.com/tutorials/mcp4725-digital-to-analog-converter-hookup-guide?_ga=2.176055202.1402343014.1607953301-893095255.1606753886
void MCP4725::write_state(float state) {
const uint16_t value = (uint16_t) round(state * (pow(2, MCP4725_RES) - 1));
constexpr uint16_t max_value = (1U << MCP4725_RES) - 1;
const uint16_t value = (uint16_t) roundf(state * max_value);
this->write_byte_16(64, value << 4);
}
+4 -3
View File
@@ -4,10 +4,11 @@
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
static const uint8_t MCP4725_ADDR = 0x60;
static const uint8_t MCP4725_RES = 12;
namespace esphome::mcp4725 {
static constexpr uint8_t MCP4725_ADDR = 0x60;
static constexpr uint8_t MCP4725_RES = 12;
class MCP4725 final : public Component, public output::FloatOutput, public i2c::I2CDevice {
public:
void setup() override;
+11 -11
View File
@@ -71,10 +71,10 @@ void MICS4514Component::update() {
float co = 0.0f;
if (red_f > 3.4f) {
co = 0.0;
} else if (red_f < 0.01) {
} else if (red_f < 0.01f) {
co = 1000.0;
} else {
co = 4.2 / pow(red_f, 1.2);
co = 4.2f / powf(red_f, 1.2f);
}
this->carbon_monoxide_sensor_->publish_state(co);
}
@@ -84,47 +84,47 @@ void MICS4514Component::update() {
if (ox_f < 0.3f) {
nitrogendioxide = 0.0;
} else {
nitrogendioxide = 0.164 * pow(ox_f, 0.975);
nitrogendioxide = 0.164f * powf(ox_f, 0.975f);
}
this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide);
}
if (this->methane_sensor_ != nullptr) {
float methane = 0.0f;
if (red_f > 0.9f || red_f < 0.5) { // outside the range->unlikely
if (red_f > 0.9f || red_f < 0.5f) { // outside the range->unlikely
methane = 0.0;
} else {
methane = 630 / pow(red_f, 4.4);
methane = 630 / powf(red_f, 4.4f);
}
this->methane_sensor_->publish_state(methane);
}
if (this->ethanol_sensor_ != nullptr) {
float ethanol = 0.0f;
if (red_f > 1.0f || red_f < 0.02) { // outside the range->unlikely
if (red_f > 1.0f || red_f < 0.02f) { // outside the range->unlikely
ethanol = 0.0;
} else {
ethanol = 1.52 / pow(red_f, 1.55);
ethanol = 1.52f / powf(red_f, 1.55f);
}
this->ethanol_sensor_->publish_state(ethanol);
}
if (this->hydrogen_sensor_ != nullptr) {
float hydrogen = 0.0f;
if (red_f > 0.9f || red_f < 0.02) { // outside the range->unlikely
if (red_f > 0.9f || red_f < 0.02f) { // outside the range->unlikely
hydrogen = 0.0;
} else {
hydrogen = 0.85 / pow(red_f, 1.75);
hydrogen = 0.85f / powf(red_f, 1.75f);
}
this->hydrogen_sensor_->publish_state(hydrogen);
}
if (this->ammonia_sensor_ != nullptr) {
float ammonia = 0.0f;
if (red_f > 0.98f || red_f < 0.2532) { // outside the ammonia range->unlikely
if (red_f > 0.98f || red_f < 0.2532f) { // outside the ammonia range->unlikely
ammonia = 0.0;
} else {
ammonia = 0.9 / pow(red_f, 4.6);
ammonia = 0.9f / powf(red_f, 4.6f);
}
this->ammonia_sensor_->publish_state(ammonia);
}
+3 -1
View File
@@ -1,6 +1,8 @@
#include "mmc5603.h"
#include "esphome/core/log.h"
#include <numbers>
namespace esphome::mmc5603 {
static const char *const TAG = "mmc5603";
@@ -143,7 +145,7 @@ void MMC5603Component::update() {
const float z = 0.00625 * (raw_z - 524288);
const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
const float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v<float>;
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading);
if (this->x_sensor_ != nullptr)
-1
View File
@@ -124,7 +124,6 @@ async def register_modbus_client_device(var, config):
async def register_modbus_server_device(var, config):
parent = await cg.get_variable(config[CONF_MODBUS_ID])
cg.add(var.set_parent(parent))
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(parent.register_device(var))
+136 -41
View File
@@ -92,14 +92,10 @@ int32_t Modbus::tx_delay_remaining() {
int32_t ModbusClientHub::tx_delay_remaining() {
const uint32_t now = millis();
// Turnaround delay only applies after a broadcast: no response is expected, so we must give listening devices
// quiet time to process it before the next request. For normal unicast request/response the received reply already
// provides the inter-frame timing, so adding turnaround there just throttles throughput.
const uint16_t turnaround = this->last_send_was_broadcast_ ? this->turnaround_delay_ms_ : 0;
return std::max(
{(int32_t) 0,
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + turnaround - (now - this->last_send_)),
(int32_t) (this->frame_delay_ms_ + turnaround - (now - this->last_modbus_byte_))});
return std::max({(int32_t) 0,
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ -
(now - this->last_send_)),
(int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))});
}
bool Modbus::tx_blocked() {
@@ -258,7 +254,7 @@ bool ModbusServerHub::parse_modbus_client_frame_() {
std::memcpy(data, this->rx_buffer_.data() + data_offset, data_len);
this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length);
this->process_modbus_client_frame_(address, function_code, data, data_len);
this->process_modbus_client_frame_(address, function_code, data);
return true;
}
@@ -321,10 +317,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t funct
}
void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *, uint16_t) {
for (auto *device : this->devices_) {
if (device->address_ == address) {
ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
}
if (this->find_device_(address) != nullptr) {
ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
}
if (this->expecting_peer_response_ == address) {
@@ -338,31 +332,124 @@ void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t funct
this->expecting_peer_response_ = 0;
}
void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data,
uint16_t len) {
bool found = false;
ModbusServerDevice *ModbusServerHub::find_device_(uint8_t address) {
for (auto *device : this->devices_) {
if (device->address_ == address) {
found = true;
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::READ_INPUT_REGISTERS) {
device->on_modbus_read_registers(function_code, helpers::get_data<uint16_t>(data, 0),
helpers::get_data<uint16_t>(data, 2));
} else if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
device->on_modbus_write_registers(function_code, std::vector<uint8_t>(data, data + len));
} else {
ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code);
device->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
}
if (device->get_address() == address) {
return device;
}
}
return nullptr;
}
if (!found) {
bool ModbusServerHub::check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
if ((uint32_t) start_address + number_of_registers > 0x10000u) {
ESP_LOGW(TAG, "Register address out of range - start: %" PRIu16 " num: %" PRIu16, start_address,
number_of_registers);
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return false;
}
return true;
}
void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data) {
ModbusServerDevice *device = this->find_device_(address);
if (device == nullptr) {
this->expecting_peer_response_ = address;
ESP_LOGV(TAG, "Request to peer %" PRIu8 " received", address);
return;
}
ServerResponseStatus status;
uint8_t response_buffer[modbus::MAX_RAW_SIZE];
const uint8_t *response_data = response_buffer;
uint16_t response_len = 0;
switch (static_cast<ModbusFunctionCode>(function_code)) {
case ModbusFunctionCode::READ_HOLDING_REGISTERS:
case ModbusFunctionCode::READ_INPUT_REGISTERS: {
// PDU data: start address(2) + quantity(2).
uint16_t start_address = helpers::get_data<uint16_t>(data, 0);
uint16_t number_of_registers = helpers::get_data<uint16_t>(data, 2);
if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16, number_of_registers);
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) {
return;
}
RegisterValues registers;
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS) {
status = device->on_modbus_read_holding_registers(start_address, number_of_registers, registers);
} else {
status = device->on_modbus_read_input_registers(start_address, number_of_registers, registers);
}
// A handler that returns an exception leaves registers partially filled, so check the exception
// first and forward it before validating the register count on the success path.
if (status.has_value()) {
this->send_exception_(address, function_code, status.value());
return;
}
if (registers.size() != number_of_registers) {
ESP_LOGE(TAG, "Incorrect response %" PRIu16 " requested, %zu returned", number_of_registers, registers.size());
this->send_exception_(address, function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return;
}
response_buffer[response_len++] = static_cast<uint8_t>(number_of_registers * 2); // actual byte count
for (auto r : registers) {
auto register_bytes = decode_value(r);
response_buffer[response_len++] = register_bytes[0];
response_buffer[response_len++] = register_bytes[1];
}
break;
}
case ModbusFunctionCode::WRITE_SINGLE_REGISTER:
case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS: {
// PDU data: start address(2) [+ quantity(2) + byte count(1)] + register values.
// A single-register write always targets one register; for a multiple-register write the
// quantity is in the frame and its byte count must equal quantity * 2. The register values are
// assembled into registers below so the handler doesn't have to know the request framing.
uint16_t start_address = helpers::get_data<uint16_t>(data, 0);
uint16_t number_of_registers = 1;
uint16_t values_offset = 2; // single write: values follow the 2-byte start address
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
number_of_registers = helpers::get_data<uint16_t>(data, 2);
uint8_t number_of_bytes = helpers::get_data<uint8_t>(data, 4);
values_offset = 5; // multiple write: values follow start address(2) + quantity(2) + byte count(1)
if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_WRITE ||
number_of_registers * 2 != number_of_bytes) {
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 " or bytes %" PRIu8, number_of_registers,
number_of_bytes);
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) {
return;
}
}
// Assemble the register values (host byte order) so the handler never sees wire framing.
RegisterValues registers;
for (uint16_t i = 0; i < number_of_registers; i++) {
registers.push_back(helpers::get_data<uint16_t>(data, values_offset + i * 2));
}
status = device->on_modbus_write_registers(start_address, registers);
response_data = data; // echo the request header per Modbus 6.6, 6.12
response_len = 4;
break;
}
default:
ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code);
this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
if (status.has_value()) {
this->send_exception_(address, function_code, status.value());
} else {
this->send_response_(address, function_code, response_data, response_len);
}
}
@@ -400,7 +487,6 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_,
now - this->last_modbus_byte_);
this->last_send_ = now;
this->last_send_was_broadcast_ = frame.size > 0 && frame.data[0] == 0;
return true;
}
@@ -416,8 +502,7 @@ void ModbusClientHub::send_next_frame_() {
ModbusDeviceCommand &command = this->tx_buffer_.front();
if (this->send_frame_(command.frame)) {
if (!this->last_send_was_broadcast_)
this->waiting_for_response_ = std::move(command);
this->waiting_for_response_ = std::move(command);
} else {
if (command.device)
command.device->on_modbus_not_sent();
@@ -455,17 +540,27 @@ float Modbus::get_setup_priority() const {
return setup_priority::BUS - 1.0f;
}
void ModbusServerHub::send(uint8_t address, uint8_t function_code, const std::vector<uint8_t> &payload) {
const uint16_t len = static_cast<uint16_t>(2 + payload.size());
if (len > MAX_RAW_SIZE) {
ESP_LOGE(TAG, "Server send frame too large (%" PRIu16 " bytes)", len);
void ModbusServerHub::send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload,
uint16_t payload_len) {
// Build the raw frame (address + function code + payload) in a stack buffer; it's consumed
// immediately by send_raw_ and a full raw frame never exceeds MAX_RAW_SIZE.
if (payload_len + 2 > MAX_RAW_SIZE) {
ESP_LOGE(TAG, "Server response too large (%" PRIu16 " bytes)", static_cast<uint16_t>(payload_len + 2));
return;
}
uint8_t raw_frame[MAX_RAW_SIZE];
raw_frame[0] = address;
raw_frame[1] = function_code;
std::memcpy(raw_frame + 2, payload.data(), payload.size());
this->send_raw_(raw_frame, len);
std::memcpy(raw_frame + 2, payload, payload_len);
this->send_raw_(raw_frame, payload_len + 2);
}
void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code) {
uint8_t raw_frame[3];
raw_frame[0] = address;
raw_frame[1] = function_code | FUNCTION_CODE_EXCEPTION_MASK;
raw_frame[2] = static_cast<uint8_t>(exception_code);
this->send_raw_(raw_frame, 3);
}
// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload.
+33 -28
View File
@@ -63,7 +63,6 @@ class Modbus : public uart::UARTDevice, public Component {
uint32_t last_receive_check_{0};
uint32_t last_send_{0};
uint32_t last_send_tx_offset_{0};
bool last_send_was_broadcast_{false};
uint16_t frame_delay_ms_{5};
uint16_t long_rx_buffer_delay_ms_{0};
@@ -130,22 +129,22 @@ class ModbusServerHub : public Modbus {
public:
ModbusServerHub() = default;
void dump_config() override;
void send(uint8_t address, uint8_t function_code, const std::vector<uint8_t> &payload);
ESPDEPRECATED("Use ModbusServerDevice::send_raw instead. Removed in 2026.10.0", "2026.4.0")
void send_raw(const std::vector<uint8_t> &payload) {
this->send_raw_(payload.data(), static_cast<uint16_t>(payload.size()));
};
void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); }
protected:
friend class ModbusServerDevice;
void parse_modbus_frames() override;
bool parse_modbus_client_frame_();
// Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here.
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override;
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len);
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data);
ModbusServerDevice *find_device_(uint8_t address);
// Returns true if [start_address, start_address + number_of_registers) fits in the 16-bit address space.
// On failure, logs and sends an ILLEGAL_DATA_ADDRESS exception to the client.
bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers);
void send_raw_(const uint8_t *payload, uint16_t len);
void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code);
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
uint8_t expecting_peer_response_{0};
std::vector<ModbusServerDevice *> devices_;
@@ -200,35 +199,41 @@ class ModbusClientDevice {
// This is for compatibility with external components using the former class name
using ModbusDevice = ModbusClientDevice;
// Result of a server register handler: std::nullopt means success, otherwise the Modbus exception code to return.
using ServerResponseStatus = std::optional<ModbusExceptionCode>;
// Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol
// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
// the capacity of this type.
using RegisterValues = StaticVector<uint16_t, MAX_NUM_OF_REGISTERS_TO_READ>;
class ModbusServerDevice {
public:
ModbusServerDevice() = default;
ModbusServerDevice(ModbusServerHub *parent, uint8_t address) : parent_(parent), address_(address) {}
virtual ~ModbusServerDevice() = default;
ModbusServerDevice() = default;
// Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
ModbusServerDevice(const ModbusServerDevice &) = delete;
ModbusServerDevice &operator=(const ModbusServerDevice &) = delete;
ModbusServerDevice(ModbusServerDevice &&) = delete;
ModbusServerDevice &operator=(ModbusServerDevice &&) = delete;
void set_parent(ModbusServerHub *parent) { this->parent_ = parent; }
void set_address(uint8_t address) { this->address_ = address; }
virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){};
virtual void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data){};
void send(uint8_t function, const std::vector<uint8_t> &payload) {
this->parent_->send(this->address_, function, payload);
}
void send_raw(const std::vector<uint8_t> &payload) {
this->parent_->send_raw_(payload.data(), static_cast<uint16_t>(payload.size()));
}
void send_error(uint8_t function_code, ModbusExceptionCode exception_code) {
uint8_t error_response[3] = {this->address_, uint8_t(function_code | FUNCTION_CODE_EXCEPTION_MASK),
static_cast<uint8_t>(exception_code)};
this->parent_->send_raw_(error_response, 3);
}
uint8_t get_address() const { return this->address_; }
virtual ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers,
RegisterValues &registers) {
return ModbusExceptionCode::ILLEGAL_FUNCTION;
};
virtual ServerResponseStatus on_modbus_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
RegisterValues &registers) {
return this->on_modbus_read_registers(start_address, number_of_registers, registers);
};
virtual ServerResponseStatus on_modbus_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
RegisterValues &registers) {
return this->on_modbus_read_registers(start_address, number_of_registers, registers);
};
virtual ServerResponseStatus on_modbus_write_registers(uint16_t start_address, const RegisterValues &registers) {
return ModbusExceptionCode::ILLEGAL_FUNCTION;
};
protected:
friend ModbusServerHub;
ModbusServerHub *parent_{nullptr};
uint8_t address_{0};
};
@@ -82,7 +82,7 @@ static constexpr uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D
// Smallest possible frame is 4 bytes (custom function with no data): address(1) + function(1) + CRC(2)
static constexpr uint16_t MIN_FRAME_SIZE = 4;
static constexpr uint16_t MAX_PDU_SIZE = 253; // Max PDU size is 256 - address(1) - CRC(2) = 253
static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 255 - CRC(2) = 254
static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 256 - CRC(2) = 254
static constexpr uint16_t MAX_FRAME_SIZE = 256;
/// End of Modbus definitions
} // namespace esphome::modbus
+32 -42
View File
@@ -101,53 +101,19 @@ static size_t required_payload_size(SensorValueType sensor_value_type) {
}
}
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
switch (value_type) {
case SensorValueType::U_WORD:
case SensorValueType::S_WORD:
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD:
case SensorValueType::FP32:
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::S_DWORD_R:
case SensorValueType::FP32_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
data.push_back((value & 0xFFFF000000000000) >> 48);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF000000000000) >> 48);
break;
default:
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type));
break;
}
void log_unsupported_value_type(SensorValueType value_type) {
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type));
}
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask, bool *error_return) {
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
// Validate offset against the buffer for all types, including RAW/unsupported, so
// a malformed or misconfigured frame still produces an error log.
if (static_cast<size_t>(offset) > data.size()) {
if (static_cast<size_t>(offset) > size) {
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
static_cast<unsigned int>(offset), data.size());
static_cast<unsigned int>(offset), size);
if (error_return)
*error_return = true;
return value;
@@ -158,10 +124,9 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
return value;
}
if (data.size() - offset < required_size) {
if (size - offset < required_size) {
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(),
required_size);
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), size, required_size);
if (error_return)
*error_return = true;
return value;
@@ -214,6 +179,31 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
return value;
}
int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type,
bool *error_return) {
const size_t required_size = required_payload_size(sensor_value_type);
if (required_size == 0) {
return 0; // RAW/unsupported: nothing to read
}
const size_t required_words = required_size / 2;
if (required_words > count) {
ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu",
static_cast<unsigned int>(sensor_value_type), count, required_words);
if (error_return)
*error_return = true;
return 0;
}
// Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the
// sign-extension behaviour stays identical to the wire path.
uint8_t bytes[8]; // at most 4 registers (QWORD)
for (size_t i = 0; i < required_words; i++) {
uint16_t reg = registers[i];
bytes[i * 2] = static_cast<uint8_t>(reg >> 8);
bytes[i * 2 + 1] = static_cast<uint8_t>(reg & 0xFF);
}
return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF, error_return);
}
StaticVector<uint8_t, MAX_PDU_SIZE> create_client_pdu(ModbusFunctionCode function_code, uint16_t start_address,
uint16_t number_of_entities, const uint8_t *values,
size_t values_len) {
+62 -9
View File
@@ -224,24 +224,77 @@ template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
return 0;
}
/** Convert float value to vector<uint16_t> suitable for sending
* @param data target for payload
* @param value float value to convert
* @param value_type defines if 16/32 or FP32 is used
* @return vector containing the modbus register words in correct order
*/
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
// Logs an error for an unsupported value type. Defined in the .cpp so logging stays out of headers.
void log_unsupported_value_type(SensorValueType value_type);
/** Convert vector<uint8_t> response payload to number.
/** Append the Modbus register words for value to data.
* Works with any container exposing push_back(uint16_t) (e.g. std::vector or StaticVector).
*/
template<typename Container> void number_to_payload(Container &data, int64_t value, SensorValueType value_type) {
switch (value_type) {
case SensorValueType::U_WORD:
case SensorValueType::S_WORD:
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD:
case SensorValueType::FP32:
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::S_DWORD_R:
case SensorValueType::FP32_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
data.push_back((value & 0xFFFF000000000000) >> 48);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF000000000000) >> 48);
break;
default:
log_unsupported_value_type(value_type);
break;
}
}
/** Convert a raw response payload to a number.
* @param data payload with the data to convert
* @param size number of bytes available in data
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
* @param offset offset to the data in data
* @param bitmask bitmask used for masking and shifting
* @return 64-bit number of the payload
*/
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask, bool *error_return = nullptr);
/** Convert vector<uint8_t> response payload to number. */
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask, bool *error_return = nullptr) {
return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask, error_return);
}
/** Reconstruct a number from register words (host byte order). Inverse of number_to_payload.
* Decodes the value at the start of the given span; advance the pointer to read successive values.
* @param registers register values in host byte order
* @param count number of registers available in registers
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
* @return 64-bit number of the registers
*/
int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type,
bool *error_return = nullptr);
/** Create a modbus clinet pdu for reading/writing single/multiple coils/register/inputs.
* @param function_code the modbus function code to use. One of:
* READ_COILS
@@ -3,26 +3,18 @@
#include "esphome/core/log.h"
namespace esphome::modbus_server {
using modbus::ModbusFunctionCode;
using modbus::ModbusExceptionCode;
using modbus::helpers::payload_to_number;
using modbus::helpers::registers_to_number;
static const char *const TAG = "modbus_server";
void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t start_address,
uint16_t number_of_registers,
modbus::RegisterValues &registers) {
ESP_LOGV(TAG,
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
"Received read holding/input registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%X.",
this->address_, start_address, number_of_registers);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 ". Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
@@ -36,10 +28,7 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value, value_buf, sizeof(value_buf)));
std::vector<uint16_t> payload;
payload.reserve(server_register->register_count * 2);
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
modbus::helpers::number_to_payload(registers, value, server_register->value_type);
current_address += server_register->register_count;
found = true;
break;
@@ -53,92 +42,37 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %" PRIu16 ".",
current_address, this->server_courtesy_response_.register_value);
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
registers.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // Just increment by 1, as the default response is a single register
} else {
ESP_LOGW(TAG,
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
current_address);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS;
}
}
}
std::vector<uint8_t> response;
if (number_of_registers != sixteen_bit_response.size())
ESP_LOGW(TAG, "Response size not matched to request register count.");
response.push_back(sixteen_bit_response.size() * 2); // actual byte count
for (auto v : sixteen_bit_response) {
auto decoded_value = decode_value(v);
response.push_back(decoded_value[0]);
response.push_back(decoded_value[1]);
}
this->send(function_code, response);
return {};
}
void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
uint16_t number_of_registers;
uint16_t payload_offset;
modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t start_address,
const modbus::RegisterValues &registers) {
// registers holds the values to write in host byte order; its size is the register count.
ESP_LOGV(TAG, "Received write registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%zX.",
this->address_, start_address, registers.size());
if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
if (data.size() < 5) {
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 ". Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
uint16_t payload_size = data[4];
if (payload_size != number_of_registers * 2) {
ESP_LOGW(TAG,
"Payload size of %" PRIu16 " bytes is not 2 times the number of registers (%" PRIu16
"). Sending exception response.",
payload_size, number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
if (data.size() < 5 + payload_size) {
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
5 + payload_size);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
payload_offset = 5;
} else if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
if (data.size() < 4) {
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = 1;
payload_offset = 2;
} else {
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
ESP_LOGD(TAG,
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
uint16_t offset = payload_offset;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
auto for_each_register =
[this, start_address,
&registers](const std::function<bool(ServerRegister *, uint16_t register_offset)> &callback) -> bool {
uint16_t register_offset = 0;
for (uint32_t current_address = start_address; current_address < start_address + registers.size();) {
bool ok = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
ok = callback(server_register, offset);
ok = callback(server_register, register_offset);
current_address += server_register->register_count;
offset += server_register->register_count * sizeof(uint16_t);
register_offset += server_register->register_count;
break;
}
}
@@ -150,36 +84,41 @@ void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::v
return true;
};
// check all registers are writable before writing to any of them:
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
return server_register->write_lambda != nullptr;
})) {
ESP_LOGW(TAG, "Invalid register address. Sending exception response.");
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
// Actually write to the registers:
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
bool error = false;
int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF, &error);
if (error) {
return false;
} else {
return server_register->write_lambda(number);
// Pre-flight: every targeted register must be writable AND have its full value present in the request,
// so we never apply a partial write before discovering a problem. The commit pass below re-runs
// registers_to_number rather than caching the decoded values: using the same function for the check and
// the write keeps a single source of truth for the decode bound, independent of how register_count was set.
ModbusExceptionCode precheck = ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; // unmatched or unwritable register
if (!for_each_register([&precheck, &registers](ServerRegister *server_register, uint16_t register_offset) -> bool {
if (server_register->write_lambda == nullptr) {
return false; // unwritable -> ILLEGAL_DATA_ADDRESS
}
bool error = false;
registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
server_register->value_type, &error);
if (error) {
precheck = ModbusExceptionCode::ILLEGAL_DATA_VALUE; // request doesn't supply the full value
return false;
}
return true;
})) {
ESP_LOGW(TAG, "Could not write all registers. Sending exception response.");
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return;
ESP_LOGW(TAG, "Write request rejected before applying any register. Sending exception response.");
return precheck;
}
std::vector<uint8_t> response;
response.reserve(6);
response.push_back(this->address_);
response.push_back(function_code);
response.insert(response.end(), data.begin(), data.begin() + 4);
this->send_raw(response);
// Commit: every value is known writable and decodable, so the only failure now is a user write callback
// rejecting the value at runtime -- which cannot be rolled back.
if (!for_each_register([&registers](ServerRegister *server_register, uint16_t register_offset) {
int64_t number = registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
server_register->value_type);
return server_register->write_lambda(number);
})) {
ESP_LOGW(TAG, "A register write callback failed mid-sequence; earlier writes were already applied.");
return ModbusExceptionCode::SERVICE_DEVICE_FAILURE;
}
// Success: the caller builds the write response (an echo of the request header).
return {};
}
void ModbusServer::dump_config() {
@@ -98,9 +98,11 @@ class ModbusServer : public Component, public modbus::ModbusServerDevice {
/// Registers a server register with the controller. Called by esphomes code generator
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
modbus::ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers,
modbus::RegisterValues &registers) final;
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
modbus::ServerResponseStatus on_modbus_write_registers(uint16_t start_address,
const modbus::RegisterValues &registers) final;
/// Called by esphome generated code to set the server courtesy response object
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
this->server_courtesy_response_ = server_courtesy_response;
+3 -3
View File
@@ -75,16 +75,16 @@ void MPL3115A2Component::update() {
float altitude = 0, pressure = 0;
if (this->altitude_ != nullptr) {
int32_t alt = encode_uint32(buffer[0], buffer[1], buffer[2], 0);
altitude = float(alt) / 65536.0;
altitude = float(alt) / 65536.0f;
this->altitude_->publish_state(altitude);
} else {
uint32_t p = encode_uint32(0, buffer[0], buffer[1], buffer[2]);
pressure = float(p) / 6400.0;
pressure = float(p) / 6400.0f;
if (this->pressure_ != nullptr)
this->pressure_->publish_state(pressure);
}
int16_t t = encode_uint16(buffer[3], buffer[4]);
float temperature = float(t) / 256.0;
float temperature = float(t) / 256.0f;
if (this->temperature_ != nullptr)
this->temperature_->publish_state(temperature);
+2 -2
View File
@@ -115,9 +115,9 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// max_temp
root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature();
// target_temp_step
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1;
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f;
// current_temp_step
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1;
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f;
// temperature units are always coerced to Celsius internally
root[MQTT_TEMPERATURE_UNIT] = "C";
+1 -1
View File
@@ -10,7 +10,7 @@ static const char *const TAG = "msa3xx";
const uint8_t MSA_3XX_PART_ID = 0x13;
const float GRAVITY_EARTH = 9.80665f;
const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9); // LSB to 1 LSB = 3.9mg = 0.0039g
const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9f); // LSB to 1 LSB = 3.9mg = 0.0039g
const float G_OFFSET_MIN = -4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
const float G_OFFSET_MAX = 4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
+21
View File
@@ -221,6 +221,27 @@ async def to_code(config):
zephyr_add_prj_conf("NET_IPV6", True)
zephyr_add_prj_conf("NET_TCP", True)
zephyr_add_prj_conf("NET_UDP", True)
# The nRF Connect SDK replaces mbedTLS with PSA/Oberon crypto and does not provide the
# legacy mbedtls_md5() symbol that Zephyr's RFC 6528 TCP ISN generator links against
# (selecting MBEDTLS_MAC_MD5_ENABLED does not bring in the legacy C API here). Disable it so
# TCP links; Zephyr falls back to sys_rand32_get() for the ISN (randomized, but not the
# RFC 6528 keyed hash).
zephyr_add_prj_conf("NET_TCP_ISN_RFC6528", False)
# Enlarge the Zephyr network buffer pool and TCP windows for the Thread path.
# Zephyr's defaults are tiny: NET_BUF_TX_COUNT=16 * NET_BUF_DATA_SIZE=128 is only
# ~2 KB of TX data -- barely one 1280-byte IPv6 packet once 6LoWPAN fragments it.
# The ESPHome API entity-sync burst overruns that instantly, so socket writes fail
# with ENOBUFS ("Buffer full") and the connection is dropped. ESP32 sidesteps this
# by enlarging the lwIP TCP window (CONFIG_LWIP_TCP_* above); give Zephyr the
# equivalent headroom, sized to RAM and the Thread 1280-byte MTU (not ESP32's 64 KB).
# The bounded send window also provides flow control so TCP stops queueing past
# what the buffer pool can hold instead of erroring.
zephyr_add_prj_conf("NET_PKT_RX_COUNT", 24)
zephyr_add_prj_conf("NET_PKT_TX_COUNT", 24)
zephyr_add_prj_conf("NET_BUF_RX_COUNT", 48)
zephyr_add_prj_conf("NET_BUF_TX_COUNT", 48)
zephyr_add_prj_conf("NET_TCP_MAX_RECV_WINDOW_SIZE", 2280)
zephyr_add_prj_conf("NET_TCP_MAX_SEND_WINDOW_SIZE", 2280)
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
@@ -176,7 +176,7 @@ void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_pr
void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("page", "page %i", page); }
void Nextion::set_backlight_brightness(float brightness) {
if (brightness < 0 || brightness > 1.0) {
if (brightness < 0 || brightness > 1.0f) {
ESP_LOGD(TAG, "Brightness out of bounds (0-1.0)");
return;
}
+164 -9
View File
@@ -4,6 +4,7 @@ import asyncio
import logging
from pathlib import Path
import re
import shutil
import subprocess
from esphome import pins
@@ -486,6 +487,16 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
from esphome.__main__ import check_permissions
from esphome.upload_targets import PortType, get_port_type
if KEY_ZEPHYR not in CORE.data:
platform_config = config.get(CORE.target_platform)
if not platform_config:
raise EsphomeError(
"nRF52 platform configuration is missing; "
"please re-validate and recompile."
)
set_core_data(platform_config)
set_framework(platform_config)
mcumgr_device: str | None = None
if get_port_type(host) == PortType.SERIAL:
@@ -494,17 +505,122 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
mcumgr_device = host
else:
if not CORE.using_toolchain_platformio:
raise EsphomeError("Not implemented yet")
result = _upload_using_platformio(config, host, ["-t", "upload"])
if result != 0:
raise EsphomeError(f"Upload failed with result: {result}")
return True # Handled: platformio serial upload
bootloader = zephyr_data()[KEY_BOOTLOADER]
if bootloader not in (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
):
raise EsphomeError("Not implemented yet")
check_and_install()
paths = get_build_paths()
env = get_build_env()
build_dir = CORE.relative_pioenvs_path(CORE.name)
dfu_package = build_dir / "firmware.zip"
if not dfu_package.is_file():
raise EsphomeError("Firmware not found. Please compile first.")
import time as _time
import serial as _serial
import serial.tools.list_ports as _list_ports
try:
ser = _serial.Serial(host, baudrate=1200, timeout=1)
ser.close()
except _serial.SerialException as err:
raise EsphomeError(f"Failed to open {host}: {err}") from err
# Wait for device to reset (port disappears)
deadline = _time.monotonic() + 5
while _time.monotonic() < deadline:
_time.sleep(0.1)
if host not in {p.device for p in _list_ports.comports()}:
break
else:
_LOGGER.warning(
"Device did not leave %s within 5 s; "
"it may not have entered bootloader mode",
host,
)
# Wait for DFU port to reappear
deadline = _time.monotonic() + 10
while _time.monotonic() < deadline:
_time.sleep(0.1)
if host in {p.device for p in _list_ports.comports()}:
break
else:
raise EsphomeError(
f"DFU port {host!r} did not reappear within 10 s. "
"Check that the device entered DFU mode."
)
# Wait for udev to finish setting up device permissions
deadline = _time.monotonic() + 5
while _time.monotonic() < deadline:
try:
check_permissions(host)
break
except EsphomeError:
_time.sleep(0.05)
else:
check_permissions(host) # raises with helpful message
python = str(paths["python_executable"])
if not run_command_ok(
[
python,
"-m",
"nordicsemi.__main__",
"dfu",
"serial",
"-pkg",
str(dfu_package),
"-p",
host,
"-b",
"115200",
"--singlebank",
],
env=env,
stream_output=True,
):
raise EsphomeError("nRF52 serial DFU upload failed")
else:
result = _upload_using_platformio(config, host, ["-t", "upload"])
if result != 0:
raise EsphomeError(f"Upload failed with result: {result}")
return True # Handled: serial upload
if host == "PYOCD":
result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"])
if result != 0:
raise EsphomeError(f"Upload failed with result: {result}")
return True # Handled: platformio PYOCD upload
if not CORE.using_toolchain_platformio:
check_and_install()
paths = get_build_paths()
env = get_build_env()
build_dir = CORE.relative_pioenvs_path(CORE.name)
west_cmd = [
str(paths["python_executable"]),
"-m",
"west",
"flash",
"--runner",
"pyocd",
"-d",
str(build_dir),
]
if not run_command_ok(
west_cmd,
env=env,
stream_output=True,
cwd=str(paths["framework_path"]),
):
raise EsphomeError("nRF52 pyocd flash failed")
else:
result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"])
if result != 0:
raise EsphomeError(f"Upload failed with result: {result}")
return True # Handled: PYOCD upload
# Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths
from .ble_logger import is_mac_address
@@ -662,4 +778,43 @@ def run_compile(args, config: ConfigType) -> bool:
):
raise EsphomeError("nRF52 native build failed")
# Zephyr's cmake places kernel artifacts in build_dir/zephyr/zephyr/ and
# merged.hex at build_dir/. Normalize to build_dir/zephyr/ so paths match
# get_download_types (which mirrors the platformio build output layout).
zephyr_dir = build_dir / "zephyr"
west_out = zephyr_dir / "zephyr"
for filename in ["zephyr.uf2"]:
src = west_out / filename
if src.is_file():
shutil.copy2(src, zephyr_dir / filename)
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
_GENPKG_PARAMS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
}
bootloader = zephyr_data()[KEY_BOOTLOADER]
if bootloader in (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
):
hex_file = west_out / "zephyr.hex"
dfu_package = build_dir / "firmware.zip"
genpkg_cmd = [
str(paths["python_executable"]),
"-m",
"nordicsemi.__main__",
"dfu",
"genpkg",
]
if bootloader in _GENPKG_PARAMS:
dev_type, sd_req = _GENPKG_PARAMS[bootloader]
genpkg_cmd += ["--dev-type", dev_type, "--sd-req", sd_req]
genpkg_cmd += ["--application", str(hex_file), str(dfu_package)]
if not run_command_ok(genpkg_cmd, env=env, stream_output=True):
raise EsphomeError("Failed to create adafruit DFU package")
return True
+7 -15
View File
@@ -54,22 +54,15 @@ def _get_toolchain_path(version: str) -> Path:
return _get_tools_path() / "toolchains" / version
# onexc/dir_fd were added to shutil.rmtree in 3.12; the 3.11 branch uses onerror.
_SITECUSTOMIZE = """\
import os, stat, shutil, sys
import os, stat, shutil
_orig = shutil.rmtree
def _handler(func, path, exc):
os.chmod(path, stat.S_IWRITE); func(path)
if sys.version_info >= (3, 12):
def _rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
if onerror is None and onexc is None:
onexc = _handler
return _orig(path, ignore_errors=ignore_errors, onerror=onerror, onexc=onexc, dir_fd=dir_fd)
else:
def _rmtree(path, ignore_errors=False, onerror=None):
if onerror is None:
onerror = _handler
return _orig(path, ignore_errors=ignore_errors, onerror=onerror)
def _rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
if onerror is None and onexc is None:
onexc = _handler
return _orig(path, ignore_errors=ignore_errors, onerror=onerror, onexc=onexc, dir_fd=dir_fd)
shutil.rmtree = _rmtree
"""
@@ -111,10 +104,9 @@ def _get_version_str() -> str:
def get_build_paths() -> dict:
version = _get_version_str()
env_path = _get_python_env_path(version)
return {
"python_executable": get_python_env_executable_path(
_get_python_env_path(version), "python"
),
"python_executable": get_python_env_executable_path(env_path, "python"),
"framework_path": _get_framework_path(version),
}
@@ -1,3 +1,4 @@
west==1.5.0
ninja==1.13.0
cmake==4.3.2
adafruit-nrfutil @ git+https://github.com/adafruit/Adafruit_nRF52_nrfutil.git@7fdfe15feee5f304fb7d9b031721dcefa1f72b58
+1 -1
View File
@@ -541,7 +541,7 @@ void OpenTherm::debug_error(OpenThermError &error) const {
error.capture, error.bit_pos);
}
float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
float OpenthermData::f88() { return ((float) this->s16()) / 256.0f; }
void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); }
@@ -12,8 +12,9 @@ void opentherm::OpenthermOutput::write_state(float state) {
#else
bool zero_means_zero = false;
#endif
this->state =
state < 0.003 && zero_means_zero ? 0.0 : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
this->state = state < 0.003f && zero_means_zero
? 0.0f
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
this->has_state_ = true;
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
}

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