mirror of
https://github.com/esphome/esphome.git
synced 2025-08-02 16:37:46 +00:00
Merge branch 'dev' into drop_unique_id
This commit is contained in:
commit
0110a376b7
@ -1 +1 @@
|
|||||||
a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a
|
07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a
|
||||||
|
92
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
92
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
name: Report an issue with ESPHome
|
||||||
|
description: Report an issue with ESPHome.
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for reporting bugs only!
|
||||||
|
|
||||||
|
If you have a feature request or enhancement, please [request them here instead][fr].
|
||||||
|
|
||||||
|
[fr]: https://github.com/orgs/esphome/discussions
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: The problem
|
||||||
|
description: >-
|
||||||
|
Describe the issue you are experiencing here to communicate to the
|
||||||
|
maintainers. Tell us what you were trying to do and what happened.
|
||||||
|
|
||||||
|
Provide a clear and concise description of what the problem is.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Environment
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which version of ESPHome has the issue?
|
||||||
|
description: >
|
||||||
|
ESPHome version like 1.19, 2025.6.0 or 2025.XX.X-dev.
|
||||||
|
- type: dropdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
id: installation
|
||||||
|
attributes:
|
||||||
|
label: What type of installation are you using?
|
||||||
|
options:
|
||||||
|
- Home Assistant Add-on
|
||||||
|
- Docker
|
||||||
|
- pip
|
||||||
|
- type: dropdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: What platform are you using?
|
||||||
|
options:
|
||||||
|
- ESP8266
|
||||||
|
- ESP32
|
||||||
|
- RP2040
|
||||||
|
- BK72XX
|
||||||
|
- RTL87XX
|
||||||
|
- LN882X
|
||||||
|
- Host
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: component_name
|
||||||
|
attributes:
|
||||||
|
label: Component causing the issue
|
||||||
|
description: >
|
||||||
|
The name of the component or platform. For example, api/i2c or ultrasonic.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Details
|
||||||
|
- type: textarea
|
||||||
|
id: config
|
||||||
|
attributes:
|
||||||
|
label: YAML Config
|
||||||
|
description: |
|
||||||
|
Include a complete YAML configuration file demonstrating the problem here. Preferably post the *entire* file - don't make assumptions about what is unimportant. However, if it's a large or complicated config then you will need to reduce it to the smallest possible file *that still demonstrates the problem*. If you don't provide enough information to *easily* reproduce the problem, it's unlikely your bug report will get any attention. Logs do not belong here, attach them below.
|
||||||
|
render: yaml
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Anything in the logs that might be useful for us?
|
||||||
|
description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs.
|
||||||
|
render: txt
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: >
|
||||||
|
If you have any additional information for us, use the field below.
|
||||||
|
Please note, you can attach screenshots or screen recordings here, by
|
||||||
|
dragging and dropping files in the field below.
|
26
.github/ISSUE_TEMPLATE/config.yml
vendored
26
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,15 +1,21 @@
|
|||||||
---
|
---
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Issue Tracker
|
- name: Report an issue with the ESPHome documentation
|
||||||
url: https://github.com/esphome/issues
|
url: https://github.com/esphome/esphome-docs/issues/new/choose
|
||||||
about: Please create bug reports in the dedicated issue tracker.
|
about: Report an issue with the ESPHome documentation.
|
||||||
- name: Feature Request Tracker
|
- name: Report an issue with the ESPHome web server
|
||||||
url: https://github.com/esphome/feature-requests
|
url: https://github.com/esphome/esphome-webserver/issues/new/choose
|
||||||
about: |
|
about: Report an issue with the ESPHome web server.
|
||||||
Please create feature requests in the dedicated feature request tracker.
|
- name: Report an issue with the ESPHome Builder / Dashboard
|
||||||
|
url: https://github.com/esphome/dashboard/issues/new/choose
|
||||||
|
about: Report an issue with the ESPHome Builder / Dashboard.
|
||||||
|
- name: Report an issue with the ESPHome API client
|
||||||
|
url: https://github.com/esphome/aioesphomeapi/issues/new/choose
|
||||||
|
about: Report an issue with the ESPHome API client.
|
||||||
|
- name: Make a Feature Request
|
||||||
|
url: https://github.com/orgs/esphome/discussions
|
||||||
|
about: Please create feature requests in the dedicated feature request tracker.
|
||||||
- name: Frequently Asked Question
|
- name: Frequently Asked Question
|
||||||
url: https://esphome.io/guides/faq.html
|
url: https://esphome.io/guides/faq.html
|
||||||
about: |
|
about: Please view the FAQ for common questions and what to include in a bug report.
|
||||||
Please view the FAQ for common questions and what
|
|
||||||
to include in a bug report.
|
|
||||||
|
1
.github/workflows/ci-clang-tidy-hash.yml
vendored
1
.github/workflows/ci-clang-tidy-hash.yml
vendored
@ -73,4 +73,3 @@ jobs:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
135
.github/workflows/ci.yml
vendored
135
.github/workflows/ci.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Generate cache-key
|
- name: Generate cache-key
|
||||||
id: cache-key
|
id: cache-key
|
||||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
|
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@ -58,55 +58,9 @@ jobs:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
pip install -r requirements.txt -r requirements_test.txt
|
pip install -r requirements.txt -r requirements_test.txt pre-commit
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
ruff:
|
|
||||||
name: Check ruff
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Run Ruff
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
ruff format esphome tests
|
|
||||||
- name: Suggested changes
|
|
||||||
run: script/ci-suggest-changes
|
|
||||||
if: always()
|
|
||||||
|
|
||||||
flake8:
|
|
||||||
name: Check flake8
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Run flake8
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
flake8 esphome
|
|
||||||
- name: Suggested changes
|
|
||||||
run: script/ci-suggest-changes
|
|
||||||
if: always()
|
|
||||||
|
|
||||||
pylint:
|
pylint:
|
||||||
name: Check pylint
|
name: Check pylint
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@ -130,29 +84,6 @@ jobs:
|
|||||||
run: script/ci-suggest-changes
|
run: script/ci-suggest-changes
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
pyupgrade:
|
|
||||||
name: Check pyupgrade
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Run pyupgrade
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f`
|
|
||||||
- name: Suggested changes
|
|
||||||
run: script/ci-suggest-changes
|
|
||||||
if: always()
|
|
||||||
|
|
||||||
ci-custom:
|
ci-custom:
|
||||||
name: Run script/ci-custom
|
name: Run script/ci-custom
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@ -248,7 +179,6 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||||
clang-format: ${{ steps.determine.outputs.clang-format }}
|
|
||||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||||
@ -276,7 +206,6 @@ jobs:
|
|||||||
# Extract individual fields
|
# Extract individual fields
|
||||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||||
echo "clang-format=$(echo "$output" | jq -r '.clang_format')" >> $GITHUB_OUTPUT
|
|
||||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||||
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
||||||
@ -317,46 +246,11 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||||
|
|
||||||
clang-format:
|
|
||||||
name: Check clang-format
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
if: needs.determine-jobs.outputs.clang-format == 'true'
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Install clang-format
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
pip install clang-format -c requirements_dev.txt
|
|
||||||
- name: Run clang-format
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
script/clang-format -i
|
|
||||||
git diff-index --quiet HEAD --
|
|
||||||
- name: Suggested changes
|
|
||||||
run: script/ci-suggest-changes
|
|
||||||
if: always()
|
|
||||||
|
|
||||||
clang-tidy:
|
clang-tidy:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- ruff
|
|
||||||
- ci-custom
|
|
||||||
- clang-format
|
|
||||||
- flake8
|
|
||||||
- pylint
|
|
||||||
- pytest
|
|
||||||
- pyupgrade
|
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
||||||
env:
|
env:
|
||||||
@ -562,24 +456,41 @@ jobs:
|
|||||||
./script/test_build_components -e compile -c $component
|
./script/test_build_components -e compile -c $component
|
||||||
done
|
done
|
||||||
|
|
||||||
|
pre-commit-ci-lite:
|
||||||
|
name: pre-commit.ci lite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v4.2.2
|
||||||
|
- name: Restore Python
|
||||||
|
uses: ./.github/actions/restore-python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
|
env:
|
||||||
|
SKIP: pylint,clang-tidy-hash
|
||||||
|
- uses: pre-commit-ci/lite-action@v1.1.0
|
||||||
|
if: always()
|
||||||
|
|
||||||
ci-status:
|
ci-status:
|
||||||
name: CI Status
|
name: CI Status
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- ruff
|
|
||||||
- ci-custom
|
- ci-custom
|
||||||
- clang-format
|
|
||||||
- flake8
|
|
||||||
- pylint
|
- pylint
|
||||||
- pytest
|
- pytest
|
||||||
- integration-tests
|
- integration-tests
|
||||||
- pyupgrade
|
|
||||||
- clang-tidy
|
- clang-tidy
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- test-build-components
|
- test-build-components
|
||||||
- test-build-components-splitter
|
- test-build-components-splitter
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
|
- pre-commit-ci-lite
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- name: Success
|
- name: Success
|
||||||
|
25
.github/workflows/yaml-lint.yml
vendored
25
.github/workflows/yaml-lint.yml
vendored
@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
name: YAML lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [dev, beta, release]
|
|
||||||
paths:
|
|
||||||
- "**.yaml"
|
|
||||||
- "**.yml"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "**.yaml"
|
|
||||||
- "**.yml"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
yamllint:
|
|
||||||
name: yamllint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
- name: Run yamllint
|
|
||||||
uses: frenck/action-yamllint@v1.5.0
|
|
||||||
with:
|
|
||||||
strict: true
|
|
@ -1,6 +1,13 @@
|
|||||||
---
|
---
|
||||||
# See https://pre-commit.com for more information
|
# See https://pre-commit.com for more information
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
|
||||||
|
ci:
|
||||||
|
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||||
|
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
||||||
|
# Skip hooks that have issues in pre-commit CI environment
|
||||||
|
skip: [pylint, clang-tidy-hash]
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
@ -20,13 +27,15 @@ repos:
|
|||||||
- pydocstyle==5.1.1
|
- pydocstyle==5.1.1
|
||||||
files: ^(esphome|tests)/.+\.py$
|
files: ^(esphome|tests)/.+\.py$
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.4.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: no-commit-to-branch
|
- id: no-commit-to-branch
|
||||||
args:
|
args:
|
||||||
- --branch=dev
|
- --branch=dev
|
||||||
- --branch=release
|
- --branch=release
|
||||||
- --branch=beta
|
- --branch=beta
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.20.0
|
rev: v3.20.0
|
||||||
hooks:
|
hooks:
|
||||||
@ -36,6 +45,7 @@ repos:
|
|||||||
rev: v1.37.1
|
rev: v1.37.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: yamllint
|
- id: yamllint
|
||||||
|
exclude: ^(\.clang-format|\.clang-tidy)$
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v13.0.1
|
rev: v13.0.1
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
|
|||||||
esphome/components/nfc/* @jesserockz @kbx81
|
esphome/components/nfc/* @jesserockz @kbx81
|
||||||
esphome/components/noblex/* @AGalfra
|
esphome/components/noblex/* @AGalfra
|
||||||
esphome/components/npi19/* @bakerkj
|
esphome/components/npi19/* @bakerkj
|
||||||
|
esphome/components/nrf52/* @tomaszduda23
|
||||||
esphome/components/number/* @esphome/core
|
esphome/components/number/* @esphome/core
|
||||||
esphome/components/one_wire/* @ssieb
|
esphome/components/one_wire/* @ssieb
|
||||||
esphome/components/online_image/* @clydebarrow @guillempages
|
esphome/components/online_image/* @clydebarrow @guillempages
|
||||||
@ -535,5 +536,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
|
|||||||
esphome/components/xl9535/* @mreditor97
|
esphome/components/xl9535/* @mreditor97
|
||||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
||||||
esphome/components/xxtea/* @clydebarrow
|
esphome/components/xxtea/* @clydebarrow
|
||||||
|
esphome/components/zephyr/* @tomaszduda23
|
||||||
esphome/components/zhlt01/* @cfeenstra1024
|
esphome/components/zhlt01/* @cfeenstra1024
|
||||||
esphome/components/zio_ultrasonic/* @kahrendt
|
esphome/components/zio_ultrasonic/* @kahrendt
|
||||||
|
@ -51,82 +51,83 @@ SAMPLING_MODES = {
|
|||||||
"max": sampling_mode.MAX,
|
"max": sampling_mode.MAX,
|
||||||
}
|
}
|
||||||
|
|
||||||
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
|
adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True)
|
||||||
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
|
|
||||||
|
adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True)
|
||||||
|
|
||||||
# pin to adc1 channel mapping
|
# pin to adc1 channel mapping
|
||||||
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
|
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
|
||||||
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32: {
|
VARIANT_ESP32: {
|
||||||
36: adc1_channel_t.ADC1_CHANNEL_0,
|
36: adc_channel_t.ADC_CHANNEL_0,
|
||||||
37: adc1_channel_t.ADC1_CHANNEL_1,
|
37: adc_channel_t.ADC_CHANNEL_1,
|
||||||
38: adc1_channel_t.ADC1_CHANNEL_2,
|
38: adc_channel_t.ADC_CHANNEL_2,
|
||||||
39: adc1_channel_t.ADC1_CHANNEL_3,
|
39: adc_channel_t.ADC_CHANNEL_3,
|
||||||
32: adc1_channel_t.ADC1_CHANNEL_4,
|
32: adc_channel_t.ADC_CHANNEL_4,
|
||||||
33: adc1_channel_t.ADC1_CHANNEL_5,
|
33: adc_channel_t.ADC_CHANNEL_5,
|
||||||
34: adc1_channel_t.ADC1_CHANNEL_6,
|
34: adc_channel_t.ADC_CHANNEL_6,
|
||||||
35: adc1_channel_t.ADC1_CHANNEL_7,
|
35: adc_channel_t.ADC_CHANNEL_7,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32C2: {
|
VARIANT_ESP32C2: {
|
||||||
0: adc1_channel_t.ADC1_CHANNEL_0,
|
0: adc_channel_t.ADC_CHANNEL_0,
|
||||||
1: adc1_channel_t.ADC1_CHANNEL_1,
|
1: adc_channel_t.ADC_CHANNEL_1,
|
||||||
2: adc1_channel_t.ADC1_CHANNEL_2,
|
2: adc_channel_t.ADC_CHANNEL_2,
|
||||||
3: adc1_channel_t.ADC1_CHANNEL_3,
|
3: adc_channel_t.ADC_CHANNEL_3,
|
||||||
4: adc1_channel_t.ADC1_CHANNEL_4,
|
4: adc_channel_t.ADC_CHANNEL_4,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32C3: {
|
VARIANT_ESP32C3: {
|
||||||
0: adc1_channel_t.ADC1_CHANNEL_0,
|
0: adc_channel_t.ADC_CHANNEL_0,
|
||||||
1: adc1_channel_t.ADC1_CHANNEL_1,
|
1: adc_channel_t.ADC_CHANNEL_1,
|
||||||
2: adc1_channel_t.ADC1_CHANNEL_2,
|
2: adc_channel_t.ADC_CHANNEL_2,
|
||||||
3: adc1_channel_t.ADC1_CHANNEL_3,
|
3: adc_channel_t.ADC_CHANNEL_3,
|
||||||
4: adc1_channel_t.ADC1_CHANNEL_4,
|
4: adc_channel_t.ADC_CHANNEL_4,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32C6: {
|
VARIANT_ESP32C6: {
|
||||||
0: adc1_channel_t.ADC1_CHANNEL_0,
|
0: adc_channel_t.ADC_CHANNEL_0,
|
||||||
1: adc1_channel_t.ADC1_CHANNEL_1,
|
1: adc_channel_t.ADC_CHANNEL_1,
|
||||||
2: adc1_channel_t.ADC1_CHANNEL_2,
|
2: adc_channel_t.ADC_CHANNEL_2,
|
||||||
3: adc1_channel_t.ADC1_CHANNEL_3,
|
3: adc_channel_t.ADC_CHANNEL_3,
|
||||||
4: adc1_channel_t.ADC1_CHANNEL_4,
|
4: adc_channel_t.ADC_CHANNEL_4,
|
||||||
5: adc1_channel_t.ADC1_CHANNEL_5,
|
5: adc_channel_t.ADC_CHANNEL_5,
|
||||||
6: adc1_channel_t.ADC1_CHANNEL_6,
|
6: adc_channel_t.ADC_CHANNEL_6,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32H2: {
|
VARIANT_ESP32H2: {
|
||||||
1: adc1_channel_t.ADC1_CHANNEL_0,
|
1: adc_channel_t.ADC_CHANNEL_0,
|
||||||
2: adc1_channel_t.ADC1_CHANNEL_1,
|
2: adc_channel_t.ADC_CHANNEL_1,
|
||||||
3: adc1_channel_t.ADC1_CHANNEL_2,
|
3: adc_channel_t.ADC_CHANNEL_2,
|
||||||
4: adc1_channel_t.ADC1_CHANNEL_3,
|
4: adc_channel_t.ADC_CHANNEL_3,
|
||||||
5: adc1_channel_t.ADC1_CHANNEL_4,
|
5: adc_channel_t.ADC_CHANNEL_4,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32S2: {
|
VARIANT_ESP32S2: {
|
||||||
1: adc1_channel_t.ADC1_CHANNEL_0,
|
1: adc_channel_t.ADC_CHANNEL_0,
|
||||||
2: adc1_channel_t.ADC1_CHANNEL_1,
|
2: adc_channel_t.ADC_CHANNEL_1,
|
||||||
3: adc1_channel_t.ADC1_CHANNEL_2,
|
3: adc_channel_t.ADC_CHANNEL_2,
|
||||||
4: adc1_channel_t.ADC1_CHANNEL_3,
|
4: adc_channel_t.ADC_CHANNEL_3,
|
||||||
5: adc1_channel_t.ADC1_CHANNEL_4,
|
5: adc_channel_t.ADC_CHANNEL_4,
|
||||||
6: adc1_channel_t.ADC1_CHANNEL_5,
|
6: adc_channel_t.ADC_CHANNEL_5,
|
||||||
7: adc1_channel_t.ADC1_CHANNEL_6,
|
7: adc_channel_t.ADC_CHANNEL_6,
|
||||||
8: adc1_channel_t.ADC1_CHANNEL_7,
|
8: adc_channel_t.ADC_CHANNEL_7,
|
||||||
9: adc1_channel_t.ADC1_CHANNEL_8,
|
9: adc_channel_t.ADC_CHANNEL_8,
|
||||||
10: adc1_channel_t.ADC1_CHANNEL_9,
|
10: adc_channel_t.ADC_CHANNEL_9,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32S3: {
|
VARIANT_ESP32S3: {
|
||||||
1: adc1_channel_t.ADC1_CHANNEL_0,
|
1: adc_channel_t.ADC_CHANNEL_0,
|
||||||
2: adc1_channel_t.ADC1_CHANNEL_1,
|
2: adc_channel_t.ADC_CHANNEL_1,
|
||||||
3: adc1_channel_t.ADC1_CHANNEL_2,
|
3: adc_channel_t.ADC_CHANNEL_2,
|
||||||
4: adc1_channel_t.ADC1_CHANNEL_3,
|
4: adc_channel_t.ADC_CHANNEL_3,
|
||||||
5: adc1_channel_t.ADC1_CHANNEL_4,
|
5: adc_channel_t.ADC_CHANNEL_4,
|
||||||
6: adc1_channel_t.ADC1_CHANNEL_5,
|
6: adc_channel_t.ADC_CHANNEL_5,
|
||||||
7: adc1_channel_t.ADC1_CHANNEL_6,
|
7: adc_channel_t.ADC_CHANNEL_6,
|
||||||
8: adc1_channel_t.ADC1_CHANNEL_7,
|
8: adc_channel_t.ADC_CHANNEL_7,
|
||||||
9: adc1_channel_t.ADC1_CHANNEL_8,
|
9: adc_channel_t.ADC_CHANNEL_8,
|
||||||
10: adc1_channel_t.ADC1_CHANNEL_9,
|
10: adc_channel_t.ADC_CHANNEL_9,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,24 +136,24 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
|||||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32: {
|
VARIANT_ESP32: {
|
||||||
4: adc2_channel_t.ADC2_CHANNEL_0,
|
4: adc_channel_t.ADC_CHANNEL_0,
|
||||||
0: adc2_channel_t.ADC2_CHANNEL_1,
|
0: adc_channel_t.ADC_CHANNEL_1,
|
||||||
2: adc2_channel_t.ADC2_CHANNEL_2,
|
2: adc_channel_t.ADC_CHANNEL_2,
|
||||||
15: adc2_channel_t.ADC2_CHANNEL_3,
|
15: adc_channel_t.ADC_CHANNEL_3,
|
||||||
13: adc2_channel_t.ADC2_CHANNEL_4,
|
13: adc_channel_t.ADC_CHANNEL_4,
|
||||||
12: adc2_channel_t.ADC2_CHANNEL_5,
|
12: adc_channel_t.ADC_CHANNEL_5,
|
||||||
14: adc2_channel_t.ADC2_CHANNEL_6,
|
14: adc_channel_t.ADC_CHANNEL_6,
|
||||||
27: adc2_channel_t.ADC2_CHANNEL_7,
|
27: adc_channel_t.ADC_CHANNEL_7,
|
||||||
25: adc2_channel_t.ADC2_CHANNEL_8,
|
25: adc_channel_t.ADC_CHANNEL_8,
|
||||||
26: adc2_channel_t.ADC2_CHANNEL_9,
|
26: adc_channel_t.ADC_CHANNEL_9,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32C2: {
|
VARIANT_ESP32C2: {
|
||||||
5: adc2_channel_t.ADC2_CHANNEL_0,
|
5: adc_channel_t.ADC_CHANNEL_0,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32C3: {
|
VARIANT_ESP32C3: {
|
||||||
5: adc2_channel_t.ADC2_CHANNEL_0,
|
5: adc_channel_t.ADC_CHANNEL_0,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32C6: {}, # no ADC2
|
VARIANT_ESP32C6: {}, # no ADC2
|
||||||
@ -160,29 +161,29 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
|||||||
VARIANT_ESP32H2: {}, # no ADC2
|
VARIANT_ESP32H2: {}, # no ADC2
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32S2: {
|
VARIANT_ESP32S2: {
|
||||||
11: adc2_channel_t.ADC2_CHANNEL_0,
|
11: adc_channel_t.ADC_CHANNEL_0,
|
||||||
12: adc2_channel_t.ADC2_CHANNEL_1,
|
12: adc_channel_t.ADC_CHANNEL_1,
|
||||||
13: adc2_channel_t.ADC2_CHANNEL_2,
|
13: adc_channel_t.ADC_CHANNEL_2,
|
||||||
14: adc2_channel_t.ADC2_CHANNEL_3,
|
14: adc_channel_t.ADC_CHANNEL_3,
|
||||||
15: adc2_channel_t.ADC2_CHANNEL_4,
|
15: adc_channel_t.ADC_CHANNEL_4,
|
||||||
16: adc2_channel_t.ADC2_CHANNEL_5,
|
16: adc_channel_t.ADC_CHANNEL_5,
|
||||||
17: adc2_channel_t.ADC2_CHANNEL_6,
|
17: adc_channel_t.ADC_CHANNEL_6,
|
||||||
18: adc2_channel_t.ADC2_CHANNEL_7,
|
18: adc_channel_t.ADC_CHANNEL_7,
|
||||||
19: adc2_channel_t.ADC2_CHANNEL_8,
|
19: adc_channel_t.ADC_CHANNEL_8,
|
||||||
20: adc2_channel_t.ADC2_CHANNEL_9,
|
20: adc_channel_t.ADC_CHANNEL_9,
|
||||||
},
|
},
|
||||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
|
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
|
||||||
VARIANT_ESP32S3: {
|
VARIANT_ESP32S3: {
|
||||||
11: adc2_channel_t.ADC2_CHANNEL_0,
|
11: adc_channel_t.ADC_CHANNEL_0,
|
||||||
12: adc2_channel_t.ADC2_CHANNEL_1,
|
12: adc_channel_t.ADC_CHANNEL_1,
|
||||||
13: adc2_channel_t.ADC2_CHANNEL_2,
|
13: adc_channel_t.ADC_CHANNEL_2,
|
||||||
14: adc2_channel_t.ADC2_CHANNEL_3,
|
14: adc_channel_t.ADC_CHANNEL_3,
|
||||||
15: adc2_channel_t.ADC2_CHANNEL_4,
|
15: adc_channel_t.ADC_CHANNEL_4,
|
||||||
16: adc2_channel_t.ADC2_CHANNEL_5,
|
16: adc_channel_t.ADC_CHANNEL_5,
|
||||||
17: adc2_channel_t.ADC2_CHANNEL_6,
|
17: adc_channel_t.ADC_CHANNEL_6,
|
||||||
18: adc2_channel_t.ADC2_CHANNEL_7,
|
18: adc_channel_t.ADC_CHANNEL_7,
|
||||||
19: adc2_channel_t.ADC2_CHANNEL_8,
|
19: adc_channel_t.ADC_CHANNEL_8,
|
||||||
20: adc2_channel_t.ADC2_CHANNEL_9,
|
20: adc_channel_t.ADC_CHANNEL_9,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,14 @@
|
|||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
#include "esphome/components/voltage_sampler/voltage_sampler.h"
|
#include "esphome/components/voltage_sampler/voltage_sampler.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
#include <esp_adc_cal.h>
|
#include "esp_adc/adc_cali.h"
|
||||||
#include "driver/adc.h"
|
#include "esp_adc/adc_cali_scheme.h"
|
||||||
|
#include "esp_adc/adc_oneshot.h"
|
||||||
|
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@ -49,33 +52,72 @@ class Aggregator {
|
|||||||
|
|
||||||
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
|
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
|
||||||
public:
|
public:
|
||||||
|
/// Update the sensor's state by reading the current ADC value.
|
||||||
|
/// This method is called periodically based on the update interval.
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
/// Set up the ADC sensor by initializing hardware and calibration parameters.
|
||||||
|
/// This method is called once during device initialization.
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
/// Output the configuration details of the ADC sensor for debugging purposes.
|
||||||
|
/// This method is called during the ESPHome setup process to log the configuration.
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
/// Return the setup priority for this component.
|
||||||
|
/// Components with higher priority are initialized earlier during setup.
|
||||||
|
/// @return A float representing the setup priority.
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
|
/// Set the GPIO pin to be used by the ADC sensor.
|
||||||
|
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
|
||||||
|
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||||
|
|
||||||
|
/// Enable or disable the output of raw ADC values (unprocessed data).
|
||||||
|
/// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false).
|
||||||
|
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
|
||||||
|
|
||||||
|
/// Set the number of samples to be taken for ADC readings to improve accuracy.
|
||||||
|
/// A higher sample count reduces noise but increases the reading time.
|
||||||
|
/// @param sample_count The number of samples (e.g., 1, 4, 8).
|
||||||
|
void set_sample_count(uint8_t sample_count);
|
||||||
|
|
||||||
|
/// Set the sampling mode for how multiple ADC samples are combined into a single measurement.
|
||||||
|
///
|
||||||
|
/// When multiple samples are taken (controlled by set_sample_count), they can be combined
|
||||||
|
/// in one of three ways:
|
||||||
|
/// - SamplingMode::AVG: Compute the average (default)
|
||||||
|
/// - SamplingMode::MIN: Use the lowest sample value
|
||||||
|
/// - SamplingMode::MAX: Use the highest sample value
|
||||||
|
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
|
||||||
|
void set_sampling_mode(SamplingMode sampling_mode);
|
||||||
|
|
||||||
|
/// Perform a single ADC sampling operation and return the measured value.
|
||||||
|
/// This function handles raw readings, calibration, and averaging as needed.
|
||||||
|
/// @return The sampled value as a float.
|
||||||
|
float sample() override;
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
/// Set the attenuation for this pin. Only available on the ESP32.
|
/// Set the ADC attenuation level to adjust the input voltage range.
|
||||||
|
/// This determines how the ADC interprets input voltages, allowing for greater precision
|
||||||
|
/// or the ability to measure higher voltages depending on the chosen attenuation level.
|
||||||
|
/// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11).
|
||||||
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
|
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
|
||||||
void set_channel1(adc1_channel_t channel) {
|
|
||||||
this->channel1_ = channel;
|
/// Configure the ADC to use a specific channel on ADC1.
|
||||||
this->channel2_ = ADC2_CHANNEL_MAX;
|
/// This sets the channel for single-shot or continuous ADC measurements.
|
||||||
}
|
/// @param channel The ADC1 channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc.
|
||||||
void set_channel2(adc2_channel_t channel) {
|
void set_channel(adc_unit_t unit, adc_channel_t channel) {
|
||||||
this->channel2_ = channel;
|
this->adc_unit_ = unit;
|
||||||
this->channel1_ = ADC1_CHANNEL_MAX;
|
this->channel_ = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set whether autoranging should be enabled for the ADC.
|
||||||
|
/// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages.
|
||||||
|
/// @param autorange Boolean indicating whether to enable autoranging.
|
||||||
void set_autorange(bool autorange) { this->autorange_ = autorange; }
|
void set_autorange(bool autorange) { this->autorange_ = autorange; }
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|
||||||
/// Update ADC values
|
|
||||||
void update() override;
|
|
||||||
/// Setup ADC
|
|
||||||
void setup() override;
|
|
||||||
void dump_config() override;
|
|
||||||
/// `HARDWARE_LATE` setup priority
|
|
||||||
float get_setup_priority() const override;
|
|
||||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
|
||||||
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
|
|
||||||
void set_sample_count(uint8_t sample_count);
|
|
||||||
void set_sampling_mode(SamplingMode sampling_mode);
|
|
||||||
float sample() override;
|
|
||||||
|
|
||||||
#ifdef USE_RP2040
|
#ifdef USE_RP2040
|
||||||
void set_is_temperature() { this->is_temperature_ = true; }
|
void set_is_temperature() { this->is_temperature_ = true; }
|
||||||
#endif // USE_RP2040
|
#endif // USE_RP2040
|
||||||
@ -86,17 +128,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
|||||||
InternalGPIOPin *pin_;
|
InternalGPIOPin *pin_;
|
||||||
SamplingMode sampling_mode_{SamplingMode::AVG};
|
SamplingMode sampling_mode_{SamplingMode::AVG};
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
float sample_autorange_();
|
||||||
|
float sample_fixed_attenuation_();
|
||||||
|
bool autorange_{false};
|
||||||
|
adc_oneshot_unit_handle_t adc_handle_{nullptr};
|
||||||
|
adc_cali_handle_t calibration_handle_{nullptr};
|
||||||
|
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
||||||
|
adc_channel_t channel_;
|
||||||
|
adc_unit_t adc_unit_;
|
||||||
|
struct SetupFlags {
|
||||||
|
uint8_t init_complete : 1;
|
||||||
|
uint8_t config_complete : 1;
|
||||||
|
uint8_t handle_init_complete : 1;
|
||||||
|
uint8_t calibration_complete : 1;
|
||||||
|
uint8_t reserved : 4;
|
||||||
|
} setup_flags_{};
|
||||||
|
static adc_oneshot_unit_handle_t shared_adc_handles[2];
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|
||||||
#ifdef USE_RP2040
|
#ifdef USE_RP2040
|
||||||
bool is_temperature_{false};
|
bool is_temperature_{false};
|
||||||
#endif // USE_RP2040
|
#endif // USE_RP2040
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
|
||||||
adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
|
|
||||||
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
|
|
||||||
bool autorange_{false};
|
|
||||||
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
|
|
||||||
#endif // USE_ESP32
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace adc
|
} // namespace adc
|
||||||
|
@ -8,145 +8,308 @@ namespace adc {
|
|||||||
|
|
||||||
static const char *const TAG = "adc.esp32";
|
static const char *const TAG = "adc.esp32";
|
||||||
|
|
||||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
|
adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
|
||||||
|
|
||||||
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
|
const LogString *attenuation_to_str(adc_atten_t attenuation) {
|
||||||
#if USE_ESP32_VARIANT_ESP32S2
|
switch (attenuation) {
|
||||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
|
case ADC_ATTEN_DB_0:
|
||||||
#else
|
return LOG_STR("0 dB");
|
||||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
|
case ADC_ATTEN_DB_2_5:
|
||||||
#endif // USE_ESP32_VARIANT_ESP32S2
|
return LOG_STR("2.5 dB");
|
||||||
#endif // SOC_ADC_RTC_MAX_BITWIDTH
|
case ADC_ATTEN_DB_6:
|
||||||
|
return LOG_STR("6 dB");
|
||||||
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;
|
case ADC_ATTEN_DB_12_COMPAT:
|
||||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;
|
return LOG_STR("12 dB");
|
||||||
|
|
||||||
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:
|
default:
|
||||||
break;
|
return LOG_STR("Unknown Attenuation");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ADCSensor::dump_config() {
|
const LogString *adc_unit_to_str(adc_unit_t unit) {
|
||||||
static const char *const ATTEN_AUTO_STR = "auto";
|
switch (unit) {
|
||||||
static const char *const ATTEN_0DB_STR = "0 db";
|
case ADC_UNIT_1:
|
||||||
static const char *const ATTEN_2_5DB_STR = "2.5 db";
|
return LOG_STR("ADC1");
|
||||||
static const char *const ATTEN_6DB_STR = "6 db";
|
case ADC_UNIT_2:
|
||||||
static const char *const ATTEN_12DB_STR = "12 db";
|
return LOG_STR("ADC2");
|
||||||
const char *atten_str = ATTEN_AUTO_STR;
|
default:
|
||||||
|
return LOG_STR("Unknown ADC Unit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_SENSOR("", "ADC Sensor", this);
|
||||||
LOG_PIN(" Pin: ", this->pin_);
|
LOG_PIN(" Pin: ", this->pin_);
|
||||||
|
|
||||||
if (!this->autorange_) {
|
|
||||||
switch (this->attenuation_) {
|
|
||||||
case ADC_ATTEN_DB_0:
|
|
||||||
atten_str = ATTEN_0DB_STR;
|
|
||||||
break;
|
|
||||||
case ADC_ATTEN_DB_2_5:
|
|
||||||
atten_str = ATTEN_2_5DB_STR;
|
|
||||||
break;
|
|
||||||
case ADC_ATTEN_DB_6:
|
|
||||||
atten_str = ATTEN_6DB_STR;
|
|
||||||
break;
|
|
||||||
case ADC_ATTEN_DB_12_COMPAT:
|
|
||||||
atten_str = ATTEN_12DB_STR;
|
|
||||||
break;
|
|
||||||
default: // This is to satisfy the unused ADC_ATTEN_MAX
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
|
" Channel: %d\n"
|
||||||
|
" Unit: %s\n"
|
||||||
" Attenuation: %s\n"
|
" Attenuation: %s\n"
|
||||||
" Samples: %i\n"
|
" Samples: %i\n"
|
||||||
" Sampling mode: %s",
|
" Sampling mode: %s",
|
||||||
atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
|
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
|
||||||
|
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
|
||||||
|
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
|
||||||
|
|
||||||
|
ESP_LOGCONFIG(
|
||||||
|
TAG,
|
||||||
|
" Setup Status:\n"
|
||||||
|
" Handle Init: %s\n"
|
||||||
|
" Config: %s\n"
|
||||||
|
" Calibration: %s\n"
|
||||||
|
" Overall Init: %s",
|
||||||
|
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
|
||||||
|
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
|
||||||
|
|
||||||
LOG_UPDATE_INTERVAL(this);
|
LOG_UPDATE_INTERVAL(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
float ADCSensor::sample() {
|
float ADCSensor::sample() {
|
||||||
if (!this->autorange_) {
|
if (this->autorange_) {
|
||||||
|
return this->sample_autorange_();
|
||||||
|
} else {
|
||||||
|
return this->sample_fixed_attenuation_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float ADCSensor::sample_fixed_attenuation_() {
|
||||||
auto aggr = Aggregator(this->sampling_mode_);
|
auto aggr = Aggregator(this->sampling_mode_);
|
||||||
|
|
||||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||||
int raw = -1;
|
int raw;
|
||||||
if (this->channel1_ != ADC1_CHANNEL_MAX) {
|
esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
|
||||||
raw = adc1_get_raw(this->channel1_);
|
|
||||||
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
|
if (err != ESP_OK) {
|
||||||
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw);
|
ESP_LOGW(TAG, "ADC read failed with error %d", err);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw == -1) {
|
if (raw == -1) {
|
||||||
return NAN;
|
ESP_LOGW(TAG, "Invalid ADC reading");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
aggr.add_sample(raw);
|
aggr.add_sample(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t final_value = aggr.aggregate();
|
||||||
|
|
||||||
if (this->output_raw_) {
|
if (this->output_raw_) {
|
||||||
return aggr.aggregate();
|
return final_value;
|
||||||
}
|
|
||||||
uint32_t mv =
|
|
||||||
esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]);
|
|
||||||
return mv / 1000.0f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
|
if (this->calibration_handle_ != nullptr) {
|
||||||
|
int voltage_mv;
|
||||||
|
esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
return voltage_mv / 1000.0f;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||||
|
if (this->calibration_handle_ != nullptr) {
|
||||||
|
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||||
|
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||||
|
#else // Other ESP32 variants use line fitting calibration
|
||||||
|
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||||
|
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C6 || ESP32S3 || ESP32H2
|
||||||
|
this->calibration_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this->channel1_ != ADC1_CHANNEL_MAX) {
|
return final_value * 3.3f / 4095.0f;
|
||||||
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT);
|
}
|
||||||
raw12 = adc1_get_raw(this->channel1_);
|
|
||||||
if (raw12 < ADC_MAX) {
|
float ADCSensor::sample_autorange_() {
|
||||||
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6);
|
// Auto-range mode
|
||||||
raw6 = adc1_get_raw(this->channel1_);
|
auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> {
|
||||||
if (raw6 < ADC_MAX) {
|
// First reconfigure the attenuation for this reading
|
||||||
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5);
|
adc_oneshot_chan_cfg_t config = {
|
||||||
raw2 = adc1_get_raw(this->channel1_);
|
.atten = atten,
|
||||||
if (raw2 < ADC_MAX) {
|
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||||
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0);
|
};
|
||||||
raw0 = adc1_get_raw(this->channel1_);
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
|
return {-1, 0.0f};
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float voltage = 0.0f;
|
||||||
|
if (handle != nullptr) {
|
||||||
|
int voltage_mv;
|
||||||
|
err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
voltage = voltage_mv / 1000.0f;
|
||||||
|
} else {
|
||||||
|
voltage = raw * 3.3f / 4095.0f;
|
||||||
|
}
|
||||||
|
// Clean up calibration handle
|
||||||
|
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||||
|
adc_cali_delete_scheme_curve_fitting(handle);
|
||||||
|
#else
|
||||||
|
adc_cali_delete_scheme_line_fitting(handle);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
voltage = raw * 3.3f / 4095.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {raw, voltage};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12);
|
||||||
|
if (raw12 == -1) {
|
||||||
|
ESP_LOGE(TAG, "Failed to read ADC in autorange mode");
|
||||||
|
return NAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
int raw6 = 4095, raw2 = 4095, raw0 = 4095;
|
||||||
|
float mv6 = 0, mv2 = 0, mv0 = 0;
|
||||||
|
|
||||||
|
if (raw12 < 4095) {
|
||||||
|
auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6);
|
||||||
|
raw6 = raw6_val;
|
||||||
|
mv6 = mv6_val;
|
||||||
|
|
||||||
|
if (raw6 < 4095 && raw6 != -1) {
|
||||||
|
auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5);
|
||||||
|
raw2 = raw2_val;
|
||||||
|
mv2 = mv2_val;
|
||||||
|
|
||||||
|
if (raw2 < 4095 && raw2 != -1) {
|
||||||
|
auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0);
|
||||||
|
raw0 = raw0_val;
|
||||||
|
mv0 = mv0_val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,19 +318,19 @@ float ADCSensor::sample() {
|
|||||||
return NAN;
|
return NAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]);
|
const int adc_half = 2048;
|
||||||
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]);
|
uint32_t c12 = std::min(raw12, adc_half);
|
||||||
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
|
uint32_t c6 = adc_half - std::abs(raw6 - adc_half);
|
||||||
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
|
uint32_t c2 = adc_half - std::abs(raw2 - adc_half);
|
||||||
|
uint32_t c0 = std::min(4095 - raw0, adc_half);
|
||||||
uint32_t c12 = std::min(raw12, ADC_HALF);
|
|
||||||
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
|
|
||||||
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
|
|
||||||
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
|
|
||||||
uint32_t csum = c12 + c6 + c2 + c0;
|
uint32_t csum = c12 + c6 + c2 + c0;
|
||||||
|
|
||||||
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
|
if (csum == 0) {
|
||||||
return mv_scaled / (float) (csum * 1000U);
|
ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
|
||||||
|
return NAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace adc
|
} // namespace adc
|
||||||
|
@ -10,13 +10,11 @@ from esphome.const import (
|
|||||||
CONF_NUMBER,
|
CONF_NUMBER,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
CONF_RAW,
|
CONF_RAW,
|
||||||
CONF_WIFI,
|
|
||||||
DEVICE_CLASS_VOLTAGE,
|
DEVICE_CLASS_VOLTAGE,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
UNIT_VOLT,
|
UNIT_VOLT,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
import esphome.final_validate as fv
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
ATTENUATION_MODES,
|
ATTENUATION_MODES,
|
||||||
@ -24,6 +22,7 @@ from . import (
|
|||||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
|
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
|
||||||
SAMPLING_MODES,
|
SAMPLING_MODES,
|
||||||
adc_ns,
|
adc_ns,
|
||||||
|
adc_unit_t,
|
||||||
validate_adc_pin,
|
validate_adc_pin,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,21 +56,6 @@ def validate_config(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def final_validate_config(config):
|
|
||||||
if CORE.is_esp32:
|
|
||||||
variant = get_esp32_variant()
|
|
||||||
if (
|
|
||||||
CONF_WIFI in fv.full_config.get()
|
|
||||||
and config[CONF_PIN][CONF_NUMBER]
|
|
||||||
in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
|
|
||||||
):
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"{variant} doesn't support ADC on this pin when Wi-Fi is configured"
|
|
||||||
)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
ADCSensor = adc_ns.class_(
|
ADCSensor = adc_ns.class_(
|
||||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||||
)
|
)
|
||||||
@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
validate_config,
|
validate_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = final_validate_config
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
@ -119,13 +101,13 @@ async def to_code(config):
|
|||||||
cg.add(var.set_sample_count(config[CONF_SAMPLES]))
|
cg.add(var.set_sample_count(config[CONF_SAMPLES]))
|
||||||
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
|
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
|
||||||
|
|
||||||
|
if CORE.is_esp32:
|
||||||
if attenuation := config.get(CONF_ATTENUATION):
|
if attenuation := config.get(CONF_ATTENUATION):
|
||||||
if attenuation == "auto":
|
if attenuation == "auto":
|
||||||
cg.add(var.set_autorange(cg.global_ns.true))
|
cg.add(var.set_autorange(cg.global_ns.true))
|
||||||
else:
|
else:
|
||||||
cg.add(var.set_attenuation(attenuation))
|
cg.add(var.set_attenuation(attenuation))
|
||||||
|
|
||||||
if CORE.is_esp32:
|
|
||||||
variant = get_esp32_variant()
|
variant = get_esp32_variant()
|
||||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||||
if (
|
if (
|
||||||
@ -133,10 +115,10 @@ async def to_code(config):
|
|||||||
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
|
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
|
||||||
):
|
):
|
||||||
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
|
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
|
||||||
cg.add(var.set_channel1(chan))
|
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan))
|
||||||
elif (
|
elif (
|
||||||
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
|
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
|
||||||
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
|
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
|
||||||
):
|
):
|
||||||
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
|
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
|
||||||
cg.add(var.set_channel2(chan))
|
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))
|
||||||
|
@ -16,6 +16,8 @@ class UserServiceDescriptor {
|
|||||||
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
||||||
|
|
||||||
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
||||||
|
|
||||||
|
bool is_internal() { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/as3935/as3935.h"
|
#include "esphome/components/as3935/as3935.h"
|
||||||
#include "esphome/components/spi/spi.h"
|
#include "esphome/components/spi/spi.h"
|
||||||
#include "esphome/components/sensor/sensor.h"
|
|
||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace as3935_spi {
|
namespace as3935_spi {
|
||||||
|
@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
if CORE.is_esp32 or CORE.is_libretiny:
|
if CORE.is_esp32 or CORE.is_libretiny:
|
||||||
# https://github.com/ESP32Async/AsyncTCP
|
# https://github.com/ESP32Async/AsyncTCP
|
||||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
|
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
|
||||||
elif CORE.is_esp8266:
|
elif CORE.is_esp8266:
|
||||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
CONF_BYTE_ORDER = "byte_order"
|
CONF_BYTE_ORDER = "byte_order"
|
||||||
|
CONF_COLOR_DEPTH = "color_depth"
|
||||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||||
CONF_REQUEST_HEADERS = "request_headers"
|
CONF_REQUEST_HEADERS = "request_headers"
|
||||||
|
@ -20,6 +20,7 @@ adjusted_ids = set()
|
|||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.ensure_list(
|
cv.ensure_list(
|
||||||
|
cv.COMPONENT_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(EspLdo),
|
cv.GenerateID(): cv.declare_id(EspLdo),
|
||||||
cv.Required(CONF_VOLTAGE): cv.All(
|
cv.Required(CONF_VOLTAGE): cv.All(
|
||||||
@ -28,6 +29,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
|
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
|
||||||
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
|
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
),
|
),
|
||||||
cv.only_with_esp_idf,
|
cv.only_with_esp_idf,
|
||||||
only_on_variant(supported=[VARIANT_ESP32P4]),
|
only_on_variant(supported=[VARIANT_ESP32P4]),
|
||||||
|
@ -17,6 +17,9 @@ class EspLdo : public Component {
|
|||||||
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
|
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
|
||||||
void set_voltage(float voltage) { this->voltage_ = voltage; }
|
void set_voltage(float voltage) { this->voltage_ = voltage; }
|
||||||
void adjust_voltage(float voltage);
|
void adjust_voltage(float voltage);
|
||||||
|
float get_setup_priority() const override {
|
||||||
|
return setup_priority::BUS; // LDO setup should be done early
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int channel_;
|
int channel_;
|
||||||
|
@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
void Fan::save_state_() {
|
void Fan::save_state_() {
|
||||||
|
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
FanRestoreState state{};
|
FanRestoreState state{};
|
||||||
state.state = this->state;
|
state.state = this->state;
|
||||||
state.oscillating = this->oscillating;
|
state.oscillating = this->oscillating;
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import binary_sensor
|
from esphome.components import binary_sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_PIN
|
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
from .. import gpio_ns
|
from .. import gpio_ns
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
GPIOBinarySensor = gpio_ns.class_(
|
GPIOBinarySensor = gpio_ns.class_(
|
||||||
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||||
)
|
)
|
||||||
@ -41,6 +46,22 @@ async def to_code(config):
|
|||||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
cg.add(var.set_pin(pin))
|
cg.add(var.set_pin(pin))
|
||||||
|
|
||||||
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
|
# Check for ESP8266 GPIO16 interrupt limitation
|
||||||
if config[CONF_USE_INTERRUPT]:
|
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
|
||||||
|
# the Arduino attachInterrupt() function. This is the only known GPIO pin
|
||||||
|
# across all supported platforms that has this limitation, so we handle it
|
||||||
|
# here instead of in the platform-specific code.
|
||||||
|
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||||
|
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
|
||||||
|
"Falling back to polling mode (same as in ESPHome <2025.7). "
|
||||||
|
"The sensor will work exactly as before, but other pins have better "
|
||||||
|
"performance with interrupts.",
|
||||||
|
config.get(CONF_NAME, config[CONF_ID]),
|
||||||
|
)
|
||||||
|
use_interrupt = False
|
||||||
|
|
||||||
|
cg.add(var.set_use_interrupt(use_interrupt))
|
||||||
|
if use_interrupt:
|
||||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||||
|
@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
|||||||
container.reset(); // Release ownership of the container's shared_ptr
|
container.reset(); // Release ownership of the container's shared_ptr
|
||||||
|
|
||||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||||
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
|
if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
|
||||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) {
|
|||||||
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
||||||
|
|
||||||
for (auto build : root["builds"].as<JsonArray>()) {
|
for (auto build : root["builds"].as<JsonArray>()) {
|
||||||
if (!build.containsKey("chipFamily")) {
|
if (!build["chipFamily"].is<const char *>()) {
|
||||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (build["chipFamily"] == ESPHOME_VARIANT) {
|
if (build["chipFamily"] == ESPHOME_VARIANT) {
|
||||||
if (!build.containsKey("ota")) {
|
if (!build["ota"].is<JsonObject>()) {
|
||||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto ota = build["ota"];
|
JsonObject ota = build["ota"].as<JsonObject>();
|
||||||
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
|
if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
|
||||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
||||||
this_update->update_info_.md5 = ota["md5"].as<std::string>();
|
this_update->update_info_.md5 = ota["md5"].as<std::string>();
|
||||||
|
|
||||||
if (ota.containsKey("summary"))
|
if (ota["summary"].is<const char *>())
|
||||||
this_update->update_info_.summary = ota["summary"].as<std::string>();
|
this_update->update_info_.summary = ota["summary"].as<std::string>();
|
||||||
if (ota.containsKey("release_url"))
|
if (ota["release_url"].is<const char *>())
|
||||||
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
|
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -180,7 +180,7 @@ async def to_code(config):
|
|||||||
await speaker.register_speaker(var, config)
|
await speaker.register_speaker(var, config)
|
||||||
|
|
||||||
if config[CONF_DAC_TYPE] == "internal":
|
if config[CONF_DAC_TYPE] == "internal":
|
||||||
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
|
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
|
||||||
else:
|
else:
|
||||||
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
||||||
if use_legacy():
|
if use_legacy():
|
||||||
|
@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
|
|
||||||
@coroutine_with_priority(1.0)
|
@coroutine_with_priority(1.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_library("bblanchon/ArduinoJson", "6.18.5")
|
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
|
||||||
cg.add_define("USE_JSON")
|
cg.add_define("USE_JSON")
|
||||||
cg.add_global(json_ns.using)
|
cg.add_global(json_ns.using)
|
||||||
|
@ -1,83 +1,76 @@
|
|||||||
#include "json_util.h"
|
#include "json_util.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace json {
|
namespace json {
|
||||||
|
|
||||||
static const char *const TAG = "json";
|
static const char *const TAG = "json";
|
||||||
|
|
||||||
static std::vector<char> global_json_build_buffer; // NOLINT
|
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||||
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
struct SpiRamAllocator : ArduinoJson::Allocator {
|
||||||
|
void *allocate(size_t size) override { return this->allocator_.allocate(size); }
|
||||||
|
|
||||||
|
void deallocate(void *pointer) override {
|
||||||
|
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
|
||||||
|
// RAMAllocator::deallocate() requires the size, which we don't have access to here.
|
||||||
|
// RAMAllocator::deallocate implementation just calls free() regardless of whether
|
||||||
|
// the memory was allocated with heap_caps_malloc or malloc.
|
||||||
|
// This is safe because ESP-IDF's heap implementation internally tracks the memory region
|
||||||
|
// and routes free() to the appropriate heap.
|
||||||
|
free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
|
||||||
|
}
|
||||||
|
|
||||||
|
void *reallocate(void *ptr, size_t new_size) override {
|
||||||
|
return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)};
|
||||||
|
};
|
||||||
|
|
||||||
std::string build_json(const json_build_t &f) {
|
std::string build_json(const json_build_t &f) {
|
||||||
// Here we are allocating up to 5kb of memory,
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
// with the heap size minus 2kb to be safe if less than 5kb
|
auto doc_allocator = SpiRamAllocator();
|
||||||
// as we can not have a true dynamic sized document.
|
JsonDocument json_document(&doc_allocator);
|
||||||
// The excess memory is freed below with `shrinkToFit()`
|
if (json_document.overflowed()) {
|
||||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
|
||||||
while (true) {
|
|
||||||
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
|
|
||||||
DynamicJsonDocument json_document(request_size);
|
|
||||||
if (json_document.capacity() == 0) {
|
|
||||||
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes",
|
|
||||||
request_size, free_heap);
|
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
JsonObject root = json_document.to<JsonObject>();
|
JsonObject root = json_document.to<JsonObject>();
|
||||||
f(root);
|
f(root);
|
||||||
if (json_document.overflowed()) {
|
if (json_document.overflowed()) {
|
||||||
if (request_size == free_heap) {
|
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||||
ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes",
|
|
||||||
free_heap);
|
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
request_size = std::min(request_size * 2, free_heap);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
json_document.shrinkToFit();
|
|
||||||
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
|
|
||||||
std::string output;
|
std::string output;
|
||||||
serializeJson(json_document, output);
|
serializeJson(json_document, output);
|
||||||
return output;
|
return output;
|
||||||
}
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||||
// Here we are allocating 1.5 times the data size,
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
// with the heap size minus 2kb to be safe if less than that
|
auto doc_allocator = SpiRamAllocator();
|
||||||
// as we can not have a true dynamic sized document.
|
JsonDocument json_document(&doc_allocator);
|
||||||
// The excess memory is freed below with `shrinkToFit()`
|
if (json_document.overflowed()) {
|
||||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
|
||||||
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
|
|
||||||
while (true) {
|
|
||||||
DynamicJsonDocument json_document(request_size);
|
|
||||||
if (json_document.capacity() == 0) {
|
|
||||||
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size,
|
|
||||||
free_heap);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DeserializationError err = deserializeJson(json_document, data);
|
DeserializationError err = deserializeJson(json_document, data);
|
||||||
json_document.shrinkToFit();
|
|
||||||
|
|
||||||
JsonObject root = json_document.as<JsonObject>();
|
JsonObject root = json_document.as<JsonObject>();
|
||||||
|
|
||||||
if (err == DeserializationError::Ok) {
|
if (err == DeserializationError::Ok) {
|
||||||
return f(root);
|
return f(root);
|
||||||
} else if (err == DeserializationError::NoMemory) {
|
} else if (err == DeserializationError::NoMemory) {
|
||||||
if (request_size * 2 >= free_heap) {
|
|
||||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ESP_LOGV(TAG, "Increasing memory allocation.");
|
|
||||||
request_size *= 2;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
};
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace json
|
} // namespace json
|
||||||
|
@ -9,6 +9,7 @@ namespace light {
|
|||||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||||
|
|
||||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
if (state.supports_effects())
|
if (state.supports_effects())
|
||||||
root["effect"] = state.get_effect_name();
|
root["effect"] = state.get_effect_name();
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
|||||||
if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
|
if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
|
||||||
root["brightness"] = uint8_t(values.get_brightness() * 255);
|
root["brightness"] = uint8_t(values.get_brightness() * 255);
|
||||||
|
|
||||||
JsonObject color = root.createNestedObject("color");
|
JsonObject color = root["color"].to<JsonObject>();
|
||||||
if (values.get_color_mode() & ColorCapability::RGB) {
|
if (values.get_color_mode() & ColorCapability::RGB) {
|
||||||
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
|
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
|
||||||
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255);
|
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255);
|
||||||
@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) {
|
void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) {
|
||||||
if (root.containsKey("state")) {
|
if (root["state"].is<const char *>()) {
|
||||||
auto val = parse_on_off(root["state"]);
|
auto val = parse_on_off(root["state"]);
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case PARSE_ON:
|
case PARSE_ON:
|
||||||
@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("brightness")) {
|
if (root["brightness"].is<uint8_t>()) {
|
||||||
call.set_brightness(float(root["brightness"]) / 255.0f);
|
call.set_brightness(float(root["brightness"]) / 255.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("color")) {
|
if (root["color"].is<JsonObject>()) {
|
||||||
JsonObject color = root["color"];
|
JsonObject color = root["color"];
|
||||||
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness.
|
// HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness.
|
||||||
float max_rgb = 0.0f;
|
float max_rgb = 0.0f;
|
||||||
if (color.containsKey("r")) {
|
if (color["r"].is<uint8_t>()) {
|
||||||
float r = float(color["r"]) / 255.0f;
|
float r = float(color["r"]) / 255.0f;
|
||||||
max_rgb = fmaxf(max_rgb, r);
|
max_rgb = fmaxf(max_rgb, r);
|
||||||
call.set_red(r);
|
call.set_red(r);
|
||||||
}
|
}
|
||||||
if (color.containsKey("g")) {
|
if (color["g"].is<uint8_t>()) {
|
||||||
float g = float(color["g"]) / 255.0f;
|
float g = float(color["g"]) / 255.0f;
|
||||||
max_rgb = fmaxf(max_rgb, g);
|
max_rgb = fmaxf(max_rgb, g);
|
||||||
call.set_green(g);
|
call.set_green(g);
|
||||||
}
|
}
|
||||||
if (color.containsKey("b")) {
|
if (color["b"].is<uint8_t>()) {
|
||||||
float b = float(color["b"]) / 255.0f;
|
float b = float(color["b"]) / 255.0f;
|
||||||
max_rgb = fmaxf(max_rgb, b);
|
max_rgb = fmaxf(max_rgb, b);
|
||||||
call.set_blue(b);
|
call.set_blue(b);
|
||||||
}
|
}
|
||||||
if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) {
|
if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) {
|
||||||
call.set_color_brightness(max_rgb);
|
call.set_color_brightness(max_rgb);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (color.containsKey("c")) {
|
if (color["c"].is<uint8_t>()) {
|
||||||
call.set_cold_white(float(color["c"]) / 255.0f);
|
call.set_cold_white(float(color["c"]) / 255.0f);
|
||||||
}
|
}
|
||||||
if (color.containsKey("w")) {
|
if (color["w"].is<uint8_t>()) {
|
||||||
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
|
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
|
||||||
// white channel in RGBWW.
|
// white channel in RGBWW.
|
||||||
if (color.containsKey("c")) {
|
if (color["c"].is<uint8_t>()) {
|
||||||
call.set_warm_white(float(color["w"]) / 255.0f);
|
call.set_warm_white(float(color["w"]) / 255.0f);
|
||||||
} else {
|
} else {
|
||||||
call.set_white(float(color["w"]) / 255.0f);
|
call.set_white(float(color["w"]) / 255.0f);
|
||||||
@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("white_value")) { // legacy API
|
if (root["white_value"].is<uint8_t>()) { // legacy API
|
||||||
call.set_white(float(root["white_value"]) / 255.0f);
|
call.set_white(float(root["white_value"]) / 255.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("color_temp")) {
|
if (root["color_temp"].is<uint16_t>()) {
|
||||||
call.set_color_temperature(float(root["color_temp"]));
|
call.set_color_temperature(float(root["color_temp"]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
|||||||
void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
|
void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) {
|
||||||
LightJSONSchema::parse_color_json(state, call, root);
|
LightJSONSchema::parse_color_json(state, call, root);
|
||||||
|
|
||||||
if (root.containsKey("flash")) {
|
if (root["flash"].is<uint32_t>()) {
|
||||||
auto length = uint32_t(float(root["flash"]) * 1000);
|
auto length = uint32_t(float(root["flash"]) * 1000);
|
||||||
call.set_flash_length(length);
|
call.set_flash_length(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("transition")) {
|
if (root["transition"].is<uint16_t>()) {
|
||||||
auto length = uint32_t(float(root["transition"]) * 1000);
|
auto length = uint32_t(float(root["transition"]) * 1000);
|
||||||
call.set_transition_length(length);
|
call.set_transition_length(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.containsKey("effect")) {
|
if (root["effect"].is<const char *>()) {
|
||||||
const char *effect = root["effect"];
|
const char *effect = root["effect"];
|
||||||
call.set_effect(effect);
|
call.set_effect(effect);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,11 @@ from esphome.components.libretiny.const import (
|
|||||||
COMPONENT_LN882X,
|
COMPONENT_LN882X,
|
||||||
COMPONENT_RTL87XX,
|
COMPONENT_RTL87XX,
|
||||||
)
|
)
|
||||||
|
from esphome.components.zephyr import (
|
||||||
|
zephyr_add_cdc_acm,
|
||||||
|
zephyr_add_overlay,
|
||||||
|
zephyr_add_prj_conf,
|
||||||
|
)
|
||||||
from esphome.config_helpers import filter_source_files_from_platform
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@ -41,6 +46,7 @@ from esphome.const import (
|
|||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_LN882X,
|
PLATFORM_LN882X,
|
||||||
|
PLATFORM_NRF52,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
PlatformFramework,
|
PlatformFramework,
|
||||||
@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG]
|
|||||||
|
|
||||||
UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
|
UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
|
||||||
|
|
||||||
|
UART_SELECTION_NRF52 = [USB_CDC, UART0]
|
||||||
|
|
||||||
HARDWARE_UART_TO_UART_SELECTION = {
|
HARDWARE_UART_TO_UART_SELECTION = {
|
||||||
UART0: logger_ns.UART_SELECTION_UART0,
|
UART0: logger_ns.UART_SELECTION_UART0,
|
||||||
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
|
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
|
||||||
@ -167,6 +175,8 @@ def uart_selection(value):
|
|||||||
return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value)
|
return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value)
|
||||||
if CORE.is_host:
|
if CORE.is_host:
|
||||||
raise cv.Invalid("Uart selection not valid for host platform")
|
raise cv.Invalid("Uart selection not valid for host platform")
|
||||||
|
if CORE.is_nrf52:
|
||||||
|
return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_(
|
|||||||
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
|
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
|
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
bk72xx=DEFAULT,
|
bk72xx=DEFAULT,
|
||||||
ln882x=DEFAULT,
|
ln882x=DEFAULT,
|
||||||
rtl87xx=DEFAULT,
|
rtl87xx=DEFAULT,
|
||||||
|
nrf52=USB_CDC,
|
||||||
): cv.All(
|
): cv.All(
|
||||||
cv.only_on(
|
cv.only_on(
|
||||||
[
|
[
|
||||||
@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_LN882X,
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
|
PLATFORM_NRF52,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
uart_selection,
|
uart_selection,
|
||||||
@ -358,6 +371,15 @@ async def to_code(config):
|
|||||||
except cv.Invalid:
|
except cv.Invalid:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if CORE.using_zephyr:
|
||||||
|
if config[CONF_HARDWARE_UART] == UART0:
|
||||||
|
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||||
|
if config[CONF_HARDWARE_UART] == UART1:
|
||||||
|
zephyr_add_overlay("""&uart1 { status = "okay";};""")
|
||||||
|
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||||
|
zephyr_add_prj_conf("UART_LINE_CTRL", True)
|
||||||
|
zephyr_add_cdc_acm(config, 0)
|
||||||
|
|
||||||
# Register at end for safe mode
|
# Register at end for safe mode
|
||||||
await cg.register_component(log, config)
|
await cg.register_component(log, config)
|
||||||
|
|
||||||
@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
|||||||
PlatformFramework.RTL87XX_ARDUINO,
|
PlatformFramework.RTL87XX_ARDUINO,
|
||||||
PlatformFramework.LN882X_ARDUINO,
|
PlatformFramework.LN882X_ARDUINO,
|
||||||
},
|
},
|
||||||
|
"logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||||
"task_log_buffer.cpp": {
|
"task_log_buffer.cpp": {
|
||||||
PlatformFramework.ESP32_ARDUINO,
|
PlatformFramework.ESP32_ARDUINO,
|
||||||
PlatformFramework.ESP32_IDF,
|
PlatformFramework.ESP32_IDF,
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
#include <memory> // For unique_ptr
|
#include <memory> // For unique_ptr
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/application.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace logger {
|
namespace logger {
|
||||||
@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
|||||||
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
|
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||||
this->main_task_ = xTaskGetCurrentTaskHandle();
|
this->main_task_ = xTaskGetCurrentTaskHandle();
|
||||||
|
#elif defined(USE_ZEPHYR)
|
||||||
|
this->main_task_ = k_current_get();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef USE_ZEPHYR
|
||||||
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
|
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
|
||||||
void Logger::loop() {
|
void Logger::loop() {
|
||||||
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
|
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
|
||||||
@ -185,8 +188,13 @@ void Logger::loop() {
|
|||||||
}
|
}
|
||||||
opened = !opened;
|
opened = !opened;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
this->process_messages_();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void Logger::process_messages_() {
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
// Process any buffered messages when available
|
// Process any buffered messages when available
|
||||||
if (this->log_buffer_->has_messages()) {
|
if (this->log_buffer_->has_messages()) {
|
||||||
@ -227,12 +235,11 @@ void Logger::loop() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||||
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
|
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
UARTSelection Logger::get_uart() const { return this->uart_; }
|
UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -29,6 +29,11 @@
|
|||||||
#include <driver/uart.h>
|
#include <driver/uart.h>
|
||||||
#endif // USE_ESP_IDF
|
#endif // USE_ESP_IDF
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
struct device;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
namespace logger {
|
namespace logger {
|
||||||
@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = {
|
|||||||
"VV", // VERY_VERBOSE
|
"VV", // VERY_VERBOSE
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
/** Enum for logging UART selection
|
/** Enum for logging UART selection
|
||||||
*
|
*
|
||||||
* Advanced configuration (pin selection, etc) is not supported.
|
* Advanced configuration (pin selection, etc) is not supported.
|
||||||
@ -82,7 +87,7 @@ enum UARTSelection : uint8_t {
|
|||||||
UART_SELECTION_UART0_SWAP,
|
UART_SELECTION_UART0_SWAP,
|
||||||
#endif // USE_ESP8266
|
#endif // USE_ESP8266
|
||||||
};
|
};
|
||||||
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY
|
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Logger component for all ESPHome logging.
|
* @brief Logger component for all ESPHome logging.
|
||||||
@ -107,7 +112,7 @@ class Logger : public Component {
|
|||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
void init_log_buffer(size_t total_buffer_size);
|
void init_log_buffer(size_t total_buffer_size);
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
|
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR)
|
||||||
void loop() override;
|
void loop() override;
|
||||||
#endif
|
#endif
|
||||||
/// Manually set the baud rate for serial, set to 0 to disable.
|
/// Manually set the baud rate for serial, set to 0 to disable.
|
||||||
@ -122,7 +127,7 @@ class Logger : public Component {
|
|||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
|
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
|
||||||
/// Get the UART used by the logger.
|
/// Get the UART used by the logger.
|
||||||
UARTSelection get_uart() const;
|
UARTSelection get_uart() const;
|
||||||
@ -157,6 +162,7 @@ class Logger : public Component {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void process_messages_();
|
||||||
void write_msg_(const char *msg);
|
void write_msg_(const char *msg);
|
||||||
|
|
||||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||||
@ -164,7 +170,7 @@ class Logger : public Component {
|
|||||||
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
||||||
va_list args, char *buffer, uint16_t *buffer_at,
|
va_list args, char *buffer, uint16_t *buffer_at,
|
||||||
uint16_t buffer_size) {
|
uint16_t buffer_size) {
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
|
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
|
||||||
#else
|
#else
|
||||||
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
|
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
|
||||||
@ -231,7 +237,10 @@ class Logger : public Component {
|
|||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
Stream *hw_serial_{nullptr};
|
Stream *hw_serial_{nullptr};
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ZEPHYR)
|
||||||
|
const device *uart_dev_{nullptr};
|
||||||
|
#endif
|
||||||
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
void *main_task_ = nullptr; // Only used for thread name identification
|
void *main_task_ = nullptr; // Only used for thread name identification
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
@ -256,7 +265,7 @@ class Logger : public Component {
|
|||||||
uint16_t tx_buffer_at_{0};
|
uint16_t tx_buffer_at_{0};
|
||||||
uint16_t tx_buffer_size_{0};
|
uint16_t tx_buffer_size_{0};
|
||||||
uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
|
uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
|
||||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040)
|
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR)
|
||||||
UARTSelection uart_{UART_SELECTION_UART0};
|
UARTSelection uart_{UART_SELECTION_UART0};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LIBRETINY
|
#ifdef USE_LIBRETINY
|
||||||
@ -268,9 +277,13 @@ class Logger : public Component {
|
|||||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
const char *HOT get_thread_name_() {
|
const char *HOT get_thread_name_() {
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
k_tid_t current_task = k_current_get();
|
||||||
|
#else
|
||||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||||
|
#endif
|
||||||
if (current_task == main_task_) {
|
if (current_task == main_task_) {
|
||||||
return nullptr; // Main task
|
return nullptr; // Main task
|
||||||
} else {
|
} else {
|
||||||
@ -278,6 +291,8 @@ class Logger : public Component {
|
|||||||
return pcTaskGetName(current_task);
|
return pcTaskGetName(current_task);
|
||||||
#elif defined(USE_LIBRETINY)
|
#elif defined(USE_LIBRETINY)
|
||||||
return pcTaskGetTaskName(current_task);
|
return pcTaskGetTaskName(current_task);
|
||||||
|
#elif defined(USE_ZEPHYR)
|
||||||
|
return k_thread_name_get(current_task);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,7 +334,7 @@ class Logger : public Component {
|
|||||||
const char *color = esphome::logger::LOG_LEVEL_COLORS[level];
|
const char *color = esphome::logger::LOG_LEVEL_COLORS[level];
|
||||||
const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level];
|
const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level];
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
if (thread_name != nullptr) {
|
if (thread_name != nullptr) {
|
||||||
// Non-main task with thread name
|
// Non-main task with thread name
|
||||||
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,
|
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,
|
||||||
|
88
esphome/components/logger/logger_zephyr.cpp
Normal file
88
esphome/components/logger/logger_zephyr.cpp
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/drivers/uart.h>
|
||||||
|
#include <zephyr/usb/usb_device.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace logger {
|
||||||
|
|
||||||
|
static const char *const TAG = "logger";
|
||||||
|
|
||||||
|
void Logger::loop() {
|
||||||
|
#ifdef USE_LOGGER_USB_CDC
|
||||||
|
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static bool opened = false;
|
||||||
|
uint32_t dtr = 0;
|
||||||
|
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
|
||||||
|
|
||||||
|
/* Poll if the DTR flag was set, optional */
|
||||||
|
if (opened == dtr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opened) {
|
||||||
|
App.schedule_dump_config();
|
||||||
|
}
|
||||||
|
opened = !opened;
|
||||||
|
#endif
|
||||||
|
this->process_messages_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::pre_setup() {
|
||||||
|
if (this->baud_rate_ > 0) {
|
||||||
|
static const struct device *uart_dev = nullptr;
|
||||||
|
switch (this->uart_) {
|
||||||
|
case UART_SELECTION_UART0:
|
||||||
|
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
|
||||||
|
break;
|
||||||
|
case UART_SELECTION_UART1:
|
||||||
|
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||||
|
break;
|
||||||
|
#ifdef USE_LOGGER_USB_CDC
|
||||||
|
case UART_SELECTION_USB_CDC:
|
||||||
|
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||||
|
if (device_is_ready(uart_dev)) {
|
||||||
|
usb_enable(nullptr);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (!device_is_ready(uart_dev)) {
|
||||||
|
ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_());
|
||||||
|
} else {
|
||||||
|
this->uart_dev_ = uart_dev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global_logger = this;
|
||||||
|
ESP_LOGI(TAG, "Log initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
void HOT Logger::write_msg_(const char *msg) {
|
||||||
|
#ifdef CONFIG_PRINTK
|
||||||
|
printk("%s\n", msg);
|
||||||
|
#endif
|
||||||
|
if (nullptr == this->uart_dev_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (*msg) {
|
||||||
|
uart_poll_out(this->uart_dev_, *msg);
|
||||||
|
++msg;
|
||||||
|
}
|
||||||
|
uart_poll_out(this->uart_dev_, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"};
|
||||||
|
|
||||||
|
const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; }
|
||||||
|
|
||||||
|
} // namespace logger
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
@ -29,9 +29,9 @@ from ..defines import (
|
|||||||
)
|
)
|
||||||
from ..helpers import add_lv_use, lvgl_components_required
|
from ..helpers import add_lv_use, lvgl_components_required
|
||||||
from ..lv_validation import (
|
from ..lv_validation import (
|
||||||
angle,
|
|
||||||
get_end_value,
|
get_end_value,
|
||||||
get_start_value,
|
get_start_value,
|
||||||
|
lv_angle,
|
||||||
lv_bool,
|
lv_bool,
|
||||||
lv_color,
|
lv_color,
|
||||||
lv_float,
|
lv_float,
|
||||||
@ -162,7 +162,7 @@ SCALE_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
||||||
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
|
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
|
||||||
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
||||||
cv.Optional(CONF_ROTATION): angle,
|
cv.Optional(CONF_ROTATION): lv_angle,
|
||||||
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -187,7 +187,7 @@ class MeterType(WidgetType):
|
|||||||
for scale_conf in config.get(CONF_SCALES, ()):
|
for scale_conf in config.get(CONF_SCALES, ()):
|
||||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||||
if CONF_ROTATION in scale_conf:
|
if CONF_ROTATION in scale_conf:
|
||||||
rotation = scale_conf[CONF_ROTATION] // 10
|
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
|
||||||
with LocalVariable(
|
with LocalVariable(
|
||||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||||
) as meter_var:
|
) as meter_var:
|
||||||
@ -205,21 +205,20 @@ class MeterType(WidgetType):
|
|||||||
var,
|
var,
|
||||||
meter_var,
|
meter_var,
|
||||||
ticks[CONF_COUNT],
|
ticks[CONF_COUNT],
|
||||||
ticks[CONF_WIDTH],
|
await size.process(ticks[CONF_WIDTH]),
|
||||||
ticks[CONF_LENGTH],
|
await size.process(ticks[CONF_LENGTH]),
|
||||||
color,
|
color,
|
||||||
)
|
)
|
||||||
if CONF_MAJOR in ticks:
|
if CONF_MAJOR in ticks:
|
||||||
major = ticks[CONF_MAJOR]
|
major = ticks[CONF_MAJOR]
|
||||||
color = await lv_color.process(major[CONF_COLOR])
|
|
||||||
lv.meter_set_scale_major_ticks(
|
lv.meter_set_scale_major_ticks(
|
||||||
var,
|
var,
|
||||||
meter_var,
|
meter_var,
|
||||||
major[CONF_STRIDE],
|
major[CONF_STRIDE],
|
||||||
major[CONF_WIDTH],
|
await size.process(major[CONF_WIDTH]),
|
||||||
major[CONF_LENGTH],
|
await size.process(major[CONF_LENGTH]),
|
||||||
color,
|
await lv_color.process(major[CONF_COLOR]),
|
||||||
major[CONF_LABEL_GAP],
|
await size.process(major[CONF_LABEL_GAP]),
|
||||||
)
|
)
|
||||||
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
||||||
(t, v) = next(iter(indicator.items()))
|
(t, v) = next(iter(indicator.items()))
|
||||||
@ -233,7 +232,11 @@ class MeterType(WidgetType):
|
|||||||
lv_assign(
|
lv_assign(
|
||||||
ivar,
|
ivar,
|
||||||
lv_expr.meter_add_needle_line(
|
lv_expr.meter_add_needle_line(
|
||||||
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
var,
|
||||||
|
meter_var,
|
||||||
|
await size.process(v[CONF_WIDTH]),
|
||||||
|
color,
|
||||||
|
await size.process(v[CONF_R_MOD]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_ARC:
|
if t == CONF_ARC:
|
||||||
@ -241,7 +244,11 @@ class MeterType(WidgetType):
|
|||||||
lv_assign(
|
lv_assign(
|
||||||
ivar,
|
ivar,
|
||||||
lv_expr.meter_add_arc(
|
lv_expr.meter_add_arc(
|
||||||
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
var,
|
||||||
|
meter_var,
|
||||||
|
await size.process(v[CONF_WIDTH]),
|
||||||
|
color,
|
||||||
|
await size.process(v[CONF_R_MOD]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_TICK_STYLE:
|
if t == CONF_TICK_STYLE:
|
||||||
@ -257,7 +264,7 @@ class MeterType(WidgetType):
|
|||||||
color_start,
|
color_start,
|
||||||
color_end,
|
color_end,
|
||||||
v[CONF_LOCAL],
|
v[CONF_LOCAL],
|
||||||
v[CONF_WIDTH],
|
size.process(v[CONF_WIDTH]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_IMAGE:
|
if t == CONF_IMAGE:
|
||||||
|
@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"]
|
|||||||
|
|
||||||
DOMAIN = "mipi_spi"
|
DOMAIN = "mipi_spi"
|
||||||
|
|
||||||
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
|
|
||||||
CONF_SPI_16 = "spi_16"
|
CONF_SPI_16 = "spi_16"
|
||||||
CONF_PIXEL_MODE = "pixel_mode"
|
CONF_PIXEL_MODE = "pixel_mode"
|
||||||
CONF_COLOR_DEPTH = "color_depth"
|
|
||||||
CONF_BUS_MODE = "bus_mode"
|
CONF_BUS_MODE = "bus_mode"
|
||||||
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
||||||
CONF_NATIVE_WIDTH = "native_width"
|
CONF_NATIVE_WIDTH = "native_width"
|
||||||
|
@ -3,11 +3,18 @@ import logging
|
|||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import display, spi
|
from esphome.components import display, spi
|
||||||
|
from esphome.components.const import (
|
||||||
|
CONF_BYTE_ORDER,
|
||||||
|
CONF_COLOR_DEPTH,
|
||||||
|
CONF_DRAW_ROUNDING,
|
||||||
|
)
|
||||||
|
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
|
||||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.config_validation import ALLOW_EXTRA
|
from esphome.config_validation import ALLOW_EXTRA
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BRIGHTNESS,
|
CONF_BRIGHTNESS,
|
||||||
|
CONF_BUFFER_SIZE,
|
||||||
CONF_COLOR_ORDER,
|
CONF_COLOR_ORDER,
|
||||||
CONF_CS_PIN,
|
CONF_CS_PIN,
|
||||||
CONF_DATA_RATE,
|
CONF_DATA_RATE,
|
||||||
@ -24,19 +31,19 @@ from esphome.const import (
|
|||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_OFFSET_HEIGHT,
|
CONF_OFFSET_HEIGHT,
|
||||||
CONF_OFFSET_WIDTH,
|
CONF_OFFSET_WIDTH,
|
||||||
|
CONF_PAGES,
|
||||||
CONF_RESET_PIN,
|
CONF_RESET_PIN,
|
||||||
CONF_ROTATION,
|
CONF_ROTATION,
|
||||||
CONF_SWAP_XY,
|
CONF_SWAP_XY,
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
)
|
)
|
||||||
from esphome.core import TimePeriod
|
from esphome.core import CORE, TimePeriod
|
||||||
|
from esphome.cpp_generator import TemplateArguments
|
||||||
|
from esphome.final_validate import full_config
|
||||||
|
|
||||||
from ..const import CONF_DRAW_ROUNDING
|
|
||||||
from ..lvgl.defines import CONF_COLOR_DEPTH
|
|
||||||
from . import (
|
from . import (
|
||||||
CONF_BUS_MODE,
|
CONF_BUS_MODE,
|
||||||
CONF_DRAW_FROM_ORIGIN,
|
|
||||||
CONF_NATIVE_HEIGHT,
|
CONF_NATIVE_HEIGHT,
|
||||||
CONF_NATIVE_WIDTH,
|
CONF_NATIVE_WIDTH,
|
||||||
CONF_PIXEL_MODE,
|
CONF_PIXEL_MODE,
|
||||||
@ -55,6 +62,7 @@ from .models import (
|
|||||||
MADCTL_XFLIP,
|
MADCTL_XFLIP,
|
||||||
MADCTL_YFLIP,
|
MADCTL_YFLIP,
|
||||||
DriverChip,
|
DriverChip,
|
||||||
|
adafruit,
|
||||||
amoled,
|
amoled,
|
||||||
cyd,
|
cyd,
|
||||||
ili,
|
ili,
|
||||||
@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"]
|
|||||||
|
|
||||||
LOGGER = logging.getLogger(DOMAIN)
|
LOGGER = logging.getLogger(DOMAIN)
|
||||||
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
|
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
|
||||||
MipiSpi = mipi_spi_ns.class_(
|
MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice)
|
||||||
"MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice
|
MipiSpiBuffer = mipi_spi_ns.class_(
|
||||||
|
"MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice
|
||||||
)
|
)
|
||||||
ColorOrder = display.display_ns.enum("ColorMode")
|
ColorOrder = display.display_ns.enum("ColorMode")
|
||||||
ColorBitness = display.display_ns.enum("ColorBitness")
|
ColorBitness = display.display_ns.enum("ColorBitness")
|
||||||
Model = mipi_spi_ns.enum("Model")
|
Model = mipi_spi_ns.enum("Model")
|
||||||
|
|
||||||
|
PixelMode = mipi_spi_ns.enum("PixelMode")
|
||||||
|
BusType = mipi_spi_ns.enum("BusType")
|
||||||
|
|
||||||
COLOR_ORDERS = {
|
COLOR_ORDERS = {
|
||||||
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
|
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
|
||||||
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
|
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
|
||||||
}
|
}
|
||||||
|
|
||||||
COLOR_DEPTHS = {
|
COLOR_DEPTHS = {
|
||||||
8: ColorBitness.COLOR_BITNESS_332,
|
8: PixelMode.PIXEL_MODE_8,
|
||||||
16: ColorBitness.COLOR_BITNESS_565,
|
16: PixelMode.PIXEL_MODE_16,
|
||||||
|
18: PixelMode.PIXEL_MODE_18,
|
||||||
}
|
}
|
||||||
|
|
||||||
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
|
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
|
||||||
|
|
||||||
|
BusTypes = {
|
||||||
|
TYPE_SINGLE: BusType.BUS_TYPE_SINGLE,
|
||||||
|
TYPE_QUAD: BusType.BUS_TYPE_QUAD,
|
||||||
|
TYPE_OCTAL: BusType.BUS_TYPE_OCTAL,
|
||||||
|
}
|
||||||
|
|
||||||
DriverChip("CUSTOM", initsequence={})
|
DriverChip("CUSTOM")
|
||||||
|
|
||||||
MODELS = DriverChip.models
|
MODELS = DriverChip.models
|
||||||
# These statements are noops, but serve to suppress linting of side-effect-only imports
|
# This loop is a noop, but suppresses linting of side-effect-only imports
|
||||||
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare):
|
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
PixelMode = mipi_spi_ns.enum("PixelMode")
|
|
||||||
|
|
||||||
PIXEL_MODE_18BIT = "18bit"
|
DISPLAY_18BIT = "18bit"
|
||||||
PIXEL_MODE_16BIT = "16bit"
|
DISPLAY_16BIT = "16bit"
|
||||||
|
|
||||||
PIXEL_MODES = {
|
DISPLAY_PIXEL_MODES = {
|
||||||
PIXEL_MODE_16BIT: 0x55,
|
DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16),
|
||||||
PIXEL_MODE_18BIT: 0x66,
|
DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_dimensions(config):
|
||||||
|
if CONF_DIMENSIONS in config:
|
||||||
|
# Explicit dimensions, just use as is
|
||||||
|
dimensions = config[CONF_DIMENSIONS]
|
||||||
|
if isinstance(dimensions, dict):
|
||||||
|
width = dimensions[CONF_WIDTH]
|
||||||
|
height = dimensions[CONF_HEIGHT]
|
||||||
|
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
||||||
|
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
||||||
|
return width, height, offset_width, offset_height
|
||||||
|
(width, height) = dimensions
|
||||||
|
return width, height, 0, 0
|
||||||
|
|
||||||
|
# Default dimensions, use model defaults
|
||||||
|
transform = get_transform(config)
|
||||||
|
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
width = model.get_default(CONF_WIDTH)
|
||||||
|
height = model.get_default(CONF_HEIGHT)
|
||||||
|
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
|
||||||
|
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
|
||||||
|
|
||||||
|
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
||||||
|
# the offset is asymmetric
|
||||||
|
if transform[CONF_MIRROR_X]:
|
||||||
|
native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
|
||||||
|
offset_width = native_width - width - offset_width
|
||||||
|
if transform[CONF_MIRROR_Y]:
|
||||||
|
native_height = model.get_default(
|
||||||
|
CONF_NATIVE_HEIGHT, height + offset_height * 2
|
||||||
|
)
|
||||||
|
offset_height = native_height - height - offset_height
|
||||||
|
# Swap default dimensions if swap_xy is set
|
||||||
|
if transform[CONF_SWAP_XY] is True:
|
||||||
|
width, height = height, width
|
||||||
|
offset_height, offset_width = offset_width, offset_height
|
||||||
|
return width, height, offset_width, offset_height
|
||||||
|
|
||||||
|
|
||||||
|
def denominator(config):
|
||||||
|
"""
|
||||||
|
Calculate the best denominator for a buffer size fraction.
|
||||||
|
The denominator must be a number between 2 and 16 that divides the display height evenly,
|
||||||
|
and the fraction represented by the denominator must be less than or equal to the given fraction.
|
||||||
|
:config: The configuration dictionary containing the buffer size fraction and display dimensions
|
||||||
|
:return: The denominator to use for the buffer size fraction
|
||||||
|
"""
|
||||||
|
frac = config.get(CONF_BUFFER_SIZE)
|
||||||
|
if frac is None or frac > 0.75:
|
||||||
|
return 1
|
||||||
|
height, _width, _offset_width, _offset_height = get_dimensions(config)
|
||||||
|
try:
|
||||||
|
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
|
||||||
|
except StopIteration:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Buffer size fraction {frac} is not compatible with display height {height}"
|
||||||
|
) from StopIteration
|
||||||
|
|
||||||
|
|
||||||
def validate_dimension(rounding):
|
def validate_dimension(rounding):
|
||||||
def validator(value):
|
def validator(value):
|
||||||
value = cv.positive_int(value)
|
value = cv.positive_int(value)
|
||||||
@ -158,25 +235,27 @@ def dimension_schema(rounding):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
def swap_xy_schema(model):
|
||||||
|
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
|
||||||
|
|
||||||
|
def validator(value):
|
||||||
|
if value:
|
||||||
|
raise cv.Invalid("Axis swapping not supported by this model")
|
||||||
|
return cv.boolean(value)
|
||||||
|
|
||||||
|
if uses_swap:
|
||||||
|
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
||||||
|
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
||||||
|
|
||||||
|
|
||||||
|
def model_schema(config):
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
|
||||||
transform = cv.Schema(
|
transform = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||||
}
|
**swap_xy_schema(model),
|
||||||
)
|
|
||||||
if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED:
|
|
||||||
transform = transform.extend(
|
|
||||||
{
|
|
||||||
cv.Optional(CONF_SWAP_XY): cv.invalid(
|
|
||||||
"Axis swapping not supported by this model"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
transform = transform.extend(
|
|
||||||
{
|
|
||||||
cv.Required(CONF_SWAP_XY): cv.boolean,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# CUSTOM model will need to provide a custom init sequence
|
# CUSTOM model will need to provide a custom init sequence
|
||||||
@ -185,14 +264,21 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
|||||||
if model.initsequence is None
|
if model.initsequence is None
|
||||||
else cv.Optional(CONF_INIT_SEQUENCE)
|
else cv.Optional(CONF_INIT_SEQUENCE)
|
||||||
)
|
)
|
||||||
# Dimensions are optional if the model has a default width and the transform is not overridden
|
# Dimensions are optional if the model has a default width and the x-y transform is not overridden
|
||||||
|
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
|
||||||
cv_dimensions = (
|
cv_dimensions = (
|
||||||
cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required
|
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
|
||||||
)
|
)
|
||||||
pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,)
|
pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,)
|
||||||
color_depth = (
|
color_depth = (
|
||||||
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
|
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
|
||||||
)
|
)
|
||||||
|
other_options = [
|
||||||
|
CONF_INVERT_COLORS,
|
||||||
|
CONF_USE_AXIS_FLIPS,
|
||||||
|
]
|
||||||
|
if bus_mode == TYPE_SINGLE:
|
||||||
|
other_options.append(CONF_SPI_16)
|
||||||
schema = (
|
schema = (
|
||||||
display.FULL_DISPLAY_SCHEMA.extend(
|
display.FULL_DISPLAY_SCHEMA.extend(
|
||||||
spi.spi_device_schema(
|
spi.spi_device_schema(
|
||||||
@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
|||||||
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
|
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
|
||||||
COLOR_ORDERS, upper=True
|
COLOR_ORDERS, upper=True
|
||||||
),
|
),
|
||||||
|
model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of(
|
||||||
|
"big_endian", "little_endian", lower=True
|
||||||
|
),
|
||||||
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
||||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||||
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any(
|
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
|
||||||
cv.one_of(*pixel_modes, lower=True),
|
*pixel_modes, lower=True
|
||||||
cv.int_range(0, 255, min_included=True, max_included=True),
|
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_TRANSFORM): transform,
|
cv.Optional(CONF_TRANSFORM): transform,
|
||||||
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
|
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
|
||||||
@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
|||||||
),
|
),
|
||||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||||
iseqconf: cv.ensure_list(map_sequence),
|
iseqconf: cv.ensure_list(map_sequence),
|
||||||
|
cv.Optional(CONF_BUFFER_SIZE): cv.All(
|
||||||
|
cv.percentage, cv.Range(0.12, 1.0)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(
|
.extend({model.option(x): cv.boolean for x in other_options})
|
||||||
{
|
|
||||||
model.option(x): cv.boolean
|
|
||||||
for x in [
|
|
||||||
CONF_DRAW_FROM_ORIGIN,
|
|
||||||
CONF_SPI_16,
|
|
||||||
CONF_INVERT_COLORS,
|
|
||||||
CONF_USE_AXIS_FLIPS,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if brightness := model.get_default(CONF_BRIGHTNESS):
|
if brightness := model.get_default(CONF_BRIGHTNESS):
|
||||||
schema = schema.extend(
|
schema = schema.extend(
|
||||||
@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
|||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
def rotation_as_transform(model, config):
|
def is_rotation_transformable(config):
|
||||||
"""
|
"""
|
||||||
Check if a rotation can be implemented in hardware using the MADCTL register.
|
Check if a rotation can be implemented in hardware using the MADCTL register.
|
||||||
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
|
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
|
||||||
"""
|
"""
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
rotation = config.get(CONF_ROTATION, 0)
|
rotation = config.get(CONF_ROTATION, 0)
|
||||||
return rotation and (
|
return rotation and (
|
||||||
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
|
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def config_schema(config):
|
def customise_schema(config):
|
||||||
|
"""
|
||||||
|
Create a customised config schema for a specific model and validate the configuration.
|
||||||
|
:param config: The configuration dictionary to validate
|
||||||
|
:return: The validated configuration dictionary
|
||||||
|
:raises cv.Invalid: If the configuration is invalid
|
||||||
|
"""
|
||||||
# First get the model and bus mode
|
# First get the model and bus mode
|
||||||
config = cv.Schema(
|
config = cv.Schema(
|
||||||
{
|
{
|
||||||
@ -288,29 +376,94 @@ def config_schema(config):
|
|||||||
extra=ALLOW_EXTRA,
|
extra=ALLOW_EXTRA,
|
||||||
)(config)
|
)(config)
|
||||||
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
|
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
|
||||||
swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
|
config = model_schema(config)(config)
|
||||||
config = model_schema(bus_mode, model, swapsies)(config)
|
|
||||||
# Check for invalid combinations of MADCTL config
|
# Check for invalid combinations of MADCTL config
|
||||||
if init_sequence := config.get(CONF_INIT_SEQUENCE):
|
if init_sequence := config.get(CONF_INIT_SEQUENCE):
|
||||||
if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config:
|
commands = [x[0] for x in init_sequence]
|
||||||
|
if MADCTL in commands and CONF_TRANSFORM in config:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
|
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
|
||||||
)
|
)
|
||||||
|
if PIXFMT in commands:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically"
|
||||||
|
)
|
||||||
|
|
||||||
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
|
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
|
||||||
raise cv.Invalid("DC pin is not supported in quad mode")
|
raise cv.Invalid("DC pin is not supported in quad mode")
|
||||||
if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE:
|
|
||||||
raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus")
|
|
||||||
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
|
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
|
||||||
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
|
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
|
||||||
|
denominator(config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = config_schema
|
CONFIG_SCHEMA = customise_schema
|
||||||
|
|
||||||
|
|
||||||
def get_transform(model, config):
|
def requires_buffer(config):
|
||||||
can_transform = rotation_as_transform(model, config)
|
"""
|
||||||
|
Check if the display configuration requires a buffer. It will do so if any drawing methods are configured.
|
||||||
|
:param config:
|
||||||
|
:return: True if a buffer is required, False otherwise
|
||||||
|
"""
|
||||||
|
return any(
|
||||||
|
config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_color_depth(config):
|
||||||
|
return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||||
|
|
||||||
|
|
||||||
|
def _final_validate(config):
|
||||||
|
global_config = full_config.get()
|
||||||
|
|
||||||
|
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||||
|
|
||||||
|
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
|
||||||
|
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||||
|
config[CONF_SHOW_TEST_CARD] = True
|
||||||
|
|
||||||
|
if "psram" not in global_config and CONF_BUFFER_SIZE not in config:
|
||||||
|
if not requires_buffer(config):
|
||||||
|
return config # No buffer needed, so no need to set a buffer size
|
||||||
|
# If PSRAM is not enabled, choose a small buffer size by default
|
||||||
|
if not requires_buffer(config):
|
||||||
|
# not our problem.
|
||||||
|
return config
|
||||||
|
color_depth = get_color_depth(config)
|
||||||
|
frac = denominator(config)
|
||||||
|
height, width, _offset_width, _offset_height = get_dimensions(config)
|
||||||
|
|
||||||
|
buffer_size = color_depth // 8 * width * height // frac
|
||||||
|
# Target a buffer size of 20kB
|
||||||
|
fraction = 20000.0 / buffer_size
|
||||||
|
try:
|
||||||
|
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||||
|
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
|
||||||
|
# PSRAM will be needed.
|
||||||
|
if CORE.is_esp32:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"PSRAM is required for this display"
|
||||||
|
) from StopIteration
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
|
def get_transform(config):
|
||||||
|
"""
|
||||||
|
Get the transformation configuration for the display.
|
||||||
|
:param config:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
can_transform = is_rotation_transformable(config)
|
||||||
transform = config.get(
|
transform = config.get(
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
{
|
{
|
||||||
@ -350,16 +503,13 @@ def get_sequence(model, config):
|
|||||||
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
|
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
|
||||||
commands = [x[0] for x in sequence]
|
commands = [x[0] for x in sequence]
|
||||||
# Set pixel format if not already in the custom sequence
|
# Set pixel format if not already in the custom sequence
|
||||||
if PIXFMT not in commands:
|
pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]]
|
||||||
pixel_mode = config[CONF_PIXEL_MODE]
|
sequence.append((PIXFMT, pixel_mode[0]))
|
||||||
if not isinstance(pixel_mode, int):
|
|
||||||
pixel_mode = PIXEL_MODES[pixel_mode]
|
|
||||||
sequence.append((PIXFMT, pixel_mode))
|
|
||||||
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
|
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
|
||||||
use_flip = config[CONF_USE_AXIS_FLIPS]
|
use_flip = config[CONF_USE_AXIS_FLIPS]
|
||||||
if MADCTL not in commands:
|
if MADCTL not in commands:
|
||||||
madctl = 0
|
madctl = 0
|
||||||
transform = get_transform(model, config)
|
transform = get_transform(config)
|
||||||
if transform.get(CONF_TRANSFORM):
|
if transform.get(CONF_TRANSFORM):
|
||||||
LOGGER.info("Using hardware transform to implement rotation")
|
LOGGER.info("Using hardware transform to implement rotation")
|
||||||
if transform.get(CONF_MIRROR_X):
|
if transform.get(CONF_MIRROR_X):
|
||||||
@ -396,63 +546,62 @@ def get_sequence(model, config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance(config):
|
||||||
|
"""
|
||||||
|
Get the type of MipiSpi instance to create based on the configuration,
|
||||||
|
and the template arguments.
|
||||||
|
:param config:
|
||||||
|
:return: type, template arguments
|
||||||
|
"""
|
||||||
|
width, height, offset_width, offset_height = get_dimensions(config)
|
||||||
|
|
||||||
|
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||||
|
bufferpixels = COLOR_DEPTHS[color_depth]
|
||||||
|
|
||||||
|
display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1]
|
||||||
|
bus_type = config[CONF_BUS_MODE]
|
||||||
|
if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False):
|
||||||
|
# If the bus mode is single and spi_16 is set, use single 16-bit mode
|
||||||
|
bus_type = BusType.BUS_TYPE_SINGLE_16
|
||||||
|
else:
|
||||||
|
bus_type = BusTypes[bus_type]
|
||||||
|
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
|
||||||
|
frac = denominator(config)
|
||||||
|
rotation = DISPLAY_ROTATIONS[
|
||||||
|
0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0)
|
||||||
|
]
|
||||||
|
templateargs = [
|
||||||
|
buffer_type,
|
||||||
|
bufferpixels,
|
||||||
|
config[CONF_BYTE_ORDER] == "big_endian",
|
||||||
|
display_pixel_mode,
|
||||||
|
bus_type,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
offset_width,
|
||||||
|
offset_height,
|
||||||
|
]
|
||||||
|
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
|
||||||
|
if requires_buffer(config):
|
||||||
|
templateargs.append(rotation)
|
||||||
|
templateargs.append(frac)
|
||||||
|
return MipiSpiBuffer, templateargs
|
||||||
|
return MipiSpi, templateargs
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
transform = get_transform(model, config)
|
var_id = config[CONF_ID]
|
||||||
if CONF_DIMENSIONS in config:
|
var_id.type, templateargs = get_instance(config)
|
||||||
# Explicit dimensions, just use as is
|
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
|
||||||
dimensions = config[CONF_DIMENSIONS]
|
|
||||||
if isinstance(dimensions, dict):
|
|
||||||
width = dimensions[CONF_WIDTH]
|
|
||||||
height = dimensions[CONF_HEIGHT]
|
|
||||||
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
|
||||||
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
|
||||||
else:
|
|
||||||
(width, height) = dimensions
|
|
||||||
offset_width = 0
|
|
||||||
offset_height = 0
|
|
||||||
else:
|
|
||||||
# Default dimensions, use model defaults and transform if needed
|
|
||||||
width = model.get_default(CONF_WIDTH)
|
|
||||||
height = model.get_default(CONF_HEIGHT)
|
|
||||||
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
|
|
||||||
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
|
|
||||||
|
|
||||||
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
|
||||||
# the offset is asymmetric
|
|
||||||
if transform[CONF_MIRROR_X]:
|
|
||||||
native_width = model.get_default(
|
|
||||||
CONF_NATIVE_WIDTH, width + offset_width * 2
|
|
||||||
)
|
|
||||||
offset_width = native_width - width - offset_width
|
|
||||||
if transform[CONF_MIRROR_Y]:
|
|
||||||
native_height = model.get_default(
|
|
||||||
CONF_NATIVE_HEIGHT, height + offset_height * 2
|
|
||||||
)
|
|
||||||
offset_height = native_height - height - offset_height
|
|
||||||
# Swap default dimensions if swap_xy is set
|
|
||||||
if transform[CONF_SWAP_XY] is True:
|
|
||||||
width, height = height, width
|
|
||||||
offset_height, offset_width = offset_width, offset_height
|
|
||||||
|
|
||||||
color_depth = config[CONF_COLOR_DEPTH]
|
|
||||||
if color_depth.endswith("bit"):
|
|
||||||
color_depth = color_depth[:-3]
|
|
||||||
color_depth = COLOR_DEPTHS[int(color_depth)]
|
|
||||||
|
|
||||||
var = cg.new_Pvariable(
|
|
||||||
config[CONF_ID], width, height, offset_width, offset_height, color_depth
|
|
||||||
)
|
|
||||||
cg.add(var.set_init_sequence(get_sequence(model, config)))
|
cg.add(var.set_init_sequence(get_sequence(model, config)))
|
||||||
if rotation_as_transform(model, config):
|
if is_rotation_transformable(config):
|
||||||
if CONF_TRANSFORM in config:
|
if CONF_TRANSFORM in config:
|
||||||
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
|
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
|
||||||
else:
|
else:
|
||||||
config[CONF_ROTATION] = 0
|
config[CONF_ROTATION] = 0
|
||||||
cg.add(var.set_model(config[CONF_MODEL]))
|
cg.add(var.set_model(config[CONF_MODEL]))
|
||||||
cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN]))
|
|
||||||
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
|
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
|
||||||
cg.add(var.set_spi_16(config[CONF_SPI_16]))
|
|
||||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||||
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
||||||
cg.add(var.set_enable_pins(enable))
|
cg.add(var.set_enable_pins(enable))
|
||||||
@ -472,4 +621,5 @@ async def to_code(config):
|
|||||||
cg.add(var.set_writer(lambda_))
|
cg.add(var.set_writer(lambda_))
|
||||||
await display.register_display(var, config)
|
await display.register_display(var, config)
|
||||||
await spi.register_spi_device(var, config)
|
await spi.register_spi_device(var, config)
|
||||||
|
# Displays are write-only, set the SPI device to write-only as well
|
||||||
cg.add(var.set_write_only(True))
|
cg.add(var.set_write_only(True))
|
||||||
|
@ -2,489 +2,5 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mipi_spi {
|
namespace mipi_spi {} // namespace mipi_spi
|
||||||
|
|
||||||
void MipiSpi::setup() {
|
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
this->spi_setup();
|
|
||||||
if (this->dc_pin_ != nullptr) {
|
|
||||||
this->dc_pin_->setup();
|
|
||||||
this->dc_pin_->digital_write(false);
|
|
||||||
}
|
|
||||||
for (auto *pin : this->enable_pins_) {
|
|
||||||
pin->setup();
|
|
||||||
pin->digital_write(true);
|
|
||||||
}
|
|
||||||
if (this->reset_pin_ != nullptr) {
|
|
||||||
this->reset_pin_->setup();
|
|
||||||
this->reset_pin_->digital_write(true);
|
|
||||||
delay(5);
|
|
||||||
this->reset_pin_->digital_write(false);
|
|
||||||
delay(5);
|
|
||||||
this->reset_pin_->digital_write(true);
|
|
||||||
}
|
|
||||||
this->bus_width_ = this->parent_->get_bus_width();
|
|
||||||
|
|
||||||
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
|
||||||
auto when = millis() + 120;
|
|
||||||
delay(10);
|
|
||||||
size_t index = 0;
|
|
||||||
auto &vec = this->init_sequence_;
|
|
||||||
while (index != vec.size()) {
|
|
||||||
if (vec.size() - index < 2) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint8_t cmd = vec[index++];
|
|
||||||
uint8_t x = vec[index++];
|
|
||||||
if (x == DELAY_FLAG) {
|
|
||||||
ESP_LOGD(TAG, "Delay %dms", cmd);
|
|
||||||
delay(cmd);
|
|
||||||
} else {
|
|
||||||
uint8_t num_args = x & 0x7F;
|
|
||||||
if (vec.size() - index < num_args) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto arg_byte = vec[index];
|
|
||||||
switch (cmd) {
|
|
||||||
case SLEEP_OUT: {
|
|
||||||
// are we ready, boots?
|
|
||||||
int duration = when - millis();
|
|
||||||
if (duration > 0) {
|
|
||||||
ESP_LOGD(TAG, "Sleep %dms", duration);
|
|
||||||
delay(duration);
|
|
||||||
}
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case INVERT_ON:
|
|
||||||
this->invert_colors_ = true;
|
|
||||||
break;
|
|
||||||
case MADCTL_CMD:
|
|
||||||
this->madctl_ = arg_byte;
|
|
||||||
break;
|
|
||||||
case PIXFMT:
|
|
||||||
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
|
|
||||||
break;
|
|
||||||
case BRIGHTNESS:
|
|
||||||
this->brightness_ = arg_byte;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const auto *ptr = vec.data() + index;
|
|
||||||
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
|
|
||||||
this->write_command_(cmd, ptr, num_args);
|
|
||||||
index += num_args;
|
|
||||||
if (cmd == SLEEP_OUT)
|
|
||||||
delay(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->setup_complete_ = true;
|
|
||||||
if (this->draw_from_origin_)
|
|
||||||
check_buffer_();
|
|
||||||
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::update() {
|
|
||||||
if (!this->setup_complete_ || this->is_failed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this->do_update_();
|
|
||||||
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
|
|
||||||
return;
|
|
||||||
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
|
|
||||||
// Some chips require that the drawing window be aligned on certain boundaries
|
|
||||||
auto dr = this->draw_rounding_;
|
|
||||||
this->x_low_ = this->x_low_ / dr * dr;
|
|
||||||
this->y_low_ = this->y_low_ / dr * dr;
|
|
||||||
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
|
|
||||||
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
|
|
||||||
if (this->draw_from_origin_) {
|
|
||||||
this->x_low_ = 0;
|
|
||||||
this->y_low_ = 0;
|
|
||||||
this->x_high_ = this->width_ - 1;
|
|
||||||
}
|
|
||||||
int w = this->x_high_ - this->x_low_ + 1;
|
|
||||||
int h = this->y_high_ - this->y_low_ + 1;
|
|
||||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
|
|
||||||
this->width_ - w - this->x_low_);
|
|
||||||
// invalidate watermarks
|
|
||||||
this->x_low_ = this->width_;
|
|
||||||
this->y_low_ = this->height_;
|
|
||||||
this->x_high_ = 0;
|
|
||||||
this->y_high_ = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::fill(Color color) {
|
|
||||||
if (!this->check_buffer_())
|
|
||||||
return;
|
|
||||||
this->x_low_ = 0;
|
|
||||||
this->y_low_ = 0;
|
|
||||||
this->x_high_ = this->get_width_internal() - 1;
|
|
||||||
this->y_high_ = this->get_height_internal() - 1;
|
|
||||||
switch (this->color_depth_) {
|
|
||||||
case display::COLOR_BITNESS_332: {
|
|
||||||
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
|
|
||||||
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
auto new_color = display::ColorUtil::color_to_565(color);
|
|
||||||
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
|
|
||||||
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
|
|
||||||
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
|
|
||||||
} else {
|
|
||||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
|
||||||
auto len = this->buffer_bytes_ / 2;
|
|
||||||
while (len--) {
|
|
||||||
*ptr_16++ = new_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) {
|
|
||||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this->check_buffer_())
|
|
||||||
return;
|
|
||||||
size_t pos = (y * this->width_) + x;
|
|
||||||
switch (this->color_depth_) {
|
|
||||||
case display::COLOR_BITNESS_332: {
|
|
||||||
uint8_t new_color = display::ColorUtil::color_to_332(color);
|
|
||||||
if (this->buffer_[pos] == new_color)
|
|
||||||
return;
|
|
||||||
this->buffer_[pos] = new_color;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case display::COLOR_BITNESS_565: {
|
|
||||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
|
||||||
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
|
|
||||||
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
|
|
||||||
uint16_t new_color = hi_byte | (lo_byte << 8); // big endian
|
|
||||||
if (ptr_16[pos] == new_color)
|
|
||||||
return;
|
|
||||||
ptr_16[pos] = new_color;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// low and high watermark may speed up drawing from buffer
|
|
||||||
if (x < this->x_low_)
|
|
||||||
this->x_low_ = x;
|
|
||||||
if (y < this->y_low_)
|
|
||||||
this->y_low_ = y;
|
|
||||||
if (x > this->x_high_)
|
|
||||||
this->x_high_ = x;
|
|
||||||
if (y > this->y_high_)
|
|
||||||
this->y_high_ = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::reset_params_() {
|
|
||||||
if (!this->is_ready())
|
|
||||||
return;
|
|
||||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
|
||||||
if (this->brightness_.has_value())
|
|
||||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_init_sequence_() {
|
|
||||||
size_t index = 0;
|
|
||||||
auto &vec = this->init_sequence_;
|
|
||||||
while (index != vec.size()) {
|
|
||||||
if (vec.size() - index < 2) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint8_t cmd = vec[index++];
|
|
||||||
uint8_t x = vec[index++];
|
|
||||||
if (x == DELAY_FLAG) {
|
|
||||||
ESP_LOGV(TAG, "Delay %dms", cmd);
|
|
||||||
delay(cmd);
|
|
||||||
} else {
|
|
||||||
uint8_t num_args = x & 0x7F;
|
|
||||||
if (vec.size() - index < num_args) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto *ptr = vec.data() + index;
|
|
||||||
this->write_command_(cmd, ptr, num_args);
|
|
||||||
index += num_args;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->setup_complete_ = true;
|
|
||||||
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
|
||||||
ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
|
||||||
uint8_t buf[4];
|
|
||||||
x1 += this->offset_width_;
|
|
||||||
x2 += this->offset_width_;
|
|
||||||
y1 += this->offset_height_;
|
|
||||||
y2 += this->offset_height_;
|
|
||||||
put16_be(buf, y1);
|
|
||||||
put16_be(buf + 2, y2);
|
|
||||||
this->write_command_(RASET, buf, sizeof buf);
|
|
||||||
put16_be(buf, x1);
|
|
||||||
put16_be(buf + 2, x2);
|
|
||||||
this->write_command_(CASET, buf, sizeof buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
|
||||||
if (!this->setup_complete_ || this->is_failed())
|
|
||||||
return;
|
|
||||||
if (w <= 0 || h <= 0)
|
|
||||||
return;
|
|
||||||
if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
|
|
||||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this->draw_from_origin_) {
|
|
||||||
auto stride = x_offset + w + x_pad;
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2,
|
|
||||||
ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2);
|
|
||||||
}
|
|
||||||
ptr = this->buffer_;
|
|
||||||
w = this->width_;
|
|
||||||
h += y_start;
|
|
||||||
x_start = 0;
|
|
||||||
y_start = 0;
|
|
||||||
x_offset = 0;
|
|
||||||
y_offset = 0;
|
|
||||||
}
|
|
||||||
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) {
|
|
||||||
stride -= w;
|
|
||||||
uint8_t transfer_buffer[6 * 256];
|
|
||||||
size_t idx = 0; // index into transfer_buffer
|
|
||||||
while (h-- != 0) {
|
|
||||||
for (auto x = w; x-- != 0;) {
|
|
||||||
auto color_val = *ptr++;
|
|
||||||
// deal with byte swapping
|
|
||||||
transfer_buffer[idx++] = (color_val & 0xF8); // Blue
|
|
||||||
transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green
|
|
||||||
transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red
|
|
||||||
if (idx == sizeof(transfer_buffer)) {
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
idx = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptr += stride;
|
|
||||||
}
|
|
||||||
if (idx != 0)
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
|
|
||||||
stride -= w;
|
|
||||||
uint8_t transfer_buffer[6 * 256];
|
|
||||||
size_t idx = 0; // index into transfer_buffer
|
|
||||||
while (h-- != 0) {
|
|
||||||
for (auto x = w; x-- != 0;) {
|
|
||||||
auto color_val = *ptr++;
|
|
||||||
transfer_buffer[idx++] = color_val & 0xE0; // Red
|
|
||||||
transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green
|
|
||||||
transfer_buffer[idx++] = color_val << 6; // Blue
|
|
||||||
if (idx == sizeof(transfer_buffer)) {
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
idx = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptr += stride;
|
|
||||||
}
|
|
||||||
if (idx != 0)
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
|
|
||||||
stride -= w;
|
|
||||||
uint8_t transfer_buffer[6 * 256];
|
|
||||||
size_t idx = 0; // index into transfer_buffer
|
|
||||||
while (h-- != 0) {
|
|
||||||
for (auto x = w; x-- != 0;) {
|
|
||||||
auto color_val = *ptr++;
|
|
||||||
transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
|
|
||||||
transfer_buffer[idx++] = (color_val & 0x3) << 3;
|
|
||||||
if (idx == sizeof(transfer_buffer)) {
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
idx = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptr += stride;
|
|
||||||
}
|
|
||||||
if (idx != 0)
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
|
||||||
int x_pad) {
|
|
||||||
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
|
|
||||||
auto stride = x_offset + w + x_pad;
|
|
||||||
const auto *offset_ptr = ptr;
|
|
||||||
if (this->color_depth_ == display::COLOR_BITNESS_332) {
|
|
||||||
offset_ptr += y_offset * stride + x_offset;
|
|
||||||
} else {
|
|
||||||
stride *= 2;
|
|
||||||
offset_ptr += y_offset * stride + x_offset * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (this->bus_width_) {
|
|
||||||
case 4:
|
|
||||||
this->enable();
|
|
||||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
|
||||||
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't
|
|
||||||
// bother
|
|
||||||
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4);
|
|
||||||
} else {
|
|
||||||
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4);
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4);
|
|
||||||
offset_ptr += stride;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 8:
|
|
||||||
this->write_command_(WDATA);
|
|
||||||
this->enable();
|
|
||||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8);
|
|
||||||
} else {
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8);
|
|
||||||
offset_ptr += stride;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this->write_command_(WDATA);
|
|
||||||
this->enable();
|
|
||||||
|
|
||||||
if (this->color_depth_ == display::COLOR_BITNESS_565) {
|
|
||||||
// Source buffer is 16-bit RGB565
|
|
||||||
if (this->pixel_mode_ == PIXEL_MODE_18) {
|
|
||||||
// Convert RGB565 to RGB666
|
|
||||||
this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2);
|
|
||||||
} else {
|
|
||||||
// Direct RGB565 output
|
|
||||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
|
||||||
this->write_array(ptr, w * h * 2);
|
|
||||||
} else {
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
this->write_array(offset_ptr, w * 2);
|
|
||||||
offset_ptr += stride;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Source buffer is 8-bit RGB332
|
|
||||||
if (this->pixel_mode_ == PIXEL_MODE_18) {
|
|
||||||
// Convert RGB332 to RGB666
|
|
||||||
this->write_18_from_8_bit_(offset_ptr, w, h, stride);
|
|
||||||
} else {
|
|
||||||
this->write_16_from_8_bit_(offset_ptr, w, h, stride);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
|
||||||
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
|
|
||||||
if (this->bus_width_ == 4) {
|
|
||||||
this->enable();
|
|
||||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
|
||||||
this->disable();
|
|
||||||
} else if (this->bus_width_ == 8) {
|
|
||||||
this->dc_pin_->digital_write(false);
|
|
||||||
this->enable();
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
|
|
||||||
this->disable();
|
|
||||||
this->dc_pin_->digital_write(true);
|
|
||||||
if (len != 0) {
|
|
||||||
this->enable();
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->dc_pin_->digital_write(false);
|
|
||||||
this->enable();
|
|
||||||
this->write_byte(cmd);
|
|
||||||
this->disable();
|
|
||||||
this->dc_pin_->digital_write(true);
|
|
||||||
if (len != 0) {
|
|
||||||
if (this->spi_16_) {
|
|
||||||
for (size_t i = 0; i != len; i++) {
|
|
||||||
this->enable();
|
|
||||||
this->write_byte(0);
|
|
||||||
this->write_byte(bytes[i]);
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->enable();
|
|
||||||
this->write_array(bytes, len);
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::dump_config() {
|
|
||||||
ESP_LOGCONFIG(TAG,
|
|
||||||
"MIPI_SPI Display\n"
|
|
||||||
" Model: %s\n"
|
|
||||||
" Width: %u\n"
|
|
||||||
" Height: %u",
|
|
||||||
this->model_, this->width_, this->height_);
|
|
||||||
if (this->offset_width_ != 0)
|
|
||||||
ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_);
|
|
||||||
if (this->offset_height_ != 0)
|
|
||||||
ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_);
|
|
||||||
ESP_LOGCONFIG(TAG,
|
|
||||||
" Swap X/Y: %s\n"
|
|
||||||
" Mirror X: %s\n"
|
|
||||||
" Mirror Y: %s\n"
|
|
||||||
" Color depth: %d bits\n"
|
|
||||||
" Invert colors: %s\n"
|
|
||||||
" Color order: %s\n"
|
|
||||||
" Pixel mode: %s",
|
|
||||||
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
|
|
||||||
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)),
|
|
||||||
this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_),
|
|
||||||
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit");
|
|
||||||
if (this->brightness_.has_value())
|
|
||||||
ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value());
|
|
||||||
if (this->spi_16_)
|
|
||||||
ESP_LOGCONFIG(TAG, " SPI 16bit: YES");
|
|
||||||
ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_);
|
|
||||||
if (this->draw_from_origin_)
|
|
||||||
ESP_LOGCONFIG(TAG, " Draw from origin: YES");
|
|
||||||
LOG_PIN(" CS Pin: ", this->cs_);
|
|
||||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
|
||||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
|
||||||
ESP_LOGCONFIG(TAG,
|
|
||||||
" SPI Mode: %d\n"
|
|
||||||
" SPI Data rate: %dMHz\n"
|
|
||||||
" SPI Bus width: %d",
|
|
||||||
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), this->bus_width_);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace mipi_spi
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -4,40 +4,39 @@
|
|||||||
|
|
||||||
#include "esphome/components/spi/spi.h"
|
#include "esphome/components/spi/spi.h"
|
||||||
#include "esphome/components/display/display.h"
|
#include "esphome/components/display/display.h"
|
||||||
#include "esphome/components/display/display_buffer.h"
|
|
||||||
#include "esphome/components/display/display_color_utils.h"
|
#include "esphome/components/display/display_color_utils.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mipi_spi {
|
namespace mipi_spi {
|
||||||
|
|
||||||
constexpr static const char *const TAG = "display.mipi_spi";
|
constexpr static const char *const TAG = "display.mipi_spi";
|
||||||
static const uint8_t SW_RESET_CMD = 0x01;
|
static constexpr uint8_t SW_RESET_CMD = 0x01;
|
||||||
static const uint8_t SLEEP_OUT = 0x11;
|
static constexpr uint8_t SLEEP_OUT = 0x11;
|
||||||
static const uint8_t NORON = 0x13;
|
static constexpr uint8_t NORON = 0x13;
|
||||||
static const uint8_t INVERT_OFF = 0x20;
|
static constexpr uint8_t INVERT_OFF = 0x20;
|
||||||
static const uint8_t INVERT_ON = 0x21;
|
static constexpr uint8_t INVERT_ON = 0x21;
|
||||||
static const uint8_t ALL_ON = 0x23;
|
static constexpr uint8_t ALL_ON = 0x23;
|
||||||
static const uint8_t WRAM = 0x24;
|
static constexpr uint8_t WRAM = 0x24;
|
||||||
static const uint8_t MIPI = 0x26;
|
static constexpr uint8_t MIPI = 0x26;
|
||||||
static const uint8_t DISPLAY_ON = 0x29;
|
static constexpr uint8_t DISPLAY_ON = 0x29;
|
||||||
static const uint8_t RASET = 0x2B;
|
static constexpr uint8_t RASET = 0x2B;
|
||||||
static const uint8_t CASET = 0x2A;
|
static constexpr uint8_t CASET = 0x2A;
|
||||||
static const uint8_t WDATA = 0x2C;
|
static constexpr uint8_t WDATA = 0x2C;
|
||||||
static const uint8_t TEON = 0x35;
|
static constexpr uint8_t TEON = 0x35;
|
||||||
static const uint8_t MADCTL_CMD = 0x36;
|
static constexpr uint8_t MADCTL_CMD = 0x36;
|
||||||
static const uint8_t PIXFMT = 0x3A;
|
static constexpr uint8_t PIXFMT = 0x3A;
|
||||||
static const uint8_t BRIGHTNESS = 0x51;
|
static constexpr uint8_t BRIGHTNESS = 0x51;
|
||||||
static const uint8_t SWIRE1 = 0x5A;
|
static constexpr uint8_t SWIRE1 = 0x5A;
|
||||||
static const uint8_t SWIRE2 = 0x5B;
|
static constexpr uint8_t SWIRE2 = 0x5B;
|
||||||
static const uint8_t PAGESEL = 0xFE;
|
static constexpr uint8_t PAGESEL = 0xFE;
|
||||||
|
|
||||||
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||||
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
||||||
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
||||||
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
||||||
static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
|
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
|
||||||
static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||||
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||||
|
|
||||||
static const uint8_t DELAY_FLAG = 0xFF;
|
static const uint8_t DELAY_FLAG = 0xFF;
|
||||||
// store a 16 bit value in a buffer, big endian.
|
// store a 16 bit value in a buffer, big endian.
|
||||||
@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) {
|
|||||||
buf[1] = value;
|
buf[1] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffer mode, conveniently also the number of bytes in a pixel
|
||||||
enum PixelMode {
|
enum PixelMode {
|
||||||
PIXEL_MODE_16,
|
PIXEL_MODE_8 = 1,
|
||||||
PIXEL_MODE_18,
|
PIXEL_MODE_16 = 2,
|
||||||
|
PIXEL_MODE_18 = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
class MipiSpi : public display::DisplayBuffer,
|
enum BusType {
|
||||||
|
BUS_TYPE_SINGLE = 1,
|
||||||
|
BUS_TYPE_QUAD = 4,
|
||||||
|
BUS_TYPE_OCTAL = 8,
|
||||||
|
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for MIPI SPI displays.
|
||||||
|
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
|
||||||
|
*
|
||||||
|
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
|
||||||
|
* @tparam BUFFERPIXEL Color depth of the buffer
|
||||||
|
* @tparam DISPLAYPIXEL Color depth of the display
|
||||||
|
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
|
||||||
|
* @tparam WIDTH Width of the display in pixels
|
||||||
|
* @tparam HEIGHT Height of the display in pixels
|
||||||
|
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
||||||
|
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
||||||
|
* buffer
|
||||||
|
*/
|
||||||
|
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||||
|
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
|
||||||
|
class MipiSpi : public display::Display,
|
||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||||
spi::DATA_RATE_1MHZ> {
|
spi::DATA_RATE_1MHZ> {
|
||||||
public:
|
public:
|
||||||
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth)
|
MipiSpi() {}
|
||||||
: width_(width),
|
void update() override { this->stop_poller(); }
|
||||||
height_(height),
|
void draw_pixel_at(int x, int y, Color color) override {}
|
||||||
offset_width_(offset_width),
|
|
||||||
offset_height_(offset_height),
|
|
||||||
color_depth_(color_depth) {}
|
|
||||||
void set_model(const char *model) { this->model_ = model; }
|
void set_model(const char *model) { this->model_ = model; }
|
||||||
void update() override;
|
|
||||||
void setup() override;
|
|
||||||
display::ColorOrder get_color_mode() {
|
|
||||||
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||||
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
|
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
|
||||||
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
|
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
|
||||||
@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer,
|
|||||||
this->brightness_ = brightness;
|
this->brightness_ = brightness;
|
||||||
this->reset_params_();
|
this->reset_params_();
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
|
|
||||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||||
void dump_config() override;
|
|
||||||
|
|
||||||
int get_width_internal() override { return this->width_; }
|
int get_width_internal() override { return WIDTH; }
|
||||||
int get_height_internal() override { return this->height_; }
|
int get_height_internal() override { return HEIGHT; }
|
||||||
bool can_proceed() override { return this->setup_complete_; }
|
|
||||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||||
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
|
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
|
||||||
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
|
|
||||||
|
// reset the display, and write the init sequence
|
||||||
|
void setup() override {
|
||||||
|
this->spi_setup();
|
||||||
|
if (this->dc_pin_ != nullptr) {
|
||||||
|
this->dc_pin_->setup();
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
}
|
||||||
|
for (auto *pin : this->enable_pins_) {
|
||||||
|
pin->setup();
|
||||||
|
pin->digital_write(true);
|
||||||
|
}
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->reset_pin_->setup();
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
delay(5);
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
delay(5);
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
||||||
|
auto when = millis() + 120;
|
||||||
|
delay(10);
|
||||||
|
size_t index = 0;
|
||||||
|
auto &vec = this->init_sequence_;
|
||||||
|
while (index != vec.size()) {
|
||||||
|
if (vec.size() - index < 2) {
|
||||||
|
esph_log_e(TAG, "Malformed init sequence");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8_t cmd = vec[index++];
|
||||||
|
uint8_t x = vec[index++];
|
||||||
|
if (x == DELAY_FLAG) {
|
||||||
|
esph_log_d(TAG, "Delay %dms", cmd);
|
||||||
|
delay(cmd);
|
||||||
|
} else {
|
||||||
|
uint8_t num_args = x & 0x7F;
|
||||||
|
if (vec.size() - index < num_args) {
|
||||||
|
esph_log_e(TAG, "Malformed init sequence");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto arg_byte = vec[index];
|
||||||
|
switch (cmd) {
|
||||||
|
case SLEEP_OUT: {
|
||||||
|
// are we ready, boots?
|
||||||
|
int duration = when - millis();
|
||||||
|
if (duration > 0) {
|
||||||
|
esph_log_d(TAG, "Sleep %dms", duration);
|
||||||
|
delay(duration);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case INVERT_ON:
|
||||||
|
this->invert_colors_ = true;
|
||||||
|
break;
|
||||||
|
case MADCTL_CMD:
|
||||||
|
this->madctl_ = arg_byte;
|
||||||
|
break;
|
||||||
|
case BRIGHTNESS:
|
||||||
|
this->brightness_ = arg_byte;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto *ptr = vec.data() + index;
|
||||||
|
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
|
||||||
|
this->write_command_(cmd, ptr, num_args);
|
||||||
|
index += num_args;
|
||||||
|
if (cmd == SLEEP_OUT)
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// init sequence no longer needed
|
||||||
|
this->init_sequence_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawing operations
|
||||||
|
|
||||||
|
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||||
|
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
|
||||||
|
if (this->is_failed())
|
||||||
|
return;
|
||||||
|
if (w <= 0 || h <= 0)
|
||||||
|
return;
|
||||||
|
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
|
||||||
|
// note that the usual logging macros are banned in header files, so use their replacement
|
||||||
|
esph_log_e(TAG, "Unsupported color depth or bit order");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset,
|
||||||
|
x_pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_config() override {
|
||||||
|
esph_log_config(TAG,
|
||||||
|
"MIPI_SPI Display\n"
|
||||||
|
" Model: %s\n"
|
||||||
|
" Width: %u\n"
|
||||||
|
" Height: %u",
|
||||||
|
this->model_, WIDTH, HEIGHT);
|
||||||
|
if constexpr (OFFSET_WIDTH != 0)
|
||||||
|
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
|
||||||
|
if constexpr (OFFSET_HEIGHT != 0)
|
||||||
|
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
|
||||||
|
esph_log_config(TAG,
|
||||||
|
" Swap X/Y: %s\n"
|
||||||
|
" Mirror X: %s\n"
|
||||||
|
" Mirror Y: %s\n"
|
||||||
|
" Invert colors: %s\n"
|
||||||
|
" Color order: %s\n"
|
||||||
|
" Display pixels: %d bits\n"
|
||||||
|
" Endianness: %s\n",
|
||||||
|
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
|
||||||
|
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
|
||||||
|
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
|
||||||
|
if (this->brightness_.has_value())
|
||||||
|
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
|
||||||
|
if (this->cs_ != nullptr)
|
||||||
|
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
|
||||||
|
if (this->reset_pin_ != nullptr)
|
||||||
|
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
|
||||||
|
if (this->dc_pin_ != nullptr)
|
||||||
|
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
|
||||||
|
esph_log_config(TAG,
|
||||||
|
" SPI Mode: %d\n"
|
||||||
|
" SPI Data rate: %dMHz\n"
|
||||||
|
" SPI Bus width: %d",
|
||||||
|
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool check_buffer_() {
|
/* METHODS */
|
||||||
if (this->is_failed())
|
// convenience functions to write commands with or without data
|
||||||
return false;
|
|
||||||
if (this->buffer_ != nullptr)
|
|
||||||
return true;
|
|
||||||
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
|
|
||||||
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
|
|
||||||
if (this->buffer_ == nullptr) {
|
|
||||||
this->mark_failed();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
void fill(Color color) override;
|
|
||||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
|
||||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
|
||||||
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
|
|
||||||
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
|
|
||||||
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
|
|
||||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
|
||||||
int x_pad);
|
|
||||||
/**
|
|
||||||
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
|
|
||||||
* sample code.)
|
|
||||||
*
|
|
||||||
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
|
|
||||||
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
|
|
||||||
* sent in 1-dataline SPI. The second indicates quad mode.
|
|
||||||
* 1: 0x00
|
|
||||||
* 2: The command (register address) byte.
|
|
||||||
* 3: 0x00
|
|
||||||
*
|
|
||||||
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
|
|
||||||
* At the conclusion of the write, de-assert /CS.
|
|
||||||
*
|
|
||||||
* @param cmd
|
|
||||||
* @param bytes
|
|
||||||
* @param len
|
|
||||||
*/
|
|
||||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
|
|
||||||
|
|
||||||
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
|
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
|
||||||
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
|
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
|
||||||
void reset_params_();
|
|
||||||
void write_init_sequence_();
|
|
||||||
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
|
|
||||||
|
|
||||||
|
// Writes a command to the display, with the given bytes.
|
||||||
|
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||||
|
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
|
||||||
|
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
|
this->enable();
|
||||||
|
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||||
|
this->disable();
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
|
||||||
|
this->disable();
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
if (len != 0) {
|
||||||
|
this->enable();
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(cmd);
|
||||||
|
this->disable();
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
if (len != 0) {
|
||||||
|
this->enable();
|
||||||
|
this->write_array(bytes, len);
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(cmd);
|
||||||
|
this->disable();
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
for (size_t i = 0; i != len; i++) {
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(0);
|
||||||
|
this->write_byte(bytes[i]);
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write changed parameters to the display
|
||||||
|
void reset_params_() {
|
||||||
|
if (!this->is_ready())
|
||||||
|
return;
|
||||||
|
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||||
|
if (this->brightness_.has_value())
|
||||||
|
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the address window for the next data write
|
||||||
|
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
||||||
|
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
||||||
|
uint8_t buf[4];
|
||||||
|
x1 += OFFSET_WIDTH;
|
||||||
|
x2 += OFFSET_WIDTH;
|
||||||
|
y1 += OFFSET_HEIGHT;
|
||||||
|
y2 += OFFSET_HEIGHT;
|
||||||
|
put16_be(buf, y1);
|
||||||
|
put16_be(buf + 2, y2);
|
||||||
|
this->write_command_(RASET, buf, sizeof buf);
|
||||||
|
put16_be(buf, x1);
|
||||||
|
put16_be(buf + 2, x2);
|
||||||
|
this->write_command_(CASET, buf, sizeof buf);
|
||||||
|
if constexpr (BUS_TYPE != BUS_TYPE_QUAD) {
|
||||||
|
this->write_command_(WDATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// map the display color bitness to the pixel mode
|
||||||
|
static PixelMode get_pixel_mode(display::ColorBitness bitness) {
|
||||||
|
switch (bitness) {
|
||||||
|
case display::COLOR_BITNESS_888:
|
||||||
|
return PIXEL_MODE_18; // 18 bits per pixel
|
||||||
|
case display::COLOR_BITNESS_565:
|
||||||
|
return PIXEL_MODE_16; // 16 bits per pixel
|
||||||
|
default:
|
||||||
|
return PIXEL_MODE_8; // Default to 8 bits per pixel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a buffer to the display.
|
||||||
|
* @param w Width of each line in bytes
|
||||||
|
* @param h Height of the buffer in rows
|
||||||
|
* @param pad Padding in bytes after each line
|
||||||
|
*/
|
||||||
|
void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) {
|
||||||
|
if (pad == 0) {
|
||||||
|
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||||
|
this->write_array(ptr, w * h);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
|
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (size_t y = 0; y != h; y++) {
|
||||||
|
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||||
|
this->write_array(ptr, w);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
|
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8);
|
||||||
|
}
|
||||||
|
ptr += w + pad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a buffer to the display.
|
||||||
|
*
|
||||||
|
* The ptr is a pointer to the pixel data
|
||||||
|
* The other parameters are all in pixel units.
|
||||||
|
*/
|
||||||
|
void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset,
|
||||||
|
int x_pad) {
|
||||||
|
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
|
||||||
|
this->enable();
|
||||||
|
ptr += y_offset * (x_offset + w + x_pad) + x_offset;
|
||||||
|
if constexpr (BUFFERPIXEL == DISPLAYPIXEL) {
|
||||||
|
this->write_display_data_(reinterpret_cast<const uint8_t *>(ptr), w * sizeof(BUFFERTYPE), h,
|
||||||
|
x_pad * sizeof(BUFFERTYPE));
|
||||||
|
} else {
|
||||||
|
// type conversion required, do it in chunks
|
||||||
|
uint8_t dbuffer[DISPLAYPIXEL * 48];
|
||||||
|
uint8_t *dptr = dbuffer;
|
||||||
|
auto stride = x_offset + w + x_pad; // stride in pixels
|
||||||
|
for (size_t y = 0; y != h; y++) {
|
||||||
|
for (size_t x = 0; x != w; x++) {
|
||||||
|
auto color_val = ptr[y * stride + x];
|
||||||
|
if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) {
|
||||||
|
// 16 to 18 bit conversion
|
||||||
|
if constexpr (IS_BIG_ENDIAN) {
|
||||||
|
*dptr++ = color_val & 0xF8;
|
||||||
|
*dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11;
|
||||||
|
*dptr++ = (color_val >> 5) & 0xF8;
|
||||||
|
} else {
|
||||||
|
*dptr++ = (color_val >> 8) & 0xF8; // Blue
|
||||||
|
*dptr++ = (color_val & 0x7E0) >> 3;
|
||||||
|
*dptr++ = color_val << 3;
|
||||||
|
}
|
||||||
|
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) {
|
||||||
|
// 8 bit to 18 bit conversion
|
||||||
|
*dptr++ = color_val << 6; // Blue
|
||||||
|
*dptr++ = (color_val & 0x1C) << 3; // Green
|
||||||
|
*dptr++ = (color_val & 0xE0); // Red
|
||||||
|
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) {
|
||||||
|
if constexpr (IS_BIG_ENDIAN) {
|
||||||
|
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
|
||||||
|
*dptr++ = (color_val & 3) << 3;
|
||||||
|
} else {
|
||||||
|
*dptr++ = (color_val & 3) << 3;
|
||||||
|
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// buffer full? Flush.
|
||||||
|
if (dptr == dbuffer + sizeof(dbuffer)) {
|
||||||
|
this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0);
|
||||||
|
dptr = dbuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// flush any remaining data
|
||||||
|
if (dptr != dbuffer) {
|
||||||
|
this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PROPERTIES */
|
||||||
|
|
||||||
|
// GPIO pins
|
||||||
GPIOPin *reset_pin_{nullptr};
|
GPIOPin *reset_pin_{nullptr};
|
||||||
std::vector<GPIOPin *> enable_pins_{};
|
std::vector<GPIOPin *> enable_pins_{};
|
||||||
GPIOPin *dc_pin_{nullptr};
|
GPIOPin *dc_pin_{nullptr};
|
||||||
uint16_t x_low_{1};
|
|
||||||
uint16_t y_low_{1};
|
|
||||||
uint16_t x_high_{0};
|
|
||||||
uint16_t y_high_{0};
|
|
||||||
bool setup_complete_{};
|
|
||||||
|
|
||||||
|
// other properties set by configuration
|
||||||
bool invert_colors_{};
|
bool invert_colors_{};
|
||||||
size_t width_;
|
|
||||||
size_t height_;
|
|
||||||
int16_t offset_width_;
|
|
||||||
int16_t offset_height_;
|
|
||||||
size_t buffer_bytes_{0};
|
|
||||||
display::ColorBitness color_depth_;
|
|
||||||
PixelMode pixel_mode_{PIXEL_MODE_16};
|
|
||||||
uint8_t bus_width_{};
|
|
||||||
bool spi_16_{};
|
|
||||||
uint8_t madctl_{};
|
|
||||||
bool draw_from_origin_{false};
|
|
||||||
unsigned draw_rounding_{2};
|
unsigned draw_rounding_{2};
|
||||||
optional<uint8_t> brightness_{};
|
optional<uint8_t> brightness_{};
|
||||||
const char *model_{"Unknown"};
|
const char *model_{"Unknown"};
|
||||||
std::vector<uint8_t> init_sequence_{};
|
std::vector<uint8_t> init_sequence_{};
|
||||||
|
uint8_t madctl_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for MIPI SPI displays with a buffer.
|
||||||
|
*
|
||||||
|
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
|
||||||
|
* @tparam BUFFERPIXEL Color depth of the buffer
|
||||||
|
* @tparam DISPLAYPIXEL Color depth of the display
|
||||||
|
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
|
||||||
|
* @tparam ROTATION The rotation of the display
|
||||||
|
* @tparam WIDTH Width of the display in pixels
|
||||||
|
* @tparam HEIGHT Height of the display in pixels
|
||||||
|
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
||||||
|
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
||||||
|
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
|
||||||
|
*/
|
||||||
|
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||||
|
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
|
||||||
|
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
||||||
|
OFFSET_WIDTH, OFFSET_HEIGHT> {
|
||||||
|
public:
|
||||||
|
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
||||||
|
|
||||||
|
void dump_config() override {
|
||||||
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||||
|
OFFSET_HEIGHT>::dump_config();
|
||||||
|
esph_log_config(TAG,
|
||||||
|
" Rotation: %d°\n"
|
||||||
|
" Buffer pixels: %d bits\n"
|
||||||
|
" Buffer fraction: 1/%d\n"
|
||||||
|
" Buffer bytes: %zu\n"
|
||||||
|
" Draw rounding: %u",
|
||||||
|
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
|
||||||
|
this->draw_rounding_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() override {
|
||||||
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||||
|
OFFSET_HEIGHT>::setup();
|
||||||
|
RAMAllocator<BUFFERTYPE> allocator{};
|
||||||
|
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
|
||||||
|
if (this->buffer_ == nullptr) {
|
||||||
|
this->mark_failed("Buffer allocation failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() override {
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
auto now = millis();
|
||||||
|
#endif
|
||||||
|
if (this->is_failed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||||
|
// the display height,
|
||||||
|
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
auto lap = millis();
|
||||||
|
#endif
|
||||||
|
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
|
||||||
|
if (this->auto_clear_enabled_) {
|
||||||
|
this->clear();
|
||||||
|
}
|
||||||
|
if (this->page_ != nullptr) {
|
||||||
|
this->page_->get_writer()(*this);
|
||||||
|
} else if (this->writer_.has_value()) {
|
||||||
|
(*this->writer_)(*this);
|
||||||
|
} else {
|
||||||
|
this->test_card();
|
||||||
|
}
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap);
|
||||||
|
lap = millis();
|
||||||
|
#endif
|
||||||
|
if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
|
||||||
|
return;
|
||||||
|
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
|
||||||
|
this->y_high_);
|
||||||
|
// Some chips require that the drawing window be aligned on certain boundaries
|
||||||
|
auto dr = this->draw_rounding_;
|
||||||
|
this->x_low_ = this->x_low_ / dr * dr;
|
||||||
|
this->y_low_ = this->y_low_ / dr * dr;
|
||||||
|
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
|
||||||
|
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
|
||||||
|
int w = this->x_high_ - this->x_low_ + 1;
|
||||||
|
int h = this->y_high_ - this->y_low_ + 1;
|
||||||
|
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||||
|
this->y_low_ - this->start_line_, WIDTH - w);
|
||||||
|
// invalidate watermarks
|
||||||
|
this->x_low_ = WIDTH;
|
||||||
|
this->y_low_ = HEIGHT;
|
||||||
|
this->x_high_ = 0;
|
||||||
|
this->y_high_ = 0;
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
esph_log_v(TAG, "Write to display took %dms", millis() - lap);
|
||||||
|
lap = millis();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
esph_log_v(TAG, "Total update took %dms", millis() - now);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a pixel at the given coordinates.
|
||||||
|
void draw_pixel_at(int x, int y, Color color) override {
|
||||||
|
rotate_coordinates_(x, y);
|
||||||
|
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
||||||
|
return;
|
||||||
|
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
|
||||||
|
if (x < this->x_low_) {
|
||||||
|
this->x_low_ = x;
|
||||||
|
}
|
||||||
|
if (x > this->x_high_) {
|
||||||
|
this->x_high_ = x;
|
||||||
|
}
|
||||||
|
if (y < this->y_low_) {
|
||||||
|
this->y_low_ = y;
|
||||||
|
}
|
||||||
|
if (y > this->y_high_) {
|
||||||
|
this->y_high_ = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills the display with a color.
|
||||||
|
void fill(Color color) override {
|
||||||
|
this->x_low_ = 0;
|
||||||
|
this->y_low_ = this->start_line_;
|
||||||
|
this->x_high_ = WIDTH - 1;
|
||||||
|
this->y_high_ = this->end_line_ - 1;
|
||||||
|
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_width() override {
|
||||||
|
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return HEIGHT;
|
||||||
|
return WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_height() override {
|
||||||
|
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return WIDTH;
|
||||||
|
return HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Rotate the coordinates to match the display orientation.
|
||||||
|
void rotate_coordinates_(int &x, int &y) const {
|
||||||
|
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||||
|
x = WIDTH - x - 1;
|
||||||
|
y = HEIGHT - y - 1;
|
||||||
|
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
|
||||||
|
auto tmp = x;
|
||||||
|
x = WIDTH - y - 1;
|
||||||
|
y = tmp;
|
||||||
|
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
|
||||||
|
auto tmp = y;
|
||||||
|
y = HEIGHT - x - 1;
|
||||||
|
x = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a color to the buffer pixel format.
|
||||||
|
BUFFERTYPE convert_color_(Color &color) const {
|
||||||
|
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
|
||||||
|
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
|
||||||
|
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
|
||||||
|
if constexpr (IS_BIG_ENDIAN) {
|
||||||
|
return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5;
|
||||||
|
} else {
|
||||||
|
return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<BUFFERTYPE>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
BUFFERTYPE *buffer_{};
|
||||||
|
uint16_t x_low_{WIDTH};
|
||||||
|
uint16_t y_low_{HEIGHT};
|
||||||
|
uint16_t x_high_{0};
|
||||||
|
uint16_t y_high_{0};
|
||||||
|
uint16_t start_line_{0};
|
||||||
|
uint16_t end_line_{1};
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace mipi_spi
|
} // namespace mipi_spi
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
30
esphome/components/mipi_spi/models/adafruit.py
Normal file
30
esphome/components/mipi_spi/models/adafruit.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from .ili import ST7789V
|
||||||
|
|
||||||
|
ST7789V.extend(
|
||||||
|
"ADAFRUIT-FUNHOUSE",
|
||||||
|
height=240,
|
||||||
|
width=240,
|
||||||
|
offset_height=0,
|
||||||
|
offset_width=0,
|
||||||
|
cs_pin=40,
|
||||||
|
dc_pin=39,
|
||||||
|
reset_pin=41,
|
||||||
|
invert_colors=True,
|
||||||
|
mirror_x=True,
|
||||||
|
mirror_y=True,
|
||||||
|
data_rate="80MHz",
|
||||||
|
)
|
||||||
|
|
||||||
|
ST7789V.extend(
|
||||||
|
"ADAFRUIT-S2-TFT-FEATHER",
|
||||||
|
height=240,
|
||||||
|
width=135,
|
||||||
|
offset_height=52,
|
||||||
|
offset_width=40,
|
||||||
|
cs_pin=7,
|
||||||
|
dc_pin=39,
|
||||||
|
reset_pin=40,
|
||||||
|
invert_colors=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
models = {}
|
@ -67,6 +67,14 @@ RM690B0 = DriverChip(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD)
|
T4_S3_AMOLED = RM690B0.extend(
|
||||||
|
"T4-S3",
|
||||||
|
width=450,
|
||||||
|
offset_width=16,
|
||||||
|
cs_pin=11,
|
||||||
|
reset_pin=13,
|
||||||
|
enable_pin=9,
|
||||||
|
bus_mode=TYPE_QUAD,
|
||||||
|
)
|
||||||
|
|
||||||
models = {}
|
models = {}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
from . import DriverChip
|
from . import DriverChip
|
||||||
from .ili import ILI9488_A
|
from .ili import ILI9488_A
|
||||||
|
|
||||||
@ -128,6 +130,7 @@ DriverChip(
|
|||||||
|
|
||||||
ILI9488_A.extend(
|
ILI9488_A.extend(
|
||||||
"PICO-RESTOUCH-LCD-3.5",
|
"PICO-RESTOUCH-LCD-3.5",
|
||||||
|
swap_xy=cv.UNDEFINED,
|
||||||
spi_16=True,
|
spi_16=True,
|
||||||
pixel_mode="16bit",
|
pixel_mode="16bit",
|
||||||
mirror_x=True,
|
mirror_x=True,
|
||||||
|
@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES);
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to<JsonArray>();
|
||||||
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
|
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
|
||||||
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
|
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
|
||||||
supported_features.add("arm_away");
|
supported_features.add("arm_away");
|
||||||
|
@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
if (!this->binary_sensor_->get_device_class().empty())
|
if (!this->binary_sensor_->get_device_class().empty())
|
||||||
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
|
||||||
if (this->binary_sensor_->is_status_binary_sensor())
|
if (this->binary_sensor_->is_status_binary_sensor())
|
||||||
|
@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
config.state_topic = false;
|
config.state_topic = false;
|
||||||
if (!this->button_->get_device_class().empty())
|
if (!this->button_->get_device_class().empty()) {
|
||||||
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
|
||||||
|
}
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MQTTButtonComponent::component_type() const { return "button"; }
|
std::string MQTTButtonComponent::component_type() const { return "button"; }
|
||||||
|
@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() {
|
|||||||
std::string topic = "esphome/discover/";
|
std::string topic = "esphome/discover/";
|
||||||
topic.append(App.get_name());
|
topic.append(App.get_name());
|
||||||
|
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
this->publish_json(
|
this->publish_json(
|
||||||
topic,
|
topic,
|
||||||
[](JsonObject root) {
|
[](JsonObject root) {
|
||||||
@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() {
|
|||||||
#endif
|
#endif
|
||||||
},
|
},
|
||||||
2, this->discovery_info_.retain);
|
2, this->discovery_info_.retain);
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTClientComponent::dump_config() {
|
void MQTTClientComponent::dump_config() {
|
||||||
|
@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate";
|
|||||||
using namespace esphome::climate;
|
using namespace esphome::climate;
|
||||||
|
|
||||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
auto traits = this->device_->get_traits();
|
auto traits = this->device_->get_traits();
|
||||||
// current_temperature_topic
|
// current_temperature_topic
|
||||||
if (traits.get_supports_current_temperature()) {
|
if (traits.get_supports_current_temperature()) {
|
||||||
@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// mode_state_topic
|
// mode_state_topic
|
||||||
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
|
root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
|
||||||
// modes
|
// modes
|
||||||
JsonArray modes = root.createNestedArray(MQTT_MODES);
|
JsonArray modes = root[MQTT_MODES].to<JsonArray>();
|
||||||
// sort array for nice UI in HA
|
// sort array for nice UI in HA
|
||||||
if (traits.supports_mode(CLIMATE_MODE_AUTO))
|
if (traits.supports_mode(CLIMATE_MODE_AUTO))
|
||||||
modes.add("auto");
|
modes.add("auto");
|
||||||
@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// preset_mode_state_topic
|
// preset_mode_state_topic
|
||||||
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
|
root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
|
||||||
// presets
|
// presets
|
||||||
JsonArray presets = root.createNestedArray("preset_modes");
|
JsonArray presets = root["preset_modes"].to<JsonArray>();
|
||||||
if (traits.supports_preset(CLIMATE_PRESET_HOME))
|
if (traits.supports_preset(CLIMATE_PRESET_HOME))
|
||||||
presets.add("home");
|
presets.add("home");
|
||||||
if (traits.supports_preset(CLIMATE_PRESET_AWAY))
|
if (traits.supports_preset(CLIMATE_PRESET_AWAY))
|
||||||
@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// fan_mode_state_topic
|
// fan_mode_state_topic
|
||||||
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
|
root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
|
||||||
// fan_modes
|
// fan_modes
|
||||||
JsonArray fan_modes = root.createNestedArray("fan_modes");
|
JsonArray fan_modes = root["fan_modes"].to<JsonArray>();
|
||||||
if (traits.supports_fan_mode(CLIMATE_FAN_ON))
|
if (traits.supports_fan_mode(CLIMATE_FAN_ON))
|
||||||
fan_modes.add("on");
|
fan_modes.add("on");
|
||||||
if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
|
if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
|
||||||
@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
// swing_mode_state_topic
|
// swing_mode_state_topic
|
||||||
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
|
root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
|
||||||
// swing_modes
|
// swing_modes
|
||||||
JsonArray swing_modes = root.createNestedArray("swing_modes");
|
JsonArray swing_modes = root["swing_modes"].to<JsonArray>();
|
||||||
if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
|
if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
|
||||||
swing_modes.add("off");
|
swing_modes.add("off");
|
||||||
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
|
if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
|
||||||
@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
|
|
||||||
config.state_topic = false;
|
config.state_topic = false;
|
||||||
config.command_topic = false;
|
config.command_topic = false;
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
void MQTTClimateComponent::setup() {
|
void MQTTClimateComponent::setup() {
|
||||||
auto traits = this->device_->get_traits();
|
auto traits = this->device_->get_traits();
|
||||||
|
@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
|
|
||||||
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
|
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
|
||||||
|
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
return global_mqtt_client->publish_json(
|
return global_mqtt_client->publish_json(
|
||||||
this->get_discovery_topic_(discovery_info),
|
this->get_discovery_topic_(discovery_info),
|
||||||
[this](JsonObject root) {
|
[this](JsonObject root) {
|
||||||
@ -150,7 +151,7 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
}
|
}
|
||||||
std::string node_area = App.get_area();
|
std::string node_area = App.get_area();
|
||||||
|
|
||||||
JsonObject device_info = root.createNestedObject(MQTT_DEVICE);
|
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
|
||||||
const auto mac = get_mac_address();
|
const auto mac = get_mac_address();
|
||||||
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
|
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
|
||||||
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
|
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
|
||||||
@ -187,6 +188,7 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
|
device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
|
||||||
},
|
},
|
||||||
this->qos_, discovery_info.retain);
|
this->qos_, discovery_info.retain);
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t MQTTComponent::get_qos() const { return this->qos_; }
|
uint8_t MQTTComponent::get_qos() const { return this->qos_; }
|
||||||
|
@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
if (!this->cover_->get_device_class().empty())
|
if (!this->cover_->get_device_class().empty())
|
||||||
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
|
||||||
|
|
||||||
|
@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
|
|||||||
void MQTTDateComponent::setup() {
|
void MQTTDateComponent::setup() {
|
||||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||||
auto call = this->date_->make_call();
|
auto call = this->date_->make_call();
|
||||||
if (root.containsKey("year")) {
|
if (root["year"].is<uint16_t>()) {
|
||||||
call.set_year(root["year"]);
|
call.set_year(root["year"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("month")) {
|
if (root["month"].is<uint8_t>()) {
|
||||||
call.set_month(root["month"]);
|
call.set_month(root["month"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("day")) {
|
if (root["day"].is<uint8_t>()) {
|
||||||
call.set_day(root["day"]);
|
call.set_day(root["day"]);
|
||||||
}
|
}
|
||||||
call.perform();
|
call.perform();
|
||||||
@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() {
|
|||||||
}
|
}
|
||||||
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
|
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
|
||||||
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
|
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root["year"] = year;
|
root["year"] = year;
|
||||||
root["month"] = month;
|
root["month"] = month;
|
||||||
root["day"] = day;
|
root["day"] = day;
|
||||||
|
@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim
|
|||||||
void MQTTDateTimeComponent::setup() {
|
void MQTTDateTimeComponent::setup() {
|
||||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||||
auto call = this->datetime_->make_call();
|
auto call = this->datetime_->make_call();
|
||||||
if (root.containsKey("year")) {
|
if (root["year"].is<uint16_t>()) {
|
||||||
call.set_year(root["year"]);
|
call.set_year(root["year"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("month")) {
|
if (root["month"].is<uint8_t>()) {
|
||||||
call.set_month(root["month"]);
|
call.set_month(root["month"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("day")) {
|
if (root["day"].is<uint8_t>()) {
|
||||||
call.set_day(root["day"]);
|
call.set_day(root["day"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("hour")) {
|
if (root["hour"].is<uint8_t>()) {
|
||||||
call.set_hour(root["hour"]);
|
call.set_hour(root["hour"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("minute")) {
|
if (root["minute"].is<uint8_t>()) {
|
||||||
call.set_minute(root["minute"]);
|
call.set_minute(root["minute"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("second")) {
|
if (root["second"].is<uint8_t>()) {
|
||||||
call.set_second(root["second"]);
|
call.set_second(root["second"]);
|
||||||
}
|
}
|
||||||
call.perform();
|
call.perform();
|
||||||
@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() {
|
|||||||
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
|
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
|
||||||
uint8_t second) {
|
uint8_t second) {
|
||||||
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
|
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root["year"] = year;
|
root["year"] = year;
|
||||||
root["month"] = month;
|
root["month"] = month;
|
||||||
root["day"] = day;
|
root["day"] = day;
|
||||||
|
@ -16,7 +16,8 @@ using namespace esphome::event;
|
|||||||
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
|
MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {}
|
||||||
|
|
||||||
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES);
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
JsonArray event_types = root[MQTT_EVENT_TYPES].to<JsonArray>();
|
||||||
for (const auto &event_type : this->event_->get_event_types())
|
for (const auto &event_type : this->event_->get_event_types())
|
||||||
event_types.add(event_type);
|
event_types.add(event_type);
|
||||||
|
|
||||||
@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
|
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
|
||||||
return this->publish_json(this->get_state_topic_(),
|
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
|
||||||
[event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; });
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
root[MQTT_EVENT_TYPE] = event_type;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MQTTEventComponent::component_type() const { return "event"; }
|
std::string MQTTEventComponent::component_type() const { return "event"; }
|
||||||
|
@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() {
|
|||||||
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
|
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
|
||||||
|
|
||||||
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
if (this->state_->get_traits().supports_direction()) {
|
if (this->state_->get_traits().supports_direction()) {
|
||||||
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
|
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
|
||||||
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();
|
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();
|
||||||
|
@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() {
|
|||||||
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
|
||||||
|
|
||||||
bool MQTTJSONLightComponent::publish_state_() {
|
bool MQTTJSONLightComponent::publish_state_() {
|
||||||
return this->publish_json(this->get_state_topic_(),
|
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
|
||||||
[this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); });
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
LightJSONSchema::dump_json(*this->state_, root);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
|
LightState *MQTTJSONLightComponent::get_state() const { return this->state_; }
|
||||||
|
|
||||||
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root["schema"] = "json";
|
root["schema"] = "json";
|
||||||
auto traits = this->state_->get_traits();
|
auto traits = this->state_->get_traits();
|
||||||
|
|
||||||
root[MQTT_COLOR_MODE] = true;
|
root[MQTT_COLOR_MODE] = true;
|
||||||
JsonArray color_modes = root.createNestedArray("supported_color_modes");
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
JsonArray color_modes = root["supported_color_modes"].to<JsonArray>();
|
||||||
if (traits.supports_color_mode(ColorMode::ON_OFF))
|
if (traits.supports_color_mode(ColorMode::ON_OFF))
|
||||||
color_modes.add("onoff");
|
color_modes.add("onoff");
|
||||||
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
|
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
|
||||||
@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
|||||||
|
|
||||||
if (this->state_->supports_effects()) {
|
if (this->state_->supports_effects()) {
|
||||||
root["effect"] = true;
|
root["effect"] = true;
|
||||||
JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST);
|
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
|
||||||
for (auto *effect : this->state_->get_effects())
|
for (auto *effect : this->state_->get_effects())
|
||||||
effect_list.add(effect->get_name());
|
effect_list.add(effect->get_name());
|
||||||
effect_list.add("None");
|
effect_list.add("None");
|
||||||
|
@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() {
|
|||||||
std::string MQTTLockComponent::component_type() const { return "lock"; }
|
std::string MQTTLockComponent::component_type() const { return "lock"; }
|
||||||
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
|
const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
|
||||||
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
if (this->lock_->traits.get_assumed_state())
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
if (this->lock_->traits.get_assumed_state()) {
|
||||||
root[MQTT_OPTIMISTIC] = true;
|
root[MQTT_OPTIMISTIC] = true;
|
||||||
|
}
|
||||||
if (this->lock_->traits.get_supports_open())
|
if (this->lock_->traits.get_supports_open())
|
||||||
root[MQTT_PAYLOAD_OPEN] = "OPEN";
|
root[MQTT_PAYLOAD_OPEN] = "OPEN";
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_
|
|||||||
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
const auto &traits = number_->traits;
|
const auto &traits = number_->traits;
|
||||||
// https://www.home-assistant.io/integrations/number.mqtt/
|
// https://www.home-assistant.io/integrations/number.mqtt/
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root[MQTT_MIN] = traits.get_min_value();
|
root[MQTT_MIN] = traits.get_min_value();
|
||||||
root[MQTT_MAX] = traits.get_max_value();
|
root[MQTT_MAX] = traits.get_max_value();
|
||||||
root[MQTT_STEP] = traits.get_step();
|
root[MQTT_STEP] = traits.get_step();
|
||||||
|
@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_
|
|||||||
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
const auto &traits = select_->traits;
|
const auto &traits = select_->traits;
|
||||||
// https://www.home-assistant.io/integrations/select.mqtt/
|
// https://www.home-assistant.io/integrations/select.mqtt/
|
||||||
JsonArray options = root.createNestedArray(MQTT_OPTIONS);
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
JsonArray options = root[MQTT_OPTIONS].to<JsonArray>();
|
||||||
for (const auto &option : traits.get_options())
|
for (const auto &option : traits.get_options())
|
||||||
options.add(option);
|
options.add(option);
|
||||||
|
|
||||||
|
@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire
|
|||||||
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
||||||
|
|
||||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
if (!this->sensor_->get_device_class().empty())
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
if (!this->sensor_->get_device_class().empty()) {
|
||||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||||
|
}
|
||||||
|
|
||||||
if (!this->sensor_->get_unit_of_measurement().empty())
|
if (!this->sensor_->get_unit_of_measurement().empty())
|
||||||
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();
|
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();
|
||||||
|
@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() {
|
|||||||
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
|
std::string MQTTSwitchComponent::component_type() const { return "switch"; }
|
||||||
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
|
const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; }
|
||||||
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
if (this->switch_->assumed_state())
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
if (this->switch_->assumed_state()) {
|
||||||
root[MQTT_OPTIMISTIC] = true;
|
root[MQTT_OPTIMISTIC] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
|
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; }
|
|||||||
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||||
|
|
||||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
switch (this->text_->traits.get_mode()) {
|
switch (this->text_->traits.get_mode()) {
|
||||||
case TEXT_MODE_TEXT:
|
case TEXT_MODE_TEXT:
|
||||||
root[MQTT_MODE] = "text";
|
root[MQTT_MODE] = "text";
|
||||||
|
@ -15,8 +15,10 @@ using namespace esphome::text_sensor;
|
|||||||
|
|
||||||
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
||||||
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
if (!this->sensor_->get_device_class().empty())
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
if (!this->sensor_->get_device_class().empty()) {
|
||||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||||
|
}
|
||||||
config.command_topic = false;
|
config.command_topic = false;
|
||||||
}
|
}
|
||||||
void MQTTTextSensor::setup() {
|
void MQTTTextSensor::setup() {
|
||||||
|
@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
|
|||||||
void MQTTTimeComponent::setup() {
|
void MQTTTimeComponent::setup() {
|
||||||
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
|
||||||
auto call = this->time_->make_call();
|
auto call = this->time_->make_call();
|
||||||
if (root.containsKey("hour")) {
|
if (root["hour"].is<uint8_t>()) {
|
||||||
call.set_hour(root["hour"]);
|
call.set_hour(root["hour"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("minute")) {
|
if (root["minute"].is<uint8_t>()) {
|
||||||
call.set_minute(root["minute"]);
|
call.set_minute(root["minute"]);
|
||||||
}
|
}
|
||||||
if (root.containsKey("second")) {
|
if (root["second"].is<uint8_t>()) {
|
||||||
call.set_second(root["second"]);
|
call.set_second(root["second"]);
|
||||||
}
|
}
|
||||||
call.perform();
|
call.perform();
|
||||||
@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() {
|
|||||||
}
|
}
|
||||||
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
|
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
|
||||||
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
|
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root["hour"] = hour;
|
root["hour"] = hour;
|
||||||
root["minute"] = minute;
|
root["minute"] = minute;
|
||||||
root["second"] = second;
|
root["second"] = second;
|
||||||
|
@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
root["schema"] = "json";
|
root["schema"] = "json";
|
||||||
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
|
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
|
||||||
}
|
}
|
||||||
|
@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
if (!this->valve_->get_device_class().empty())
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
if (!this->valve_->get_device_class().empty()) {
|
||||||
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
|
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
|
||||||
|
}
|
||||||
|
|
||||||
auto traits = this->valve_->get_traits();
|
auto traits = this->valve_->get_traits();
|
||||||
if (traits.get_is_assumed_state()) {
|
if (traits.get_is_assumed_state()) {
|
||||||
|
@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) {
|
|||||||
|
|
||||||
// map 16 bit humidity value into range [-6%, 118%]
|
// map 16 bit humidity value into range [-6%, 118%]
|
||||||
float const humidity_partial = double(humidity) / (1 << 16);
|
float const humidity_partial = double(humidity) / (1 << 16);
|
||||||
float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0);
|
float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial);
|
||||||
float const compensated_humidity_percentage =
|
float const compensated_humidity_percentage =
|
||||||
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
|
humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
|
||||||
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
|
ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
|
||||||
|
218
esphome/components/nrf52/__init__.py
Normal file
218
esphome/components/nrf52/__init__.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.zephyr import (
|
||||||
|
copy_files as zephyr_copy_files,
|
||||||
|
zephyr_add_pm_static,
|
||||||
|
zephyr_set_core_data,
|
||||||
|
zephyr_to_code,
|
||||||
|
)
|
||||||
|
from esphome.components.zephyr.const import (
|
||||||
|
BOOTLOADER_MCUBOOT,
|
||||||
|
KEY_BOOTLOADER,
|
||||||
|
KEY_ZEPHYR,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BOARD,
|
||||||
|
CONF_FRAMEWORK,
|
||||||
|
KEY_CORE,
|
||||||
|
KEY_FRAMEWORK_VERSION,
|
||||||
|
KEY_TARGET_FRAMEWORK,
|
||||||
|
KEY_TARGET_PLATFORM,
|
||||||
|
PLATFORM_NRF52,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE, EsphomeError, coroutine_with_priority
|
||||||
|
from esphome.storage_json import StorageJSON
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
|
from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG
|
||||||
|
from .const import (
|
||||||
|
BOOTLOADER_ADAFRUIT,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||||
|
)
|
||||||
|
|
||||||
|
# force import gpio to register pin schema
|
||||||
|
from .gpio import nrf52_pin_to_code # noqa
|
||||||
|
|
||||||
|
CODEOWNERS = ["@tomaszduda23"]
|
||||||
|
AUTO_LOAD = ["zephyr"]
|
||||||
|
IS_TARGET_PLATFORM = True
|
||||||
|
|
||||||
|
|
||||||
|
def set_core_data(config: ConfigType) -> ConfigType:
|
||||||
|
zephyr_set_core_data(config)
|
||||||
|
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
|
||||||
|
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
|
||||||
|
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1)
|
||||||
|
|
||||||
|
if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
|
||||||
|
zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]])
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
BOOTLOADERS = [
|
||||||
|
BOOTLOADER_ADAFRUIT,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||||
|
BOOTLOADER_MCUBOOT,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_bootloader(config: ConfigType) -> ConfigType:
|
||||||
|
"""Detect the bootloader for the given board."""
|
||||||
|
config = config.copy()
|
||||||
|
bootloaders: list[str] = []
|
||||||
|
board = config[CONF_BOARD]
|
||||||
|
|
||||||
|
if board in BOARDS_ZEPHYR and KEY_BOOTLOADER in BOARDS_ZEPHYR[board]:
|
||||||
|
# this board have bootloaders config available
|
||||||
|
bootloaders = BOARDS_ZEPHYR[board][KEY_BOOTLOADER]
|
||||||
|
|
||||||
|
if KEY_BOOTLOADER not in config:
|
||||||
|
if bootloaders:
|
||||||
|
# there is no bootloader in config -> take first one
|
||||||
|
config[KEY_BOOTLOADER] = bootloaders[0]
|
||||||
|
else:
|
||||||
|
# make mcuboot as default if there is no configuration for that board
|
||||||
|
config[KEY_BOOTLOADER] = BOOTLOADER_MCUBOOT
|
||||||
|
elif bootloaders and config[KEY_BOOTLOADER] not in bootloaders:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"{board} does not support {config[KEY_BOOTLOADER]}, select one of: {', '.join(bootloaders)}"
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_BOARD): cv.string_strict,
|
||||||
|
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_detect_bootloader,
|
||||||
|
set_core_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@coroutine_with_priority(1000)
|
||||||
|
async def to_code(config: ConfigType) -> None:
|
||||||
|
"""Convert the configuration to code."""
|
||||||
|
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||||
|
cg.add_build_flag("-DUSE_NRF52")
|
||||||
|
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||||
|
cg.add_define("ESPHOME_VARIANT", "NRF52")
|
||||||
|
cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK])
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"platform",
|
||||||
|
"https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip",
|
||||||
|
)
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"platform_packages",
|
||||||
|
[
|
||||||
|
"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip",
|
||||||
|
"platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
|
||||||
|
# make sure that firmware.zip is created
|
||||||
|
# for Adafruit_nRF52_Bootloader
|
||||||
|
cg.add_platformio_option("board_upload.protocol", "nrfutil")
|
||||||
|
cg.add_platformio_option("board_upload.use_1200bps_touch", "true")
|
||||||
|
cg.add_platformio_option("board_upload.require_upload_port", "true")
|
||||||
|
cg.add_platformio_option("board_upload.wait_for_upload_port", "true")
|
||||||
|
|
||||||
|
zephyr_to_code(config)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_files() -> None:
|
||||||
|
"""Copy files to the build directory."""
|
||||||
|
zephyr_copy_files()
|
||||||
|
|
||||||
|
|
||||||
|
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
|
||||||
|
"""Get the download types for the firmware."""
|
||||||
|
types = []
|
||||||
|
UF2_PATH = "zephyr/zephyr.uf2"
|
||||||
|
DFU_PATH = "firmware.zip"
|
||||||
|
HEX_PATH = "zephyr/zephyr.hex"
|
||||||
|
HEX_MERGED_PATH = "zephyr/merged.hex"
|
||||||
|
APP_IMAGE_PATH = "zephyr/app_update.bin"
|
||||||
|
build_dir = Path(storage_json.firmware_bin_path).parent
|
||||||
|
if (build_dir / UF2_PATH).is_file():
|
||||||
|
types = [
|
||||||
|
{
|
||||||
|
"title": "UF2 package (recommended)",
|
||||||
|
"description": "For flashing via Adafruit nRF52 Bootloader as a flash drive.",
|
||||||
|
"file": UF2_PATH,
|
||||||
|
"download": f"{storage_json.name}.uf2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "DFU package",
|
||||||
|
"description": "For flashing via adafruit-nrfutil using USB CDC.",
|
||||||
|
"file": DFU_PATH,
|
||||||
|
"download": f"dfu-{storage_json.name}.zip",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
types = [
|
||||||
|
{
|
||||||
|
"title": "HEX package",
|
||||||
|
"description": "For flashing via pyocd using SWD.",
|
||||||
|
"file": (
|
||||||
|
HEX_MERGED_PATH
|
||||||
|
if (build_dir / HEX_MERGED_PATH).is_file()
|
||||||
|
else HEX_PATH
|
||||||
|
),
|
||||||
|
"download": f"{storage_json.name}.hex",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if (build_dir / APP_IMAGE_PATH).is_file():
|
||||||
|
types += [
|
||||||
|
{
|
||||||
|
"title": "App update package",
|
||||||
|
"description": "For flashing via mcumgr-web using BLE or smpclient using USB CDC.",
|
||||||
|
"file": APP_IMAGE_PATH,
|
||||||
|
"download": f"app-{storage_json.name}.img",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return types
|
||||||
|
|
||||||
|
|
||||||
|
def _upload_using_platformio(
|
||||||
|
config: ConfigType, port: str, upload_args: list[str]
|
||||||
|
) -> int | str:
|
||||||
|
from esphome import platformio_api
|
||||||
|
|
||||||
|
if port is not None:
|
||||||
|
upload_args += ["--upload-port", port]
|
||||||
|
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||||
|
from esphome.__main__ import check_permissions, get_port_type
|
||||||
|
|
||||||
|
result = 0
|
||||||
|
handled = False
|
||||||
|
|
||||||
|
if get_port_type(host) == "SERIAL":
|
||||||
|
check_permissions(host)
|
||||||
|
result = _upload_using_platformio(config, host, ["-t", "upload"])
|
||||||
|
handled = True
|
||||||
|
|
||||||
|
if host == "PYOCD":
|
||||||
|
result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"])
|
||||||
|
handled = True
|
||||||
|
|
||||||
|
if result != 0:
|
||||||
|
raise EsphomeError(f"Upload failed with result: {result}")
|
||||||
|
|
||||||
|
return handled
|
34
esphome/components/nrf52/boards.py
Normal file
34
esphome/components/nrf52/boards.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from esphome.components.zephyr import Section
|
||||||
|
from esphome.components.zephyr.const import KEY_BOOTLOADER
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
BOOTLOADER_ADAFRUIT,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||||
|
)
|
||||||
|
|
||||||
|
BOARDS_ZEPHYR = {
|
||||||
|
"adafruit_itsybitsy_nrf52840": {
|
||||||
|
KEY_BOOTLOADER: [
|
||||||
|
BOOTLOADER_ADAFRUIT,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go
|
||||||
|
# https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map
|
||||||
|
BOOTLOADER_CONFIG = {
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD132: [
|
||||||
|
Section("empty_app_offset", 0x0, 0x26000, "flash_primary"),
|
||||||
|
],
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [
|
||||||
|
Section("empty_app_offset", 0x0, 0x26000, "flash_primary"),
|
||||||
|
],
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [
|
||||||
|
Section("empty_app_offset", 0x0, 0x27000, "flash_primary"),
|
||||||
|
],
|
||||||
|
}
|
4
esphome/components/nrf52/const.py
Normal file
4
esphome/components/nrf52/const.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
BOOTLOADER_ADAFRUIT = "adafruit"
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132"
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7"
|
53
esphome/components/nrf52/gpio.py
Normal file
53
esphome/components/nrf52/gpio.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from esphome import pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.zephyr.const import zephyr_ns
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52
|
||||||
|
|
||||||
|
ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin)
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_pin(value):
|
||||||
|
if isinstance(value, dict) or value is None:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"This variable only supports pin numbers, not full pin schemas "
|
||||||
|
"(with inverted and mode)."
|
||||||
|
)
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# e.g. P0.27
|
||||||
|
if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".":
|
||||||
|
return cv.int_(value[len("P")].strip()) * 32 + cv.int_(
|
||||||
|
value[len("P0.") :].strip()
|
||||||
|
)
|
||||||
|
raise cv.Invalid(f"Invalid pin: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_gpio_pin(value):
|
||||||
|
value = _translate_pin(value)
|
||||||
|
if value < 0 or value > (32 + 16):
|
||||||
|
raise cv.Invalid(f"NRF52: Invalid pin number: {value}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
NRF52_PIN_SCHEMA = cv.All(
|
||||||
|
pins.gpio_base_schema(
|
||||||
|
ZephyrGPIOPin,
|
||||||
|
validate_gpio_pin,
|
||||||
|
modes=pins.GPIO_STANDARD_MODES,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA)
|
||||||
|
async def nrf52_pin_to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
num = config[CONF_NUMBER]
|
||||||
|
cg.add(var.set_pin(num))
|
||||||
|
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||||
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
|
return var
|
@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_REQUEST_HEADERS
|
from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS
|
||||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||||
from esphome.components.image import (
|
from esphome.components.image import (
|
||||||
CONF_INVERT_ALPHA,
|
CONF_INVERT_ALPHA,
|
||||||
@ -11,6 +11,7 @@ from esphome.components.image import (
|
|||||||
Image_,
|
Image_,
|
||||||
get_image_type_enum,
|
get_image_type_enum,
|
||||||
get_transparency_enum,
|
get_transparency_enum,
|
||||||
|
validate_settings,
|
||||||
)
|
)
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
rp2040_arduino=cv.Version(0, 0, 0),
|
rp2040_arduino=cv.Version(0, 0, 0),
|
||||||
host=cv.Version(0, 0, 0),
|
host=cv.Version(0, 0, 0),
|
||||||
),
|
),
|
||||||
|
validate_settings,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -213,6 +215,7 @@ async def to_code(config):
|
|||||||
get_image_type_enum(config[CONF_TYPE]),
|
get_image_type_enum(config[CONF_TYPE]),
|
||||||
transparent,
|
transparent,
|
||||||
config[CONF_BUFFER_SIZE],
|
config[CONF_BUFFER_SIZE],
|
||||||
|
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
|
||||||
)
|
)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||||
|
@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||||
image::Transparency transparency, uint32_t download_buffer_size)
|
image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
|
||||||
: Image(nullptr, 0, 0, type, transparency),
|
: Image(nullptr, 0, 0, type, transparency),
|
||||||
buffer_(nullptr),
|
buffer_(nullptr),
|
||||||
download_buffer_(download_buffer_size),
|
download_buffer_(download_buffer_size),
|
||||||
download_buffer_initial_size_(download_buffer_size),
|
download_buffer_initial_size_(download_buffer_size),
|
||||||
format_(format),
|
format_(format),
|
||||||
fixed_width_(width),
|
fixed_width_(width),
|
||||||
fixed_height_(height) {
|
fixed_height_(height),
|
||||||
|
is_big_endian_(is_big_endian) {
|
||||||
this->set_url(url);
|
this->set_url(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||||
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||||
if (gray == 1) {
|
if (gray == 1) {
|
||||||
gray = 0;
|
gray = 0;
|
||||||
@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
|||||||
case ImageType::IMAGE_TYPE_RGB565: {
|
case ImageType::IMAGE_TYPE_RGB565: {
|
||||||
this->map_chroma_key(color);
|
this->map_chroma_key(color);
|
||||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||||
|
if (this->is_big_endian_) {
|
||||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||||
|
} else {
|
||||||
|
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
|
||||||
|
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||||
|
}
|
||||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||||
this->buffer_[pos + 2] = color.w;
|
this->buffer_[pos + 2] = color.w;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent,
|
|||||||
* @param buffer_size Size of the buffer used to download the image.
|
* @param buffer_size Size of the buffer used to download the image.
|
||||||
*/
|
*/
|
||||||
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
||||||
image::Transparency transparency, uint32_t buffer_size);
|
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
|
||||||
|
|
||||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||||
|
|
||||||
@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent,
|
|||||||
const int fixed_width_;
|
const int fixed_width_;
|
||||||
/** height requested on configuration, or 0 if non specified. */
|
/** height requested on configuration, or 0 if non specified. */
|
||||||
const int fixed_height_;
|
const int fixed_height_;
|
||||||
|
/**
|
||||||
|
* Whether the image is stored in big-endian format.
|
||||||
|
* This is used to determine how to store 16 bit colors in the buffer.
|
||||||
|
*/
|
||||||
|
bool is_big_endian_;
|
||||||
/**
|
/**
|
||||||
* Actual width of the current image. If fixed_width_ is specified,
|
* Actual width of the current image. If fixed_width_ is specified,
|
||||||
* this will be equal to it; otherwise it will be set once the decoding
|
* this will be equal to it; otherwise it will be set once the decoding
|
||||||
|
@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) {
|
|||||||
ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
|
ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
|
||||||
this->state = state < 0.003 && this->zero_means_zero_
|
this->state = state < 0.003 && this->zero_means_zero_
|
||||||
? 0.0
|
? 0.0
|
||||||
: clamp(lerp(state, min_value_, max_value_), min_value_, max_value_);
|
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
|
||||||
this->has_state_ = true;
|
this->has_state_ = true;
|
||||||
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
|
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
|
||||||
}
|
}
|
||||||
|
@ -88,9 +88,9 @@ void Servo::internal_write(float value) {
|
|||||||
value = clamp(value, -1.0f, 1.0f);
|
value = clamp(value, -1.0f, 1.0f);
|
||||||
float level;
|
float level;
|
||||||
if (value < 0.0) {
|
if (value < 0.0) {
|
||||||
level = lerp(-value, this->idle_level_, this->min_level_);
|
level = std::lerp(this->idle_level_, this->min_level_, -value);
|
||||||
} else {
|
} else {
|
||||||
level = lerp(value, this->idle_level_, this->max_level_);
|
level = std::lerp(this->idle_level_, this->max_level_, value);
|
||||||
}
|
}
|
||||||
this->output_->set_level(level);
|
this->output_->set_level(level);
|
||||||
this->current_value_ = value;
|
this->current_value_ = value;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
@ -139,6 +140,27 @@ def get_hw_interface_list():
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def one_of_interface_validator(additional_values: list[str] | None = None) -> Any:
|
||||||
|
"""Helper to create a one_of validator for SPI interfaces.
|
||||||
|
|
||||||
|
This delays evaluation of get_hw_interface_list() until validation time,
|
||||||
|
avoiding access to CORE.data during module import.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
additional_values: List of additional valid values to include
|
||||||
|
"""
|
||||||
|
if additional_values is None:
|
||||||
|
additional_values = []
|
||||||
|
|
||||||
|
def validator(value: str) -> str:
|
||||||
|
return cv.one_of(
|
||||||
|
*sum(get_hw_interface_list(), additional_values),
|
||||||
|
lower=True,
|
||||||
|
)(value)
|
||||||
|
|
||||||
|
return cv.All(cv.string, validator)
|
||||||
|
|
||||||
|
|
||||||
# Given an SPI name, return the index of it in the available list
|
# Given an SPI name, return the index of it in the available list
|
||||||
def get_spi_index(name):
|
def get_spi_index(name):
|
||||||
for i, ilist in enumerate(get_hw_interface_list()):
|
for i, ilist in enumerate(get_hw_interface_list()):
|
||||||
@ -274,9 +296,8 @@ SPI_SINGLE_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_FORCE_SW): cv.invalid(
|
cv.Optional(CONF_FORCE_SW): cv.invalid(
|
||||||
"force_sw is deprecated - use interface: software"
|
"force_sw is deprecated - use interface: software"
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INTERFACE, default="any"): cv.one_of(
|
cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator(
|
||||||
*sum(get_hw_interface_list(), ["software", "hardware", "any"]),
|
["software", "hardware", "any"]
|
||||||
lower=True,
|
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_DATA_PINS): cv.invalid(
|
cv.Optional(CONF_DATA_PINS): cv.invalid(
|
||||||
"'data_pins' should be used with 'type: quad or octal' only"
|
"'data_pins' should be used with 'type: quad or octal' only"
|
||||||
@ -309,10 +330,9 @@ def spi_mode_schema(mode):
|
|||||||
cv.ensure_list(pins.internal_gpio_output_pin_number),
|
cv.ensure_list(pins.internal_gpio_output_pin_number),
|
||||||
cv.Length(min=pin_count, max=pin_count),
|
cv.Length(min=pin_count, max=pin_count),
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of(
|
cv.Optional(
|
||||||
*sum(get_hw_interface_list(), ["hardware"]),
|
CONF_INTERFACE, default="hardware"
|
||||||
lower=True,
|
): one_of_interface_validator(["hardware"]),
|
||||||
),
|
|
||||||
cv.Optional(CONF_MISO_PIN): cv.invalid(
|
cv.Optional(CONF_MISO_PIN): cv.invalid(
|
||||||
f"'miso_pin' should not be used with {mode} SPI"
|
f"'miso_pin' should not be used with {mode} SPI"
|
||||||
),
|
),
|
||||||
|
@ -146,6 +146,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing):
|
|||||||
if sub is not None:
|
if sub is not None:
|
||||||
item[k] = sub
|
item[k] = sub
|
||||||
for old, new in replace_keys:
|
for old, new in replace_keys:
|
||||||
|
if str(new) == str(old):
|
||||||
|
item[new] = item[old]
|
||||||
|
else:
|
||||||
item[new] = merge_config(item.get(old), item.get(new))
|
item[new] = merge_config(item.get(old), item.get(new))
|
||||||
del item[old]
|
del item[old]
|
||||||
elif isinstance(item, str):
|
elif isinstance(item, str):
|
||||||
|
@ -6,6 +6,7 @@ import tzlocal
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.automation import Condition
|
from esphome.automation import Condition
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AT,
|
CONF_AT,
|
||||||
@ -25,7 +26,7 @@ from esphome.const import (
|
|||||||
CONF_TIMEZONE,
|
CONF_TIMEZONE,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
)
|
)
|
||||||
from esphome.core import coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -341,6 +342,8 @@ async def register_time(time_var, config):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
|
if CORE.using_zephyr:
|
||||||
|
zephyr_add_prj_conf("POSIX_CLOCK", True)
|
||||||
cg.add_define("USE_TIME")
|
cg.add_define("USE_TIME")
|
||||||
cg.add_global(time_ns.using)
|
cg.add_global(time_ns.using)
|
||||||
|
|
||||||
|
@ -2,13 +2,15 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#ifdef USE_HOST
|
#ifdef USE_HOST
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
|
#elif defined(USE_ZEPHYR)
|
||||||
|
#include <zephyr/posix/time.h>
|
||||||
#else
|
#else
|
||||||
#include "lwip/opt.h"
|
#include "lwip/opt.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
#include "sys/time.h"
|
#include "sys/time.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_RP2040
|
#if defined(USE_RP2040) || defined(USE_ZEPHYR)
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#endif
|
#endif
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
@ -22,11 +24,22 @@ static const char *const TAG = "time";
|
|||||||
|
|
||||||
RealTimeClock::RealTimeClock() = default;
|
RealTimeClock::RealTimeClock() = default;
|
||||||
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||||
|
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
|
||||||
// Update UTC epoch time.
|
// Update UTC epoch time.
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
struct timespec ts;
|
||||||
|
ts.tv_nsec = 0;
|
||||||
|
ts.tv_sec = static_cast<time_t>(epoch);
|
||||||
|
|
||||||
|
int ret = clock_settime(CLOCK_REALTIME, &ts);
|
||||||
|
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGW(TAG, "clock_settime() failed with code %d", ret);
|
||||||
|
}
|
||||||
|
#else
|
||||||
struct timeval timev {
|
struct timeval timev {
|
||||||
.tv_sec = static_cast<time_t>(epoch), .tv_usec = 0,
|
.tv_sec = static_cast<time_t>(epoch), .tv_usec = 0,
|
||||||
};
|
};
|
||||||
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
|
|
||||||
struct timezone tz = {0, 0};
|
struct timezone tz = {0, 0};
|
||||||
int ret = settimeofday(&timev, &tz);
|
int ret = settimeofday(&timev, &tz);
|
||||||
if (ret == EINVAL) {
|
if (ret == EINVAL) {
|
||||||
@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
|||||||
if (ret != 0) {
|
if (ret != 0) {
|
||||||
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
|
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
auto time = this->now();
|
auto time = this->now();
|
||||||
ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
|
ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
|
||||||
time.minute, time.second);
|
time.minute, time.second);
|
||||||
|
@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
|
|||||||
|
|
||||||
light::LightJSONSchema::dump_json(*obj, root);
|
light::LightJSONSchema::dump_json(*obj, root);
|
||||||
if (start_config == DETAIL_ALL) {
|
if (start_config == DETAIL_ALL) {
|
||||||
JsonArray opt = root.createNestedArray("effects");
|
JsonArray opt = root["effects"].to<JsonArray>();
|
||||||
opt.add("None");
|
opt.add("None");
|
||||||
for (auto const &option : obj->get_effects()) {
|
for (auto const &option : obj->get_effects()) {
|
||||||
opt.add(option->get_name());
|
opt.add(option->get_name());
|
||||||
@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
|
|||||||
return json::build_json([this, obj, value, start_config](JsonObject root) {
|
return json::build_json([this, obj, value, start_config](JsonObject root) {
|
||||||
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
|
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
|
||||||
if (start_config == DETAIL_ALL) {
|
if (start_config == DETAIL_ALL) {
|
||||||
JsonArray opt = root.createNestedArray("option");
|
JsonArray opt = root["option"].to<JsonArray>();
|
||||||
for (auto &option : obj->traits.get_options()) {
|
for (auto &option : obj->traits.get_options()) {
|
||||||
opt.add(option);
|
opt.add(option);
|
||||||
}
|
}
|
||||||
@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s
|
|||||||
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
|
return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL);
|
||||||
}
|
}
|
||||||
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
|
std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
return json::build_json([this, obj, start_config](JsonObject root) {
|
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||||
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
|
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config);
|
||||||
const auto traits = obj->get_traits();
|
const auto traits = obj->get_traits();
|
||||||
@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
|||||||
char buf[16];
|
char buf[16];
|
||||||
|
|
||||||
if (start_config == DETAIL_ALL) {
|
if (start_config == DETAIL_ALL) {
|
||||||
JsonArray opt = root.createNestedArray("modes");
|
JsonArray opt = root["modes"].to<JsonArray>();
|
||||||
for (climate::ClimateMode m : traits.get_supported_modes())
|
for (climate::ClimateMode m : traits.get_supported_modes())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
|
opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
|
||||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||||
JsonArray opt = root.createNestedArray("fan_modes");
|
JsonArray opt = root["fan_modes"].to<JsonArray>();
|
||||||
for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
|
for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
|
opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||||
JsonArray opt = root.createNestedArray("custom_fan_modes");
|
JsonArray opt = root["custom_fan_modes"].to<JsonArray>();
|
||||||
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
|
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
|
||||||
opt.add(custom_fan_mode);
|
opt.add(custom_fan_mode);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes()) {
|
if (traits.get_supports_swing_modes()) {
|
||||||
JsonArray opt = root.createNestedArray("swing_modes");
|
JsonArray opt = root["swing_modes"].to<JsonArray>();
|
||||||
for (auto swing_mode : traits.get_supported_swing_modes())
|
for (auto swing_mode : traits.get_supported_swing_modes())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
|
opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode)));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() && obj->preset.has_value()) {
|
if (traits.get_supports_presets() && obj->preset.has_value()) {
|
||||||
JsonArray opt = root.createNestedArray("presets");
|
JsonArray opt = root["presets"].to<JsonArray>();
|
||||||
for (climate::ClimatePreset m : traits.get_supported_presets())
|
for (climate::ClimatePreset m : traits.get_supported_presets())
|
||||||
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
|
opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) {
|
if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) {
|
||||||
JsonArray opt = root.createNestedArray("custom_presets");
|
JsonArray opt = root["custom_presets"].to<JsonArray>();
|
||||||
for (auto const &custom_preset : traits.get_supported_custom_presets())
|
for (auto const &custom_preset : traits.get_supported_custom_presets())
|
||||||
opt.add(custom_preset);
|
opt.add(custom_preset);
|
||||||
}
|
}
|
||||||
@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
|||||||
root["state"] = root["target_temperature"];
|
root["state"] = root["target_temperature"];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -1635,7 +1637,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
|
|||||||
root["event_type"] = event_type;
|
root["event_type"] = event_type;
|
||||||
}
|
}
|
||||||
if (start_config == DETAIL_ALL) {
|
if (start_config == DETAIL_ALL) {
|
||||||
JsonArray event_types = root.createNestedArray("event_types");
|
JsonArray event_types = root["event_types"].to<JsonArray>();
|
||||||
for (auto const &event_type : obj->get_event_types()) {
|
for (auto const &event_type : obj->get_event_types()) {
|
||||||
event_types.add(event_type);
|
event_types.add(event_type);
|
||||||
}
|
}
|
||||||
@ -1682,6 +1684,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so
|
|||||||
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
|
return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE);
|
||||||
}
|
}
|
||||||
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
|
std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
return json::build_json([this, obj, start_config](JsonObject root) {
|
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||||
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
|
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
|
||||||
root["value"] = obj->update_info.latest_version;
|
root["value"] = obj->update_info.latest_version;
|
||||||
@ -1707,166 +1710,166 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
|
|||||||
this->add_sorting_info_(root, obj);
|
this->add_sorting_info_(root, obj);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||||
if (request->url() == "/")
|
const auto &url = request->url();
|
||||||
|
const auto method = request->method();
|
||||||
|
|
||||||
|
// Simple URL checks
|
||||||
|
if (url == "/")
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
if (request->url() == "/events") {
|
if (url == "/events")
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||||
if (request->url() == "/0.css")
|
if (url == "/0.css")
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||||
if (request->url() == "/0.js")
|
if (url == "/0.js")
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
||||||
if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) {
|
if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA))
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Store the URL to prevent temporary string destruction
|
// Parse URL for component checks
|
||||||
// request->url() returns a reference to a String (on Arduino) or std::string (on ESP-IDF)
|
|
||||||
// UrlMatch stores pointers to the string's data, so we must ensure the string outlives match_url()
|
|
||||||
const auto &url = request->url();
|
|
||||||
UrlMatch match = match_url(url.c_str(), url.length(), true);
|
UrlMatch match = match_url(url.c_str(), url.length(), true);
|
||||||
if (!match.valid)
|
if (!match.valid)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Common pattern check
|
||||||
|
bool is_get = method == HTTP_GET;
|
||||||
|
bool is_post = method == HTTP_POST;
|
||||||
|
bool is_get_or_post = is_get || is_post;
|
||||||
|
|
||||||
|
if (!is_get_or_post)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// GET-only components
|
||||||
|
if (is_get) {
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("sensor"))
|
if (match.domain_equals("sensor"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SWITCH
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_BUTTON
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("binary_sensor"))
|
if (match.domain_equals("binary_sensor"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_FAN
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_LIGHT
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("text_sensor"))
|
if (match.domain_equals("text_sensor"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_COVER
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_NUMBER
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATE
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_DATETIME_TIME
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATETIME
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_TEXT
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_SELECT
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_CLIMATE
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_LOCK
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_VALVE
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
|
||||||
if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("event"))
|
if (match.domain_equals("event"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_UPDATE
|
// GET+POST components
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update"))
|
if (is_get_or_post) {
|
||||||
|
#ifdef USE_SWITCH
|
||||||
|
if (match.domain_equals("switch"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_BUTTON
|
||||||
|
if (match.domain_equals("button"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_FAN
|
||||||
|
if (match.domain_equals("fan"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_LIGHT
|
||||||
|
if (match.domain_equals("light"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_COVER
|
||||||
|
if (match.domain_equals("cover"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_NUMBER
|
||||||
|
if (match.domain_equals("number"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_DATETIME_DATE
|
||||||
|
if (match.domain_equals("date"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_DATETIME_TIME
|
||||||
|
if (match.domain_equals("time"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_DATETIME_DATETIME
|
||||||
|
if (match.domain_equals("datetime"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_TEXT
|
||||||
|
if (match.domain_equals("text"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
if (match.domain_equals("select"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_CLIMATE
|
||||||
|
if (match.domain_equals("climate"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_LOCK
|
||||||
|
if (match.domain_equals("lock"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VALVE
|
||||||
|
if (match.domain_equals("valve"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
|
if (match.domain_equals("alarm_control_panel"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_UPDATE
|
||||||
|
if (match.domain_equals("update"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||||
if (request->url() == "/") {
|
const auto &url = request->url();
|
||||||
|
|
||||||
|
// Handle static routes first
|
||||||
|
if (url == "/") {
|
||||||
this->handle_index_request(request);
|
this->handle_index_request(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
if (request->url() == "/events") {
|
if (url == "/events") {
|
||||||
this->events_.add_new_client(this, request);
|
this->events_.add_new_client(this, request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||||
if (request->url() == "/0.css") {
|
if (url == "/0.css") {
|
||||||
this->handle_css_request(request);
|
this->handle_css_request(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||||
if (request->url() == "/0.js") {
|
if (url == "/0.js") {
|
||||||
this->handle_js_request(request);
|
this->handle_js_request(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1879,147 +1882,85 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// See comment in canHandle() for why we store the URL reference
|
// Parse URL for component routing
|
||||||
const auto &url = request->url();
|
|
||||||
UrlMatch match = match_url(url.c_str(), url.length(), false);
|
UrlMatch match = match_url(url.c_str(), url.length(), false);
|
||||||
|
|
||||||
|
// Component routing using minimal code repetition
|
||||||
|
struct ComponentRoute {
|
||||||
|
const char *domain;
|
||||||
|
void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &);
|
||||||
|
};
|
||||||
|
|
||||||
|
static const ComponentRoute ROUTES[] = {
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
if (match.domain_equals("sensor")) {
|
{"sensor", &WebServer::handle_sensor_request},
|
||||||
this->handle_sensor_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SWITCH
|
#ifdef USE_SWITCH
|
||||||
if (match.domain_equals("switch")) {
|
{"switch", &WebServer::handle_switch_request},
|
||||||
this->handle_switch_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BUTTON
|
#ifdef USE_BUTTON
|
||||||
if (match.domain_equals("button")) {
|
{"button", &WebServer::handle_button_request},
|
||||||
this->handle_button_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
if (match.domain_equals("binary_sensor")) {
|
{"binary_sensor", &WebServer::handle_binary_sensor_request},
|
||||||
this->handle_binary_sensor_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
if (match.domain_equals("fan")) {
|
{"fan", &WebServer::handle_fan_request},
|
||||||
this->handle_fan_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
if (match.domain_equals("light")) {
|
{"light", &WebServer::handle_light_request},
|
||||||
this->handle_light_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
if (match.domain_equals("text_sensor")) {
|
{"text_sensor", &WebServer::handle_text_sensor_request},
|
||||||
this->handle_text_sensor_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
if (match.domain_equals("cover")) {
|
{"cover", &WebServer::handle_cover_request},
|
||||||
this->handle_cover_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
if (match.domain_equals("number")) {
|
{"number", &WebServer::handle_number_request},
|
||||||
this->handle_number_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATE
|
#ifdef USE_DATETIME_DATE
|
||||||
if (match.domain_equals("date")) {
|
{"date", &WebServer::handle_date_request},
|
||||||
this->handle_date_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_TIME
|
#ifdef USE_DATETIME_TIME
|
||||||
if (match.domain_equals("time")) {
|
{"time", &WebServer::handle_time_request},
|
||||||
this->handle_time_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATETIME
|
#ifdef USE_DATETIME_DATETIME
|
||||||
if (match.domain_equals("datetime")) {
|
{"datetime", &WebServer::handle_datetime_request},
|
||||||
this->handle_datetime_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_TEXT
|
#ifdef USE_TEXT
|
||||||
if (match.domain_equals("text")) {
|
{"text", &WebServer::handle_text_request},
|
||||||
this->handle_text_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
if (match.domain_equals("select")) {
|
{"select", &WebServer::handle_select_request},
|
||||||
this->handle_select_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
if (match.domain_equals("climate")) {
|
{"climate", &WebServer::handle_climate_request},
|
||||||
this->handle_climate_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LOCK
|
#ifdef USE_LOCK
|
||||||
if (match.domain_equals("lock")) {
|
{"lock", &WebServer::handle_lock_request},
|
||||||
this->handle_lock_request(request, match);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_VALVE
|
#ifdef USE_VALVE
|
||||||
if (match.domain_equals("valve")) {
|
{"valve", &WebServer::handle_valve_request},
|
||||||
this->handle_valve_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
if (match.domain_equals("alarm_control_panel")) {
|
{"alarm_control_panel", &WebServer::handle_alarm_control_panel_request},
|
||||||
this->handle_alarm_control_panel_request(request, match);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
if (match.domain_equals("update")) {
|
{"update", &WebServer::handle_update_request},
|
||||||
this->handle_update_request(request, match);
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check each route
|
||||||
|
for (const auto &route : ROUTES) {
|
||||||
|
if (match.domain_equals(route.domain)) {
|
||||||
|
(this->*route.handler)(request, match);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
}
|
||||||
|
|
||||||
// No matching handler found - send 404
|
// No matching handler found - send 404
|
||||||
ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str());
|
ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str());
|
||||||
request->send(404, "text/plain", "Not Found");
|
request->send(404, "text/plain", "Not Found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,4 +40,4 @@ async def to_code(config):
|
|||||||
if CORE.is_esp8266:
|
if CORE.is_esp8266:
|
||||||
cg.add_library("ESP8266WiFi", None)
|
cg.add_library("ESP8266WiFi", None)
|
||||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
|
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")
|
||||||
|
@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
|||||||
|
|
||||||
#ifdef USE_WEBSERVER_SORTING
|
#ifdef USE_WEBSERVER_SORTING
|
||||||
for (auto &group : ws->sorting_groups_) {
|
for (auto &group : ws->sorting_groups_) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
message = json::build_json([group](JsonObject root) {
|
message = json::build_json([group](JsonObject root) {
|
||||||
root["name"] = group.second.name;
|
root["name"] = group.second.name;
|
||||||
root["sorting_weight"] = group.second.weight;
|
root["sorting_weight"] = group.second.weight;
|
||||||
});
|
});
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
|
|
||||||
// a (very) large number of these should be able to be queued initially without defer
|
// a (very) large number of these should be able to be queued initially without defer
|
||||||
// since the only thing in the send buffer at this point is the initial ping/config
|
// since the only thing in the send buffer at this point is the initial ping/config
|
||||||
|
231
esphome/components/zephyr/__init__.py
Normal file
231
esphome/components/zephyr/__init__.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import os
|
||||||
|
from typing import Final, TypedDict
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import CONF_BOARD
|
||||||
|
from esphome.core import CORE
|
||||||
|
from esphome.helpers import copy_file_if_changed, write_file_if_changed
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
BOOTLOADER_MCUBOOT,
|
||||||
|
KEY_BOOTLOADER,
|
||||||
|
KEY_EXTRA_BUILD_FILES,
|
||||||
|
KEY_OVERLAY,
|
||||||
|
KEY_PM_STATIC,
|
||||||
|
KEY_PRJ_CONF,
|
||||||
|
KEY_ZEPHYR,
|
||||||
|
zephyr_ns,
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@tomaszduda23"]
|
||||||
|
AUTO_LOAD = ["preferences"]
|
||||||
|
KEY_BOARD: Final = "board"
|
||||||
|
|
||||||
|
PrjConfValueType = bool | str | int
|
||||||
|
|
||||||
|
|
||||||
|
class Section:
|
||||||
|
def __init__(self, name, address, size, region):
|
||||||
|
self.name = name
|
||||||
|
self.address = address
|
||||||
|
self.size = size
|
||||||
|
self.region = region
|
||||||
|
self.end_address = self.address + self.size
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
f"{self.name}:\n"
|
||||||
|
f" address: 0x{self.address:X}\n"
|
||||||
|
f" end_address: 0x{self.end_address:X}\n"
|
||||||
|
f" region: {self.region}\n"
|
||||||
|
f" size: 0x{self.size:X}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ZephyrData(TypedDict):
|
||||||
|
board: str
|
||||||
|
bootloader: str
|
||||||
|
prj_conf: dict[str, tuple[PrjConfValueType, bool]]
|
||||||
|
overlay: str
|
||||||
|
extra_build_files: dict[str, str]
|
||||||
|
pm_static: list[Section]
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_set_core_data(config):
|
||||||
|
CORE.data[KEY_ZEPHYR] = ZephyrData(
|
||||||
|
board=config[CONF_BOARD],
|
||||||
|
bootloader=config[KEY_BOOTLOADER],
|
||||||
|
prj_conf={},
|
||||||
|
overlay="",
|
||||||
|
extra_build_files={},
|
||||||
|
pm_static=[],
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_data() -> ZephyrData:
|
||||||
|
return CORE.data[KEY_ZEPHYR]
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_add_prj_conf(
|
||||||
|
name: str, value: PrjConfValueType, required: bool = True
|
||||||
|
) -> None:
|
||||||
|
"""Set an zephyr prj conf value."""
|
||||||
|
if not name.startswith("CONFIG_"):
|
||||||
|
name = "CONFIG_" + name
|
||||||
|
prj_conf = zephyr_data()[KEY_PRJ_CONF]
|
||||||
|
if name not in prj_conf:
|
||||||
|
prj_conf[name] = (value, required)
|
||||||
|
return
|
||||||
|
old_value, old_required = prj_conf[name]
|
||||||
|
if old_value != value and old_required:
|
||||||
|
raise ValueError(
|
||||||
|
f"{name} already set with value '{old_value}', cannot set again to '{value}'"
|
||||||
|
)
|
||||||
|
if required:
|
||||||
|
prj_conf[name] = (value, required)
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_add_overlay(content):
|
||||||
|
zephyr_data()[KEY_OVERLAY] += content
|
||||||
|
|
||||||
|
|
||||||
|
def add_extra_build_file(filename: str, path: str) -> bool:
|
||||||
|
"""Add an extra build file to the project."""
|
||||||
|
extra_build_files = zephyr_data()[KEY_EXTRA_BUILD_FILES]
|
||||||
|
if filename not in extra_build_files:
|
||||||
|
extra_build_files[filename] = path
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_extra_script(stage: str, filename: str, path: str):
|
||||||
|
"""Add an extra script to the project."""
|
||||||
|
key = f"{stage}:{filename}"
|
||||||
|
if add_extra_build_file(filename, path):
|
||||||
|
cg.add_platformio_option("extra_scripts", [key])
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_to_code(config):
|
||||||
|
cg.add(zephyr_ns.setup_preferences())
|
||||||
|
cg.add_build_flag("-DUSE_ZEPHYR")
|
||||||
|
cg.set_cpp_standard("gnu++20")
|
||||||
|
# build is done by west so bypass board checking in platformio
|
||||||
|
cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards"))
|
||||||
|
|
||||||
|
# c++ support
|
||||||
|
zephyr_add_prj_conf("NEWLIB_LIBC", True)
|
||||||
|
zephyr_add_prj_conf("CONFIG_FPU", True)
|
||||||
|
zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True)
|
||||||
|
zephyr_add_prj_conf("CPLUSPLUS", True)
|
||||||
|
zephyr_add_prj_conf("CONFIG_STD_CPP20", True)
|
||||||
|
zephyr_add_prj_conf("LIB_CPLUSPLUS", True)
|
||||||
|
# preferences
|
||||||
|
zephyr_add_prj_conf("SETTINGS", True)
|
||||||
|
zephyr_add_prj_conf("NVS", True)
|
||||||
|
zephyr_add_prj_conf("FLASH_MAP", True)
|
||||||
|
zephyr_add_prj_conf("CONFIG_FLASH", True)
|
||||||
|
# watchdog
|
||||||
|
zephyr_add_prj_conf("WATCHDOG", True)
|
||||||
|
zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False)
|
||||||
|
# disable console
|
||||||
|
zephyr_add_prj_conf("UART_CONSOLE", False)
|
||||||
|
zephyr_add_prj_conf("CONSOLE", False, False)
|
||||||
|
# use NFC pins as GPIO
|
||||||
|
zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True)
|
||||||
|
|
||||||
|
# <err> os: ***** USAGE FAULT *****
|
||||||
|
# <err> os: Illegal load of EXC_RETURN into PC
|
||||||
|
zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048)
|
||||||
|
|
||||||
|
add_extra_script(
|
||||||
|
"pre",
|
||||||
|
"pre_build.py",
|
||||||
|
os.path.join(os.path.dirname(__file__), "pre_build.py.script"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_prj_conf_val(value: PrjConfValueType) -> str:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "y" if value else "n"
|
||||||
|
if isinstance(value, int):
|
||||||
|
return str(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
return f'"{value}"'
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_add_cdc_acm(config, id):
|
||||||
|
zephyr_add_prj_conf("USB_DEVICE_STACK", True)
|
||||||
|
zephyr_add_prj_conf("USB_CDC_ACM", True)
|
||||||
|
# prevent device to go to susspend, without this communication stop working in python
|
||||||
|
# there should be a way to solve it
|
||||||
|
zephyr_add_prj_conf("USB_DEVICE_REMOTE_WAKEUP", False)
|
||||||
|
# prevent logging when buffer is full
|
||||||
|
zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True)
|
||||||
|
zephyr_add_overlay(
|
||||||
|
f"""
|
||||||
|
&zephyr_udc0 {{
|
||||||
|
cdc_acm_uart{id}: cdc_acm_uart{id} {{
|
||||||
|
compatible = "zephyr,cdc-acm-uart";
|
||||||
|
}};
|
||||||
|
}};
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_add_pm_static(section: Section):
|
||||||
|
CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_files():
|
||||||
|
want_opts = zephyr_data()[KEY_PRJ_CONF]
|
||||||
|
|
||||||
|
prj_conf = (
|
||||||
|
"\n".join(
|
||||||
|
f"{name}={_format_prj_conf_val(value[0])}"
|
||||||
|
for name, value in sorted(want_opts.items())
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf)
|
||||||
|
|
||||||
|
write_file_if_changed(
|
||||||
|
CORE.relative_build_path("zephyr/app.overlay"),
|
||||||
|
zephyr_data()[KEY_OVERLAY],
|
||||||
|
)
|
||||||
|
|
||||||
|
if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[
|
||||||
|
KEY_BOARD
|
||||||
|
] in ["xiao_ble"]:
|
||||||
|
fake_board_manifest = """
|
||||||
|
{
|
||||||
|
"frameworks": [
|
||||||
|
"zephyr"
|
||||||
|
],
|
||||||
|
"name": "esphome nrf52",
|
||||||
|
"upload": {
|
||||||
|
"maximum_ram_size": 248832,
|
||||||
|
"maximum_size": 815104
|
||||||
|
},
|
||||||
|
"url": "https://esphome.io/",
|
||||||
|
"vendor": "esphome"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
write_file_if_changed(
|
||||||
|
CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"),
|
||||||
|
fake_board_manifest,
|
||||||
|
)
|
||||||
|
|
||||||
|
for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items():
|
||||||
|
copy_file_if_changed(
|
||||||
|
path,
|
||||||
|
CORE.relative_build_path(filename),
|
||||||
|
)
|
||||||
|
|
||||||
|
pm_static = "\n".join(str(item) for item in zephyr_data()[KEY_PM_STATIC])
|
||||||
|
if pm_static:
|
||||||
|
write_file_if_changed(
|
||||||
|
CORE.relative_build_path("zephyr/pm_static.yml"), pm_static
|
||||||
|
)
|
14
esphome/components/zephyr/const.py
Normal file
14
esphome/components/zephyr/const.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
|
||||||
|
BOOTLOADER_MCUBOOT = "mcuboot"
|
||||||
|
|
||||||
|
KEY_BOOTLOADER: Final = "bootloader"
|
||||||
|
KEY_EXTRA_BUILD_FILES: Final = "extra_build_files"
|
||||||
|
KEY_OVERLAY: Final = "overlay"
|
||||||
|
KEY_PM_STATIC: Final = "pm_static"
|
||||||
|
KEY_PRJ_CONF: Final = "prj_conf"
|
||||||
|
KEY_ZEPHYR = "zephyr"
|
||||||
|
|
||||||
|
zephyr_ns = cg.esphome_ns.namespace("zephyr")
|
86
esphome/components/zephyr/core.cpp
Normal file
86
esphome/components/zephyr/core.cpp
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/drivers/watchdog.h>
|
||||||
|
#include <zephyr/sys/reboot.h>
|
||||||
|
#include <zephyr/random/rand32.h>
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
|
||||||
|
static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0));
|
||||||
|
|
||||||
|
void yield() { ::k_yield(); }
|
||||||
|
uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); }
|
||||||
|
uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); }
|
||||||
|
void delayMicroseconds(uint32_t us) { ::k_usleep(us); }
|
||||||
|
void delay(uint32_t ms) { ::k_msleep(ms); }
|
||||||
|
|
||||||
|
void arch_init() {
|
||||||
|
if (device_is_ready(WDT)) {
|
||||||
|
static wdt_timeout_cfg wdt_config{};
|
||||||
|
wdt_config.flags = WDT_FLAG_RESET_SOC;
|
||||||
|
wdt_config.window.max = 2000;
|
||||||
|
wdt_channel_id = wdt_install_timeout(WDT, &wdt_config);
|
||||||
|
if (wdt_channel_id >= 0) {
|
||||||
|
wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void arch_feed_wdt() {
|
||||||
|
if (wdt_channel_id >= 0) {
|
||||||
|
wdt_feed(WDT, wdt_channel_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
|
||||||
|
uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
|
||||||
|
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
|
||||||
|
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||||
|
|
||||||
|
Mutex::Mutex() {
|
||||||
|
auto *mutex = new k_mutex();
|
||||||
|
this->handle_ = mutex;
|
||||||
|
k_mutex_init(mutex);
|
||||||
|
}
|
||||||
|
Mutex::~Mutex() { delete static_cast<k_mutex *>(this->handle_); }
|
||||||
|
void Mutex::lock() { k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_FOREVER); }
|
||||||
|
bool Mutex::try_lock() { return k_mutex_lock(static_cast<k_mutex *>(this->handle_), K_NO_WAIT) == 0; }
|
||||||
|
void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); }
|
||||||
|
|
||||||
|
IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); }
|
||||||
|
IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); }
|
||||||
|
|
||||||
|
uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp)
|
||||||
|
bool random_bytes(uint8_t *data, size_t len) {
|
||||||
|
sys_rand_get(data, len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
|
mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0;
|
||||||
|
mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF;
|
||||||
|
mac[2] = NRF_FICR->DEVICEADDR[0] >> 24;
|
||||||
|
mac[3] = NRF_FICR->DEVICEADDR[0] >> 16;
|
||||||
|
mac[4] = NRF_FICR->DEVICEADDR[0] >> 8;
|
||||||
|
mac[5] = NRF_FICR->DEVICEADDR[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
setup();
|
||||||
|
while (true) {
|
||||||
|
loop();
|
||||||
|
esphome::yield();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
120
esphome/components/zephyr/gpio.cpp
Normal file
120
esphome/components/zephyr/gpio.cpp
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
#include "gpio.h"
|
||||||
|
#include <zephyr/drivers/gpio.h>
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace zephyr {
|
||||||
|
|
||||||
|
static const char *const TAG = "zephyr";
|
||||||
|
|
||||||
|
static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) {
|
||||||
|
int ret = 0;
|
||||||
|
if (flags & gpio::FLAG_INPUT) {
|
||||||
|
ret |= GPIO_INPUT;
|
||||||
|
}
|
||||||
|
if (flags & gpio::FLAG_OUTPUT) {
|
||||||
|
ret |= GPIO_OUTPUT;
|
||||||
|
if (value != inverted) {
|
||||||
|
ret |= GPIO_OUTPUT_INIT_HIGH;
|
||||||
|
} else {
|
||||||
|
ret |= GPIO_OUTPUT_INIT_LOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags & gpio::FLAG_PULLUP) {
|
||||||
|
ret |= GPIO_PULL_UP;
|
||||||
|
}
|
||||||
|
if (flags & gpio::FLAG_PULLDOWN) {
|
||||||
|
ret |= GPIO_PULL_DOWN;
|
||||||
|
}
|
||||||
|
if (flags & gpio::FLAG_OPEN_DRAIN) {
|
||||||
|
ret |= GPIO_OPEN_DRAIN;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ISRPinArg {
|
||||||
|
uint8_t pin;
|
||||||
|
bool inverted;
|
||||||
|
};
|
||||||
|
|
||||||
|
ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const {
|
||||||
|
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
arg->pin = this->pin_;
|
||||||
|
arg->inverted = this->inverted_;
|
||||||
|
return ISRInternalGPIOPin((void *) arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZephyrGPIOPin::setup() {
|
||||||
|
const struct device *gpio = nullptr;
|
||||||
|
if (this->pin_ < 32) {
|
||||||
|
#define GPIO0 DT_NODELABEL(gpio0)
|
||||||
|
#if DT_NODE_HAS_STATUS(GPIO0, okay)
|
||||||
|
gpio = DEVICE_DT_GET(GPIO0);
|
||||||
|
#else
|
||||||
|
#error "gpio0 is disabled"
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
#define GPIO1 DT_NODELABEL(gpio1)
|
||||||
|
#if DT_NODE_HAS_STATUS(GPIO1, okay)
|
||||||
|
gpio = DEVICE_DT_GET(GPIO1);
|
||||||
|
#else
|
||||||
|
#error "gpio1 is disabled"
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (device_is_ready(gpio)) {
|
||||||
|
this->gpio_ = gpio;
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->pin_mode(this->flags_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZephyrGPIOPin::pin_mode(gpio::Flags flags) {
|
||||||
|
if (nullptr == this->gpio_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ZephyrGPIOPin::dump_summary() const {
|
||||||
|
char buffer[32];
|
||||||
|
snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ZephyrGPIOPin::digital_read() {
|
||||||
|
if (nullptr == this->gpio_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZephyrGPIOPin::digital_write(bool value) {
|
||||||
|
// make sure that value is not ignored since it can be inverted e.g. on switch side
|
||||||
|
// that way init state should be correct
|
||||||
|
this->value_ = value;
|
||||||
|
if (nullptr == this->gpio_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0);
|
||||||
|
}
|
||||||
|
void ZephyrGPIOPin::detach_interrupt() const {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace zephyr
|
||||||
|
|
||||||
|
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||||
|
// TODO
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
38
esphome/components/zephyr/gpio.h
Normal file
38
esphome/components/zephyr/gpio.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
struct device;
|
||||||
|
namespace esphome {
|
||||||
|
namespace zephyr {
|
||||||
|
|
||||||
|
class ZephyrGPIOPin : public InternalGPIOPin {
|
||||||
|
public:
|
||||||
|
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||||
|
void set_inverted(bool inverted) { this->inverted_ = inverted; }
|
||||||
|
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void pin_mode(gpio::Flags flags) override;
|
||||||
|
bool digital_read() override;
|
||||||
|
void digital_write(bool value) override;
|
||||||
|
std::string dump_summary() const override;
|
||||||
|
void detach_interrupt() const override;
|
||||||
|
ISRInternalGPIOPin to_isr() const override;
|
||||||
|
uint8_t get_pin() const override { return this->pin_; }
|
||||||
|
bool is_inverted() const override { return this->inverted_; }
|
||||||
|
gpio::Flags get_flags() const override { return flags_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
|
uint8_t pin_;
|
||||||
|
bool inverted_;
|
||||||
|
gpio::Flags flags_;
|
||||||
|
const device *gpio_ = nullptr;
|
||||||
|
bool value_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace zephyr
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ZEPHYR
|
4
esphome/components/zephyr/pre_build.py.script
Normal file
4
esphome/components/zephyr/pre_build.py.script
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Import("env")
|
||||||
|
|
||||||
|
board_config = env.BoardConfig()
|
||||||
|
board_config.update("frameworks", ["arduino", "zephyr"])
|
156
esphome/components/zephyr/preferences.cpp
Normal file
156
esphome/components/zephyr/preferences.cpp
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include "esphome/core/preferences.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include <zephyr/settings/settings.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace zephyr {
|
||||||
|
|
||||||
|
static const char *const TAG = "zephyr.preferences";
|
||||||
|
|
||||||
|
#define ESPHOME_SETTINGS_KEY "esphome"
|
||||||
|
|
||||||
|
class ZephyrPreferenceBackend : public ESPPreferenceBackend {
|
||||||
|
public:
|
||||||
|
ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; }
|
||||||
|
ZephyrPreferenceBackend(uint32_t type, std::vector<uint8_t> &&data) : data(std::move(data)) { this->type_ = type; }
|
||||||
|
|
||||||
|
bool save(const uint8_t *data, size_t len) override {
|
||||||
|
this->data.resize(len);
|
||||||
|
std::memcpy(this->data.data(), data, len);
|
||||||
|
ESP_LOGVV(TAG, "save key: %u, len: %d", this->type_, len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool load(uint8_t *data, size_t len) override {
|
||||||
|
if (len != this->data.size()) {
|
||||||
|
ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(data, this->data.data(), len);
|
||||||
|
ESP_LOGVV(TAG, "load key: %u, len: %d", this->type_, len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t get_type() const { return this->type_; }
|
||||||
|
std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); }
|
||||||
|
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint32_t type_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ZephyrPreferences : public ESPPreferences {
|
||||||
|
public:
|
||||||
|
void open() {
|
||||||
|
int err = settings_subsys_init();
|
||||||
|
if (err) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct settings_handler settings_cb = {
|
||||||
|
.name = ESPHOME_SETTINGS_KEY,
|
||||||
|
.h_set = load_setting,
|
||||||
|
.h_export = export_settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
err = settings_register(&settings_cb);
|
||||||
|
if (err) {
|
||||||
|
ESP_LOGE(TAG, "setting_register failed, err, %d", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = settings_load_subtree(ESPHOME_SETTINGS_KEY);
|
||||||
|
if (err) {
|
||||||
|
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Loaded %u settings.", this->backends_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
|
||||||
|
return make_preference(length, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
|
||||||
|
for (auto *backend : this->backends_) {
|
||||||
|
if (backend->get_type() == type) {
|
||||||
|
return ESPPreferenceObject(backend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("type %u size %u\n", type, this->backends_.size());
|
||||||
|
auto *pref = new ZephyrPreferenceBackend(type); // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str());
|
||||||
|
this->backends_.push_back(pref);
|
||||||
|
return ESPPreferenceObject(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sync() override {
|
||||||
|
ESP_LOGD(TAG, "Save settings");
|
||||||
|
int err = settings_save();
|
||||||
|
if (err) {
|
||||||
|
ESP_LOGE(TAG, "Cannot save settings, err: %d", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reset() override {
|
||||||
|
ESP_LOGD(TAG, "Reset settings");
|
||||||
|
for (auto *backend : this->backends_) {
|
||||||
|
// save empty delete data
|
||||||
|
backend->data.clear();
|
||||||
|
}
|
||||||
|
sync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::vector<ZephyrPreferenceBackend *> backends_;
|
||||||
|
|
||||||
|
static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) {
|
||||||
|
auto type = parse_hex<uint32_t>(name);
|
||||||
|
if (!type.has_value()) {
|
||||||
|
std::string full_name(ESPHOME_SETTINGS_KEY);
|
||||||
|
full_name += "/";
|
||||||
|
full_name += name;
|
||||||
|
// Delete unusable keys. Otherwise it will stay in flash forever.
|
||||||
|
settings_delete(full_name.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> data(len);
|
||||||
|
int err = read_cb(cb_arg, data.data(), len);
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err);
|
||||||
|
auto *pref = new ZephyrPreferenceBackend(*type, std::move(data)); // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
static_cast<ZephyrPreferences *>(global_preferences)->backends_.push_back(pref);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) {
|
||||||
|
for (auto *backend : static_cast<ZephyrPreferences *>(global_preferences)->backends_) {
|
||||||
|
auto name = backend->get_key();
|
||||||
|
int err = cb(name.c_str(), backend->data.data(), backend->data.size());
|
||||||
|
ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setup_preferences() {
|
||||||
|
auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
global_preferences = prefs;
|
||||||
|
prefs->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace zephyr
|
||||||
|
|
||||||
|
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
13
esphome/components/zephyr/preferences.h
Normal file
13
esphome/components/zephyr/preferences.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace zephyr {
|
||||||
|
|
||||||
|
void setup_preferences();
|
||||||
|
|
||||||
|
} // namespace zephyr
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
@ -21,6 +21,7 @@ class Platform(StrEnum):
|
|||||||
HOST = "host"
|
HOST = "host"
|
||||||
LIBRETINY_OLDSTYLE = "libretiny"
|
LIBRETINY_OLDSTYLE = "libretiny"
|
||||||
LN882X = "ln882x"
|
LN882X = "ln882x"
|
||||||
|
NRF52 = "nrf52"
|
||||||
RP2040 = "rp2040"
|
RP2040 = "rp2040"
|
||||||
RTL87XX = "rtl87xx"
|
RTL87XX = "rtl87xx"
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ class Framework(StrEnum):
|
|||||||
ARDUINO = "arduino"
|
ARDUINO = "arduino"
|
||||||
ESP_IDF = "esp-idf"
|
ESP_IDF = "esp-idf"
|
||||||
NATIVE = "host"
|
NATIVE = "host"
|
||||||
|
ZEPHYR = "zephyr"
|
||||||
|
|
||||||
|
|
||||||
class PlatformFramework(Enum):
|
class PlatformFramework(Enum):
|
||||||
@ -47,6 +49,9 @@ class PlatformFramework(Enum):
|
|||||||
RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO)
|
RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO)
|
||||||
LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO)
|
LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO)
|
||||||
|
|
||||||
|
# Zephyr framework platforms
|
||||||
|
NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR)
|
||||||
|
|
||||||
# Host platform (native)
|
# Host platform (native)
|
||||||
HOST_NATIVE = (Platform.HOST, Framework.NATIVE)
|
HOST_NATIVE = (Platform.HOST, Framework.NATIVE)
|
||||||
|
|
||||||
@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266
|
|||||||
PLATFORM_HOST = Platform.HOST
|
PLATFORM_HOST = Platform.HOST
|
||||||
PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE
|
PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE
|
||||||
PLATFORM_LN882X = Platform.LN882X
|
PLATFORM_LN882X = Platform.LN882X
|
||||||
|
PLATFORM_NRF52 = Platform.NRF52
|
||||||
PLATFORM_RP2040 = Platform.RP2040
|
PLATFORM_RP2040 = Platform.RP2040
|
||||||
PLATFORM_RTL87XX = Platform.RTL87XX
|
PLATFORM_RTL87XX = Platform.RTL87XX
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from esphome.const import (
|
|||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_HOST,
|
PLATFORM_HOST,
|
||||||
PLATFORM_LN882X,
|
PLATFORM_LN882X,
|
||||||
|
PLATFORM_NRF52,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
@ -670,6 +671,10 @@ class EsphomeCore:
|
|||||||
def is_libretiny(self):
|
def is_libretiny(self):
|
||||||
return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x
|
return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_nrf52(self):
|
||||||
|
return self.target_platform == PLATFORM_NRF52
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_host(self):
|
def is_host(self):
|
||||||
return self.target_platform == PLATFORM_HOST
|
return self.target_platform == PLATFORM_HOST
|
||||||
@ -686,6 +691,10 @@ class EsphomeCore:
|
|||||||
def using_esp_idf(self):
|
def using_esp_idf(self):
|
||||||
return self.target_framework == "esp-idf"
|
return self.target_framework == "esp-idf"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def using_zephyr(self):
|
||||||
|
return self.target_framework == "zephyr"
|
||||||
|
|
||||||
def add_job(self, func, *args, **kwargs) -> None:
|
def add_job(self, func, *args, **kwargs) -> None:
|
||||||
self.event_loop.add_job(func, *args, **kwargs)
|
self.event_loop.add_job(func, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -309,6 +309,12 @@ void Application::disable_component_loop_(Component *component) {
|
|||||||
if (this->in_loop_ && i == this->current_loop_index_) {
|
if (this->in_loop_ && i == this->current_loop_index_) {
|
||||||
// Decrement so we'll process the swapped component next
|
// Decrement so we'll process the swapped component next
|
||||||
this->current_loop_index_--;
|
this->current_loop_index_--;
|
||||||
|
// Update the loop start time to current time so the swapped component
|
||||||
|
// gets correct timing instead of inheriting stale timing.
|
||||||
|
// This prevents integer underflow in timing calculations by ensuring
|
||||||
|
// the swapped component starts with a fresh timing reference, avoiding
|
||||||
|
// errors caused by stale or wrapped timing values.
|
||||||
|
this->loop_component_start_time_ = millis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -138,7 +138,7 @@ void Component::call_dump_config() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg);
|
ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), error_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void Component::mark_failed() {
|
void Component::mark_failed() {
|
||||||
ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source());
|
ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source());
|
||||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
this->component_state_ |= COMPONENT_STATE_FAILED;
|
this->component_state_ |= COMPONENT_STATE_FAILED;
|
||||||
this->status_set_error();
|
this->status_set_error();
|
||||||
@ -229,7 +229,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
|
|||||||
}
|
}
|
||||||
void Component::reset_to_construction_state() {
|
void Component::reset_to_construction_state() {
|
||||||
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
|
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
|
||||||
ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source());
|
ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source());
|
||||||
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
this->component_state_ &= ~COMPONENT_STATE_MASK;
|
||||||
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
|
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
|
||||||
// Clear error status when resetting
|
// Clear error status when resetting
|
||||||
@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
|
|||||||
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||||
bool Component::is_ready() const {
|
bool Component::is_ready() const {
|
||||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
||||||
|
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
|
||||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||||
}
|
}
|
||||||
bool Component::can_proceed() { return true; }
|
bool Component::can_proceed() { return true; }
|
||||||
@ -275,14 +276,14 @@ void Component::status_set_warning(const char *message) {
|
|||||||
return;
|
return;
|
||||||
this->component_state_ |= STATUS_LED_WARNING;
|
this->component_state_ |= STATUS_LED_WARNING;
|
||||||
App.app_state_ |= STATUS_LED_WARNING;
|
App.app_state_ |= STATUS_LED_WARNING;
|
||||||
ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message);
|
ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message);
|
||||||
}
|
}
|
||||||
void Component::status_set_error(const char *message) {
|
void Component::status_set_error(const char *message) {
|
||||||
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
||||||
return;
|
return;
|
||||||
this->component_state_ |= STATUS_LED_ERROR;
|
this->component_state_ |= STATUS_LED_ERROR;
|
||||||
App.app_state_ |= STATUS_LED_ERROR;
|
App.app_state_ |= STATUS_LED_ERROR;
|
||||||
ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message);
|
ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message);
|
||||||
if (strcmp(message, "unspecified") != 0) {
|
if (strcmp(message, "unspecified") != 0) {
|
||||||
// Lazy allocate the error messages vector if needed
|
// Lazy allocate the error messages vector if needed
|
||||||
if (!component_error_messages) {
|
if (!component_error_messages) {
|
||||||
@ -303,13 +304,13 @@ void Component::status_clear_warning() {
|
|||||||
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
|
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
|
||||||
return;
|
return;
|
||||||
this->component_state_ &= ~STATUS_LED_WARNING;
|
this->component_state_ &= ~STATUS_LED_WARNING;
|
||||||
ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source());
|
ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source());
|
||||||
}
|
}
|
||||||
void Component::status_clear_error() {
|
void Component::status_clear_error() {
|
||||||
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
|
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
|
||||||
return;
|
return;
|
||||||
this->component_state_ &= ~STATUS_LED_ERROR;
|
this->component_state_ &= ~STATUS_LED_ERROR;
|
||||||
ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source());
|
ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source());
|
||||||
}
|
}
|
||||||
void Component::status_momentary_warning(const std::string &name, uint32_t length) {
|
void Component::status_momentary_warning(const std::string &name, uint32_t length) {
|
||||||
this->status_set_warning();
|
this->status_set_warning();
|
||||||
@ -403,7 +404,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
|||||||
}
|
}
|
||||||
if (should_warn) {
|
if (should_warn) {
|
||||||
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
|
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
|
||||||
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
|
ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
|
||||||
ESP_LOGW(TAG, "Components should block for at most 30 ms");
|
ESP_LOGW(TAG, "Components should block for at most 30 ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,373 +16,186 @@ void ComponentIterator::begin(bool include_internal) {
|
|||||||
this->at_ = 0;
|
this->at_ = 0;
|
||||||
this->include_internal_ = include_internal;
|
this->include_internal_ = include_internal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename PlatformItem>
|
||||||
|
void ComponentIterator::process_platform_item_(const std::vector<PlatformItem *> &items,
|
||||||
|
bool (ComponentIterator::*on_item)(PlatformItem *)) {
|
||||||
|
if (this->at_ >= items.size()) {
|
||||||
|
this->advance_platform_();
|
||||||
|
} else {
|
||||||
|
PlatformItem *item = items[this->at_];
|
||||||
|
if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) {
|
||||||
|
this->at_++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComponentIterator::advance_platform_() {
|
||||||
|
this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1);
|
||||||
|
this->at_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void ComponentIterator::advance() {
|
void ComponentIterator::advance() {
|
||||||
bool advance_platform = false;
|
|
||||||
bool success = true;
|
|
||||||
switch (this->state_) {
|
switch (this->state_) {
|
||||||
case IteratorState::NONE:
|
case IteratorState::NONE:
|
||||||
// not started
|
// not started
|
||||||
return;
|
return;
|
||||||
case IteratorState::BEGIN:
|
case IteratorState::BEGIN:
|
||||||
if (this->on_begin()) {
|
if (this->on_begin()) {
|
||||||
advance_platform = true;
|
advance_platform_();
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
case IteratorState::BINARY_SENSOR:
|
case IteratorState::BINARY_SENSOR:
|
||||||
if (this->at_ >= App.get_binary_sensors().size()) {
|
this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *binary_sensor = App.get_binary_sensors()[this->at_];
|
|
||||||
if (binary_sensor->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_binary_sensor(binary_sensor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
case IteratorState::COVER:
|
case IteratorState::COVER:
|
||||||
if (this->at_ >= App.get_covers().size()) {
|
this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *cover = App.get_covers()[this->at_];
|
|
||||||
if (cover->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_cover(cover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
case IteratorState::FAN:
|
case IteratorState::FAN:
|
||||||
if (this->at_ >= App.get_fans().size()) {
|
this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *fan = App.get_fans()[this->at_];
|
|
||||||
if (fan->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_fan(fan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
case IteratorState::LIGHT:
|
case IteratorState::LIGHT:
|
||||||
if (this->at_ >= App.get_lights().size()) {
|
this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *light = App.get_lights()[this->at_];
|
|
||||||
if (light->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_light(light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
case IteratorState::SENSOR:
|
case IteratorState::SENSOR:
|
||||||
if (this->at_ >= App.get_sensors().size()) {
|
this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *sensor = App.get_sensors()[this->at_];
|
|
||||||
if (sensor->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_sensor(sensor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SWITCH
|
#ifdef USE_SWITCH
|
||||||
case IteratorState::SWITCH:
|
case IteratorState::SWITCH:
|
||||||
if (this->at_ >= App.get_switches().size()) {
|
this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *a_switch = App.get_switches()[this->at_];
|
|
||||||
if (a_switch->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_switch(a_switch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BUTTON
|
#ifdef USE_BUTTON
|
||||||
case IteratorState::BUTTON:
|
case IteratorState::BUTTON:
|
||||||
if (this->at_ >= App.get_buttons().size()) {
|
this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *button = App.get_buttons()[this->at_];
|
|
||||||
if (button->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_button(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
case IteratorState::TEXT_SENSOR:
|
case IteratorState::TEXT_SENSOR:
|
||||||
if (this->at_ >= App.get_text_sensors().size()) {
|
this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *text_sensor = App.get_text_sensors()[this->at_];
|
|
||||||
if (text_sensor->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_text_sensor(text_sensor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
case IteratorState ::SERVICE:
|
case IteratorState::SERVICE:
|
||||||
if (this->at_ >= api::global_api_server->get_user_services().size()) {
|
this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *service = api::global_api_server->get_user_services()[this->at_];
|
|
||||||
success = this->on_service(service);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
case IteratorState::CAMERA:
|
case IteratorState::CAMERA: {
|
||||||
if (camera::Camera::instance() == nullptr) {
|
camera::Camera *camera_instance = camera::Camera::instance();
|
||||||
advance_platform = true;
|
if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) {
|
||||||
} else {
|
this->on_camera(camera_instance);
|
||||||
if (camera::Camera::instance()->is_internal() && !this->include_internal_) {
|
|
||||||
advance_platform = success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
advance_platform = success = this->on_camera(camera::Camera::instance());
|
|
||||||
}
|
}
|
||||||
}
|
advance_platform_();
|
||||||
break;
|
} break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
case IteratorState::CLIMATE:
|
case IteratorState::CLIMATE:
|
||||||
if (this->at_ >= App.get_climates().size()) {
|
this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *climate = App.get_climates()[this->at_];
|
|
||||||
if (climate->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_climate(climate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
case IteratorState::NUMBER:
|
case IteratorState::NUMBER:
|
||||||
if (this->at_ >= App.get_numbers().size()) {
|
this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *number = App.get_numbers()[this->at_];
|
|
||||||
if (number->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_number(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATE
|
#ifdef USE_DATETIME_DATE
|
||||||
case IteratorState::DATETIME_DATE:
|
case IteratorState::DATETIME_DATE:
|
||||||
if (this->at_ >= App.get_dates().size()) {
|
this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *date = App.get_dates()[this->at_];
|
|
||||||
if (date->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_date(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_TIME
|
#ifdef USE_DATETIME_TIME
|
||||||
case IteratorState::DATETIME_TIME:
|
case IteratorState::DATETIME_TIME:
|
||||||
if (this->at_ >= App.get_times().size()) {
|
this->process_platform_item_(App.get_times(), &ComponentIterator::on_time);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *time = App.get_times()[this->at_];
|
|
||||||
if (time->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_time(time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATETIME
|
#ifdef USE_DATETIME_DATETIME
|
||||||
case IteratorState::DATETIME_DATETIME:
|
case IteratorState::DATETIME_DATETIME:
|
||||||
if (this->at_ >= App.get_datetimes().size()) {
|
this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *datetime = App.get_datetimes()[this->at_];
|
|
||||||
if (datetime->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_datetime(datetime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_TEXT
|
#ifdef USE_TEXT
|
||||||
case IteratorState::TEXT:
|
case IteratorState::TEXT:
|
||||||
if (this->at_ >= App.get_texts().size()) {
|
this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *text = App.get_texts()[this->at_];
|
|
||||||
if (text->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_text(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
case IteratorState::SELECT:
|
case IteratorState::SELECT:
|
||||||
if (this->at_ >= App.get_selects().size()) {
|
this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *select = App.get_selects()[this->at_];
|
|
||||||
if (select->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_select(select);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LOCK
|
#ifdef USE_LOCK
|
||||||
case IteratorState::LOCK:
|
case IteratorState::LOCK:
|
||||||
if (this->at_ >= App.get_locks().size()) {
|
this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *a_lock = App.get_locks()[this->at_];
|
|
||||||
if (a_lock->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_lock(a_lock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_VALVE
|
#ifdef USE_VALVE
|
||||||
case IteratorState::VALVE:
|
case IteratorState::VALVE:
|
||||||
if (this->at_ >= App.get_valves().size()) {
|
this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *valve = App.get_valves()[this->at_];
|
|
||||||
if (valve->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_valve(valve);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
case IteratorState::MEDIA_PLAYER:
|
case IteratorState::MEDIA_PLAYER:
|
||||||
if (this->at_ >= App.get_media_players().size()) {
|
this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *media_player = App.get_media_players()[this->at_];
|
|
||||||
if (media_player->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_media_player(media_player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
case IteratorState::ALARM_CONTROL_PANEL:
|
case IteratorState::ALARM_CONTROL_PANEL:
|
||||||
if (this->at_ >= App.get_alarm_control_panels().size()) {
|
this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_];
|
|
||||||
if (a_alarm_control_panel->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_alarm_control_panel(a_alarm_control_panel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
case IteratorState::EVENT:
|
case IteratorState::EVENT:
|
||||||
if (this->at_ >= App.get_events().size()) {
|
this->process_platform_item_(App.get_events(), &ComponentIterator::on_event);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *event = App.get_events()[this->at_];
|
|
||||||
if (event->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_event(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
case IteratorState::UPDATE:
|
case IteratorState::UPDATE:
|
||||||
if (this->at_ >= App.get_updates().size()) {
|
this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update);
|
||||||
advance_platform = true;
|
|
||||||
} else {
|
|
||||||
auto *update = App.get_updates()[this->at_];
|
|
||||||
if (update->is_internal() && !this->include_internal_) {
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
success = this->on_update(update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
case IteratorState::MAX:
|
case IteratorState::MAX:
|
||||||
if (this->on_end()) {
|
if (this->on_end()) {
|
||||||
this->state_ = IteratorState::NONE;
|
this->state_ = IteratorState::NONE;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (advance_platform) {
|
|
||||||
this->state_ = static_cast<IteratorState>(static_cast<uint8_t>(this->state_) + 1);
|
|
||||||
this->at_ = 0;
|
|
||||||
} else if (success) {
|
|
||||||
this->at_++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ComponentIterator::on_end() { return true; }
|
bool ComponentIterator::on_end() { return true; }
|
||||||
bool ComponentIterator::on_begin() { return true; }
|
bool ComponentIterator::on_begin() { return true; }
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
|
@ -171,6 +171,11 @@ class ComponentIterator {
|
|||||||
} state_{IteratorState::NONE};
|
} state_{IteratorState::NONE};
|
||||||
uint16_t at_{0}; // Supports up to 65,535 entities per type
|
uint16_t at_{0}; // Supports up to 65,535 entities per type
|
||||||
bool include_internal_{false};
|
bool include_internal_{false};
|
||||||
|
|
||||||
|
template<typename PlatformItem>
|
||||||
|
void process_platform_item_(const std::vector<PlatformItem *> &items,
|
||||||
|
bool (ComponentIterator::*on_item)(PlatformItem *));
|
||||||
|
void advance_platform_();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -258,7 +258,9 @@ std::string format_hex(const uint8_t *data, size_t length) {
|
|||||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||||
|
|
||||||
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
|
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
|
||||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
|
|
||||||
|
// Shared implementation for uint8_t and string hex formatting
|
||||||
|
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||||
if (data == nullptr || length == 0)
|
if (data == nullptr || length == 0)
|
||||||
return "";
|
return "";
|
||||||
std::string ret;
|
std::string ret;
|
||||||
@ -274,6 +276,10 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
|
|||||||
return ret + " (" + std::to_string(length) + ")";
|
return ret + " (" + std::to_string(length) + ")";
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||||
|
return format_hex_pretty_uint8(data, length, separator, show_length);
|
||||||
|
}
|
||||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
|
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
|
||||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||||
}
|
}
|
||||||
@ -300,20 +306,7 @@ std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator,
|
|||||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||||
}
|
}
|
||||||
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
|
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
|
||||||
if (data.empty())
|
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
|
||||||
return "";
|
|
||||||
std::string ret;
|
|
||||||
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
|
|
||||||
ret.resize(multiple * data.length() - (separator ? 1 : 0));
|
|
||||||
for (size_t i = 0; i < data.length(); i++) {
|
|
||||||
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
|
||||||
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
|
||||||
if (separator && i != data.length() - 1)
|
|
||||||
ret[multiple * i + 2] = separator;
|
|
||||||
}
|
|
||||||
if (show_length && data.length() > 4)
|
|
||||||
return ret + " (" + std::to_string(data.length()) + ")";
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string format_bin(const uint8_t *data, size_t length) {
|
std::string format_bin(const uint8_t *data, size_t length) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -678,7 +679,7 @@ class InterruptLock {
|
|||||||
~InterruptLock();
|
~InterruptLock();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
#if defined(USE_ESP8266) || defined(USE_RP2040)
|
#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR)
|
||||||
uint32_t state_;
|
uint32_t state_;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
@ -783,7 +784,7 @@ template<class T> class RAMAllocator {
|
|||||||
T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); }
|
T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); }
|
||||||
|
|
||||||
T *reallocate(T *p, size_t n, size_t manual_size) {
|
T *reallocate(T *p, size_t n, size_t manual_size) {
|
||||||
size_t size = n * sizeof(T);
|
size_t size = n * manual_size;
|
||||||
T *ptr = nullptr;
|
T *ptr = nullptr;
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
|
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
|
||||||
|
@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
|||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path())
|
"PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path())
|
||||||
)
|
)
|
||||||
|
# Suppress Python syntax warnings from third-party scripts during compilation
|
||||||
|
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
|
||||||
cmd = ["platformio"] + list(args)
|
cmd = ["platformio"] + list(args)
|
||||||
|
|
||||||
if not CORE.verbose:
|
if not CORE.verbose:
|
||||||
|
@ -162,6 +162,9 @@ def get_ini_content():
|
|||||||
# Sort to avoid changing build unflags order
|
# Sort to avoid changing build unflags order
|
||||||
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
||||||
|
|
||||||
|
# Add extra script for C++ flags
|
||||||
|
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||||
|
|
||||||
content = "[platformio]\n"
|
content = "[platformio]\n"
|
||||||
content += f"description = ESPHome {__version__}\n"
|
content += f"description = ESPHome {__version__}\n"
|
||||||
|
|
||||||
@ -222,6 +225,9 @@ def write_platformio_project():
|
|||||||
write_gitignore()
|
write_gitignore()
|
||||||
write_platformio_ini(content)
|
write_platformio_ini(content)
|
||||||
|
|
||||||
|
# Write extra script for C++ specific flags
|
||||||
|
write_cxx_flags_script()
|
||||||
|
|
||||||
|
|
||||||
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
|
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
|
||||||
#pragma once
|
#pragma once
|
||||||
@ -394,3 +400,20 @@ def write_gitignore():
|
|||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
with open(file=path, mode="w", encoding="utf-8") as f:
|
with open(file=path, mode="w", encoding="utf-8") as f:
|
||||||
f.write(GITIGNORE_CONTENT)
|
f.write(GITIGNORE_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
|
||||||
|
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
|
||||||
|
Import("env")
|
||||||
|
|
||||||
|
# Add C++ specific flags
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def write_cxx_flags_script() -> None:
|
||||||
|
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||||
|
contents = CXX_FLAGS_FILE_CONTENTS
|
||||||
|
if not CORE.is_host:
|
||||||
|
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||||
|
contents += "\n"
|
||||||
|
write_file_if_changed(path, contents)
|
||||||
|
@ -35,7 +35,7 @@ build_flags =
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
esphome/noise-c@0.1.10 ; api
|
esphome/noise-c@0.1.10 ; api
|
||||||
improv/Improv@1.2.4 ; improv_serial / esp32_improv
|
improv/Improv@1.2.4 ; improv_serial / esp32_improv
|
||||||
bblanchon/ArduinoJson@6.18.5 ; json
|
bblanchon/ArduinoJson@7.4.2 ; json
|
||||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||||
@ -235,7 +235,7 @@ build_flags =
|
|||||||
-DUSE_ZEPHYR
|
-DUSE_ZEPHYR
|
||||||
-DUSE_NRF52
|
-DUSE_NRF52
|
||||||
lib_deps =
|
lib_deps =
|
||||||
bblanchon/ArduinoJson@7.0.0 ; json
|
bblanchon/ArduinoJson@7.4.2 ; json
|
||||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||||
|
@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
|||||||
esptool==4.9.0
|
esptool==4.9.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20250514.0
|
esphome-dashboard==20250514.0
|
||||||
aioesphomeapi==34.2.1
|
aioesphomeapi==35.0.1
|
||||||
zeroconf==0.147.0
|
zeroconf==0.147.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.18.14 # dashboard_import
|
ruamel.yaml==0.18.14 # dashboard_import
|
||||||
|
@ -62,26 +62,6 @@ def get_clang_tidy_version_from_requirements() -> str:
|
|||||||
return "clang-tidy version not found"
|
return "clang-tidy version not found"
|
||||||
|
|
||||||
|
|
||||||
def extract_platformio_flags() -> str:
|
|
||||||
"""Extract clang-tidy related flags from platformio.ini"""
|
|
||||||
flags: list[str] = []
|
|
||||||
in_clangtidy_section = False
|
|
||||||
|
|
||||||
platformio_path = Path(__file__).parent.parent / "platformio.ini"
|
|
||||||
lines = read_file_lines(platformio_path)
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith("[flags:clangtidy]"):
|
|
||||||
in_clangtidy_section = True
|
|
||||||
continue
|
|
||||||
elif line.startswith("[") and in_clangtidy_section:
|
|
||||||
break
|
|
||||||
elif in_clangtidy_section and line and not line.startswith("#"):
|
|
||||||
flags.append(line)
|
|
||||||
|
|
||||||
return "\n".join(sorted(flags))
|
|
||||||
|
|
||||||
|
|
||||||
def read_file_bytes(path: Path) -> bytes:
|
def read_file_bytes(path: Path) -> bytes:
|
||||||
"""Read bytes from a file."""
|
"""Read bytes from a file."""
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
@ -101,9 +81,10 @@ def calculate_clang_tidy_hash() -> str:
|
|||||||
version = get_clang_tidy_version_from_requirements()
|
version = get_clang_tidy_version_from_requirements()
|
||||||
hasher.update(version.encode())
|
hasher.update(version.encode())
|
||||||
|
|
||||||
# Hash relevant platformio.ini sections
|
# Hash the entire platformio.ini file
|
||||||
pio_flags = extract_platformio_flags()
|
platformio_path = Path(__file__).parent.parent / "platformio.ini"
|
||||||
hasher.update(pio_flags.encode())
|
platformio_content = read_file_bytes(platformio_path)
|
||||||
|
hasher.update(platformio_content)
|
||||||
|
|
||||||
return hasher.hexdigest()
|
return hasher.hexdigest()
|
||||||
|
|
||||||
@ -126,7 +107,8 @@ def write_file_content(path: Path, content: str) -> None:
|
|||||||
def write_hash(hash_value: str) -> None:
|
def write_hash(hash_value: str) -> None:
|
||||||
"""Write hash to file"""
|
"""Write hash to file"""
|
||||||
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
|
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
|
||||||
write_file_content(hash_file, hash_value)
|
# Strip any trailing newlines to ensure consistent formatting
|
||||||
|
write_file_content(hash_file, hash_value.strip() + "\n")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""ESPHome tests package."""
|
0
tests/component_tests/__init__.py
Normal file
0
tests/component_tests/__init__.py
Normal file
0
tests/component_tests/binary_sensor/__init__.py
Normal file
0
tests/component_tests/binary_sensor/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user