1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-15 15:18:16 +00:00

Merge branch 'dev' into defaults2

This commit is contained in:
tomaszduda23 2025-02-22 10:58:35 +01:00 committed by GitHub
commit 24391a65b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2340 changed files with 28436 additions and 38828 deletions

View File

@ -31,7 +31,7 @@
"ms-python.python", "ms-python.python",
"ms-python.pylint", "ms-python.pylint",
"ms-python.flake8", "ms-python.flake8",
"ms-python.black-formatter", "charliermarsh.ruff",
"visualstudioexptteam.vscodeintellicode", "visualstudioexptteam.vscodeintellicode",
// yaml // yaml
"redhat.vscode-yaml", "redhat.vscode-yaml",
@ -49,14 +49,11 @@
"flake8.args": [ "flake8.args": [
"--config=${workspaceFolder}/.flake8" "--config=${workspaceFolder}/.flake8"
], ],
"black-formatter.args": [ "ruff.configuration": "${workspaceFolder}/pyproject.toml",
"--config",
"${workspaceFolder}/pyproject.toml"
],
"[python]": { "[python]": {
// VS will say "Value is not accepted" before building the devcontainer, but the warning // VS will say "Value is not accepted" before building the devcontainer, but the warning
// should go away after build is completed. // should go away after build is completed.
"editor.defaultFormatter": "ms-python.black-formatter" "editor.defaultFormatter": "charliermarsh.ruff"
}, },
"editor.formatOnPaste": false, "editor.formatOnPaste": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,

View File

@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@v6.12.0 uses: docker/build-push-action@v6.14.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@ -72,7 +72,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@v6.12.0 uses: docker/build-push-action@v6.14.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false

View File

@ -17,12 +17,12 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.2.0 uses: actions/cache/restore@v4.2.1
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length

View File

@ -23,7 +23,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.11" python-version: "3.11"

View File

@ -33,22 +33,20 @@ concurrency:
jobs: jobs:
check-docker: check-docker:
name: Build docker containers name: Build docker containers
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: [amd64, armv7, aarch64] os: ["ubuntu-latest", "ubuntu-24.04-arm"]
build_type: ["ha-addon", "docker", "lint"] build_type: ["ha-addon", "docker", "lint"]
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0 uses: docker/setup-buildx-action@v3.9.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
- name: Set TAG - name: Set TAG
run: | run: |
@ -58,6 +56,6 @@ jobs:
run: | run: |
docker/build.py \ docker/build.py \
--tag "${TAG}" \ --tag "${TAG}" \
--arch "${{ matrix.arch }}" \ --arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
--build-type "${{ matrix.build_type }}" \ --build-type "${{ matrix.build_type }}" \
build build

View File

@ -42,12 +42,12 @@ jobs:
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $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.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.0 uses: actions/cache@v4.2.1
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -61,8 +61,8 @@ jobs:
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e . pip install -e .
black: ruff:
name: Check black name: Check ruff
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
@ -74,10 +74,10 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run black - name: Run Ruff
run: | run: |
. venv/bin/activate . venv/bin/activate
black --verbose esphome tests ruff format esphome tests
- name: Suggested changes - name: Suggested changes
run: script/ci-suggest-changes run: script/ci-suggest-changes
if: always() if: always()
@ -255,7 +255,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- black - ruff
- ci-custom - ci-custom
- clang-format - clang-format
- flake8 - flake8
@ -303,14 +303,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@v4.2.0 uses: actions/cache@v4.2.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.2.0 uses: actions/cache/restore@v4.2.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}
@ -482,7 +482,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- black - ruff
- ci-custom - ci-custom
- clang-format - clang-format
- flake8 - flake8

View File

@ -1,11 +1,11 @@
{ {
"problemMatcher": [ "problemMatcher": [
{ {
"owner": "black", "owner": "ruff",
"severity": "error", "severity": "error",
"pattern": [ "pattern": [
{ {
"regexp": "^(.*): (Please format this file with the black formatter)", "regexp": "^(.*): (Please format this file with the ruff formatter)",
"file": 1, "file": 1,
"message": 2 "message": 2
} }

View File

@ -53,7 +53,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment - name: Set up python environment
@ -65,7 +65,7 @@ jobs:
pip3 install build pip3 install build
python3 -m build python3 -m build
- name: Publish - name: Publish
uses: pypa/gh-action-pypi-publish@v1.12.3 uses: pypa/gh-action-pypi-publish@v1.12.4
deploy-docker: deploy-docker:
name: Build ESPHome ${{ matrix.platform }} name: Build ESPHome ${{ matrix.platform }}
@ -80,20 +80,19 @@ jobs:
matrix: matrix:
platform: platform:
- linux/amd64 - linux/amd64
- linux/arm/v7
- linux/arm64 - linux/arm64
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0 uses: docker/setup-buildx-action@v3.9.0
- name: Set up QEMU - name: Set up QEMU
if: matrix.platform != 'linux/amd64' if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.4.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@v3.3.0 uses: docker/login-action@v3.3.0
@ -184,7 +183,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0 uses: docker/setup-buildx-action@v3.9.0
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'

View File

@ -22,7 +22,7 @@ jobs:
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.4.0
with: with:
python-version: 3.12 python-version: 3.12

View File

@ -4,7 +4,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.5.4 rev: v0.9.2
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@ -45,6 +45,6 @@ repos:
hooks: hooks:
- id: pylint - id: pylint
name: pylint name: pylint
entry: python script/run-in-env pylint entry: python3 script/run-in-env.py pylint
language: system language: system
types: [python] types: [python]

View File

@ -148,6 +148,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/ezo_pmp/* @carlos-sarmiento
@ -233,6 +234,7 @@ esphome/components/kuntze/* @ssieb
esphome/components/lcd_menu/* @numo68 esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2420/* @descipher esphome/components/ld2420/* @descipher
esphome/components/ld2450/* @hareeshmu
esphome/components/ledc/* @OttoWinter esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2
@ -241,6 +243,7 @@ esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow
esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita esphome/components/ltr_als_ps/* @latonita
@ -276,6 +279,7 @@ esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi/* @RubyBailey
esphome/components/mixer/speaker/* @kahrendt
esphome/components/mlx90393/* @functionpointer esphome/components/mlx90393/* @functionpointer
esphome/components/mlx90614/* @jesserockz esphome/components/mlx90614/* @jesserockz
esphome/components/mmc5603/* @benhoff esphome/components/mmc5603/* @benhoff
@ -342,6 +346,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet esphome/components/rc522_spi/* @glmnet
esphome/components/resampler/speaker/* @kahrendt
esphome/components/restart/* @esphome/core esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz esphome/components/rgbct/* @jesserockz
@ -354,7 +359,7 @@ esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
esphome/components/sdl/* @clydebarrow esphome/components/sdl/* @bdm310 @clydebarrow
esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath esphome/components/sdp3x/* @Azimath
esphome/components/seeed_mr24hpc1/* @limengdu esphome/components/seeed_mr24hpc1/* @limengdu
@ -386,6 +391,7 @@ esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow
@ -496,5 +502,6 @@ esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xiaomi_rtcgq02lm/* @jesserockz
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/zhlt01/* @cfeenstra1024 esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zio_ultrasonic/* @kahrendt

View File

@ -1,12 +1,14 @@
# Contributing to ESPHome # Contributing to ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphome We welcome contributions to the ESPHome suite of code and documentation!
Things to note when contributing: Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
- Please test your changes :) **See also:**
- If a new feature is added or an existing user-facing feature is changed, you should also
update the [docs](https://github.com/esphome/esphome-docs). See [contributing to esphome-docs](https://esphome.io/guides/contributing.html#contributing-to-esphomedocs) [Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
for more information.
- Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files ---
which checks if your new feature compiles correctly.
[![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/)

View File

@ -7,10 +7,10 @@
</picture> </picture>
</a> </a>
**Documentation:** https://esphome.io/ ---
For issues, please go to [the issue tracker](https://github.com/esphome/issues/issues). [Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
For feature requests, please see [feature requests](https://github.com/esphome/feature-requests/issues). ---
[![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/) [![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/)

View File

@ -35,7 +35,7 @@ RUN \
iputils-ping=3:20221126-1+deb12u1 \ iputils-ping=3:20221126-1+deb12u1 \
git=1:2.39.5-0+deb12u1 \ git=1:2.39.5-0+deb12u1 \
curl=7.88.1-10+deb12u8 \ curl=7.88.1-10+deb12u8 \
openssh-client=1:9.2p1-2+deb12u3 \ openssh-client=1:9.2p1-2+deb12u4 \
python3-cffi=1.15.1-5 \ python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \ libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \ libmagic1=1:5.44-3 \
@ -51,19 +51,7 @@ ENV \
# Store globally installed pio libs in /piolibs # Store globally installed pio libs in /piolibs
PLATFORMIO_GLOBALLIB_DIR=/piolibs PLATFORMIO_GLOBALLIB_DIR=/piolibs
# Support legacy binaries on Debian multiarch system. There is no "correct" way
# to do this, other than using properly built toolchains...
# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian
RUN \ RUN \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
fi
RUN \
# Ubuntu python3-pip is missing wheel
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \ pip3 install \
--break-system-packages --no-cache-dir \ --break-system-packages --no-cache-dir \
# Keep platformio version in sync with requirements.txt # Keep platformio version in sync with requirements.txt
@ -82,14 +70,6 @@ RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
# Fail on any non-zero status # Fail on any non-zero status
set -e set -e
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
then
curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple";
fi
# install build tools in case wheels are not available # install build tools in case wheels are not available
BUILD_DEPS=" BUILD_DEPS="
build-essential=12.9 build-essential=12.9
@ -106,7 +86,7 @@ LIB_DEPS="
libtiff6=4.5.0-6+deb12u1 libtiff6=4.5.0-6+deb12u1
libopenjp2-7=2.5.0-2 libopenjp2-7=2.5.0-2
" "
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ] if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
then then
apt-get update apt-get update
apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS
@ -115,7 +95,7 @@ fi
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo
pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ] if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
then then
apt-get remove -y --purge --auto-remove $BUILD_DEPS apt-get remove -y --purge --auto-remove $BUILD_DEPS
rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/* rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*
@ -135,11 +115,7 @@ FROM base AS docker
# Copy esphome and install # Copy esphome and install
COPY . /esphome COPY . /esphome
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir -e /esphome
# Settings for dashboard # Settings for dashboard
ENV USERNAME="" PASSWORD="" ENV USERNAME="" PASSWORD=""
@ -197,11 +173,7 @@ COPY docker/ha-addon-rootfs/ /
# Copy esphome and install # Copy esphome and install
COPY . /esphome COPY . /esphome
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir -e /esphome
# Labels # Labels
LABEL \ LABEL \
@ -232,21 +204,14 @@ RUN \
nano=7.2-1+deb12u1 \ nano=7.2-1+deb12u1 \
build-essential=12.9 \ build-essential=12.9 \
python3-dev=3.11.2-1+b1 \ python3-dev=3.11.2-1+b1 \
&& if [ "$TARGETARCH$TARGETVARIANT" != "armv7" ]; then \ clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 \
# move this up after armv7 is retired && rm -rf \
apt-get install -y --no-install-recommends clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 ; \
fi; \
rm -rf \
/tmp/* \ /tmp/* \
/var/{cache,log}/* \ /var/{cache,log}/* \
/var/lib/apt/lists/* /var/lib/apt/lists/*
COPY requirements_test.txt / COPY requirements_test.txt /
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ RUN pip3 install --break-system-packages --no-cache-dir -r /requirements_test.txt
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir -r /requirements_test.txt
VOLUME ["/esphome"] VOLUME ["/esphome"]
WORKDIR /esphome WORKDIR /esphome

View File

@ -1,22 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from dataclasses import dataclass
import subprocess
import argparse import argparse
from platform import machine from dataclasses import dataclass
import shlex
import re import re
import shlex
import subprocess
import sys import sys
CHANNEL_DEV = "dev" CHANNEL_DEV = "dev"
CHANNEL_BETA = "beta" CHANNEL_BETA = "beta"
CHANNEL_RELEASE = "release" CHANNEL_RELEASE = "release"
CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE]
ARCH_AMD64 = "amd64" ARCH_AMD64 = "amd64"
ARCH_ARMV7 = "armv7"
ARCH_AARCH64 = "aarch64" ARCH_AARCH64 = "aarch64"
ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] ARCHS = [ARCH_AMD64, ARCH_AARCH64]
TYPE_DOCKER = "docker" TYPE_DOCKER = "docker"
TYPE_HA_ADDON = "ha-addon" TYPE_HA_ADDON = "ha-addon"
@ -76,7 +73,6 @@ class DockerParams:
}[build_type] }[build_type]
platform = { platform = {
ARCH_AMD64: "linux/amd64", ARCH_AMD64: "linux/amd64",
ARCH_ARMV7: "linux/arm/v7",
ARCH_AARCH64: "linux/arm64", ARCH_AARCH64: "linux/arm64",
}[arch] }[arch]
target = { target = {

View File

@ -66,7 +66,7 @@ def choose_prompt(options, purpose: str = None):
return options[0][1] return options[0][1]
safe_print( safe_print(
f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:' f"Found multiple options{f' for {purpose}' if purpose else ''}, please choose one:"
) )
for i, (desc, _) in enumerate(options): for i, (desc, _) in enumerate(options):
safe_print(f" [{i + 1}] {desc}") safe_print(f" [{i + 1}] {desc}")

View File

@ -36,6 +36,14 @@ ATTENUATION_MODES = {
"auto": "auto", "auto": "auto",
} }
sampling_mode = adc_ns.enum("SamplingMode", is_class=True)
SAMPLING_MODES = {
"avg": sampling_mode.AVG,
"min": sampling_mode.MIN,
"max": sampling_mode.MAX,
}
adc1_channel_t = cg.global_ns.enum("adc1_channel_t") adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
adc2_channel_t = cg.global_ns.enum("adc2_channel_t") adc2_channel_t = cg.global_ns.enum("adc2_channel_t")

View File

@ -28,6 +28,21 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11;
#endif #endif
#endif // USE_ESP32 #endif // USE_ESP32
enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 };
const LogString *sampling_mode_to_str(SamplingMode mode);
class Aggregator {
public:
void add_sample(uint32_t value);
uint32_t aggregate();
Aggregator(SamplingMode mode);
protected:
SamplingMode mode_{SamplingMode::AVG};
uint32_t aggr_{0};
uint32_t samples_{0};
};
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public: public:
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -54,6 +69,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
void set_sample_count(uint8_t sample_count); void set_sample_count(uint8_t sample_count);
void set_sampling_mode(SamplingMode sampling_mode);
float sample() override; float sample() override;
#ifdef USE_ESP8266 #ifdef USE_ESP8266
@ -68,6 +84,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
InternalGPIOPin *pin_; InternalGPIOPin *pin_;
bool output_raw_{false}; bool output_raw_{false};
uint8_t sample_count_{1}; uint8_t sample_count_{1};
SamplingMode sampling_mode_{SamplingMode::AVG};
#ifdef USE_RP2040 #ifdef USE_RP2040
bool is_temperature_{false}; bool is_temperature_{false};

View File

@ -6,6 +6,59 @@ namespace adc {
static const char *const TAG = "adc.common"; static const char *const TAG = "adc.common";
const LogString *sampling_mode_to_str(SamplingMode mode) {
switch (mode) {
case SamplingMode::AVG:
return LOG_STR("average");
case SamplingMode::MIN:
return LOG_STR("minimum");
case SamplingMode::MAX:
return LOG_STR("maximum");
}
return LOG_STR("unknown");
}
Aggregator::Aggregator(SamplingMode mode) {
this->mode_ = mode;
// set to max uint if mode is "min"
if (mode == SamplingMode::MIN) {
this->aggr_ = UINT32_MAX;
}
}
void Aggregator::add_sample(uint32_t value) {
this->samples_ += 1;
switch (this->mode_) {
case SamplingMode::AVG:
this->aggr_ += value;
break;
case SamplingMode::MIN:
if (value < this->aggr_) {
this->aggr_ = value;
}
break;
case SamplingMode::MAX:
if (value > this->aggr_) {
this->aggr_ = value;
}
}
}
uint32_t Aggregator::aggregate() {
if (this->mode_ == SamplingMode::AVG) {
if (this->samples_ == 0) {
return this->aggr_;
}
return (this->aggr_ + (this->samples_ >> 1)) / this->samples_; // NOLINT(clang-analyzer-core.DivideZero)
}
return this->aggr_;
}
void ADCSensor::update() { void ADCSensor::update() {
float value_v = this->sample(); float value_v = this->sample();
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
@ -18,6 +71,8 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
} }
} }
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace adc } // namespace adc

View File

@ -78,12 +78,14 @@ void ADCSensor::dump_config() {
} }
} }
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
} }
float ADCSensor::sample() { float ADCSensor::sample() {
if (!this->autorange_) { if (!this->autorange_) {
uint32_t sum = 0; 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 = -1;
if (this->channel1_ != ADC1_CHANNEL_MAX) { if (this->channel1_ != ADC1_CHANNEL_MAX) {
@ -94,13 +96,14 @@ float ADCSensor::sample() {
if (raw == -1) { if (raw == -1) {
return NAN; return NAN;
} }
sum += raw;
aggr.add_sample(raw);
} }
sum = (sum + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) { if (this->output_raw_) {
return sum; return aggr.aggregate();
} }
uint32_t mv = esp_adc_cal_raw_to_voltage(sum, &this->cal_characteristics_[(int32_t) this->attenuation_]); uint32_t mv =
esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]);
return mv / 1000.0f; return mv / 1000.0f;
} }

View File

@ -31,23 +31,27 @@ void ADCSensor::dump_config() {
LOG_PIN(" Pin: ", this->pin_); LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC #endif // USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
} }
float ADCSensor::sample() { float ADCSensor::sample() {
uint32_t raw = 0; 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++) {
uint32_t raw = 0;
#ifdef USE_ADC_SENSOR_VCC #ifdef USE_ADC_SENSOR_VCC
raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
#else #else
raw += analogRead(this->pin_->get_pin()); // NOLINT raw = analogRead(this->pin_->get_pin()); // NOLINT
#endif // USE_ADC_SENSOR_VCC #endif // USE_ADC_SENSOR_VCC
aggr.add_sample(raw);
} }
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) { if (this->output_raw_) {
return raw; return aggr.aggregate();
} }
return raw / 1024.0f; return aggr.aggregate() / 1024.0f;
} }
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }

View File

@ -23,23 +23,28 @@ void ADCSensor::dump_config() {
LOG_PIN(" Pin: ", this->pin_); LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC #endif // USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
} }
float ADCSensor::sample() { float ADCSensor::sample() {
uint32_t raw = 0; uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_);
if (this->output_raw_) { if (this->output_raw_) {
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogRead(this->pin_->get_pin()); // NOLINT raw = analogRead(this->pin_->get_pin()); // NOLINT
aggr.add_sample(raw);
} }
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) return aggr.aggregate();
return raw;
} }
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT raw = analogReadVoltage(this->pin_->get_pin()); // NOLINT
aggr.add_sample(raw);
} }
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw / 1000.0f; return aggr.aggregate() / 1000.0f;
} }
} // namespace adc } // namespace adc

View File

@ -34,24 +34,28 @@ void ADCSensor::dump_config() {
#endif // USE_ADC_SENSOR_VCC #endif // USE_ADC_SENSOR_VCC
} }
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
} }
float ADCSensor::sample() { float ADCSensor::sample() {
uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_);
if (this->is_temperature_) { if (this->is_temperature_) {
adc_set_temp_sensor_enabled(true); adc_set_temp_sensor_enabled(true);
delay(1); delay(1);
adc_select_input(4); adc_select_input(4);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read(); raw = adc_read();
aggr.add_sample(raw);
} }
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
adc_set_temp_sensor_enabled(false); adc_set_temp_sensor_enabled(false);
if (this->output_raw_) { if (this->output_raw_) {
return raw; return aggr.aggregate();
} }
return raw * 3.3f / 4096.0f; return aggr.aggregate() * 3.3f / 4096.0f;
} }
uint8_t pin = this->pin_->get_pin(); uint8_t pin = this->pin_->get_pin();
@ -68,11 +72,10 @@ float ADCSensor::sample() {
adc_gpio_init(pin); adc_gpio_init(pin);
adc_select_input(pin - 26); adc_select_input(pin - 26);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read(); raw = adc_read();
aggr.add_sample(raw);
} }
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
#ifdef CYW43_USES_VSYS_PIN #ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) { if (pin == PICO_VSYS_PIN) {
@ -81,10 +84,10 @@ float ADCSensor::sample() {
#endif // CYW43_USES_VSYS_PIN #endif // CYW43_USES_VSYS_PIN
if (this->output_raw_) { if (this->output_raw_) {
return raw; return aggr.aggregate();
} }
float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f; float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f;
return raw * 3.3f / 4096.0f * coeff; return aggr.aggregate() * 3.3f / 4096.0f * coeff;
} }
} // namespace adc } // namespace adc

View File

@ -1,11 +1,9 @@
import logging import logging
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.core import CORE
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ATTENUATION, CONF_ATTENUATION,
CONF_ID, CONF_ID,
@ -17,10 +15,14 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.core import CORE
import esphome.final_validate as fv
from . import ( from . import (
ATTENUATION_MODES, ATTENUATION_MODES,
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
SAMPLING_MODES,
adc_ns, adc_ns,
validate_adc_pin, validate_adc_pin,
) )
@ -30,9 +32,11 @@ _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["voltage_sampler"] AUTO_LOAD = ["voltage_sampler"]
CONF_SAMPLES = "samples" CONF_SAMPLES = "samples"
CONF_SAMPLING_MODE = "sampling_mode"
_attenuation = cv.enum(ATTENUATION_MODES, lower=True) _attenuation = cv.enum(ATTENUATION_MODES, lower=True)
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
def validate_config(config): def validate_config(config):
@ -88,6 +92,7 @@ CONFIG_SCHEMA = cv.All(
cv.only_on_esp32, _attenuation cv.only_on_esp32, _attenuation
), ),
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255), cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
} }
) )
.extend(cv.polling_component_schema("60s")), .extend(cv.polling_component_schema("60s")),
@ -112,6 +117,7 @@ async def to_code(config):
cg.add(var.set_output_raw(config[CONF_RAW])) cg.add(var.set_output_raw(config[CONF_RAW]))
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]))
if attenuation := config.get(CONF_ATTENUATION): if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto": if attenuation == "auto":

View File

@ -1381,6 +1381,7 @@ message BluetoothConnectionsFreeResponse {
uint32 free = 1; uint32 free = 1;
uint32 limit = 2; uint32 limit = 2;
repeated uint64 allocated = 3;
} }
message BluetoothGATTErrorResponse { message BluetoothGATTErrorResponse {

View File

@ -6430,6 +6430,10 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar
this->limit = value.as_uint32(); this->limit = value.as_uint32();
return true; return true;
} }
case 3: {
this->allocated.push_back(value.as_uint64());
return true;
}
default: default:
return false; return false;
} }
@ -6437,6 +6441,9 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar
void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->free); buffer.encode_uint32(1, this->free);
buffer.encode_uint32(2, this->limit); buffer.encode_uint32(2, this->limit);
for (auto &it : this->allocated) {
buffer.encode_uint64(3, it, true);
}
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const {
@ -6451,6 +6458,13 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const {
sprintf(buffer, "%" PRIu32, this->limit); sprintf(buffer, "%" PRIu32, this->limit);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
for (const auto &it : this->allocated) {
out.append(" allocated: ");
sprintf(buffer, "%llu", it);
out.append(buffer);
out.append("\n");
}
out.append("}"); out.append("}");
} }
#endif #endif

View File

@ -1624,6 +1624,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage {
public: public:
uint32_t free{0}; uint32_t free{0};
uint32_t limit{0}; uint32_t limit{0};
std::vector<uint64_t> allocated{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;

View File

@ -1,12 +1,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging
from datetime import datetime from datetime import datetime
from typing import Any import logging
from typing import TYPE_CHECKING, Any
from aioesphomeapi import APIClient from aioesphomeapi import APIClient
from aioesphomeapi.api_pb2 import SubscribeLogsResponse
from aioesphomeapi.log_runner import async_run from aioesphomeapi.log_runner import async_run
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
@ -14,6 +13,12 @@ from esphome.core import CORE
from . import CONF_ENCRYPTION from . import CONF_ENCRYPTION
if TYPE_CHECKING:
from aioesphomeapi.api_pb2 import (
SubscribeLogsResponse, # pylint: disable=no-name-in-module
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -1,9 +1,121 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
import esphome.final_validate as fv
CODEOWNERS = ["@kahrendt"] CODEOWNERS = ["@kahrendt"]
audio_ns = cg.esphome_ns.namespace("audio") audio_ns = cg.esphome_ns.namespace("audio")
AudioFile = audio_ns.struct("AudioFile")
AudioFileType = audio_ns.enum("AudioFileType", is_class=True)
AUDIO_FILE_TYPE_ENUM = {
"NONE": AudioFileType.NONE,
"WAV": AudioFileType.WAV,
"MP3": AudioFileType.MP3,
"FLAC": AudioFileType.FLAC,
}
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
CONF_MIN_CHANNELS = "min_channels"
CONF_MAX_CHANNELS = "max_channels"
CONF_MIN_SAMPLE_RATE = "min_sample_rate"
CONF_MAX_SAMPLE_RATE = "max_sample_rate"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema({}), cv.Schema({}),
) )
AUDIO_COMPONENT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_BITS_PER_SAMPLE): cv.int_range(8, 32),
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(8000, 48000),
}
)
_UNDEF = object()
def set_stream_limits(
min_bits_per_sample: int = _UNDEF,
max_bits_per_sample: int = _UNDEF,
min_channels: int = _UNDEF,
max_channels: int = _UNDEF,
min_sample_rate: int = _UNDEF,
max_sample_rate: int = _UNDEF,
):
def set_limits_in_config(config):
if min_bits_per_sample is not _UNDEF:
config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample
if max_bits_per_sample is not _UNDEF:
config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample
if min_channels is not _UNDEF:
config[CONF_MIN_CHANNELS] = min_channels
if max_channels is not _UNDEF:
config[CONF_MAX_CHANNELS] = max_channels
if min_sample_rate is not _UNDEF:
config[CONF_MIN_SAMPLE_RATE] = min_sample_rate
if max_sample_rate is not _UNDEF:
config[CONF_MAX_SAMPLE_RATE] = max_sample_rate
return set_limits_in_config
def final_validate_audio_schema(
name: str,
*,
audio_device: str,
bits_per_sample: int,
channels: int,
sample_rate: int,
):
def validate_audio_compatiblity(audio_config):
audio_schema = {}
try:
cv.int_range(
min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE),
)(bits_per_sample)
except cv.Invalid as exc:
raise cv.Invalid(
f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
) from exc
try:
cv.int_range(
min=audio_config.get(CONF_MIN_CHANNELS),
max=audio_config.get(CONF_MAX_CHANNELS),
)(channels)
except cv.Invalid as exc:
raise cv.Invalid(
f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
) from exc
try:
cv.int_range(
min=audio_config.get(CONF_MIN_SAMPLE_RATE),
max=audio_config.get(CONF_MAX_SAMPLE_RATE),
)(sample_rate)
return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config)
except cv.Invalid as exc:
raise cv.Invalid(
f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}"
) from exc
return cv.Schema(
{
cv.Required(audio_device): fv.id_declaration_match_schema(
validate_audio_compatiblity
)
},
extra=cv.ALLOW_EXTRA,
)
async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.1")

View File

@ -0,0 +1,67 @@
#include "audio.h"
namespace esphome {
namespace audio {
// Euclidean's algorithm for finding the greatest common divisor
static uint32_t gcd(uint32_t a, uint32_t b) {
while (b != 0) {
uint32_t t = b;
b = a % b;
a = t;
}
return a;
}
AudioStreamInfo::AudioStreamInfo(uint8_t bits_per_sample, uint8_t channels, uint32_t sample_rate)
: bits_per_sample_(bits_per_sample), channels_(channels), sample_rate_(sample_rate) {
this->ms_sample_rate_gcd_ = gcd(1000, this->sample_rate_);
this->bytes_per_sample_ = (this->bits_per_sample_ + 7) / 8;
}
uint32_t AudioStreamInfo::frames_to_microseconds(uint32_t frames) const {
return (frames * 1000000 + (this->sample_rate_ >> 1)) / this->sample_rate_;
}
uint32_t AudioStreamInfo::frames_to_milliseconds_with_remainder(uint32_t *total_frames) const {
uint32_t unprocessable_frames = *total_frames % (this->sample_rate_ / this->ms_sample_rate_gcd_);
uint32_t frames_for_ms_calculation = *total_frames - unprocessable_frames;
uint32_t playback_ms = (frames_for_ms_calculation * 1000) / this->sample_rate_;
*total_frames = unprocessable_frames;
return playback_ms;
}
bool AudioStreamInfo::operator==(const AudioStreamInfo &rhs) const {
return (this->bits_per_sample_ == rhs.get_bits_per_sample()) && (this->channels_ == rhs.get_channels()) &&
(this->sample_rate_ == rhs.get_sample_rate());
}
const char *audio_file_type_to_string(AudioFileType file_type) {
switch (file_type) {
#ifdef USE_AUDIO_FLAC_SUPPORT
case AudioFileType::FLAC:
return "FLAC";
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
case AudioFileType::MP3:
return "MP3";
#endif
case AudioFileType::WAV:
return "WAV";
default:
return "unknown";
}
}
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale) {
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
for (int i = 0; i < samples_to_scale; i++) {
int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
output_buffer[i] = (int16_t) (acc >> 15);
}
}
} // namespace audio
} // namespace esphome

View File

@ -1,21 +1,139 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
namespace esphome { namespace esphome {
namespace audio { namespace audio {
struct AudioStreamInfo { class AudioStreamInfo {
bool operator==(const AudioStreamInfo &rhs) const { /* Class to respresent important parameters of the audio stream that also provides helper function to convert between
return (channels == rhs.channels) && (bits_per_sample == rhs.bits_per_sample) && (sample_rate == rhs.sample_rate); * various audio related units.
*
* - An audio sample represents a unit of audio for one channel.
* - A frame represents a unit of audio with a sample for every channel.
*
* In gneneral, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames
* are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates;
* e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes
* into account the remainder rather than just ignoring any rounding.
*/
public:
AudioStreamInfo()
: AudioStreamInfo(16, 1, 16000){}; // Default values represent ESPHome's audio components historical values
AudioStreamInfo(uint8_t bits_per_sample, uint8_t channels, uint32_t sample_rate);
uint8_t get_bits_per_sample() const { return this->bits_per_sample_; }
uint8_t get_channels() const { return this->channels_; }
uint32_t get_sample_rate() const { return this->sample_rate_; }
/// @brief Convert bytes to duration in milliseconds.
/// @param bytes Number of bytes to convert
/// @return Duration in milliseconds that will store `bytes` bytes of audio. May round down for certain sample rates
/// or values of `bytes`.
uint32_t bytes_to_ms(size_t bytes) const {
return bytes * 1000 / (this->sample_rate_ * this->bytes_per_sample_ * this->channels_);
} }
/// @brief Convert bytes to frames.
/// @param bytes Number of bytes to convert
/// @return Audio frames that will store `bytes` bytes.
uint32_t bytes_to_frames(size_t bytes) const { return (bytes / (this->bytes_per_sample_ * this->channels_)); }
/// @brief Convert bytes to samples.
/// @param bytes Number of bytes to convert
/// @return Audio samples that will store `bytes` bytes.
uint32_t bytes_to_samples(size_t bytes) const { return (bytes / this->bytes_per_sample_); }
/// @brief Converts frames to bytes.
/// @param frames Number of frames to convert.
/// @return Number of bytes that will store `frames` frames of audio.
size_t frames_to_bytes(uint32_t frames) const { return frames * this->bytes_per_sample_ * this->channels_; }
/// @brief Converts samples to bytes.
/// @param samples Number of samples to convert.
/// @return Number of bytes that will store `samples` samples of audio.
size_t samples_to_bytes(uint32_t samples) const { return samples * this->bytes_per_sample_; }
/// @brief Converts duration to frames.
/// @param ms Duration in milliseconds
/// @return Audio frames that will store `ms` milliseconds of audio. May round down for certain sample rates.
uint32_t ms_to_frames(uint32_t ms) const { return (ms * this->sample_rate_) / 1000; }
/// @brief Converts duration to samples.
/// @param ms Duration in milliseconds
/// @return Audio samples that will store `ms` milliseconds of audio. May round down for certain sample rates.
uint32_t ms_to_samples(uint32_t ms) const { return (ms * this->channels_ * this->sample_rate_) / 1000; }
/// @brief Converts duration to bytes. May round down for certain sample rates.
/// @param ms Duration in milliseconds
/// @return Bytes that will store `ms` milliseconds of audio. May round down for certain sample rates.
size_t ms_to_bytes(uint32_t ms) const {
return (ms * this->bytes_per_sample_ * this->channels_ * this->sample_rate_) / 1000;
}
/// @brief Computes the duration, in microseconds, the given amount of frames represents.
/// @param frames Number of audio frames
/// @return Duration in microseconds `frames` respresents. May be slightly inaccurate due to integer divison rounding
/// for certain sample rates.
uint32_t frames_to_microseconds(uint32_t frames) const;
/// @brief Computes the duration, in milliseconds, the given amount of frames represents. Avoids
/// accumulating rounding errors by updating `frames` with the remainder after converting.
/// @param frames Pointer to uint32_t with the number of audio frames. Replaced with the remainder.
/// @return Duration in milliseconds `frames` represents. Always less than or equal to the actual value due to
/// rounding.
uint32_t frames_to_milliseconds_with_remainder(uint32_t *frames) const;
// Class comparison operators
bool operator==(const AudioStreamInfo &rhs) const;
bool operator!=(const AudioStreamInfo &rhs) const { return !operator==(rhs); } bool operator!=(const AudioStreamInfo &rhs) const { return !operator==(rhs); }
size_t get_bytes_per_sample() const { return bits_per_sample / 8; }
uint8_t channels = 1; protected:
uint8_t bits_per_sample = 16; uint8_t bits_per_sample_;
uint32_t sample_rate = 16000; uint8_t channels_;
uint32_t sample_rate_;
// The greatest common divisor between 1000 ms = 1 second and the sample rate. Used to avoid accumulating error when
// converting from frames to duration. Computed at construction.
uint32_t ms_sample_rate_gcd_;
// Conversion factor derived from the number of bits per sample. Assumes audio data is aligned to the byte. Computed
// at construction.
size_t bytes_per_sample_;
}; };
enum class AudioFileType : uint8_t {
NONE = 0,
#ifdef USE_AUDIO_FLAC_SUPPORT
FLAC,
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
MP3,
#endif
WAV,
};
struct AudioFile {
const uint8_t *data;
size_t length;
AudioFileType file_type;
};
/// @brief Helper function to convert file type to a const char string
/// @param file_type
/// @return const char pointer to the readable file type
const char *audio_file_type_to_string(AudioFileType file_type);
/// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer.
/// @param audio_samples PCM int16 audio samples
/// @param output_buffer Buffer to store the scaled samples
/// @param scale_factor Q15 fixed point scaling factor
/// @param samples_to_scale Number of samples to scale
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale);
} // namespace audio } // namespace audio
} // namespace esphome } // namespace esphome

View File

@ -0,0 +1,361 @@
#include "audio_decoder.h"
#ifdef USE_ESP32
#include "esphome/core/hal.h"
namespace esphome {
namespace audio {
static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration
static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data
static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10;
AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) {
this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size);
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size);
}
AudioDecoder::~AudioDecoder() {
#ifdef USE_AUDIO_MP3_SUPPORT
if (this->audio_file_type_ == AudioFileType::MP3) {
esp_audio_libs::helix_decoder::MP3FreeDecoder(this->mp3_decoder_);
}
#endif
}
esp_err_t AudioDecoder::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) {
if (this->input_transfer_buffer_ != nullptr) {
this->input_transfer_buffer_->set_source(input_ring_buffer);
return ESP_OK;
}
return ESP_ERR_NO_MEM;
}
esp_err_t AudioDecoder::add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer) {
if (this->output_transfer_buffer_ != nullptr) {
this->output_transfer_buffer_->set_sink(output_ring_buffer);
return ESP_OK;
}
return ESP_ERR_NO_MEM;
}
#ifdef USE_SPEAKER
esp_err_t AudioDecoder::add_sink(speaker::Speaker *speaker) {
if (this->output_transfer_buffer_ != nullptr) {
this->output_transfer_buffer_->set_sink(speaker);
return ESP_OK;
}
return ESP_ERR_NO_MEM;
}
#endif
esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) {
return ESP_ERR_NO_MEM;
}
this->audio_file_type_ = audio_file_type;
this->potentially_failed_count_ = 0;
this->end_of_file_ = false;
switch (this->audio_file_type_) {
#ifdef USE_AUDIO_FLAC_SUPPORT
case AudioFileType::FLAC:
this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>();
this->free_buffer_required_ =
this->output_transfer_buffer_->capacity(); // We'll revise this after reading the header
break;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
case AudioFileType::MP3:
this->mp3_decoder_ = esp_audio_libs::helix_decoder::MP3InitDecoder();
this->free_buffer_required_ = 1152 * sizeof(int16_t) * 2; // samples * size per sample * channels
break;
#endif
case AudioFileType::WAV:
this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>();
this->wav_decoder_->reset();
this->free_buffer_required_ = 1024;
break;
case AudioFileType::NONE:
default:
return ESP_ERR_NOT_SUPPORTED;
break;
}
return ESP_OK;
}
AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
if (stop_gracefully) {
if (this->output_transfer_buffer_->available() == 0) {
if (this->end_of_file_) {
// The file decoder indicates it reached the end of file
return AudioDecoderState::FINISHED;
}
if (!this->input_transfer_buffer_->has_buffered_data()) {
// If all the internal buffers are empty, the decoding is done
return AudioDecoderState::FINISHED;
}
}
}
if (this->potentially_failed_count_ > MAX_POTENTIALLY_FAILED_COUNT) {
if (stop_gracefully) {
// No more new data is going to come in, so decoding is done
return AudioDecoderState::FINISHED;
}
return AudioDecoderState::FAILED;
}
FileDecoderState state = FileDecoderState::MORE_TO_PROCESS;
uint32_t decoding_start = millis();
while (state == FileDecoderState::MORE_TO_PROCESS) {
// Transfer decoded out
if (!this->pause_output_) {
size_t bytes_written = this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
if (this->audio_stream_info_.has_value()) {
this->accumulated_frames_written_ += this->audio_stream_info_.value().bytes_to_frames(bytes_written);
this->playback_ms_ +=
this->audio_stream_info_.value().frames_to_milliseconds_with_remainder(&this->accumulated_frames_written_);
}
} else {
// If paused, block to avoid wasting CPU resources
delay(READ_WRITE_TIMEOUT_MS);
}
// Verify there is enough space to store more decoded audio and that the function hasn't been running too long
if ((this->output_transfer_buffer_->free() < this->free_buffer_required_) ||
(millis() - decoding_start > DECODING_TIMEOUT_MS)) {
return AudioDecoderState::DECODING;
}
// Decode more audio
size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
// Failed to decode in last attempt and there is no new data
if (this->input_transfer_buffer_->free() == 0) {
// The input buffer is full. Since it previously failed on the exact same data, we can never recover
state = FileDecoderState::FAILED;
} else {
// Attempt to get more data next time
state = FileDecoderState::IDLE;
}
} else if (this->input_transfer_buffer_->available() == 0) {
// No data to decode, attempt to get more data next time
state = FileDecoderState::IDLE;
} else {
switch (this->audio_file_type_) {
#ifdef USE_AUDIO_FLAC_SUPPORT
case AudioFileType::FLAC:
state = this->decode_flac_();
break;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
case AudioFileType::MP3:
state = this->decode_mp3_();
break;
#endif
case AudioFileType::WAV:
state = this->decode_wav_();
break;
case AudioFileType::NONE:
default:
state = FileDecoderState::IDLE;
break;
}
}
if (state == FileDecoderState::POTENTIALLY_FAILED) {
++this->potentially_failed_count_;
} else if (state == FileDecoderState::END_OF_FILE) {
this->end_of_file_ = true;
} else if (state == FileDecoderState::FAILED) {
return AudioDecoderState::FAILED;
} else if (state == FileDecoderState::MORE_TO_PROCESS) {
this->potentially_failed_count_ = 0;
}
}
return AudioDecoderState::DECODING;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
FileDecoderState AudioDecoder::decode_flac_() {
if (!this->audio_stream_info_.has_value()) {
// Header hasn't been read
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available());
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::POTENTIALLY_FAILED;
}
if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
// Couldn't read FLAC header
return FileDecoderState::FAILED;
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) {
// Output buffer is not big enough
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
// Couldn't reallocate output buffer
return FileDecoderState::FAILED;
}
}
this->audio_stream_info_ =
audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(),
this->flac_decoder_->get_sample_rate());
return FileDecoderState::MORE_TO_PROCESS;
}
uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame(
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Not an issue, just needs more data that we'll get next time.
return FileDecoderState::POTENTIALLY_FAILED;
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Corrupted frame, don't retry with current buffer content, wait for new sync
return FileDecoderState::POTENTIALLY_FAILED;
}
// We have successfully decoded some input data and have new output data
this->output_transfer_buffer_->increase_buffer_length(
this->audio_stream_info_.value().samples_to_bytes(output_samples));
if (result == esp_audio_libs::flac::FLAC_DECODER_NO_MORE_FRAMES) {
return FileDecoderState::END_OF_FILE;
}
return FileDecoderState::MORE_TO_PROCESS;
}
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
FileDecoderState AudioDecoder::decode_mp3_() {
// Look for the next sync word
int buffer_length = (int) this->input_transfer_buffer_->available();
int32_t offset =
esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_transfer_buffer_->get_buffer_start(), buffer_length);
if (offset < 0) {
// New data may have the sync word
this->input_transfer_buffer_->decrease_buffer_length(buffer_length);
return FileDecoderState::POTENTIALLY_FAILED;
}
// Advance read pointer to match the offset for the syncword
this->input_transfer_buffer_->decrease_buffer_length(offset);
uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
buffer_length = (int) this->input_transfer_buffer_->available();
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,
(int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0);
size_t consumed = this->input_transfer_buffer_->available() - buffer_length;
this->input_transfer_buffer_->decrease_buffer_length(consumed);
if (err) {
switch (err) {
case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY:
// Intentional fallthrough
case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER:
return FileDecoderState::FAILED;
break;
default:
// Most errors are recoverable by moving on to the next frame, so mark as potentailly failed
return FileDecoderState::POTENTIALLY_FAILED;
break;
}
} else {
esp_audio_libs::helix_decoder::MP3FrameInfo mp3_frame_info;
esp_audio_libs::helix_decoder::MP3GetLastFrameInfo(this->mp3_decoder_, &mp3_frame_info);
if (mp3_frame_info.outputSamps > 0) {
int bytes_per_sample = (mp3_frame_info.bitsPerSample / 8);
this->output_transfer_buffer_->increase_buffer_length(mp3_frame_info.outputSamps * bytes_per_sample);
if (!this->audio_stream_info_.has_value()) {
this->audio_stream_info_ =
audio::AudioStreamInfo(mp3_frame_info.bitsPerSample, mp3_frame_info.nChans, mp3_frame_info.samprate);
}
}
}
return FileDecoderState::MORE_TO_PROCESS;
}
#endif
FileDecoderState AudioDecoder::decode_wav_() {
if (!this->audio_stream_info_.has_value()) {
// Header hasn't been processed
esp_audio_libs::wav_decoder::WAVDecoderResult result = this->wav_decoder_->decode_header(
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available());
if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) {
this->input_transfer_buffer_->decrease_buffer_length(this->wav_decoder_->bytes_processed());
this->audio_stream_info_ = audio::AudioStreamInfo(
this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate());
this->wav_bytes_left_ = this->wav_decoder_->chunk_bytes_left();
this->wav_has_known_end_ = (this->wav_bytes_left_ > 0);
return FileDecoderState::MORE_TO_PROCESS;
} else if (result == esp_audio_libs::wav_decoder::WAV_DECODER_WARNING_INCOMPLETE_DATA) {
// Available data didn't have the full header
return FileDecoderState::POTENTIALLY_FAILED;
} else {
return FileDecoderState::FAILED;
}
} else {
if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) {
size_t bytes_to_copy = this->input_transfer_buffer_->available();
if (this->wav_has_known_end_) {
bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_);
}
bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free());
if (bytes_to_copy > 0) {
std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_transfer_buffer_->get_buffer_start(),
bytes_to_copy);
this->input_transfer_buffer_->decrease_buffer_length(bytes_to_copy);
this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy);
if (this->wav_has_known_end_) {
this->wav_bytes_left_ -= bytes_to_copy;
}
}
return FileDecoderState::IDLE;
}
}
return FileDecoderState::END_OF_FILE;
}
} // namespace audio
} // namespace esphome
#endif

View File

@ -0,0 +1,135 @@
#pragma once
#ifdef USE_ESP32
#include "audio.h"
#include "audio_transfer_buffer.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/ring_buffer.h"
#ifdef USE_SPEAKER
#include "esphome/components/speaker/speaker.h"
#endif
#include "esp_err.h"
// esp-audio-libs
#ifdef USE_AUDIO_FLAC_SUPPORT
#include <flac_decoder.h>
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
#include <mp3_decoder.h>
#endif
#include <wav_decoder.h>
namespace esphome {
namespace audio {
enum class AudioDecoderState : uint8_t {
DECODING = 0, // More data is available to decode
FINISHED, // All file data has been decoded and transferred
FAILED, // Encountered an error
};
// Only used within the AudioDecoder class; conveys the state of the particular file type decoder
enum class FileDecoderState : uint8_t {
MORE_TO_PROCESS, // Successsfully read a file chunk and more data is available to decode
IDLE, // Not enough data to decode, waiting for more to be transferred
POTENTIALLY_FAILED, // Decoder encountered a potentially recoverable error if more file data is available
FAILED, // Decoder encoutnered an uncrecoverable error
END_OF_FILE, // The specific file decoder knows its the end of the file
};
class AudioDecoder {
/*
* @brief Class that facilitates decoding an audio file.
* The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker
* component).
* Supports wav, flac, and mp3 formats.
*/
public:
/// @brief Allocates the input and output transfer buffers
/// @param input_buffer_size Size of the input transfer buffer in bytes.
/// @param output_buffer_size Size of the output transfer buffer in bytes.
AudioDecoder(size_t input_buffer_size, size_t output_buffer_size);
/// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically)
~AudioDecoder();
/// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr.
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
esp_err_t add_source(std::weak_ptr<RingBuffer> &input_ring_buffer);
/// @brief Adds a sink ring buffer for decoded audio. Takes ownership of the ring buffer in a shared_ptr.
/// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
esp_err_t add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer);
#ifdef USE_SPEAKER
/// @brief Adds a sink speaker for decoded audio.
/// @param speaker pointer to speaker component
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
esp_err_t add_sink(speaker::Speaker *speaker);
#endif
/// @brief Sets up decoding the file
/// @param audio_file_type AudioFileType of the file
/// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffers fail to allocate, or ESP_ERR_NOT_SUPPORTED if
/// the format isn't supported.
esp_err_t start(AudioFileType audio_file_type);
/// @brief Decodes audio from the ring buffer source and writes to the sink.
/// @param stop_gracefully If true, it indicates the file source is finished. The decoder will decode all the
/// reamining data and then finish.
/// @return AudioDecoderState
AudioDecoderState decode(bool stop_gracefully);
/// @brief Gets the audio stream information, if it has been decoded from the files header
/// @return optional<AudioStreamInfo> with the audio information. If not available yet, returns no value.
const optional<audio::AudioStreamInfo> &get_audio_stream_info() const { return this->audio_stream_info_; }
/// @brief Returns the duration of audio (in milliseconds) decoded and sent to the sink
/// @return Duration of decoded audio in milliseconds
uint32_t get_playback_ms() const { return this->playback_ms_; }
/// @brief Pauses sending resampled audio to the sink. If paused, it will continue to process internal buffers.
/// @param pause_state If true, audio data is not sent to the sink.
void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; }
protected:
std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_;
#ifdef USE_AUDIO_FLAC_SUPPORT
FileDecoderState decode_flac_();
std::unique_ptr<esp_audio_libs::flac::FLACDecoder> flac_decoder_;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
FileDecoderState decode_mp3_();
esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_;
#endif
FileDecoderState decode_wav_();
std::unique_ptr<AudioSourceTransferBuffer> input_transfer_buffer_;
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
AudioFileType audio_file_type_{AudioFileType::NONE};
optional<AudioStreamInfo> audio_stream_info_{};
size_t free_buffer_required_{0};
size_t wav_bytes_left_{0};
uint32_t potentially_failed_count_{0};
bool end_of_file_{false};
bool wav_has_known_end_{false};
bool pause_output_{false};
uint32_t accumulated_frames_written_{0};
uint32_t playback_ms_{0};
};
} // namespace audio
} // namespace esphome
#endif

View File

@ -0,0 +1,308 @@
#include "audio_reader.h"
#ifdef USE_ESP_IDF
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
#endif
namespace esphome {
namespace audio {
static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
// The number of times the http read times out with no data before throwing an error
static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
static const uint8_t MAX_REDIRECTION = 5;
// Some common HTTP status codes - borrowed from http_request component accessed 20241224
enum HttpStatus {
HTTP_STATUS_OK = 200,
HTTP_STATUS_NO_CONTENT = 204,
HTTP_STATUS_PARTIAL_CONTENT = 206,
/* 3xx - Redirection */
HTTP_STATUS_MULTIPLE_CHOICES = 300,
HTTP_STATUS_MOVED_PERMANENTLY = 301,
HTTP_STATUS_FOUND = 302,
HTTP_STATUS_SEE_OTHER = 303,
HTTP_STATUS_NOT_MODIFIED = 304,
HTTP_STATUS_TEMPORARY_REDIRECT = 307,
HTTP_STATUS_PERMANENT_REDIRECT = 308,
/* 4XX - CLIENT ERROR */
HTTP_STATUS_BAD_REQUEST = 400,
HTTP_STATUS_UNAUTHORIZED = 401,
HTTP_STATUS_FORBIDDEN = 403,
HTTP_STATUS_NOT_FOUND = 404,
HTTP_STATUS_METHOD_NOT_ALLOWED = 405,
HTTP_STATUS_NOT_ACCEPTABLE = 406,
HTTP_STATUS_LENGTH_REQUIRED = 411,
/* 5xx - Server Error */
HTTP_STATUS_INTERNAL_ERROR = 500
};
AudioReader::~AudioReader() { this->cleanup_connection_(); }
esp_err_t AudioReader::add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer) {
if (current_audio_file_ != nullptr) {
// A transfer buffer isn't ncessary for a local file
this->file_ring_buffer_ = output_ring_buffer.lock();
return ESP_OK;
}
if (this->output_transfer_buffer_ != nullptr) {
this->output_transfer_buffer_->set_sink(output_ring_buffer);
return ESP_OK;
}
return ESP_ERR_INVALID_STATE;
}
esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
file_type = AudioFileType::NONE;
this->current_audio_file_ = audio_file;
this->file_current_ = audio_file->data;
file_type = audio_file->file_type;
return ESP_OK;
}
esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
file_type = AudioFileType::NONE;
this->cleanup_connection_();
if (uri.empty()) {
return ESP_ERR_INVALID_ARG;
}
esp_http_client_config_t client_config = {};
client_config.url = uri.c_str();
client_config.cert_pem = nullptr;
client_config.disable_auto_redirect = false;
client_config.max_redirection_count = 10;
client_config.event_handler = http_event_handler;
client_config.user_data = this;
client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
client_config.keep_alive_enable = true;
client_config.timeout_ms = 5000; // Shouldn't trigger watchdog resets if caller runs in a task
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (uri.find("https:") != std::string::npos) {
client_config.crt_bundle_attach = esp_crt_bundle_attach;
}
#endif
this->client_ = esp_http_client_init(&client_config);
if (this->client_ == nullptr) {
return ESP_FAIL;
}
esp_err_t err = esp_http_client_open(this->client_, 0);
if (err != ESP_OK) {
this->cleanup_connection_();
return err;
}
int64_t header_length = esp_http_client_fetch_headers(this->client_);
if (header_length < 0) {
this->cleanup_connection_();
return ESP_FAIL;
}
int status_code = esp_http_client_get_status_code(this->client_);
if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
this->cleanup_connection_();
return ESP_FAIL;
}
ssize_t redirect_count = 0;
while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) {
err = esp_http_client_open(this->client_, 0);
if (err != ESP_OK) {
this->cleanup_connection_();
return ESP_FAIL;
}
header_length = esp_http_client_fetch_headers(this->client_);
if (header_length < 0) {
this->cleanup_connection_();
return ESP_FAIL;
}
status_code = esp_http_client_get_status_code(this->client_);
if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
this->cleanup_connection_();
return ESP_FAIL;
}
++redirect_count;
}
if (this->audio_file_type_ == AudioFileType::NONE) {
// Failed to determine the file type from the header, fallback to using the url
char url[500];
err = esp_http_client_get_url(this->client_, url, 500);
if (err != ESP_OK) {
this->cleanup_connection_();
return err;
}
std::string url_string = str_lower_case(url);
if (str_endswith(url_string, ".wav")) {
file_type = AudioFileType::WAV;
}
#ifdef USE_AUDIO_MP3_SUPPORT
else if (str_endswith(url_string, ".mp3")) {
file_type = AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
else if (str_endswith(url_string, ".flac")) {
file_type = AudioFileType::FLAC;
}
#endif
else {
file_type = AudioFileType::NONE;
this->cleanup_connection_();
return ESP_ERR_NOT_SUPPORTED;
}
} else {
file_type = this->audio_file_type_;
}
this->no_data_read_count_ = 0;
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(this->buffer_size_);
if (this->output_transfer_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
AudioReaderState AudioReader::read() {
if (this->client_ != nullptr) {
return this->http_read_();
} else if (this->current_audio_file_ != nullptr) {
return this->file_read_();
}
return AudioReaderState::FAILED;
}
AudioFileType AudioReader::get_audio_type(const char *content_type) {
#ifdef USE_AUDIO_MP3_SUPPORT
if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
strcasecmp(content_type, "audio/mpeg") == 0) {
return AudioFileType::MP3;
}
#endif
if (strcasecmp(content_type, "audio/wav") == 0) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
return AudioFileType::FLAC;
}
#endif
return AudioFileType::NONE;
}
esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
// Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
AudioReader *this_reader = (AudioReader *) evt->user_data;
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER:
if (strcasecmp(evt->header_key, "Content-Type") == 0) {
this_reader->audio_file_type_ = get_audio_type(evt->header_value);
}
break;
default:
break;
}
return ESP_OK;
}
AudioReaderState AudioReader::file_read_() {
size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data);
if (remaining_bytes > 0) {
size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes,
pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
this->file_current_ += bytes_written;
return AudioReaderState::READING;
}
return AudioReaderState::FINISHED;
}
AudioReaderState AudioReader::http_read_() {
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
if (esp_http_client_is_complete_data_received(this->client_)) {
if (this->output_transfer_buffer_->available() == 0) {
this->cleanup_connection_();
return AudioReaderState::FINISHED;
}
} else {
size_t bytes_to_read = this->output_transfer_buffer_->free();
int received_len =
esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);
if (received_len > 0) {
this->output_transfer_buffer_->increase_buffer_length(received_len);
this->no_data_read_count_ = 0;
} else if (received_len < 0) {
// HTTP read error
this->cleanup_connection_();
return AudioReaderState::FAILED;
} else {
if (bytes_to_read > 0) {
// Read timed out
++this->no_data_read_count_;
if (this->no_data_read_count_ >= ERROR_COUNT_NO_DATA_READ_TIMEOUT) {
// Timed out with no data read too many times, so the http read has failed
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
delay(READ_WRITE_TIMEOUT_MS);
}
}
}
return AudioReaderState::READING;
}
void AudioReader::cleanup_connection_() {
if (this->client_ != nullptr) {
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
this->client_ = nullptr;
}
}
} // namespace audio
} // namespace esphome
#endif

View File

@ -0,0 +1,85 @@
#pragma once
#ifdef USE_ESP_IDF
#include "audio.h"
#include "audio_transfer_buffer.h"
#include "esphome/core/ring_buffer.h"
#include "esp_err.h"
#include <esp_http_client.h>
namespace esphome {
namespace audio {
enum class AudioReaderState : uint8_t {
READING = 0, // More data is available to read
FINISHED, // All data has been read and transferred
FAILED, // Encountered an error
};
class AudioReader {
/*
* @brief Class that facilitates reading a raw audio file.
* Files can be read from flash (stored in a AudioFile struct) or from an http source.
* The file data is sent to a ring buffer sink.
*/
public:
/// @brief Constructs an AudioReader object.
/// The transfer buffer isn't allocated here, but only if necessary (an http source) in the start function.
/// @param buffer_size Transfer buffer size in bytes.
AudioReader(size_t buffer_size) : buffer_size_(buffer_size) {}
~AudioReader();
/// @brief Adds a sink ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr
/// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
/// @return ESP_OK if successful, ESP_ERR_INVALID_STATE otherwise
esp_err_t add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer);
/// @brief Starts reading an audio file from an http source. The transfer buffer is allocated here.
/// @param uri Web url to the http file.
/// @param file_type AudioFileType variable passed-by-reference indicating the type of file being read.
/// @return ESP_OK if successful, an ESP_ERR* code otherwise.
esp_err_t start(const std::string &uri, AudioFileType &file_type);
/// @brief Starts reading an audio file from flash. No transfer buffer is allocated.
/// @param audio_file AudioFile struct containing the file.
/// @param file_type AudioFileType variable passed-by-reference indicating the type of file being read.
/// @return ESP_OK
esp_err_t start(AudioFile *audio_file, AudioFileType &file_type);
/// @brief Reads new file data from the source and sends to the ring buffer sink.
/// @return AudioReaderState
AudioReaderState read();
protected:
/// @brief Monitors the http client events to attempt determining the file type from the Content-Type header
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
/// @brief Determines the audio file type from the http header's Content-Type key
/// @param content_type string with the Content-Type key
/// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE.
static AudioFileType get_audio_type(const char *content_type);
AudioReaderState file_read_();
AudioReaderState http_read_();
std::shared_ptr<RingBuffer> file_ring_buffer_;
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
void cleanup_connection_();
size_t buffer_size_;
uint32_t no_data_read_count_;
esp_http_client_handle_t client_{nullptr};
AudioFile *current_audio_file_{nullptr};
AudioFileType audio_file_type_{AudioFileType::NONE};
const uint8_t *file_current_{nullptr};
};
} // namespace audio
} // namespace esphome
#endif

View File

@ -0,0 +1,159 @@
#include "audio_resampler.h"
#ifdef USE_ESP32
#include "esphome/core/hal.h"
namespace esphome {
namespace audio {
static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
AudioResampler::AudioResampler(size_t input_buffer_size, size_t output_buffer_size)
: input_buffer_size_(input_buffer_size), output_buffer_size_(output_buffer_size) {
this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size);
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size);
}
esp_err_t AudioResampler::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) {
if (this->input_transfer_buffer_ != nullptr) {
this->input_transfer_buffer_->set_source(input_ring_buffer);
return ESP_OK;
}
return ESP_ERR_NO_MEM;
}
esp_err_t AudioResampler::add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer) {
if (this->output_transfer_buffer_ != nullptr) {
this->output_transfer_buffer_->set_sink(output_ring_buffer);
return ESP_OK;
}
return ESP_ERR_NO_MEM;
}
#ifdef USE_SPEAKER
esp_err_t AudioResampler::add_sink(speaker::Speaker *speaker) {
if (this->output_transfer_buffer_ != nullptr) {
this->output_transfer_buffer_->set_sink(speaker);
return ESP_OK;
}
return ESP_ERR_NO_MEM;
}
#endif
esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamInfo &output_stream_info,
uint16_t number_of_taps, uint16_t number_of_filters) {
this->input_stream_info_ = input_stream_info;
this->output_stream_info_ = output_stream_info;
if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) {
return ESP_ERR_NO_MEM;
}
if ((input_stream_info.get_bits_per_sample() > 32) || (output_stream_info.get_bits_per_sample() > 32) ||
(input_stream_info_.get_channels() != output_stream_info.get_channels())) {
return ESP_ERR_NOT_SUPPORTED;
}
if ((input_stream_info.get_sample_rate() != output_stream_info.get_sample_rate()) ||
(input_stream_info.get_bits_per_sample() != output_stream_info.get_bits_per_sample())) {
this->resampler_ = make_unique<esp_audio_libs::resampler::Resampler>(
input_stream_info.bytes_to_samples(this->input_buffer_size_),
output_stream_info.bytes_to_samples(this->output_buffer_size_));
// Use cascaded biquad filters when downsampling to avoid aliasing
bool use_pre_filter = output_stream_info.get_sample_rate() < input_stream_info.get_sample_rate();
esp_audio_libs::resampler::ResamplerConfiguration resample_config = {
.source_sample_rate = static_cast<float>(input_stream_info.get_sample_rate()),
.target_sample_rate = static_cast<float>(output_stream_info.get_sample_rate()),
.source_bits_per_sample = input_stream_info.get_bits_per_sample(),
.target_bits_per_sample = output_stream_info.get_bits_per_sample(),
.channels = input_stream_info_.get_channels(),
.use_pre_or_post_filter = use_pre_filter,
.subsample_interpolate = false, // Doubles the CPU load. Using more filters is a better alternative
.number_of_taps = number_of_taps,
.number_of_filters = number_of_filters,
};
if (!this->resampler_->initialize(resample_config)) {
// Failed to allocate the resampler's internal buffers
return ESP_ERR_NO_MEM;
}
}
return ESP_OK;
}
AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_differential) {
if (stop_gracefully) {
if (!this->input_transfer_buffer_->has_buffered_data() && (this->output_transfer_buffer_->available() == 0)) {
return AudioResamplerState::FINISHED;
}
}
if (!this->pause_output_) {
// Move audio data to the sink
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
} else {
// If paused, block to avoid wasting CPU resources
delay(READ_WRITE_TIMEOUT_MS);
}
this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
if (this->input_transfer_buffer_->available() == 0) {
// No samples available to process
return AudioResamplerState::RESAMPLING;
}
const size_t bytes_free = this->output_transfer_buffer_->free();
const uint32_t frames_free = this->output_stream_info_.bytes_to_frames(bytes_free);
const size_t bytes_available = this->input_transfer_buffer_->available();
const uint32_t frames_available = this->input_stream_info_.bytes_to_frames(bytes_available);
if ((this->input_stream_info_.get_sample_rate() != this->output_stream_info_.get_sample_rate()) ||
(this->input_stream_info_.get_bits_per_sample() != this->output_stream_info_.get_bits_per_sample())) {
esp_audio_libs::resampler::ResamplerResults results =
this->resampler_->resample(this->input_transfer_buffer_->get_buffer_start(),
this->output_transfer_buffer_->get_buffer_end(), frames_available, frames_free, -3);
this->input_transfer_buffer_->decrease_buffer_length(this->input_stream_info_.frames_to_bytes(results.frames_used));
this->output_transfer_buffer_->increase_buffer_length(
this->output_stream_info_.frames_to_bytes(results.frames_generated));
// Resampling causes slight differences in the durations used versus generated. Computes the difference in
// millisconds. The callback function passing the played audio duration uses the difference to convert from output
// duration to input duration.
this->accumulated_frames_used_ += results.frames_used;
this->accumulated_frames_generated_ += results.frames_generated;
const int32_t used_ms =
this->input_stream_info_.frames_to_milliseconds_with_remainder(&this->accumulated_frames_used_);
const int32_t generated_ms =
this->output_stream_info_.frames_to_milliseconds_with_remainder(&this->accumulated_frames_generated_);
*ms_differential = used_ms - generated_ms;
} else {
// No resampling required, copy samples directly to the output transfer buffer
*ms_differential = 0;
const size_t bytes_to_transfer = std::min(this->output_stream_info_.frames_to_bytes(frames_free),
this->input_stream_info_.frames_to_bytes(frames_available));
std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(),
(void *) this->input_transfer_buffer_->get_buffer_start(), bytes_to_transfer);
this->input_transfer_buffer_->decrease_buffer_length(bytes_to_transfer);
this->output_transfer_buffer_->increase_buffer_length(bytes_to_transfer);
}
return AudioResamplerState::RESAMPLING;
}
} // namespace audio
} // namespace esphome
#endif

View File

@ -0,0 +1,101 @@
#pragma once
#ifdef USE_ESP32
#include "audio.h"
#include "audio_transfer_buffer.h"
#include "esphome/core/defines.h"
#include "esphome/core/ring_buffer.h"
#ifdef USE_SPEAKER
#include "esphome/components/speaker/speaker.h"
#endif
#include "esp_err.h"
#include <resampler.h> // esp-audio-libs
namespace esphome {
namespace audio {
enum class AudioResamplerState : uint8_t {
RESAMPLING, // More data is available to resample
FINISHED, // All file data has been resampled and transferred
FAILED, // Unused state included for consistency among Audio classes
};
class AudioResampler {
/*
* @brief Class that facilitates resampling audio.
* The audio data is read from a ring buffer source, resampled, and sent to an audio sink (ring buffer or speaker
* component). Also supports converting bits per sample.
*/
public:
/// @brief Allocates the input and output transfer buffers
/// @param input_buffer_size Size of the input transfer buffer in bytes.
/// @param output_buffer_size Size of the output transfer buffer in bytes.
AudioResampler(size_t input_buffer_size, size_t output_buffer_size);
/// @brief Adds a source ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr.
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
esp_err_t add_source(std::weak_ptr<RingBuffer> &input_ring_buffer);
/// @brief Adds a sink ring buffer for resampled audio. Takes ownership of the ring buffer in a shared_ptr.
/// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
esp_err_t add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer);
#ifdef USE_SPEAKER
/// @brief Adds a sink speaker for decoded audio.
/// @param speaker pointer to speaker component
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
esp_err_t add_sink(speaker::Speaker *speaker);
#endif
/// @brief Sets up the class to resample.
/// @param input_stream_info The incoming sample rate, bits per sample, and number of channels
/// @param output_stream_info The desired outgoing sample rate, bits per sample, and number of channels
/// @param number_of_taps Number of taps per FIR filter
/// @param number_of_filters Number of FIR filters
/// @return ESP_OK if it is able to convert the incoming stream,
/// ESP_ERR_NO_MEM if the transfer buffers failed to allocate,
/// ESP_ERR_NOT_SUPPORTED if the stream can't be converted.
esp_err_t start(AudioStreamInfo &input_stream_info, AudioStreamInfo &output_stream_info, uint16_t number_of_taps,
uint16_t number_of_filters);
/// @brief Resamples audio from the ring buffer source and writes to the sink.
/// @param stop_gracefully If true, it indicates the file decoder is finished. The resampler will resample all the
/// remaining audio and then finish.
/// @param ms_differential Pointer to a (int32_t) variable that will store the difference, in milliseconds, between
/// the duration of input audio used and the duration of output audio generated.
/// @return AudioResamplerState
AudioResamplerState resample(bool stop_gracefully, int32_t *ms_differential);
/// @brief Pauses sending resampled audio to the sink. If paused, it will continue to process internal buffers.
/// @param pause_state If true, audio data is not sent to the sink.
void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; }
protected:
std::unique_ptr<AudioSourceTransferBuffer> input_transfer_buffer_;
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
size_t input_buffer_size_;
size_t output_buffer_size_;
uint32_t accumulated_frames_used_{0};
uint32_t accumulated_frames_generated_{0};
bool pause_output_{false};
AudioStreamInfo input_stream_info_;
AudioStreamInfo output_stream_info_;
std::unique_ptr<esp_audio_libs::resampler::Resampler> resampler_;
};
} // namespace audio
} // namespace esphome
#endif

View File

@ -0,0 +1,165 @@
#include "audio_transfer_buffer.h"
#ifdef USE_ESP32
#include "esphome/core/helpers.h"
namespace esphome {
namespace audio {
AudioTransferBuffer::~AudioTransferBuffer() { this->deallocate_buffer_(); };
std::unique_ptr<AudioSinkTransferBuffer> AudioSinkTransferBuffer::create(size_t buffer_size) {
std::unique_ptr<AudioSinkTransferBuffer> sink_buffer = make_unique<AudioSinkTransferBuffer>();
if (!sink_buffer->allocate_buffer_(buffer_size)) {
return nullptr;
}
return sink_buffer;
}
std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
if (!source_buffer->allocate_buffer_(buffer_size)) {
return nullptr;
}
return source_buffer;
}
size_t AudioTransferBuffer::free() const {
if (this->buffer_size_ == 0) {
return 0;
}
return this->buffer_size_ - (this->buffer_length_ - (this->data_start_ - this->buffer_));
}
void AudioTransferBuffer::decrease_buffer_length(size_t bytes) {
this->buffer_length_ -= bytes;
this->data_start_ += bytes;
}
void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_length_ += bytes; }
void AudioTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0;
if (this->ring_buffer_.use_count() > 0) {
this->ring_buffer_->reset();
}
}
void AudioSinkTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0;
if (this->ring_buffer_.use_count() > 0) {
this->ring_buffer_->reset();
}
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
this->speaker_->stop();
}
#endif
}
bool AudioTransferBuffer::has_buffered_data() const {
if (this->ring_buffer_.use_count() > 0) {
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
}
return (this->available() > 0);
}
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
if (this->buffer_length_ > 0) {
// Already has data in the buffer, fail
return false;
}
this->deallocate_buffer_();
return this->allocate_buffer_(new_buffer_size);
}
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
this->buffer_size_ = buffer_size;
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buffer_ = allocator.allocate(this->buffer_size_);
if (this->buffer_ == nullptr) {
return false;
}
this->data_start_ = this->buffer_;
this->buffer_length_ = 0;
return true;
}
void AudioTransferBuffer::deallocate_buffer_() {
if (this->buffer_ != nullptr) {
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
allocator.deallocate(this->buffer_, this->buffer_size_);
this->buffer_ = nullptr;
this->data_start_ = nullptr;
}
this->buffer_size_ = 0;
this->buffer_length_ = 0;
}
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
size_t bytes_to_read = this->free();
size_t bytes_read = 0;
if (bytes_to_read > 0) {
if (this->ring_buffer_.use_count() > 0) {
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
}
this->increase_buffer_length(bytes_read);
}
return bytes_read;
}
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait) {
size_t bytes_written = 0;
if (this->available()) {
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
} else
#endif
if (this->ring_buffer_.use_count() > 0) {
bytes_written =
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
}
this->decrease_buffer_length(bytes_written);
// Shift unwritten data to the start of the buffer
memmove(this->buffer_, this->data_start_, this->buffer_length_);
this->data_start_ = this->buffer_;
}
return bytes_written;
}
bool AudioSinkTransferBuffer::has_buffered_data() const {
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
return (this->speaker_->has_buffered_data() || (this->available() > 0));
}
#endif
if (this->ring_buffer_.use_count() > 0) {
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
}
return (this->available() > 0);
}
} // namespace audio
} // namespace esphome
#endif

View File

@ -0,0 +1,139 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "esphome/core/ring_buffer.h"
#ifdef USE_SPEAKER
#include "esphome/components/speaker/speaker.h"
#endif
#include "esp_err.h"
#include <freertos/FreeRTOS.h>
namespace esphome {
namespace audio {
class AudioTransferBuffer {
/*
* @brief Class that facilitates tranferring data between a buffer and an audio source or sink.
* The transfer buffer is a typical C array that temporarily holds data for processing in other audio components.
* Both sink and source transfer buffers can use a ring buffer as the sink/source.
* - The ring buffer is stored in a shared_ptr, so destroying the transfer buffer object will release ownership.
*/
public:
/// @brief Destructor that deallocates the transfer buffer
~AudioTransferBuffer();
/// @brief Returns a pointer to the start of the transfer buffer where available() bytes of exisiting data can be read
uint8_t *get_buffer_start() const { return this->data_start_; }
/// @brief Returns a pointer to the end of the transfer buffer where free() bytes of new data can be written
uint8_t *get_buffer_end() const { return this->data_start_ + this->buffer_length_; }
/// @brief Updates the internal state of the transfer buffer. This should be called after reading data
/// @param bytes The number of bytes consumed/read
void decrease_buffer_length(size_t bytes);
/// @brief Updates the internal state of the transfer buffer. This should be called after writing data
/// @param bytes The number of bytes written
void increase_buffer_length(size_t bytes);
/// @brief Returns the transfer buffer's currently available bytes to read
size_t available() const { return this->buffer_length_; }
/// @brief Returns the transfer buffers allocated bytes
size_t capacity() const { return this->buffer_size_; }
/// @brief Returns the transfer buffer's currrently free bytes available to write
size_t free() const;
/// @brief Clears data in the transfer buffer and, if possible, the source/sink.
virtual void clear_buffered_data();
/// @brief Tests if there is any data in the tranfer buffer or the source/sink.
/// @return True if there is data, false otherwise.
virtual bool has_buffered_data() const;
bool reallocate(size_t new_buffer_size);
protected:
/// @brief Allocates the transfer buffer in external memory, if available.
/// @return True is successful, false otherwise.
bool allocate_buffer_(size_t buffer_size);
/// @brief Deallocates the buffer and resets the class variables.
void deallocate_buffer_();
// A possible source or sink for the transfer buffer
std::shared_ptr<RingBuffer> ring_buffer_;
uint8_t *buffer_{nullptr};
uint8_t *data_start_{nullptr};
size_t buffer_size_{0};
size_t buffer_length_{0};
};
class AudioSinkTransferBuffer : public AudioTransferBuffer {
/*
* @brief A class that implements a transfer buffer for audio sinks.
* Supports writing processed data in the transfer buffer to a ring buffer or a speaker component.
*/
public:
/// @brief Creates a new sink transfer buffer.
/// @param buffer_size Size of the transfer buffer in bytes.
/// @return unique_ptr if successfully allocated, nullptr otherwise
static std::unique_ptr<AudioSinkTransferBuffer> create(size_t buffer_size);
/// @brief Writes any available data in the transfer buffer to the sink.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for the sink to have enough space
/// @return Number of bytes written
size_t transfer_data_to_sink(TickType_t ticks_to_wait);
/// @brief Adds a ring buffer as the transfer buffer's sink.
/// @param ring_buffer weak_ptr to the allocated ring buffer
void set_sink(const std::weak_ptr<RingBuffer> &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); }
#ifdef USE_SPEAKER
/// @brief Adds a speaker as the transfer buffer's sink.
/// @param speaker Pointer to the speaker component
void set_sink(speaker::Speaker *speaker) { this->speaker_ = speaker; }
#endif
void clear_buffered_data() override;
bool has_buffered_data() const override;
protected:
#ifdef USE_SPEAKER
speaker::Speaker *speaker_{nullptr};
#endif
};
class AudioSourceTransferBuffer : public AudioTransferBuffer {
/*
* @brief A class that implements a transfer buffer for audio sources.
* Supports reading audio data from a ring buffer into the transfer buffer for processing.
*/
public:
/// @brief Creates a new source transfer buffer.
/// @param buffer_size Size of the transfer buffer in bytes.
/// @return unique_ptr if successfully allocated, nullptr otherwise
static std::unique_ptr<AudioSourceTransferBuffer> create(size_t buffer_size);
/// @brief Reads any available data from the sink into the transfer buffer.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data
/// @return Number of bytes read
size_t transfer_data_from_source(TickType_t ticks_to_wait);
/// @brief Adds a ring buffer as the transfer buffer's source.
/// @param ring_buffer weak_ptr to the allocated ring buffer
void set_source(const std::weak_ptr<RingBuffer> &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); };
};
} // namespace audio
} // namespace esphome
#endif

View File

@ -11,6 +11,7 @@ from esphome.const import (
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_DECIBEL_MILLIWATT, UNIT_DECIBEL_MILLIWATT,
CONF_NOTIFY,
) )
from .. import ble_client_ns from .. import ble_client_ns
@ -19,7 +20,6 @@ DEPENDENCIES = ["ble_client"]
CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_NOTIFY = "notify"
CONF_ON_NOTIFY = "on_notify" CONF_ON_NOTIFY = "on_notify"
TYPE_CHARACTERISTIC = "characteristic" TYPE_CHARACTERISTIC = "characteristic"
TYPE_RSSI = "rssi" TYPE_RSSI = "rssi"

View File

@ -6,6 +6,7 @@ from esphome.const import (
CONF_CHARACTERISTIC_UUID, CONF_CHARACTERISTIC_UUID,
CONF_ID, CONF_ID,
CONF_SERVICE_UUID, CONF_SERVICE_UUID,
CONF_NOTIFY,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
) )
@ -15,7 +16,6 @@ DEPENDENCIES = ["ble_client"]
CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_NOTIFY = "notify"
CONF_ON_NOTIFY = "on_notify" CONF_ON_NOTIFY = "on_notify"
adv_data_t = cg.std_vector.template(cg.uint8) adv_data_t = cg.std_vector.template(cg.uint8)

View File

@ -475,6 +475,11 @@ void BluetoothProxy::send_connections_free() {
api::BluetoothConnectionsFreeResponse call; api::BluetoothConnectionsFreeResponse call;
call.free = this->get_bluetooth_connections_free(); call.free = this->get_bluetooth_connections_free();
call.limit = this->get_bluetooth_connections_limit(); call.limit = this->get_bluetooth_connections_limit();
for (auto *connection : this->connections_) {
if (connection->address_ != 0) {
call.allocated.push_back(connection->address_);
}
}
this->api_connection_->send_bluetooth_connections_free_response(call); this->api_connection_->send_bluetooth_connections_free_response(call);
} }

View File

@ -57,6 +57,8 @@ class CH422GGPIOPin : public GPIOPin {
void set_inverted(bool inverted) { inverted_ = inverted; } void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags); void set_flags(gpio::Flags flags);
gpio::Flags get_flags() const override { return this->flags_; }
protected: protected:
CH422GComponent *parent_{}; CH422GComponent *parent_{};
uint8_t pin_{}; uint8_t pin_{};

View File

@ -128,7 +128,6 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
def visual_temperature_step(value): def visual_temperature_step(value):
# Allow defining target/current temperature steps separately # Allow defining target/current temperature steps separately
if isinstance(value, dict): if isinstance(value, dict):
return VISUAL_TEMPERATURE_STEP_SCHEMA(value) return VISUAL_TEMPERATURE_STEP_SCHEMA(value)

View File

@ -1,8 +1,5 @@
#include "cse7766.h" #include "cse7766.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes>
#include <iomanip>
#include <sstream>
namespace esphome { namespace esphome {
namespace cse7766 { namespace cse7766 {
@ -72,12 +69,8 @@ bool CSE7766Component::check_byte_() {
void CSE7766Component::parse_data_() { void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{ {
std::stringstream ss; std::string s = format_hex_pretty(this->raw_data_, sizeof(this->raw_data_));
ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0'); ESP_LOGVV(TAG, "Raw data: %s", s.c_str());
for (uint8_t i = 0; i < 23; i++) {
ss << ' ' << std::setw(2) << static_cast<unsigned>(this->raw_data_[i]);
}
ESP_LOGVV(TAG, "%s", ss.str().c_str());
} }
#endif #endif
@ -211,21 +204,20 @@ void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{ {
std::stringstream ss; std::string buf = "Parsed:";
ss << "Parsed:";
if (have_voltage) { if (have_voltage) {
ss << " V=" << voltage << "V"; buf += str_sprintf(" V=%fV", voltage);
} }
if (have_current) { if (have_current) {
ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)"; buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f);
} }
if (have_power) { if (have_power) {
ss << " P=" << power << "W"; buf += str_sprintf(" P=%fW", power);
} }
if (energy != 0.0f) { if (energy != 0.0f) {
ss << " E=" << energy << "kWh (" << cf_pulses << ")"; buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses);
} }
ESP_LOGVV(TAG, "%s", ss.str().c_str()); ESP_LOGVV(TAG, "%s", buf.c_str());
} }
#endif #endif
} }

View File

@ -23,6 +23,7 @@ void DHT::dump_config() {
} else { } else {
ESP_LOGCONFIG(TAG, " Model: DHT22 (or equivalent)"); ESP_LOGCONFIG(TAG, " Model: DHT22 (or equivalent)");
} }
ESP_LOGCONFIG(TAG, " Internal Pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
@ -101,7 +102,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
} else { } else {
delayMicroseconds(800); delayMicroseconds(800);
} }
this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->pin_->pin_mode(this->pin_->get_flags());
{ {
InterruptLock lock; InterruptLock lock;

View File

@ -34,7 +34,7 @@ DHT = dht_ns.class_("DHT", cg.PollingComponent)
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(DHT), cv.GenerateID(): cv.declare_id(DHT),
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_PIN): pins.internal_gpio_input_pullup_pin_schema,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1, accuracy_decimals=1,

View File

@ -101,7 +101,7 @@ async def setup_display_core_(var, config):
if CONF_ROTATION in config: if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED): if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None:
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but # Default to true if pages or lambda is specified. Ideally this would be done during validation, but
# the possible schemas are too complex to do this easily. # the possible schemas are too complex to do this easily.
if auto_clear == CONF_UNSPECIFIED: if auto_clear == CONF_UNSPECIFIED:

View File

@ -815,8 +815,20 @@ void Display::test_card() {
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); } void DisplayPage::show() { this->parent_->show_page(this); }
void DisplayPage::show_next() { this->next_->show(); } void DisplayPage::show_next() {
void DisplayPage::show_prev() { this->prev_->show(); } if (this->next_ == nullptr) {
ESP_LOGE(TAG, "no next page");
return;
}
this->next_->show();
}
void DisplayPage::show_prev() {
if (this->prev_ == nullptr) {
ESP_LOGE(TAG, "no previous page");
return;
}
this->prev_->show();
}
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }

View File

@ -13,6 +13,7 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
void set_inverted(bool inverted) { inverted_ = inverted; } void set_inverted(bool inverted) { inverted_ = inverted; }
void set_drive_strength(gpio_drive_cap_t drive_strength) { drive_strength_ = drive_strength; } void set_drive_strength(gpio_drive_cap_t drive_strength) { drive_strength_ = drive_strength; }
void set_flags(gpio::Flags flags) { flags_ = flags; } void set_flags(gpio::Flags flags) { flags_ = flags; }
void setup() override; void setup() override;
void pin_mode(gpio::Flags flags) override; void pin_mode(gpio::Flags flags) override;
bool digital_read() override; bool digital_read() override;
@ -21,6 +22,7 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
void detach_interrupt() const override; void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override; ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return (uint8_t) pin_; } uint8_t get_pin() const override { return (uint8_t) pin_; }
gpio::Flags get_flags() const override { return flags_; }
bool is_inverted() const override { return inverted_; } bool is_inverted() const override { return inverted_; }
protected: protected:

View File

@ -1,3 +1,5 @@
import re
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
@ -64,6 +66,43 @@ CONFIG_SCHEMA = cv.Schema(
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
bt_uuid16_format = "XXXX"
bt_uuid32_format = "XXXXXXXX"
bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
def bt_uuid(value):
in_value = cv.string_strict(value)
value = in_value.upper()
if len(value) == len(bt_uuid16_format):
pattern = re.compile("^[A-F|0-9]{4,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid32_format):
pattern = re.compile("^[A-F|0-9]{8,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid128_format):
pattern = re.compile(
"^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$"
)
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 128 UUID format: '{in_value}'"
)
return value
raise cv.Invalid(
f"Bluetooth UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format"
)
def validate_variant(_): def validate_variant(_):
variant = get_esp32_variant() variant = get_esp32_variant()
if variant in NO_BLUETOOTH_VARIANTS: if variant in NO_BLUETOOTH_VARIANTS:

View File

@ -66,7 +66,9 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
async def to_code(config): async def to_code(config):
uuid = config[CONF_UUID].hex uuid = config[CONF_UUID].hex
uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)] uuid_arr = [
cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2)
]
var = cg.new_Pvariable(config[CONF_ID], uuid_arr) var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])

View File

@ -1,37 +1,526 @@
import encodings
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import bt_uuid
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODEL from esphome.config_validation import UNDEFINED
from esphome.const import (
CONF_DATA,
CONF_ESPHOME,
CONF_ID,
CONF_MAX_LENGTH,
CONF_MODEL,
CONF_NOTIFY,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_PROJECT,
CONF_SERVICES,
CONF_TYPE,
CONF_UUID,
CONF_VALUE,
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE from esphome.core import CORE
from esphome.schema_extractors import SCHEMA_EXTRACT
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
DOMAIN = "esp32_ble_server"
CONF_ADVERTISE = "advertise"
CONF_BROADCAST = "broadcast"
CONF_CHARACTERISTICS = "characteristics"
CONF_DESCRIPTION = "description"
CONF_DESCRIPTORS = "descriptors"
CONF_ENDIANNESS = "endianness"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_INDICATE = "indicate"
CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER = "manufacturer"
CONF_MANUFACTURER_DATA = "manufacturer_data" CONF_MANUFACTURER_DATA = "manufacturer_data"
CONF_ON_WRITE = "on_write"
CONF_READ = "read"
CONF_STRING = "string"
CONF_STRING_ENCODING = "string_encoding"
CONF_WRITE = "write"
CONF_WRITE_NO_RESPONSE = "write_no_response"
# Internal configuration keys
CONF_CHAR_VALUE_ACTION_ID_ = "char_value_action_id_"
# BLE reserverd UUIDs
CCCD_DESCRIPTOR_UUID = 0x2902
CUD_DESCRIPTOR_UUID = 0x2901
DEVICE_INFORMATION_SERVICE_UUID = 0x180A
MANUFACTURER_NAME_CHARACTERISTIC_UUID = 0x2A29
MODEL_CHARACTERISTIC_UUID = 0x2A24
FIRMWARE_VERSION_CHARACTERISTIC_UUID = 0x2A26
# Core key to store the global configuration
KEY_NOTIFY_REQUIRED = "notify_required"
KEY_SET_VALUE = "set_value"
esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server")
ESPBTUUID_ns = cg.esphome_ns.namespace("esp32_ble").namespace("ESPBTUUID")
BLECharacteristic_ns = esp32_ble_server_ns.namespace("BLECharacteristic")
BLEServer = esp32_ble_server_ns.class_( BLEServer = esp32_ble_server_ns.class_(
"BLEServer", "BLEServer",
cg.Component, cg.Component,
esp32_ble.GATTsEventHandler, esp32_ble.GATTsEventHandler,
cg.Parented.template(esp32_ble.ESP32BLE), cg.Parented.template(esp32_ble.ESP32BLE),
) )
BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") esp32_ble_server_automations_ns = esp32_ble_server_ns.namespace(
"esp32_ble_server_automations"
)
BLETriggers_ns = esp32_ble_server_automations_ns.namespace("BLETriggers")
BLEDescriptor = esp32_ble_server_ns.class_("BLEDescriptor")
BLECharacteristic = esp32_ble_server_ns.class_("BLECharacteristic")
BLEService = esp32_ble_server_ns.class_("BLEService")
BLECharacteristicSetValueAction = esp32_ble_server_automations_ns.class_(
"BLECharacteristicSetValueAction", automation.Action
)
BLEDescriptorSetValueAction = esp32_ble_server_automations_ns.class_(
"BLEDescriptorSetValueAction", automation.Action
)
BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_(
"BLECharacteristicNotifyAction", automation.Action
)
bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer")
Endianness_ns = bytebuffer_ns.namespace("Endian")
ByteBuffer_ns = bytebuffer_ns.namespace("ByteBuffer")
ByteBuffer = bytebuffer_ns.class_("ByteBuffer")
PROPERTY_MAP = {
CONF_READ: BLECharacteristic_ns.PROPERTY_READ,
CONF_WRITE: BLECharacteristic_ns.PROPERTY_WRITE,
CONF_NOTIFY: BLECharacteristic_ns.PROPERTY_NOTIFY,
CONF_BROADCAST: BLECharacteristic_ns.PROPERTY_BROADCAST,
CONF_INDICATE: BLECharacteristic_ns.PROPERTY_INDICATE,
CONF_WRITE_NO_RESPONSE: BLECharacteristic_ns.PROPERTY_WRITE_NR,
}
class ValueType:
def __init__(self, type_, validator, length):
self.type_ = type_
self.validator = validator
self.length = length
def validate(self, value, encoding):
value = self.validator(value)
if self.type_ == "string":
try:
value.encode(encoding)
except UnicodeEncodeError as e:
raise cv.Invalid(str(e)) from e
return value
VALUE_TYPES = {
type_name: ValueType(type_name, validator, length)
for type_name, validator, length in (
("uint8_t", cv.uint8_t, 1),
("uint16_t", cv.uint16_t, 2),
("uint32_t", cv.uint32_t, 4),
("uint64_t", cv.uint64_t, 8),
("int8_t", cv.int_range(-128, 127), 1),
("int16_t", cv.int_range(-32768, 32767), 2),
("int32_t", cv.int_range(-2147483648, 2147483647), 4),
("int64_t", cv.int_range(-9223372036854775808, 9223372036854775807), 8),
("float", cv.float_, 4),
("double", cv.float_, 8),
("string", cv.string_strict, None), # Length is variable
)
}
def validate_char_on_write(char_config):
if CONF_ON_WRITE in char_config:
if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]:
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set"
)
return char_config
def validate_descriptor(desc_config):
if CONF_ON_WRITE in desc_config:
if not desc_config[CONF_WRITE]:
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set"
)
if CONF_MAX_LENGTH not in desc_config:
value = desc_config[CONF_VALUE][CONF_DATA]
if cg.is_template(value):
raise cv.Invalid(
f"Descriptor {desc_config[CONF_UUID]} has a templatable value and the {CONF_MAX_LENGTH} property is not set"
)
if isinstance(value, list):
desc_config[CONF_MAX_LENGTH] = len(value)
elif isinstance(value, str):
desc_config[CONF_MAX_LENGTH] = len(
value.encode(desc_config[CONF_VALUE][CONF_STRING_ENCODING])
)
else:
desc_config[CONF_MAX_LENGTH] = VALUE_TYPES[
desc_config[CONF_VALUE][CONF_TYPE]
].length
return desc_config
def validate_notify_action(config):
# Store the characteristic ID in the global data for the final validation
data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_NOTIFY_REQUIRED, set())
data.add(config[CONF_ID])
return config
def validate_set_value_action(config):
# Store the characteristic ID in the global data for the final validation
data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_SET_VALUE, set())
data.add(config[CONF_ID])
return config
def create_description_cud(char_config):
if CONF_DESCRIPTION not in char_config:
return char_config
# If the config displays a description, there cannot be a descriptor with the CUD UUID
for desc in char_config[CONF_DESCRIPTORS]:
if desc[CONF_UUID] == CUD_DESCRIPTOR_UUID:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has a description, but a CUD descriptor is already present"
)
# Manually add the CUD descriptor
char_config[CONF_DESCRIPTORS].append(
DESCRIPTOR_SCHEMA(
{
CONF_UUID: CUD_DESCRIPTOR_UUID,
CONF_READ: True,
CONF_WRITE: False,
CONF_VALUE: char_config[CONF_DESCRIPTION],
}
)
)
return char_config
def create_notify_cccd(char_config):
if not char_config[CONF_NOTIFY] and not char_config[CONF_INDICATE]:
return char_config
# If the CCCD descriptor is already present, return the config
for desc in char_config[CONF_DESCRIPTORS]:
if desc[CONF_UUID] == CCCD_DESCRIPTOR_UUID:
# Check if the WRITE property is set
if not desc[CONF_WRITE]:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has notify actions, but the CCCD descriptor does not have the {CONF_WRITE} property set"
)
return char_config
# Manually add the CCCD descriptor
char_config[CONF_DESCRIPTORS].append(
DESCRIPTOR_SCHEMA(
{
CONF_UUID: CCCD_DESCRIPTOR_UUID,
CONF_READ: True,
CONF_WRITE: True,
CONF_MAX_LENGTH: 2,
CONF_VALUE: [0, 0],
}
)
)
return char_config
def create_device_information_service(config):
# If there is already a device information service,
# there cannot be CONF_MODEL, CONF_MANUFACTURER or CONF_FIRMWARE_VERSION properties
for service in config[CONF_SERVICES]:
if service[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
if (
CONF_MODEL in config
or CONF_MANUFACTURER in config
or CONF_FIRMWARE_VERSION in config
):
raise cv.Invalid(
"Device information service already present, cannot add manufacturer, model or firmware version"
)
return config
project = CORE.raw_config[CONF_ESPHOME].get(CONF_PROJECT, {})
model = config.get(CONF_MODEL, project.get("name", CORE.data["esp32"]["board"]))
version = config.get(
CONF_FIRMWARE_VERSION, project.get("version", "ESPHome " + ESPHOME_VERSION)
)
# Manually add the device information service
config[CONF_SERVICES].append(
SERVICE_SCHEMA(
{
CONF_UUID: DEVICE_INFORMATION_SERVICE_UUID,
CONF_CHARACTERISTICS: [
{
CONF_UUID: MANUFACTURER_NAME_CHARACTERISTIC_UUID,
CONF_READ: True,
CONF_VALUE: config.get(CONF_MANUFACTURER, "ESPHome"),
},
{
CONF_UUID: MODEL_CHARACTERISTIC_UUID,
CONF_READ: True,
CONF_VALUE: model,
},
{
CONF_UUID: FIRMWARE_VERSION_CHARACTERISTIC_UUID,
CONF_READ: True,
CONF_VALUE: version,
},
],
}
)
)
return config
def final_validate_config(config):
# Check if all characteristics that require notifications have the notify property set
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()):
# Look for the characteristic in the configuration
char_config = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
if not char_config[CONF_NOTIFY]:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set"
)
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()):
# Look for the characteristic in the configuration
char_config = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda):
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value"
)
return config
def validate_value_type(value_config):
# If the value is a not a templatable, the type must be set
value = value_config[CONF_DATA]
if type_ := value_config.get(CONF_TYPE):
if cg.is_template(value):
raise cv.Invalid(
f'The "{CONF_TYPE}" property is not allowed for templatable values'
)
value_config[CONF_DATA] = VALUE_TYPES[type_].validate(
value, value_config[CONF_STRING_ENCODING]
)
elif isinstance(value, (float, int)):
raise cv.Invalid(
f'The "{CONF_TYPE}" property is required for the value "{value}"'
)
return value_config
def validate_encoding(value):
if value == SCHEMA_EXTRACT:
return cv.one_of("utf-8", "latin-1", "ascii", "utf-16", "utf-32")
value = encodings.normalize_encoding(value)
if not value:
raise cv.Invalid("Invalid encoding")
return value
def value_schema(default_type=UNDEFINED, templatable=True):
data_validators = [
cv.string_strict,
cv.int_,
cv.float_,
cv.All([cv.uint8_t], cv.Length(min=1)),
]
if templatable:
data_validators.append(cv.returning_lambda)
return cv.maybe_simple_value(
cv.All(
{
cv.Required(CONF_DATA): cv.Any(*data_validators),
cv.Optional(CONF_TYPE, default=default_type): cv.one_of(
*VALUE_TYPES, lower=True
),
cv.Optional(CONF_STRING_ENCODING, default="utf_8"): validate_encoding,
cv.Optional(CONF_ENDIANNESS, default="LITTLE"): cv.enum(
{
"LITTLE": Endianness_ns.LITTLE,
"BIG": Endianness_ns.BIG,
}
),
},
validate_value_type,
),
key=CONF_DATA,
)
DESCRIPTOR_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(BLEDescriptor),
cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t),
cv.Optional(CONF_READ, default=True): cv.boolean,
cv.Optional(CONF_WRITE, default=True): cv.boolean,
cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True),
cv.Required(CONF_VALUE): value_schema(templatable=False),
cv.Optional(CONF_MAX_LENGTH): cv.uint16_t,
},
validate_descriptor,
)
CHARACTERISTIC_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLECharacteristic),
cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t),
cv.Optional(CONF_VALUE): value_schema(templatable=True),
cv.GenerateID(CONF_CHAR_VALUE_ACTION_ID_): cv.declare_id(
BLECharacteristicSetValueAction
),
cv.Optional(CONF_DESCRIPTORS, default=[]): cv.ensure_list(DESCRIPTOR_SCHEMA),
cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True),
cv.Optional(CONF_DESCRIPTION): value_schema(
default_type="string", templatable=False
),
},
extra_schemas=[
validate_char_on_write,
create_description_cud,
create_notify_cccd,
],
).extend({cv.Optional(k, default=False): cv.boolean for k in PROPERTY_MAP})
SERVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEService),
cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t),
cv.Optional(CONF_ADVERTISE, default=False): cv.boolean,
cv.Optional(CONF_CHARACTERISTICS, default=[]): cv.ensure_list(
CHARACTERISTIC_SCHEMA
),
}
)
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(BLEServer), cv.GenerateID(): cv.declare_id(BLEServer),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, cv.Optional(CONF_MANUFACTURER): value_schema("string", templatable=False),
cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.hex_uint8_t]), cv.Optional(CONF_MODEL): value_schema("string", templatable=False),
cv.Optional(CONF_MODEL): cv.string, cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False),
} cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]),
cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA),
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
},
extra_schemas=[create_device_information_service],
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = final_validate_config
def parse_properties(char_conf):
return sum(
(PROPERTY_MAP[k] for k in char_conf if k in PROPERTY_MAP and char_conf[k]),
start=0,
)
def parse_uuid(uuid):
# If the UUID is a int, use from_uint32
if isinstance(uuid, int):
return ESPBTUUID_ns.from_uint32(uuid)
# Otherwise, use ESPBTUUID_ns.from_raw
return ESPBTUUID_ns.from_raw(uuid)
async def parse_value(value_config, args):
value = value_config[CONF_DATA]
if isinstance(value, cv.Lambda):
return await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
if isinstance(value, str):
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list):
return cg.std_vector.template(cg.uint8)(value)
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
def calculate_num_handles(service_config):
total = 1 + len(service_config[CONF_CHARACTERISTICS]) * 2
total += sum(
len(char_conf[CONF_DESCRIPTORS])
for char_conf in service_config[CONF_CHARACTERISTICS]
)
return total
async def to_code_descriptor(descriptor_conf, char_var):
value = await parse_value(descriptor_conf[CONF_VALUE], {})
desc_var = cg.new_Pvariable(
descriptor_conf[CONF_ID],
parse_uuid(descriptor_conf[CONF_UUID]),
descriptor_conf[CONF_MAX_LENGTH],
descriptor_conf[CONF_READ],
descriptor_conf[CONF_WRITE],
)
cg.add(char_var.add_descriptor(desc_var))
cg.add(desc_var.set_value(value))
if CONF_ON_WRITE in descriptor_conf:
on_write_conf = descriptor_conf[CONF_ON_WRITE]
await automation.build_automation(
BLETriggers_ns.create_descriptor_on_write_trigger(desc_var),
[(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")],
on_write_conf,
)
async def to_code_characteristic(service_var, char_conf):
char_var = cg.Pvariable(
char_conf[CONF_ID],
service_var.create_characteristic(
parse_uuid(char_conf[CONF_UUID]),
parse_properties(char_conf),
),
)
if CONF_ON_WRITE in char_conf:
on_write_conf = char_conf[CONF_ON_WRITE]
await automation.build_automation(
BLETriggers_ns.create_characteristic_on_write_trigger(char_var),
[(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")],
on_write_conf,
)
if CONF_VALUE in char_conf:
action_conf = {
CONF_ID: char_conf[CONF_ID],
CONF_VALUE: char_conf[CONF_VALUE],
}
value_action = await ble_server_characteristic_set_value(
action_conf,
char_conf[CONF_CHAR_VALUE_ACTION_ID_],
cg.TemplateArguments(),
{},
)
cg.add(value_action.play())
for descriptor_conf in char_conf[CONF_DESCRIPTORS]:
await to_code_descriptor(descriptor_conf, char_var)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@ -42,13 +531,94 @@ async def to_code(config):
cg.add(parent.register_gatts_event_handler(var)) cg.add(parent.register_gatts_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var))
cg.add(var.set_parent(parent)) cg.add(var.set_parent(parent))
cg.add(var.set_manufacturer(config[CONF_MANUFACTURER]))
if CONF_MANUFACTURER_DATA in config: if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
if CONF_MODEL in config: for service_config in config[CONF_SERVICES]:
cg.add(var.set_model(config[CONF_MODEL])) # Calculate the optimal number of handles based on the number of characteristics and descriptors
num_handles = calculate_num_handles(service_config)
service_var = cg.Pvariable(
service_config[CONF_ID],
var.create_service(
parse_uuid(service_config[CONF_UUID]),
service_config[CONF_ADVERTISE],
num_handles,
),
)
for char_conf in service_config[CONF_CHARACTERISTICS]:
await to_code_characteristic(service_var, char_conf)
if service_config[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
cg.add(var.set_device_information_service(service_var))
else:
cg.add(var.enqueue_start_service(service_var))
if CONF_ON_CONNECT in config:
await automation.build_automation(
BLETriggers_ns.create_server_on_connect_trigger(var),
[(cg.uint16, "id")],
config[CONF_ON_CONNECT],
)
if CONF_ON_DISCONNECT in config:
await automation.build_automation(
BLETriggers_ns.create_server_on_disconnect_trigger(var),
[(cg.uint16, "id")],
config[CONF_ON_DISCONNECT],
)
cg.add_define("USE_ESP32_BLE_SERVER") cg.add_define("USE_ESP32_BLE_SERVER")
if CORE.using_esp_idf: if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
@automation.register_action(
"ble_server.characteristic.set_value",
BLECharacteristicSetValueAction,
cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(BLECharacteristic),
cv.Required(CONF_VALUE): value_schema(),
}
),
validate_set_value_action,
),
)
async def ble_server_characteristic_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
return var
@automation.register_action(
"ble_server.descriptor.set_value",
BLEDescriptorSetValueAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(BLEDescriptor),
cv.Required(CONF_VALUE): value_schema(),
}
),
)
async def ble_server_descriptor_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
return var
@automation.register_action(
"ble_server.characteristic.notify",
BLECharacteristicNotifyAction,
cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(BLECharacteristic),
}
),
validate_notify_action,
),
)
async def ble_server_characteristic_notify(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var

View File

@ -1,18 +0,0 @@
#include "ble_2901.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
BLE2901::BLE2901(const std::string &value) : BLE2901((uint8_t *) value.data(), value.length()) {}
BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2901)) {
this->set_value(data, length);
this->permissions_ = ESP_GATT_PERM_READ;
}
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@ -1,19 +0,0 @@
#pragma once
#include "ble_descriptor.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
class BLE2901 : public BLEDescriptor {
public:
BLE2901(const std::string &value);
BLE2901(const uint8_t *data, size_t length);
};
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@ -32,70 +32,36 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0); this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0);
} }
void BLECharacteristic::set_value(std::vector<uint8_t> value) { void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) {
xSemaphoreTake(this->set_value_lock_, 0L); xSemaphoreTake(this->set_value_lock_, 0L);
this->value_ = std::move(value); this->value_ = buffer;
xSemaphoreGive(this->set_value_lock_); xSemaphoreGive(this->set_value_lock_);
} }
void BLECharacteristic::set_value(const std::string &value) { void BLECharacteristic::set_value(const std::string &buffer) {
this->set_value(std::vector<uint8_t>(value.begin(), value.end())); this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));
}
void BLECharacteristic::set_value(const uint8_t *data, size_t length) {
this->set_value(std::vector<uint8_t>(data, data + length));
}
void BLECharacteristic::set_value(uint8_t &data) {
uint8_t temp[1];
temp[0] = data;
this->set_value(temp, 1);
}
void BLECharacteristic::set_value(uint16_t &data) {
uint8_t temp[2];
temp[0] = data;
temp[1] = data >> 8;
this->set_value(temp, 2);
}
void BLECharacteristic::set_value(uint32_t &data) {
uint8_t temp[4];
temp[0] = data;
temp[1] = data >> 8;
temp[2] = data >> 16;
temp[3] = data >> 24;
this->set_value(temp, 4);
}
void BLECharacteristic::set_value(int &data) {
uint8_t temp[4];
temp[0] = data;
temp[1] = data >> 8;
temp[2] = data >> 16;
temp[3] = data >> 24;
this->set_value(temp, 4);
}
void BLECharacteristic::set_value(float &data) {
float temp = data;
this->set_value((uint8_t *) &temp, 4);
}
void BLECharacteristic::set_value(double &data) {
double temp = data;
this->set_value((uint8_t *) &temp, 8);
}
void BLECharacteristic::set_value(bool &data) {
uint8_t temp[1];
temp[0] = data;
this->set_value(temp, 1);
} }
void BLECharacteristic::notify(bool notification) { void BLECharacteristic::notify() {
if (!notification) { if (this->service_ == nullptr || this->service_->get_server() == nullptr ||
ESP_LOGW(TAG, "notification=false is not yet supported"); this->service_->get_server()->get_connected_client_count() == 0)
// TODO: Handle when notification=false
}
if (this->service_->get_server()->get_connected_client_count() == 0)
return; return;
for (auto &client : this->service_->get_server()->get_clients()) { for (auto &client : this->service_->get_server()->get_clients()) {
size_t length = this->value_.size(); size_t length = this->value_.size();
esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client.first, // If the client is not in the list of clients to notify, skip it
this->handle_, length, this->value_.data(), false); if (this->clients_to_notify_.count(client) == 0)
continue;
// If the client is in the list of clients to notify, check if it requires an ack (i.e. INDICATE)
bool require_ack = this->clients_to_notify_[client];
// TODO: Remove this block when INDICATE acknowledgment is supported
if (require_ack) {
ESP_LOGW(TAG, "INDICATE acknowledgment is not yet supported (i.e. it works as a NOTIFY)");
require_ack = false;
}
esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client, this->handle_,
length, this->value_.data(), require_ack);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err); ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err);
return; return;
@ -103,7 +69,24 @@ void BLECharacteristic::notify(bool notification) {
} }
} }
void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); } void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
// If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified
if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &value, uint16_t conn_id) {
if (value.size() != 2)
return;
uint16_t cccd = encode_uint16(value[1], value[0]);
bool notify = (cccd & 1) != 0;
bool indicate = (cccd & 2) != 0;
if (notify || indicate) {
this->clients_to_notify_[conn_id] = indicate;
} else {
this->clients_to_notify_.erase(conn_id);
}
});
}
this->descriptors_.push_back(descriptor);
}
void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) { void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) {
this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor), this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor),
@ -223,6 +206,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
if (!param->read.need_rsp) if (!param->read.need_rsp)
break; // For some reason you can request a read but not want a response break; // For some reason you can request a read but not want a response
this->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ,
param->read.conn_id);
uint16_t max_offset = 22; uint16_t max_offset = 22;
esp_gatt_rsp_t response; esp_gatt_rsp_t response;
@ -262,13 +248,13 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
} }
case ESP_GATTS_WRITE_EVT: { case ESP_GATTS_WRITE_EVT: {
if (this->handle_ != param->write.handle) if (this->handle_ != param->write.handle)
return; break;
if (param->write.is_prep) { if (param->write.is_prep) {
this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len);
this->write_event_ = true; this->write_event_ = true;
} else { } else {
this->set_value(param->write.value, param->write.len); this->set_value(ByteBuffer::wrap(param->write.value, param->write.len));
} }
if (param->write.need_rsp) { if (param->write.need_rsp) {
@ -289,7 +275,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
} }
if (!param->write.is_prep) { if (!param->write.is_prep) {
this->on_write_(this->value_); this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id);
} }
break; break;
@ -300,7 +287,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break; break;
this->write_event_ = false; this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
this->on_write_(this->value_); this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id);
} }
esp_err_t err = esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);

View File

@ -2,8 +2,11 @@
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#include <vector> #include <vector>
#include <unordered_map>
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -19,24 +22,30 @@ namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLEService; class BLEService;
class BLECharacteristic { namespace BLECharacteristicEvt {
enum VectorEvt {
ON_WRITE,
};
enum EmptyEvt {
ON_READ,
};
} // namespace BLECharacteristicEvt
class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>,
public EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t> {
public: public:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties); BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
~BLECharacteristic(); ~BLECharacteristic();
void set_value(const uint8_t *data, size_t length); void set_value(ByteBuffer buffer);
void set_value(std::vector<uint8_t> value); void set_value(const std::vector<uint8_t> &buffer);
void set_value(const std::string &value); void set_value(const std::string &buffer);
void set_value(uint8_t &data);
void set_value(uint16_t &data);
void set_value(uint32_t &data);
void set_value(int &data);
void set_value(float &data);
void set_value(double &data);
void set_value(bool &data);
void set_broadcast_property(bool value); void set_broadcast_property(bool value);
void set_indicate_property(bool value); void set_indicate_property(bool value);
@ -45,13 +54,12 @@ class BLECharacteristic {
void set_write_property(bool value); void set_write_property(bool value);
void set_write_no_response_property(bool value); void set_write_no_response_property(bool value);
void notify(bool notification = true); void notify();
void do_create(BLEService *service); void do_create(BLEService *service);
void do_delete() { this->clients_to_notify_.clear(); }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
void on_write(const std::function<void(const std::vector<uint8_t> &)> &&func) { this->on_write_ = func; }
void add_descriptor(BLEDescriptor *descriptor); void add_descriptor(BLEDescriptor *descriptor);
void remove_descriptor(BLEDescriptor *descriptor); void remove_descriptor(BLEDescriptor *descriptor);
@ -71,7 +79,7 @@ class BLECharacteristic {
protected: protected:
bool write_event_{false}; bool write_event_{false};
BLEService *service_; BLEService *service_{};
ESPBTUUID uuid_; ESPBTUUID uuid_;
esp_gatt_char_prop_t properties_; esp_gatt_char_prop_t properties_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};
@ -81,8 +89,7 @@ class BLECharacteristic {
SemaphoreHandle_t set_value_lock_; SemaphoreHandle_t set_value_lock_;
std::vector<BLEDescriptor *> descriptors_; std::vector<BLEDescriptor *> descriptors_;
std::unordered_map<uint16_t, bool> clients_to_notify_;
std::function<void(const std::vector<uint8_t> &)> on_write_;
esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;

View File

@ -12,11 +12,19 @@ namespace esp32_ble_server {
static const char *const TAG = "esp32_ble_server.descriptor"; static const char *const TAG = "esp32_ble_server.descriptor";
BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) { static RAMAllocator<uint8_t> descriptor_allocator{}; // NOLINT
BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len, bool read, bool write) {
this->uuid_ = uuid; this->uuid_ = uuid;
this->value_.attr_len = 0; this->value_.attr_len = 0;
this->value_.attr_max_len = max_len; this->value_.attr_max_len = max_len;
this->value_.attr_value = (uint8_t *) malloc(max_len); // NOLINT this->value_.attr_value = descriptor_allocator.allocate(max_len);
if (read) {
this->permissions_ |= ESP_GATT_PERM_READ;
}
if (write) {
this->permissions_ |= ESP_GATT_PERM_WRITE;
}
} }
BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT
@ -38,14 +46,15 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->state_ = CREATING; this->state_ = CREATING;
} }
void BLEDescriptor::set_value(const std::string &value) { this->set_value((uint8_t *) value.data(), value.length()); } void BLEDescriptor::set_value(std::vector<uint8_t> buffer) {
void BLEDescriptor::set_value(const uint8_t *data, size_t length) { size_t length = buffer.size();
if (length > this->value_.attr_max_len) { if (length > this->value_.attr_max_len) {
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
return; return;
} }
this->value_.attr_len = length; this->value_.attr_len = length;
memcpy(this->value_.attr_value, data, length); memcpy(this->value_.attr_value, buffer.data(), length);
} }
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
@ -61,10 +70,13 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break; break;
} }
case ESP_GATTS_WRITE_EVT: { case ESP_GATTS_WRITE_EVT: {
if (this->handle_ == param->write.handle) { if (this->handle_ != param->write.handle)
break;
this->value_.attr_len = param->write.len; this->value_.attr_len = param->write.len;
memcpy(this->value_.attr_value, param->write.value, param->write.len); memcpy(this->value_.attr_value, param->write.value, param->write.len);
} this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE,
std::vector<uint8_t>(param->write.value, param->write.value + param->write.len),
param->write.conn_id);
break; break;
} }
default: default:

View File

@ -1,6 +1,8 @@
#pragma once #pragma once
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -11,17 +13,26 @@ namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLECharacteristic; class BLECharacteristic;
class BLEDescriptor { namespace BLEDescriptorEvt {
enum VectorEvt {
ON_WRITE,
};
} // namespace BLEDescriptorEvt
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
public: public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100); BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
virtual ~BLEDescriptor(); virtual ~BLEDescriptor();
void do_create(BLECharacteristic *characteristic); void do_create(BLECharacteristic *characteristic);
ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(const std::string &value); void set_value(std::vector<uint8_t> buffer);
void set_value(const uint8_t *data, size_t length); void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
@ -33,9 +44,9 @@ class BLEDescriptor {
ESPBTUUID uuid_; ESPBTUUID uuid_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};
esp_attr_value_t value_; esp_attr_value_t value_{};
esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; esp_gatt_perm_t permissions_{};
enum State : uint8_t { enum State : uint8_t {
FAILED = 0x00, FAILED = 0x00,

View File

@ -19,11 +19,6 @@ namespace esp32_ble_server {
static const char *const TAG = "esp32_ble_server"; static const char *const TAG = "esp32_ble_server";
static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A;
static const uint16_t MODEL_UUID = 0x2A24;
static const uint16_t VERSION_UUID = 0x2A26;
static const uint16_t MANUFACTURER_UUID = 0x2A29;
void BLEServer::setup() { void BLEServer::setup() {
if (this->parent_->is_failed()) { if (this->parent_->is_failed()) {
this->mark_failed(); this->mark_failed();
@ -38,9 +33,27 @@ void BLEServer::loop() {
return; return;
} }
switch (this->state_) { switch (this->state_) {
case RUNNING: case RUNNING: {
return; // Start all services that are pending to start
if (!this->services_to_start_.empty()) {
uint16_t index_to_remove = 0;
// Iterate over the services to start
for (unsigned i = 0; i < this->services_to_start_.size(); i++) {
BLEService *service = this->services_to_start_[i];
if (service->is_created()) {
service->start(); // Needs to be called once per characteristic in the service
} else {
index_to_remove = i + 1;
}
}
// Remove the services that have been started
if (index_to_remove > 0) {
this->services_to_start_.erase(this->services_to_start_.begin(),
this->services_to_start_.begin() + index_to_remove - 1);
}
}
break;
}
case INIT: { case INIT: {
esp_err_t err = esp_ble_gatts_app_register(0); esp_err_t err = esp_ble_gatts_app_register(0);
if (err != ESP_OK) { if (err != ESP_OK) {
@ -53,29 +66,26 @@ void BLEServer::loop() {
} }
case REGISTERING: { case REGISTERING: {
if (this->registered_) { if (this->registered_) {
// Create the device information service first so
// it is at the top of the GATT table
this->device_information_service_->do_create(this);
// Create all services previously created // Create all services previously created
for (auto &pair : this->services_) { for (auto &pair : this->services_) {
pair.second->do_create(this); if (pair.second == this->device_information_service_) {
continue;
} }
if (this->device_information_service_ == nullptr) { pair.second->do_create(this);
this->create_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID));
this->device_information_service_ =
this->get_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID));
this->create_device_characteristics_();
} }
this->state_ = STARTING_SERVICE; this->state_ = STARTING_SERVICE;
} }
break; break;
} }
case STARTING_SERVICE: { case STARTING_SERVICE: {
if (!this->device_information_service_->is_created()) {
break;
}
if (this->device_information_service_->is_running()) { if (this->device_information_service_->is_running()) {
this->state_ = RUNNING; this->state_ = RUNNING;
this->restart_advertising_(); this->restart_advertising_();
ESP_LOGD(TAG, "BLE server setup successfully"); ESP_LOGD(TAG, "BLE server setup successfully");
} else if (!this->device_information_service_->is_starting()) { } else if (this->device_information_service_->is_created()) {
this->device_information_service_->start(); this->device_information_service_->start();
} }
break; break;
@ -93,81 +103,66 @@ void BLEServer::restart_advertising_() {
} }
} }
bool BLEServer::create_device_characteristics_() { BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
if (this->model_.has_value()) {
BLECharacteristic *model =
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(this->model_.value());
} else {
BLECharacteristic *model =
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(ESPHOME_BOARD);
}
BLECharacteristic *version =
this->device_information_service_->create_characteristic(VERSION_UUID, BLECharacteristic::PROPERTY_READ);
version->set_value("ESPHome " ESPHOME_VERSION);
BLECharacteristic *manufacturer =
this->device_information_service_->create_characteristic(MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ);
manufacturer->set_value(this->manufacturer_);
return true;
}
void BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) {
ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str()); ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str());
// If the service already exists, do nothing // Calculate the inst_id for the service
BLEService *service = this->get_service(uuid); uint8_t inst_id = 0;
if (service != nullptr) { for (; inst_id < 0xFF; inst_id++) {
ESP_LOGW(TAG, "BLE service %s already exists", uuid.to_string().c_str()); if (this->get_service(uuid, inst_id) == nullptr) {
return; break;
} }
service = new BLEService(uuid, num_handles, inst_id, advertise); // NOLINT(cppcoreguidelines-owning-memory) }
this->services_.emplace(uuid.to_string(), service); if (inst_id == 0xFF) {
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str());
return nullptr;
}
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
new BLEService(uuid, num_handles, inst_id, advertise);
this->services_.emplace(BLEServer::get_service_key(uuid, inst_id), service);
if (this->parent_->is_active() && this->registered_) {
service->do_create(this); service->do_create(this);
} }
return service;
}
void BLEServer::remove_service(ESPBTUUID uuid) { void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
ESP_LOGV(TAG, "Removing BLE service - %s", uuid.to_string().c_str()); ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id);
BLEService *service = this->get_service(uuid); BLEService *service = this->get_service(uuid, inst_id);
if (service == nullptr) { if (service == nullptr) {
ESP_LOGW(TAG, "BLE service %s not found", uuid.to_string().c_str()); ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id);
return; return;
} }
service->do_delete(); service->do_delete();
delete service; // NOLINT(cppcoreguidelines-owning-memory) delete service; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.erase(uuid.to_string()); this->services_.erase(BLEServer::get_service_key(uuid, inst_id));
} }
BLEService *BLEServer::get_service(ESPBTUUID uuid) { BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
BLEService *service = nullptr; BLEService *service = nullptr;
if (this->services_.count(uuid.to_string()) > 0) { if (this->services_.count(BLEServer::get_service_key(uuid, inst_id)) > 0) {
service = this->services_.at(uuid.to_string()); service = this->services_.at(BLEServer::get_service_key(uuid, inst_id));
} }
return service; return service;
} }
std::string BLEServer::get_service_key(ESPBTUUID uuid, uint8_t inst_id) {
return uuid.to_string() + std::to_string(inst_id);
}
void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTS_CONNECT_EVT: { case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected"); ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id, (void *) this); this->add_client_(param->connect.conn_id);
this->connected_clients_++; this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id);
for (auto *component : this->service_components_) {
component->on_client_connect();
}
break; break;
} }
case ESP_GATTS_DISCONNECT_EVT: { case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected"); ESP_LOGD(TAG, "BLE Client disconnected");
if (this->remove_client_(param->disconnect.conn_id)) this->remove_client_(param->disconnect.conn_id);
this->connected_clients_--;
this->parent_->advertising_start(); this->parent_->advertising_start();
for (auto *component : this->service_components_) { this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id);
component->on_client_disconnect();
}
break; break;
} }
case ESP_GATTS_REG_EVT: { case ESP_GATTS_REG_EVT: {

View File

@ -4,36 +4,38 @@
#include "ble_characteristic.h" #include "ble_characteristic.h"
#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble/ble_advertising.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/queue.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gap_ble_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
namespace esphome { namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer;
class BLEServiceComponent { namespace BLEServerEvt {
public: enum EmptyEvt {
virtual void on_client_connect(){}; ON_CONNECT,
virtual void on_client_disconnect(){}; ON_DISCONNECT,
virtual void start();
virtual void stop();
}; };
} // namespace BLEServerEvt
class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> { class BLEServer : public Component,
public GATTsEventHandler,
public BLEStatusEventHandler,
public Parented<ESP32BLE>,
public EventEmitter<BLEServerEvt::EmptyEvt, uint16_t> {
public: public:
void setup() override; void setup() override;
void loop() override; void loop() override;
@ -44,47 +46,41 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
void teardown(); void teardown();
bool is_running(); bool is_running();
void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; }
void set_model(const std::string &model) { this->model_ = model; }
void set_manufacturer_data(const std::vector<uint8_t> &data) { void set_manufacturer_data(const std::vector<uint8_t> &data) {
this->manufacturer_data_ = data; this->manufacturer_data_ = data;
this->restart_advertising_(); this->restart_advertising_();
} }
void create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15);
void remove_service(ESPBTUUID uuid); void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0);
BLEService *get_service(ESPBTUUID uuid); BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0);
void enqueue_start_service(BLEService *service) { this->services_to_start_.push_back(service); }
void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
uint32_t get_connected_client_count() { return this->connected_clients_; } uint32_t get_connected_client_count() { return this->clients_.size(); }
const std::unordered_map<uint16_t, void *> &get_clients() { return this->clients_; } const std::unordered_set<uint16_t> &get_clients() { return this->clients_; }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) override; esp_ble_gatts_cb_param_t *param) override;
void ble_before_disabled_event_handler() override; void ble_before_disabled_event_handler() override;
void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); }
protected: protected:
bool create_device_characteristics_(); static std::string get_service_key(ESPBTUUID uuid, uint8_t inst_id);
void restart_advertising_(); void restart_advertising_();
void add_client_(uint16_t conn_id, void *client) { this->clients_.emplace(conn_id, client); } void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; } void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
std::string manufacturer_; std::vector<uint8_t> manufacturer_data_{};
optional<std::string> model_;
std::vector<uint8_t> manufacturer_data_;
esp_gatt_if_t gatts_if_{0}; esp_gatt_if_t gatts_if_{0};
bool registered_{false}; bool registered_{false};
uint32_t connected_clients_{0}; std::unordered_set<uint16_t> clients_;
std::unordered_map<uint16_t, void *> clients_; std::unordered_map<std::string, BLEService *> services_{};
std::unordered_map<std::string, BLEService *> services_; std::vector<BLEService *> services_to_start_{};
BLEService *device_information_service_; BLEService *device_information_service_{};
std::vector<BLEServiceComponent *> service_components_;
enum State : uint8_t { enum State : uint8_t {
INIT = 0x00, INIT = 0x00,

View File

@ -0,0 +1,77 @@
#include "ble_server_automations.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
// Interface to interact with ESPHome automations and triggers
namespace esp32_ble_server_automations {
using namespace esp32_ble;
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_write_trigger(
BLECharacteristic *characteristic) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
BLECharacteristicEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
return on_write_trigger;
}
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on(
BLEDescriptorEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
return on_write_trigger;
}
Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_CONNECT,
[on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
return on_connect_trigger;
}
Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
return on_disconnect_trigger;
}
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener) {
// Check if there is already a listener for this characteristic
if (this->listeners_.count(characteristic) > 0) {
// Unpack the pair listener_id, pre_notify_listener_id
auto listener_pairs = this->listeners_[characteristic];
EventEmitterListenerID old_listener_id = listener_pairs.first;
EventEmitterListenerID old_pre_notify_listener_id = listener_pairs.second;
// Remove the previous listener
characteristic->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::off(BLECharacteristicEvt::EmptyEvt::ON_READ,
old_listener_id);
// Remove the pre-notify listener
this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, old_pre_notify_listener_id);
}
// Create a new listener for the pre-notify event
EventEmitterListenerID pre_notify_listener_id =
this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY,
[pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) {
// Only call the pre-notify listener if the characteristic is the one we are interested in
if (characteristic == evt_characteristic) {
pre_notify_listener();
}
});
// Save the pair listener_id, pre_notify_listener_id to the map
this->listeners_[characteristic] = std::make_pair(listener_id, pre_notify_listener_id);
}
} // namespace esp32_ble_server_automations
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@ -0,0 +1,115 @@
#pragma once
#include "ble_server.h"
#include "ble_characteristic.h"
#include "ble_descriptor.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/core/automation.h"
#include <vector>
#include <unordered_map>
#include <functional>
#ifdef USE_ESP32
namespace esphome {
namespace esp32_ble_server {
// Interface to interact with ESPHome actions and triggers
namespace esp32_ble_server_automations {
using namespace esp32_ble;
using namespace event_emitter;
class BLETriggers {
public:
static Trigger<std::vector<uint8_t>, uint16_t> *create_characteristic_on_write_trigger(
BLECharacteristic *characteristic);
static Trigger<std::vector<uint8_t>, uint16_t> *create_descriptor_on_write_trigger(BLEDescriptor *descriptor);
static Trigger<uint16_t> *create_server_on_connect_trigger(BLEServer *server);
static Trigger<uint16_t> *create_server_on_disconnect_trigger(BLEServer *server);
};
enum BLECharacteristicSetValueActionEvt {
PRE_NOTIFY,
};
// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic
class BLECharacteristicSetValueActionManager
: public EventEmitter<BLECharacteristicSetValueActionEvt, BLECharacteristic *> {
public:
// Singleton pattern
static BLECharacteristicSetValueActionManager *get_instance() {
static BLECharacteristicSetValueActionManager instance;
return &instance;
}
void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener);
EventEmitterListenerID get_listener(BLECharacteristic *characteristic) {
return this->listeners_[characteristic].first;
}
void emit_pre_notify(BLECharacteristic *characteristic) {
this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic);
}
private:
std::unordered_map<BLECharacteristic *, std::pair<EventEmitterListenerID, EventEmitterListenerID>> listeners_;
};
template<typename... Ts> class BLECharacteristicSetValueAction : public Action<Ts...> {
public:
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(Ts... x) override {
// If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_)
return;
// Set initial value
this->parent_->set_value(this->buffer_.value(x...));
// Set the listener for read events
this->listener_id_ = this->parent_->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::on(
BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) {
// Set the value of the characteristic every time it is read
this->parent_->set_value(this->buffer_.value(x...));
});
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
BLECharacteristicSetValueActionManager::get_instance()->set_listener(
this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
}
protected:
BLECharacteristic *parent_;
EventEmitterListenerID listener_id_;
};
template<typename... Ts> class BLECharacteristicNotifyAction : public Action<Ts...> {
public:
BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
void play(Ts... x) override {
// Call the pre-notify event
BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_);
// Notify the characteristic
this->parent_->notify();
}
protected:
BLECharacteristic *parent_;
};
template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts...> {
public:
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); }
protected:
BLEDescriptor *parent_;
};
} // namespace esp32_ble_server_automations
} // namespace esp32_ble_server
} // namespace esphome
#endif

View File

@ -52,18 +52,21 @@ void BLEService::do_create(BLEServer *server) {
esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_); esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err); ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err);
this->init_state_ = FAILED; this->state_ = FAILED;
return; return;
} }
this->init_state_ = CREATING; this->state_ = CREATING;
} }
void BLEService::do_delete() { void BLEService::do_delete() {
if (this->init_state_ == DELETING || this->init_state_ == DELETED) if (this->state_ == DELETING || this->state_ == DELETED)
return; return;
this->init_state_ = DELETING; this->state_ = DELETING;
this->created_characteristic_count_ = 0; this->created_characteristic_count_ = 0;
this->last_created_characteristic_ = nullptr; this->last_created_characteristic_ = nullptr;
// Call all characteristics to delete
for (auto *characteristic : this->characteristics_)
characteristic->do_delete();
this->stop_(); this->stop_();
esp_err_t err = esp_ble_gatts_delete_service(this->handle_); esp_err_t err = esp_ble_gatts_delete_service(this->handle_);
if (err != ESP_OK) { if (err != ESP_OK) {
@ -91,6 +94,7 @@ void BLEService::start() {
return; return;
should_start_ = true; should_start_ = true;
this->state_ = STARTING;
esp_err_t err = esp_ble_gatts_start_service(this->handle_); esp_err_t err = esp_ble_gatts_start_service(this->handle_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err); ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err);
@ -98,7 +102,6 @@ void BLEService::start() {
} }
if (this->advertise_) if (this->advertise_)
esp32_ble::global_ble->advertising_add_service_uuid(this->uuid_); esp32_ble::global_ble->advertising_add_service_uuid(this->uuid_);
this->running_state_ = STARTING;
} }
void BLEService::stop() { void BLEService::stop() {
@ -107,9 +110,9 @@ void BLEService::stop() {
} }
void BLEService::stop_() { void BLEService::stop_() {
if (this->running_state_ == STOPPING || this->running_state_ == STOPPED) if (this->state_ == STOPPING || this->state_ == STOPPED)
return; return;
this->running_state_ = STOPPING; this->state_ = STOPPING;
esp_err_t err = esp_ble_gatts_stop_service(this->handle_); esp_err_t err = esp_ble_gatts_stop_service(this->handle_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err);
@ -119,17 +122,16 @@ void BLEService::stop_() {
esp32_ble::global_ble->advertising_remove_service_uuid(this->uuid_); esp32_ble::global_ble->advertising_remove_service_uuid(this->uuid_);
} }
bool BLEService::is_created() { return this->init_state_ == CREATED; }
bool BLEService::is_failed() { bool BLEService::is_failed() {
if (this->init_state_ == FAILED) if (this->state_ == FAILED)
return true; return true;
bool failed = false; bool failed = false;
for (auto *characteristic : this->characteristics_) for (auto *characteristic : this->characteristics_)
failed |= characteristic->is_failed(); failed |= characteristic->is_failed();
if (failed) if (failed)
this->init_state_ = FAILED; this->state_ = FAILED;
return this->init_state_ == FAILED; return this->state_ == FAILED;
} }
void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
@ -139,7 +141,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g
if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) && if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) &&
this->inst_id_ == param->create.service_id.id.inst_id) { this->inst_id_ == param->create.service_id.id.inst_id) {
this->handle_ = param->create.service_handle; this->handle_ = param->create.service_handle;
this->init_state_ = CREATED; this->state_ = CREATED;
if (this->should_start_) if (this->should_start_)
this->start(); this->start();
} }
@ -147,18 +149,18 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g
} }
case ESP_GATTS_DELETE_EVT: case ESP_GATTS_DELETE_EVT:
if (param->del.service_handle == this->handle_) { if (param->del.service_handle == this->handle_) {
this->init_state_ = DELETED; this->state_ = DELETED;
} }
break; break;
case ESP_GATTS_START_EVT: { case ESP_GATTS_START_EVT: {
if (param->start.service_handle == this->handle_) { if (param->start.service_handle == this->handle_) {
this->running_state_ = RUNNING; this->state_ = RUNNING;
} }
break; break;
} }
case ESP_GATTS_STOP_EVT: { case ESP_GATTS_STOP_EVT: {
if (param->start.service_handle == this->handle_) { if (param->start.service_handle == this->handle_) {
this->running_state_ = STOPPED; this->state_ = STOPPED;
} }
break; break;
} }

View File

@ -32,6 +32,7 @@ class BLEService {
BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties); BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties);
ESPBTUUID get_uuid() { return this->uuid_; } ESPBTUUID get_uuid() { return this->uuid_; }
uint8_t get_inst_id() { return this->inst_id_; }
BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; } BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; }
uint16_t get_handle() { return this->handle_; } uint16_t get_handle() { return this->handle_; }
@ -44,18 +45,17 @@ class BLEService {
void start(); void start();
void stop(); void stop();
bool is_created();
bool is_failed(); bool is_failed();
bool is_created() { return this->state_ == CREATED; }
bool is_running() { return this->running_state_ == RUNNING; } bool is_running() { return this->state_ == RUNNING; }
bool is_starting() { return this->running_state_ == STARTING; } bool is_starting() { return this->state_ == STARTING; }
bool is_deleted() { return this->init_state_ == DELETED; } bool is_deleted() { return this->state_ == DELETED; }
protected: protected:
std::vector<BLECharacteristic *> characteristics_; std::vector<BLECharacteristic *> characteristics_;
BLECharacteristic *last_created_characteristic_{nullptr}; BLECharacteristic *last_created_characteristic_{nullptr};
uint32_t created_characteristic_count_{0}; uint32_t created_characteristic_count_{0};
BLEServer *server_; BLEServer *server_ = nullptr;
ESPBTUUID uuid_; ESPBTUUID uuid_;
uint16_t num_handles_; uint16_t num_handles_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};
@ -66,22 +66,18 @@ class BLEService {
bool do_create_characteristics_(); bool do_create_characteristics_();
void stop_(); void stop_();
enum InitState : uint8_t { enum State : uint8_t {
FAILED = 0x00, FAILED = 0x00,
INIT, INIT,
CREATING, CREATING,
CREATING_DEPENDENTS,
CREATED, CREATED,
DELETING,
DELETED,
} init_state_{INIT};
enum RunningState : uint8_t {
STARTING, STARTING,
RUNNING, RUNNING,
STOPPING, STOPPING,
STOPPED, STOPPED,
} running_state_{STOPPED}; DELETING,
DELETED,
} state_{INIT};
}; };
} // namespace esp32_ble_server } // namespace esp32_ble_server

View File

@ -1,9 +1,13 @@
import re
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import (
bt_uuid,
bt_uuid16_format,
bt_uuid32_format,
bt_uuid128_format,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTIVE, CONF_ACTIVE,
@ -86,43 +90,6 @@ def validate_scan_parameters(config):
return config return config
bt_uuid16_format = "XXXX"
bt_uuid32_format = "XXXXXXXX"
bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
def bt_uuid(value):
in_value = cv.string_strict(value)
value = in_value.upper()
if len(value) == len(bt_uuid16_format):
pattern = re.compile("^[A-F|0-9]{4,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid32_format):
pattern = re.compile("^[A-F|0-9]{8,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid128_format):
pattern = re.compile(
"^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$"
)
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 128 UUID format: '{in_value}'"
)
return value
raise cv.Invalid(
f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format"
)
def as_hex(value): def as_hex(value):
return cg.RawExpression(f"0x{value}ULL") return cg.RawExpression(f"0x{value}ULL")

View File

@ -7,13 +7,16 @@
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <esp32-hal-dac.h> #include <esp32-hal-dac.h>
#endif #endif
#ifdef USE_ESP_IDF
#include <driver/dac.h>
#endif
namespace esphome { namespace esphome {
namespace esp32_dac { namespace esp32_dac {
#ifdef USE_ESP32_VARIANT_ESP32S2
static constexpr uint8_t DAC0_PIN = 17;
#else
static constexpr uint8_t DAC0_PIN = 25;
#endif
static const char *const TAG = "esp32_dac"; static const char *const TAG = "esp32_dac";
void ESP32DAC::setup() { void ESP32DAC::setup() {
@ -22,8 +25,15 @@ void ESP32DAC::setup() {
this->turn_off(); this->turn_off();
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
auto channel = pin_->get_pin() == 25 ? DAC_CHANNEL_1 : DAC_CHANNEL_2; const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
dac_output_enable(channel); const dac_oneshot_config_t oneshot_cfg{channel};
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
#endif
}
void ESP32DAC::on_safe_shutdown() {
#ifdef USE_ESP_IDF
dac_oneshot_del_channel(this->dac_handle_);
#endif #endif
} }
@ -40,8 +50,7 @@ void ESP32DAC::write_state(float state) {
state = state * 255; state = state * 255;
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
auto channel = pin_->get_pin() == 25 ? DAC_CHANNEL_1 : DAC_CHANNEL_2; dac_oneshot_output_voltage(this->dac_handle_, state);
dac_output_voltage(channel, (uint8_t) state);
#endif #endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
dacWrite(this->pin_->get_pin(), state); dacWrite(this->pin_->get_pin(), state);

View File

@ -7,6 +7,10 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <driver/dac_oneshot.h>
#endif
namespace esphome { namespace esphome {
namespace esp32_dac { namespace esp32_dac {
@ -16,6 +20,7 @@ class ESP32DAC : public output::FloatOutput, public Component {
/// Initialize pin /// Initialize pin
void setup() override; void setup() override;
void on_safe_shutdown() override;
void dump_config() override; void dump_config() override;
/// HARDWARE setup_priority /// HARDWARE setup_priority
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
@ -24,6 +29,9 @@ class ESP32DAC : public output::FloatOutput, public Component {
void write_state(float state) override; void write_state(float state) override;
InternalGPIOPin *pin_; InternalGPIOPin *pin_;
#ifdef USE_ESP_IDF
dac_oneshot_handle_t dac_handle_;
#endif
}; };
} // namespace esp32_dac } // namespace esp32_dac

View File

@ -1,15 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins from esphome import pins
from esphome.components import output from esphome.components import output
import esphome.config_validation as cv from esphome.components.esp32 import get_esp32_variant
import esphome.codegen as cg from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_ESP32S2
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
DAC_PINS = {
VARIANT_ESP32: (25, 26),
VARIANT_ESP32S2: (17, 18),
}
def valid_dac_pin(value): def valid_dac_pin(value):
num = value[CONF_NUMBER] variant = get_esp32_variant()
cv.one_of(25, 26)(num) try:
valid_pins = DAC_PINS[variant]
except KeyError as ex:
raise cv.Invalid(f"DAC is not supported on {variant}") from ex
given_pin = value[CONF_NUMBER]
cv.one_of(*valid_pins)(given_pin)
return value return value

View File

@ -1,6 +1,6 @@
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble_server, output from esphome.components import binary_sensor, output
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
@ -24,9 +24,7 @@ Error = improv_ns.enum("Error")
State = improv_ns.enum("State") State = improv_ns.enum("State")
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
ESP32ImprovComponent = esp32_improv_ns.class_( ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component)
"ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent
)
ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_( ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisionedTrigger", automation.Trigger.template() "ESP32ImprovProvisionedTrigger", automation.Trigger.template()
) )
@ -47,7 +45,6 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer),
cv.Required(CONF_AUTHORIZER): cv.Any( cv.Required(CONF_AUTHORIZER): cv.Any(
cv.none, cv.use_id(binary_sensor.BinarySensor) cv.none, cv.use_id(binary_sensor.BinarySensor)
), ),
@ -100,9 +97,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
ble_server = await cg.get_variable(config[CONF_BLE_SERVER_ID])
cg.add(ble_server.register_service_component(var))
cg.add_define("USE_IMPROV") cg.add_define("USE_IMPROV")
cg.add_library("improv/Improv", "1.2.4") cg.add_library("improv/Improv", "1.2.4")

View File

@ -4,12 +4,15 @@
#include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/components/esp32_ble_server/ble_2902.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
namespace esphome { namespace esphome {
namespace esp32_improv { namespace esp32_improv {
using namespace bytebuffer;
static const char *const TAG = "esp32_improv.component"; static const char *const TAG = "esp32_improv.component";
static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
@ -26,6 +29,8 @@ void ESP32ImprovComponent::setup() {
}); });
} }
#endif #endif
global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
} }
void ESP32ImprovComponent::setup_characteristics() { void ESP32ImprovComponent::setup_characteristics() {
@ -40,7 +45,8 @@ void ESP32ImprovComponent::setup_characteristics() {
this->error_->add_descriptor(error_descriptor); this->error_->add_descriptor(error_descriptor);
this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
this->rpc_->on_write([this](const std::vector<uint8_t> &data) { this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) {
if (!data.empty()) { if (!data.empty()) {
this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
} }
@ -62,7 +68,7 @@ void ESP32ImprovComponent::setup_characteristics() {
if (this->status_indicator_ != nullptr) if (this->status_indicator_ != nullptr)
capabilities |= improv::CAPABILITY_IDENTIFY; capabilities |= improv::CAPABILITY_IDENTIFY;
#endif #endif
this->capabilities_->set_value(capabilities); this->capabilities_->set_value(ByteBuffer::wrap(capabilities));
this->setup_complete_ = true; this->setup_complete_ = true;
} }
@ -80,8 +86,7 @@ void ESP32ImprovComponent::loop() {
if (this->service_ == nullptr) { if (this->service_ == nullptr) {
// Setup the service // Setup the service
ESP_LOGD(TAG, "Creating Improv service"); ESP_LOGD(TAG, "Creating Improv service");
global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true); this->service_ = global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true);
this->service_ = global_ble_server->get_service(ESPBTUUID::from_raw(improv::SERVICE_UUID));
this->setup_characteristics(); this->setup_characteristics();
} }
@ -93,15 +98,15 @@ void ESP32ImprovComponent::loop() {
case improv::STATE_STOPPED: case improv::STATE_STOPPED:
this->set_status_indicator_state_(false); this->set_status_indicator_state_(false);
if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { if (this->should_start_ && this->setup_complete_) {
if (this->service_->is_running()) { if (this->service_->is_created()) {
this->service_->start();
} else if (this->service_->is_running()) {
esp32_ble::global_ble->advertising_start(); esp32_ble::global_ble->advertising_start();
this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
this->set_error_(improv::ERROR_NONE); this->set_error_(improv::ERROR_NONE);
ESP_LOGD(TAG, "Service started!"); ESP_LOGD(TAG, "Service started!");
} else {
this->service_->start();
} }
} }
break; break;
@ -199,8 +204,7 @@ void ESP32ImprovComponent::set_state_(improv::State state) {
ESP_LOGV(TAG, "Setting state: %d", state); ESP_LOGV(TAG, "Setting state: %d", state);
this->state_ = state; this->state_ = state;
if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) { if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) {
uint8_t data[1]{state}; this->status_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(state)));
this->status_->set_value(data, 1);
if (state != improv::STATE_STOPPED) if (state != improv::STATE_STOPPED)
this->status_->notify(); this->status_->notify();
} }
@ -232,15 +236,14 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
ESP_LOGE(TAG, "Error: %d", error); ESP_LOGE(TAG, "Error: %d", error);
} }
if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) {
uint8_t data[1]{error}; this->error_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(error)));
this->error_->set_value(data, 1);
if (this->state_ != improv::STATE_STOPPED) if (this->state_ != improv::STATE_STOPPED)
this->error_->notify(); this->error_->notify();
} }
} }
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) { void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
this->rpc_response_->set_value(response); this->rpc_response_->set_value(ByteBuffer::wrap(response));
if (this->state_ != improv::STATE_STOPPED) if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify(); this->rpc_response_->notify();
} }
@ -339,8 +342,6 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
wifi::global_wifi_component->clear_sta(); wifi::global_wifi_component->clear_sta();
} }
void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); };
ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esp32_improv } // namespace esp32_improv

View File

@ -32,18 +32,17 @@ namespace esp32_improv {
using namespace esp32_ble_server; using namespace esp32_ble_server;
class ESP32ImprovComponent : public Component, public BLEServiceComponent { class ESP32ImprovComponent : public Component {
public: public:
ESP32ImprovComponent(); ESP32ImprovComponent();
void dump_config() override; void dump_config() override;
void loop() override; void loop() override;
void setup() override; void setup() override;
void setup_characteristics(); void setup_characteristics();
void on_client_disconnect() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void start() override; void start();
void stop() override; void stop();
bool is_active() const { return this->state_ != improv::STATE_STOPPED; } bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK #ifdef USE_ESP32_IMPROV_STATE_CALLBACK

View File

@ -127,12 +127,12 @@ CONFIG_SCHEMA = cv.All(
), ),
OptionalForIDF5( OptionalForIDF5(
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
esp32_idf=64, esp32_idf=192,
esp32_s2_idf=64, esp32_s2_idf=192,
esp32_s3_idf=48, esp32_s3_idf=192,
esp32_c3_idf=48, esp32_c3_idf=96,
esp32_c6_idf=48, esp32_c6_idf=96,
esp32_h2_idf=48, esp32_h2_idf=96,
): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)),
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),

View File

@ -22,6 +22,7 @@ class ESP8266GPIOPin : public InternalGPIOPin {
void detach_interrupt() const override; void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override; ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; } uint8_t get_pin() const override { return pin_; }
gpio::Flags get_flags() const override { return flags_; }
bool is_inverted() const override { return inverted_; } bool is_inverted() const override { return inverted_; }
protected: protected:

View File

@ -112,8 +112,7 @@ def validate_supports(value):
) )
if is_pullup and num == 16: if is_pullup and num == 16:
raise cv.Invalid( raise cv.Invalid(
"GPIO Pin 16 does not support pullup pin mode. " "GPIO Pin 16 does not support pullup pin mode. Please choose another pin.",
"Please choose another pin.",
[CONF_MODE, CONF_PULLUP], [CONF_MODE, CONF_PULLUP],
) )
if is_pulldown and num != 16: if is_pulldown and num != 16:

View File

@ -0,0 +1,5 @@
CODEOWNERS = ["@Rapsssito"]
# Allows event_emitter to be configured in yaml, to allow use of the C++ api.
CONFIG_SCHEMA = {}

View File

@ -0,0 +1,14 @@
#include "event_emitter.h"
namespace esphome {
namespace event_emitter {
static const char *const TAG = "event_emitter";
void raise_event_emitter_full_error() {
ESP_LOGE(TAG, "EventEmitter has reached the maximum number of listeners for event");
ESP_LOGW(TAG, "Removing listener to make space for new listener");
}
} // namespace event_emitter
} // namespace esphome

View File

@ -0,0 +1,63 @@
#pragma once
#include <unordered_map>
#include <vector>
#include <functional>
#include <limits>
#include "esphome/core/log.h"
namespace esphome {
namespace event_emitter {
using EventEmitterListenerID = uint32_t;
void raise_event_emitter_full_error();
// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this)
// and a list of arguments. Supports multiple listeners for each event.
template<typename EvtType, typename... Args> class EventEmitter {
public:
EventEmitterListenerID on(EvtType event, std::function<void(Args...)> listener) {
EventEmitterListenerID listener_id = get_next_id_(event);
listeners_[event][listener_id] = listener;
return listener_id;
}
void off(EvtType event, EventEmitterListenerID id) {
if (listeners_.count(event) == 0)
return;
listeners_[event].erase(id);
}
protected:
void emit_(EvtType event, Args... args) {
if (listeners_.count(event) == 0)
return;
for (const auto &listener : listeners_[event]) {
listener.second(args...);
}
}
EventEmitterListenerID get_next_id_(EvtType event) {
// Check if the map is full
if (listeners_[event].size() == std::numeric_limits<EventEmitterListenerID>::max()) {
// Raise an error if the map is full
raise_event_emitter_full_error();
off(event, 0);
return 0;
}
// Get the next ID for the given event.
EventEmitterListenerID next_id = (current_id_ + 1) % std::numeric_limits<EventEmitterListenerID>::max();
while (listeners_[event].count(next_id) > 0) {
next_id = (next_id + 1) % std::numeric_limits<EventEmitterListenerID>::max();
}
current_id_ = next_id;
return current_id_;
}
private:
std::unordered_map<EvtType, std::unordered_map<EventEmitterListenerID, std::function<void(Args...)>>> listeners_;
EventEmitterListenerID current_id_ = 0;
};
} // namespace event_emitter
} // namespace esphome

View File

@ -5,8 +5,8 @@ import os
from pathlib import Path from pathlib import Path
import re import re
import esphome_glyphsets as glyphsets
import freetype import freetype
import glyphsets
import requests import requests
from esphome import core, external_files from esphome import core, external_files

View File

@ -4,9 +4,6 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include <algorithm> #include <algorithm>
#include <sstream>
#include <iostream> // std::cout, std::fixed
#include <iomanip>
namespace esphome { namespace esphome {
namespace graph { namespace graph {
@ -231,9 +228,8 @@ void GraphLegend::init(Graph *g) {
ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh); ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
if (this->values_ != VALUE_POSITION_TYPE_NONE) { if (this->values_ != VALUE_POSITION_TYPE_NONE) {
std::stringstream ss; std::string valstr =
ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
std::string valstr = ss.str();
if (this->units_) { if (this->units_) {
valstr += trace->sensor_->get_unit_of_measurement(); valstr += trace->sensor_->get_unit_of_measurement();
} }
@ -368,9 +364,8 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of
if (legend_->values_ != VALUE_POSITION_TYPE_NONE) { if (legend_->values_ != VALUE_POSITION_TYPE_NONE) {
int xv = x + legend_->xv_; int xv = x + legend_->xv_;
int yv = y + legend_->yv_; int yv = y + legend_->yv_;
std::stringstream ss; std::string valstr =
ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state(); value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
std::string valstr = ss.str();
if (legend_->units_) { if (legend_->units_) {
valstr += trace->sensor_->get_unit_of_measurement(); valstr += trace->sensor_->get_unit_of_measurement();
} }

View File

@ -1,9 +1,15 @@
import logging import logging
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components import uart, climate, logger
from esphome import automation from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, logger, uart
from esphome.components.climate import (
CONF_CURRENT_TEMPERATURE,
ClimateMode,
ClimatePreset,
ClimateSwingMode,
)
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BEEPER, CONF_BEEPER,
CONF_DISPLAY, CONF_DISPLAY,
@ -24,12 +30,7 @@ from esphome.const import (
CONF_VISUAL, CONF_VISUAL,
CONF_WIFI, CONF_WIFI,
) )
from esphome.components.climate import ( import esphome.final_validate as fv
ClimateMode,
ClimatePreset,
ClimateSwingMode,
CONF_CURRENT_TEMPERATURE,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -21,6 +21,7 @@ class HostGPIOPin : public InternalGPIOPin {
void detach_interrupt() const override; void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override; ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; } uint8_t get_pin() const override { return pin_; }
gpio::Flags get_flags() const override { return flags_; }
bool is_inverted() const override { return inverted_; } bool is_inverted() const override { return inverted_; }
protected: protected:

View File

@ -18,8 +18,8 @@ namespace esphome {
namespace http_request { namespace http_request {
struct Header { struct Header {
const char *name; std::string name;
const char *value; std::string value;
}; };
// Some common HTTP status codes // Some common HTTP status codes

View File

@ -96,7 +96,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
container->client_.setUserAgent(this->useragent_); container->client_.setUserAgent(this->useragent_);
} }
for (const auto &header : headers) { for (const auto &header : headers) {
container->client_.addHeader(header.name, header.value, false, true); container->client_.addHeader(header.name.c_str(), header.value.c_str(), false, true);
} }
// returned needed headers must be collected before the requests // returned needed headers must be collected before the requests

View File

@ -84,7 +84,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
container->set_secure(secure); container->set_secure(secure);
for (const auto &header : headers) { for (const auto &header : headers) {
esp_http_client_set_header(client, header.name, header.value); esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
} }
const int body_len = body.length(); const int body_len = body.length();

View File

@ -39,6 +39,10 @@ void IDFI2CBus::setup() {
conf.scl_io_num = scl_pin_; conf.scl_io_num = scl_pin_;
conf.scl_pullup_en = scl_pullup_enabled_; conf.scl_pullup_en = scl_pullup_enabled_;
conf.master.clk_speed = frequency_; conf.master.clk_speed = frequency_;
#ifdef USE_ESP32_VARIANT_ESP32S2
// workaround for https://github.com/esphome/issues/issues/6718
conf.clk_flags = I2C_SCLK_SRC_FLAG_AWARE_DFS;
#endif
esp_err_t err = i2c_param_config(port_, &conf); esp_err_t err = i2c_param_config(port_, &conf);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_param_config failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "i2c_param_config failed: %s", esp_err_to_name(err));

View File

@ -1,13 +1,25 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, speaker from esphome.components import audio, esp32, speaker
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODE, CONF_TIMEOUT from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_BUFFER_DURATION,
CONF_CHANNEL,
CONF_ID,
CONF_MODE,
CONF_NEVER,
CONF_NUM_CHANNELS,
CONF_SAMPLE_RATE,
CONF_TIMEOUT,
)
from .. import ( from .. import (
CONF_I2S_DOUT_PIN, CONF_I2S_DOUT_PIN,
CONF_I2S_MODE,
CONF_LEFT, CONF_LEFT,
CONF_MONO, CONF_MONO,
CONF_PRIMARY,
CONF_RIGHT, CONF_RIGHT,
CONF_STEREO, CONF_STEREO,
I2SAudioOut, I2SAudioOut,
@ -24,10 +36,8 @@ I2SAudioSpeaker = i2s_audio_ns.class_(
"I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut
) )
CONF_BUFFER_DURATION = "buffer_duration"
CONF_DAC_TYPE = "dac_type" CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt" CONF_I2S_COMM_FMT = "i2s_comm_fmt"
CONF_NEVER = "never"
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
INTERNAL_DAC_OPTIONS = { INTERNAL_DAC_OPTIONS = {
@ -53,7 +63,41 @@ I2C_COMM_FMT_OPTIONS = {
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
def validate_esp32_variant(config): def _set_num_channels_from_config(config):
if config[CONF_CHANNEL] in (CONF_MONO, CONF_LEFT, CONF_RIGHT):
config[CONF_NUM_CHANNELS] = 1
else:
config[CONF_NUM_CHANNELS] = 2
return config
def _set_stream_limits(config):
if config[CONF_I2S_MODE] == CONF_PRIMARY:
# Primary mode has modifiable stream settings
audio.set_stream_limits(
min_bits_per_sample=8,
max_bits_per_sample=32,
min_channels=1,
max_channels=2,
min_sample_rate=16000,
max_sample_rate=48000,
)(config)
else:
# Secondary mode has unmodifiable max bits per sample and min/max sample rates
audio.set_stream_limits(
min_bits_per_sample=8,
max_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE),
min_channels=1,
max_channels=2,
min_sample_rate=config.get(CONF_SAMPLE_RATE),
max_sample_rate=config.get(CONF_SAMPLE_RATE),
)
return config
def _validate_esp32_variant(config):
if config[CONF_DAC_TYPE] != "internal": if config[CONF_DAC_TYPE] != "internal":
return config return config
variant = esp32.get_esp32_variant() variant = esp32.get_esp32_variant()
@ -85,6 +129,7 @@ BASE_SCHEMA = (
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
) )
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.typed_schema( cv.typed_schema(
{ {
@ -106,7 +151,9 @@ CONFIG_SCHEMA = cv.All(
}, },
key=CONF_DAC_TYPE, key=CONF_DAC_TYPE,
), ),
validate_esp32_variant, _validate_esp32_variant,
_set_num_channels_from_config,
_set_stream_limits,
) )

View File

@ -148,9 +148,11 @@ void I2SAudioSpeaker::loop() {
this->status_set_error("Failed to adjust I2S bus to match the incoming audio"); this->status_set_error("Failed to adjust I2S bus to match the incoming audio");
ESP_LOGE(TAG, ESP_LOGE(TAG,
"Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8,
this->audio_stream_info_.sample_rate, this->audio_stream_info_.channels, this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(),
this->audio_stream_info_.bits_per_sample); this->audio_stream_info_.get_bits_per_sample());
} }
xEventGroupClearBits(this->event_group_, ALL_ERR_ESP_BITS);
} }
void I2SAudioSpeaker::set_volume(float volume) { void I2SAudioSpeaker::set_volume(float volume) {
@ -201,6 +203,12 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick
this->start(); this->start();
} }
if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() == 1)) {
// Unable to write data to a running speaker, so delay the max amount of time so it can get ready
vTaskDelay(ticks_to_wait);
ticks_to_wait = 0;
}
size_t bytes_written = 0; size_t bytes_written = 0;
if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) { if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) {
// Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are // Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are
@ -223,6 +231,8 @@ bool I2SAudioSpeaker::has_buffered_data() const {
void I2SAudioSpeaker::speaker_task(void *params) { void I2SAudioSpeaker::speaker_task(void *params) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
this_speaker->task_created_ = true;
uint32_t event_group_bits = uint32_t event_group_bits =
xEventGroupWaitBits(this_speaker->event_group_, xEventGroupWaitBits(this_speaker->event_group_,
SpeakerEventGroupBits::COMMAND_START | SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_START | SpeakerEventGroupBits::COMMAND_STOP |
@ -240,19 +250,20 @@ void I2SAudioSpeaker::speaker_task(void *params) {
audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_; audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_;
const uint32_t bytes_per_ms = const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT;
audio_stream_info.channels * audio_stream_info.get_bytes_per_sample() * audio_stream_info.sample_rate / 1000; // Ensure ring buffer duration is at least the duration of all DMA buffers
const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_);
const size_t dma_buffers_size = DMA_BUFFERS_COUNT * DMA_BUFFER_DURATION_MS * bytes_per_ms; // The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info
const size_t data_buffer_size = audio_stream_info.ms_to_bytes(dma_buffers_duration_ms);
const size_t ring_buffer_size = audio_stream_info.ms_to_bytes(ring_buffer_duration);
// Ensure ring buffer is at least as large as the total size of the DMA buffers const size_t single_dma_buffer_input_size = data_buffer_size / DMA_BUFFERS_COUNT;
const size_t ring_buffer_size =
std::max((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) { if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(data_buffer_size, ring_buffer_size))) {
// Failed to allocate buffers // Failed to allocate buffers
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
this_speaker->delete_task_(dma_buffers_size); this_speaker->delete_task_(data_buffer_size);
} }
if (!this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_(audio_stream_info))) { if (!this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_(audio_stream_info))) {
@ -262,20 +273,25 @@ void I2SAudioSpeaker::speaker_task(void *params) {
uint32_t last_data_received_time = millis(); uint32_t last_data_received_time = millis();
bool tx_dma_underflow = false; bool tx_dma_underflow = false;
while (!this_speaker->timeout_.has_value() || this_speaker->accumulated_frames_written_ = 0;
// Keep looping if paused, there is no timeout configured, or data was received more recently than the configured
// timeout
while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() ||
(millis() - last_data_received_time) <= this_speaker->timeout_.value()) { (millis() - last_data_received_time) <= this_speaker->timeout_.value()) {
event_group_bits = xEventGroupGetBits(this_speaker->event_group_); event_group_bits = xEventGroupGetBits(this_speaker->event_group_);
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
break; break;
} }
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) {
xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY);
stop_gracefully = true; stop_gracefully = true;
} }
if (this_speaker->audio_stream_info_ != audio_stream_info) { if (this_speaker->audio_stream_info_ != audio_stream_info) {
// Audio stream info has changed, stop the speaker task so it will restart with the proper settings. // Audio stream info changed, stop the speaker task so it will restart with the proper settings.
break; break;
} }
@ -286,33 +302,64 @@ void I2SAudioSpeaker::speaker_task(void *params) {
} }
} }
size_t bytes_to_read = dma_buffers_size; if (this_speaker->pause_state_) {
size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, bytes_to_read, // Pause state is accessed atomically, so thread safe
// Delay so the task can yields, then skip transferring audio data
delay(TASK_DELAY_MS);
continue;
}
size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, data_buffer_size,
pdMS_TO_TICKS(TASK_DELAY_MS)); pdMS_TO_TICKS(TASK_DELAY_MS));
if (bytes_read > 0) { if (bytes_read > 0) {
size_t bytes_written = 0; if ((audio_stream_info.get_bits_per_sample() == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) {
if ((audio_stream_info.bits_per_sample == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) {
// Scale samples by the volume factor in place // Scale samples by the volume factor in place
q15_multiplication((int16_t *) this_speaker->data_buffer_, (int16_t *) this_speaker->data_buffer_, q15_multiplication((int16_t *) this_speaker->data_buffer_, (int16_t *) this_speaker->data_buffer_,
bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_); bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_);
} }
if (audio_stream_info.bits_per_sample == (uint8_t) this_speaker->bits_per_sample_) { // Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played
i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_, bytes_read, &bytes_written, // callback.
portMAX_DELAY); const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size;
} else if (audio_stream_info.bits_per_sample < (uint8_t) this_speaker->bits_per_sample_) {
i2s_write_expand(this_speaker->parent_->get_port(), this_speaker->data_buffer_, bytes_read, for (uint32_t i = 0; i < batches; ++i) {
audio_stream_info.bits_per_sample, this_speaker->bits_per_sample_, &bytes_written, size_t bytes_written = 0;
portMAX_DELAY); size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read);
if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
} else if (audio_stream_info.get_bits_per_sample() < (uint8_t) this_speaker->bits_per_sample_) {
i2s_write_expand(this_speaker->parent_->get_port(),
this_speaker->data_buffer_ + i * single_dma_buffer_input_size, bytes_to_write,
audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written,
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
} }
if (bytes_written != bytes_read) { uint32_t write_timestamp = micros();
if (bytes_written != bytes_to_write) {
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE);
} }
bytes_read -= bytes_written;
this_speaker->accumulated_frames_written_ += audio_stream_info.bytes_to_frames(bytes_written);
const uint32_t new_playback_ms =
audio_stream_info.frames_to_milliseconds_with_remainder(&this_speaker->accumulated_frames_written_);
const uint32_t remainder_us =
audio_stream_info.frames_to_microseconds(this_speaker->accumulated_frames_written_);
uint32_t pending_frames =
audio_stream_info.bytes_to_frames(bytes_read + this_speaker->audio_ring_buffer_->available());
const uint32_t pending_ms = audio_stream_info.frames_to_milliseconds_with_remainder(&pending_frames);
this_speaker->audio_output_callback_(new_playback_ms, remainder_us, pending_ms, write_timestamp);
tx_dma_underflow = false; tx_dma_underflow = false;
last_data_received_time = millis(); last_data_received_time = millis();
}
} else { } else {
// No data received // No data received
if (stop_gracefully && tx_dma_underflow) { if (stop_gracefully && tx_dma_underflow) {
@ -328,7 +375,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
this_speaker->parent_->unlock(); this_speaker->parent_->unlock();
} }
this_speaker->delete_task_(dma_buffers_size); this_speaker->delete_task_(data_buffer_size);
} }
void I2SAudioSpeaker::start() { void I2SAudioSpeaker::start() {
@ -337,18 +384,17 @@ void I2SAudioSpeaker::start() {
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
return; return;
if (this->speaker_task_handle_ == nullptr) { if (!this->task_created_ && (this->speaker_task_handle_ == nullptr)) {
xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_); &this->speaker_task_handle_);
}
if (this->speaker_task_handle_ != nullptr) { if (this->speaker_task_handle_ != nullptr) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
this->task_created_ = true;
} else { } else {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START);
} }
} }
}
void I2SAudioSpeaker::stop() { this->stop_(false); } void I2SAudioSpeaker::stop() { this->stop_(false); }
@ -416,12 +462,12 @@ esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t rin
} }
esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.sample_rate)) { // NOLINT if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
// Can't reconfigure I2S bus, so the sample rate must match the configured value // Can't reconfigure I2S bus, so the sample rate must match the configured value
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
if ((i2s_bits_per_sample_t) audio_stream_info.bits_per_sample > this->bits_per_sample_) { if ((i2s_bits_per_sample_t) audio_stream_info.get_bits_per_sample() > this->bits_per_sample_) {
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value // Currently can't handle the case when the incoming audio has more bits per sample than the configured value
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
@ -432,21 +478,21 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
i2s_channel_fmt_t channel = this->channel_; i2s_channel_fmt_t channel = this->channel_;
if (audio_stream_info.channels == 1) { if (audio_stream_info.get_channels() == 1) {
if (this->channel_ == I2S_CHANNEL_FMT_ONLY_LEFT) { if (this->channel_ == I2S_CHANNEL_FMT_ONLY_LEFT) {
channel = I2S_CHANNEL_FMT_ONLY_LEFT; channel = I2S_CHANNEL_FMT_ONLY_LEFT;
} else { } else {
channel = I2S_CHANNEL_FMT_ONLY_RIGHT; channel = I2S_CHANNEL_FMT_ONLY_RIGHT;
} }
} else if (audio_stream_info.channels == 2) { } else if (audio_stream_info.get_channels() == 2) {
channel = I2S_CHANNEL_FMT_RIGHT_LEFT; channel = I2S_CHANNEL_FMT_RIGHT_LEFT;
} }
int dma_buffer_length = DMA_BUFFER_DURATION_MS * this->sample_rate_ / 1000; int dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
i2s_driver_config_t config = { i2s_driver_config_t config = {
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX), .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX),
.sample_rate = audio_stream_info.sample_rate, .sample_rate = audio_stream_info.get_sample_rate(),
.bits_per_sample = this->bits_per_sample_, .bits_per_sample = this->bits_per_sample_,
.channel_format = channel, .channel_format = channel,
.communication_format = this->i2s_comm_fmt_, .communication_format = this->i2s_comm_fmt_,
@ -504,7 +550,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
} }
void I2SAudioSpeaker::delete_task_(size_t buffer_size) { void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
this->audio_ring_buffer_.reset(); // Releases onwership of the shared_ptr this->audio_ring_buffer_.reset(); // Releases ownership of the shared_ptr
if (this->data_buffer_ != nullptr) { if (this->data_buffer_ != nullptr) {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);

View File

@ -40,6 +40,9 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
void stop() override; void stop() override;
void finish() override; void finish() override;
void set_pause_state(bool pause_state) override { this->pause_state_ = pause_state; }
bool get_pause_state() const override { return this->pause_state_; }
/// @brief Plays the provided audio data. /// @brief Plays the provided audio data.
/// Starts the speaker task, if necessary. Writes the audio data to the ring buffer. /// Starts the speaker task, if necessary. Writes the audio data to the ring buffer.
/// @param data Audio data in the format set by the parent speaker classes ``set_audio_stream_info`` method. /// @param data Audio data in the format set by the parent speaker classes ``set_audio_stream_info`` method.
@ -121,13 +124,18 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
uint8_t dout_pin_; uint8_t dout_pin_;
bool task_created_{false}; bool task_created_{false};
bool pause_state_{false};
int16_t q15_volume_factor_{INT16_MAX}; int16_t q15_volume_factor_{INT16_MAX};
size_t bytes_written_{0};
#if SOC_I2S_SUPPORTS_DAC #if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif #endif
i2s_comm_format_t i2s_comm_fmt_; i2s_comm_format_t i2s_comm_fmt_;
uint32_t accumulated_frames_written_{0};
}; };
} // namespace i2s_audio } // namespace i2s_audio

View File

@ -273,11 +273,9 @@ IMAGE_TYPE = {
"GRAYSCALE": ImageGrayscale, "GRAYSCALE": ImageGrayscale,
"RGB565": ImageRGB565, "RGB565": ImageRGB565,
"RGB": ImageRGB, "RGB": ImageRGB,
"TRANSPARENT_BINARY": ReplaceWith( "TRANSPARENT_BINARY": ReplaceWith("'type: BINARY' and 'transparency: chroma_key'"),
"'type: BINARY' and 'use_transparency: chroma_key'"
),
"RGB24": ReplaceWith("'type: RGB'"), "RGB24": ReplaceWith("'type: RGB'"),
"RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"), "RGBA": ReplaceWith("'type: RGB' and 'transparency: alpha_channel'"),
} }
TransparencyType = image_ns.enum("TransparencyType") TransparencyType = image_ns.enum("TransparencyType")

View File

@ -0,0 +1,51 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import (
CONF_ID,
CONF_THROTTLE,
)
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@hareeshmu"]
MULTI_CONF = True
ld2450_ns = cg.esphome_ns.namespace("ld2450")
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
CONF_LD2450_ID = "ld2450_id"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LD2450Component),
cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=cv.TimePeriod(milliseconds=1)),
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
LD2450BaseSchema = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
},
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"ld2450",
require_tx=True,
require_rx=True,
parity="NONE",
stop_bits=1,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(var.set_throttle(config[CONF_THROTTLE]))

View File

@ -0,0 +1,47 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_HAS_MOVING_TARGET,
CONF_HAS_STILL_TARGET,
CONF_HAS_TARGET,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
)
from . import CONF_LD2450_ID, LD2450Component
DEPENDENCIES = ["ld2450"]
ICON_MEDITATION = "mdi:meditation"
ICON_SHIELD_ACCOUNT = "mdi:shield-account"
ICON_TARGET_ACCOUNT = "mdi:target-account"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
icon=ICON_SHIELD_ACCOUNT,
),
cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_MOTION,
icon=ICON_TARGET_ACCOUNT,
),
cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
icon=ICON_MEDITATION,
),
}
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if has_target_config := config.get(CONF_HAS_TARGET):
sens = await binary_sensor.new_binary_sensor(has_target_config)
cg.add(ld2450_component.set_target_binary_sensor(sens))
if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET):
sens = await binary_sensor.new_binary_sensor(has_moving_target_config)
cg.add(ld2450_component.set_moving_target_binary_sensor(sens))
if has_still_target_config := config.get(CONF_HAS_STILL_TARGET):
sens = await binary_sensor.new_binary_sensor(has_still_target_config)
cg.add(ld2450_component.set_still_target_binary_sensor(sens))

View File

@ -0,0 +1,45 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_FACTORY_RESET,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_RESTART,
ICON_RESTART_ALERT,
)
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
ResetButton = ld2450_ns.class_("ResetButton", button.Button)
RestartButton = ld2450_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
ResetButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
),
}
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_reset_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_restart_button(b))

View File

@ -0,0 +1,9 @@
#include "reset_button.h"
namespace esphome {
namespace ld2450 {
void ResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class ResetButton : public button::Button, public Parented<LD2450Component> {
public:
ResetButton() = default;
protected:
void press_action() override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,9 @@
#include "restart_button.h"
namespace esphome {
namespace ld2450 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class RestartButton : public button::Button, public Parented<LD2450Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,867 @@
#include "ld2450.h"
#include <utility>
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#include "esphome/core/component.h"
#define highbyte(val) (uint8_t)((val) >> 8)
#define lowbyte(val) (uint8_t)((val) &0xff)
namespace esphome {
namespace ld2450 {
static const char *const TAG = "ld2450";
static const char *const UNKNOWN_MAC("unknown");
// LD2450 UART Serial Commands
static const uint8_t CMD_ENABLE_CONF = 0x00FF;
static const uint8_t CMD_DISABLE_CONF = 0x00FE;
static const uint8_t CMD_VERSION = 0x00A0;
static const uint8_t CMD_MAC = 0x00A5;
static const uint8_t CMD_RESET = 0x00A2;
static const uint8_t CMD_RESTART = 0x00A3;
static const uint8_t CMD_BLUETOOTH = 0x00A4;
static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080;
static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090;
static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091;
static const uint8_t CMD_SET_BAUD_RATE = 0x00A1;
static const uint8_t CMD_QUERY_ZONE = 0x00C1;
static const uint8_t CMD_SET_ZONE = 0x00C2;
static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
static inline std::string convert_signed_int_to_hex(int value) {
auto value_as_str = str_snprintf("%04x", 4, value & 0xFFFF);
return value_as_str;
}
static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
for (int i = 0; i < 4; i++) {
std::string temp_hex = convert_signed_int_to_hex(values[i]);
bytes[i * 2] = std::stoi(temp_hex.substr(2, 2), nullptr, 16); // Store high byte
bytes[i * 2 + 1] = std::stoi(temp_hex.substr(0, 2), nullptr, 16); // Store low byte
}
}
static inline int16_t decode_coordinate(uint8_t low_byte, uint8_t high_byte) {
int16_t coordinate = (high_byte & 0x7F) << 8 | low_byte;
if ((high_byte & 0x80) == 0) {
coordinate = -coordinate;
}
return coordinate; // mm
}
static inline int16_t decode_speed(uint8_t low_byte, uint8_t high_byte) {
int16_t speed = (high_byte & 0x7F) << 8 | low_byte;
if ((high_byte & 0x80) == 0) {
speed = -speed;
}
return speed * 10; // mm/s
}
static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) {
uint16_t hex_val = (buffer[offset + 1] << 8) | buffer[offset];
int16_t dec_val = static_cast<int16_t>(hex_val);
if (dec_val & 0x8000) {
dec_val -= 65536;
}
return dec_val;
}
static inline float calculate_angle(float base, float hypotenuse) {
if (base < 0.0 || hypotenuse <= 0.0) {
return 0.0;
}
float angle_radians = std::acos(base / hypotenuse);
float angle_degrees = angle_radians * (180.0 / M_PI);
return angle_degrees;
}
static inline std::string get_direction(int16_t speed) {
static const char *const APPROACHING = "Approaching";
static const char *const MOVING_AWAY = "Moving away";
static const char *const STATIONARY = "Stationary";
if (speed > 0) {
return MOVING_AWAY;
}
if (speed < 0) {
return APPROACHING;
}
return STATIONARY;
}
static inline std::string format_mac(uint8_t *buffer) {
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14],
buffer[15]);
}
static inline std::string format_version(uint8_t *buffer) {
return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15],
buffer[14]);
}
LD2450Component::LD2450Component() {}
void LD2450Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up HLK-LD2450...");
#ifdef USE_NUMBER
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash());
this->set_presence_timeout();
#endif
this->read_all_info();
}
void LD2450Component::dump_config() {
ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:");
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_);
#endif
#ifdef USE_SWITCH
LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_);
LOG_SWITCH(" ", "MultiTargetSwitch", this->multi_target_switch_);
#endif
#ifdef USE_BUTTON
LOG_BUTTON(" ", "ResetButton", this->reset_button_);
LOG_BUTTON(" ", "RestartButton", this->restart_button_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "TargetCountSensor", this->target_count_sensor_);
LOG_SENSOR(" ", "StillTargetCountSensor", this->still_target_count_sensor_);
LOG_SENSOR(" ", "MovingTargetCountSensor", this->moving_target_count_sensor_);
for (sensor::Sensor *s : this->move_x_sensors_) {
LOG_SENSOR(" ", "NthTargetXSensor", s);
}
for (sensor::Sensor *s : this->move_y_sensors_) {
LOG_SENSOR(" ", "NthTargetYSensor", s);
}
for (sensor::Sensor *s : this->move_speed_sensors_) {
LOG_SENSOR(" ", "NthTargetSpeedSensor", s);
}
for (sensor::Sensor *s : this->move_angle_sensors_) {
LOG_SENSOR(" ", "NthTargetAngleSensor", s);
}
for (sensor::Sensor *s : this->move_distance_sensors_) {
LOG_SENSOR(" ", "NthTargetDistanceSensor", s);
}
for (sensor::Sensor *s : this->move_resolution_sensors_) {
LOG_SENSOR(" ", "NthTargetResolutionSensor", s);
}
for (sensor::Sensor *s : this->zone_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneTargetCountSensor", s);
}
for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneStillTargetCountSensor", s);
}
for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneMovingTargetCountSensor", s);
}
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_);
LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_);
for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
LOG_TEXT_SENSOR(" ", "NthDirectionTextSensor", s);
}
#endif
#ifdef USE_NUMBER
for (number::Number *n : this->zone_x1_numbers_) {
LOG_NUMBER(" ", "ZoneX1Number", n);
}
for (number::Number *n : this->zone_y1_numbers_) {
LOG_NUMBER(" ", "ZoneY1Number", n);
}
for (number::Number *n : this->zone_x2_numbers_) {
LOG_NUMBER(" ", "ZoneX2Number", n);
}
for (number::Number *n : this->zone_y2_numbers_) {
LOG_NUMBER(" ", "ZoneY2Number", n);
}
#endif
#ifdef USE_SELECT
LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_);
LOG_SELECT(" ", "ZoneTypeSelect", this->zone_type_select_);
#endif
#ifdef USE_NUMBER
LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_);
#endif
ESP_LOGCONFIG(TAG, " Throttle : %ums", this->throttle_);
ESP_LOGCONFIG(TAG, " MAC Address : %s", const_cast<char *>(this->mac_.c_str()));
ESP_LOGCONFIG(TAG, " Firmware version : %s", const_cast<char *>(this->version_.c_str()));
}
void LD2450Component::loop() {
while (this->available()) {
this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH);
}
}
// Count targets in zone
uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) {
uint8_t count = 0;
for (auto &index : this->target_info_) {
if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 &&
index.is_moving == is_moving) {
count++;
}
}
return count;
}
// Service reset_radar_zone
void LD2450Component::reset_radar_zone() {
this->zone_type_ = 0;
for (auto &i : this->zone_config_) {
i.x1 = 0;
i.y1 = 0;
i.x2 = 0;
i.y2 = 0;
}
this->send_set_zone_command_();
}
void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2,
int32_t zone1_y2, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2,
int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2,
int32_t zone3_y2) {
this->zone_type_ = zone_type;
int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
for (int i = 0; i < MAX_ZONES; i++) {
this->zone_config_[i].x1 = zone_parameters[i * 4];
this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
this->zone_config_[i].y2 = zone_parameters[i * 4 + 3];
}
this->send_set_zone_command_();
}
// Set Zone on LD2450 Sensor
void LD2450Component::send_set_zone_command_() {
uint8_t cmd_value[26] = {};
uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
uint8_t area_config[24] = {};
for (int i = 0; i < MAX_ZONES; i++) {
int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
this->zone_config_[i].y2};
ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
}
std::memcpy(cmd_value, zone_type_bytes, 2);
std::memcpy(cmd_value + 2, area_config, 24);
this->set_config_mode_(true);
this->send_command_(CMD_SET_ZONE, cmd_value, 26);
this->set_config_mode_(false);
}
// Check presense timeout to reset presence status
bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
if (check_millis == 0) {
return true;
}
if (this->timeout_ == 0) {
this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
}
auto current_millis = millis();
return current_millis - check_millis >= this->timeout_;
}
// Extract, store and publish zone details LD2450 buffer
void LD2450Component::process_zone_(uint8_t *buffer) {
uint8_t index, start;
for (index = 0; index < MAX_ZONES; index++) {
start = 12 + index * 8;
this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start);
this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2);
this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
#ifdef USE_NUMBER
this->zone_x1_numbers_[index]->publish_state(this->zone_config_[index].x1);
this->zone_y1_numbers_[index]->publish_state(this->zone_config_[index].y1);
this->zone_x2_numbers_[index]->publish_state(this->zone_config_[index].x2);
this->zone_y2_numbers_[index]->publish_state(this->zone_config_[index].y2);
#endif
}
}
// Read all info from LD2450 buffer
void LD2450Component::read_all_info() {
this->set_config_mode_(true);
this->get_version_();
this->get_mac_();
this->query_target_tracking_mode_();
this->query_zone_();
this->set_config_mode_(false);
#ifdef USE_SELECT
const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) {
this->baud_rate_select_->publish_state(baud_rate);
}
this->publish_zone_type();
#endif
}
// Read zone info from LD2450 buffer
void LD2450Component::query_zone_info() {
this->set_config_mode_(true);
this->query_zone_();
this->set_config_mode_(false);
}
// Restart LD2450 and read all info from buffer
void LD2450Component::restart_and_read_all_info() {
this->set_config_mode_(true);
this->restart_();
this->set_timeout(1000, [this]() { this->read_all_info(); });
}
// Send command with values to LD2450
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
ESP_LOGV(TAG, "Sending command %02X", command);
// frame header
this->write_array(CMD_FRAME_HEADER, 4);
// length bytes
int len = 2;
if (command_value != nullptr) {
len += command_value_len;
}
this->write_byte(lowbyte(len));
this->write_byte(highbyte(len));
// command
this->write_byte(lowbyte(command));
this->write_byte(highbyte(command));
// command value bytes
if (command_value != nullptr) {
for (int i = 0; i < command_value_len; i++) {
this->write_byte(command_value[i]);
}
}
// footer
this->write_array(CMD_FRAME_END, 4);
// FIXME to remove
delay(50); // NOLINT
}
// LD2450 Radar data message:
// [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
// Header Target 1 Target 2 Target 3 End
void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
ESP_LOGE(TAG, "Periodic data: invalid message length");
return;
}
if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header
ESP_LOGE(TAG, "Periodic data: invalid message header");
return;
}
if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer
ESP_LOGE(TAG, "Periodic data: invalid message footer");
return;
}
auto current_millis = millis();
if (current_millis - this->last_periodic_millis_ < this->throttle_) {
ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
return;
}
this->last_periodic_millis_ = current_millis;
int16_t target_count = 0;
int16_t still_target_count = 0;
int16_t moving_target_count = 0;
int16_t start = 0;
int16_t val = 0;
uint8_t index = 0;
int16_t tx = 0;
int16_t ty = 0;
int16_t td = 0;
int16_t ts = 0;
int16_t angle = 0;
std::string direction{};
bool is_moving = false;
#ifdef USE_SENSOR
// Loop thru targets
// X
for (index = 0; index < MAX_TARGETS; index++) {
start = TARGET_X + index * 8;
is_moving = false;
sensor::Sensor *sx = this->move_x_sensors_[index];
if (sx != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
tx = val;
sx->publish_state(val);
}
// Y
start = TARGET_Y + index * 8;
sensor::Sensor *sy = this->move_y_sensors_[index];
if (sy != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
ty = val;
sy->publish_state(val);
}
// SPEED
start = TARGET_SPEED + index * 8;
sensor::Sensor *ss = this->move_speed_sensors_[index];
if (ss != nullptr) {
val = ld2450::decode_speed(buffer[start], buffer[start + 1]);
ts = val;
if (val) {
is_moving = true;
moving_target_count++;
}
ss->publish_state(val);
}
// RESOLUTION
start = TARGET_RESOLUTION + index * 8;
sensor::Sensor *sr = this->move_resolution_sensors_[index];
if (sr != nullptr) {
val = (buffer[start + 1] << 8) | buffer[start];
sr->publish_state(val);
}
// DISTANCE
sensor::Sensor *sd = this->move_distance_sensors_[index];
if (sd != nullptr) {
val = (uint16_t) sqrt(
pow(ld2450::decode_coordinate(buffer[TARGET_X + index * 8], buffer[(TARGET_X + index * 8) + 1]), 2) +
pow(ld2450::decode_coordinate(buffer[TARGET_Y + index * 8], buffer[(TARGET_Y + index * 8) + 1]), 2));
td = val;
if (val > 0) {
target_count++;
}
sd->publish_state(val);
}
// ANGLE
angle = calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) {
angle = angle * -1;
}
sensor::Sensor *sa = this->move_angle_sensors_[index];
if (sa != nullptr) {
sa->publish_state(angle);
}
#endif
// DIRECTION
#ifdef USE_TEXT_SENSOR
direction = get_direction(ts);
if (td == 0) {
direction = "NA";
}
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(direction);
}
#endif
// Store target info for zone target count
this->target_info_[index].x = tx;
this->target_info_[index].y = ty;
this->target_info_[index].is_moving = is_moving;
} // End loop thru targets
#ifdef USE_SENSOR
// Loop thru zones
uint8_t zone_still_targets = 0;
uint8_t zone_moving_targets = 0;
uint8_t zone_all_targets = 0;
for (index = 0; index < MAX_ZONES; index++) {
// Publish Still Target Count in Zones
sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index];
if (szstc != nullptr) {
zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
szstc->publish_state(zone_still_targets);
}
// Publish Moving Target Count in Zones
sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index];
if (szmtc != nullptr) {
zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
szmtc->publish_state(zone_moving_targets);
}
zone_all_targets = zone_still_targets + zone_moving_targets;
// Publish All Target Count in Zones
sensor::Sensor *sztc = this->zone_target_count_sensors_[index];
if (sztc != nullptr) {
sztc->publish_state(zone_all_targets);
}
} // End loop thru zones
still_target_count = target_count - moving_target_count;
// Target Count
if (this->target_count_sensor_ != nullptr) {
this->target_count_sensor_->publish_state(target_count);
}
// Still Target Count
if (this->still_target_count_sensor_ != nullptr) {
this->still_target_count_sensor_->publish_state(still_target_count);
}
// Moving Target Count
if (this->moving_target_count_sensor_ != nullptr) {
this->moving_target_count_sensor_->publish_state(moving_target_count);
}
#endif
#ifdef USE_BINARY_SENSOR
// Target Presence
if (this->target_binary_sensor_ != nullptr) {
if (target_count > 0) {
this->target_binary_sensor_->publish_state(true);
} else {
if (this->get_timeout_status_(this->presence_millis_)) {
this->target_binary_sensor_->publish_state(false);
} else {
ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_);
}
}
}
// Moving Target Presence
if (this->moving_target_binary_sensor_ != nullptr) {
if (moving_target_count > 0) {
this->moving_target_binary_sensor_->publish_state(true);
} else {
if (this->get_timeout_status_(this->moving_presence_millis_)) {
this->moving_target_binary_sensor_->publish_state(false);
}
}
}
// Still Target Presence
if (this->still_target_binary_sensor_ != nullptr) {
if (still_target_count > 0) {
this->still_target_binary_sensor_->publish_state(true);
} else {
if (this->get_timeout_status_(this->still_presence_millis_)) {
this->still_target_binary_sensor_->publish_state(false);
}
}
}
#endif
#ifdef USE_SENSOR
// For presence timeout check
if (target_count > 0) {
this->presence_millis_ = millis();
}
if (moving_target_count > 0) {
this->moving_presence_millis_ = millis();
}
if (still_target_count > 0) {
this->still_presence_millis_ = millis();
}
#endif
}
bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]);
if (len < 10) {
ESP_LOGE(TAG, "Ack data: invalid length");
return true;
}
if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header
ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]);
return true;
}
if (buffer[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Ack data: invalid status");
return true;
}
if (buffer[8] || buffer[9]) {
ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]);
return true;
}
switch (buffer[COMMAND]) {
case lowbyte(CMD_ENABLE_CONF):
ESP_LOGV(TAG, "Got enable conf command");
break;
case lowbyte(CMD_DISABLE_CONF):
ESP_LOGV(TAG, "Got disable conf command");
break;
case lowbyte(CMD_SET_BAUD_RATE):
ESP_LOGV(TAG, "Got baud rate change command");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str());
}
#endif
break;
case lowbyte(CMD_VERSION):
this->version_ = ld2450::format_version(buffer);
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(this->version_);
}
#endif
break;
case lowbyte(CMD_MAC):
if (len < 20) {
return false;
}
this->mac_ = ld2450::format_mac(buffer);
ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(this->mac_);
}
#endif
#ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC);
}
#endif
break;
case lowbyte(CMD_BLUETOOTH):
ESP_LOGV(TAG, "Got Bluetooth command");
break;
case lowbyte(CMD_SINGLE_TARGET_MODE):
ESP_LOGV(TAG, "Got single target conf command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(false);
}
#endif
break;
case lowbyte(CMD_MULTI_TARGET_MODE):
ESP_LOGV(TAG, "Got multi target conf command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(true);
}
#endif
break;
case lowbyte(CMD_QUERY_TARGET_MODE):
ESP_LOGV(TAG, "Got query target tracking mode command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(buffer[10] == 0x02);
}
#endif
break;
case lowbyte(CMD_QUERY_ZONE):
ESP_LOGV(TAG, "Got query zone conf command");
this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16);
this->publish_zone_type();
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
}
#endif
if (buffer[10] == 0x00) {
ESP_LOGV(TAG, "Zone: Disabled");
}
if (buffer[10] == 0x01) {
ESP_LOGV(TAG, "Zone: Area detection");
}
if (buffer[10] == 0x02) {
ESP_LOGV(TAG, "Zone: Area filter");
}
this->process_zone_(buffer);
break;
case lowbyte(CMD_SET_ZONE):
ESP_LOGV(TAG, "Got set zone conf command");
this->query_zone_info();
break;
default:
break;
}
return true;
}
// Read LD2450 buffer data
void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) {
if (readch < 0) {
return;
}
if (this->buffer_pos_ < len - 1) {
buffer[this->buffer_pos_++] = readch;
buffer[this->buffer_pos_] = 0;
} else {
this->buffer_pos_ = 0;
}
if (this->buffer_pos_ < 4) {
return;
}
if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) {
ESP_LOGV(TAG, "Handle periodic radar data");
this->handle_periodic_data_(buffer, this->buffer_pos_);
this->buffer_pos_ = 0; // Reset position index for next frame
} else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 &&
buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) {
ESP_LOGV(TAG, "Handle command ack data");
if (this->handle_ack_data_(buffer, this->buffer_pos_)) {
this->buffer_pos_ = 0; // Reset position index for next frame
} else {
ESP_LOGV(TAG, "Command ack data invalid");
}
}
}
// Set Config Mode - Pre-requisite sending commands
void LD2450Component::set_config_mode_(bool enable) {
uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(cmd, enable ? cmd_value : nullptr, 2);
}
// Set Bluetooth Enable/Disable
void LD2450Component::set_bluetooth(bool enable) {
this->set_config_mode_(true);
uint8_t enable_cmd_value[2] = {0x01, 0x00};
uint8_t disable_cmd_value[2] = {0x00, 0x00};
this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2);
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
// Set Baud rate
void LD2450Component::set_baud_rate(const std::string &state) {
this->set_config_mode_(true);
uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2);
this->set_timeout(200, [this]() { this->restart_(); });
}
// Set Zone Type - one of: Disabled, Detection, Filter
void LD2450Component::set_zone_type(const std::string &state) {
ESP_LOGV(TAG, "Set zone type: %s", state.c_str());
uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state);
this->zone_type_ = zone_type;
this->send_set_zone_command_();
}
// Publish Zone Type to Select component
void LD2450Component::publish_zone_type() {
#ifdef USE_SELECT
std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast<ZoneTypeStructure>(this->zone_type_));
if (this->zone_type_select_ != nullptr) {
this->zone_type_select_->publish_state(zone_type);
}
#endif
}
// Set Single/Multiplayer target detection
void LD2450Component::set_multi_target(bool enable) {
this->set_config_mode_(true);
uint8_t cmd = enable ? CMD_MULTI_TARGET_MODE : CMD_SINGLE_TARGET_MODE;
this->send_command_(cmd, nullptr, 0);
this->set_config_mode_(false);
}
// LD2450 factory reset
void LD2450Component::factory_reset() {
this->set_config_mode_(true);
this->send_command_(CMD_RESET, nullptr, 0);
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
// Restart LD2450 module
void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
// Get LD2450 firmware version
void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); }
// Get LD2450 mac address
void LD2450Component::get_mac_() {
uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(CMD_MAC, cmd_value, 2);
}
// Query for target tracking mode
void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QUERY_TARGET_MODE, nullptr, 0); }
// Query for zone info
void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
#ifdef USE_SENSOR
void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { this->move_x_sensors_[target] = s; }
void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { this->move_y_sensors_[target] = s; }
void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
this->move_speed_sensors_[target] = s;
}
void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
this->move_angle_sensors_[target] = s;
}
void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
this->move_distance_sensors_[target] = s;
}
void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
this->move_resolution_sensors_[target] = s;
}
void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_target_count_sensors_[zone] = s;
}
void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_still_target_count_sensors_[zone] = s;
}
void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_moving_target_count_sensors_[zone] = s;
}
#endif
#ifdef USE_TEXT_SENSOR
void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s) {
this->direction_text_sensors_[target] = s;
}
#endif
// Send Zone coordinates data to LD2450
#ifdef USE_NUMBER
void LD2450Component::set_zone_coordinate(uint8_t zone) {
number::Number *x1sens = this->zone_x1_numbers_[zone];
number::Number *y1sens = this->zone_y1_numbers_[zone];
number::Number *x2sens = this->zone_x2_numbers_[zone];
number::Number *y2sens = this->zone_y2_numbers_[zone];
if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) {
return;
}
this->zone_config_[zone].x1 = static_cast<int>(x1sens->state);
this->zone_config_[zone].y1 = static_cast<int>(y1sens->state);
this->zone_config_[zone].x2 = static_cast<int>(x2sens->state);
this->zone_config_[zone].y2 = static_cast<int>(y2sens->state);
this->send_set_zone_command_();
}
void LD2450Component::set_zone_x1_number(uint8_t zone, number::Number *n) { this->zone_x1_numbers_[zone] = n; }
void LD2450Component::set_zone_y1_number(uint8_t zone, number::Number *n) { this->zone_y1_numbers_[zone] = n; }
void LD2450Component::set_zone_x2_number(uint8_t zone, number::Number *n) { this->zone_x2_numbers_[zone] = n; }
void LD2450Component::set_zone_y2_number(uint8_t zone, number::Number *n) { this->zone_y2_numbers_[zone] = n; }
#endif
// Set Presence Timeout load and save from flash
#ifdef USE_NUMBER
void LD2450Component::set_presence_timeout() {
if (this->presence_timeout_number_ != nullptr) {
if (this->presence_timeout_number_->state == 0) {
float timeout = this->restore_from_flash_();
this->presence_timeout_number_->publish_state(timeout);
this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
}
if (this->presence_timeout_number_->has_state()) {
this->save_to_flash_(this->presence_timeout_number_->state);
this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
}
}
}
// Save Presence Timeout to flash
void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); }
// Load Presence Timeout from flash
float LD2450Component::restore_from_flash_() {
float value;
if (!this->pref_.load(&value)) {
value = DEFAULT_PRESENCE_TIMEOUT;
}
return value;
}
#endif
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,231 @@
#pragma once
#include <iomanip>
#include <map>
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifndef M_PI
#define M_PI 3.14
#endif
namespace esphome {
namespace ld2450 {
// Constants
static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec.
static const uint8_t MAX_LINE_LENGTH = 60; // Max characters for serial buffer
static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
// Target coordinate struct
struct Target {
int16_t x;
int16_t y;
bool is_moving;
};
// Zone coordinate struct
struct Zone {
int16_t x1 = 0;
int16_t y1 = 0;
int16_t x2 = 0;
int16_t y2 = 0;
};
enum BaudRateStructure : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
// Convert baud rate enum to int
static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}};
// Zone type struct
enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 };
// Convert zone type int to enum
static const std::map<ZoneTypeStructure, std::string> ZONE_TYPE_INT_TO_ENUM{
{ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}};
// Convert zone type enum to int
static const std::map<std::string, uint8_t> ZONE_TYPE_ENUM_TO_INT{
{"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}};
// LD2450 serial command header & footer
static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
enum PeriodicDataStructure : uint8_t {
TARGET_X = 4,
TARGET_Y = 6,
TARGET_SPEED = 8,
TARGET_RESOLUTION = 10,
};
enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 };
enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 };
class LD2450Component : public Component, public uart::UARTDevice {
#ifdef USE_SENSOR
SUB_SENSOR(target_count)
SUB_SENSOR(still_target_count)
SUB_SENSOR(moving_target_count)
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(target)
SUB_BINARY_SENSOR(moving_target)
SUB_BINARY_SENSOR(still_target)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
SUB_TEXT_SENSOR(mac)
#endif
#ifdef USE_SELECT
SUB_SELECT(baud_rate)
SUB_SELECT(zone_type)
#endif
#ifdef USE_SWITCH
SUB_SWITCH(bluetooth)
SUB_SWITCH(multi_target)
#endif
#ifdef USE_BUTTON
SUB_BUTTON(reset)
SUB_BUTTON(restart)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(presence_timeout)
#endif
public:
LD2450Component();
void setup() override;
void dump_config() override;
void loop() override;
void set_presence_timeout();
void set_throttle(uint16_t value) { this->throttle_ = value; };
void read_all_info();
void query_zone_info();
void restart_and_read_all_info();
void set_bluetooth(bool enable);
void set_multi_target(bool enable);
void set_baud_rate(const std::string &state);
void set_zone_type(const std::string &state);
void publish_zone_type();
void factory_reset();
#ifdef USE_TEXT_SENSOR
void set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s);
#endif
#ifdef USE_NUMBER
void set_zone_coordinate(uint8_t zone);
void set_zone_x1_number(uint8_t zone, number::Number *n);
void set_zone_y1_number(uint8_t zone, number::Number *n);
void set_zone_x2_number(uint8_t zone, number::Number *n);
void set_zone_y2_number(uint8_t zone, number::Number *n);
#endif
#ifdef USE_SENSOR
void set_move_x_sensor(uint8_t target, sensor::Sensor *s);
void set_move_y_sensor(uint8_t target, sensor::Sensor *s);
void set_move_speed_sensor(uint8_t target, sensor::Sensor *s);
void set_move_angle_sensor(uint8_t target, sensor::Sensor *s);
void set_move_distance_sensor(uint8_t target, sensor::Sensor *s);
void set_move_resolution_sensor(uint8_t target, sensor::Sensor *s);
void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s);
#endif
void reset_radar_zone();
void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2,
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
protected:
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable);
void handle_periodic_data_(uint8_t *buffer, uint8_t len);
bool handle_ack_data_(uint8_t *buffer, uint8_t len);
void process_zone_(uint8_t *buffer);
void readline_(int readch, uint8_t *buffer, uint8_t len);
void get_version_();
void get_mac_();
void query_target_tracking_mode_();
void query_zone_();
void restart_();
void send_set_zone_command_();
void save_to_flash_(float value);
float restore_from_flash_();
bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint32_t last_periodic_millis_ = 0;
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0;
uint16_t timeout_ = 5;
uint8_t zone_type_ = 0;
std::string version_{};
std::string mac_{};
#ifdef USE_NUMBER
ESPPreferenceObject pref_; // only used when numbers are in use
std::vector<number::Number *> zone_x1_numbers_ = std::vector<number::Number *>(MAX_ZONES);
std::vector<number::Number *> zone_y1_numbers_ = std::vector<number::Number *>(MAX_ZONES);
std::vector<number::Number *> zone_x2_numbers_ = std::vector<number::Number *>(MAX_ZONES);
std::vector<number::Number *> zone_y2_numbers_ = std::vector<number::Number *>(MAX_ZONES);
#endif
#ifdef USE_SENSOR
std::vector<sensor::Sensor *> move_x_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_y_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_speed_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_angle_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_distance_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_resolution_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> zone_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
std::vector<sensor::Sensor *> zone_still_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
std::vector<sensor::Sensor *> zone_moving_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
#endif
#ifdef USE_TEXT_SENSOR
std::vector<text_sensor::TextSensor *> direction_text_sensors_ = std::vector<text_sensor::TextSensor *>(3);
#endif
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,120 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,
ENTITY_CATEGORY_CONFIG,
ICON_TIMELAPSE,
UNIT_MILLIMETER,
UNIT_SECOND,
)
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
CONF_PRESENCE_TIMEOUT = "presence_timeout"
CONF_X1 = "x1"
CONF_X2 = "x2"
CONF_Y1 = "y1"
CONF_Y2 = "y2"
ICON_ARROW_BOTTOM_RIGHT = "mdi:arrow-bottom-right"
ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE = "mdi:arrow-bottom-right-bold-box-outline"
ICON_ARROW_TOP_LEFT = "mdi:arrow-top-left"
ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE = "mdi:arrow-top-left-bold-box-outline"
MAX_ZONES = 3
PresenceTimeoutNumber = ld2450_ns.class_("PresenceTimeoutNumber", number.Number)
ZoneCoordinateNumber = ld2450_ns.class_("ZoneCoordinateNumber", number.Number)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Required(CONF_PRESENCE_TIMEOUT): number.number_schema(
PresenceTimeoutNumber,
unit_of_measurement=UNIT_SECOND,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
),
}
)
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(f"zone_{n + 1}"): cv.Schema(
{
cv.Required(CONF_X1): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE,
),
cv.Required(CONF_Y1): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_TOP_LEFT,
),
cv.Required(CONF_X2): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE,
),
cv.Required(CONF_Y2): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_BOTTOM_RIGHT,
),
}
)
for n in range(MAX_ZONES)
}
)
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if presence_timeout_config := config.get(CONF_PRESENCE_TIMEOUT):
n = await number.new_number(
presence_timeout_config,
min_value=0,
max_value=3600,
step=1,
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_presence_timeout_number(n))
for x in range(MAX_ZONES):
if zone_conf := config.get(f"zone_{x + 1}"):
if zone_x1_config := zone_conf.get(CONF_X1):
n = cg.new_Pvariable(zone_x1_config[CONF_ID], x)
await number.register_number(
n, zone_x1_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_x1_number(x, n))
if zone_y1_config := zone_conf.get(CONF_Y1):
n = cg.new_Pvariable(zone_y1_config[CONF_ID], x)
await number.register_number(
n, zone_y1_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_y1_number(x, n))
if zone_x2_config := zone_conf.get(CONF_X2):
n = cg.new_Pvariable(zone_x2_config[CONF_ID], x)
await number.register_number(
n, zone_x2_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_x2_number(x, n))
if zone_y2_config := zone_conf.get(CONF_Y2):
n = cg.new_Pvariable(zone_y2_config[CONF_ID], x)
await number.register_number(
n, zone_y2_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_y2_number(x, n))

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