mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 09:08:41 +00:00
Merge branch 'dev' into lint-no-powf-core
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6
|
||||
b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Developer breaking change (an API change that could break external components)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change)
|
||||
- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api)
|
||||
- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
- [ ] Other
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ module.exports = {
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'developer-breaking-change',
|
||||
'undocumented-api-change',
|
||||
'code-quality',
|
||||
'deprecated-component'
|
||||
],
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
const fs = require('fs');
|
||||
const { DOCS_PR_PATTERNS } = require('./constants');
|
||||
const {
|
||||
COMPONENT_REGEX,
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
} = require('../detect-tags');
|
||||
|
||||
// Strategy: Merge branch detection
|
||||
async function detectMergeBranch(context) {
|
||||
@@ -20,15 +27,13 @@ async function detectMergeBranch(context) {
|
||||
// Strategy: Component and platform labeling
|
||||
async function detectComponentPlatforms(changedFiles, apiData) {
|
||||
const labels = new Set();
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
labels.add(`component: ${componentMatch[1]}`);
|
||||
}
|
||||
for (const comp of detectComponents(changedFiles)) {
|
||||
labels.add(`component: ${comp}`);
|
||||
}
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
labels.add(`platform: ${platformMatch[1]}`);
|
||||
@@ -90,15 +95,9 @@ async function detectNewPlatforms(prFiles, apiData) {
|
||||
// Strategy: Core files detection
|
||||
async function detectCoreChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
if (hasCoreChanges(changedFiles)) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
@@ -131,29 +130,18 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
|
||||
// Strategy: Dashboard changes
|
||||
async function detectDashboardChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
if (hasDashboardChanges(changedFiles)) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
async function detectGitHubActionsChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
if (hasGitHubActionsChanges(changedFiles)) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
@@ -238,6 +226,7 @@ async function detectPRTemplateCheckboxes(context) {
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||
{ pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
@@ -258,7 +247,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Compile regex once for better performance
|
||||
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const componentFileRegex = COMPONENT_REGEX;
|
||||
|
||||
// Get files that are modified or added in components directory
|
||||
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Shared tag detection from changed file paths.
|
||||
* Used by pr-title-check and auto-label-pr workflows.
|
||||
*/
|
||||
|
||||
const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//;
|
||||
|
||||
/**
|
||||
* Detect component names from changed files.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {Set<string>} Set of component names
|
||||
*/
|
||||
function detectComponents(changedFiles) {
|
||||
const components = new Set();
|
||||
for (const file of changedFiles) {
|
||||
const match = file.match(COMPONENT_REGEX);
|
||||
if (match) {
|
||||
components.add(match[1]);
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if core files were changed.
|
||||
* Core files are in esphome/core/ or top-level esphome/ directory.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasCoreChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if dashboard files were changed.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasDashboardChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if GitHub Actions files were changed.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasGitHubActionsChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
COMPONENT_REGEX,
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
};
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
+43
-14
@@ -686,7 +686,7 @@ jobs:
|
||||
ram_usage: ${{ steps.extract.outputs.ram_usage }}
|
||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||
cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
|
||||
skip: ${{ steps.check-script.outputs.skip }}
|
||||
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
|
||||
steps:
|
||||
- name: Check out target branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -705,10 +705,39 @@ jobs:
|
||||
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
|
||||
fi
|
||||
|
||||
# All remaining steps only run if script exists
|
||||
# Check if test files exist on the target branch for the requested
|
||||
# components and platform. When a PR adds new test files for a platform,
|
||||
# the target branch won't have them yet, so skip instead of failing.
|
||||
# This check must be done here (not in determine-jobs.py) because
|
||||
# determine-jobs runs on the PR branch and cannot see what the target
|
||||
# branch has.
|
||||
- name: Check for test files on target branch
|
||||
id: check-tests
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
run: |
|
||||
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
|
||||
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
|
||||
found=false
|
||||
for component in $(echo "$components" | jq -r '.[]'); do
|
||||
# Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml)
|
||||
for f in tests/components/${component}/test*.${platform}.yaml; do
|
||||
if [ -f "$f" ]; then
|
||||
found=true
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [ "$found" = false ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis"
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# All remaining steps only run if script and tests exist
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
run: |
|
||||
# Get the commit SHA of the target branch
|
||||
target_sha=$(git rev-parse HEAD)
|
||||
@@ -735,14 +764,14 @@ jobs:
|
||||
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
|
||||
- name: Cache status
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
|
||||
echo "✓ Cache hit! Using cached memory analysis results."
|
||||
@@ -752,21 +781,21 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Restore Python
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Build, compile, and analyze memory
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
id: build
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -800,7 +829,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
@@ -808,7 +837,7 @@ jobs:
|
||||
|
||||
- name: Extract memory usage for outputs
|
||||
id: extract
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ -f memory-analysis-target.json ]; then
|
||||
ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
|
||||
@@ -822,7 +851,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -886,7 +915,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -916,13 +945,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
name: PR Title Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
} = require('./.github/scripts/detect-tags.js');
|
||||
|
||||
const title = context.payload.pull_request.title;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
|
||||
// Skip bot PRs (e.g. dependabot) - they have their own title format
|
||||
if (author === 'dependabot[bot]') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block titles starting with "word:" or "word(scope):" patterns
|
||||
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
|
||||
if (commitStylePattern.test(title)) {
|
||||
core.setFailed(
|
||||
`PR title should not start with a "prefix:" style format.\n` +
|
||||
`Please use the format: [component] Brief description\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get changed files to detect tags
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
});
|
||||
const filenames = files.map(f => f.filename);
|
||||
|
||||
// Detect tags from changed files using shared logic
|
||||
const tags = new Set();
|
||||
|
||||
for (const comp of detectComponents(filenames)) {
|
||||
tags.add(comp);
|
||||
}
|
||||
if (hasCoreChanges(filenames)) tags.add('core');
|
||||
if (hasDashboardChanges(filenames)) tags.add('dashboard');
|
||||
if (hasGitHubActionsChanges(filenames)) tags.add('ci');
|
||||
|
||||
if (tags.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check title starts with [tag] prefix
|
||||
const bracketPattern = /^\[\w+\]/;
|
||||
if (!bracketPattern.test(title)) {
|
||||
const suggestion = [...tags].map(c => `[${c}]`).join('');
|
||||
// Skip if the suggested prefix would be too long for a readable title
|
||||
if (suggestion.length > 40) {
|
||||
return;
|
||||
}
|
||||
core.setFailed(
|
||||
`PR modifies: ${[...tags].join(', ')}\n` +
|
||||
`Title must start with a [tag] prefix.\n` +
|
||||
`Suggested: ${suggestion} <description>`
|
||||
);
|
||||
}
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.2
|
||||
rev: v0.15.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -429,6 +429,7 @@ esphome/components/select/* @esphome/core
|
||||
esphome/components/sen0321/* @notjj
|
||||
esphome/components/sen21231/* @shreyaskarnik
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/sfa30/* @ghsensdev
|
||||
|
||||
+53
-4
@@ -57,8 +57,23 @@ def maybe_conf(conf, *validators):
|
||||
return validate
|
||||
|
||||
|
||||
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
|
||||
return ACTION_REGISTRY.register(name, action_type, schema)
|
||||
def register_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool = False,
|
||||
):
|
||||
"""Register an action type.
|
||||
|
||||
Actions default to ``synchronous=False`` (safe default), meaning string
|
||||
arguments use owning std::string to prevent dangling references.
|
||||
|
||||
Set ``synchronous=True`` only for actions that complete synchronously
|
||||
and never store trigger arguments for later execution. This allows
|
||||
the code generator to use non-owning StringRef for zero-copy access.
|
||||
"""
|
||||
return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous)
|
||||
|
||||
|
||||
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
|
||||
@@ -335,7 +350,9 @@ async def component_is_idle_condition_to_code(
|
||||
|
||||
|
||||
@register_action(
|
||||
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
|
||||
"delay",
|
||||
DelayAction,
|
||||
cv.templatable(cv.positive_time_period_milliseconds),
|
||||
)
|
||||
async def delay_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -366,6 +383,7 @@ async def delay_action_to_code(
|
||||
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
|
||||
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def if_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -394,6 +412,7 @@ async def if_action_to_code(
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def while_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -417,6 +436,7 @@ async def while_action_to_code(
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def repeat_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -461,7 +481,12 @@ async def wait_until_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
@register_action("lambda", LambdaAction, cv.lambda_)
|
||||
# Lambda executes user C++ inline and returns — synchronous by execution model.
|
||||
# User code could theoretically store the StringRef for deferred use, but StringRef
|
||||
# is a view type and storing views beyond their scope is always unsafe regardless
|
||||
# of this optimization. Marking non-synchronous would disable StringRef for nearly
|
||||
# all user services since most use lambda.
|
||||
@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True)
|
||||
async def lambda_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
@@ -480,6 +505,7 @@ async def lambda_action_to_code(
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_update_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -499,6 +525,7 @@ async def component_update_action_to_code(
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_suspend_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -521,6 +548,7 @@ async def component_suspend_action_to_code(
|
||||
),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_resume_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -578,6 +606,27 @@ async def build_condition_list(
|
||||
return conditions
|
||||
|
||||
|
||||
def has_non_synchronous_actions(actions: ConfigType) -> bool:
|
||||
"""Check if a validated action list contains any non-synchronous actions.
|
||||
|
||||
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
|
||||
trigger args for later execution, making non-owning types like StringRef
|
||||
unsafe. Actions that haven't been audited default to non-synchronous.
|
||||
"""
|
||||
if isinstance(actions, list):
|
||||
return any(has_non_synchronous_actions(item) for item in actions)
|
||||
if isinstance(actions, dict):
|
||||
for key in actions:
|
||||
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
|
||||
return True
|
||||
return any(
|
||||
has_non_synchronous_actions(v)
|
||||
for v in actions.values()
|
||||
if isinstance(v, (list, dict))
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
async def build_automation(
|
||||
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
||||
) -> MockObj:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -199,12 +199,19 @@ void AcDimmer::setup() {
|
||||
setTimer1Callback(&timer_interrupt);
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
|
||||
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
// For ESP32, we can't use dynamic interval calculation because the timerX functions
|
||||
// are not callable from ISR (placed in flash storage).
|
||||
// Here we just use an interrupt firing every 50 µs.
|
||||
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
|
||||
if (dimmer_timer == nullptr) {
|
||||
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
|
||||
if (dimmer_timer == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
// For ESP32, we can't use dynamic interval calculation because the timerX functions
|
||||
// are not callable from ISR (placed in flash storage).
|
||||
// Here we just use an interrupt firing every 50 µs.
|
||||
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent
|
||||
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
|
||||
if (code != nullptr) {
|
||||
this->code_ = std::string(code);
|
||||
return this->set_code(code, strlen(code));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) {
|
||||
if (code != nullptr) {
|
||||
this->code_ = std::string(code, len);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ class AlarmControlPanelCall {
|
||||
AlarmControlPanelCall(AlarmControlPanel *parent);
|
||||
|
||||
AlarmControlPanelCall &set_code(const char *code);
|
||||
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
|
||||
AlarmControlPanelCall &set_code(const char *code, size_t len);
|
||||
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); }
|
||||
AlarmControlPanelCall &arm_away();
|
||||
AlarmControlPanelCall &arm_home();
|
||||
AlarmControlPanelCall &arm_night();
|
||||
|
||||
@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
|
||||
"bool": cg.bool_,
|
||||
"int": cg.int32,
|
||||
"float": cg.float_,
|
||||
"string": cg.std_string,
|
||||
"string": cg.StringRef,
|
||||
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
|
||||
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
|
||||
@@ -380,9 +380,18 @@ async def to_code(config: ConfigType) -> None:
|
||||
if is_optional:
|
||||
func_args.append((cg.bool_, "return_response"))
|
||||
|
||||
# Check if action chain has non-synchronous actions that would make
|
||||
# non-owning StringRef dangle (rx_buf_ reused after delay)
|
||||
has_non_synchronous = automation.has_non_synchronous_actions(
|
||||
conf.get(CONF_THEN, [])
|
||||
)
|
||||
|
||||
service_arg_names: list[str] = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
# Fall back to std::string for string args if non-synchronous actions exist
|
||||
if has_non_synchronous and native is cg.StringRef:
|
||||
native = cg.std_string
|
||||
service_template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
@@ -509,11 +518,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||
"homeassistant.action",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"homeassistant.service",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_service_to_code(
|
||||
config: ConfigType,
|
||||
@@ -524,24 +535,31 @@ async def homeassistant_service_to_code(
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
# output_type=None because lambdas can return non-string types (int,
|
||||
# float, char*) that TemplatableStringValue converts via to_string.
|
||||
# Static strings are manually wrapped for PROGMEM on ESP8266.
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
if on_error := config.get(CONF_ON_ERROR):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||
@@ -604,29 +622,37 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
"homeassistant.event",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
# output_type=None because lambdas can return non-string types (int,
|
||||
# float, char*) that TemplatableStringValue converts via to_string.
|
||||
# Static strings are manually wrapped for PROGMEM on ESP8266.
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
return var
|
||||
|
||||
@@ -644,16 +670,17 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
"homeassistant.tag_scanned",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
cg.add(var.set_service("esphome.tag_scanned"))
|
||||
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
|
||||
# Initialize FixedVector with exact size (1 data field)
|
||||
cg.add(var.init_data(1))
|
||||
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
||||
cg.add(var.add_data("tag_id", templ))
|
||||
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -834,6 +834,33 @@ message GetTimeRequest {
|
||||
option (source) = SOURCE_SERVER;
|
||||
}
|
||||
|
||||
enum DSTRuleType {
|
||||
DST_RULE_TYPE_NONE = 0;
|
||||
DST_RULE_TYPE_MONTH_WEEK_DAY = 1;
|
||||
DST_RULE_TYPE_JULIAN_NO_LEAP = 2;
|
||||
DST_RULE_TYPE_DAY_OF_YEAR = 3;
|
||||
}
|
||||
|
||||
message DSTRule {
|
||||
option (source) = SOURCE_CLIENT;
|
||||
|
||||
sint32 time_seconds = 1;
|
||||
uint32 day = 2;
|
||||
DSTRuleType type = 3;
|
||||
uint32 month = 4;
|
||||
uint32 week = 5;
|
||||
uint32 day_of_week = 6;
|
||||
}
|
||||
|
||||
message ParsedTimezone {
|
||||
option (source) = SOURCE_CLIENT;
|
||||
|
||||
sint32 std_offset_seconds = 1;
|
||||
sint32 dst_offset_seconds = 2;
|
||||
DSTRule dst_start = 3;
|
||||
DSTRule dst_end = 4;
|
||||
}
|
||||
|
||||
message GetTimeResponse {
|
||||
option (id) = 37;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -841,6 +868,7 @@ message GetTimeResponse {
|
||||
|
||||
fixed32 epoch_seconds = 1;
|
||||
string timezone = 2;
|
||||
ParsedTimezone parsed_timezone = 3;
|
||||
}
|
||||
|
||||
// ==================== USER-DEFINES SERVICES ====================
|
||||
@@ -989,6 +1017,7 @@ enum ClimateAction {
|
||||
CLIMATE_ACTION_IDLE = 4;
|
||||
CLIMATE_ACTION_DRYING = 5;
|
||||
CLIMATE_ACTION_FAN = 6;
|
||||
CLIMATE_ACTION_DEFROSTING = 7;
|
||||
}
|
||||
enum ClimatePreset {
|
||||
CLIMATE_PRESET_NONE = 0;
|
||||
|
||||
@@ -242,24 +242,11 @@ void APIConnection::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->flags_.sent_ping) {
|
||||
// Disconnect if not responded within 2.5*keepalive
|
||||
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
|
||||
on_fatal_error();
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting"));
|
||||
}
|
||||
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
|
||||
// Only send ping if we're not disconnecting
|
||||
ESP_LOGVV(TAG, "Sending keepalive PING");
|
||||
PingRequest req;
|
||||
this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE);
|
||||
if (!this->flags_.sent_ping) {
|
||||
// If we can't send the ping request directly (tx_buffer full),
|
||||
// schedule it at the front of the batch so it will be sent with priority
|
||||
ESP_LOGW(TAG, "Buffer full, ping queued");
|
||||
this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE);
|
||||
this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
|
||||
}
|
||||
// Keepalive: only call into the cold path when enough time has elapsed.
|
||||
// When sent_ping is true, last_traffic_ hasn't been updated so this
|
||||
// condition is already satisfied — covers both send-ping and disconnect cases.
|
||||
if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) {
|
||||
this->check_keepalive_(now);
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
@@ -275,6 +262,29 @@ void APIConnection::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::check_keepalive_(uint32_t now) {
|
||||
// Caller guarantees: now - last_traffic_ > KEEPALIVE_TIMEOUT_MS
|
||||
if (this->flags_.sent_ping) {
|
||||
// Disconnect if not responded within 2.5*keepalive
|
||||
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
|
||||
on_fatal_error();
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting"));
|
||||
}
|
||||
} else if (!this->flags_.remove) {
|
||||
// Only send ping if we're not disconnecting
|
||||
ESP_LOGVV(TAG, "Sending keepalive PING");
|
||||
PingRequest req;
|
||||
this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE);
|
||||
if (!this->flags_.sent_ping) {
|
||||
// If we can't send the ping request directly (tx_buffer full),
|
||||
// schedule it at the front of the batch so it will be sent with priority
|
||||
ESP_LOGW(TAG, "Buffer full, ping queued");
|
||||
this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE);
|
||||
this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::process_active_iterator_() {
|
||||
// Caller ensures active_iterator_ != NONE
|
||||
if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
|
||||
@@ -879,7 +889,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
|
||||
}
|
||||
void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
|
||||
call.set_value(msg.state);
|
||||
call.set_value(msg.state.c_str(), msg.state.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1113,7 +1123,30 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
if (!value.timezone.empty()) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
|
||||
// Check if the sender provided pre-parsed timezone data.
|
||||
// If std_offset is non-zero or DST rules are present, the parsed data was populated.
|
||||
// For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent.
|
||||
const auto &pt = value.parsed_timezone;
|
||||
if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) {
|
||||
time::ParsedTimezone tz{};
|
||||
tz.std_offset_seconds = pt.std_offset_seconds;
|
||||
tz.dst_offset_seconds = pt.dst_offset_seconds;
|
||||
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
|
||||
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
|
||||
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
|
||||
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
|
||||
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
|
||||
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
|
||||
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
|
||||
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
|
||||
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
|
||||
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
|
||||
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
|
||||
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
|
||||
time::set_global_tz(tz);
|
||||
} else {
|
||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1327,7 +1360,7 @@ void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPan
|
||||
call.pending();
|
||||
break;
|
||||
}
|
||||
call.set_code(msg.code);
|
||||
call.set_code(msg.code.c_str(), msg.code.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1701,37 +1734,42 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
return;
|
||||
}
|
||||
|
||||
// Null-terminate state in-place for safe c_str() usage (e.g., parse_number in callbacks).
|
||||
// Safe: decode is complete, byte after string data was already consumed during parse,
|
||||
// and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte in rx_buf_.
|
||||
// const_cast is safe: msg references mutable rx_buf_ data; the const& handler
|
||||
// signature is a generated protobuf pattern, not a true immutability contract.
|
||||
if (!msg.state.empty()) {
|
||||
const_cast<char *>(msg.state.c_str())[msg.state.size()] = '\0';
|
||||
}
|
||||
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
// Compare entity_id: check length matches and content matches
|
||||
size_t entity_id_len = strlen(it.entity_id);
|
||||
if (entity_id_len != msg.entity_id.size() ||
|
||||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
|
||||
if (msg.entity_id != it.entity_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare attribute: either both have matching attribute, or both have none
|
||||
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
|
||||
if (sub_attr_len != msg.attribute.size() ||
|
||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
|
||||
// If subscriber has attribute filter (non-null), message attribute must match it;
|
||||
// if subscriber has no filter (nullptr), message must have no attribute.
|
||||
if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
||||
// HA state max length is 255 characters, but attributes can be much longer
|
||||
// Use stack buffer for common case (states), heap fallback for large attributes
|
||||
size_t state_len = msg.state.size();
|
||||
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
|
||||
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
|
||||
if (state_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), state_len);
|
||||
}
|
||||
state_buf[state_len] = '\0';
|
||||
it.callback(StringRef(state_buf, state_len));
|
||||
it.callback(msg.state);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
|
||||
// Null-terminate string args in-place for safe c_str() usage in YAML service triggers.
|
||||
// Safe: full ExecuteServiceRequest decode is complete, all bytes in rx_buf_ consumed,
|
||||
// and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte for the last field.
|
||||
// const_cast is safe: msg references mutable rx_buf_ data; the const& handler
|
||||
// signature is a generated protobuf pattern, not a true immutability contract.
|
||||
for (auto &arg : const_cast<ExecuteServiceRequest &>(msg).args) {
|
||||
if (!arg.string_.empty()) {
|
||||
const_cast<char *>(arg.string_.c_str())[arg.string_.size()] = '\0';
|
||||
}
|
||||
}
|
||||
bool found = false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Register the call and get a unique server-generated action_call_id
|
||||
|
||||
@@ -370,6 +370,10 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||
}
|
||||
|
||||
// Send keepalive ping or disconnect unresponsive client.
|
||||
// Cold path — extracted from loop() to reduce instruction cache pressure.
|
||||
void __attribute__((noinline)) check_keepalive_(uint32_t now);
|
||||
|
||||
// Process active iterator (list_entities/initial_state) during connection setup.
|
||||
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
|
||||
void __attribute__((noinline)) process_active_iterator_();
|
||||
|
||||
@@ -29,6 +29,10 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
|
||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
|
||||
#endif
|
||||
|
||||
// Extra byte reserved in rx_buf_ beyond the message size so protobuf
|
||||
// StringRef fields can be null-terminated in-place after decode.
|
||||
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
|
||||
|
||||
// Maximum number of messages to batch in a single write operation
|
||||
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
|
||||
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
|
||||
|
||||
@@ -194,16 +194,21 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
||||
|
||||
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
|
||||
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
|
||||
bool is_data = (state_ == State::DATA);
|
||||
uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
|
||||
if (msg_size > limit) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
|
||||
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||
return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||
}
|
||||
|
||||
// Reserve space for body
|
||||
if (this->rx_buf_.size() != msg_size) {
|
||||
this->rx_buf_.resize(msg_size);
|
||||
// Reserve space for body (+ null terminator in DATA state so protobuf
|
||||
// StringRef fields can be safely null-terminated in-place after decode.
|
||||
// During handshake, rx_buf_.size() is used in prologue construction, so
|
||||
// the buffer must be exactly msg_size to avoid prologue mismatch.)
|
||||
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
|
||||
if (this->rx_buf_.size() != alloc_size) {
|
||||
this->rx_buf_.resize(alloc_size);
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < msg_size) {
|
||||
@@ -407,7 +412,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
|
||||
// read_packet() must only be called in DATA state; the extra
|
||||
// RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state
|
||||
// (see try_read_frame_), so calling this during handshake would
|
||||
// underflow the size calculation below.
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
// rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination
|
||||
// (only added in DATA state — see try_read_frame_), so subtract it
|
||||
// to get the actual encrypted data size for decryption.
|
||||
size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR;
|
||||
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size);
|
||||
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
|
||||
APIError decrypt_err =
|
||||
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
|
||||
@@ -574,7 +590,9 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(state_ == State::HANDSHAKE);
|
||||
#endif
|
||||
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
|
||||
|
||||
@@ -163,9 +163,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
||||
}
|
||||
// header reading done
|
||||
|
||||
// Reserve space for body
|
||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
|
||||
this->rx_buf_.resize(this->rx_header_parsed_len_);
|
||||
// Reserve space for body (+ null terminator so protobuf StringRef fields
|
||||
// can be safely null-terminated in-place after decode)
|
||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) {
|
||||
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||
|
||||
@@ -954,12 +954,66 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
bool DSTRule::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->time_seconds = value.as_sint32();
|
||||
break;
|
||||
case 2:
|
||||
this->day = value.as_uint32();
|
||||
break;
|
||||
case 3:
|
||||
this->type = static_cast<enums::DSTRuleType>(value.as_uint32());
|
||||
break;
|
||||
case 4:
|
||||
this->month = value.as_uint32();
|
||||
break;
|
||||
case 5:
|
||||
this->week = value.as_uint32();
|
||||
break;
|
||||
case 6:
|
||||
this->day_of_week = value.as_uint32();
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ParsedTimezone::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->std_offset_seconds = value.as_sint32();
|
||||
break;
|
||||
case 2:
|
||||
this->dst_offset_seconds = value.as_sint32();
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 3:
|
||||
value.decode_to_message(this->dst_start);
|
||||
break;
|
||||
case 4:
|
||||
value.decode_to_message(this->dst_end);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->timezone = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
value.decode_to_message(this->parsed_timezone);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See script/api_protobuf/api_protobuf.py
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include "proto.h"
|
||||
@@ -64,6 +63,12 @@ enum LogLevel : uint32_t {
|
||||
LOG_LEVEL_VERBOSE = 6,
|
||||
LOG_LEVEL_VERY_VERBOSE = 7,
|
||||
};
|
||||
enum DSTRuleType : uint32_t {
|
||||
DST_RULE_TYPE_NONE = 0,
|
||||
DST_RULE_TYPE_MONTH_WEEK_DAY = 1,
|
||||
DST_RULE_TYPE_JULIAN_NO_LEAP = 2,
|
||||
DST_RULE_TYPE_DAY_OF_YEAR = 3,
|
||||
};
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_BOOL = 0,
|
||||
@@ -117,6 +122,7 @@ enum ClimateAction : uint32_t {
|
||||
CLIMATE_ACTION_IDLE = 4,
|
||||
CLIMATE_ACTION_DRYING = 5,
|
||||
CLIMATE_ACTION_FAN = 6,
|
||||
CLIMATE_ACTION_DEFROSTING = 7,
|
||||
};
|
||||
enum ClimatePreset : uint32_t {
|
||||
CLIMATE_PRESET_NONE = 0,
|
||||
@@ -316,7 +322,6 @@ enum ZWaveProxyRequestType : uint32_t {
|
||||
|
||||
class InfoResponseProtoMessage : public ProtoMessage {
|
||||
public:
|
||||
~InfoResponseProtoMessage() override = default;
|
||||
StringRef object_id{};
|
||||
uint32_t key{0};
|
||||
StringRef name{};
|
||||
@@ -330,28 +335,29 @@ class InfoResponseProtoMessage : public ProtoMessage {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
~InfoResponseProtoMessage() = default;
|
||||
};
|
||||
|
||||
class StateResponseProtoMessage : public ProtoMessage {
|
||||
public:
|
||||
~StateResponseProtoMessage() override = default;
|
||||
uint32_t key{0};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
~StateResponseProtoMessage() = default;
|
||||
};
|
||||
|
||||
class CommandProtoMessage : public ProtoDecodableMessage {
|
||||
public:
|
||||
~CommandProtoMessage() override = default;
|
||||
uint32_t key{0};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
~CommandProtoMessage() = default;
|
||||
};
|
||||
class HelloRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
@@ -1116,15 +1122,45 @@ class GetTimeRequest final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class DSTRule final : public ProtoDecodableMessage {
|
||||
public:
|
||||
int32_t time_seconds{0};
|
||||
uint32_t day{0};
|
||||
enums::DSTRuleType type{};
|
||||
uint32_t month{0};
|
||||
uint32_t week{0};
|
||||
uint32_t day_of_week{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class ParsedTimezone final : public ProtoDecodableMessage {
|
||||
public:
|
||||
int32_t std_offset_seconds{0};
|
||||
int32_t dst_offset_seconds{0};
|
||||
DSTRule dst_start{};
|
||||
DSTRule dst_end{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 37;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 14;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 31;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "get_time_response"; }
|
||||
#endif
|
||||
uint32_t epoch_seconds{0};
|
||||
StringRef timezone{};
|
||||
ParsedTimezone parsed_timezone{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// This file was automatically generated with a tool.
|
||||
// See script/api_protobuf/api_protobuf.py
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#ifndef USE_API_VARINT64
|
||||
#define USE_API_VARINT64
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::api {} // namespace esphome::api
|
||||
@@ -208,6 +208,20 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::DSTRuleType>(enums::DSTRuleType value) {
|
||||
switch (value) {
|
||||
case enums::DST_RULE_TYPE_NONE:
|
||||
return "DST_RULE_TYPE_NONE";
|
||||
case enums::DST_RULE_TYPE_MONTH_WEEK_DAY:
|
||||
return "DST_RULE_TYPE_MONTH_WEEK_DAY";
|
||||
case enums::DST_RULE_TYPE_JULIAN_NO_LEAP:
|
||||
return "DST_RULE_TYPE_JULIAN_NO_LEAP";
|
||||
case enums::DST_RULE_TYPE_DAY_OF_YEAR:
|
||||
return "DST_RULE_TYPE_DAY_OF_YEAR";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
|
||||
switch (value) {
|
||||
@@ -321,6 +335,8 @@ template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::Climate
|
||||
return "CLIMATE_ACTION_DRYING";
|
||||
case enums::CLIMATE_ACTION_FAN:
|
||||
return "CLIMATE_ACTION_FAN";
|
||||
case enums::CLIMATE_ACTION_DEFROSTING:
|
||||
return "CLIMATE_ACTION_DEFROSTING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -1252,10 +1268,35 @@ const char *GetTimeRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("GetTimeRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *DSTRule::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "DSTRule");
|
||||
dump_field(out, "time_seconds", this->time_seconds);
|
||||
dump_field(out, "day", this->day);
|
||||
dump_field(out, "type", static_cast<enums::DSTRuleType>(this->type));
|
||||
dump_field(out, "month", this->month);
|
||||
dump_field(out, "week", this->week);
|
||||
dump_field(out, "day_of_week", this->day_of_week);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ParsedTimezone");
|
||||
dump_field(out, "std_offset_seconds", this->std_offset_seconds);
|
||||
dump_field(out, "dst_offset_seconds", this->dst_offset_seconds);
|
||||
out.append(" dst_start: ");
|
||||
this->dst_start.dump_to(out);
|
||||
out.append("\n");
|
||||
out.append(" dst_end: ");
|
||||
this->dst_end.dump_to(out);
|
||||
out.append("\n");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "GetTimeResponse");
|
||||
dump_field(out, "epoch_seconds", this->epoch_seconds);
|
||||
dump_field(out, "timezone", this->timezone);
|
||||
out.append(" parsed_timezone: ");
|
||||
this->parsed_timezone.dump_to(out);
|
||||
out.append("\n");
|
||||
return out.c_str();
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
@@ -433,8 +433,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper to add subscription (reduces duplication)
|
||||
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
|
||||
bool once) {
|
||||
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
|
||||
std::function<void(StringRef)> &&f, bool once) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
|
||||
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
|
||||
@@ -443,7 +443,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri
|
||||
|
||||
// Helper to add subscription with heap-allocated strings (reduces duplication)
|
||||
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f, bool once) {
|
||||
std::function<void(StringRef)> &&f, bool once) {
|
||||
HomeAssistantStateSubscription sub;
|
||||
// Allocate heap storage for the strings
|
||||
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
|
||||
@@ -463,29 +463,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
|
||||
}
|
||||
|
||||
// std::string overload with StringRef callback (zero-allocation callback)
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
||||
}
|
||||
|
||||
// Legacy helper: wraps std::string callback and delegates to StringRef version
|
||||
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f, bool once) {
|
||||
std::function<void(const std::string &)> &&f, bool once) {
|
||||
// Wrap callback to convert StringRef -> std::string, then delegate
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
|
||||
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
|
||||
@@ -494,12 +494,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
|
||||
|
||||
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f) {
|
||||
std::function<void(const std::string &)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f) {
|
||||
std::function<void(const std::string &)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
||||
}
|
||||
|
||||
|
||||
@@ -201,20 +201,20 @@ class APIServer : public Component,
|
||||
};
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
|
||||
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
|
||||
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
|
||||
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
|
||||
|
||||
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
|
||||
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f);
|
||||
std::function<void(StringRef)> &&f);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f);
|
||||
std::function<void(StringRef)> &&f);
|
||||
|
||||
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
|
||||
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f);
|
||||
std::function<void(const std::string &)> &&f);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f);
|
||||
std::function<void(const std::string &)> &&f);
|
||||
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#endif
|
||||
@@ -241,13 +241,13 @@ class APIServer : public Component,
|
||||
#endif // USE_API_NOISE
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper methods to reduce code duplication
|
||||
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
|
||||
bool once);
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
|
||||
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f,
|
||||
bool once);
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> &&f, bool once);
|
||||
// Legacy helper: wraps std::string callback and delegates to StringRef version
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f, bool once);
|
||||
std::function<void(const std::string &)> &&f, bool once);
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
// No explicit close() needed — listen sockets have no active connections on
|
||||
// failure/shutdown. Destructor handles fd cleanup (close or abort per platform).
|
||||
@@ -257,7 +257,7 @@ class APIServer : public Component,
|
||||
}
|
||||
void socket_failed_(const LogString *msg);
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
socket::Socket *socket_{nullptr};
|
||||
socket::ListenSocket *socket_{nullptr};
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> client_connected_trigger_;
|
||||
#endif
|
||||
|
||||
@@ -130,6 +130,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
|
||||
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
|
||||
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
template<typename T> void set_response_template(T response_template) {
|
||||
this->response_template_ = response_template;
|
||||
@@ -221,7 +235,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
Ts... x) {
|
||||
dest.init(source.size());
|
||||
|
||||
// Count non-static strings to allocate exact storage needed
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
|
||||
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
|
||||
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
|
||||
// values unconditionally: keys via _P functions (may be in PROGMEM), values
|
||||
// via value() which handles FLASH_STRING internally.
|
||||
value_storage.init(source.size() * 2);
|
||||
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
|
||||
// Key: copy from possible PROGMEM
|
||||
{
|
||||
size_t key_len = strlen_P(it.key);
|
||||
value_storage.push_back(std::string(key_len, '\0'));
|
||||
memcpy_P(value_storage.back().data(), it.key, key_len);
|
||||
kv.key = StringRef(value_storage.back());
|
||||
}
|
||||
|
||||
// Value: value() handles FLASH_STRING via _P functions internally
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
#else
|
||||
// On non-ESP8266, strings are directly readable from flash-mapped memory.
|
||||
// Count non-static strings to allocate exact storage needed.
|
||||
size_t lambda_count = 0;
|
||||
for (const auto &it : source) {
|
||||
if (!it.value.is_static_string()) {
|
||||
@@ -235,14 +274,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
kv.key = StringRef(it.key);
|
||||
|
||||
if (it.value.is_static_string()) {
|
||||
// Static string from YAML - zero allocation
|
||||
// Static string — pointer directly readable, zero allocation
|
||||
kv.value = StringRef(it.value.get_static_string());
|
||||
} else {
|
||||
// Lambda evaluation - store result, reference it
|
||||
// Lambda — evaluate and store result
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
APIServer *parent_;
|
||||
|
||||
@@ -15,7 +15,7 @@ class APIConnection;
|
||||
return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
|
||||
}
|
||||
|
||||
class ListEntitiesIterator : public ComponentIterator {
|
||||
class ListEntitiesIterator final : public ComponentIterator {
|
||||
public:
|
||||
ListEntitiesIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -7,6 +7,23 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.proto";
|
||||
|
||||
#ifdef USE_API_VARINT64
|
||||
optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed,
|
||||
uint32_t result32) {
|
||||
uint64_t result64 = result32;
|
||||
uint32_t limit = std::min(len, uint32_t(10));
|
||||
for (uint32_t i = 4; i < limit; i++) {
|
||||
uint8_t val = buffer[i];
|
||||
result64 |= uint64_t(val & 0x7F) << (i * 7);
|
||||
if ((val & 0x80) == 0) {
|
||||
*consumed = i + 1;
|
||||
return ProtoVarInt(result64);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
|
||||
uint32_t count = 0;
|
||||
const uint8_t *ptr = buffer;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "api_pb2_defines.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -110,59 +111,78 @@ class ProtoVarInt {
|
||||
#endif
|
||||
if (len == 0)
|
||||
return {};
|
||||
|
||||
// Most common case: single-byte varint (values 0-127)
|
||||
// Fast path: single-byte varints (0-127) are the most common case
|
||||
// (booleans, small enums, field tags). Avoid loop overhead entirely.
|
||||
if ((buffer[0] & 0x80) == 0) {
|
||||
*consumed = 1;
|
||||
return ProtoVarInt(buffer[0]);
|
||||
}
|
||||
|
||||
// General case for multi-byte varints
|
||||
// Since we know buffer[0]'s high bit is set, initialize with its value
|
||||
uint64_t result = buffer[0] & 0x7F;
|
||||
uint8_t bitpos = 7;
|
||||
|
||||
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
|
||||
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
|
||||
uint32_t max_len = std::min(len, uint32_t(10));
|
||||
|
||||
// Start from the second byte since we've already processed the first
|
||||
for (uint32_t i = 1; i < max_len; i++) {
|
||||
// 32-bit phase: process remaining bytes with native 32-bit shifts.
|
||||
// Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t
|
||||
// shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values.
|
||||
// With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles
|
||||
// byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX).
|
||||
uint32_t result32 = buffer[0] & 0x7F;
|
||||
#ifdef USE_API_VARINT64
|
||||
uint32_t limit = std::min(len, uint32_t(4));
|
||||
#else
|
||||
uint32_t limit = std::min(len, uint32_t(5));
|
||||
#endif
|
||||
for (uint32_t i = 1; i < limit; i++) {
|
||||
uint8_t val = buffer[i];
|
||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||
bitpos += 7;
|
||||
result32 |= uint32_t(val & 0x7F) << (i * 7);
|
||||
if ((val & 0x80) == 0) {
|
||||
*consumed = i + 1;
|
||||
return ProtoVarInt(result);
|
||||
return ProtoVarInt(result32);
|
||||
}
|
||||
}
|
||||
|
||||
return {}; // Incomplete or invalid varint
|
||||
// 64-bit phase for remaining bytes (BLE addresses etc.)
|
||||
#ifdef USE_API_VARINT64
|
||||
return parse_wide(buffer, len, consumed, result32);
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_API_VARINT64
|
||||
protected:
|
||||
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
|
||||
/// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path.
|
||||
static optional<ProtoVarInt> parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32)
|
||||
__attribute__((noinline));
|
||||
|
||||
public:
|
||||
#endif
|
||||
|
||||
constexpr uint16_t as_uint16() const { return this->value_; }
|
||||
constexpr uint32_t as_uint32() const { return this->value_; }
|
||||
constexpr uint64_t as_uint64() const { return this->value_; }
|
||||
constexpr bool as_bool() const { return this->value_; }
|
||||
constexpr int32_t as_int32() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int32_t>(this->as_int64());
|
||||
}
|
||||
constexpr int64_t as_int64() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int64_t>(this->value_);
|
||||
return static_cast<int32_t>(this->value_);
|
||||
}
|
||||
constexpr int32_t as_sint32() const {
|
||||
// with ZigZag encoding
|
||||
return decode_zigzag32(static_cast<uint32_t>(this->value_));
|
||||
}
|
||||
#ifdef USE_API_VARINT64
|
||||
constexpr uint64_t as_uint64() const { return this->value_; }
|
||||
constexpr int64_t as_int64() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int64_t>(this->value_);
|
||||
}
|
||||
constexpr int64_t as_sint64() const {
|
||||
// with ZigZag encoding
|
||||
return decode_zigzag64(this->value_);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef USE_API_VARINT64
|
||||
uint64_t value_;
|
||||
#else
|
||||
uint32_t value_;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
|
||||
@@ -432,7 +452,6 @@ class DumpBuffer {
|
||||
|
||||
class ProtoMessage {
|
||||
public:
|
||||
virtual ~ProtoMessage() = default;
|
||||
// Default implementation for messages with no fields
|
||||
virtual void encode(ProtoWriteBuffer &buffer) const {}
|
||||
// Default implementation for messages with no fields
|
||||
@@ -443,6 +462,11 @@ class ProtoMessage {
|
||||
virtual const char *dump_to(DumpBuffer &out) const = 0;
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Non-virtual: messages are never deleted polymorphically.
|
||||
// Protected prevents accidental `delete base_ptr` (compile error).
|
||||
~ProtoMessage() = default;
|
||||
};
|
||||
|
||||
// Base class for messages that support decoding
|
||||
@@ -462,6 +486,7 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
||||
|
||||
protected:
|
||||
~ProtoDecodableMessage() = default;
|
||||
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
||||
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
|
||||
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
|
||||
|
||||
@@ -16,7 +16,7 @@ class APIConnection;
|
||||
return this->client_->send_##entity_type##_state(entity); \
|
||||
}
|
||||
|
||||
class InitialStateIterator : public ComponentIterator {
|
||||
class InitialStateIterator final : public ComponentIterator {
|
||||
public:
|
||||
InitialStateIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "user_services.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
|
||||
}
|
||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
|
||||
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
|
||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||
@@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
|
||||
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||
// Zero-copy StringRef version for YAML-generated services
|
||||
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||
|
||||
@@ -230,7 +230,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void set_data(std::function<void(Ts..., JsonObject)> func) {
|
||||
void set_data(std::function<void(Ts..., JsonObject)> &&func) {
|
||||
this->json_builder_ = std::move(func);
|
||||
this->has_data_ = true;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_POWER_MODE,
|
||||
CONF_RANGE,
|
||||
CONF_WATCHDOG,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@ammmze"]
|
||||
@@ -57,7 +58,6 @@ FAST_FILTER = {
|
||||
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_START_POSITION = "start_position"
|
||||
|
||||
@@ -23,7 +23,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone
|
||||
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_PWM_FREQUENCY = "pwm_frequency"
|
||||
|
||||
@@ -214,4 +214,4 @@ async def to_code(config):
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
if data.opus_support:
|
||||
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.3")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.4")
|
||||
|
||||
@@ -550,22 +550,8 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
|
||||
CONF_PUBLISH_INITIAL_STATE, False
|
||||
)
|
||||
cg.add(var.set_trigger_on_initial_state(trigger))
|
||||
if inverted := config.get(CONF_INVERTED):
|
||||
cg.add(var.set_inverted(inverted))
|
||||
if filters_config := config.get(CONF_FILTERS):
|
||||
cg.add_define("USE_BINARY_SENSOR_FILTER")
|
||||
filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
|
||||
cg.add(var.add_filters(filters))
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_binary_sensor_automations(var, config):
|
||||
for conf in config.get(CONF_ON_PRESS, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
@@ -617,6 +603,25 @@ async def setup_binary_sensor_core_(var, config):
|
||||
conf,
|
||||
)
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
|
||||
CONF_PUBLISH_INITIAL_STATE, False
|
||||
)
|
||||
cg.add(var.set_trigger_on_initial_state(trigger))
|
||||
if inverted := config.get(CONF_INVERTED):
|
||||
cg.add(var.set_inverted(inverted))
|
||||
if filters_config := config.get(CONF_FILTERS):
|
||||
cg.add_define("USE_BINARY_SENSOR_FILTER")
|
||||
filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
|
||||
cg.add(var.add_filters(filters))
|
||||
|
||||
CORE.add_job(_build_binary_sensor_automations, var, config)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
@@ -52,12 +52,12 @@ void BL0942::loop() {
|
||||
return;
|
||||
}
|
||||
if (avail < sizeof(buffer)) {
|
||||
if (!this->rx_start_) {
|
||||
if (!this->rx_start_.has_value()) {
|
||||
this->rx_start_ = millis();
|
||||
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
||||
} else if (millis() - *this->rx_start_ > PKT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
|
||||
this->read_array((uint8_t *) &buffer, avail);
|
||||
this->rx_start_ = 0;
|
||||
this->rx_start_.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ void BL0942::loop() {
|
||||
this->received_package_(&buffer);
|
||||
}
|
||||
}
|
||||
this->rx_start_ = 0;
|
||||
this->rx_start_.reset();
|
||||
}
|
||||
|
||||
bool BL0942::validate_checksum_(DataPacket *data) {
|
||||
|
||||
@@ -140,7 +140,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
|
||||
uint8_t address_ = 0;
|
||||
bool reset_ = false;
|
||||
LineFrequency line_freq_ = LINE_FREQUENCY_50HZ;
|
||||
uint32_t rx_start_ = 0;
|
||||
optional<uint32_t> rx_start_{};
|
||||
uint32_t prev_cf_cnt_ = 0;
|
||||
|
||||
bool validate_checksum_(DataPacket *data);
|
||||
|
||||
@@ -101,7 +101,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
if (this->found_ && this->last_seen_ + this->timeout_ < millis())
|
||||
if (this->found_ && millis() - this->last_seen_ > this->timeout_)
|
||||
this->set_found_(false);
|
||||
}
|
||||
void dump_config() override;
|
||||
|
||||
@@ -123,7 +123,9 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("button.press", PressAction, BUTTON_PRESS_SCHEMA)
|
||||
@automation.register_action(
|
||||
"button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True
|
||||
)
|
||||
async def button_press_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -22,7 +22,7 @@ class DNSServer {
|
||||
}
|
||||
static constexpr size_t DNS_BUFFER_SIZE = 192;
|
||||
|
||||
socket::Socket *socket_{nullptr};
|
||||
socket::ListenSocket *socket_{nullptr};
|
||||
network::IPAddress server_ip_;
|
||||
uint8_t buffer_[DNS_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
@@ -242,6 +242,9 @@ void CC1101Component::begin_tx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
|
||||
// block TX entry when strobing from RX, and to ensure FS_AUTOCAL calibration
|
||||
this->enter_idle_();
|
||||
if (!this->enter_tx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter TX state!");
|
||||
}
|
||||
@@ -252,6 +255,8 @@ void CC1101Component::begin_rx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
// Transition through IDLE to ensure FS_AUTOCAL calibration occurs
|
||||
this->enter_idle_();
|
||||
if (!this->enter_rx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter RX state!");
|
||||
}
|
||||
|
||||
@@ -173,14 +173,17 @@ ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); }
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(const char *mode, size_t len) {
|
||||
StringRef mode_ref(mode, len);
|
||||
for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(mode, mode_entry.str)) {
|
||||
if (str_equals_case_insensitive(mode_ref, mode_entry.str)) {
|
||||
this->set_mode(static_cast<ClimateMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -266,13 +269,18 @@ ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
|
||||
return this->set_swing_mode(swing_mode.c_str(), swing_mode.size());
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(const char *swing_mode, size_t len) {
|
||||
StringRef mode_ref(swing_mode, len);
|
||||
for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
|
||||
if (str_equals_case_insensitive(mode_ref, mode_entry.str)) {
|
||||
this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %.*s", this->parent_->get_name().c_str(), (int) len, swing_mode);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ class ClimateCall {
|
||||
ClimateCall &set_mode(optional<ClimateMode> mode);
|
||||
/// Set the mode of the climate device based on a string.
|
||||
ClimateCall &set_mode(const std::string &mode);
|
||||
/// Set the mode of the climate device based on a C string.
|
||||
ClimateCall &set_mode(const char *mode, size_t len);
|
||||
/// Set the target temperature of the climate device.
|
||||
ClimateCall &set_target_temperature(float target_temperature);
|
||||
/// Set the target temperature of the climate device.
|
||||
@@ -87,6 +89,8 @@ class ClimateCall {
|
||||
ClimateCall &set_swing_mode(optional<ClimateSwingMode> swing_mode);
|
||||
/// Set the swing mode of the climate device based on a string.
|
||||
ClimateCall &set_swing_mode(const std::string &swing_mode);
|
||||
/// Set the swing mode of the climate device based on a C string.
|
||||
ClimateCall &set_swing_mode(const char *swing_mode, size_t len);
|
||||
/// Set the preset of the climate device.
|
||||
ClimateCall &set_preset(ClimatePreset preset);
|
||||
/// Set the preset of the climate device.
|
||||
|
||||
@@ -10,8 +10,10 @@ const LogString *climate_mode_to_string(ClimateMode mode) {
|
||||
return ClimateModeStrings::get_log_str(static_cast<uint8_t>(mode), ClimateModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
|
||||
PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN");
|
||||
// Climate action strings indexed by ClimateAction enum (0,2-7): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN,
|
||||
// DEFROSTING
|
||||
PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN",
|
||||
"DEFROSTING", "UNKNOWN");
|
||||
|
||||
const LogString *climate_action_to_string(ClimateAction action) {
|
||||
return ClimateActionStrings::get_log_str(static_cast<uint8_t>(action), ClimateActionStrings::LAST_INDEX);
|
||||
|
||||
@@ -41,6 +41,8 @@ enum ClimateAction : uint8_t {
|
||||
CLIMATE_ACTION_DRYING = 5,
|
||||
/// The climate device is in fan only mode
|
||||
CLIMATE_ACTION_FAN = 6,
|
||||
/// The climate device is defrosting
|
||||
CLIMATE_ACTION_DEFROSTING = 7,
|
||||
};
|
||||
|
||||
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
|
||||
|
||||
@@ -248,25 +248,33 @@ COVER_ACTION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("cover.open", OpenAction, COVER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_open_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action("cover.close", CloseAction, COVER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_close_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action("cover.stop", StopAction, COVER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_stop_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_toggle_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
@@ -283,7 +291,9 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_control_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cse7766 {
|
||||
namespace esphome::cse7766 {
|
||||
|
||||
static const char *const TAG = "cse7766";
|
||||
|
||||
@@ -258,5 +257,4 @@ void CSE7766Component::dump_config() {
|
||||
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
|
||||
}
|
||||
|
||||
} // namespace cse7766
|
||||
} // namespace esphome
|
||||
} // namespace esphome::cse7766
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cse7766 {
|
||||
namespace esphome::cse7766 {
|
||||
|
||||
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
|
||||
|
||||
@@ -49,5 +48,4 @@ class CSE7766Component : public Component, public uart::UARTDevice {
|
||||
uint16_t cf_pulses_last_{0};
|
||||
};
|
||||
|
||||
} // namespace cse7766
|
||||
} // namespace esphome
|
||||
} // namespace esphome::cse7766
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "dlms_meter.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
||||
#include <bearssl/bearssl.h>
|
||||
#elif defined(USE_ESP32)
|
||||
@@ -410,7 +408,7 @@ void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_lengt
|
||||
if (current_position + 1 < message_length) {
|
||||
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
|
||||
if (scaler != 0) {
|
||||
value *= powf(10.0f, scaler);
|
||||
value *= pow10_int(scaler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -790,8 +790,15 @@ def _detect_variant(value):
|
||||
engineering_sample = value.get(CONF_ENGINEERING_SAMPLE)
|
||||
if engineering_sample is None:
|
||||
_LOGGER.warning(
|
||||
"No board specified for ESP32-P4. Defaulting to production silicon (rev3). "
|
||||
"If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'."
|
||||
"No board specified for ESP32-P4. Defaulting to production silicon (rev3).\n"
|
||||
"If you have an early engineering sample (pre-rev3), add this to your config:\n"
|
||||
"\n"
|
||||
" esp32:\n"
|
||||
" engineering_sample: true\n"
|
||||
"\n"
|
||||
"To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n"
|
||||
"Engineering samples will show a revision below v3.0.\n"
|
||||
"The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)."
|
||||
)
|
||||
elif engineering_sample:
|
||||
value[CONF_BOARD] = "esp32-p4-evboard"
|
||||
@@ -883,10 +890,10 @@ def final_validate(config):
|
||||
)
|
||||
)
|
||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant",
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
|
||||
)
|
||||
)
|
||||
@@ -945,6 +952,7 @@ CONF_HEAP_IN_IRAM = "heap_in_iram"
|
||||
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle"
|
||||
CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs"
|
||||
CONF_ENABLE_FULL_PRINTF = "enable_full_printf"
|
||||
CONF_DISABLE_OCD_AWARE = "disable_ocd_aware"
|
||||
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
|
||||
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
|
||||
@@ -1119,6 +1127,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(
|
||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[]
|
||||
): cv.ensure_list(cv.string_strict),
|
||||
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
@@ -1429,6 +1438,7 @@ async def to_code(config):
|
||||
|
||||
cg.set_cpp_standard("gnu++20")
|
||||
cg.add_build_flag("-DUSE_ESP32")
|
||||
cg.add_define("USE_NATIVE_64BIT_TIME")
|
||||
cg.add_build_flag("-Wl,-z,noexecstack")
|
||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
variant = config[CONF_VARIANT]
|
||||
@@ -1462,6 +1472,14 @@ async def to_code(config):
|
||||
"_ZSt25__throw_bad_function_callv",
|
||||
]:
|
||||
cg.add_build_flag(f"-Wl,--wrap={mangled}")
|
||||
|
||||
# Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r
|
||||
# (~11 KB). See printf_stubs.cpp for implementation.
|
||||
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]:
|
||||
cg.add_define("USE_FULL_PRINTF")
|
||||
else:
|
||||
for symbol in ("vprintf", "printf", "fprintf"):
|
||||
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
||||
else:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||
@@ -1620,8 +1638,13 @@ async def to_code(config):
|
||||
_configure_lwip_max_sockets(conf)
|
||||
|
||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
if variant == VARIANT_ESP32S3:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
elif variant == VARIANT_ESP32P4:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
|
||||
else:
|
||||
raise ValueError("Unhandled ESP32 variant")
|
||||
|
||||
# Apply LWIP core locking for better socket performance
|
||||
# This is already enabled by default in Arduino framework, where it provides
|
||||
|
||||
@@ -22,7 +22,8 @@ extern "C" __attribute__((weak)) void initArduino() {}
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { vPortYield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); }
|
||||
uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); }
|
||||
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
|
||||
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
@@ -47,6 +48,7 @@ void arch_init() {
|
||||
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
uint32_t freq = 0;
|
||||
|
||||
@@ -19,16 +19,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -41,14 +32,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.set_data(data, len);
|
||||
obj.data.set(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -56,11 +47,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.len != len) {
|
||||
if (obj.data.size() != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.get(), len);
|
||||
memcpy(data, obj.data.data(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -133,10 +124,10 @@ class ESP32Preferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -144,7 +135,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
cached++;
|
||||
}
|
||||
}
|
||||
@@ -176,7 +167,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.len) {
|
||||
if (actual_len != to_save.data.size()) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
@@ -186,7 +177,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Linker wrap stubs for FILE*-based printf functions.
|
||||
*
|
||||
* ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference
|
||||
* fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r
|
||||
* (~11 KB). This is a separate implementation from _svfprintf_r (used by
|
||||
* snprintf/vsnprintf) that handles FILE* stream I/O with buffering and
|
||||
* locking.
|
||||
*
|
||||
* ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(),
|
||||
* so the SDK's vprintf() path is dead code at runtime. The fprintf()
|
||||
* and printf() calls in SDK components are only in debug/assert paths
|
||||
* (gpio_dump_io_configuration, ringbuf diagnostics) that are either
|
||||
* GC'd or never called. Crash backtraces and panic output are
|
||||
* unaffected — they use esp_rom_printf() which is a ROM function
|
||||
* and does not go through libc.
|
||||
*
|
||||
* These stubs redirect through vsnprintf() (which uses _svfprintf_r
|
||||
* already in the binary) and fwrite(), allowing the linker to
|
||||
* dead-code eliminate _vfprintf_r.
|
||||
*
|
||||
* Saves ~11 KB of flash.
|
||||
*
|
||||
* To disable these wraps, set enable_full_printf: true in the esp32
|
||||
* advanced config section.
|
||||
*/
|
||||
|
||||
#if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF)
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
|
||||
#include "esp_system.h"
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
static constexpr size_t PRINTF_BUFFER_SIZE = 512;
|
||||
|
||||
// These stubs are essentially dead code at runtime — ESPHome replaces the
|
||||
// ESP-IDF log handler, and the SDK's printf/fprintf calls only exist in
|
||||
// debug/assert paths that are never reached in normal operation.
|
||||
// The buffer overflow check is purely defensive and should never trigger.
|
||||
static int write_printf_buffer(FILE *stream, char *buf, int len) {
|
||||
if (len < 0) {
|
||||
return len;
|
||||
}
|
||||
size_t write_len = len;
|
||||
if (write_len >= PRINTF_BUFFER_SIZE) {
|
||||
fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream);
|
||||
esp_system_abort("printf buffer overflow; set enable_full_printf: true in esp32 framework advanced config");
|
||||
}
|
||||
if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) {
|
||||
return -1;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
|
||||
int __wrap_vprintf(const char *fmt, va_list ap) {
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
}
|
||||
|
||||
int __wrap_printf(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
int len = __wrap_vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
return len;
|
||||
}
|
||||
|
||||
int __wrap_fprintf(FILE *stream, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
va_end(ap);
|
||||
return len;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_ESP_IDF && !USE_FULL_PRINTF
|
||||
@@ -21,6 +21,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
||||
@@ -188,6 +189,9 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
|
||||
|
||||
CONF_BLE_ID = "ble_id"
|
||||
CONF_IO_CAPABILITY = "io_capability"
|
||||
CONF_AUTH_REQ_MODE = "auth_req_mode"
|
||||
CONF_MAX_KEY_SIZE = "max_key_size"
|
||||
CONF_MIN_KEY_SIZE = "min_key_size"
|
||||
CONF_ADVERTISING = "advertising"
|
||||
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
|
||||
CONF_DISABLE_BT_LOGS = "disable_bt_logs"
|
||||
@@ -238,6 +242,18 @@ IO_CAPABILITY = {
|
||||
"display_yes_no": IoCapability.IO_CAP_IO,
|
||||
}
|
||||
|
||||
AuthReqMode = esp32_ble_ns.enum("AuthReqMode")
|
||||
AUTH_REQ_MODE = {
|
||||
"no_bond": AuthReqMode.AUTH_REQ_NO_BOND,
|
||||
"bond": AuthReqMode.AUTH_REQ_BOND,
|
||||
"mitm": AuthReqMode.AUTH_REQ_MITM,
|
||||
"bond_mitm": AuthReqMode.AUTH_REQ_BOND_MITM,
|
||||
"sc_only": AuthReqMode.AUTH_REQ_SC_ONLY,
|
||||
"sc_bond": AuthReqMode.AUTH_REQ_SC_BOND,
|
||||
"sc_mitm": AuthReqMode.AUTH_REQ_SC_MITM,
|
||||
"sc_mitm_bond": AuthReqMode.AUTH_REQ_SC_MITM_BOND,
|
||||
}
|
||||
|
||||
esp_power_level_t = cg.global_ns.enum("esp_power_level_t")
|
||||
|
||||
TX_POWER_LEVELS = {
|
||||
@@ -258,6 +274,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum(
|
||||
IO_CAPABILITY, lower=True
|
||||
),
|
||||
# note: no defaults so we can action them not being present
|
||||
cv.Optional(CONF_AUTH_REQ_MODE): cv.enum(AUTH_REQ_MODE, lower=True),
|
||||
cv.Optional(CONF_MAX_KEY_SIZE): cv.int_range(min=7, max=16),
|
||||
cv.Optional(CONF_MIN_KEY_SIZE): cv.int_range(min=7, max=16),
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADVERTISING, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
@@ -279,6 +299,23 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def _validate_key_sizes(config: ConfigType) -> ConfigType:
|
||||
if (
|
||||
CONF_MIN_KEY_SIZE in config
|
||||
and CONF_MAX_KEY_SIZE in config
|
||||
and config[CONF_MIN_KEY_SIZE] > config[CONF_MAX_KEY_SIZE]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"min_key_size ({config[CONF_MIN_KEY_SIZE]}) must be "
|
||||
f"less than or equal to "
|
||||
f"max_key_size ({config[CONF_MAX_KEY_SIZE]})"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes)
|
||||
|
||||
|
||||
bt_uuid16_format = "XXXX"
|
||||
bt_uuid32_format = "XXXXXXXX"
|
||||
bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
@@ -487,6 +524,21 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
|
||||
cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY]))
|
||||
|
||||
if (
|
||||
CONF_AUTH_REQ_MODE in config
|
||||
or CONF_MAX_KEY_SIZE in config
|
||||
or CONF_MIN_KEY_SIZE in config
|
||||
):
|
||||
cg.add_define("ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS", None)
|
||||
|
||||
if CONF_AUTH_REQ_MODE in config:
|
||||
cg.add(var.set_auth_req(config[CONF_AUTH_REQ_MODE]))
|
||||
if CONF_MAX_KEY_SIZE in config:
|
||||
cg.add(var.set_max_key_size(config[CONF_MAX_KEY_SIZE]))
|
||||
if CONF_MIN_KEY_SIZE in config:
|
||||
cg.add(var.set_min_key_size(config[CONF_MIN_KEY_SIZE]))
|
||||
|
||||
cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME]))
|
||||
if (name := config.get(CONF_NAME)) is not None:
|
||||
cg.add(var.set_name(name))
|
||||
|
||||
@@ -296,12 +296,39 @@ bool ESP32BLE::ble_setup_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t));
|
||||
err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(esp_ble_io_cap_t));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err);
|
||||
ESP_LOGE(TAG, "esp_ble_gap_set_security_param iocap_mode failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
if (this->max_key_size_) {
|
||||
err = esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &(this->max_key_size_), sizeof(uint8_t));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_set_security_param max_key_size failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->min_key_size_) {
|
||||
err = esp_ble_gap_set_security_param(ESP_BLE_SM_MIN_KEY_SIZE, &(this->min_key_size_), sizeof(uint8_t));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_set_security_param min_key_size failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->auth_req_mode_) {
|
||||
err = esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &(this->auth_req_mode_.value()),
|
||||
sizeof(esp_ble_auth_req_t));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_set_security_param authen_req_mode failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
|
||||
// BLE takes some time to be fully set up, 200ms should be more than enough
|
||||
delay(200); // NOLINT
|
||||
|
||||
@@ -645,6 +672,7 @@ void ESP32BLE::dump_config() {
|
||||
io_capability_s = "invalid";
|
||||
break;
|
||||
}
|
||||
|
||||
char mac_s[18];
|
||||
format_mac_addr_upper(mac_address, mac_s);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -652,6 +680,48 @@ void ESP32BLE::dump_config() {
|
||||
" MAC address: %s\n"
|
||||
" IO Capability: %s",
|
||||
mac_s, io_capability_s);
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
const char *auth_req_mode_s = "<default>";
|
||||
if (this->auth_req_mode_) {
|
||||
switch (this->auth_req_mode_.value()) {
|
||||
case AUTH_REQ_NO_BOND:
|
||||
auth_req_mode_s = "no_bond";
|
||||
break;
|
||||
case AUTH_REQ_BOND:
|
||||
auth_req_mode_s = "bond";
|
||||
break;
|
||||
case AUTH_REQ_MITM:
|
||||
auth_req_mode_s = "mitm";
|
||||
break;
|
||||
case AUTH_REQ_BOND_MITM:
|
||||
auth_req_mode_s = "bond_mitm";
|
||||
break;
|
||||
case AUTH_REQ_SC_ONLY:
|
||||
auth_req_mode_s = "sc_only";
|
||||
break;
|
||||
case AUTH_REQ_SC_BOND:
|
||||
auth_req_mode_s = "sc_bond";
|
||||
break;
|
||||
case AUTH_REQ_SC_MITM:
|
||||
auth_req_mode_s = "sc_mitm";
|
||||
break;
|
||||
case AUTH_REQ_SC_MITM_BOND:
|
||||
auth_req_mode_s = "sc_mitm_bond";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Auth Req Mode: %s", auth_req_mode_s);
|
||||
if (this->max_key_size_ && this->min_key_size_) {
|
||||
ESP_LOGCONFIG(TAG, " Key Size: %u - %u", this->min_key_size_, this->max_key_size_);
|
||||
} else if (this->max_key_size_) {
|
||||
ESP_LOGCONFIG(TAG, " Key Size: <default> - %u", this->max_key_size_);
|
||||
} else if (this->min_key_size_) {
|
||||
ESP_LOGCONFIG(TAG, " Key Size: %u - <default>", this->min_key_size_);
|
||||
}
|
||||
#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
|
||||
}
|
||||
|
||||
@@ -52,6 +52,19 @@ enum IoCapability {
|
||||
IO_CAP_KBDISP = ESP_IO_CAP_KBDISP,
|
||||
};
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
enum AuthReqMode {
|
||||
AUTH_REQ_NO_BOND = ESP_LE_AUTH_NO_BOND,
|
||||
AUTH_REQ_BOND = ESP_LE_AUTH_BOND,
|
||||
AUTH_REQ_MITM = ESP_LE_AUTH_REQ_MITM,
|
||||
AUTH_REQ_BOND_MITM = ESP_LE_AUTH_REQ_BOND_MITM,
|
||||
AUTH_REQ_SC_ONLY = ESP_LE_AUTH_REQ_SC_ONLY,
|
||||
AUTH_REQ_SC_BOND = ESP_LE_AUTH_REQ_SC_BOND,
|
||||
AUTH_REQ_SC_MITM = ESP_LE_AUTH_REQ_SC_MITM,
|
||||
AUTH_REQ_SC_MITM_BOND = ESP_LE_AUTH_REQ_SC_MITM_BOND,
|
||||
};
|
||||
#endif
|
||||
|
||||
enum BLEComponentState : uint8_t {
|
||||
/** Nothing has been initialized yet. */
|
||||
BLE_COMPONENT_STATE_OFF = 0,
|
||||
@@ -100,6 +113,12 @@ class ESP32BLE : public Component {
|
||||
public:
|
||||
void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; }
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
void set_max_key_size(uint8_t key_size) { this->max_key_size_ = key_size; }
|
||||
void set_min_key_size(uint8_t key_size) { this->min_key_size_ = key_size; }
|
||||
void set_auth_req(AuthReqMode req) { this->auth_req_mode_ = (esp_ble_auth_req_t) req; }
|
||||
#endif
|
||||
|
||||
void set_advertising_cycle_time(uint32_t advertising_cycle_time) {
|
||||
this->advertising_cycle_time_ = advertising_cycle_time;
|
||||
}
|
||||
@@ -209,6 +228,13 @@ class ESP32BLE : public Component {
|
||||
// 1-byte aligned members (grouped together to minimize padding)
|
||||
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
|
||||
bool enable_on_boot_{}; // 1 byte
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
optional<esp_ble_auth_req_t> auth_req_mode_;
|
||||
|
||||
uint8_t max_key_size_{0}; // range is 7..16, 0 is unset
|
||||
uint8_t min_key_size_{0}; // range is 7..16, 0 is unset
|
||||
#endif
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -24,6 +24,7 @@ from esphome.const import (
|
||||
__version__ as ESPHOME_VERSION,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
|
||||
AUTO_LOAD = ["esp32_ble", "bytebuffer"]
|
||||
@@ -42,6 +43,7 @@ CONF_FIRMWARE_VERSION = "firmware_version"
|
||||
CONF_INDICATE = "indicate"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
CONF_MANUFACTURER_DATA = "manufacturer_data"
|
||||
CONF_MAX_CLIENTS = "max_clients"
|
||||
CONF_ON_WRITE = "on_write"
|
||||
CONF_READ = "read"
|
||||
CONF_STRING = "string"
|
||||
@@ -287,6 +289,22 @@ def create_device_information_service(config):
|
||||
|
||||
|
||||
def final_validate_config(config):
|
||||
# Validate max_clients does not exceed esp32_ble max_connections
|
||||
max_clients = config[CONF_MAX_CLIENTS]
|
||||
if max_clients > 1:
|
||||
full_config = fv.full_config.get()
|
||||
ble_config = full_config.get("esp32_ble", {})
|
||||
max_connections = ble_config.get(
|
||||
"max_connections", esp32_ble.DEFAULT_MAX_CONNECTIONS
|
||||
)
|
||||
if max_clients > max_connections:
|
||||
raise cv.Invalid(
|
||||
f"'max_clients' ({max_clients}) cannot exceed esp32_ble "
|
||||
f"'max_connections' ({max_connections}). "
|
||||
f"Please set 'max_connections: {max_clients}' in the "
|
||||
f"'esp32_ble' component."
|
||||
)
|
||||
|
||||
# Check if all characteristics that require notifications have the notify property set
|
||||
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()):
|
||||
# Look for the characteristic in the configuration
|
||||
@@ -428,6 +446,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_MODEL): value_schema("string", templatable=False),
|
||||
cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False),
|
||||
cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]),
|
||||
cv.Optional(CONF_MAX_CLIENTS, default=1): cv.int_range(min=1, max=9),
|
||||
cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA),
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
|
||||
@@ -552,6 +571,7 @@ async def to_code(config):
|
||||
esp32_ble.register_ble_status_event_handler(parent, var)
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
|
||||
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
|
||||
if CONF_MANUFACTURER_DATA in config:
|
||||
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
|
||||
for service_config in config[CONF_SERVICES]:
|
||||
|
||||
@@ -175,6 +175,10 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
|
||||
case ESP_GATTS_CONNECT_EVT: {
|
||||
ESP_LOGD(TAG, "BLE Client connected");
|
||||
this->add_client_(param->connect.conn_id);
|
||||
// Resume advertising so additional clients can discover and connect
|
||||
if (this->client_count_ < this->max_clients_) {
|
||||
this->parent_->advertising_start();
|
||||
}
|
||||
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
|
||||
break;
|
||||
}
|
||||
@@ -241,7 +245,12 @@ void BLEServer::ble_before_disabled_event_handler() {
|
||||
|
||||
float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; }
|
||||
|
||||
void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); }
|
||||
void BLEServer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ESP32 BLE Server:\n"
|
||||
" Max clients: %u",
|
||||
this->max_clients_);
|
||||
}
|
||||
|
||||
BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
||||
this->restart_advertising_();
|
||||
}
|
||||
|
||||
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
|
||||
uint8_t get_max_clients() const { return this->max_clients_; }
|
||||
|
||||
BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15);
|
||||
void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0);
|
||||
BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0);
|
||||
@@ -95,6 +98,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
||||
|
||||
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
|
||||
uint8_t client_count_{0};
|
||||
uint8_t max_clients_{1};
|
||||
std::vector<ServiceEntry> services_{};
|
||||
std::vector<BLEService *> services_to_start_{};
|
||||
BLEService *device_information_service_{};
|
||||
|
||||
@@ -23,6 +23,7 @@ CONF_D1_PIN = "d1_pin"
|
||||
CONF_D2_PIN = "d2_pin"
|
||||
CONF_D3_PIN = "d3_pin"
|
||||
CONF_SLOT = "slot"
|
||||
CONF_SDIO_FREQUENCY = "sdio_frequency"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
@@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1),
|
||||
cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All(
|
||||
cv.frequency, cv.Range(min=400e3, max=50e6)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -91,6 +95,10 @@ async def to_code(config):
|
||||
config[CONF_D3_PIN],
|
||||
)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ",
|
||||
int(config[CONF_SDIO_FREQUENCY] // 1000),
|
||||
)
|
||||
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
@@ -21,6 +24,8 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["binary_sensor"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
@@ -37,135 +42,161 @@ CONF_WATERPROOF_SHIELD_DRIVER = "waterproof_shield_driver"
|
||||
esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch")
|
||||
ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component)
|
||||
|
||||
# Channel ID mappings: GPIO pin number -> integer channel ID
|
||||
# These are plain integers - the new unified API uses int chan_id directly.
|
||||
TOUCH_PADS = {
|
||||
VARIANT_ESP32: {
|
||||
4: cg.global_ns.TOUCH_PAD_NUM0,
|
||||
0: cg.global_ns.TOUCH_PAD_NUM1,
|
||||
2: cg.global_ns.TOUCH_PAD_NUM2,
|
||||
15: cg.global_ns.TOUCH_PAD_NUM3,
|
||||
13: cg.global_ns.TOUCH_PAD_NUM4,
|
||||
12: cg.global_ns.TOUCH_PAD_NUM5,
|
||||
14: cg.global_ns.TOUCH_PAD_NUM6,
|
||||
27: cg.global_ns.TOUCH_PAD_NUM7,
|
||||
33: cg.global_ns.TOUCH_PAD_NUM8,
|
||||
32: cg.global_ns.TOUCH_PAD_NUM9,
|
||||
4: 0,
|
||||
0: 1,
|
||||
2: 2,
|
||||
15: 3,
|
||||
13: 4,
|
||||
12: 5,
|
||||
14: 6,
|
||||
27: 7,
|
||||
33: 8,
|
||||
32: 9,
|
||||
},
|
||||
VARIANT_ESP32S2: {
|
||||
1: cg.global_ns.TOUCH_PAD_NUM1,
|
||||
2: cg.global_ns.TOUCH_PAD_NUM2,
|
||||
3: cg.global_ns.TOUCH_PAD_NUM3,
|
||||
4: cg.global_ns.TOUCH_PAD_NUM4,
|
||||
5: cg.global_ns.TOUCH_PAD_NUM5,
|
||||
6: cg.global_ns.TOUCH_PAD_NUM6,
|
||||
7: cg.global_ns.TOUCH_PAD_NUM7,
|
||||
8: cg.global_ns.TOUCH_PAD_NUM8,
|
||||
9: cg.global_ns.TOUCH_PAD_NUM9,
|
||||
10: cg.global_ns.TOUCH_PAD_NUM10,
|
||||
11: cg.global_ns.TOUCH_PAD_NUM11,
|
||||
12: cg.global_ns.TOUCH_PAD_NUM12,
|
||||
13: cg.global_ns.TOUCH_PAD_NUM13,
|
||||
14: cg.global_ns.TOUCH_PAD_NUM14,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 4,
|
||||
5: 5,
|
||||
6: 6,
|
||||
7: 7,
|
||||
8: 8,
|
||||
9: 9,
|
||||
10: 10,
|
||||
11: 11,
|
||||
12: 12,
|
||||
13: 13,
|
||||
14: 14,
|
||||
},
|
||||
VARIANT_ESP32S3: {
|
||||
1: cg.global_ns.TOUCH_PAD_NUM1,
|
||||
2: cg.global_ns.TOUCH_PAD_NUM2,
|
||||
3: cg.global_ns.TOUCH_PAD_NUM3,
|
||||
4: cg.global_ns.TOUCH_PAD_NUM4,
|
||||
5: cg.global_ns.TOUCH_PAD_NUM5,
|
||||
6: cg.global_ns.TOUCH_PAD_NUM6,
|
||||
7: cg.global_ns.TOUCH_PAD_NUM7,
|
||||
8: cg.global_ns.TOUCH_PAD_NUM8,
|
||||
9: cg.global_ns.TOUCH_PAD_NUM9,
|
||||
10: cg.global_ns.TOUCH_PAD_NUM10,
|
||||
11: cg.global_ns.TOUCH_PAD_NUM11,
|
||||
12: cg.global_ns.TOUCH_PAD_NUM12,
|
||||
13: cg.global_ns.TOUCH_PAD_NUM13,
|
||||
14: cg.global_ns.TOUCH_PAD_NUM14,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 4,
|
||||
5: 5,
|
||||
6: 6,
|
||||
7: 7,
|
||||
8: 8,
|
||||
9: 9,
|
||||
10: 10,
|
||||
11: 11,
|
||||
12: 12,
|
||||
13: 13,
|
||||
14: 14,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
2: 1,
|
||||
3: 2,
|
||||
4: 3,
|
||||
5: 4,
|
||||
6: 5,
|
||||
7: 6,
|
||||
8: 7,
|
||||
9: 8,
|
||||
10: 9,
|
||||
11: 10,
|
||||
12: 11,
|
||||
13: 12,
|
||||
14: 13,
|
||||
15: 14,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
TOUCH_PAD_DENOISE_GRADE = {
|
||||
"BIT12": cg.global_ns.TOUCH_PAD_DENOISE_BIT12,
|
||||
"BIT10": cg.global_ns.TOUCH_PAD_DENOISE_BIT10,
|
||||
"BIT8": cg.global_ns.TOUCH_PAD_DENOISE_BIT8,
|
||||
"BIT4": cg.global_ns.TOUCH_PAD_DENOISE_BIT4,
|
||||
"BIT12": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT12,
|
||||
"BIT10": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT10,
|
||||
"BIT8": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT8,
|
||||
"BIT4": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT4,
|
||||
}
|
||||
|
||||
TOUCH_PAD_DENOISE_CAP_LEVEL = {
|
||||
"L0": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L0,
|
||||
"L1": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L1,
|
||||
"L2": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L2,
|
||||
"L3": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L3,
|
||||
"L4": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L4,
|
||||
"L5": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L5,
|
||||
"L6": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L6,
|
||||
"L7": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L7,
|
||||
"L0": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_5PF,
|
||||
"L1": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_6PF,
|
||||
"L2": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_7PF,
|
||||
"L3": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_9PF,
|
||||
"L4": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_10PF,
|
||||
"L5": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_12PF,
|
||||
"L6": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_13PF,
|
||||
"L7": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_14PF,
|
||||
}
|
||||
|
||||
TOUCH_PAD_FILTER_MODE = {
|
||||
"IIR_4": cg.global_ns.TOUCH_PAD_FILTER_IIR_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_PAD_FILTER_IIR_8,
|
||||
"IIR_16": cg.global_ns.TOUCH_PAD_FILTER_IIR_16,
|
||||
"IIR_32": cg.global_ns.TOUCH_PAD_FILTER_IIR_32,
|
||||
"IIR_64": cg.global_ns.TOUCH_PAD_FILTER_IIR_64,
|
||||
"IIR_128": cg.global_ns.TOUCH_PAD_FILTER_IIR_128,
|
||||
"IIR_256": cg.global_ns.TOUCH_PAD_FILTER_IIR_256,
|
||||
"JITTER": cg.global_ns.TOUCH_PAD_FILTER_JITTER,
|
||||
"IIR_4": cg.global_ns.TOUCH_BM_IIR_FILTER_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_BM_IIR_FILTER_8,
|
||||
"IIR_16": cg.global_ns.TOUCH_BM_IIR_FILTER_16,
|
||||
"IIR_32": cg.global_ns.TOUCH_BM_IIR_FILTER_32,
|
||||
"IIR_64": cg.global_ns.TOUCH_BM_IIR_FILTER_64,
|
||||
"IIR_128": cg.global_ns.TOUCH_BM_IIR_FILTER_128,
|
||||
"IIR_256": cg.global_ns.TOUCH_BM_IIR_FILTER_256,
|
||||
"JITTER": cg.global_ns.TOUCH_BM_JITTER_FILTER,
|
||||
}
|
||||
|
||||
TOUCH_PAD_SMOOTH_MODE = {
|
||||
"OFF": cg.global_ns.TOUCH_PAD_SMOOTH_OFF,
|
||||
"IIR_2": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_2,
|
||||
"IIR_4": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_8,
|
||||
"OFF": cg.global_ns.TOUCH_SMOOTH_NO_FILTER,
|
||||
"IIR_2": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_2,
|
||||
"IIR_4": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_8,
|
||||
}
|
||||
|
||||
LOW_VOLTAGE_REFERENCE = {
|
||||
"0.5V": cg.global_ns.TOUCH_LVOLT_0V5,
|
||||
"0.6V": cg.global_ns.TOUCH_LVOLT_0V6,
|
||||
"0.7V": cg.global_ns.TOUCH_LVOLT_0V7,
|
||||
"0.8V": cg.global_ns.TOUCH_LVOLT_0V8,
|
||||
"0.5V": cg.global_ns.TOUCH_VOLT_LIM_L_0V5,
|
||||
"0.6V": cg.global_ns.TOUCH_VOLT_LIM_L_0V6,
|
||||
"0.7V": cg.global_ns.TOUCH_VOLT_LIM_L_0V7,
|
||||
"0.8V": cg.global_ns.TOUCH_VOLT_LIM_L_0V8,
|
||||
}
|
||||
HIGH_VOLTAGE_REFERENCE = {
|
||||
"2.4V": cg.global_ns.TOUCH_HVOLT_2V4,
|
||||
"2.5V": cg.global_ns.TOUCH_HVOLT_2V5,
|
||||
"2.6V": cg.global_ns.TOUCH_HVOLT_2V6,
|
||||
"2.7V": cg.global_ns.TOUCH_HVOLT_2V7,
|
||||
"2.4V": cg.global_ns.TOUCH_VOLT_LIM_H_2V4,
|
||||
"2.5V": cg.global_ns.TOUCH_VOLT_LIM_H_2V5,
|
||||
"2.6V": cg.global_ns.TOUCH_VOLT_LIM_H_2V6,
|
||||
"2.7V": cg.global_ns.TOUCH_VOLT_LIM_H_2V7,
|
||||
}
|
||||
VOLTAGE_ATTENUATION = {
|
||||
"1.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V5,
|
||||
"1V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V,
|
||||
"0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5,
|
||||
"0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V,
|
||||
}
|
||||
TOUCH_PAD_WATERPROOF_SHIELD_DRIVER = {
|
||||
"L0": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L0,
|
||||
"L1": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L1,
|
||||
"L2": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L2,
|
||||
"L3": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L3,
|
||||
"L4": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L4,
|
||||
"L5": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L5,
|
||||
"L6": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L6,
|
||||
"L7": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L7,
|
||||
VOLTAGE_ATTENUATION = {"1.5V", "1V", "0.5V", "0V"}
|
||||
|
||||
# ESP32 V1: The new API's touch_volt_lim_h_t combines the old high_voltage_reference
|
||||
# and voltage_attenuation into a single enum representing the effective upper voltage.
|
||||
# Effective voltage = high_voltage_reference - voltage_attenuation
|
||||
EFFECTIVE_HIGH_VOLTAGE = {
|
||||
("2.4V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_0V9,
|
||||
("2.5V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V0,
|
||||
("2.6V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V1,
|
||||
("2.7V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V2,
|
||||
("2.4V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V4,
|
||||
("2.5V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V5,
|
||||
("2.6V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V6,
|
||||
("2.7V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V7,
|
||||
("2.4V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V9,
|
||||
("2.5V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V0,
|
||||
("2.6V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V1,
|
||||
("2.7V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V2,
|
||||
("2.4V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V4,
|
||||
("2.5V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V5,
|
||||
("2.6V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V6,
|
||||
("2.7V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V7,
|
||||
}
|
||||
|
||||
|
||||
def validate_touch_pad(value):
|
||||
value = gpio.gpio_pin_number_validator(value)
|
||||
variant = get_esp32_variant()
|
||||
if variant not in TOUCH_PADS:
|
||||
pads = TOUCH_PADS.get(variant)
|
||||
if pads is None:
|
||||
raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.")
|
||||
|
||||
pads = TOUCH_PADS[variant]
|
||||
if value not in pads:
|
||||
raise cv.Invalid(f"Pin {value} does not support touch pads.")
|
||||
return cv.enum(pads)(value)
|
||||
return pads[value] # Return integer channel ID
|
||||
|
||||
|
||||
def validate_variant_vars(config):
|
||||
if get_esp32_variant() == VARIANT_ESP32:
|
||||
variant_vars = {
|
||||
variant = get_esp32_variant()
|
||||
invalid_vars = set()
|
||||
if variant == VARIANT_ESP32:
|
||||
invalid_vars = {
|
||||
CONF_DEBOUNCE_COUNT,
|
||||
CONF_DENOISE_GRADE,
|
||||
CONF_DENOISE_CAP_LEVEL,
|
||||
@@ -176,15 +207,14 @@ def validate_variant_vars(config):
|
||||
CONF_WATERPROOF_GUARD_RING,
|
||||
CONF_WATERPROOF_SHIELD_DRIVER,
|
||||
}
|
||||
for vvar in variant_vars:
|
||||
if vvar in config:
|
||||
raise cv.Invalid(f"{vvar} is not valid on {VARIANT_ESP32}")
|
||||
elif (
|
||||
get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3
|
||||
) and CONF_IIR_FILTER in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_IIR_FILTER} is not valid on {VARIANT_ESP32S2} or {VARIANT_ESP32S3}"
|
||||
)
|
||||
elif variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4):
|
||||
invalid_vars = {CONF_IIR_FILTER}
|
||||
if variant == VARIANT_ESP32P4:
|
||||
invalid_vars |= {CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL}
|
||||
unsupported = invalid_vars.intersection(config)
|
||||
if unsupported:
|
||||
keys = ", ".join(sorted(f"'{k}'" for k in unsupported))
|
||||
raise cv.Invalid(f"{keys} not valid on {variant}")
|
||||
|
||||
return config
|
||||
|
||||
@@ -219,12 +249,17 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage(
|
||||
HIGH_VOLTAGE_REFERENCE
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage(
|
||||
VOLTAGE_ATTENUATION
|
||||
),
|
||||
# ESP32 V1 only: attenuates the high voltage reference
|
||||
cv.SplitDefault(
|
||||
CONF_VOLTAGE_ATTENUATION,
|
||||
esp32="0V",
|
||||
esp32_s2=cv.UNDEFINED,
|
||||
esp32_s3=cv.UNDEFINED,
|
||||
esp32_p4=cv.UNDEFINED,
|
||||
): validate_voltage(VOLTAGE_ATTENUATION),
|
||||
# ESP32 only
|
||||
cv.Optional(CONF_IIR_FILTER): cv.positive_time_period_milliseconds,
|
||||
# ESP32-S2/S3 only
|
||||
# ESP32-S2/S3/P4 only
|
||||
cv.Optional(CONF_DEBOUNCE_COUNT): cv.int_range(min=0, max=7),
|
||||
cv.Optional(CONF_FILTER_MODE): cv.enum(
|
||||
TOUCH_PAD_FILTER_MODE, upper=True, space="_"
|
||||
@@ -241,9 +276,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
TOUCH_PAD_DENOISE_CAP_LEVEL, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_WATERPROOF_GUARD_RING): validate_touch_pad,
|
||||
cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.enum(
|
||||
TOUCH_PAD_WATERPROOF_SHIELD_DRIVER, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.int_range(min=0, max=7),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL),
|
||||
@@ -260,6 +293,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp32.VARIANT_ESP32,
|
||||
esp32.VARIANT_ESP32S2,
|
||||
esp32.VARIANT_ESP32S3,
|
||||
esp32.VARIANT_ESP32P4,
|
||||
]
|
||||
),
|
||||
validate_variant_vars,
|
||||
@@ -267,44 +301,67 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
|
||||
# New unified touch sensor driver
|
||||
include_builtin_idf_component("esp_driver_touch_sens")
|
||||
# Legacy driver component provides driver/touch_sensor.h header
|
||||
include_builtin_idf_component("driver")
|
||||
|
||||
touch = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(touch, config)
|
||||
|
||||
cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE]))
|
||||
|
||||
sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15))
|
||||
cg.add(touch.set_sleep_duration(sleep_duration))
|
||||
# sleep_duration -> meas_interval_us (pass microseconds directly)
|
||||
cg.add(touch.set_meas_interval_us(config[CONF_SLEEP_DURATION].total_microseconds))
|
||||
|
||||
measurement_duration = int(
|
||||
round(config[CONF_MEASUREMENT_DURATION].total_microseconds * 7.99987793)
|
||||
)
|
||||
cg.add(touch.set_measurement_duration(measurement_duration))
|
||||
variant = get_esp32_variant()
|
||||
|
||||
cg.add(
|
||||
touch.set_low_voltage_reference(
|
||||
LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]]
|
||||
# measurement_duration handling differs per variant
|
||||
if variant == VARIANT_ESP32:
|
||||
# V1: charge_duration_ms (convert from microseconds to milliseconds)
|
||||
charge_duration_ms = (
|
||||
config[CONF_MEASUREMENT_DURATION].total_microseconds / 1000.0
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
touch.set_high_voltage_reference(
|
||||
HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]]
|
||||
cg.add(touch.set_charge_duration_ms(charge_duration_ms))
|
||||
else:
|
||||
# V2/V3: charge_times (approximate conversion from duration)
|
||||
# The old API used clock cycles; the new API uses charge_times count.
|
||||
# Default is 500 for V2/V3. Use measurement_duration as a rough scaling factor.
|
||||
# 65535 / 8192 ≈ 7.9999 maps the microsecond duration to charge_times.
|
||||
charge_times = int(
|
||||
round(config[CONF_MEASUREMENT_DURATION].total_microseconds * (65535 / 8192))
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
touch.set_voltage_attenuation(
|
||||
VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]]
|
||||
)
|
||||
)
|
||||
charge_times = max(charge_times, 1)
|
||||
cg.add(touch.set_charge_times(charge_times))
|
||||
|
||||
if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config:
|
||||
# Voltage references (not applicable to P4)
|
||||
if variant != VARIANT_ESP32P4:
|
||||
if CONF_LOW_VOLTAGE_REFERENCE in config:
|
||||
cg.add(
|
||||
touch.set_low_voltage_reference(
|
||||
LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]]
|
||||
)
|
||||
)
|
||||
if CONF_HIGH_VOLTAGE_REFERENCE in config:
|
||||
if variant == VARIANT_ESP32:
|
||||
# V1: combine high_voltage_reference with voltage_attenuation
|
||||
high_ref = config[CONF_HIGH_VOLTAGE_REFERENCE]
|
||||
atten = config[CONF_VOLTAGE_ATTENUATION]
|
||||
cg.add(
|
||||
touch.set_high_voltage_reference(
|
||||
EFFECTIVE_HIGH_VOLTAGE[(high_ref, atten)]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# V2/V3: no attenuation concept, use directly
|
||||
cg.add(
|
||||
touch.set_high_voltage_reference(
|
||||
HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]]
|
||||
)
|
||||
)
|
||||
|
||||
if variant == VARIANT_ESP32 and CONF_IIR_FILTER in config:
|
||||
cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
|
||||
if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3:
|
||||
if variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4):
|
||||
if CONF_FILTER_MODE in config:
|
||||
cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE]))
|
||||
if CONF_DEBOUNCE_COUNT in config:
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::esp32_touch {
|
||||
|
||||
template<size_t N> static const char *lookup_str(const char *const (&table)[N], size_t index) {
|
||||
return (index < N) ? table[index] : "UNKNOWN";
|
||||
}
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
static constexpr uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250;
|
||||
static constexpr uint32_t INITIAL_STATE_DELAY_MS = 1500;
|
||||
static constexpr uint32_t ONESHOT_SCAN_COUNT = 3;
|
||||
static constexpr uint32_t ONESHOT_SCAN_TIMEOUT_MS = 2000;
|
||||
|
||||
// V1: called from esp_timer context (software filter)
|
||||
// V2/V3: called from ISR context
|
||||
// xQueueSendFromISR is safe from both contexts.
|
||||
|
||||
bool IRAM_ATTR ESP32TouchComponent::on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event,
|
||||
void *ctx) {
|
||||
auto *comp = static_cast<ESP32TouchComponent *>(ctx);
|
||||
TouchEvent te{event->chan_id, true};
|
||||
BaseType_t higher = pdFALSE;
|
||||
xQueueSendFromISR(comp->touch_queue_, &te, &higher);
|
||||
comp->enable_loop_soon_any_context();
|
||||
return higher == pdTRUE;
|
||||
}
|
||||
|
||||
bool IRAM_ATTR ESP32TouchComponent::on_inactive_cb(touch_sensor_handle_t handle,
|
||||
const touch_inactive_event_data_t *event, void *ctx) {
|
||||
auto *comp = static_cast<ESP32TouchComponent *>(ctx);
|
||||
TouchEvent te{event->chan_id, false};
|
||||
BaseType_t higher = pdFALSE;
|
||||
xQueueSendFromISR(comp->touch_queue_, &te, &higher);
|
||||
comp->enable_loop_soon_any_context();
|
||||
return higher == pdTRUE;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create sample config - differs per hardware version
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V1_DEFAULT_SAMPLE_CONFIG(
|
||||
this->charge_duration_ms_, this->low_voltage_reference_, this->high_voltage_reference_);
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
// div_num=8 (data scaling divisor), coarse_freq_tune=2, fine_freq_tune=2
|
||||
touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG(8, 2, 2);
|
||||
sample_cfg.charge_times = this->charge_times_;
|
||||
#else
|
||||
// ESP32-S2/S3 (V2)
|
||||
touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V2_DEFAULT_SAMPLE_CONFIG(
|
||||
this->charge_times_, this->low_voltage_reference_, this->high_voltage_reference_);
|
||||
#endif
|
||||
|
||||
// Create controller
|
||||
touch_sensor_config_t sens_cfg = TOUCH_SENSOR_DEFAULT_BASIC_CONFIG(1, &sample_cfg);
|
||||
sens_cfg.meas_interval_us = this->meas_interval_us_;
|
||||
#ifndef USE_ESP32_VARIANT_ESP32
|
||||
sens_cfg.max_meas_time_us = 0; // Disable measurement timeout (V2/V3 only)
|
||||
#endif
|
||||
|
||||
esp_err_t err = touch_sensor_new_controller(&sens_cfg, &this->sens_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create touch controller: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create channels for all children
|
||||
for (auto *child : this->children_) {
|
||||
touch_channel_config_t chan_cfg = {};
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
chan_cfg.abs_active_thresh[0] = child->get_threshold();
|
||||
chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
chan_cfg.group = TOUCH_CHAN_TRIG_GROUP_BOTH;
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
chan_cfg.active_thresh[0] = child->get_threshold();
|
||||
#else
|
||||
// ESP32-S2/S3 (V2)
|
||||
chan_cfg.active_thresh[0] = child->get_threshold();
|
||||
chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
#endif
|
||||
|
||||
err = touch_sensor_new_channel(this->sens_handle_, child->get_channel_id(), &chan_cfg, &child->chan_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create touch channel %d: %s", child->get_channel_id(), esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Configure filter
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// Software filter is REQUIRED for V1 on_active/on_inactive callbacks
|
||||
{
|
||||
touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG();
|
||||
if (this->iir_filter_enabled_()) {
|
||||
filter_cfg.interval_ms = this->iir_filter_;
|
||||
}
|
||||
err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to configure filter: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// V2/V3: Hardware benchmark filter
|
||||
{
|
||||
touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG();
|
||||
if (this->filter_configured_) {
|
||||
filter_cfg.benchmark.filter_mode = this->filter_mode_;
|
||||
filter_cfg.benchmark.jitter_step = this->jitter_step_;
|
||||
filter_cfg.benchmark.denoise_lvl = this->noise_threshold_;
|
||||
filter_cfg.data.smooth_filter = this->smooth_level_;
|
||||
filter_cfg.data.debounce_cnt = this->debounce_count_;
|
||||
}
|
||||
err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure filter: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
if (this->denoise_configured_) {
|
||||
touch_denoise_chan_config_t denoise_cfg = {};
|
||||
denoise_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
denoise_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
denoise_cfg.ref_cap = this->denoise_cap_level_;
|
||||
denoise_cfg.resolution = this->denoise_grade_;
|
||||
err = touch_sensor_config_denoise_channel(this->sens_handle_, &denoise_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure denoise: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_WATERPROOF
|
||||
if (this->waterproof_configured_) {
|
||||
touch_channel_handle_t guard_chan = nullptr;
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_channel_id() == this->waterproof_guard_ring_pad_) {
|
||||
guard_chan = child->chan_handle_;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
touch_channel_handle_t shield_chan = nullptr;
|
||||
touch_channel_config_t shield_cfg = {};
|
||||
#ifdef USE_ESP32_VARIANT_ESP32P4
|
||||
shield_cfg.active_thresh[0] = 0;
|
||||
err = touch_sensor_new_channel(this->sens_handle_, SOC_TOUCH_MAX_CHAN_ID, &shield_cfg, &shield_chan);
|
||||
#else
|
||||
shield_cfg.active_thresh[0] = 0;
|
||||
shield_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
shield_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
err = touch_sensor_new_channel(this->sens_handle_, TOUCH_SHIELD_CHAN_ID, &shield_cfg, &shield_chan);
|
||||
#endif
|
||||
if (err == ESP_OK) {
|
||||
touch_waterproof_config_t wp_cfg = {};
|
||||
wp_cfg.guard_chan = guard_chan;
|
||||
wp_cfg.shield_chan = shield_chan;
|
||||
wp_cfg.shield_drv = this->waterproof_shield_driver_;
|
||||
wp_cfg.flags.immersion_proof = 1;
|
||||
err = touch_sensor_config_waterproof(this->sens_handle_, &wp_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure waterproof: %s", esp_err_to_name(err));
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to create shield channel: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Configure wakeup pads before enabling (must be done in INIT state)
|
||||
this->configure_wakeup_pads_();
|
||||
|
||||
// Register callbacks
|
||||
touch_event_callbacks_t cbs = {};
|
||||
cbs.on_active = on_active_cb;
|
||||
cbs.on_inactive = on_inactive_cb;
|
||||
err = touch_sensor_register_callbacks(this->sens_handle_, &cbs, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register callbacks: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable and start scanning
|
||||
err = touch_sensor_enable(this->sens_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to enable touch sensor: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Do initial oneshot scans to populate baseline values
|
||||
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
|
||||
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Oneshot scan %d failed: %s", i, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
err = touch_sensor_start_continuous_scanning(this->sens_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start continuous scanning: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
static constexpr const char *LV_STRS[] = {"0.5V", "0.6V", "0.7V", "0.8V"};
|
||||
static constexpr const char *HV_STRS[] = {"0.9V", "1.0V", "1.1V", "1.2V", "1.4V", "1.5V", "1.6V", "1.7V",
|
||||
"1.9V", "2.0V", "2.1V", "2.2V", "2.4V", "2.5V", "2.6V", "2.7V"};
|
||||
const char *lv_s = lookup_str(LV_STRS, this->low_voltage_reference_);
|
||||
const char *hv_s = lookup_str(HV_STRS, this->high_voltage_reference_);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Measurement interval: %.1fus\n"
|
||||
" Low Voltage Reference: %s\n"
|
||||
" High Voltage Reference: %s",
|
||||
this->meas_interval_us_, lv_s, hv_s);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Measurement interval: %.1fus",
|
||||
this->meas_interval_us_);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
if (this->iir_filter_enabled_()) {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: 10ms (default)");
|
||||
}
|
||||
#else
|
||||
if (this->filter_configured_) {
|
||||
// TOUCH_BM_IIR_FILTER_256 only exists on V2, shifting JITTER's position
|
||||
static constexpr const char *FILTER_STRS[] = {
|
||||
"IIR_4",
|
||||
"IIR_8",
|
||||
"IIR_16",
|
||||
"IIR_32",
|
||||
"IIR_64",
|
||||
"IIR_128",
|
||||
#if SOC_TOUCH_SENSOR_VERSION == 2
|
||||
"IIR_256",
|
||||
#endif
|
||||
"JITTER",
|
||||
};
|
||||
static constexpr const char *SMOOTH_STRS[] = {"OFF", "IIR_2", "IIR_4", "IIR_8"};
|
||||
const char *filter_s = lookup_str(FILTER_STRS, this->filter_mode_);
|
||||
const char *smooth_s = lookup_str(SMOOTH_STRS, this->smooth_level_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Filter mode: %s\n"
|
||||
" Debounce count: %" PRIu32 "\n"
|
||||
" Noise threshold coefficient: %" PRIu32 "\n"
|
||||
" Jitter filter step size: %" PRIu32 "\n"
|
||||
" Smooth level: %s",
|
||||
filter_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_, smooth_s);
|
||||
}
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
if (this->denoise_configured_) {
|
||||
static constexpr const char *GRADE_STRS[] = {"BIT12", "BIT10", "BIT8", "BIT4"};
|
||||
static constexpr const char *CAP_STRS[] = {"5pF", "6.4pF", "7.8pF", "9.2pF", "10.6pF", "12pF", "13.4pF", "14.8pF"};
|
||||
const char *grade_s = lookup_str(GRADE_STRS, this->denoise_grade_);
|
||||
const char *cap_s = lookup_str(CAP_STRS, this->denoise_cap_level_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Denoise grade: %s\n"
|
||||
" Denoise capacitance level: %s",
|
||||
grade_s, cap_s);
|
||||
}
|
||||
#endif
|
||||
#endif // !USE_ESP32_VARIANT_ESP32
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
LOG_BINARY_SENSOR(" ", "Touch Pad", child);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Channel: %d\n"
|
||||
" Threshold: %" PRIu32 "\n"
|
||||
" Benchmark: %" PRIu32,
|
||||
child->channel_id_, child->threshold_, child->benchmark_);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// In setup mode, periodically log all pad values
|
||||
this->process_setup_mode_logging_(now);
|
||||
|
||||
// Process queued touch events from callbacks
|
||||
TouchEvent event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_channel_id() != event.chan_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read current smooth value
|
||||
uint32_t value = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &value);
|
||||
child->value_ = value;
|
||||
|
||||
#ifndef USE_ESP32_VARIANT_ESP32
|
||||
// V2/V3: also read benchmark
|
||||
uint32_t benchmark = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark);
|
||||
child->benchmark_ = benchmark;
|
||||
#endif
|
||||
|
||||
bool new_state = event.is_active;
|
||||
|
||||
if (new_state != child->last_state_) {
|
||||
child->initial_state_published_ = true;
|
||||
child->last_state_ = new_state;
|
||||
child->publish_state(new_state);
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 ", threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), ONOFF(new_state), value, child->get_threshold());
|
||||
#else
|
||||
if (new_state) {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 ", benchmark: %" PRIu32 ", threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), value, benchmark, child->get_threshold());
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish initial OFF state for sensors that haven't received events yet
|
||||
for (auto *child : this->children_) {
|
||||
this->publish_initial_state_if_needed_(child, now);
|
||||
}
|
||||
|
||||
if (!this->setup_mode_) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
if (this->sens_handle_ == nullptr)
|
||||
return;
|
||||
|
||||
touch_sensor_stop_continuous_scanning(this->sens_handle_);
|
||||
touch_sensor_disable(this->sens_handle_);
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
if (child->chan_handle_ != nullptr) {
|
||||
touch_sensor_del_channel(child->chan_handle_);
|
||||
child->chan_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
touch_sensor_del_controller(this->sens_handle_);
|
||||
this->sens_handle_ = nullptr;
|
||||
|
||||
this->cleanup_touch_queue_();
|
||||
}
|
||||
|
||||
bool ESP32TouchComponent::create_touch_queue_() {
|
||||
size_t queue_size = this->children_.size() * 4;
|
||||
if (queue_size < 8)
|
||||
queue_size = 8;
|
||||
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchEvent));
|
||||
|
||||
if (this->touch_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::cleanup_touch_queue_() {
|
||||
if (this->touch_queue_) {
|
||||
vQueueDelete(this->touch_queue_);
|
||||
this->touch_queue_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::configure_wakeup_pads_() {
|
||||
#if SOC_TOUCH_SUPPORT_SLEEP_WAKEUP
|
||||
bool has_wakeup = false;
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
has_wakeup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_wakeup)
|
||||
return;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// V1: Simple sleep config - threshold is set via channel config's abs_active_thresh
|
||||
touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG();
|
||||
sleep_cfg.deep_slp_sens_cfg = nullptr;
|
||||
esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err));
|
||||
}
|
||||
#else
|
||||
// V2/V3: Need to specify a deep sleep channel and threshold
|
||||
touch_channel_handle_t wakeup_chan = nullptr;
|
||||
uint32_t wakeup_thresh = 0;
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
wakeup_chan = child->chan_handle_;
|
||||
wakeup_thresh = child->get_wakeup_threshold();
|
||||
break; // Only one deep sleep wakeup channel is supported
|
||||
}
|
||||
}
|
||||
|
||||
if (wakeup_chan != nullptr) {
|
||||
touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG();
|
||||
sleep_cfg.deep_slp_chan = wakeup_chan;
|
||||
sleep_cfg.deep_slp_thresh[0] = wakeup_thresh;
|
||||
sleep_cfg.deep_slp_sens_cfg = nullptr;
|
||||
esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif // SOC_TOUCH_SUPPORT_SLEEP_WAKEUP
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) {
|
||||
if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) {
|
||||
for (auto *child : this->children_) {
|
||||
if (child->chan_handle_ == nullptr)
|
||||
continue;
|
||||
|
||||
uint32_t smooth_value = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &smooth_value);
|
||||
child->value_ = smooth_value;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (Ch%d): %" PRIu32, child->get_name().c_str(), child->channel_id_, smooth_value);
|
||||
#else
|
||||
uint32_t benchmark = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark);
|
||||
child->benchmark_ = benchmark;
|
||||
int32_t difference = static_cast<int32_t>(smooth_value) - static_cast<int32_t>(benchmark);
|
||||
ESP_LOGD(TAG,
|
||||
"Touch Pad '%s' (Ch%d): value=%" PRIu32 ", benchmark=%" PRIu32 ", difference=%" PRId32
|
||||
" (set threshold < %" PRId32 " to detect touch)",
|
||||
child->get_name().c_str(), child->channel_id_, smooth_value, benchmark, difference, difference);
|
||||
#endif
|
||||
}
|
||||
this->setup_mode_last_log_print_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) {
|
||||
if (!child->initial_state_published_) {
|
||||
if (now > INITIAL_STATE_DELAY_MS) {
|
||||
child->publish_initial_state(false);
|
||||
child->initial_state_published_ = true;
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::esp32_touch
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -4,49 +4,49 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <driver/touch_sensor.h>
|
||||
#include <driver/touch_sens.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
namespace esphome::esp32_touch {
|
||||
|
||||
// IMPORTANT: Touch detection logic differs between ESP32 variants:
|
||||
// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease)
|
||||
// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase)
|
||||
// This inversion is due to different hardware implementations between chip generations.
|
||||
// - ESP32 v1 (original): Touch detected when value < threshold (absolute threshold, capacitance increase causes
|
||||
// value decrease)
|
||||
// - ESP32-S2/S3 v2, ESP32-P4 v3: Touch detected when (smooth - benchmark) > threshold (relative threshold)
|
||||
//
|
||||
// INTERRUPT BEHAVIOR:
|
||||
// - ESP32 v1: Interrupts fire when ANY pad is touched and continue while touched.
|
||||
// Releases are detected by timeout since hardware doesn't generate release interrupts.
|
||||
// - ESP32-S2/S3 v2: Hardware supports both touch and release interrupts, but release
|
||||
// interrupts are unreliable and sometimes don't fire. We now only use touch interrupts
|
||||
// and detect releases via timeout, similar to v1.
|
||||
|
||||
static const uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250;
|
||||
// CALLBACK BEHAVIOR:
|
||||
// - ESP32 v1: on_active/on_inactive fire from a software filter timer (esp_timer context).
|
||||
// The software filter MUST be configured for these callbacks to fire.
|
||||
// - ESP32-S2/S3 v2, ESP32-P4 v3: on_active/on_inactive fire from hardware ISR context.
|
||||
// Release detection via on_inactive is used, with timeout as safety fallback.
|
||||
|
||||
class ESP32TouchBinarySensor;
|
||||
|
||||
class ESP32TouchComponent : public Component {
|
||||
class ESP32TouchComponent final : public Component {
|
||||
public:
|
||||
void register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); }
|
||||
|
||||
void set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; }
|
||||
void set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; }
|
||||
void set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; }
|
||||
void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) {
|
||||
void set_meas_interval_us(float meas_interval_us) { this->meas_interval_us_ = meas_interval_us; }
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
void set_charge_duration_ms(float charge_duration_ms) { this->charge_duration_ms_ = charge_duration_ms; }
|
||||
#else
|
||||
void set_charge_times(uint32_t charge_times) { this->charge_times_ = charge_times; }
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
void set_low_voltage_reference(touch_volt_lim_l_t low_voltage_reference) {
|
||||
this->low_voltage_reference_ = low_voltage_reference;
|
||||
}
|
||||
void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) {
|
||||
void set_high_voltage_reference(touch_volt_lim_h_t high_voltage_reference) {
|
||||
this->high_voltage_reference_ = high_voltage_reference;
|
||||
}
|
||||
void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) {
|
||||
this->voltage_attenuation_ = voltage_attenuation;
|
||||
}
|
||||
#endif
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -54,183 +54,130 @@ class ESP32TouchComponent : public Component {
|
||||
|
||||
void on_shutdown() override;
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; }
|
||||
void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; }
|
||||
void set_noise_threshold(uint32_t noise_threshold) { this->noise_threshold_ = noise_threshold; }
|
||||
void set_jitter_step(uint32_t jitter_step) { this->jitter_step_ = jitter_step; }
|
||||
void set_smooth_level(touch_smooth_mode_t smooth_level) { this->smooth_level_ = smooth_level; }
|
||||
void set_denoise_grade(touch_pad_denoise_grade_t denoise_grade) { this->grade_ = denoise_grade; }
|
||||
void set_denoise_cap(touch_pad_denoise_cap_t cap_level) { this->cap_level_ = cap_level; }
|
||||
void set_waterproof_guard_ring_pad(touch_pad_t pad) { this->waterproof_guard_ring_pad_ = pad; }
|
||||
void set_waterproof_shield_driver(touch_pad_shield_driver_t drive_capability) {
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
void set_filter_mode(touch_benchmark_filter_mode_t filter_mode) {
|
||||
this->filter_mode_ = filter_mode;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_debounce_count(uint32_t debounce_count) {
|
||||
this->debounce_count_ = debounce_count;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_noise_threshold(uint32_t noise_threshold) {
|
||||
this->noise_threshold_ = noise_threshold;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_jitter_step(uint32_t jitter_step) {
|
||||
this->jitter_step_ = jitter_step;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_smooth_level(touch_smooth_filter_mode_t smooth_level) {
|
||||
this->smooth_level_ = smooth_level;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
void set_denoise_grade(touch_denoise_chan_resolution_t denoise_grade) {
|
||||
this->denoise_grade_ = denoise_grade;
|
||||
this->denoise_configured_ = true;
|
||||
}
|
||||
void set_denoise_cap(touch_denoise_chan_cap_t cap_level) {
|
||||
this->denoise_cap_level_ = cap_level;
|
||||
this->denoise_configured_ = true;
|
||||
}
|
||||
#endif
|
||||
void set_waterproof_guard_ring_pad(int channel_id) {
|
||||
this->waterproof_guard_ring_pad_ = channel_id;
|
||||
this->waterproof_configured_ = true;
|
||||
}
|
||||
void set_waterproof_shield_driver(uint32_t drive_capability) {
|
||||
this->waterproof_shield_driver_ = drive_capability;
|
||||
this->waterproof_configured_ = true;
|
||||
}
|
||||
#else
|
||||
void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Unified touch event for queue communication
|
||||
struct TouchEvent {
|
||||
int chan_id;
|
||||
bool is_active;
|
||||
};
|
||||
|
||||
// Common helper methods
|
||||
void dump_config_base_();
|
||||
void dump_config_sensors_();
|
||||
bool create_touch_queue_();
|
||||
void cleanup_touch_queue_();
|
||||
void configure_wakeup_pads_();
|
||||
|
||||
// Helper methods for loop() logic
|
||||
void process_setup_mode_logging_(uint32_t now);
|
||||
bool should_check_for_releases_(uint32_t now);
|
||||
void publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now);
|
||||
void check_and_disable_loop_if_all_released_(size_t pads_off);
|
||||
void calculate_release_timeout_();
|
||||
|
||||
// Unified callbacks for new API
|
||||
static bool on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, void *ctx);
|
||||
static bool on_inactive_cb(touch_sensor_handle_t handle, const touch_inactive_event_data_t *event, void *ctx);
|
||||
|
||||
// Common members
|
||||
std::vector<ESP32TouchBinarySensor *> children_;
|
||||
bool setup_mode_{false};
|
||||
uint32_t setup_mode_last_log_print_{0};
|
||||
uint32_t last_release_check_{0};
|
||||
uint32_t release_timeout_ms_{1500};
|
||||
uint32_t release_check_interval_ms_{50};
|
||||
|
||||
// Controller handle (new API)
|
||||
touch_sensor_handle_t sens_handle_{nullptr};
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
|
||||
// Common configuration parameters
|
||||
uint16_t sleep_cycle_{4095};
|
||||
uint16_t meas_cycle_{65535};
|
||||
touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5};
|
||||
touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7};
|
||||
touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V};
|
||||
float meas_interval_us_{320.0f};
|
||||
|
||||
// Common constants
|
||||
static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100;
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
float charge_duration_ms_{1.0f};
|
||||
#else
|
||||
uint32_t charge_times_{500};
|
||||
#endif
|
||||
|
||||
// ==================== PLATFORM SPECIFIC ====================
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
touch_volt_lim_l_t low_voltage_reference_{TOUCH_VOLT_LIM_L_0V5};
|
||||
touch_volt_lim_h_t high_voltage_reference_{TOUCH_VOLT_LIM_H_2V7};
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// ESP32 v1 specific
|
||||
|
||||
static void touch_isr_handler(void *arg);
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
|
||||
private:
|
||||
// Touch event structure for ESP32 v1
|
||||
// Contains touch pad info, value, and touch state for queue communication
|
||||
struct TouchPadEventV1 {
|
||||
touch_pad_t pad;
|
||||
uint32_t value;
|
||||
bool is_touched;
|
||||
};
|
||||
|
||||
protected:
|
||||
uint32_t iir_filter_{0};
|
||||
|
||||
bool iir_filter_enabled_() const { return this->iir_filter_ > 0; }
|
||||
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// ESP32-S2/S3 v2 specific
|
||||
static void touch_isr_handler(void *arg);
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
// ESP32-S2/S3/P4 v2/v3 specific
|
||||
|
||||
private:
|
||||
// Touch event structure for ESP32 v2 (S2/S3)
|
||||
// Contains touch pad and interrupt mask for queue communication
|
||||
struct TouchPadEventV2 {
|
||||
touch_pad_t pad;
|
||||
uint32_t intr_mask;
|
||||
};
|
||||
|
||||
protected:
|
||||
// Filter configuration
|
||||
touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX};
|
||||
// Filter configuration - use sentinel values to detect "not configured"
|
||||
touch_benchmark_filter_mode_t filter_mode_{TOUCH_BM_JITTER_FILTER};
|
||||
uint32_t debounce_count_{0};
|
||||
uint32_t noise_threshold_{0};
|
||||
uint32_t jitter_step_{0};
|
||||
touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX};
|
||||
touch_smooth_filter_mode_t smooth_level_{TOUCH_SMOOTH_NO_FILTER};
|
||||
bool filter_configured_{false};
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
// Denoise configuration
|
||||
touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX};
|
||||
touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX};
|
||||
|
||||
// Waterproof configuration
|
||||
touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX};
|
||||
touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX};
|
||||
|
||||
bool filter_configured_() const {
|
||||
return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX);
|
||||
}
|
||||
bool denoise_configured_() const {
|
||||
return (this->grade_ != TOUCH_PAD_DENOISE_MAX) && (this->cap_level_ != TOUCH_PAD_DENOISE_CAP_MAX);
|
||||
}
|
||||
bool waterproof_configured_() const {
|
||||
return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) &&
|
||||
(this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX);
|
||||
}
|
||||
|
||||
// Helper method to read touch values - non-blocking operation
|
||||
// Returns the current touch pad value using either filtered or raw reading
|
||||
// based on the filter configuration
|
||||
uint32_t read_touch_value(touch_pad_t pad) const;
|
||||
|
||||
// Helper to update touch state with a known state and value
|
||||
void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value);
|
||||
|
||||
// Helper to read touch value and update state for a given child
|
||||
bool check_and_update_touch_state_(ESP32TouchBinarySensor *child);
|
||||
touch_denoise_chan_resolution_t denoise_grade_{TOUCH_DENOISE_CHAN_RESOLUTION_BIT12};
|
||||
touch_denoise_chan_cap_t denoise_cap_level_{TOUCH_DENOISE_CHAN_CAP_5PF};
|
||||
bool denoise_configured_{false};
|
||||
#endif
|
||||
|
||||
// Helper functions for dump_config - common to both implementations
|
||||
static const char *get_low_voltage_reference_str(touch_low_volt_t ref) {
|
||||
switch (ref) {
|
||||
case TOUCH_LVOLT_0V5:
|
||||
return "0.5V";
|
||||
case TOUCH_LVOLT_0V6:
|
||||
return "0.6V";
|
||||
case TOUCH_LVOLT_0V7:
|
||||
return "0.7V";
|
||||
case TOUCH_LVOLT_0V8:
|
||||
return "0.8V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_high_voltage_reference_str(touch_high_volt_t ref) {
|
||||
switch (ref) {
|
||||
case TOUCH_HVOLT_2V4:
|
||||
return "2.4V";
|
||||
case TOUCH_HVOLT_2V5:
|
||||
return "2.5V";
|
||||
case TOUCH_HVOLT_2V6:
|
||||
return "2.6V";
|
||||
case TOUCH_HVOLT_2V7:
|
||||
return "2.7V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) {
|
||||
switch (atten) {
|
||||
case TOUCH_HVOLT_ATTEN_1V5:
|
||||
return "1.5V";
|
||||
case TOUCH_HVOLT_ATTEN_1V:
|
||||
return "1V";
|
||||
case TOUCH_HVOLT_ATTEN_0V5:
|
||||
return "0.5V";
|
||||
case TOUCH_HVOLT_ATTEN_0V:
|
||||
return "0V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
// Waterproof configuration
|
||||
int waterproof_guard_ring_pad_{-1};
|
||||
uint32_t waterproof_shield_driver_{0};
|
||||
bool waterproof_configured_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Simple helper class to expose a touch pad value as a binary sensor.
|
||||
class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
public:
|
||||
ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold)
|
||||
: touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {}
|
||||
ESP32TouchBinarySensor(int channel_id, uint32_t threshold, uint32_t wakeup_threshold)
|
||||
: channel_id_(channel_id), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {}
|
||||
|
||||
touch_pad_t get_touch_pad() const { return this->touch_pad_; }
|
||||
int get_channel_id() const { return this->channel_id_; }
|
||||
uint32_t get_threshold() const { return this->threshold_; }
|
||||
void set_threshold(uint32_t threshold) { this->threshold_ = threshold; }
|
||||
|
||||
@@ -242,39 +189,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
|
||||
uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; }
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
/// Ensure benchmark value is read (v2 touch hardware only).
|
||||
/// Called from multiple places - kept as helper to document shared usage.
|
||||
void ensure_benchmark_read() {
|
||||
if (this->benchmark_ == 0) {
|
||||
touch_pad_read_benchmark(this->touch_pad_, &this->benchmark_);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
friend ESP32TouchComponent;
|
||||
|
||||
touch_pad_t touch_pad_{TOUCH_PAD_MAX};
|
||||
int channel_id_;
|
||||
touch_channel_handle_t chan_handle_{nullptr};
|
||||
uint32_t threshold_{0};
|
||||
uint32_t benchmark_{};
|
||||
uint32_t benchmark_{0};
|
||||
/// Stores the last raw touch measurement value.
|
||||
uint32_t value_{0};
|
||||
bool last_state_{false};
|
||||
const uint32_t wakeup_threshold_{0};
|
||||
|
||||
// Track last touch time for timeout-based release detection
|
||||
// Design note: last_touch_time_ does not require synchronization primitives because:
|
||||
// 1. ESP32 guarantees atomic 32-bit aligned reads/writes
|
||||
// 2. ISR only writes timestamps, main loop only reads
|
||||
// 3. Timing tolerance allows for occasional stale reads (50ms check interval)
|
||||
// 4. Queue operations provide implicit memory barriers
|
||||
// Using atomic/critical sections would add overhead without meaningful benefit
|
||||
uint32_t last_touch_time_{};
|
||||
bool initial_state_published_{};
|
||||
bool initial_state_published_{false};
|
||||
};
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_touch
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
#include "soc/rtc.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
void ESP32TouchComponent::dump_config_base_() {
|
||||
const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_);
|
||||
const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_);
|
||||
const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Meas cycle: %.2fms\n"
|
||||
" Sleep cycle: %.2fms\n"
|
||||
" Low Voltage Reference: %s\n"
|
||||
" High Voltage Reference: %s\n"
|
||||
" Voltage Attenuation: %s\n"
|
||||
" Release Timeout: %" PRIu32 "ms\n",
|
||||
this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s,
|
||||
atten_s, this->release_timeout_ms_);
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config_sensors_() {
|
||||
for (auto *child : this->children_) {
|
||||
LOG_BINARY_SENSOR(" ", "Touch Pad", child);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Pad: T%u\n"
|
||||
" Threshold: %" PRIu32 "\n"
|
||||
" Benchmark: %" PRIu32,
|
||||
(unsigned) child->touch_pad_, child->threshold_, child->benchmark_);
|
||||
}
|
||||
}
|
||||
|
||||
bool ESP32TouchComponent::create_touch_queue_() {
|
||||
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
||||
// fires multiple times before main loop processes.
|
||||
size_t queue_size = this->children_.size() * 4;
|
||||
if (queue_size < 8)
|
||||
queue_size = 8;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1));
|
||||
#else
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2));
|
||||
#endif
|
||||
|
||||
if (this->touch_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::cleanup_touch_queue_() {
|
||||
if (this->touch_queue_) {
|
||||
vQueueDelete(this->touch_queue_);
|
||||
this->touch_queue_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::configure_wakeup_pads_() {
|
||||
bool is_wakeup_source = false;
|
||||
|
||||
// Check if any pad is configured for wakeup
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
is_wakeup_source = true;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// ESP32 v1: No filter available when using as wake-up source.
|
||||
touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold());
|
||||
#else
|
||||
// ESP32-S2/S3 v2: Set threshold for wakeup
|
||||
touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_wakeup_source) {
|
||||
// If no pad is configured for wakeup, deinitialize touch pad
|
||||
touch_pad_deinit();
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) {
|
||||
if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) {
|
||||
for (auto *child : this->children_) {
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(),
|
||||
(uint32_t) child->get_touch_pad(), child->value_);
|
||||
#else
|
||||
// Read the value being used for touch detection
|
||||
uint32_t value = this->read_touch_value(child->get_touch_pad());
|
||||
// Store the value for get_value() access in lambdas
|
||||
child->value_ = value;
|
||||
// Read benchmark if not already read
|
||||
child->ensure_benchmark_read();
|
||||
// Calculate difference to help user set threshold
|
||||
// For ESP32-S2/S3 v2: touch detected when value > benchmark + threshold
|
||||
// So threshold should be < (value - benchmark) when touched
|
||||
int32_t difference = static_cast<int32_t>(value) - static_cast<int32_t>(child->benchmark_);
|
||||
ESP_LOGD(TAG,
|
||||
"Touch Pad '%s' (T%d): value=%d, benchmark=%" PRIu32 ", difference=%" PRId32 " (set threshold < %" PRId32
|
||||
" to detect touch)",
|
||||
child->get_name().c_str(), child->get_touch_pad(), value, child->benchmark_, difference, difference);
|
||||
#endif
|
||||
}
|
||||
this->setup_mode_last_log_print_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) {
|
||||
if (now - this->last_release_check_ < this->release_check_interval_ms_) {
|
||||
return false;
|
||||
}
|
||||
this->last_release_check_ = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) {
|
||||
if (!child->initial_state_published_) {
|
||||
// Check if enough time has passed since startup
|
||||
if (now > this->release_timeout_ms_) {
|
||||
child->publish_initial_state(false);
|
||||
child->initial_state_published_ = true;
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::check_and_disable_loop_if_all_released_(size_t pads_off) {
|
||||
// Disable the loop to save CPU cycles when all pads are off and not in setup mode.
|
||||
if (pads_off == this->children_.size() && !this->setup_mode_) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::calculate_release_timeout_() {
|
||||
// Calculate release timeout based on sleep cycle
|
||||
// Design note: Hardware limitation - interrupts only fire reliably on touch (not release)
|
||||
// We must use timeout-based detection for release events
|
||||
// Formula: 3 sleep cycles converted to ms, with MINIMUM_RELEASE_TIME_MS minimum
|
||||
// Per ESP-IDF docs: t_sleep = sleep_cycle / SOC_CLK_RC_SLOW_FREQ_APPROX
|
||||
|
||||
uint32_t rtc_freq = rtc_clk_slow_freq_get_hz();
|
||||
|
||||
// Calculate timeout as 3 sleep cycles
|
||||
this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / rtc_freq;
|
||||
|
||||
if (this->release_timeout_ms_ < MINIMUM_RELEASE_TIME_MS) {
|
||||
this->release_timeout_ms_ = MINIMUM_RELEASE_TIME_MS;
|
||||
}
|
||||
|
||||
// Check for releases at 1/4 the timeout interval
|
||||
// Since hardware doesn't generate reliable release interrupts, we must poll
|
||||
// for releases in the main loop. Checking at 1/4 the timeout interval provides
|
||||
// a good balance between responsiveness and efficiency.
|
||||
this->release_check_interval_ms_ = this->release_timeout_ms_ / 4;
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,244 +0,0 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
// Include HAL for ISR-safe touch reading
|
||||
#include "hal/touch_sensor_ll.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
// Create queue for touch events
|
||||
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
||||
// fires multiple times before main loop processes. This is important because
|
||||
// ESP32 v1 scans all pads on each interrupt, potentially sending multiple events.
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_pad_init();
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
|
||||
// Set up IIR filter if enabled
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_start(this->iir_filter_);
|
||||
}
|
||||
|
||||
// Configure measurement parameters
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
touch_pad_set_measurement_clock_cycles(this->meas_cycle_);
|
||||
touch_pad_set_measurement_interval(this->sleep_cycle_);
|
||||
#else
|
||||
touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_);
|
||||
#endif
|
||||
touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
|
||||
|
||||
// Configure each touch pad
|
||||
for (auto *child : this->children_) {
|
||||
if (this->setup_mode_) {
|
||||
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
|
||||
} else {
|
||||
touch_pad_config(child->get_touch_pad(), child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
// Register ISR handler
|
||||
esp_err_t err = touch_pad_isr_register(touch_isr_handler, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate release timeout based on sleep cycle
|
||||
this->calculate_release_timeout_();
|
||||
|
||||
// Enable touch pad interrupt
|
||||
touch_pad_intr_enable();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
this->dump_config_base_();
|
||||
|
||||
if (this->iir_filter_enabled_()) {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter DISABLED");
|
||||
}
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
this->dump_config_sensors_();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Print debug info for all pads in setup mode
|
||||
this->process_setup_mode_logging_(now);
|
||||
|
||||
// Process any queued touch events from interrupts
|
||||
// Note: Events are only sent by ISR for pads that were measured in that cycle (value != 0)
|
||||
// This is more efficient than sending all pad states every interrupt
|
||||
TouchPadEventV1 event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
// Find the corresponding sensor - O(n) search is acceptable since events are infrequent
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_touch_pad() != event.pad) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found matching pad - process it
|
||||
child->value_ = event.value;
|
||||
|
||||
// The interrupt gives us the touch state directly
|
||||
bool new_state = event.is_touched;
|
||||
|
||||
// Track when we last saw this pad as touched
|
||||
if (new_state) {
|
||||
child->last_touch_time_ = now;
|
||||
}
|
||||
|
||||
// Only publish if state changed - this filters out repeated events
|
||||
if (new_state != child->last_state_) {
|
||||
child->initial_state_published_ = true;
|
||||
child->last_state_ = new_state;
|
||||
child->publish_state(new_state);
|
||||
// Original ESP32: ISR only fires when touched, release is detected by timeout
|
||||
// Note: ESP32 v1 uses inverted logic - touched when value < threshold
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
|
||||
}
|
||||
break; // Exit inner loop after processing matching pad
|
||||
}
|
||||
}
|
||||
|
||||
// Check for released pads periodically
|
||||
if (!this->should_check_for_releases_(now)) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t pads_off = 0;
|
||||
for (auto *child : this->children_) {
|
||||
// Handle initial state publication after startup
|
||||
this->publish_initial_state_if_needed_(child, now);
|
||||
|
||||
if (child->last_state_) {
|
||||
// Pad is currently in touched state - check for release timeout
|
||||
// Using subtraction handles 32-bit rollover correctly
|
||||
uint32_t time_diff = now - child->last_touch_time_;
|
||||
|
||||
// Check if we haven't seen this pad recently
|
||||
if (time_diff > this->release_timeout_ms_) {
|
||||
// Haven't seen this pad recently, assume it's released
|
||||
child->last_state_ = false;
|
||||
child->publish_state(false);
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str());
|
||||
pads_off++;
|
||||
}
|
||||
} else {
|
||||
// Pad is already off
|
||||
pads_off++;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable the loop to save CPU cycles when all pads are off and not in setup mode.
|
||||
// The loop will be re-enabled by the ISR when any touch pad is touched.
|
||||
// v1 hardware limitations require us to check all pads are off because:
|
||||
// - v1 only generates interrupts on touch events (not releases)
|
||||
// - We must poll for release timeouts in the main loop
|
||||
// - We can only safely disable when no pads need timeout monitoring
|
||||
this->check_and_disable_loop_if_all_released_(pads_off);
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
touch_pad_intr_disable();
|
||||
touch_pad_isr_deregister(touch_isr_handler, this);
|
||||
this->cleanup_touch_queue_();
|
||||
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_stop();
|
||||
touch_pad_filter_delete();
|
||||
}
|
||||
|
||||
// Configure wakeup pads if any are set
|
||||
this->configure_wakeup_pads_();
|
||||
}
|
||||
|
||||
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
||||
|
||||
uint32_t mask = 0;
|
||||
touch_ll_read_trigger_status_mask(&mask);
|
||||
touch_ll_clear_trigger_status_mask();
|
||||
touch_pad_clear_status();
|
||||
|
||||
// INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured
|
||||
// touch pad detects a touch (value goes below threshold). The hardware does NOT
|
||||
// generate interrupts on release - only on touch events.
|
||||
// The interrupt will continue to fire periodically (based on sleep_cycle) as long
|
||||
// as any pad remains touched. This allows us to detect both new touches and
|
||||
// continued touches, but releases must be detected by timeout in the main loop.
|
||||
|
||||
// Process all configured pads to check their current state
|
||||
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
|
||||
// so we must scan all configured pads to find which ones were touched
|
||||
for (auto *child : component->children_) {
|
||||
touch_pad_t pad = child->get_touch_pad();
|
||||
|
||||
// Read current value using ISR-safe API
|
||||
// IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe
|
||||
// In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes:
|
||||
// "assert failed: xQueueSemaphoreTake queue.c:1718"
|
||||
// We must use raw values even when filter is enabled as a workaround.
|
||||
// Users should adjust thresholds to compensate for the lack of IIR filtering.
|
||||
// See: https://github.com/espressif/esp-idf/issues/17045
|
||||
uint32_t value = touch_ll_read_raw_data(pad);
|
||||
|
||||
// Skip pads that aren’t in the trigger mask
|
||||
if (((mask >> pad) & 1) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
||||
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
||||
// Therefore: touched = (value < threshold)
|
||||
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
||||
bool is_touched = value < child->get_threshold();
|
||||
|
||||
// Always send the current state - the main loop will filter for changes
|
||||
// We send both touched and untouched states because the ISR doesn't
|
||||
// track previous state (to keep ISR fast and simple)
|
||||
TouchPadEventV1 event;
|
||||
event.pad = pad;
|
||||
event.value = value;
|
||||
event.is_touched = is_touched;
|
||||
|
||||
// Send to queue from ISR - non-blocking, drops if queue full
|
||||
BaseType_t x_higher_priority_task_woken = pdFALSE;
|
||||
xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
|
||||
component->enable_loop_soon_any_context();
|
||||
if (x_higher_priority_task_woken) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32
|
||||
@@ -1,402 +0,0 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
// Helper to update touch state with a known state and value
|
||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) {
|
||||
// Store the value for get_value() access in lambdas
|
||||
child->value_ = value;
|
||||
|
||||
// Always update timer when touched
|
||||
if (is_touched) {
|
||||
child->last_touch_time_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
if (child->last_state_ != is_touched) {
|
||||
child->last_state_ = is_touched;
|
||||
child->publish_state(is_touched);
|
||||
if (is_touched) {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(),
|
||||
value, child->threshold_ + child->benchmark_);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to read touch value and update state for a given child (used for timeout events)
|
||||
bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) {
|
||||
// Read current touch value
|
||||
uint32_t value = this->read_touch_value(child->touch_pad_);
|
||||
|
||||
// ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark
|
||||
ESP_LOGV(TAG,
|
||||
"Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32,
|
||||
child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_);
|
||||
bool is_touched = value > child->benchmark_ + child->threshold_;
|
||||
|
||||
this->update_touch_state_(child, is_touched, value);
|
||||
return is_touched;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
// Create queue for touch events first
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize touch pad peripheral
|
||||
esp_err_t init_err = touch_pad_init();
|
||||
if (init_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure each touch pad first
|
||||
for (auto *child : this->children_) {
|
||||
esp_err_t config_err = touch_pad_config(child->touch_pad_);
|
||||
if (config_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err));
|
||||
}
|
||||
}
|
||||
|
||||
// Set up filtering if configured
|
||||
if (this->filter_configured_()) {
|
||||
touch_filter_config_t filter_info = {
|
||||
.mode = this->filter_mode_,
|
||||
.debounce_cnt = this->debounce_count_,
|
||||
.noise_thr = this->noise_threshold_,
|
||||
.jitter_step = this->jitter_step_,
|
||||
.smh_lvl = this->smooth_level_,
|
||||
};
|
||||
touch_pad_filter_set_config(&filter_info);
|
||||
touch_pad_filter_enable();
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
touch_pad_denoise_t denoise = {
|
||||
.grade = this->grade_,
|
||||
.cap_level = this->cap_level_,
|
||||
};
|
||||
touch_pad_denoise_set_config(&denoise);
|
||||
touch_pad_denoise_enable();
|
||||
}
|
||||
|
||||
if (this->waterproof_configured_()) {
|
||||
touch_pad_waterproof_t waterproof = {
|
||||
.guard_ring_pad = this->waterproof_guard_ring_pad_,
|
||||
.shield_driver = this->waterproof_shield_driver_,
|
||||
};
|
||||
touch_pad_waterproof_set_config(&waterproof);
|
||||
touch_pad_waterproof_enable();
|
||||
}
|
||||
|
||||
// Configure measurement parameters
|
||||
touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
|
||||
touch_pad_set_charge_discharge_times(this->meas_cycle_);
|
||||
touch_pad_set_measurement_interval(this->sleep_cycle_);
|
||||
|
||||
// Disable hardware timeout - it causes continuous interrupts with high-capacitance
|
||||
// setups (e.g., pressure sensors under cushions). The periodic release check in
|
||||
// loop() handles state detection reliably without needing hardware timeout.
|
||||
touch_pad_timeout_set(false, TOUCH_PAD_THRESHOLD_MAX);
|
||||
|
||||
// Register ISR handler with interrupt mask
|
||||
esp_err_t err =
|
||||
touch_pad_isr_register(touch_isr_handler, this, static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ALL));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set thresholds for each pad BEFORE starting FSM
|
||||
for (auto *child : this->children_) {
|
||||
if (child->threshold_ != 0) {
|
||||
touch_pad_set_thresh(child->touch_pad_, child->threshold_);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable interrupts - only ACTIVE and TIMEOUT
|
||||
// NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable
|
||||
// on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based
|
||||
// release detection with the ability to verify the actual state.
|
||||
touch_pad_intr_enable(static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT));
|
||||
|
||||
// Set FSM mode before starting
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
|
||||
// Start FSM
|
||||
touch_pad_fsm_start();
|
||||
|
||||
// Calculate release timeout based on sleep cycle
|
||||
this->calculate_release_timeout_();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
this->dump_config_base_();
|
||||
|
||||
if (this->filter_configured_()) {
|
||||
const char *filter_mode_s;
|
||||
switch (this->filter_mode_) {
|
||||
case TOUCH_PAD_FILTER_IIR_4:
|
||||
filter_mode_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_8:
|
||||
filter_mode_s = "IIR_8";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_16:
|
||||
filter_mode_s = "IIR_16";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_32:
|
||||
filter_mode_s = "IIR_32";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_64:
|
||||
filter_mode_s = "IIR_64";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_128:
|
||||
filter_mode_s = "IIR_128";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_256:
|
||||
filter_mode_s = "IIR_256";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_JITTER:
|
||||
filter_mode_s = "JITTER";
|
||||
break;
|
||||
default:
|
||||
filter_mode_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Filter mode: %s\n"
|
||||
" Debounce count: %" PRIu32 "\n"
|
||||
" Noise threshold coefficient: %" PRIu32 "\n"
|
||||
" Jitter filter step size: %" PRIu32,
|
||||
filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_);
|
||||
const char *smooth_level_s;
|
||||
switch (this->smooth_level_) {
|
||||
case TOUCH_PAD_SMOOTH_OFF:
|
||||
smooth_level_s = "OFF";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_2:
|
||||
smooth_level_s = "IIR_2";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_4:
|
||||
smooth_level_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_8:
|
||||
smooth_level_s = "IIR_8";
|
||||
break;
|
||||
default:
|
||||
smooth_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s);
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
const char *grade_s;
|
||||
switch (this->grade_) {
|
||||
case TOUCH_PAD_DENOISE_BIT12:
|
||||
grade_s = "BIT12";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT10:
|
||||
grade_s = "BIT10";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT8:
|
||||
grade_s = "BIT8";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT4:
|
||||
grade_s = "BIT4";
|
||||
break;
|
||||
default:
|
||||
grade_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s);
|
||||
|
||||
const char *cap_level_s;
|
||||
switch (this->cap_level_) {
|
||||
case TOUCH_PAD_DENOISE_CAP_L0:
|
||||
cap_level_s = "L0";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L1:
|
||||
cap_level_s = "L1";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L2:
|
||||
cap_level_s = "L2";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L3:
|
||||
cap_level_s = "L3";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L4:
|
||||
cap_level_s = "L4";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L5:
|
||||
cap_level_s = "L5";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L6:
|
||||
cap_level_s = "L6";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L7:
|
||||
cap_level_s = "L7";
|
||||
break;
|
||||
default:
|
||||
cap_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s);
|
||||
}
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
this->dump_config_sensors_();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// V2 TOUCH HANDLING:
|
||||
// Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach:
|
||||
// 1. Process ACTIVE interrupts when pads are touched
|
||||
// 2. Use timeout-based release detection (like v1)
|
||||
// 3. But smarter than v1: verify actual state before releasing on timeout
|
||||
// This prevents false releases if we missed interrupts
|
||||
|
||||
// In setup mode, periodically log all pad values
|
||||
this->process_setup_mode_logging_(now);
|
||||
|
||||
// Process any queued touch events from interrupts
|
||||
TouchPadEventV2 event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad);
|
||||
// Handle timeout events
|
||||
if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
|
||||
// Resume measurement after timeout
|
||||
touch_pad_timeout_resume();
|
||||
// For timeout events, always check the current state
|
||||
} else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) {
|
||||
// Skip if not an active/timeout event
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the child for the pad that triggered the interrupt
|
||||
for (auto *child : this->children_) {
|
||||
if (child->touch_pad_ == event.pad) {
|
||||
if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
|
||||
// For timeout events, we need to read the value to determine state
|
||||
this->check_and_update_touch_state_(child);
|
||||
} else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
|
||||
// We only get ACTIVE interrupts now, releases are detected by timeout
|
||||
// Read the current value
|
||||
uint32_t value = this->read_touch_value(child->touch_pad_);
|
||||
this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for released pads periodically (like v1)
|
||||
if (!this->should_check_for_releases_(now)) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t pads_off = 0;
|
||||
for (auto *child : this->children_) {
|
||||
child->ensure_benchmark_read();
|
||||
// Handle initial state publication after startup
|
||||
this->publish_initial_state_if_needed_(child, now);
|
||||
|
||||
if (child->last_state_) {
|
||||
// Pad is currently in touched state - check for release timeout
|
||||
// Using subtraction handles 32-bit rollover correctly
|
||||
uint32_t time_diff = now - child->last_touch_time_;
|
||||
|
||||
// Check if we haven't seen this pad recently
|
||||
if (time_diff > this->release_timeout_ms_) {
|
||||
// Haven't seen this pad recently - verify actual state
|
||||
// Unlike v1, v2 hardware allows us to read the current state anytime
|
||||
// This makes v2 smarter: we can verify if it's actually released before
|
||||
// declaring a timeout, preventing false releases if interrupts were missed
|
||||
bool still_touched = this->check_and_update_touch_state_(child);
|
||||
|
||||
if (still_touched) {
|
||||
// Still touched! Timer was reset in update_touch_state_
|
||||
ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer",
|
||||
child->get_name().c_str(), this->release_timeout_ms_);
|
||||
} else {
|
||||
// Actually released - already handled by check_and_update_touch_state_
|
||||
pads_off++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pad is already off
|
||||
pads_off++;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable the loop when all pads are off and not in setup mode (like v1)
|
||||
// We need to keep checking for timeouts, so only disable when all pads are confirmed off
|
||||
this->check_and_disable_loop_if_all_released_(pads_off);
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
// Disable interrupts
|
||||
touch_pad_intr_disable(TOUCH_PAD_INTR_MASK_ACTIVE);
|
||||
touch_pad_isr_deregister(touch_isr_handler, this);
|
||||
this->cleanup_touch_queue_();
|
||||
|
||||
// Configure wakeup pads if any are set
|
||||
this->configure_wakeup_pads_();
|
||||
}
|
||||
|
||||
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
||||
BaseType_t x_higher_priority_task_woken = pdFALSE;
|
||||
|
||||
// Read interrupt status
|
||||
TouchPadEventV2 event;
|
||||
event.intr_mask = touch_pad_read_intr_status_mask();
|
||||
event.pad = touch_pad_get_current_meas_channel();
|
||||
|
||||
// Send event to queue for processing in main loop
|
||||
xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
|
||||
component->enable_loop_soon_any_context();
|
||||
|
||||
if (x_higher_priority_task_woken) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const {
|
||||
// Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations.
|
||||
// The hardware continuously samples in the background and we can read the
|
||||
// latest value at any time without waiting.
|
||||
uint32_t value = 0;
|
||||
if (this->filter_configured_()) {
|
||||
// Read filtered/smoothed value when filter is enabled
|
||||
touch_pad_filter_read_smooth(pad, &value);
|
||||
} else {
|
||||
// Read raw value when filter is not configured
|
||||
touch_pad_read_raw_data(pad, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
|
||||
@@ -205,6 +205,7 @@ async def to_code(config):
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
"pre:remove_float_scanf.py",
|
||||
"post:post_build.py",
|
||||
],
|
||||
)
|
||||
@@ -342,3 +343,8 @@ def copy_files() -> None:
|
||||
exclude_waveform_file,
|
||||
CORE.relative_build_path("exclude_waveform.py"),
|
||||
)
|
||||
remove_float_scanf_file = dir / "remove_float_scanf.py.script"
|
||||
copy_file_if_changed(
|
||||
remove_float_scanf_file,
|
||||
CORE.relative_build_path("remove_float_scanf.py"),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
#include <Arduino.h>
|
||||
@@ -16,6 +17,7 @@ namespace esphome {
|
||||
|
||||
void HOT yield() { ::yield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
|
||||
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
@@ -32,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) {
|
||||
return pgm_read_word(addr); // NOLINT
|
||||
}
|
||||
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
# Remove forced scanf linkage to allow garbage collection of unused code
|
||||
#
|
||||
# The ESP8266 Arduino framework unconditionally adds:
|
||||
# -u _printf_float -u _scanf_float
|
||||
#
|
||||
# The -u flag forces symbols to be linked even if unreferenced, which pulls
|
||||
# in the entire scanf family (~7-8KB). ESPHome doesn't use scanf at all
|
||||
# (verified by CI check in PR #13657), so this is pure dead weight.
|
||||
#
|
||||
# By removing -u _scanf_float, --gc-sections can eliminate:
|
||||
# - scanf family functions (~7KB)
|
||||
# - _strtod_l (~3.7KB)
|
||||
# - Related parsing infrastructure
|
||||
#
|
||||
# We keep -u _printf_float because components still use %f in logging.
|
||||
|
||||
|
||||
def remove_scanf_float_flag(source, target, env):
|
||||
"""Remove -u _scanf_float from linker flags.
|
||||
|
||||
This is called as a pre-action before the link step, after the
|
||||
Arduino framework has added its default flags.
|
||||
"""
|
||||
linkflags = env.get("LINKFLAGS", [])
|
||||
new_linkflags = []
|
||||
i = 0
|
||||
|
||||
while i < len(linkflags):
|
||||
flag = linkflags[i]
|
||||
if flag == "-u" and i + 1 < len(linkflags):
|
||||
next_flag = linkflags[i + 1]
|
||||
if next_flag == "_scanf_float":
|
||||
print("ESPHome: Removing _scanf_float (saves ~8KB flash)")
|
||||
i += 2 # Skip both -u and the symbol
|
||||
continue
|
||||
new_linkflags.append(flag)
|
||||
i += 1
|
||||
|
||||
env.Replace(LINKFLAGS=new_linkflags)
|
||||
|
||||
|
||||
# Register the callback to run before the link step
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", remove_scanf_float_flag)
|
||||
@@ -13,22 +13,63 @@ esp_ldo_ns = cg.esphome_ns.namespace("esp_ldo")
|
||||
EspLdo = esp_ldo_ns.class_("EspLdo", cg.Component)
|
||||
AdjustAction = esp_ldo_ns.class_("AdjustAction", Action)
|
||||
|
||||
CHANNELS = (3, 4)
|
||||
CHANNELS = (1, 2, 3, 4)
|
||||
CHANNELS_INTERNAL = (1, 2)
|
||||
CONF_ADJUSTABLE = "adjustable"
|
||||
CONF_ALLOW_INTERNAL_CHANNEL = "allow_internal_channel"
|
||||
CONF_PASSTHROUGH = "passthrough"
|
||||
|
||||
adjusted_ids = set()
|
||||
|
||||
|
||||
def validate_ldo_voltage(value):
|
||||
if isinstance(value, str) and value.lower() == CONF_PASSTHROUGH:
|
||||
return CONF_PASSTHROUGH
|
||||
value = cv.voltage(value)
|
||||
if 0.5 <= value <= 2.7:
|
||||
return value
|
||||
raise cv.Invalid(
|
||||
f"LDO voltage must be in range 0.5V-2.7V or 'passthrough' (bypass mode), got {value}V"
|
||||
)
|
||||
|
||||
|
||||
def validate_ldo_config(config):
|
||||
channel = config[CONF_CHANNEL]
|
||||
allow_internal = config[CONF_ALLOW_INTERNAL_CHANNEL]
|
||||
if allow_internal and channel not in CHANNELS_INTERNAL:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_ALLOW_INTERNAL_CHANNEL}' is only valid for internal channels (1, 2). "
|
||||
f"Channel {channel} is a user-configurable channel — its usage depends on your board schematic.",
|
||||
path=[CONF_ALLOW_INTERNAL_CHANNEL],
|
||||
)
|
||||
if channel in CHANNELS_INTERNAL and not allow_internal:
|
||||
raise cv.Invalid(
|
||||
f"LDO channel {channel} is normally used internally by the chip (flash/PSRAM). "
|
||||
f"Set '{CONF_ALLOW_INTERNAL_CHANNEL}: true' to confirm you know what you are doing.",
|
||||
path=[CONF_CHANNEL],
|
||||
)
|
||||
if config[CONF_VOLTAGE] == CONF_PASSTHROUGH and config[CONF_ADJUSTABLE]:
|
||||
raise cv.Invalid(
|
||||
"Passthrough mode passes the supply voltage directly to the output and does not support "
|
||||
"runtime voltage adjustment.",
|
||||
path=[CONF_ADJUSTABLE],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.ensure_list(
|
||||
cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EspLdo),
|
||||
cv.Required(CONF_VOLTAGE): cv.All(
|
||||
cv.voltage, cv.float_range(min=0.5, max=2.7)
|
||||
),
|
||||
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
|
||||
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
|
||||
}
|
||||
cv.All(
|
||||
cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EspLdo),
|
||||
cv.Required(CONF_VOLTAGE): validate_ldo_voltage,
|
||||
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
|
||||
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ALLOW_INTERNAL_CHANNEL, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
validate_ldo_config,
|
||||
)
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
@@ -40,7 +81,11 @@ async def to_code(configs):
|
||||
for config in configs:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_CHANNEL])
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_voltage(config[CONF_VOLTAGE]))
|
||||
voltage = config[CONF_VOLTAGE]
|
||||
if voltage == CONF_PASSTHROUGH:
|
||||
cg.add(var.set_voltage(3300))
|
||||
else:
|
||||
cg.add(var.set_voltage(int(round(voltage * 1000))))
|
||||
cg.add(var.set_adjustable(config[CONF_ADJUSTABLE]))
|
||||
|
||||
|
||||
|
||||
@@ -10,32 +10,34 @@ static const char *const TAG = "esp_ldo";
|
||||
void EspLdo::setup() {
|
||||
esp_ldo_channel_config_t config{};
|
||||
config.chan_id = this->channel_;
|
||||
config.voltage_mv = (int) (this->voltage_ * 1000.0f);
|
||||
config.voltage_mv = this->voltage_mv_;
|
||||
config.flags.adjustable = this->adjustable_;
|
||||
auto err = esp_ldo_acquire_channel(&config, &this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
|
||||
ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_);
|
||||
this->mark_failed(LOG_STR("Failed to acquire LDO channel"));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_);
|
||||
ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_);
|
||||
}
|
||||
}
|
||||
void EspLdo::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ESP LDO Channel %d:\n"
|
||||
" Voltage: %fV\n"
|
||||
" Voltage: %dmV\n"
|
||||
" Adjustable: %s",
|
||||
this->channel_, this->voltage_, YESNO(this->adjustable_));
|
||||
this->channel_, this->voltage_mv_, YESNO(this->adjustable_));
|
||||
}
|
||||
|
||||
void EspLdo::adjust_voltage(float voltage) {
|
||||
if (!std::isfinite(voltage) || voltage < 0.5f || voltage > 2.7f) {
|
||||
ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d", voltage, this->channel_);
|
||||
ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d (must be 0.5V-2.7V)", voltage, this->channel_);
|
||||
return;
|
||||
}
|
||||
auto erro = esp_ldo_channel_adjust_voltage(this->handle_, (int) (voltage * 1000.0f));
|
||||
if (erro != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %fV: %s", this->channel_, voltage, esp_err_to_name(erro));
|
||||
int voltage_mv = (int) roundf(voltage * 1000.0f);
|
||||
auto err = esp_ldo_channel_adjust_voltage(this->handle_, voltage_mv);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %dmV: %s", this->channel_, voltage_mv,
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class EspLdo : public Component {
|
||||
void dump_config() override;
|
||||
|
||||
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
|
||||
void set_voltage(float voltage) { this->voltage_ = voltage; }
|
||||
void set_voltage(int voltage_mv) { this->voltage_mv_ = voltage_mv; }
|
||||
void adjust_voltage(float voltage);
|
||||
float get_setup_priority() const override {
|
||||
return setup_priority::BUS; // LDO setup should be done early
|
||||
@@ -23,7 +23,7 @@ class EspLdo : public Component {
|
||||
|
||||
protected:
|
||||
int channel_;
|
||||
float voltage_{2.7};
|
||||
int voltage_mv_{2700};
|
||||
bool adjustable_{false};
|
||||
esp_ldo_channel_handle_t handle_{};
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
namespace esphome {
|
||||
|
||||
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
|
||||
class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||
class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
public:
|
||||
enum class OTAState : uint8_t {
|
||||
IDLE,
|
||||
@@ -84,7 +84,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||
std::unique_ptr<uint8_t[]> auth_buf_;
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
socket::Socket *server_{nullptr};
|
||||
socket::ListenSocket *server_{nullptr};
|
||||
std::unique_ptr<socket::Socket> client_;
|
||||
std::unique_ptr<ota::OTABackend> backend_;
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
#include <driver/spi_master.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet {
|
||||
namespace esphome::ethernet {
|
||||
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
|
||||
// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637
|
||||
@@ -881,7 +880,6 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace ethernet
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
#include "esp_mac.h"
|
||||
#include "esp_idf_version.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet {
|
||||
namespace esphome::ethernet {
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
/** Listener interface for Ethernet IP state changes.
|
||||
@@ -218,7 +217,6 @@ extern EthernetComponent *global_eth_component;
|
||||
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
|
||||
#endif
|
||||
|
||||
} // namespace ethernet
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -311,13 +311,17 @@ FAN_ACTION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("fan.toggle", ToggleAction, FAN_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"fan.toggle", ToggleAction, FAN_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def fan_toggle_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action("fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def fan_turn_off_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
@@ -336,6 +340,7 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
|
||||
),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def fan_turn_on_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
@@ -102,6 +102,7 @@ async def to_code(config):
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.string_strict),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
namespace esphome {
|
||||
namespace gp8403 {
|
||||
|
||||
enum GP8403Voltage {
|
||||
enum GP8403Voltage : uint8_t {
|
||||
GP8403_VOLTAGE_5V = 0x00,
|
||||
GP8403_VOLTAGE_10V = 0x11,
|
||||
};
|
||||
|
||||
enum GP8403Model {
|
||||
enum GP8403Model : uint8_t {
|
||||
GP8403,
|
||||
GP8413,
|
||||
};
|
||||
|
||||
@@ -98,33 +98,25 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if latitude_config := config.get(CONF_LATITUDE):
|
||||
sens = await sensor.new_sensor(latitude_config)
|
||||
cg.add(var.set_latitude_sensor(sens))
|
||||
# Pre-create all sensor variables so automations that reference
|
||||
# sibling sensors don't deadlock waiting for unregistered IDs.
|
||||
sensors = [
|
||||
(cg.new_Pvariable(conf[CONF_ID]), conf, setter)
|
||||
for key, setter in (
|
||||
(CONF_LATITUDE, "set_latitude_sensor"),
|
||||
(CONF_LONGITUDE, "set_longitude_sensor"),
|
||||
(CONF_SPEED, "set_speed_sensor"),
|
||||
(CONF_COURSE, "set_course_sensor"),
|
||||
(CONF_ALTITUDE, "set_altitude_sensor"),
|
||||
(CONF_SATELLITES, "set_satellites_sensor"),
|
||||
(CONF_HDOP, "set_hdop_sensor"),
|
||||
)
|
||||
if (conf := config.get(key))
|
||||
]
|
||||
|
||||
if longitude_config := config.get(CONF_LONGITUDE):
|
||||
sens = await sensor.new_sensor(longitude_config)
|
||||
cg.add(var.set_longitude_sensor(sens))
|
||||
|
||||
if speed_config := config.get(CONF_SPEED):
|
||||
sens = await sensor.new_sensor(speed_config)
|
||||
cg.add(var.set_speed_sensor(sens))
|
||||
|
||||
if course_config := config.get(CONF_COURSE):
|
||||
sens = await sensor.new_sensor(course_config)
|
||||
cg.add(var.set_course_sensor(sens))
|
||||
|
||||
if altitude_config := config.get(CONF_ALTITUDE):
|
||||
sens = await sensor.new_sensor(altitude_config)
|
||||
cg.add(var.set_altitude_sensor(sens))
|
||||
|
||||
if satellites_config := config.get(CONF_SATELLITES):
|
||||
sens = await sensor.new_sensor(satellites_config)
|
||||
cg.add(var.set_satellites_sensor(sens))
|
||||
|
||||
if hdop_config := config.get(CONF_HDOP):
|
||||
sens = await sensor.new_sensor(hdop_config)
|
||||
cg.add(var.set_hdop_sensor(sens))
|
||||
for sens, conf, setter in sensors:
|
||||
await sensor.register_sensor(sens, conf)
|
||||
cg.add(getattr(var, setter)(sens))
|
||||
|
||||
# https://platformio.org/lib/show/1655/TinyGPSPlus
|
||||
# Using fork of TinyGPSPlus patched to build on ESP-IDF
|
||||
|
||||
@@ -16,7 +16,7 @@ GT911Touchscreen = gt911_ns.class_(
|
||||
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(GT911Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x5D))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gt911 {
|
||||
@@ -26,15 +27,17 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
|
||||
|
||||
void GT911Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
// temporarily set the interrupt pin to output to control address selection
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// temporarily set the interrupt pin to output to control address selection
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
this->interrupt_pin_->digital_write(false);
|
||||
}
|
||||
delay(2);
|
||||
this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet
|
||||
this->reset_pin_->digital_write(true);
|
||||
// wait at least T3+T4 ms as per the datasheet
|
||||
this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); });
|
||||
return;
|
||||
}
|
||||
@@ -43,11 +46,10 @@ void GT911Touchscreen::setup() {
|
||||
|
||||
void GT911Touchscreen::setup_internal_() {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
// set pre-configured input mode
|
||||
this->interrupt_pin_->setup();
|
||||
if (this->interrupt_pin_->is_internal())
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
|
||||
// check the configuration of the int line.
|
||||
uint8_t data[4];
|
||||
i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
|
||||
@@ -58,12 +60,25 @@ void GT911Touchscreen::setup_internal_() {
|
||||
err = this->read(data, 1);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]);
|
||||
|
||||
// data[0] & 1 == 1 => controller uses falling edge => active-low
|
||||
// data[0] & 1 == 0 => controller uses rising edge => active-high
|
||||
bool active_high = !(data[0] & 1);
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->attach_interrupt_(this->interrupt_pin_,
|
||||
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
|
||||
if (this->interrupt_pin_->is_internal()) {
|
||||
// Direct MCU pin: attach a hardware interrupt, no polling needed.
|
||||
this->attach_interrupt_(static_cast<InternalGPIOPin *>(this->interrupt_pin_),
|
||||
active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE);
|
||||
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW");
|
||||
} else {
|
||||
// IO expander pin: leave as output for configuration only.
|
||||
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
|
||||
// no calibration? Attempt to read the max values from the touchscreen.
|
||||
if (err == i2c::ERROR_OK) {
|
||||
|
||||
@@ -30,7 +30,9 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
||||
void dump_config() override;
|
||||
bool can_proceed() override { return this->setup_done_; }
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
/// Set a interrupt pin (supports hardware interrupts or expander connected).
|
||||
void set_interrupt_pin(GPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
||||
|
||||
@@ -49,7 +51,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
||||
/// @brief True if the touchscreen setup has completed successfully.
|
||||
bool setup_done_{false};
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
GPIOPin *interrupt_pin_{nullptr};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
std::vector<GT911ButtonListener *> button_listeners_;
|
||||
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
|
||||
|
||||
@@ -95,7 +95,7 @@ void HMC5883LComponent::update() {
|
||||
float mg_per_bit;
|
||||
switch (this->range_) {
|
||||
case HMC5883L_RANGE_88_UT:
|
||||
mg_per_bit = 0.073f;
|
||||
mg_per_bit = 0.73f;
|
||||
break;
|
||||
case HMC5883L_RANGE_130_UT:
|
||||
mg_per_bit = 0.92f;
|
||||
|
||||
@@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_HOST")
|
||||
cg.add_define("USE_NATIVE_64BIT_TIME")
|
||||
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
|
||||
cg.add_build_flag("-std=gnu++20")
|
||||
cg.add_define("ESPHOME_BOARD", "host")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
@@ -19,6 +20,11 @@ uint32_t IRAM_ATTR HOT millis() {
|
||||
uint32_t ms = round(spec.tv_nsec / 1e6);
|
||||
return ((uint32_t) seconds) * 1000U + ms;
|
||||
}
|
||||
uint64_t millis_64() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint64_t>(spec.tv_sec) * 1000ULL + static_cast<uint64_t>(spec.tv_nsec) / 1000000ULL;
|
||||
}
|
||||
void HOT delay(uint32_t ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
@@ -53,6 +59,7 @@ void HOT arch_feed_wdt() {
|
||||
}
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "ota_http_request.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -209,6 +211,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
return ota::OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
// URL-encode characters that are not unreserved per RFC 3986 section 2.3.
|
||||
// This is needed for embedding userinfo (username/password) in URLs safely.
|
||||
static std::string url_encode(const std::string &str) {
|
||||
std::string result;
|
||||
result.reserve(str.size());
|
||||
for (char c : str) {
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
result += c;
|
||||
} else {
|
||||
result += '%';
|
||||
result += format_hex_pretty_char((static_cast<uint8_t>(c) >> 4) & 0x0F);
|
||||
result += format_hex_pretty_char(static_cast<uint8_t>(c) & 0x0F);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); }
|
||||
void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); }
|
||||
|
||||
std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
|
||||
if (this->username_.empty() || this->password_.empty()) {
|
||||
return url;
|
||||
|
||||
@@ -22,16 +22,16 @@ enum OtaHttpRequestError : uint8_t {
|
||||
OTA_CONNECTION_ERROR = 0x12,
|
||||
};
|
||||
|
||||
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
|
||||
class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<HttpRequestComponent> {
|
||||
public:
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void set_md5_url(const std::string &md5_url);
|
||||
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
|
||||
void set_password(const std::string &password) { this->password_ = password; }
|
||||
void set_password(const std::string &password);
|
||||
void set_url(const std::string &url);
|
||||
void set_username(const std::string &username) { this->username_ = username; }
|
||||
void set_username(const std::string &username);
|
||||
|
||||
std::string md5_computed() { return this->md5_computed_; }
|
||||
std::string md5_expected() { return this->md5_expected_; }
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include <memory>
|
||||
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
namespace esphome::i2c {
|
||||
|
||||
static const char *const TAG = "i2c";
|
||||
|
||||
@@ -109,5 +108,4 @@ uint8_t I2CRegister16::get() const {
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
} // namespace esphome::i2c
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
#include "esphome/core/optional.h"
|
||||
#include "i2c_bus.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
namespace esphome::i2c {
|
||||
|
||||
#define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_);
|
||||
|
||||
@@ -272,5 +271,4 @@ class I2CDevice {
|
||||
I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance
|
||||
};
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
} // namespace esphome::i2c
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
namespace esphome::i2c {
|
||||
|
||||
/// @brief Error codes returned by I2CBus and I2CDevice methods
|
||||
enum ErrorCode {
|
||||
@@ -69,5 +68,4 @@ class InternalI2CBus : public I2CBus {
|
||||
virtual int get_port() const = 0;
|
||||
};
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
} // namespace esphome::i2c
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
namespace esphome::i2c {
|
||||
|
||||
static const char *const TAG = "i2c.arduino";
|
||||
|
||||
@@ -262,7 +261,6 @@ void ArduinoI2CBus::recover_() {
|
||||
|
||||
recovery_result_ = RECOVERY_COMPLETED;
|
||||
}
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "i2c_bus.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
namespace esphome::i2c {
|
||||
|
||||
enum RecoveryCode {
|
||||
RECOVERY_FAILED_SCL_LOW,
|
||||
@@ -45,7 +44,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
namespace esphome::i2c {
|
||||
|
||||
static const char *const TAG = "i2c.idf";
|
||||
|
||||
@@ -312,6 +311,5 @@ void IDFI2CBus::recover_() {
|
||||
recovery_result_ = RECOVERY_COMPLETED;
|
||||
}
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
} // namespace esphome::i2c
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
#include "i2c_bus.h"
|
||||
#include <driver/i2c_master.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
namespace esphome::i2c {
|
||||
|
||||
enum RecoveryCode {
|
||||
RECOVERY_FAILED_SCL_LOW,
|
||||
@@ -56,7 +55,6 @@ class IDFI2CBus : public InternalI2CBus, public Component {
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user