mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into proto_field_ifdefs
This commit is contained in:
		| @@ -1 +1 @@ | ||||
| a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a | ||||
| 07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| [run] | ||||
| omit =  | ||||
| omit = | ||||
|     esphome/components/* | ||||
|     tests/integration/* | ||||
|   | ||||
							
								
								
									
										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 | ||||
| contact_links: | ||||
|   - name: Issue Tracker | ||||
|     url: https://github.com/esphome/issues | ||||
|     about: Please create bug reports in the dedicated issue tracker. | ||||
|   - name: Feature Request Tracker | ||||
|     url: https://github.com/esphome/feature-requests | ||||
|     about: | | ||||
|       Please create feature requests in the dedicated feature request tracker. | ||||
|   - name: Report an issue with the ESPHome documentation | ||||
|     url: https://github.com/esphome/esphome-docs/issues/new/choose | ||||
|     about: Report an issue with the ESPHome documentation. | ||||
|   - name: Report an issue with the ESPHome web server | ||||
|     url: https://github.com/esphome/esphome-webserver/issues/new/choose | ||||
|     about: Report an issue with the ESPHome web server. | ||||
|   - 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 | ||||
|     url: https://esphome.io/guides/faq.html | ||||
|     about: | | ||||
|       Please view the FAQ for common questions and what | ||||
|       to include in a bug report. | ||||
|     about: 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 | ||||
|       - name: Generate 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 }} | ||||
|         id: python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
| @@ -58,55 +58,9 @@ jobs: | ||||
|           python -m venv venv | ||||
|           . venv/bin/activate | ||||
|           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 . | ||||
|  | ||||
|   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: | ||||
|     name: Check pylint | ||||
|     runs-on: ubuntu-24.04 | ||||
| @@ -130,29 +84,6 @@ jobs: | ||||
|         run: script/ci-suggest-changes | ||||
|         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: | ||||
|     name: Run script/ci-custom | ||||
|     runs-on: ubuntu-24.04 | ||||
| @@ -248,7 +179,6 @@ jobs: | ||||
|     outputs: | ||||
|       integration-tests: ${{ steps.determine.outputs.integration-tests }} | ||||
|       clang-tidy: ${{ steps.determine.outputs.clang-tidy }} | ||||
|       clang-format: ${{ steps.determine.outputs.clang-format }} | ||||
|       python-linters: ${{ steps.determine.outputs.python-linters }} | ||||
|       changed-components: ${{ steps.determine.outputs.changed-components }} | ||||
|       component-test-count: ${{ steps.determine.outputs.component-test-count }} | ||||
| @@ -276,7 +206,6 @@ jobs: | ||||
|           # Extract individual fields | ||||
|           echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $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 "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT | ||||
|           echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT | ||||
| @@ -317,46 +246,11 @@ jobs: | ||||
|           . venv/bin/activate | ||||
|           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: | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - ruff | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - pyupgrade | ||||
|       - determine-jobs | ||||
|     if: needs.determine-jobs.outputs.clang-tidy == 'true' | ||||
|     env: | ||||
| @@ -562,24 +456,41 @@ jobs: | ||||
|             ./script/test_build_components -e compile -c $component | ||||
|           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: | ||||
|     name: CI Status | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - ruff | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - integration-tests | ||||
|       - pyupgrade | ||||
|       - clang-tidy | ||||
|       - determine-jobs | ||||
|       - test-build-components | ||||
|       - test-build-components-splitter | ||||
|       - test-build-components-split | ||||
|       - pre-commit-ci-lite | ||||
|     if: always() | ||||
|     steps: | ||||
|       - 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/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: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
| @@ -20,13 +27,15 @@ repos: | ||||
|           - pydocstyle==5.1.1 | ||||
|         files: ^(esphome|tests)/.+\.py$ | ||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||
|     rev: v3.4.0 | ||||
|     rev: v5.0.0 | ||||
|     hooks: | ||||
|       - id: no-commit-to-branch | ||||
|         args: | ||||
|           - --branch=dev | ||||
|           - --branch=release | ||||
|           - --branch=beta | ||||
|       - id: end-of-file-fixer | ||||
|       - id: trailing-whitespace | ||||
|   - repo: https://github.com/asottile/pyupgrade | ||||
|     rev: v3.20.0 | ||||
|     hooks: | ||||
| @@ -36,6 +45,7 @@ repos: | ||||
|     rev: v1.37.1 | ||||
|     hooks: | ||||
|       - id: yamllint | ||||
|         exclude: ^(\.clang-format|\.clang-tidy)$ | ||||
|   - repo: https://github.com/pre-commit/mirrors-clang-format | ||||
|     rev: v13.0.1 | ||||
|     hooks: | ||||
|   | ||||
| @@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw | ||||
| esphome/components/nfc/* @jesserockz @kbx81 | ||||
| esphome/components/noblex/* @AGalfra | ||||
| esphome/components/npi19/* @bakerkj | ||||
| esphome/components/nrf52/* @tomaszduda23 | ||||
| esphome/components/number/* @esphome/core | ||||
| esphome/components/one_wire/* @ssieb | ||||
| esphome/components/online_image/* @clydebarrow @guillempages | ||||
| @@ -378,6 +379,7 @@ esphome/components/rp2040_pwm/* @jesserockz | ||||
| esphome/components/rpi_dpi_rgb/* @clydebarrow | ||||
| esphome/components/rtl87xx/* @kuba2k2 | ||||
| esphome/components/rtttl/* @glmnet | ||||
| esphome/components/runtime_stats/* @bdraco | ||||
| esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti | ||||
| esphome/components/scd4x/* @martgras @sjtrny | ||||
| esphome/components/script/* @esphome/core | ||||
| @@ -535,5 +537,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix | ||||
| esphome/components/xl9535/* @mreditor97 | ||||
| esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||
| esphome/components/xxtea/* @clydebarrow | ||||
| esphome/components/zephyr/* @tomaszduda23 | ||||
| esphome/components/zhlt01/* @cfeenstra1024 | ||||
| esphome/components/zio_ultrasonic/* @kahrendt | ||||
|   | ||||
| @@ -51,82 +51,83 @@ SAMPLING_MODES = { | ||||
|     "max": sampling_mode.MAX, | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
| adc2_channel_t = cg.global_ns.enum("adc2_channel_t") | ||||
| adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True) | ||||
|  | ||||
| adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True) | ||||
|  | ||||
| # pin to adc1 channel mapping | ||||
| # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         36: adc_channel_t.ADC_CHANNEL_0, | ||||
|         37: adc_channel_t.ADC_CHANNEL_1, | ||||
|         38: adc_channel_t.ADC_CHANNEL_2, | ||||
|         39: adc_channel_t.ADC_CHANNEL_3, | ||||
|         32: adc_channel_t.ADC_CHANNEL_4, | ||||
|         33: adc_channel_t.ADC_CHANNEL_5, | ||||
|         34: adc_channel_t.ADC_CHANNEL_6, | ||||
|         35: adc_channel_t.ADC_CHANNEL_7, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32C6: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         0: adc_channel_t.ADC_CHANNEL_0, | ||||
|         1: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         3: adc_channel_t.ADC_CHANNEL_3, | ||||
|         4: adc_channel_t.ADC_CHANNEL_4, | ||||
|         5: adc_channel_t.ADC_CHANNEL_5, | ||||
|         6: adc_channel_t.ADC_CHANNEL_6, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32H2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|         6: adc_channel_t.ADC_CHANNEL_5, | ||||
|         7: adc_channel_t.ADC_CHANNEL_6, | ||||
|         8: adc_channel_t.ADC_CHANNEL_7, | ||||
|         9: adc_channel_t.ADC_CHANNEL_8, | ||||
|         10: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|         1: adc_channel_t.ADC_CHANNEL_0, | ||||
|         2: adc_channel_t.ADC_CHANNEL_1, | ||||
|         3: adc_channel_t.ADC_CHANNEL_2, | ||||
|         4: adc_channel_t.ADC_CHANNEL_3, | ||||
|         5: adc_channel_t.ADC_CHANNEL_4, | ||||
|         6: adc_channel_t.ADC_CHANNEL_5, | ||||
|         7: adc_channel_t.ADC_CHANNEL_6, | ||||
|         8: adc_channel_t.ADC_CHANNEL_7, | ||||
|         9: adc_channel_t.ADC_CHANNEL_8, | ||||
|         10: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| @@ -135,24 +136,24 @@ ESP32_VARIANT_ADC1_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 | ||||
|     VARIANT_ESP32: { | ||||
|         4: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         0: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         2: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         27: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         25: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         26: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         4: adc_channel_t.ADC_CHANNEL_0, | ||||
|         0: adc_channel_t.ADC_CHANNEL_1, | ||||
|         2: adc_channel_t.ADC_CHANNEL_2, | ||||
|         15: adc_channel_t.ADC_CHANNEL_3, | ||||
|         13: adc_channel_t.ADC_CHANNEL_4, | ||||
|         12: adc_channel_t.ADC_CHANNEL_5, | ||||
|         14: adc_channel_t.ADC_CHANNEL_6, | ||||
|         27: adc_channel_t.ADC_CHANNEL_7, | ||||
|         25: adc_channel_t.ADC_CHANNEL_8, | ||||
|         26: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h | ||||
|     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 | ||||
|     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 | ||||
|     VARIANT_ESP32C6: {},  # no ADC2 | ||||
| @@ -160,29 +161,29 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { | ||||
|     VARIANT_ESP32H2: {},  # no ADC2 | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S2: { | ||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         11: adc_channel_t.ADC_CHANNEL_0, | ||||
|         12: adc_channel_t.ADC_CHANNEL_1, | ||||
|         13: adc_channel_t.ADC_CHANNEL_2, | ||||
|         14: adc_channel_t.ADC_CHANNEL_3, | ||||
|         15: adc_channel_t.ADC_CHANNEL_4, | ||||
|         16: adc_channel_t.ADC_CHANNEL_5, | ||||
|         17: adc_channel_t.ADC_CHANNEL_6, | ||||
|         18: adc_channel_t.ADC_CHANNEL_7, | ||||
|         19: adc_channel_t.ADC_CHANNEL_8, | ||||
|         20: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
|     # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h | ||||
|     VARIANT_ESP32S3: { | ||||
|         11: adc2_channel_t.ADC2_CHANNEL_0, | ||||
|         12: adc2_channel_t.ADC2_CHANNEL_1, | ||||
|         13: adc2_channel_t.ADC2_CHANNEL_2, | ||||
|         14: adc2_channel_t.ADC2_CHANNEL_3, | ||||
|         15: adc2_channel_t.ADC2_CHANNEL_4, | ||||
|         16: adc2_channel_t.ADC2_CHANNEL_5, | ||||
|         17: adc2_channel_t.ADC2_CHANNEL_6, | ||||
|         18: adc2_channel_t.ADC2_CHANNEL_7, | ||||
|         19: adc2_channel_t.ADC2_CHANNEL_8, | ||||
|         20: adc2_channel_t.ADC2_CHANNEL_9, | ||||
|         11: adc_channel_t.ADC_CHANNEL_0, | ||||
|         12: adc_channel_t.ADC_CHANNEL_1, | ||||
|         13: adc_channel_t.ADC_CHANNEL_2, | ||||
|         14: adc_channel_t.ADC_CHANNEL_3, | ||||
|         15: adc_channel_t.ADC_CHANNEL_4, | ||||
|         16: adc_channel_t.ADC_CHANNEL_5, | ||||
|         17: adc_channel_t.ADC_CHANNEL_6, | ||||
|         18: adc_channel_t.ADC_CHANNEL_7, | ||||
|         19: adc_channel_t.ADC_CHANNEL_8, | ||||
|         20: adc_channel_t.ADC_CHANNEL_9, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,12 +3,15 @@ | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #include <esp_adc_cal.h> | ||||
| #include "driver/adc.h" | ||||
| #endif  // USE_ESP32 | ||||
| #include "esp_adc/adc_cali.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 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
| @@ -49,33 +52,72 @@ class Aggregator { | ||||
|  | ||||
| class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | ||||
|  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 | ||||
|   /// 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_channel1(adc1_channel_t channel) { | ||||
|     this->channel1_ = channel; | ||||
|     this->channel2_ = ADC2_CHANNEL_MAX; | ||||
|   } | ||||
|   void set_channel2(adc2_channel_t channel) { | ||||
|     this->channel2_ = channel; | ||||
|     this->channel1_ = ADC1_CHANNEL_MAX; | ||||
|  | ||||
|   /// Configure the ADC to use a specific channel on ADC1. | ||||
|   /// 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_channel(adc_unit_t unit, adc_channel_t channel) { | ||||
|     this->adc_unit_ = unit; | ||||
|     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; } | ||||
| #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_ESP8266 | ||||
|   std::string unique_id() override; | ||||
| #endif  // USE_ESP8266 | ||||
| @@ -90,17 +132,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   InternalGPIOPin *pin_; | ||||
|   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 | ||||
|   bool is_temperature_{false}; | ||||
| #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 | ||||
|   | ||||
| @@ -8,145 +8,308 @@ namespace adc { | ||||
|  | ||||
| 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 | ||||
| #if USE_ESP32_VARIANT_ESP32S2 | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; | ||||
| #endif  // USE_ESP32_VARIANT_ESP32S2 | ||||
| #endif  // SOC_ADC_RTC_MAX_BITWIDTH | ||||
|  | ||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; | ||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); | ||||
|     if (!this->autorange_) { | ||||
|       adc1_config_channel_atten(this->channel1_, this->attenuation_); | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     if (!this->autorange_) { | ||||
|       adc2_config_channel_atten(this->channel2_, this->attenuation_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { | ||||
|     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; | ||||
|     } | ||||
| const LogString *attenuation_to_str(adc_atten_t attenuation) { | ||||
|   switch (attenuation) { | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       return LOG_STR("0 dB"); | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       return LOG_STR("2.5 dB"); | ||||
|     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"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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; | ||||
| const LogString *adc_unit_to_str(adc_unit_t unit) { | ||||
|   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"); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   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; | ||||
| void ADCSensor::setup() { | ||||
|   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_]; | ||||
|  | ||||
|   this->setup_flags_.handle_init_complete = true; | ||||
|  | ||||
|   adc_oneshot_chan_cfg_t config = { | ||||
|       .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; | ||||
|   } | ||||
|   this->setup_flags_.config_complete = true; | ||||
|  | ||||
|   // 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 | ||||
|   } | ||||
|  | ||||
|   this->setup_flags_.init_complete = true; | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Attenuation: %s\n" | ||||
|                 "  Samples: %i\n" | ||||
|                 "  Channel:       %d\n" | ||||
|                 "  Unit:          %s\n" | ||||
|                 "  Attenuation:   %s\n" | ||||
|                 "  Samples:       %i\n" | ||||
|                 "  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); | ||||
| } | ||||
|  | ||||
| float ADCSensor::sample() { | ||||
|   if (!this->autorange_) { | ||||
|     auto aggr = Aggregator(this->sampling_mode_); | ||||
|   if (this->autorange_) { | ||||
|     return this->sample_autorange_(); | ||||
|   } else { | ||||
|     return this->sample_fixed_attenuation_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
|     for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|       int raw = -1; | ||||
|       if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|         raw = adc1_get_raw(this->channel1_); | ||||
|       } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); | ||||
|       } | ||||
|       if (raw == -1) { | ||||
|         return NAN; | ||||
|       } | ||||
| float ADCSensor::sample_fixed_attenuation_() { | ||||
|   auto aggr = Aggregator(this->sampling_mode_); | ||||
|  | ||||
|       aggr.add_sample(raw); | ||||
|   for (uint8_t sample = 0; sample < this->sample_count_; sample++) { | ||||
|     int raw; | ||||
|     esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||
|  | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "ADC read failed with error %d", err); | ||||
|       continue; | ||||
|     } | ||||
|     if (this->output_raw_) { | ||||
|       return aggr.aggregate(); | ||||
|  | ||||
|     if (raw == -1) { | ||||
|       ESP_LOGW(TAG, "Invalid ADC reading"); | ||||
|       continue; | ||||
|     } | ||||
|     uint32_t mv = | ||||
|         esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); | ||||
|     return mv / 1000.0f; | ||||
|  | ||||
|     aggr.add_sample(raw); | ||||
|   } | ||||
|  | ||||
|   int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|   uint32_t final_value = aggr.aggregate(); | ||||
|  | ||||
|   if (this->channel1_ != ADC1_CHANNEL_MAX) { | ||||
|     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_); | ||||
|         } | ||||
|   if (this->output_raw_) { | ||||
|     return final_value; | ||||
|   } | ||||
|  | ||||
|   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; | ||||
|       } | ||||
|     } | ||||
|   } else if (this->channel2_ != ADC2_CHANNEL_MAX) { | ||||
|     adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); | ||||
|     adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); | ||||
|     if (raw12 < ADC_MAX) { | ||||
|       adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); | ||||
|       adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); | ||||
|       if (raw6 < ADC_MAX) { | ||||
|         adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); | ||||
|         adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); | ||||
|         if (raw2 < ADC_MAX) { | ||||
|           adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); | ||||
|           adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); | ||||
|         } | ||||
|   } | ||||
|  | ||||
|   return final_value * 3.3f / 4095.0f; | ||||
| } | ||||
|  | ||||
| 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; | ||||
|     } | ||||
|  | ||||
|     // Create new calibration handle for this attenuation | ||||
|     adc_cali_handle_t handle = nullptr; | ||||
|  | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 | ||||
|     adc_cali_curve_fitting_config_t cali_config = {}; | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|     cali_config.chan = this->channel_; | ||||
| #endif | ||||
|     cali_config.unit_id = this->adc_unit_; | ||||
|     cali_config.atten = atten; | ||||
|     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; | ||||
|   } | ||||
|  | ||||
|   uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); | ||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   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); | ||||
|   const int adc_half = 2048; | ||||
|   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(4095 - raw0, adc_half); | ||||
|   uint32_t csum = c12 + c6 + c2 + c0; | ||||
|  | ||||
|   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
|   if (csum == 0) { | ||||
|     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; | ||||
| } | ||||
|  | ||||
| }  // namespace adc | ||||
|   | ||||
| @@ -10,13 +10,11 @@ from esphome.const import ( | ||||
|     CONF_NUMBER, | ||||
|     CONF_PIN, | ||||
|     CONF_RAW, | ||||
|     CONF_WIFI, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| from . import ( | ||||
|     ATTENUATION_MODES, | ||||
| @@ -24,6 +22,7 @@ from . import ( | ||||
|     ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, | ||||
|     SAMPLING_MODES, | ||||
|     adc_ns, | ||||
|     adc_unit_t, | ||||
|     validate_adc_pin, | ||||
| ) | ||||
|  | ||||
| @@ -57,21 +56,6 @@ def validate_config(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", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler | ||||
| ) | ||||
| @@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|     validate_config, | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = final_validate_config | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     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_sampling_mode(config[CONF_SAMPLING_MODE])) | ||||
|  | ||||
|     if attenuation := config.get(CONF_ATTENUATION): | ||||
|         if attenuation == "auto": | ||||
|             cg.add(var.set_autorange(cg.global_ns.true)) | ||||
|         else: | ||||
|             cg.add(var.set_attenuation(attenuation)) | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         if attenuation := config.get(CONF_ATTENUATION): | ||||
|             if attenuation == "auto": | ||||
|                 cg.add(var.set_autorange(cg.global_ns.true)) | ||||
|             else: | ||||
|                 cg.add(var.set_attenuation(attenuation)) | ||||
|  | ||||
|         variant = get_esp32_variant() | ||||
|         pin_num = config[CONF_PIN][CONF_NUMBER] | ||||
|         if ( | ||||
| @@ -133,10 +115,10 @@ async def to_code(config): | ||||
|             and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             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 ( | ||||
|             variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL | ||||
|             and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] | ||||
|         ): | ||||
|             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)) | ||||
|   | ||||
| @@ -86,8 +86,8 @@ void APIConnection::start() { | ||||
|   APIError err = this->helper_->init(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|              api_error_to_str(err), errno); | ||||
|     ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), | ||||
|              errno); | ||||
|     return; | ||||
|   } | ||||
|   this->client_info_ = helper_->getpeername(); | ||||
| @@ -119,7 +119,7 @@ void APIConnection::loop() { | ||||
|   APIError err = this->helper_->loop(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|              api_error_to_str(err), errno); | ||||
|     return; | ||||
|   } | ||||
| @@ -136,14 +136,8 @@ void APIConnection::loop() { | ||||
|         break; | ||||
|       } else if (err != APIError::OK) { | ||||
|         on_fatal_error(); | ||||
|         if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { | ||||
|           ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); | ||||
|         } else if (err == APIError::CONNECTION_CLOSED) { | ||||
|           ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); | ||||
|         } else { | ||||
|           ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|                    api_error_to_str(err), errno); | ||||
|         } | ||||
|         ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), | ||||
|                  errno); | ||||
|         return; | ||||
|       } else { | ||||
|         this->last_traffic_ = now; | ||||
| @@ -1435,6 +1429,24 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char | ||||
|   return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| void APIConnection::complete_authentication_() { | ||||
|   // Early return if already authenticated | ||||
|   if (this->flags_.connection_state == static_cast<uint8_t>(ConnectionState::AUTHENTICATED)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); | ||||
|   ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|   this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); | ||||
| #endif | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   if (homeassistant::global_homeassistant_time != nullptr) { | ||||
|     this->send_time_request(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|   this->client_info_ = msg.client_info; | ||||
|   this->client_peername_ = this->helper_->getpeername(); | ||||
| @@ -1450,7 +1462,14 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|   resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; | ||||
|   resp.name = App.get_name(); | ||||
|  | ||||
| #ifdef USE_API_PASSWORD | ||||
|   // Password required - wait for authentication | ||||
|   this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED); | ||||
| #else | ||||
|   // No password configured - auto-authenticate | ||||
|   this->complete_authentication_(); | ||||
| #endif | ||||
|  | ||||
|   return resp; | ||||
| } | ||||
| ConnectResponse APIConnection::connect(const ConnectRequest &msg) { | ||||
| @@ -1463,23 +1482,14 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { | ||||
|   // bool invalid_password = 1; | ||||
|   resp.invalid_password = !correct; | ||||
|   if (correct) { | ||||
|     ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); | ||||
|     this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|     this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); | ||||
| #endif | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|     if (homeassistant::global_homeassistant_time != nullptr) { | ||||
|       this->send_time_request(); | ||||
|     } | ||||
| #endif | ||||
|     this->complete_authentication_(); | ||||
|   } | ||||
|   return resp; | ||||
| } | ||||
| DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
|   DeviceInfoResponse resp{}; | ||||
| #ifdef USE_API_PASSWORD | ||||
|   resp.uses_password = this->parent_->uses_password(); | ||||
|   resp.uses_password = true; | ||||
| #else | ||||
|   resp.uses_password = false; | ||||
| #endif | ||||
| @@ -1598,7 +1608,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { | ||||
|   APIError err = this->helper_->loop(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|              api_error_to_str(err), errno); | ||||
|     return false; | ||||
|   } | ||||
| @@ -1619,12 +1629,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | ||||
|     return false; | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { | ||||
|       ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|                api_error_to_str(err), errno); | ||||
|     } | ||||
|     ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|              api_error_to_str(err), errno); | ||||
|     return false; | ||||
|   } | ||||
|   // Do not set last_traffic_ on send | ||||
| @@ -1632,11 +1638,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | ||||
| } | ||||
| void APIConnection::on_unauthenticated_access() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); | ||||
|   ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); | ||||
| } | ||||
| void APIConnection::on_no_setup_connection() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); | ||||
|   ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); | ||||
| } | ||||
| void APIConnection::on_fatal_error() { | ||||
|   this->helper_->close(); | ||||
| @@ -1801,12 +1807,8 @@ void APIConnection::process_batch_() { | ||||
|       this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); | ||||
|   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|     on_fatal_error(); | ||||
|     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { | ||||
|       ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|                api_error_to_str(err), errno); | ||||
|     } | ||||
|     ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), | ||||
|              errno); | ||||
|   } | ||||
|  | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   | ||||
| @@ -273,6 +273,9 @@ class APIConnection : public APIServerConnection { | ||||
|   ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); | ||||
|  | ||||
|  protected: | ||||
|   // Helper function to handle authentication completion | ||||
|   void complete_authentication_(); | ||||
|  | ||||
|   // Helper function to fill common entity info fields | ||||
|   static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { | ||||
|     // Set common fields that are shared by all entity types | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -219,8 +219,6 @@ void APIServer::dump_config() { | ||||
| } | ||||
|  | ||||
| #ifdef USE_API_PASSWORD | ||||
| bool APIServer::uses_password() const { return !this->password_.empty(); } | ||||
|  | ||||
| bool APIServer::check_password(const std::string &password) const { | ||||
|   // depend only on input password length | ||||
|   const char *a = this->password_.c_str(); | ||||
| @@ -428,7 +426,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
|   ESP_LOGD(TAG, "Noise PSK saved"); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         c->send_message(DisconnectRequest()); | ||||
|   | ||||
| @@ -39,7 +39,6 @@ class APIServer : public Component, public Controller { | ||||
|   bool teardown() override; | ||||
| #ifdef USE_API_PASSWORD | ||||
|   bool check_password(const std::string &password) const; | ||||
|   bool uses_password() const; | ||||
|   void set_password(const std::string &password); | ||||
| #endif | ||||
|   void set_port(uint16_t port); | ||||
|   | ||||
| @@ -175,23 +175,7 @@ class Proto32Bit { | ||||
|   const uint32_t value_; | ||||
| }; | ||||
|  | ||||
| class Proto64Bit { | ||||
|  public: | ||||
|   explicit Proto64Bit(uint64_t value) : value_(value) {} | ||||
|   uint64_t as_fixed64() const { return this->value_; } | ||||
|   int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); } | ||||
|   double as_double() const { | ||||
|     union { | ||||
|       uint64_t raw; | ||||
|       double value; | ||||
|     } s{}; | ||||
|     s.raw = this->value_; | ||||
|     return s.value; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   const uint64_t value_; | ||||
| }; | ||||
| // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported | ||||
|  | ||||
| class ProtoWriteBuffer { | ||||
|  public: | ||||
| @@ -258,20 +242,10 @@ class ProtoWriteBuffer { | ||||
|     this->write((value >> 16) & 0xFF); | ||||
|     this->write((value >> 24) & 0xFF); | ||||
|   } | ||||
|   void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { | ||||
|     if (value == 0 && !force) | ||||
|       return; | ||||
|  | ||||
|     this->encode_field_raw(field_id, 1);  // type 1: 64-bit fixed64 | ||||
|     this->write((value >> 0) & 0xFF); | ||||
|     this->write((value >> 8) & 0xFF); | ||||
|     this->write((value >> 16) & 0xFF); | ||||
|     this->write((value >> 24) & 0xFF); | ||||
|     this->write((value >> 32) & 0xFF); | ||||
|     this->write((value >> 40) & 0xFF); | ||||
|     this->write((value >> 48) & 0xFF); | ||||
|     this->write((value >> 56) & 0xFF); | ||||
|   } | ||||
|   // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally | ||||
|   // not supported to reduce overhead on embedded systems. All ESPHome devices are | ||||
|   // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support | ||||
|   // is needed in the future, the necessary encoding/decoding functions must be added. | ||||
|   void encode_float(uint32_t field_id, float value, bool force = false) { | ||||
|     if (value == 0.0f && !force) | ||||
|       return; | ||||
| @@ -337,7 +311,7 @@ class ProtoMessage { | ||||
|   virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } | ||||
|   virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } | ||||
|   virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } | ||||
|   virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } | ||||
|   // NOTE: decode_64bit removed - wire type 1 not supported | ||||
| }; | ||||
|  | ||||
| class ProtoSize { | ||||
| @@ -662,33 +636,8 @@ class ProtoSize { | ||||
|     total_size += field_id_size + varint(value); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a sint64 field to the total message size | ||||
|    * | ||||
|    * Sint64 fields use ZigZag encoding, which is more efficient for negative values. | ||||
|    */ | ||||
|   static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) { | ||||
|     // Skip calculation if value is zero | ||||
|     if (value == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) | ||||
|     uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63)); | ||||
|     total_size += field_id_size + varint(zigzag); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a sint64 field to the total message size (repeated field version) | ||||
|    * | ||||
|    * Sint64 fields use ZigZag encoding, which is more efficient for negative values. | ||||
|    */ | ||||
|   static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) { | ||||
|     // Always calculate size for repeated fields | ||||
|     // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) | ||||
|     uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63)); | ||||
|     total_size += field_id_size + varint(zigzag); | ||||
|   } | ||||
|   // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed | ||||
|   // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems | ||||
|  | ||||
|   /** | ||||
|    * @brief Calculates and adds the size of a string/bytes field to the total message size | ||||
|   | ||||
| @@ -16,6 +16,8 @@ class UserServiceDescriptor { | ||||
|   virtual ListEntitiesServicesResponse encode_list_service_response() = 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); | ||||
|   | ||||
| @@ -3,8 +3,6 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/as3935/as3935.h" | ||||
| #include "esphome/components/spi/spi.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace as3935_spi { | ||||
|   | ||||
| @@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     if CORE.is_esp32 or CORE.is_libretiny: | ||||
|         # 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: | ||||
|         # https://github.com/ESP32Async/ESPAsyncTCP | ||||
|         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| CONF_BYTE_ORDER = "byte_order" | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_DRAW_ROUNDING = "draw_rounding" | ||||
| CONF_ON_STATE_CHANGE = "on_state_change" | ||||
| CONF_REQUEST_HEADERS = "request_headers" | ||||
|   | ||||
| @@ -20,14 +20,16 @@ adjusted_ids = set() | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.ensure_list( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(EspLdo), | ||||
|             cv.Required(CONF_VOLTAGE): cv.All( | ||||
|                 cv.voltage, cv.float_range(min=0.5, max=2.7) | ||||
|             ), | ||||
|             cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), | ||||
|             cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, | ||||
|         } | ||||
|         cv.COMPONENT_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.GenerateID(): cv.declare_id(EspLdo), | ||||
|                 cv.Required(CONF_VOLTAGE): cv.All( | ||||
|                     cv.voltage, cv.float_range(min=0.5, max=2.7) | ||||
|                 ), | ||||
|                 cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), | ||||
|                 cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, | ||||
|             } | ||||
|         ) | ||||
|     ), | ||||
|     cv.only_with_esp_idf, | ||||
|     only_on_variant(supported=[VARIANT_ESP32P4]), | ||||
|   | ||||
| @@ -17,6 +17,9 @@ class EspLdo : public Component { | ||||
|   void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } | ||||
|   void set_voltage(float voltage) { this->voltage_ = voltage; } | ||||
|   void adjust_voltage(float voltage); | ||||
|   float get_setup_priority() const override { | ||||
|     return setup_priority::BUS;  // LDO setup should be done early | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   int channel_; | ||||
|   | ||||
| @@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() { | ||||
|   return {}; | ||||
| } | ||||
| void Fan::save_state_() { | ||||
|   if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   FanRestoreState state{}; | ||||
|   state.state = this->state; | ||||
|   state.oscillating = this->oscillating; | ||||
|   | ||||
| @@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) { | ||||
|     container.reset();  // Release ownership of the container's shared_ptr | ||||
|  | ||||
|     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"); | ||||
|         return false; | ||||
|       } | ||||
| @@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) { | ||||
|       this_update->update_info_.latest_version = root["version"].as<std::string>(); | ||||
|  | ||||
|       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"); | ||||
|           return false; | ||||
|         } | ||||
|         if (build["chipFamily"] == ESPHOME_VARIANT) { | ||||
|           if (!build.containsKey("ota")) { | ||||
|           if (!build["ota"].is<JsonObject>()) { | ||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||
|             return false; | ||||
|           } | ||||
|           auto ota = build["ota"]; | ||||
|           if (!ota.containsKey("path") || !ota.containsKey("md5")) { | ||||
|           JsonObject ota = build["ota"].as<JsonObject>(); | ||||
|           if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) { | ||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||
|             return false; | ||||
|           } | ||||
|           this_update->update_info_.firmware_url = ota["path"].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>(); | ||||
|           if (ota.containsKey("release_url")) | ||||
|           if (ota["release_url"].is<const char *>()) | ||||
|             this_update->update_info_.release_url = ota["release_url"].as<std::string>(); | ||||
|  | ||||
|           return true; | ||||
|   | ||||
| @@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
| @coroutine_with_priority(1.0) | ||||
| 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_global(json_ns.using) | ||||
|   | ||||
| @@ -1,83 +1,76 @@ | ||||
| #include "json_util.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| // ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h | ||||
|  | ||||
| namespace esphome { | ||||
| namespace json { | ||||
|  | ||||
| static const char *const TAG = "json"; | ||||
|  | ||||
| static std::vector<char> global_json_build_buffer;  // NOLINT | ||||
| static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL); | ||||
| // Build an allocator for the JSON Library using the RAMAllocator class | ||||
| 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) { | ||||
|   // Here we are allocating up to 5kb of memory, | ||||
|   // with the heap size minus 2kb to be safe if less than 5kb | ||||
|   // as we can not have a true dynamic sized document. | ||||
|   // The excess memory is freed below with `shrinkToFit()` | ||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); | ||||
|   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 "{}"; | ||||
|     } | ||||
|     JsonObject root = json_document.to<JsonObject>(); | ||||
|     f(root); | ||||
|     if (json_document.overflowed()) { | ||||
|       if (request_size == free_heap) { | ||||
|         ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes", | ||||
|                  free_heap); | ||||
|         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; | ||||
|     serializeJson(json_document, output); | ||||
|     return output; | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   auto doc_allocator = SpiRamAllocator(); | ||||
|   JsonDocument json_document(&doc_allocator); | ||||
|   if (json_document.overflowed()) { | ||||
|     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||
|     return "{}"; | ||||
|   } | ||||
|   JsonObject root = json_document.to<JsonObject>(); | ||||
|   f(root); | ||||
|   if (json_document.overflowed()) { | ||||
|     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||
|     return "{}"; | ||||
|   } | ||||
|   std::string output; | ||||
|   serializeJson(json_document, output); | ||||
|   return output; | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
|  | ||||
| bool parse_json(const std::string &data, const json_parse_t &f) { | ||||
|   // Here we are allocating 1.5 times the data size, | ||||
|   // with the heap size minus 2kb to be safe if less than that | ||||
|   // as we can not have a true dynamic sized document. | ||||
|   // The excess memory is freed below with `shrinkToFit()` | ||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); | ||||
|   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; | ||||
|     } | ||||
|     DeserializationError err = deserializeJson(json_document, data); | ||||
|     json_document.shrinkToFit(); | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   auto doc_allocator = SpiRamAllocator(); | ||||
|   JsonDocument json_document(&doc_allocator); | ||||
|   if (json_document.overflowed()) { | ||||
|     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||
|     return false; | ||||
|   } | ||||
|   DeserializationError err = deserializeJson(json_document, data); | ||||
|  | ||||
|     JsonObject root = json_document.as<JsonObject>(); | ||||
|   JsonObject root = json_document.as<JsonObject>(); | ||||
|  | ||||
|     if (err == DeserializationError::Ok) { | ||||
|       return f(root); | ||||
|     } 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"); | ||||
|         return false; | ||||
|       } | ||||
|       ESP_LOGV(TAG, "Increasing memory allocation."); | ||||
|       request_size *= 2; | ||||
|       continue; | ||||
|     } else { | ||||
|       ESP_LOGE(TAG, "Parse error: %s", err.c_str()); | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
|   if (err == DeserializationError::Ok) { | ||||
|     return f(root); | ||||
|   } else if (err == DeserializationError::NoMemory) { | ||||
|     ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGE(TAG, "Parse error: %s", err.c_str()); | ||||
|   return false; | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
|  | ||||
| }  // namespace json | ||||
|   | ||||
| @@ -9,6 +9,7 @@ namespace light { | ||||
| // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema | ||||
|  | ||||
| void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | ||||
|   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   if (state.supports_effects()) | ||||
|     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) | ||||
|     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) { | ||||
|     color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 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) { | ||||
|   if (root.containsKey("state")) { | ||||
|   if (root["state"].is<const char *>()) { | ||||
|     auto val = parse_on_off(root["state"]); | ||||
|     switch (val) { | ||||
|       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); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("color")) { | ||||
|   if (root["color"].is<JsonObject>()) { | ||||
|     JsonObject color = root["color"]; | ||||
|     // 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; | ||||
|     if (color.containsKey("r")) { | ||||
|     if (color["r"].is<uint8_t>()) { | ||||
|       float r = float(color["r"]) / 255.0f; | ||||
|       max_rgb = fmaxf(max_rgb, r); | ||||
|       call.set_red(r); | ||||
|     } | ||||
|     if (color.containsKey("g")) { | ||||
|     if (color["g"].is<uint8_t>()) { | ||||
|       float g = float(color["g"]) / 255.0f; | ||||
|       max_rgb = fmaxf(max_rgb, g); | ||||
|       call.set_green(g); | ||||
|     } | ||||
|     if (color.containsKey("b")) { | ||||
|     if (color["b"].is<uint8_t>()) { | ||||
|       float b = float(color["b"]) / 255.0f; | ||||
|       max_rgb = fmaxf(max_rgb, 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); | ||||
|     } | ||||
|  | ||||
|     if (color.containsKey("c")) { | ||||
|     if (color["c"].is<uint8_t>()) { | ||||
|       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 | ||||
|       // white channel in RGBWW. | ||||
|       if (color.containsKey("c")) { | ||||
|       if (color["c"].is<uint8_t>()) { | ||||
|         call.set_warm_white(float(color["w"]) / 255.0f); | ||||
|       } else { | ||||
|         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); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("color_temp")) { | ||||
|   if (root["color_temp"].is<uint16_t>()) { | ||||
|     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) { | ||||
|   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); | ||||
|     call.set_flash_length(length); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("transition")) { | ||||
|   if (root["transition"].is<uint16_t>()) { | ||||
|     auto length = uint32_t(float(root["transition"]) * 1000); | ||||
|     call.set_transition_length(length); | ||||
|   } | ||||
|  | ||||
|   if (root.containsKey("effect")) { | ||||
|   if (root["effect"].is<const char *>()) { | ||||
|     const char *effect = root["effect"]; | ||||
|     call.set_effect(effect); | ||||
|   } | ||||
|   | ||||
| @@ -21,6 +21,11 @@ from esphome.components.libretiny.const import ( | ||||
|     COMPONENT_LN882X, | ||||
|     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 | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -41,6 +46,7 @@ from esphome.const import ( | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_LN882X, | ||||
|     PLATFORM_NRF52, | ||||
|     PLATFORM_RP2040, | ||||
|     PLATFORM_RTL87XX, | ||||
|     PlatformFramework, | ||||
| @@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] | ||||
|  | ||||
| UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] | ||||
|  | ||||
| UART_SELECTION_NRF52 = [USB_CDC, UART0] | ||||
|  | ||||
| HARDWARE_UART_TO_UART_SELECTION = { | ||||
|     UART0: logger_ns.UART_SELECTION_UART0, | ||||
|     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) | ||||
|     if CORE.is_host: | ||||
|         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 | ||||
|  | ||||
|  | ||||
| @@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_( | ||||
|     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" | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
| @@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 bk72xx=DEFAULT, | ||||
|                 ln882x=DEFAULT, | ||||
|                 rtl87xx=DEFAULT, | ||||
|                 nrf52=USB_CDC, | ||||
|             ): cv.All( | ||||
|                 cv.only_on( | ||||
|                     [ | ||||
| @@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                         PLATFORM_BK72XX, | ||||
|                         PLATFORM_LN882X, | ||||
|                         PLATFORM_RTL87XX, | ||||
|                         PLATFORM_NRF52, | ||||
|                     ] | ||||
|                 ), | ||||
|                 uart_selection, | ||||
| @@ -358,6 +371,15 @@ async def to_code(config): | ||||
|     except cv.Invalid: | ||||
|         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 | ||||
|     await cg.register_component(log, config) | ||||
|  | ||||
| @@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||
|             PlatformFramework.RTL87XX_ARDUINO, | ||||
|             PlatformFramework.LN882X_ARDUINO, | ||||
|         }, | ||||
|         "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, | ||||
|         "task_log_buffer.cpp": { | ||||
|             PlatformFramework.ESP32_ARDUINO, | ||||
|             PlatformFramework.ESP32_IDF, | ||||
|   | ||||
| @@ -4,9 +4,9 @@ | ||||
| #include <memory>  // For unique_ptr | ||||
| #endif | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| namespace esphome { | ||||
| 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 | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
|   this->main_task_ = xTaskGetCurrentTaskHandle(); | ||||
| #elif defined(USE_ZEPHYR) | ||||
|   this->main_task_ = k_current_get(); | ||||
| #endif | ||||
| } | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
| @@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifndef USE_ZEPHYR | ||||
| #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) | ||||
| void Logger::loop() { | ||||
| #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | ||||
| @@ -185,8 +188,13 @@ void Logger::loop() { | ||||
|     } | ||||
|     opened = !opened; | ||||
|   } | ||||
| #endif | ||||
|   this->process_messages_(); | ||||
| } | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| void Logger::process_messages_() { | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   // Process any buffered messages when available | ||||
|   if (this->log_buffer_->has_messages()) { | ||||
| @@ -227,12 +235,11 @@ void Logger::loop() { | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|  | ||||
| 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; } | ||||
|  | ||||
| #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_; } | ||||
| #endif | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,11 @@ | ||||
| #include <driver/uart.h> | ||||
| #endif  // USE_ESP_IDF | ||||
|  | ||||
| #ifdef USE_ZEPHYR | ||||
| #include <zephyr/kernel.h> | ||||
| struct device; | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| namespace logger { | ||||
| @@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { | ||||
|     "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 | ||||
|  * | ||||
|  * Advanced configuration (pin selection, etc) is not supported. | ||||
| @@ -82,7 +87,7 @@ enum UARTSelection : uint8_t { | ||||
|   UART_SELECTION_UART0_SWAP, | ||||
| #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. | ||||
| @@ -107,7 +112,7 @@ class Logger : public Component { | ||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||
|   void init_log_buffer(size_t total_buffer_size); | ||||
| #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; | ||||
| #endif | ||||
|   /// Manually set the baud rate for serial, set to 0 to disable. | ||||
| @@ -122,7 +127,7 @@ class Logger : public Component { | ||||
| #ifdef USE_ESP32 | ||||
|   void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } | ||||
| #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; } | ||||
|   /// Get the UART used by the logger. | ||||
|   UARTSelection get_uart() const; | ||||
| @@ -157,6 +162,7 @@ class Logger : public Component { | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   void process_messages_(); | ||||
|   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 | ||||
| @@ -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, | ||||
|                                                         va_list args, char *buffer, uint16_t *buffer_at, | ||||
|                                                         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); | ||||
| #else | ||||
|     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 | ||||
|   Stream *hw_serial_{nullptr}; | ||||
| #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 | ||||
| #endif | ||||
| #ifdef USE_ESP32 | ||||
| @@ -256,7 +265,7 @@ class Logger : public Component { | ||||
|   uint16_t tx_buffer_at_{0}; | ||||
|   uint16_t tx_buffer_size_{0}; | ||||
|   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}; | ||||
| #endif | ||||
| #ifdef USE_LIBRETINY | ||||
| @@ -268,9 +277,13 @@ class Logger : public Component { | ||||
|   bool global_recursion_guard_{false};  // Simple global recursion guard for single-task platforms | ||||
| #endif | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) | ||||
|   const char *HOT get_thread_name_() { | ||||
| #ifdef USE_ZEPHYR | ||||
|     k_tid_t current_task = k_current_get(); | ||||
| #else | ||||
|     TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); | ||||
| #endif | ||||
|     if (current_task == main_task_) { | ||||
|       return nullptr;  // Main task | ||||
|     } else { | ||||
| @@ -278,6 +291,8 @@ class Logger : public Component { | ||||
|       return pcTaskGetName(current_task); | ||||
| #elif defined(USE_LIBRETINY) | ||||
|       return pcTaskGetTaskName(current_task); | ||||
| #elif defined(USE_ZEPHYR) | ||||
|       return k_thread_name_get(current_task); | ||||
| #endif | ||||
|     } | ||||
|   } | ||||
| @@ -319,7 +334,7 @@ class Logger : public Component { | ||||
|     const char *color = esphome::logger::LOG_LEVEL_COLORS[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) { | ||||
|       // 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, | ||||
|   | ||||
							
								
								
									
										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 | ||||
| @@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"] | ||||
|  | ||||
| DOMAIN = "mipi_spi" | ||||
|  | ||||
| CONF_DRAW_FROM_ORIGIN = "draw_from_origin" | ||||
| CONF_SPI_16 = "spi_16" | ||||
| CONF_PIXEL_MODE = "pixel_mode" | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_BUS_MODE = "bus_mode" | ||||
| CONF_USE_AXIS_FLIPS = "use_axis_flips" | ||||
| CONF_NATIVE_WIDTH = "native_width" | ||||
|   | ||||
| @@ -3,11 +3,18 @@ import logging | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| 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 | ||||
| import esphome.config_validation as cv | ||||
| from esphome.config_validation import ALLOW_EXTRA | ||||
| from esphome.const import ( | ||||
|     CONF_BRIGHTNESS, | ||||
|     CONF_BUFFER_SIZE, | ||||
|     CONF_COLOR_ORDER, | ||||
|     CONF_CS_PIN, | ||||
|     CONF_DATA_RATE, | ||||
| @@ -24,19 +31,19 @@ from esphome.const import ( | ||||
|     CONF_MODEL, | ||||
|     CONF_OFFSET_HEIGHT, | ||||
|     CONF_OFFSET_WIDTH, | ||||
|     CONF_PAGES, | ||||
|     CONF_RESET_PIN, | ||||
|     CONF_ROTATION, | ||||
|     CONF_SWAP_XY, | ||||
|     CONF_TRANSFORM, | ||||
|     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 ( | ||||
|     CONF_BUS_MODE, | ||||
|     CONF_DRAW_FROM_ORIGIN, | ||||
|     CONF_NATIVE_HEIGHT, | ||||
|     CONF_NATIVE_WIDTH, | ||||
|     CONF_PIXEL_MODE, | ||||
| @@ -55,6 +62,7 @@ from .models import ( | ||||
|     MADCTL_XFLIP, | ||||
|     MADCTL_YFLIP, | ||||
|     DriverChip, | ||||
|     adafruit, | ||||
|     amoled, | ||||
|     cyd, | ||||
|     ili, | ||||
| @@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"] | ||||
|  | ||||
| LOGGER = logging.getLogger(DOMAIN) | ||||
| mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") | ||||
| MipiSpi = mipi_spi_ns.class_( | ||||
|     "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice | ||||
| MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice) | ||||
| MipiSpiBuffer = mipi_spi_ns.class_( | ||||
|     "MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice | ||||
| ) | ||||
| ColorOrder = display.display_ns.enum("ColorMode") | ||||
| ColorBitness = display.display_ns.enum("ColorBitness") | ||||
| Model = mipi_spi_ns.enum("Model") | ||||
|  | ||||
| PixelMode = mipi_spi_ns.enum("PixelMode") | ||||
| BusType = mipi_spi_ns.enum("BusType") | ||||
|  | ||||
| COLOR_ORDERS = { | ||||
|     MODE_RGB: ColorOrder.COLOR_ORDER_RGB, | ||||
|     MODE_BGR: ColorOrder.COLOR_ORDER_BGR, | ||||
| } | ||||
|  | ||||
| COLOR_DEPTHS = { | ||||
|     8: ColorBitness.COLOR_BITNESS_332, | ||||
|     16: ColorBitness.COLOR_BITNESS_565, | ||||
|     8: PixelMode.PIXEL_MODE_8, | ||||
|     16: PixelMode.PIXEL_MODE_16, | ||||
|     18: PixelMode.PIXEL_MODE_18, | ||||
| } | ||||
|  | ||||
| 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 | ||||
| # These statements are noops, but serve to suppress linting of side-effect-only imports | ||||
| for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): | ||||
| # This loop is a noop, but suppresses linting of side-effect-only imports | ||||
| for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit): | ||||
|     pass | ||||
|  | ||||
| PixelMode = mipi_spi_ns.enum("PixelMode") | ||||
|  | ||||
| PIXEL_MODE_18BIT = "18bit" | ||||
| PIXEL_MODE_16BIT = "16bit" | ||||
| DISPLAY_18BIT = "18bit" | ||||
| DISPLAY_16BIT = "16bit" | ||||
|  | ||||
| PIXEL_MODES = { | ||||
|     PIXEL_MODE_16BIT: 0x55, | ||||
|     PIXEL_MODE_18BIT: 0x66, | ||||
| DISPLAY_PIXEL_MODES = { | ||||
|     DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16), | ||||
|     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 validator(value): | ||||
|         value = cv.positive_int(value) | ||||
| @@ -158,41 +235,50 @@ 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( | ||||
|         { | ||||
|             cv.Required(CONF_MIRROR_X): 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 | ||||
|     iseqconf = ( | ||||
|         cv.Required(CONF_INIT_SEQUENCE) | ||||
|         if model.initsequence is None | ||||
|         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.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 = ( | ||||
|         ("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 = ( | ||||
|         display.FULL_DISPLAY_SCHEMA.extend( | ||||
|             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( | ||||
|                     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_DRAW_ROUNDING, 2): power_of_two, | ||||
|                 model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( | ||||
|                     cv.one_of(*pixel_modes, lower=True), | ||||
|                     cv.int_range(0, 255, min_included=True, max_included=True), | ||||
|                 model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( | ||||
|                     *pixel_modes, lower=True | ||||
|                 ), | ||||
|                 cv.Optional(CONF_TRANSFORM): transform, | ||||
|                 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), | ||||
|                 iseqconf: cv.ensure_list(map_sequence), | ||||
|                 cv.Optional(CONF_BUFFER_SIZE): cv.All( | ||||
|                     cv.percentage, cv.Range(0.12, 1.0) | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|         .extend( | ||||
|             { | ||||
|                 model.option(x): cv.boolean | ||||
|                 for x in [ | ||||
|                     CONF_DRAW_FROM_ORIGIN, | ||||
|                     CONF_SPI_16, | ||||
|                     CONF_INVERT_COLORS, | ||||
|                     CONF_USE_AXIS_FLIPS, | ||||
|                 ] | ||||
|             } | ||||
|         ) | ||||
|         .extend({model.option(x): cv.boolean for x in other_options}) | ||||
|     ) | ||||
|     if brightness := model.get_default(CONF_BRIGHTNESS): | ||||
|         schema = schema.extend( | ||||
| @@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): | ||||
|     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. | ||||
|     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) | ||||
|     return rotation and ( | ||||
|         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 | ||||
|     config = cv.Schema( | ||||
|         { | ||||
| @@ -288,29 +376,94 @@ def config_schema(config): | ||||
|         extra=ALLOW_EXTRA, | ||||
|     )(config) | ||||
|     bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) | ||||
|     swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True | ||||
|     config = model_schema(bus_mode, model, swapsies)(config) | ||||
|     config = model_schema(config)(config) | ||||
|     # Check for invalid combinations of MADCTL config | ||||
|     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( | ||||
|                 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: | ||||
|         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: | ||||
|         raise cv.Invalid(f"DC pin is required in {bus_mode} mode") | ||||
|     denominator(config) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = config_schema | ||||
| CONFIG_SCHEMA = customise_schema | ||||
|  | ||||
|  | ||||
| def get_transform(model, config): | ||||
|     can_transform = rotation_as_transform(model, config) | ||||
| def requires_buffer(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( | ||||
|         CONF_TRANSFORM, | ||||
|         { | ||||
| @@ -350,16 +503,13 @@ def get_sequence(model, config): | ||||
|     sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] | ||||
|     commands = [x[0] for x in sequence] | ||||
|     # Set pixel format if not already in the custom sequence | ||||
|     if PIXFMT not in commands: | ||||
|         pixel_mode = config[CONF_PIXEL_MODE] | ||||
|         if not isinstance(pixel_mode, int): | ||||
|             pixel_mode = PIXEL_MODES[pixel_mode] | ||||
|         sequence.append((PIXFMT, pixel_mode)) | ||||
|     pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]] | ||||
|     sequence.append((PIXFMT, pixel_mode[0])) | ||||
|     # Does the chip use the flipping bits for mirroring rather than the reverse order bits? | ||||
|     use_flip = config[CONF_USE_AXIS_FLIPS] | ||||
|     if MADCTL not in commands: | ||||
|         madctl = 0 | ||||
|         transform = get_transform(model, config) | ||||
|         transform = get_transform(config) | ||||
|         if transform.get(CONF_TRANSFORM): | ||||
|             LOGGER.info("Using hardware transform to implement rotation") | ||||
|         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): | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|     transform = get_transform(model, 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] | ||||
|         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 | ||||
|     ) | ||||
|     var_id = config[CONF_ID] | ||||
|     var_id.type, templateargs = get_instance(config) | ||||
|     var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) | ||||
|     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: | ||||
|             LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") | ||||
|         else: | ||||
|             config[CONF_ROTATION] = 0 | ||||
|     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_spi_16(config[CONF_SPI_16])) | ||||
|     if enable_pin := config.get(CONF_ENABLE_PIN): | ||||
|         enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] | ||||
|         cg.add(var.set_enable_pins(enable)) | ||||
| @@ -472,4 +621,5 @@ async def to_code(config): | ||||
|         cg.add(var.set_writer(lambda_)) | ||||
|     await display.register_display(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)) | ||||
|   | ||||
| @@ -2,489 +2,5 @@ | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| 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 mipi_spi {}  // namespace mipi_spi | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -4,40 +4,39 @@ | ||||
|  | ||||
| #include "esphome/components/spi/spi.h" | ||||
| #include "esphome/components/display/display.h" | ||||
| #include "esphome/components/display/display_buffer.h" | ||||
| #include "esphome/components/display/display_color_utils.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace mipi_spi { | ||||
|  | ||||
| constexpr static const char *const TAG = "display.mipi_spi"; | ||||
| static const uint8_t SW_RESET_CMD = 0x01; | ||||
| static const uint8_t SLEEP_OUT = 0x11; | ||||
| static const uint8_t NORON = 0x13; | ||||
| static const uint8_t INVERT_OFF = 0x20; | ||||
| static const uint8_t INVERT_ON = 0x21; | ||||
| static const uint8_t ALL_ON = 0x23; | ||||
| static const uint8_t WRAM = 0x24; | ||||
| static const uint8_t MIPI = 0x26; | ||||
| static const uint8_t DISPLAY_ON = 0x29; | ||||
| static const uint8_t RASET = 0x2B; | ||||
| static const uint8_t CASET = 0x2A; | ||||
| static const uint8_t WDATA = 0x2C; | ||||
| static const uint8_t TEON = 0x35; | ||||
| static const uint8_t MADCTL_CMD = 0x36; | ||||
| static const uint8_t PIXFMT = 0x3A; | ||||
| static const uint8_t BRIGHTNESS = 0x51; | ||||
| static const uint8_t SWIRE1 = 0x5A; | ||||
| static const uint8_t SWIRE2 = 0x5B; | ||||
| static const uint8_t PAGESEL = 0xFE; | ||||
| static constexpr uint8_t SW_RESET_CMD = 0x01; | ||||
| static constexpr uint8_t SLEEP_OUT = 0x11; | ||||
| static constexpr uint8_t NORON = 0x13; | ||||
| static constexpr uint8_t INVERT_OFF = 0x20; | ||||
| static constexpr uint8_t INVERT_ON = 0x21; | ||||
| static constexpr uint8_t ALL_ON = 0x23; | ||||
| static constexpr uint8_t WRAM = 0x24; | ||||
| static constexpr uint8_t MIPI = 0x26; | ||||
| static constexpr uint8_t DISPLAY_ON = 0x29; | ||||
| static constexpr uint8_t RASET = 0x2B; | ||||
| static constexpr uint8_t CASET = 0x2A; | ||||
| static constexpr uint8_t WDATA = 0x2C; | ||||
| static constexpr uint8_t TEON = 0x35; | ||||
| static constexpr uint8_t MADCTL_CMD = 0x36; | ||||
| static constexpr uint8_t PIXFMT = 0x3A; | ||||
| static constexpr uint8_t BRIGHTNESS = 0x51; | ||||
| static constexpr uint8_t SWIRE1 = 0x5A; | ||||
| static constexpr uint8_t SWIRE2 = 0x5B; | ||||
| static constexpr uint8_t PAGESEL = 0xFE; | ||||
|  | ||||
| static const uint8_t MADCTL_MY = 0x80;     // Bit 7 Bottom to top | ||||
| static const uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | ||||
| static const uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | ||||
| static const 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 const 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_MY = 0x80;     // Bit 7 Bottom to top | ||||
| static constexpr uint8_t MADCTL_MX = 0x40;     // Bit 6 Right to left | ||||
| static constexpr uint8_t MADCTL_MV = 0x20;     // Bit 5 Swap axes | ||||
| static constexpr uint8_t MADCTL_RGB = 0x00;    // Bit 3 Red-Green-Blue pixel order | ||||
| static constexpr uint8_t MADCTL_BGR = 0x08;    // Bit 3 Blue-Green-Red pixel order | ||||
| static constexpr uint8_t MADCTL_XFLIP = 0x02;  // Mirror the display horizontally | ||||
| static constexpr uint8_t MADCTL_YFLIP = 0x01;  // Mirror the display vertically | ||||
|  | ||||
| static const uint8_t DELAY_FLAG = 0xFF; | ||||
| // 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; | ||||
| } | ||||
|  | ||||
| // Buffer mode, conveniently also the number of bytes in a pixel | ||||
| enum PixelMode { | ||||
|   PIXEL_MODE_16, | ||||
|   PIXEL_MODE_18, | ||||
|   PIXEL_MODE_8 = 1, | ||||
|   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, | ||||
|                                       spi::DATA_RATE_1MHZ> { | ||||
|  public: | ||||
|   MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) | ||||
|       : width_(width), | ||||
|         height_(height), | ||||
|         offset_width_(offset_width), | ||||
|         offset_height_(offset_height), | ||||
|         color_depth_(color_depth) {} | ||||
|   MipiSpi() {} | ||||
|   void update() override { this->stop_poller(); } | ||||
|   void draw_pixel_at(int x, int y, Color color) override {} | ||||
|   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_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; } | ||||
| @@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer, | ||||
|     this->brightness_ = brightness; | ||||
|     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; } | ||||
|   void dump_config() override; | ||||
|  | ||||
|   int get_width_internal() override { return this->width_; } | ||||
|   int get_height_internal() override { return this->height_; } | ||||
|   bool can_proceed() override { return this->setup_complete_; } | ||||
|   int get_width_internal() override { return WIDTH; } | ||||
|   int get_height_internal() override { return HEIGHT; } | ||||
|   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_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: | ||||
|   bool check_buffer_() { | ||||
|     if (this->is_failed()) | ||||
|       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); | ||||
|  | ||||
|   /* METHODS */ | ||||
|   // convenience functions to write commands with or without data | ||||
|   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 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}; | ||||
|   std::vector<GPIOPin *> enable_pins_{}; | ||||
|   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_{}; | ||||
|   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}; | ||||
|   optional<uint8_t> brightness_{}; | ||||
|   const char *model_{"Unknown"}; | ||||
|   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 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 = {} | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from . import DriverChip | ||||
| from .ili import ILI9488_A | ||||
|  | ||||
| @@ -128,6 +130,7 @@ DriverChip( | ||||
|  | ||||
| ILI9488_A.extend( | ||||
|     "PICO-RESTOUCH-LCD-3.5", | ||||
|     swap_xy=cv.UNDEFINED, | ||||
|     spi_16=True, | ||||
|     pixel_mode="16bit", | ||||
|     mirror_x=True, | ||||
|   | ||||
| @@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_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(); | ||||
|   if (acp_supported_features & ACP_FEAT_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) { | ||||
|   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   if (!this->binary_sensor_->get_device_class().empty()) | ||||
|     root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); | ||||
|   if (this->binary_sensor_->is_status_binary_sensor()) | ||||
|   | ||||
| @@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() { | ||||
| } | ||||
|  | ||||
| void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   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(); | ||||
|   } | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
|  | ||||
| std::string MQTTButtonComponent::component_type() const { return "button"; } | ||||
|   | ||||
| @@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() { | ||||
|   std::string topic = "esphome/discover/"; | ||||
|   topic.append(App.get_name()); | ||||
|  | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   this->publish_json( | ||||
|       topic, | ||||
|       [](JsonObject root) { | ||||
| @@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() { | ||||
| #endif | ||||
|       }, | ||||
|       2, this->discovery_info_.retain); | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
|  | ||||
| void MQTTClientComponent::dump_config() { | ||||
|   | ||||
| @@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate"; | ||||
| using namespace esphome::climate; | ||||
|  | ||||
| void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   auto traits = this->device_->get_traits(); | ||||
|   // current_temperature_topic | ||||
|   if (traits.get_supports_current_temperature()) { | ||||
| @@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | ||||
|   // mode_state_topic | ||||
|   root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); | ||||
|   // modes | ||||
|   JsonArray modes = root.createNestedArray(MQTT_MODES); | ||||
|   JsonArray modes = root[MQTT_MODES].to<JsonArray>(); | ||||
|   // sort array for nice UI in HA | ||||
|   if (traits.supports_mode(CLIMATE_MODE_AUTO)) | ||||
|     modes.add("auto"); | ||||
| @@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | ||||
|     // preset_mode_state_topic | ||||
|     root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); | ||||
|     // presets | ||||
|     JsonArray presets = root.createNestedArray("preset_modes"); | ||||
|     JsonArray presets = root["preset_modes"].to<JsonArray>(); | ||||
|     if (traits.supports_preset(CLIMATE_PRESET_HOME)) | ||||
|       presets.add("home"); | ||||
|     if (traits.supports_preset(CLIMATE_PRESET_AWAY)) | ||||
| @@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | ||||
|     // fan_mode_state_topic | ||||
|     root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); | ||||
|     // 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)) | ||||
|       fan_modes.add("on"); | ||||
|     if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) | ||||
| @@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | ||||
|     // swing_mode_state_topic | ||||
|     root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); | ||||
|     // 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)) | ||||
|       swing_modes.add("off"); | ||||
|     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.command_topic = false; | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
| void MQTTClimateComponent::setup() { | ||||
|   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()); | ||||
|  | ||||
|   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   return global_mqtt_client->publish_json( | ||||
|       this->get_discovery_topic_(discovery_info), | ||||
|       [this](JsonObject root) { | ||||
| @@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() { | ||||
|         } | ||||
|         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(); | ||||
|         device_info[MQTT_DEVICE_IDENTIFIERS] = mac; | ||||
|         device_info[MQTT_DEVICE_NAME] = node_friendly_name; | ||||
| @@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() { | ||||
|         device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; | ||||
|       }, | ||||
|       this->qos_, discovery_info.retain); | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
|  | ||||
| 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) { | ||||
|   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   if (!this->cover_->get_device_class().empty()) | ||||
|     root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); | ||||
|  | ||||
|   | ||||
| @@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {} | ||||
| void MQTTDateComponent::setup() { | ||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||
|     auto call = this->date_->make_call(); | ||||
|     if (root.containsKey("year")) { | ||||
|     if (root["year"].is<uint16_t>()) { | ||||
|       call.set_year(root["year"]); | ||||
|     } | ||||
|     if (root.containsKey("month")) { | ||||
|     if (root["month"].is<uint8_t>()) { | ||||
|       call.set_month(root["month"]); | ||||
|     } | ||||
|     if (root.containsKey("day")) { | ||||
|     if (root["day"].is<uint8_t>()) { | ||||
|       call.set_day(root["day"]); | ||||
|     } | ||||
|     call.perform(); | ||||
| @@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() { | ||||
| } | ||||
| 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) { | ||||
|     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|     root["year"] = year; | ||||
|     root["month"] = month; | ||||
|     root["day"] = day; | ||||
|   | ||||
| @@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim | ||||
| void MQTTDateTimeComponent::setup() { | ||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||
|     auto call = this->datetime_->make_call(); | ||||
|     if (root.containsKey("year")) { | ||||
|     if (root["year"].is<uint16_t>()) { | ||||
|       call.set_year(root["year"]); | ||||
|     } | ||||
|     if (root.containsKey("month")) { | ||||
|     if (root["month"].is<uint8_t>()) { | ||||
|       call.set_month(root["month"]); | ||||
|     } | ||||
|     if (root.containsKey("day")) { | ||||
|     if (root["day"].is<uint8_t>()) { | ||||
|       call.set_day(root["day"]); | ||||
|     } | ||||
|     if (root.containsKey("hour")) { | ||||
|     if (root["hour"].is<uint8_t>()) { | ||||
|       call.set_hour(root["hour"]); | ||||
|     } | ||||
|     if (root.containsKey("minute")) { | ||||
|     if (root["minute"].is<uint8_t>()) { | ||||
|       call.set_minute(root["minute"]); | ||||
|     } | ||||
|     if (root.containsKey("second")) { | ||||
|     if (root["second"].is<uint8_t>()) { | ||||
|       call.set_second(root["second"]); | ||||
|     } | ||||
|     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, | ||||
|                                           uint8_t second) { | ||||
|   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["month"] = month; | ||||
|     root["day"] = day; | ||||
|   | ||||
| @@ -16,7 +16,8 @@ using namespace esphome::event; | ||||
| MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} | ||||
|  | ||||
| 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()) | ||||
|     event_types.add(event_type); | ||||
|  | ||||
| @@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() { | ||||
| } | ||||
|  | ||||
| bool MQTTEventComponent::publish_event_(const std::string &event_type) { | ||||
|   return this->publish_json(this->get_state_topic_(), | ||||
|                             [event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); | ||||
|   return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) { | ||||
|     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|     root[MQTT_EVENT_TYPE] = event_type; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| 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(); } | ||||
|  | ||||
| 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()) { | ||||
|     root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); | ||||
|     root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); | ||||
|   | ||||
| @@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() { | ||||
| MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} | ||||
|  | ||||
| bool MQTTJSONLightComponent::publish_state_() { | ||||
|   return this->publish_json(this->get_state_topic_(), | ||||
|                             [this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); | ||||
|   return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { | ||||
|     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|     LightJSONSchema::dump_json(*this->state_, root); | ||||
|   }); | ||||
| } | ||||
| LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } | ||||
|  | ||||
| void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||
|   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   root["schema"] = "json"; | ||||
|   auto traits = this->state_->get_traits(); | ||||
|  | ||||
|   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)) | ||||
|     color_modes.add("onoff"); | ||||
|   if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) | ||||
| @@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery | ||||
|  | ||||
|   if (this->state_->supports_effects()) { | ||||
|     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()) | ||||
|       effect_list.add(effect->get_name()); | ||||
|     effect_list.add("None"); | ||||
|   | ||||
| @@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() { | ||||
| std::string MQTTLockComponent::component_type() const { return "lock"; } | ||||
| const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } | ||||
| 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; | ||||
|   } | ||||
|   if (this->lock_->traits.get_supports_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) { | ||||
|   const auto &traits = number_->traits; | ||||
|   // 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_MAX] = traits.get_max_value(); | ||||
|   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) { | ||||
|   const auto &traits = select_->traits; | ||||
|   // 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()) | ||||
|     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::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(); | ||||
|   } | ||||
|  | ||||
|   if (!this->sensor_->get_unit_of_measurement().empty()) | ||||
|     root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); | ||||
|   | ||||
| @@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() { | ||||
| std::string MQTTSwitchComponent::component_type() const { return "switch"; } | ||||
| const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } | ||||
| 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; | ||||
|   } | ||||
| } | ||||
| bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; } | ||||
| const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } | ||||
|  | ||||
| void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||
|   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   switch (this->text_->traits.get_mode()) { | ||||
|     case TEXT_MODE_TEXT: | ||||
|       root[MQTT_MODE] = "text"; | ||||
|   | ||||
| @@ -15,8 +15,10 @@ using namespace esphome::text_sensor; | ||||
|  | ||||
| MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} | ||||
| 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(); | ||||
|   } | ||||
|   config.command_topic = false; | ||||
| } | ||||
| void MQTTTextSensor::setup() { | ||||
|   | ||||
| @@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {} | ||||
| void MQTTTimeComponent::setup() { | ||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||
|     auto call = this->time_->make_call(); | ||||
|     if (root.containsKey("hour")) { | ||||
|     if (root["hour"].is<uint8_t>()) { | ||||
|       call.set_hour(root["hour"]); | ||||
|     } | ||||
|     if (root.containsKey("minute")) { | ||||
|     if (root["minute"].is<uint8_t>()) { | ||||
|       call.set_minute(root["minute"]); | ||||
|     } | ||||
|     if (root.containsKey("second")) { | ||||
|     if (root["second"].is<uint8_t>()) { | ||||
|       call.set_second(root["second"]); | ||||
|     } | ||||
|     call.perform(); | ||||
| @@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() { | ||||
| } | ||||
| 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) { | ||||
|     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|     root["hour"] = hour; | ||||
|     root["minute"] = minute; | ||||
|     root["second"] = second; | ||||
|   | ||||
| @@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() { | ||||
| } | ||||
|  | ||||
| void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||
|   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|   root["schema"] = "json"; | ||||
|   root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; | ||||
| } | ||||
|   | ||||
| @@ -49,8 +49,10 @@ void MQTTValveComponent::dump_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(); | ||||
|   } | ||||
|  | ||||
|   auto traits = this->valve_->get_traits(); | ||||
|   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%] | ||||
|   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 = | ||||
|       humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; | ||||
|   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 | ||||
| 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.image import ( | ||||
|     CONF_INVERT_ALPHA, | ||||
| @@ -11,6 +11,7 @@ from esphome.components.image import ( | ||||
|     Image_, | ||||
|     get_image_type_enum, | ||||
|     get_transparency_enum, | ||||
|     validate_settings, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|             rp2040_arduino=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]), | ||||
|         transparent, | ||||
|         config[CONF_BUFFER_SIZE], | ||||
|         config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", | ||||
|     ) | ||||
|     await cg.register_component(var, config) | ||||
|     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, | ||||
|                          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), | ||||
|       buffer_(nullptr), | ||||
|       download_buffer_(download_buffer_size), | ||||
|       download_buffer_initial_size_(download_buffer_size), | ||||
|       format_(format), | ||||
|       fixed_width_(width), | ||||
|       fixed_height_(height) { | ||||
|       fixed_height_(height), | ||||
|       is_big_endian_(is_big_endian) { | ||||
|   this->set_url(url); | ||||
| } | ||||
|  | ||||
| @@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { | ||||
|       break; | ||||
|     } | ||||
|     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 (gray == 1) { | ||||
|           gray = 0; | ||||
| @@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { | ||||
|     case ImageType::IMAGE_TYPE_RGB565: { | ||||
|       this->map_chroma_key(color); | ||||
|       uint16_t col565 = display::ColorUtil::color_to_565(color); | ||||
|       this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); | ||||
|       this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); | ||||
|       if (this->is_big_endian_) { | ||||
|         this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 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) { | ||||
|         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. | ||||
|    */ | ||||
|   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; | ||||
|  | ||||
| @@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent, | ||||
|   const int fixed_width_; | ||||
|   /** height requested on configuration, or 0 if non specified. */ | ||||
|   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, | ||||
|    * 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_); | ||||
|   this->state = state < 0.003 && this->zero_means_zero_ | ||||
|                     ? 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; | ||||
|   ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); | ||||
| } | ||||
|   | ||||
							
								
								
									
										34
									
								
								esphome/components/runtime_stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/runtime_stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| """ | ||||
| Runtime statistics component for ESPHome. | ||||
| """ | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| CODEOWNERS = ["@bdraco"] | ||||
|  | ||||
| CONF_LOG_INTERVAL = "log_interval" | ||||
|  | ||||
| runtime_stats_ns = cg.esphome_ns.namespace("runtime_stats") | ||||
| RuntimeStatsCollector = runtime_stats_ns.class_("RuntimeStatsCollector") | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(RuntimeStatsCollector), | ||||
|         cv.Optional( | ||||
|             CONF_LOG_INTERVAL, default="60s" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     """Generate code for the runtime statistics component.""" | ||||
|     # Define USE_RUNTIME_STATS when this component is used | ||||
|     cg.add_define("USE_RUNTIME_STATS") | ||||
|  | ||||
|     # Create the runtime stats instance (constructor sets global_runtime_stats) | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|  | ||||
|     cg.add(var.set_log_interval(config[CONF_LOG_INTERVAL])) | ||||
							
								
								
									
										102
									
								
								esphome/components/runtime_stats/runtime_stats.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								esphome/components/runtime_stats/runtime_stats.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| #include "runtime_stats.h" | ||||
|  | ||||
| #ifdef USE_RUNTIME_STATS | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include <algorithm> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| namespace runtime_stats { | ||||
|  | ||||
| RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) { | ||||
|   global_runtime_stats = this; | ||||
| } | ||||
|  | ||||
| void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) { | ||||
|   if (component == nullptr) | ||||
|     return; | ||||
|  | ||||
|   // Check if we have cached the name for this component | ||||
|   auto name_it = this->component_names_cache_.find(component); | ||||
|   if (name_it == this->component_names_cache_.end()) { | ||||
|     // First time seeing this component, cache its name | ||||
|     const char *source = component->get_component_source(); | ||||
|     this->component_names_cache_[component] = source; | ||||
|     this->component_stats_[source].record_time(duration_ms); | ||||
|   } else { | ||||
|     this->component_stats_[name_it->second].record_time(duration_ms); | ||||
|   } | ||||
|  | ||||
|   if (this->next_log_time_ == 0) { | ||||
|     this->next_log_time_ = current_time + this->log_interval_; | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RuntimeStatsCollector::log_stats_() { | ||||
|   ESP_LOGI(TAG, "Component Runtime Statistics"); | ||||
|   ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_); | ||||
|  | ||||
|   // First collect stats we want to display | ||||
|   std::vector<ComponentStatPair> stats_to_display; | ||||
|  | ||||
|   for (const auto &it : this->component_stats_) { | ||||
|     const ComponentRuntimeStats &stats = it.second; | ||||
|     if (stats.get_period_count() > 0) { | ||||
|       ComponentStatPair pair = {it.first, &stats}; | ||||
|       stats_to_display.push_back(pair); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Sort by period runtime (descending) | ||||
|   std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>()); | ||||
|  | ||||
|   // Log top components by period runtime | ||||
|   for (const auto &it : stats_to_display) { | ||||
|     const char *source = it.name; | ||||
|     const ComponentRuntimeStats *stats = it.stats; | ||||
|  | ||||
|     ESP_LOGI(TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, | ||||
|              stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(), | ||||
|              stats->get_period_time_ms()); | ||||
|   } | ||||
|  | ||||
|   // Log total stats since boot | ||||
|   ESP_LOGI(TAG, "Total stats (since boot):"); | ||||
|  | ||||
|   // Re-sort by total runtime for all-time stats | ||||
|   std::sort(stats_to_display.begin(), stats_to_display.end(), | ||||
|             [](const ComponentStatPair &a, const ComponentStatPair &b) { | ||||
|               return a.stats->get_total_time_ms() > b.stats->get_total_time_ms(); | ||||
|             }); | ||||
|  | ||||
|   for (const auto &it : stats_to_display) { | ||||
|     const char *source = it.name; | ||||
|     const ComponentRuntimeStats *stats = it.stats; | ||||
|  | ||||
|     ESP_LOGI(TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, | ||||
|              stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(), | ||||
|              stats->get_total_time_ms()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { | ||||
|   if (this->next_log_time_ == 0) | ||||
|     return; | ||||
|  | ||||
|   if (current_time >= this->next_log_time_) { | ||||
|     this->log_stats_(); | ||||
|     this->reset_stats_(); | ||||
|     this->next_log_time_ = current_time + this->log_interval_; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace runtime_stats | ||||
|  | ||||
| runtime_stats::RuntimeStatsCollector *global_runtime_stats = | ||||
|     nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_RUNTIME_STATS | ||||
							
								
								
									
										132
									
								
								esphome/components/runtime_stats/runtime_stats.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								esphome/components/runtime_stats/runtime_stats.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| #ifdef USE_RUNTIME_STATS | ||||
|  | ||||
| #include <map> | ||||
| #include <vector> | ||||
| #include <cstdint> | ||||
| #include <cstring> | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| class Component;  // Forward declaration | ||||
|  | ||||
| namespace runtime_stats { | ||||
|  | ||||
| static const char *const TAG = "runtime_stats"; | ||||
|  | ||||
| class ComponentRuntimeStats { | ||||
|  public: | ||||
|   ComponentRuntimeStats() | ||||
|       : period_count_(0), | ||||
|         period_time_ms_(0), | ||||
|         period_max_time_ms_(0), | ||||
|         total_count_(0), | ||||
|         total_time_ms_(0), | ||||
|         total_max_time_ms_(0) {} | ||||
|  | ||||
|   void record_time(uint32_t duration_ms) { | ||||
|     // Update period counters | ||||
|     this->period_count_++; | ||||
|     this->period_time_ms_ += duration_ms; | ||||
|     if (duration_ms > this->period_max_time_ms_) | ||||
|       this->period_max_time_ms_ = duration_ms; | ||||
|  | ||||
|     // Update total counters | ||||
|     this->total_count_++; | ||||
|     this->total_time_ms_ += duration_ms; | ||||
|     if (duration_ms > this->total_max_time_ms_) | ||||
|       this->total_max_time_ms_ = duration_ms; | ||||
|   } | ||||
|  | ||||
|   void reset_period_stats() { | ||||
|     this->period_count_ = 0; | ||||
|     this->period_time_ms_ = 0; | ||||
|     this->period_max_time_ms_ = 0; | ||||
|   } | ||||
|  | ||||
|   // Period stats (reset each logging interval) | ||||
|   uint32_t get_period_count() const { return this->period_count_; } | ||||
|   uint32_t get_period_time_ms() const { return this->period_time_ms_; } | ||||
|   uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; } | ||||
|   float get_period_avg_time_ms() const { | ||||
|     return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f; | ||||
|   } | ||||
|  | ||||
|   // Total stats (persistent until reboot) | ||||
|   uint32_t get_total_count() const { return this->total_count_; } | ||||
|   uint32_t get_total_time_ms() const { return this->total_time_ms_; } | ||||
|   uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; } | ||||
|   float get_total_avg_time_ms() const { | ||||
|     return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   // Period stats (reset each logging interval) | ||||
|   uint32_t period_count_; | ||||
|   uint32_t period_time_ms_; | ||||
|   uint32_t period_max_time_ms_; | ||||
|  | ||||
|   // Total stats (persistent until reboot) | ||||
|   uint32_t total_count_; | ||||
|   uint32_t total_time_ms_; | ||||
|   uint32_t total_max_time_ms_; | ||||
| }; | ||||
|  | ||||
| // For sorting components by run time | ||||
| struct ComponentStatPair { | ||||
|   const char *name; | ||||
|   const ComponentRuntimeStats *stats; | ||||
|  | ||||
|   bool operator>(const ComponentStatPair &other) const { | ||||
|     // Sort by period time as that's what we're displaying in the logs | ||||
|     return stats->get_period_time_ms() > other.stats->get_period_time_ms(); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class RuntimeStatsCollector { | ||||
|  public: | ||||
|   RuntimeStatsCollector(); | ||||
|  | ||||
|   void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; } | ||||
|   uint32_t get_log_interval() const { return this->log_interval_; } | ||||
|  | ||||
|   void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time); | ||||
|  | ||||
|   // Process any pending stats printing (should be called after component loop) | ||||
|   void process_pending_stats(uint32_t current_time); | ||||
|  | ||||
|  protected: | ||||
|   void log_stats_(); | ||||
|  | ||||
|   void reset_stats_() { | ||||
|     for (auto &it : this->component_stats_) { | ||||
|       it.second.reset_period_stats(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Use const char* keys for efficiency | ||||
|   // Custom comparator for const char* keys in map | ||||
|   // Without this, std::map would compare pointer addresses instead of string contents, | ||||
|   // causing identical component names at different addresses to be treated as different keys | ||||
|   struct CStrCompare { | ||||
|     bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; } | ||||
|   }; | ||||
|   std::map<const char *, ComponentRuntimeStats, CStrCompare> component_stats_; | ||||
|   std::map<Component *, const char *> component_names_cache_; | ||||
|   uint32_t log_interval_; | ||||
|   uint32_t next_log_time_; | ||||
| }; | ||||
|  | ||||
| }  // namespace runtime_stats | ||||
|  | ||||
| extern runtime_stats::RuntimeStatsCollector | ||||
|     *global_runtime_stats;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_RUNTIME_STATS | ||||
| @@ -88,9 +88,9 @@ void Servo::internal_write(float value) { | ||||
|   value = clamp(value, -1.0f, 1.0f); | ||||
|   float level; | ||||
|   if (value < 0.0) { | ||||
|     level = lerp(-value, this->idle_level_, this->min_level_); | ||||
|     level = std::lerp(this->idle_level_, this->min_level_, -value); | ||||
|   } 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->current_value_ = value; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import re | ||||
| from typing import Any | ||||
|  | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| @@ -139,6 +140,27 @@ def get_hw_interface_list(): | ||||
|     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 | ||||
| def get_spi_index(name): | ||||
|     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( | ||||
|                 "force_sw is deprecated - use interface: software" | ||||
|             ), | ||||
|             cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( | ||||
|                 *sum(get_hw_interface_list(), ["software", "hardware", "any"]), | ||||
|                 lower=True, | ||||
|             cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator( | ||||
|                 ["software", "hardware", "any"] | ||||
|             ), | ||||
|             cv.Optional(CONF_DATA_PINS): cv.invalid( | ||||
|                 "'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.Length(min=pin_count, max=pin_count), | ||||
|                 ), | ||||
|                 cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( | ||||
|                     *sum(get_hw_interface_list(), ["hardware"]), | ||||
|                     lower=True, | ||||
|                 ), | ||||
|                 cv.Optional( | ||||
|                     CONF_INTERFACE, default="hardware" | ||||
|                 ): one_of_interface_validator(["hardware"]), | ||||
|                 cv.Optional(CONF_MISO_PIN): cv.invalid( | ||||
|                     f"'miso_pin' should not be used with {mode} SPI" | ||||
|                 ), | ||||
|   | ||||
| @@ -146,8 +146,11 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): | ||||
|             if sub is not None: | ||||
|                 item[k] = sub | ||||
|         for old, new in replace_keys: | ||||
|             item[new] = merge_config(item.get(old), item.get(new)) | ||||
|             del item[old] | ||||
|             if str(new) == str(old): | ||||
|                 item[new] = item[old] | ||||
|             else: | ||||
|                 item[new] = merge_config(item.get(old), item.get(new)) | ||||
|                 del item[old] | ||||
|     elif isinstance(item, str): | ||||
|         sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) | ||||
|         if isinstance(sub, JinjaStr) or sub != item: | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import tzlocal | ||||
| from esphome import automation | ||||
| from esphome.automation import Condition | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.zephyr import zephyr_add_prj_conf | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_AT, | ||||
| @@ -25,7 +26,7 @@ from esphome.const import ( | ||||
|     CONF_TIMEZONE, | ||||
|     CONF_TRIGGER_ID, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -341,6 +342,8 @@ async def register_time(time_var, config): | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| async def to_code(config): | ||||
|     if CORE.using_zephyr: | ||||
|         zephyr_add_prj_conf("POSIX_CLOCK", True) | ||||
|     cg.add_define("USE_TIME") | ||||
|     cg.add_global(time_ns.using) | ||||
|  | ||||
|   | ||||
| @@ -2,13 +2,15 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #ifdef USE_HOST | ||||
| #include <sys/time.h> | ||||
| #elif defined(USE_ZEPHYR) | ||||
| #include <zephyr/posix/time.h> | ||||
| #else | ||||
| #include "lwip/opt.h" | ||||
| #endif | ||||
| #ifdef USE_ESP8266 | ||||
| #include "sys/time.h" | ||||
| #endif | ||||
| #ifdef USE_RP2040 | ||||
| #if defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||
| #include <sys/time.h> | ||||
| #endif | ||||
| #include <cerrno> | ||||
| @@ -22,11 +24,22 @@ static const char *const TAG = "time"; | ||||
|  | ||||
| RealTimeClock::RealTimeClock() = default; | ||||
| void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | ||||
|   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); | ||||
|   // 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 { | ||||
|     .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, | ||||
|   }; | ||||
|   ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); | ||||
|   struct timezone tz = {0, 0}; | ||||
|   int ret = settimeofday(&timev, &tz); | ||||
|   if (ret == EINVAL) { | ||||
| @@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { | ||||
|   if (ret != 0) { | ||||
|     ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); | ||||
|   } | ||||
|  | ||||
| #endif | ||||
|   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, | ||||
|            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); | ||||
|     if (start_config == DETAIL_ALL) { | ||||
|       JsonArray opt = root.createNestedArray("effects"); | ||||
|       JsonArray opt = root["effects"].to<JsonArray>(); | ||||
|       opt.add("None"); | ||||
|       for (auto const &option : obj->get_effects()) { | ||||
|         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) { | ||||
|     set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); | ||||
|     if (start_config == DETAIL_ALL) { | ||||
|       JsonArray opt = root.createNestedArray("option"); | ||||
|       JsonArray opt = root["option"].to<JsonArray>(); | ||||
|       for (auto &option : obj->traits.get_options()) { | ||||
|         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); | ||||
| } | ||||
| 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) { | ||||
|     set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); | ||||
|     const auto traits = obj->get_traits(); | ||||
| @@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf | ||||
|     char buf[16]; | ||||
|  | ||||
|     if (start_config == DETAIL_ALL) { | ||||
|       JsonArray opt = root.createNestedArray("modes"); | ||||
|       JsonArray opt = root["modes"].to<JsonArray>(); | ||||
|       for (climate::ClimateMode m : traits.get_supported_modes()) | ||||
|         opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); | ||||
|       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()) | ||||
|           opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); | ||||
|       } | ||||
|  | ||||
|       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()) | ||||
|           opt.add(custom_fan_mode); | ||||
|       } | ||||
|       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()) | ||||
|           opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); | ||||
|       } | ||||
|       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()) | ||||
|           opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); | ||||
|       } | ||||
|       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()) | ||||
|           opt.add(custom_preset); | ||||
|       } | ||||
| @@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf | ||||
|         root["state"] = root["target_temperature"]; | ||||
|     } | ||||
|   }); | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -1635,7 +1637,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty | ||||
|       root["event_type"] = event_type; | ||||
|     } | ||||
|     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()) { | ||||
|         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); | ||||
| } | ||||
| 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) { | ||||
|     set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); | ||||
|     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); | ||||
|     } | ||||
|   }); | ||||
|   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
| } | ||||
| #endif | ||||
|  | ||||
| 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; | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
|   if (request->url() == "/events") { | ||||
|   if (url == "/events") | ||||
|     return true; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
|   if (request->url() == "/0.css") | ||||
|   if (url == "/0.css") | ||||
|     return true; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | ||||
|   if (request->url() == "/0.js") | ||||
|   if (url == "/0.js") | ||||
|     return true; | ||||
| #endif | ||||
|  | ||||
| #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; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // Store the URL to prevent temporary string destruction | ||||
|   // 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(); | ||||
|   // Parse URL for component checks | ||||
|   UrlMatch match = match_url(url.c_str(), url.length(), true); | ||||
|   if (!match.valid) | ||||
|     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 | ||||
|   if (request->method() == HTTP_GET && match.domain_equals("sensor")) | ||||
|     return true; | ||||
|     if (match.domain_equals("sensor")) | ||||
|       return true; | ||||
| #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 | ||||
|   if (request->method() == HTTP_GET && match.domain_equals("binary_sensor")) | ||||
|     return true; | ||||
|     if (match.domain_equals("binary_sensor")) | ||||
|       return true; | ||||
| #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 | ||||
|   if (request->method() == HTTP_GET && match.domain_equals("text_sensor")) | ||||
|     return true; | ||||
|     if (match.domain_equals("text_sensor")) | ||||
|       return true; | ||||
| #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 | ||||
|   if (request->method() == HTTP_GET && match.domain_equals("event")) | ||||
|     return true; | ||||
|     if (match.domain_equals("event")) | ||||
|       return true; | ||||
| #endif | ||||
|   } | ||||
|  | ||||
| #ifdef USE_UPDATE | ||||
|   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update")) | ||||
|     return true; | ||||
|   // GET+POST components | ||||
|   if (is_get_or_post) { | ||||
| #ifdef USE_SWITCH | ||||
|     if (match.domain_equals("switch")) | ||||
|       return true; | ||||
| #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; | ||||
| } | ||||
| void WebServer::handleRequest(AsyncWebServerRequest *request) { | ||||
|   if (request->url() == "/") { | ||||
|   const auto &url = request->url(); | ||||
|  | ||||
|   // Handle static routes first | ||||
|   if (url == "/") { | ||||
|     this->handle_index_request(request); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
|   if (request->url() == "/events") { | ||||
|   if (url == "/events") { | ||||
|     this->events_.add_new_client(this, request); | ||||
|     return; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
|   if (request->url() == "/0.css") { | ||||
|   if (url == "/0.css") { | ||||
|     this->handle_css_request(request); | ||||
|     return; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | ||||
|   if (request->url() == "/0.js") { | ||||
|   if (url == "/0.js") { | ||||
|     this->handle_js_request(request); | ||||
|     return; | ||||
|   } | ||||
| @@ -1879,147 +1882,85 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // See comment in canHandle() for why we store the URL reference | ||||
|   const auto &url = request->url(); | ||||
|   // Parse URL for component routing | ||||
|   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 | ||||
|   if (match.domain_equals("sensor")) { | ||||
|     this->handle_sensor_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"sensor", &WebServer::handle_sensor_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SWITCH | ||||
|   if (match.domain_equals("switch")) { | ||||
|     this->handle_switch_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"switch", &WebServer::handle_switch_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BUTTON | ||||
|   if (match.domain_equals("button")) { | ||||
|     this->handle_button_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"button", &WebServer::handle_button_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   if (match.domain_equals("binary_sensor")) { | ||||
|     this->handle_binary_sensor_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"binary_sensor", &WebServer::handle_binary_sensor_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
|   if (match.domain_equals("fan")) { | ||||
|     this->handle_fan_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"fan", &WebServer::handle_fan_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
|   if (match.domain_equals("light")) { | ||||
|     this->handle_light_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"light", &WebServer::handle_light_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   if (match.domain_equals("text_sensor")) { | ||||
|     this->handle_text_sensor_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"text_sensor", &WebServer::handle_text_sensor_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_COVER | ||||
|   if (match.domain_equals("cover")) { | ||||
|     this->handle_cover_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"cover", &WebServer::handle_cover_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
|   if (match.domain_equals("number")) { | ||||
|     this->handle_number_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"number", &WebServer::handle_number_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATE | ||||
|   if (match.domain_equals("date")) { | ||||
|     this->handle_date_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"date", &WebServer::handle_date_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_TIME | ||||
|   if (match.domain_equals("time")) { | ||||
|     this->handle_time_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"time", &WebServer::handle_time_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
|   if (match.domain_equals("datetime")) { | ||||
|     this->handle_datetime_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"datetime", &WebServer::handle_datetime_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT | ||||
|   if (match.domain_equals("text")) { | ||||
|     this->handle_text_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"text", &WebServer::handle_text_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
|   if (match.domain_equals("select")) { | ||||
|     this->handle_select_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"select", &WebServer::handle_select_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CLIMATE | ||||
|   if (match.domain_equals("climate")) { | ||||
|     this->handle_climate_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"climate", &WebServer::handle_climate_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOCK | ||||
|   if (match.domain_equals("lock")) { | ||||
|     this->handle_lock_request(request, match); | ||||
|  | ||||
|     return; | ||||
|   } | ||||
|       {"lock", &WebServer::handle_lock_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_VALVE | ||||
|   if (match.domain_equals("valve")) { | ||||
|     this->handle_valve_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"valve", &WebServer::handle_valve_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|   if (match.domain_equals("alarm_control_panel")) { | ||||
|     this->handle_alarm_control_panel_request(request, match); | ||||
|  | ||||
|     return; | ||||
|   } | ||||
|       {"alarm_control_panel", &WebServer::handle_alarm_control_panel_request}, | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_UPDATE | ||||
|   if (match.domain_equals("update")) { | ||||
|     this->handle_update_request(request, match); | ||||
|     return; | ||||
|   } | ||||
|       {"update", &WebServer::handle_update_request}, | ||||
| #endif | ||||
|   }; | ||||
|  | ||||
|   // Check each route | ||||
|   for (const auto &route : ROUTES) { | ||||
|     if (match.domain_equals(route.domain)) { | ||||
|       (this->*route.handler)(request, match); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 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"); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -40,4 +40,4 @@ async def to_code(config): | ||||
|         if CORE.is_esp8266: | ||||
|             cg.add_library("ESP8266WiFi", None) | ||||
|         # 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 | ||||
|   for (auto &group : ws->sorting_groups_) { | ||||
|     // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|     message = json::build_json([group](JsonObject root) { | ||||
|       root["name"] = group.second.name; | ||||
|       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 | ||||
|     // 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" | ||||
|     LIBRETINY_OLDSTYLE = "libretiny" | ||||
|     LN882X = "ln882x" | ||||
|     NRF52 = "nrf52" | ||||
|     RP2040 = "rp2040" | ||||
|     RTL87XX = "rtl87xx" | ||||
|  | ||||
| @@ -31,6 +32,7 @@ class Framework(StrEnum): | ||||
|     ARDUINO = "arduino" | ||||
|     ESP_IDF = "esp-idf" | ||||
|     NATIVE = "host" | ||||
|     ZEPHYR = "zephyr" | ||||
|  | ||||
|  | ||||
| class PlatformFramework(Enum): | ||||
| @@ -47,6 +49,9 @@ class PlatformFramework(Enum): | ||||
|     RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) | ||||
|     LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) | ||||
|  | ||||
|     # Zephyr framework platforms | ||||
|     NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR) | ||||
|  | ||||
|     # Host platform (native) | ||||
|     HOST_NATIVE = (Platform.HOST, Framework.NATIVE) | ||||
|  | ||||
| @@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266 | ||||
| PLATFORM_HOST = Platform.HOST | ||||
| PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE | ||||
| PLATFORM_LN882X = Platform.LN882X | ||||
| PLATFORM_NRF52 = Platform.NRF52 | ||||
| PLATFORM_RP2040 = Platform.RP2040 | ||||
| PLATFORM_RTL87XX = Platform.RTL87XX | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ from esphome.const import ( | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_HOST, | ||||
|     PLATFORM_LN882X, | ||||
|     PLATFORM_NRF52, | ||||
|     PLATFORM_RP2040, | ||||
|     PLATFORM_RTL87XX, | ||||
| ) | ||||
| @@ -670,6 +671,10 @@ class EsphomeCore: | ||||
|     def is_libretiny(self): | ||||
|         return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x | ||||
|  | ||||
|     @property | ||||
|     def is_nrf52(self): | ||||
|         return self.target_platform == PLATFORM_NRF52 | ||||
|  | ||||
|     @property | ||||
|     def is_host(self): | ||||
|         return self.target_platform == PLATFORM_HOST | ||||
| @@ -686,6 +691,10 @@ class EsphomeCore: | ||||
|     def using_esp_idf(self): | ||||
|         return self.target_framework == "esp-idf" | ||||
|  | ||||
|     @property | ||||
|     def using_zephyr(self): | ||||
|         return self.target_framework == "zephyr" | ||||
|  | ||||
|     def add_job(self, func, *args, **kwargs) -> None: | ||||
|         self.event_loop.add_job(func, *args, **kwargs) | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,9 @@ | ||||
| #include "esphome/core/hal.h" | ||||
| #include <algorithm> | ||||
| #include <ranges> | ||||
| #ifdef USE_RUNTIME_STATS | ||||
| #include "esphome/components/runtime_stats/runtime_stats.h" | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_STATUS_LED | ||||
| #include "esphome/components/status_led/status_led.h" | ||||
| @@ -141,6 +144,14 @@ void Application::loop() { | ||||
|   this->in_loop_ = false; | ||||
|   this->app_state_ = new_app_state; | ||||
|  | ||||
| #ifdef USE_RUNTIME_STATS | ||||
|   // Process any pending runtime stats printing after all components have run | ||||
|   // This ensures stats printing doesn't affect component timing measurements | ||||
|   if (global_runtime_stats != nullptr) { | ||||
|     global_runtime_stats->process_pending_stats(last_op_end_time); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // Use the last component's end time instead of calling millis() again | ||||
|   auto elapsed = last_op_end_time - this->last_loop_; | ||||
|   if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { | ||||
| @@ -309,6 +320,12 @@ void Application::disable_component_loop_(Component *component) { | ||||
|         if (this->in_loop_ && i == this->current_loop_index_) { | ||||
|           // Decrement so we'll process the swapped component next | ||||
|           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; | ||||
|   | ||||
| @@ -9,6 +9,9 @@ | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #ifdef USE_RUNTIME_STATS | ||||
| #include "esphome/components/runtime_stats/runtime_stats.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| @@ -138,7 +141,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 +194,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { | ||||
|   return false; | ||||
| } | ||||
| 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_FAILED; | ||||
|   this->status_set_error(); | ||||
| @@ -229,7 +232,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { | ||||
| } | ||||
| void Component::reset_to_construction_state() { | ||||
|   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_CONSTRUCTION; | ||||
|     // Clear error status when resetting | ||||
| @@ -264,6 +267,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_ready() const { | ||||
|   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; | ||||
| } | ||||
| bool Component::can_proceed() { return true; } | ||||
| @@ -275,14 +279,14 @@ void Component::status_set_warning(const char *message) { | ||||
|     return; | ||||
|   this->component_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) { | ||||
|   if ((this->component_state_ & STATUS_LED_ERROR) != 0) | ||||
|     return; | ||||
|   this->component_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) { | ||||
|     // Lazy allocate the error messages vector if needed | ||||
|     if (!component_error_messages) { | ||||
| @@ -303,13 +307,13 @@ void Component::status_clear_warning() { | ||||
|   if ((this->component_state_ & STATUS_LED_WARNING) == 0) | ||||
|     return; | ||||
|   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() { | ||||
|   if ((this->component_state_ & STATUS_LED_ERROR) == 0) | ||||
|     return; | ||||
|   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) { | ||||
|   this->status_set_warning(); | ||||
| @@ -395,6 +399,13 @@ uint32_t WarnIfComponentBlockingGuard::finish() { | ||||
|   uint32_t curr_time = millis(); | ||||
|  | ||||
|   uint32_t blocking_time = curr_time - this->started_; | ||||
|  | ||||
| #ifdef USE_RUNTIME_STATS | ||||
|   // Record component runtime stats | ||||
|   if (global_runtime_stats != nullptr) { | ||||
|     global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); | ||||
|   } | ||||
| #endif | ||||
|   bool should_warn; | ||||
|   if (this->component_ != nullptr) { | ||||
|     should_warn = this->component_->should_warn_of_blocking(blocking_time); | ||||
| @@ -403,7 +414,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() { | ||||
|   } | ||||
|   if (should_warn) { | ||||
|     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"); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -16,373 +16,186 @@ void ComponentIterator::begin(bool include_internal) { | ||||
|   this->at_ = 0; | ||||
|   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() { | ||||
|   bool advance_platform = false; | ||||
|   bool success = true; | ||||
|   switch (this->state_) { | ||||
|     case IteratorState::NONE: | ||||
|       // not started | ||||
|       return; | ||||
|     case IteratorState::BEGIN: | ||||
|       if (this->on_begin()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         return; | ||||
|         advance_platform_(); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|     case IteratorState::BINARY_SENSOR: | ||||
|       if (this->at_ >= App.get_binary_sensors().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_COVER | ||||
|     case IteratorState::COVER: | ||||
|       if (this->at_ >= App.get_covers().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
|     case IteratorState::FAN: | ||||
|       if (this->at_ >= App.get_fans().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
|     case IteratorState::LIGHT: | ||||
|       if (this->at_ >= App.get_lights().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|     case IteratorState::SENSOR: | ||||
|       if (this->at_ >= App.get_sensors().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SWITCH | ||||
|     case IteratorState::SWITCH: | ||||
|       if (this->at_ >= App.get_switches().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BUTTON | ||||
|     case IteratorState::BUTTON: | ||||
|       if (this->at_ >= App.get_buttons().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|     case IteratorState::TEXT_SENSOR: | ||||
|       if (this->at_ >= App.get_text_sensors().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
|     case IteratorState ::SERVICE: | ||||
|       if (this->at_ >= api::global_api_server->get_user_services().size()) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         auto *service = api::global_api_server->get_user_services()[this->at_]; | ||||
|         success = this->on_service(service); | ||||
|       } | ||||
|     case IteratorState::SERVICE: | ||||
|       this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CAMERA | ||||
|     case IteratorState::CAMERA: | ||||
|       if (camera::Camera::instance() == nullptr) { | ||||
|         advance_platform = true; | ||||
|       } else { | ||||
|         if (camera::Camera::instance()->is_internal() && !this->include_internal_) { | ||||
|           advance_platform = success = true; | ||||
|           break; | ||||
|         } else { | ||||
|           advance_platform = success = this->on_camera(camera::Camera::instance()); | ||||
|         } | ||||
|     case IteratorState::CAMERA: { | ||||
|       camera::Camera *camera_instance = camera::Camera::instance(); | ||||
|       if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) { | ||||
|         this->on_camera(camera_instance); | ||||
|       } | ||||
|       break; | ||||
|       advance_platform_(); | ||||
|     } break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CLIMATE | ||||
|     case IteratorState::CLIMATE: | ||||
|       if (this->at_ >= App.get_climates().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
|     case IteratorState::NUMBER: | ||||
|       if (this->at_ >= App.get_numbers().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATE | ||||
|     case IteratorState::DATETIME_DATE: | ||||
|       if (this->at_ >= App.get_dates().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_TIME | ||||
|     case IteratorState::DATETIME_TIME: | ||||
|       if (this->at_ >= App.get_times().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_times(), &ComponentIterator::on_time); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
|     case IteratorState::DATETIME_DATETIME: | ||||
|       if (this->at_ >= App.get_datetimes().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT | ||||
|     case IteratorState::TEXT: | ||||
|       if (this->at_ >= App.get_texts().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
|     case IteratorState::SELECT: | ||||
|       if (this->at_ >= App.get_selects().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOCK | ||||
|     case IteratorState::LOCK: | ||||
|       if (this->at_ >= App.get_locks().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_VALVE | ||||
|     case IteratorState::VALVE: | ||||
|       if (this->at_ >= App.get_valves().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|     case IteratorState::MEDIA_PLAYER: | ||||
|       if (this->at_ >= App.get_media_players().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|     case IteratorState::ALARM_CONTROL_PANEL: | ||||
|       if (this->at_ >= App.get_alarm_control_panels().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_EVENT | ||||
|     case IteratorState::EVENT: | ||||
|       if (this->at_ >= App.get_events().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_UPDATE | ||||
|     case IteratorState::UPDATE: | ||||
|       if (this->at_ >= App.get_updates().size()) { | ||||
|         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); | ||||
|         } | ||||
|       } | ||||
|       this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update); | ||||
|       break; | ||||
| #endif | ||||
|  | ||||
|     case IteratorState::MAX: | ||||
|       if (this->on_end()) { | ||||
|         this->state_ = IteratorState::NONE; | ||||
|       } | ||||
|       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_begin() { return true; } | ||||
| #ifdef USE_API_SERVICES | ||||
|   | ||||
| @@ -171,6 +171,11 @@ class ComponentIterator { | ||||
|   } state_{IteratorState::NONE}; | ||||
|   uint16_t at_{0};  // Supports up to 65,535 entities per type | ||||
|   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 | ||||
|   | ||||
| @@ -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()); } | ||||
|  | ||||
| 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) | ||||
|     return ""; | ||||
|   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::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) { | ||||
|   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); | ||||
| } | ||||
| std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { | ||||
|   if (data.empty()) | ||||
|     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; | ||||
|   return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length); | ||||
| } | ||||
|  | ||||
| std::string format_bin(const uint8_t *data, size_t length) { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
| #include <cmath> | ||||
| #include <cstdint> | ||||
| #include <cstring> | ||||
| @@ -678,7 +679,7 @@ class InterruptLock { | ||||
|   ~InterruptLock(); | ||||
|  | ||||
|  protected: | ||||
| #if defined(USE_ESP8266) || defined(USE_RP2040) | ||||
| #if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) | ||||
|   uint32_t state_; | ||||
| #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, size_t manual_size) { | ||||
|     size_t size = n * sizeof(T); | ||||
|     size_t size = n * manual_size; | ||||
|     T *ptr = nullptr; | ||||
| #ifdef USE_ESP32 | ||||
|     if (this->flags_ & Flags::ALLOC_EXTERNAL) { | ||||
|   | ||||
| @@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int: | ||||
|     os.environ.setdefault( | ||||
|         "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) | ||||
|  | ||||
|     if not CORE.verbose: | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user