mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 23:21:54 +00:00 
			
		
		
		
	Merge upstream/dev into drop_unique_id
This commit is contained in:
		| @@ -1 +1 @@ | |||||||
| a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a | 07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a | ||||||
|   | |||||||
							
								
								
									
										92
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | name: Report an issue with ESPHome | ||||||
|  | description: Report an issue with ESPHome. | ||||||
|  | body: | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: | | ||||||
|  |         This issue form is for reporting bugs only! | ||||||
|  |  | ||||||
|  |         If you have a feature request or enhancement, please [request them here instead][fr]. | ||||||
|  |  | ||||||
|  |         [fr]: https://github.com/orgs/esphome/discussions | ||||||
|  |   - type: textarea | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |     id: problem | ||||||
|  |     attributes: | ||||||
|  |       label: The problem | ||||||
|  |       description: >- | ||||||
|  |         Describe the issue you are experiencing here to communicate to the | ||||||
|  |         maintainers. Tell us what you were trying to do and what happened. | ||||||
|  |  | ||||||
|  |         Provide a clear and concise description of what the problem is. | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: | | ||||||
|  |         ## Environment | ||||||
|  |   - type: input | ||||||
|  |     id: version | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |     attributes: | ||||||
|  |       label: Which version of ESPHome has the issue? | ||||||
|  |       description: > | ||||||
|  |         ESPHome version like 1.19, 2025.6.0 or 2025.XX.X-dev. | ||||||
|  |   - type: dropdown | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |     id: installation | ||||||
|  |     attributes: | ||||||
|  |       label: What type of installation are you using? | ||||||
|  |       options: | ||||||
|  |         - Home Assistant Add-on | ||||||
|  |         - Docker | ||||||
|  |         - pip | ||||||
|  |   - type: dropdown | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |     id: platform | ||||||
|  |     attributes: | ||||||
|  |       label: What platform are you using? | ||||||
|  |       options: | ||||||
|  |         - ESP8266 | ||||||
|  |         - ESP32 | ||||||
|  |         - RP2040 | ||||||
|  |         - BK72XX | ||||||
|  |         - RTL87XX | ||||||
|  |         - LN882X | ||||||
|  |         - Host | ||||||
|  |         - Other | ||||||
|  |   - type: input | ||||||
|  |     id: component_name | ||||||
|  |     attributes: | ||||||
|  |       label: Component causing the issue | ||||||
|  |       description: > | ||||||
|  |         The name of the component or platform. For example, api/i2c or ultrasonic. | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: | | ||||||
|  |         # Details | ||||||
|  |   - type: textarea | ||||||
|  |     id: config | ||||||
|  |     attributes: | ||||||
|  |       label: YAML Config | ||||||
|  |       description: | | ||||||
|  |         Include a complete YAML configuration file demonstrating the problem here. Preferably post the *entire* file - don't make assumptions about what is unimportant. However, if it's a large or complicated config then you will need to reduce it to the smallest possible file *that still demonstrates the problem*. If you don't provide enough information to *easily* reproduce the problem, it's unlikely your bug report will get any attention. Logs do not belong here, attach them below. | ||||||
|  |       render: yaml | ||||||
|  |   - type: textarea | ||||||
|  |     id: logs | ||||||
|  |     attributes: | ||||||
|  |       label: Anything in the logs that might be useful for us? | ||||||
|  |       description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs. | ||||||
|  |       render: txt | ||||||
|  |   - type: textarea | ||||||
|  |     id: additional | ||||||
|  |     attributes: | ||||||
|  |       label: Additional information | ||||||
|  |       description: > | ||||||
|  |         If you have any additional information for us, use the field below. | ||||||
|  |         Please note, you can attach screenshots or screen recordings here, by | ||||||
|  |         dragging and dropping files in the field below. | ||||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,15 +1,21 @@ | |||||||
| --- | --- | ||||||
| blank_issues_enabled: false | blank_issues_enabled: false | ||||||
| contact_links: | contact_links: | ||||||
|   - name: Issue Tracker |   - name: Report an issue with the ESPHome documentation | ||||||
|     url: https://github.com/esphome/issues |     url: https://github.com/esphome/esphome-docs/issues/new/choose | ||||||
|     about: Please create bug reports in the dedicated issue tracker. |     about: Report an issue with the ESPHome documentation. | ||||||
|   - name: Feature Request Tracker |   - name: Report an issue with the ESPHome web server | ||||||
|     url: https://github.com/esphome/feature-requests |     url: https://github.com/esphome/esphome-webserver/issues/new/choose | ||||||
|     about: | |     about: Report an issue with the ESPHome web server. | ||||||
|       Please create feature requests in the dedicated feature request tracker. |   - name: Report an issue with the ESPHome Builder / Dashboard | ||||||
|  |     url: https://github.com/esphome/dashboard/issues/new/choose | ||||||
|  |     about: Report an issue with the ESPHome Builder / Dashboard. | ||||||
|  |   - name: Report an issue with the ESPHome API client | ||||||
|  |     url: https://github.com/esphome/aioesphomeapi/issues/new/choose | ||||||
|  |     about: Report an issue with the ESPHome API client. | ||||||
|  |   - name: Make a Feature Request | ||||||
|  |     url: https://github.com/orgs/esphome/discussions | ||||||
|  |     about: Please create feature requests in the dedicated feature request tracker. | ||||||
|   - name: Frequently Asked Question |   - name: Frequently Asked Question | ||||||
|     url: https://esphome.io/guides/faq.html |     url: https://esphome.io/guides/faq.html | ||||||
|     about: | |     about: Please view the FAQ for common questions and what to include in a bug report. | ||||||
|       Please view the FAQ for common questions and what |  | ||||||
|       to include in a bug report. |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							| @@ -73,4 +73,3 @@ jobs: | |||||||
|                 }); |                 }); | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										135
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										135
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -39,7 +39,7 @@ jobs: | |||||||
|         uses: actions/checkout@v4.2.2 |         uses: actions/checkout@v4.2.2 | ||||||
|       - name: Generate cache-key |       - name: Generate cache-key | ||||||
|         id: cache-key |         id: cache-key | ||||||
|         run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT |         run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         id: python |         id: python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v5.6.0 | ||||||
| @@ -58,55 +58,9 @@ jobs: | |||||||
|           python -m venv venv |           python -m venv venv | ||||||
|           . venv/bin/activate |           . venv/bin/activate | ||||||
|           python --version |           python --version | ||||||
|           pip install -r requirements.txt -r requirements_test.txt |           pip install -r requirements.txt -r requirements_test.txt pre-commit | ||||||
|           pip install -e . |           pip install -e . | ||||||
|  |  | ||||||
|   ruff: |  | ||||||
|     name: Check ruff |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     needs: |  | ||||||
|       - common |  | ||||||
|       - determine-jobs |  | ||||||
|     if: needs.determine-jobs.outputs.python-linters == 'true' |  | ||||||
|     steps: |  | ||||||
|       - name: Check out code from GitHub |  | ||||||
|         uses: actions/checkout@v4.2.2 |  | ||||||
|       - name: Restore Python |  | ||||||
|         uses: ./.github/actions/restore-python |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |  | ||||||
|           cache-key: ${{ needs.common.outputs.cache-key }} |  | ||||||
|       - name: Run Ruff |  | ||||||
|         run: | |  | ||||||
|           . venv/bin/activate |  | ||||||
|           ruff format esphome tests |  | ||||||
|       - name: Suggested changes |  | ||||||
|         run: script/ci-suggest-changes |  | ||||||
|         if: always() |  | ||||||
|  |  | ||||||
|   flake8: |  | ||||||
|     name: Check flake8 |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     needs: |  | ||||||
|       - common |  | ||||||
|       - determine-jobs |  | ||||||
|     if: needs.determine-jobs.outputs.python-linters == 'true' |  | ||||||
|     steps: |  | ||||||
|       - name: Check out code from GitHub |  | ||||||
|         uses: actions/checkout@v4.2.2 |  | ||||||
|       - name: Restore Python |  | ||||||
|         uses: ./.github/actions/restore-python |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |  | ||||||
|           cache-key: ${{ needs.common.outputs.cache-key }} |  | ||||||
|       - name: Run flake8 |  | ||||||
|         run: | |  | ||||||
|           . venv/bin/activate |  | ||||||
|           flake8 esphome |  | ||||||
|       - name: Suggested changes |  | ||||||
|         run: script/ci-suggest-changes |  | ||||||
|         if: always() |  | ||||||
|  |  | ||||||
|   pylint: |   pylint: | ||||||
|     name: Check pylint |     name: Check pylint | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
| @@ -130,29 +84,6 @@ jobs: | |||||||
|         run: script/ci-suggest-changes |         run: script/ci-suggest-changes | ||||||
|         if: always() |         if: always() | ||||||
|  |  | ||||||
|   pyupgrade: |  | ||||||
|     name: Check pyupgrade |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     needs: |  | ||||||
|       - common |  | ||||||
|       - determine-jobs |  | ||||||
|     if: needs.determine-jobs.outputs.python-linters == 'true' |  | ||||||
|     steps: |  | ||||||
|       - name: Check out code from GitHub |  | ||||||
|         uses: actions/checkout@v4.2.2 |  | ||||||
|       - name: Restore Python |  | ||||||
|         uses: ./.github/actions/restore-python |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |  | ||||||
|           cache-key: ${{ needs.common.outputs.cache-key }} |  | ||||||
|       - name: Run pyupgrade |  | ||||||
|         run: | |  | ||||||
|           . venv/bin/activate |  | ||||||
|           pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f` |  | ||||||
|       - name: Suggested changes |  | ||||||
|         run: script/ci-suggest-changes |  | ||||||
|         if: always() |  | ||||||
|  |  | ||||||
|   ci-custom: |   ci-custom: | ||||||
|     name: Run script/ci-custom |     name: Run script/ci-custom | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
| @@ -248,7 +179,6 @@ jobs: | |||||||
|     outputs: |     outputs: | ||||||
|       integration-tests: ${{ steps.determine.outputs.integration-tests }} |       integration-tests: ${{ steps.determine.outputs.integration-tests }} | ||||||
|       clang-tidy: ${{ steps.determine.outputs.clang-tidy }} |       clang-tidy: ${{ steps.determine.outputs.clang-tidy }} | ||||||
|       clang-format: ${{ steps.determine.outputs.clang-format }} |  | ||||||
|       python-linters: ${{ steps.determine.outputs.python-linters }} |       python-linters: ${{ steps.determine.outputs.python-linters }} | ||||||
|       changed-components: ${{ steps.determine.outputs.changed-components }} |       changed-components: ${{ steps.determine.outputs.changed-components }} | ||||||
|       component-test-count: ${{ steps.determine.outputs.component-test-count }} |       component-test-count: ${{ steps.determine.outputs.component-test-count }} | ||||||
| @@ -276,7 +206,6 @@ jobs: | |||||||
|           # Extract individual fields |           # Extract individual fields | ||||||
|           echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT |           echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT | ||||||
|           echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT |           echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT | ||||||
|           echo "clang-format=$(echo "$output" | jq -r '.clang_format')" >> $GITHUB_OUTPUT |  | ||||||
|           echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT |           echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT | ||||||
|           echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT |           echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT | ||||||
|           echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT |           echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT | ||||||
| @@ -317,46 +246,11 @@ jobs: | |||||||
|           . venv/bin/activate |           . venv/bin/activate | ||||||
|           pytest -vv --no-cov --tb=native -n auto tests/integration/ |           pytest -vv --no-cov --tb=native -n auto tests/integration/ | ||||||
|  |  | ||||||
|   clang-format: |  | ||||||
|     name: Check clang-format |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     needs: |  | ||||||
|       - common |  | ||||||
|       - determine-jobs |  | ||||||
|     if: needs.determine-jobs.outputs.clang-format == 'true' |  | ||||||
|     steps: |  | ||||||
|       - name: Check out code from GitHub |  | ||||||
|         uses: actions/checkout@v4.2.2 |  | ||||||
|       - name: Restore Python |  | ||||||
|         uses: ./.github/actions/restore-python |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |  | ||||||
|           cache-key: ${{ needs.common.outputs.cache-key }} |  | ||||||
|       - name: Install clang-format |  | ||||||
|         run: | |  | ||||||
|           . venv/bin/activate |  | ||||||
|           pip install clang-format -c requirements_dev.txt |  | ||||||
|       - name: Run clang-format |  | ||||||
|         run: | |  | ||||||
|           . venv/bin/activate |  | ||||||
|           script/clang-format -i |  | ||||||
|           git diff-index --quiet HEAD -- |  | ||||||
|       - name: Suggested changes |  | ||||||
|         run: script/ci-suggest-changes |  | ||||||
|         if: always() |  | ||||||
|  |  | ||||||
|   clang-tidy: |   clang-tidy: | ||||||
|     name: ${{ matrix.name }} |     name: ${{ matrix.name }} | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|     needs: |     needs: | ||||||
|       - common |       - common | ||||||
|       - ruff |  | ||||||
|       - ci-custom |  | ||||||
|       - clang-format |  | ||||||
|       - flake8 |  | ||||||
|       - pylint |  | ||||||
|       - pytest |  | ||||||
|       - pyupgrade |  | ||||||
|       - determine-jobs |       - determine-jobs | ||||||
|     if: needs.determine-jobs.outputs.clang-tidy == 'true' |     if: needs.determine-jobs.outputs.clang-tidy == 'true' | ||||||
|     env: |     env: | ||||||
| @@ -562,24 +456,41 @@ jobs: | |||||||
|             ./script/test_build_components -e compile -c $component |             ./script/test_build_components -e compile -c $component | ||||||
|           done |           done | ||||||
|  |  | ||||||
|  |   pre-commit-ci-lite: | ||||||
|  |     name: pre-commit.ci lite | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release' | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v4.2.2 | ||||||
|  |       - name: Restore Python | ||||||
|  |         uses: ./.github/actions/restore-python | ||||||
|  |         with: | ||||||
|  |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|  |           cache-key: ${{ needs.common.outputs.cache-key }} | ||||||
|  |       - uses: pre-commit/action@v3.0.1 | ||||||
|  |         env: | ||||||
|  |           SKIP: pylint,clang-tidy-hash | ||||||
|  |       - uses: pre-commit-ci/lite-action@v1.1.0 | ||||||
|  |         if: always() | ||||||
|  |  | ||||||
|   ci-status: |   ci-status: | ||||||
|     name: CI Status |     name: CI Status | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|     needs: |     needs: | ||||||
|       - common |       - common | ||||||
|       - ruff |  | ||||||
|       - ci-custom |       - ci-custom | ||||||
|       - clang-format |  | ||||||
|       - flake8 |  | ||||||
|       - pylint |       - pylint | ||||||
|       - pytest |       - pytest | ||||||
|       - integration-tests |       - integration-tests | ||||||
|       - pyupgrade |  | ||||||
|       - clang-tidy |       - clang-tidy | ||||||
|       - determine-jobs |       - determine-jobs | ||||||
|       - test-build-components |       - test-build-components | ||||||
|       - test-build-components-splitter |       - test-build-components-splitter | ||||||
|       - test-build-components-split |       - test-build-components-split | ||||||
|  |       - pre-commit-ci-lite | ||||||
|     if: always() |     if: always() | ||||||
|     steps: |     steps: | ||||||
|       - name: Success |       - name: Success | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,25 +0,0 @@ | |||||||
| --- |  | ||||||
| name: YAML lint |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     branches: [dev, beta, release] |  | ||||||
|     paths: |  | ||||||
|       - "**.yaml" |  | ||||||
|       - "**.yml" |  | ||||||
|   pull_request: |  | ||||||
|     paths: |  | ||||||
|       - "**.yaml" |  | ||||||
|       - "**.yml" |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   yamllint: |  | ||||||
|     name: yamllint |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - name: Check out code from GitHub |  | ||||||
|         uses: actions/checkout@v4.2.2 |  | ||||||
|       - name: Run yamllint |  | ||||||
|         uses: frenck/action-yamllint@v1.5.0 |  | ||||||
|         with: |  | ||||||
|           strict: true |  | ||||||
| @@ -1,6 +1,13 @@ | |||||||
| --- | --- | ||||||
| # See https://pre-commit.com for more information | # See https://pre-commit.com for more information | ||||||
| # See https://pre-commit.com/hooks.html for more hooks | # See https://pre-commit.com/hooks.html for more hooks | ||||||
|  |  | ||||||
|  | ci: | ||||||
|  |   autoupdate_commit_msg: 'pre-commit: autoupdate' | ||||||
|  |   autoupdate_schedule: off  # Disabled until ruff versions are synced between deps and pre-commit | ||||||
|  |   # Skip hooks that have issues in pre-commit CI environment | ||||||
|  |   skip: [pylint, clang-tidy-hash] | ||||||
|  |  | ||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit |   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||||
|     # Ruff version. |     # Ruff version. | ||||||
| @@ -20,13 +27,15 @@ repos: | |||||||
|           - pydocstyle==5.1.1 |           - pydocstyle==5.1.1 | ||||||
|         files: ^(esphome|tests)/.+\.py$ |         files: ^(esphome|tests)/.+\.py$ | ||||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks |   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||||
|     rev: v3.4.0 |     rev: v5.0.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: no-commit-to-branch |       - id: no-commit-to-branch | ||||||
|         args: |         args: | ||||||
|           - --branch=dev |           - --branch=dev | ||||||
|           - --branch=release |           - --branch=release | ||||||
|           - --branch=beta |           - --branch=beta | ||||||
|  |       - id: end-of-file-fixer | ||||||
|  |       - id: trailing-whitespace | ||||||
|   - repo: https://github.com/asottile/pyupgrade |   - repo: https://github.com/asottile/pyupgrade | ||||||
|     rev: v3.20.0 |     rev: v3.20.0 | ||||||
|     hooks: |     hooks: | ||||||
| @@ -36,6 +45,7 @@ repos: | |||||||
|     rev: v1.37.1 |     rev: v1.37.1 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: yamllint |       - id: yamllint | ||||||
|  |         exclude: ^(\.clang-format|\.clang-tidy)$ | ||||||
|   - repo: https://github.com/pre-commit/mirrors-clang-format |   - repo: https://github.com/pre-commit/mirrors-clang-format | ||||||
|     rev: v13.0.1 |     rev: v13.0.1 | ||||||
|     hooks: |     hooks: | ||||||
|   | |||||||
| @@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw | |||||||
| esphome/components/nfc/* @jesserockz @kbx81 | esphome/components/nfc/* @jesserockz @kbx81 | ||||||
| esphome/components/noblex/* @AGalfra | esphome/components/noblex/* @AGalfra | ||||||
| esphome/components/npi19/* @bakerkj | esphome/components/npi19/* @bakerkj | ||||||
|  | esphome/components/nrf52/* @tomaszduda23 | ||||||
| esphome/components/number/* @esphome/core | esphome/components/number/* @esphome/core | ||||||
| esphome/components/one_wire/* @ssieb | esphome/components/one_wire/* @ssieb | ||||||
| esphome/components/online_image/* @clydebarrow @guillempages | esphome/components/online_image/* @clydebarrow @guillempages | ||||||
| @@ -535,5 +536,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix | |||||||
| esphome/components/xl9535/* @mreditor97 | esphome/components/xl9535/* @mreditor97 | ||||||
| esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||||
| esphome/components/xxtea/* @clydebarrow | esphome/components/xxtea/* @clydebarrow | ||||||
|  | esphome/components/zephyr/* @tomaszduda23 | ||||||
| esphome/components/zhlt01/* @cfeenstra1024 | esphome/components/zhlt01/* @cfeenstra1024 | ||||||
| esphome/components/zio_ultrasonic/* @kahrendt | esphome/components/zio_ultrasonic/* @kahrendt | ||||||
|   | |||||||
| @@ -51,82 +51,83 @@ SAMPLING_MODES = { | |||||||
|     "max": sampling_mode.MAX, |     "max": sampling_mode.MAX, | ||||||
| } | } | ||||||
|  |  | ||||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True) | ||||||
| adc2_channel_t = cg.global_ns.enum("adc2_channel_t") |  | ||||||
|  | adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True) | ||||||
|  |  | ||||||
| # pin to adc1 channel mapping | # pin to adc1 channel mapping | ||||||
| # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h | # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h | ||||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32: { |     VARIANT_ESP32: { | ||||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, |         36: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, |         37: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, |         38: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, |         39: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, |         32: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, |         33: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, |         34: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, |         35: adc_channel_t.ADC_CHANNEL_7, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C2: { |     VARIANT_ESP32C2: { | ||||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, |         0: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, |         1: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, |         3: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, |         4: adc_channel_t.ADC_CHANNEL_4, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C3: { |     VARIANT_ESP32C3: { | ||||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, |         0: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, |         1: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, |         3: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, |         4: adc_channel_t.ADC_CHANNEL_4, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C6: { |     VARIANT_ESP32C6: { | ||||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, |         0: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, |         1: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, |         3: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, |         4: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_5, |         5: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         6: adc1_channel_t.ADC1_CHANNEL_6, |         6: adc_channel_t.ADC_CHANNEL_6, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32H2: { |     VARIANT_ESP32H2: { | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, |         1: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, |         2: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, |         3: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, |         4: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, |         5: adc_channel_t.ADC_CHANNEL_4, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S2: { |     VARIANT_ESP32S2: { | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, |         1: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, |         2: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, |         3: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, |         4: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, |         5: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, |         6: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, |         7: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, |         8: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, |         9: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, |         10: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S3: { |     VARIANT_ESP32S3: { | ||||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, |         1: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, |         2: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, |         3: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, |         4: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, |         5: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, |         6: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, |         7: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, |         8: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, |         9: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, |         10: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -135,24 +136,24 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | |||||||
| ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32: { |     VARIANT_ESP32: { | ||||||
|         4: adc2_channel_t.ADC2_CHANNEL_0, |         4: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         0: adc2_channel_t.ADC2_CHANNEL_1, |         0: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         2: adc2_channel_t.ADC2_CHANNEL_2, |         2: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         15: adc2_channel_t.ADC2_CHANNEL_3, |         15: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         13: adc2_channel_t.ADC2_CHANNEL_4, |         13: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         12: adc2_channel_t.ADC2_CHANNEL_5, |         12: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         14: adc2_channel_t.ADC2_CHANNEL_6, |         14: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         27: adc2_channel_t.ADC2_CHANNEL_7, |         27: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         25: adc2_channel_t.ADC2_CHANNEL_8, |         25: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         26: adc2_channel_t.ADC2_CHANNEL_9, |         26: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C2: { |     VARIANT_ESP32C2: { | ||||||
|         5: adc2_channel_t.ADC2_CHANNEL_0, |         5: adc_channel_t.ADC_CHANNEL_0, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C3: { |     VARIANT_ESP32C3: { | ||||||
|         5: adc2_channel_t.ADC2_CHANNEL_0, |         5: adc_channel_t.ADC_CHANNEL_0, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32C6: {},  # no ADC2 |     VARIANT_ESP32C6: {},  # no ADC2 | ||||||
| @@ -160,29 +161,29 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | |||||||
|     VARIANT_ESP32H2: {},  # no ADC2 |     VARIANT_ESP32H2: {},  # no ADC2 | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S2: { |     VARIANT_ESP32S2: { | ||||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, |         11: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, |         12: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, |         13: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, |         14: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, |         15: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, |         16: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, |         17: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, |         18: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, |         19: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, |         20: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h |     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||||
|     VARIANT_ESP32S3: { |     VARIANT_ESP32S3: { | ||||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, |         11: adc_channel_t.ADC_CHANNEL_0, | ||||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, |         12: adc_channel_t.ADC_CHANNEL_1, | ||||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, |         13: adc_channel_t.ADC_CHANNEL_2, | ||||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, |         14: adc_channel_t.ADC_CHANNEL_3, | ||||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, |         15: adc_channel_t.ADC_CHANNEL_4, | ||||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, |         16: adc_channel_t.ADC_CHANNEL_5, | ||||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, |         17: adc_channel_t.ADC_CHANNEL_6, | ||||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, |         18: adc_channel_t.ADC_CHANNEL_7, | ||||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, |         19: adc_channel_t.ADC_CHANNEL_8, | ||||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, |         20: adc_channel_t.ADC_CHANNEL_9, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,11 +3,14 @@ | |||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/defines.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| #include <esp_adc_cal.h> | #include "esp_adc/adc_cali.h" | ||||||
| #include "driver/adc.h" | #include "esp_adc/adc_cali_scheme.h" | ||||||
|  | #include "esp_adc/adc_oneshot.h" | ||||||
|  | #include "hal/adc_types.h"  // This defines ADC_CHANNEL_MAX | ||||||
| #endif                      // USE_ESP32 | #endif                      // USE_ESP32 | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -49,33 +52,72 @@ class Aggregator { | |||||||
|  |  | ||||||
| class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | ||||||
|  public: |  public: | ||||||
|  |   /// Update the sensor's state by reading the current ADC value. | ||||||
|  |   /// This method is called periodically based on the update interval. | ||||||
|  |   void update() override; | ||||||
|  |  | ||||||
|  |   /// Set up the ADC sensor by initializing hardware and calibration parameters. | ||||||
|  |   /// This method is called once during device initialization. | ||||||
|  |   void setup() override; | ||||||
|  |  | ||||||
|  |   /// Output the configuration details of the ADC sensor for debugging purposes. | ||||||
|  |   /// This method is called during the ESPHome setup process to log the configuration. | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   /// Return the setup priority for this component. | ||||||
|  |   /// Components with higher priority are initialized earlier during setup. | ||||||
|  |   /// @return A float representing the setup priority. | ||||||
|  |   float get_setup_priority() const override; | ||||||
|  |  | ||||||
|  |   /// Set the GPIO pin to be used by the ADC sensor. | ||||||
|  |   /// @param pin Pointer to an InternalGPIOPin representing the ADC input pin. | ||||||
|  |   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } | ||||||
|  |  | ||||||
|  |   /// Enable or disable the output of raw ADC values (unprocessed data). | ||||||
|  |   /// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false). | ||||||
|  |   void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } | ||||||
|  |  | ||||||
|  |   /// Set the number of samples to be taken for ADC readings to improve accuracy. | ||||||
|  |   /// A higher sample count reduces noise but increases the reading time. | ||||||
|  |   /// @param sample_count The number of samples (e.g., 1, 4, 8). | ||||||
|  |   void set_sample_count(uint8_t sample_count); | ||||||
|  |  | ||||||
|  |   /// Set the sampling mode for how multiple ADC samples are combined into a single measurement. | ||||||
|  |   /// | ||||||
|  |   /// When multiple samples are taken (controlled by set_sample_count), they can be combined | ||||||
|  |   /// in one of three ways: | ||||||
|  |   ///   - SamplingMode::AVG: Compute the average (default) | ||||||
|  |   ///   - SamplingMode::MIN: Use the lowest sample value | ||||||
|  |   ///   - SamplingMode::MAX: Use the highest sample value | ||||||
|  |   /// @param sampling_mode The desired sampling mode to use for aggregating ADC samples. | ||||||
|  |   void set_sampling_mode(SamplingMode sampling_mode); | ||||||
|  |  | ||||||
|  |   /// Perform a single ADC sampling operation and return the measured value. | ||||||
|  |   /// This function handles raw readings, calibration, and averaging as needed. | ||||||
|  |   /// @return The sampled value as a float. | ||||||
|  |   float sample() override; | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|   /// Set the attenuation for this pin. Only available on the ESP32. |   /// Set the ADC attenuation level to adjust the input voltage range. | ||||||
|  |   /// This determines how the ADC interprets input voltages, allowing for greater precision | ||||||
|  |   /// or the ability to measure higher voltages depending on the chosen attenuation level. | ||||||
|  |   /// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11). | ||||||
|   void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } |   void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||||
|   void set_channel1(adc1_channel_t channel) { |  | ||||||
|     this->channel1_ = channel; |   /// Configure the ADC to use a specific channel on ADC1. | ||||||
|     this->channel2_ = ADC2_CHANNEL_MAX; |   /// This sets the channel for single-shot or continuous ADC measurements. | ||||||
|   } |   /// @param channel The ADC1 channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc. | ||||||
|   void set_channel2(adc2_channel_t channel) { |   void set_channel(adc_unit_t unit, adc_channel_t channel) { | ||||||
|     this->channel2_ = channel; |     this->adc_unit_ = unit; | ||||||
|     this->channel1_ = ADC1_CHANNEL_MAX; |     this->channel_ = channel; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /// Set whether autoranging should be enabled for the ADC. | ||||||
|  |   /// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages. | ||||||
|  |   /// @param autorange Boolean indicating whether to enable autoranging. | ||||||
|   void set_autorange(bool autorange) { this->autorange_ = autorange; } |   void set_autorange(bool autorange) { this->autorange_ = autorange; } | ||||||
| #endif  // USE_ESP32 | #endif  // USE_ESP32 | ||||||
|  |  | ||||||
|   /// Update ADC values |  | ||||||
|   void update() override; |  | ||||||
|   /// Setup ADC |  | ||||||
|   void setup() override; |  | ||||||
|   void dump_config() override; |  | ||||||
|   /// `HARDWARE_LATE` setup priority |  | ||||||
|   float get_setup_priority() const override; |  | ||||||
|   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } |  | ||||||
|   void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } |  | ||||||
|   void set_sample_count(uint8_t sample_count); |  | ||||||
|   void set_sampling_mode(SamplingMode sampling_mode); |  | ||||||
|   float sample() override; |  | ||||||
|  |  | ||||||
| #ifdef USE_RP2040 | #ifdef USE_RP2040 | ||||||
|   void set_is_temperature() { this->is_temperature_ = true; } |   void set_is_temperature() { this->is_temperature_ = true; } | ||||||
| #endif  // USE_RP2040 | #endif  // USE_RP2040 | ||||||
| @@ -86,17 +128,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | |||||||
|   InternalGPIOPin *pin_; |   InternalGPIOPin *pin_; | ||||||
|   SamplingMode sampling_mode_{SamplingMode::AVG}; |   SamplingMode sampling_mode_{SamplingMode::AVG}; | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |   float sample_autorange_(); | ||||||
|  |   float sample_fixed_attenuation_(); | ||||||
|  |   bool autorange_{false}; | ||||||
|  |   adc_oneshot_unit_handle_t adc_handle_{nullptr}; | ||||||
|  |   adc_cali_handle_t calibration_handle_{nullptr}; | ||||||
|  |   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||||
|  |   adc_channel_t channel_; | ||||||
|  |   adc_unit_t adc_unit_; | ||||||
|  |   struct SetupFlags { | ||||||
|  |     uint8_t init_complete : 1; | ||||||
|  |     uint8_t config_complete : 1; | ||||||
|  |     uint8_t handle_init_complete : 1; | ||||||
|  |     uint8_t calibration_complete : 1; | ||||||
|  |     uint8_t reserved : 4; | ||||||
|  |   } setup_flags_{}; | ||||||
|  |   static adc_oneshot_unit_handle_t shared_adc_handles[2]; | ||||||
|  | #endif  // USE_ESP32 | ||||||
|  |  | ||||||
| #ifdef USE_RP2040 | #ifdef USE_RP2040 | ||||||
|   bool is_temperature_{false}; |   bool is_temperature_{false}; | ||||||
| #endif  // USE_RP2040 | #endif  // USE_RP2040 | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 |  | ||||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; |  | ||||||
|   adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; |  | ||||||
|   adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; |  | ||||||
|   bool autorange_{false}; |  | ||||||
|   esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; |  | ||||||
| #endif  // USE_ESP32 |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace adc | }  // namespace adc | ||||||
|   | |||||||
| @@ -8,145 +8,308 @@ namespace adc { | |||||||
|  |  | ||||||
| static const char *const TAG = "adc.esp32"; | static const char *const TAG = "adc.esp32"; | ||||||
|  |  | ||||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr}; | ||||||
|  |  | ||||||
| #ifndef SOC_ADC_RTC_MAX_BITWIDTH | const LogString *attenuation_to_str(adc_atten_t attenuation) { | ||||||
| #if USE_ESP32_VARIANT_ESP32S2 |   switch (attenuation) { | ||||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; |     case ADC_ATTEN_DB_0: | ||||||
| #else |       return LOG_STR("0 dB"); | ||||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; |     case ADC_ATTEN_DB_2_5: | ||||||
| #endif  // USE_ESP32_VARIANT_ESP32S2 |       return LOG_STR("2.5 dB"); | ||||||
| #endif  // SOC_ADC_RTC_MAX_BITWIDTH |     case ADC_ATTEN_DB_6: | ||||||
|  |       return LOG_STR("6 dB"); | ||||||
|  |     case ADC_ATTEN_DB_12_COMPAT: | ||||||
|  |       return LOG_STR("12 dB"); | ||||||
|  |     default: | ||||||
|  |       return LOG_STR("Unknown Attenuation"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; | const LogString *adc_unit_to_str(adc_unit_t unit) { | ||||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; |   switch (unit) { | ||||||
|  |     case ADC_UNIT_1: | ||||||
|  |       return LOG_STR("ADC1"); | ||||||
|  |     case ADC_UNIT_2: | ||||||
|  |       return LOG_STR("ADC2"); | ||||||
|  |     default: | ||||||
|  |       return LOG_STR("Unknown ADC Unit"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| void ADCSensor::setup() { | void ADCSensor::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); |   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||||
|  |   // Check if another sensor already initialized this ADC unit | ||||||
|  |   if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) { | ||||||
|  |     adc_oneshot_unit_init_cfg_t init_config = {};  // Zero initialize | ||||||
|  |     init_config.unit_id = this->adc_unit_; | ||||||
|  |     init_config.ulp_mode = ADC_ULP_MODE_DISABLE; | ||||||
|  | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|  |     init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; | ||||||
|  | #endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|  |     esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); | ||||||
|  |     if (err != ESP_OK) { | ||||||
|  |       ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_]; | ||||||
|  |  | ||||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { |   this->setup_flags_.handle_init_complete = true; | ||||||
|     adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); |  | ||||||
|     if (!this->autorange_) { |   adc_oneshot_chan_cfg_t config = { | ||||||
|       adc1_config_channel_atten(this->channel1_, this->attenuation_); |       .atten = this->attenuation_, | ||||||
|  |       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||||
|  |   }; | ||||||
|  |   esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); | ||||||
|  |   if (err != ESP_OK) { | ||||||
|  |     ESP_LOGE(TAG, "Error configuring channel: %d", err); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|   } |   } | ||||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { |   this->setup_flags_.config_complete = true; | ||||||
|     if (!this->autorange_) { |  | ||||||
|       adc2_config_channel_atten(this->channel2_, this->attenuation_); |   // Initialize ADC calibration | ||||||
|  |   if (this->calibration_handle_ == nullptr) { | ||||||
|  |     adc_cali_handle_t handle = nullptr; | ||||||
|  |     esp_err_t err; | ||||||
|  |  | ||||||
|  | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|  |     // RISC-V variants and S3 use curve fitting calibration | ||||||
|  |     adc_cali_curve_fitting_config_t cali_config = {};  // Zero initialize first | ||||||
|  | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||||
|  |     cali_config.chan = this->channel_; | ||||||
|  | #endif  // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||||
|  |     cali_config.unit_id = this->adc_unit_; | ||||||
|  |     cali_config.atten = this->attenuation_; | ||||||
|  |     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; | ||||||
|  |  | ||||||
|  |     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); | ||||||
|  |     if (err == ESP_OK) { | ||||||
|  |       this->calibration_handle_ = handle; | ||||||
|  |       this->setup_flags_.calibration_complete = true; | ||||||
|  |       ESP_LOGV(TAG, "Using curve fitting calibration"); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); | ||||||
|  |       this->setup_flags_.calibration_complete = false; | ||||||
|     } |     } | ||||||
|  | #else  // Other ESP32 variants use line fitting calibration | ||||||
|  |     adc_cali_line_fitting_config_t cali_config = { | ||||||
|  |       .unit_id = this->adc_unit_, | ||||||
|  |       .atten = this->attenuation_, | ||||||
|  |       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||||
|  | #if !defined(USE_ESP32_VARIANT_ESP32S2) | ||||||
|  |       .default_vref = 1100,  // Default reference voltage in mV | ||||||
|  | #endif  // !defined(USE_ESP32_VARIANT_ESP32S2) | ||||||
|  |     }; | ||||||
|  |     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); | ||||||
|  |     if (err == ESP_OK) { | ||||||
|  |       this->calibration_handle_ = handle; | ||||||
|  |       this->setup_flags_.calibration_complete = true; | ||||||
|  |       ESP_LOGV(TAG, "Using line fitting calibration"); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); | ||||||
|  |       this->setup_flags_.calibration_complete = false; | ||||||
|  |     } | ||||||
|  | #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2 | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { |   this->setup_flags_.init_complete = true; | ||||||
|     auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; |  | ||||||
|     auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, |  | ||||||
|                                               1100,  // default vref |  | ||||||
|                                               &this->cal_characteristics_[i]); |  | ||||||
|     switch (cal_value) { |  | ||||||
|       case ESP_ADC_CAL_VAL_EFUSE_VREF: |  | ||||||
|         ESP_LOGV(TAG, "Using eFuse Vref for calibration"); |  | ||||||
|         break; |  | ||||||
|       case ESP_ADC_CAL_VAL_EFUSE_TP: |  | ||||||
|         ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); |  | ||||||
|         break; |  | ||||||
|       case ESP_ADC_CAL_VAL_DEFAULT_VREF: |  | ||||||
|       default: |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void ADCSensor::dump_config() { | void ADCSensor::dump_config() { | ||||||
|   static const char *const ATTEN_AUTO_STR = "auto"; |  | ||||||
|   static const char *const ATTEN_0DB_STR = "0 db"; |  | ||||||
|   static const char *const ATTEN_2_5DB_STR = "2.5 db"; |  | ||||||
|   static const char *const ATTEN_6DB_STR = "6 db"; |  | ||||||
|   static const char *const ATTEN_12DB_STR = "12 db"; |  | ||||||
|   const char *atten_str = ATTEN_AUTO_STR; |  | ||||||
|  |  | ||||||
|   LOG_SENSOR("", "ADC Sensor", this); |   LOG_SENSOR("", "ADC Sensor", this); | ||||||
|   LOG_PIN("  Pin: ", this->pin_); |   LOG_PIN("  Pin: ", this->pin_); | ||||||
|  |  | ||||||
|   if (!this->autorange_) { |  | ||||||
|     switch (this->attenuation_) { |  | ||||||
|       case ADC_ATTEN_DB_0: |  | ||||||
|         atten_str = ATTEN_0DB_STR; |  | ||||||
|         break; |  | ||||||
|       case ADC_ATTEN_DB_2_5: |  | ||||||
|         atten_str = ATTEN_2_5DB_STR; |  | ||||||
|         break; |  | ||||||
|       case ADC_ATTEN_DB_6: |  | ||||||
|         atten_str = ATTEN_6DB_STR; |  | ||||||
|         break; |  | ||||||
|       case ADC_ATTEN_DB_12_COMPAT: |  | ||||||
|         atten_str = ATTEN_12DB_STR; |  | ||||||
|         break; |  | ||||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "  Channel:       %d\n" | ||||||
|  |                 "  Unit:          %s\n" | ||||||
|                 "  Attenuation:   %s\n" |                 "  Attenuation:   %s\n" | ||||||
|                 "  Samples:       %i\n" |                 "  Samples:       %i\n" | ||||||
|                 "  Sampling mode: %s", |                 "  Sampling mode: %s", | ||||||
|                 atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); |                 this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), | ||||||
|  |                 this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, | ||||||
|  |                 LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG( | ||||||
|  |       TAG, | ||||||
|  |       "  Setup Status:\n" | ||||||
|  |       "    Handle Init:  %s\n" | ||||||
|  |       "    Config:       %s\n" | ||||||
|  |       "    Calibration:  %s\n" | ||||||
|  |       "    Overall Init: %s", | ||||||
|  |       this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", | ||||||
|  |       this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); | ||||||
|  |  | ||||||
|   LOG_UPDATE_INTERVAL(this); |   LOG_UPDATE_INTERVAL(this); | ||||||
| } | } | ||||||
|  |  | ||||||
| float ADCSensor::sample() { | float ADCSensor::sample() { | ||||||
|   if (!this->autorange_) { |   if (this->autorange_) { | ||||||
|  |     return this->sample_autorange_(); | ||||||
|  |   } else { | ||||||
|  |     return this->sample_fixed_attenuation_(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float ADCSensor::sample_fixed_attenuation_() { | ||||||
|   auto aggr = Aggregator(this->sampling_mode_); |   auto aggr = Aggregator(this->sampling_mode_); | ||||||
|  |  | ||||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { |   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||||
|       int raw = -1; |     int raw; | ||||||
|       if (this->channel1_ != ADC1_CHANNEL_MAX) { |     esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||||
|         raw = adc1_get_raw(this->channel1_); |  | ||||||
|       } else if (this->channel2_ != ADC2_CHANNEL_MAX) { |     if (err != ESP_OK) { | ||||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); |       ESP_LOGW(TAG, "ADC read failed with error %d", err); | ||||||
|  |       continue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (raw == -1) { |     if (raw == -1) { | ||||||
|         return NAN; |       ESP_LOGW(TAG, "Invalid ADC reading"); | ||||||
|  |       continue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     aggr.add_sample(raw); |     aggr.add_sample(raw); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   uint32_t final_value = aggr.aggregate(); | ||||||
|  |  | ||||||
|   if (this->output_raw_) { |   if (this->output_raw_) { | ||||||
|       return aggr.aggregate(); |     return final_value; | ||||||
|     } |  | ||||||
|     uint32_t mv = |  | ||||||
|         esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); |  | ||||||
|     return mv / 1000.0f; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; |   if (this->calibration_handle_ != nullptr) { | ||||||
|  |     int voltage_mv; | ||||||
|  |     esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv); | ||||||
|  |     if (err == ESP_OK) { | ||||||
|  |       return voltage_mv / 1000.0f; | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); | ||||||
|  |       if (this->calibration_handle_ != nullptr) { | ||||||
|  | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|  |         adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||||
|  | #else   // Other ESP32 variants use line fitting calibration | ||||||
|  |         adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||||
|  | #endif  // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2 | ||||||
|  |         this->calibration_handle_ = nullptr; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { |   return final_value * 3.3f / 4095.0f; | ||||||
|     adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); |  | ||||||
|     raw12 = adc1_get_raw(this->channel1_); |  | ||||||
|     if (raw12 < ADC_MAX) { |  | ||||||
|       adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); |  | ||||||
|       raw6 = adc1_get_raw(this->channel1_); |  | ||||||
|       if (raw6 < ADC_MAX) { |  | ||||||
|         adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); |  | ||||||
|         raw2 = adc1_get_raw(this->channel1_); |  | ||||||
|         if (raw2 < ADC_MAX) { |  | ||||||
|           adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); |  | ||||||
|           raw0 = adc1_get_raw(this->channel1_); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | float ADCSensor::sample_autorange_() { | ||||||
|  |   // Auto-range mode | ||||||
|  |   auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> { | ||||||
|  |     // First reconfigure the attenuation for this reading | ||||||
|  |     adc_oneshot_chan_cfg_t config = { | ||||||
|  |         .atten = atten, | ||||||
|  |         .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); | ||||||
|  |  | ||||||
|  |     if (err != ESP_OK) { | ||||||
|  |       ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err); | ||||||
|  |       return {-1, 0.0f}; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Need to recalibrate for the new attenuation | ||||||
|  |     if (this->calibration_handle_ != nullptr) { | ||||||
|  |       // Delete old calibration handle | ||||||
|  | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|  |       adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); | ||||||
|  | #else | ||||||
|  |       adc_cali_delete_scheme_line_fitting(this->calibration_handle_); | ||||||
|  | #endif | ||||||
|  |       this->calibration_handle_ = nullptr; | ||||||
|     } |     } | ||||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { |  | ||||||
|     adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); |     // Create new calibration handle for this attenuation | ||||||
|     adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); |     adc_cali_handle_t handle = nullptr; | ||||||
|     if (raw12 < ADC_MAX) { |  | ||||||
|       adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|       adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); |     adc_cali_curve_fitting_config_t cali_config = {}; | ||||||
|       if (raw6 < ADC_MAX) { | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||||
|         adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); |     cali_config.chan = this->channel_; | ||||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); | #endif | ||||||
|         if (raw2 < ADC_MAX) { |     cali_config.unit_id = this->adc_unit_; | ||||||
|           adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); |     cali_config.atten = atten; | ||||||
|           adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); |     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; | ||||||
|  |  | ||||||
|  |     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); | ||||||
|  | #else | ||||||
|  |     adc_cali_line_fitting_config_t cali_config = { | ||||||
|  |       .unit_id = this->adc_unit_, | ||||||
|  |       .atten = atten, | ||||||
|  |       .bitwidth = ADC_BITWIDTH_DEFAULT, | ||||||
|  | #if !defined(USE_ESP32_VARIANT_ESP32S2) | ||||||
|  |       .default_vref = 1100, | ||||||
|  | #endif | ||||||
|  |     }; | ||||||
|  |     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |     int raw; | ||||||
|  |     err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||||
|  |  | ||||||
|  |     if (err != ESP_OK) { | ||||||
|  |       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); | ||||||
|  |       if (handle != nullptr) { | ||||||
|  | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|  |         adc_cali_delete_scheme_curve_fitting(handle); | ||||||
|  | #else | ||||||
|  |         adc_cali_delete_scheme_line_fitting(handle); | ||||||
|  | #endif | ||||||
|       } |       } | ||||||
|  |       return {-1, 0.0f}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     float voltage = 0.0f; | ||||||
|  |     if (handle != nullptr) { | ||||||
|  |       int voltage_mv; | ||||||
|  |       err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); | ||||||
|  |       if (err == ESP_OK) { | ||||||
|  |         voltage = voltage_mv / 1000.0f; | ||||||
|  |       } else { | ||||||
|  |         voltage = raw * 3.3f / 4095.0f; | ||||||
|  |       } | ||||||
|  |       // Clean up calibration handle | ||||||
|  | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||||
|  |       adc_cali_delete_scheme_curve_fitting(handle); | ||||||
|  | #else | ||||||
|  |       adc_cali_delete_scheme_line_fitting(handle); | ||||||
|  | #endif | ||||||
|  |     } else { | ||||||
|  |       voltage = raw * 3.3f / 4095.0f; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return {raw, voltage}; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12); | ||||||
|  |   if (raw12 == -1) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to read ADC in autorange mode"); | ||||||
|  |     return NAN; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   int raw6 = 4095, raw2 = 4095, raw0 = 4095; | ||||||
|  |   float mv6 = 0, mv2 = 0, mv0 = 0; | ||||||
|  |  | ||||||
|  |   if (raw12 < 4095) { | ||||||
|  |     auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6); | ||||||
|  |     raw6 = raw6_val; | ||||||
|  |     mv6 = mv6_val; | ||||||
|  |  | ||||||
|  |     if (raw6 < 4095 && raw6 != -1) { | ||||||
|  |       auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5); | ||||||
|  |       raw2 = raw2_val; | ||||||
|  |       mv2 = mv2_val; | ||||||
|  |  | ||||||
|  |       if (raw2 < 4095 && raw2 != -1) { | ||||||
|  |         auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0); | ||||||
|  |         raw0 = raw0_val; | ||||||
|  |         mv0 = mv0_val; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -155,19 +318,19 @@ float ADCSensor::sample() { | |||||||
|     return NAN; |     return NAN; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); |   const int adc_half = 2048; | ||||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); |   uint32_t c12 = std::min(raw12, adc_half); | ||||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); |   uint32_t c6 = adc_half - std::abs(raw6 - adc_half); | ||||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); |   uint32_t c2 = adc_half - std::abs(raw2 - adc_half); | ||||||
|  |   uint32_t c0 = std::min(4095 - raw0, adc_half); | ||||||
|   uint32_t c12 = std::min(raw12, ADC_HALF); |  | ||||||
|   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); |  | ||||||
|   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); |  | ||||||
|   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); |  | ||||||
|   uint32_t csum = c12 + c6 + c2 + c0; |   uint32_t csum = c12 + c6 + c2 + c0; | ||||||
|  |  | ||||||
|   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); |   if (csum == 0) { | ||||||
|   return mv_scaled / (float) (csum * 1000U); |     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); | ||||||
|  |     return NAN; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace adc | }  // namespace adc | ||||||
|   | |||||||
| @@ -10,13 +10,11 @@ from esphome.const import ( | |||||||
|     CONF_NUMBER, |     CONF_NUMBER, | ||||||
|     CONF_PIN, |     CONF_PIN, | ||||||
|     CONF_RAW, |     CONF_RAW, | ||||||
|     CONF_WIFI, |  | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| import esphome.final_validate as fv |  | ||||||
|  |  | ||||||
| from . import ( | from . import ( | ||||||
|     ATTENUATION_MODES, |     ATTENUATION_MODES, | ||||||
| @@ -24,6 +22,7 @@ from . import ( | |||||||
|     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, |     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, | ||||||
|     SAMPLING_MODES, |     SAMPLING_MODES, | ||||||
|     adc_ns, |     adc_ns, | ||||||
|  |     adc_unit_t, | ||||||
|     validate_adc_pin, |     validate_adc_pin, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -57,21 +56,6 @@ def validate_config(config): | |||||||
|     return config |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| def final_validate_config(config): |  | ||||||
|     if CORE.is_esp32: |  | ||||||
|         variant = get_esp32_variant() |  | ||||||
|         if ( |  | ||||||
|             CONF_WIFI in fv.full_config.get() |  | ||||||
|             and config[CONF_PIN][CONF_NUMBER] |  | ||||||
|             in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] |  | ||||||
|         ): |  | ||||||
|             raise cv.Invalid( |  | ||||||
|                 f"{variant} doesn't support ADC on this pin when Wi-Fi is configured" |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     return config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ADCSensor = adc_ns.class_( | ADCSensor = adc_ns.class_( | ||||||
|     "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler |     "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler | ||||||
| ) | ) | ||||||
| @@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     validate_config, |     validate_config, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| FINAL_VALIDATE_SCHEMA = final_validate_config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
| @@ -119,13 +101,13 @@ async def to_code(config): | |||||||
|     cg.add(var.set_sample_count(config[CONF_SAMPLES])) |     cg.add(var.set_sample_count(config[CONF_SAMPLES])) | ||||||
|     cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) |     cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) | ||||||
|  |  | ||||||
|  |     if CORE.is_esp32: | ||||||
|         if attenuation := config.get(CONF_ATTENUATION): |         if attenuation := config.get(CONF_ATTENUATION): | ||||||
|             if attenuation == "auto": |             if attenuation == "auto": | ||||||
|                 cg.add(var.set_autorange(cg.global_ns.true)) |                 cg.add(var.set_autorange(cg.global_ns.true)) | ||||||
|             else: |             else: | ||||||
|                 cg.add(var.set_attenuation(attenuation)) |                 cg.add(var.set_attenuation(attenuation)) | ||||||
|  |  | ||||||
|     if CORE.is_esp32: |  | ||||||
|         variant = get_esp32_variant() |         variant = get_esp32_variant() | ||||||
|         pin_num = config[CONF_PIN][CONF_NUMBER] |         pin_num = config[CONF_PIN][CONF_NUMBER] | ||||||
|         if ( |         if ( | ||||||
| @@ -133,10 +115,10 @@ async def to_code(config): | |||||||
|             and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] |             and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] | ||||||
|         ): |         ): | ||||||
|             chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] |             chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] | ||||||
|             cg.add(var.set_channel1(chan)) |             cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan)) | ||||||
|         elif ( |         elif ( | ||||||
|             variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL |             variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL | ||||||
|             and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] |             and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] | ||||||
|         ): |         ): | ||||||
|             chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] |             chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] | ||||||
|             cg.add(var.set_channel2(chan)) |             cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan)) | ||||||
|   | |||||||
| @@ -16,6 +16,8 @@ class UserServiceDescriptor { | |||||||
|   virtual ListEntitiesServicesResponse encode_list_service_response() = 0; |   virtual ListEntitiesServicesResponse encode_list_service_response() = 0; | ||||||
|  |  | ||||||
|   virtual bool execute_service(const ExecuteServiceRequest &req) = 0; |   virtual bool execute_service(const ExecuteServiceRequest &req) = 0; | ||||||
|  |  | ||||||
|  |   bool is_internal() { return false; } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg); | template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg); | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ | |||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/components/as3935/as3935.h" | #include "esphome/components/as3935/as3935.h" | ||||||
| #include "esphome/components/spi/spi.h" | #include "esphome/components/spi/spi.h" | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/components/binary_sensor/binary_sensor.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace as3935_spi { | namespace as3935_spi { | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     if CORE.is_esp32 or CORE.is_libretiny: |     if CORE.is_esp32 or CORE.is_libretiny: | ||||||
|         # https://github.com/ESP32Async/AsyncTCP |         # https://github.com/ESP32Async/AsyncTCP | ||||||
|         cg.add_library("ESP32Async/AsyncTCP", "3.4.4") |         cg.add_library("ESP32Async/AsyncTCP", "3.4.5") | ||||||
|     elif CORE.is_esp8266: |     elif CORE.is_esp8266: | ||||||
|         # https://github.com/ESP32Async/ESPAsyncTCP |         # https://github.com/ESP32Async/ESPAsyncTCP | ||||||
|         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") |         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
|  |  | ||||||
| CONF_BYTE_ORDER = "byte_order" | CONF_BYTE_ORDER = "byte_order" | ||||||
|  | CONF_COLOR_DEPTH = "color_depth" | ||||||
| CONF_DRAW_ROUNDING = "draw_rounding" | CONF_DRAW_ROUNDING = "draw_rounding" | ||||||
| CONF_ON_STATE_CHANGE = "on_state_change" | CONF_ON_STATE_CHANGE = "on_state_change" | ||||||
| CONF_REQUEST_HEADERS = "request_headers" | CONF_REQUEST_HEADERS = "request_headers" | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ adjusted_ids = set() | |||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.ensure_list( |     cv.ensure_list( | ||||||
|  |         cv.COMPONENT_SCHEMA.extend( | ||||||
|             { |             { | ||||||
|                 cv.GenerateID(): cv.declare_id(EspLdo), |                 cv.GenerateID(): cv.declare_id(EspLdo), | ||||||
|                 cv.Required(CONF_VOLTAGE): cv.All( |                 cv.Required(CONF_VOLTAGE): cv.All( | ||||||
| @@ -28,6 +29,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), |                 cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), | ||||||
|                 cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, |                 cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, | ||||||
|             } |             } | ||||||
|  |         ) | ||||||
|     ), |     ), | ||||||
|     cv.only_with_esp_idf, |     cv.only_with_esp_idf, | ||||||
|     only_on_variant(supported=[VARIANT_ESP32P4]), |     only_on_variant(supported=[VARIANT_ESP32P4]), | ||||||
|   | |||||||
| @@ -17,6 +17,9 @@ class EspLdo : public Component { | |||||||
|   void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } |   void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } | ||||||
|   void set_voltage(float voltage) { this->voltage_ = voltage; } |   void set_voltage(float voltage) { this->voltage_ = voltage; } | ||||||
|   void adjust_voltage(float voltage); |   void adjust_voltage(float voltage); | ||||||
|  |   float get_setup_priority() const override { | ||||||
|  |     return setup_priority::BUS;  // LDO setup should be done early | ||||||
|  |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   int channel_; |   int channel_; | ||||||
|   | |||||||
| @@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() { | |||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
| void Fan::save_state_() { | void Fan::save_state_() { | ||||||
|  |   if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   FanRestoreState state{}; |   FanRestoreState state{}; | ||||||
|   state.state = this->state; |   state.state = this->state; | ||||||
|   state.oscillating = this->oscillating; |   state.oscillating = this->oscillating; | ||||||
|   | |||||||
| @@ -1,11 +1,16 @@ | |||||||
|  | import logging | ||||||
|  |  | ||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components import binary_sensor | from esphome.components import binary_sensor | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import CONF_PIN | from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN | ||||||
|  | from esphome.core import CORE | ||||||
|  |  | ||||||
| from .. import gpio_ns | from .. import gpio_ns | ||||||
|  |  | ||||||
|  | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| GPIOBinarySensor = gpio_ns.class_( | GPIOBinarySensor = gpio_ns.class_( | ||||||
|     "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component |     "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||||
| ) | ) | ||||||
| @@ -41,6 +46,22 @@ async def to_code(config): | |||||||
|     pin = await cg.gpio_pin_expression(config[CONF_PIN]) |     pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||||
|     cg.add(var.set_pin(pin)) |     cg.add(var.set_pin(pin)) | ||||||
|  |  | ||||||
|     cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) |     # Check for ESP8266 GPIO16 interrupt limitation | ||||||
|     if config[CONF_USE_INTERRUPT]: |     # GPIO16 on ESP8266 is a special pin that doesn't support interrupts through | ||||||
|  |     # the Arduino attachInterrupt() function. This is the only known GPIO pin | ||||||
|  |     # across all supported platforms that has this limitation, so we handle it | ||||||
|  |     # here instead of in the platform-specific code. | ||||||
|  |     use_interrupt = config[CONF_USE_INTERRUPT] | ||||||
|  |     if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16: | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. " | ||||||
|  |             "Falling back to polling mode (same as in ESPHome <2025.7). " | ||||||
|  |             "The sensor will work exactly as before, but other pins have better " | ||||||
|  |             "performance with interrupts.", | ||||||
|  |             config.get(CONF_NAME, config[CONF_ID]), | ||||||
|  |         ) | ||||||
|  |         use_interrupt = False | ||||||
|  |  | ||||||
|  |     cg.add(var.set_use_interrupt(use_interrupt)) | ||||||
|  |     if use_interrupt: | ||||||
|         cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) |         cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) | ||||||
|   | |||||||
| @@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) { | |||||||
|     container.reset();  // Release ownership of the container's shared_ptr |     container.reset();  // Release ownership of the container's shared_ptr | ||||||
|  |  | ||||||
|     valid = json::parse_json(response, [this_update](JsonObject root) -> bool { |     valid = json::parse_json(response, [this_update](JsonObject root) -> bool { | ||||||
|       if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { |       if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) { | ||||||
|         ESP_LOGE(TAG, "Manifest does not contain required fields"); |         ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
| @@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) { | |||||||
|       this_update->update_info_.latest_version = root["version"].as<std::string>(); |       this_update->update_info_.latest_version = root["version"].as<std::string>(); | ||||||
|  |  | ||||||
|       for (auto build : root["builds"].as<JsonArray>()) { |       for (auto build : root["builds"].as<JsonArray>()) { | ||||||
|         if (!build.containsKey("chipFamily")) { |         if (!build["chipFamily"].is<const char *>()) { | ||||||
|           ESP_LOGE(TAG, "Manifest does not contain required fields"); |           ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|           return false; |           return false; | ||||||
|         } |         } | ||||||
|         if (build["chipFamily"] == ESPHOME_VARIANT) { |         if (build["chipFamily"] == ESPHOME_VARIANT) { | ||||||
|           if (!build.containsKey("ota")) { |           if (!build["ota"].is<JsonObject>()) { | ||||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); |             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
|           auto ota = build["ota"]; |           JsonObject ota = build["ota"].as<JsonObject>(); | ||||||
|           if (!ota.containsKey("path") || !ota.containsKey("md5")) { |           if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) { | ||||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); |             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
|           this_update->update_info_.firmware_url = ota["path"].as<std::string>(); |           this_update->update_info_.firmware_url = ota["path"].as<std::string>(); | ||||||
|           this_update->update_info_.md5 = ota["md5"].as<std::string>(); |           this_update->update_info_.md5 = ota["md5"].as<std::string>(); | ||||||
|  |  | ||||||
|           if (ota.containsKey("summary")) |           if (ota["summary"].is<const char *>()) | ||||||
|             this_update->update_info_.summary = ota["summary"].as<std::string>(); |             this_update->update_info_.summary = ota["summary"].as<std::string>(); | ||||||
|           if (ota.containsKey("release_url")) |           if (ota["release_url"].is<const char *>()) | ||||||
|             this_update->update_info_.release_url = ota["release_url"].as<std::string>(); |             this_update->update_info_.release_url = ota["release_url"].as<std::string>(); | ||||||
|  |  | ||||||
|           return true; |           return true; | ||||||
|   | |||||||
| @@ -180,7 +180,7 @@ async def to_code(config): | |||||||
|     await speaker.register_speaker(var, config) |     await speaker.register_speaker(var, config) | ||||||
|  |  | ||||||
|     if config[CONF_DAC_TYPE] == "internal": |     if config[CONF_DAC_TYPE] == "internal": | ||||||
|         cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) |         cg.add(var.set_internal_dac_mode(config[CONF_MODE])) | ||||||
|     else: |     else: | ||||||
|         cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) |         cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) | ||||||
|         if use_legacy(): |         if use_legacy(): | ||||||
|   | |||||||
| @@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|  |  | ||||||
| @coroutine_with_priority(1.0) | @coroutine_with_priority(1.0) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_library("bblanchon/ArduinoJson", "6.18.5") |     cg.add_library("bblanchon/ArduinoJson", "7.4.2") | ||||||
|     cg.add_define("USE_JSON") |     cg.add_define("USE_JSON") | ||||||
|     cg.add_global(json_ns.using) |     cg.add_global(json_ns.using) | ||||||
|   | |||||||
| @@ -1,83 +1,76 @@ | |||||||
| #include "json_util.h" | #include "json_util.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | // ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace json { | namespace json { | ||||||
|  |  | ||||||
| static const char *const TAG = "json"; | static const char *const TAG = "json"; | ||||||
|  |  | ||||||
| static std::vector<char> global_json_build_buffer;  // NOLINT | // Build an allocator for the JSON Library using the RAMAllocator class | ||||||
| static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL); | struct SpiRamAllocator : ArduinoJson::Allocator { | ||||||
|  |   void *allocate(size_t size) override { return this->allocator_.allocate(size); } | ||||||
|  |  | ||||||
|  |   void deallocate(void *pointer) override { | ||||||
|  |     // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. | ||||||
|  |     // RAMAllocator::deallocate() requires the size, which we don't have access to here. | ||||||
|  |     // RAMAllocator::deallocate implementation just calls free() regardless of whether | ||||||
|  |     // the memory was allocated with heap_caps_malloc or malloc. | ||||||
|  |     // This is safe because ESP-IDF's heap implementation internally tracks the memory region | ||||||
|  |     // and routes free() to the appropriate heap. | ||||||
|  |     free(pointer);  // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void *reallocate(void *ptr, size_t new_size) override { | ||||||
|  |     return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)}; | ||||||
|  | }; | ||||||
|  |  | ||||||
| std::string build_json(const json_build_t &f) { | std::string build_json(const json_build_t &f) { | ||||||
|   // Here we are allocating up to 5kb of memory, |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   // with the heap size minus 2kb to be safe if less than 5kb |   auto doc_allocator = SpiRamAllocator(); | ||||||
|   // as we can not have a true dynamic sized document. |   JsonDocument json_document(&doc_allocator); | ||||||
|   // The excess memory is freed below with `shrinkToFit()` |   if (json_document.overflowed()) { | ||||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); |     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||||
|   size_t request_size = std::min(free_heap, (size_t) 512); |  | ||||||
|   while (true) { |  | ||||||
|     ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); |  | ||||||
|     DynamicJsonDocument json_document(request_size); |  | ||||||
|     if (json_document.capacity() == 0) { |  | ||||||
|       ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes", |  | ||||||
|                request_size, free_heap); |  | ||||||
|     return "{}"; |     return "{}"; | ||||||
|   } |   } | ||||||
|   JsonObject root = json_document.to<JsonObject>(); |   JsonObject root = json_document.to<JsonObject>(); | ||||||
|   f(root); |   f(root); | ||||||
|   if (json_document.overflowed()) { |   if (json_document.overflowed()) { | ||||||
|       if (request_size == free_heap) { |     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||||
|         ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes", |  | ||||||
|                  free_heap); |  | ||||||
|     return "{}"; |     return "{}"; | ||||||
|   } |   } | ||||||
|       request_size = std::min(request_size * 2, free_heap); |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|     json_document.shrinkToFit(); |  | ||||||
|     ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); |  | ||||||
|   std::string output; |   std::string output; | ||||||
|   serializeJson(json_document, output); |   serializeJson(json_document, output); | ||||||
|   return output; |   return output; | ||||||
|   } |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
|  |  | ||||||
| bool parse_json(const std::string &data, const json_parse_t &f) { | bool parse_json(const std::string &data, const json_parse_t &f) { | ||||||
|   // Here we are allocating 1.5 times the data size, |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   // with the heap size minus 2kb to be safe if less than that |   auto doc_allocator = SpiRamAllocator(); | ||||||
|   // as we can not have a true dynamic sized document. |   JsonDocument json_document(&doc_allocator); | ||||||
|   // The excess memory is freed below with `shrinkToFit()` |   if (json_document.overflowed()) { | ||||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); |     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||||
|   size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); |  | ||||||
|   while (true) { |  | ||||||
|     DynamicJsonDocument json_document(request_size); |  | ||||||
|     if (json_document.capacity() == 0) { |  | ||||||
|       ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size, |  | ||||||
|                free_heap); |  | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   DeserializationError err = deserializeJson(json_document, data); |   DeserializationError err = deserializeJson(json_document, data); | ||||||
|     json_document.shrinkToFit(); |  | ||||||
|  |  | ||||||
|   JsonObject root = json_document.as<JsonObject>(); |   JsonObject root = json_document.as<JsonObject>(); | ||||||
|  |  | ||||||
|   if (err == DeserializationError::Ok) { |   if (err == DeserializationError::Ok) { | ||||||
|     return f(root); |     return f(root); | ||||||
|   } else if (err == DeserializationError::NoMemory) { |   } else if (err == DeserializationError::NoMemory) { | ||||||
|       if (request_size * 2 >= free_heap) { |  | ||||||
|     ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); |     ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|       ESP_LOGV(TAG, "Increasing memory allocation."); |  | ||||||
|       request_size *= 2; |  | ||||||
|       continue; |  | ||||||
|     } else { |  | ||||||
|   ESP_LOGE(TAG, "Parse error: %s", err.c_str()); |   ESP_LOGE(TAG, "Parse error: %s", err.c_str()); | ||||||
|   return false; |   return false; | ||||||
|     } |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
|   }; |  | ||||||
|   return false; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace json | }  // namespace json | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ namespace light { | |||||||
| // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema | // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema | ||||||
|  |  | ||||||
| void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (state.supports_effects()) |   if (state.supports_effects()) | ||||||
|     root["effect"] = state.get_effect_name(); |     root["effect"] = state.get_effect_name(); | ||||||
|  |  | ||||||
| @@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | |||||||
|   if (values.get_color_mode() & ColorCapability::BRIGHTNESS) |   if (values.get_color_mode() & ColorCapability::BRIGHTNESS) | ||||||
|     root["brightness"] = uint8_t(values.get_brightness() * 255); |     root["brightness"] = uint8_t(values.get_brightness() * 255); | ||||||
|  |  | ||||||
|   JsonObject color = root.createNestedObject("color"); |   JsonObject color = root["color"].to<JsonObject>(); | ||||||
|   if (values.get_color_mode() & ColorCapability::RGB) { |   if (values.get_color_mode() & ColorCapability::RGB) { | ||||||
|     color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); |     color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); | ||||||
|     color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); |     color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); | ||||||
| @@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { | void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { | ||||||
|   if (root.containsKey("state")) { |   if (root["state"].is<const char *>()) { | ||||||
|     auto val = parse_on_off(root["state"]); |     auto val = parse_on_off(root["state"]); | ||||||
|     switch (val) { |     switch (val) { | ||||||
|       case PARSE_ON: |       case PARSE_ON: | ||||||
| @@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("brightness")) { |   if (root["brightness"].is<uint8_t>()) { | ||||||
|     call.set_brightness(float(root["brightness"]) / 255.0f); |     call.set_brightness(float(root["brightness"]) / 255.0f); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("color")) { |   if (root["color"].is<JsonObject>()) { | ||||||
|     JsonObject color = root["color"]; |     JsonObject color = root["color"]; | ||||||
|     // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. |     // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. | ||||||
|     float max_rgb = 0.0f; |     float max_rgb = 0.0f; | ||||||
|     if (color.containsKey("r")) { |     if (color["r"].is<uint8_t>()) { | ||||||
|       float r = float(color["r"]) / 255.0f; |       float r = float(color["r"]) / 255.0f; | ||||||
|       max_rgb = fmaxf(max_rgb, r); |       max_rgb = fmaxf(max_rgb, r); | ||||||
|       call.set_red(r); |       call.set_red(r); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("g")) { |     if (color["g"].is<uint8_t>()) { | ||||||
|       float g = float(color["g"]) / 255.0f; |       float g = float(color["g"]) / 255.0f; | ||||||
|       max_rgb = fmaxf(max_rgb, g); |       max_rgb = fmaxf(max_rgb, g); | ||||||
|       call.set_green(g); |       call.set_green(g); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("b")) { |     if (color["b"].is<uint8_t>()) { | ||||||
|       float b = float(color["b"]) / 255.0f; |       float b = float(color["b"]) / 255.0f; | ||||||
|       max_rgb = fmaxf(max_rgb, b); |       max_rgb = fmaxf(max_rgb, b); | ||||||
|       call.set_blue(b); |       call.set_blue(b); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { |     if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) { | ||||||
|       call.set_color_brightness(max_rgb); |       call.set_color_brightness(max_rgb); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (color.containsKey("c")) { |     if (color["c"].is<uint8_t>()) { | ||||||
|       call.set_cold_white(float(color["c"]) / 255.0f); |       call.set_cold_white(float(color["c"]) / 255.0f); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("w")) { |     if (color["w"].is<uint8_t>()) { | ||||||
|       // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm |       // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm | ||||||
|       // white channel in RGBWW. |       // white channel in RGBWW. | ||||||
|       if (color.containsKey("c")) { |       if (color["c"].is<uint8_t>()) { | ||||||
|         call.set_warm_white(float(color["w"]) / 255.0f); |         call.set_warm_white(float(color["w"]) / 255.0f); | ||||||
|       } else { |       } else { | ||||||
|         call.set_white(float(color["w"]) / 255.0f); |         call.set_white(float(color["w"]) / 255.0f); | ||||||
| @@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("white_value")) {  // legacy API |   if (root["white_value"].is<uint8_t>()) {  // legacy API | ||||||
|     call.set_white(float(root["white_value"]) / 255.0f); |     call.set_white(float(root["white_value"]) / 255.0f); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("color_temp")) { |   if (root["color_temp"].is<uint16_t>()) { | ||||||
|     call.set_color_temperature(float(root["color_temp"])); |     call.set_color_temperature(float(root["color_temp"])); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | |||||||
| void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { | void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { | ||||||
|   LightJSONSchema::parse_color_json(state, call, root); |   LightJSONSchema::parse_color_json(state, call, root); | ||||||
|  |  | ||||||
|   if (root.containsKey("flash")) { |   if (root["flash"].is<uint32_t>()) { | ||||||
|     auto length = uint32_t(float(root["flash"]) * 1000); |     auto length = uint32_t(float(root["flash"]) * 1000); | ||||||
|     call.set_flash_length(length); |     call.set_flash_length(length); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("transition")) { |   if (root["transition"].is<uint16_t>()) { | ||||||
|     auto length = uint32_t(float(root["transition"]) * 1000); |     auto length = uint32_t(float(root["transition"]) * 1000); | ||||||
|     call.set_transition_length(length); |     call.set_transition_length(length); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("effect")) { |   if (root["effect"].is<const char *>()) { | ||||||
|     const char *effect = root["effect"]; |     const char *effect = root["effect"]; | ||||||
|     call.set_effect(effect); |     call.set_effect(effect); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -21,6 +21,11 @@ from esphome.components.libretiny.const import ( | |||||||
|     COMPONENT_LN882X, |     COMPONENT_LN882X, | ||||||
|     COMPONENT_RTL87XX, |     COMPONENT_RTL87XX, | ||||||
| ) | ) | ||||||
|  | from esphome.components.zephyr import ( | ||||||
|  |     zephyr_add_cdc_acm, | ||||||
|  |     zephyr_add_overlay, | ||||||
|  |     zephyr_add_prj_conf, | ||||||
|  | ) | ||||||
| from esphome.config_helpers import filter_source_files_from_platform | from esphome.config_helpers import filter_source_files_from_platform | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -41,6 +46,7 @@ from esphome.const import ( | |||||||
|     PLATFORM_ESP32, |     PLATFORM_ESP32, | ||||||
|     PLATFORM_ESP8266, |     PLATFORM_ESP8266, | ||||||
|     PLATFORM_LN882X, |     PLATFORM_LN882X, | ||||||
|  |     PLATFORM_NRF52, | ||||||
|     PLATFORM_RP2040, |     PLATFORM_RP2040, | ||||||
|     PLATFORM_RTL87XX, |     PLATFORM_RTL87XX, | ||||||
|     PlatformFramework, |     PlatformFramework, | ||||||
| @@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] | |||||||
|  |  | ||||||
| UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] | UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] | ||||||
|  |  | ||||||
|  | UART_SELECTION_NRF52 = [USB_CDC, UART0] | ||||||
|  |  | ||||||
| HARDWARE_UART_TO_UART_SELECTION = { | HARDWARE_UART_TO_UART_SELECTION = { | ||||||
|     UART0: logger_ns.UART_SELECTION_UART0, |     UART0: logger_ns.UART_SELECTION_UART0, | ||||||
|     UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, |     UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, | ||||||
| @@ -167,6 +175,8 @@ def uart_selection(value): | |||||||
|             return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) |             return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) | ||||||
|     if CORE.is_host: |     if CORE.is_host: | ||||||
|         raise cv.Invalid("Uart selection not valid for host platform") |         raise cv.Invalid("Uart selection not valid for host platform") | ||||||
|  |     if CORE.is_nrf52: | ||||||
|  |         return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value) | ||||||
|     raise NotImplementedError |     raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_( | |||||||
|     automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), |     automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" | CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
| @@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 bk72xx=DEFAULT, |                 bk72xx=DEFAULT, | ||||||
|                 ln882x=DEFAULT, |                 ln882x=DEFAULT, | ||||||
|                 rtl87xx=DEFAULT, |                 rtl87xx=DEFAULT, | ||||||
|  |                 nrf52=USB_CDC, | ||||||
|             ): cv.All( |             ): cv.All( | ||||||
|                 cv.only_on( |                 cv.only_on( | ||||||
|                     [ |                     [ | ||||||
| @@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                         PLATFORM_BK72XX, |                         PLATFORM_BK72XX, | ||||||
|                         PLATFORM_LN882X, |                         PLATFORM_LN882X, | ||||||
|                         PLATFORM_RTL87XX, |                         PLATFORM_RTL87XX, | ||||||
|  |                         PLATFORM_NRF52, | ||||||
|                     ] |                     ] | ||||||
|                 ), |                 ), | ||||||
|                 uart_selection, |                 uart_selection, | ||||||
| @@ -358,6 +371,15 @@ async def to_code(config): | |||||||
|     except cv.Invalid: |     except cv.Invalid: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |     if CORE.using_zephyr: | ||||||
|  |         if config[CONF_HARDWARE_UART] == UART0: | ||||||
|  |             zephyr_add_overlay("""&uart0 { status = "okay";};""") | ||||||
|  |         if config[CONF_HARDWARE_UART] == UART1: | ||||||
|  |             zephyr_add_overlay("""&uart1 { status = "okay";};""") | ||||||
|  |         if config[CONF_HARDWARE_UART] == USB_CDC: | ||||||
|  |             zephyr_add_prj_conf("UART_LINE_CTRL", True) | ||||||
|  |             zephyr_add_cdc_acm(config, 0) | ||||||
|  |  | ||||||
|     # Register at end for safe mode |     # Register at end for safe mode | ||||||
|     await cg.register_component(log, config) |     await cg.register_component(log, config) | ||||||
|  |  | ||||||
| @@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( | |||||||
|             PlatformFramework.RTL87XX_ARDUINO, |             PlatformFramework.RTL87XX_ARDUINO, | ||||||
|             PlatformFramework.LN882X_ARDUINO, |             PlatformFramework.LN882X_ARDUINO, | ||||||
|         }, |         }, | ||||||
|  |         "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, | ||||||
|         "task_log_buffer.cpp": { |         "task_log_buffer.cpp": { | ||||||
|             PlatformFramework.ESP32_ARDUINO, |             PlatformFramework.ESP32_ARDUINO, | ||||||
|             PlatformFramework.ESP32_IDF, |             PlatformFramework.ESP32_IDF, | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ | |||||||
| #include <memory>  // For unique_ptr | #include <memory>  // For unique_ptr | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/application.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace logger { | namespace logger { | ||||||
| @@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate | |||||||
|   this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];  // NOLINT |   this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];  // NOLINT | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||||
|   this->main_task_ = xTaskGetCurrentTaskHandle(); |   this->main_task_ = xTaskGetCurrentTaskHandle(); | ||||||
|  | #elif defined(USE_ZEPHYR) | ||||||
|  |   this->main_task_ = k_current_get(); | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||||
| @@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) { | |||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifndef USE_ZEPHYR | ||||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | ||||||
| void Logger::loop() { | void Logger::loop() { | ||||||
| #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | ||||||
| @@ -185,8 +188,13 @@ void Logger::loop() { | |||||||
|     } |     } | ||||||
|     opened = !opened; |     opened = !opened; | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
|  |   this->process_messages_(); | ||||||
|  | } | ||||||
|  | #endif | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | void Logger::process_messages_() { | ||||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||||
|   // Process any buffered messages when available |   // Process any buffered messages when available | ||||||
|   if (this->log_buffer_->has_messages()) { |   if (this->log_buffer_->has_messages()) { | ||||||
| @@ -227,12 +235,11 @@ void Logger::loop() { | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
| #endif |  | ||||||
|  |  | ||||||
| void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } | void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } | ||||||
| void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } | void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
| UARTSelection Logger::get_uart() const { return this->uart_; } | UARTSelection Logger::get_uart() const { return this->uart_; } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -29,6 +29,11 @@ | |||||||
| #include <driver/uart.h> | #include <driver/uart.h> | ||||||
| #endif  // USE_ESP_IDF | #endif  // USE_ESP_IDF | ||||||
|  |  | ||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  | #include <zephyr/kernel.h> | ||||||
|  | struct device; | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| namespace logger { | namespace logger { | ||||||
| @@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { | |||||||
|     "VV",  // VERY_VERBOSE |     "VV",  // VERY_VERBOSE | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
| /** Enum for logging UART selection | /** Enum for logging UART selection | ||||||
|  * |  * | ||||||
|  * Advanced configuration (pin selection, etc) is not supported. |  * Advanced configuration (pin selection, etc) is not supported. | ||||||
| @@ -82,7 +87,7 @@ enum UARTSelection : uint8_t { | |||||||
|   UART_SELECTION_UART0_SWAP, |   UART_SELECTION_UART0_SWAP, | ||||||
| #endif  // USE_ESP8266 | #endif  // USE_ESP8266 | ||||||
| }; | }; | ||||||
| #endif  // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY | #endif  // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @brief Logger component for all ESPHome logging. |  * @brief Logger component for all ESPHome logging. | ||||||
| @@ -107,7 +112,7 @@ class Logger : public Component { | |||||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||||
|   void init_log_buffer(size_t total_buffer_size); |   void init_log_buffer(size_t total_buffer_size); | ||||||
| #endif | #endif | ||||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR) | ||||||
|   void loop() override; |   void loop() override; | ||||||
| #endif | #endif | ||||||
|   /// Manually set the baud rate for serial, set to 0 to disable. |   /// Manually set the baud rate for serial, set to 0 to disable. | ||||||
| @@ -122,7 +127,7 @@ class Logger : public Component { | |||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|   void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } |   void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } | ||||||
| #endif | #endif | ||||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
|   void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } |   void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } | ||||||
|   /// Get the UART used by the logger. |   /// Get the UART used by the logger. | ||||||
|   UARTSelection get_uart() const; |   UARTSelection get_uart() const; | ||||||
| @@ -157,6 +162,7 @@ class Logger : public Component { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   void process_messages_(); | ||||||
|   void write_msg_(const char *msg); |   void write_msg_(const char *msg); | ||||||
|  |  | ||||||
|   // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator |   // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator | ||||||
| @@ -164,7 +170,7 @@ class Logger : public Component { | |||||||
|   inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, |   inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, | ||||||
|                                                         va_list args, char *buffer, uint16_t *buffer_at, |                                                         va_list args, char *buffer, uint16_t *buffer_at, | ||||||
|                                                         uint16_t buffer_size) { |                                                         uint16_t buffer_size) { | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
|     this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); |     this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); | ||||||
| #else | #else | ||||||
|     this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); |     this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); | ||||||
| @@ -231,7 +237,10 @@ class Logger : public Component { | |||||||
| #ifdef USE_ARDUINO | #ifdef USE_ARDUINO | ||||||
|   Stream *hw_serial_{nullptr}; |   Stream *hw_serial_{nullptr}; | ||||||
| #endif | #endif | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ZEPHYR) | ||||||
|  |   const device *uart_dev_{nullptr}; | ||||||
|  | #endif | ||||||
|  | #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
|   void *main_task_ = nullptr;  // Only used for thread name identification |   void *main_task_ = nullptr;  // Only used for thread name identification | ||||||
| #endif | #endif | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| @@ -256,7 +265,7 @@ class Logger : public Component { | |||||||
|   uint16_t tx_buffer_at_{0}; |   uint16_t tx_buffer_at_{0}; | ||||||
|   uint16_t tx_buffer_size_{0}; |   uint16_t tx_buffer_size_{0}; | ||||||
|   uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; |   uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; | ||||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) | #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||||
|   UARTSelection uart_{UART_SELECTION_UART0}; |   UARTSelection uart_{UART_SELECTION_UART0}; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_LIBRETINY | #ifdef USE_LIBRETINY | ||||||
| @@ -268,9 +277,13 @@ class Logger : public Component { | |||||||
|   bool global_recursion_guard_{false};  // Simple global recursion guard for single-task platforms |   bool global_recursion_guard_{false};  // Simple global recursion guard for single-task platforms | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
|   const char *HOT get_thread_name_() { |   const char *HOT get_thread_name_() { | ||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  |     k_tid_t current_task = k_current_get(); | ||||||
|  | #else | ||||||
|     TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); |     TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); | ||||||
|  | #endif | ||||||
|     if (current_task == main_task_) { |     if (current_task == main_task_) { | ||||||
|       return nullptr;  // Main task |       return nullptr;  // Main task | ||||||
|     } else { |     } else { | ||||||
| @@ -278,6 +291,8 @@ class Logger : public Component { | |||||||
|       return pcTaskGetName(current_task); |       return pcTaskGetName(current_task); | ||||||
| #elif defined(USE_LIBRETINY) | #elif defined(USE_LIBRETINY) | ||||||
|       return pcTaskGetTaskName(current_task); |       return pcTaskGetTaskName(current_task); | ||||||
|  | #elif defined(USE_ZEPHYR) | ||||||
|  |       return k_thread_name_get(current_task); | ||||||
| #endif | #endif | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -319,7 +334,7 @@ class Logger : public Component { | |||||||
|     const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; |     const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; | ||||||
|     const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; |     const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||||
|     if (thread_name != nullptr) { |     if (thread_name != nullptr) { | ||||||
|       // Non-main task with thread name |       // Non-main task with thread name | ||||||
|       this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, |       this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, | ||||||
|   | |||||||
							
								
								
									
										88
									
								
								esphome/components/logger/logger_zephyr.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								esphome/components/logger/logger_zephyr.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  |  | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "logger.h" | ||||||
|  |  | ||||||
|  | #include <zephyr/device.h> | ||||||
|  | #include <zephyr/drivers/uart.h> | ||||||
|  | #include <zephyr/usb/usb_device.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace logger { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "logger"; | ||||||
|  |  | ||||||
|  | void Logger::loop() { | ||||||
|  | #ifdef USE_LOGGER_USB_CDC | ||||||
|  |   if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   static bool opened = false; | ||||||
|  |   uint32_t dtr = 0; | ||||||
|  |   uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); | ||||||
|  |  | ||||||
|  |   /* Poll if the DTR flag was set, optional */ | ||||||
|  |   if (opened == dtr) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!opened) { | ||||||
|  |     App.schedule_dump_config(); | ||||||
|  |   } | ||||||
|  |   opened = !opened; | ||||||
|  | #endif | ||||||
|  |   this->process_messages_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Logger::pre_setup() { | ||||||
|  |   if (this->baud_rate_ > 0) { | ||||||
|  |     static const struct device *uart_dev = nullptr; | ||||||
|  |     switch (this->uart_) { | ||||||
|  |       case UART_SELECTION_UART0: | ||||||
|  |         uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0)); | ||||||
|  |         break; | ||||||
|  |       case UART_SELECTION_UART1: | ||||||
|  |         uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1)); | ||||||
|  |         break; | ||||||
|  | #ifdef USE_LOGGER_USB_CDC | ||||||
|  |       case UART_SELECTION_USB_CDC: | ||||||
|  |         uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0)); | ||||||
|  |         if (device_is_ready(uart_dev)) { | ||||||
|  |           usb_enable(nullptr); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  | #endif | ||||||
|  |     } | ||||||
|  |     if (!device_is_ready(uart_dev)) { | ||||||
|  |       ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_()); | ||||||
|  |     } else { | ||||||
|  |       this->uart_dev_ = uart_dev; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   global_logger = this; | ||||||
|  |   ESP_LOGI(TAG, "Log initialized"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HOT Logger::write_msg_(const char *msg) { | ||||||
|  | #ifdef CONFIG_PRINTK | ||||||
|  |   printk("%s\n", msg); | ||||||
|  | #endif | ||||||
|  |   if (nullptr == this->uart_dev_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   while (*msg) { | ||||||
|  |     uart_poll_out(this->uart_dev_, *msg); | ||||||
|  |     ++msg; | ||||||
|  |   } | ||||||
|  |   uart_poll_out(this->uart_dev_, '\n'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; | ||||||
|  |  | ||||||
|  | const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } | ||||||
|  |  | ||||||
|  | }  // namespace logger | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
| @@ -29,9 +29,9 @@ from ..defines import ( | |||||||
| ) | ) | ||||||
| from ..helpers import add_lv_use, lvgl_components_required | from ..helpers import add_lv_use, lvgl_components_required | ||||||
| from ..lv_validation import ( | from ..lv_validation import ( | ||||||
|     angle, |  | ||||||
|     get_end_value, |     get_end_value, | ||||||
|     get_start_value, |     get_start_value, | ||||||
|  |     lv_angle, | ||||||
|     lv_bool, |     lv_bool, | ||||||
|     lv_color, |     lv_color, | ||||||
|     lv_float, |     lv_float, | ||||||
| @@ -162,7 +162,7 @@ SCALE_SCHEMA = cv.Schema( | |||||||
|         cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, |         cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, | ||||||
|         cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, |         cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, | ||||||
|         cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), |         cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), | ||||||
|         cv.Optional(CONF_ROTATION): angle, |         cv.Optional(CONF_ROTATION): lv_angle, | ||||||
|         cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), |         cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
| @@ -187,7 +187,7 @@ class MeterType(WidgetType): | |||||||
|         for scale_conf in config.get(CONF_SCALES, ()): |         for scale_conf in config.get(CONF_SCALES, ()): | ||||||
|             rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 |             rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 | ||||||
|             if CONF_ROTATION in scale_conf: |             if CONF_ROTATION in scale_conf: | ||||||
|                 rotation = scale_conf[CONF_ROTATION] // 10 |                 rotation = await lv_angle.process(scale_conf[CONF_ROTATION]) | ||||||
|             with LocalVariable( |             with LocalVariable( | ||||||
|                 "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) |                 "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) | ||||||
|             ) as meter_var: |             ) as meter_var: | ||||||
| @@ -205,21 +205,20 @@ class MeterType(WidgetType): | |||||||
|                         var, |                         var, | ||||||
|                         meter_var, |                         meter_var, | ||||||
|                         ticks[CONF_COUNT], |                         ticks[CONF_COUNT], | ||||||
|                         ticks[CONF_WIDTH], |                         await size.process(ticks[CONF_WIDTH]), | ||||||
|                         ticks[CONF_LENGTH], |                         await size.process(ticks[CONF_LENGTH]), | ||||||
|                         color, |                         color, | ||||||
|                     ) |                     ) | ||||||
|                     if CONF_MAJOR in ticks: |                     if CONF_MAJOR in ticks: | ||||||
|                         major = ticks[CONF_MAJOR] |                         major = ticks[CONF_MAJOR] | ||||||
|                         color = await lv_color.process(major[CONF_COLOR]) |  | ||||||
|                         lv.meter_set_scale_major_ticks( |                         lv.meter_set_scale_major_ticks( | ||||||
|                             var, |                             var, | ||||||
|                             meter_var, |                             meter_var, | ||||||
|                             major[CONF_STRIDE], |                             major[CONF_STRIDE], | ||||||
|                             major[CONF_WIDTH], |                             await size.process(major[CONF_WIDTH]), | ||||||
|                             major[CONF_LENGTH], |                             await size.process(major[CONF_LENGTH]), | ||||||
|                             color, |                             await lv_color.process(major[CONF_COLOR]), | ||||||
|                             major[CONF_LABEL_GAP], |                             await size.process(major[CONF_LABEL_GAP]), | ||||||
|                         ) |                         ) | ||||||
|                 for indicator in scale_conf.get(CONF_INDICATORS, ()): |                 for indicator in scale_conf.get(CONF_INDICATORS, ()): | ||||||
|                     (t, v) = next(iter(indicator.items())) |                     (t, v) = next(iter(indicator.items())) | ||||||
| @@ -233,7 +232,11 @@ class MeterType(WidgetType): | |||||||
|                         lv_assign( |                         lv_assign( | ||||||
|                             ivar, |                             ivar, | ||||||
|                             lv_expr.meter_add_needle_line( |                             lv_expr.meter_add_needle_line( | ||||||
|                                 var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] |                                 var, | ||||||
|  |                                 meter_var, | ||||||
|  |                                 await size.process(v[CONF_WIDTH]), | ||||||
|  |                                 color, | ||||||
|  |                                 await size.process(v[CONF_R_MOD]), | ||||||
|                             ), |                             ), | ||||||
|                         ) |                         ) | ||||||
|                     if t == CONF_ARC: |                     if t == CONF_ARC: | ||||||
| @@ -241,7 +244,11 @@ class MeterType(WidgetType): | |||||||
|                         lv_assign( |                         lv_assign( | ||||||
|                             ivar, |                             ivar, | ||||||
|                             lv_expr.meter_add_arc( |                             lv_expr.meter_add_arc( | ||||||
|                                 var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] |                                 var, | ||||||
|  |                                 meter_var, | ||||||
|  |                                 await size.process(v[CONF_WIDTH]), | ||||||
|  |                                 color, | ||||||
|  |                                 await size.process(v[CONF_R_MOD]), | ||||||
|                             ), |                             ), | ||||||
|                         ) |                         ) | ||||||
|                     if t == CONF_TICK_STYLE: |                     if t == CONF_TICK_STYLE: | ||||||
| @@ -257,7 +264,7 @@ class MeterType(WidgetType): | |||||||
|                                 color_start, |                                 color_start, | ||||||
|                                 color_end, |                                 color_end, | ||||||
|                                 v[CONF_LOCAL], |                                 v[CONF_LOCAL], | ||||||
|                                 v[CONF_WIDTH], |                                 size.process(v[CONF_WIDTH]), | ||||||
|                             ), |                             ), | ||||||
|                         ) |                         ) | ||||||
|                     if t == CONF_IMAGE: |                     if t == CONF_IMAGE: | ||||||
|   | |||||||
| @@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"] | |||||||
|  |  | ||||||
| DOMAIN = "mipi_spi" | DOMAIN = "mipi_spi" | ||||||
|  |  | ||||||
| CONF_DRAW_FROM_ORIGIN = "draw_from_origin" |  | ||||||
| CONF_SPI_16 = "spi_16" | CONF_SPI_16 = "spi_16" | ||||||
| CONF_PIXEL_MODE = "pixel_mode" | CONF_PIXEL_MODE = "pixel_mode" | ||||||
| CONF_COLOR_DEPTH = "color_depth" |  | ||||||
| CONF_BUS_MODE = "bus_mode" | CONF_BUS_MODE = "bus_mode" | ||||||
| CONF_USE_AXIS_FLIPS = "use_axis_flips" | CONF_USE_AXIS_FLIPS = "use_axis_flips" | ||||||
| CONF_NATIVE_WIDTH = "native_width" | CONF_NATIVE_WIDTH = "native_width" | ||||||
|   | |||||||
| @@ -3,11 +3,18 @@ import logging | |||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components import display, spi | from esphome.components import display, spi | ||||||
|  | from esphome.components.const import ( | ||||||
|  |     CONF_BYTE_ORDER, | ||||||
|  |     CONF_COLOR_DEPTH, | ||||||
|  |     CONF_DRAW_ROUNDING, | ||||||
|  | ) | ||||||
|  | from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS | ||||||
| from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE | from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.config_validation import ALLOW_EXTRA | from esphome.config_validation import ALLOW_EXTRA | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_BRIGHTNESS, |     CONF_BRIGHTNESS, | ||||||
|  |     CONF_BUFFER_SIZE, | ||||||
|     CONF_COLOR_ORDER, |     CONF_COLOR_ORDER, | ||||||
|     CONF_CS_PIN, |     CONF_CS_PIN, | ||||||
|     CONF_DATA_RATE, |     CONF_DATA_RATE, | ||||||
| @@ -24,19 +31,19 @@ from esphome.const import ( | |||||||
|     CONF_MODEL, |     CONF_MODEL, | ||||||
|     CONF_OFFSET_HEIGHT, |     CONF_OFFSET_HEIGHT, | ||||||
|     CONF_OFFSET_WIDTH, |     CONF_OFFSET_WIDTH, | ||||||
|  |     CONF_PAGES, | ||||||
|     CONF_RESET_PIN, |     CONF_RESET_PIN, | ||||||
|     CONF_ROTATION, |     CONF_ROTATION, | ||||||
|     CONF_SWAP_XY, |     CONF_SWAP_XY, | ||||||
|     CONF_TRANSFORM, |     CONF_TRANSFORM, | ||||||
|     CONF_WIDTH, |     CONF_WIDTH, | ||||||
| ) | ) | ||||||
| from esphome.core import TimePeriod | from esphome.core import CORE, TimePeriod | ||||||
|  | from esphome.cpp_generator import TemplateArguments | ||||||
|  | from esphome.final_validate import full_config | ||||||
|  |  | ||||||
| from ..const import CONF_DRAW_ROUNDING |  | ||||||
| from ..lvgl.defines import CONF_COLOR_DEPTH |  | ||||||
| from . import ( | from . import ( | ||||||
|     CONF_BUS_MODE, |     CONF_BUS_MODE, | ||||||
|     CONF_DRAW_FROM_ORIGIN, |  | ||||||
|     CONF_NATIVE_HEIGHT, |     CONF_NATIVE_HEIGHT, | ||||||
|     CONF_NATIVE_WIDTH, |     CONF_NATIVE_WIDTH, | ||||||
|     CONF_PIXEL_MODE, |     CONF_PIXEL_MODE, | ||||||
| @@ -55,6 +62,7 @@ from .models import ( | |||||||
|     MADCTL_XFLIP, |     MADCTL_XFLIP, | ||||||
|     MADCTL_YFLIP, |     MADCTL_YFLIP, | ||||||
|     DriverChip, |     DriverChip, | ||||||
|  |     adafruit, | ||||||
|     amoled, |     amoled, | ||||||
|     cyd, |     cyd, | ||||||
|     ili, |     ili, | ||||||
| @@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"] | |||||||
|  |  | ||||||
| LOGGER = logging.getLogger(DOMAIN) | LOGGER = logging.getLogger(DOMAIN) | ||||||
| mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") | mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") | ||||||
| MipiSpi = mipi_spi_ns.class_( | MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice) | ||||||
|     "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice | MipiSpiBuffer = mipi_spi_ns.class_( | ||||||
|  |     "MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice | ||||||
| ) | ) | ||||||
| ColorOrder = display.display_ns.enum("ColorMode") | ColorOrder = display.display_ns.enum("ColorMode") | ||||||
| ColorBitness = display.display_ns.enum("ColorBitness") | ColorBitness = display.display_ns.enum("ColorBitness") | ||||||
| Model = mipi_spi_ns.enum("Model") | Model = mipi_spi_ns.enum("Model") | ||||||
|  |  | ||||||
|  | PixelMode = mipi_spi_ns.enum("PixelMode") | ||||||
|  | BusType = mipi_spi_ns.enum("BusType") | ||||||
|  |  | ||||||
| COLOR_ORDERS = { | COLOR_ORDERS = { | ||||||
|     MODE_RGB: ColorOrder.COLOR_ORDER_RGB, |     MODE_RGB: ColorOrder.COLOR_ORDER_RGB, | ||||||
|     MODE_BGR: ColorOrder.COLOR_ORDER_BGR, |     MODE_BGR: ColorOrder.COLOR_ORDER_BGR, | ||||||
| } | } | ||||||
|  |  | ||||||
| COLOR_DEPTHS = { | COLOR_DEPTHS = { | ||||||
|     8: ColorBitness.COLOR_BITNESS_332, |     8: PixelMode.PIXEL_MODE_8, | ||||||
|     16: ColorBitness.COLOR_BITNESS_565, |     16: PixelMode.PIXEL_MODE_16, | ||||||
|  |     18: PixelMode.PIXEL_MODE_18, | ||||||
| } | } | ||||||
|  |  | ||||||
| DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema | DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema | ||||||
|  |  | ||||||
|  | BusTypes = { | ||||||
|  |     TYPE_SINGLE: BusType.BUS_TYPE_SINGLE, | ||||||
|  |     TYPE_QUAD: BusType.BUS_TYPE_QUAD, | ||||||
|  |     TYPE_OCTAL: BusType.BUS_TYPE_OCTAL, | ||||||
|  | } | ||||||
|  |  | ||||||
| DriverChip("CUSTOM", initsequence={}) | DriverChip("CUSTOM") | ||||||
|  |  | ||||||
| MODELS = DriverChip.models | MODELS = DriverChip.models | ||||||
| # These statements are noops, but serve to suppress linting of side-effect-only imports | # This loop is a noop, but suppresses linting of side-effect-only imports | ||||||
| for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): | for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit): | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
| PixelMode = mipi_spi_ns.enum("PixelMode") |  | ||||||
|  |  | ||||||
| PIXEL_MODE_18BIT = "18bit" | DISPLAY_18BIT = "18bit" | ||||||
| PIXEL_MODE_16BIT = "16bit" | DISPLAY_16BIT = "16bit" | ||||||
|  |  | ||||||
| PIXEL_MODES = { | DISPLAY_PIXEL_MODES = { | ||||||
|     PIXEL_MODE_16BIT: 0x55, |     DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16), | ||||||
|     PIXEL_MODE_18BIT: 0x66, |     DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18), | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_dimensions(config): | ||||||
|  |     if CONF_DIMENSIONS in config: | ||||||
|  |         # Explicit dimensions, just use as is | ||||||
|  |         dimensions = config[CONF_DIMENSIONS] | ||||||
|  |         if isinstance(dimensions, dict): | ||||||
|  |             width = dimensions[CONF_WIDTH] | ||||||
|  |             height = dimensions[CONF_HEIGHT] | ||||||
|  |             offset_width = dimensions[CONF_OFFSET_WIDTH] | ||||||
|  |             offset_height = dimensions[CONF_OFFSET_HEIGHT] | ||||||
|  |             return width, height, offset_width, offset_height | ||||||
|  |         (width, height) = dimensions | ||||||
|  |         return width, height, 0, 0 | ||||||
|  |  | ||||||
|  |     # Default dimensions, use model defaults | ||||||
|  |     transform = get_transform(config) | ||||||
|  |  | ||||||
|  |     model = MODELS[config[CONF_MODEL]] | ||||||
|  |     width = model.get_default(CONF_WIDTH) | ||||||
|  |     height = model.get_default(CONF_HEIGHT) | ||||||
|  |     offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) | ||||||
|  |     offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) | ||||||
|  |  | ||||||
|  |     # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where | ||||||
|  |     # the offset is asymmetric | ||||||
|  |     if transform[CONF_MIRROR_X]: | ||||||
|  |         native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) | ||||||
|  |         offset_width = native_width - width - offset_width | ||||||
|  |     if transform[CONF_MIRROR_Y]: | ||||||
|  |         native_height = model.get_default( | ||||||
|  |             CONF_NATIVE_HEIGHT, height + offset_height * 2 | ||||||
|  |         ) | ||||||
|  |         offset_height = native_height - height - offset_height | ||||||
|  |     # Swap default dimensions if swap_xy is set | ||||||
|  |     if transform[CONF_SWAP_XY] is True: | ||||||
|  |         width, height = height, width | ||||||
|  |         offset_height, offset_width = offset_width, offset_height | ||||||
|  |     return width, height, offset_width, offset_height | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def denominator(config): | ||||||
|  |     """ | ||||||
|  |     Calculate the best denominator for a buffer size fraction. | ||||||
|  |     The denominator must be a number between 2 and 16 that divides the display height evenly, | ||||||
|  |     and the fraction represented by the denominator must be less than or equal to the given fraction. | ||||||
|  |     :config: The configuration dictionary containing the buffer size fraction and display dimensions | ||||||
|  |     :return: The denominator to use for the buffer size fraction | ||||||
|  |     """ | ||||||
|  |     frac = config.get(CONF_BUFFER_SIZE) | ||||||
|  |     if frac is None or frac > 0.75: | ||||||
|  |         return 1 | ||||||
|  |     height, _width, _offset_width, _offset_height = get_dimensions(config) | ||||||
|  |     try: | ||||||
|  |         return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0) | ||||||
|  |     except StopIteration: | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             f"Buffer size fraction {frac} is not compatible with display height {height}" | ||||||
|  |         ) from StopIteration | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_dimension(rounding): | def validate_dimension(rounding): | ||||||
|     def validator(value): |     def validator(value): | ||||||
|         value = cv.positive_int(value) |         value = cv.positive_int(value) | ||||||
| @@ -158,25 +235,27 @@ def dimension_schema(rounding): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def model_schema(bus_mode, model: DriverChip, swapsies: bool): | def swap_xy_schema(model): | ||||||
|  |     uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED | ||||||
|  |  | ||||||
|  |     def validator(value): | ||||||
|  |         if value: | ||||||
|  |             raise cv.Invalid("Axis swapping not supported by this model") | ||||||
|  |         return cv.boolean(value) | ||||||
|  |  | ||||||
|  |     if uses_swap: | ||||||
|  |         return {cv.Required(CONF_SWAP_XY): cv.boolean} | ||||||
|  |     return {cv.Optional(CONF_SWAP_XY, default=False): validator} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def model_schema(config): | ||||||
|  |     model = MODELS[config[CONF_MODEL]] | ||||||
|  |     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) | ||||||
|     transform = cv.Schema( |     transform = cv.Schema( | ||||||
|         { |         { | ||||||
|             cv.Required(CONF_MIRROR_X): cv.boolean, |             cv.Required(CONF_MIRROR_X): cv.boolean, | ||||||
|             cv.Required(CONF_MIRROR_Y): cv.boolean, |             cv.Required(CONF_MIRROR_Y): cv.boolean, | ||||||
|         } |             **swap_xy_schema(model), | ||||||
|     ) |  | ||||||
|     if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED: |  | ||||||
|         transform = transform.extend( |  | ||||||
|             { |  | ||||||
|                 cv.Optional(CONF_SWAP_XY): cv.invalid( |  | ||||||
|                     "Axis swapping not supported by this model" |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|     else: |  | ||||||
|         transform = transform.extend( |  | ||||||
|             { |  | ||||||
|                 cv.Required(CONF_SWAP_XY): cv.boolean, |  | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     # CUSTOM model will need to provide a custom init sequence |     # CUSTOM model will need to provide a custom init sequence | ||||||
| @@ -185,14 +264,21 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | |||||||
|         if model.initsequence is None |         if model.initsequence is None | ||||||
|         else cv.Optional(CONF_INIT_SEQUENCE) |         else cv.Optional(CONF_INIT_SEQUENCE) | ||||||
|     ) |     ) | ||||||
|     # Dimensions are optional if the model has a default width and the transform is not overridden |     # Dimensions are optional if the model has a default width and the x-y transform is not overridden | ||||||
|  |     is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True | ||||||
|     cv_dimensions = ( |     cv_dimensions = ( | ||||||
|         cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required |         cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required | ||||||
|     ) |     ) | ||||||
|     pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) |     pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,) | ||||||
|     color_depth = ( |     color_depth = ( | ||||||
|         ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") |         ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") | ||||||
|     ) |     ) | ||||||
|  |     other_options = [ | ||||||
|  |         CONF_INVERT_COLORS, | ||||||
|  |         CONF_USE_AXIS_FLIPS, | ||||||
|  |     ] | ||||||
|  |     if bus_mode == TYPE_SINGLE: | ||||||
|  |         other_options.append(CONF_SPI_16) | ||||||
|     schema = ( |     schema = ( | ||||||
|         display.FULL_DISPLAY_SCHEMA.extend( |         display.FULL_DISPLAY_SCHEMA.extend( | ||||||
|             spi.spi_device_schema( |             spi.spi_device_schema( | ||||||
| @@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | |||||||
|                 model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( |                 model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( | ||||||
|                     COLOR_ORDERS, upper=True |                     COLOR_ORDERS, upper=True | ||||||
|                 ), |                 ), | ||||||
|  |                 model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of( | ||||||
|  |                     "big_endian", "little_endian", lower=True | ||||||
|  |                 ), | ||||||
|                 model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), |                 model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), | ||||||
|                 model.option(CONF_DRAW_ROUNDING, 2): power_of_two, |                 model.option(CONF_DRAW_ROUNDING, 2): power_of_two, | ||||||
|                 model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( |                 model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( | ||||||
|                     cv.one_of(*pixel_modes, lower=True), |                     *pixel_modes, lower=True | ||||||
|                     cv.int_range(0, 255, min_included=True, max_included=True), |  | ||||||
|                 ), |                 ), | ||||||
|                 cv.Optional(CONF_TRANSFORM): transform, |                 cv.Optional(CONF_TRANSFORM): transform, | ||||||
|                 cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( |                 cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( | ||||||
| @@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | |||||||
|                 ), |                 ), | ||||||
|                 cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), |                 cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), | ||||||
|                 iseqconf: cv.ensure_list(map_sequence), |                 iseqconf: cv.ensure_list(map_sequence), | ||||||
|  |                 cv.Optional(CONF_BUFFER_SIZE): cv.All( | ||||||
|  |                     cv.percentage, cv.Range(0.12, 1.0) | ||||||
|  |                 ), | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         .extend( |         .extend({model.option(x): cv.boolean for x in other_options}) | ||||||
|             { |  | ||||||
|                 model.option(x): cv.boolean |  | ||||||
|                 for x in [ |  | ||||||
|                     CONF_DRAW_FROM_ORIGIN, |  | ||||||
|                     CONF_SPI_16, |  | ||||||
|                     CONF_INVERT_COLORS, |  | ||||||
|                     CONF_USE_AXIS_FLIPS, |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|     ) |     ) | ||||||
|     if brightness := model.get_default(CONF_BRIGHTNESS): |     if brightness := model.get_default(CONF_BRIGHTNESS): | ||||||
|         schema = schema.extend( |         schema = schema.extend( | ||||||
| @@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | |||||||
|     return schema |     return schema | ||||||
|  |  | ||||||
|  |  | ||||||
| def rotation_as_transform(model, config): | def is_rotation_transformable(config): | ||||||
|     """ |     """ | ||||||
|     Check if a rotation can be implemented in hardware using the MADCTL register. |     Check if a rotation can be implemented in hardware using the MADCTL register. | ||||||
|     A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. |     A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. | ||||||
|     """ |     """ | ||||||
|  |     model = MODELS[config[CONF_MODEL]] | ||||||
|     rotation = config.get(CONF_ROTATION, 0) |     rotation = config.get(CONF_ROTATION, 0) | ||||||
|     return rotation and ( |     return rotation and ( | ||||||
|         model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 |         model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def config_schema(config): | def customise_schema(config): | ||||||
|  |     """ | ||||||
|  |     Create a customised config schema for a specific model and validate the configuration. | ||||||
|  |     :param config: The configuration dictionary to validate | ||||||
|  |     :return: The validated configuration dictionary | ||||||
|  |     :raises cv.Invalid: If the configuration is invalid | ||||||
|  |     """ | ||||||
|     # First get the model and bus mode |     # First get the model and bus mode | ||||||
|     config = cv.Schema( |     config = cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -288,29 +376,94 @@ def config_schema(config): | |||||||
|         extra=ALLOW_EXTRA, |         extra=ALLOW_EXTRA, | ||||||
|     )(config) |     )(config) | ||||||
|     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) |     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) | ||||||
|     swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True |     config = model_schema(config)(config) | ||||||
|     config = model_schema(bus_mode, model, swapsies)(config) |  | ||||||
|     # Check for invalid combinations of MADCTL config |     # Check for invalid combinations of MADCTL config | ||||||
|     if init_sequence := config.get(CONF_INIT_SEQUENCE): |     if init_sequence := config.get(CONF_INIT_SEQUENCE): | ||||||
|         if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: |         commands = [x[0] for x in init_sequence] | ||||||
|  |         if MADCTL in commands and CONF_TRANSFORM in config: | ||||||
|             raise cv.Invalid( |             raise cv.Invalid( | ||||||
|                 f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" |                 f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" | ||||||
|             ) |             ) | ||||||
|  |         if PIXFMT in commands: | ||||||
|  |             raise cv.Invalid( | ||||||
|  |                 f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: |     if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: | ||||||
|         raise cv.Invalid("DC pin is not supported in quad mode") |         raise cv.Invalid("DC pin is not supported in quad mode") | ||||||
|     if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE: |  | ||||||
|         raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus") |  | ||||||
|     if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: |     if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: | ||||||
|         raise cv.Invalid(f"DC pin is required in {bus_mode} mode") |         raise cv.Invalid(f"DC pin is required in {bus_mode} mode") | ||||||
|  |     denominator(config) | ||||||
|     return config |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = config_schema | CONFIG_SCHEMA = customise_schema | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_transform(model, config): | def requires_buffer(config): | ||||||
|     can_transform = rotation_as_transform(model, config) |     """ | ||||||
|  |     Check if the display configuration requires a buffer. It will do so if any drawing methods are configured. | ||||||
|  |     :param config: | ||||||
|  |     :return:  True if a buffer is required, False otherwise | ||||||
|  |     """ | ||||||
|  |     return any( | ||||||
|  |         config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_color_depth(config): | ||||||
|  |     return int(config[CONF_COLOR_DEPTH].removesuffix("bit")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _final_validate(config): | ||||||
|  |     global_config = full_config.get() | ||||||
|  |  | ||||||
|  |     from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN | ||||||
|  |  | ||||||
|  |     if not requires_buffer(config) and LVGL_DOMAIN not in global_config: | ||||||
|  |         # If no drawing methods are configured, and LVGL is not enabled, show a test card | ||||||
|  |         config[CONF_SHOW_TEST_CARD] = True | ||||||
|  |  | ||||||
|  |     if "psram" not in global_config and CONF_BUFFER_SIZE not in config: | ||||||
|  |         if not requires_buffer(config): | ||||||
|  |             return config  # No buffer needed, so no need to set a buffer size | ||||||
|  |         # If PSRAM is not enabled, choose a small buffer size by default | ||||||
|  |         if not requires_buffer(config): | ||||||
|  |             # not our problem. | ||||||
|  |             return config | ||||||
|  |         color_depth = get_color_depth(config) | ||||||
|  |         frac = denominator(config) | ||||||
|  |         height, width, _offset_width, _offset_height = get_dimensions(config) | ||||||
|  |  | ||||||
|  |         buffer_size = color_depth // 8 * width * height // frac | ||||||
|  |         # Target a buffer size of 20kB | ||||||
|  |         fraction = 20000.0 / buffer_size | ||||||
|  |         try: | ||||||
|  |             config[CONF_BUFFER_SIZE] = 1.0 / next( | ||||||
|  |                 x for x in range(2, 17) if fraction >= 1 / x and height % x == 0 | ||||||
|  |             ) | ||||||
|  |         except StopIteration: | ||||||
|  |             # Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0 | ||||||
|  |             # PSRAM will be needed. | ||||||
|  |             if CORE.is_esp32: | ||||||
|  |                 raise cv.Invalid( | ||||||
|  |                     "PSRAM is required for this display" | ||||||
|  |                 ) from StopIteration | ||||||
|  |  | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FINAL_VALIDATE_SCHEMA = _final_validate | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_transform(config): | ||||||
|  |     """ | ||||||
|  |     Get the transformation configuration for the display. | ||||||
|  |     :param config: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     model = MODELS[config[CONF_MODEL]] | ||||||
|  |     can_transform = is_rotation_transformable(config) | ||||||
|     transform = config.get( |     transform = config.get( | ||||||
|         CONF_TRANSFORM, |         CONF_TRANSFORM, | ||||||
|         { |         { | ||||||
| @@ -350,16 +503,13 @@ def get_sequence(model, config): | |||||||
|     sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] |     sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] | ||||||
|     commands = [x[0] for x in sequence] |     commands = [x[0] for x in sequence] | ||||||
|     # Set pixel format if not already in the custom sequence |     # Set pixel format if not already in the custom sequence | ||||||
|     if PIXFMT not in commands: |     pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]] | ||||||
|         pixel_mode = config[CONF_PIXEL_MODE] |     sequence.append((PIXFMT, pixel_mode[0])) | ||||||
|         if not isinstance(pixel_mode, int): |  | ||||||
|             pixel_mode = PIXEL_MODES[pixel_mode] |  | ||||||
|         sequence.append((PIXFMT, pixel_mode)) |  | ||||||
|     # Does the chip use the flipping bits for mirroring rather than the reverse order bits? |     # Does the chip use the flipping bits for mirroring rather than the reverse order bits? | ||||||
|     use_flip = config[CONF_USE_AXIS_FLIPS] |     use_flip = config[CONF_USE_AXIS_FLIPS] | ||||||
|     if MADCTL not in commands: |     if MADCTL not in commands: | ||||||
|         madctl = 0 |         madctl = 0 | ||||||
|         transform = get_transform(model, config) |         transform = get_transform(config) | ||||||
|         if transform.get(CONF_TRANSFORM): |         if transform.get(CONF_TRANSFORM): | ||||||
|             LOGGER.info("Using hardware transform to implement rotation") |             LOGGER.info("Using hardware transform to implement rotation") | ||||||
|         if transform.get(CONF_MIRROR_X): |         if transform.get(CONF_MIRROR_X): | ||||||
| @@ -396,63 +546,62 @@ def get_sequence(model, config): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_instance(config): | ||||||
|  |     """ | ||||||
|  |     Get the type of MipiSpi instance to create based on the configuration, | ||||||
|  |     and the template arguments. | ||||||
|  |     :param config: | ||||||
|  |     :return: type, template arguments | ||||||
|  |     """ | ||||||
|  |     width, height, offset_width, offset_height = get_dimensions(config) | ||||||
|  |  | ||||||
|  |     color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit")) | ||||||
|  |     bufferpixels = COLOR_DEPTHS[color_depth] | ||||||
|  |  | ||||||
|  |     display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1] | ||||||
|  |     bus_type = config[CONF_BUS_MODE] | ||||||
|  |     if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False): | ||||||
|  |         # If the bus mode is single and spi_16 is set, use single 16-bit mode | ||||||
|  |         bus_type = BusType.BUS_TYPE_SINGLE_16 | ||||||
|  |     else: | ||||||
|  |         bus_type = BusTypes[bus_type] | ||||||
|  |     buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 | ||||||
|  |     frac = denominator(config) | ||||||
|  |     rotation = DISPLAY_ROTATIONS[ | ||||||
|  |         0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0) | ||||||
|  |     ] | ||||||
|  |     templateargs = [ | ||||||
|  |         buffer_type, | ||||||
|  |         bufferpixels, | ||||||
|  |         config[CONF_BYTE_ORDER] == "big_endian", | ||||||
|  |         display_pixel_mode, | ||||||
|  |         bus_type, | ||||||
|  |         width, | ||||||
|  |         height, | ||||||
|  |         offset_width, | ||||||
|  |         offset_height, | ||||||
|  |     ] | ||||||
|  |     # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi | ||||||
|  |     if requires_buffer(config): | ||||||
|  |         templateargs.append(rotation) | ||||||
|  |         templateargs.append(frac) | ||||||
|  |         return MipiSpiBuffer, templateargs | ||||||
|  |     return MipiSpi, templateargs | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     model = MODELS[config[CONF_MODEL]] |     model = MODELS[config[CONF_MODEL]] | ||||||
|     transform = get_transform(model, config) |     var_id = config[CONF_ID] | ||||||
|     if CONF_DIMENSIONS in config: |     var_id.type, templateargs = get_instance(config) | ||||||
|         # Explicit dimensions, just use as is |     var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) | ||||||
|         dimensions = config[CONF_DIMENSIONS] |  | ||||||
|         if isinstance(dimensions, dict): |  | ||||||
|             width = dimensions[CONF_WIDTH] |  | ||||||
|             height = dimensions[CONF_HEIGHT] |  | ||||||
|             offset_width = dimensions[CONF_OFFSET_WIDTH] |  | ||||||
|             offset_height = dimensions[CONF_OFFSET_HEIGHT] |  | ||||||
|         else: |  | ||||||
|             (width, height) = dimensions |  | ||||||
|             offset_width = 0 |  | ||||||
|             offset_height = 0 |  | ||||||
|     else: |  | ||||||
|         # Default dimensions, use model defaults and transform if needed |  | ||||||
|         width = model.get_default(CONF_WIDTH) |  | ||||||
|         height = model.get_default(CONF_HEIGHT) |  | ||||||
|         offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) |  | ||||||
|         offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) |  | ||||||
|  |  | ||||||
|         # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where |  | ||||||
|         # the offset is asymmetric |  | ||||||
|         if transform[CONF_MIRROR_X]: |  | ||||||
|             native_width = model.get_default( |  | ||||||
|                 CONF_NATIVE_WIDTH, width + offset_width * 2 |  | ||||||
|             ) |  | ||||||
|             offset_width = native_width - width - offset_width |  | ||||||
|         if transform[CONF_MIRROR_Y]: |  | ||||||
|             native_height = model.get_default( |  | ||||||
|                 CONF_NATIVE_HEIGHT, height + offset_height * 2 |  | ||||||
|             ) |  | ||||||
|             offset_height = native_height - height - offset_height |  | ||||||
|         # Swap default dimensions if swap_xy is set |  | ||||||
|         if transform[CONF_SWAP_XY] is True: |  | ||||||
|             width, height = height, width |  | ||||||
|             offset_height, offset_width = offset_width, offset_height |  | ||||||
|  |  | ||||||
|     color_depth = config[CONF_COLOR_DEPTH] |  | ||||||
|     if color_depth.endswith("bit"): |  | ||||||
|         color_depth = color_depth[:-3] |  | ||||||
|     color_depth = COLOR_DEPTHS[int(color_depth)] |  | ||||||
|  |  | ||||||
|     var = cg.new_Pvariable( |  | ||||||
|         config[CONF_ID], width, height, offset_width, offset_height, color_depth |  | ||||||
|     ) |  | ||||||
|     cg.add(var.set_init_sequence(get_sequence(model, config))) |     cg.add(var.set_init_sequence(get_sequence(model, config))) | ||||||
|     if rotation_as_transform(model, config): |     if is_rotation_transformable(config): | ||||||
|         if CONF_TRANSFORM in config: |         if CONF_TRANSFORM in config: | ||||||
|             LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") |             LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") | ||||||
|         else: |         else: | ||||||
|             config[CONF_ROTATION] = 0 |             config[CONF_ROTATION] = 0 | ||||||
|     cg.add(var.set_model(config[CONF_MODEL])) |     cg.add(var.set_model(config[CONF_MODEL])) | ||||||
|     cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN])) |  | ||||||
|     cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) |     cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) | ||||||
|     cg.add(var.set_spi_16(config[CONF_SPI_16])) |  | ||||||
|     if enable_pin := config.get(CONF_ENABLE_PIN): |     if enable_pin := config.get(CONF_ENABLE_PIN): | ||||||
|         enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] |         enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] | ||||||
|         cg.add(var.set_enable_pins(enable)) |         cg.add(var.set_enable_pins(enable)) | ||||||
| @@ -472,4 +621,5 @@ async def to_code(config): | |||||||
|         cg.add(var.set_writer(lambda_)) |         cg.add(var.set_writer(lambda_)) | ||||||
|     await display.register_display(var, config) |     await display.register_display(var, config) | ||||||
|     await spi.register_spi_device(var, config) |     await spi.register_spi_device(var, config) | ||||||
|  |     # Displays are write-only, set the SPI device to write-only as well | ||||||
|     cg.add(var.set_write_only(True)) |     cg.add(var.set_write_only(True)) | ||||||
|   | |||||||
| @@ -2,489 +2,5 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace mipi_spi { | namespace mipi_spi {}  // namespace mipi_spi | ||||||
|  |  | ||||||
| void MipiSpi::setup() { |  | ||||||
|   ESP_LOGCONFIG(TAG, "Running setup"); |  | ||||||
|   this->spi_setup(); |  | ||||||
|   if (this->dc_pin_ != nullptr) { |  | ||||||
|     this->dc_pin_->setup(); |  | ||||||
|     this->dc_pin_->digital_write(false); |  | ||||||
|   } |  | ||||||
|   for (auto *pin : this->enable_pins_) { |  | ||||||
|     pin->setup(); |  | ||||||
|     pin->digital_write(true); |  | ||||||
|   } |  | ||||||
|   if (this->reset_pin_ != nullptr) { |  | ||||||
|     this->reset_pin_->setup(); |  | ||||||
|     this->reset_pin_->digital_write(true); |  | ||||||
|     delay(5); |  | ||||||
|     this->reset_pin_->digital_write(false); |  | ||||||
|     delay(5); |  | ||||||
|     this->reset_pin_->digital_write(true); |  | ||||||
|   } |  | ||||||
|   this->bus_width_ = this->parent_->get_bus_width(); |  | ||||||
|  |  | ||||||
|   // need to know when the display is ready for SLPOUT command - will be 120ms after reset |  | ||||||
|   auto when = millis() + 120; |  | ||||||
|   delay(10); |  | ||||||
|   size_t index = 0; |  | ||||||
|   auto &vec = this->init_sequence_; |  | ||||||
|   while (index != vec.size()) { |  | ||||||
|     if (vec.size() - index < 2) { |  | ||||||
|       ESP_LOGE(TAG, "Malformed init sequence"); |  | ||||||
|       this->mark_failed(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     uint8_t cmd = vec[index++]; |  | ||||||
|     uint8_t x = vec[index++]; |  | ||||||
|     if (x == DELAY_FLAG) { |  | ||||||
|       ESP_LOGD(TAG, "Delay %dms", cmd); |  | ||||||
|       delay(cmd); |  | ||||||
|     } else { |  | ||||||
|       uint8_t num_args = x & 0x7F; |  | ||||||
|       if (vec.size() - index < num_args) { |  | ||||||
|         ESP_LOGE(TAG, "Malformed init sequence"); |  | ||||||
|         this->mark_failed(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       auto arg_byte = vec[index]; |  | ||||||
|       switch (cmd) { |  | ||||||
|         case SLEEP_OUT: { |  | ||||||
|           // are we ready, boots? |  | ||||||
|           int duration = when - millis(); |  | ||||||
|           if (duration > 0) { |  | ||||||
|             ESP_LOGD(TAG, "Sleep %dms", duration); |  | ||||||
|             delay(duration); |  | ||||||
|           } |  | ||||||
|         } break; |  | ||||||
|  |  | ||||||
|         case INVERT_ON: |  | ||||||
|           this->invert_colors_ = true; |  | ||||||
|           break; |  | ||||||
|         case MADCTL_CMD: |  | ||||||
|           this->madctl_ = arg_byte; |  | ||||||
|           break; |  | ||||||
|         case PIXFMT: |  | ||||||
|           this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18; |  | ||||||
|           break; |  | ||||||
|         case BRIGHTNESS: |  | ||||||
|           this->brightness_ = arg_byte; |  | ||||||
|           break; |  | ||||||
|  |  | ||||||
|         default: |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
|       const auto *ptr = vec.data() + index; |  | ||||||
|       ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); |  | ||||||
|       this->write_command_(cmd, ptr, num_args); |  | ||||||
|       index += num_args; |  | ||||||
|       if (cmd == SLEEP_OUT) |  | ||||||
|         delay(10); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   this->setup_complete_ = true; |  | ||||||
|   if (this->draw_from_origin_) |  | ||||||
|     check_buffer_(); |  | ||||||
|   ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::update() { |  | ||||||
|   if (!this->setup_complete_ || this->is_failed()) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   this->do_update_(); |  | ||||||
|   if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) |  | ||||||
|     return; |  | ||||||
|   ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); |  | ||||||
|   // Some chips require that the drawing window be aligned on certain boundaries |  | ||||||
|   auto dr = this->draw_rounding_; |  | ||||||
|   this->x_low_ = this->x_low_ / dr * dr; |  | ||||||
|   this->y_low_ = this->y_low_ / dr * dr; |  | ||||||
|   this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; |  | ||||||
|   this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; |  | ||||||
|   if (this->draw_from_origin_) { |  | ||||||
|     this->x_low_ = 0; |  | ||||||
|     this->y_low_ = 0; |  | ||||||
|     this->x_high_ = this->width_ - 1; |  | ||||||
|   } |  | ||||||
|   int w = this->x_high_ - this->x_low_ + 1; |  | ||||||
|   int h = this->y_high_ - this->y_low_ + 1; |  | ||||||
|   this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_, |  | ||||||
|                           this->width_ - w - this->x_low_); |  | ||||||
|   // invalidate watermarks |  | ||||||
|   this->x_low_ = this->width_; |  | ||||||
|   this->y_low_ = this->height_; |  | ||||||
|   this->x_high_ = 0; |  | ||||||
|   this->y_high_ = 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::fill(Color color) { |  | ||||||
|   if (!this->check_buffer_()) |  | ||||||
|     return; |  | ||||||
|   this->x_low_ = 0; |  | ||||||
|   this->y_low_ = 0; |  | ||||||
|   this->x_high_ = this->get_width_internal() - 1; |  | ||||||
|   this->y_high_ = this->get_height_internal() - 1; |  | ||||||
|   switch (this->color_depth_) { |  | ||||||
|     case display::COLOR_BITNESS_332: { |  | ||||||
|       auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); |  | ||||||
|       memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     default: { |  | ||||||
|       auto new_color = display::ColorUtil::color_to_565(color); |  | ||||||
|       if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) { |  | ||||||
|         // Upper and lower is equal can use quicker memset operation. Takes ~20ms. |  | ||||||
|         memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); |  | ||||||
|       } else { |  | ||||||
|         auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_); |  | ||||||
|         auto len = this->buffer_bytes_ / 2; |  | ||||||
|         while (len--) { |  | ||||||
|           *ptr_16++ = new_color; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) { |  | ||||||
|   if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (!this->check_buffer_()) |  | ||||||
|     return; |  | ||||||
|   size_t pos = (y * this->width_) + x; |  | ||||||
|   switch (this->color_depth_) { |  | ||||||
|     case display::COLOR_BITNESS_332: { |  | ||||||
|       uint8_t new_color = display::ColorUtil::color_to_332(color); |  | ||||||
|       if (this->buffer_[pos] == new_color) |  | ||||||
|         return; |  | ||||||
|       this->buffer_[pos] = new_color; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     case display::COLOR_BITNESS_565: { |  | ||||||
|       auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_); |  | ||||||
|       uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5); |  | ||||||
|       uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); |  | ||||||
|       uint16_t new_color = hi_byte | (lo_byte << 8);  // big endian |  | ||||||
|       if (ptr_16[pos] == new_color) |  | ||||||
|         return; |  | ||||||
|       ptr_16[pos] = new_color; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     default: |  | ||||||
|       return; |  | ||||||
|   } |  | ||||||
|   // low and high watermark may speed up drawing from buffer |  | ||||||
|   if (x < this->x_low_) |  | ||||||
|     this->x_low_ = x; |  | ||||||
|   if (y < this->y_low_) |  | ||||||
|     this->y_low_ = y; |  | ||||||
|   if (x > this->x_high_) |  | ||||||
|     this->x_high_ = x; |  | ||||||
|   if (y > this->y_high_) |  | ||||||
|     this->y_high_ = y; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::reset_params_() { |  | ||||||
|   if (!this->is_ready()) |  | ||||||
|     return; |  | ||||||
|   this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); |  | ||||||
|   if (this->brightness_.has_value()) |  | ||||||
|     this->write_command_(BRIGHTNESS, this->brightness_.value()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::write_init_sequence_() { |  | ||||||
|   size_t index = 0; |  | ||||||
|   auto &vec = this->init_sequence_; |  | ||||||
|   while (index != vec.size()) { |  | ||||||
|     if (vec.size() - index < 2) { |  | ||||||
|       ESP_LOGE(TAG, "Malformed init sequence"); |  | ||||||
|       this->mark_failed(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     uint8_t cmd = vec[index++]; |  | ||||||
|     uint8_t x = vec[index++]; |  | ||||||
|     if (x == DELAY_FLAG) { |  | ||||||
|       ESP_LOGV(TAG, "Delay %dms", cmd); |  | ||||||
|       delay(cmd); |  | ||||||
|     } else { |  | ||||||
|       uint8_t num_args = x & 0x7F; |  | ||||||
|       if (vec.size() - index < num_args) { |  | ||||||
|         ESP_LOGE(TAG, "Malformed init sequence"); |  | ||||||
|         this->mark_failed(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       const auto *ptr = vec.data() + index; |  | ||||||
|       this->write_command_(cmd, ptr, num_args); |  | ||||||
|       index += num_args; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   this->setup_complete_ = true; |  | ||||||
|   ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { |  | ||||||
|   ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); |  | ||||||
|   uint8_t buf[4]; |  | ||||||
|   x1 += this->offset_width_; |  | ||||||
|   x2 += this->offset_width_; |  | ||||||
|   y1 += this->offset_height_; |  | ||||||
|   y2 += this->offset_height_; |  | ||||||
|   put16_be(buf, y1); |  | ||||||
|   put16_be(buf + 2, y2); |  | ||||||
|   this->write_command_(RASET, buf, sizeof buf); |  | ||||||
|   put16_be(buf, x1); |  | ||||||
|   put16_be(buf + 2, x2); |  | ||||||
|   this->write_command_(CASET, buf, sizeof buf); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, |  | ||||||
|                              display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { |  | ||||||
|   if (!this->setup_complete_ || this->is_failed()) |  | ||||||
|     return; |  | ||||||
|   if (w <= 0 || h <= 0) |  | ||||||
|     return; |  | ||||||
|   if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) { |  | ||||||
|     Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (this->draw_from_origin_) { |  | ||||||
|     auto stride = x_offset + w + x_pad; |  | ||||||
|     for (int y = 0; y != h; y++) { |  | ||||||
|       memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2, |  | ||||||
|              ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2); |  | ||||||
|     } |  | ||||||
|     ptr = this->buffer_; |  | ||||||
|     w = this->width_; |  | ||||||
|     h += y_start; |  | ||||||
|     x_start = 0; |  | ||||||
|     y_start = 0; |  | ||||||
|     x_offset = 0; |  | ||||||
|     y_offset = 0; |  | ||||||
|   } |  | ||||||
|   this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) { |  | ||||||
|   stride -= w; |  | ||||||
|   uint8_t transfer_buffer[6 * 256]; |  | ||||||
|   size_t idx = 0;  // index into transfer_buffer |  | ||||||
|   while (h-- != 0) { |  | ||||||
|     for (auto x = w; x-- != 0;) { |  | ||||||
|       auto color_val = *ptr++; |  | ||||||
|       // deal with byte swapping |  | ||||||
|       transfer_buffer[idx++] = (color_val & 0xF8);                                       // Blue |  | ||||||
|       transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11);  // Green |  | ||||||
|       transfer_buffer[idx++] = (color_val >> 5) & 0xF8;                                  // Red |  | ||||||
|       if (idx == sizeof(transfer_buffer)) { |  | ||||||
|         this->write_array(transfer_buffer, idx); |  | ||||||
|         idx = 0; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     ptr += stride; |  | ||||||
|   } |  | ||||||
|   if (idx != 0) |  | ||||||
|     this->write_array(transfer_buffer, idx); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { |  | ||||||
|   stride -= w; |  | ||||||
|   uint8_t transfer_buffer[6 * 256]; |  | ||||||
|   size_t idx = 0;  // index into transfer_buffer |  | ||||||
|   while (h-- != 0) { |  | ||||||
|     for (auto x = w; x-- != 0;) { |  | ||||||
|       auto color_val = *ptr++; |  | ||||||
|       transfer_buffer[idx++] = color_val & 0xE0;         // Red |  | ||||||
|       transfer_buffer[idx++] = (color_val << 3) & 0xE0;  // Green |  | ||||||
|       transfer_buffer[idx++] = color_val << 6;           // Blue |  | ||||||
|       if (idx == sizeof(transfer_buffer)) { |  | ||||||
|         this->write_array(transfer_buffer, idx); |  | ||||||
|         idx = 0; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     ptr += stride; |  | ||||||
|   } |  | ||||||
|   if (idx != 0) |  | ||||||
|     this->write_array(transfer_buffer, idx); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { |  | ||||||
|   stride -= w; |  | ||||||
|   uint8_t transfer_buffer[6 * 256]; |  | ||||||
|   size_t idx = 0;  // index into transfer_buffer |  | ||||||
|   while (h-- != 0) { |  | ||||||
|     for (auto x = w; x-- != 0;) { |  | ||||||
|       auto color_val = *ptr++; |  | ||||||
|       transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); |  | ||||||
|       transfer_buffer[idx++] = (color_val & 0x3) << 3; |  | ||||||
|       if (idx == sizeof(transfer_buffer)) { |  | ||||||
|         this->write_array(transfer_buffer, idx); |  | ||||||
|         idx = 0; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     ptr += stride; |  | ||||||
|   } |  | ||||||
|   if (idx != 0) |  | ||||||
|     this->write_array(transfer_buffer, idx); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, |  | ||||||
|                                 int x_pad) { |  | ||||||
|   this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); |  | ||||||
|   auto stride = x_offset + w + x_pad; |  | ||||||
|   const auto *offset_ptr = ptr; |  | ||||||
|   if (this->color_depth_ == display::COLOR_BITNESS_332) { |  | ||||||
|     offset_ptr += y_offset * stride + x_offset; |  | ||||||
|   } else { |  | ||||||
|     stride *= 2; |  | ||||||
|     offset_ptr += y_offset * stride + x_offset * 2; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   switch (this->bus_width_) { |  | ||||||
|     case 4: |  | ||||||
|       this->enable(); |  | ||||||
|       if (x_offset == 0 && x_pad == 0 && y_offset == 0) { |  | ||||||
|         // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't |  | ||||||
|         // bother |  | ||||||
|         this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4); |  | ||||||
|       } else { |  | ||||||
|         this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4); |  | ||||||
|         for (int y = 0; y != h; y++) { |  | ||||||
|           this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4); |  | ||||||
|           offset_ptr += stride; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|  |  | ||||||
|     case 8: |  | ||||||
|       this->write_command_(WDATA); |  | ||||||
|       this->enable(); |  | ||||||
|       if (x_offset == 0 && x_pad == 0 && y_offset == 0) { |  | ||||||
|         this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8); |  | ||||||
|       } else { |  | ||||||
|         for (int y = 0; y != h; y++) { |  | ||||||
|           this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8); |  | ||||||
|           offset_ptr += stride; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|  |  | ||||||
|     default: |  | ||||||
|       this->write_command_(WDATA); |  | ||||||
|       this->enable(); |  | ||||||
|  |  | ||||||
|       if (this->color_depth_ == display::COLOR_BITNESS_565) { |  | ||||||
|         // Source buffer is 16-bit RGB565 |  | ||||||
|         if (this->pixel_mode_ == PIXEL_MODE_18) { |  | ||||||
|           // Convert RGB565 to RGB666 |  | ||||||
|           this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2); |  | ||||||
|         } else { |  | ||||||
|           // Direct RGB565 output |  | ||||||
|           if (x_offset == 0 && x_pad == 0 && y_offset == 0) { |  | ||||||
|             this->write_array(ptr, w * h * 2); |  | ||||||
|           } else { |  | ||||||
|             for (int y = 0; y != h; y++) { |  | ||||||
|               this->write_array(offset_ptr, w * 2); |  | ||||||
|               offset_ptr += stride; |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         // Source buffer is 8-bit RGB332 |  | ||||||
|         if (this->pixel_mode_ == PIXEL_MODE_18) { |  | ||||||
|           // Convert RGB332 to RGB666 |  | ||||||
|           this->write_18_from_8_bit_(offset_ptr, w, h, stride); |  | ||||||
|         } else { |  | ||||||
|           this->write_16_from_8_bit_(offset_ptr, w, h, stride); |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|   } |  | ||||||
|   this->disable(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { |  | ||||||
|   ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); |  | ||||||
|   if (this->bus_width_ == 4) { |  | ||||||
|     this->enable(); |  | ||||||
|     this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); |  | ||||||
|     this->disable(); |  | ||||||
|   } else if (this->bus_width_ == 8) { |  | ||||||
|     this->dc_pin_->digital_write(false); |  | ||||||
|     this->enable(); |  | ||||||
|     this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); |  | ||||||
|     this->disable(); |  | ||||||
|     this->dc_pin_->digital_write(true); |  | ||||||
|     if (len != 0) { |  | ||||||
|       this->enable(); |  | ||||||
|       this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); |  | ||||||
|       this->disable(); |  | ||||||
|     } |  | ||||||
|   } else { |  | ||||||
|     this->dc_pin_->digital_write(false); |  | ||||||
|     this->enable(); |  | ||||||
|     this->write_byte(cmd); |  | ||||||
|     this->disable(); |  | ||||||
|     this->dc_pin_->digital_write(true); |  | ||||||
|     if (len != 0) { |  | ||||||
|       if (this->spi_16_) { |  | ||||||
|         for (size_t i = 0; i != len; i++) { |  | ||||||
|           this->enable(); |  | ||||||
|           this->write_byte(0); |  | ||||||
|           this->write_byte(bytes[i]); |  | ||||||
|           this->disable(); |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         this->enable(); |  | ||||||
|         this->write_array(bytes, len); |  | ||||||
|         this->disable(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MipiSpi::dump_config() { |  | ||||||
|   ESP_LOGCONFIG(TAG, |  | ||||||
|                 "MIPI_SPI Display\n" |  | ||||||
|                 "  Model: %s\n" |  | ||||||
|                 "  Width: %u\n" |  | ||||||
|                 "  Height: %u", |  | ||||||
|                 this->model_, this->width_, this->height_); |  | ||||||
|   if (this->offset_width_ != 0) |  | ||||||
|     ESP_LOGCONFIG(TAG, "  Offset width: %u", this->offset_width_); |  | ||||||
|   if (this->offset_height_ != 0) |  | ||||||
|     ESP_LOGCONFIG(TAG, "  Offset height: %u", this->offset_height_); |  | ||||||
|   ESP_LOGCONFIG(TAG, |  | ||||||
|                 "  Swap X/Y: %s\n" |  | ||||||
|                 "  Mirror X: %s\n" |  | ||||||
|                 "  Mirror Y: %s\n" |  | ||||||
|                 "  Color depth: %d bits\n" |  | ||||||
|                 "  Invert colors: %s\n" |  | ||||||
|                 "  Color order: %s\n" |  | ||||||
|                 "  Pixel mode: %s", |  | ||||||
|                 YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), |  | ||||||
|                 YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), |  | ||||||
|                 this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_), |  | ||||||
|                 this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit"); |  | ||||||
|   if (this->brightness_.has_value()) |  | ||||||
|     ESP_LOGCONFIG(TAG, "  Brightness: %u", this->brightness_.value()); |  | ||||||
|   if (this->spi_16_) |  | ||||||
|     ESP_LOGCONFIG(TAG, "  SPI 16bit: YES"); |  | ||||||
|   ESP_LOGCONFIG(TAG, "  Draw rounding: %u", this->draw_rounding_); |  | ||||||
|   if (this->draw_from_origin_) |  | ||||||
|     ESP_LOGCONFIG(TAG, "  Draw from origin: YES"); |  | ||||||
|   LOG_PIN("  CS Pin: ", this->cs_); |  | ||||||
|   LOG_PIN("  Reset Pin: ", this->reset_pin_); |  | ||||||
|   LOG_PIN("  DC Pin: ", this->dc_pin_); |  | ||||||
|   ESP_LOGCONFIG(TAG, |  | ||||||
|                 "  SPI Mode: %d\n" |  | ||||||
|                 "  SPI Data rate: %dMHz\n" |  | ||||||
|                 "  SPI Bus width: %d", |  | ||||||
|                 this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), this->bus_width_); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| }  // namespace mipi_spi |  | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -4,40 +4,39 @@ | |||||||
|  |  | ||||||
| #include "esphome/components/spi/spi.h" | #include "esphome/components/spi/spi.h" | ||||||
| #include "esphome/components/display/display.h" | #include "esphome/components/display/display.h" | ||||||
| #include "esphome/components/display/display_buffer.h" |  | ||||||
| #include "esphome/components/display/display_color_utils.h" | #include "esphome/components/display/display_color_utils.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace mipi_spi { | namespace mipi_spi { | ||||||
|  |  | ||||||
| constexpr static const char *const TAG = "display.mipi_spi"; | constexpr static const char *const TAG = "display.mipi_spi"; | ||||||
| static const uint8_t SW_RESET_CMD = 0x01; | static constexpr uint8_t SW_RESET_CMD = 0x01; | ||||||
| static const uint8_t SLEEP_OUT = 0x11; | static constexpr uint8_t SLEEP_OUT = 0x11; | ||||||
| static const uint8_t NORON = 0x13; | static constexpr uint8_t NORON = 0x13; | ||||||
| static const uint8_t INVERT_OFF = 0x20; | static constexpr uint8_t INVERT_OFF = 0x20; | ||||||
| static const uint8_t INVERT_ON = 0x21; | static constexpr uint8_t INVERT_ON = 0x21; | ||||||
| static const uint8_t ALL_ON = 0x23; | static constexpr uint8_t ALL_ON = 0x23; | ||||||
| static const uint8_t WRAM = 0x24; | static constexpr uint8_t WRAM = 0x24; | ||||||
| static const uint8_t MIPI = 0x26; | static constexpr uint8_t MIPI = 0x26; | ||||||
| static const uint8_t DISPLAY_ON = 0x29; | static constexpr uint8_t DISPLAY_ON = 0x29; | ||||||
| static const uint8_t RASET = 0x2B; | static constexpr uint8_t RASET = 0x2B; | ||||||
| static const uint8_t CASET = 0x2A; | static constexpr uint8_t CASET = 0x2A; | ||||||
| static const uint8_t WDATA = 0x2C; | static constexpr uint8_t WDATA = 0x2C; | ||||||
| static const uint8_t TEON = 0x35; | static constexpr uint8_t TEON = 0x35; | ||||||
| static const uint8_t MADCTL_CMD = 0x36; | static constexpr uint8_t MADCTL_CMD = 0x36; | ||||||
| static const uint8_t PIXFMT = 0x3A; | static constexpr uint8_t PIXFMT = 0x3A; | ||||||
| static const uint8_t BRIGHTNESS = 0x51; | static constexpr uint8_t BRIGHTNESS = 0x51; | ||||||
| static const uint8_t SWIRE1 = 0x5A; | static constexpr uint8_t SWIRE1 = 0x5A; | ||||||
| static const uint8_t SWIRE2 = 0x5B; | static constexpr uint8_t SWIRE2 = 0x5B; | ||||||
| static const uint8_t PAGESEL = 0xFE; | static constexpr uint8_t PAGESEL = 0xFE; | ||||||
|  |  | ||||||
| static const uint8_t MADCTL_MY = 0x80;     // Bit 7 Bottom to top | static constexpr uint8_t MADCTL_MY = 0x80;     // Bit 7 Bottom to top | ||||||
| static const uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | static constexpr uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | ||||||
| static const uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | static constexpr uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | ||||||
| static const uint8_t MADCTL_RGB = 0x00;    // Bit 3 Red-Green-Blue pixel order | static constexpr uint8_t MADCTL_RGB = 0x00;    // Bit 3 Red-Green-Blue pixel order | ||||||
| static const uint8_t MADCTL_BGR = 0x08;    // Bit 3 Blue-Green-Red pixel order | static constexpr uint8_t MADCTL_BGR = 0x08;    // Bit 3 Blue-Green-Red pixel order | ||||||
| static const uint8_t MADCTL_XFLIP = 0x02;  // Mirror the display horizontally | static constexpr uint8_t MADCTL_XFLIP = 0x02;  // Mirror the display horizontally | ||||||
| static const uint8_t MADCTL_YFLIP = 0x01;  // Mirror the display vertically | static constexpr uint8_t MADCTL_YFLIP = 0x01;  // Mirror the display vertically | ||||||
|  |  | ||||||
| static const uint8_t DELAY_FLAG = 0xFF; | static const uint8_t DELAY_FLAG = 0xFF; | ||||||
| // store a 16 bit value in a buffer, big endian. | // store a 16 bit value in a buffer, big endian. | ||||||
| @@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) { | |||||||
|   buf[1] = value; |   buf[1] = value; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Buffer mode, conveniently also the number of bytes in a pixel | ||||||
| enum PixelMode { | enum PixelMode { | ||||||
|   PIXEL_MODE_16, |   PIXEL_MODE_8 = 1, | ||||||
|   PIXEL_MODE_18, |   PIXEL_MODE_16 = 2, | ||||||
|  |   PIXEL_MODE_18 = 3, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class MipiSpi : public display::DisplayBuffer, | enum BusType { | ||||||
|  |   BUS_TYPE_SINGLE = 1, | ||||||
|  |   BUS_TYPE_QUAD = 4, | ||||||
|  |   BUS_TYPE_OCTAL = 8, | ||||||
|  |   BUS_TYPE_SINGLE_16 = 16,  // Single bit bus, but 16 bits per transfer | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Base class for MIPI SPI displays. | ||||||
|  |  * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. | ||||||
|  |  * | ||||||
|  |  * @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t | ||||||
|  |  * @tparam BUFFERPIXEL Color depth of the buffer | ||||||
|  |  * @tparam DISPLAYPIXEL Color depth of the display | ||||||
|  |  * @tparam BUS_TYPE The type of the interface bus (single, quad, octal) | ||||||
|  |  * @tparam WIDTH Width of the display in pixels | ||||||
|  |  * @tparam HEIGHT Height of the display in pixels | ||||||
|  |  * @tparam OFFSET_WIDTH The x-offset of the display in pixels | ||||||
|  |  * @tparam OFFSET_HEIGHT The y-offset of the display in pixels | ||||||
|  |  * buffer | ||||||
|  |  */ | ||||||
|  | template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE, | ||||||
|  |          int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT> | ||||||
|  | class MipiSpi : public display::Display, | ||||||
|                 public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, |                 public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, | ||||||
|                                       spi::DATA_RATE_1MHZ> { |                                       spi::DATA_RATE_1MHZ> { | ||||||
|  public: |  public: | ||||||
|   MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) |   MipiSpi() {} | ||||||
|       : width_(width), |   void update() override { this->stop_poller(); } | ||||||
|         height_(height), |   void draw_pixel_at(int x, int y, Color color) override {} | ||||||
|         offset_width_(offset_width), |  | ||||||
|         offset_height_(offset_height), |  | ||||||
|         color_depth_(color_depth) {} |  | ||||||
|   void set_model(const char *model) { this->model_ = model; } |   void set_model(const char *model) { this->model_ = model; } | ||||||
|   void update() override; |  | ||||||
|   void setup() override; |  | ||||||
|   display::ColorOrder get_color_mode() { |  | ||||||
|     return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } |   void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } | ||||||
|   void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); } |   void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); } | ||||||
|   void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } |   void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } | ||||||
| @@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer, | |||||||
|     this->brightness_ = brightness; |     this->brightness_ = brightness; | ||||||
|     this->reset_params_(); |     this->reset_params_(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; } |  | ||||||
|   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } |   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||||
|   void dump_config() override; |  | ||||||
|  |  | ||||||
|   int get_width_internal() override { return this->width_; } |   int get_width_internal() override { return WIDTH; } | ||||||
|   int get_height_internal() override { return this->height_; } |   int get_height_internal() override { return HEIGHT; } | ||||||
|   bool can_proceed() override { return this->setup_complete_; } |  | ||||||
|   void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } |   void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } | ||||||
|   void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } |   void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } | ||||||
|   void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; } |  | ||||||
|  |   // reset the display, and write the init sequence | ||||||
|  |   void setup() override { | ||||||
|  |     this->spi_setup(); | ||||||
|  |     if (this->dc_pin_ != nullptr) { | ||||||
|  |       this->dc_pin_->setup(); | ||||||
|  |       this->dc_pin_->digital_write(false); | ||||||
|  |     } | ||||||
|  |     for (auto *pin : this->enable_pins_) { | ||||||
|  |       pin->setup(); | ||||||
|  |       pin->digital_write(true); | ||||||
|  |     } | ||||||
|  |     if (this->reset_pin_ != nullptr) { | ||||||
|  |       this->reset_pin_->setup(); | ||||||
|  |       this->reset_pin_->digital_write(true); | ||||||
|  |       delay(5); | ||||||
|  |       this->reset_pin_->digital_write(false); | ||||||
|  |       delay(5); | ||||||
|  |       this->reset_pin_->digital_write(true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // need to know when the display is ready for SLPOUT command - will be 120ms after reset | ||||||
|  |     auto when = millis() + 120; | ||||||
|  |     delay(10); | ||||||
|  |     size_t index = 0; | ||||||
|  |     auto &vec = this->init_sequence_; | ||||||
|  |     while (index != vec.size()) { | ||||||
|  |       if (vec.size() - index < 2) { | ||||||
|  |         esph_log_e(TAG, "Malformed init sequence"); | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       uint8_t cmd = vec[index++]; | ||||||
|  |       uint8_t x = vec[index++]; | ||||||
|  |       if (x == DELAY_FLAG) { | ||||||
|  |         esph_log_d(TAG, "Delay %dms", cmd); | ||||||
|  |         delay(cmd); | ||||||
|  |       } else { | ||||||
|  |         uint8_t num_args = x & 0x7F; | ||||||
|  |         if (vec.size() - index < num_args) { | ||||||
|  |           esph_log_e(TAG, "Malformed init sequence"); | ||||||
|  |           this->mark_failed(); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         auto arg_byte = vec[index]; | ||||||
|  |         switch (cmd) { | ||||||
|  |           case SLEEP_OUT: { | ||||||
|  |             // are we ready, boots? | ||||||
|  |             int duration = when - millis(); | ||||||
|  |             if (duration > 0) { | ||||||
|  |               esph_log_d(TAG, "Sleep %dms", duration); | ||||||
|  |               delay(duration); | ||||||
|  |             } | ||||||
|  |           } break; | ||||||
|  |  | ||||||
|  |           case INVERT_ON: | ||||||
|  |             this->invert_colors_ = true; | ||||||
|  |             break; | ||||||
|  |           case MADCTL_CMD: | ||||||
|  |             this->madctl_ = arg_byte; | ||||||
|  |             break; | ||||||
|  |           case BRIGHTNESS: | ||||||
|  |             this->brightness_ = arg_byte; | ||||||
|  |             break; | ||||||
|  |  | ||||||
|  |           default: | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         const auto *ptr = vec.data() + index; | ||||||
|  |         esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); | ||||||
|  |         this->write_command_(cmd, ptr, num_args); | ||||||
|  |         index += num_args; | ||||||
|  |         if (cmd == SLEEP_OUT) | ||||||
|  |           delay(10); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     // init sequence no longer needed | ||||||
|  |     this->init_sequence_.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Drawing operations | ||||||
|  |  | ||||||
|  |   void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, | ||||||
|  |                       display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override { | ||||||
|  |     if (this->is_failed()) | ||||||
|  |       return; | ||||||
|  |     if (w <= 0 || h <= 0) | ||||||
|  |       return; | ||||||
|  |     if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) { | ||||||
|  |       // note that the usual logging macros are banned in header files, so use their replacement | ||||||
|  |       esph_log_e(TAG, "Unsupported color depth or bit order"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset, | ||||||
|  |                             x_pad); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void dump_config() override { | ||||||
|  |     esph_log_config(TAG, | ||||||
|  |                     "MIPI_SPI Display\n" | ||||||
|  |                     "  Model: %s\n" | ||||||
|  |                     "  Width: %u\n" | ||||||
|  |                     "  Height: %u", | ||||||
|  |                     this->model_, WIDTH, HEIGHT); | ||||||
|  |     if constexpr (OFFSET_WIDTH != 0) | ||||||
|  |       esph_log_config(TAG, "  Offset width: %u", OFFSET_WIDTH); | ||||||
|  |     if constexpr (OFFSET_HEIGHT != 0) | ||||||
|  |       esph_log_config(TAG, "  Offset height: %u", OFFSET_HEIGHT); | ||||||
|  |     esph_log_config(TAG, | ||||||
|  |                     "  Swap X/Y: %s\n" | ||||||
|  |                     "  Mirror X: %s\n" | ||||||
|  |                     "  Mirror Y: %s\n" | ||||||
|  |                     "  Invert colors: %s\n" | ||||||
|  |                     "  Color order: %s\n" | ||||||
|  |                     "  Display pixels: %d bits\n" | ||||||
|  |                     "  Endianness: %s\n", | ||||||
|  |                     YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), | ||||||
|  |                     YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_), | ||||||
|  |                     this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); | ||||||
|  |     if (this->brightness_.has_value()) | ||||||
|  |       esph_log_config(TAG, "  Brightness: %u", this->brightness_.value()); | ||||||
|  |     if (this->cs_ != nullptr) | ||||||
|  |       esph_log_config(TAG, "  CS Pin: %s", this->cs_->dump_summary().c_str()); | ||||||
|  |     if (this->reset_pin_ != nullptr) | ||||||
|  |       esph_log_config(TAG, "  Reset Pin: %s", this->reset_pin_->dump_summary().c_str()); | ||||||
|  |     if (this->dc_pin_ != nullptr) | ||||||
|  |       esph_log_config(TAG, "  DC Pin: %s", this->dc_pin_->dump_summary().c_str()); | ||||||
|  |     esph_log_config(TAG, | ||||||
|  |                     "  SPI Mode: %d\n" | ||||||
|  |                     "  SPI Data rate: %dMHz\n" | ||||||
|  |                     "  SPI Bus width: %d", | ||||||
|  |                     this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool check_buffer_() { |   /* METHODS */ | ||||||
|     if (this->is_failed()) |   // convenience functions to write commands with or without data | ||||||
|       return false; |  | ||||||
|     if (this->buffer_ != nullptr) |  | ||||||
|       return true; |  | ||||||
|     auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1; |  | ||||||
|     this->init_internal_(this->width_ * this->height_ * bytes_per_pixel); |  | ||||||
|     if (this->buffer_ == nullptr) { |  | ||||||
|       this->mark_failed(); |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel; |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|   void fill(Color color) override; |  | ||||||
|   void draw_absolute_pixel_internal(int x, int y, Color color) override; |  | ||||||
|   void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, |  | ||||||
|                       display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; |  | ||||||
|   void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride); |  | ||||||
|   void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); |  | ||||||
|   void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); |  | ||||||
|   void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, |  | ||||||
|                          int x_pad); |  | ||||||
|   /** |  | ||||||
|    * the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the |  | ||||||
|    * sample code.) |  | ||||||
|    * |  | ||||||
|    * Immediately after enabling /CS send 4 bytes in single-dataline SPI mode: |  | ||||||
|    *    0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be |  | ||||||
|    *        sent in 1-dataline SPI. The second indicates quad mode. |  | ||||||
|    *    1: 0x00 |  | ||||||
|    *    2: The command (register address) byte. |  | ||||||
|    *    3: 0x00 |  | ||||||
|    * |  | ||||||
|    *    This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte. |  | ||||||
|    *    At the conclusion of the write, de-assert /CS. |  | ||||||
|    * |  | ||||||
|    * @param cmd |  | ||||||
|    * @param bytes |  | ||||||
|    * @param len |  | ||||||
|    */ |  | ||||||
|   void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len); |  | ||||||
|  |  | ||||||
|   void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } |   void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } | ||||||
|   void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } |   void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } | ||||||
|   void reset_params_(); |  | ||||||
|   void write_init_sequence_(); |  | ||||||
|   void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2); |  | ||||||
|  |  | ||||||
|  |   // Writes a command to the display, with the given bytes. | ||||||
|  |   void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { | ||||||
|  |     esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); | ||||||
|  |     if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { | ||||||
|  |       this->enable(); | ||||||
|  |       this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); | ||||||
|  |       this->disable(); | ||||||
|  |     } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { | ||||||
|  |       this->dc_pin_->digital_write(false); | ||||||
|  |       this->enable(); | ||||||
|  |       this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); | ||||||
|  |       this->disable(); | ||||||
|  |       this->dc_pin_->digital_write(true); | ||||||
|  |       if (len != 0) { | ||||||
|  |         this->enable(); | ||||||
|  |         this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); | ||||||
|  |         this->disable(); | ||||||
|  |       } | ||||||
|  |     } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) { | ||||||
|  |       this->dc_pin_->digital_write(false); | ||||||
|  |       this->enable(); | ||||||
|  |       this->write_byte(cmd); | ||||||
|  |       this->disable(); | ||||||
|  |       this->dc_pin_->digital_write(true); | ||||||
|  |       if (len != 0) { | ||||||
|  |         this->enable(); | ||||||
|  |         this->write_array(bytes, len); | ||||||
|  |         this->disable(); | ||||||
|  |       } | ||||||
|  |     } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) { | ||||||
|  |       this->dc_pin_->digital_write(false); | ||||||
|  |       this->enable(); | ||||||
|  |       this->write_byte(cmd); | ||||||
|  |       this->disable(); | ||||||
|  |       this->dc_pin_->digital_write(true); | ||||||
|  |       for (size_t i = 0; i != len; i++) { | ||||||
|  |         this->enable(); | ||||||
|  |         this->write_byte(0); | ||||||
|  |         this->write_byte(bytes[i]); | ||||||
|  |         this->disable(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // write changed parameters to the display | ||||||
|  |   void reset_params_() { | ||||||
|  |     if (!this->is_ready()) | ||||||
|  |       return; | ||||||
|  |     this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); | ||||||
|  |     if (this->brightness_.has_value()) | ||||||
|  |       this->write_command_(BRIGHTNESS, this->brightness_.value()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // set the address window for the next data write | ||||||
|  |   void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { | ||||||
|  |     esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); | ||||||
|  |     uint8_t buf[4]; | ||||||
|  |     x1 += OFFSET_WIDTH; | ||||||
|  |     x2 += OFFSET_WIDTH; | ||||||
|  |     y1 += OFFSET_HEIGHT; | ||||||
|  |     y2 += OFFSET_HEIGHT; | ||||||
|  |     put16_be(buf, y1); | ||||||
|  |     put16_be(buf + 2, y2); | ||||||
|  |     this->write_command_(RASET, buf, sizeof buf); | ||||||
|  |     put16_be(buf, x1); | ||||||
|  |     put16_be(buf + 2, x2); | ||||||
|  |     this->write_command_(CASET, buf, sizeof buf); | ||||||
|  |     if constexpr (BUS_TYPE != BUS_TYPE_QUAD) { | ||||||
|  |       this->write_command_(WDATA); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // map the display color bitness to the pixel mode | ||||||
|  |   static PixelMode get_pixel_mode(display::ColorBitness bitness) { | ||||||
|  |     switch (bitness) { | ||||||
|  |       case display::COLOR_BITNESS_888: | ||||||
|  |         return PIXEL_MODE_18;  // 18 bits per pixel | ||||||
|  |       case display::COLOR_BITNESS_565: | ||||||
|  |         return PIXEL_MODE_16;  // 16 bits per pixel | ||||||
|  |       default: | ||||||
|  |         return PIXEL_MODE_8;  // Default to 8 bits per pixel | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Writes a buffer to the display. | ||||||
|  |    * @param w Width of each line in bytes | ||||||
|  |    * @param h Height of the buffer in rows | ||||||
|  |    * @param pad Padding in bytes after each line | ||||||
|  |    */ | ||||||
|  |   void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) { | ||||||
|  |     if (pad == 0) { | ||||||
|  |       if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { | ||||||
|  |         this->write_array(ptr, w * h); | ||||||
|  |       } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { | ||||||
|  |         this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4); | ||||||
|  |       } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { | ||||||
|  |         this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       for (size_t y = 0; y != h; y++) { | ||||||
|  |         if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { | ||||||
|  |           this->write_array(ptr, w); | ||||||
|  |         } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { | ||||||
|  |           this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4); | ||||||
|  |         } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { | ||||||
|  |           this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8); | ||||||
|  |         } | ||||||
|  |         ptr += w + pad; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Writes a buffer to the display. | ||||||
|  |    * | ||||||
|  |    * The ptr is a pointer to the pixel data | ||||||
|  |    * The other parameters are all in pixel units. | ||||||
|  |    */ | ||||||
|  |   void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset, | ||||||
|  |                          int x_pad) { | ||||||
|  |     this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); | ||||||
|  |     this->enable(); | ||||||
|  |     ptr += y_offset * (x_offset + w + x_pad) + x_offset; | ||||||
|  |     if constexpr (BUFFERPIXEL == DISPLAYPIXEL) { | ||||||
|  |       this->write_display_data_(reinterpret_cast<const uint8_t *>(ptr), w * sizeof(BUFFERTYPE), h, | ||||||
|  |                                 x_pad * sizeof(BUFFERTYPE)); | ||||||
|  |     } else { | ||||||
|  |       // type conversion required, do it in chunks | ||||||
|  |       uint8_t dbuffer[DISPLAYPIXEL * 48]; | ||||||
|  |       uint8_t *dptr = dbuffer; | ||||||
|  |       auto stride = x_offset + w + x_pad;  // stride in pixels | ||||||
|  |       for (size_t y = 0; y != h; y++) { | ||||||
|  |         for (size_t x = 0; x != w; x++) { | ||||||
|  |           auto color_val = ptr[y * stride + x]; | ||||||
|  |           if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { | ||||||
|  |             // 16 to 18 bit conversion | ||||||
|  |             if constexpr (IS_BIG_ENDIAN) { | ||||||
|  |               *dptr++ = color_val & 0xF8; | ||||||
|  |               *dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11; | ||||||
|  |               *dptr++ = (color_val >> 5) & 0xF8; | ||||||
|  |             } else { | ||||||
|  |               *dptr++ = (color_val >> 8) & 0xF8;  // Blue | ||||||
|  |               *dptr++ = (color_val & 0x7E0) >> 3; | ||||||
|  |               *dptr++ = color_val << 3; | ||||||
|  |             } | ||||||
|  |           } else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) { | ||||||
|  |             // 8 bit to 18 bit conversion | ||||||
|  |             *dptr++ = color_val << 6;           // Blue | ||||||
|  |             *dptr++ = (color_val & 0x1C) << 3;  // Green | ||||||
|  |             *dptr++ = (color_val & 0xE0);       // Red | ||||||
|  |           } else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) { | ||||||
|  |             if constexpr (IS_BIG_ENDIAN) { | ||||||
|  |               *dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); | ||||||
|  |               *dptr++ = (color_val & 3) << 3; | ||||||
|  |             } else { | ||||||
|  |               *dptr++ = (color_val & 3) << 3; | ||||||
|  |               *dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           // buffer full? Flush. | ||||||
|  |           if (dptr == dbuffer + sizeof(dbuffer)) { | ||||||
|  |             this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0); | ||||||
|  |             dptr = dbuffer; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // flush any remaining data | ||||||
|  |       if (dptr != dbuffer) { | ||||||
|  |         this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     this->disable(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* PROPERTIES */ | ||||||
|  |  | ||||||
|  |   // GPIO pins | ||||||
|   GPIOPin *reset_pin_{nullptr}; |   GPIOPin *reset_pin_{nullptr}; | ||||||
|   std::vector<GPIOPin *> enable_pins_{}; |   std::vector<GPIOPin *> enable_pins_{}; | ||||||
|   GPIOPin *dc_pin_{nullptr}; |   GPIOPin *dc_pin_{nullptr}; | ||||||
|   uint16_t x_low_{1}; |  | ||||||
|   uint16_t y_low_{1}; |  | ||||||
|   uint16_t x_high_{0}; |  | ||||||
|   uint16_t y_high_{0}; |  | ||||||
|   bool setup_complete_{}; |  | ||||||
|  |  | ||||||
|  |   // other properties set by configuration | ||||||
|   bool invert_colors_{}; |   bool invert_colors_{}; | ||||||
|   size_t width_; |  | ||||||
|   size_t height_; |  | ||||||
|   int16_t offset_width_; |  | ||||||
|   int16_t offset_height_; |  | ||||||
|   size_t buffer_bytes_{0}; |  | ||||||
|   display::ColorBitness color_depth_; |  | ||||||
|   PixelMode pixel_mode_{PIXEL_MODE_16}; |  | ||||||
|   uint8_t bus_width_{}; |  | ||||||
|   bool spi_16_{}; |  | ||||||
|   uint8_t madctl_{}; |  | ||||||
|   bool draw_from_origin_{false}; |  | ||||||
|   unsigned draw_rounding_{2}; |   unsigned draw_rounding_{2}; | ||||||
|   optional<uint8_t> brightness_{}; |   optional<uint8_t> brightness_{}; | ||||||
|   const char *model_{"Unknown"}; |   const char *model_{"Unknown"}; | ||||||
|   std::vector<uint8_t> init_sequence_{}; |   std::vector<uint8_t> init_sequence_{}; | ||||||
|  |   uint8_t madctl_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class for MIPI SPI displays with a buffer. | ||||||
|  |  * | ||||||
|  |  * @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t | ||||||
|  |  * @tparam BUFFERPIXEL Color depth of the buffer | ||||||
|  |  * @tparam DISPLAYPIXEL Color depth of the display | ||||||
|  |  * @tparam BUS_TYPE The type of the interface bus (single, quad, octal) | ||||||
|  |  * @tparam ROTATION The rotation of the display | ||||||
|  |  * @tparam WIDTH Width of the display in pixels | ||||||
|  |  * @tparam HEIGHT Height of the display in pixels | ||||||
|  |  * @tparam OFFSET_WIDTH The x-offset of the display in pixels | ||||||
|  |  * @tparam OFFSET_HEIGHT The y-offset of the display in pixels | ||||||
|  |  * @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer). | ||||||
|  |  */ | ||||||
|  | template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE, | ||||||
|  |          int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION> | ||||||
|  | class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, | ||||||
|  |                                      OFFSET_WIDTH, OFFSET_HEIGHT> { | ||||||
|  |  public: | ||||||
|  |   MipiSpiBuffer() { this->rotation_ = ROTATION; } | ||||||
|  |  | ||||||
|  |   void dump_config() override { | ||||||
|  |     MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, | ||||||
|  |             OFFSET_HEIGHT>::dump_config(); | ||||||
|  |     esph_log_config(TAG, | ||||||
|  |                     "  Rotation: %d°\n" | ||||||
|  |                     "  Buffer pixels: %d bits\n" | ||||||
|  |                     "  Buffer fraction: 1/%d\n" | ||||||
|  |                     "  Buffer bytes: %zu\n" | ||||||
|  |                     "  Draw rounding: %u", | ||||||
|  |                     this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION, | ||||||
|  |                     this->draw_rounding_); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void setup() override { | ||||||
|  |     MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, | ||||||
|  |             OFFSET_HEIGHT>::setup(); | ||||||
|  |     RAMAllocator<BUFFERTYPE> allocator{}; | ||||||
|  |     this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION); | ||||||
|  |     if (this->buffer_ == nullptr) { | ||||||
|  |       this->mark_failed("Buffer allocation failed"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void update() override { | ||||||
|  | #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||||
|  |     auto now = millis(); | ||||||
|  | #endif | ||||||
|  |     if (this->is_failed()) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     // for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of | ||||||
|  |     // the display height, | ||||||
|  |     for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) { | ||||||
|  | #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||||
|  |       auto lap = millis(); | ||||||
|  | #endif | ||||||
|  |       this->end_line_ = this->start_line_ + HEIGHT / FRACTION; | ||||||
|  |       if (this->auto_clear_enabled_) { | ||||||
|  |         this->clear(); | ||||||
|  |       } | ||||||
|  |       if (this->page_ != nullptr) { | ||||||
|  |         this->page_->get_writer()(*this); | ||||||
|  |       } else if (this->writer_.has_value()) { | ||||||
|  |         (*this->writer_)(*this); | ||||||
|  |       } else { | ||||||
|  |         this->test_card(); | ||||||
|  |       } | ||||||
|  | #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||||
|  |       esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap); | ||||||
|  |       lap = millis(); | ||||||
|  | #endif | ||||||
|  |       if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) | ||||||
|  |         return; | ||||||
|  |       esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, | ||||||
|  |                  this->y_high_); | ||||||
|  |       // Some chips require that the drawing window be aligned on certain boundaries | ||||||
|  |       auto dr = this->draw_rounding_; | ||||||
|  |       this->x_low_ = this->x_low_ / dr * dr; | ||||||
|  |       this->y_low_ = this->y_low_ / dr * dr; | ||||||
|  |       this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; | ||||||
|  |       this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; | ||||||
|  |       int w = this->x_high_ - this->x_low_ + 1; | ||||||
|  |       int h = this->y_high_ - this->y_low_ + 1; | ||||||
|  |       this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, | ||||||
|  |                               this->y_low_ - this->start_line_, WIDTH - w); | ||||||
|  |       // invalidate watermarks | ||||||
|  |       this->x_low_ = WIDTH; | ||||||
|  |       this->y_low_ = HEIGHT; | ||||||
|  |       this->x_high_ = 0; | ||||||
|  |       this->y_high_ = 0; | ||||||
|  | #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||||
|  |       esph_log_v(TAG, "Write to display took %dms", millis() - lap); | ||||||
|  |       lap = millis(); | ||||||
|  | #endif | ||||||
|  |     } | ||||||
|  | #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE | ||||||
|  |     esph_log_v(TAG, "Total update took %dms", millis() - now); | ||||||
|  | #endif | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Draw a pixel at the given coordinates. | ||||||
|  |   void draw_pixel_at(int x, int y, Color color) override { | ||||||
|  |     rotate_coordinates_(x, y); | ||||||
|  |     if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_) | ||||||
|  |       return; | ||||||
|  |     this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color); | ||||||
|  |     if (x < this->x_low_) { | ||||||
|  |       this->x_low_ = x; | ||||||
|  |     } | ||||||
|  |     if (x > this->x_high_) { | ||||||
|  |       this->x_high_ = x; | ||||||
|  |     } | ||||||
|  |     if (y < this->y_low_) { | ||||||
|  |       this->y_low_ = y; | ||||||
|  |     } | ||||||
|  |     if (y > this->y_high_) { | ||||||
|  |       this->y_high_ = y; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Fills the display with a color. | ||||||
|  |   void fill(Color color) override { | ||||||
|  |     this->x_low_ = 0; | ||||||
|  |     this->y_low_ = this->start_line_; | ||||||
|  |     this->x_high_ = WIDTH - 1; | ||||||
|  |     this->y_high_ = this->end_line_ - 1; | ||||||
|  |     std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   int get_width() override { | ||||||
|  |     if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) | ||||||
|  |       return HEIGHT; | ||||||
|  |     return WIDTH; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   int get_height() override { | ||||||
|  |     if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) | ||||||
|  |       return WIDTH; | ||||||
|  |     return HEIGHT; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   // Rotate the coordinates to match the display orientation. | ||||||
|  |   void rotate_coordinates_(int &x, int &y) const { | ||||||
|  |     if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) { | ||||||
|  |       x = WIDTH - x - 1; | ||||||
|  |       y = HEIGHT - y - 1; | ||||||
|  |     } else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) { | ||||||
|  |       auto tmp = x; | ||||||
|  |       x = WIDTH - y - 1; | ||||||
|  |       y = tmp; | ||||||
|  |     } else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) { | ||||||
|  |       auto tmp = y; | ||||||
|  |       y = HEIGHT - x - 1; | ||||||
|  |       x = tmp; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Convert a color to the buffer pixel format. | ||||||
|  |   BUFFERTYPE convert_color_(Color &color) const { | ||||||
|  |     if constexpr (BUFFERPIXEL == PIXEL_MODE_8) { | ||||||
|  |       return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6; | ||||||
|  |     } else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) { | ||||||
|  |       if constexpr (IS_BIG_ENDIAN) { | ||||||
|  |         return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5; | ||||||
|  |       } else { | ||||||
|  |         return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return static_cast<BUFFERTYPE>(0); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   BUFFERTYPE *buffer_{}; | ||||||
|  |   uint16_t x_low_{WIDTH}; | ||||||
|  |   uint16_t y_low_{HEIGHT}; | ||||||
|  |   uint16_t x_high_{0}; | ||||||
|  |   uint16_t y_high_{0}; | ||||||
|  |   uint16_t start_line_{0}; | ||||||
|  |   uint16_t end_line_{1}; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace mipi_spi | }  // namespace mipi_spi | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								esphome/components/mipi_spi/models/adafruit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/mipi_spi/models/adafruit.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | from .ili import ST7789V | ||||||
|  |  | ||||||
|  | ST7789V.extend( | ||||||
|  |     "ADAFRUIT-FUNHOUSE", | ||||||
|  |     height=240, | ||||||
|  |     width=240, | ||||||
|  |     offset_height=0, | ||||||
|  |     offset_width=0, | ||||||
|  |     cs_pin=40, | ||||||
|  |     dc_pin=39, | ||||||
|  |     reset_pin=41, | ||||||
|  |     invert_colors=True, | ||||||
|  |     mirror_x=True, | ||||||
|  |     mirror_y=True, | ||||||
|  |     data_rate="80MHz", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | ST7789V.extend( | ||||||
|  |     "ADAFRUIT-S2-TFT-FEATHER", | ||||||
|  |     height=240, | ||||||
|  |     width=135, | ||||||
|  |     offset_height=52, | ||||||
|  |     offset_width=40, | ||||||
|  |     cs_pin=7, | ||||||
|  |     dc_pin=39, | ||||||
|  |     reset_pin=40, | ||||||
|  |     invert_colors=True, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | models = {} | ||||||
| @@ -67,6 +67,14 @@ RM690B0 = DriverChip( | |||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
|  |  | ||||||
| T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) | T4_S3_AMOLED = RM690B0.extend( | ||||||
|  |     "T4-S3", | ||||||
|  |     width=450, | ||||||
|  |     offset_width=16, | ||||||
|  |     cs_pin=11, | ||||||
|  |     reset_pin=13, | ||||||
|  |     enable_pin=9, | ||||||
|  |     bus_mode=TYPE_QUAD, | ||||||
|  | ) | ||||||
|  |  | ||||||
| models = {} | models = {} | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import esphome.config_validation as cv | ||||||
|  |  | ||||||
| from . import DriverChip | from . import DriverChip | ||||||
| from .ili import ILI9488_A | from .ili import ILI9488_A | ||||||
|  |  | ||||||
| @@ -128,6 +130,7 @@ DriverChip( | |||||||
|  |  | ||||||
| ILI9488_A.extend( | ILI9488_A.extend( | ||||||
|     "PICO-RESTOUCH-LCD-3.5", |     "PICO-RESTOUCH-LCD-3.5", | ||||||
|  |     swap_xy=cv.UNDEFINED, | ||||||
|     spi_16=True, |     spi_16=True, | ||||||
|     pixel_mode="16bit", |     pixel_mode="16bit", | ||||||
|     mirror_x=True, |     mirror_x=True, | ||||||
|   | |||||||
| @@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to<JsonArray>(); | ||||||
|   const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); |   const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); | ||||||
|   if (acp_supported_features & ACP_FEAT_ARM_AWAY) { |   if (acp_supported_features & ACP_FEAT_ARM_AWAY) { | ||||||
|     supported_features.add("arm_away"); |     supported_features.add("arm_away"); | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (!this->binary_sensor_->get_device_class().empty()) |   if (!this->binary_sensor_->get_device_class().empty()) | ||||||
|     root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); | ||||||
|   if (this->binary_sensor_->is_status_binary_sensor()) |   if (this->binary_sensor_->is_status_binary_sensor()) | ||||||
|   | |||||||
| @@ -31,10 +31,13 @@ void MQTTButtonComponent::dump_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   config.state_topic = false; |   config.state_topic = false; | ||||||
|   if (!this->button_->get_device_class().empty()) |   if (!this->button_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); | ||||||
|   } |   } | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
|  | } | ||||||
|  |  | ||||||
| std::string MQTTButtonComponent::component_type() const { return "button"; } | std::string MQTTButtonComponent::component_type() const { return "button"; } | ||||||
| const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } | const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } | ||||||
|   | |||||||
| @@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() { | |||||||
|   std::string topic = "esphome/discover/"; |   std::string topic = "esphome/discover/"; | ||||||
|   topic.append(App.get_name()); |   topic.append(App.get_name()); | ||||||
|  |  | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   this->publish_json( |   this->publish_json( | ||||||
|       topic, |       topic, | ||||||
|       [](JsonObject root) { |       [](JsonObject root) { | ||||||
| @@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() { | |||||||
| #endif | #endif | ||||||
|       }, |       }, | ||||||
|       2, this->discovery_info_.retain); |       2, this->discovery_info_.retain); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTClientComponent::dump_config() { | void MQTTClientComponent::dump_config() { | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate"; | |||||||
| using namespace esphome::climate; | using namespace esphome::climate; | ||||||
|  |  | ||||||
| void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   auto traits = this->device_->get_traits(); |   auto traits = this->device_->get_traits(); | ||||||
|   // current_temperature_topic |   // current_temperature_topic | ||||||
|   if (traits.get_supports_current_temperature()) { |   if (traits.get_supports_current_temperature()) { | ||||||
| @@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|   // mode_state_topic |   // mode_state_topic | ||||||
|   root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); |   root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); | ||||||
|   // modes |   // modes | ||||||
|   JsonArray modes = root.createNestedArray(MQTT_MODES); |   JsonArray modes = root[MQTT_MODES].to<JsonArray>(); | ||||||
|   // sort array for nice UI in HA |   // sort array for nice UI in HA | ||||||
|   if (traits.supports_mode(CLIMATE_MODE_AUTO)) |   if (traits.supports_mode(CLIMATE_MODE_AUTO)) | ||||||
|     modes.add("auto"); |     modes.add("auto"); | ||||||
| @@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|     // preset_mode_state_topic |     // preset_mode_state_topic | ||||||
|     root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); |     root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); | ||||||
|     // presets |     // presets | ||||||
|     JsonArray presets = root.createNestedArray("preset_modes"); |     JsonArray presets = root["preset_modes"].to<JsonArray>(); | ||||||
|     if (traits.supports_preset(CLIMATE_PRESET_HOME)) |     if (traits.supports_preset(CLIMATE_PRESET_HOME)) | ||||||
|       presets.add("home"); |       presets.add("home"); | ||||||
|     if (traits.supports_preset(CLIMATE_PRESET_AWAY)) |     if (traits.supports_preset(CLIMATE_PRESET_AWAY)) | ||||||
| @@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|     // fan_mode_state_topic |     // fan_mode_state_topic | ||||||
|     root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); |     root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); | ||||||
|     // fan_modes |     // fan_modes | ||||||
|     JsonArray fan_modes = root.createNestedArray("fan_modes"); |     JsonArray fan_modes = root["fan_modes"].to<JsonArray>(); | ||||||
|     if (traits.supports_fan_mode(CLIMATE_FAN_ON)) |     if (traits.supports_fan_mode(CLIMATE_FAN_ON)) | ||||||
|       fan_modes.add("on"); |       fan_modes.add("on"); | ||||||
|     if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) |     if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) | ||||||
| @@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|     // swing_mode_state_topic |     // swing_mode_state_topic | ||||||
|     root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); |     root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); | ||||||
|     // swing_modes |     // swing_modes | ||||||
|     JsonArray swing_modes = root.createNestedArray("swing_modes"); |     JsonArray swing_modes = root["swing_modes"].to<JsonArray>(); | ||||||
|     if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) |     if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) | ||||||
|       swing_modes.add("off"); |       swing_modes.add("off"); | ||||||
|     if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) |     if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) | ||||||
| @@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|  |  | ||||||
|   config.state_topic = false; |   config.state_topic = false; | ||||||
|   config.command_topic = false; |   config.command_topic = false; | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
| void MQTTClimateComponent::setup() { | void MQTTClimateComponent::setup() { | ||||||
|   auto traits = this->device_->get_traits(); |   auto traits = this->device_->get_traits(); | ||||||
|   | |||||||
| @@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() { | |||||||
|  |  | ||||||
|   ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); |   ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); | ||||||
|  |  | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   return global_mqtt_client->publish_json( |   return global_mqtt_client->publish_json( | ||||||
|       this->get_discovery_topic_(discovery_info), |       this->get_discovery_topic_(discovery_info), | ||||||
|       [this](JsonObject root) { |       [this](JsonObject root) { | ||||||
| @@ -150,7 +151,7 @@ bool MQTTComponent::send_discovery_() { | |||||||
|         } |         } | ||||||
|         std::string node_area = App.get_area(); |         std::string node_area = App.get_area(); | ||||||
|  |  | ||||||
|         JsonObject device_info = root.createNestedObject(MQTT_DEVICE); |         JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>(); | ||||||
|         const auto mac = get_mac_address(); |         const auto mac = get_mac_address(); | ||||||
|         device_info[MQTT_DEVICE_IDENTIFIERS] = mac; |         device_info[MQTT_DEVICE_IDENTIFIERS] = mac; | ||||||
|         device_info[MQTT_DEVICE_NAME] = node_friendly_name; |         device_info[MQTT_DEVICE_NAME] = node_friendly_name; | ||||||
| @@ -187,6 +188,7 @@ bool MQTTComponent::send_discovery_() { | |||||||
|         device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; |         device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; | ||||||
|       }, |       }, | ||||||
|       this->qos_, discovery_info.retain); |       this->qos_, discovery_info.retain); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
|  |  | ||||||
| uint8_t MQTTComponent::get_qos() const { return this->qos_; } | uint8_t MQTTComponent::get_qos() const { return this->qos_; } | ||||||
|   | |||||||
| @@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (!this->cover_->get_device_class().empty()) |   if (!this->cover_->get_device_class().empty()) | ||||||
|     root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {} | |||||||
| void MQTTDateComponent::setup() { | void MQTTDateComponent::setup() { | ||||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { |   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||||
|     auto call = this->date_->make_call(); |     auto call = this->date_->make_call(); | ||||||
|     if (root.containsKey("year")) { |     if (root["year"].is<uint16_t>()) { | ||||||
|       call.set_year(root["year"]); |       call.set_year(root["year"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("month")) { |     if (root["month"].is<uint8_t>()) { | ||||||
|       call.set_month(root["month"]); |       call.set_month(root["month"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("day")) { |     if (root["day"].is<uint8_t>()) { | ||||||
|       call.set_day(root["day"]); |       call.set_day(root["day"]); | ||||||
|     } |     } | ||||||
|     call.perform(); |     call.perform(); | ||||||
| @@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() { | |||||||
| } | } | ||||||
| bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { | bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { | ||||||
|   return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { |   return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { | ||||||
|  |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     root["year"] = year; |     root["year"] = year; | ||||||
|     root["month"] = month; |     root["month"] = month; | ||||||
|     root["day"] = day; |     root["day"] = day; | ||||||
|   | |||||||
| @@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim | |||||||
| void MQTTDateTimeComponent::setup() { | void MQTTDateTimeComponent::setup() { | ||||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { |   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||||
|     auto call = this->datetime_->make_call(); |     auto call = this->datetime_->make_call(); | ||||||
|     if (root.containsKey("year")) { |     if (root["year"].is<uint16_t>()) { | ||||||
|       call.set_year(root["year"]); |       call.set_year(root["year"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("month")) { |     if (root["month"].is<uint8_t>()) { | ||||||
|       call.set_month(root["month"]); |       call.set_month(root["month"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("day")) { |     if (root["day"].is<uint8_t>()) { | ||||||
|       call.set_day(root["day"]); |       call.set_day(root["day"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("hour")) { |     if (root["hour"].is<uint8_t>()) { | ||||||
|       call.set_hour(root["hour"]); |       call.set_hour(root["hour"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("minute")) { |     if (root["minute"].is<uint8_t>()) { | ||||||
|       call.set_minute(root["minute"]); |       call.set_minute(root["minute"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("second")) { |     if (root["second"].is<uint8_t>()) { | ||||||
|       call.set_second(root["second"]); |       call.set_second(root["second"]); | ||||||
|     } |     } | ||||||
|     call.perform(); |     call.perform(); | ||||||
| @@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() { | |||||||
| bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, | bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, | ||||||
|                                           uint8_t second) { |                                           uint8_t second) { | ||||||
|   return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { |   return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { | ||||||
|  |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     root["year"] = year; |     root["year"] = year; | ||||||
|     root["month"] = month; |     root["month"] = month; | ||||||
|     root["day"] = day; |     root["day"] = day; | ||||||
|   | |||||||
| @@ -16,7 +16,8 @@ using namespace esphome::event; | |||||||
| MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} | MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} | ||||||
|  |  | ||||||
| void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray event_types = root[MQTT_EVENT_TYPES].to<JsonArray>(); | ||||||
|   for (const auto &event_type : this->event_->get_event_types()) |   for (const auto &event_type : this->event_->get_event_types()) | ||||||
|     event_types.add(event_type); |     event_types.add(event_type); | ||||||
|  |  | ||||||
| @@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| bool MQTTEventComponent::publish_event_(const std::string &event_type) { | bool MQTTEventComponent::publish_event_(const std::string &event_type) { | ||||||
|   return this->publish_json(this->get_state_topic_(), |   return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) { | ||||||
|                             [event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |     root[MQTT_EVENT_TYPE] = event_type; | ||||||
|  |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| std::string MQTTEventComponent::component_type() const { return "event"; } | std::string MQTTEventComponent::component_type() const { return "event"; } | ||||||
|   | |||||||
| @@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() { | |||||||
| bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } | bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } | ||||||
|  |  | ||||||
| void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (this->state_->get_traits().supports_direction()) { |   if (this->state_->get_traits().supports_direction()) { | ||||||
|     root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); |     root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); | ||||||
|     root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); |     root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); | ||||||
|   | |||||||
| @@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() { | |||||||
| MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} | MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} | ||||||
|  |  | ||||||
| bool MQTTJSONLightComponent::publish_state_() { | bool MQTTJSONLightComponent::publish_state_() { | ||||||
|   return this->publish_json(this->get_state_topic_(), |   return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { | ||||||
|                             [this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |     LightJSONSchema::dump_json(*this->state_, root); | ||||||
|  |   }); | ||||||
| } | } | ||||||
| LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } | LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } | ||||||
|  |  | ||||||
| void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   root["schema"] = "json"; |   root["schema"] = "json"; | ||||||
|   auto traits = this->state_->get_traits(); |   auto traits = this->state_->get_traits(); | ||||||
|  |  | ||||||
|   root[MQTT_COLOR_MODE] = true; |   root[MQTT_COLOR_MODE] = true; | ||||||
|   JsonArray color_modes = root.createNestedArray("supported_color_modes"); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray color_modes = root["supported_color_modes"].to<JsonArray>(); | ||||||
|   if (traits.supports_color_mode(ColorMode::ON_OFF)) |   if (traits.supports_color_mode(ColorMode::ON_OFF)) | ||||||
|     color_modes.add("onoff"); |     color_modes.add("onoff"); | ||||||
|   if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) |   if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) | ||||||
| @@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery | |||||||
|  |  | ||||||
|   if (this->state_->supports_effects()) { |   if (this->state_->supports_effects()) { | ||||||
|     root["effect"] = true; |     root["effect"] = true; | ||||||
|     JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST); |     JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>(); | ||||||
|     for (auto *effect : this->state_->get_effects()) |     for (auto *effect : this->state_->get_effects()) | ||||||
|       effect_list.add(effect->get_name()); |       effect_list.add(effect->get_name()); | ||||||
|     effect_list.add("None"); |     effect_list.add("None"); | ||||||
|   | |||||||
| @@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() { | |||||||
| std::string MQTTLockComponent::component_type() const { return "lock"; } | std::string MQTTLockComponent::component_type() const { return "lock"; } | ||||||
| const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } | const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } | ||||||
| void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (this->lock_->traits.get_assumed_state()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (this->lock_->traits.get_assumed_state()) { | ||||||
|     root[MQTT_OPTIMISTIC] = true; |     root[MQTT_OPTIMISTIC] = true; | ||||||
|  |   } | ||||||
|   if (this->lock_->traits.get_supports_open()) |   if (this->lock_->traits.get_supports_open()) | ||||||
|     root[MQTT_PAYLOAD_OPEN] = "OPEN"; |     root[MQTT_PAYLOAD_OPEN] = "OPEN"; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_ | |||||||
| void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   const auto &traits = number_->traits; |   const auto &traits = number_->traits; | ||||||
|   // https://www.home-assistant.io/integrations/number.mqtt/ |   // https://www.home-assistant.io/integrations/number.mqtt/ | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   root[MQTT_MIN] = traits.get_min_value(); |   root[MQTT_MIN] = traits.get_min_value(); | ||||||
|   root[MQTT_MAX] = traits.get_max_value(); |   root[MQTT_MAX] = traits.get_max_value(); | ||||||
|   root[MQTT_STEP] = traits.get_step(); |   root[MQTT_STEP] = traits.get_step(); | ||||||
|   | |||||||
| @@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_ | |||||||
| void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   const auto &traits = select_->traits; |   const auto &traits = select_->traits; | ||||||
|   // https://www.home-assistant.io/integrations/select.mqtt/ |   // https://www.home-assistant.io/integrations/select.mqtt/ | ||||||
|   JsonArray options = root.createNestedArray(MQTT_OPTIONS); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray options = root[MQTT_OPTIONS].to<JsonArray>(); | ||||||
|   for (const auto &option : traits.get_options()) |   for (const auto &option : traits.get_options()) | ||||||
|     options.add(option); |     options.add(option); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire | |||||||
| void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } | void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } | ||||||
|  |  | ||||||
| void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (!this->sensor_->get_device_class().empty()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (!this->sensor_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (!this->sensor_->get_unit_of_measurement().empty()) |   if (!this->sensor_->get_unit_of_measurement().empty()) | ||||||
|     root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); |     root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); | ||||||
|   | |||||||
| @@ -45,9 +45,11 @@ void MQTTSwitchComponent::dump_config() { | |||||||
| std::string MQTTSwitchComponent::component_type() const { return "switch"; } | std::string MQTTSwitchComponent::component_type() const { return "switch"; } | ||||||
| const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } | const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } | ||||||
| void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (this->switch_->assumed_state()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (this->switch_->assumed_state()) { | ||||||
|     root[MQTT_OPTIMISTIC] = true; |     root[MQTT_OPTIMISTIC] = true; | ||||||
|   } |   } | ||||||
|  | } | ||||||
| bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } | bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } | ||||||
|  |  | ||||||
| bool MQTTSwitchComponent::publish_state(bool state) { | bool MQTTSwitchComponent::publish_state(bool state) { | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; } | |||||||
| const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } | const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } | ||||||
|  |  | ||||||
| void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   switch (this->text_->traits.get_mode()) { |   switch (this->text_->traits.get_mode()) { | ||||||
|     case TEXT_MODE_TEXT: |     case TEXT_MODE_TEXT: | ||||||
|       root[MQTT_MODE] = "text"; |       root[MQTT_MODE] = "text"; | ||||||
|   | |||||||
| @@ -15,8 +15,10 @@ using namespace esphome::text_sensor; | |||||||
|  |  | ||||||
| MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} | MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} | ||||||
| void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (!this->sensor_->get_device_class().empty()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (!this->sensor_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); | ||||||
|  |   } | ||||||
|   config.command_topic = false; |   config.command_topic = false; | ||||||
| } | } | ||||||
| void MQTTTextSensor::setup() { | void MQTTTextSensor::setup() { | ||||||
|   | |||||||
| @@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {} | |||||||
| void MQTTTimeComponent::setup() { | void MQTTTimeComponent::setup() { | ||||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { |   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||||
|     auto call = this->time_->make_call(); |     auto call = this->time_->make_call(); | ||||||
|     if (root.containsKey("hour")) { |     if (root["hour"].is<uint8_t>()) { | ||||||
|       call.set_hour(root["hour"]); |       call.set_hour(root["hour"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("minute")) { |     if (root["minute"].is<uint8_t>()) { | ||||||
|       call.set_minute(root["minute"]); |       call.set_minute(root["minute"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("second")) { |     if (root["second"].is<uint8_t>()) { | ||||||
|       call.set_second(root["second"]); |       call.set_second(root["second"]); | ||||||
|     } |     } | ||||||
|     call.perform(); |     call.perform(); | ||||||
| @@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() { | |||||||
| } | } | ||||||
| bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { | bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { | ||||||
|   return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { |   return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { | ||||||
|  |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     root["hour"] = hour; |     root["hour"] = hour; | ||||||
|     root["minute"] = minute; |     root["minute"] = minute; | ||||||
|     root["second"] = second; |     root["second"] = second; | ||||||
|   | |||||||
| @@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   root["schema"] = "json"; |   root["schema"] = "json"; | ||||||
|   root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; |   root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (!this->valve_->get_device_class().empty()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (!this->valve_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   auto traits = this->valve_->get_traits(); |   auto traits = this->valve_->get_traits(); | ||||||
|   if (traits.get_is_assumed_state()) { |   if (traits.get_is_assumed_state()) { | ||||||
|   | |||||||
| @@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) { | |||||||
|  |  | ||||||
|   // map 16 bit humidity value into range [-6%, 118%] |   // map 16 bit humidity value into range [-6%, 118%] | ||||||
|   float const humidity_partial = double(humidity) / (1 << 16); |   float const humidity_partial = double(humidity) / (1 << 16); | ||||||
|   float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0); |   float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial); | ||||||
|   float const compensated_humidity_percentage = |   float const compensated_humidity_percentage = | ||||||
|       humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; |       humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; | ||||||
|   ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); |   ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); | ||||||
|   | |||||||
							
								
								
									
										218
									
								
								esphome/components/nrf52/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								esphome/components/nrf52/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components.zephyr import ( | ||||||
|  |     copy_files as zephyr_copy_files, | ||||||
|  |     zephyr_add_pm_static, | ||||||
|  |     zephyr_set_core_data, | ||||||
|  |     zephyr_to_code, | ||||||
|  | ) | ||||||
|  | from esphome.components.zephyr.const import ( | ||||||
|  |     BOOTLOADER_MCUBOOT, | ||||||
|  |     KEY_BOOTLOADER, | ||||||
|  |     KEY_ZEPHYR, | ||||||
|  | ) | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_BOARD, | ||||||
|  |     CONF_FRAMEWORK, | ||||||
|  |     KEY_CORE, | ||||||
|  |     KEY_FRAMEWORK_VERSION, | ||||||
|  |     KEY_TARGET_FRAMEWORK, | ||||||
|  |     KEY_TARGET_PLATFORM, | ||||||
|  |     PLATFORM_NRF52, | ||||||
|  | ) | ||||||
|  | from esphome.core import CORE, EsphomeError, coroutine_with_priority | ||||||
|  | from esphome.storage_json import StorageJSON | ||||||
|  | from esphome.types import ConfigType | ||||||
|  |  | ||||||
|  | from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG | ||||||
|  | from .const import ( | ||||||
|  |     BOOTLOADER_ADAFRUIT, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # force import gpio to register pin schema | ||||||
|  | from .gpio import nrf52_pin_to_code  # noqa | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@tomaszduda23"] | ||||||
|  | AUTO_LOAD = ["zephyr"] | ||||||
|  | IS_TARGET_PLATFORM = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def set_core_data(config: ConfigType) -> ConfigType: | ||||||
|  |     zephyr_set_core_data(config) | ||||||
|  |     CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52 | ||||||
|  |     CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR | ||||||
|  |     CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1) | ||||||
|  |  | ||||||
|  |     if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: | ||||||
|  |         zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]) | ||||||
|  |  | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | BOOTLOADERS = [ | ||||||
|  |     BOOTLOADER_ADAFRUIT, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||||
|  |     BOOTLOADER_MCUBOOT, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _detect_bootloader(config: ConfigType) -> ConfigType: | ||||||
|  |     """Detect the bootloader for the given board.""" | ||||||
|  |     config = config.copy() | ||||||
|  |     bootloaders: list[str] = [] | ||||||
|  |     board = config[CONF_BOARD] | ||||||
|  |  | ||||||
|  |     if board in BOARDS_ZEPHYR and KEY_BOOTLOADER in BOARDS_ZEPHYR[board]: | ||||||
|  |         # this board have bootloaders config available | ||||||
|  |         bootloaders = BOARDS_ZEPHYR[board][KEY_BOOTLOADER] | ||||||
|  |  | ||||||
|  |     if KEY_BOOTLOADER not in config: | ||||||
|  |         if bootloaders: | ||||||
|  |             # there is no bootloader in config -> take first one | ||||||
|  |             config[KEY_BOOTLOADER] = bootloaders[0] | ||||||
|  |         else: | ||||||
|  |             # make mcuboot as default if there is no configuration for that board | ||||||
|  |             config[KEY_BOOTLOADER] = BOOTLOADER_MCUBOOT | ||||||
|  |     elif bootloaders and config[KEY_BOOTLOADER] not in bootloaders: | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             f"{board} does not support {config[KEY_BOOTLOADER]}, select one of: {', '.join(bootloaders)}" | ||||||
|  |         ) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.All( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_BOARD): cv.string_strict, | ||||||
|  |             cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  |     _detect_bootloader, | ||||||
|  |     set_core_data, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @coroutine_with_priority(1000) | ||||||
|  | async def to_code(config: ConfigType) -> None: | ||||||
|  |     """Convert the configuration to code.""" | ||||||
|  |     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||||
|  |     cg.add_build_flag("-DUSE_NRF52") | ||||||
|  |     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||||
|  |     cg.add_define("ESPHOME_VARIANT", "NRF52") | ||||||
|  |     cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) | ||||||
|  |     cg.add_platformio_option( | ||||||
|  |         "platform", | ||||||
|  |         "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip", | ||||||
|  |     ) | ||||||
|  |     cg.add_platformio_option( | ||||||
|  |         "platform_packages", | ||||||
|  |         [ | ||||||
|  |             "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip", | ||||||
|  |             "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip", | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT: | ||||||
|  |         # make sure that firmware.zip is created | ||||||
|  |         # for Adafruit_nRF52_Bootloader | ||||||
|  |         cg.add_platformio_option("board_upload.protocol", "nrfutil") | ||||||
|  |         cg.add_platformio_option("board_upload.use_1200bps_touch", "true") | ||||||
|  |         cg.add_platformio_option("board_upload.require_upload_port", "true") | ||||||
|  |         cg.add_platformio_option("board_upload.wait_for_upload_port", "true") | ||||||
|  |  | ||||||
|  |     zephyr_to_code(config) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def copy_files() -> None: | ||||||
|  |     """Copy files to the build directory.""" | ||||||
|  |     zephyr_copy_files() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: | ||||||
|  |     """Get the download types for the firmware.""" | ||||||
|  |     types = [] | ||||||
|  |     UF2_PATH = "zephyr/zephyr.uf2" | ||||||
|  |     DFU_PATH = "firmware.zip" | ||||||
|  |     HEX_PATH = "zephyr/zephyr.hex" | ||||||
|  |     HEX_MERGED_PATH = "zephyr/merged.hex" | ||||||
|  |     APP_IMAGE_PATH = "zephyr/app_update.bin" | ||||||
|  |     build_dir = Path(storage_json.firmware_bin_path).parent | ||||||
|  |     if (build_dir / UF2_PATH).is_file(): | ||||||
|  |         types = [ | ||||||
|  |             { | ||||||
|  |                 "title": "UF2 package (recommended)", | ||||||
|  |                 "description": "For flashing via Adafruit nRF52 Bootloader as a flash drive.", | ||||||
|  |                 "file": UF2_PATH, | ||||||
|  |                 "download": f"{storage_json.name}.uf2", | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "title": "DFU package", | ||||||
|  |                 "description": "For flashing via adafruit-nrfutil using USB CDC.", | ||||||
|  |                 "file": DFU_PATH, | ||||||
|  |                 "download": f"dfu-{storage_json.name}.zip", | ||||||
|  |             }, | ||||||
|  |         ] | ||||||
|  |     else: | ||||||
|  |         types = [ | ||||||
|  |             { | ||||||
|  |                 "title": "HEX package", | ||||||
|  |                 "description": "For flashing via pyocd using SWD.", | ||||||
|  |                 "file": ( | ||||||
|  |                     HEX_MERGED_PATH | ||||||
|  |                     if (build_dir / HEX_MERGED_PATH).is_file() | ||||||
|  |                     else HEX_PATH | ||||||
|  |                 ), | ||||||
|  |                 "download": f"{storage_json.name}.hex", | ||||||
|  |             }, | ||||||
|  |         ] | ||||||
|  |         if (build_dir / APP_IMAGE_PATH).is_file(): | ||||||
|  |             types += [ | ||||||
|  |                 { | ||||||
|  |                     "title": "App update package", | ||||||
|  |                     "description": "For flashing via mcumgr-web using BLE or smpclient using USB CDC.", | ||||||
|  |                     "file": APP_IMAGE_PATH, | ||||||
|  |                     "download": f"app-{storage_json.name}.img", | ||||||
|  |                 }, | ||||||
|  |             ] | ||||||
|  |  | ||||||
|  |     return types | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _upload_using_platformio( | ||||||
|  |     config: ConfigType, port: str, upload_args: list[str] | ||||||
|  | ) -> int | str: | ||||||
|  |     from esphome import platformio_api | ||||||
|  |  | ||||||
|  |     if port is not None: | ||||||
|  |         upload_args += ["--upload-port", port] | ||||||
|  |     return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def upload_program(config: ConfigType, args, host: str) -> bool: | ||||||
|  |     from esphome.__main__ import check_permissions, get_port_type | ||||||
|  |  | ||||||
|  |     result = 0 | ||||||
|  |     handled = False | ||||||
|  |  | ||||||
|  |     if get_port_type(host) == "SERIAL": | ||||||
|  |         check_permissions(host) | ||||||
|  |         result = _upload_using_platformio(config, host, ["-t", "upload"]) | ||||||
|  |         handled = True | ||||||
|  |  | ||||||
|  |     if host == "PYOCD": | ||||||
|  |         result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"]) | ||||||
|  |         handled = True | ||||||
|  |  | ||||||
|  |     if result != 0: | ||||||
|  |         raise EsphomeError(f"Upload failed with result: {result}") | ||||||
|  |  | ||||||
|  |     return handled | ||||||
							
								
								
									
										34
									
								
								esphome/components/nrf52/boards.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/nrf52/boards.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | from esphome.components.zephyr import Section | ||||||
|  | from esphome.components.zephyr.const import KEY_BOOTLOADER | ||||||
|  |  | ||||||
|  | from .const import ( | ||||||
|  |     BOOTLOADER_ADAFRUIT, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | BOARDS_ZEPHYR = { | ||||||
|  |     "adafruit_itsybitsy_nrf52840": { | ||||||
|  |         KEY_BOOTLOADER: [ | ||||||
|  |             BOOTLOADER_ADAFRUIT, | ||||||
|  |             BOOTLOADER_ADAFRUIT_NRF52_SD132, | ||||||
|  |             BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, | ||||||
|  |             BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, | ||||||
|  |         ] | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go | ||||||
|  | # https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map | ||||||
|  | BOOTLOADER_CONFIG = { | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD132: [ | ||||||
|  |         Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), | ||||||
|  |     ], | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [ | ||||||
|  |         Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), | ||||||
|  |     ], | ||||||
|  |     BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [ | ||||||
|  |         Section("empty_app_offset", 0x0, 0x27000, "flash_primary"), | ||||||
|  |     ], | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								esphome/components/nrf52/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								esphome/components/nrf52/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | BOOTLOADER_ADAFRUIT = "adafruit" | ||||||
|  | BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132" | ||||||
|  | BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6" | ||||||
|  | BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7" | ||||||
							
								
								
									
										53
									
								
								esphome/components/nrf52/gpio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								esphome/components/nrf52/gpio.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | from esphome import pins | ||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components.zephyr.const import zephyr_ns | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52 | ||||||
|  |  | ||||||
|  | ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _translate_pin(value): | ||||||
|  |     if isinstance(value, dict) or value is None: | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             "This variable only supports pin numbers, not full pin schemas " | ||||||
|  |             "(with inverted and mode)." | ||||||
|  |         ) | ||||||
|  |     if isinstance(value, int): | ||||||
|  |         return value | ||||||
|  |     try: | ||||||
|  |         return int(value) | ||||||
|  |     except ValueError: | ||||||
|  |         pass | ||||||
|  |     # e.g. P0.27 | ||||||
|  |     if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".": | ||||||
|  |         return cv.int_(value[len("P")].strip()) * 32 + cv.int_( | ||||||
|  |             value[len("P0.") :].strip() | ||||||
|  |         ) | ||||||
|  |     raise cv.Invalid(f"Invalid pin: {value}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_gpio_pin(value): | ||||||
|  |     value = _translate_pin(value) | ||||||
|  |     if value < 0 or value > (32 + 16): | ||||||
|  |         raise cv.Invalid(f"NRF52: Invalid pin number: {value}") | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | NRF52_PIN_SCHEMA = cv.All( | ||||||
|  |     pins.gpio_base_schema( | ||||||
|  |         ZephyrGPIOPin, | ||||||
|  |         validate_gpio_pin, | ||||||
|  |         modes=pins.GPIO_STANDARD_MODES, | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA) | ||||||
|  | async def nrf52_pin_to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     num = config[CONF_NUMBER] | ||||||
|  |     cg.add(var.set_pin(num)) | ||||||
|  |     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||||
|  |     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||||
|  |     return var | ||||||
| @@ -2,7 +2,7 @@ import logging | |||||||
|  |  | ||||||
| from esphome import automation | from esphome import automation | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components.const import CONF_REQUEST_HEADERS | from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS | ||||||
| from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent | from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent | ||||||
| from esphome.components.image import ( | from esphome.components.image import ( | ||||||
|     CONF_INVERT_ALPHA, |     CONF_INVERT_ALPHA, | ||||||
| @@ -11,6 +11,7 @@ from esphome.components.image import ( | |||||||
|     Image_, |     Image_, | ||||||
|     get_image_type_enum, |     get_image_type_enum, | ||||||
|     get_transparency_enum, |     get_transparency_enum, | ||||||
|  |     validate_settings, | ||||||
| ) | ) | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|             rp2040_arduino=cv.Version(0, 0, 0), |             rp2040_arduino=cv.Version(0, 0, 0), | ||||||
|             host=cv.Version(0, 0, 0), |             host=cv.Version(0, 0, 0), | ||||||
|         ), |         ), | ||||||
|  |         validate_settings, | ||||||
|     ) |     ) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -213,6 +215,7 @@ async def to_code(config): | |||||||
|         get_image_type_enum(config[CONF_TYPE]), |         get_image_type_enum(config[CONF_TYPE]), | ||||||
|         transparent, |         transparent, | ||||||
|         config[CONF_BUFFER_SIZE], |         config[CONF_BUFFER_SIZE], | ||||||
|  |         config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", | ||||||
|     ) |     ) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) |     await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) | ||||||
|   | |||||||
| @@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) { | |||||||
| } | } | ||||||
|  |  | ||||||
| OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, | OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, | ||||||
|                          image::Transparency transparency, uint32_t download_buffer_size) |                          image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) | ||||||
|     : Image(nullptr, 0, 0, type, transparency), |     : Image(nullptr, 0, 0, type, transparency), | ||||||
|       buffer_(nullptr), |       buffer_(nullptr), | ||||||
|       download_buffer_(download_buffer_size), |       download_buffer_(download_buffer_size), | ||||||
|       download_buffer_initial_size_(download_buffer_size), |       download_buffer_initial_size_(download_buffer_size), | ||||||
|       format_(format), |       format_(format), | ||||||
|       fixed_width_(width), |       fixed_width_(width), | ||||||
|       fixed_height_(height) { |       fixed_height_(height), | ||||||
|  |       is_big_endian_(is_big_endian) { | ||||||
|   this->set_url(url); |   this->set_url(url); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { | |||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ImageType::IMAGE_TYPE_GRAYSCALE: { |     case ImageType::IMAGE_TYPE_GRAYSCALE: { | ||||||
|       uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); |       auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); | ||||||
|       if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { |       if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { | ||||||
|         if (gray == 1) { |         if (gray == 1) { | ||||||
|           gray = 0; |           gray = 0; | ||||||
| @@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { | |||||||
|     case ImageType::IMAGE_TYPE_RGB565: { |     case ImageType::IMAGE_TYPE_RGB565: { | ||||||
|       this->map_chroma_key(color); |       this->map_chroma_key(color); | ||||||
|       uint16_t col565 = display::ColorUtil::color_to_565(color); |       uint16_t col565 = display::ColorUtil::color_to_565(color); | ||||||
|  |       if (this->is_big_endian_) { | ||||||
|         this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); |         this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); | ||||||
|         this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); |         this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); | ||||||
|  |       } else { | ||||||
|  |         this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF); | ||||||
|  |         this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF); | ||||||
|  |       } | ||||||
|       if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { |       if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { | ||||||
|         this->buffer_[pos + 2] = color.w; |         this->buffer_[pos + 2] = color.w; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent, | |||||||
|    * @param buffer_size Size of the buffer used to download the image. |    * @param buffer_size Size of the buffer used to download the image. | ||||||
|    */ |    */ | ||||||
|   OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, |   OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, | ||||||
|               image::Transparency transparency, uint32_t buffer_size); |               image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); | ||||||
|  |  | ||||||
|   void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; |   void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; | ||||||
|  |  | ||||||
| @@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent, | |||||||
|   const int fixed_width_; |   const int fixed_width_; | ||||||
|   /** height requested on configuration, or 0 if non specified. */ |   /** height requested on configuration, or 0 if non specified. */ | ||||||
|   const int fixed_height_; |   const int fixed_height_; | ||||||
|  |   /** | ||||||
|  |    * Whether the image is stored in big-endian format. | ||||||
|  |    * This is used to determine how to store 16 bit colors in the buffer. | ||||||
|  |    */ | ||||||
|  |   bool is_big_endian_; | ||||||
|   /** |   /** | ||||||
|    * Actual width of the current image. If fixed_width_ is specified, |    * Actual width of the current image. If fixed_width_ is specified, | ||||||
|    * this will be equal to it; otherwise it will be set once the decoding |    * this will be equal to it; otherwise it will be set once the decoding | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) { | |||||||
|   ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); |   ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); | ||||||
|   this->state = state < 0.003 && this->zero_means_zero_ |   this->state = state < 0.003 && this->zero_means_zero_ | ||||||
|                     ? 0.0 |                     ? 0.0 | ||||||
|                     : clamp(lerp(state, min_value_, max_value_), min_value_, max_value_); |                     : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_); | ||||||
|   this->has_state_ = true; |   this->has_state_ = true; | ||||||
|   ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); |   ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -88,9 +88,9 @@ void Servo::internal_write(float value) { | |||||||
|   value = clamp(value, -1.0f, 1.0f); |   value = clamp(value, -1.0f, 1.0f); | ||||||
|   float level; |   float level; | ||||||
|   if (value < 0.0) { |   if (value < 0.0) { | ||||||
|     level = lerp(-value, this->idle_level_, this->min_level_); |     level = std::lerp(this->idle_level_, this->min_level_, -value); | ||||||
|   } else { |   } else { | ||||||
|     level = lerp(value, this->idle_level_, this->max_level_); |     level = std::lerp(this->idle_level_, this->max_level_, value); | ||||||
|   } |   } | ||||||
|   this->output_->set_level(level); |   this->output_->set_level(level); | ||||||
|   this->current_value_ = value; |   this->current_value_ = value; | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import re | import re | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| @@ -139,6 +140,27 @@ def get_hw_interface_list(): | |||||||
|     return [] |     return [] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def one_of_interface_validator(additional_values: list[str] | None = None) -> Any: | ||||||
|  |     """Helper to create a one_of validator for SPI interfaces. | ||||||
|  |  | ||||||
|  |     This delays evaluation of get_hw_interface_list() until validation time, | ||||||
|  |     avoiding access to CORE.data during module import. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         additional_values: List of additional valid values to include | ||||||
|  |     """ | ||||||
|  |     if additional_values is None: | ||||||
|  |         additional_values = [] | ||||||
|  |  | ||||||
|  |     def validator(value: str) -> str: | ||||||
|  |         return cv.one_of( | ||||||
|  |             *sum(get_hw_interface_list(), additional_values), | ||||||
|  |             lower=True, | ||||||
|  |         )(value) | ||||||
|  |  | ||||||
|  |     return cv.All(cv.string, validator) | ||||||
|  |  | ||||||
|  |  | ||||||
| # Given an SPI name, return the index of it in the available list | # Given an SPI name, return the index of it in the available list | ||||||
| def get_spi_index(name): | def get_spi_index(name): | ||||||
|     for i, ilist in enumerate(get_hw_interface_list()): |     for i, ilist in enumerate(get_hw_interface_list()): | ||||||
| @@ -274,9 +296,8 @@ SPI_SINGLE_SCHEMA = cv.All( | |||||||
|             cv.Optional(CONF_FORCE_SW): cv.invalid( |             cv.Optional(CONF_FORCE_SW): cv.invalid( | ||||||
|                 "force_sw is deprecated - use interface: software" |                 "force_sw is deprecated - use interface: software" | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( |             cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator( | ||||||
|                 *sum(get_hw_interface_list(), ["software", "hardware", "any"]), |                 ["software", "hardware", "any"] | ||||||
|                 lower=True, |  | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_DATA_PINS): cv.invalid( |             cv.Optional(CONF_DATA_PINS): cv.invalid( | ||||||
|                 "'data_pins' should be used with 'type: quad or octal' only" |                 "'data_pins' should be used with 'type: quad or octal' only" | ||||||
| @@ -309,10 +330,9 @@ def spi_mode_schema(mode): | |||||||
|                     cv.ensure_list(pins.internal_gpio_output_pin_number), |                     cv.ensure_list(pins.internal_gpio_output_pin_number), | ||||||
|                     cv.Length(min=pin_count, max=pin_count), |                     cv.Length(min=pin_count, max=pin_count), | ||||||
|                 ), |                 ), | ||||||
|                 cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( |                 cv.Optional( | ||||||
|                     *sum(get_hw_interface_list(), ["hardware"]), |                     CONF_INTERFACE, default="hardware" | ||||||
|                     lower=True, |                 ): one_of_interface_validator(["hardware"]), | ||||||
|                 ), |  | ||||||
|                 cv.Optional(CONF_MISO_PIN): cv.invalid( |                 cv.Optional(CONF_MISO_PIN): cv.invalid( | ||||||
|                     f"'miso_pin' should not be used with {mode} SPI" |                     f"'miso_pin' should not be used with {mode} SPI" | ||||||
|                 ), |                 ), | ||||||
|   | |||||||
| @@ -146,6 +146,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): | |||||||
|             if sub is not None: |             if sub is not None: | ||||||
|                 item[k] = sub |                 item[k] = sub | ||||||
|         for old, new in replace_keys: |         for old, new in replace_keys: | ||||||
|  |             if str(new) == str(old): | ||||||
|  |                 item[new] = item[old] | ||||||
|  |             else: | ||||||
|                 item[new] = merge_config(item.get(old), item.get(new)) |                 item[new] = merge_config(item.get(old), item.get(new)) | ||||||
|                 del item[old] |                 del item[old] | ||||||
|     elif isinstance(item, str): |     elif isinstance(item, str): | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import tzlocal | |||||||
| from esphome import automation | from esphome import automation | ||||||
| from esphome.automation import Condition | from esphome.automation import Condition | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
|  | from esphome.components.zephyr import zephyr_add_prj_conf | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_AT, |     CONF_AT, | ||||||
| @@ -25,7 +26,7 @@ from esphome.const import ( | |||||||
|     CONF_TIMEZONE, |     CONF_TIMEZONE, | ||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
| ) | ) | ||||||
| from esphome.core import coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -341,6 +342,8 @@ async def register_time(time_var, config): | |||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(100.0) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|  |     if CORE.using_zephyr: | ||||||
|  |         zephyr_add_prj_conf("POSIX_CLOCK", True) | ||||||
|     cg.add_define("USE_TIME") |     cg.add_define("USE_TIME") | ||||||
|     cg.add_global(time_ns.using) |     cg.add_global(time_ns.using) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,13 +2,15 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #ifdef USE_HOST | #ifdef USE_HOST | ||||||
| #include <sys/time.h> | #include <sys/time.h> | ||||||
|  | #elif defined(USE_ZEPHYR) | ||||||
|  | #include <zephyr/posix/time.h> | ||||||
| #else | #else | ||||||
| #include "lwip/opt.h" | #include "lwip/opt.h" | ||||||
| #endif | #endif | ||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
| #include "sys/time.h" | #include "sys/time.h" | ||||||
| #endif | #endif | ||||||
| #ifdef USE_RP2040 | #if defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||||
| #include <sys/time.h> | #include <sys/time.h> | ||||||
| #endif | #endif | ||||||
| #include <cerrno> | #include <cerrno> | ||||||
| @@ -22,11 +24,22 @@ static const char *const TAG = "time"; | |||||||
|  |  | ||||||
| RealTimeClock::RealTimeClock() = default; | RealTimeClock::RealTimeClock() = default; | ||||||
| void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | ||||||
|  |   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); | ||||||
|   // Update UTC epoch time. |   // Update UTC epoch time. | ||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  |   struct timespec ts; | ||||||
|  |   ts.tv_nsec = 0; | ||||||
|  |   ts.tv_sec = static_cast<time_t>(epoch); | ||||||
|  |  | ||||||
|  |   int ret = clock_settime(CLOCK_REALTIME, &ts); | ||||||
|  |  | ||||||
|  |   if (ret != 0) { | ||||||
|  |     ESP_LOGW(TAG, "clock_settime() failed with code %d", ret); | ||||||
|  |   } | ||||||
|  | #else | ||||||
|   struct timeval timev { |   struct timeval timev { | ||||||
|     .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, |     .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, | ||||||
|   }; |   }; | ||||||
|   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); |  | ||||||
|   struct timezone tz = {0, 0}; |   struct timezone tz = {0, 0}; | ||||||
|   int ret = settimeofday(&timev, &tz); |   int ret = settimeofday(&timev, &tz); | ||||||
|   if (ret == EINVAL) { |   if (ret == EINVAL) { | ||||||
| @@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | |||||||
|   if (ret != 0) { |   if (ret != 0) { | ||||||
|     ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); |     ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
|   auto time = this->now(); |   auto time = this->now(); | ||||||
|   ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, |   ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, | ||||||
|            time.minute, time.second); |            time.minute, time.second); | ||||||
|   | |||||||
| @@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi | |||||||
|  |  | ||||||
|     light::LightJSONSchema::dump_json(*obj, root); |     light::LightJSONSchema::dump_json(*obj, root); | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray opt = root.createNestedArray("effects"); |       JsonArray opt = root["effects"].to<JsonArray>(); | ||||||
|       opt.add("None"); |       opt.add("None"); | ||||||
|       for (auto const &option : obj->get_effects()) { |       for (auto const &option : obj->get_effects()) { | ||||||
|         opt.add(option->get_name()); |         opt.add(option->get_name()); | ||||||
| @@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value | |||||||
|   return json::build_json([this, obj, value, start_config](JsonObject root) { |   return json::build_json([this, obj, value, start_config](JsonObject root) { | ||||||
|     set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); |     set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray opt = root.createNestedArray("option"); |       JsonArray opt = root["option"].to<JsonArray>(); | ||||||
|       for (auto &option : obj->traits.get_options()) { |       for (auto &option : obj->traits.get_options()) { | ||||||
|         opt.add(option); |         opt.add(option); | ||||||
|       } |       } | ||||||
| @@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s | |||||||
|   return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); |   return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); | ||||||
| } | } | ||||||
| std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { | std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   return json::build_json([this, obj, start_config](JsonObject root) { |   return json::build_json([this, obj, start_config](JsonObject root) { | ||||||
|     set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); |     set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); | ||||||
|     const auto traits = obj->get_traits(); |     const auto traits = obj->get_traits(); | ||||||
| @@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf | |||||||
|     char buf[16]; |     char buf[16]; | ||||||
|  |  | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray opt = root.createNestedArray("modes"); |       JsonArray opt = root["modes"].to<JsonArray>(); | ||||||
|       for (climate::ClimateMode m : traits.get_supported_modes()) |       for (climate::ClimateMode m : traits.get_supported_modes()) | ||||||
|         opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); |         opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); | ||||||
|       if (!traits.get_supported_custom_fan_modes().empty()) { |       if (!traits.get_supported_custom_fan_modes().empty()) { | ||||||
|         JsonArray opt = root.createNestedArray("fan_modes"); |         JsonArray opt = root["fan_modes"].to<JsonArray>(); | ||||||
|         for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) |         for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) | ||||||
|           opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); |           opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (!traits.get_supported_custom_fan_modes().empty()) { |       if (!traits.get_supported_custom_fan_modes().empty()) { | ||||||
|         JsonArray opt = root.createNestedArray("custom_fan_modes"); |         JsonArray opt = root["custom_fan_modes"].to<JsonArray>(); | ||||||
|         for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) |         for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) | ||||||
|           opt.add(custom_fan_mode); |           opt.add(custom_fan_mode); | ||||||
|       } |       } | ||||||
|       if (traits.get_supports_swing_modes()) { |       if (traits.get_supports_swing_modes()) { | ||||||
|         JsonArray opt = root.createNestedArray("swing_modes"); |         JsonArray opt = root["swing_modes"].to<JsonArray>(); | ||||||
|         for (auto swing_mode : traits.get_supported_swing_modes()) |         for (auto swing_mode : traits.get_supported_swing_modes()) | ||||||
|           opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); |           opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); | ||||||
|       } |       } | ||||||
|       if (traits.get_supports_presets() && obj->preset.has_value()) { |       if (traits.get_supports_presets() && obj->preset.has_value()) { | ||||||
|         JsonArray opt = root.createNestedArray("presets"); |         JsonArray opt = root["presets"].to<JsonArray>(); | ||||||
|         for (climate::ClimatePreset m : traits.get_supported_presets()) |         for (climate::ClimatePreset m : traits.get_supported_presets()) | ||||||
|           opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); |           opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); | ||||||
|       } |       } | ||||||
|       if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { |       if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { | ||||||
|         JsonArray opt = root.createNestedArray("custom_presets"); |         JsonArray opt = root["custom_presets"].to<JsonArray>(); | ||||||
|         for (auto const &custom_preset : traits.get_supported_custom_presets()) |         for (auto const &custom_preset : traits.get_supported_custom_presets()) | ||||||
|           opt.add(custom_preset); |           opt.add(custom_preset); | ||||||
|       } |       } | ||||||
| @@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf | |||||||
|         root["state"] = root["target_temperature"]; |         root["state"] = root["target_temperature"]; | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -1635,7 +1637,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty | |||||||
|       root["event_type"] = event_type; |       root["event_type"] = event_type; | ||||||
|     } |     } | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray event_types = root.createNestedArray("event_types"); |       JsonArray event_types = root["event_types"].to<JsonArray>(); | ||||||
|       for (auto const &event_type : obj->get_event_types()) { |       for (auto const &event_type : obj->get_event_types()) { | ||||||
|         event_types.add(event_type); |         event_types.add(event_type); | ||||||
|       } |       } | ||||||
| @@ -1682,6 +1684,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so | |||||||
|   return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); |   return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); | ||||||
| } | } | ||||||
| std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { | std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   return json::build_json([this, obj, start_config](JsonObject root) { |   return json::build_json([this, obj, start_config](JsonObject root) { | ||||||
|     set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); |     set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); | ||||||
|     root["value"] = obj->update_info.latest_version; |     root["value"] = obj->update_info.latest_version; | ||||||
| @@ -1707,166 +1710,166 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c | |||||||
|       this->add_sorting_info_(root, obj); |       this->add_sorting_info_(root, obj); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| bool WebServer::canHandle(AsyncWebServerRequest *request) const { | bool WebServer::canHandle(AsyncWebServerRequest *request) const { | ||||||
|   if (request->url() == "/") |   const auto &url = request->url(); | ||||||
|  |   const auto method = request->method(); | ||||||
|  |  | ||||||
|  |   // Simple URL checks | ||||||
|  |   if (url == "/") | ||||||
|     return true; |     return true; | ||||||
|  |  | ||||||
| #ifdef USE_ARDUINO | #ifdef USE_ARDUINO | ||||||
|   if (request->url() == "/events") { |   if (url == "/events") | ||||||
|     return true; |     return true; | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||||
|   if (request->url() == "/0.css") |   if (url == "/0.css") | ||||||
|     return true; |     return true; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | #ifdef USE_WEBSERVER_JS_INCLUDE | ||||||
|   if (request->url() == "/0.js") |   if (url == "/0.js") | ||||||
|     return true; |     return true; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS | #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS | ||||||
|   if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { |   if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) | ||||||
|     return true; |     return true; | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   // Store the URL to prevent temporary string destruction |   // Parse URL for component checks | ||||||
|   // request->url() returns a reference to a String (on Arduino) or std::string (on ESP-IDF) |  | ||||||
|   // UrlMatch stores pointers to the string's data, so we must ensure the string outlives match_url() |  | ||||||
|   const auto &url = request->url(); |  | ||||||
|   UrlMatch match = match_url(url.c_str(), url.length(), true); |   UrlMatch match = match_url(url.c_str(), url.length(), true); | ||||||
|   if (!match.valid) |   if (!match.valid) | ||||||
|     return false; |     return false; | ||||||
|  |  | ||||||
|  |   // Common pattern check | ||||||
|  |   bool is_get = method == HTTP_GET; | ||||||
|  |   bool is_post = method == HTTP_POST; | ||||||
|  |   bool is_get_or_post = is_get || is_post; | ||||||
|  |  | ||||||
|  |   if (!is_get_or_post) | ||||||
|  |     return false; | ||||||
|  |  | ||||||
|  |   // GET-only components | ||||||
|  |   if (is_get) { | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   if (request->method() == HTTP_GET && match.domain_equals("sensor")) |     if (match.domain_equals("sensor")) | ||||||
|       return true; |       return true; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SWITCH |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_BUTTON |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   if (request->method() == HTTP_GET && match.domain_equals("binary_sensor")) |     if (match.domain_equals("binary_sensor")) | ||||||
|       return true; |       return true; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_FAN |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_LIGHT |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   if (request->method() == HTTP_GET && match.domain_equals("text_sensor")) |     if (match.domain_equals("text_sensor")) | ||||||
|       return true; |       return true; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_COVER |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_NUMBER |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATE |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_TIME |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATETIME |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_TEXT |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_SELECT |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_CLIMATE |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_LOCK |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_VALVE |  | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_ALARM_CONTROL_PANEL |  | ||||||
|   if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel")) |  | ||||||
|     return true; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_EVENT | #ifdef USE_EVENT | ||||||
|   if (request->method() == HTTP_GET && match.domain_equals("event")) |     if (match.domain_equals("event")) | ||||||
|       return true; |       return true; | ||||||
| #endif | #endif | ||||||
|  |   } | ||||||
|  |  | ||||||
| #ifdef USE_UPDATE |   // GET+POST components | ||||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update")) |   if (is_get_or_post) { | ||||||
|  | #ifdef USE_SWITCH | ||||||
|  |     if (match.domain_equals("switch")) | ||||||
|       return true; |       return true; | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_BUTTON | ||||||
|  |     if (match.domain_equals("button")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_FAN | ||||||
|  |     if (match.domain_equals("fan")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_LIGHT | ||||||
|  |     if (match.domain_equals("light")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_COVER | ||||||
|  |     if (match.domain_equals("cover")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  |     if (match.domain_equals("number")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_DATETIME_DATE | ||||||
|  |     if (match.domain_equals("date")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_DATETIME_TIME | ||||||
|  |     if (match.domain_equals("time")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_DATETIME_DATETIME | ||||||
|  |     if (match.domain_equals("datetime")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_TEXT | ||||||
|  |     if (match.domain_equals("text")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_SELECT | ||||||
|  |     if (match.domain_equals("select")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_CLIMATE | ||||||
|  |     if (match.domain_equals("climate")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_LOCK | ||||||
|  |     if (match.domain_equals("lock")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_VALVE | ||||||
|  |     if (match.domain_equals("valve")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |     if (match.domain_equals("alarm_control_panel")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_UPDATE | ||||||
|  |     if (match.domain_equals("update")) | ||||||
|  |       return true; | ||||||
|  | #endif | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return false; |   return false; | ||||||
| } | } | ||||||
| void WebServer::handleRequest(AsyncWebServerRequest *request) { | void WebServer::handleRequest(AsyncWebServerRequest *request) { | ||||||
|   if (request->url() == "/") { |   const auto &url = request->url(); | ||||||
|  |  | ||||||
|  |   // Handle static routes first | ||||||
|  |   if (url == "/") { | ||||||
|     this->handle_index_request(request); |     this->handle_index_request(request); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| #ifdef USE_ARDUINO | #ifdef USE_ARDUINO | ||||||
|   if (request->url() == "/events") { |   if (url == "/events") { | ||||||
|     this->events_.add_new_client(this, request); |     this->events_.add_new_client(this, request); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||||
|   if (request->url() == "/0.css") { |   if (url == "/0.css") { | ||||||
|     this->handle_css_request(request); |     this->handle_css_request(request); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | #ifdef USE_WEBSERVER_JS_INCLUDE | ||||||
|   if (request->url() == "/0.js") { |   if (url == "/0.js") { | ||||||
|     this->handle_js_request(request); |     this->handle_js_request(request); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @@ -1879,147 +1882,85 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   // See comment in canHandle() for why we store the URL reference |   // Parse URL for component routing | ||||||
|   const auto &url = request->url(); |  | ||||||
|   UrlMatch match = match_url(url.c_str(), url.length(), false); |   UrlMatch match = match_url(url.c_str(), url.length(), false); | ||||||
|  |  | ||||||
|  |   // Component routing using minimal code repetition | ||||||
|  |   struct ComponentRoute { | ||||||
|  |     const char *domain; | ||||||
|  |     void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   static const ComponentRoute ROUTES[] = { | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   if (match.domain_equals("sensor")) { |       {"sensor", &WebServer::handle_sensor_request}, | ||||||
|     this->handle_sensor_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|   if (match.domain_equals("switch")) { |       {"switch", &WebServer::handle_switch_request}, | ||||||
|     this->handle_switch_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_BUTTON | #ifdef USE_BUTTON | ||||||
|   if (match.domain_equals("button")) { |       {"button", &WebServer::handle_button_request}, | ||||||
|     this->handle_button_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   if (match.domain_equals("binary_sensor")) { |       {"binary_sensor", &WebServer::handle_binary_sensor_request}, | ||||||
|     this->handle_binary_sensor_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_FAN | #ifdef USE_FAN | ||||||
|   if (match.domain_equals("fan")) { |       {"fan", &WebServer::handle_fan_request}, | ||||||
|     this->handle_fan_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LIGHT | #ifdef USE_LIGHT | ||||||
|   if (match.domain_equals("light")) { |       {"light", &WebServer::handle_light_request}, | ||||||
|     this->handle_light_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   if (match.domain_equals("text_sensor")) { |       {"text_sensor", &WebServer::handle_text_sensor_request}, | ||||||
|     this->handle_text_sensor_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_COVER | #ifdef USE_COVER | ||||||
|   if (match.domain_equals("cover")) { |       {"cover", &WebServer::handle_cover_request}, | ||||||
|     this->handle_cover_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   if (match.domain_equals("number")) { |       {"number", &WebServer::handle_number_request}, | ||||||
|     this->handle_number_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATE | #ifdef USE_DATETIME_DATE | ||||||
|   if (match.domain_equals("date")) { |       {"date", &WebServer::handle_date_request}, | ||||||
|     this->handle_date_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_TIME | #ifdef USE_DATETIME_TIME | ||||||
|   if (match.domain_equals("time")) { |       {"time", &WebServer::handle_time_request}, | ||||||
|     this->handle_time_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATETIME | #ifdef USE_DATETIME_DATETIME | ||||||
|   if (match.domain_equals("datetime")) { |       {"datetime", &WebServer::handle_datetime_request}, | ||||||
|     this->handle_datetime_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_TEXT | #ifdef USE_TEXT | ||||||
|   if (match.domain_equals("text")) { |       {"text", &WebServer::handle_text_request}, | ||||||
|     this->handle_text_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   if (match.domain_equals("select")) { |       {"select", &WebServer::handle_select_request}, | ||||||
|     this->handle_select_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_CLIMATE | #ifdef USE_CLIMATE | ||||||
|   if (match.domain_equals("climate")) { |       {"climate", &WebServer::handle_climate_request}, | ||||||
|     this->handle_climate_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LOCK | #ifdef USE_LOCK | ||||||
|   if (match.domain_equals("lock")) { |       {"lock", &WebServer::handle_lock_request}, | ||||||
|     this->handle_lock_request(request, match); |  | ||||||
|  |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_VALVE | #ifdef USE_VALVE | ||||||
|   if (match.domain_equals("valve")) { |       {"valve", &WebServer::handle_valve_request}, | ||||||
|     this->handle_valve_request(request, match); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_ALARM_CONTROL_PANEL | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|   if (match.domain_equals("alarm_control_panel")) { |       {"alarm_control_panel", &WebServer::handle_alarm_control_panel_request}, | ||||||
|     this->handle_alarm_control_panel_request(request, match); |  | ||||||
|  |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_UPDATE | #ifdef USE_UPDATE | ||||||
|   if (match.domain_equals("update")) { |       {"update", &WebServer::handle_update_request}, | ||||||
|     this->handle_update_request(request, match); | #endif | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // Check each route | ||||||
|  |   for (const auto &route : ROUTES) { | ||||||
|  |     if (match.domain_equals(route.domain)) { | ||||||
|  |       (this->*route.handler)(request, match); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| #endif |   } | ||||||
|  |  | ||||||
|   // No matching handler found - send 404 |   // No matching handler found - send 404 | ||||||
|   ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str()); |   ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); | ||||||
|   request->send(404, "text/plain", "Not Found"); |   request->send(404, "text/plain", "Not Found"); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -40,4 +40,4 @@ async def to_code(config): | |||||||
|         if CORE.is_esp8266: |         if CORE.is_esp8266: | ||||||
|             cg.add_library("ESP8266WiFi", None) |             cg.add_library("ESP8266WiFi", None) | ||||||
|         # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json |         # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json | ||||||
|         cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") |         cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") | ||||||
|   | |||||||
| @@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * | |||||||
|  |  | ||||||
| #ifdef USE_WEBSERVER_SORTING | #ifdef USE_WEBSERVER_SORTING | ||||||
|   for (auto &group : ws->sorting_groups_) { |   for (auto &group : ws->sorting_groups_) { | ||||||
|  |     // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     message = json::build_json([group](JsonObject root) { |     message = json::build_json([group](JsonObject root) { | ||||||
|       root["name"] = group.second.name; |       root["name"] = group.second.name; | ||||||
|       root["sorting_weight"] = group.second.weight; |       root["sorting_weight"] = group.second.weight; | ||||||
|     }); |     }); | ||||||
|  |     // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
|  |  | ||||||
|     // a (very) large number of these should be able to be queued initially without defer |     // a (very) large number of these should be able to be queued initially without defer | ||||||
|     // since the only thing in the send buffer at this point is the initial ping/config |     // since the only thing in the send buffer at this point is the initial ping/config | ||||||
|   | |||||||
							
								
								
									
										231
									
								
								esphome/components/zephyr/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								esphome/components/zephyr/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | |||||||
|  | import os | ||||||
|  | from typing import Final, TypedDict | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.const import CONF_BOARD | ||||||
|  | from esphome.core import CORE | ||||||
|  | from esphome.helpers import copy_file_if_changed, write_file_if_changed | ||||||
|  |  | ||||||
|  | from .const import ( | ||||||
|  |     BOOTLOADER_MCUBOOT, | ||||||
|  |     KEY_BOOTLOADER, | ||||||
|  |     KEY_EXTRA_BUILD_FILES, | ||||||
|  |     KEY_OVERLAY, | ||||||
|  |     KEY_PM_STATIC, | ||||||
|  |     KEY_PRJ_CONF, | ||||||
|  |     KEY_ZEPHYR, | ||||||
|  |     zephyr_ns, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@tomaszduda23"] | ||||||
|  | AUTO_LOAD = ["preferences"] | ||||||
|  | KEY_BOARD: Final = "board" | ||||||
|  |  | ||||||
|  | PrjConfValueType = bool | str | int | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Section: | ||||||
|  |     def __init__(self, name, address, size, region): | ||||||
|  |         self.name = name | ||||||
|  |         self.address = address | ||||||
|  |         self.size = size | ||||||
|  |         self.region = region | ||||||
|  |         self.end_address = self.address + self.size | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return ( | ||||||
|  |             f"{self.name}:\n" | ||||||
|  |             f"  address: 0x{self.address:X}\n" | ||||||
|  |             f"  end_address: 0x{self.end_address:X}\n" | ||||||
|  |             f"  region: {self.region}\n" | ||||||
|  |             f"  size: 0x{self.size:X}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ZephyrData(TypedDict): | ||||||
|  |     board: str | ||||||
|  |     bootloader: str | ||||||
|  |     prj_conf: dict[str, tuple[PrjConfValueType, bool]] | ||||||
|  |     overlay: str | ||||||
|  |     extra_build_files: dict[str, str] | ||||||
|  |     pm_static: list[Section] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zephyr_set_core_data(config): | ||||||
|  |     CORE.data[KEY_ZEPHYR] = ZephyrData( | ||||||
|  |         board=config[CONF_BOARD], | ||||||
|  |         bootloader=config[KEY_BOOTLOADER], | ||||||
|  |         prj_conf={}, | ||||||
|  |         overlay="", | ||||||
|  |         extra_build_files={}, | ||||||
|  |         pm_static=[], | ||||||
|  |     ) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zephyr_data() -> ZephyrData: | ||||||
|  |     return CORE.data[KEY_ZEPHYR] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zephyr_add_prj_conf( | ||||||
|  |     name: str, value: PrjConfValueType, required: bool = True | ||||||
|  | ) -> None: | ||||||
|  |     """Set an zephyr prj conf value.""" | ||||||
|  |     if not name.startswith("CONFIG_"): | ||||||
|  |         name = "CONFIG_" + name | ||||||
|  |     prj_conf = zephyr_data()[KEY_PRJ_CONF] | ||||||
|  |     if name not in prj_conf: | ||||||
|  |         prj_conf[name] = (value, required) | ||||||
|  |         return | ||||||
|  |     old_value, old_required = prj_conf[name] | ||||||
|  |     if old_value != value and old_required: | ||||||
|  |         raise ValueError( | ||||||
|  |             f"{name} already set with value '{old_value}', cannot set again to '{value}'" | ||||||
|  |         ) | ||||||
|  |     if required: | ||||||
|  |         prj_conf[name] = (value, required) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zephyr_add_overlay(content): | ||||||
|  |     zephyr_data()[KEY_OVERLAY] += content | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def add_extra_build_file(filename: str, path: str) -> bool: | ||||||
|  |     """Add an extra build file to the project.""" | ||||||
|  |     extra_build_files = zephyr_data()[KEY_EXTRA_BUILD_FILES] | ||||||
|  |     if filename not in extra_build_files: | ||||||
|  |         extra_build_files[filename] = path | ||||||
|  |         return True | ||||||
|  |     return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def add_extra_script(stage: str, filename: str, path: str): | ||||||
|  |     """Add an extra script to the project.""" | ||||||
|  |     key = f"{stage}:{filename}" | ||||||
|  |     if add_extra_build_file(filename, path): | ||||||
|  |         cg.add_platformio_option("extra_scripts", [key]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zephyr_to_code(config): | ||||||
|  |     cg.add(zephyr_ns.setup_preferences()) | ||||||
|  |     cg.add_build_flag("-DUSE_ZEPHYR") | ||||||
|  |     cg.set_cpp_standard("gnu++20") | ||||||
|  |     # build is done by west so bypass board checking in platformio | ||||||
|  |     cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) | ||||||
|  |  | ||||||
|  |     # c++ support | ||||||
|  |     zephyr_add_prj_conf("NEWLIB_LIBC", True) | ||||||
|  |     zephyr_add_prj_conf("CONFIG_FPU", True) | ||||||
|  |     zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) | ||||||
|  |     zephyr_add_prj_conf("CPLUSPLUS", True) | ||||||
|  |     zephyr_add_prj_conf("CONFIG_STD_CPP20", True) | ||||||
|  |     zephyr_add_prj_conf("LIB_CPLUSPLUS", True) | ||||||
|  |     # preferences | ||||||
|  |     zephyr_add_prj_conf("SETTINGS", True) | ||||||
|  |     zephyr_add_prj_conf("NVS", True) | ||||||
|  |     zephyr_add_prj_conf("FLASH_MAP", True) | ||||||
|  |     zephyr_add_prj_conf("CONFIG_FLASH", True) | ||||||
|  |     # watchdog | ||||||
|  |     zephyr_add_prj_conf("WATCHDOG", True) | ||||||
|  |     zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) | ||||||
|  |     # disable console | ||||||
|  |     zephyr_add_prj_conf("UART_CONSOLE", False) | ||||||
|  |     zephyr_add_prj_conf("CONSOLE", False, False) | ||||||
|  |     # use NFC pins as GPIO | ||||||
|  |     zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) | ||||||
|  |  | ||||||
|  |     # <err> os: ***** USAGE FAULT ***** | ||||||
|  |     # <err> os:   Illegal load of EXC_RETURN into PC | ||||||
|  |     zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048) | ||||||
|  |  | ||||||
|  |     add_extra_script( | ||||||
|  |         "pre", | ||||||
|  |         "pre_build.py", | ||||||
|  |         os.path.join(os.path.dirname(__file__), "pre_build.py.script"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _format_prj_conf_val(value: PrjConfValueType) -> str: | ||||||
|  |     if isinstance(value, bool): | ||||||
|  |         return "y" if value else "n" | ||||||
|  |     if isinstance(value, int): | ||||||
|  |         return str(value) | ||||||
|  |     if isinstance(value, str): | ||||||
|  |         return f'"{value}"' | ||||||
|  |     raise ValueError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zephyr_add_cdc_acm(config, id): | ||||||
|  |     zephyr_add_prj_conf("USB_DEVICE_STACK", True) | ||||||
|  |     zephyr_add_prj_conf("USB_CDC_ACM", True) | ||||||
|  |     # prevent device to go to susspend, without this communication stop working in python | ||||||
|  |     # there should be a way to solve it | ||||||
|  |     zephyr_add_prj_conf("USB_DEVICE_REMOTE_WAKEUP", False) | ||||||
|  |     # prevent logging when buffer is full | ||||||
|  |     zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True) | ||||||
|  |     zephyr_add_overlay( | ||||||
|  |         f""" | ||||||
|  | &zephyr_udc0 {{ | ||||||
|  |     cdc_acm_uart{id}: cdc_acm_uart{id} {{ | ||||||
|  |         compatible = "zephyr,cdc-acm-uart"; | ||||||
|  |     }}; | ||||||
|  | }}; | ||||||
|  | """ | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zephyr_add_pm_static(section: Section): | ||||||
|  |     CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def copy_files(): | ||||||
|  |     want_opts = zephyr_data()[KEY_PRJ_CONF] | ||||||
|  |  | ||||||
|  |     prj_conf = ( | ||||||
|  |         "\n".join( | ||||||
|  |             f"{name}={_format_prj_conf_val(value[0])}" | ||||||
|  |             for name, value in sorted(want_opts.items()) | ||||||
|  |         ) | ||||||
|  |         + "\n" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf) | ||||||
|  |  | ||||||
|  |     write_file_if_changed( | ||||||
|  |         CORE.relative_build_path("zephyr/app.overlay"), | ||||||
|  |         zephyr_data()[KEY_OVERLAY], | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[ | ||||||
|  |         KEY_BOARD | ||||||
|  |     ] in ["xiao_ble"]: | ||||||
|  |         fake_board_manifest = """ | ||||||
|  | { | ||||||
|  | "frameworks": [ | ||||||
|  |     "zephyr" | ||||||
|  | ], | ||||||
|  | "name": "esphome nrf52", | ||||||
|  | "upload": { | ||||||
|  |     "maximum_ram_size": 248832, | ||||||
|  |     "maximum_size": 815104 | ||||||
|  | }, | ||||||
|  | "url": "https://esphome.io/", | ||||||
|  | "vendor": "esphome" | ||||||
|  | } | ||||||
|  | """ | ||||||
|  |         write_file_if_changed( | ||||||
|  |             CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"), | ||||||
|  |             fake_board_manifest, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items(): | ||||||
|  |         copy_file_if_changed( | ||||||
|  |             path, | ||||||
|  |             CORE.relative_build_path(filename), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     pm_static = "\n".join(str(item) for item in zephyr_data()[KEY_PM_STATIC]) | ||||||
|  |     if pm_static: | ||||||
|  |         write_file_if_changed( | ||||||
|  |             CORE.relative_build_path("zephyr/pm_static.yml"), pm_static | ||||||
|  |         ) | ||||||
							
								
								
									
										14
									
								
								esphome/components/zephyr/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								esphome/components/zephyr/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | from typing import Final | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  |  | ||||||
|  | BOOTLOADER_MCUBOOT = "mcuboot" | ||||||
|  |  | ||||||
|  | KEY_BOOTLOADER: Final = "bootloader" | ||||||
|  | KEY_EXTRA_BUILD_FILES: Final = "extra_build_files" | ||||||
|  | KEY_OVERLAY: Final = "overlay" | ||||||
|  | KEY_PM_STATIC: Final = "pm_static" | ||||||
|  | KEY_PRJ_CONF: Final = "prj_conf" | ||||||
|  | KEY_ZEPHYR = "zephyr" | ||||||
|  |  | ||||||
|  | zephyr_ns = cg.esphome_ns.namespace("zephyr") | ||||||
							
								
								
									
										86
									
								
								esphome/components/zephyr/core.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								esphome/components/zephyr/core.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  |  | ||||||
|  | #include <zephyr/kernel.h> | ||||||
|  | #include <zephyr/drivers/watchdog.h> | ||||||
|  | #include <zephyr/sys/reboot.h> | ||||||
|  | #include <zephyr/random/rand32.h> | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  |  | ||||||
|  | static int wdt_channel_id = -1;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  | static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); | ||||||
|  |  | ||||||
|  | void yield() { ::k_yield(); } | ||||||
|  | uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } | ||||||
|  | uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } | ||||||
|  | void delayMicroseconds(uint32_t us) { ::k_usleep(us); } | ||||||
|  | void delay(uint32_t ms) { ::k_msleep(ms); } | ||||||
|  |  | ||||||
|  | void arch_init() { | ||||||
|  |   if (device_is_ready(WDT)) { | ||||||
|  |     static wdt_timeout_cfg wdt_config{}; | ||||||
|  |     wdt_config.flags = WDT_FLAG_RESET_SOC; | ||||||
|  |     wdt_config.window.max = 2000; | ||||||
|  |     wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); | ||||||
|  |     if (wdt_channel_id >= 0) { | ||||||
|  |       wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void arch_feed_wdt() { | ||||||
|  |   if (wdt_channel_id >= 0) { | ||||||
|  |     wdt_feed(WDT, wdt_channel_id); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } | ||||||
|  | uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } | ||||||
|  | uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } | ||||||
|  | uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } | ||||||
|  |  | ||||||
|  | Mutex::Mutex() { | ||||||
|  |   auto *mutex = new k_mutex(); | ||||||
|  |   this->handle_ = mutex; | ||||||
|  |   k_mutex_init(mutex); | ||||||
|  | } | ||||||
|  | Mutex::~Mutex() { delete static_cast<k_mutex *>(this->handle_); } | ||||||
|  | void Mutex::lock() { k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_FOREVER); } | ||||||
|  | bool Mutex::try_lock() { return k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_NO_WAIT) == 0; } | ||||||
|  | void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); } | ||||||
|  |  | ||||||
|  | IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); } | ||||||
|  | IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } | ||||||
|  |  | ||||||
|  | uint32_t random_uint32() { return rand(); }  // NOLINT(cert-msc30-c, cert-msc50-cpp) | ||||||
|  | bool random_bytes(uint8_t *data, size_t len) { | ||||||
|  |   sys_rand_get(data, len); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||||
|  |   mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0; | ||||||
|  |   mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF; | ||||||
|  |   mac[2] = NRF_FICR->DEVICEADDR[0] >> 24; | ||||||
|  |   mac[3] = NRF_FICR->DEVICEADDR[0] >> 16; | ||||||
|  |   mac[4] = NRF_FICR->DEVICEADDR[0] >> 8; | ||||||
|  |   mac[5] = NRF_FICR->DEVICEADDR[0]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | void setup(); | ||||||
|  | void loop(); | ||||||
|  |  | ||||||
|  | int main() { | ||||||
|  |   setup(); | ||||||
|  |   while (true) { | ||||||
|  |     loop(); | ||||||
|  |     esphome::yield(); | ||||||
|  |   } | ||||||
|  |   return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										120
									
								
								esphome/components/zephyr/gpio.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								esphome/components/zephyr/gpio.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  | #include "gpio.h" | ||||||
|  | #include <zephyr/drivers/gpio.h> | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace zephyr { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "zephyr"; | ||||||
|  |  | ||||||
|  | static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) { | ||||||
|  |   int ret = 0; | ||||||
|  |   if (flags & gpio::FLAG_INPUT) { | ||||||
|  |     ret |= GPIO_INPUT; | ||||||
|  |   } | ||||||
|  |   if (flags & gpio::FLAG_OUTPUT) { | ||||||
|  |     ret |= GPIO_OUTPUT; | ||||||
|  |     if (value != inverted) { | ||||||
|  |       ret |= GPIO_OUTPUT_INIT_HIGH; | ||||||
|  |     } else { | ||||||
|  |       ret |= GPIO_OUTPUT_INIT_LOW; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (flags & gpio::FLAG_PULLUP) { | ||||||
|  |     ret |= GPIO_PULL_UP; | ||||||
|  |   } | ||||||
|  |   if (flags & gpio::FLAG_PULLDOWN) { | ||||||
|  |     ret |= GPIO_PULL_DOWN; | ||||||
|  |   } | ||||||
|  |   if (flags & gpio::FLAG_OPEN_DRAIN) { | ||||||
|  |     ret |= GPIO_OPEN_DRAIN; | ||||||
|  |   } | ||||||
|  |   return ret; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ISRPinArg { | ||||||
|  |   uint8_t pin; | ||||||
|  |   bool inverted; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const { | ||||||
|  |   auto *arg = new ISRPinArg{};  // NOLINT(cppcoreguidelines-owning-memory) | ||||||
|  |   arg->pin = this->pin_; | ||||||
|  |   arg->inverted = this->inverted_; | ||||||
|  |   return ISRInternalGPIOPin((void *) arg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { | ||||||
|  |   // TODO | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ZephyrGPIOPin::setup() { | ||||||
|  |   const struct device *gpio = nullptr; | ||||||
|  |   if (this->pin_ < 32) { | ||||||
|  | #define GPIO0 DT_NODELABEL(gpio0) | ||||||
|  | #if DT_NODE_HAS_STATUS(GPIO0, okay) | ||||||
|  |     gpio = DEVICE_DT_GET(GPIO0); | ||||||
|  | #else | ||||||
|  | #error "gpio0 is disabled" | ||||||
|  | #endif | ||||||
|  |   } else { | ||||||
|  | #define GPIO1 DT_NODELABEL(gpio1) | ||||||
|  | #if DT_NODE_HAS_STATUS(GPIO1, okay) | ||||||
|  |     gpio = DEVICE_DT_GET(GPIO1); | ||||||
|  | #else | ||||||
|  | #error "gpio1 is disabled" | ||||||
|  | #endif | ||||||
|  |   } | ||||||
|  |   if (device_is_ready(gpio)) { | ||||||
|  |     this->gpio_ = gpio; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->pin_mode(this->flags_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { | ||||||
|  |   if (nullptr == this->gpio_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::string ZephyrGPIOPin::dump_summary() const { | ||||||
|  |   char buffer[32]; | ||||||
|  |   snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); | ||||||
|  |   return buffer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool ZephyrGPIOPin::digital_read() { | ||||||
|  |   if (nullptr == this->gpio_) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ZephyrGPIOPin::digital_write(bool value) { | ||||||
|  |   // make sure that value is not ignored since it can be inverted e.g. on switch side | ||||||
|  |   // that way init state should be correct | ||||||
|  |   this->value_ = value; | ||||||
|  |   if (nullptr == this->gpio_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0); | ||||||
|  | } | ||||||
|  | void ZephyrGPIOPin::detach_interrupt() const { | ||||||
|  |   // TODO | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace zephyr | ||||||
|  |  | ||||||
|  | bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { | ||||||
|  |   // TODO | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										38
									
								
								esphome/components/zephyr/gpio.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/zephyr/gpio.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | struct device; | ||||||
|  | namespace esphome { | ||||||
|  | namespace zephyr { | ||||||
|  |  | ||||||
|  | class ZephyrGPIOPin : public InternalGPIOPin { | ||||||
|  |  public: | ||||||
|  |   void set_pin(uint8_t pin) { this->pin_ = pin; } | ||||||
|  |   void set_inverted(bool inverted) { this->inverted_ = inverted; } | ||||||
|  |   void set_flags(gpio::Flags flags) { this->flags_ = flags; } | ||||||
|  |  | ||||||
|  |   void setup() override; | ||||||
|  |   void pin_mode(gpio::Flags flags) override; | ||||||
|  |   bool digital_read() override; | ||||||
|  |   void digital_write(bool value) override; | ||||||
|  |   std::string dump_summary() const override; | ||||||
|  |   void detach_interrupt() const override; | ||||||
|  |   ISRInternalGPIOPin to_isr() const override; | ||||||
|  |   uint8_t get_pin() const override { return this->pin_; } | ||||||
|  |   bool is_inverted() const override { return this->inverted_; } | ||||||
|  |   gpio::Flags get_flags() const override { return flags_; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||||
|  |   uint8_t pin_; | ||||||
|  |   bool inverted_; | ||||||
|  |   gpio::Flags flags_; | ||||||
|  |   const device *gpio_ = nullptr; | ||||||
|  |   bool value_ = false; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace zephyr | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_ZEPHYR | ||||||
							
								
								
									
										4
									
								
								esphome/components/zephyr/pre_build.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								esphome/components/zephyr/pre_build.py.script
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | Import("env") | ||||||
|  |  | ||||||
|  | board_config = env.BoardConfig() | ||||||
|  | board_config.update("frameworks", ["arduino", "zephyr"]) | ||||||
							
								
								
									
										156
									
								
								esphome/components/zephyr/preferences.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								esphome/components/zephyr/preferences.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | |||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  |  | ||||||
|  | #include <zephyr/kernel.h> | ||||||
|  | #include "esphome/core/preferences.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <zephyr/settings/settings.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace zephyr { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "zephyr.preferences"; | ||||||
|  |  | ||||||
|  | #define ESPHOME_SETTINGS_KEY "esphome" | ||||||
|  |  | ||||||
|  | class ZephyrPreferenceBackend : public ESPPreferenceBackend { | ||||||
|  |  public: | ||||||
|  |   ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; } | ||||||
|  |   ZephyrPreferenceBackend(uint32_t type, std::vector<uint8_t> &&data) : data(std::move(data)) { this->type_ = type; } | ||||||
|  |  | ||||||
|  |   bool save(const uint8_t *data, size_t len) override { | ||||||
|  |     this->data.resize(len); | ||||||
|  |     std::memcpy(this->data.data(), data, len); | ||||||
|  |     ESP_LOGVV(TAG, "save key: %u, len: %d", this->type_, len); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bool load(uint8_t *data, size_t len) override { | ||||||
|  |     if (len != this->data.size()) { | ||||||
|  |       ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     std::memcpy(data, this->data.data(), len); | ||||||
|  |     ESP_LOGVV(TAG, "load key: %u, len: %d", this->type_, len); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint32_t get_type() const { return this->type_; } | ||||||
|  |   std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); } | ||||||
|  |  | ||||||
|  |   std::vector<uint8_t> data; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   uint32_t type_ = 0; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class ZephyrPreferences : public ESPPreferences { | ||||||
|  |  public: | ||||||
|  |   void open() { | ||||||
|  |     int err = settings_subsys_init(); | ||||||
|  |     if (err) { | ||||||
|  |       ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     static struct settings_handler settings_cb = { | ||||||
|  |         .name = ESPHOME_SETTINGS_KEY, | ||||||
|  |         .h_set = load_setting, | ||||||
|  |         .h_export = export_settings, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     err = settings_register(&settings_cb); | ||||||
|  |     if (err) { | ||||||
|  |       ESP_LOGE(TAG, "setting_register failed, err, %d", err); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     err = settings_load_subtree(ESPHOME_SETTINGS_KEY); | ||||||
|  |     if (err) { | ||||||
|  |       ESP_LOGE(TAG, "Cannot load settings, err: %d", err); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     ESP_LOGD(TAG, "Loaded %u settings.", this->backends_.size()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { | ||||||
|  |     return make_preference(length, type); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESPPreferenceObject make_preference(size_t length, uint32_t type) override { | ||||||
|  |     for (auto *backend : this->backends_) { | ||||||
|  |       if (backend->get_type() == type) { | ||||||
|  |         return ESPPreferenceObject(backend); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     printf("type %u size %u\n", type, this->backends_.size()); | ||||||
|  |     auto *pref = new ZephyrPreferenceBackend(type);  // NOLINT(cppcoreguidelines-owning-memory) | ||||||
|  |     ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str()); | ||||||
|  |     this->backends_.push_back(pref); | ||||||
|  |     return ESPPreferenceObject(pref); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bool sync() override { | ||||||
|  |     ESP_LOGD(TAG, "Save settings"); | ||||||
|  |     int err = settings_save(); | ||||||
|  |     if (err) { | ||||||
|  |       ESP_LOGE(TAG, "Cannot save settings, err: %d", err); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bool reset() override { | ||||||
|  |     ESP_LOGD(TAG, "Reset settings"); | ||||||
|  |     for (auto *backend : this->backends_) { | ||||||
|  |       // save empty delete data | ||||||
|  |       backend->data.clear(); | ||||||
|  |     } | ||||||
|  |     sync(); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::vector<ZephyrPreferenceBackend *> backends_; | ||||||
|  |  | ||||||
|  |   static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { | ||||||
|  |     auto type = parse_hex<uint32_t>(name); | ||||||
|  |     if (!type.has_value()) { | ||||||
|  |       std::string full_name(ESPHOME_SETTINGS_KEY); | ||||||
|  |       full_name += "/"; | ||||||
|  |       full_name += name; | ||||||
|  |       // Delete unusable keys. Otherwise it will stay in flash forever. | ||||||
|  |       settings_delete(full_name.c_str()); | ||||||
|  |       return 1; | ||||||
|  |     } | ||||||
|  |     std::vector<uint8_t> data(len); | ||||||
|  |     int err = read_cb(cb_arg, data.data(), len); | ||||||
|  |  | ||||||
|  |     ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err); | ||||||
|  |     auto *pref = new ZephyrPreferenceBackend(*type, std::move(data));  // NOLINT(cppcoreguidelines-owning-memory) | ||||||
|  |     static_cast<ZephyrPreferences *>(global_preferences)->backends_.push_back(pref); | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) { | ||||||
|  |     for (auto *backend : static_cast<ZephyrPreferences *>(global_preferences)->backends_) { | ||||||
|  |       auto name = backend->get_key(); | ||||||
|  |       int err = cb(name.c_str(), backend->data.data(), backend->data.size()); | ||||||
|  |       ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err); | ||||||
|  |     } | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | void setup_preferences() { | ||||||
|  |   auto *prefs = new ZephyrPreferences();  // NOLINT(cppcoreguidelines-owning-memory) | ||||||
|  |   global_preferences = prefs; | ||||||
|  |   prefs->open(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace zephyr | ||||||
|  |  | ||||||
|  | ESPPreferences *global_preferences;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										13
									
								
								esphome/components/zephyr/preferences.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								esphome/components/zephyr/preferences.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #ifdef USE_ZEPHYR | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace zephyr { | ||||||
|  |  | ||||||
|  | void setup_preferences(); | ||||||
|  |  | ||||||
|  | }  // namespace zephyr | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
| @@ -21,6 +21,7 @@ class Platform(StrEnum): | |||||||
|     HOST = "host" |     HOST = "host" | ||||||
|     LIBRETINY_OLDSTYLE = "libretiny" |     LIBRETINY_OLDSTYLE = "libretiny" | ||||||
|     LN882X = "ln882x" |     LN882X = "ln882x" | ||||||
|  |     NRF52 = "nrf52" | ||||||
|     RP2040 = "rp2040" |     RP2040 = "rp2040" | ||||||
|     RTL87XX = "rtl87xx" |     RTL87XX = "rtl87xx" | ||||||
|  |  | ||||||
| @@ -31,6 +32,7 @@ class Framework(StrEnum): | |||||||
|     ARDUINO = "arduino" |     ARDUINO = "arduino" | ||||||
|     ESP_IDF = "esp-idf" |     ESP_IDF = "esp-idf" | ||||||
|     NATIVE = "host" |     NATIVE = "host" | ||||||
|  |     ZEPHYR = "zephyr" | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlatformFramework(Enum): | class PlatformFramework(Enum): | ||||||
| @@ -47,6 +49,9 @@ class PlatformFramework(Enum): | |||||||
|     RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) |     RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) | ||||||
|     LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) |     LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) | ||||||
|  |  | ||||||
|  |     # Zephyr framework platforms | ||||||
|  |     NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR) | ||||||
|  |  | ||||||
|     # Host platform (native) |     # Host platform (native) | ||||||
|     HOST_NATIVE = (Platform.HOST, Framework.NATIVE) |     HOST_NATIVE = (Platform.HOST, Framework.NATIVE) | ||||||
|  |  | ||||||
| @@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266 | |||||||
| PLATFORM_HOST = Platform.HOST | PLATFORM_HOST = Platform.HOST | ||||||
| PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE | PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE | ||||||
| PLATFORM_LN882X = Platform.LN882X | PLATFORM_LN882X = Platform.LN882X | ||||||
|  | PLATFORM_NRF52 = Platform.NRF52 | ||||||
| PLATFORM_RP2040 = Platform.RP2040 | PLATFORM_RP2040 = Platform.RP2040 | ||||||
| PLATFORM_RTL87XX = Platform.RTL87XX | PLATFORM_RTL87XX = Platform.RTL87XX | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ from esphome.const import ( | |||||||
|     PLATFORM_ESP8266, |     PLATFORM_ESP8266, | ||||||
|     PLATFORM_HOST, |     PLATFORM_HOST, | ||||||
|     PLATFORM_LN882X, |     PLATFORM_LN882X, | ||||||
|  |     PLATFORM_NRF52, | ||||||
|     PLATFORM_RP2040, |     PLATFORM_RP2040, | ||||||
|     PLATFORM_RTL87XX, |     PLATFORM_RTL87XX, | ||||||
| ) | ) | ||||||
| @@ -670,6 +671,10 @@ class EsphomeCore: | |||||||
|     def is_libretiny(self): |     def is_libretiny(self): | ||||||
|         return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x |         return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def is_nrf52(self): | ||||||
|  |         return self.target_platform == PLATFORM_NRF52 | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def is_host(self): |     def is_host(self): | ||||||
|         return self.target_platform == PLATFORM_HOST |         return self.target_platform == PLATFORM_HOST | ||||||
| @@ -686,6 +691,10 @@ class EsphomeCore: | |||||||
|     def using_esp_idf(self): |     def using_esp_idf(self): | ||||||
|         return self.target_framework == "esp-idf" |         return self.target_framework == "esp-idf" | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def using_zephyr(self): | ||||||
|  |         return self.target_framework == "zephyr" | ||||||
|  |  | ||||||
|     def add_job(self, func, *args, **kwargs) -> None: |     def add_job(self, func, *args, **kwargs) -> None: | ||||||
|         self.event_loop.add_job(func, *args, **kwargs) |         self.event_loop.add_job(func, *args, **kwargs) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -309,6 +309,12 @@ void Application::disable_component_loop_(Component *component) { | |||||||
|         if (this->in_loop_ && i == this->current_loop_index_) { |         if (this->in_loop_ && i == this->current_loop_index_) { | ||||||
|           // Decrement so we'll process the swapped component next |           // Decrement so we'll process the swapped component next | ||||||
|           this->current_loop_index_--; |           this->current_loop_index_--; | ||||||
|  |           // Update the loop start time to current time so the swapped component | ||||||
|  |           // gets correct timing instead of inheriting stale timing. | ||||||
|  |           // This prevents integer underflow in timing calculations by ensuring | ||||||
|  |           // the swapped component starts with a fresh timing reference, avoiding | ||||||
|  |           // errors caused by stale or wrapped timing values. | ||||||
|  |           this->loop_component_start_time_ = millis(); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       return; |       return; | ||||||
|   | |||||||
| @@ -138,7 +138,7 @@ void Component::call_dump_config() { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     ESP_LOGE(TAG, "  Component %s is marked FAILED: %s", this->get_component_source(), error_msg); |     ESP_LOGE(TAG, "  %s is marked FAILED: %s", this->get_component_source(), error_msg); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -191,7 +191,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { | |||||||
|   return false; |   return false; | ||||||
| } | } | ||||||
| void Component::mark_failed() { | void Component::mark_failed() { | ||||||
|   ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source()); |   ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source()); | ||||||
|   this->component_state_ &= ~COMPONENT_STATE_MASK; |   this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||||
|   this->component_state_ |= COMPONENT_STATE_FAILED; |   this->component_state_ |= COMPONENT_STATE_FAILED; | ||||||
|   this->status_set_error(); |   this->status_set_error(); | ||||||
| @@ -229,7 +229,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { | |||||||
| } | } | ||||||
| void Component::reset_to_construction_state() { | void Component::reset_to_construction_state() { | ||||||
|   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { |   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { | ||||||
|     ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); |     ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); | ||||||
|     this->component_state_ &= ~COMPONENT_STATE_MASK; |     this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||||
|     this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; |     this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; | ||||||
|     // Clear error status when resetting |     // Clear error status when resetting | ||||||
| @@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: | |||||||
| bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } | bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } | ||||||
| bool Component::is_ready() const { | bool Component::is_ready() const { | ||||||
|   return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || |   return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || | ||||||
|  |          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || | ||||||
|          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; |          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; | ||||||
| } | } | ||||||
| bool Component::can_proceed() { return true; } | bool Component::can_proceed() { return true; } | ||||||
| @@ -275,14 +276,14 @@ void Component::status_set_warning(const char *message) { | |||||||
|     return; |     return; | ||||||
|   this->component_state_ |= STATUS_LED_WARNING; |   this->component_state_ |= STATUS_LED_WARNING; | ||||||
|   App.app_state_ |= STATUS_LED_WARNING; |   App.app_state_ |= STATUS_LED_WARNING; | ||||||
|   ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message); |   ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message); | ||||||
| } | } | ||||||
| void Component::status_set_error(const char *message) { | void Component::status_set_error(const char *message) { | ||||||
|   if ((this->component_state_ & STATUS_LED_ERROR) != 0) |   if ((this->component_state_ & STATUS_LED_ERROR) != 0) | ||||||
|     return; |     return; | ||||||
|   this->component_state_ |= STATUS_LED_ERROR; |   this->component_state_ |= STATUS_LED_ERROR; | ||||||
|   App.app_state_ |= STATUS_LED_ERROR; |   App.app_state_ |= STATUS_LED_ERROR; | ||||||
|   ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); |   ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message); | ||||||
|   if (strcmp(message, "unspecified") != 0) { |   if (strcmp(message, "unspecified") != 0) { | ||||||
|     // Lazy allocate the error messages vector if needed |     // Lazy allocate the error messages vector if needed | ||||||
|     if (!component_error_messages) { |     if (!component_error_messages) { | ||||||
| @@ -303,13 +304,13 @@ void Component::status_clear_warning() { | |||||||
|   if ((this->component_state_ & STATUS_LED_WARNING) == 0) |   if ((this->component_state_ & STATUS_LED_WARNING) == 0) | ||||||
|     return; |     return; | ||||||
|   this->component_state_ &= ~STATUS_LED_WARNING; |   this->component_state_ &= ~STATUS_LED_WARNING; | ||||||
|   ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source()); |   ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source()); | ||||||
| } | } | ||||||
| void Component::status_clear_error() { | void Component::status_clear_error() { | ||||||
|   if ((this->component_state_ & STATUS_LED_ERROR) == 0) |   if ((this->component_state_ & STATUS_LED_ERROR) == 0) | ||||||
|     return; |     return; | ||||||
|   this->component_state_ &= ~STATUS_LED_ERROR; |   this->component_state_ &= ~STATUS_LED_ERROR; | ||||||
|   ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source()); |   ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source()); | ||||||
| } | } | ||||||
| void Component::status_momentary_warning(const std::string &name, uint32_t length) { | void Component::status_momentary_warning(const std::string &name, uint32_t length) { | ||||||
|   this->status_set_warning(); |   this->status_set_warning(); | ||||||
| @@ -403,7 +404,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() { | |||||||
|   } |   } | ||||||
|   if (should_warn) { |   if (should_warn) { | ||||||
|     const char *src = component_ == nullptr ? "<null>" : component_->get_component_source(); |     const char *src = component_ == nullptr ? "<null>" : component_->get_component_source(); | ||||||
|     ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); |     ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); | ||||||
|     ESP_LOGW(TAG, "Components should block for at most 30 ms"); |     ESP_LOGW(TAG, "Components should block for at most 30 ms"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,373 +16,186 @@ void ComponentIterator::begin(bool include_internal) { | |||||||
|   this->at_ = 0; |   this->at_ = 0; | ||||||
|   this->include_internal_ = include_internal; |   this->include_internal_ = include_internal; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | template<typename PlatformItem> | ||||||
|  | void ComponentIterator::process_platform_item_(const std::vector<PlatformItem *> &items, | ||||||
|  |                                                bool (ComponentIterator::*on_item)(PlatformItem *)) { | ||||||
|  |   if (this->at_ >= items.size()) { | ||||||
|  |     this->advance_platform_(); | ||||||
|  |   } else { | ||||||
|  |     PlatformItem *item = items[this->at_]; | ||||||
|  |     if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) { | ||||||
|  |       this->at_++; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ComponentIterator::advance_platform_() { | ||||||
|  |   this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1); | ||||||
|  |   this->at_ = 0; | ||||||
|  | } | ||||||
|  |  | ||||||
| void ComponentIterator::advance() { | void ComponentIterator::advance() { | ||||||
|   bool advance_platform = false; |  | ||||||
|   bool success = true; |  | ||||||
|   switch (this->state_) { |   switch (this->state_) { | ||||||
|     case IteratorState::NONE: |     case IteratorState::NONE: | ||||||
|       // not started |       // not started | ||||||
|       return; |       return; | ||||||
|     case IteratorState::BEGIN: |     case IteratorState::BEGIN: | ||||||
|       if (this->on_begin()) { |       if (this->on_begin()) { | ||||||
|         advance_platform = true; |         advance_platform_(); | ||||||
|       } else { |  | ||||||
|         return; |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|  |  | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|     case IteratorState::BINARY_SENSOR: |     case IteratorState::BINARY_SENSOR: | ||||||
|       if (this->at_ >= App.get_binary_sensors().size()) { |       this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *binary_sensor = App.get_binary_sensors()[this->at_]; |  | ||||||
|         if (binary_sensor->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_binary_sensor(binary_sensor); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_COVER | #ifdef USE_COVER | ||||||
|     case IteratorState::COVER: |     case IteratorState::COVER: | ||||||
|       if (this->at_ >= App.get_covers().size()) { |       this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *cover = App.get_covers()[this->at_]; |  | ||||||
|         if (cover->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_cover(cover); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_FAN | #ifdef USE_FAN | ||||||
|     case IteratorState::FAN: |     case IteratorState::FAN: | ||||||
|       if (this->at_ >= App.get_fans().size()) { |       this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *fan = App.get_fans()[this->at_]; |  | ||||||
|         if (fan->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_fan(fan); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LIGHT | #ifdef USE_LIGHT | ||||||
|     case IteratorState::LIGHT: |     case IteratorState::LIGHT: | ||||||
|       if (this->at_ >= App.get_lights().size()) { |       this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *light = App.get_lights()[this->at_]; |  | ||||||
|         if (light->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_light(light); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|     case IteratorState::SENSOR: |     case IteratorState::SENSOR: | ||||||
|       if (this->at_ >= App.get_sensors().size()) { |       this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *sensor = App.get_sensors()[this->at_]; |  | ||||||
|         if (sensor->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_sensor(sensor); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|     case IteratorState::SWITCH: |     case IteratorState::SWITCH: | ||||||
|       if (this->at_ >= App.get_switches().size()) { |       this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *a_switch = App.get_switches()[this->at_]; |  | ||||||
|         if (a_switch->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_switch(a_switch); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_BUTTON | #ifdef USE_BUTTON | ||||||
|     case IteratorState::BUTTON: |     case IteratorState::BUTTON: | ||||||
|       if (this->at_ >= App.get_buttons().size()) { |       this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *button = App.get_buttons()[this->at_]; |  | ||||||
|         if (button->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_button(button); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|     case IteratorState::TEXT_SENSOR: |     case IteratorState::TEXT_SENSOR: | ||||||
|       if (this->at_ >= App.get_text_sensors().size()) { |       this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *text_sensor = App.get_text_sensors()[this->at_]; |  | ||||||
|         if (text_sensor->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_text_sensor(text_sensor); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
|     case IteratorState::SERVICE: |     case IteratorState::SERVICE: | ||||||
|       if (this->at_ >= api::global_api_server->get_user_services().size()) { |       this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *service = api::global_api_server->get_user_services()[this->at_]; |  | ||||||
|         success = this->on_service(service); |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
|     case IteratorState::CAMERA: |     case IteratorState::CAMERA: { | ||||||
|       if (camera::Camera::instance() == nullptr) { |       camera::Camera *camera_instance = camera::Camera::instance(); | ||||||
|         advance_platform = true; |       if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) { | ||||||
|       } else { |         this->on_camera(camera_instance); | ||||||
|         if (camera::Camera::instance()->is_internal() && !this->include_internal_) { |  | ||||||
|           advance_platform = success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           advance_platform = success = this->on_camera(camera::Camera::instance()); |  | ||||||
|       } |       } | ||||||
|       } |       advance_platform_(); | ||||||
|       break; |     } break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_CLIMATE | #ifdef USE_CLIMATE | ||||||
|     case IteratorState::CLIMATE: |     case IteratorState::CLIMATE: | ||||||
|       if (this->at_ >= App.get_climates().size()) { |       this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *climate = App.get_climates()[this->at_]; |  | ||||||
|         if (climate->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_climate(climate); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|     case IteratorState::NUMBER: |     case IteratorState::NUMBER: | ||||||
|       if (this->at_ >= App.get_numbers().size()) { |       this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *number = App.get_numbers()[this->at_]; |  | ||||||
|         if (number->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_number(number); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATE | #ifdef USE_DATETIME_DATE | ||||||
|     case IteratorState::DATETIME_DATE: |     case IteratorState::DATETIME_DATE: | ||||||
|       if (this->at_ >= App.get_dates().size()) { |       this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *date = App.get_dates()[this->at_]; |  | ||||||
|         if (date->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_date(date); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_TIME | #ifdef USE_DATETIME_TIME | ||||||
|     case IteratorState::DATETIME_TIME: |     case IteratorState::DATETIME_TIME: | ||||||
|       if (this->at_ >= App.get_times().size()) { |       this->process_platform_item_(App.get_times(), &ComponentIterator::on_time); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *time = App.get_times()[this->at_]; |  | ||||||
|         if (time->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_time(time); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_DATETIME_DATETIME | #ifdef USE_DATETIME_DATETIME | ||||||
|     case IteratorState::DATETIME_DATETIME: |     case IteratorState::DATETIME_DATETIME: | ||||||
|       if (this->at_ >= App.get_datetimes().size()) { |       this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *datetime = App.get_datetimes()[this->at_]; |  | ||||||
|         if (datetime->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_datetime(datetime); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_TEXT | #ifdef USE_TEXT | ||||||
|     case IteratorState::TEXT: |     case IteratorState::TEXT: | ||||||
|       if (this->at_ >= App.get_texts().size()) { |       this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *text = App.get_texts()[this->at_]; |  | ||||||
|         if (text->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_text(text); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|     case IteratorState::SELECT: |     case IteratorState::SELECT: | ||||||
|       if (this->at_ >= App.get_selects().size()) { |       this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *select = App.get_selects()[this->at_]; |  | ||||||
|         if (select->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_select(select); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LOCK | #ifdef USE_LOCK | ||||||
|     case IteratorState::LOCK: |     case IteratorState::LOCK: | ||||||
|       if (this->at_ >= App.get_locks().size()) { |       this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *a_lock = App.get_locks()[this->at_]; |  | ||||||
|         if (a_lock->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_lock(a_lock); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_VALVE | #ifdef USE_VALVE | ||||||
|     case IteratorState::VALVE: |     case IteratorState::VALVE: | ||||||
|       if (this->at_ >= App.get_valves().size()) { |       this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *valve = App.get_valves()[this->at_]; |  | ||||||
|         if (valve->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_valve(valve); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|     case IteratorState::MEDIA_PLAYER: |     case IteratorState::MEDIA_PLAYER: | ||||||
|       if (this->at_ >= App.get_media_players().size()) { |       this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *media_player = App.get_media_players()[this->at_]; |  | ||||||
|         if (media_player->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_media_player(media_player); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_ALARM_CONTROL_PANEL | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|     case IteratorState::ALARM_CONTROL_PANEL: |     case IteratorState::ALARM_CONTROL_PANEL: | ||||||
|       if (this->at_ >= App.get_alarm_control_panels().size()) { |       this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_]; |  | ||||||
|         if (a_alarm_control_panel->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_alarm_control_panel(a_alarm_control_panel); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_EVENT | #ifdef USE_EVENT | ||||||
|     case IteratorState::EVENT: |     case IteratorState::EVENT: | ||||||
|       if (this->at_ >= App.get_events().size()) { |       this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *event = App.get_events()[this->at_]; |  | ||||||
|         if (event->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_event(event); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_UPDATE | #ifdef USE_UPDATE | ||||||
|     case IteratorState::UPDATE: |     case IteratorState::UPDATE: | ||||||
|       if (this->at_ >= App.get_updates().size()) { |       this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update); | ||||||
|         advance_platform = true; |  | ||||||
|       } else { |  | ||||||
|         auto *update = App.get_updates()[this->at_]; |  | ||||||
|         if (update->is_internal() && !this->include_internal_) { |  | ||||||
|           success = true; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           success = this->on_update(update); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|     case IteratorState::MAX: |     case IteratorState::MAX: | ||||||
|       if (this->on_end()) { |       if (this->on_end()) { | ||||||
|         this->state_ = IteratorState::NONE; |         this->state_ = IteratorState::NONE; | ||||||
|       } |       } | ||||||
|       return; |       return; | ||||||
|   } |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|   if (advance_platform) { |  | ||||||
|     this->state_ = static_cast<IteratorState>(static_cast<uint8_t>(this->state_) + 1); |  | ||||||
|     this->at_ = 0; |  | ||||||
|   } else if (success) { |  | ||||||
|     this->at_++; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| bool ComponentIterator::on_end() { return true; } | bool ComponentIterator::on_end() { return true; } | ||||||
| bool ComponentIterator::on_begin() { return true; } | bool ComponentIterator::on_begin() { return true; } | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
|   | |||||||
| @@ -171,6 +171,11 @@ class ComponentIterator { | |||||||
|   } state_{IteratorState::NONE}; |   } state_{IteratorState::NONE}; | ||||||
|   uint16_t at_{0};  // Supports up to 65,535 entities per type |   uint16_t at_{0};  // Supports up to 65,535 entities per type | ||||||
|   bool include_internal_{false}; |   bool include_internal_{false}; | ||||||
|  |  | ||||||
|  |   template<typename PlatformItem> | ||||||
|  |   void process_platform_item_(const std::vector<PlatformItem *> &items, | ||||||
|  |                               bool (ComponentIterator::*on_item)(PlatformItem *)); | ||||||
|  |   void advance_platform_(); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -258,7 +258,9 @@ std::string format_hex(const uint8_t *data, size_t length) { | |||||||
| std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); } | std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); } | ||||||
|  |  | ||||||
| static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } | static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } | ||||||
| std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { |  | ||||||
|  | // Shared implementation for uint8_t and string hex formatting | ||||||
|  | static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { | ||||||
|   if (data == nullptr || length == 0) |   if (data == nullptr || length == 0) | ||||||
|     return ""; |     return ""; | ||||||
|   std::string ret; |   std::string ret; | ||||||
| @@ -274,6 +276,10 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator | |||||||
|     return ret + " (" + std::to_string(length) + ")"; |     return ret + " (" + std::to_string(length) + ")"; | ||||||
|   return ret; |   return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { | ||||||
|  |   return format_hex_pretty_uint8(data, length, separator, show_length); | ||||||
|  | } | ||||||
| std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) { | std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) { | ||||||
|   return format_hex_pretty(data.data(), data.size(), separator, show_length); |   return format_hex_pretty(data.data(), data.size(), separator, show_length); | ||||||
| } | } | ||||||
| @@ -300,20 +306,7 @@ std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, | |||||||
|   return format_hex_pretty(data.data(), data.size(), separator, show_length); |   return format_hex_pretty(data.data(), data.size(), separator, show_length); | ||||||
| } | } | ||||||
| std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { | std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { | ||||||
|   if (data.empty()) |   return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length); | ||||||
|     return ""; |  | ||||||
|   std::string ret; |  | ||||||
|   uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise |  | ||||||
|   ret.resize(multiple * data.length() - (separator ? 1 : 0)); |  | ||||||
|   for (size_t i = 0; i < data.length(); i++) { |  | ||||||
|     ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); |  | ||||||
|     ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); |  | ||||||
|     if (separator && i != data.length() - 1) |  | ||||||
|       ret[multiple * i + 2] = separator; |  | ||||||
|   } |  | ||||||
|   if (show_length && data.length() > 4) |  | ||||||
|     return ret + " (" + std::to_string(data.length()) + ")"; |  | ||||||
|   return ret; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| std::string format_bin(const uint8_t *data, size_t length) { | std::string format_bin(const uint8_t *data, size_t length) { | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <array> | ||||||
| #include <cmath> | #include <cmath> | ||||||
| #include <cstdint> | #include <cstdint> | ||||||
| #include <cstring> | #include <cstring> | ||||||
| @@ -678,7 +679,7 @@ class InterruptLock { | |||||||
|   ~InterruptLock(); |   ~InterruptLock(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| #if defined(USE_ESP8266) || defined(USE_RP2040) | #if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||||
|   uint32_t state_; |   uint32_t state_; | ||||||
| #endif | #endif | ||||||
| }; | }; | ||||||
| @@ -783,7 +784,7 @@ template<class T> class RAMAllocator { | |||||||
|   T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); } |   T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); } | ||||||
|  |  | ||||||
|   T *reallocate(T *p, size_t n, size_t manual_size) { |   T *reallocate(T *p, size_t n, size_t manual_size) { | ||||||
|     size_t size = n * sizeof(T); |     size_t size = n * manual_size; | ||||||
|     T *ptr = nullptr; |     T *ptr = nullptr; | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|     if (this->flags_ & Flags::ALLOC_EXTERNAL) { |     if (this->flags_ & Flags::ALLOC_EXTERNAL) { | ||||||
|   | |||||||
| @@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int: | |||||||
|     os.environ.setdefault( |     os.environ.setdefault( | ||||||
|         "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) |         "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) | ||||||
|     ) |     ) | ||||||
|  |     # Suppress Python syntax warnings from third-party scripts during compilation | ||||||
|  |     os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") | ||||||
|     cmd = ["platformio"] + list(args) |     cmd = ["platformio"] + list(args) | ||||||
|  |  | ||||||
|     if not CORE.verbose: |     if not CORE.verbose: | ||||||
|   | |||||||
| @@ -162,6 +162,9 @@ def get_ini_content(): | |||||||
|     # Sort to avoid changing build unflags order |     # Sort to avoid changing build unflags order | ||||||
|     CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) |     CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) | ||||||
|  |  | ||||||
|  |     # Add extra script for C++ flags | ||||||
|  |     CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) | ||||||
|  |  | ||||||
|     content = "[platformio]\n" |     content = "[platformio]\n" | ||||||
|     content += f"description = ESPHome {__version__}\n" |     content += f"description = ESPHome {__version__}\n" | ||||||
|  |  | ||||||
| @@ -222,6 +225,9 @@ def write_platformio_project(): | |||||||
|         write_gitignore() |         write_gitignore() | ||||||
|     write_platformio_ini(content) |     write_platformio_ini(content) | ||||||
|  |  | ||||||
|  |     # Write extra script for C++ specific flags | ||||||
|  |     write_cxx_flags_script() | ||||||
|  |  | ||||||
|  |  | ||||||
| DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ | DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ | ||||||
| #pragma once | #pragma once | ||||||
| @@ -394,3 +400,20 @@ def write_gitignore(): | |||||||
|     if not os.path.isfile(path): |     if not os.path.isfile(path): | ||||||
|         with open(file=path, mode="w", encoding="utf-8") as f: |         with open(file=path, mode="w", encoding="utf-8") as f: | ||||||
|             f.write(GITIGNORE_CONTENT) |             f.write(GITIGNORE_CONTENT) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CXX_FLAGS_FILE_NAME = "cxx_flags.py" | ||||||
|  | CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags | ||||||
|  | Import("env") | ||||||
|  |  | ||||||
|  | # Add C++ specific flags | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def write_cxx_flags_script() -> None: | ||||||
|  |     path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) | ||||||
|  |     contents = CXX_FLAGS_FILE_CONTENTS | ||||||
|  |     if not CORE.is_host: | ||||||
|  |         contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' | ||||||
|  |         contents += "\n" | ||||||
|  |     write_file_if_changed(path, contents) | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ build_flags = | |||||||
| lib_deps = | lib_deps = | ||||||
|     esphome/noise-c@0.1.10                  ; api |     esphome/noise-c@0.1.10                  ; api | ||||||
|     improv/Improv@1.2.4                    ; improv_serial / esp32_improv |     improv/Improv@1.2.4                    ; improv_serial / esp32_improv | ||||||
|     bblanchon/ArduinoJson@6.18.5           ; json |     bblanchon/ArduinoJson@7.4.2            ; json | ||||||
|     wjtje/qr-code-generator-library@1.7.0  ; qr_code |     wjtje/qr-code-generator-library@1.7.0  ; qr_code | ||||||
|     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 |     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 | ||||||
|     pavlodn/HaierProtocol@0.9.31           ; haier |     pavlodn/HaierProtocol@0.9.31           ; haier | ||||||
| @@ -235,7 +235,7 @@ build_flags = | |||||||
|     -DUSE_ZEPHYR |     -DUSE_ZEPHYR | ||||||
|     -DUSE_NRF52 |     -DUSE_NRF52 | ||||||
| lib_deps = | lib_deps = | ||||||
|     bblanchon/ArduinoJson@7.0.0           ; json |     bblanchon/ArduinoJson@7.4.2           ; json | ||||||
|     wjtje/qr-code-generator-library@1.7.0  ; qr_code |     wjtje/qr-code-generator-library@1.7.0  ; qr_code | ||||||
|     pavlodn/HaierProtocol@0.9.31           ; haier |     pavlodn/HaierProtocol@0.9.31           ; haier | ||||||
|     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 |     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile | |||||||
| esptool==4.9.0 | esptool==4.9.0 | ||||||
| click==8.1.7 | click==8.1.7 | ||||||
| esphome-dashboard==20250514.0 | esphome-dashboard==20250514.0 | ||||||
| aioesphomeapi==34.2.1 | aioesphomeapi==35.0.1 | ||||||
| zeroconf==0.147.0 | zeroconf==0.147.0 | ||||||
| puremagic==1.30 | puremagic==1.30 | ||||||
| ruamel.yaml==0.18.14 # dashboard_import | ruamel.yaml==0.18.14 # dashboard_import | ||||||
|   | |||||||
| @@ -62,26 +62,6 @@ def get_clang_tidy_version_from_requirements() -> str: | |||||||
|     return "clang-tidy version not found" |     return "clang-tidy version not found" | ||||||
|  |  | ||||||
|  |  | ||||||
| def extract_platformio_flags() -> str: |  | ||||||
|     """Extract clang-tidy related flags from platformio.ini""" |  | ||||||
|     flags: list[str] = [] |  | ||||||
|     in_clangtidy_section = False |  | ||||||
|  |  | ||||||
|     platformio_path = Path(__file__).parent.parent / "platformio.ini" |  | ||||||
|     lines = read_file_lines(platformio_path) |  | ||||||
|     for line in lines: |  | ||||||
|         line = line.strip() |  | ||||||
|         if line.startswith("[flags:clangtidy]"): |  | ||||||
|             in_clangtidy_section = True |  | ||||||
|             continue |  | ||||||
|         elif line.startswith("[") and in_clangtidy_section: |  | ||||||
|             break |  | ||||||
|         elif in_clangtidy_section and line and not line.startswith("#"): |  | ||||||
|             flags.append(line) |  | ||||||
|  |  | ||||||
|     return "\n".join(sorted(flags)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def read_file_bytes(path: Path) -> bytes: | def read_file_bytes(path: Path) -> bytes: | ||||||
|     """Read bytes from a file.""" |     """Read bytes from a file.""" | ||||||
|     with open(path, "rb") as f: |     with open(path, "rb") as f: | ||||||
| @@ -101,9 +81,10 @@ def calculate_clang_tidy_hash() -> str: | |||||||
|     version = get_clang_tidy_version_from_requirements() |     version = get_clang_tidy_version_from_requirements() | ||||||
|     hasher.update(version.encode()) |     hasher.update(version.encode()) | ||||||
|  |  | ||||||
|     # Hash relevant platformio.ini sections |     # Hash the entire platformio.ini file | ||||||
|     pio_flags = extract_platformio_flags() |     platformio_path = Path(__file__).parent.parent / "platformio.ini" | ||||||
|     hasher.update(pio_flags.encode()) |     platformio_content = read_file_bytes(platformio_path) | ||||||
|  |     hasher.update(platformio_content) | ||||||
|  |  | ||||||
|     return hasher.hexdigest() |     return hasher.hexdigest() | ||||||
|  |  | ||||||
| @@ -126,7 +107,8 @@ def write_file_content(path: Path, content: str) -> None: | |||||||
| def write_hash(hash_value: str) -> None: | def write_hash(hash_value: str) -> None: | ||||||
|     """Write hash to file""" |     """Write hash to file""" | ||||||
|     hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" |     hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" | ||||||
|     write_file_content(hash_file, hash_value) |     # Strip any trailing newlines to ensure consistent formatting | ||||||
|  |     write_file_content(hash_file, hash_value.strip() + "\n") | ||||||
|  |  | ||||||
|  |  | ||||||
| def main() -> None: | def main() -> None: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | """ESPHome tests package.""" | ||||||
							
								
								
									
										0
									
								
								tests/component_tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/component_tests/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/component_tests/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user