mirror of
https://github.com/esphome/esphome.git
synced 2026-09-02 02:56:01 +00:00
Merge branch 'dev' into sendspin-artwork
This commit is contained in:
@@ -116,7 +116,6 @@ Checks: >-
|
||||
-portability-template-virtual-member-function,
|
||||
-readability-ambiguous-smartptr-reset-call,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
-readability-container-contains,
|
||||
-readability-container-data-pointer,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-else-after-return,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
593fd53fa09944a59af3f38521e31d87fe10b60326b8d82bb76413c5149b312c
|
||||
27aaab4e0ebfc10491720345aa746fc2dffa6a3985f73ec111b12dd99078d46f
|
||||
|
||||
@@ -27,6 +27,14 @@ runs:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv. ``uv pip install``
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
@@ -34,8 +42,8 @@ runs:
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
uv pip install -e .
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
|
||||
shell: bash
|
||||
@@ -43,5 +51,5 @@ runs:
|
||||
python -m venv venv
|
||||
source ./venv/Scripts/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
uv pip install -e .
|
||||
|
||||
@@ -26,6 +26,12 @@ jobs:
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install apt dependencies
|
||||
run: |
|
||||
@@ -34,7 +40,7 @@ jobs:
|
||||
sudo apt install -y protobuf-compiler
|
||||
protoc --version
|
||||
- name: Install python dependencies
|
||||
run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt
|
||||
run: uv pip install --system aioesphomeapi -c requirements.txt -r requirements_dev.txt
|
||||
- name: Generate files
|
||||
run: script/api_protobuf/api_protobuf.py
|
||||
- name: Check for changes
|
||||
|
||||
+55
-16
@@ -52,14 +52,22 @@ jobs:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv. ``uv pip install``
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
|
||||
pip install -e .
|
||||
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
|
||||
uv pip install -e .
|
||||
|
||||
pylint:
|
||||
name: Check pylint
|
||||
@@ -181,7 +189,7 @@ jobs:
|
||||
# own CI). No ``--cov`` here -- this is purely a downstream
|
||||
# smoke check against this PR's esphome code.
|
||||
working-directory: device-builder
|
||||
run: pytest -q -n auto --maxfail=5 --durations=10 --no-cov --ignore=tests/benchmarks
|
||||
run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
@@ -222,14 +230,14 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
. ./venv/Scripts/activate.ps1
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
|
||||
- name: Run pytest
|
||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
@@ -249,6 +257,7 @@ jobs:
|
||||
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
clang-tidy-full-scan: ${{ steps.determine.outputs.clang-tidy-full-scan }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
import-time: ${{ steps.determine.outputs.import-time }}
|
||||
device-builder: ${{ steps.determine.outputs.device-builder }}
|
||||
@@ -287,7 +296,12 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
output=$(python script/determine-jobs.py)
|
||||
EXTRA_ARGS=""
|
||||
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-run-all') }}" == "true" ]]; then
|
||||
EXTRA_ARGS="--force-all"
|
||||
echo "::notice::ci-run-all label detected -- forcing every CI job to run"
|
||||
fi
|
||||
output=$(python script/determine-jobs.py $EXTRA_ARGS)
|
||||
echo "Test determination output:"
|
||||
echo "$output" | jq
|
||||
|
||||
@@ -296,6 +310,7 @@ jobs:
|
||||
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-full-scan=$(echo "$output" | jq -r '.clang_tidy_full_scan')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
|
||||
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
|
||||
@@ -344,14 +359,20 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
uv pip install -e .
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
@@ -363,7 +384,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
|
||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}"
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
@@ -500,7 +521,13 @@ jobs:
|
||||
id: check_full_scan
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if python script/clang_tidy_hash.py --check; then
|
||||
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
|
||||
# OR the ci-run-all label forced --force-all. Independent of the
|
||||
# hash check, both must produce a full scan in the job itself.
|
||||
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
|
||||
elif python script/clang_tidy_hash.py --check; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -512,7 +539,7 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (hash changed)"
|
||||
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
@@ -572,7 +599,13 @@ jobs:
|
||||
id: check_full_scan
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if python script/clang_tidy_hash.py --check; then
|
||||
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
|
||||
# OR the ci-run-all label forced --force-all. Independent of the
|
||||
# hash check, both must produce a full scan in the job itself.
|
||||
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
|
||||
elif python script/clang_tidy_hash.py --check; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -584,7 +617,7 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (hash changed)"
|
||||
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
@@ -661,7 +694,13 @@ jobs:
|
||||
id: check_full_scan
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if python script/clang_tidy_hash.py --check; then
|
||||
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
|
||||
# OR the ci-run-all label forced --force-all. Independent of the
|
||||
# hash check, both must produce a full scan in the job itself.
|
||||
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
|
||||
elif python script/clang_tidy_hash.py --check; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -673,7 +712,7 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (hash changed)"
|
||||
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }}
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -12,6 +12,12 @@ jobs:
|
||||
dashboard-deprecation-comment:
|
||||
name: Dashboard deprecation comment
|
||||
runs-on: ubuntu-latest
|
||||
# Release-bump PRs (bump-X.Y.Z -> beta, beta -> release) inevitably
|
||||
# roll up everything merged into dev since the last cut, which can
|
||||
# include dashboard changes that have already been reviewed once.
|
||||
# The bot's purpose is to warn new contributors before they invest
|
||||
# time -- that only applies to PRs entering dev.
|
||||
if: github.event.pull_request.base.ref == 'dev'
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
|
||||
@@ -29,10 +29,11 @@ jobs:
|
||||
} = require('./.github/scripts/detect-tags.js');
|
||||
|
||||
const title = context.payload.pull_request.title;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
const user = context.payload.pull_request.user;
|
||||
|
||||
// Skip bot PRs (e.g. dependabot) - they have their own title format
|
||||
if (author === 'dependabot[bot]') {
|
||||
// Skip bot PRs (e.g. dependabot, esphome[bot] device-class sync) -
|
||||
// they have their own title formats.
|
||||
if (user.type === 'Bot') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -212,74 +212,6 @@ jobs:
|
||||
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
|
||||
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
|
||||
|
||||
deploy-ha-addon-repo:
|
||||
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- init
|
||||
- deploy-manifest
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: home-assistant-addon
|
||||
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
let description = "ESPHome";
|
||||
if (context.eventName == "release") {
|
||||
description = ${{ toJSON(github.event.release.body) }};
|
||||
}
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: "esphome",
|
||||
repo: "home-assistant-addon",
|
||||
workflow_id: "bump-version.yml",
|
||||
ref: "main",
|
||||
inputs: {
|
||||
version: "${{ needs.init.outputs.tag }}",
|
||||
content: description
|
||||
}
|
||||
})
|
||||
|
||||
deploy-esphome-schema:
|
||||
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [init]
|
||||
environment: ${{ needs.init.outputs.deploy_env }}
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: esphome-schema
|
||||
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: "esphome",
|
||||
repo: "esphome-schema",
|
||||
workflow_id: "generate-schemas.yml",
|
||||
ref: "main",
|
||||
inputs: {
|
||||
version: "${{ needs.init.outputs.tag }}",
|
||||
}
|
||||
})
|
||||
|
||||
version-notifier:
|
||||
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -302,7 +234,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: "esphome",
|
||||
repo: "version-notifier",
|
||||
workflow_id: "notify.yml",
|
||||
|
||||
@@ -41,19 +41,52 @@ jobs:
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Set up uv
|
||||
# An order of magnitude faster than pip on cold boots, with its
|
||||
# own wheel cache. ``--system`` (below) installs into the
|
||||
# setup-python interpreter so subsequent ``pre-commit`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install Home Assistant
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e lib/home-assistant
|
||||
pip install -r requirements_test.txt pre-commit
|
||||
uv pip install --system -e lib/home-assistant
|
||||
uv pip install --system -r requirements.txt -r requirements_test.txt pre-commit
|
||||
|
||||
- name: Sync
|
||||
run: |
|
||||
python ./script/sync-device_class.py
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
run: |
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
- name: Apply pre-commit auto-fixes
|
||||
# First pass: let formatters (ruff, end-of-file-fixer, etc.) modify
|
||||
# files. pre-commit exits non-zero whenever a hook touches anything,
|
||||
# which would otherwise abort the workflow before the auto-fixes
|
||||
# can flow into the sync PR.
|
||||
#
|
||||
# SKIP:
|
||||
# - no-commit-to-branch is a local guard against committing on
|
||||
# dev/release/beta; CI runs on dev by definition, and
|
||||
# peter-evans/create-pull-request creates the branch itself.
|
||||
# - pylint surfaces import-error / relative-beyond-top-level
|
||||
# noise here because this workflow installs only a subset of
|
||||
# the runtime deps (HA + requirements*.txt); main CI already
|
||||
# gates pylint on real PRs.
|
||||
env:
|
||||
SKIP: pylint,no-commit-to-branch
|
||||
run: python script/run-in-env.py pre-commit run --all-files || true
|
||||
|
||||
- name: Verify pre-commit clean
|
||||
# Second pass: re-run all hooks against the now-fixed tree.
|
||||
# Auto-fixers exit 0 (nothing to change); any remaining failure
|
||||
# from a check-only hook (flake8 / yamllint / ci-custom) is a
|
||||
# real issue and fails the workflow loudly. Same SKIP list as
|
||||
# above for the same reasons.
|
||||
env:
|
||||
SKIP: pylint,no-commit-to-branch
|
||||
run: python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
|
||||
+7
-3
@@ -13,12 +13,16 @@ RUN git config --system --add safe.directory "*" \
|
||||
&& git config --system advice.detachedHead false
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
# (e.g., ruamel.yaml.clib used by ESP-IDF's idf-component-manager).
|
||||
# Also install libusb-1.0 at runtime so the ESP-IDF tools installer can
|
||||
# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without
|
||||
# it idf_tools.py rejects the openocd install with exit 127 and aborts
|
||||
# the whole framework setup.
|
||||
RUN if command -v apk > /dev/null; then \
|
||||
apk add --no-cache build-base; \
|
||||
apk add --no-cache build-base libusb; \
|
||||
else \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential \
|
||||
&& apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ from esphome.const import (
|
||||
CONF_TOPIC,
|
||||
CONF_USERNAME,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WIFI,
|
||||
ENV_NOGITIGNORE,
|
||||
KEY_CORE,
|
||||
KEY_TARGET_PLATFORM,
|
||||
@@ -733,6 +734,13 @@ def write_cpp_file() -> int:
|
||||
|
||||
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
# Keep this gate here, NOT in config validation: device-builder needs
|
||||
# `esphome config` to keep succeeding with placeholders so onboarding can run.
|
||||
if CONF_WIFI in config:
|
||||
from esphome.components.wifi import check_placeholder_credentials
|
||||
|
||||
check_placeholder_credentials(config)
|
||||
|
||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
||||
# If you change this format, update the regex in that script as well
|
||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32 import get_esp32_variant, idf_version
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
from esphome.writer import update_storage_json
|
||||
@@ -61,6 +62,11 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
variant = get_esp32_variant()
|
||||
idf_target = variant.lower().replace("-", "")
|
||||
|
||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
||||
# --format=raw because the legacy mode doesn't support it.
|
||||
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
||||
|
||||
# Project-wide compile options: -D defines and -W warning flags (skip
|
||||
# -Wl, linker flags — those go on the src component via
|
||||
# target_link_options below). Emitted via idf_build_set_property so the
|
||||
@@ -146,7 +152,7 @@ project({CORE.name})
|
||||
# Emit raw JSON size data for ESPHome to read post-build.
|
||||
add_custom_command(
|
||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size --ng --format=raw
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
|
||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||
${{CMAKE_PROJECT_NAME}}.map
|
||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||
|
||||
+12
-85
@@ -260,42 +260,20 @@ class ConfigBundleCreator:
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
Deliberately uses a fresh re-parse and force-loads every deferred
|
||||
``IncludeFile`` to include *all* potentially-reachable includes,
|
||||
even branches not selected by the local substitutions. Bundles are
|
||||
meant to be compiled on another system where command-line
|
||||
substitution overrides may choose a different branch — e.g.
|
||||
``!include network/${eth_model}/config.yaml`` must ship every
|
||||
candidate so the remote build can pick any one.
|
||||
|
||||
Entries with unresolved substitution variables in the filename
|
||||
path are skipped with a warning (they cannot be resolved without
|
||||
the substitution pass).
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
Delegates to :func:`yaml_util.discover_user_yaml_files`, which does a
|
||||
fresh re-parse and force-loads every deferred ``IncludeFile`` so that
|
||||
*all* potentially-reachable includes are captured (even branches not
|
||||
selected by local substitutions). Bundles are meant to be compiled on
|
||||
another system where command-line substitution overrides may choose a
|
||||
different branch — e.g. ``!include network/${eth_model}/config.yaml``
|
||||
must ship every candidate so the remote build can pick any one.
|
||||
"""
|
||||
# Must be a fresh parse: IncludeFile.load() caches its result in
|
||||
# _content, and we discover files by listening for loader calls. On
|
||||
# an already-parsed tree the cache is populated, .load() returns
|
||||
# without calling the loader, the listener never fires, and the
|
||||
# referenced files would be silently dropped from the bundle.
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
data = yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
else:
|
||||
_force_load_include_files(data)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
discovered = yaml_util.discover_user_yaml_files(self._config_path)
|
||||
self._secrets_paths.update(discovered.secrets)
|
||||
config_resolved = self._config_path.resolve()
|
||||
for fpath in discovered.files:
|
||||
if fpath == config_resolved:
|
||||
continue # Already added as config
|
||||
if fpath.name in const.SECRETS_FILES:
|
||||
self._secrets_paths.add(fpath)
|
||||
self._add_file(fpath)
|
||||
|
||||
def _discover_component_files(self) -> None:
|
||||
@@ -625,57 +603,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
|
||||
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
|
||||
|
||||
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
|
||||
resolved during the substitution pass. During bundle discovery we need
|
||||
the referenced files to actually load so the ``track_yaml_loads``
|
||||
listener fires for them.
|
||||
|
||||
``IncludeFile`` instances with unresolved substitution variables in the
|
||||
filename cannot be loaded — we skip and warn about those.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
|
||||
if isinstance(obj, yaml_util.IncludeFile):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
if obj.has_unresolved_expressions():
|
||||
_LOGGER.warning(
|
||||
"Bundle: cannot resolve !include %s (referenced from %s) "
|
||||
"with substitutions in path",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
)
|
||||
return
|
||||
try:
|
||||
loaded = obj.load()
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"Bundle: failed to load !include %s (referenced from %s): %s",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
err,
|
||||
)
|
||||
return
|
||||
_force_load_include_files(loaded, _seen)
|
||||
elif isinstance(obj, dict):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for value in obj.values():
|
||||
_force_load_include_files(value, _seen)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for item in obj:
|
||||
_force_load_include_files(item, _seen)
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
|
||||
@@ -395,7 +395,7 @@ async def to_code(config):
|
||||
)
|
||||
if data.mp3_support:
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-mp3", ref="0.2.0")
|
||||
add_idf_component(name="esphome/micro-mp3", ref="0.2.1")
|
||||
_emit_memory_pair(
|
||||
data.mp3.buffer_memory,
|
||||
"CONFIG_MP3_DECODER_PREFER_PSRAM",
|
||||
|
||||
@@ -161,7 +161,7 @@ void BME680BSECComponent::dump_config() {
|
||||
" IAQ Mode: %s\n"
|
||||
" Supply Voltage: %sV\n"
|
||||
" Sample Rate: %s\n"
|
||||
" State Save Interval: %ims",
|
||||
" State Save Interval: %" PRIu32 "ms",
|
||||
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
|
||||
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
|
||||
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
|
||||
@@ -461,7 +461,7 @@ int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t devid, uint8_t a_registe
|
||||
}
|
||||
|
||||
void BME680BSECComponent::delay_ms(uint32_t period) {
|
||||
ESP_LOGV(TAG, "Delaying for %ums", period);
|
||||
ESP_LOGV(TAG, "Delaying for %" PRIu32 "ms", period);
|
||||
delay(period);
|
||||
}
|
||||
|
||||
|
||||
@@ -1767,9 +1767,11 @@ async def to_code(config):
|
||||
else:
|
||||
cg.add_build_flag("-Wno-error=format")
|
||||
cg.add_build_flag("-Wno-error=maybe-uninitialized")
|
||||
cg.add_build_flag("-Wno-error=missing-field-initializers")
|
||||
cg.add_build_flag("-Wno-error=overloaded-virtual")
|
||||
cg.add_build_flag("-Wno-error=reorder")
|
||||
cg.add_build_flag("-Wno-error=volatile")
|
||||
# -Wno- (not -Wno-error=): suppress entirely, too noisy on C++ aggregates
|
||||
cg.add_build_flag("-Wno-missing-field-initializers")
|
||||
|
||||
cg.set_cpp_standard("gnu++20")
|
||||
cg.add_build_flag("-DUSE_ESP32")
|
||||
@@ -2002,7 +2004,8 @@ async def to_code(config):
|
||||
if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]:
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
||||
if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]:
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
||||
# Kconfig range is [1,63]; 0 gets clamped to the default.
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 1)
|
||||
|
||||
_configure_lwip_max_sockets(conf)
|
||||
|
||||
@@ -2249,7 +2252,8 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2)
|
||||
elif advanced[CONF_DISABLE_FATFS]:
|
||||
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 0)
|
||||
# Kconfig range is [1,10]; 0 gets clamped to the default.
|
||||
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 1)
|
||||
|
||||
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
|
||||
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
|
||||
@@ -2486,9 +2490,8 @@ def _write_sdkconfig():
|
||||
|
||||
def _platformio_library_to_dependency(library: Library) -> tuple[str, dict[str, str]]:
|
||||
dependency: dict[str, str] = {}
|
||||
name, version, path = generate_idf_component(library)
|
||||
name, _version, path = generate_idf_component(library)
|
||||
dependency["override_path"] = str(path)
|
||||
dependency["version"] = version
|
||||
return name, dependency
|
||||
|
||||
|
||||
|
||||
@@ -196,42 +196,35 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
|
||||
(*this->on_read_callback_)(param->read.conn_id);
|
||||
}
|
||||
|
||||
uint16_t max_offset = 22;
|
||||
|
||||
// Use the client-supplied offset for long reads; short reads always start at 0.
|
||||
// The Bluedroid stack truncates ATT_READ_RSP / ATT_READ_BLOB_RSP to MTU-1, so we
|
||||
// just provide as much data as we have from the requested offset and let the stack
|
||||
// handle framing. The client issues subsequent blob reads with increasing offsets
|
||||
// until it has received the whole value.
|
||||
const uint16_t offset = param->read.is_long ? param->read.offset : 0;
|
||||
esp_gatt_status_t status = ESP_GATT_OK;
|
||||
esp_gatt_rsp_t response;
|
||||
if (param->read.is_long) {
|
||||
if (this->value_read_offset_ >= this->value_.size()) {
|
||||
response.attr_value.len = 0;
|
||||
response.attr_value.offset = this->value_read_offset_;
|
||||
this->value_read_offset_ = 0;
|
||||
} else if (this->value_.size() - this->value_read_offset_ < max_offset) {
|
||||
// Last message in the chain
|
||||
response.attr_value.len = this->value_.size() - this->value_read_offset_;
|
||||
response.attr_value.offset = this->value_read_offset_;
|
||||
memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len);
|
||||
this->value_read_offset_ = 0;
|
||||
} else {
|
||||
response.attr_value.len = max_offset;
|
||||
response.attr_value.offset = this->value_read_offset_;
|
||||
memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len);
|
||||
this->value_read_offset_ += max_offset;
|
||||
}
|
||||
response.attr_value.offset = offset;
|
||||
|
||||
if (offset > this->value_.size()) {
|
||||
status = ESP_GATT_INVALID_OFFSET;
|
||||
response.attr_value.len = 0;
|
||||
} else {
|
||||
response.attr_value.offset = 0;
|
||||
if (this->value_.size() + 1 > max_offset) {
|
||||
response.attr_value.len = max_offset;
|
||||
this->value_read_offset_ = max_offset;
|
||||
} else {
|
||||
response.attr_value.len = this->value_.size();
|
||||
size_t remaining = this->value_.size() - offset;
|
||||
if (remaining > ESP_GATT_MAX_ATTR_LEN) {
|
||||
ESP_LOGW(TAG, "Characteristic length %u exceeds buffer size of %u, truncating",
|
||||
static_cast<unsigned>(remaining), ESP_GATT_MAX_ATTR_LEN);
|
||||
remaining = ESP_GATT_MAX_ATTR_LEN;
|
||||
}
|
||||
memcpy(response.attr_value.value, this->value_.data(), response.attr_value.len);
|
||||
response.attr_value.len = remaining;
|
||||
memcpy(response.attr_value.value, this->value_.data() + offset, remaining);
|
||||
}
|
||||
|
||||
response.attr_value.handle = this->handle_;
|
||||
response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
|
||||
|
||||
esp_err_t err =
|
||||
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &response);
|
||||
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, status, &response);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ class BLECharacteristic {
|
||||
esp_gatt_char_prop_t properties_;
|
||||
uint16_t handle_{0xFFFF};
|
||||
|
||||
uint16_t value_read_offset_{0};
|
||||
std::vector<uint8_t> value_;
|
||||
std::vector<BLEDescriptor *> descriptors_;
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ async def to_code(config):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1")
|
||||
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.7")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -92,7 +92,7 @@ void Esp32HostedUpdate::setup() {
|
||||
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
|
||||
// 16 bytes: "255.255.255" (11 chars) + null + safety margin
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
|
||||
snprintf(buf, sizeof(buf), "%" PRIu32 ".%" PRIu32 ".%" PRIu32, ver_info.major1, ver_info.minor1, ver_info.patch1);
|
||||
this->update_info_.current_version = buf;
|
||||
} else {
|
||||
this->update_info_.current_version = "unknown";
|
||||
@@ -120,8 +120,8 @@ void Esp32HostedUpdate::setup() {
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word,
|
||||
ESP_APP_DESC_MAGIC_WORD);
|
||||
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")",
|
||||
app_desc->magic_word, static_cast<uint32_t>(ESP_APP_DESC_MAGIC_WORD));
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -58,6 +58,12 @@ __attribute__((always_inline)) inline const char *progmem_read_ptr(const char *c
|
||||
__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) {
|
||||
return pgm_read_word(addr); // NOLINT
|
||||
}
|
||||
// Bulk PROGMEM copy: routes to the SDK's aligned-flash `memcpy_P` so callers
|
||||
// don't have to drop to a byte-by-byte `progmem_read_byte` loop, which on
|
||||
// ESP8266 is ~4x as many flash accesses as the bulk path.
|
||||
__attribute__((always_inline)) inline void progmem_memcpy(void *dst, const void *src, size_t len) {
|
||||
memcpy_P(dst, src, len); // NOLINT
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
|
||||
@@ -108,8 +108,8 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Partition access allowed\n"
|
||||
" Running app:\n"
|
||||
" Partition address: 0x%X\n"
|
||||
" Used size: %zu bytes (0x%X)",
|
||||
" Partition address: 0x%" PRIX32 "\n"
|
||||
" Used size: %zu bytes (0x%zX)",
|
||||
this->running_app_offset_, this->running_app_size_, this->running_app_size_);
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -378,7 +378,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
|
||||
(static_cast<size_t>(buf[2]) << 8) | buf[3];
|
||||
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
|
||||
ESP_LOGV(TAG, "Size is %zu bytes", ota_size);
|
||||
|
||||
#ifndef USE_OTA_PARTITIONS
|
||||
if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
|
||||
@@ -749,7 +749,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
|
||||
this->auth_buf_[0] = this->auth_type_;
|
||||
hasher.get_hex(buf);
|
||||
|
||||
ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf);
|
||||
ESP_LOGV(TAG, "Auth: Nonce is %.*s", (int) hex_size, buf);
|
||||
}
|
||||
|
||||
// Try to write auth_type + nonce
|
||||
@@ -809,13 +809,13 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
|
||||
hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
|
||||
hasher.calculate();
|
||||
|
||||
ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce);
|
||||
ESP_LOGV(TAG, "Auth: CNonce is %.*s", (int) hex_size, cnonce);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator)
|
||||
hasher.get_hex(computed_hash);
|
||||
ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash);
|
||||
ESP_LOGV(TAG, "Auth: Result is %.*s", (int) hex_size, computed_hash);
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response);
|
||||
ESP_LOGV(TAG, "Auth: Response is %.*s", (int) hex_size, response);
|
||||
|
||||
// Compare response
|
||||
bool matches = hasher.equals_hex(response);
|
||||
|
||||
@@ -19,7 +19,7 @@ void FastLEDLightOutput::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"FastLED light:\n"
|
||||
" Num LEDs: %u\n"
|
||||
" Max refresh rate: %u",
|
||||
" Max refresh rate: %" PRIu32,
|
||||
this->num_leds_, this->max_refresh_rate_.value_or(0));
|
||||
}
|
||||
void FastLEDLightOutput::write_state(light::LightState *state) {
|
||||
|
||||
@@ -206,6 +206,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() {
|
||||
break;
|
||||
case ENROLL_MISMATCH:
|
||||
ESP_LOGE(TAG, "Scans do not match");
|
||||
[[fallthrough]];
|
||||
default:
|
||||
return this->data_[0];
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@ void FT5x06Touchscreen::setup() {
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
// reading the chip registers to get max x/y does not seem to work.
|
||||
if (this->display_ != nullptr) {
|
||||
if (this->x_raw_max_ == this->x_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
}
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
|
||||
// wait 200ms after reset.
|
||||
this->set_timeout(200, [this] { this->continue_setup_(); });
|
||||
}
|
||||
@@ -39,15 +49,6 @@ void FT5x06Touchscreen::continue_setup_() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// reading the chip registers to get max x/y does not seem to work.
|
||||
if (this->display_ != nullptr) {
|
||||
if (this->x_raw_max_ == this->x_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
}
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FT5x06Touchscreen::update_touches() {
|
||||
@@ -71,7 +72,7 @@ void FT5x06Touchscreen::update_touches() {
|
||||
uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]);
|
||||
uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]);
|
||||
|
||||
ESP_LOGD(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
|
||||
ESP_LOGV(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
|
||||
if (status == 0 || status == 2) {
|
||||
this->add_raw_touch_position_(id, x, y);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only
|
||||
void HDC2080Component::setup() {
|
||||
const uint8_t data = 0x00; // automatic measurement mode disabled, heater off
|
||||
if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ async def to_code(config):
|
||||
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||
cg.add_build_flag("-Wno-error=overloaded-virtual")
|
||||
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.41")
|
||||
if CORE.is_libretiny or CORE.is_esp32:
|
||||
|
||||
@@ -89,10 +89,10 @@ def _set_num_channels_from_config(config):
|
||||
|
||||
def _set_stream_limits(config):
|
||||
if config.get(CONF_SPDIF_MODE, False):
|
||||
# SPDIF mode: fixed to 16-bit stereo at configured sample rate
|
||||
# SPDIF mode: 16/24/32-bit audio and stereo at configured sample rate
|
||||
audio.set_stream_limits(
|
||||
min_bits_per_sample=16,
|
||||
max_bits_per_sample=16,
|
||||
max_bits_per_sample=32,
|
||||
min_channels=2,
|
||||
max_channels=2,
|
||||
min_sample_rate=config.get(CONF_SAMPLE_RATE),
|
||||
@@ -213,9 +213,6 @@ def _final_validate(config):
|
||||
)
|
||||
if config[CONF_CHANNEL] != CONF_STEREO:
|
||||
raise cv.Invalid("SPDIF mode only supports stereo channel configuration")
|
||||
# bits_per_sample is converted to float by the schema
|
||||
if config[CONF_BITS_PER_SAMPLE] != 16:
|
||||
raise cv.Invalid("SPDIF mode only supports 16 bits per sample")
|
||||
if not config[CONF_USE_APLL]:
|
||||
raise cv.Invalid(
|
||||
"SPDIF mode requires 'use_apll: true' for accurate clock generation"
|
||||
|
||||
@@ -138,21 +138,21 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
// Reset lockstep records queue so it starts paired with the (also-reset) i2s_event_queue_.
|
||||
xQueueReset(this->write_records_queue_);
|
||||
|
||||
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * SPDIF_DMA_BUFFERS_COUNT;
|
||||
// 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->buffer_duration_ms_);
|
||||
|
||||
// The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info
|
||||
const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1);
|
||||
// Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and
|
||||
// avoids unnecessary single-frame splices.
|
||||
const size_t ring_buffer_size =
|
||||
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame;
|
||||
|
||||
// For SPDIF mode, one DMA buffer = one SPDIF block = 192 PCM frames
|
||||
// For SPDIF mode, one DMA buffer = one SPDIF block = 192 PCM frames (~4 ms at 48 kHz),
|
||||
// not the ~15 ms a standard I2S DMA buffer holds. Derive the DMA floor from actual block size.
|
||||
const uint32_t frames_to_fill_single_dma_buffer = SPDIF_BLOCK_SAMPLES;
|
||||
const size_t bytes_to_fill_single_dma_buffer =
|
||||
this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer);
|
||||
const size_t dma_buffers_floor_bytes = bytes_to_fill_single_dma_buffer * SPDIF_DMA_BUFFERS_COUNT;
|
||||
|
||||
// Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and
|
||||
// avoids unnecessary single-frame splices. Ensure it is at least large enough to cover all DMA buffers.
|
||||
const size_t requested_ring_buffer_bytes =
|
||||
(this->current_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame;
|
||||
const size_t ring_buffer_size = std::max(dma_buffers_floor_bytes, requested_ring_buffer_bytes);
|
||||
|
||||
bool successful_setup = false;
|
||||
std::unique_ptr<audio::RingBufferAudioSource> audio_source;
|
||||
@@ -177,7 +177,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
// on_sent events drain in lockstep without crediting any audio frames.
|
||||
this->spdif_encoder_->set_preload_mode(true);
|
||||
for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) {
|
||||
esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
|
||||
// i2s_channel_preload_data is non-blocking (returns immediately when the preload buffer fills), so no wait.
|
||||
esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(0);
|
||||
if (preload_err != ESP_OK) {
|
||||
break; // DMA preload buffer full or error
|
||||
}
|
||||
@@ -410,8 +411,9 @@ esp_err_t I2SAudioSpeakerSPDIF::start_i2s_driver(audio::AudioStreamInfo &audio_s
|
||||
this->sample_rate_, audio_stream_info.get_sample_rate());
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
if (audio_stream_info.get_bits_per_sample() != 16) {
|
||||
ESP_LOGE(TAG, "Only supports 16 bits per sample");
|
||||
const uint8_t bits_per_sample = audio_stream_info.get_bits_per_sample();
|
||||
if (bits_per_sample != 16 && bits_per_sample != 24 && bits_per_sample != 32) {
|
||||
ESP_LOGE(TAG, "Only supports 16, 24, or 32 bits per sample (got %u)", (unsigned) bits_per_sample);
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
if (audio_stream_info.get_channels() != 2) {
|
||||
@@ -419,11 +421,8 @@ esp_err_t I2SAudioSpeakerSPDIF::start_i2s_driver(audio::AudioStreamInfo &audio_s
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO &&
|
||||
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
|
||||
ESP_LOGE(TAG, "Stream bits per sample must be less than or equal to the speaker's configuration");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
// Tell the encoder what input width to expect. 32-bit input is truncated to 24-bit on the wire.
|
||||
this->spdif_encoder_->set_bytes_per_sample(bits_per_sample / 8);
|
||||
|
||||
if (!this->parent_->try_lock()) {
|
||||
ESP_LOGE(TAG, "Parent bus is busy");
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
namespace esphome::i2s_audio {
|
||||
|
||||
// Shared constants used by both standard and SPDIF speaker implementations
|
||||
static constexpr uint32_t DMA_BUFFER_DURATION_MS = 15;
|
||||
static constexpr size_t TASK_STACK_SIZE = 4096;
|
||||
static constexpr ssize_t TASK_PRIORITY = 19;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace esphome::i2s_audio {
|
||||
|
||||
static const char *const TAG = "i2s_audio.speaker.std";
|
||||
|
||||
static constexpr uint32_t DMA_BUFFER_DURATION_MS = 15;
|
||||
static constexpr size_t DMA_BUFFERS_COUNT = 4;
|
||||
// Sized to comfortably absorb scheduling jitter: at most DMA_BUFFERS_COUNT events can be in flight,
|
||||
// doubled so that a transient backlog never overruns the queue (which would desync the lockstep
|
||||
|
||||
@@ -17,7 +17,7 @@ static constexpr uint8_t PREAMBLE_M = 0x1d; // Left channel (not block start)
|
||||
static constexpr uint8_t PREAMBLE_W = 0x1b; // Right channel
|
||||
|
||||
// BMC encoding of 4 zero bits starting at phase HIGH: 00_11_00_11 = 0x33
|
||||
// Since both aux nibbles (bits 4-7, 8-11) are zero for 16-bit audio and phase is preserved, both are 0x33.
|
||||
// Used as a constant in the 16-bit subframe path, where bits 4-11 are always zero.
|
||||
static constexpr uint32_t BMC_ZERO_NIBBLE = 0x33;
|
||||
|
||||
// Constexpr BMC encoder for compile-time LUT generation.
|
||||
@@ -36,21 +36,43 @@ static constexpr uint16_t bmc_lut_encode(uint32_t data, uint8_t num_bits) {
|
||||
return bmc;
|
||||
}
|
||||
|
||||
// 4-bit BMC lookup table: 16 entries (16 bytes in flash)
|
||||
// Index: 4-bit data value (0-15), always phase=true start
|
||||
// Compile-time parity helper (constexpr-friendly, runs only at LUT build time).
|
||||
static constexpr uint32_t bmc_lut_parity(uint32_t value, uint32_t num_bits) {
|
||||
uint32_t p = 0;
|
||||
for (uint32_t b = 0; b < num_bits; b++)
|
||||
p ^= (value >> b) & 1u;
|
||||
return p;
|
||||
}
|
||||
|
||||
// Combined BMC + phase-delta lookup tables.
|
||||
// Each entry packs the BMC pattern (lower bits, phase=high start) together with
|
||||
// a phase-mask delta in bits 16-31 (0xFFFF if the input has odd parity, else 0).
|
||||
// XORing the delta into the running phase mask propagates parity across chunks
|
||||
// without an explicit popcount.
|
||||
|
||||
// 4-bit BMC lookup table: 16 entries x uint32_t = 64 bytes in flash.
|
||||
// Bits 0-7 : 8-bit BMC pattern (phase=high start)
|
||||
// Bits 16-31 : phase-mask delta (0xFFFFu if odd parity, else 0)
|
||||
static constexpr auto BMC_LUT_4 = [] {
|
||||
std::array<uint8_t, 16> t{};
|
||||
for (uint32_t i = 0; i < 16; i++)
|
||||
t[i] = static_cast<uint8_t>(bmc_lut_encode(i, 4));
|
||||
std::array<uint32_t, 16> t{};
|
||||
for (uint32_t i = 0; i < 16; i++) {
|
||||
uint32_t bmc = bmc_lut_encode(i, 4);
|
||||
uint32_t delta = bmc_lut_parity(i, 4) ? 0xFFFF0000u : 0u;
|
||||
t[i] = bmc | delta;
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
|
||||
// 8-bit BMC lookup table: 256 entries (512 bytes in flash)
|
||||
// Index: 8-bit data value (0-255), always phase=true start
|
||||
// 8-bit BMC lookup table: 256 entries x uint32_t = 1024 bytes in flash.
|
||||
// Bits 0-15 : 16-bit BMC pattern (phase=high start)
|
||||
// Bits 16-31 : phase-mask delta (0xFFFFu if odd parity, else 0)
|
||||
static constexpr auto BMC_LUT_8 = [] {
|
||||
std::array<uint16_t, 256> t{};
|
||||
for (uint32_t i = 0; i < 256; i++)
|
||||
t[i] = bmc_lut_encode(i, 8);
|
||||
std::array<uint32_t, 256> t{};
|
||||
for (uint32_t i = 0; i < 256; i++) {
|
||||
uint32_t bmc = bmc_lut_encode(i, 8);
|
||||
uint32_t delta = bmc_lut_parity(i, 8) ? 0xFFFF0000u : 0u;
|
||||
t[i] = bmc | delta;
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
|
||||
@@ -63,7 +85,7 @@ bool SPDIFEncoder::setup() {
|
||||
}
|
||||
ESP_LOGV(TAG, "Buffer allocated (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES);
|
||||
|
||||
// Build initial channel status block with default sample rate
|
||||
// Build initial channel status block with default sample rate and width
|
||||
this->build_channel_status_();
|
||||
|
||||
this->reset();
|
||||
@@ -73,7 +95,7 @@ bool SPDIFEncoder::setup() {
|
||||
void SPDIFEncoder::reset() {
|
||||
this->spdif_block_ptr_ = this->spdif_block_buf_.get();
|
||||
this->frame_in_block_ = 0;
|
||||
this->is_left_channel_ = true;
|
||||
this->block_buf_is_silence_block_ = false;
|
||||
}
|
||||
|
||||
void SPDIFEncoder::set_sample_rate(uint32_t sample_rate) {
|
||||
@@ -84,31 +106,27 @@ void SPDIFEncoder::set_sample_rate(uint32_t sample_rate) {
|
||||
}
|
||||
}
|
||||
|
||||
void SPDIFEncoder::set_bytes_per_sample(uint8_t bytes_per_sample) {
|
||||
if (bytes_per_sample != 2 && bytes_per_sample != 3 && bytes_per_sample != 4) {
|
||||
ESP_LOGE(TAG, "Unsupported bytes per sample: %u", (unsigned) bytes_per_sample);
|
||||
return;
|
||||
}
|
||||
if (this->bytes_per_sample_ != bytes_per_sample) {
|
||||
this->bytes_per_sample_ = bytes_per_sample;
|
||||
this->build_channel_status_();
|
||||
// Discard any partial block built at the previous width so we never mix widths on the wire.
|
||||
this->reset();
|
||||
ESP_LOGD(TAG, "Input width set to %u-bit", (unsigned) bytes_per_sample * 8);
|
||||
}
|
||||
}
|
||||
|
||||
void SPDIFEncoder::build_channel_status_() {
|
||||
// IEC 60958-3 Consumer Channel Status Block (192 bits = 24 bytes)
|
||||
// Transmitted LSB-first within each byte, one bit per frame via C bit
|
||||
//
|
||||
// Byte 0: Control bits
|
||||
// Bit 0: 0 = Consumer format (not professional AES3)
|
||||
// Bit 1: 0 = PCM audio (not non-audio data like AC3)
|
||||
// Bit 2: 0 = No copyright assertion
|
||||
// Bits 3-5: 000 = No pre-emphasis
|
||||
// Bits 6-7: 00 = Mode 0 (basic consumer format)
|
||||
//
|
||||
// Byte 1: Category code (0x00 = general, 0x01 = CD, etc.)
|
||||
//
|
||||
// Byte 2: Source/channel numbers
|
||||
// Bits 0-3: Source number (0 = unspecified)
|
||||
// Bits 4-7: Channel number (0 = unspecified)
|
||||
//
|
||||
// Byte 3: Sample frequency and clock accuracy
|
||||
// Bits 0-3: Sample frequency code
|
||||
// Bits 4-5: Clock accuracy (00 = Level II, ±1000 ppm, appropriate for ESP32)
|
||||
// Bits 6-7: Reserved (0)
|
||||
//
|
||||
// Bytes 4-23: Reserved (zeros for basic compliance)
|
||||
// Transmitted LSB-first within each byte, one bit per frame via C bit.
|
||||
|
||||
// Any cached silence block was built for the previous channel status; it is now stale.
|
||||
this->block_buf_is_silence_block_ = false;
|
||||
|
||||
// Clear all bytes first
|
||||
this->channel_status_.fill(0);
|
||||
|
||||
// Byte 0: Consumer, PCM audio, no copyright, no pre-emphasis, Mode 0
|
||||
@@ -140,132 +158,148 @@ void SPDIFEncoder::build_channel_status_() {
|
||||
// Byte 3: freq_code in bits 0-3, clock accuracy (00) in bits 4-5
|
||||
this->channel_status_[3] = freq_code; // Clock accuracy bits 4-5 are already 0
|
||||
|
||||
// Bytes 4-23 remain zero (word length not specified, no original sample freq, etc.)
|
||||
// Byte 4: Word length encoding (IEC 60958-3 consumer)
|
||||
// bit 0: max length flag (0 = max 20 bits, 1 = max 24 bits)
|
||||
// bits 1-3: word length code relative to the max
|
||||
// For our supported widths:
|
||||
// 16-bit (max 20): 0b0010 = 0x02 -- "16 bits, max 20"
|
||||
// 24-bit (max 24): 0b1101 = 0x0D -- "24 bits, max 24"
|
||||
// 32-bit input is truncated to 24-bit on the wire, so use the 24-bit code.
|
||||
uint8_t word_length_code;
|
||||
switch (this->bytes_per_sample_) {
|
||||
case 2:
|
||||
word_length_code = 0x02;
|
||||
break;
|
||||
case 3: // Shared case
|
||||
case 4:
|
||||
word_length_code = 0x0D;
|
||||
break;
|
||||
default:
|
||||
word_length_code = 0x00; // not specified
|
||||
break;
|
||||
}
|
||||
this->channel_status_[4] = word_length_code;
|
||||
}
|
||||
|
||||
HOT void SPDIFEncoder::encode_sample_(const uint8_t *pcm_sample) {
|
||||
// ============================================================================
|
||||
// Build raw 32-bit subframe (IEC 60958 format)
|
||||
// ============================================================================
|
||||
// Bit layout:
|
||||
// Bits 0-3: Preamble (handled separately, not in raw_subframe)
|
||||
// Bits 4-7: Auxiliary audio data (zeros for 16-bit audio)
|
||||
// Bits 8-11: Audio LSB extension (zeros for 16-bit audio)
|
||||
// Bits 12-27: 16-bit audio sample (MSB-aligned in 20-bit audio field)
|
||||
// Bit 28: V (Validity) - 0 = valid audio
|
||||
// Bit 29: U (User data) - 0
|
||||
// Bit 30: C (Channel status) - from channel status block
|
||||
// Bit 31: P (Parity) - even parity over bits 4-31
|
||||
// ============================================================================
|
||||
// Extract the C bit for the given frame from channel_status_ and shift it into bit 30
|
||||
// so it can be OR'd directly into a raw subframe.
|
||||
ESPHOME_ALWAYS_INLINE static inline uint32_t c_bit_for_frame(const std::array<uint8_t, 24> &channel_status,
|
||||
uint32_t frame) {
|
||||
return static_cast<uint32_t>((channel_status[frame >> 3] >> (frame & 7)) & 1u) << 30;
|
||||
}
|
||||
|
||||
// Place 16-bit audio sample at bits 12-27 (little-endian input: [0]=LSB, [1]=MSB)
|
||||
uint32_t raw_subframe = (static_cast<uint32_t>(pcm_sample[1]) << 20) | (static_cast<uint32_t>(pcm_sample[0]) << 12);
|
||||
// ============================================================================
|
||||
// IEC 60958 subframe bit layout
|
||||
// ============================================================================
|
||||
// Bits 0-3: Preamble (handled separately, not in raw_subframe)
|
||||
// Bits 4-7: Auxiliary audio data / 24-bit audio LSB
|
||||
// Bits 8-11: Audio LSB extension (zero for 16-bit, low nibble of audio for 24-bit)
|
||||
// Bits 12-27: Audio sample (16 high bits in 16-bit mode, mid 16 bits in 24-bit mode)
|
||||
// Bit 28: V (Validity) - 0 = valid audio
|
||||
// Bit 29: U (User data) - 0
|
||||
// Bit 30: C (Channel status) - from channel status block
|
||||
// Bit 31: P (Parity) - even parity over bits 4-31
|
||||
// ============================================================================
|
||||
|
||||
// V = 0 (valid audio), U = 0 (no user data)
|
||||
// C = channel status bit for current frame (same bit used for both L and R subframes)
|
||||
bool c_bit = this->get_channel_status_bit_(this->frame_in_block_);
|
||||
if (c_bit) {
|
||||
raw_subframe |= (1U << 30);
|
||||
// Build a raw IEC 60958 subframe from PCM little-endian input of width Bps bytes.
|
||||
// Caller is responsible for OR-ing in the C bit and parity.
|
||||
template<uint8_t Bps> ESPHOME_ALWAYS_INLINE static inline uint32_t build_raw_subframe(const uint8_t *pcm_sample) {
|
||||
static_assert(Bps == 2 || Bps == 3 || Bps == 4, "Unsupported bytes per sample");
|
||||
if constexpr (Bps == 2) {
|
||||
// 16-bit input: MSB-aligned in the 20-bit audio field, bits 12-27.
|
||||
return (static_cast<uint32_t>(pcm_sample[1]) << 20) | (static_cast<uint32_t>(pcm_sample[0]) << 12);
|
||||
} else if constexpr (Bps == 3) {
|
||||
// 24-bit input: full 24-bit audio field, bits 4-27.
|
||||
return (static_cast<uint32_t>(pcm_sample[2]) << 20) | (static_cast<uint32_t>(pcm_sample[1]) << 12) |
|
||||
(static_cast<uint32_t>(pcm_sample[0]) << 4);
|
||||
} else { // Bps == 4
|
||||
// 32-bit input truncated to 24-bit: drop the lowest byte.
|
||||
return (static_cast<uint32_t>(pcm_sample[3]) << 20) | (static_cast<uint32_t>(pcm_sample[2]) << 12) |
|
||||
(static_cast<uint32_t>(pcm_sample[1]) << 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate even parity over bits 4-30
|
||||
// This ensures consistent BMC ending phase regardless of audio content
|
||||
uint32_t bits_4_30 = (raw_subframe >> 4) & 0x07FFFFFF; // 27 bits (4-30)
|
||||
uint32_t ones_count = __builtin_popcount(bits_4_30);
|
||||
uint32_t parity = ones_count & 1; // 1 if odd count, 0 if even
|
||||
raw_subframe |= parity << 31; // Set P bit to make total even
|
||||
// BMC-encode a subframe and write the two output uint32 words to dst. Caller passes
|
||||
// raw_subframe with the C bit set (bit 30) and the P bit cleared (bit 31 = 0). P is
|
||||
// derived from the cumulative parity-mask delta of the per-byte LUT lookups.
|
||||
//
|
||||
// I2S halfword swap means word[0] transmits as: bits 24-31, 16-23, 8-15, 0-7.
|
||||
// word[1] transmits as: bits 16-31, 0-15. Within each halfword, MSB-first.
|
||||
// All preambles end at phase HIGH, so phase=true at the start of bit 4.
|
||||
//
|
||||
// P-bit derivation: BMC_LUT_*'s upper half encodes the parity of the input chunk. Each
|
||||
// chunk's parity delta is shifted down (`lut >> 16`) into a phase_mask that lives in the
|
||||
// low 16 bits, so the same value can also be XORed against subsequent BMC patterns to
|
||||
// invert phase. XOR'ing those deltas through all chunks (with bit 31 = 0) yields the
|
||||
// parity of bits 4-30 in the low bits of phase_mask -- the required value of the P bit
|
||||
// for even total parity. The BMC of bit 31 lives in bit 0 of the high-byte BMC output
|
||||
// (i = 7 maps to position (8-1-7)*2 = 0); flipping the source bit flips only the lower
|
||||
// BMC bit (= phase XOR bit), so applying P is `bmc_24_31 ^= phase_mask & 1u`.
|
||||
template<uint8_t Bps>
|
||||
ESPHOME_ALWAYS_INLINE static inline void bmc_encode_subframe(uint32_t raw_subframe, uint8_t preamble, uint32_t *dst) {
|
||||
if constexpr (Bps == 2) {
|
||||
// 16-bit path: bits 4-11 are zero, encoded inline as BMC_ZERO_NIBBLE constants.
|
||||
// Eight zero source bits with start phase=HIGH end at phase=HIGH (popcount of zeros is even),
|
||||
// so encoding of bits 12-15 starts at phase=true. Zeros contribute 0 to parity.
|
||||
uint32_t nibble = (raw_subframe >> 12) & 0xF;
|
||||
uint32_t lut_n = BMC_LUT_4[nibble];
|
||||
uint32_t bmc_12_15 = lut_n & 0xFFu;
|
||||
uint32_t phase_mask = lut_n >> 16; // 0xFFFFu if odd parity, else 0
|
||||
|
||||
// ============================================================================
|
||||
// Select preamble based on position in block and channel
|
||||
// ============================================================================
|
||||
// B = block start (left channel, frame 0 of 192-frame block)
|
||||
// M = left channel (frames 1-191)
|
||||
// W = right channel (all frames)
|
||||
uint8_t preamble;
|
||||
if (this->is_left_channel_) {
|
||||
preamble = (this->frame_in_block_ == 0) ? PREAMBLE_B : PREAMBLE_M;
|
||||
uint32_t byte_mid = (raw_subframe >> 16) & 0xFF;
|
||||
uint32_t lut_m = BMC_LUT_8[byte_mid];
|
||||
uint32_t bmc_16_23 = (lut_m & 0xFFFFu) ^ phase_mask;
|
||||
phase_mask ^= lut_m >> 16;
|
||||
|
||||
uint32_t byte_hi = (raw_subframe >> 24) & 0xFF; // bit 7 (= P) is 0 by precondition
|
||||
uint32_t lut_h = BMC_LUT_8[byte_hi];
|
||||
uint32_t bmc_24_31 = (lut_h & 0xFFFFu) ^ phase_mask;
|
||||
phase_mask ^= lut_h >> 16;
|
||||
// phase_mask now reflects parity of bits 4-30. Apply P by flipping bit 0 of bmc_24_31.
|
||||
bmc_24_31 ^= phase_mask & 1u;
|
||||
|
||||
dst[0] = bmc_12_15 | (BMC_ZERO_NIBBLE << 8) | (BMC_ZERO_NIBBLE << 16) | (static_cast<uint32_t>(preamble) << 24);
|
||||
dst[1] = bmc_24_31 | (bmc_16_23 << 16);
|
||||
} else {
|
||||
preamble = PREAMBLE_W;
|
||||
// 24-bit (and 32-bit truncated) path: bits 4-11 are live audio.
|
||||
uint32_t byte_lo = (raw_subframe >> 4) & 0xFF;
|
||||
uint32_t lut_l = BMC_LUT_8[byte_lo];
|
||||
uint32_t bmc_4_11 = lut_l & 0xFFFFu;
|
||||
uint32_t phase_mask = lut_l >> 16; // 0xFFFFu if odd parity, else 0
|
||||
|
||||
uint32_t nibble = (raw_subframe >> 12) & 0xF;
|
||||
uint32_t lut_n = BMC_LUT_4[nibble];
|
||||
uint32_t bmc_12_15 = (lut_n & 0xFFu) ^ (phase_mask & 0xFFu);
|
||||
phase_mask ^= lut_n >> 16;
|
||||
|
||||
uint32_t byte_mid = (raw_subframe >> 16) & 0xFF;
|
||||
uint32_t lut_m = BMC_LUT_8[byte_mid];
|
||||
uint32_t bmc_16_23 = (lut_m & 0xFFFFu) ^ phase_mask;
|
||||
phase_mask ^= lut_m >> 16;
|
||||
|
||||
uint32_t byte_hi = (raw_subframe >> 24) & 0xFF; // bit 7 (= P) is 0 by precondition
|
||||
uint32_t lut_h = BMC_LUT_8[byte_hi];
|
||||
uint32_t bmc_24_31 = (lut_h & 0xFFFFu) ^ phase_mask;
|
||||
phase_mask ^= lut_h >> 16;
|
||||
bmc_24_31 ^= phase_mask & 1u;
|
||||
|
||||
// word[0]: bits 24-31 = preamble, bits 8-23 = bmc(4-11), bits 0-7 = bmc(12-15)
|
||||
// word[1]: bits 16-31 = bmc(16-23), bits 0-15 = bmc(24-31)
|
||||
dst[0] = bmc_12_15 | (bmc_4_11 << 8) | (static_cast<uint32_t>(preamble) << 24);
|
||||
dst[1] = bmc_24_31 | (bmc_16_23 << 16);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BMC encode the data portion (bits 4-31) using lookup tables
|
||||
// ============================================================================
|
||||
// The I2S uses 16-bit halfword swap: bits 16-31 transmit before bits 0-15.
|
||||
// This applies to BOTH word[0] and word[1].
|
||||
//
|
||||
// word[0] transmission order: [16-23] → [24-31] → [0-7] → [8-15]
|
||||
// For correct S/PDIF subframe order (preamble → aux → audio):
|
||||
// - bits 16-23: preamble (8 BMC bits)
|
||||
// - bits 24-31: BMC(subframe bits 4-7) - first aux nibble
|
||||
// - bits 0-7: BMC(subframe bits 8-11) - second aux nibble
|
||||
// - bits 8-15: BMC(subframe bits 12-15) - audio low nibble
|
||||
//
|
||||
// word[1] transmission order: [16-31] → [0-15]
|
||||
// For correct S/PDIF subframe order:
|
||||
// - bits 16-31: BMC(subframe bits 16-23) - audio mid byte
|
||||
// - bits 0-15: BMC(subframe bits 24-31) - audio high nibble + VUCP
|
||||
// ============================================================================
|
||||
|
||||
// All preambles end at phase HIGH. Bits 4-11 are always zero for 16-bit audio;
|
||||
// two zero nibbles flip phase 8 times total → back to HIGH.
|
||||
// So bits 12-15 always start encoding at phase=true.
|
||||
|
||||
// Bits 12-15: 4-bit LUT lookup (always phase=true start)
|
||||
uint32_t nibble = (raw_subframe >> 12) & 0xF;
|
||||
uint32_t bmc_12_15 = BMC_LUT_4[nibble];
|
||||
|
||||
// Phase tracking via branchless XOR mask:
|
||||
// - 0x0000 means phase=true (use LUT value directly)
|
||||
// - 0xFFFF means phase=false (complement LUT value)
|
||||
// End phase = start XOR (popcount & 1) since zero-bits flip phase,
|
||||
// and for even bit widths: #zeros parity == popcount parity.
|
||||
uint32_t phase_mask = -(__builtin_popcount(nibble) & 1u) & 0xFFFF;
|
||||
|
||||
// Bits 16-23: 8-bit LUT lookup with phase correction
|
||||
uint32_t byte_mid = (raw_subframe >> 16) & 0xFF;
|
||||
uint32_t bmc_16_23 = BMC_LUT_8[byte_mid] ^ phase_mask;
|
||||
phase_mask ^= -(__builtin_popcount(byte_mid) & 1u) & 0xFFFF;
|
||||
|
||||
// Bits 24-31: 8-bit LUT lookup with phase correction
|
||||
uint32_t byte_hi = (raw_subframe >> 24) & 0xFF;
|
||||
uint32_t bmc_24_31 = BMC_LUT_8[byte_hi] ^ phase_mask;
|
||||
|
||||
// ============================================================================
|
||||
// Combine with correct positioning for I2S transmission
|
||||
// ============================================================================
|
||||
// I2S with halfword swap: transmits bits 16-31, then bits 0-15.
|
||||
// Within each halfword, MSB (highest bit) is transmitted first.
|
||||
//
|
||||
// For upper halfword (bits 16-31): bit 31 → bit 16
|
||||
// For lower halfword (bits 0-15): bit 15 → bit 0
|
||||
//
|
||||
// Desired S/PDIF order: preamble → bmc_4_7 → bmc_8_11 → bmc_12_15
|
||||
//
|
||||
// word[0] layout for correct transmission:
|
||||
// bits 24-31: preamble (transmitted 1st, as MSB of upper halfword)
|
||||
// bits 16-23: BMC_ZERO_NIBBLE (transmitted 2nd, aux bits 4-7)
|
||||
// bits 8-15: BMC_ZERO_NIBBLE (transmitted 3rd, aux bits 8-11)
|
||||
// bits 0-7: bmc_12_15 (transmitted 4th, audio low nibble)
|
||||
//
|
||||
// word[1] layout:
|
||||
// bits 16-31: bmc_16_23 (transmitted 5th)
|
||||
// bits 0-15: bmc_24_31 (transmitted 6th)
|
||||
this->spdif_block_ptr_[0] =
|
||||
bmc_12_15 | (BMC_ZERO_NIBBLE << 8) | (BMC_ZERO_NIBBLE << 16) | (static_cast<uint32_t>(preamble) << 24);
|
||||
this->spdif_block_ptr_[1] = bmc_24_31 | (bmc_16_23 << 16);
|
||||
this->spdif_block_ptr_ += 2;
|
||||
|
||||
// ============================================================================
|
||||
// Update position tracking
|
||||
// ============================================================================
|
||||
if (!this->is_left_channel_) {
|
||||
// Completed a stereo frame, advance frame counter
|
||||
if (++this->frame_in_block_ >= SPDIF_BLOCK_SAMPLES) {
|
||||
this->frame_in_block_ = 0;
|
||||
}
|
||||
template<uint8_t Bps> void SPDIFEncoder::encode_silence_frame_() {
|
||||
static constexpr uint8_t SILENCE[4] = {0, 0, 0, 0};
|
||||
uint32_t raw = build_raw_subframe<Bps>(SILENCE) | c_bit_for_frame(this->channel_status_, this->frame_in_block_);
|
||||
uint8_t preamble_l = (this->frame_in_block_ == 0) ? PREAMBLE_B : PREAMBLE_M;
|
||||
bmc_encode_subframe<Bps>(raw, preamble_l, this->spdif_block_ptr_);
|
||||
bmc_encode_subframe<Bps>(raw, PREAMBLE_W, this->spdif_block_ptr_ + 2);
|
||||
this->spdif_block_ptr_ += 4;
|
||||
if (++this->frame_in_block_ >= SPDIF_BLOCK_SAMPLES) {
|
||||
this->frame_in_block_ = 0;
|
||||
}
|
||||
this->is_left_channel_ = !this->is_left_channel_;
|
||||
}
|
||||
|
||||
esp_err_t SPDIFEncoder::send_block_(TickType_t ticks_to_wait) {
|
||||
@@ -295,79 +329,162 @@ esp_err_t SPDIFEncoder::send_block_(TickType_t ticks_to_wait) {
|
||||
return err;
|
||||
}
|
||||
|
||||
size_t SPDIFEncoder::get_pending_pcm_bytes() const {
|
||||
if (this->spdif_block_ptr_ == nullptr || this->spdif_block_buf_ == nullptr) {
|
||||
return 0;
|
||||
template<uint8_t Bps>
|
||||
HOT esp_err_t SPDIFEncoder::write_typed_(const uint8_t *src, size_t size, TickType_t ticks_to_wait,
|
||||
uint32_t *blocks_sent, size_t *bytes_consumed) {
|
||||
const uint8_t *pcm_data = src;
|
||||
const uint8_t *const pcm_end = src + size;
|
||||
uint32_t block_count = 0;
|
||||
|
||||
// Hot state lives in locals so the compiler can keep it in registers across the
|
||||
// per-frame encoding work; byte writes through block_ptr may alias the member fields,
|
||||
// which would block register allocation if the encoding read them directly from this->*.
|
||||
uint32_t *block_ptr = this->spdif_block_ptr_;
|
||||
uint32_t *const block_buf = this->spdif_block_buf_.get();
|
||||
uint32_t *const block_end = block_buf + SPDIF_BLOCK_SIZE_U32;
|
||||
uint32_t frame = this->frame_in_block_;
|
||||
const std::array<uint8_t, 24> &channel_status = this->channel_status_;
|
||||
|
||||
auto save_state = [&]() {
|
||||
this->spdif_block_ptr_ = block_ptr;
|
||||
this->frame_in_block_ = static_cast<uint8_t>(frame);
|
||||
};
|
||||
|
||||
auto report_out_params = [&]() {
|
||||
if (blocks_sent != nullptr)
|
||||
*blocks_sent = block_count;
|
||||
if (bytes_consumed != nullptr)
|
||||
*bytes_consumed = pcm_data - src;
|
||||
};
|
||||
|
||||
// Send a completed block if the buffer is full, propagating any error.
|
||||
// send_block_ resets this->spdif_block_ptr_ to block_buf on success and leaves it
|
||||
// unchanged on error -- mirror both behaviors in our local block_ptr.
|
||||
auto maybe_send = [&]() -> esp_err_t {
|
||||
if (block_ptr >= block_end) {
|
||||
esp_err_t err = this->send_block_(ticks_to_wait);
|
||||
if (err != ESP_OK) {
|
||||
save_state();
|
||||
report_out_params();
|
||||
return err;
|
||||
}
|
||||
block_ptr = block_buf;
|
||||
++block_count;
|
||||
}
|
||||
return ESP_OK;
|
||||
};
|
||||
|
||||
// Hot path: encode L+R pairs in two peeled sub-loops. Frame 0 carries the only
|
||||
// buffer-full check and uses PREAMBLE_B (a block fills exactly when frame wraps from
|
||||
// 191 back to 0). Frames 1..191 use PREAMBLE_M and need no buffer-full check or
|
||||
// preamble branch. The encoding body is inlined here so block_ptr lives in a register
|
||||
// for the duration of the loop.
|
||||
while (pcm_data + 2 * Bps <= pcm_end) {
|
||||
if (frame == 0) {
|
||||
esp_err_t err = maybe_send();
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
uint32_t c_bit = c_bit_for_frame(channel_status, 0);
|
||||
uint32_t raw_l = build_raw_subframe<Bps>(pcm_data) | c_bit;
|
||||
uint32_t raw_r = build_raw_subframe<Bps>(pcm_data + Bps) | c_bit;
|
||||
bmc_encode_subframe<Bps>(raw_l, PREAMBLE_B, block_ptr);
|
||||
bmc_encode_subframe<Bps>(raw_r, PREAMBLE_W, block_ptr + 2);
|
||||
block_ptr += 4;
|
||||
frame = 1;
|
||||
pcm_data += 2 * Bps;
|
||||
}
|
||||
|
||||
// The inner loop runs until min(SPDIF_BLOCK_SAMPLES, frame + input_frames). The
|
||||
// input-size bound is folded into end_frame so a single `frame < end_frame` test
|
||||
// governs termination.
|
||||
uint32_t input_frames = static_cast<uint32_t>(pcm_end - pcm_data) / (2u * Bps);
|
||||
uint32_t end_frame = SPDIF_BLOCK_SAMPLES;
|
||||
if (frame + input_frames < end_frame)
|
||||
end_frame = frame + input_frames;
|
||||
|
||||
while (frame < end_frame) {
|
||||
uint32_t c_bit = c_bit_for_frame(channel_status, frame);
|
||||
uint32_t raw_l = build_raw_subframe<Bps>(pcm_data) | c_bit;
|
||||
uint32_t raw_r = build_raw_subframe<Bps>(pcm_data + Bps) | c_bit;
|
||||
bmc_encode_subframe<Bps>(raw_l, PREAMBLE_M, block_ptr);
|
||||
bmc_encode_subframe<Bps>(raw_r, PREAMBLE_W, block_ptr + 2);
|
||||
block_ptr += 4;
|
||||
++frame;
|
||||
pcm_data += 2 * Bps;
|
||||
}
|
||||
if (frame >= SPDIF_BLOCK_SAMPLES)
|
||||
frame = 0;
|
||||
}
|
||||
// Each PCM sample (2 bytes) produces 2 uint32_t values in the SPDIF buffer
|
||||
// So pending uint32s / 2 = pending samples, and each sample is 2 bytes
|
||||
size_t pending_uint32s = this->spdif_block_ptr_ - this->spdif_block_buf_.get();
|
||||
size_t pending_samples = pending_uint32s / 2;
|
||||
return pending_samples * 2; // 2 bytes per sample
|
||||
|
||||
// Send any complete block that was just finished.
|
||||
if (block_ptr >= block_end) {
|
||||
esp_err_t err = this->send_block_(ticks_to_wait);
|
||||
if (err != ESP_OK) {
|
||||
save_state();
|
||||
report_out_params();
|
||||
return err;
|
||||
}
|
||||
block_ptr = block_buf;
|
||||
++block_count;
|
||||
}
|
||||
|
||||
save_state();
|
||||
report_out_params();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
HOT esp_err_t SPDIFEncoder::write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent,
|
||||
size_t *bytes_consumed) {
|
||||
const uint8_t *pcm_data = src;
|
||||
const uint8_t *pcm_end = src + size;
|
||||
uint32_t block_count = 0;
|
||||
if (size > 0) {
|
||||
// Real PCM is about to be encoded into the buffer, so it is no longer a full-silence block.
|
||||
this->block_buf_is_silence_block_ = false;
|
||||
}
|
||||
switch (this->bytes_per_sample_) {
|
||||
case 2:
|
||||
return this->write_typed_<2>(src, size, ticks_to_wait, blocks_sent, bytes_consumed);
|
||||
case 3:
|
||||
return this->write_typed_<3>(src, size, ticks_to_wait, blocks_sent, bytes_consumed);
|
||||
case 4:
|
||||
return this->write_typed_<4>(src, size, ticks_to_wait, blocks_sent, bytes_consumed);
|
||||
default:
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
}
|
||||
|
||||
while (pcm_data < pcm_end) {
|
||||
// Check if there's a pending complete block from a previous failed send
|
||||
if (this->spdif_block_ptr_ >= &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
esp_err_t err = this->send_block_(ticks_to_wait);
|
||||
if (err != ESP_OK) {
|
||||
if (blocks_sent != nullptr) {
|
||||
*blocks_sent = block_count;
|
||||
}
|
||||
if (bytes_consumed != nullptr) {
|
||||
*bytes_consumed = pcm_data - src;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
++block_count;
|
||||
template<uint8_t Bps> esp_err_t SPDIFEncoder::flush_with_silence_typed_(TickType_t ticks_to_wait) {
|
||||
// If a complete block is already pending (from a previous failed send), emit just that block.
|
||||
// Otherwise pad the partial block with silence (or generate a full silence block if empty) and
|
||||
// send. Always emits exactly one block on success.
|
||||
if (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
const bool was_empty = (this->spdif_block_ptr_ == this->spdif_block_buf_.get());
|
||||
// Continuous-silence idle case: a full silence block is byte-identical every time for the
|
||||
// active channel status, so when the buffer already holds one, re-send it as-is.
|
||||
if (was_empty && this->block_buf_is_silence_block_) {
|
||||
return this->send_block_(ticks_to_wait);
|
||||
}
|
||||
|
||||
// Encode one 16-bit sample
|
||||
this->encode_sample_(pcm_data);
|
||||
pcm_data += 2;
|
||||
}
|
||||
|
||||
// Send any complete block that was just finished
|
||||
if (this->spdif_block_ptr_ >= &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
esp_err_t err = this->send_block_(ticks_to_wait);
|
||||
if (err != ESP_OK) {
|
||||
if (blocks_sent != nullptr) {
|
||||
*blocks_sent = block_count;
|
||||
}
|
||||
if (bytes_consumed != nullptr) {
|
||||
*bytes_consumed = pcm_data - src;
|
||||
}
|
||||
return err;
|
||||
// Pad with silence frames at the configured width.
|
||||
while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
this->encode_silence_frame_<Bps>();
|
||||
}
|
||||
++block_count;
|
||||
// The buffer is a reusable full-silence block only if it was built entirely from silence; a
|
||||
// partial real-audio block padded out with silence is not.
|
||||
this->block_buf_is_silence_block_ = was_empty;
|
||||
}
|
||||
|
||||
if (blocks_sent != nullptr) {
|
||||
*blocks_sent = block_count;
|
||||
}
|
||||
if (bytes_consumed != nullptr) {
|
||||
*bytes_consumed = size;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->send_block_(ticks_to_wait);
|
||||
}
|
||||
|
||||
esp_err_t SPDIFEncoder::flush_with_silence(TickType_t ticks_to_wait) {
|
||||
// If a complete block is already pending (from a previous failed send), emit just that block.
|
||||
// Otherwise pad the partial block with silence (or generate a full silence block if empty)
|
||||
// and send. Always emits exactly one block on success.
|
||||
if (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
static const uint8_t SILENCE[2] = {0, 0};
|
||||
while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
|
||||
this->encode_sample_(SILENCE);
|
||||
}
|
||||
switch (this->bytes_per_sample_) {
|
||||
case 2:
|
||||
return this->flush_with_silence_typed_<2>(ticks_to_wait);
|
||||
case 3:
|
||||
return this->flush_with_silence_typed_<3>(ticks_to_wait);
|
||||
case 4:
|
||||
return this->flush_with_silence_typed_<4>(ticks_to_wait);
|
||||
default:
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return this->send_block_(ticks_to_wait);
|
||||
}
|
||||
|
||||
} // namespace esphome::i2s_audio
|
||||
|
||||
@@ -24,8 +24,6 @@ static constexpr uint16_t SPDIF_BLOCK_SIZE_BYTES = SPDIF_BLOCK_SAMPLES * (EMULAT
|
||||
static constexpr uint32_t SPDIF_BLOCK_SIZE_U32 = SPDIF_BLOCK_SIZE_BYTES / sizeof(uint32_t); // 3072 bytes / 4 = 768
|
||||
// I2S frame count for one SPDIF block (for new driver where frame = 8 bytes for 32-bit stereo)
|
||||
static constexpr uint32_t SPDIF_BLOCK_I2S_FRAMES = SPDIF_BLOCK_SIZE_BYTES / 8; // 3072 / 8 = 384 frames
|
||||
// PCM bytes needed for one complete SPDIF block (192 stereo frames * 2 bytes per sample * 2 channels)
|
||||
static constexpr uint16_t SPDIF_PCM_BYTES_PER_BLOCK = SPDIF_BLOCK_SAMPLES * 2 * 2; // = 768 bytes
|
||||
|
||||
/// Callback signature for block completion (raw function pointer for minimal overhead)
|
||||
/// @param user_ctx User context pointer passed during callback registration
|
||||
@@ -64,8 +62,16 @@ class SPDIFEncoder {
|
||||
/// @brief Check if currently in preload mode
|
||||
bool is_preload_mode() const { return this->preload_mode_; }
|
||||
|
||||
/// @brief Set input PCM width: 2 = 16-bit, 3 = 24-bit, 4 = 32-bit (truncated to 24-bit on the wire).
|
||||
/// Must be called before write() if input width changes from the default (16-bit). Triggers a
|
||||
/// channel-status rebuild to reflect the new word length.
|
||||
void set_bytes_per_sample(uint8_t bytes_per_sample);
|
||||
|
||||
/// @brief Get the configured input PCM width in bytes per sample
|
||||
uint8_t get_bytes_per_sample() const { return this->bytes_per_sample_; }
|
||||
|
||||
/// @brief Convert PCM audio data to SPDIF BMC encoded data
|
||||
/// @param src Source PCM audio data (16-bit stereo)
|
||||
/// @param src Source PCM audio data (stereo, width matches set_bytes_per_sample)
|
||||
/// @param size Size of source data in bytes
|
||||
/// @param ticks_to_wait Timeout for blocking writes
|
||||
/// @param blocks_sent Optional pointer to receive the number of complete SPDIF blocks sent
|
||||
@@ -74,17 +80,6 @@ class SPDIFEncoder {
|
||||
esp_err_t write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent = nullptr,
|
||||
size_t *bytes_consumed = nullptr);
|
||||
|
||||
/// @brief Get the number of PCM bytes currently pending in the partial block buffer
|
||||
/// @return Number of pending PCM bytes (0 to SPDIF_PCM_BYTES_PER_BLOCK - 1)
|
||||
size_t get_pending_pcm_bytes() const;
|
||||
|
||||
/// @brief Get the number of PCM frames currently pending in the partial block buffer
|
||||
/// @return Number of pending PCM frames (0 to SPDIF_BLOCK_SAMPLES - 1)
|
||||
uint32_t get_pending_frames() const { return this->get_pending_pcm_bytes() / 4; }
|
||||
|
||||
/// @brief Check if there is a partial block pending
|
||||
bool has_pending_data() const { return this->spdif_block_ptr_ != this->spdif_block_buf_.get(); }
|
||||
|
||||
/// @brief Emit one complete SPDIF block: pad any pending partial block with silence and send,
|
||||
/// or send a full silence block if nothing is pending. Always produces exactly one block on success.
|
||||
/// @param ticks_to_wait Timeout for blocking writes
|
||||
@@ -95,7 +90,7 @@ class SPDIFEncoder {
|
||||
void reset();
|
||||
|
||||
/// @brief Set the sample rate for Channel Status Block encoding
|
||||
/// @param sample_rate Sample rate in Hz (e.g., 44100, 48000, 96000)
|
||||
/// @param sample_rate Sample rate in Hz (e.g., 44100, 48000)
|
||||
/// Call this before writing audio data to ensure correct channel status.
|
||||
void set_sample_rate(uint32_t sample_rate);
|
||||
|
||||
@@ -103,8 +98,19 @@ class SPDIFEncoder {
|
||||
uint32_t get_sample_rate() const { return this->sample_rate_; }
|
||||
|
||||
protected:
|
||||
/// @brief Encode a single 16-bit PCM sample into the current block position
|
||||
HOT void encode_sample_(const uint8_t *pcm_sample);
|
||||
/// @brief Encode a single stereo silence frame at the current block position.
|
||||
/// @note Used only by flush_with_silence_typed_ to pad; the hot write path inlines the
|
||||
/// encoding body directly into write_typed_ to keep block_ptr / frame_in_block_ in registers.
|
||||
template<uint8_t Bps> void encode_silence_frame_();
|
||||
|
||||
/// @brief Templated write loop. Called from the public write() via runtime dispatch on bytes_per_sample_.
|
||||
template<uint8_t Bps>
|
||||
HOT esp_err_t write_typed_(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent,
|
||||
size_t *bytes_consumed);
|
||||
|
||||
/// @brief Templated flush-with-silence. Pads the pending block with zeros at the configured width
|
||||
/// (or builds a full silence block when nothing is pending) and sends it. Always emits one block.
|
||||
template<uint8_t Bps> esp_err_t flush_with_silence_typed_(TickType_t ticks_to_wait);
|
||||
|
||||
/// @brief Send the completed block via the appropriate callback
|
||||
esp_err_t send_block_(TickType_t ticks_to_wait);
|
||||
@@ -112,15 +118,6 @@ class SPDIFEncoder {
|
||||
/// @brief Build the channel status block from current configuration
|
||||
void build_channel_status_();
|
||||
|
||||
/// @brief Get the channel status bit for a specific frame
|
||||
/// @param frame Frame number (0-191)
|
||||
/// @return The C bit value for this frame
|
||||
ESPHOME_ALWAYS_INLINE inline bool get_channel_status_bit_(uint8_t frame) const {
|
||||
// Channel status is 192 bits transmitted over 192 frames
|
||||
// Bit N is transmitted in frame N, LSB-first within each byte
|
||||
return (this->channel_status_[frame >> 3] >> (frame & 7)) & 1;
|
||||
}
|
||||
|
||||
// Member ordering optimized to minimize padding (largest alignment first)
|
||||
|
||||
// 4-byte aligned members (pointers and uint32_t)
|
||||
@@ -133,9 +130,13 @@ class SPDIFEncoder {
|
||||
uint32_t sample_rate_{48000}; // Sample rate for Channel Status Block encoding
|
||||
|
||||
// 1-byte aligned members (grouped together to avoid internal padding)
|
||||
uint8_t frame_in_block_{0}; // 0-191, tracks stereo frame position within block
|
||||
bool is_left_channel_{true}; // Alternates L/R for stereo samples
|
||||
bool preload_mode_{false}; // Whether to use preload callback vs write callback
|
||||
uint8_t bytes_per_sample_{2}; // Input PCM width: 2/3/4 (16/24/32-bit). 32-bit truncates to 24-bit on the wire.
|
||||
uint8_t frame_in_block_{0}; // 0-191, tracks stereo frame position within block
|
||||
bool preload_mode_{false}; // Whether to use preload callback vs write callback
|
||||
// True when spdif_block_buf_ currently holds a complete full-silence block valid for the active
|
||||
// channel status. A full silence block is deterministic for a given sample rate and word length,
|
||||
// so when this is set flush_with_silence() can re-send the buffer verbatim instead of re-encoding.
|
||||
bool block_buf_is_silence_block_{false};
|
||||
|
||||
// Channel Status Block (192 bits = 24 bytes, transmitted over 192 frames)
|
||||
// Placed last since std::array<uint8_t> has 1-byte alignment
|
||||
|
||||
@@ -319,7 +319,7 @@ void Inkplate::fill(Color color) {
|
||||
memset(this->partial_buffer_, fill, this->get_buffer_length_());
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time);
|
||||
ESP_LOGV(TAG, "Fill finished (%" PRIu32 "ms)", millis() - start_time);
|
||||
}
|
||||
|
||||
void Inkplate::display() {
|
||||
|
||||
@@ -506,13 +506,13 @@ async def _late_logger_init(config: ConfigType) -> None:
|
||||
def validate_printf(value):
|
||||
# https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python
|
||||
cfmt = r"""
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
(?:[-+0 #]{0,5}) # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:h|l|ll|w|I|I32|I64)? # size
|
||||
[cCdiouxXeEfgGaAnpsSZ] # type
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
(?:[-+0 #]{0,5}) # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size
|
||||
[cCdiouxXeEfgGaAnpsSZ] # type
|
||||
)
|
||||
""" # noqa
|
||||
matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE)
|
||||
|
||||
@@ -55,6 +55,7 @@ from .automation import layers_to_code, lvgl_update
|
||||
from .defines import (
|
||||
CONF_ALIGN_TO_LAMBDA_ID,
|
||||
LOGGER,
|
||||
add_lv_use,
|
||||
get_focused_widgets,
|
||||
get_lv_images_used,
|
||||
get_refreshed_widgets,
|
||||
@@ -71,6 +72,7 @@ from .keypads import KEYPADS_CONFIG, keypads_to_code
|
||||
from .lv_validation import lv_bool
|
||||
from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static
|
||||
from .schemas import (
|
||||
BASE_PROPS,
|
||||
DISP_BG_SCHEMA,
|
||||
FULL_STYLE_SCHEMA,
|
||||
STYLE_REMAP,
|
||||
@@ -100,6 +102,7 @@ from .widgets import (
|
||||
get_screen_active,
|
||||
set_obj_properties,
|
||||
)
|
||||
from .widgets.img import CONF_IMAGE
|
||||
|
||||
# Import only what we actually use directly in this file
|
||||
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
|
||||
@@ -433,6 +436,8 @@ async def to_code(configs):
|
||||
|
||||
# This must be done after all widgets are created
|
||||
styles_used = df.get_styles_used()
|
||||
if any(BASE_PROPS.get(x) is lvalid.lv_image for x in styles_used):
|
||||
add_lv_use(CONF_IMAGE)
|
||||
for use in df.get_lv_uses():
|
||||
df.add_define(f"LV_USE_{use.upper()}")
|
||||
cg.add_define(f"USE_LVGL_{use.upper()}")
|
||||
|
||||
@@ -9,13 +9,13 @@ CONF_IF_NAN = "if_nan"
|
||||
# noqa
|
||||
f_regex = re.compile(
|
||||
r"""
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:h|l|ll|w|I|I32|I64)? # size
|
||||
f # type
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size
|
||||
f # type
|
||||
)
|
||||
""",
|
||||
flags=re.VERBOSE,
|
||||
@@ -23,13 +23,13 @@ f_regex = re.compile(
|
||||
# noqa
|
||||
c_regex = re.compile(
|
||||
r"""
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:h|l|ll|w|I|I32|I64)? # size
|
||||
[cCdiouxXeEfgGaAnpsSZ] # type
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size
|
||||
[cCdiouxXeEfgGaAnpsSZ] # type
|
||||
)
|
||||
""",
|
||||
flags=re.VERBOSE,
|
||||
|
||||
@@ -572,7 +572,7 @@ void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) {
|
||||
auto key_idx = lv_buttonmatrix_get_selected_button(self->obj);
|
||||
if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE)
|
||||
return;
|
||||
if (self->key_map_.count(key_idx) != 0) {
|
||||
if (self->key_map_.contains(key_idx)) {
|
||||
self->send_key_(self->key_map_[key_idx]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,11 +74,11 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
lv_style_set_text_font(style, font->get_lv_font());
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
|
||||
#if LV_USE_IMAGE
|
||||
|
||||
#ifdef USE_IMAGE
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
#endif // LV_USE_IMAGE
|
||||
|
||||
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
@@ -93,7 +93,8 @@ inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) {
|
||||
inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) {
|
||||
::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc());
|
||||
}
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
|
||||
auto *dsc = static_cast<std::vector<lv_image_dsc_t *> *>(lv_obj_get_user_data(img));
|
||||
@@ -109,6 +110,7 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
|
||||
lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size());
|
||||
}
|
||||
#endif // USE_LVGL_ANIMIMG
|
||||
#endif // USE_IMAGE
|
||||
|
||||
#ifdef USE_LVGL_METER
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value);
|
||||
|
||||
@@ -9,6 +9,7 @@ from .defines import (
|
||||
CONF_THEME,
|
||||
LValidator,
|
||||
add_lv_use,
|
||||
get_styles_used,
|
||||
get_theme_widget_map,
|
||||
literal,
|
||||
)
|
||||
@@ -25,6 +26,7 @@ def has_style_props(config) -> bool:
|
||||
async def style_set(svar, style):
|
||||
for prop, validator in ALL_STYLES.items():
|
||||
if (value := style.get(prop)) is not None:
|
||||
get_styles_used().add(prop)
|
||||
if isinstance(validator, LValidator):
|
||||
value = await validator.process(value)
|
||||
if isinstance(value, list):
|
||||
|
||||
@@ -130,8 +130,8 @@ ClimateTraits AirConditioner::traits() {
|
||||
void AirConditioner::dump_config() {
|
||||
ESP_LOGCONFIG(Constants::TAG,
|
||||
"MideaDongle:\n"
|
||||
" [x] Period: %dms\n"
|
||||
" [x] Response timeout: %dms\n"
|
||||
" [x] Period: %" PRIu32 "ms\n"
|
||||
" [x] Response timeout: %" PRIu32 "ms\n"
|
||||
" [x] Request attempts: %d",
|
||||
this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts());
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, esp32, uart
|
||||
@@ -39,6 +41,8 @@ from .base_component import (
|
||||
CONF_WAKE_UP_PAGE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
@@ -55,6 +59,15 @@ NextionSetBrightnessAction = nextion_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
def _deprecated_dump_device_info(value):
|
||||
_LOGGER.warning(
|
||||
"'dump_device_info' is deprecated and will be removed in ESPHome 2026.11.0. "
|
||||
"Device info is now always logged at connection time. "
|
||||
"Please remove this option from your configuration."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _validate_tft_upload(config):
|
||||
has_tft_url = CONF_TFT_URL in config
|
||||
for conf_key in (
|
||||
@@ -81,7 +94,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=TimePeriod(milliseconds=255)),
|
||||
),
|
||||
cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean,
|
||||
# Deprecated — device info is now always logged. Remove before 2026.11.0.
|
||||
cv.Optional(CONF_DUMP_DEVICE_INFO): cv.All(
|
||||
cv.boolean, _deprecated_dump_device_info
|
||||
),
|
||||
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
@@ -277,9 +293,6 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
|
||||
|
||||
if config[CONF_DUMP_DEVICE_INFO]:
|
||||
cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO")
|
||||
|
||||
if config[CONF_EXIT_REPARSE_ON_START]:
|
||||
cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START")
|
||||
|
||||
|
||||
@@ -117,30 +117,41 @@ bool Nextion::check_connect_() {
|
||||
|
||||
ESP_LOGN(TAG, "connect: %s", response.c_str());
|
||||
|
||||
size_t start;
|
||||
// Parse comok response fields directly
|
||||
// Format: comok <touch>,<reserved>,<model>,<fw>,<mcu_code>,<serial>,<flash>
|
||||
size_t field_count = 0;
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
std::vector<std::string> connect_info;
|
||||
auto copy_field = [&](char *dst, size_t cap) {
|
||||
size_t len = (end == std::string::npos ? response.size() : end) - start;
|
||||
size_t n = len < cap ? len : cap;
|
||||
std::memcpy(dst, response.data() + start, n);
|
||||
dst[n] = '\0';
|
||||
};
|
||||
while ((start = response.find_first_not_of(',', end)) != std::string::npos) {
|
||||
end = response.find(',', start);
|
||||
connect_info.push_back(response.substr(start, end - start));
|
||||
switch (field_count) {
|
||||
case 2:
|
||||
copy_field(this->device_model_, this->NEXTION_MODEL_MAX);
|
||||
break;
|
||||
case 3:
|
||||
copy_field(this->firmware_version_, this->NEXTION_FW_MAX);
|
||||
break;
|
||||
case 5:
|
||||
copy_field(this->serial_number_, this->NEXTION_SERIAL_MAX);
|
||||
break;
|
||||
case 6:
|
||||
this->flash_size_ = static_cast<uint32_t>(std::strtoul(response.data() + start, nullptr, 10));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
++field_count;
|
||||
}
|
||||
|
||||
this->is_detected_ = (connect_info.size() == 7);
|
||||
this->is_detected_ = (field_count == 7);
|
||||
if (this->is_detected_) {
|
||||
ESP_LOGN(TAG, "Connect info: %zu", connect_info.size());
|
||||
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
this->device_model_ = connect_info[2];
|
||||
this->firmware_version_ = connect_info[3];
|
||||
this->serial_number_ = connect_info[5];
|
||||
this->flash_size_ = connect_info[6];
|
||||
#else // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
ESP_LOGI(TAG,
|
||||
" Device Model: %s\n"
|
||||
" FW Version: %s\n"
|
||||
" Serial Number: %s\n"
|
||||
" Flash Size: %s\n",
|
||||
connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str());
|
||||
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
ESP_LOGN(TAG, "Connect info: %zu fields", field_count);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str());
|
||||
}
|
||||
@@ -178,24 +189,26 @@ void Nextion::dump_config() {
|
||||
#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
ESP_LOGCONFIG(TAG, " Skip handshake: YES");
|
||||
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
if (this->is_setup()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Device Model: %s\n"
|
||||
" FW Version: %s\n"
|
||||
" Serial Number: %s\n"
|
||||
" Flash Size: %" PRIu32 " bytes",
|
||||
this->device_model_, this->firmware_version_, this->serial_number_, this->flash_size_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Device info: not yet detected");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Device Model: %s\n"
|
||||
" FW Version: %s\n"
|
||||
" Serial Number: %s\n"
|
||||
" Flash Size: %s\n"
|
||||
" Max queue age: %u ms\n"
|
||||
" Startup override: %u ms\n",
|
||||
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
|
||||
this->flash_size_.c_str(), this->max_q_age_ms_, this->startup_override_ms_);
|
||||
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
|
||||
ESP_LOGCONFIG(TAG, " Exit reparse: YES\n");
|
||||
" Exit reparse: YES\n"
|
||||
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Max queue age: %u ms\n"
|
||||
" Startup override: %u ms\n"
|
||||
" Wake On Touch: %s\n"
|
||||
" Touch Timeout: %" PRIu16,
|
||||
YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
|
||||
this->max_q_age_ms_, this->startup_override_ms_, YESNO(this->connection_state_.auto_wake_on_touch_),
|
||||
this->touch_sleep_timeout_);
|
||||
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
@@ -1610,12 +1610,15 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
nextion_writer_t writer_;
|
||||
optional<float> brightness_;
|
||||
|
||||
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
std::string device_model_;
|
||||
std::string firmware_version_;
|
||||
std::string serial_number_;
|
||||
std::string flash_size_;
|
||||
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
// Device info populated from comok response (fixed-size, no heap allocation).
|
||||
// Sizes derived from Nextion Upload Protocol documentation and observed hardware.
|
||||
static constexpr size_t NEXTION_MODEL_MAX = 24; ///< Max observed ~18 chars from product numbering rules
|
||||
static constexpr size_t NEXTION_FW_MAX = 7; ///< 'S' prefix + integer (e.g. 'S99' or `123`)
|
||||
static constexpr size_t NEXTION_SERIAL_MAX = 20; ///< Consistently 16 hex chars across all documented examples
|
||||
char device_model_[NEXTION_MODEL_MAX + 1]{};
|
||||
char firmware_version_[NEXTION_FW_MAX + 1]{};
|
||||
char serial_number_[NEXTION_SERIAL_MAX + 1]{};
|
||||
uint32_t flash_size_ = 0; ///< Flash size in bytes — plain integer, no string needed
|
||||
|
||||
void remove_front_no_sensors_();
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() {
|
||||
ESP_LOGE(TAG, "Cannot resolve running app partition at address 0x%" PRIX32, running_app_offset);
|
||||
return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE;
|
||||
}
|
||||
ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address,
|
||||
ESP_LOGD(TAG, "Copying running app from 0x%" PRIX32 " to 0x%" PRIX32 " (size: 0x%zX)", running_app_part->address,
|
||||
plan.copy_dest_part->address, running_app_size);
|
||||
err = esp_partition_copy(plan.copy_dest_part, 0, running_app_part, 0, running_app_size);
|
||||
if (err != ESP_OK) {
|
||||
@@ -261,7 +261,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() {
|
||||
ESP_LOGE(TAG, "Selected app partition not found after partition table update");
|
||||
return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting next boot partition to 0x%X", new_boot_partition->address);
|
||||
ESP_LOGD(TAG, "Setting next boot partition to 0x%" PRIX32, new_boot_partition->address);
|
||||
err = esp_ota_set_boot_partition(new_boot_partition);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (err=0x%X)", err);
|
||||
|
||||
@@ -150,7 +150,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) {
|
||||
edge_state.last_sent_edge_us_ = now;
|
||||
state.last_detected_edge_us_ = now;
|
||||
state.last_rising_edge_us_ = now;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
state.count_ += 1;
|
||||
}
|
||||
|
||||
// This ISR is bound to rising edges, so the pin is high
|
||||
@@ -173,7 +173,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) {
|
||||
} else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
pulse_state.latched_ = true;
|
||||
state.last_detected_edge_us_ = pulse_state.last_intr_;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
state.count_ += 1;
|
||||
}
|
||||
|
||||
// Due to order of operations this includes
|
||||
|
||||
@@ -78,10 +78,10 @@ void RemoteReceiverComponent::setup() {
|
||||
void RemoteReceiverComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Remote Receiver:\n"
|
||||
" Buffer Size: %u\n"
|
||||
" Tolerance: %u%s\n"
|
||||
" Filter out pulses shorter than: %u us\n"
|
||||
" Signal is done after %u us of no changes",
|
||||
" Buffer Size: %" PRIu32 "\n"
|
||||
" Tolerance: %" PRIu32 "%s\n"
|
||||
" Filter out pulses shorter than: %" PRIu32 " us\n"
|
||||
" Signal is done after %" PRIu32 " us of no changes",
|
||||
this->buffer_size_, this->tolerance_,
|
||||
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_,
|
||||
this->idle_us_);
|
||||
|
||||
@@ -25,7 +25,6 @@ from esphome.const import (
|
||||
CONF_TEMPERATURE_COMPENSATION,
|
||||
CONF_TIME_CONSTANT,
|
||||
CONF_VOC,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
@@ -77,7 +76,6 @@ def _gas_sensor(
|
||||
return sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@ from esphome.const import (
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
CONF_VOC,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
@@ -93,13 +92,11 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_VOC): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_NOX): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
|
||||
@@ -230,7 +230,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.5.0")
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.6.0")
|
||||
|
||||
cg.add_define("USE_SENDSPIN", True) # for MDNS
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) {
|
||||
LastPlayedServerPref pref{.server_id_hash = hash};
|
||||
bool ok = this->last_played_server_pref_.save(&pref);
|
||||
if (ok) {
|
||||
ESP_LOGD(TAG, "Persisted last played server hash: 0x%08X", hash);
|
||||
ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to persist last played server hash");
|
||||
}
|
||||
@@ -168,7 +168,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) {
|
||||
std::optional<uint32_t> SendspinHub::load_last_server_hash() {
|
||||
LastPlayedServerPref pref{};
|
||||
if (this->last_played_server_pref_.load(&pref)) {
|
||||
ESP_LOGI(TAG, "Loaded last played server hash: 0x%08X", pref.server_id_hash);
|
||||
ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash);
|
||||
return pref.server_id_hash;
|
||||
}
|
||||
return std::nullopt;
|
||||
|
||||
@@ -96,6 +96,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TEMPERATURE_DELTA,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
DEVICE_CLASS_UPTIME,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
@@ -174,6 +175,7 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TEMPERATURE_DELTA,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
DEVICE_CLASS_UPTIME,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
|
||||
@@ -15,7 +15,6 @@ from esphome.const import (
|
||||
CONF_STORE_BASELINE,
|
||||
CONF_TEMPERATURE_SOURCE,
|
||||
CONF_VOC,
|
||||
DEVICE_CLASS_AQI,
|
||||
ICON_RADIATOR,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
@@ -72,13 +71,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_VOC): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(VOC_SENSOR),
|
||||
cv.Optional(CONF_NOX): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(NOX_SENSOR),
|
||||
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
|
||||
|
||||
@@ -126,7 +126,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Else fall thru ...
|
||||
[[fallthrough]];
|
||||
}
|
||||
case STATE_CHECK_SMS:
|
||||
send_cmd_("AT+CMGL=\"ALL\"");
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace esphome::sound_level {
|
||||
|
||||
static const char *const TAG = "sound_level";
|
||||
|
||||
static const uint32_t AUDIO_BUFFER_DURATION_MS = 30;
|
||||
static const uint32_t MAX_FILL_DURATION_MS = 30;
|
||||
static const uint32_t RING_BUFFER_DURATION_MS = 120;
|
||||
|
||||
// Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2
|
||||
@@ -30,8 +30,7 @@ void SoundLevelComponent::dump_config() {
|
||||
void SoundLevelComponent::setup() {
|
||||
this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
|
||||
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
if (this->ring_buffer_.use_count() == 2) {
|
||||
// ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write
|
||||
if (temp_ring_buffer != nullptr) {
|
||||
temp_ring_buffer->write((void *) data.data(), data.size());
|
||||
}
|
||||
});
|
||||
@@ -81,10 +80,11 @@ void SoundLevelComponent::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy data from ring buffer into the transfer buffer - don't block to avoid slowing the main loop
|
||||
this->audio_buffer_->transfer_data_from_source(0);
|
||||
// Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop.
|
||||
// pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
|
||||
this->audio_source_->fill(0, false);
|
||||
|
||||
if (this->audio_buffer_->available() == 0) {
|
||||
if (this->audio_source_->available() == 0) {
|
||||
// No new audio available for processing
|
||||
return;
|
||||
}
|
||||
@@ -92,11 +92,11 @@ void SoundLevelComponent::loop() {
|
||||
const uint32_t samples_in_window =
|
||||
this->microphone_source_->get_audio_stream_info().ms_to_samples(this->measurement_duration_ms_);
|
||||
const uint32_t samples_available_to_process =
|
||||
this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_buffer_->available());
|
||||
this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_source_->available());
|
||||
const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process);
|
||||
|
||||
// MicrophoneSource always provides int16 samples due to Python codegen settings
|
||||
const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_buffer_->get_buffer_start());
|
||||
const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_source_->data());
|
||||
|
||||
// Process all the new audio samples
|
||||
for (uint32_t i = 0; i < samples_to_process; ++i) {
|
||||
@@ -115,9 +115,8 @@ void SoundLevelComponent::loop() {
|
||||
++this->sample_count_;
|
||||
}
|
||||
|
||||
// Remove the processed samples from ``audio_buffer_``
|
||||
this->audio_buffer_->decrease_buffer_length(
|
||||
this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process));
|
||||
// Remove the processed samples from ``audio_source_``
|
||||
this->audio_source_->consume(this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process));
|
||||
|
||||
if (this->sample_count_ == samples_in_window) {
|
||||
// Processed enough samples for the measurement window, compute and publish the sensor values
|
||||
@@ -158,36 +157,39 @@ void SoundLevelComponent::stop() {
|
||||
}
|
||||
|
||||
bool SoundLevelComponent::start_() {
|
||||
if (this->audio_buffer_ != nullptr) {
|
||||
if (this->audio_source_ != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allocate a transfer buffer
|
||||
this->audio_buffer_ = audio::AudioSourceTransferBuffer::create(
|
||||
this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS));
|
||||
if (this->audio_buffer_ == nullptr) {
|
||||
this->status_momentary_error("transfer_buffer", 15000);
|
||||
const auto &stream_info = this->microphone_source_->get_audio_stream_info();
|
||||
const size_t bytes_per_frame = stream_info.frames_to_bytes(1);
|
||||
|
||||
// Allocate a ring buffer for the microphone callback to write into. Round the size down to a multiple
|
||||
// of bytes_per_frame so the wrap boundary stays frame-aligned and avoids unnecessary single-frame splices.
|
||||
this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation
|
||||
const size_t ring_buffer_size =
|
||||
(stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame;
|
||||
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
|
||||
if (temp_ring_buffer == nullptr) {
|
||||
this->status_momentary_error("ring_buffer", 15000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it
|
||||
this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation
|
||||
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(
|
||||
this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS));
|
||||
if (temp_ring_buffer.use_count() == 0) {
|
||||
this->status_momentary_error("ring_buffer", 15000);
|
||||
this->stop_();
|
||||
// Zero-copy source that reads directly from the ring buffer's internal storage. Frame-aligned reads
|
||||
// ensure multi-channel frames are never split across the ring buffer's wrap boundary.
|
||||
this->audio_source_ = audio::RingBufferAudioSource::create(
|
||||
temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast<uint8_t>(bytes_per_frame));
|
||||
if (this->audio_source_ == nullptr) {
|
||||
this->status_momentary_error("audio_source", 15000);
|
||||
return false;
|
||||
} else {
|
||||
this->ring_buffer_ = temp_ring_buffer;
|
||||
this->audio_buffer_->set_source(temp_ring_buffer);
|
||||
}
|
||||
this->ring_buffer_ = temp_ring_buffer;
|
||||
|
||||
this->status_clear_error();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundLevelComponent::stop_() { this->audio_buffer_.reset(); }
|
||||
void SoundLevelComponent::stop_() { this->audio_source_.reset(); }
|
||||
|
||||
} // namespace esphome::sound_level
|
||||
|
||||
|
||||
@@ -36,11 +36,12 @@ class SoundLevelComponent : public Component {
|
||||
void stop();
|
||||
|
||||
protected:
|
||||
/// @brief Internal start command that, if necessary, allocates ``audio_buffer_`` and a ring buffer which
|
||||
/// ``audio_buffer_`` owns and ``ring_buffer_`` points to. Returns true if allocations were successful.
|
||||
/// @brief Internal start command that, if necessary, allocates a ring buffer and a zero-copy
|
||||
/// ``RingBufferAudioSource`` that reads directly from it. ``ring_buffer_`` weakly references the
|
||||
/// ring buffer owned by ``audio_source_``. Returns true if allocations were successful.
|
||||
bool start_();
|
||||
|
||||
/// @brief Internal stop command the deallocates ``audio_buffer_`` (which automatically deallocates its ring buffer)
|
||||
/// @brief Internal stop command that deallocates ``audio_source_`` (which releases its ring buffer)
|
||||
void stop_();
|
||||
|
||||
microphone::MicrophoneSource *microphone_source_{nullptr};
|
||||
@@ -48,7 +49,7 @@ class SoundLevelComponent : public Component {
|
||||
sensor::Sensor *peak_sensor_{nullptr};
|
||||
sensor::Sensor *rms_sensor_{nullptr};
|
||||
|
||||
std::unique_ptr<audio::AudioSourceTransferBuffer> audio_buffer_;
|
||||
std::unique_ptr<audio::RingBufferAudioSource> audio_source_;
|
||||
std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_;
|
||||
|
||||
int32_t squared_peak_{0};
|
||||
|
||||
@@ -16,7 +16,7 @@ GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-a
|
||||
|
||||
SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate,
|
||||
GPIOPin *cs_pin, bool release_device, bool write_only) {
|
||||
if (this->devices_.count(device) != 0) {
|
||||
if (this->devices_.contains(device)) {
|
||||
ESP_LOGE(TAG, "Device already registered");
|
||||
return this->devices_[device];
|
||||
}
|
||||
@@ -27,7 +27,7 @@ SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIB
|
||||
}
|
||||
|
||||
void SPIComponent::unregister_device(SPIClient *device) {
|
||||
if (this->devices_.count(device) == 0) {
|
||||
if (!this->devices_.contains(device)) {
|
||||
esph_log_e(TAG, "Device not registered");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome import final_validate as fv
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import (
|
||||
@@ -8,7 +9,7 @@ from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.const import CONF_HARDWARE_UART, CONF_ID
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
CONFLICTS_WITH = ["usb_host"]
|
||||
@@ -20,6 +21,13 @@ CONF_USB_PRODUCT_STR = "usb_product_str"
|
||||
CONF_USB_SERIAL_STR = "usb_serial_str"
|
||||
CONF_USB_VENDOR_ID = "usb_vendor_id"
|
||||
|
||||
# Components that provide a USB device class (CDC, HID, MSC, ...) on top of
|
||||
# tinyusb. Configuring `tinyusb:` without any of these triggers a 5s hang in
|
||||
# esp_tinyusb's driver install (descriptors_set fails with no class and no
|
||||
# user-provided full_speed_config), which trips the task watchdog before
|
||||
# loop() ever runs.
|
||||
_USB_CLASS_COMPONENTS = ("usb_cdc_acm",)
|
||||
|
||||
tinyusb_ns = cg.esphome_ns.namespace("tinyusb")
|
||||
TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component)
|
||||
|
||||
@@ -41,6 +49,29 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
full_config = fv.full_config.get()
|
||||
if not any(name in full_config for name in _USB_CLASS_COMPONENTS):
|
||||
raise cv.Invalid(
|
||||
"The 'tinyusb' component requires at least one USB class component"
|
||||
)
|
||||
# tinyusb owns the USB OTG peripheral. The logger's USB_CDC backend routes
|
||||
# the ROM console through that same peripheral, so the two cannot coexist.
|
||||
# (USB_SERIAL_JTAG is a separate peripheral and is fine alongside tinyusb.)
|
||||
logger_config = full_config.get("logger")
|
||||
if logger_config and logger_config.get(CONF_HARDWARE_UART) == "USB_CDC":
|
||||
raise cv.Invalid(
|
||||
"'tinyusb' cannot be used with 'logger.hardware_uart: USB_CDC' "
|
||||
"because both share the USB OTG peripheral. Set "
|
||||
"'logger.hardware_uart' to a hardware UART (e.g. UART0), or to "
|
||||
"USB_SERIAL_JTAG on variants that support it (ESP32-S3, ESP32-P4)"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -26,6 +26,21 @@ void TinyUSB::setup() {
|
||||
.string_count = SIZE,
|
||||
};
|
||||
|
||||
// Defense-in-depth: esp_tinyusb's tinyusb_descriptors_set() fails with
|
||||
// ESP_ERR_INVALID_ARG when no configuration descriptor is provided and
|
||||
// no class that has a built-in default (CDC/MSC/NCM) is compiled in. In
|
||||
// that case the internal task exits without notifying us, and
|
||||
// tinyusb_driver_install() blocks 5s on the notify-take -- long enough
|
||||
// to trip the task watchdog. Bail early so the rest of the device can
|
||||
// still boot.
|
||||
#if !(CFG_TUD_CDC > 0 || CFG_TUD_MSC > 0 || CFG_TUD_NCM > 0)
|
||||
if (this->tusb_cfg_.descriptor.full_speed_config == nullptr) {
|
||||
ESP_LOGE(TAG, "No USB class configured");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_);
|
||||
if (result != ESP_OK) {
|
||||
ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result));
|
||||
|
||||
@@ -72,7 +72,7 @@ void TotalDailyEnergy::schedule_midnight_reset_() {
|
||||
timeout_seconds = seconds_until_midnight + 1;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds);
|
||||
ESP_LOGD(TAG, "Scheduling midnight check in %" PRIu32 "s", timeout_seconds);
|
||||
this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND,
|
||||
[this]() { this->schedule_midnight_reset_(); });
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r
|
||||
if (this->swap_x_y_) {
|
||||
std::swap(x_raw, y_raw);
|
||||
}
|
||||
if (this->touches_.count(id) == 0) {
|
||||
if (!this->touches_.contains(id)) {
|
||||
tp.state = STATE_PRESSED;
|
||||
tp.id = id;
|
||||
} else {
|
||||
|
||||
@@ -684,8 +684,10 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType
|
||||
case 4:
|
||||
data.push_back(value >> 24);
|
||||
data.push_back(value >> 16);
|
||||
[[fallthrough]];
|
||||
case 2:
|
||||
data.push_back(value >> 8);
|
||||
[[fallthrough]];
|
||||
case 1:
|
||||
data.push_back(value >> 0);
|
||||
break;
|
||||
|
||||
@@ -135,7 +135,7 @@ void Tx20Component::decode_and_publish_() {
|
||||
}
|
||||
if (tx20_se == tx20_sb) {
|
||||
tx20_wind_direction = tx20_se;
|
||||
if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) {
|
||||
if (tx20_wind_direction < 16) {
|
||||
wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction];
|
||||
}
|
||||
ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction);
|
||||
@@ -164,7 +164,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) {
|
||||
}
|
||||
arg->buffer[arg->buffer_index] = 1;
|
||||
arg->start_time = now;
|
||||
arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
arg->buffer_index += 1;
|
||||
return;
|
||||
}
|
||||
const uint32_t delay = now - arg->start_time;
|
||||
@@ -195,7 +195,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) {
|
||||
}
|
||||
arg->spent_time += delay;
|
||||
arg->start_time = now;
|
||||
arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
arg->buffer_index += 1;
|
||||
}
|
||||
void IRAM_ATTR Tx20ComponentStore::reset() {
|
||||
tx20_available = false;
|
||||
|
||||
@@ -154,7 +154,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
}
|
||||
|
||||
// Log unknown device addresses
|
||||
if (!found && !this->unknown_devices_.count(device_address)) {
|
||||
if (!found && !this->unknown_devices_.contains(device_address)) {
|
||||
ESP_LOGI(TAG, "Received packet for unknown device address 0x%08" PRIX32 " ", device_address);
|
||||
this->unknown_devices_.insert(device_address);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_TIME_ID,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
DEVICE_CLASS_UPTIME,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_TIMER,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
@@ -33,9 +33,8 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||
).extend(cv.polling_component_schema("60s")),
|
||||
"timestamp": sensor.sensor_schema(
|
||||
UptimeTimestampSensor,
|
||||
icon=ICON_TIMER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
device_class=DEVICE_CLASS_UPTIME,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
.extend(
|
||||
|
||||
@@ -135,7 +135,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
|
||||
// Computed as ceil(buffer_size / 64) + 1 in Python codegen; defaults to 5 (256 / 64 + 1).
|
||||
static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = USB_UART_OUTPUT_CHUNK_COUNT;
|
||||
|
||||
USBUartChannel(uint8_t index, uint16_t buffer_size) : index_(index), input_buffer_(RingBuffer(buffer_size)) {}
|
||||
USBUartChannel(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
|
||||
void write_array(const uint8_t *data, size_t len) override;
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
|
||||
@@ -611,7 +611,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
|
||||
}
|
||||
|
||||
// Helper to get request detail parameter
|
||||
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
||||
[[maybe_unused]] static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
||||
return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
|
||||
}
|
||||
|
||||
@@ -2638,9 +2638,9 @@ bool WebServer::isRequestHandlerTrivial() const { return false; }
|
||||
|
||||
void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) {
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) {
|
||||
if (this->sorting_entitys_.contains(entity)) {
|
||||
root[ESPHOME_F("sorting_weight")] = this->sorting_entitys_[entity].weight;
|
||||
if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) {
|
||||
if (this->sorting_groups_.contains(this->sorting_entitys_[entity].group_id)) {
|
||||
root[ESPHOME_F("sorting_group")] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace {
|
||||
* - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK).
|
||||
* - HTTPD_SOCK_ERR_FAIL for other errors.
|
||||
*/
|
||||
int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
|
||||
[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
|
||||
if (buf == nullptr) {
|
||||
return HTTPD_SOCK_ERR_INVALID;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#";
|
||||
void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) {
|
||||
if (arg->d0.digital_read())
|
||||
return;
|
||||
arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
arg->count += 1;
|
||||
arg->value <<= 1;
|
||||
arg->last_bit_time = millis();
|
||||
arg->done = false;
|
||||
@@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) {
|
||||
void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) {
|
||||
if (arg->d1.digital_read())
|
||||
return;
|
||||
arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
arg->count += 1;
|
||||
arg->value = (arg->value << 1) | 1;
|
||||
arg->last_bit_time = millis();
|
||||
arg->done = false;
|
||||
|
||||
@@ -54,10 +54,18 @@ from esphome.const import (
|
||||
CONF_TTLS_PHASE_2,
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_USERNAME,
|
||||
CONF_WIFI,
|
||||
PLACEHOLDER_WIFI_SSID,
|
||||
Platform,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
CoroPriority,
|
||||
EsphomeError,
|
||||
HexInt,
|
||||
coroutine_with_priority,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -903,3 +911,45 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
"wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _placeholder_wifi_credentials(config: ConfigType) -> list[str]:
|
||||
"""Return human-readable locations where the dashboard's placeholder wifi
|
||||
values still appear. Empty list means no placeholders were found.
|
||||
"""
|
||||
placeholders: list[str] = []
|
||||
wifi_conf = config.get(CONF_WIFI)
|
||||
if not wifi_conf:
|
||||
return placeholders
|
||||
|
||||
for idx, network in enumerate(wifi_conf.get(CONF_NETWORKS, [])):
|
||||
ssid = network.get(CONF_SSID)
|
||||
if isinstance(ssid, str) and ssid == PLACEHOLDER_WIFI_SSID:
|
||||
placeholders.append(f"wifi.networks[{idx}].ssid")
|
||||
|
||||
ap_conf = wifi_conf.get(CONF_AP)
|
||||
if ap_conf:
|
||||
ap_ssid = ap_conf.get(CONF_SSID)
|
||||
if isinstance(ap_ssid, str) and ap_ssid == PLACEHOLDER_WIFI_SSID:
|
||||
placeholders.append("wifi.ap.ssid")
|
||||
|
||||
return placeholders
|
||||
|
||||
|
||||
def check_placeholder_credentials(config: ConfigType) -> None:
|
||||
"""Raise EsphomeError if any wifi credential is the dashboard placeholder.
|
||||
|
||||
Call only at compile time. NEVER from CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA,
|
||||
or any path reached by `esphome config`; device-builder relies on
|
||||
validation passing with the placeholders still in place.
|
||||
"""
|
||||
locations = _placeholder_wifi_credentials(config)
|
||||
if not locations:
|
||||
return
|
||||
formatted = ", ".join(locations)
|
||||
raise EsphomeError(
|
||||
f"wifi configuration still contains the dashboard placeholder value "
|
||||
f"'{PLACEHOLDER_WIFI_SSID}' at: {formatted}. "
|
||||
f"Open secrets.yaml and replace 'wifi_ssid' (and 'wifi_password') "
|
||||
f"with your real wifi credentials before flashing."
|
||||
)
|
||||
|
||||
@@ -50,6 +50,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@luar123", "@tomaszduda23"]
|
||||
|
||||
CONFLICTS_WITH = ["openthread"]
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_REPORT): cv.All(
|
||||
|
||||
@@ -117,15 +117,11 @@ def final_validate_esp32(config: ConfigType) -> ConfigType:
|
||||
if not CORE.is_esp32:
|
||||
return config
|
||||
if CONF_WIFI in fv.full_config.get():
|
||||
if config[CONF_ROUTER] and CONF_AP in fv.full_config.get()[CONF_WIFI]:
|
||||
raise cv.Invalid(
|
||||
"Only Zigbee End Device can be used together with a Wifi Access Point."
|
||||
)
|
||||
if CONF_AP in fv.full_config.get()[CONF_WIFI]:
|
||||
_LOGGER.warning(
|
||||
"Wifi Access Point might be unstable while Zigbee is active, use only as fallback."
|
||||
raise cv.Invalid(
|
||||
"A Wifi Access Point can not be used together with Zigbee."
|
||||
)
|
||||
elif config[CONF_ROUTER]:
|
||||
if config[CONF_ROUTER]:
|
||||
_LOGGER.warning(
|
||||
"The Zigbee Router might miss packets while Wifi is active and could destabilize "
|
||||
"your network. Use only if Wifi is off most of the time."
|
||||
|
||||
@@ -1367,6 +1367,7 @@ DEVICE_CLASS_TEMPERATURE = "temperature"
|
||||
DEVICE_CLASS_TEMPERATURE_DELTA = "temperature_delta"
|
||||
DEVICE_CLASS_TIMESTAMP = "timestamp"
|
||||
DEVICE_CLASS_UPDATE = "update"
|
||||
DEVICE_CLASS_UPTIME = "uptime"
|
||||
DEVICE_CLASS_VIBRATION = "vibration"
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts"
|
||||
@@ -1415,3 +1416,12 @@ ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic"
|
||||
# The corresponding constant exists in c++
|
||||
# when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds
|
||||
SCHEDULER_DONT_RUN = 4294967295
|
||||
|
||||
# Sentinel values written by the esphome-device-builder dashboard into
|
||||
# secrets.yaml on first boot so that !secret wifi_ssid / !secret wifi_password
|
||||
# references resolve cleanly through validation before the user has finished
|
||||
# the onboarding wizard. Compilation refuses if these reach the binary so that
|
||||
# a user who dismisses onboarding can't accidentally flash a device that will
|
||||
# never associate with their wifi.
|
||||
PLACEHOLDER_WIFI_SSID = "REPLACE_WITH_YOUR_WIFI_NETWORK"
|
||||
PLACEHOLDER_WIFI_PASSWORD = "REPLACE_WITH_YOUR_WIFI_PASSWORD" # noqa: S105
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
#define USE_MEDIA_SOURCE
|
||||
#define USE_NEXTION_COMMAND_SPACING
|
||||
#define USE_NEXTION_CONF_START_UP_PAGE
|
||||
#define USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
#define USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
|
||||
#define USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
#define USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
@@ -42,6 +43,9 @@ void __attribute__((noreturn)) arch_restart();
|
||||
inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
inline const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
// Bulk copy out of PROGMEM. PROGMEM is a no-op everywhere except ESP8266, so a
|
||||
// plain `std::memcpy` is correct and the fast path here.
|
||||
inline void progmem_memcpy(void *dst, const void *src, size_t len) { std::memcpy(dst, src, len); }
|
||||
#endif
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
+11
-55
@@ -154,41 +154,6 @@ class IDFComponent:
|
||||
self.path = self.source.download(self.get_sanitized_name(), force=force)
|
||||
|
||||
|
||||
def _sanitize_version(version: str) -> str:
|
||||
"""
|
||||
Sanitize a version string by removing common requirement prefixes or a leading v.
|
||||
|
||||
Args:
|
||||
version: Version string to clean.
|
||||
|
||||
Returns:
|
||||
Cleaned version string without common requirement symbols.
|
||||
"""
|
||||
version = version.strip()
|
||||
|
||||
prefixes = (
|
||||
"^",
|
||||
"~=",
|
||||
"~",
|
||||
">=",
|
||||
"<=",
|
||||
"==",
|
||||
"!=",
|
||||
">",
|
||||
"<",
|
||||
"=",
|
||||
"v",
|
||||
"V",
|
||||
)
|
||||
|
||||
for p in prefixes:
|
||||
if version.startswith(p):
|
||||
version = version[len(p) :]
|
||||
break
|
||||
|
||||
return version.strip()
|
||||
|
||||
|
||||
def _get_package_from_pio_registry(
|
||||
username: str | None, pkgname: str, requirements: str
|
||||
) -> tuple[str, str, str | None, str | None]:
|
||||
@@ -396,7 +361,8 @@ def _convert_library_to_component(library: Library) -> IDFComponent:
|
||||
|
||||
# Repository is provided directly
|
||||
if library.repository:
|
||||
# Parse repository URL to extract name and version
|
||||
# Parse repository URL: path becomes the component name, fragment
|
||||
# becomes the git ref stored on GitSource.
|
||||
split_result = urlsplit(library.repository)
|
||||
if not split_result.fragment.strip():
|
||||
raise ValueError(f"Missing ref in URL {library.repository}")
|
||||
@@ -405,8 +371,10 @@ def _convert_library_to_component(library: Library) -> IDFComponent:
|
||||
name = str(split_result.path).strip("/")
|
||||
name = name.removesuffix(".git")
|
||||
|
||||
# Sanitize version
|
||||
version = _sanitize_version(split_result.fragment)
|
||||
# IDF Component Manager only accepts "*", a 40-char commit hash, or
|
||||
# semver here. The actual git ref is preserved in GitSource.ref;
|
||||
# override_path makes this field cosmetic at build time.
|
||||
version = "*"
|
||||
repository = urlunsplit(split_result._replace(fragment=""))
|
||||
|
||||
source = GitSource(str(repository), split_result.fragment)
|
||||
@@ -619,9 +587,6 @@ def generate_idf_component_yml(component: IDFComponent) -> str:
|
||||
if description:
|
||||
data["description"] = description
|
||||
|
||||
# Do not use the version from library.json/library.properties; it may be incorrect.
|
||||
data["version"] = component.version
|
||||
|
||||
repository = component.data.get("repository", {}).get("url", None)
|
||||
if repository:
|
||||
data["repository"] = repository
|
||||
@@ -631,20 +596,11 @@ def generate_idf_component_yml(component: IDFComponent) -> str:
|
||||
if "dependencies" not in data:
|
||||
data["dependencies"] = {}
|
||||
|
||||
# Add this dependency to dependencies
|
||||
dep = {}
|
||||
dep["version"] = dependency.version
|
||||
|
||||
# Should use dependency.path as override path
|
||||
try:
|
||||
dep["override_path"] = str(dependency.path)
|
||||
except RuntimeError as e:
|
||||
# No local path: only a GitSource can substitute its URL.
|
||||
if not isinstance(dependency.source, GitSource):
|
||||
raise e
|
||||
dep["git"] = dependency.source.url
|
||||
|
||||
data["dependencies"][dependency.get_sanitized_name()] = dep
|
||||
# Every dependency goes through _generate_idf_component →
|
||||
# component.download() before this runs, so .path is always set.
|
||||
data["dependencies"][dependency.get_sanitized_name()] = {
|
||||
"override_path": str(dependency.path),
|
||||
}
|
||||
|
||||
return yaml_util.dump(data)
|
||||
|
||||
|
||||
@@ -26,16 +26,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
_SCRIPTS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def _str_to_lst_of_str(a: str) -> list[str]:
|
||||
def _str_to_lst_of_str(a: str | list[str]) -> list[str]:
|
||||
"""
|
||||
Convert a string to a list of string
|
||||
|
||||
Args:
|
||||
a: A string containing semicolon-separated values
|
||||
a: A string containing semicolon-separated values, or an already-split list
|
||||
|
||||
Returns:
|
||||
list of strings
|
||||
"""
|
||||
if isinstance(a, list):
|
||||
return a
|
||||
return list(f.strip() for f in a.split(";") if f.strip())
|
||||
|
||||
|
||||
@@ -67,10 +69,11 @@ ESPHOME_IDF_DEFAULT_FEATURES = _str_to_lst_of_str(
|
||||
)
|
||||
|
||||
ESPHOME_IDF_FRAMEWORK_MIRRORS = _str_to_lst_of_str(
|
||||
os.environ.get(
|
||||
"ESPHOME_IDF_FRAMEWORK_MIRRORS",
|
||||
"https://github.com/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip;https://github.com/espressif/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.zip",
|
||||
)
|
||||
os.environ.get("ESPHOME_IDF_FRAMEWORK_MIRRORS")
|
||||
or [
|
||||
"https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz",
|
||||
"https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.tar.xz",
|
||||
]
|
||||
)
|
||||
|
||||
ESP_IDF_CONSTRAINTS_MIRRORS = _str_to_lst_of_str(
|
||||
|
||||
@@ -94,9 +94,10 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
_LOGGER.debug("Skipping size summary: %s", e)
|
||||
return
|
||||
|
||||
dram = data.get("memory_types", {}).get("DRAM") or {}
|
||||
ram_used = dram.get("used")
|
||||
ram_total = dram.get("size")
|
||||
memory_types = data.get("memory_types", {})
|
||||
ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {}
|
||||
ram_used = ram_region.get("used")
|
||||
ram_total = ram_region.get("size")
|
||||
if ram_total and ram_used is not None:
|
||||
print(f"RAM: {_format_bar(ram_used, ram_total)}")
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ dependencies:
|
||||
esphome/micro-flac:
|
||||
version: 0.2.0
|
||||
esphome/micro-mp3:
|
||||
version: 0.2.0
|
||||
version: 0.2.1
|
||||
esphome/micro-opus:
|
||||
version: 0.4.1
|
||||
esphome/micro-wav:
|
||||
@@ -36,7 +36,7 @@ dependencies:
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
espressif/esp_hosted:
|
||||
version: 2.12.6
|
||||
version: 2.12.7
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
zorxx/multipart-parser:
|
||||
@@ -100,6 +100,6 @@ dependencies:
|
||||
esp32async/asynctcp:
|
||||
version: 3.4.91
|
||||
sendspin/sendspin-cpp:
|
||||
version: 0.5.0
|
||||
version: 0.6.0
|
||||
lvgl/lvgl:
|
||||
version: 9.5.0
|
||||
|
||||
+12
-1
@@ -273,10 +273,21 @@ class StorageJSON:
|
||||
"""
|
||||
CORE.name = self.name
|
||||
CORE.build_path = self.build_path
|
||||
target_platform = self.core_platform or self.target_platform.lower()
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: self.core_platform or self.target_platform.lower(),
|
||||
KEY_TARGET_PLATFORM: target_platform,
|
||||
KEY_TARGET_FRAMEWORK: self.framework,
|
||||
}
|
||||
# The compile pipeline populates CORE.data[KEY_ESP32] when esp32's
|
||||
# validator runs; on the cache fast path that validator is skipped,
|
||||
# so populate the variant upload_using_esptool reads via
|
||||
# esp32.get_esp32_variant(). target_platform on disk is the variant
|
||||
# (e.g. "ESP32S3"); core_platform is the family (e.g. "esp32").
|
||||
if target_platform == const.PLATFORM_ESP32:
|
||||
from esphome.components.esp32.const import KEY_ESP32
|
||||
from esphome.const import KEY_VARIANT
|
||||
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: self.target_platform}
|
||||
|
||||
def __eq__(self, o) -> bool:
|
||||
return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict()
|
||||
|
||||
@@ -87,6 +87,21 @@ def replace_file_content(text, pattern, repl):
|
||||
|
||||
|
||||
def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
|
||||
"""Return True when the build tree must be wiped before reuse.
|
||||
|
||||
Predicate is True when *old* is missing (first build),
|
||||
``src_version`` differs, ``build_path`` differs, or a previously
|
||||
loaded integration was removed in *new*. Adding integrations or
|
||||
changing unrelated fields (friendly name, esphome version, etc.)
|
||||
does not trigger a clean.
|
||||
|
||||
Used by esphome-device-builder (esphome/device-builder) to gate
|
||||
its remote-build artifact materialiser so a local → remote → local
|
||||
cycle preserves PlatformIO's local object cache instead of wiping
|
||||
it on every cycle. The signature, semantics, and ``None`` handling
|
||||
for *old* are part of the public contract; keep them stable so the
|
||||
offloader's wipe decision tracks core's.
|
||||
"""
|
||||
if old is None:
|
||||
return True
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager, suppress
|
||||
from dataclasses import dataclass, field
|
||||
import functools
|
||||
import inspect
|
||||
from io import BytesIO, TextIOBase, TextIOWrapper
|
||||
@@ -233,6 +234,130 @@ class IncludeFile:
|
||||
return has_substitution_or_expression(str(self.file))
|
||||
|
||||
|
||||
def force_load_include_files(
|
||||
obj: Any,
|
||||
*,
|
||||
warn_on_unresolved: bool = True,
|
||||
_seen: set[int] | None = None,
|
||||
) -> None:
|
||||
"""Recursively resolve any deferred ``IncludeFile`` instances in a YAML tree.
|
||||
|
||||
Nested ``!include`` returns a deferred ``IncludeFile`` that is only resolved
|
||||
later (substitution / packages pass). Callers that need every referenced
|
||||
file to actually load — bundle discovery, on-device YAML recovery — invoke
|
||||
this while a :func:`track_yaml_loads` listener is active so the underlying
|
||||
loader fires and records every reachable file.
|
||||
|
||||
``IncludeFile`` instances whose path contains unresolved substitution
|
||||
variables cannot be loaded. By default a warning is logged for each one;
|
||||
pass ``warn_on_unresolved=False`` (used by discovery paths that run on a
|
||||
fresh re-parse where substitutions haven't been applied yet) to demote it
|
||||
to a debug log.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
|
||||
if isinstance(obj, IncludeFile):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
if obj.has_unresolved_expressions():
|
||||
log = _LOGGER.warning if warn_on_unresolved else _LOGGER.debug
|
||||
log(
|
||||
"Cannot resolve !include %s (referenced from %s) with substitutions in path",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
)
|
||||
return
|
||||
try:
|
||||
loaded = obj.load()
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"Failed to load !include %s (referenced from %s): %s",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
err,
|
||||
)
|
||||
return
|
||||
force_load_include_files(
|
||||
loaded, warn_on_unresolved=warn_on_unresolved, _seen=_seen
|
||||
)
|
||||
elif isinstance(obj, dict):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for value in obj.values():
|
||||
force_load_include_files(
|
||||
value, warn_on_unresolved=warn_on_unresolved, _seen=_seen
|
||||
)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for item in obj:
|
||||
force_load_include_files(
|
||||
item, warn_on_unresolved=warn_on_unresolved, _seen=_seen
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DiscoveredYamlFiles:
|
||||
"""Result of :func:`discover_user_yaml_files`.
|
||||
|
||||
``files`` contains every resolved path the YAML loader touched while we
|
||||
were re-parsing the user's config; ``secrets`` is the subset whose
|
||||
*un-resolved* filename matched :data:`esphome.const.SECRETS_FILES` (so
|
||||
a ``secrets.yaml`` symlinked to a differently-named target is still
|
||||
flagged as secrets).
|
||||
"""
|
||||
|
||||
files: list[Path] = field(default_factory=list)
|
||||
secrets: set[Path] = field(default_factory=set)
|
||||
|
||||
|
||||
def discover_user_yaml_files(config_path: Path) -> DiscoveredYamlFiles:
|
||||
"""Fresh-re-parse ``config_path`` and report every file the YAML loader
|
||||
pulled in, plus which of them came in under a secrets filename.
|
||||
|
||||
Does NOT run schema validation, substitutions, or package resolution — so
|
||||
component-internal YAML loaded by validators (LVGL helpers, dashboard
|
||||
imports, etc.) is *not* captured. Deferred ``!include`` references whose
|
||||
paths don't depend on substitutions are force-loaded here so they're
|
||||
captured too.
|
||||
|
||||
Must run on a fresh parse because :meth:`IncludeFile.load` caches its
|
||||
result; on an already-resolved tree :meth:`load` returns without invoking
|
||||
the loader and the listener would not fire for the referenced files.
|
||||
"""
|
||||
from esphome.const import SECRETS_FILES
|
||||
|
||||
secrets: set[Path] = set()
|
||||
|
||||
def _capture_secret(fname: Path) -> None:
|
||||
if Path(fname).name in SECRETS_FILES:
|
||||
secrets.add(Path(fname).resolve())
|
||||
|
||||
with track_yaml_loads() as loaded:
|
||||
_load_listeners.append(_capture_secret)
|
||||
try:
|
||||
try:
|
||||
data = load_yaml(config_path)
|
||||
except EsphomeError:
|
||||
return DiscoveredYamlFiles(list(loaded), secrets)
|
||||
force_load_include_files(data, warn_on_unresolved=False)
|
||||
finally:
|
||||
_load_listeners.remove(_capture_secret)
|
||||
|
||||
# Deduplicate while preserving first-seen order.
|
||||
seen: set[Path] = set()
|
||||
unique: list[Path] = []
|
||||
for path in loaded:
|
||||
if path not in seen:
|
||||
seen.add(path)
|
||||
unique.append(path)
|
||||
return DiscoveredYamlFiles(unique, secrets)
|
||||
|
||||
|
||||
def _add_data_ref(fn):
|
||||
@functools.wraps(fn)
|
||||
def wrapped(loader, node):
|
||||
|
||||
+4
-4
@@ -12,19 +12,19 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.3
|
||||
esphome-dashboard==20260425.0
|
||||
aioesphomeapi==45.0.0
|
||||
zeroconf==0.148.0
|
||||
aioesphomeapi==45.0.4
|
||||
zeroconf==0.149.12
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==12.2.0
|
||||
resvg-py==0.3.1
|
||||
resvg-py==0.3.2
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
smpclient==6.0.0
|
||||
requests==2.34.1
|
||||
requests==2.34.2
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.3.2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.12 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.13 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
@@ -16,7 +16,7 @@ hypothesis==6.92.1
|
||||
# CodSpeed benchmarks under tests/benchmarks/python/
|
||||
# (skipped via pytest.importorskip when missing -- only required for the
|
||||
# benchmarks job in .github/workflows/ci.yml)
|
||||
pytest-codspeed==5.0.1
|
||||
pytest-codspeed==5.0.2
|
||||
|
||||
# Used by the import-time regression check (.github/workflows/ci.yml → import-time job)
|
||||
importtime-waterfall==1.0.0
|
||||
|
||||
+60
-19
@@ -1062,22 +1062,42 @@ def main() -> None:
|
||||
parser.add_argument(
|
||||
"-b", "--branch", help="Branch to compare changed files against"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force-all",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Force every job to run regardless of what changed. Used by CI "
|
||||
"when the ci-run-all label is applied to a PR (escape hatch for "
|
||||
"changes that need full-matrix validation but don't touch enough "
|
||||
"files to trigger it organically)."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine what should run
|
||||
integration_run_all, integration_test_files = determine_integration_tests(
|
||||
args.branch
|
||||
)
|
||||
if args.force_all:
|
||||
integration_run_all, integration_test_files = True, []
|
||||
run_clang_tidy = True
|
||||
run_clang_format = True
|
||||
run_python_linters = True
|
||||
run_import_time = True
|
||||
run_device_builder = True
|
||||
native_idf_components = sorted(NATIVE_IDF_TEST_COMPONENTS)
|
||||
run_native_idf = True
|
||||
else:
|
||||
integration_run_all, integration_test_files = determine_integration_tests(
|
||||
args.branch
|
||||
)
|
||||
run_clang_tidy = should_run_clang_tidy(args.branch)
|
||||
run_clang_format = should_run_clang_format(args.branch)
|
||||
run_python_linters = should_run_python_linters(args.branch)
|
||||
run_import_time = should_run_import_time(args.branch)
|
||||
run_device_builder = should_run_device_builder(args.branch)
|
||||
native_idf_components = native_idf_components_to_test(args.branch)
|
||||
run_native_idf = bool(native_idf_components)
|
||||
run_integration, integration_test_buckets = _compute_integration_test_buckets(
|
||||
integration_run_all, integration_test_files
|
||||
)
|
||||
run_clang_tidy = should_run_clang_tidy(args.branch)
|
||||
run_clang_format = should_run_clang_format(args.branch)
|
||||
run_python_linters = should_run_python_linters(args.branch)
|
||||
run_import_time = should_run_import_time(args.branch)
|
||||
run_device_builder = should_run_device_builder(args.branch)
|
||||
native_idf_components = native_idf_components_to_test(args.branch)
|
||||
run_native_idf = bool(native_idf_components)
|
||||
changed_cpp_file_count = count_changed_cpp_files(args.branch)
|
||||
|
||||
# Get changed components
|
||||
@@ -1106,11 +1126,27 @@ def main() -> None:
|
||||
changed_components = changed_components_result
|
||||
is_core_change = False
|
||||
|
||||
# Filter to only components that have test files
|
||||
# Components without tests shouldn't generate CI test jobs
|
||||
changed_components_with_tests = [
|
||||
component for component in changed_components if _component_has_tests(component)
|
||||
]
|
||||
if args.force_all:
|
||||
# Force every component with tests into the CI matrix. Each disk entry
|
||||
# under tests/components/<name> is treated as a component; filtered
|
||||
# below by _component_has_tests so components without YAML tests are
|
||||
# still excluded.
|
||||
tests_root = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH
|
||||
all_components = sorted(d.name for d in tests_root.iterdir() if d.is_dir())
|
||||
changed_components_with_tests = [
|
||||
component for component in all_components if _component_has_tests(component)
|
||||
]
|
||||
# Treat as a core change so downstream logic (clang-tidy full scan,
|
||||
# dep expansion) sees the same world as when esphome/core/ changes.
|
||||
is_core_change = True
|
||||
else:
|
||||
# Filter to only components that have test files
|
||||
# Components without tests shouldn't generate CI test jobs
|
||||
changed_components_with_tests = [
|
||||
component
|
||||
for component in changed_components
|
||||
if _component_has_tests(component)
|
||||
]
|
||||
|
||||
# Get directly changed components with tests (for isolated testing)
|
||||
# These will be tested WITHOUT --testing-mode in CI to enable full validation
|
||||
@@ -1143,8 +1179,10 @@ def main() -> None:
|
||||
memory_impact = detect_memory_impact_config(args.branch)
|
||||
|
||||
# Determine clang-tidy mode based on actual files that will be checked
|
||||
is_full_scan = False
|
||||
if run_clang_tidy:
|
||||
# Full scan needed if: hash changed OR core files changed
|
||||
# (is_core_change is forced True under --force-all)
|
||||
is_full_scan = _is_clang_tidy_full_scan() or is_core_change
|
||||
|
||||
if is_full_scan:
|
||||
@@ -1177,10 +1215,12 @@ def main() -> None:
|
||||
|
||||
# Build output
|
||||
# Determine which C++ unit tests to run
|
||||
cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch)
|
||||
|
||||
# Determine if benchmarks should run
|
||||
run_benchmarks = should_run_benchmarks(args.branch)
|
||||
if args.force_all:
|
||||
cpp_run_all, cpp_components = True, []
|
||||
run_benchmarks = True
|
||||
else:
|
||||
cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch)
|
||||
run_benchmarks = should_run_benchmarks(args.branch)
|
||||
|
||||
# Split components into batches for CI testing
|
||||
# This intelligently groups components with similar bus configurations
|
||||
@@ -1219,6 +1259,7 @@ def main() -> None:
|
||||
"integration_test_buckets": integration_test_buckets,
|
||||
"clang_tidy": run_clang_tidy,
|
||||
"clang_tidy_mode": clang_tidy_mode,
|
||||
"clang_tidy_full_scan": is_full_scan,
|
||||
"clang_format": run_clang_format,
|
||||
"python_linters": run_python_linters,
|
||||
"import_time": run_import_time,
|
||||
|
||||
@@ -29,12 +29,12 @@ esp32_ble_tracker:
|
||||
- service_uuid: ABCD
|
||||
then:
|
||||
- lambda: !lambda |-
|
||||
ESP_LOGD("main", "Length of service data is %i", x.size());
|
||||
ESP_LOGD("main", "Length of service data is %zu", x.size());
|
||||
on_ble_manufacturer_data_advertise:
|
||||
- manufacturer_id: ABCD
|
||||
then:
|
||||
- lambda: !lambda |-
|
||||
ESP_LOGD("main", "Length of manufacturer data is %i", x.size());
|
||||
ESP_LOGD("main", "Length of manufacturer data is %zu", x.size());
|
||||
on_scan_end:
|
||||
- then:
|
||||
- lambda: |-
|
||||
|
||||
-11
@@ -1,13 +1,3 @@
|
||||
substitutions:
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_lrclk_pin: GPIO26
|
||||
i2s_mclk_pin: GPIO25
|
||||
i2s_dout_pin: GPIO12
|
||||
spdif_data_pin: GPIO4
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
i2s_audio:
|
||||
- id: i2s_output
|
||||
|
||||
@@ -20,6 +10,5 @@ speaker:
|
||||
use_apll: true
|
||||
timeout: 2s
|
||||
sample_rate: 48000
|
||||
bits_per_sample: 16bit
|
||||
channel: stereo
|
||||
i2s_mode: primary
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_lrclk_pin: GPIO26
|
||||
i2s_mclk_pin: GPIO25
|
||||
i2s_dout_pin: GPIO12
|
||||
spdif_data_pin: GPIO4
|
||||
|
||||
<<: !include common-spdif_mode.yaml
|
||||
@@ -123,7 +123,7 @@ select:
|
||||
- lambda: |-
|
||||
id(uart_bus).flush();
|
||||
uint32_t new_baud_rate = stoi(x);
|
||||
ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_bus).get_baud_rate(), new_baud_rate);
|
||||
ESP_LOGD("change_baud_rate", "Changing baud rate from %" PRIu32 " to %" PRIu32, id(uart_bus).get_baud_rate(), new_baud_rate);
|
||||
if (id(uart_bus).get_baud_rate() != new_baud_rate) {
|
||||
id(uart_bus).set_baud_rate(new_baud_rate);
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
|
||||
@@ -660,13 +660,13 @@ lvgl:
|
||||
on_release:
|
||||
logger.log:
|
||||
format: Button released at %d/%d
|
||||
args: [point.x, point.y]
|
||||
args: ['(int) point.x', '(int) point.y']
|
||||
on_long_press_repeat:
|
||||
logger.log: Button clicked
|
||||
on_pressing:
|
||||
logger.log:
|
||||
format: Button pressing at %d/%d
|
||||
args: [point.x, point.y]
|
||||
args: ['(int) point.x', '(int) point.y']
|
||||
on_press_lost:
|
||||
logger.log: Button press lost
|
||||
on_single_click:
|
||||
@@ -944,7 +944,7 @@ lvgl:
|
||||
on_release:
|
||||
logger.log:
|
||||
format: Slider released at %d/%d with value %.0f
|
||||
args: [point.x, point.y, x]
|
||||
args: ['(int) point.x', '(int) point.y', x]
|
||||
- button:
|
||||
styles: spin_button
|
||||
id: spin_up
|
||||
|
||||
@@ -21,7 +21,7 @@ modbus_server:
|
||||
read_lambda: |-
|
||||
return 31;
|
||||
write_lambda: |-
|
||||
printf("address=%d, value=%d", x);
|
||||
printf("address=%d, value=%" PRId32 "\n", (int) address, x);
|
||||
return true;
|
||||
- id: modbus_server4
|
||||
modbus_id: mod_bus2
|
||||
|
||||
@@ -64,7 +64,7 @@ mqtt:
|
||||
topic: some/topic
|
||||
payload: Good-bye
|
||||
- lambda: |-
|
||||
ESP_LOGD("MQTT", "Disconnect reason %d", reason);
|
||||
ESP_LOGD("MQTT", "Disconnect reason %d", (int) reason);
|
||||
publish_nan_as_none: false
|
||||
|
||||
binary_sensor:
|
||||
|
||||
@@ -276,7 +276,6 @@ display:
|
||||
auto_wake_on_touch: true
|
||||
brightness: 80%
|
||||
command_spacing: 5ms
|
||||
dump_device_info: true
|
||||
exit_reparse_on_start: true
|
||||
lambda: |-
|
||||
ESP_LOGD("display","Display is being tested!");
|
||||
@@ -299,7 +298,7 @@ display:
|
||||
- lambda: |-
|
||||
// key: StringRef, value: int32_t
|
||||
if (key == "temperature_raw") {
|
||||
ESP_LOGD("nextion.custom", "%s=%d", key.c_str(), value);
|
||||
ESP_LOGD("nextion.custom", "%s=%" PRId32, key.c_str(), value);
|
||||
}
|
||||
on_custom_binary_sensor:
|
||||
then:
|
||||
|
||||
@@ -12,7 +12,7 @@ on_brennenstuhl:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "on_brennenstuhl: %u"
|
||||
args: ["x.code"]
|
||||
args: ["(unsigned) x.code"]
|
||||
on_aeha:
|
||||
then:
|
||||
- logger.log:
|
||||
|
||||
@@ -49,7 +49,7 @@ script:
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "ints=%d floats=%f bools=%d strings=%s",
|
||||
ints[0], floats[0], bools[0], strings[0].c_str());
|
||||
ints[0], floats[0], (int) bools[0], strings[0].c_str());
|
||||
- id: my_script_with_params
|
||||
parameters:
|
||||
prefix: string
|
||||
|
||||
@@ -6,3 +6,8 @@ tinyusb:
|
||||
usb_product_str: ESPHomeTestProduct
|
||||
usb_serial_str: ESPHomeTestSerialNumber
|
||||
usb_vendor_id: 0x2345
|
||||
|
||||
# tinyusb requires at least one USB class companion; usb_cdc_acm satisfies that.
|
||||
usb_cdc_acm:
|
||||
interfaces:
|
||||
- id: tinyusb_test_cdc
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared
|
||||
# USB OTG peripheral; route the logger to UART0 so the fixture builds.
|
||||
logger:
|
||||
hardware_uart: UART0
|
||||
|
||||
@@ -11,7 +11,7 @@ udp:
|
||||
- "10.0.0.255"
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received %d bytes"
|
||||
format: "Received %zu bytes"
|
||||
args: [data.size()]
|
||||
- udp.write:
|
||||
id: my_udp
|
||||
|
||||
@@ -4,7 +4,7 @@ udp:
|
||||
addresses: ["239.0.60.53"]
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received %d bytes"
|
||||
format: "Received %zu bytes"
|
||||
args: [data.size()]
|
||||
- udp.write:
|
||||
id: my_udp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user