mirror of
https://github.com/esphome/esphome.git
synced 2026-07-06 15:09:36 +00:00
Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd82a91d8f | |||
| 86ec218f75 | |||
| 2a6ec597b4 | |||
| 8dd69207ea | |||
| d203a46ef8 | |||
| 1920d8a887 | |||
| 95dea59382 | |||
| f3cddcee21 | |||
| 21e384cafd | |||
| 32db055b98 | |||
| 2c87260046 | |||
| 51ccad8461 | |||
| 7f500c4b6e | |||
| 564d155cb6 | |||
| edf5542559 | |||
| 51335e8830 | |||
| 391ffe34f8 | |||
| 12ead0408a | |||
| 2d39cc2540 | |||
| a9a8f4cb3b | |||
| 8fa2e75afa | |||
| 0b01f9fc42 | |||
| ed8c062d9f | |||
| 5e516e78e4 | |||
| 896b6ec8c9 | |||
| 9e7cdaf475 | |||
| a3fd1d5d00 | |||
| 7257bed1e9 | |||
| 5a9977cf5c | |||
| 12b3aec567 | |||
| d59c006ff9 | |||
| 02ada93ea5 | |||
| 6e87f8eb4e | |||
| 7df550f2a9 | |||
| b02f0e3c5f | |||
| 151f71e033 | |||
| 7ac001e994 | |||
| de177d2445 | |||
| a9cb7143dc | |||
| 902258b56e | |||
| c2a96ea293 | |||
| 37a3c3ab3a | |||
| a8ed781f3e | |||
| 63f0d054b7 | |||
| e7dcf54a77 | |||
| 1ba5504944 | |||
| 5637116378 | |||
| cdc4ba6295 | |||
| d1aa1881bb | |||
| 14107ec452 | |||
| 2ca6681896 | |||
| 40a65d36b4 | |||
| 16ec237ac6 | |||
| 0afcdbfe73 | |||
| b9439036d4 | |||
| cb23f9453f | |||
| 0858ecbb8e | |||
| 96da6dd075 | |||
| 2c31bdc6a2 | |||
| 0a3393bed3 | |||
| c2c50ceea7 | |||
| 2341d510d3 | |||
| 9d6f2f71e8 | |||
| e1334cf57f | |||
| a1aff7cadf | |||
| 2271ac6470 | |||
| 8fe36cde23 | |||
| fdd5956c1e | |||
| 403ba262c6 | |||
| f8be27ce6d | |||
| a50d70c8d3 | |||
| 4d86049c21 | |||
| 44037c4f9b | |||
| 5856d05701 | |||
| a2a048e3bf | |||
| 9f4c773963 | |||
| ef0eef8117 | |||
| 097e6eb41f | |||
| 73a49493a2 | |||
| 4a93d5b544 | |||
| cc0655a904 | |||
| a859cb3cce | |||
| 47909d5299 | |||
| 16667bf5be | |||
| ef3afe3e21 | |||
| 3a47317fc8 | |||
| 89066e3e20 | |||
| 9a80c980cb | |||
| c9e6c85e6a | |||
| e88c9ba066 | |||
| 45be290392 | |||
| 3f28ab88ca | |||
| 1d07f37d62 | |||
| 16c5224341 | |||
| e83372e2f3 | |||
| 2531fb1a02 | |||
| 3e845d387a | |||
| b9e8da92c7 | |||
| 0c5f055d45 | |||
| 342020e1d3 | |||
| 62f9bc79c4 | |||
| 53bfb02a21 | |||
| 83484a8828 | |||
| ece235218f | |||
| f3409acfa8 | |||
| 77b7201eb8 | |||
| 6b91df8d75 | |||
| 1670f04a87 | |||
| 1adf05e2d5 | |||
| a94bb74d04 | |||
| c19c75220b | |||
| 97382ed814 | |||
| 5f06679d78 | |||
| 851e8b6c0d | |||
| 9a729608d5 | |||
| 53fa346ddc | |||
| b3210de374 | |||
| 82ccc37ba1 | |||
| 3826e95506 | |||
| b083491e74 | |||
| 73ca0ff106 | |||
| bba11b3b1e | |||
| 2142bc1b76 | |||
| f81e04b036 | |||
| c3327d0b43 | |||
| 8577c26358 | |||
| 80730fd012 | |||
| 5ee3e94ca1 | |||
| 037f75e0ff | |||
| c47f4fbc1c | |||
| 2f86e48a83 | |||
| 0bbba75757 | |||
| 9362d9745e | |||
| c8f708c13c | |||
| 05590a3a21 | |||
| cdf2867baf | |||
| b142557979 | |||
| db405c483e | |||
| 808c7b67b3 | |||
| 7131eafc09 | |||
| 7b4af76a61 | |||
| 2cd93daa5e | |||
| f86bb2bdb0 | |||
| 414182fe6d | |||
| 2ee0df1da3 | |||
| e1252e32d1 | |||
| 1183ef825b | |||
| c09edb94c1 | |||
| 9948adc6a0 | |||
| ccb467b219 | |||
| 1377776d21 | |||
| 29501ef4f8 | |||
| d97c23b8e3 | |||
| 92d5e7b18c | |||
| 15ce4b3616 | |||
| 33f9ad9cee | |||
| 18a082de30 | |||
| 7f418d969e | |||
| fe9f19d9ed | |||
| d37f8876d7 | |||
| d7c42bc9ec | |||
| efc508a82b | |||
| 0edc0fd9c8 | |||
| cc4c13930f | |||
| 234ca7c951 | |||
| 447c4669b1 | |||
| 27942f1973 | |||
| 158a119a5a | |||
| b126f3af3b | |||
| d4e1e32a30 | |||
| 271b423b22 | |||
| 417858f098 | |||
| c52042e023 | |||
| f12531e7e0 | |||
| ca279110c9 | |||
| f2968e0449 | |||
| 0043be6165 | |||
| 0716c9f722 | |||
| fcf5637aa5 | |||
| 5e3c44d48f | |||
| d6d3bbbad8 | |||
| 86b7933081 | |||
| 7cceb72cc3 | |||
| 56f7b3e61b | |||
| 22062d79a2 | |||
| ab3b677113 | |||
| cdb445f69d | |||
| 1eed1adfa0 | |||
| a6c08576be | |||
| f41aa8b18c | |||
| 6700347a48 | |||
| b147830ef9 | |||
| bd844fcd0a | |||
| 8936be628f | |||
| 5920fa97e4 | |||
| 326769e43c | |||
| 7524590bcf | |||
| 15ec46abfe | |||
| 920af91db6 | |||
| a744261934 | |||
| 59c1368440 | |||
| 7e8e085a04 | |||
| 22b25724ae | |||
| 89719cf4b2 | |||
| e15b19b223 | |||
| 2ca13972b9 | |||
| 7bb4e75459 | |||
| fd8e510745 | |||
| 25c74c8f99 | |||
| 05d285ba86 | |||
| 186ca4e458 | |||
| 618312f0ee | |||
| 70d188202a | |||
| 4a21afe7ce | |||
| fd1d016795 | |||
| 03c091adfc | |||
| a3a88acfcf | |||
| 07f8ae6c82 | |||
| 25c30ac5bb | |||
| a76767a0ab | |||
| 511d185772 | |||
| c4c19c8a6c | |||
| fe2d60ccec | |||
| 657890695f | |||
| 8a5f008aee | |||
| f8a22b87b8 | |||
| 7f38d95424 | |||
| bb7d96b954 | |||
| 8daa946afa | |||
| ddc40f44fa | |||
| 409640c0ee | |||
| 822c9161c6 | |||
| a060f175ad | |||
| 73f305ff9c | |||
| b6ff7185e7 | |||
| 928f6f1866 | |||
| e7c3277eeb |
+1
-1
@@ -1 +1 @@
|
||||
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
|
||||
9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
- if: failure()
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
|
||||
})
|
||||
|
||||
- if: success()
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
|
||||
+71
-19
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -106,6 +106,7 @@ jobs:
|
||||
script/build_codeowners.py --check
|
||||
script/build_language_schema.py --check
|
||||
script/generate-esp32-boards.py --check
|
||||
script/generate-rp2040-boards.py --check
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
@@ -153,12 +154,12 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -170,6 +171,8 @@ jobs:
|
||||
- common
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
|
||||
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
@@ -182,6 +185,7 @@ jobs:
|
||||
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
||||
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -194,7 +198,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -210,6 +214,8 @@ jobs:
|
||||
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
@@ -222,9 +228,10 @@ jobs:
|
||||
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -246,7 +253,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -261,9 +268,20 @@ jobs:
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
env:
|
||||
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
|
||||
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
|
||||
echo "Running all integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
else
|
||||
# Parse JSON array into bash array to avoid shell expansion issues
|
||||
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
|
||||
echo "Running ${#test_files[@]} specific integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
fi
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
@@ -292,6 +310,40 @@ jobs:
|
||||
script/cpp_unit_test.py $ARGS
|
||||
fi
|
||||
|
||||
benchmarks:
|
||||
name: Run CodSpeed benchmarks
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: >-
|
||||
(github.event_name == 'push' && github.ref_name == 'dev') ||
|
||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Build benchmarks
|
||||
id: build
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
|
||||
# --build-only prints BUILD_BINARY=<path> to stdout
|
||||
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
|
||||
clang-tidy-single:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -335,14 +387,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -414,14 +466,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -503,14 +555,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -765,7 +817,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@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -789,7 +841,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@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -830,7 +882,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@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -877,7 +929,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -945,13 +997,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -10,6 +10,9 @@ name: Codeowner Approved Label
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches-ignore:
|
||||
- release
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
@@ -13,6 +13,9 @@ on:
|
||||
# Needs to be pull_request_target to get write permissions
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
branches-ignore:
|
||||
- release
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -65,6 +65,18 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for angle brackets not wrapped in backticks.
|
||||
// Astro docs MDX treats bare < as JSX component opening tags.
|
||||
const stripped = title.replace(/`[^`]*`/g, '');
|
||||
if (/[<>]/.test(stripped)) {
|
||||
core.setFailed(
|
||||
'PR title contains `<` or `>` not wrapped in backticks.\n' +
|
||||
'Astro docs MDX interprets bare `<` as JSX components.\n' +
|
||||
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check title starts with [tag] prefix
|
||||
const bracketPattern = /^\[\w+\]/;
|
||||
if (!bracketPattern.test(title)) {
|
||||
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Install Home Assistant
|
||||
run: |
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.5
|
||||
rev: v0.15.6
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
+2
-1
@@ -244,7 +244,6 @@ esphome/components/hyt271/* @Philippe12
|
||||
esphome/components/i2c/* @esphome/core
|
||||
esphome/components/i2c_device/* @gabest11
|
||||
esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/media_player/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
@@ -458,6 +457,8 @@ esphome/components/sn74hc165/* @jesserockz
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/sound_level/* @kahrendt
|
||||
esphome/components/spa06_base/* @danielkent-net
|
||||
esphome/components/spa06_i2c/* @danielkent-net
|
||||
esphome/components/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
||||
esphome/components/speaker_source/* @kahrendt
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.3.0
|
||||
PROJECT_NUMBER = 2026.4.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
|
||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/) [](https://codspeed.io/esphome/esphome)
|
||||
|
||||
<a href="https://esphome.io/">
|
||||
<picture>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Memory usage analyzer for ESPHome compiled binaries."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -40,6 +40,15 @@ _READELF_SECTION_PATTERN = re.compile(
|
||||
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
|
||||
)
|
||||
|
||||
# Regex for extracting call targets from objdump disassembly
|
||||
# Matches direct call instructions across architectures:
|
||||
# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 <addr> <symbol>
|
||||
# ARM: bl/blx <addr> <symbol>
|
||||
# Captures the mangled symbol name inside angle brackets.
|
||||
_CALL_TARGET_PATTERN = re.compile(
|
||||
r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>"
|
||||
)
|
||||
|
||||
# Component category prefixes
|
||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
||||
@@ -192,20 +201,27 @@ class MemoryAnalyzer:
|
||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
||||
# Library symbol mapping: symbol_name -> library_name
|
||||
self._lib_symbol_map: dict[str, str] = {}
|
||||
# Source file symbol mapping: symbol_name -> component_name
|
||||
# Used for extern "C" and other symbols without C++ namespace
|
||||
self._source_symbol_map: dict[str, str] = {}
|
||||
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
|
||||
# "espressif__mdns" -> "mdns"
|
||||
self._lib_hash_to_name: dict[str, str] = {}
|
||||
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
|
||||
self._heuristic_to_lib: dict[str, str] = {}
|
||||
# Function call counts: mangled_name -> call_count
|
||||
self._function_call_counts: Counter[str] = Counter()
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._scan_libraries()
|
||||
self._scan_source_symbols()
|
||||
self._categorize_symbols()
|
||||
self._analyze_cswtch_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
self._analyze_function_calls()
|
||||
return dict(self.components)
|
||||
|
||||
def _parse_sections(self) -> None:
|
||||
@@ -351,6 +367,11 @@ class MemoryAnalyzer:
|
||||
if lib_name := self._lib_symbol_map.get(symbol_name):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
|
||||
|
||||
# Check source file mapping (catches extern "C" functions in ESPHome sources)
|
||||
# Must be before heuristic patterns since source attribution is authoritative
|
||||
if component := self._source_symbol_map.get(symbol_name):
|
||||
return component
|
||||
|
||||
# Check against symbol patterns
|
||||
for component, patterns in SYMBOL_PATTERNS.items():
|
||||
if any(pattern in symbol_name for pattern in patterns):
|
||||
@@ -384,8 +405,9 @@ class MemoryAnalyzer:
|
||||
return
|
||||
|
||||
_LOGGER.info("Demangling %d symbols", len(symbols))
|
||||
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
|
||||
demangled = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
self._demangle_cache.update(demangled)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(demangled))
|
||||
|
||||
def _demangle_symbol(self, symbol: str) -> str:
|
||||
"""Get demangled C++ symbol name from cache."""
|
||||
@@ -640,6 +662,7 @@ class MemoryAnalyzer:
|
||||
return None
|
||||
|
||||
symbol_map: dict[str, str] = {}
|
||||
source_symbol_map: dict[str, str] = {}
|
||||
current_symbol: str | None = None
|
||||
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
|
||||
|
||||
@@ -675,9 +698,18 @@ class MemoryAnalyzer:
|
||||
if dir_key in source_path:
|
||||
symbol_map[current_symbol] = lib_name
|
||||
break
|
||||
else:
|
||||
# Map ESPHome source files to components for extern "C"
|
||||
# and other symbols without C++ namespace
|
||||
component = self._source_file_to_component(source_path)
|
||||
if component.startswith(
|
||||
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
|
||||
):
|
||||
source_symbol_map[current_symbol] = component
|
||||
|
||||
current_symbol = None
|
||||
|
||||
self._source_symbol_map = source_symbol_map
|
||||
return symbol_map or None
|
||||
|
||||
def _scan_libraries(self) -> None:
|
||||
@@ -728,6 +760,112 @@ class MemoryAnalyzer:
|
||||
len(libraries),
|
||||
)
|
||||
|
||||
def _scan_source_symbols(self) -> None:
|
||||
"""Scan ESPHome source object files to map extern "C" symbols to components.
|
||||
|
||||
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
|
||||
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
|
||||
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
|
||||
|
||||
Skips scanning if ``_source_symbol_map`` was already populated by
|
||||
``_parse_map_file()``.
|
||||
"""
|
||||
if self._source_symbol_map or not self.nm_path:
|
||||
return
|
||||
|
||||
obj_dir = self._find_object_files_dir()
|
||||
if obj_dir is None:
|
||||
return
|
||||
|
||||
# Find ESPHome source object files
|
||||
esphome_src_dir = obj_dir / "src" / "esphome"
|
||||
if not esphome_src_dir.is_dir():
|
||||
return
|
||||
|
||||
obj_files = sorted(esphome_src_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return
|
||||
|
||||
# Run nm with --print-file-name to get file:symbol mapping
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-g", "--defined-only"]
|
||||
+ [str(f) for f in obj_files],
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug("nm scan of source objects failed")
|
||||
return
|
||||
|
||||
self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir)
|
||||
if self._source_symbol_map:
|
||||
_LOGGER.info(
|
||||
"Built source symbol map from nm: %d symbols",
|
||||
len(self._source_symbol_map),
|
||||
)
|
||||
|
||||
def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]:
|
||||
"""Parse nm output to map non-namespaced symbols to ESPHome components.
|
||||
|
||||
Extracts global defined symbols from ESPHome source object files that
|
||||
don't use C++ namespacing (e.g. ``extern "C"`` functions).
|
||||
|
||||
Args:
|
||||
output: Raw stdout from ``nm --print-file-name -g --defined-only``
|
||||
or ``nm --print-file-name -S``.
|
||||
base_dir: Build directory for computing relative paths.
|
||||
|
||||
Returns:
|
||||
Dict mapping symbol names to component names.
|
||||
"""
|
||||
source_map: dict[str, str] = {}
|
||||
for line in output.splitlines():
|
||||
# Format: /path/to/file.o: addr type name
|
||||
# or: /path/to/file.o: addr size type name (with -S)
|
||||
colon_idx = line.rfind(".o:")
|
||||
if colon_idx == -1:
|
||||
continue
|
||||
|
||||
file_path = line[: colon_idx + 2]
|
||||
fields = line[colon_idx + 3 :].split()
|
||||
if len(fields) < 3:
|
||||
continue
|
||||
|
||||
# With -S flag, format is: addr size type name
|
||||
# Without -S flag: addr type name
|
||||
# type is a single char; size is hex digits
|
||||
# Detect by checking if fields[1] is a single uppercase letter (type)
|
||||
if len(fields[1]) == 1 and fields[1].isalpha():
|
||||
# addr type name
|
||||
sym_type = fields[1]
|
||||
symbol_name = fields[2]
|
||||
elif len(fields) >= 4:
|
||||
# addr size type name
|
||||
sym_type = fields[2]
|
||||
symbol_name = fields[3]
|
||||
else:
|
||||
continue
|
||||
|
||||
# Only global defined symbols (uppercase type)
|
||||
if not sym_type.isupper() or sym_type == "U":
|
||||
continue
|
||||
|
||||
# Skip symbols already in esphome:: namespace
|
||||
if symbol_name.startswith("_ZN7esphome"):
|
||||
continue
|
||||
|
||||
# Make path relative to base_dir for _source_file_to_component
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(base_dir))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
component = self._source_file_to_component(rel_path)
|
||||
if component.startswith(
|
||||
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
|
||||
):
|
||||
source_map[symbol_name] = component
|
||||
|
||||
return source_map
|
||||
|
||||
def _find_object_files_dir(self) -> Path | None:
|
||||
"""Find the directory containing object files for this build.
|
||||
|
||||
@@ -1011,6 +1149,43 @@ class MemoryAnalyzer:
|
||||
total_size,
|
||||
)
|
||||
|
||||
def _analyze_function_calls(self) -> None:
|
||||
"""Count function call sites by parsing disassembly output.
|
||||
|
||||
Parses direct call instructions (call0/call8/bl/blx) from objdump -d
|
||||
to count how many times each function is called. This helps identify
|
||||
inlining candidates — frequently called small functions benefit most
|
||||
from inlining.
|
||||
"""
|
||||
result = run_tool(
|
||||
[self.objdump_path, "-d", str(self.elf_path)],
|
||||
timeout=60,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug("Failed to disassemble ELF for function call analysis")
|
||||
return
|
||||
|
||||
self._function_call_counts = Counter(
|
||||
match.group(1)
|
||||
for line in result.stdout.splitlines()
|
||||
if (match := _CALL_TARGET_PATTERN.search(line))
|
||||
)
|
||||
|
||||
# Demangle any call targets not already in the cache
|
||||
missing = [
|
||||
name
|
||||
for name in self._function_call_counts
|
||||
if name not in self._demangle_cache
|
||||
]
|
||||
if missing:
|
||||
self._batch_demangle_symbols(missing)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Function call analysis: %d unique targets, %d total calls",
|
||||
len(self._function_call_counts),
|
||||
sum(self._function_call_counts.values()),
|
||||
)
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
|
||||
@@ -231,6 +231,110 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f" {size:>6,} B {sym_name}")
|
||||
lines.append("")
|
||||
|
||||
# Number of top called functions to show
|
||||
TOP_CALLS_LIMIT: int = 50
|
||||
# Number of inlining candidates to show
|
||||
INLINE_CANDIDATES_LIMIT: int = 25
|
||||
# Maximum function size in bytes to consider for inlining
|
||||
INLINE_SIZE_THRESHOLD: int = 16
|
||||
|
||||
def _build_symbol_sizes(self) -> dict[str, int]:
|
||||
"""Build a size lookup from all component symbols: mangled_name -> size."""
|
||||
return {
|
||||
symbol: size
|
||||
for symbols in self._component_symbols.values()
|
||||
for symbol, _, size, _ in symbols
|
||||
}
|
||||
|
||||
def _format_call_row(
|
||||
self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int]
|
||||
) -> str:
|
||||
"""Format a single row for call frequency tables."""
|
||||
demangled = self._demangle_cache.get(mangled, mangled)
|
||||
if len(demangled) > 80:
|
||||
demangled = f"{demangled[:77]}..."
|
||||
size = symbol_sizes.get(mangled)
|
||||
size_str = f"{size:>5,} B" if size is not None else " ?"
|
||||
return f"{index:>3} {count:>5} {size_str} {demangled}"
|
||||
|
||||
def _add_call_table_header(self, lines: list[str]) -> None:
|
||||
"""Add the header row for call frequency tables."""
|
||||
lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function")
|
||||
lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}")
|
||||
|
||||
def _add_function_call_analysis(self, lines: list[str]) -> None:
|
||||
"""Add function call frequency analysis section.
|
||||
|
||||
Shows the most frequently called functions by call site count.
|
||||
"""
|
||||
self._add_section_header(lines, "Top Called Functions")
|
||||
|
||||
symbol_sizes = self._build_symbol_sizes()
|
||||
|
||||
# Sort by call count descending
|
||||
sorted_calls = sorted(
|
||||
self._function_call_counts.items(), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
|
||||
self._add_call_table_header(lines)
|
||||
|
||||
for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]):
|
||||
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
|
||||
|
||||
total_calls = sum(self._function_call_counts.values())
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Total: {len(self._function_call_counts)} unique targets, "
|
||||
f"{total_calls:,} call sites"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
def _add_inline_candidates(self, lines: list[str]) -> None:
|
||||
"""Add inlining candidates section.
|
||||
|
||||
Shows frequently called functions that are small enough to benefit
|
||||
from inlining (< 16 bytes). These are the best candidates for
|
||||
reducing call overhead.
|
||||
"""
|
||||
self._add_section_header(
|
||||
lines,
|
||||
f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)",
|
||||
)
|
||||
|
||||
symbol_sizes = self._build_symbol_sizes()
|
||||
|
||||
# Filter to small functions with known size, sort by call count
|
||||
candidates = sorted(
|
||||
(
|
||||
(mangled, count)
|
||||
for mangled, count in self._function_call_counts.items()
|
||||
if mangled in symbol_sizes
|
||||
and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD
|
||||
),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
lines.append("No candidates found.")
|
||||
lines.append("")
|
||||
return
|
||||
|
||||
self._add_call_table_header(lines)
|
||||
|
||||
for i, (mangled, count) in enumerate(
|
||||
candidates[: self.INLINE_CANDIDATES_LIMIT]
|
||||
):
|
||||
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} "
|
||||
f"of {len(candidates)} functions under "
|
||||
f"{self.INLINE_SIZE_THRESHOLD} B"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -533,6 +637,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
if self._cswtch_symbols:
|
||||
self._add_cswtch_analysis(lines)
|
||||
|
||||
# Function call frequency analysis
|
||||
if self._function_call_counts:
|
||||
self._add_function_call_analysis(lines)
|
||||
self._add_inline_candidates(lines)
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
|
||||
@@ -408,7 +408,6 @@ SYMBOL_PATTERNS = {
|
||||
],
|
||||
"arduino_core": [
|
||||
"pinMode",
|
||||
"resetPins",
|
||||
"millis",
|
||||
"micros",
|
||||
"delay(", # More specific - Arduino delay function with parenthesis
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "absolute_humidity.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
namespace esphome::absolute_humidity {
|
||||
|
||||
static const char *const TAG = "absolute_humidity.sensor";
|
||||
static const char *const TAG{"absolute_humidity.sensor"};
|
||||
|
||||
void AbsoluteHumidityComponent::setup() {
|
||||
this->temperature_sensor_->add_on_state_callback([this](float state) {
|
||||
this->temperature_ = state;
|
||||
this->enable_loop();
|
||||
});
|
||||
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
|
||||
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
|
||||
// Get initial value
|
||||
if (this->temperature_sensor_->has_state()) {
|
||||
this->temperature_callback_(this->temperature_sensor_->get_state());
|
||||
this->temperature_ = this->temperature_sensor_->get_state();
|
||||
}
|
||||
|
||||
this->humidity_sensor_->add_on_state_callback([this](float state) {
|
||||
this->humidity_ = state;
|
||||
this->enable_loop();
|
||||
});
|
||||
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
|
||||
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
|
||||
// Get initial value
|
||||
if (this->humidity_sensor_->has_state()) {
|
||||
this->humidity_callback_(this->humidity_sensor_->get_state());
|
||||
this->humidity_ = this->humidity_sensor_->get_state();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() {
|
||||
}
|
||||
|
||||
void AbsoluteHumidityComponent::loop() {
|
||||
if (!this->next_update_) {
|
||||
return;
|
||||
}
|
||||
this->next_update_ = false;
|
||||
// Only run once
|
||||
this->disable_loop();
|
||||
|
||||
// Ensure we have source data
|
||||
const bool no_temperature = std::isnan(this->temperature_);
|
||||
const bool no_humidity = std::isnan(this->humidity_);
|
||||
const bool no_temperature{std::isnan(this->temperature_)};
|
||||
const bool no_humidity{std::isnan(this->humidity_)};
|
||||
if (no_temperature || no_humidity) {
|
||||
if (no_temperature) {
|
||||
ESP_LOGW(TAG, "No valid state from temperature sensor!");
|
||||
@@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() {
|
||||
}
|
||||
|
||||
// Convert to desired units
|
||||
const float temperature_c = this->temperature_;
|
||||
const float temperature_k = temperature_c + 273.15;
|
||||
const float hr = this->humidity_ / 100;
|
||||
const float temperature_c{this->temperature_};
|
||||
const float temperature_k{temperature_c + 273.15f};
|
||||
const float hr{this->humidity_ / 100.0f};
|
||||
|
||||
// Calculate saturation vapor pressure
|
||||
float es;
|
||||
@@ -90,7 +95,7 @@ void AbsoluteHumidityComponent::loop() {
|
||||
}
|
||||
|
||||
// Calculate absolute humidity
|
||||
const float absolute_humidity = vapor_density(es, hr, temperature_k);
|
||||
const float absolute_humidity{vapor_density(es, hr, temperature_k)};
|
||||
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
|
||||
|
||||
@@ -103,16 +108,16 @@ void AbsoluteHumidityComponent::loop() {
|
||||
// More accurate than Tetens in normal meteorologic conditions
|
||||
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
|
||||
float a, b, c, d;
|
||||
if (temperature_c >= 0) {
|
||||
a = 0.61121;
|
||||
b = 18.678;
|
||||
c = 234.5;
|
||||
d = 257.14;
|
||||
if (temperature_c >= 0.0f) {
|
||||
a = 0.61121f;
|
||||
b = 18.678f;
|
||||
c = 234.5f;
|
||||
d = 257.14f;
|
||||
} else {
|
||||
a = 0.61115;
|
||||
b = 18.678;
|
||||
c = 233.7;
|
||||
d = 279.82;
|
||||
a = 0.61115f;
|
||||
b = 18.678f;
|
||||
c = 233.7f;
|
||||
d = 279.82f;
|
||||
}
|
||||
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
|
||||
}
|
||||
@@ -120,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) {
|
||||
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
|
||||
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
|
||||
float a, b;
|
||||
if (temperature_c >= 0) {
|
||||
a = 17.27;
|
||||
b = 237.3;
|
||||
if (temperature_c >= 0.0f) {
|
||||
a = 17.27f;
|
||||
b = 237.3f;
|
||||
} else {
|
||||
a = 21.875;
|
||||
b = 265.5;
|
||||
a = 21.875f;
|
||||
b = 265.5f;
|
||||
}
|
||||
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
|
||||
return 0.61078f * expf((a * temperature_c) / (temperature_c + b));
|
||||
}
|
||||
|
||||
// Wobus equation
|
||||
@@ -146,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
|
||||
//
|
||||
// Baker, Schlatter 17-MAY-1982 Original version.
|
||||
|
||||
const float c0 = +0.99999683e00;
|
||||
const float c1 = -0.90826951e-02;
|
||||
const float c2 = +0.78736169e-04;
|
||||
const float c3 = -0.61117958e-06;
|
||||
const float c4 = +0.43884187e-08;
|
||||
const float c5 = -0.29883885e-10;
|
||||
const float c6 = +0.21874425e-12;
|
||||
const float c7 = -0.17892321e-14;
|
||||
const float c8 = +0.11112018e-16;
|
||||
const float c9 = -0.30994571e-19;
|
||||
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
|
||||
return 0.61078 / pow(p, 8);
|
||||
constexpr float c0{+0.99999683e+00f};
|
||||
constexpr float c1{-0.90826951e-02f};
|
||||
constexpr float c2{+0.78736169e-04f};
|
||||
constexpr float c3{-0.61117958e-06f};
|
||||
constexpr float c4{+0.43884187e-08f};
|
||||
constexpr float c5{-0.29883885e-10f};
|
||||
constexpr float c6{+0.21874425e-12f};
|
||||
constexpr float c7{-0.17892321e-14f};
|
||||
constexpr float c8{+0.11112018e-16f};
|
||||
constexpr float c9{-0.30994571e-19f};
|
||||
const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))};
|
||||
return 0.61078f / powf(p, 8.0f);
|
||||
}
|
||||
|
||||
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
|
||||
@@ -168,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
|
||||
// hr = relative humidity [0-1]
|
||||
// ta = absolute temperature (K)
|
||||
|
||||
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
|
||||
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
|
||||
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
|
||||
const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa)
|
||||
const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹)
|
||||
const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹)
|
||||
return (ea * mw) / (r * ta);
|
||||
}
|
||||
|
||||
} // namespace absolute_humidity
|
||||
} // namespace esphome
|
||||
} // namespace esphome::absolute_humidity
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
namespace esphome::absolute_humidity {
|
||||
|
||||
/// Enum listing all implemented saturation vapor pressure equations.
|
||||
enum SaturationVaporPressureEquation {
|
||||
@@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation {
|
||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
public:
|
||||
AbsoluteHumidityComponent() = default;
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
|
||||
@@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
void temperature_callback_(float state) {
|
||||
this->next_update_ = true;
|
||||
this->temperature_ = state;
|
||||
}
|
||||
void humidity_callback_(float state) {
|
||||
this->next_update_ = true;
|
||||
this->humidity_ = state;
|
||||
}
|
||||
|
||||
/** Buck equation for saturation vapor pressure in kPa.
|
||||
*
|
||||
* @param temperature_c Air temperature in °C.
|
||||
@@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
* @param es Saturation vapor pressure in kPa.
|
||||
* @param hr Relative humidity 0 to 1.
|
||||
* @param ta Absolute temperature in K.
|
||||
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
|
||||
*/
|
||||
static float vapor_density(float es, float hr, float ta);
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
bool next_update_{false};
|
||||
|
||||
float temperature_{NAN};
|
||||
float humidity_{NAN};
|
||||
SaturationVaporPressureEquation equation_;
|
||||
};
|
||||
|
||||
} // namespace absolute_humidity
|
||||
} // namespace esphome
|
||||
} // namespace esphome::absolute_humidity
|
||||
|
||||
@@ -22,7 +22,8 @@ namespace adc {
|
||||
|
||||
#ifdef USE_ESP32
|
||||
// clang-format off
|
||||
#if (ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
#if ESP_IDF_VERSION_MAJOR >= 6 || \
|
||||
(ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
|
||||
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
|
||||
(ESP_IDF_VERSION_MINOR >= 2)) \
|
||||
|
||||
@@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
}
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
|
||||
this->cleared_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
|
||||
this->chime_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
|
||||
this->ready_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
|
||||
const char *code) {
|
||||
auto call = this->make_call();
|
||||
|
||||
@@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase {
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_cleared_callback(std::function<void()> &&callback);
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered. */
|
||||
template<typename F> void add_on_cleared_callback(F &&callback) {
|
||||
this->cleared_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** Add a callback for when a chime zone goes from closed to open
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_chime_callback(std::function<void()> &&callback);
|
||||
/** Add a callback for when a chime zone goes from closed to open. */
|
||||
template<typename F> void add_on_chime_callback(F &&callback) {
|
||||
this->chime_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** Add a callback for when a ready state changes
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_ready_callback(std::function<void()> &&callback);
|
||||
/** Add a callback for when a ready state changes. */
|
||||
template<typename F> void add_on_ready_callback(F &&callback) {
|
||||
this->ready_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** A numeric representation of the supported features as per HomeAssistant
|
||||
*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
@@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
# Maximum queued send buffers per connection before dropping connection
|
||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
||||
# Platform defaults based on available RAM and typical message rates:
|
||||
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_SEND_QUEUE,
|
||||
esp8266=5, # Limited RAM, need to fail fast
|
||||
esp8266=4, # Limited RAM, need to fail fast
|
||||
esp32=8, # More RAM, can buffer more
|
||||
rp2040=5, # Limited RAM
|
||||
rp2040=8, # Moderate RAM
|
||||
bk72xx=8, # Moderate RAM
|
||||
nrf52=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
@@ -454,6 +455,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.11")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
|
||||
@@ -44,6 +44,12 @@ class APIBuffer {
|
||||
this->reserve(n);
|
||||
this->size_ = n; // no zero-fill
|
||||
}
|
||||
/// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size.
|
||||
/// Single grow_ check regardless of argument order.
|
||||
inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
|
||||
this->reserve(std::max(reserve_size, new_size));
|
||||
this->size_ = new_size;
|
||||
}
|
||||
uint8_t *data() { return this->data_.get(); }
|
||||
const uint8_t *data() const { return this->data_.get(); }
|
||||
size_t size() const { return this->size_; }
|
||||
|
||||
@@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS *
|
||||
// A stalled handshake from a buggy client or network glitch holds a connection
|
||||
// slot, which can prevent legitimate clients from reconnecting. Also hardens
|
||||
// against the less likely case of intentional connection slot exhaustion.
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
|
||||
//
|
||||
// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak
|
||||
// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to
|
||||
// 28-30s. See https://github.com/esphome/esphome/issues/14999
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
|
||||
|
||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||
|
||||
@@ -2025,8 +2029,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode
|
||||
// Batch message second or later
|
||||
// Add padding for previous message footer + this message header
|
||||
size_t current_size = shared_buf.size();
|
||||
shared_buf.reserve(current_size + total_calculated_size);
|
||||
shared_buf.resize(current_size + footer_size + header_padding);
|
||||
shared_buf.reserve_and_resize(current_size + total_calculated_size, current_size + footer_size + header_padding);
|
||||
}
|
||||
|
||||
// Pre-resize buffer to include payload, then encode through raw pointer
|
||||
|
||||
@@ -305,9 +305,9 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Reserve space for header padding + message + footer
|
||||
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
||||
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
|
||||
shared_buf.reserve(total_size);
|
||||
// Resize to add header padding so message encoding starts at the correct position
|
||||
shared_buf.resize(header_padding);
|
||||
// Reserve full size but only set initial size to header padding
|
||||
// so message encoding starts at the correct position
|
||||
shared_buf.reserve_and_resize(total_size, header_padding);
|
||||
}
|
||||
|
||||
// Convenience overload - computes frame overhead internally
|
||||
|
||||
@@ -100,149 +100,61 @@ const LogString *api_error_to_logstr(APIError err) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
// Default implementation for loop - handles sending buffered data
|
||||
APIError APIFrameHelper::loop() {
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
APIError err = try_send_tx_buf_();
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
return err;
|
||||
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
|
||||
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
}
|
||||
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Common socket write error handling
|
||||
APIError APIFrameHelper::handle_socket_write_error_() {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
|
||||
uint16_t offset) {
|
||||
// Check if queue is full
|
||||
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
|
||||
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
|
||||
this->state_ = State::FAILED;
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t buffer_size = total_write_len - offset;
|
||||
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
|
||||
buffer = std::make_unique<SendBuffer>(SendBuffer{
|
||||
.data = std::make_unique<uint8_t[]>(buffer_size),
|
||||
.size = buffer_size,
|
||||
.offset = 0,
|
||||
});
|
||||
|
||||
uint16_t to_skip = offset;
|
||||
uint16_t write_pos = 0;
|
||||
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_skip >= iov[i].iov_len) {
|
||||
// Skip this entire segment
|
||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
||||
} else {
|
||||
// Include this segment (partially or fully)
|
||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
||||
std::memcpy(buffer->data.get() + write_pos, src, len);
|
||||
write_pos += len;
|
||||
to_skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Update circular buffer tracking
|
||||
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->tx_buf_count_++;
|
||||
}
|
||||
|
||||
// This method writes data to socket or buffers it
|
||||
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
|
||||
// Returns OK if all data was sent or successfully queued.
|
||||
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
|
||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
|
||||
// Returns APIError::OK if successful (or would block, but data has been buffered)
|
||||
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
|
||||
|
||||
if (iovcnt == 0)
|
||||
return APIError::OK; // Nothing to do, success
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Try to send any existing buffered data first if there is any
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
APIError send_result = try_send_tx_buf_();
|
||||
// If real error occurred (not just WOULD_BLOCK), return it
|
||||
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
|
||||
return send_result;
|
||||
}
|
||||
uint16_t skip = 0;
|
||||
|
||||
// If there is still data in the buffer, we can't send, buffer
|
||||
// the new data and return
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
// Drain any existing backlog first
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
APIError err = this->drain_overflow_and_handle_errors_();
|
||||
if (err != APIError::OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
// Try to send directly if no buffered data
|
||||
// Optimize for single iovec case (common for plaintext API)
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
// If backlog is clear, try direct send
|
||||
if (this->overflow_buf_.empty()) [[likely]] {
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
|
||||
if (sent == -1) {
|
||||
APIError err = this->handle_socket_write_error_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
// Socket would block, buffer the data
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
return err; // Socket write failed
|
||||
} else if (static_cast<uint16_t>(sent) < total_write_len) {
|
||||
// Partially sent, buffer the remaining data
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
|
||||
}
|
||||
|
||||
return APIError::OK; // Success, all data sent or buffered
|
||||
}
|
||||
|
||||
// Common implementation for trying to send buffered data
|
||||
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
|
||||
APIError APIFrameHelper::try_send_tx_buf_() {
|
||||
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
|
||||
while (this->tx_buf_count_ > 0) {
|
||||
// Get the first buffer in the queue
|
||||
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
|
||||
|
||||
// Try to send the remaining data in this buffer
|
||||
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
|
||||
|
||||
if (sent == -1) {
|
||||
return this->handle_socket_write_error_();
|
||||
} else if (sent == 0) {
|
||||
// Nothing sent but not an error
|
||||
return APIError::WOULD_BLOCK;
|
||||
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
|
||||
// Partially sent, update offset
|
||||
// Cast to ensure no overflow issues with uint16_t
|
||||
front_buffer->offset += static_cast<uint16_t>(sent);
|
||||
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
|
||||
if (sent == -1) [[unlikely]] {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
|
||||
return APIError::OK;
|
||||
} else {
|
||||
// Buffer completely sent, remove it from the queue
|
||||
this->tx_buf_[this->tx_buf_head_].reset();
|
||||
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->tx_buf_count_--;
|
||||
// Continue loop to try sending the next buffer
|
||||
skip = static_cast<uint16_t>(sent);
|
||||
}
|
||||
}
|
||||
|
||||
return APIError::OK; // All buffers sent successfully
|
||||
// Queue unsent data into overflow buffer
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
|
||||
HELPER_LOG("Overflow buffer full, dropping connection");
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
@@ -278,11 +190,12 @@ APIError APIFrameHelper::init_common_() {
|
||||
|
||||
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
|
||||
if (received == -1) {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
const int err = errno;
|
||||
if (err == EWOULDBLOCK || err == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
HELPER_LOG("Socket read failed with errno %d", err);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
#include "esphome/components/api/api_overflow_buffer.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -37,8 +39,6 @@ static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
|
||||
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
|
||||
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
|
||||
|
||||
class ProtoWriteBuffer;
|
||||
|
||||
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
|
||||
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
|
||||
|
||||
@@ -105,9 +105,9 @@ class APIFrameHelper {
|
||||
}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop();
|
||||
virtual APIError loop() = 0;
|
||||
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||
APIError close() {
|
||||
if (state_ == State::CLOSED)
|
||||
@@ -147,25 +147,28 @@ class APIFrameHelper {
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
if (this->nodelay_counter_) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
this->nodelay_counter_ = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush)
|
||||
if (this->nodelay_state_ == NODELAY_ON) {
|
||||
// Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT
|
||||
if (!this->nodelay_counter_)
|
||||
this->set_nodelay_raw_(false);
|
||||
this->nodelay_state_ = 1;
|
||||
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||
if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
} else {
|
||||
this->nodelay_state_++;
|
||||
this->nodelay_counter_ = 0;
|
||||
}
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize buffer to include footer space if needed (e.g. Noise MAC)
|
||||
if (frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
// Write multiple protobuf messages in a single operation
|
||||
// messages contains (message_type, offset, length) for each message in the buffer
|
||||
// The buffer contains all messages with appropriate padding before each
|
||||
@@ -187,28 +190,23 @@ class APIFrameHelper {
|
||||
}
|
||||
|
||||
protected:
|
||||
// Buffer containing data to be sent
|
||||
struct SendBuffer {
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
uint16_t size{0}; // Total size of the buffer
|
||||
uint16_t offset{0}; // Current offset within the buffer
|
||||
|
||||
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
|
||||
uint16_t remaining() const { return size - offset; }
|
||||
const uint8_t *current_data() const { return data.get() + offset; }
|
||||
};
|
||||
// Drain backlogged overflow data to the socket and handle errors.
|
||||
// Called when overflow_buf_.empty() is false. Out-of-line to keep the
|
||||
// fast path (empty check) inline at call sites.
|
||||
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError drain_overflow_and_handle_errors_();
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||
|
||||
// Try to send data from the tx buffer
|
||||
APIError try_send_tx_buf_();
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
|
||||
|
||||
// Common socket write error handling
|
||||
APIError handle_socket_write_error_();
|
||||
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
|
||||
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError check_socket_write_err_(int err) {
|
||||
if (err == EWOULDBLOCK || err == EAGAIN)
|
||||
return APIError::WOULD_BLOCK;
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
@@ -243,8 +241,8 @@ class APIFrameHelper {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
|
||||
// Backlog for unsent data when TCP send buffer is full (rarely used in production)
|
||||
APIOverflowBuffer overflow_buf_;
|
||||
APIBuffer rx_buf_;
|
||||
|
||||
// Client name buffer - stores name from Hello message or initial peername
|
||||
@@ -255,21 +253,17 @@ class APIFrameHelper {
|
||||
State state_{State::INITIALIZE};
|
||||
uint8_t frame_header_padding_{0};
|
||||
uint8_t frame_footer_size_{0};
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
// Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
|
||||
// Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
|
||||
// ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
|
||||
// ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||
static constexpr uint8_t LOG_NAGLE_COUNT = 2;
|
||||
#else
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 3;
|
||||
static constexpr uint8_t LOG_NAGLE_COUNT = 3;
|
||||
#endif
|
||||
int8_t nodelay_state_{NODELAY_ON};
|
||||
uint8_t nodelay_counter_{0};
|
||||
|
||||
// Internal helper to set TCP_NODELAY socket option
|
||||
void set_nodelay_raw_(bool enable) {
|
||||
|
||||
@@ -153,8 +153,10 @@ APIError APINoiseFrameHelper::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
return this->drain_overflow_and_handle_errors_();
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_.
|
||||
@@ -450,14 +452,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize to include MAC space (required for Noise encryption)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
|
||||
@@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -64,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
return this->drain_overflow_and_handle_errors_();
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_.
|
||||
@@ -235,11 +237,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = this->rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
|
||||
std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
@@ -257,9 +254,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
for (const auto &msg : messages) {
|
||||
// Calculate varint sizes for header layout
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
|
||||
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
|
||||
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
|
||||
? 1
|
||||
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
@@ -281,8 +280,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
//
|
||||
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
||||
// [0] - 0x00 indicator byte
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-32767)
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-16383)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// The message starts at offset + frame_header_padding_
|
||||
|
||||
@@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "api_overflow_buffer.h"
|
||||
#ifdef USE_API
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
APIOverflowBuffer::~APIOverflowBuffer() {
|
||||
for (auto *entry : this->queue_) {
|
||||
if (entry != nullptr)
|
||||
Entry::destroy(entry);
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
|
||||
while (this->count_ > 0) {
|
||||
Entry *front = this->queue_[this->head_];
|
||||
|
||||
ssize_t sent = socket->write(front->current_data(), front->remaining());
|
||||
|
||||
if (sent <= 0) {
|
||||
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
|
||||
// 0 = nothing sent (treat as no progress)
|
||||
return sent;
|
||||
}
|
||||
|
||||
if (static_cast<uint16_t>(sent) < front->remaining()) {
|
||||
// Partially sent, update offset and stop
|
||||
front->offset += static_cast<uint16_t>(sent);
|
||||
return sent;
|
||||
}
|
||||
|
||||
// Entry fully sent — free it and advance
|
||||
Entry::destroy(front);
|
||||
this->queue_[this->head_] = nullptr;
|
||||
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_--;
|
||||
}
|
||||
|
||||
return 0; // All drained
|
||||
}
|
||||
|
||||
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
|
||||
if (this->count_ >= API_MAX_SEND_QUEUE)
|
||||
return false;
|
||||
|
||||
uint16_t buffer_size = total_len - skip;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
|
||||
this->queue_[this->tail_] = entry;
|
||||
|
||||
uint16_t to_skip = skip;
|
||||
uint16_t write_pos = 0;
|
||||
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_skip >= iov[i].iov_len) {
|
||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
||||
} else {
|
||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
||||
std::memcpy(entry->data + write_pos, src, len);
|
||||
write_pos += len;
|
||||
to_skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_++;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
#endif // USE_API
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
|
||||
#include "esphome/components/socket/headers.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
|
||||
///
|
||||
/// Under normal operation this buffer is **never used** — data goes straight
|
||||
/// from the frame helper to the socket. It only fills when the LWIP TCP
|
||||
/// send buffer is full (slow client, congested network, heavy logging).
|
||||
/// The queue drains automatically on subsequent write/loop calls once the
|
||||
/// socket becomes writable again.
|
||||
///
|
||||
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
|
||||
/// config). If the queue fills completely the connection is marked failed.
|
||||
class APIOverflowBuffer {
|
||||
public:
|
||||
/// A single heap-allocated send-backlog entry.
|
||||
/// Lifetime is manually managed — see destroy().
|
||||
struct Entry {
|
||||
uint8_t *data;
|
||||
uint16_t size; // Total size of the buffer
|
||||
uint16_t offset; // Current send offset within the buffer
|
||||
|
||||
uint16_t remaining() const { return this->size - this->offset; }
|
||||
const uint8_t *current_data() const { return this->data + this->offset; }
|
||||
|
||||
/// Free this entry and its data buffer.
|
||||
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
|
||||
delete[] entry->data;
|
||||
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
};
|
||||
|
||||
~APIOverflowBuffer();
|
||||
|
||||
/// True when no backlogged data is waiting.
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
|
||||
/// True when the queue has no room for another entry.
|
||||
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
|
||||
|
||||
/// Number of entries currently queued.
|
||||
uint8_t count() const { return this->count_; }
|
||||
|
||||
/// Try to drain queued data to the socket.
|
||||
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
|
||||
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
|
||||
/// Callers only need to act on -1; 0 and positive values both mean "no error".
|
||||
/// Frees entries as they are fully sent.
|
||||
ssize_t try_drain(socket::Socket *socket);
|
||||
|
||||
/// Enqueue unsent IOV data into the backlog.
|
||||
/// Copies iov data starting at byte offset `skip` into a new entry.
|
||||
/// Returns false if the queue is full (caller should fail the connection).
|
||||
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
|
||||
|
||||
protected:
|
||||
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
|
||||
uint8_t head_{0};
|
||||
uint8_t tail_{0};
|
||||
uint8_t count_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
#endif // USE_API
|
||||
@@ -36,11 +36,11 @@ struct SavedNoisePsk {
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
class APIServer : public Component,
|
||||
public Controller
|
||||
class APIServer final : public Component,
|
||||
public Controller
|
||||
#ifdef USE_CAMERA
|
||||
,
|
||||
public camera::CameraListener
|
||||
public camera::CameraListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -136,8 +136,9 @@ class CustomAPIDevice {
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
|
||||
auto *obj = static_cast<T *>(this);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
[obj, callback](StringRef state) { (obj->*callback)(state); });
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
|
||||
@@ -148,10 +149,12 @@ class CustomAPIDevice {
|
||||
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
|
||||
auto *obj = static_cast<T *>(this);
|
||||
// Explicit type to disambiguate overload resolution
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(f));
|
||||
global_api_server->subscribe_home_assistant_state(
|
||||
entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(
|
||||
[obj, callback](const std::string &state) { (obj->*callback)(state); }));
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||
@@ -176,8 +179,10 @@ class CustomAPIDevice {
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
|
||||
auto *obj = static_cast<T *>(this);
|
||||
global_api_server->subscribe_home_assistant_state(
|
||||
entity_id, optional<std::string>(attribute),
|
||||
[obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); });
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
|
||||
@@ -188,10 +193,12 @@ class CustomAPIDevice {
|
||||
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
auto *obj = static_cast<T *>(this);
|
||||
// Explicit type to disambiguate overload resolution
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(f));
|
||||
global_api_server->subscribe_home_assistant_state(
|
||||
entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(
|
||||
[obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); }));
|
||||
}
|
||||
#else
|
||||
template<typename T>
|
||||
|
||||
@@ -477,6 +477,12 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
// Varint encoding thresholds: values below each threshold fit in N bytes
|
||||
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
|
||||
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
|
||||
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
|
||||
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
@@ -484,7 +490,7 @@ class ProtoSize {
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
|
||||
if (value < 128) [[likely]]
|
||||
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
|
||||
return 1; // Fast path: 7 bits, most common case
|
||||
if (__builtin_is_constant_evaluated())
|
||||
return varint_wide(value);
|
||||
@@ -496,11 +502,11 @@ class ProtoSize {
|
||||
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
|
||||
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
|
||||
if (value < 16384)
|
||||
if (value < VARINT_THRESHOLD_2_BYTE)
|
||||
return 2;
|
||||
if (value < 2097152)
|
||||
if (value < VARINT_THRESHOLD_3_BYTE)
|
||||
return 3;
|
||||
if (value < 268435456)
|
||||
if (value < VARINT_THRESHOLD_4_BYTE)
|
||||
return 4;
|
||||
return 5;
|
||||
}
|
||||
@@ -606,7 +612,7 @@ class ProtoSize {
|
||||
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
|
||||
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
|
||||
}
|
||||
static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
return field_id_size + varint(encode_zigzag32(value));
|
||||
}
|
||||
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
|
||||
@@ -618,13 +624,13 @@ class ProtoSize {
|
||||
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
|
||||
return value ? field_id_size + varint(value) : 0;
|
||||
}
|
||||
static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
|
||||
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
|
||||
}
|
||||
static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) {
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) {
|
||||
return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
}
|
||||
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
|
||||
@@ -642,7 +648,8 @@ class ProtoSize {
|
||||
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
|
||||
return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
|
||||
}
|
||||
static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) {
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size,
|
||||
uint32_t nested_size) {
|
||||
return field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,11 +52,12 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
connect_cb_(connect_arg_, this);
|
||||
return true;
|
||||
}
|
||||
if (errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", errno);
|
||||
const int saved_errno = errno;
|
||||
if (saved_errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", saved_errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
error_cb_(error_arg_, this, saved_errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,11 +80,12 @@ size_t AsyncClient::write(const char *data, size_t len) {
|
||||
|
||||
ssize_t sent = socket_->write(data, len);
|
||||
if (sent < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", errno);
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -129,10 +131,11 @@ void AsyncClient::loop() {
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Select error: %d", errno);
|
||||
const int err = errno;
|
||||
ESP_LOGE(TAG, "Select error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
@@ -148,11 +151,14 @@ void AsyncClient::loop() {
|
||||
} else if (len > 0) {
|
||||
if (data_cb_)
|
||||
data_cb_(data_arg_, this, buf, len);
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGW(TAG, "Read error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
} else {
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK) {
|
||||
ESP_LOGW(TAG, "Read error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ class BedjetCodec {
|
||||
|
||||
BedjetPacket packet_;
|
||||
|
||||
BedjetStatusPacket *status_packet_;
|
||||
BedjetStatusPacket *status_packet_{nullptr};
|
||||
BedjetStatusPacket buf_;
|
||||
};
|
||||
|
||||
|
||||
@@ -96,8 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
||||
|
||||
void setup() override {
|
||||
this->last_state_ = this->parent_->get_state_default(false);
|
||||
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
|
||||
this->parent_->add_on_state_callback(f);
|
||||
this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); });
|
||||
}
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -37,7 +37,7 @@ class TimeoutFilter : public Filter, public Component {
|
||||
TemplatableValue<uint32_t> timeout_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnOffFilter : public Filter, public Component {
|
||||
class DelayedOnOffFilter final : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
|
||||
switch (event) {
|
||||
// server response on RSSI request:
|
||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||
if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr))
|
||||
return;
|
||||
if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) {
|
||||
int8_t rssi = param->read_rssi_cmpl.rssi;
|
||||
ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi);
|
||||
|
||||
@@ -102,6 +102,10 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.value_len == 0) {
|
||||
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
if (param->notify.handle != this->handle)
|
||||
@@ -131,8 +135,10 @@ float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
|
||||
if (this->has_data_to_value_) {
|
||||
std::vector<uint8_t> data(value, value + value_len);
|
||||
return this->data_to_value_func_(data);
|
||||
} else {
|
||||
} else if (value_len > 0) {
|
||||
return value[0];
|
||||
} else {
|
||||
return NAN;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,10 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle != this->handle)
|
||||
break;
|
||||
if (param->notify.value_len == 0) {
|
||||
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
this->publish_state(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len);
|
||||
|
||||
@@ -521,7 +521,7 @@ int BME680BSECComponent::reinit_bsec_lib_() {
|
||||
}
|
||||
|
||||
void BME680BSECComponent::load_state_() {
|
||||
uint32_t hash = fnv1_hash("bme680_bsec_state_" + this->device_id_);
|
||||
uint32_t hash = fnv1_hash_extend(fnv1_hash("bme680_bsec_state_"), this->device_id_);
|
||||
this->bsec_state_ = global_preferences->make_preference<uint8_t[BSEC_MAX_STATE_BLOB_SIZE]>(hash, true);
|
||||
|
||||
if (!this->bsec_state_.load(&this->bsec_state_data_)) {
|
||||
|
||||
@@ -279,7 +279,8 @@ void BME68xBSEC2Component::run_() {
|
||||
uint32_t meas_dur = 0;
|
||||
meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
|
||||
ESP_LOGV(TAG, "Queueing read in %uus", meas_dur);
|
||||
this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); });
|
||||
this->trigger_time_ns_ = curr_time_ns;
|
||||
this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Measurement not required");
|
||||
this->read_(curr_time_ns);
|
||||
|
||||
@@ -116,6 +116,8 @@ class BME68xBSEC2Component : public Component {
|
||||
int8_t bme68x_status_{BME68X_OK};
|
||||
|
||||
int64_t last_time_ms_{0};
|
||||
int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit
|
||||
// toolchains with small std::function SBO
|
||||
uint32_t millis_overflow_counter_{0};
|
||||
|
||||
std::queue<std::function<void()>> queue_;
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#include "mbedtls/ccm.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace bthome_mithermometer {
|
||||
@@ -196,6 +201,37 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
|
||||
const uint8_t *ciphertext = data.data() + 1;
|
||||
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// PSA AEAD expects ciphertext + tag concatenated
|
||||
// BLE advertisement max payload is 31 bytes, so this is always sufficient
|
||||
static constexpr size_t MAX_CT_WITH_TAG = 32;
|
||||
uint8_t ct_with_tag[MAX_CT_WITH_TAG];
|
||||
size_t ct_with_tag_size = ciphertext_size + BTHOME_MIC_SIZE;
|
||||
memcpy(ct_with_tag, ciphertext, ciphertext_size);
|
||||
memcpy(ct_with_tag + ciphertext_size, mic, BTHOME_MIC_SIZE);
|
||||
|
||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attributes, BTHOME_BINDKEY_SIZE * 8);
|
||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
|
||||
psa_set_key_algorithm(&attributes, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE));
|
||||
|
||||
mbedtls_svc_key_id_t key_id;
|
||||
if (psa_import_key(&attributes, this->bindkey_, BTHOME_BINDKEY_SIZE, &key_id) != PSA_SUCCESS) {
|
||||
ESP_LOGVV(TAG, "psa_import_key() failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t plaintext_length;
|
||||
psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE),
|
||||
nonce.data(), nonce.size(), nullptr, 0, ct_with_tag, ct_with_tag_size,
|
||||
payload.data(), ciphertext_size, &plaintext_length);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || plaintext_length != ciphertext_size) {
|
||||
ESP_LOGVV(TAG, "BTHome decryption failed.");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
mbedtls_ccm_context ctx;
|
||||
mbedtls_ccm_init(&ctx);
|
||||
|
||||
@@ -213,6 +249,7 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
|
||||
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,5 @@ void Button::press() {
|
||||
this->press_action();
|
||||
this->press_callback_.call();
|
||||
}
|
||||
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
|
||||
|
||||
} // namespace esphome::button
|
||||
|
||||
@@ -34,7 +34,9 @@ class Button : public EntityBase {
|
||||
*
|
||||
* @param callback The void() callback.
|
||||
*/
|
||||
void add_on_press_callback(std::function<void()> &&callback);
|
||||
template<typename F> void add_on_press_callback(F &&callback) {
|
||||
this->press_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
/** You should implement this virtual method if you want to create your own button.
|
||||
|
||||
@@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
|
||||
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
|
||||
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
|
||||
@@ -91,10 +91,7 @@ class Canbus : public Component {
|
||||
* - rtr If this is a remote transmission request
|
||||
* - data The message data
|
||||
*/
|
||||
void add_callback(
|
||||
std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) {
|
||||
this->callback_manager_.add(std::move(callback));
|
||||
}
|
||||
template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); }
|
||||
|
||||
protected:
|
||||
template<typename... Ts> friend class CanbusSendAction;
|
||||
|
||||
@@ -100,8 +100,9 @@ void DNSServer::process_next_request() {
|
||||
&client_addr_len);
|
||||
|
||||
if (len < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
||||
ESP_LOGE(TAG, "recvfrom failed: %d", errno);
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
|
||||
ESP_LOGE(TAG, "recvfrom failed: %d", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,14 +356,6 @@ ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode)
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Climate::add_on_state_callback(std::function<void(Climate &)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callback) {
|
||||
this->control_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Random 32bit value; If this changes existing restore preferences are invalidated
|
||||
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
|
||||
|
||||
|
||||
@@ -192,7 +192,9 @@ class Climate : public EntityBase {
|
||||
*
|
||||
* @param callback The callback to call.
|
||||
*/
|
||||
void add_on_state_callback(std::function<void(Climate &)> &&callback);
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a callback for the climate device configuration; each time the configuration parameters of a climate device
|
||||
@@ -200,7 +202,9 @@ class Climate : public EntityBase {
|
||||
*
|
||||
* @param callback The callback to call.
|
||||
*/
|
||||
void add_on_control_callback(std::function<void(ClimateCall &)> &&callback);
|
||||
template<typename F> void add_on_control_callback(F &&callback) {
|
||||
this->control_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** Make a climate device control call, this is used to control the climate device, see the ClimateCall description
|
||||
* for more info.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
@@ -20,6 +21,10 @@ CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_STOP_BITS = "stop_bits"
|
||||
CONF_USE_PSRAM = "use_psram"
|
||||
CONF_VOLUME_INCREMENT = "volume_increment"
|
||||
CONF_VOLUME_INITIAL = "volume_initial"
|
||||
CONF_VOLUME_MAX = "volume_max"
|
||||
CONF_VOLUME_MIN = "volume_min"
|
||||
|
||||
ICON_CURRENT_DC = "mdi:current-dc"
|
||||
ICON_SOLAR_PANEL = "mdi:solar-panel"
|
||||
|
||||
@@ -105,17 +105,18 @@ template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<f
|
||||
|
||||
template<bool OPEN> class CoverPositionTrigger : public Trigger<> {
|
||||
public:
|
||||
CoverPositionTrigger(Cover *a_cover) {
|
||||
a_cover->add_on_state_callback([this, a_cover]() {
|
||||
if (a_cover->position != this->last_position_) {
|
||||
this->last_position_ = a_cover->position;
|
||||
if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED))
|
||||
CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) {
|
||||
a_cover->add_on_state_callback([this]() {
|
||||
if (this->cover_->position != this->last_position_) {
|
||||
this->last_position_ = this->cover_->position;
|
||||
if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED))
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
Cover *cover_;
|
||||
float last_position_{NAN};
|
||||
};
|
||||
|
||||
@@ -124,9 +125,9 @@ using CoverClosedTrigger = CoverPositionTrigger<false>;
|
||||
|
||||
template<CoverOperation OP> class CoverTrigger : public Trigger<> {
|
||||
public:
|
||||
CoverTrigger(Cover *a_cover) {
|
||||
a_cover->add_on_state_callback([this, a_cover]() {
|
||||
auto current_op = a_cover->current_operation;
|
||||
CoverTrigger(Cover *a_cover) : cover_(a_cover) {
|
||||
a_cover->add_on_state_callback([this]() {
|
||||
auto current_op = this->cover_->current_operation;
|
||||
if (current_op == OP) {
|
||||
if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) {
|
||||
this->trigger();
|
||||
@@ -137,6 +138,7 @@ template<CoverOperation OP> class CoverTrigger : public Trigger<> {
|
||||
}
|
||||
|
||||
protected:
|
||||
Cover *cover_;
|
||||
optional<CoverOperation> last_operation_{};
|
||||
};
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -139,7 +139,6 @@ bool CoverCall::get_stop() const { return this->stop_; }
|
||||
|
||||
CoverCall Cover::make_call() { return {this}; }
|
||||
|
||||
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
|
||||
void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
|
||||
|
||||
@@ -125,7 +125,7 @@ class Cover : public EntityBase {
|
||||
/// Construct a new cover call used to control the cover.
|
||||
CoverCall make_call();
|
||||
|
||||
void add_on_state_callback(std::function<void()> &&f);
|
||||
template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); }
|
||||
|
||||
/** Publish the current state of the cover.
|
||||
*
|
||||
|
||||
@@ -14,7 +14,9 @@ class DateTimeBase : public EntityBase {
|
||||
public:
|
||||
virtual ESPTime state_as_esptime() const = 0;
|
||||
|
||||
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
|
||||
@@ -31,9 +33,12 @@ class DateTimeBase : public EntityBase {
|
||||
|
||||
class DateTimeStateTrigger : public Trigger<ESPTime> {
|
||||
public:
|
||||
explicit DateTimeStateTrigger(DateTimeBase *parent) {
|
||||
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
|
||||
explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); });
|
||||
}
|
||||
|
||||
protected:
|
||||
DateTimeBase *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::datetime
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <esp_sleep.h>
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_system.h>
|
||||
@@ -48,7 +49,8 @@ static const size_t REBOOT_MAX_LEN = 24;
|
||||
void DebugComponent::on_shutdown() {
|
||||
auto *component = App.get_current_component();
|
||||
char buffer[REBOOT_MAX_LEN]{};
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
|
||||
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
|
||||
if (component != nullptr) {
|
||||
strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1);
|
||||
buffer[REBOOT_MAX_LEN - 1] = '\0';
|
||||
@@ -65,7 +67,8 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
unsigned reason = esp_reset_reason();
|
||||
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
|
||||
if (reason == ESP_RST_SW) {
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
|
||||
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
|
||||
char reboot_source[REBOOT_MAX_LEN]{};
|
||||
if (pref.load(&reboot_source)) {
|
||||
reboot_source[REBOOT_MAX_LEN - 1] = '\0';
|
||||
@@ -82,32 +85,74 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buf;
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
static const char *const WAKEUP_CAUSES[] = {
|
||||
"undefined",
|
||||
"undefined",
|
||||
"external signal using RTC_IO",
|
||||
"external signal using RTC_CNTL",
|
||||
"timer",
|
||||
"touchpad",
|
||||
"ULP program",
|
||||
"GPIO",
|
||||
"UART",
|
||||
"WIFI",
|
||||
"COCPU int",
|
||||
"COCPU crash",
|
||||
"BT",
|
||||
"undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0)
|
||||
"undefined", // ESP_SLEEP_WAKEUP_ALL (1)
|
||||
"external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2)
|
||||
"external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3)
|
||||
"timer", // ESP_SLEEP_WAKEUP_TIMER (4)
|
||||
"touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5)
|
||||
"ULP program", // ESP_SLEEP_WAKEUP_ULP (6)
|
||||
"GPIO", // ESP_SLEEP_WAKEUP_GPIO (7)
|
||||
"UART", // ESP_SLEEP_WAKEUP_UART (8)
|
||||
"UART1", // ESP_SLEEP_WAKEUP_UART1 (9)
|
||||
"UART2", // ESP_SLEEP_WAKEUP_UART2 (10)
|
||||
"WIFI", // ESP_SLEEP_WAKEUP_WIFI (11)
|
||||
"COCPU int", // ESP_SLEEP_WAKEUP_COCPU (12)
|
||||
"COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (13)
|
||||
"BT", // ESP_SLEEP_WAKEUP_BT (14)
|
||||
"VAD", // ESP_SLEEP_WAKEUP_VAD (15)
|
||||
"VBAT under voltage", // ESP_SLEEP_WAKEUP_VBAT_UNDER_VOLT (16)
|
||||
};
|
||||
#else
|
||||
static const char *const WAKEUP_CAUSES[] = {
|
||||
"undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0)
|
||||
"undefined", // ESP_SLEEP_WAKEUP_ALL (1)
|
||||
"external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2)
|
||||
"external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3)
|
||||
"timer", // ESP_SLEEP_WAKEUP_TIMER (4)
|
||||
"touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5)
|
||||
"ULP program", // ESP_SLEEP_WAKEUP_ULP (6)
|
||||
"GPIO", // ESP_SLEEP_WAKEUP_GPIO (7)
|
||||
"UART", // ESP_SLEEP_WAKEUP_UART (8)
|
||||
"WIFI", // ESP_SLEEP_WAKEUP_WIFI (9)
|
||||
"COCPU int", // ESP_SLEEP_WAKEUP_COCPU (10)
|
||||
"COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (11)
|
||||
"BT", // ESP_SLEEP_WAKEUP_BT (12)
|
||||
};
|
||||
#endif
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
const char *wake_reason;
|
||||
unsigned reason = esp_sleep_get_wakeup_cause();
|
||||
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
|
||||
wake_reason = WAKEUP_CAUSES[reason];
|
||||
} else {
|
||||
wake_reason = "unknown source";
|
||||
static constexpr auto NUM_CAUSES = sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0]);
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// IDF 6.0+ returns a bitmap of all wakeup sources
|
||||
uint32_t causes = esp_sleep_get_wakeup_causes();
|
||||
if (causes == 0) {
|
||||
return WAKEUP_CAUSES[0]; // "undefined"
|
||||
}
|
||||
// Return the static string directly - no need to copy to buffer
|
||||
return wake_reason;
|
||||
char *p = buffer.data();
|
||||
char *end = p + buffer.size();
|
||||
*p = '\0';
|
||||
const char *sep = "";
|
||||
for (unsigned i = 0; i < NUM_CAUSES && p < end; i++) {
|
||||
if (causes & (1U << i)) {
|
||||
size_t needed = strlen(sep) + strlen(WAKEUP_CAUSES[i]);
|
||||
if (p + needed >= end) {
|
||||
break;
|
||||
}
|
||||
p += snprintf(p, end - p, "%s%s", sep, WAKEUP_CAUSES[i]);
|
||||
sep = ", ";
|
||||
}
|
||||
}
|
||||
return buffer.data();
|
||||
#else
|
||||
unsigned reason = esp_sleep_get_wakeup_cause();
|
||||
if (reason < NUM_CAUSES) {
|
||||
return WAKEUP_CAUSES[reason];
|
||||
}
|
||||
return "unknown source";
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugComponent::log_partition_info_() {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "deep_sleep_component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace deep_sleep {
|
||||
@@ -26,7 +27,7 @@ namespace deep_sleep {
|
||||
// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup)
|
||||
// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup)
|
||||
// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup)
|
||||
// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup)
|
||||
// - GPIO wakeup: GPIO wakeup for RTC pins
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
@@ -135,8 +136,13 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
}
|
||||
// Internal pullup/pulldown resistors are enabled automatically, when
|
||||
// ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is)
|
||||
esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(),
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << this->wakeup_pin_->get_pin(),
|
||||
static_cast<esp_sleep_gpio_wake_up_mode_t>(level));
|
||||
#else
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << this->wakeup_pin_->get_pin(),
|
||||
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ class DFPlayer : public uart::UARTDevice, public Component {
|
||||
bool is_playing() { return is_playing_; }
|
||||
void dump_config() override;
|
||||
|
||||
void add_on_finished_playback_callback(std::function<void()> callback) {
|
||||
this->on_finished_playback_callback_.add(std::move(callback));
|
||||
template<typename F> void add_on_finished_playback_callback(F &&callback) {
|
||||
this->on_finished_playback_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -96,37 +96,52 @@ template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
|
||||
|
||||
class DisplayMenuOnEnterTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) {
|
||||
parent->add_on_enter_callback([this, parent]() { this->trigger(parent); });
|
||||
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_enter_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnLeaveTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) {
|
||||
parent->add_on_leave_callback([this, parent]() { this->trigger(parent); });
|
||||
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_leave_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnValueTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnValueTrigger(MenuItem *parent) {
|
||||
parent->add_on_value_callback([this, parent]() { this->trigger(parent); });
|
||||
explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_value_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnNextTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) {
|
||||
parent->add_on_next_callback([this, parent]() { this->trigger(parent); });
|
||||
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_next_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnPrevTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) {
|
||||
parent->add_on_prev_callback([this, parent]() { this->trigger(parent); });
|
||||
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_prev_callback([this]() { this->trigger(this->parent_); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
} // namespace display_menu_base
|
||||
|
||||
@@ -44,9 +44,9 @@ class MenuItem {
|
||||
MenuItemMenu *get_parent() { return this->parent_; }
|
||||
MenuItemType get_type() const { return this->item_type_; }
|
||||
template<typename V> void set_text(V val) { this->text_ = val; }
|
||||
void add_on_enter_callback(std::function<void()> &&cb) { this->on_enter_callbacks_.add(std::move(cb)); }
|
||||
void add_on_leave_callback(std::function<void()> &&cb) { this->on_leave_callbacks_.add(std::move(cb)); }
|
||||
void add_on_value_callback(std::function<void()> &&cb) { this->on_value_callbacks_.add(std::move(cb)); }
|
||||
template<typename F> void add_on_enter_callback(F &&cb) { this->on_enter_callbacks_.add(std::forward<F>(cb)); }
|
||||
template<typename F> void add_on_leave_callback(F &&cb) { this->on_leave_callbacks_.add(std::forward<F>(cb)); }
|
||||
template<typename F> void add_on_value_callback(F &&cb) { this->on_value_callbacks_.add(std::forward<F>(cb)); }
|
||||
|
||||
std::string get_text() const { return const_cast<MenuItem *>(this)->text_.value(this); }
|
||||
virtual bool get_immediate_edit() const { return false; }
|
||||
@@ -170,8 +170,8 @@ class MenuItemCommand : public MenuItem {
|
||||
class MenuItemCustom : public MenuItemEditable {
|
||||
public:
|
||||
explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {}
|
||||
void add_on_next_callback(std::function<void()> &&cb) { this->on_next_callbacks_.add(std::move(cb)); }
|
||||
void add_on_prev_callback(std::function<void()> &&cb) { this->on_prev_callbacks_.add(std::move(cb)); }
|
||||
template<typename F> void add_on_next_callback(F &&cb) { this->on_next_callbacks_.add(std::forward<F>(cb)); }
|
||||
template<typename F> void add_on_prev_callback(F &&cb) { this->on_prev_callbacks_.add(std::forward<F>(cb)); }
|
||||
|
||||
bool has_value() const override { return this->value_getter_.has_value(); }
|
||||
std::string get_value_text() const override;
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
||||
#include <bearssl/bearssl.h>
|
||||
#elif defined(USE_ESP32)
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#include "mbedtls/esp_config.h"
|
||||
#include "mbedtls/gcm.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
@@ -240,6 +245,35 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m
|
||||
br_gcm_flip(&gcm_ctx);
|
||||
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
|
||||
#elif defined(USE_ESP32)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// PSA Crypto multipart AEAD (no tag verification, matching legacy behavior)
|
||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attributes, this->decryption_key_.size() * 8);
|
||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
|
||||
psa_set_key_algorithm(&attributes, PSA_ALG_GCM);
|
||||
|
||||
mbedtls_svc_key_id_t key_id;
|
||||
bool decrypt_failed = true;
|
||||
if (psa_import_key(&attributes, this->decryption_key_.data(), this->decryption_key_.size(), &key_id) == PSA_SUCCESS) {
|
||||
psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT;
|
||||
if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS &&
|
||||
psa_aead_set_nonce(&op, iv, sizeof(iv)) == PSA_SUCCESS) {
|
||||
size_t outlen = 0;
|
||||
if (psa_aead_update(&op, payload_ptr, message_length, payload_ptr, message_length, &outlen) == PSA_SUCCESS &&
|
||||
outlen == message_length) {
|
||||
decrypt_failed = false;
|
||||
}
|
||||
}
|
||||
psa_aead_abort(&op);
|
||||
psa_destroy_key(key_id);
|
||||
}
|
||||
if (decrypt_failed) {
|
||||
ESP_LOGE(TAG, "Decryption failed");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
size_t outlen = 0;
|
||||
mbedtls_gcm_context gcm_ctx;
|
||||
mbedtls_gcm_init(&gcm_ctx);
|
||||
@@ -252,6 +286,7 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
#error "Invalid Platform"
|
||||
#endif
|
||||
|
||||
@@ -127,8 +127,7 @@ void DPS310Component::read_() {
|
||||
this->update_in_progress_ = false;
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
auto f = std::bind(&DPS310Component::read_, this);
|
||||
this->set_timeout("dps310", 10, f);
|
||||
this->set_timeout("dps310", 10, [this]() { this->read_(); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from esphome.const import (
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
CONF_REF,
|
||||
CONF_SAFE_MODE,
|
||||
CONF_SIZE,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_VARIANT,
|
||||
@@ -59,6 +60,7 @@ from .const import ( # noqa
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_FULL_CERT_BUNDLE,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
KEY_REPO,
|
||||
@@ -95,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_RELEASE = "release"
|
||||
CONF_SUBTYPE = "subtype"
|
||||
|
||||
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
|
||||
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
|
||||
@@ -420,9 +423,20 @@ def set_core_data(config):
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
|
||||
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
|
||||
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
|
||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
|
||||
config[CONF_FRAMEWORK][CONF_VERSION]
|
||||
)
|
||||
framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION])
|
||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver
|
||||
|
||||
# Store the underlying IDF version for framework-agnostic checks
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
|
||||
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
|
||||
else:
|
||||
raise cv.Invalid(
|
||||
f"Arduino version {framework_ver} has no known ESP-IDF version mapping. "
|
||||
"Please update ARDUINO_IDF_VERSION_LOOKUP.",
|
||||
path=[CONF_FRAMEWORK, CONF_VERSION],
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
|
||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
||||
@@ -600,10 +614,12 @@ def _format_framework_espidf_version(
|
||||
ext = "tar.xz"
|
||||
else:
|
||||
ext = "zip"
|
||||
# Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1")
|
||||
# Build version string with extra separator based on type:
|
||||
# numeric extra uses dot (e.g., "5.5.3.1"), string extra uses dash (e.g., "6.0.0-rc1")
|
||||
ver_str = f"{ver.major}.{ver.minor}.{ver.patch}"
|
||||
if ver.extra:
|
||||
ver_str += f".{ver.extra}"
|
||||
sep = "." if str(ver.extra).isdigit() else "-"
|
||||
ver_str += f"{sep}{ver.extra}"
|
||||
if release:
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}"
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}"
|
||||
@@ -974,6 +990,7 @@ KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
||||
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
@@ -1043,6 +1060,25 @@ def require_mbedtls_pkcs7() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_sha512() -> None:
|
||||
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
|
||||
|
||||
Call this from components that need to verify TLS certificates or signatures
|
||||
using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C
|
||||
and CONFIG_MBEDTLS_SHA512_C from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True
|
||||
|
||||
|
||||
def idf_version() -> cv.Version:
|
||||
"""Return the underlying ESP-IDF version regardless of framework choice.
|
||||
|
||||
For ESP-IDF builds this is the framework version directly.
|
||||
For Arduino builds this is the mapped IDF version from ARDUINO_IDF_VERSION_LOOKUP.
|
||||
"""
|
||||
return CORE.data[KEY_ESP32][KEY_IDF_VERSION]
|
||||
|
||||
|
||||
def require_fatfs() -> None:
|
||||
"""Mark that FATFS support is required by a component.
|
||||
|
||||
@@ -1224,6 +1260,43 @@ def _set_default_framework(config):
|
||||
return config
|
||||
|
||||
|
||||
RESERVED_PARTITION_NAMES = {
|
||||
"nvs",
|
||||
"app0",
|
||||
"app1",
|
||||
"otadata",
|
||||
"eeprom",
|
||||
"spiffs",
|
||||
"phy_init",
|
||||
}
|
||||
|
||||
VALID_APP_SUBTYPES = {"factory", "test"}
|
||||
VALID_DATA_SUBTYPES = {
|
||||
"nvs",
|
||||
"nvs_keys",
|
||||
"spiffs",
|
||||
"coredump",
|
||||
"efuse",
|
||||
"fat",
|
||||
"undefined",
|
||||
"littlefs",
|
||||
}
|
||||
|
||||
|
||||
def _validate_custom_partition(config: ConfigType) -> ConfigType:
|
||||
"""Voluptuous validator for custom partition schema."""
|
||||
try:
|
||||
_validate_partition(
|
||||
config[CONF_NAME],
|
||||
config[CONF_TYPE],
|
||||
config[CONF_SUBTYPE],
|
||||
config[CONF_SIZE],
|
||||
)
|
||||
except ValueError as e:
|
||||
raise cv.Invalid(str(e)) from e
|
||||
return config
|
||||
|
||||
|
||||
FLASH_SIZES = [
|
||||
"2MB",
|
||||
"4MB",
|
||||
@@ -1246,7 +1319,28 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
|
||||
*FLASH_SIZES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_PARTITIONS): cv.file_,
|
||||
cv.Optional(CONF_PARTITIONS): cv.Any(
|
||||
cv.file_,
|
||||
cv.ensure_list(
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Required(CONF_TYPE): cv.All(
|
||||
cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)),
|
||||
cv.int_to_hex_string,
|
||||
),
|
||||
cv.Required(CONF_SUBTYPE): cv.All(
|
||||
cv.Any(cv.string_strict, cv.int_range(0, 0xFE)),
|
||||
cv.int_to_hex_string,
|
||||
),
|
||||
cv.Required(CONF_SIZE): cv.int_range(min=0x1000),
|
||||
}
|
||||
),
|
||||
_validate_custom_partition,
|
||||
),
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
|
||||
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
|
||||
}
|
||||
@@ -1715,9 +1809,18 @@ async def to_code(config):
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if CONF_PARTITIONS in config:
|
||||
add_extra_build_file(
|
||||
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
||||
)
|
||||
if isinstance(config[CONF_PARTITIONS], list):
|
||||
for partition in config[CONF_PARTITIONS]:
|
||||
add_partition(
|
||||
partition[CONF_NAME],
|
||||
partition[CONF_TYPE],
|
||||
partition[CONF_SUBTYPE],
|
||||
partition[CONF_SIZE],
|
||||
)
|
||||
else:
|
||||
add_extra_build_file(
|
||||
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
||||
)
|
||||
|
||||
if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
|
||||
for key, flag in ASSERTION_LEVELS.items():
|
||||
@@ -1802,6 +1905,33 @@ async def to_code(config):
|
||||
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
|
||||
|
||||
# Disable SHA-384 and SHA-512 in mbedTLS
|
||||
# ESPHome doesn't use either algorithm. SHA-384 shares the same
|
||||
# compression function as SHA-512 (mbedtls_internal_sha512_process),
|
||||
# so both must be disabled to eliminate the ~3KB software fallback
|
||||
# that IDF 6.0's PSA parallel engine always links in.
|
||||
# On IDF < 6.0 these are a single config and hardware-only (no
|
||||
# software fallback), so there was no code size cost to leaving
|
||||
# them enabled.
|
||||
# Components that need SHA-384/SHA-512 can call require_mbedtls_sha512()
|
||||
if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get(
|
||||
KEY_MBEDTLS_SHA512_REQUIRED, False
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
|
||||
|
||||
# Disable PicolibC Newlib compatibility shim on IDF 6.0+
|
||||
# IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local
|
||||
# stdin/stdout/stderr and getreent() for code compiled against Newlib.
|
||||
# ESPHome doesn't link against Newlib-built libraries that use stdio.
|
||||
# If a component needs it (e.g. precompiled Newlib binaries), re-enable via:
|
||||
# esp32:
|
||||
# framework:
|
||||
# sdkconfig_options:
|
||||
# CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y"
|
||||
if idf_version() >= cv.Version(6, 0, 0):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False)
|
||||
|
||||
# Disable regi2c control functions in IRAM
|
||||
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
|
||||
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
|
||||
@@ -1836,45 +1966,175 @@ async def to_code(config):
|
||||
CORE.add_job(_write_arduino_libraries_sdkconfig)
|
||||
|
||||
|
||||
APP_PARTITION_SIZES = {
|
||||
"2MB": 0x0C0000, # 768 KB
|
||||
"4MB": 0x1C0000, # 1792 KB
|
||||
"8MB": 0x3C0000, # 3840 KB
|
||||
"16MB": 0x7C0000, # 7936 KB
|
||||
"32MB": 0xFC0000, # 16128 KB
|
||||
KEY_CUSTOM_PARTITIONS = "custom_partitions"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PartitionEntry:
|
||||
name: str
|
||||
type: str
|
||||
subtype: str
|
||||
size: int
|
||||
|
||||
|
||||
# Partition sizes (offsets auto-placed by gen_esp32part.py).
|
||||
# These constants are the single source of truth — used in both
|
||||
# the CSV generation and the overhead calculation.
|
||||
BOOTLOADER_SIZE = 0x8000
|
||||
PARTITION_TABLE_SIZE = 0x1000
|
||||
FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE
|
||||
OTADATA_SIZE = 0x2000
|
||||
PHY_INIT_SIZE = 0x1000
|
||||
EEPROM_SIZE = 0x1000 # Arduino only
|
||||
SPIFFS_SIZE = 0xF000 # Arduino only
|
||||
ARDUINO_NVS_SIZE = 0x60000
|
||||
IDF_NVS_SIZE = 0x70000
|
||||
|
||||
|
||||
def _get_partition_overhead() -> int:
|
||||
"""Total non-app partition budget (system partitions + nvs + padding).
|
||||
|
||||
Custom partitions are appended at the end and steal from app.
|
||||
"""
|
||||
# otadata + phy_init are followed by app0 which requires 64KB alignment,
|
||||
# so pad up to the next 64KB boundary.
|
||||
overhead = (
|
||||
FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF
|
||||
) & ~0xFFFF
|
||||
if CORE.using_arduino:
|
||||
overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE
|
||||
else:
|
||||
overhead += IDF_NVS_SIZE
|
||||
return overhead
|
||||
|
||||
|
||||
VALID_SUBTYPES: dict[str, set[str]] = {
|
||||
"app": VALID_APP_SUBTYPES,
|
||||
"data": VALID_DATA_SUBTYPES,
|
||||
}
|
||||
|
||||
|
||||
def get_arduino_partition_csv(flash_size: str):
|
||||
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
||||
eeprom_partition_size = 0x1000 # 4 KB
|
||||
spiffs_partition_size = 0xF000 # 60 KB
|
||||
|
||||
app0_partition_start = 0x010000 # 64 KB
|
||||
app1_partition_start = app0_partition_start + app_partition_size
|
||||
eeprom_partition_start = app1_partition_start + app_partition_size
|
||||
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
|
||||
|
||||
return f"""\
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xE000, 0x2000,
|
||||
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
|
||||
app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
|
||||
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
|
||||
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
|
||||
"""
|
||||
def _validate_partition(
|
||||
name: str, p_type: str | int, subtype: str | int, size: int
|
||||
) -> None:
|
||||
"""Validate partition parameters. Raises ValueError on invalid input."""
|
||||
if name in RESERVED_PARTITION_NAMES:
|
||||
raise ValueError(f"Partition name '{name}' is reserved.")
|
||||
if size % 0x1000 != 0:
|
||||
raise ValueError("Partition size must be 4KB (0x1000) aligned.")
|
||||
# Numeric or already-normalized hex types/subtypes skip string validation
|
||||
if not isinstance(p_type, str) or p_type.startswith("0x"):
|
||||
return
|
||||
if p_type not in VALID_SUBTYPES:
|
||||
raise ValueError(
|
||||
f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed."
|
||||
" Use numbers for custom types."
|
||||
)
|
||||
if not isinstance(subtype, str) or subtype.startswith("0x"):
|
||||
return
|
||||
valid = VALID_SUBTYPES[p_type]
|
||||
if subtype not in valid:
|
||||
raise ValueError(
|
||||
f"Subtype '{subtype}' is invalid for {p_type} type."
|
||||
f" Only {', '.join(sorted(valid))} are allowed."
|
||||
" Use numbers for custom subtypes."
|
||||
)
|
||||
|
||||
|
||||
def get_idf_partition_csv(flash_size: str):
|
||||
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
||||
def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None:
|
||||
"""Register a custom partition to be appended to the partition table.
|
||||
|
||||
return f"""\
|
||||
otadata, data, ota, , 0x2000,
|
||||
phy_init, data, phy, , 0x1000,
|
||||
app0, app, ota_0, , 0x{app_partition_size:X},
|
||||
app1, app, ota_1, , 0x{app_partition_size:X},
|
||||
nvs, data, nvs, , 0x6D000,
|
||||
"""
|
||||
Called from component to_code() to request additional flash partitions.
|
||||
Size must be 4KB aligned. Integer types/subtypes are converted to hex strings.
|
||||
"""
|
||||
if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}):
|
||||
raise ValueError(f"Partition name '{name}' is already defined.")
|
||||
_validate_partition(name, p_type, subtype, size)
|
||||
p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type
|
||||
subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype
|
||||
custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {})
|
||||
custom_partitions[name] = PartitionEntry(
|
||||
name=name, type=p_type_str, subtype=subtype_str, size=size
|
||||
)
|
||||
|
||||
|
||||
def _flash_size_to_bytes(flash_size_mb: str) -> int:
|
||||
"""Convert flash size string (e.g. '4MB') to bytes."""
|
||||
return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024
|
||||
|
||||
|
||||
def _get_custom_partitions_total_size() -> int:
|
||||
"""Total size of custom partitions including alignment padding."""
|
||||
size = 0
|
||||
for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values():
|
||||
if partition.type == "app":
|
||||
size = (size + 0xFFFF) & ~0xFFFF # align to 64KB
|
||||
size += partition.size
|
||||
return size
|
||||
|
||||
|
||||
def _get_app_partition_size(flash_size_mb: str) -> int:
|
||||
flash_bytes = _flash_size_to_bytes(flash_size_mb)
|
||||
custom_total = _get_custom_partitions_total_size()
|
||||
# Align down to 64KB — app partitions require 64KB-aligned offsets,
|
||||
# so the size must also be aligned to avoid unbudgeted padding.
|
||||
raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2
|
||||
app_size = raw_size & ~0xFFFF
|
||||
wasted = (raw_size - app_size) * 2
|
||||
if wasted:
|
||||
_LOGGER.info(
|
||||
"Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.",
|
||||
wasted // 1024,
|
||||
)
|
||||
if app_size <= 0x10000: # 64 KB
|
||||
raise ValueError(
|
||||
"Custom partitions are too large to fit in the available flash size. "
|
||||
"Reduce custom partition sizes."
|
||||
)
|
||||
if app_size <= 0x80000: # 512 KB
|
||||
_LOGGER.warning(
|
||||
"App partition size is only %dKB. This may be too small for firmware with "
|
||||
"many components. Consider reducing custom partition sizes or using a "
|
||||
"larger flash chip.",
|
||||
app_size // 1024,
|
||||
)
|
||||
return app_size
|
||||
|
||||
|
||||
def get_partition_csv(flash_size_mb: str) -> str:
|
||||
app_size = _get_app_partition_size(flash_size_mb)
|
||||
|
||||
partitions: list[PartitionEntry] = [
|
||||
PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE),
|
||||
PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE),
|
||||
PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size),
|
||||
PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size),
|
||||
]
|
||||
if CORE.using_arduino:
|
||||
partitions.append(
|
||||
PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE)
|
||||
)
|
||||
partitions.append(
|
||||
PartitionEntry(
|
||||
name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE
|
||||
)
|
||||
)
|
||||
partitions.append(
|
||||
PartitionEntry(
|
||||
name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE
|
||||
)
|
||||
)
|
||||
else:
|
||||
partitions.append(
|
||||
PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE)
|
||||
)
|
||||
partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values())
|
||||
|
||||
csv = "".join(
|
||||
f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions
|
||||
)
|
||||
_LOGGER.debug("Partition table:\n%s", csv)
|
||||
return csv
|
||||
|
||||
|
||||
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
|
||||
@@ -1981,16 +2241,10 @@ def copy_files():
|
||||
|
||||
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
|
||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||
if CORE.using_arduino:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_arduino_partition_csv(flash_size),
|
||||
)
|
||||
else:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_idf_partition_csv(flash_size),
|
||||
)
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_partition_csv(flash_size),
|
||||
)
|
||||
# IDF build scripts look for version string to put in the build.
|
||||
# However, if the build path does not have an initialized git repo,
|
||||
# and no version.txt file exists, the CMake script fails for some setups.
|
||||
|
||||
@@ -15,6 +15,7 @@ KEY_PATH = "path"
|
||||
KEY_SUBMODULES = "submodules"
|
||||
KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
||||
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
|
||||
KEY_IDF_VERSION = "idf_version"
|
||||
|
||||
VARIANT_ESP32 = "ESP32"
|
||||
VARIANT_ESP32C2 = "ESP32C2"
|
||||
|
||||
@@ -53,9 +53,6 @@ void arch_init() {
|
||||
}
|
||||
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
uint32_t freq = 0;
|
||||
|
||||
@@ -14,18 +14,11 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
uint32_t random_uint32() { return esp_random(); }
|
||||
bool random_bytes(uint8_t *data, size_t len) {
|
||||
esp_fill_random(data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
|
||||
Mutex::~Mutex() {}
|
||||
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
|
||||
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
|
||||
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
|
||||
// only affects the executing core
|
||||
// so should not be used as a mutex lock, only to get accurate timing
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
class ESP32PreferenceBackend final {
|
||||
public:
|
||||
bool save(const uint8_t *data, size_t len);
|
||||
bool load(uint8_t *data, size_t len);
|
||||
|
||||
uint32_t key;
|
||||
uint32_t nvs_handle;
|
||||
};
|
||||
|
||||
class ESP32Preferences;
|
||||
ESP32Preferences *get_preferences();
|
||||
|
||||
} // namespace esphome::esp32
|
||||
|
||||
namespace esphome {
|
||||
using PreferenceBackend = esp32::ESP32PreferenceBackend;
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,18 +1,16 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "preferences.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32 {
|
||||
namespace esphome::esp32 {
|
||||
|
||||
static const char *const TAG = "esp32.preferences";
|
||||
static const char *const TAG = "preferences";
|
||||
|
||||
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
|
||||
static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
@@ -24,185 +22,175 @@ struct NVSData {
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
public:
|
||||
uint32_t key;
|
||||
uint32_t nvs_handle;
|
||||
bool save(const uint8_t *data, size_t len) override {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
return true;
|
||||
}
|
||||
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
return true;
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
bool load(uint8_t *data, size_t len) override {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
if (actual_len != len) {
|
||||
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
|
||||
return false;
|
||||
}
|
||||
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32Preferences::open() {
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
return;
|
||||
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
|
||||
err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err != 0) {
|
||||
this->nvs_handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = this->nvs_handle;
|
||||
pref->key = type;
|
||||
|
||||
return ESPPreferenceObject(pref);
|
||||
}
|
||||
|
||||
bool ESP32Preferences::sync() {
|
||||
if (s_pending_save.empty())
|
||||
return true;
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
if (actual_len != len) {
|
||||
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
|
||||
return false;
|
||||
}
|
||||
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class ESP32Preferences : public ESPPreferences {
|
||||
public:
|
||||
uint32_t nvs_handle;
|
||||
|
||||
void open() {
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
|
||||
if (err == 0)
|
||||
return;
|
||||
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
|
||||
err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
|
||||
if (err != 0) {
|
||||
nvs_handle = 0;
|
||||
}
|
||||
}
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
|
||||
return this->make_preference(length, type);
|
||||
}
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = this->nvs_handle;
|
||||
pref->key = type;
|
||||
|
||||
return ESPPreferenceObject(pref);
|
||||
}
|
||||
|
||||
bool sync() override {
|
||||
if (s_pending_save.empty())
|
||||
return true;
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
continue;
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
cached++;
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
continue;
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.clear();
|
||||
}
|
||||
s_pending_save.clear();
|
||||
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
|
||||
last_key);
|
||||
}
|
||||
|
||||
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
|
||||
esp_err_t err = nvs_commit(this->nvs_handle);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return failed == 0;
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
|
||||
last_key);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256> stored_data(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
|
||||
esp_err_t err = nvs_commit(this->nvs_handle);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
ESP_LOGD(TAG, "Erasing storage");
|
||||
s_pending_save.clear();
|
||||
return failed == 0;
|
||||
}
|
||||
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
// Make the handle invalid to prevent any saves until restart
|
||||
nvs_handle = 0;
|
||||
bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256> stored_data(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
}
|
||||
|
||||
bool ESP32Preferences::reset() {
|
||||
ESP_LOGD(TAG, "Erasing storage");
|
||||
s_pending_save.clear();
|
||||
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
// Make the handle invalid to prevent any saves until restart
|
||||
this->nvs_handle = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
ESP32Preferences *get_preferences() { return &s_preferences; }
|
||||
|
||||
void setup_preferences() {
|
||||
s_preferences.open();
|
||||
global_preferences = &s_preferences;
|
||||
}
|
||||
|
||||
} // namespace esp32
|
||||
} // namespace esphome::esp32
|
||||
|
||||
namespace esphome {
|
||||
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32 {
|
||||
#include "esphome/core/preference_backend.h"
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
struct NVSData;
|
||||
|
||||
class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
|
||||
public:
|
||||
using PreferencesMixin<ESP32Preferences>::make_preference;
|
||||
void open();
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) {
|
||||
return this->make_preference(length, type);
|
||||
}
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type);
|
||||
bool sync();
|
||||
bool reset();
|
||||
|
||||
uint32_t nvs_handle;
|
||||
|
||||
protected:
|
||||
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str);
|
||||
};
|
||||
|
||||
void setup_preferences();
|
||||
|
||||
} // namespace esp32
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32
|
||||
|
||||
DECLARE_PREFERENCE_ALIASES(esphome::esp32::ESP32Preferences)
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -81,8 +81,6 @@ void ESP32BLE::disable() {
|
||||
this->state_ = BLE_COMPONENT_STATE_DISABLE;
|
||||
}
|
||||
|
||||
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_start() {
|
||||
this->advertising_init_();
|
||||
|
||||
@@ -135,7 +135,7 @@ class ESP32BLE : public Component {
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
bool is_active();
|
||||
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <algorithm>
|
||||
#include <nvs_flash.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <esp_bt_main.h>
|
||||
@@ -39,16 +38,17 @@ void BLEServer::loop() {
|
||||
case RUNNING: {
|
||||
// Start all services that are pending to start
|
||||
if (!this->services_to_start_.empty()) {
|
||||
for (auto &service : this->services_to_start_) {
|
||||
size_t write_idx = 0;
|
||||
for (auto *service : this->services_to_start_) {
|
||||
if (service->is_created()) {
|
||||
service->start(); // Needs to be called once per characteristic in the service
|
||||
}
|
||||
// Remove services that have started or are starting
|
||||
if (!service->is_starting() && !service->is_running()) {
|
||||
this->services_to_start_[write_idx++] = service;
|
||||
}
|
||||
}
|
||||
// Remove services that have been started
|
||||
this->services_to_start_.erase(
|
||||
std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(),
|
||||
[](BLEService *service) { return service->is_starting() || service->is_running(); }),
|
||||
this->services_to_start_.end());
|
||||
this->services_to_start_.erase(this->services_to_start_.begin() + write_idx, this->services_to_start_.end());
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -91,8 +91,6 @@ void BLEServer::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
|
||||
|
||||
bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); }
|
||||
|
||||
void BLEServer::restart_advertising_() {
|
||||
|
||||
@@ -32,7 +32,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
|
||||
bool is_running();
|
||||
ESPHOME_ALWAYS_INLINE bool is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
|
||||
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->manufacturer_data_ = data;
|
||||
|
||||
@@ -27,8 +27,14 @@
|
||||
#include <esp_coexist.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
#ifdef USE_BLE_TRACKER_PSA_AES
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#define MBEDTLS_AES_ALT
|
||||
#include <aes_alt.h>
|
||||
#endif
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
// bt_trace.h
|
||||
#undef TAG
|
||||
@@ -738,23 +744,48 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
|
||||
}
|
||||
|
||||
bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
||||
uint8_t ecb_key[16];
|
||||
uint8_t ecb_plaintext[16];
|
||||
uint8_t ecb_ciphertext[16];
|
||||
static constexpr size_t AES_BLOCK_SIZE = 16;
|
||||
static constexpr size_t AES_KEY_BITS = 128;
|
||||
|
||||
uint8_t ecb_key[AES_BLOCK_SIZE];
|
||||
uint8_t ecb_plaintext[AES_BLOCK_SIZE];
|
||||
uint8_t ecb_ciphertext[AES_BLOCK_SIZE];
|
||||
|
||||
uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_);
|
||||
|
||||
memcpy(&ecb_key, irk, 16);
|
||||
memset(&ecb_plaintext, 0, 16);
|
||||
memcpy(&ecb_key, irk, AES_BLOCK_SIZE);
|
||||
memset(&ecb_plaintext, 0, AES_BLOCK_SIZE);
|
||||
|
||||
ecb_plaintext[13] = (addr64 >> 40) & 0xff;
|
||||
ecb_plaintext[14] = (addr64 >> 32) & 0xff;
|
||||
ecb_plaintext[15] = (addr64 >> 24) & 0xff;
|
||||
|
||||
#ifdef USE_BLE_TRACKER_PSA_AES
|
||||
// Use PSA Crypto API (mbedtls 4.0 / IDF 6.0+)
|
||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attributes, AES_KEY_BITS);
|
||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT);
|
||||
psa_set_key_algorithm(&attributes, PSA_ALG_ECB_NO_PADDING);
|
||||
|
||||
mbedtls_svc_key_id_t key_id;
|
||||
if (psa_import_key(&attributes, ecb_key, AES_BLOCK_SIZE, &key_id) != PSA_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t output_length;
|
||||
psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING, ecb_plaintext, AES_BLOCK_SIZE,
|
||||
ecb_ciphertext, AES_BLOCK_SIZE, &output_length);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || output_length != AES_BLOCK_SIZE) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
// Use legacy mbedtls AES API (IDF < 6.0)
|
||||
mbedtls_aes_context ctx = {0, 0, {0}};
|
||||
mbedtls_aes_init(&ctx);
|
||||
|
||||
if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) {
|
||||
if (mbedtls_aes_setkey_enc(&ctx, ecb_key, AES_KEY_BITS) != 0) {
|
||||
mbedtls_aes_free(&ctx);
|
||||
return false;
|
||||
}
|
||||
@@ -765,6 +796,7 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
||||
}
|
||||
|
||||
mbedtls_aes_free(&ctx);
|
||||
#endif
|
||||
|
||||
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
|
||||
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls AES API.
|
||||
// Use the PSA Crypto API instead.
|
||||
#define USE_BLE_TRACKER_PSA_AES
|
||||
#endif
|
||||
|
||||
#include <esp_bt_defs.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
@@ -400,7 +400,7 @@ async def to_code(config):
|
||||
if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG":
|
||||
cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION")
|
||||
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
|
||||
|
||||
|
||||
@@ -4,14 +4,7 @@ from pathlib import Path
|
||||
from esphome import pins
|
||||
from esphome.components import esp32
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CLK_PIN,
|
||||
CONF_RESET_PIN,
|
||||
CONF_VARIANT,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT
|
||||
from esphome.cpp_generator import add_define
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
@@ -100,9 +93,9 @@ async def to_code(config):
|
||||
int(config[CONF_SDIO_FREQUENCY] // 1000),
|
||||
)
|
||||
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||
if framework_ver >= cv.Version(5, 5, 0):
|
||||
idf_ver = esp32.idf_version()
|
||||
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
|
||||
if idf_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1")
|
||||
|
||||
@@ -12,58 +12,73 @@ namespace esp32_improv {
|
||||
|
||||
class ESP32ImprovProvisionedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONED && !parent->is_failed()) {
|
||||
trigger();
|
||||
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ESP32ImprovProvisioningTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONING && !parent->is_failed()) {
|
||||
trigger();
|
||||
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ESP32ImprovStartTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
|
||||
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
|
||||
!parent->is_failed()) {
|
||||
trigger();
|
||||
!this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ESP32ImprovStateTrigger : public Trigger<improv::State, improv::Error> {
|
||||
public:
|
||||
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
|
||||
if (!parent->is_failed()) {
|
||||
trigger(state, error);
|
||||
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (!this->parent_->is_failed()) {
|
||||
this->trigger(state, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
class ESP32ImprovStoppedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) {
|
||||
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_STOPPED && !parent->is_failed()) {
|
||||
trigger();
|
||||
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
|
||||
if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) {
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
ESP32ImprovComponent *parent_;
|
||||
};
|
||||
|
||||
} // namespace esp32_improv
|
||||
|
||||
@@ -350,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
||||
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
|
||||
this->set_timeout("wifi-connect-timeout", 30000, f);
|
||||
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
|
||||
this->incoming_data_.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
||||
bool should_start() const { return this->should_start_; }
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -99,8 +99,6 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
channel.gpio_num = gpio_num_t(this->pin_);
|
||||
channel.mem_block_symbols = this->rmt_symbols_;
|
||||
channel.trans_queue_depth = 1;
|
||||
channel.flags.io_loop_back = 0;
|
||||
channel.flags.io_od_mode = 0;
|
||||
channel.flags.invert_out = this->invert_out_;
|
||||
channel.flags.with_dma = this->use_dma_;
|
||||
channel.intr_priority = 0;
|
||||
|
||||
@@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() {
|
||||
}
|
||||
|
||||
// Publish initial OFF state for sensors that haven't received events yet
|
||||
bool all_initial_published = true;
|
||||
for (auto *child : this->children_) {
|
||||
this->publish_initial_state_if_needed_(child, now);
|
||||
if (!child->initial_state_published_) {
|
||||
all_initial_published = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->setup_mode_) {
|
||||
// Only disable loop once all initial states are published
|
||||
if (!this->setup_mode_ && all_initial_published) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,8 @@ namespace esphome {
|
||||
uint32_t random_uint32() { return os_random(); }
|
||||
bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
|
||||
|
||||
// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
|
||||
Mutex::Mutex() {}
|
||||
Mutex::~Mutex() {}
|
||||
void Mutex::lock() {}
|
||||
bool Mutex::try_lock() { return true; }
|
||||
void Mutex::unlock() {}
|
||||
// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2040) is set,
|
||||
// independent of the ESPHOME_THREAD_SINGLE thread model define.
|
||||
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
class ESP8266PreferenceBackend final {
|
||||
public:
|
||||
bool save(const uint8_t *data, size_t len);
|
||||
bool load(uint8_t *data, size_t len);
|
||||
|
||||
uint32_t type = 0;
|
||||
uint16_t offset = 0;
|
||||
uint8_t length_words = 0; // Max 255 words (1020 bytes of data)
|
||||
bool in_flash = false;
|
||||
};
|
||||
|
||||
class ESP8266Preferences;
|
||||
ESP8266Preferences *get_preferences();
|
||||
|
||||
} // namespace esphome::esp8266
|
||||
|
||||
namespace esphome {
|
||||
using PreferenceBackend = esp8266::ESP8266PreferenceBackend;
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -5,17 +5,15 @@ extern "C" {
|
||||
#include "spi_flash.h"
|
||||
}
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "preferences.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
static const char *const TAG = "esp8266.preferences";
|
||||
static const char *const TAG = "preferences";
|
||||
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
|
||||
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
|
||||
@@ -137,155 +135,135 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
||||
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
public:
|
||||
uint32_t type = 0;
|
||||
uint16_t offset = 0;
|
||||
uint8_t length_words = 0; // Max 255 words (1020 bytes of data)
|
||||
bool in_flash = false;
|
||||
bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||
memcpy(buffer, data, len);
|
||||
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
||||
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
||||
: save_to_rtc(this->offset, buffer, buffer_size);
|
||||
}
|
||||
|
||||
bool save(const uint8_t *data, size_t len) override {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||
memcpy(buffer, data, len);
|
||||
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
||||
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
||||
: save_to_rtc(this->offset, buffer, buffer_size);
|
||||
bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||
if (!ret)
|
||||
return false;
|
||||
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
||||
return false;
|
||||
memcpy(data, buffer, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP8266Preferences::setup() {
|
||||
ESP_LOGVV(TAG, "Loading preferences from flash");
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
|
||||
}
|
||||
}
|
||||
|
||||
ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
|
||||
const uint32_t length_words = bytes_to_words(length);
|
||||
if (length_words > MAX_PREFERENCE_WORDS) {
|
||||
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
|
||||
return {};
|
||||
}
|
||||
|
||||
bool load(uint8_t *data, size_t len) override {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||
if (!ret)
|
||||
return false;
|
||||
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
||||
return false;
|
||||
memcpy(data, buffer, len);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const uint32_t total_words = length_words + 1; // +1 for CRC
|
||||
uint16_t offset;
|
||||
|
||||
class ESP8266Preferences : public ESPPreferences {
|
||||
public:
|
||||
uint32_t current_offset = 0;
|
||||
uint32_t current_flash_offset = 0; // in words
|
||||
|
||||
void setup() {
|
||||
ESP_LOGVV(TAG, "Loading preferences from flash");
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
|
||||
}
|
||||
}
|
||||
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
|
||||
const uint32_t length_words = bytes_to_words(length);
|
||||
if (length_words > MAX_PREFERENCE_WORDS) {
|
||||
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
|
||||
if (in_flash) {
|
||||
if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE)
|
||||
return {};
|
||||
offset = static_cast<uint16_t>(this->current_flash_offset);
|
||||
this->current_flash_offset += total_words;
|
||||
} else {
|
||||
uint32_t start = this->current_offset;
|
||||
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
|
||||
// Normal: offset 0-95 maps to RTC offset 32-127
|
||||
// Eboot: offset 96-127 maps to RTC offset 0-31
|
||||
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
|
||||
// start is in normal but end is not -> switch to Eboot
|
||||
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
|
||||
in_normal = false;
|
||||
}
|
||||
|
||||
const uint32_t total_words = length_words + 1; // +1 for CRC
|
||||
uint16_t offset;
|
||||
|
||||
if (in_flash) {
|
||||
if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE)
|
||||
return {};
|
||||
offset = static_cast<uint16_t>(this->current_flash_offset);
|
||||
this->current_flash_offset += total_words;
|
||||
} else {
|
||||
uint32_t start = this->current_offset;
|
||||
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
|
||||
// Normal: offset 0-95 maps to RTC offset 32-127
|
||||
// Eboot: offset 96-127 maps to RTC offset 0-31
|
||||
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
|
||||
// start is in normal but end is not -> switch to Eboot
|
||||
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
|
||||
in_normal = false;
|
||||
}
|
||||
if (start + total_words > PREF_TOTAL_WORDS)
|
||||
return {}; // Doesn't fit in RTC memory
|
||||
// Convert preference offset to RTC memory offset
|
||||
offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS);
|
||||
this->current_offset = start + total_words;
|
||||
}
|
||||
|
||||
auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->offset = offset;
|
||||
pref->type = type;
|
||||
pref->length_words = static_cast<uint8_t>(length_words);
|
||||
pref->in_flash = in_flash;
|
||||
return pref;
|
||||
if (start + total_words > PREF_TOTAL_WORDS)
|
||||
return {}; // Doesn't fit in RTC memory
|
||||
// Convert preference offset to RTC memory offset
|
||||
offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS);
|
||||
this->current_offset = start + total_words;
|
||||
}
|
||||
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
|
||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||
return make_preference(length, type, true);
|
||||
#else
|
||||
return make_preference(length, type, false);
|
||||
#endif
|
||||
}
|
||||
auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->offset = offset;
|
||||
pref->type = type;
|
||||
pref->length_words = static_cast<uint8_t>(length_words);
|
||||
pref->in_flash = in_flash;
|
||||
return ESPPreferenceObject(pref);
|
||||
}
|
||||
|
||||
bool sync() override {
|
||||
if (!s_flash_dirty)
|
||||
return true;
|
||||
if (s_prevent_write)
|
||||
return false;
|
||||
|
||||
ESP_LOGD(TAG, "Saving");
|
||||
SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK;
|
||||
{
|
||||
InterruptLock lock;
|
||||
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
|
||||
if (erase_res == SPI_FLASH_RESULT_OK) {
|
||||
write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
|
||||
}
|
||||
}
|
||||
if (erase_res != SPI_FLASH_RESULT_OK) {
|
||||
ESP_LOGE(TAG, "Erasing failed");
|
||||
return false;
|
||||
}
|
||||
if (write_res != SPI_FLASH_RESULT_OK) {
|
||||
ESP_LOGE(TAG, "Writing failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_flash_dirty = false;
|
||||
bool ESP8266Preferences::sync() {
|
||||
if (!s_flash_dirty)
|
||||
return true;
|
||||
if (s_prevent_write)
|
||||
return false;
|
||||
|
||||
ESP_LOGD(TAG, "Saving");
|
||||
SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK;
|
||||
{
|
||||
InterruptLock lock;
|
||||
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
|
||||
if (erase_res == SPI_FLASH_RESULT_OK) {
|
||||
write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
|
||||
}
|
||||
}
|
||||
if (erase_res != SPI_FLASH_RESULT_OK) {
|
||||
ESP_LOGE(TAG, "Erasing failed");
|
||||
return false;
|
||||
}
|
||||
if (write_res != SPI_FLASH_RESULT_OK) {
|
||||
ESP_LOGE(TAG, "Writing failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
ESP_LOGD(TAG, "Erasing storage");
|
||||
SpiFlashOpResult erase_res;
|
||||
{
|
||||
InterruptLock lock;
|
||||
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
|
||||
}
|
||||
if (erase_res != SPI_FLASH_RESULT_OK) {
|
||||
ESP_LOGE(TAG, "Erasing failed");
|
||||
return false;
|
||||
}
|
||||
s_flash_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Protect flash from writing till restart
|
||||
s_prevent_write = true;
|
||||
return true;
|
||||
bool ESP8266Preferences::reset() {
|
||||
ESP_LOGD(TAG, "Erasing storage");
|
||||
SpiFlashOpResult erase_res;
|
||||
{
|
||||
InterruptLock lock;
|
||||
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
|
||||
}
|
||||
};
|
||||
if (erase_res != SPI_FLASH_RESULT_OK) {
|
||||
ESP_LOGE(TAG, "Erasing failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Protect flash from writing till restart
|
||||
s_prevent_write = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
ESP8266Preferences *get_preferences() { return &s_preferences; }
|
||||
|
||||
void setup_preferences() {
|
||||
s_preferences.setup();
|
||||
global_preferences = &s_preferences;
|
||||
|
||||
@@ -1,12 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "esphome/core/preference_backend.h"
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
class ESP8266Preferences final : public PreferencesMixin<ESP8266Preferences> {
|
||||
public:
|
||||
using PreferencesMixin<ESP8266Preferences>::make_preference;
|
||||
void setup();
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash);
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type) {
|
||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||
return this->make_preference(length, type, true);
|
||||
#else
|
||||
return this->make_preference(length, type, false);
|
||||
#endif
|
||||
}
|
||||
bool sync();
|
||||
bool reset();
|
||||
|
||||
uint32_t current_offset = 0;
|
||||
uint32_t current_flash_offset = 0; // in words
|
||||
};
|
||||
|
||||
void setup_preferences();
|
||||
void preferences_prevent_write(bool prevent);
|
||||
|
||||
} // namespace esphome::esp8266
|
||||
|
||||
DECLARE_PREFERENCE_ALIASES(esphome::esp8266::ESP8266Preferences)
|
||||
|
||||
#endif // USE_ESP8266
|
||||
|
||||
@@ -332,12 +332,13 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
||||
ssize_t read = this->client_->read(buf, requested);
|
||||
if (read == -1) {
|
||||
if (this->would_block_(errno)) {
|
||||
const int err = errno;
|
||||
if (this->would_block_(err)) {
|
||||
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
|
||||
App.feed_wdt();
|
||||
continue;
|
||||
}
|
||||
ESP_LOGW(TAG, "Read err %d", errno);
|
||||
ESP_LOGW(TAG, "Read err %d", err);
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
} else if (read == 0) {
|
||||
ESP_LOGW(TAG, "Remote closed");
|
||||
@@ -426,8 +427,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
|
||||
|
||||
ssize_t read = this->client_->read(buf + at, len - at);
|
||||
if (read == -1) {
|
||||
if (!this->would_block_(errno)) {
|
||||
ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, errno);
|
||||
const int err = errno;
|
||||
if (!this->would_block_(err)) {
|
||||
ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, err);
|
||||
return false;
|
||||
}
|
||||
} else if (read == 0) {
|
||||
@@ -455,8 +457,9 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
|
||||
|
||||
ssize_t written = this->client_->write(buf + at, len - at);
|
||||
if (written == -1) {
|
||||
if (!this->would_block_(errno)) {
|
||||
ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno);
|
||||
const int err = errno;
|
||||
if (!this->would_block_(err)) {
|
||||
ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, err);
|
||||
return false;
|
||||
}
|
||||
// EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
@@ -49,6 +36,9 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_NATIVE_IDF,
|
||||
Platform,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
@@ -60,12 +50,14 @@ import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONFLICTS_WITH = ["wifi"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
AUTO_LOAD = ["network"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Key for tracking IP state listener count in CORE.data
|
||||
ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners"
|
||||
# Key for tracking configured ethernet type
|
||||
ETHERNET_TYPE_KEY = "ethernet_type"
|
||||
KEY_ETHERNET = "ethernet"
|
||||
|
||||
|
||||
def request_ethernet_ip_state_listener() -> None:
|
||||
@@ -139,10 +131,35 @@ _PHY_TYPE_TO_DEFINE = {
|
||||
"JL1101": "USE_ETHERNET_JL1101",
|
||||
"KSZ8081": "USE_ETHERNET_KSZ8081",
|
||||
"KSZ8081RNA": "USE_ETHERNET_KSZ8081",
|
||||
"W5500": "USE_ETHERNET_W5500",
|
||||
"DM9051": "USE_ETHERNET_DM9051",
|
||||
"LAN8670": "USE_ETHERNET_LAN8670",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IDFRegistryComponent:
|
||||
"""An ESP-IDF component from the Espressif Component Registry."""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
|
||||
|
||||
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry.
|
||||
_IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
|
||||
"LAN8720": IDFRegistryComponent("espressif/lan87xx", "1.0.0"),
|
||||
"RTL8201": IDFRegistryComponent("espressif/rtl8201", "1.0.1"),
|
||||
"DP83848": IDFRegistryComponent("espressif/dp83848", "1.0.0"),
|
||||
"IP101": IDFRegistryComponent("espressif/ip101", "1.0.0"),
|
||||
"KSZ8081": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
|
||||
"KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
|
||||
"W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"),
|
||||
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"),
|
||||
}
|
||||
|
||||
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
|
||||
# RP2040-supported SPI ethernet types
|
||||
RP2040_SPI_ETHERNET_TYPES = ["W5500"]
|
||||
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
|
||||
|
||||
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
|
||||
@@ -173,16 +190,22 @@ EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
|
||||
ManualIP = ethernet_ns.struct("ManualIP")
|
||||
|
||||
|
||||
def _is_framework_spi_polling_mode_supported():
|
||||
# SPI Ethernet without IRQ feature is added in
|
||||
# esp-idf >= (5.3+ ,5.2.1+, 5.1.4)
|
||||
# Note: Arduino now uses ESP-IDF as a component, so we only check IDF version
|
||||
framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
if framework_version >= cv.Version(5, 3, 0):
|
||||
def _is_framework_spi_polling_mode_supported() -> bool:
|
||||
"""Check if ESP-IDF framework supports SPI polling mode (ESP32 only).
|
||||
|
||||
SPI Ethernet without IRQ feature is added in
|
||||
esp-idf >= (5.3+, 5.2.1+, 5.1.4)
|
||||
"""
|
||||
if not CORE.is_esp32:
|
||||
return False
|
||||
from esphome.components.esp32 import idf_version
|
||||
|
||||
ver = idf_version()
|
||||
if ver >= cv.Version(5, 3, 0):
|
||||
return True
|
||||
if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1):
|
||||
if cv.Version(5, 3, 0) > ver >= cv.Version(5, 2, 1):
|
||||
return True
|
||||
if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103
|
||||
if cv.Version(5, 2, 0) > ver >= cv.Version(5, 1, 4): # noqa: SIM103
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -195,52 +218,68 @@ def _validate(config):
|
||||
use_address = CORE.name + config[CONF_DOMAIN]
|
||||
config[CONF_USE_ADDRESS] = use_address
|
||||
|
||||
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
|
||||
if _is_framework_spi_polling_mode_supported():
|
||||
if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
|
||||
raise cv.Invalid(
|
||||
f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
|
||||
)
|
||||
if CONF_POLLING_INTERVAL not in config and CONF_INTERRUPT_PIN not in config:
|
||||
config[CONF_POLLING_INTERVAL] = SPI_ETHERNET_DEFAULT_POLLING_INTERVAL
|
||||
else:
|
||||
if CONF_POLLING_INTERVAL in config:
|
||||
raise cv.Invalid(
|
||||
"In this version of the framework "
|
||||
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
|
||||
f"'{CONF_POLLING_INTERVAL}' is not supported."
|
||||
)
|
||||
if CONF_INTERRUPT_PIN not in config:
|
||||
raise cv.Invalid(
|
||||
"In this version of the framework "
|
||||
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] != "OPENETH":
|
||||
if CONF_CLK_MODE in config:
|
||||
mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
|
||||
LOGGER.warning(
|
||||
"[ethernet] The 'clk_mode' option is deprecated. "
|
||||
"Please replace 'clk_mode: %s' with:\n"
|
||||
" clk:\n"
|
||||
" mode: %s\n"
|
||||
" pin: %s\n"
|
||||
"Removal scheduled for 2026.7.0.",
|
||||
config[CONF_CLK_MODE],
|
||||
mode,
|
||||
pin,
|
||||
)
|
||||
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin})
|
||||
del config[CONF_CLK_MODE]
|
||||
elif CONF_CLK not in config:
|
||||
raise cv.Invalid("'clk' is a required option for [ethernet].")
|
||||
variant = get_esp32_variant()
|
||||
if variant not in (VARIANT_ESP32, VARIANT_ESP32P4):
|
||||
raise cv.Invalid(
|
||||
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
|
||||
f"on ESP32 classic and ESP32-P4, not {variant}"
|
||||
if CORE.is_esp32:
|
||||
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
|
||||
if _is_framework_spi_polling_mode_supported():
|
||||
if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
|
||||
raise cv.Invalid(
|
||||
f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
|
||||
)
|
||||
if (
|
||||
CONF_POLLING_INTERVAL not in config
|
||||
and CONF_INTERRUPT_PIN not in config
|
||||
):
|
||||
config[CONF_POLLING_INTERVAL] = (
|
||||
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL
|
||||
)
|
||||
else:
|
||||
if CONF_POLLING_INTERVAL in config:
|
||||
raise cv.Invalid(
|
||||
"In this version of the framework "
|
||||
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
|
||||
f"'{CONF_POLLING_INTERVAL}' is not supported."
|
||||
)
|
||||
if CONF_INTERRUPT_PIN not in config:
|
||||
raise cv.Invalid(
|
||||
"In this version of the framework "
|
||||
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] != "OPENETH":
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32P4,
|
||||
get_esp32_variant,
|
||||
)
|
||||
|
||||
if CONF_CLK_MODE in config:
|
||||
mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
|
||||
LOGGER.warning(
|
||||
"[ethernet] The 'clk_mode' option is deprecated. "
|
||||
"Please replace 'clk_mode: %s' with:\n"
|
||||
" clk:\n"
|
||||
" mode: %s\n"
|
||||
" pin: %s\n"
|
||||
"Removal scheduled for 2026.7.0.",
|
||||
config[CONF_CLK_MODE],
|
||||
mode,
|
||||
pin,
|
||||
)
|
||||
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin})
|
||||
del config[CONF_CLK_MODE]
|
||||
elif CONF_CLK not in config:
|
||||
raise cv.Invalid("'clk' is a required option for [ethernet].")
|
||||
variant = get_esp32_variant()
|
||||
if variant not in (VARIANT_ESP32, VARIANT_ESP32P4):
|
||||
raise cv.Invalid(
|
||||
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
|
||||
f"on ESP32 classic and ESP32-P4, not {variant}"
|
||||
)
|
||||
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES:
|
||||
raise cv.Invalid(
|
||||
f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, "
|
||||
f"not {config[CONF_TYPE]}"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -269,41 +308,50 @@ CLK_SCHEMA = cv.Schema(
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_pin_number,
|
||||
}
|
||||
)
|
||||
RMII_SCHEMA = 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_CLK_MODE): cv.enum(
|
||||
CLK_MODES_DEPRECATED, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_CLK): CLK_SCHEMA,
|
||||
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),
|
||||
}
|
||||
)
|
||||
RMII_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_CLK_MODE): cv.enum(
|
||||
CLK_MODES_DEPRECATED, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_CLK): CLK_SCHEMA,
|
||||
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 = BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All(
|
||||
cv.frequency, cv.int_range(int(8e6), int(80e6))
|
||||
),
|
||||
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
|
||||
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
|
||||
),
|
||||
}
|
||||
SPI_SCHEMA = cv.All(
|
||||
BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.SplitDefault(CONF_CLOCK_SPEED, esp32="26.67MHz"): cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.frequency,
|
||||
cv.int_range(int(8e6), int(80e6)),
|
||||
),
|
||||
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
|
||||
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
|
||||
),
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.only_on([Platform.ESP32, Platform.RP2040]),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -317,7 +365,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
"KSZ8081": RMII_SCHEMA,
|
||||
"KSZ8081RNA": RMII_SCHEMA,
|
||||
"W5500": SPI_SCHEMA,
|
||||
"OPENETH": BASE_SCHEMA,
|
||||
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
|
||||
"DM9051": SPI_SCHEMA,
|
||||
"LAN8670": RMII_SCHEMA,
|
||||
},
|
||||
@@ -328,8 +376,21 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def _final_validate_spi(config):
|
||||
if not CORE.is_esp32:
|
||||
return # SPI interface validation is ESP32-only
|
||||
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
|
||||
return
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||
|
||||
if spi_configs := fv.full_config.get().get(CONF_SPI):
|
||||
variant = get_esp32_variant()
|
||||
if variant in (
|
||||
@@ -378,6 +439,51 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if CORE.is_esp32:
|
||||
await _to_code_esp32(var, config)
|
||||
elif CORE.is_rp2040:
|
||||
await _to_code_rp2040(var, config)
|
||||
|
||||
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
|
||||
|
||||
if CONF_MANUAL_IP in config:
|
||||
cg.add_define("USE_ETHERNET_MANUAL_IP")
|
||||
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||
|
||||
# Add compile-time define for PHY types with specific code
|
||||
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
|
||||
cg.add_define(phy_define)
|
||||
|
||||
if mac_address := config.get(CONF_MAC_ADDRESS):
|
||||
cg.add(var.set_fixed_mac(mac_address.parts))
|
||||
|
||||
cg.add_define("USE_ETHERNET")
|
||||
|
||||
if on_connect_config := config.get(CONF_ON_CONNECT):
|
||||
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_connect_trigger(), [], on_connect_config
|
||||
)
|
||||
|
||||
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
|
||||
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_disconnect_trigger(), [], on_disconnect_config
|
||||
)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
|
||||
|
||||
async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
idf_version,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
|
||||
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
|
||||
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
|
||||
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
|
||||
@@ -395,7 +501,11 @@ async def to_code(config):
|
||||
|
||||
cg.add_define("USE_ETHERNET_SPI")
|
||||
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
|
||||
add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True)
|
||||
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
|
||||
if idf_version() < cv.Version(6, 0, 0):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True
|
||||
)
|
||||
elif config[CONF_TYPE] == "OPENETH":
|
||||
cg.add_define("USE_ETHERNET_OPENETH")
|
||||
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
|
||||
@@ -415,22 +525,6 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.add_phy_register(reg))
|
||||
|
||||
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
|
||||
if CONF_MANUAL_IP in config:
|
||||
cg.add_define("USE_ETHERNET_MANUAL_IP")
|
||||
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||
|
||||
# Add compile-time define for PHY types with specific code
|
||||
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
|
||||
cg.add_define(phy_define)
|
||||
|
||||
if mac_address := config.get(CONF_MAC_ADDRESS):
|
||||
cg.add(var.set_fixed_mac(mac_address.parts))
|
||||
|
||||
cg.add_define("USE_ETHERNET")
|
||||
|
||||
# Disable WiFi when using Ethernet to save memory
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
|
||||
# Also disable WiFi/BT coexistence since WiFi is disabled
|
||||
@@ -443,27 +537,41 @@ async def to_code(config):
|
||||
# Add LAN867x 10BASE-T1S PHY support component
|
||||
add_idf_component(name="espressif/lan867x", ref="2.0.0")
|
||||
|
||||
if on_connect_config := config.get(CONF_ON_CONNECT):
|
||||
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_connect_trigger(), [], on_connect_config
|
||||
)
|
||||
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
|
||||
if idf_version() >= cv.Version(6, 0, 0) and (
|
||||
component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE])
|
||||
):
|
||||
add_idf_component(name=component.name, ref=component.version)
|
||||
|
||||
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
|
||||
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_disconnect_trigger(), [], on_disconnect_config
|
||||
)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
|
||||
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
|
||||
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
|
||||
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
|
||||
if CONF_INTERRUPT_PIN in config:
|
||||
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
|
||||
if CONF_RESET_PIN in config:
|
||||
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
|
||||
|
||||
cg.add_define("USE_ETHERNET_SPI")
|
||||
cg.add_library("lwIP_w5500", None)
|
||||
|
||||
|
||||
def _final_validate_rmii_pins(config: ConfigType) -> None:
|
||||
"""Validate that RMII pins are not used by other components."""
|
||||
if not CORE.is_esp32:
|
||||
return # RMII validation is ESP32-only
|
||||
# Only validate for RMII-based PHYs on ESP32/ESP32P4
|
||||
if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH":
|
||||
return # SPI and OPENETH don't use RMII
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32P4,
|
||||
get_esp32_variant,
|
||||
)
|
||||
|
||||
variant = get_esp32_variant()
|
||||
if variant == VARIANT_ESP32:
|
||||
rmii_pins = ESP32_RMII_FIXED_PINS
|
||||
@@ -521,3 +629,39 @@ async def final_step():
|
||||
if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0):
|
||||
cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS")
|
||||
cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count)
|
||||
|
||||
|
||||
_platform_filter = filter_source_files_from_platform(
|
||||
{
|
||||
"ethernet_component_esp32.cpp": {
|
||||
PlatformFramework.ESP32_IDF,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
},
|
||||
"ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
|
||||
"esp_eth_phy_jl1101.c": {
|
||||
PlatformFramework.ESP32_IDF,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _filter_source_files() -> list[str]:
|
||||
excluded = _platform_filter()
|
||||
eth_data = CORE.data.get(KEY_ETHERNET, {})
|
||||
eth_type = eth_data.get(ETHERNET_TYPE_KEY)
|
||||
# Only compile the custom JL1101 driver when JL1101 is configured
|
||||
# and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x)
|
||||
if eth_type != "JL1101":
|
||||
excluded.append("esp_eth_phy_jl1101.c")
|
||||
elif CORE.is_esp32 and not CORE.data.get(KEY_NATIVE_IDF, False):
|
||||
from esphome.components.esp32 import idf_version
|
||||
|
||||
# pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver
|
||||
# to avoid shadowing. Native IDF builds always need the custom driver.
|
||||
if cv.Version(5, 4, 2) <= idf_version() < cv.Version(6, 0, 0):
|
||||
excluded.append("esp_eth_phy_jl1101.c")
|
||||
return excluded
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = _filter_source_files
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
#include "esp_rom_sys.h"
|
||||
#include "esp_idf_version.h"
|
||||
|
||||
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
|
||||
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \
|
||||
ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
|
||||
|
||||
static const char *TAG = "jl1101";
|
||||
#define PHY_CHECK(a, str, goto_tag, ...) \
|
||||
|
||||
@@ -1,547 +1,28 @@
|
||||
#include "ethernet_component.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ETHERNET
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <lwip/dns.h>
|
||||
#include <cinttypes>
|
||||
#include "esp_event.h"
|
||||
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
#include "esp_eth_phy_lan867x.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_master.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
static const char *const TAG = "ethernet";
|
||||
|
||||
// PHY register size for hex logging
|
||||
static constexpr size_t PHY_REG_SIZE = 2;
|
||||
|
||||
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
|
||||
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
#define ESPHL_ERROR_CHECK(err, message) \
|
||||
if ((err) != ESP_OK) { \
|
||||
this->log_error_and_mark_failed_(err, message); \
|
||||
return; \
|
||||
}
|
||||
|
||||
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
|
||||
if ((err) != ESP_OK) { \
|
||||
this->log_error_and_mark_failed_(err, message); \
|
||||
return ret; \
|
||||
}
|
||||
|
||||
EthernetComponent::EthernetComponent() { global_eth_component = this; }
|
||||
|
||||
void EthernetComponent::setup() {
|
||||
if (esp_reset_reason() != ESP_RST_DEEPSLEEP) {
|
||||
// Delay here to allow power to stabilise before Ethernet is initialized.
|
||||
delay(300); // NOLINT
|
||||
}
|
||||
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
|
||||
gpio_install_isr_service(0);
|
||||
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = this->mosi_pin_,
|
||||
.miso_io_num = this->miso_pin_,
|
||||
.sclk_io_num = this->clk_pin_,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.data4_io_num = -1,
|
||||
.data5_io_num = -1,
|
||||
.data6_io_num = -1,
|
||||
.data7_io_num = -1,
|
||||
.max_transfer_sz = 0,
|
||||
.flags = 0,
|
||||
.intr_flags = 0,
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
auto host = SPI2_HOST;
|
||||
#else
|
||||
auto host = SPI3_HOST;
|
||||
#endif
|
||||
|
||||
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
||||
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
||||
#endif
|
||||
|
||||
err = esp_netif_init();
|
||||
ESPHL_ERROR_CHECK(err, "ETH netif init error");
|
||||
err = esp_event_loop_create_default();
|
||||
ESPHL_ERROR_CHECK(err, "ETH event loop error");
|
||||
|
||||
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
|
||||
this->eth_netif_ = esp_netif_new(&cfg);
|
||||
|
||||
// Init MAC and PHY configs to default
|
||||
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
|
||||
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
|
||||
|
||||
#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.command_bits = 0,
|
||||
.address_bits = 0,
|
||||
.dummy_bits = 0,
|
||||
.mode = 0,
|
||||
.duty_cycle_pos = 0,
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.clock_speed_hz = this->clock_speed_,
|
||||
.input_delay_ns = 0,
|
||||
.spics_io_num = this->cs_pin_,
|
||||
.flags = 0,
|
||||
.queue_size = 20,
|
||||
.pre_cb = nullptr,
|
||||
.post_cb = nullptr,
|
||||
};
|
||||
|
||||
#if CONFIG_ETH_SPI_ETHERNET_W5500
|
||||
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_DM9051
|
||||
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
|
||||
#endif
|
||||
|
||||
#if CONFIG_ETH_SPI_ETHERNET_W5500
|
||||
w5500_config.int_gpio_num = this->interrupt_pin_;
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
w5500_config.poll_period_ms = this->polling_interval_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if CONFIG_ETH_SPI_ETHERNET_DM9051
|
||||
dm9051_config.int_gpio_num = this->interrupt_pin_;
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
dm9051_config.poll_period_ms = this->polling_interval_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
phy_config.phy_addr = this->phy_addr_spi_;
|
||||
phy_config.reset_gpio_num = this->reset_pin_;
|
||||
|
||||
esp_eth_mac_t *mac = nullptr;
|
||||
#elif defined(USE_ETHERNET_OPENETH)
|
||||
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
|
||||
#else
|
||||
phy_config.phy_addr = this->phy_addr_;
|
||||
phy_config.reset_gpio_num = this->power_pin_;
|
||||
|
||||
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
|
||||
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
|
||||
#else
|
||||
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 = (emac_rmii_clock_gpio_t) this->clk_pin_;
|
||||
|
||||
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
|
||||
#endif
|
||||
|
||||
switch (this->type_) {
|
||||
#ifdef USE_ETHERNET_OPENETH
|
||||
case ETHERNET_TYPE_OPENETH: {
|
||||
phy_config.autonego_timeout_ms = 1000;
|
||||
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ETH_USE_ESP32_EMAC
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720: {
|
||||
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
case ETHERNET_TYPE_RTL8201: {
|
||||
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
case ETHERNET_TYPE_DP83848: {
|
||||
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
case ETHERNET_TYPE_IP101: {
|
||||
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
|
||||
case ETHERNET_TYPE_JL1101: {
|
||||
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
case ETHERNET_TYPE_KSZ8081RNA: {
|
||||
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670: {
|
||||
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#if CONFIG_ETH_SPI_ETHERNET_W5500
|
||||
case ETHERNET_TYPE_W5500: {
|
||||
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
|
||||
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_DM9051
|
||||
case ETHERNET_TYPE_DM9051: {
|
||||
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
|
||||
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
default: {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_);
|
||||
this->eth_handle_ = nullptr;
|
||||
err = esp_eth_driver_install(ð_config, &this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH driver install error");
|
||||
|
||||
#ifndef USE_ETHERNET_SPI
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
|
||||
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
|
||||
this->ksz8081_set_clock_reference_(mac);
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
for (const auto &phy_register : this->phy_registers_) {
|
||||
this->write_phy_register_(mac, phy_register);
|
||||
}
|
||||
#endif
|
||||
|
||||
// use ESP internal eth mac
|
||||
uint8_t mac_addr[6];
|
||||
if (this->fixed_mac_.has_value()) {
|
||||
memcpy(mac_addr, this->fixed_mac_->data(), 6);
|
||||
} else {
|
||||
esp_read_mac(mac_addr, ESP_MAC_ETH);
|
||||
}
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
|
||||
ESPHL_ERROR_CHECK(err, "set mac address error");
|
||||
|
||||
/* attach Ethernet driver to TCP/IP stack */
|
||||
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
|
||||
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
|
||||
|
||||
// Register user defined event handers
|
||||
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
|
||||
ESPHL_ERROR_CHECK(err, "ETH event handler register error");
|
||||
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr);
|
||||
ESPHL_ERROR_CHECK(err, "GOT IP event handler register error");
|
||||
#if USE_NETWORK_IPV6
|
||||
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr);
|
||||
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
/* start Ethernet driver state machine */
|
||||
err = esp_eth_start(this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||
}
|
||||
|
||||
void EthernetComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
switch (this->state_) {
|
||||
case EthernetComponentState::STOPPED:
|
||||
if (this->started_) {
|
||||
ESP_LOGI(TAG, "Starting connection");
|
||||
this->state_ = EthernetComponentState::CONNECTING;
|
||||
this->start_connect_();
|
||||
}
|
||||
break;
|
||||
case EthernetComponentState::CONNECTING:
|
||||
if (!this->started_) {
|
||||
ESP_LOGI(TAG, "Stopped connection");
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
} else if (this->connected_) {
|
||||
// connection established
|
||||
ESP_LOGI(TAG, "Connected");
|
||||
this->state_ = EthernetComponentState::CONNECTED;
|
||||
|
||||
this->dump_connect_params_();
|
||||
this->status_clear_warning();
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
this->connect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (now - this->connect_begin_ > 15000) {
|
||||
ESP_LOGW(TAG, "Connecting failed; reconnecting");
|
||||
this->start_connect_();
|
||||
}
|
||||
break;
|
||||
case EthernetComponentState::CONNECTED:
|
||||
if (!this->started_) {
|
||||
ESP_LOGI(TAG, "Stopped connection");
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (!this->connected_) {
|
||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||
this->state_ = EthernetComponentState::CONNECTING;
|
||||
this->start_connect_();
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else {
|
||||
this->finish_connect_();
|
||||
// When connected and stable, disable the loop to save CPU cycles
|
||||
this->disable_loop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_config() {
|
||||
const char *eth_type;
|
||||
switch (this->type_) {
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720:
|
||||
eth_type = "LAN8720";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
case ETHERNET_TYPE_RTL8201:
|
||||
eth_type = "RTL8201";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
case ETHERNET_TYPE_DP83848:
|
||||
eth_type = "DP83848";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
case ETHERNET_TYPE_IP101:
|
||||
eth_type = "IP101";
|
||||
break;
|
||||
#endif
|
||||
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
|
||||
case ETHERNET_TYPE_JL1101:
|
||||
eth_type = "JL1101";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
eth_type = "KSZ8081";
|
||||
break;
|
||||
|
||||
case ETHERNET_TYPE_KSZ8081RNA:
|
||||
eth_type = "KSZ8081RNA";
|
||||
break;
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_W5500
|
||||
case ETHERNET_TYPE_W5500:
|
||||
eth_type = "W5500";
|
||||
break;
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_DM9051
|
||||
case ETHERNET_TYPE_DM9051:
|
||||
eth_type = "DM9051";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_OPENETH
|
||||
case ETHERNET_TYPE_OPENETH:
|
||||
eth_type = "OPENETH";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670:
|
||||
eth_type = "LAN8670";
|
||||
break;
|
||||
#endif
|
||||
|
||||
default:
|
||||
eth_type = "Unknown";
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Ethernet:\n"
|
||||
" Connected: %s",
|
||||
YESNO(this->is_connected()));
|
||||
this->dump_connect_params_();
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CLK Pin: %u\n"
|
||||
" MISO Pin: %u\n"
|
||||
" MOSI Pin: %u\n"
|
||||
" CS Pin: %u",
|
||||
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
if (this->polling_interval_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Reset Pin: %d\n"
|
||||
" Clock Speed: %d MHz",
|
||||
this->reset_pin_, this->clock_speed_ / 1000000);
|
||||
#else
|
||||
if (this->power_pin_ != -1) {
|
||||
ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CLK Pin: %u\n"
|
||||
" MDC Pin: %u\n"
|
||||
" MDIO Pin: %u\n"
|
||||
" PHY addr: %u",
|
||||
this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_);
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
|
||||
}
|
||||
|
||||
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
|
||||
// TODO: do something smarter
|
||||
// return false;
|
||||
} else {
|
||||
addresses[0] = network::IPAddress(&ip.ip);
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
uint8_t count = 0;
|
||||
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
|
||||
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
|
||||
assert(count < addresses.size());
|
||||
for (int i = 0; i < count; i++) {
|
||||
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
||||
LwIPLock lock;
|
||||
const ip_addr_t *dns_ip = dns_getserver(num);
|
||||
return dns_ip;
|
||||
}
|
||||
|
||||
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) {
|
||||
const char *event_name;
|
||||
|
||||
switch (event) {
|
||||
case ETHERNET_EVENT_START:
|
||||
event_name = "ETH started";
|
||||
global_eth_component->started_ = true;
|
||||
global_eth_component->enable_loop_soon_any_context();
|
||||
break;
|
||||
case ETHERNET_EVENT_STOP:
|
||||
event_name = "ETH stopped";
|
||||
global_eth_component->started_ = false;
|
||||
global_eth_component->connected_ = false;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
break;
|
||||
case ETHERNET_EVENT_CONNECTED:
|
||||
event_name = "ETH connected";
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
|
||||
if (global_eth_component->manual_ip_.has_value()) {
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
}
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
break;
|
||||
case ETHERNET_EVENT_DISCONNECTED:
|
||||
event_name = "ETH disconnected";
|
||||
global_eth_component->connected_ = false;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
|
||||
}
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const char *EthernetComponent::get_use_address() const { return this->use_address_; }
|
||||
|
||||
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
|
||||
void *event_data) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
|
||||
const esp_netif_ip_info_t *ip_info = &event->ip_info;
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
|
||||
global_eth_component->got_ipv4_address_ = true;
|
||||
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#else
|
||||
global_eth_component->connected_ = true;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
|
||||
void *event_data) {
|
||||
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
|
||||
global_eth_component->ipv6_count_ += 1;
|
||||
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ =
|
||||
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#else
|
||||
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void EthernetComponent::notify_ip_state_listeners_() {
|
||||
@@ -554,315 +35,6 @@ void EthernetComponent::notify_ip_state_listeners_() {
|
||||
}
|
||||
#endif // USE_ETHERNET_IP_STATE_LISTENERS
|
||||
|
||||
void EthernetComponent::finish_connect_() {
|
||||
#if USE_NETWORK_IPV6
|
||||
// Retry IPv6 link-local setup if it failed during initial connect
|
||||
// This handles the case where min_ipv6_addr_count is NOT set (or is 0),
|
||||
// allowing us to reach CONNECTED state with just IPv4.
|
||||
// If IPv6 setup failed in start_connect_() because the interface wasn't ready:
|
||||
// - Bootup timing issues (#10281)
|
||||
// - Cable unplugged/network interruption (#10705)
|
||||
// We can now retry since we're in CONNECTED state and the interface is definitely up.
|
||||
if (!this->ipv6_setup_done_) {
|
||||
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
|
||||
}
|
||||
// Always set the flag to prevent continuous retries
|
||||
// If IPv6 setup fails here with the interface up and stable, it's
|
||||
// likely a persistent issue (IPv6 disabled at router, hardware
|
||||
// limitation, etc.) that won't be resolved by further retries.
|
||||
// The device continues to work with IPv4.
|
||||
this->ipv6_setup_done_ = true;
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
void EthernetComponent::start_connect_() {
|
||||
global_eth_component->got_ipv4_address_ = false;
|
||||
#if USE_NETWORK_IPV6
|
||||
global_eth_component->ipv6_count_ = 0;
|
||||
this->ipv6_setup_done_ = false;
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
this->connect_begin_ = millis();
|
||||
this->status_set_warning(LOG_STR("waiting for IP configuration"));
|
||||
|
||||
esp_err_t err;
|
||||
err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str());
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t info;
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
info.ip = this->manual_ip_->static_ip;
|
||||
info.gw = this->manual_ip_->gateway;
|
||||
info.netmask = this->manual_ip_->subnet;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
info.ip.addr = 0;
|
||||
info.gw.addr = 0;
|
||||
info.netmask.addr = 0;
|
||||
}
|
||||
|
||||
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
|
||||
|
||||
err = esp_netif_dhcpc_get_status(this->eth_netif_, &status);
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!");
|
||||
|
||||
ESP_LOGV(TAG, "DHCP Client Status: %d", status);
|
||||
|
||||
err = esp_netif_dhcpc_stop(this->eth_netif_);
|
||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC stop error");
|
||||
}
|
||||
|
||||
err = esp_netif_set_ip_info(this->eth_netif_, &info);
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns1;
|
||||
dns_setserver(0, &d);
|
||||
}
|
||||
if (this->manual_ip_->dns2.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns2;
|
||||
dns_setserver(1, &d);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
err = esp_netif_dhcpc_start(this->eth_netif_);
|
||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC start error");
|
||||
}
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
// Attempt to create IPv6 link-local address
|
||||
// We MUST attempt this here, not just in finish_connect_(), because with
|
||||
// min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
|
||||
// However, this may fail with ESP_FAIL if the interface is not up yet:
|
||||
// - At bootup when link isn't ready (#10281)
|
||||
// - After disconnection/cable unplugged (#10705)
|
||||
// We'll retry in finish_connect_() if it fails here.
|
||||
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
|
||||
// This is a programming error, not a transient failure
|
||||
ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
|
||||
} else {
|
||||
// ESP_FAIL means the interface isn't up yet
|
||||
// This is expected and non-fatal, happens in multiple scenarios:
|
||||
// - During reconnection after network interruptions (#10705)
|
||||
// - At bootup when the link isn't ready yet (#10281)
|
||||
// We'll retry once we reach CONNECTED state and the interface is up
|
||||
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
|
||||
// Don't mark component as failed - this is a transient error
|
||||
}
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
this->connect_begin_ = millis();
|
||||
this->status_set_warning();
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_connect_params_() {
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
const ip_addr_t *dns_ip1;
|
||||
const ip_addr_t *dns_ip2;
|
||||
{
|
||||
LwIPLock lock;
|
||||
dns_ip1 = dns_getserver(0);
|
||||
dns_ip2 = dns_getserver(1);
|
||||
}
|
||||
|
||||
// Use stack buffers for IP address formatting to avoid heap allocations
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
" Hostname: '%s'\n"
|
||||
" Subnet: %s\n"
|
||||
" Gateway: %s\n"
|
||||
" DNS1: %s\n"
|
||||
" DNS2: %s\n"
|
||||
" MAC Address: %s\n"
|
||||
" Is Full Duplex: %s\n"
|
||||
" Link Speed: %u",
|
||||
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
|
||||
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);
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
uint8_t count = 0;
|
||||
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
|
||||
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
|
||||
for (int i = 0; i < count; i++) {
|
||||
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const char *EthernetComponent::get_use_address() const { return this->use_address_; }
|
||||
|
||||
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
|
||||
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
esp_err_t err;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
|
||||
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
||||
}
|
||||
|
||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[6];
|
||||
get_eth_mac_address_raw(mac);
|
||||
format_mac_addr_upper(mac, buf.data());
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||
esp_err_t err;
|
||||
eth_duplex_t duplex_mode;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
|
||||
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF);
|
||||
return duplex_mode;
|
||||
}
|
||||
|
||||
eth_speed_t EthernetComponent::get_link_speed() {
|
||||
esp_err_t err;
|
||||
eth_speed_t speed;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
|
||||
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M);
|
||||
return speed;
|
||||
}
|
||||
|
||||
bool EthernetComponent::powerdown() {
|
||||
ESP_LOGI(TAG, "Powering down ethernet PHY");
|
||||
if (this->phy_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Ethernet PHY not assigned");
|
||||
return false;
|
||||
}
|
||||
this->connected_ = false;
|
||||
this->started_ = false;
|
||||
// No need to enable_loop() here as this is only called during shutdown/reboot
|
||||
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error powering down ethernet PHY");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifndef USE_ETHERNET_SPI
|
||||
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F;
|
||||
|
||||
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
esp_err_t err;
|
||||
|
||||
uint32_t phy_control_2;
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
* KSZ8081RNA:
|
||||
* 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode.
|
||||
* 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
|
||||
* KSZ8081RND:
|
||||
* 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
|
||||
* 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode.
|
||||
*/
|
||||
if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
|
||||
phy_control_2 |= 1 << 7;
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
|
||||
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
}
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value);
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
|
||||
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ETHERNET
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ETHERNET
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "esp_eth.h"
|
||||
#include "esp_eth_mac.h"
|
||||
#include "esp_eth_mac_esp.h"
|
||||
@@ -19,6 +20,11 @@
|
||||
#if CONFIG_ETH_USE_ESP32_EMAC
|
||||
extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2040
|
||||
#include <W5500lwIP.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
@@ -73,7 +79,13 @@ enum class EthernetComponentState : uint8_t {
|
||||
CONNECTED,
|
||||
};
|
||||
|
||||
class EthernetComponent : public Component {
|
||||
// Platform-neutral duplex/speed types
|
||||
#ifndef USE_ESP32
|
||||
enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL };
|
||||
enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M };
|
||||
#endif
|
||||
|
||||
class EthernetComponent final : public Component {
|
||||
public:
|
||||
EthernetComponent();
|
||||
void setup() override;
|
||||
@@ -83,6 +95,28 @@ class EthernetComponent : public Component {
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
||||
|
||||
void set_type(EthernetType type);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
network::IPAddress get_dns_address(uint8_t num);
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string get_eth_mac_address_pretty();
|
||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
eth_duplex_t get_duplex_mode();
|
||||
eth_speed_t get_link_speed();
|
||||
bool powerdown();
|
||||
|
||||
#ifdef USE_ESP32
|
||||
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
@@ -102,26 +136,17 @@ class EthernetComponent : public Component {
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_clk_mode(emac_rmii_clock_mode_t clk_mode);
|
||||
void add_phy_register(PHYRegister register_value);
|
||||
#endif
|
||||
void set_type(EthernetType type);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
|
||||
#endif // USE_ETHERNET_SPI
|
||||
#endif // USE_ESP32
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
network::IPAddress get_dns_address(uint8_t num);
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string get_eth_mac_address_pretty();
|
||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
eth_duplex_t get_duplex_mode();
|
||||
eth_speed_t get_link_speed();
|
||||
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
|
||||
bool powerdown();
|
||||
#ifdef USE_RP2040
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
void set_mosi_pin(uint8_t mosi_pin);
|
||||
void set_cs_pin(uint8_t cs_pin);
|
||||
void set_interrupt_pin(int8_t interrupt_pin);
|
||||
void set_reset_pin(int8_t reset_pin);
|
||||
#endif // USE_RP2040
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
|
||||
@@ -133,19 +158,22 @@ class EthernetComponent : public Component {
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void start_connect_();
|
||||
void finish_connect_();
|
||||
void dump_connect_params_();
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void notify_ip_state_listeners_();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
|
||||
static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
|
||||
#if LWIP_IPV6
|
||||
static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
|
||||
#endif /* LWIP_IPV6 */
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void notify_ip_state_listeners_();
|
||||
#endif
|
||||
|
||||
void start_connect_();
|
||||
void finish_connect_();
|
||||
void dump_connect_params_();
|
||||
void log_error_and_mark_failed_(esp_err_t err, const char *message);
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
|
||||
@@ -177,7 +205,27 @@ class EthernetComponent : public Component {
|
||||
uint8_t phy_addr_{0};
|
||||
uint8_t mdc_pin_{23};
|
||||
uint8_t mdio_pin_{18};
|
||||
#endif
|
||||
#endif // USE_ETHERNET_SPI
|
||||
|
||||
// ESP32 pointers
|
||||
esp_netif_t *eth_netif_{nullptr};
|
||||
esp_eth_handle_t eth_handle_;
|
||||
esp_eth_phy_t *phy_{nullptr};
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2040
|
||||
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
|
||||
Wiznet5500lwIP *eth_{nullptr};
|
||||
uint32_t last_link_check_{0};
|
||||
uint8_t clk_pin_;
|
||||
uint8_t miso_pin_;
|
||||
uint8_t mosi_pin_;
|
||||
uint8_t cs_pin_;
|
||||
int8_t interrupt_pin_{-1};
|
||||
int8_t reset_pin_{-1};
|
||||
#endif // USE_RP2040
|
||||
|
||||
// Common members
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
optional<ManualIP> manual_ip_{};
|
||||
#endif
|
||||
@@ -194,10 +242,6 @@ class EthernetComponent : public Component {
|
||||
bool ipv6_setup_done_{false};
|
||||
#endif /* LWIP_IPV6 */
|
||||
|
||||
// Pointers at the end (naturally aligned)
|
||||
esp_netif_t *eth_netif_{nullptr};
|
||||
esp_eth_handle_t eth_handle_;
|
||||
esp_eth_phy_t *phy_{nullptr};
|
||||
optional<std::array<uint8_t, 6>> fixed_mac_;
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
@@ -219,10 +263,13 @@ class EthernetComponent : public Component {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern EthernetComponent *global_eth_component;
|
||||
|
||||
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \
|
||||
ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
|
||||
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ETHERNET
|
||||
|
||||
@@ -0,0 +1,878 @@
|
||||
#include "ethernet_component.h"
|
||||
|
||||
#if defined(USE_ETHERNET) && defined(USE_ESP32)
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <lwip/dns.h>
|
||||
#include <cinttypes>
|
||||
#include "esp_event.h"
|
||||
|
||||
// IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry;
|
||||
// they are no longer included via esp_eth.h and need explicit includes.
|
||||
// On IDF 5.x these headers don't exist as standalone files.
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
#include "esp_eth_phy_lan87xx.h"
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
#include "esp_eth_phy_rtl8201.h"
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
#include "esp_eth_phy_dp83848.h"
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
#include "esp_eth_phy_ip101.h"
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
#include "esp_eth_phy_ksz80xx.h"
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_W5500
|
||||
#include "esp_eth_mac_w5500.h"
|
||||
#include "esp_eth_phy_w5500.h"
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DM9051
|
||||
#include "esp_eth_mac_dm9051.h"
|
||||
#include "esp_eth_phy_dm9051.h"
|
||||
#endif
|
||||
#endif // ESP_IDF_VERSION >= 6.0.0
|
||||
|
||||
// LAN867x header exists on all IDF versions (external component since IDF 5.3)
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
#include "esp_eth_phy_lan867x.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_master.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
static const char *const TAG = "ethernet";
|
||||
|
||||
// PHY register size for hex logging
|
||||
static constexpr size_t PHY_REG_SIZE = 2;
|
||||
|
||||
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
|
||||
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
#define ESPHL_ERROR_CHECK(err, message) \
|
||||
if ((err) != ESP_OK) { \
|
||||
this->log_error_and_mark_failed_(err, message); \
|
||||
return; \
|
||||
}
|
||||
|
||||
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
|
||||
if ((err) != ESP_OK) { \
|
||||
this->log_error_and_mark_failed_(err, message); \
|
||||
return ret; \
|
||||
}
|
||||
|
||||
void EthernetComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
switch (this->state_) {
|
||||
case EthernetComponentState::STOPPED:
|
||||
if (this->started_) {
|
||||
ESP_LOGI(TAG, "Starting connection");
|
||||
this->state_ = EthernetComponentState::CONNECTING;
|
||||
this->start_connect_();
|
||||
}
|
||||
break;
|
||||
case EthernetComponentState::CONNECTING:
|
||||
if (!this->started_) {
|
||||
ESP_LOGI(TAG, "Stopped connection");
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
} else if (this->connected_) {
|
||||
// connection established
|
||||
ESP_LOGI(TAG, "Connected");
|
||||
this->state_ = EthernetComponentState::CONNECTED;
|
||||
|
||||
this->dump_connect_params_();
|
||||
this->status_clear_warning();
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
this->connect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (now - this->connect_begin_ > 15000) {
|
||||
ESP_LOGW(TAG, "Connecting failed; reconnecting");
|
||||
this->start_connect_();
|
||||
}
|
||||
break;
|
||||
case EthernetComponentState::CONNECTED:
|
||||
if (!this->started_) {
|
||||
ESP_LOGI(TAG, "Stopped connection");
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (!this->connected_) {
|
||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||
this->state_ = EthernetComponentState::CONNECTING;
|
||||
this->start_connect_();
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else {
|
||||
this->finish_connect_();
|
||||
// When connected and stable, disable the loop to save CPU cycles
|
||||
this->disable_loop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EthernetComponent::setup() {
|
||||
if (esp_reset_reason() != ESP_RST_DEEPSLEEP) {
|
||||
// Delay here to allow power to stabilise before Ethernet is initialized.
|
||||
delay(300); // NOLINT
|
||||
}
|
||||
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
|
||||
gpio_install_isr_service(0);
|
||||
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = this->mosi_pin_,
|
||||
.miso_io_num = this->miso_pin_,
|
||||
.sclk_io_num = this->clk_pin_,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.data4_io_num = -1,
|
||||
.data5_io_num = -1,
|
||||
.data6_io_num = -1,
|
||||
.data7_io_num = -1,
|
||||
.max_transfer_sz = 0,
|
||||
.flags = 0,
|
||||
.intr_flags = 0,
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
auto host = SPI2_HOST;
|
||||
#else
|
||||
auto host = SPI3_HOST;
|
||||
#endif
|
||||
|
||||
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
||||
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
||||
#endif
|
||||
|
||||
err = esp_netif_init();
|
||||
ESPHL_ERROR_CHECK(err, "ETH netif init error");
|
||||
err = esp_event_loop_create_default();
|
||||
ESPHL_ERROR_CHECK(err, "ETH event loop error");
|
||||
|
||||
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
|
||||
this->eth_netif_ = esp_netif_new(&cfg);
|
||||
|
||||
// Init MAC and PHY configs to default
|
||||
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
|
||||
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
|
||||
|
||||
#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.command_bits = 0,
|
||||
.address_bits = 0,
|
||||
.dummy_bits = 0,
|
||||
.mode = 0,
|
||||
.duty_cycle_pos = 0,
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.clock_speed_hz = this->clock_speed_,
|
||||
.input_delay_ns = 0,
|
||||
.spics_io_num = this->cs_pin_,
|
||||
.flags = 0,
|
||||
.queue_size = 20,
|
||||
.pre_cb = nullptr,
|
||||
.post_cb = nullptr,
|
||||
};
|
||||
|
||||
#ifdef USE_ETHERNET_W5500
|
||||
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DM9051
|
||||
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_W5500
|
||||
w5500_config.int_gpio_num = this->interrupt_pin_;
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
w5500_config.poll_period_ms = this->polling_interval_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_DM9051
|
||||
dm9051_config.int_gpio_num = this->interrupt_pin_;
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
dm9051_config.poll_period_ms = this->polling_interval_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
phy_config.phy_addr = this->phy_addr_spi_;
|
||||
phy_config.reset_gpio_num = this->reset_pin_;
|
||||
|
||||
esp_eth_mac_t *mac = nullptr;
|
||||
#elif defined(USE_ETHERNET_OPENETH)
|
||||
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
|
||||
#else
|
||||
phy_config.phy_addr = this->phy_addr_;
|
||||
phy_config.reset_gpio_num = this->power_pin_;
|
||||
|
||||
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
|
||||
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
|
||||
#else
|
||||
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_);
|
||||
|
||||
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
|
||||
#endif
|
||||
|
||||
switch (this->type_) {
|
||||
#ifdef USE_ETHERNET_OPENETH
|
||||
case ETHERNET_TYPE_OPENETH: {
|
||||
phy_config.autonego_timeout_ms = 1000;
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
this->phy_ = esp_eth_phy_new_generic(&phy_config);
|
||||
#else
|
||||
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ETH_USE_ESP32_EMAC
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720: {
|
||||
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
case ETHERNET_TYPE_RTL8201: {
|
||||
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
case ETHERNET_TYPE_DP83848: {
|
||||
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
case ETHERNET_TYPE_IP101: {
|
||||
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_JL1101
|
||||
case ETHERNET_TYPE_JL1101: {
|
||||
// PlatformIO (pioarduino): builtin esp_eth_phy_new_jl1101() on all IDF versions
|
||||
// Non-PlatformIO: custom ESPHome driver (esp_eth_phy_jl1101.c)
|
||||
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
case ETHERNET_TYPE_KSZ8081RNA: {
|
||||
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670: {
|
||||
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
#ifdef USE_ETHERNET_W5500
|
||||
case ETHERNET_TYPE_W5500: {
|
||||
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
|
||||
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DM9051
|
||||
case ETHERNET_TYPE_DM9051: {
|
||||
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
|
||||
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
default: {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_);
|
||||
this->eth_handle_ = nullptr;
|
||||
err = esp_eth_driver_install(ð_config, &this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH driver install error");
|
||||
|
||||
#ifndef USE_ETHERNET_SPI
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
|
||||
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
|
||||
this->ksz8081_set_clock_reference_(mac);
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
for (const auto &phy_register : this->phy_registers_) {
|
||||
this->write_phy_register_(mac, phy_register);
|
||||
}
|
||||
#endif
|
||||
|
||||
// use ESP internal eth mac
|
||||
uint8_t mac_addr[6];
|
||||
if (this->fixed_mac_.has_value()) {
|
||||
memcpy(mac_addr, this->fixed_mac_->data(), 6);
|
||||
} else {
|
||||
esp_read_mac(mac_addr, ESP_MAC_ETH);
|
||||
}
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
|
||||
ESPHL_ERROR_CHECK(err, "set mac address error");
|
||||
|
||||
/* attach Ethernet driver to TCP/IP stack */
|
||||
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
|
||||
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
|
||||
|
||||
// Register user defined event handers
|
||||
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
|
||||
ESPHL_ERROR_CHECK(err, "ETH event handler register error");
|
||||
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr);
|
||||
ESPHL_ERROR_CHECK(err, "GOT IP event handler register error");
|
||||
#if USE_NETWORK_IPV6
|
||||
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr);
|
||||
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
/* start Ethernet driver state machine */
|
||||
err = esp_eth_start(this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_config() {
|
||||
const char *eth_type;
|
||||
switch (this->type_) {
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720:
|
||||
eth_type = "LAN8720";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
case ETHERNET_TYPE_RTL8201:
|
||||
eth_type = "RTL8201";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
case ETHERNET_TYPE_DP83848:
|
||||
eth_type = "DP83848";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
case ETHERNET_TYPE_IP101:
|
||||
eth_type = "IP101";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_JL1101
|
||||
case ETHERNET_TYPE_JL1101:
|
||||
eth_type = "JL1101";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
eth_type = "KSZ8081";
|
||||
break;
|
||||
|
||||
case ETHERNET_TYPE_KSZ8081RNA:
|
||||
eth_type = "KSZ8081RNA";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_W5500
|
||||
case ETHERNET_TYPE_W5500:
|
||||
eth_type = "W5500";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DM9051
|
||||
case ETHERNET_TYPE_DM9051:
|
||||
eth_type = "DM9051";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_OPENETH
|
||||
case ETHERNET_TYPE_OPENETH:
|
||||
eth_type = "OPENETH";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670:
|
||||
eth_type = "LAN8670";
|
||||
break;
|
||||
#endif
|
||||
|
||||
default:
|
||||
eth_type = "Unknown";
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Ethernet:\n"
|
||||
" Connected: %s",
|
||||
YESNO(this->is_connected()));
|
||||
this->dump_connect_params_();
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CLK Pin: %u\n"
|
||||
" MISO Pin: %u\n"
|
||||
" MOSI Pin: %u\n"
|
||||
" CS Pin: %u",
|
||||
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
if (this->polling_interval_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Reset Pin: %d\n"
|
||||
" Clock Speed: %d MHz",
|
||||
this->reset_pin_, this->clock_speed_ / 1000000);
|
||||
#else
|
||||
if (this->power_pin_ != -1) {
|
||||
ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CLK Pin: %u\n"
|
||||
" MDC Pin: %u\n"
|
||||
" MDIO Pin: %u\n"
|
||||
" PHY addr: %u",
|
||||
this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_);
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
|
||||
}
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
|
||||
// TODO: do something smarter
|
||||
// return false;
|
||||
} else {
|
||||
addresses[0] = network::IPAddress(&ip.ip);
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
uint8_t count = 0;
|
||||
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
|
||||
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
|
||||
assert(count < addresses.size());
|
||||
for (int i = 0; i < count; i++) {
|
||||
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
||||
LwIPLock lock;
|
||||
const ip_addr_t *dns_ip = dns_getserver(num);
|
||||
return dns_ip;
|
||||
}
|
||||
|
||||
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) {
|
||||
const char *event_name;
|
||||
|
||||
switch (event) {
|
||||
case ETHERNET_EVENT_START:
|
||||
event_name = "ETH started";
|
||||
global_eth_component->started_ = true;
|
||||
global_eth_component->enable_loop_soon_any_context();
|
||||
break;
|
||||
case ETHERNET_EVENT_STOP:
|
||||
event_name = "ETH stopped";
|
||||
global_eth_component->started_ = false;
|
||||
global_eth_component->connected_ = false;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
break;
|
||||
case ETHERNET_EVENT_CONNECTED:
|
||||
event_name = "ETH connected";
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
|
||||
if (global_eth_component->manual_ip_.has_value()) {
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case ETHERNET_EVENT_DISCONNECTED:
|
||||
event_name = "ETH disconnected";
|
||||
global_eth_component->connected_ = false;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
|
||||
}
|
||||
|
||||
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
|
||||
void *event_data) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
|
||||
const esp_netif_ip_info_t *ip_info = &event->ip_info;
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
|
||||
global_eth_component->got_ipv4_address_ = true;
|
||||
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#else
|
||||
global_eth_component->connected_ = true;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
|
||||
void *event_data) {
|
||||
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
|
||||
global_eth_component->ipv6_count_ += 1;
|
||||
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ =
|
||||
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#else
|
||||
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
void EthernetComponent::finish_connect_() {
|
||||
#if USE_NETWORK_IPV6
|
||||
// Retry IPv6 link-local setup if it failed during initial connect
|
||||
// This handles the case where min_ipv6_addr_count is NOT set (or is 0),
|
||||
// allowing us to reach CONNECTED state with just IPv4.
|
||||
// If IPv6 setup failed in start_connect_() because the interface wasn't ready:
|
||||
// - Bootup timing issues (#10281)
|
||||
// - Cable unplugged/network interruption (#10705)
|
||||
// We can now retry since we're in CONNECTED state and the interface is definitely up.
|
||||
if (!this->ipv6_setup_done_) {
|
||||
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
|
||||
}
|
||||
// Always set the flag to prevent continuous retries
|
||||
// If IPv6 setup fails here with the interface up and stable, it's
|
||||
// likely a persistent issue (IPv6 disabled at router, hardware
|
||||
// limitation, etc.) that won't be resolved by further retries.
|
||||
// The device continues to work with IPv4.
|
||||
this->ipv6_setup_done_ = true;
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
void EthernetComponent::start_connect_() {
|
||||
global_eth_component->got_ipv4_address_ = false;
|
||||
#if USE_NETWORK_IPV6
|
||||
global_eth_component->ipv6_count_ = 0;
|
||||
this->ipv6_setup_done_ = false;
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
this->connect_begin_ = millis();
|
||||
this->status_set_warning(LOG_STR("waiting for IP configuration"));
|
||||
|
||||
esp_err_t err;
|
||||
err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str());
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t info;
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
info.ip = this->manual_ip_->static_ip;
|
||||
info.gw = this->manual_ip_->gateway;
|
||||
info.netmask = this->manual_ip_->subnet;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
info.ip.addr = 0;
|
||||
info.gw.addr = 0;
|
||||
info.netmask.addr = 0;
|
||||
}
|
||||
|
||||
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
|
||||
|
||||
err = esp_netif_dhcpc_get_status(this->eth_netif_, &status);
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!");
|
||||
|
||||
ESP_LOGV(TAG, "DHCP Client Status: %d", status);
|
||||
|
||||
err = esp_netif_dhcpc_stop(this->eth_netif_);
|
||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC stop error");
|
||||
}
|
||||
|
||||
err = esp_netif_set_ip_info(this->eth_netif_, &info);
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns1;
|
||||
dns_setserver(0, &d);
|
||||
}
|
||||
if (this->manual_ip_->dns2.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns2;
|
||||
dns_setserver(1, &d);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
err = esp_netif_dhcpc_start(this->eth_netif_);
|
||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC start error");
|
||||
}
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
// Attempt to create IPv6 link-local address
|
||||
// We MUST attempt this here, not just in finish_connect_(), because with
|
||||
// min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
|
||||
// However, this may fail with ESP_FAIL if the interface is not up yet:
|
||||
// - At bootup when link isn't ready (#10281)
|
||||
// - After disconnection/cable unplugged (#10705)
|
||||
// We'll retry in finish_connect_() if it fails here.
|
||||
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
|
||||
// This is a programming error, not a transient failure
|
||||
ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
|
||||
} else {
|
||||
// ESP_FAIL means the interface isn't up yet
|
||||
// This is expected and non-fatal, happens in multiple scenarios:
|
||||
// - During reconnection after network interruptions (#10705)
|
||||
// - At bootup when the link isn't ready yet (#10281)
|
||||
// We'll retry once we reach CONNECTED state and the interface is up
|
||||
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
|
||||
// Don't mark component as failed - this is a transient error
|
||||
}
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
this->connect_begin_ = millis();
|
||||
this->status_set_warning();
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_connect_params_() {
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
const ip_addr_t *dns_ip1;
|
||||
const ip_addr_t *dns_ip2;
|
||||
{
|
||||
LwIPLock lock;
|
||||
dns_ip1 = dns_getserver(0);
|
||||
dns_ip2 = dns_getserver(1);
|
||||
}
|
||||
|
||||
// Use stack buffers for IP address formatting to avoid heap allocations
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
" Hostname: '%s'\n"
|
||||
" Subnet: %s\n"
|
||||
" Gateway: %s\n"
|
||||
" DNS1: %s\n"
|
||||
" DNS2: %s\n"
|
||||
" MAC Address: %s\n"
|
||||
" Is Full Duplex: %s\n"
|
||||
" Link Speed: %u",
|
||||
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
|
||||
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);
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
uint8_t count = 0;
|
||||
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
|
||||
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
|
||||
for (int i = 0; i < count; i++) {
|
||||
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
|
||||
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
esp_err_t err;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
|
||||
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
||||
}
|
||||
|
||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[6];
|
||||
get_eth_mac_address_raw(mac);
|
||||
format_mac_addr_upper(mac, buf.data());
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||
esp_err_t err;
|
||||
eth_duplex_t duplex_mode;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
|
||||
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF);
|
||||
return duplex_mode;
|
||||
}
|
||||
|
||||
eth_speed_t EthernetComponent::get_link_speed() {
|
||||
esp_err_t err;
|
||||
eth_speed_t speed;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
|
||||
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M);
|
||||
return speed;
|
||||
}
|
||||
|
||||
bool EthernetComponent::powerdown() {
|
||||
ESP_LOGI(TAG, "Powering down ethernet PHY");
|
||||
if (this->phy_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Ethernet PHY not assigned");
|
||||
return false;
|
||||
}
|
||||
this->connected_ = false;
|
||||
this->started_ = false;
|
||||
// No need to enable_loop() here as this is only called during shutdown/reboot
|
||||
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error powering down ethernet PHY");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifndef USE_ETHERNET_SPI
|
||||
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F;
|
||||
|
||||
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
esp_err_t err;
|
||||
|
||||
uint32_t phy_control_2;
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
* KSZ8081RNA:
|
||||
* 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode.
|
||||
* 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
|
||||
* KSZ8081RND:
|
||||
* 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
|
||||
* 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode.
|
||||
*/
|
||||
if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
|
||||
phy_control_2 |= 1 << 7;
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
|
||||
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
}
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value);
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
|
||||
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ETHERNET && USE_ESP32
|
||||
@@ -0,0 +1,315 @@
|
||||
#include "ethernet_component.h"
|
||||
|
||||
#if defined(USE_ETHERNET) && defined(USE_RP2040)
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "esphome/components/rp2040/gpio.h"
|
||||
|
||||
#include <SPI.h>
|
||||
#include <lwip/dns.h>
|
||||
#include <lwip/netif.h>
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
static const char *const TAG = "ethernet";
|
||||
|
||||
void EthernetComponent::setup() {
|
||||
// Configure SPI pins
|
||||
SPI.setRX(this->miso_pin_);
|
||||
SPI.setTX(this->mosi_pin_);
|
||||
SPI.setSCK(this->clk_pin_);
|
||||
|
||||
// Toggle reset pin if configured
|
||||
if (this->reset_pin_ >= 0) {
|
||||
rp2040::RP2040GPIOPin reset_pin;
|
||||
reset_pin.set_pin(this->reset_pin_);
|
||||
reset_pin.set_flags(gpio::FLAG_OUTPUT);
|
||||
reset_pin.setup();
|
||||
reset_pin.digital_write(false);
|
||||
delay(1); // NOLINT
|
||||
reset_pin.digital_write(true);
|
||||
delay(10); // NOLINT - wait for W5500 to initialize after reset
|
||||
}
|
||||
|
||||
// Create the W5500 device instance
|
||||
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
|
||||
|
||||
// Set hostname before begin() so the LWIP netif gets it
|
||||
this->eth_->hostname(App.get_name().c_str());
|
||||
|
||||
// Configure static IP if set (must be done before begin())
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
IPAddress ip(this->manual_ip_->static_ip);
|
||||
IPAddress gateway(this->manual_ip_->gateway);
|
||||
IPAddress subnet(this->manual_ip_->subnet);
|
||||
IPAddress dns1(this->manual_ip_->dns1);
|
||||
IPAddress dns2(this->manual_ip_->dns2);
|
||||
this->eth_->config(ip, gateway, subnet, dns1, dns2);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Begin with fixed MAC or auto-generated
|
||||
bool success;
|
||||
if (this->fixed_mac_.has_value()) {
|
||||
success = this->eth_->begin(this->fixed_mac_->data());
|
||||
} else {
|
||||
success = this->eth_->begin();
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet");
|
||||
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
this->eth_ = nullptr;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make this the default interface for routing
|
||||
this->eth_->setDefault(true);
|
||||
|
||||
// The arduino-pico LwipIntfDev automatically handles packet processing
|
||||
// via __addEthernetPacketHandler when no interrupt pin is used,
|
||||
// or via GPIO interrupt when one is provided.
|
||||
|
||||
// Don't set started_ here — let the link polling in loop() set it
|
||||
// when the W5500 link is actually up. Setting it prematurely causes
|
||||
// a "Starting → Stopped → Starting" log sequence because the W5500
|
||||
// needs time after begin() before the PHY link is ready.
|
||||
}
|
||||
|
||||
void EthernetComponent::loop() {
|
||||
// On RP2040, we need to poll connection state since there are no events.
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Throttle link/IP polling to avoid excessive SPI transactions from linkStatus()
|
||||
// which reads the W5500 PHY register via SPI on every call.
|
||||
// connected() reads netif->ip_addr without LwIPLock, but this is a single
|
||||
// 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
|
||||
// value, which is benign for polling.
|
||||
if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
|
||||
this->last_link_check_ = now;
|
||||
bool link_up = this->eth_->linkStatus() == LinkON;
|
||||
bool has_ip = this->eth_->connected();
|
||||
|
||||
if (!link_up) {
|
||||
if (this->started_) {
|
||||
this->started_ = false;
|
||||
this->connected_ = false;
|
||||
}
|
||||
} else {
|
||||
if (!this->started_) {
|
||||
this->started_ = true;
|
||||
}
|
||||
bool was_connected = this->connected_;
|
||||
this->connected_ = has_ip;
|
||||
if (this->connected_ && !was_connected) {
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
this->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// State machine
|
||||
switch (this->state_) {
|
||||
case EthernetComponentState::STOPPED:
|
||||
if (this->started_) {
|
||||
ESP_LOGI(TAG, "Starting connection");
|
||||
this->state_ = EthernetComponentState::CONNECTING;
|
||||
this->start_connect_();
|
||||
}
|
||||
break;
|
||||
case EthernetComponentState::CONNECTING:
|
||||
if (!this->started_) {
|
||||
ESP_LOGI(TAG, "Stopped connection");
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
} else if (this->connected_) {
|
||||
// connection established
|
||||
ESP_LOGI(TAG, "Connected");
|
||||
this->state_ = EthernetComponentState::CONNECTED;
|
||||
|
||||
this->dump_connect_params_();
|
||||
this->status_clear_warning();
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
this->connect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (now - this->connect_begin_ > 15000) {
|
||||
ESP_LOGW(TAG, "Connecting failed; reconnecting");
|
||||
this->start_connect_();
|
||||
}
|
||||
break;
|
||||
case EthernetComponentState::CONNECTED:
|
||||
if (!this->started_) {
|
||||
ESP_LOGI(TAG, "Stopped connection");
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else if (!this->connected_) {
|
||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||
this->state_ = EthernetComponentState::CONNECTING;
|
||||
this->start_connect_();
|
||||
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
||||
this->disconnect_trigger_.trigger();
|
||||
#endif
|
||||
} else {
|
||||
this->finish_connect_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Ethernet:\n"
|
||||
" Type: W5500\n"
|
||||
" Connected: %s\n"
|
||||
" CLK Pin: %u\n"
|
||||
" MISO Pin: %u\n"
|
||||
" MOSI Pin: %u\n"
|
||||
" CS Pin: %u\n"
|
||||
" IRQ Pin: %d\n"
|
||||
" Reset Pin: %d",
|
||||
YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
|
||||
this->interrupt_pin_, this->reset_pin_);
|
||||
this->dump_connect_params_();
|
||||
}
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
if (this->eth_ != nullptr) {
|
||||
LwIPLock lock;
|
||||
addresses[0] = network::IPAddress(this->eth_->localIP());
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
||||
LwIPLock lock;
|
||||
const ip_addr_t *dns_ip = dns_getserver(num);
|
||||
return dns_ip;
|
||||
}
|
||||
|
||||
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
if (this->eth_ != nullptr) {
|
||||
this->eth_->macAddress(mac);
|
||||
} else {
|
||||
memset(mac, 0, 6);
|
||||
}
|
||||
}
|
||||
|
||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[6];
|
||||
get_eth_mac_address_raw(mac);
|
||||
format_mac_addr_upper(mac, buf.data());
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||
// W5500 is always full duplex
|
||||
return ETH_DUPLEX_FULL;
|
||||
}
|
||||
|
||||
eth_speed_t EthernetComponent::get_link_speed() {
|
||||
// W5500 is always 100Mbps
|
||||
return ETH_SPEED_100M;
|
||||
}
|
||||
|
||||
bool EthernetComponent::powerdown() {
|
||||
ESP_LOGI(TAG, "Powering down ethernet");
|
||||
if (this->eth_ != nullptr) {
|
||||
this->eth_->end();
|
||||
}
|
||||
this->connected_ = false;
|
||||
this->started_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EthernetComponent::start_connect_() {
|
||||
this->got_ipv4_address_ = false;
|
||||
this->connect_begin_ = millis();
|
||||
this->status_set_warning(LOG_STR("waiting for IP configuration"));
|
||||
|
||||
// Hostname is already set in setup() via LwipIntf::setHostname()
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
// Static IP was already configured before begin() in setup()
|
||||
// Set DNS servers
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns1;
|
||||
dns_setserver(0, &d);
|
||||
}
|
||||
if (this->manual_ip_->dns2.is_set()) {
|
||||
ip_addr_t d;
|
||||
d = this->manual_ip_->dns2;
|
||||
dns_setserver(1, &d);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void EthernetComponent::finish_connect_() {
|
||||
// No additional work needed on RP2040 for now
|
||||
// IPv6 link-local could be added here in the future
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_connect_params_() {
|
||||
if (this->eth_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
|
||||
// Copy all lwIP state under the lock to avoid races with IRQ callbacks
|
||||
ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr;
|
||||
{
|
||||
LwIPLock lock;
|
||||
auto *netif = this->eth_->getNetIf();
|
||||
ip_addr = netif->ip_addr;
|
||||
netmask = netif->netmask;
|
||||
gw = netif->gw;
|
||||
dns1_addr = *dns_getserver(0);
|
||||
dns2_addr = *dns_getserver(1);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
" Hostname: '%s'\n"
|
||||
" Subnet: %s\n"
|
||||
" Gateway: %s\n"
|
||||
" DNS1: %s\n"
|
||||
" DNS2: %s\n"
|
||||
" MAC Address: %s",
|
||||
network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(),
|
||||
network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf),
|
||||
network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf),
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
|
||||
}
|
||||
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ETHERNET && USE_RP2040
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user