Merge branch 'dev' into fix-bk72xx-loop-time

This commit is contained in:
J. Nick Koston
2026-03-02 15:26:31 -10:00
committed by GitHub
226 changed files with 7851 additions and 3389 deletions
+15 -27
View File
@@ -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;
}
@@ -259,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));
+66
View 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,
};
+1 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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}}"
+81
View File
@@ -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>`
);
}
+2 -2
View File
@@ -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
+2
View File
@@ -316,6 +316,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/media_source/* @kahrendt
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05
esphome/components/microphone/* @jesserockz @kahrendt
@@ -429,6 +430,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
View File
@@ -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:
+1
View File
@@ -11,6 +11,7 @@
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LineComment,
LogStringLiteral,
MockObj,
@@ -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();
+38 -11
View File
@@ -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
+28
View File
@@ -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 ====================
+78 -40
View File
@@ -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
+4
View File
@@ -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_) {
+54
View File
@@ -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;
}
+40 -4
View File
@@ -63,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,
@@ -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
+39
View File
@@ -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) {
@@ -1254,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
+1 -1
View File
@@ -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
+43 -3
View File
@@ -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_;
+6 -1
View File
@@ -452,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
@@ -463,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
@@ -482,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; }
+5
View File
@@ -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; }
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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")
+21 -16
View File
@@ -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)
+3 -1
View File
@@ -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];
};
+13 -5
View File
@@ -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;
}
+4
View File
@@ -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.
+15 -5
View File
@@ -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)
+20 -4
View File
@@ -890,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],
)
)
@@ -952,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"
@@ -1126,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(
@@ -1436,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]
@@ -1469,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")
@@ -1627,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
+3 -1
View File
@@ -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;
+12 -21
View File
@@ -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 {
+85
View File
@@ -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
+52
View File
@@ -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))
+72 -2
View File
@@ -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");
}
+26
View File
@@ -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)
@@ -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}"
+188 -131
View File
@@ -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
+107 -177
View File
@@ -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 arent 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
+6
View File
@@ -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"),
)
+5
View File
@@ -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)
+1 -1
View File
@@ -84,7 +84,7 @@ class ESPHomeOTAComponent final : 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_;
+7 -2
View File
@@ -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])
+1
View File
@@ -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])
+18 -26
View File
@@ -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.
+1
View File
@@ -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")
+7
View File
@@ -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);
@@ -43,3 +43,4 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await improv_base.setup_improv_core(var, config, "improv_serial")
cg.add_define("USE_IMPROV_SERIAL")
+8 -13
View File
@@ -173,8 +173,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
@@ -361,17 +359,14 @@ void LD2410Component::handle_periodic_data_() {
Detect distance: 16~17th bytes
*/
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(
this->moving_target_distance_sensor_,
ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]))
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
SAFE_PUBLISH_SENSOR(
this->still_target_distance_sensor_,
ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]));
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
SAFE_PUBLISH_SENSOR(
this->detection_distance_sensor_,
ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH]));
SAFE_PUBLISH_SENSOR(this->detection_distance_sensor_,
encode_uint16(this->buffer_data_[DETECT_DISTANCE_HIGH], this->buffer_data_[DETECT_DISTANCE_LOW]));
if (engineering_mode) {
/*
@@ -578,8 +573,8 @@ bool LD2410Component::handle_ack_data_() {
/*
None Duration: 33~34th bytes
*/
updates.push_back(set_number_value(this->timeout_number_,
ld2410::two_byte_to_int(this->buffer_data_[32], this->buffer_data_[33])));
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32])));
for (auto &update : updates) {
update();
}
+9 -14
View File
@@ -192,8 +192,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
@@ -398,22 +396,19 @@ void LD2412Component::handle_periodic_data_() {
Detect distance: 16~17th bytes
*/
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(
this->moving_target_distance_sensor_,
ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]))
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
SAFE_PUBLISH_SENSOR(
this->still_target_distance_sensor_,
ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]))
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY])
if (this->detection_distance_sensor_ != nullptr) {
int new_detect_distance = 0;
if (target_state != 0x00 && (target_state & MOVE_BITMASK)) {
new_detect_distance =
ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]);
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]);
} else if (target_state != 0x00) {
new_detect_distance =
ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]);
new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]);
}
this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance);
}
@@ -637,9 +632,9 @@ bool LD2412Component::handle_ack_data_() {
/*
None Duration: 11~12th bytes
*/
updates.push_back(set_number_value(this->timeout_number_,
ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])));
ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]));
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12])));
ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
/*
Output pin configuration: 13th bytes
*/
+19 -28
View File
@@ -168,15 +168,6 @@ static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) {
return dec_val;
}
static inline float calculate_angle(float base, float hypotenuse) {
if (base < 0.0f || hypotenuse <= 0.0f) {
return 0.0f;
}
float angle_radians = acosf(base / hypotenuse);
float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v<float>);
return angle_degrees;
}
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
@@ -292,16 +283,19 @@ void LD2450Component::loop() {
}
}
// Count targets in zone
uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) {
uint8_t count = 0;
for (auto &index : this->target_info_) {
if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 &&
index.is_moving == is_moving) {
count++;
// Count targets in zone (single pass for both still and moving)
void LD2450Component::count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving) {
still = 0;
moving = 0;
for (auto &target : this->target_info_) {
if (target.x > zone.x1 && target.x < zone.x2 && target.y > zone.y1 && target.y < zone.y2) {
if (target.is_moving) {
moving++;
} else {
still++;
}
}
}
return count;
}
// Service reset_radar_zone
@@ -510,11 +504,8 @@ void LD2450Component::handle_periodic_data_() {
}
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
// ANGLE
angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) {
angle = angle * -1;
}
// ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed
angle = atan2f(static_cast<float>(-tx), static_cast<float>(ty)) * (180.0f / std::numbers::pi_v<float>);
SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
#endif
#ifdef USE_TEXT_SENSOR
@@ -528,10 +519,11 @@ void LD2450Component::handle_periodic_data_() {
} else {
direction = DIRECTION_STATIONARY;
}
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction);
if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) {
tsd->publish_state(dir_str);
if (this->direction_dedup_[index].next(direction)) {
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
}
}
#endif
@@ -551,8 +543,7 @@ void LD2450Component::handle_periodic_data_() {
uint8_t zone_moving_targets = 0;
uint8_t zone_all_targets = 0;
for (index = 0; index < MAX_ZONES; index++) {
zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
this->count_targets_in_zone_(this->zone_config_[index], zone_still_targets, zone_moving_targets);
zone_all_targets = zone_still_targets + zone_moving_targets;
// Publish Still Target Count in Zones
+3 -2
View File
@@ -163,7 +163,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
void save_to_flash_(float value);
float restore_from_flash_();
bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving);
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
@@ -194,7 +194,8 @@ class LD2450Component : public Component, public uart::UARTDevice {
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_moving_target_count_sensors_{};
#endif
#ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
std::array<Deduplicator<uint8_t>, MAX_TARGETS> direction_dedup_{};
#endif
LazyCallbackManager<void()> data_callback_;
+3
View File
@@ -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"
@@ -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()); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void HOT delay(uint32_t ms) { ::delay(ms); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
@@ -52,6 +54,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); }
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
} // namespace esphome
+12 -21
View File
@@ -17,16 +17,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 LibreTinyPreferenceBackend : 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;
}
@@ -57,11 +48,11 @@ class LibreTinyPreferenceBackend : 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;
}
}
@@ -122,11 +113,11 @@ class LibreTinyPreferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
fdb_blob_make(&this->blob, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
failed++;
last_err = err;
last_key = save.key;
@@ -134,7 +125,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
cached++;
}
}
@@ -159,7 +150,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Check size first - if different, data has changed
if (kv.value_len != to_save.len) {
if (kv.value_len != to_save.data.size()) {
return true;
}
@@ -173,7 +164,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Compare the actual data
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
}
bool reset() override {
+39 -1
View File
@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
import enum
import esphome.automation as auto
@@ -37,7 +38,7 @@ from esphome.const import (
CONF_WEB_SERVER,
CONF_WHITE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
@@ -66,6 +67,40 @@ from .types import ( # noqa
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
DOMAIN = "light"
@dataclass
class LightData:
gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr
def _get_data() -> LightData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = LightData()
return CORE.data[DOMAIN]
def _get_or_create_gamma_table(gamma_correct):
data = _get_data()
if gamma_correct in data.gamma_tables:
return data.gamma_tables[gamma_correct]
if gamma_correct > 0:
forward = [
HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
for i in range(256)
]
else:
forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
gamma_str = f"{gamma_correct}".replace(".", "_")
fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16)
fwd_arr = cg.progmem_array(fwd_id, forward)
data.gamma_tables[gamma_correct] = fwd_arr
return fwd_arr
LightRestoreMode = light_ns.enum("LightRestoreMode")
RESTORE_MODES = {
"RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF,
@@ -239,6 +274,9 @@ async def setup_light_core_(light_var, output_var, config):
cg.add(light_var.set_flash_transition_length(flash_transition_length))
if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None:
cg.add(light_var.set_gamma_correct(gamma_correct))
fwd_arr = _get_or_create_gamma_table(gamma_correct)
cg.add(light_var.set_gamma_table(fwd_arr))
cg.add_define("USE_LIGHT_GAMMA_LUT")
effects = await cg.build_registry_list(
EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])
)
+3 -1
View File
@@ -66,7 +66,9 @@ class AddressableLight : public LightOutput, public Component {
Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white)));
}
void setup_state(LightState *state) override {
this->correction_.calculate_gamma_table(state->get_gamma_correct());
#ifdef USE_LIGHT_GAMMA_LUT
this->correction_.set_gamma_table(state->get_gamma_table());
#endif
this->state_parent_ = state;
}
void update_state(LightState *state) override;
@@ -74,11 +74,10 @@ class AddressableLightWrapper : public light::AddressableLight {
return;
}
float gamma = this->light_state_->get_gamma_correct();
float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma);
float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma);
float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma);
float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma);
float r = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[0] / 255.0f);
float g = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[1] / 255.0f);
float b = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[2] / 255.0f);
float w = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[3] / 255.0f);
auto call = this->light_state_->make_call();
+1 -1
View File
@@ -41,7 +41,7 @@ template<typename... Ts> class LightControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, color_temperature)
TEMPLATABLE_VALUE(float, cold_white)
TEMPLATABLE_VALUE(float, warm_white)
TEMPLATABLE_VALUE(std::string, effect)
TEMPLATABLE_VALUE(uint32_t, effect)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
+56 -6
View File
@@ -10,12 +10,14 @@ from esphome.const import (
CONF_COLOR_MODE,
CONF_COLOR_TEMPERATURE,
CONF_EFFECT,
CONF_EFFECTS,
CONF_FLASH_LENGTH,
CONF_GREEN,
CONF_ID,
CONF_LIMIT_MODE,
CONF_MAX_BRIGHTNESS,
CONF_MIN_BRIGHTNESS,
CONF_NAME,
CONF_RANGE_FROM,
CONF_RANGE_TO,
CONF_RED,
@@ -24,6 +26,9 @@ from esphome.const import (
CONF_WARM_WHITE,
CONF_WHITE,
)
from esphome.core import CORE, Lambda
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType
from .types import (
COLOR_MODES,
@@ -51,6 +56,7 @@ from .types import (
),
}
),
synchronous=True,
)
async def light_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -110,14 +116,34 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(
)
def _resolve_effect_index(config: ConfigType) -> int:
"""Resolve a static effect name to its 1-based index at codegen time.
Effect index 0 means "None" (no effect). Effects are 1-indexed matching
the C++ convention in LightState.
"""
original_name = config[CONF_EFFECT]
effect_name = original_name.lower()
if effect_name == "none":
return 0
light_id = config[CONF_ID]
light_path = CORE.config.get_path_for_id(light_id)[:-1]
light_config = CORE.config.get_config_for_path(light_path)
for i, effect_conf in enumerate(light_config.get(CONF_EFFECTS, [])):
key = next(iter(effect_conf))
if effect_conf[key][CONF_NAME].lower() == effect_name:
return i + 1
raise ValueError(f"Effect '{original_name}' not found in light '{light_id}'")
@automation.register_action(
"light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA
"light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA
"light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA
"light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA, synchronous=True
)
async def light_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -164,8 +190,29 @@ async def light_control_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float)
cg.add(var.set_warm_white(template_))
if CONF_EFFECT in config:
template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string)
cg.add(var.set_effect(template_))
if isinstance(config[CONF_EFFECT], Lambda):
# Lambda returns a string — wrap in a C++ lambda that resolves
# the effect name to its uint32_t index at runtime
inner_lambda = await cg.process_lambda(
config[CONF_EFFECT], args, return_type=cg.std_string
)
fwd_args = ", ".join(n for _, n in args)
# capture="" is correct: paren is a global variable name
# string-interpolated into the body at codegen time, not a
# C++ runtime capture.
wrapper = LambdaExpression(
f"auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"return {paren}->get_effect_index("
f"__effect_s.c_str(), __effect_s.size());",
args,
capture="",
return_type=cg.uint32,
)
cg.add(var.set_effect(wrapper))
else:
# Static string — resolve effect name to index at codegen time
effect_index = _resolve_effect_index(config)
cg.add(var.set_effect(effect_index))
return var
@@ -193,7 +240,10 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"light.dim_relative", DimRelativeAction, LIGHT_DIM_RELATIVE_ACTION_SCHEMA
"light.dim_relative",
DimRelativeAction,
LIGHT_DIM_RELATIVE_ACTION_SCHEMA,
synchronous=True,
)
async def light_dim_relative_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -1,25 +1,25 @@
#include "esp_color_correction.h"
#include "light_color_values.h"
#include "esphome/core/log.h"
namespace esphome::light {
void ESPColorCorrection::calculate_gamma_table(float gamma) {
for (uint16_t i = 0; i < 256; i++) {
// corrected = val ^ gamma
auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma));
this->gamma_table_[i] = corrected;
}
if (gamma == 0.0f) {
for (uint16_t i = 0; i < 256; i++)
this->gamma_reverse_table_[i] = i;
return;
}
for (uint16_t i = 0; i < 256; i++) {
// val = corrected ^ (1/gamma)
auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma));
this->gamma_reverse_table_[i] = uncorrected;
}
uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const {
if (this->gamma_table_ == nullptr)
return value;
return static_cast<uint8_t>((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257);
}
uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
if (this->gamma_table_ == nullptr)
return value;
if (value == 0)
return 0;
uint16_t target = value * 257; // Scale 0-255 to 0-65535
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
if (lo >= 255)
return 255;
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
return (target - a <= b - target) ? lo : lo + 1;
}
} // namespace esphome::light
+32 -13
View File
@@ -1,15 +1,30 @@
#pragma once
#include "esphome/core/color.h"
#include "esphome/core/hal.h"
namespace esphome::light {
/// Binary search a monotonically increasing uint16[256] PROGMEM table.
/// Returns the largest index where table[index] <= target.
inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) {
uint8_t lo = 0, hi = 255;
while (lo < hi) {
uint8_t mid = (lo + hi + 1) / 2;
if (progmem_read_uint16(&table[mid]) <= target) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
class ESPColorCorrection {
public:
ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {}
void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; }
void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; }
void calculate_gamma_table(float gamma);
void set_gamma_table(const uint16_t *table) { this->gamma_table_ = table; }
inline Color color_correct(Color color) const ESPHOME_ALWAYS_INLINE {
// corrected = (uncorrected * max_brightness * local_brightness) ^ gamma
return Color(this->color_correct_red(color.red), this->color_correct_green(color.green),
@@ -17,19 +32,19 @@ class ESPColorCorrection {
}
inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
@@ -39,36 +54,40 @@ class ESPColorCorrection {
inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.red == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.green == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.white == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
protected:
uint8_t gamma_table_[256];
uint8_t gamma_reverse_table_[256];
Color max_brightness_;
/// Forward gamma: read uint16 PROGMEM table, convert to uint8
uint8_t gamma_correct_(uint8_t value) const;
/// Reverse gamma: binary search the forward PROGMEM table
uint8_t gamma_uncorrect_(uint8_t value) const;
const uint16_t *gamma_table_{nullptr};
Color max_brightness_{255, 255, 255, 255};
uint8_t local_brightness_{255};
};
+2 -3
View File
@@ -389,9 +389,8 @@ void LightCall::transform_parameters_() {
const float ww_fraction = (color_temp - min_mireds) / range;
const float cw_fraction = 1.0f - ww_fraction;
const float max_cw_ww = std::max(ww_fraction, cw_fraction);
const float gamma = this->parent_->get_gamma_correct();
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
this->cold_white_ = this->parent_->gamma_uncorrect_lut(cw_fraction / max_cw_ww);
this->warm_white_ = this->parent_->gamma_uncorrect_lut(ww_fraction / max_cw_ww);
this->set_flag_(FLAG_HAS_COLD_WHITE);
this->set_flag_(FLAG_HAS_WARM_WHITE);
}
+21 -27
View File
@@ -111,60 +111,54 @@ class LightColorValues {
}
}
// Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters
// are always used or necessary. Methods will be deprecated later.
/// Convert these light color values to a binary representation and write them to binary.
void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; }
/// Convert these light color values to a brightness-only representation and write them to brightness.
void as_brightness(float *brightness, float gamma = 0) const {
*brightness = gamma_correct(this->state_ * this->brightness_, gamma);
}
void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; }
/// Convert these light color values to an RGB representation and write them to red, green, blue.
void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const {
void as_rgb(float *red, float *green, float *blue) const {
if (this->color_mode_ & ColorCapability::RGB) {
float brightness = this->state_ * this->brightness_ * this->color_brightness_;
*red = gamma_correct(brightness * this->red_, gamma);
*green = gamma_correct(brightness * this->green_, gamma);
*blue = gamma_correct(brightness * this->blue_, gamma);
*red = brightness * this->red_;
*green = brightness * this->green_;
*blue = brightness * this->blue_;
} else {
*red = *green = *blue = 0;
}
}
/// Convert these light color values to an RGBW representation and write them to red, green, blue, white.
void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0,
bool color_interlock = false) const {
this->as_rgb(red, green, blue, gamma);
void as_rgbw(float *red, float *green, float *blue, float *white) const {
this->as_rgb(red, green, blue);
if (this->color_mode_ & ColorCapability::WHITE) {
*white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma);
*white = this->state_ * this->brightness_ * this->white_;
} else {
*white = 0;
}
}
/// Convert these light color values to an RGBWW representation with the given parameters.
void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0,
void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness = false) const {
this->as_rgb(red, green, blue, gamma);
this->as_cwww(cold_white, warm_white, gamma, constant_brightness);
this->as_rgb(red, green, blue);
this->as_cwww(cold_white, warm_white, constant_brightness);
}
/// Convert these light color values to an RGB+CT+BR representation with the given parameters.
void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue,
float *color_temperature, float *white_brightness, float gamma = 0) const {
this->as_rgb(red, green, blue, gamma);
this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma);
float *color_temperature, float *white_brightness) const {
this->as_rgb(red, green, blue);
this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness);
}
/// Convert these light color values to an CWWW representation with the given parameters.
void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const {
void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const {
if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) {
const float cw_level = gamma_correct(this->cold_white_, gamma);
const float ww_level = gamma_correct(this->warm_white_, gamma);
const float white_level = gamma_correct(this->state_ * this->brightness_, gamma);
const float cw_level = this->cold_white_;
const float ww_level = this->warm_white_;
const float white_level = this->state_ * this->brightness_;
if (!constant_brightness) {
*cold_white = white_level * cw_level;
*warm_white = white_level * ww_level;
@@ -184,13 +178,13 @@ class LightColorValues {
}
/// Convert these light color values to a CT+BR representation with the given parameters.
void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness,
float gamma = 0) const {
void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature,
float *white_brightness) const {
const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1;
if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) {
*color_temperature =
(this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw);
*white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma);
*white_brightness = this->state_ * this->brightness_ * white_level;
} else { // Probably won't get here but put this here anyway.
*white_brightness = 0;
}
+68 -10
View File
@@ -1,4 +1,5 @@
#include "light_state.h"
#include "esp_color_correction.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
@@ -204,33 +205,90 @@ void LightState::add_effects(const std::initializer_list<LightEffect *> &effects
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
void LightState::current_values_as_brightness(float *brightness) {
this->current_values.as_brightness(brightness, this->gamma_correct_);
this->current_values.as_brightness(brightness);
*brightness = this->gamma_correct_lut(*brightness);
}
void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) {
this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false);
void LightState::current_values_as_rgb(float *red, float *green, float *blue) {
this->current_values.as_rgb(red, green, blue);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
}
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) {
this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false);
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) {
this->current_values.as_rgbw(red, green, blue, white);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*white = this->gamma_correct_lut(*white);
}
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness) {
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness);
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
}
void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature,
float *white_brightness) {
auto traits = this->get_traits();
this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature,
white_brightness, this->gamma_correct_);
white_brightness);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness);
this->current_values.as_cwww(cold_white, warm_white, constant_brightness);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
}
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
auto traits = this->get_traits();
this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness,
this->gamma_correct_);
this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness);
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
#ifdef USE_LIGHT_GAMMA_LUT
float LightState::gamma_correct_lut(float value) const {
if (value <= 0.0f)
return 0.0f;
if (value >= 1.0f)
return 1.0f;
if (this->gamma_table_ == nullptr)
return value;
float scaled = value * 255.0f;
auto idx = static_cast<uint8_t>(scaled);
if (idx >= 255)
return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f;
float frac = scaled - idx;
float a = progmem_read_uint16(&this->gamma_table_[idx]);
float b = progmem_read_uint16(&this->gamma_table_[idx + 1]);
return (a + frac * (b - a)) / 65535.0f;
}
float LightState::gamma_uncorrect_lut(float value) const {
if (value <= 0.0f)
return 0.0f;
if (value >= 1.0f)
return 1.0f;
if (this->gamma_table_ == nullptr)
return value;
uint16_t target = static_cast<uint16_t>(value * 65535.0f);
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
if (lo >= 255)
return 1.0f;
// Interpolate between lo and lo+1
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
if (b == a)
return lo / 255.0f;
float frac = static_cast<float>(target - a) / static_cast<float>(b - a);
return (lo + frac) / 255.0f;
}
#endif // USE_LIGHT_GAMMA_LUT
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
void LightState::start_effect_(uint32_t effect_index) {
+39 -2
View File
@@ -11,7 +11,9 @@
#include "light_traits.h"
#include "light_transformer.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <strings.h>
#include <vector>
@@ -166,6 +168,23 @@ class LightState : public EntityBase, public Component {
void set_gamma_correct(float gamma_correct);
float get_gamma_correct() const { return this->gamma_correct_; }
#ifdef USE_LIGHT_GAMMA_LUT
/// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array)
void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; }
/// Get the forward gamma lookup table
const uint16_t *get_gamma_table() const { return this->gamma_table_; }
/// Apply gamma correction using the pre-computed forward LUT
float gamma_correct_lut(float value) const;
/// Reverse gamma correction by binary-searching the forward LUT
float gamma_uncorrect_lut(float value) const;
#else
/// No gamma LUT — passthrough
float gamma_correct_lut(float value) const { return value; }
float gamma_uncorrect_lut(float value) const { return value; }
#endif // USE_LIGHT_GAMMA_LUT
/// Set the restore mode of this light
void set_restore_mode(LightRestoreMode restore_mode);
@@ -200,6 +219,20 @@ class LightState : public EntityBase, public Component {
return 0; // Effect not found
}
/// Get effect index by name (const char* overload, avoids std::string construction).
uint32_t get_effect_index(const char *name, size_t len) const {
if (len == 4 && ESPHOME_strncasecmp_P(name, ESPHOME_PSTR("none"), 4) == 0) {
return 0;
}
StringRef ref(name, len);
for (size_t i = 0; i < this->effects_.size(); i++) {
if (str_equals_case_insensitive(ref, this->effects_[i]->get_name())) {
return i + 1;
}
}
return 0;
}
/// Get effect by index. Returns nullptr if index is invalid.
LightEffect *get_effect_by_index(uint32_t index) const {
if (index == 0 || index > this->effects_.size()) {
@@ -224,9 +257,9 @@ class LightState : public EntityBase, public Component {
void current_values_as_brightness(float *brightness);
void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false);
void current_values_as_rgb(float *red, float *green, float *blue);
void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false);
void current_values_as_rgbw(float *red, float *green, float *blue, float *white);
void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness = false);
@@ -297,6 +330,10 @@ class LightState : public EntityBase, public Component {
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
#ifdef USE_LIGHT_GAMMA_LUT
const uint16_t *gamma_table_{nullptr};
#endif // USE_LIGHT_GAMMA_LUT
/// Whether the light value should be written in the next cycle.
bool next_write_{true};
// for effects, true if a transformer (transition) is active.
+3 -1
View File
@@ -519,7 +519,9 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(
)
@automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA)
@automation.register_action(
CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA, synchronous=True
)
async def logger_log_action_to_code(config, action_id, template_arg, args):
esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]]
args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]]
+3 -2
View File
@@ -8,6 +8,7 @@ static constexpr const char *const TAG = "lps22";
static constexpr uint8_t WHO_AM_I = 0x0F;
static constexpr uint8_t LPS22HB_ID = 0xB1;
static constexpr uint8_t LPS22HH_ID = 0xB3;
static constexpr uint8_t LPS22DF_ID = 0xB4;
static constexpr uint8_t CTRL_REG2 = 0x11;
static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
static constexpr uint8_t STATUS = 0x27;
@@ -24,8 +25,8 @@ static constexpr float TEMPERATURE_SCALE = 0.01f;
void LPS22Component::setup() {
uint8_t value = 0x00;
this->read_register(WHO_AM_I, &value, 1);
if (value != LPS22HB_ID && value != LPS22HH_ID) {
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
if (value != LPS22HB_ID && value != LPS22HH_ID && value != LPS22DF_ID) {
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB/HH/DF ID", value);
this->mark_failed();
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ class LVLight : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue;
state->current_values_as_rgb(&red, &green, &blue, false);
state->current_values_as_rgb(&red, &green, &blue);
auto color = lv_color_make(red * 255, green * 255, blue * 255);
if (this->obj_ != nullptr) {
this->set_value_(color);
+26 -45
View File
@@ -8,90 +8,71 @@ namespace mcp23016 {
static const char *const TAG = "mcp23016";
void MCP23016::setup() {
uint8_t iocon;
if (!this->read_reg_(MCP23016_IOCON0, &iocon)) {
uint16_t iocon;
// MCP23016 registers operate as paired 16-bit registers. Addressing the
// odd register (e.g. IOCON1) reads/writes that register first, then wraps
// to the even register (IOCON0) in the same pair. Starting from the odd
// address gives the correct byte order for 1 << pin mapping:
// high byte = port 1 (pins 8-15), low byte = port 0 (pins 0-7).
if (!this->read_reg_(MCP23016_IOCON1, &iocon)) {
this->mark_failed();
return;
}
// Read current output register state
this->read_reg_(MCP23016_OLAT0, &this->olat_0_);
this->read_reg_(MCP23016_OLAT1, &this->olat_1_);
this->read_reg_(MCP23016_OLAT1, &this->olat_);
// all pins input
this->write_reg_(MCP23016_IODIR0, 0xFF);
this->write_reg_(MCP23016_IODIR1, 0xFF);
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
}
void MCP23016::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
}
bool MCP23016::digital_read_hw(uint8_t pin) {
uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1;
uint8_t value = 0;
if (!this->read_reg_(reg_addr, &value)) {
return false;
}
// Update the appropriate part of input_mask_
if (pin < 8) {
this->input_mask_ = (this->input_mask_ & 0xFF00) | value;
} else {
this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8);
}
return true;
}
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void MCP23016::digital_write_hw(uint8_t pin, bool value) {
uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1;
this->update_reg_(pin, value, reg_addr);
}
void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin, value, MCP23016_OLAT1); }
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
uint8_t iodir = pin < 8 ? MCP23016_IODIR0 : MCP23016_IODIR1;
if (flags == gpio::FLAG_INPUT) {
this->update_reg_(pin, true, iodir);
this->update_reg_(pin, true, MCP23016_IODIR1);
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg_(pin, false, iodir);
this->update_reg_(pin, false, MCP23016_IODIR1);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
bool MCP23016::read_reg_(uint8_t reg, uint16_t *value) {
if (this->is_failed())
return false;
return this->read_byte(reg, value);
return this->read_byte_16(reg, value);
}
bool MCP23016::write_reg_(uint8_t reg, uint8_t value) {
bool MCP23016::write_reg_(uint8_t reg, uint16_t value) {
if (this->is_failed())
return false;
return this->write_byte(reg, value);
return this->write_byte_16(reg, value);
}
void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
uint8_t bit = pin % 8;
uint8_t reg_value = 0;
if (reg_addr == MCP23016_OLAT0) {
reg_value = this->olat_0_;
} else if (reg_addr == MCP23016_OLAT1) {
reg_value = this->olat_1_;
uint16_t reg_value = 0;
if (reg_addr == MCP23016_OLAT1) {
reg_value = this->olat_;
} else {
this->read_reg_(reg_addr, &reg_value);
}
if (pin_value) {
reg_value |= 1 << bit;
reg_value |= 1 << pin;
} else {
reg_value &= ~(1 << bit);
reg_value &= ~(1 << pin);
}
this->write_reg_(reg_addr, reg_value);
if (reg_addr == MCP23016_OLAT0) {
this->olat_0_ = reg_value;
} else if (reg_addr == MCP23016_OLAT1) {
this->olat_1_ = reg_value;
if (reg_addr == MCP23016_OLAT1) {
this->olat_ = reg_value;
}
}
+7 -8
View File
@@ -19,13 +19,13 @@ enum MCP23016GPIORegisters {
// 1 side
MCP23016_GP1 = 0x01,
MCP23016_OLAT1 = 0x03,
MCP23016_IPOL1 = 0x04,
MCP23016_IPOL1 = 0x05,
MCP23016_IODIR1 = 0x07,
MCP23016_INTCAP1 = 0x08,
MCP23016_INTCAP1 = 0x09,
MCP23016_IOCON1 = 0x0B,
};
class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint8_t, 16> {
class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint16_t, 16> {
public:
MCP23016() = default;
@@ -42,16 +42,15 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
void digital_write_hw(uint8_t pin, bool value) override;
// read a given register
bool read_reg_(uint8_t reg, uint8_t *value);
bool read_reg_(uint8_t reg, uint16_t *value);
// write a value to a given register
bool write_reg_(uint8_t reg, uint8_t value);
bool write_reg_(uint8_t reg, uint16_t value);
// update registers with given pin value.
void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a);
uint8_t olat_0_{0x00};
uint8_t olat_1_{0x00};
uint16_t olat_{0x0000};
// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x00};
uint16_t input_mask_{0x0000};
};
class MCP23016GPIOPin : public GPIOPin {
+1 -1
View File
@@ -163,7 +163,7 @@ async def to_code(config):
cg.add_library("LEAmDNS", None)
if CORE.is_esp32:
add_idf_component(name="espressif/mdns", ref="1.9.1")
add_idf_component(name="espressif/mdns", ref="1.10.0")
cg.add_define("USE_MDNS")
@@ -0,0 +1,40 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
IS_PLATFORM_COMPONENT = True
media_source_ns = cg.esphome_ns.namespace("media_source")
MediaSource = media_source_ns.class_("MediaSource")
async def register_media_source(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
CORE.register_platform_component("media_source", var)
return var
_MEDIA_SOURCE_SCHEMA = cv.Schema({})
def media_source_schema(
class_: MockObjClass,
) -> cv.Schema:
schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)}
return _MEDIA_SOURCE_SCHEMA.extend(schema)
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config):
cg.add_global(media_source_ns.using)
cg.add_define("USE_MEDIA_SOURCE")
@@ -0,0 +1,159 @@
#pragma once
#include "esphome/components/audio/audio.h"
#include "esphome/core/helpers.h"
#include <cstdint>
#include <string>
namespace esphome::media_source {
enum class MediaSourceState : uint8_t {
IDLE, // Not playing, ready to accept play_uri
PLAYING, // Currently playing media
PAUSED, // Playback paused, can be resumed
ERROR, // Error occurred during playback; sources are responsible for logging their own error details
};
/// @brief Commands that are sent from the orchestrator to a media source
enum class MediaSourceCommand : uint8_t {
// All sources should support these basic commands.
PLAY,
PAUSE,
STOP,
// Only sources with internal playlists will handle these; simple sources should ignore them.
NEXT,
PREVIOUS,
CLEAR_PLAYLIST,
REPEAT_ALL,
REPEAT_ONE,
REPEAT_OFF,
SHUFFLE,
UNSHUFFLE,
};
/// @brief Callbacks from a MediaSource to its orchestrator
class MediaSourceListener {
public:
virtual ~MediaSourceListener() = default;
// Callbacks that all sources use to send data and state changes to the orchestrator.
/// @brief Send audio data to the listener
virtual size_t write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms,
const audio::AudioStreamInfo &stream_info) = 0;
/// @brief Notify listener of state changes
virtual void report_state(MediaSourceState state) = 0;
// Callbacks from smart sources requesting the orchestrator to change volume, mute, or start a new URI.
// Simple sources never invoke these.
/// @brief Request the orchestrator to change volume
virtual void request_volume(float volume) {}
/// @brief Request the orchestrator to change mute state
virtual void request_mute(bool is_muted) {}
/// @brief Request the orchestrator to play a new URI
virtual void request_play_uri(const std::string &uri) {}
};
/// @brief Abstract base class for media sources
/// MediaSource provides audio data to an orchestrator via the MediaSourceListener interface. It also receives commands
/// from the orchestrator to control playback.
class MediaSource {
public:
virtual ~MediaSource() = default;
// === Playback Control ===
/// @brief Start playing the given URI
/// Sources should validate the URI and state, returning false if the source is busy.
/// The orchestrator is responsible for stopping active sources before starting a new one.
/// @param uri URI to play; e.g., "http://stream_url"
/// @return true if playback started successfully, false otherwise
virtual bool play_uri(const std::string &uri) = 0;
/// @brief Handle playback commands (pause, stop, next, etc.)
/// @param command Command to execute
virtual void handle_command(MediaSourceCommand command) = 0;
/// @brief Whether this source manages its own playlist internally
/// Smart sources that handle next/previous/repeat/shuffle should override this to return true.
virtual bool has_internal_playlist() const { return false; }
// === State Access ===
/// @brief Get current playback state (must only be called from the main loop)
/// @return Current state of this source
MediaSourceState get_state() const { return this->state_; }
// === URI Matching ===
/// @brief Check if this source can handle the given URI
/// Each source must override this to match its supported URI scheme(s).
/// @param uri URI to check
/// @return true if this source can handle the URI
virtual bool can_handle(const std::string &uri) const = 0;
// === Listener: Source -> Orchestrator ===
/// @brief Set the listener that receives callbacks from this source
/// @param listener Pointer to the MediaSourceListener implementation
void set_listener(MediaSourceListener *listener) { this->listener_ = listener; }
/// @brief Check if a listener has been registered
bool has_listener() const { return this->listener_ != nullptr; }
/// @brief Write audio data to the listener
/// @param data Pointer to audio data buffer (not modified by this method)
/// @param length Number of bytes to write
/// @param timeout_ms Milliseconds to wait if the listener can't accept data immediately
/// @param stream_info Audio stream format information
/// @return Number of bytes written, or 0 if no listener is set
size_t write_output(const uint8_t *data, size_t length, uint32_t timeout_ms,
const audio::AudioStreamInfo &stream_info) {
if (this->listener_ != nullptr) {
return this->listener_->write_audio(data, length, timeout_ms, stream_info);
}
return 0;
}
// === Callbacks: Orchestrator -> Source ===
/// @brief Notify the source that volume changed
/// Simple sources ignore this. Override for smart sources that track volume state.
/// @param volume New volume level (0.0 to 1.0)
virtual void notify_volume_changed(float volume) {}
/// @brief Notify the source that mute state changed
/// Simple sources ignore this. Override for smart sources that track mute state.
/// @param is_muted New mute state
virtual void notify_mute_changed(bool is_muted) {}
/// @brief Notify the source about audio that has been played
/// Called when the speaker reports that audio frames have been written to the DAC.
/// Sources can override this to track playback progress for synchronization.
/// @param frames Number of audio frames that were played
/// @param timestamp System time in microseconds when the frames finished writing to the DAC
virtual void notify_audio_played(uint32_t frames, int64_t timestamp) {}
protected:
/// @brief Update state and notify listener (must only be called from the main loop)
/// This is the only way to change state_, ensuring listener notifications always fire.
/// Sources running FreeRTOS tasks should signal via event groups and call this from loop().
/// @param state New state to set
void set_state_(MediaSourceState state) {
if (this->state_ != state) {
this->state_ = state;
if (this->listener_ != nullptr) {
this->listener_->report_state(state);
}
}
}
private:
// Private to enforce the invariant that listener notifications always fire on state changes.
// All state transitions must go through set_state_() which couples the update with notification.
MediaSourceState state_{MediaSourceState::IDLE};
MediaSourceListener *listener_{nullptr};
};
} // namespace esphome::media_source
@@ -103,8 +103,6 @@ DriverChip(
(0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
(0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00),
(0xEF, 0xFF, 0xFF, 0x01),
(0x11, 0x00),
(0x29, 0x00),
],
)
@@ -125,6 +123,7 @@ DriverChip(
lane_bit_rate="900Mbps",
no_transform=True,
color_order="RGB",
reset_pin=33,
initsequence=[
(0x80, 0x8B),
(0x81, 0x78),
+5 -2
View File
@@ -492,7 +492,7 @@ MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA
"mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA, synchronous=True
)
async def mqtt_publish_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -521,7 +521,10 @@ MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"mqtt.publish_json", MQTTPublishJsonAction, MQTT_PUBLISH_JSON_ACTION_SCHEMA
"mqtt.publish_json",
MQTTPublishJsonAction,
MQTT_PUBLISH_JSON_ACTION_SCHEMA,
synchronous=True,
)
async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -405,12 +405,6 @@ void MQTTComponent::process_resend() {
this->schedule_resend_state();
}
}
void MQTTComponent::call_dump_config() {
if (this->is_internal())
return;
this->dump_config();
}
void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
-2
View File
@@ -98,8 +98,6 @@ class MQTTComponent : public Component {
/// Override setup_ so that we can call send_discovery() when needed.
void call_setup() override;
void call_dump_config() override;
/// Send discovery info the Home Assistant, override this.
virtual void send_discovery(JsonObject root, SendDiscoveryConfig &config) = 0;
+24 -13
View File
@@ -240,6 +240,23 @@ def number_schema(
return _NUMBER_SCHEMA.extend(schema)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_number_automations(var, config):
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(float, "x")], conf)
for conf in config.get(CONF_ON_VALUE_RANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await cg.register_component(trigger, conf)
if CONF_ABOVE in conf:
template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float)
cg.add(trigger.set_min(template_))
if CONF_BELOW in conf:
template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float)
cg.add(trigger.set_max(template_))
await automation.build_automation(trigger, [(float, "x")], conf)
async def setup_number_core_(
var, config, *, min_value: float, max_value: float, step: float
):
@@ -254,19 +271,7 @@ async def setup_number_core_(
if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO:
cg.add(var.traits.set_mode(config[CONF_MODE]))
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(float, "x")], conf)
for conf in config.get(CONF_ON_VALUE_RANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await cg.register_component(trigger, conf)
if CONF_ABOVE in conf:
template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float)
cg.add(trigger.set_min(template_))
if CONF_BELOW in conf:
template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float)
cg.add(trigger.set_max(template_))
await automation.build_automation(trigger, [(float, "x")], conf)
CORE.add_job(_build_number_automations, var, config)
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
cg.add(var.traits.set_unit_of_measurement(unit_of_measurement))
@@ -347,6 +352,7 @@ OPERATION_BASE_SCHEMA = cv.Schema(
cv.Required(CONF_VALUE): cv.templatable(cv.float_),
}
),
synchronous=True,
)
async def number_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -369,6 +375,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.decrement",
@@ -383,6 +390,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.to_min",
@@ -396,6 +404,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.to_max",
@@ -409,6 +418,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.operation",
@@ -421,6 +431,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
}
),
synchronous=True,
)
async def number_to_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -52,6 +52,11 @@ def set_sdkconfig_options(config):
# There is a conflict if the logger's uart also uses the default UART, which is seen as a watchdog failure on "ot_cli"
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False)
# Console is the transport layer for CLI; disable it too since CLI is disabled
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CONSOLE_ENABLE", False)
# Diag unused, if needed for lab/cert/etc tests then enable separately
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DIAG", False)
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
+5 -2
View File
@@ -74,14 +74,16 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action("output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA)
@automation.register_action(
"output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True
)
async def output_turn_on_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(
"output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA
"output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True
)
async def output_turn_off_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -97,6 +99,7 @@ async def output_turn_off_to_code(config, action_id, template_arg, args):
cv.Required(CONF_LEVEL): cv.templatable(cv.percentage),
}
),
synchronous=True,
)
async def output_set_level_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+1 -1
View File
@@ -20,7 +20,7 @@ class RGBLightOutput : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue;
state->current_values_as_rgb(&red, &green, &blue, false);
state->current_values_as_rgb(&red, &green, &blue);
this->red_->set_level(red);
this->green_->set_level(green);
this->blue_->set_level(blue);
+1 -1
View File
@@ -25,7 +25,7 @@ class RGBWLightOutput : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue, white;
state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_);
state->current_values_as_rgbw(&red, &green, &blue, &white);
this->red_->set_level(red);
this->green_->set_level(green);
this->blue_->set_level(blue);
+1
View File
@@ -169,6 +169,7 @@ async def to_code(config):
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_RP2040")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
+7 -4
View File
@@ -5,15 +5,17 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "hardware/timer.h"
#include "hardware/watchdog.h"
namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return micros_to_millis<uint64_t>(time_us_64()); }
uint32_t HOT millis() { return micros_to_millis(time_us_64()); }
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); }
uint32_t HOT micros() { return ::micros(); }
void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
watchdog_reboot(0, 0, 10);
while (1) {
@@ -32,7 +34,8 @@ void HOT arch_feed_wdt() { watchdog_update(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
} // namespace esphome
+103 -86
View File
@@ -8,18 +8,26 @@ namespace esphome::rtttl {
static const char *const TAG = "rtttl";
// These values can also be found as constants in the Tone library (Tone.h)
static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
static constexpr uint8_t SONG_NAME_LENGTH_LIMIT = 64;
static constexpr uint8_t SEMITONES_IN_OCTAVE = 12;
#if defined(USE_OUTPUT) || defined(USE_SPEAKER)
static const uint32_t DOUBLE_NOTE_GAP_MS = 10;
#endif // USE_OUTPUT || USE_SPEAKER
static constexpr uint8_t MIN_OCTAVE = 4;
static constexpr uint8_t MAX_OCTAVE = 7;
static constexpr uint8_t DEFAULT_BPM = 63; // Default beats per minute
// These values can also be found as constants in the Tone library (Tone.h)
static constexpr uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
static constexpr uint8_t NOTES_COUNT = static_cast<uint8_t>(sizeof(NOTES) / sizeof(NOTES[0]));
static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10;
#ifdef USE_SPEAKER
static const size_t SAMPLE_BUFFER_SIZE = 2048;
static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048;
static constexpr uint16_t SAMPLE_RATE = 16000;
struct SpeakerSample {
int8_t left{0};
@@ -27,7 +35,7 @@ struct SpeakerSample {
};
inline double deg2rad(double degrees) {
static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0;
static constexpr double PI_ON_180 = M_PI / 180.0;
return degrees * PI_ON_180;
}
#endif // USE_SPEAKER
@@ -85,7 +93,7 @@ void Rtttl::loop() {
}
#ifdef USE_OUTPUT
if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) {
if (this->output_ != nullptr && millis() - this->last_note_start_time_ < this->note_duration_) {
return;
}
#endif // USE_OUTPUT
@@ -113,36 +121,34 @@ void Rtttl::loop() {
}
if (this->samples_sent_ != this->samples_count_) {
SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2];
int x = 0;
uint16_t sample_index = 0;
double rem = 0.0;
while (true) {
// Try and send out the remainder of the existing note, one per loop()
if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note//
// Try and send out the remainder of the existing note, one per `loop()`
if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note
rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_);
int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152
sample[x].left = val;
sample[x].right = val;
int8_t val = (127 * this->gain_) * sin(deg2rad(rem));
sample[sample_index].left = val;
sample[sample_index].right = val;
} else {
sample[x].left = 0;
sample[x].right = 0;
sample[sample_index].left = 0;
sample[sample_index].right = 0;
}
if (static_cast<size_t>(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
if (sample_index >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
break;
}
this->samples_sent_++;
x++;
sample_index++;
}
if (x > 0) {
size_t bytes_to_send = x * sizeof(SpeakerSample);
if (sample_index > 0) {
size_t bytes_to_send = sample_index * sizeof(SpeakerSample);
size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send);
if (send != bytes_to_send) {
this->samples_sent_ -= (x - (send / sizeof(SpeakerSample)));
this->samples_sent_ -= (sample_index - (send / sizeof(SpeakerSample)));
}
return;
}
@@ -155,83 +161,84 @@ void Rtttl::loop() {
return;
}
// align to note: most rtttl's out there does not add and space after the ',' separator but just in case...
// Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') {
this->position_++;
}
// first, get note duration, if available
uint8_t num = this->get_integer_();
// First, get note duration, if available
uint8_t note_denominator = this->get_integer_();
if (num) {
this->note_duration_ = this->wholenote_ / num;
if (note_denominator) {
this->note_duration_ = this->wholenote_duration_ / note_denominator;
} else {
this->note_duration_ =
this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after
// We will need to check if we are a dotted note after
this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_;
}
uint8_t note = note_index_from_char(this->rtttl_[this->position_]);
uint8_t note_index_in_octave = note_index_from_char(this->rtttl_[this->position_]);
this->position_++;
// now, get optional '#' sharp
// Now, get optional '#' sharp
if (this->rtttl_[this->position_] == '#') {
note++;
note_index_in_octave++;
this->position_++;
}
// now, get scale
// Now, get scale
uint8_t scale = this->get_integer_();
if (scale == 0) {
scale = this->default_octave_;
}
if (scale < 4 || scale > 7) {
ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale);
if (scale < MIN_OCTAVE || scale > MAX_OCTAVE) {
ESP_LOGE(TAG, "Octave must be between %d and %d (it is %d)", MIN_OCTAVE, MAX_OCTAVE, scale);
this->finish_();
return;
}
// now, get optional '.' dotted note
// Now, get optional '.' dotted note
if (this->rtttl_[this->position_] == '.') {
this->note_duration_ += this->note_duration_ / 2;
this->note_duration_ += this->note_duration_ / 2; // Duration +50%
this->position_++;
}
// Now play the note
bool need_note_gap = false;
if (note) {
auto note_index = (scale - 4) * 12 + note;
if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) {
ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index,
(int) (sizeof(NOTES) / sizeof(NOTES[0])));
// Now play the note
if (note_index_in_octave == 0) {
this->output_freq_ = 0;
ESP_LOGVV(TAG, "Waiting: %dms", this->note_duration_);
} else {
uint8_t note_index = (scale - MIN_OCTAVE) * SEMITONES_IN_OCTAVE + note_index_in_octave;
if (note_index >= NOTES_COUNT) {
ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note_index_in_octave, scale,
note_index, NOTES_COUNT);
this->finish_();
return;
}
auto freq = NOTES[note_index];
uint16_t freq = NOTES[note_index];
need_note_gap = freq == this->output_freq_;
// Add small silence gap between same note
this->output_freq_ = freq;
ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_);
} else {
ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_);
this->output_freq_ = 0;
ESP_LOGVV(TAG, "Playing note: %d for %dms", note_index_in_octave, this->note_duration_);
}
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) {
if (this->output_freq_ == 0) {
this->output_->set_level(0.0);
delay(DOUBLE_NOTE_GAP_MS);
this->note_duration_ -= DOUBLE_NOTE_GAP_MS;
}
if (this->output_freq_ != 0) {
} else {
if (need_note_gap && this->note_duration_ > REPEATING_NOTE_GAP_MS) {
this->output_->set_level(0.0);
delay(REPEATING_NOTE_GAP_MS);
this->note_duration_ -= REPEATING_NOTE_GAP_MS;
}
this->output_->update_frequency(this->output_freq_);
this->output_->set_level(this->gain_);
} else {
this->output_->set_level(0.0);
}
}
#endif // USE_OUTPUT
@@ -241,28 +248,26 @@ void Rtttl::loop() {
this->samples_sent_ = 0;
this->samples_gap_ = 0;
this->samples_per_wave_ = 0;
this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000;
this->samples_count_ = (SAMPLE_RATE * this->note_duration_) / 1000;
if (need_note_gap) {
this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000;
this->samples_gap_ = (SAMPLE_RATE * REPEATING_NOTE_GAP_MS) / 1000;
}
if (this->output_freq_ != 0) {
// make sure there is enough samples to add a full last sinus.
uint16_t samples_wish = this->samples_count_;
this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_;
// Make sure there is enough samples to add a full last sinus.
uint32_t samples_wish = this->samples_count_;
this->samples_per_wave_ = (SAMPLE_RATE << 10) / this->output_freq_;
uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1;
this->samples_count_ = (division * this->samples_per_wave_);
this->samples_count_ = this->samples_count_ >> 10;
ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_,
division, this->samples_per_wave_);
this->samples_count_ = (division * this->samples_per_wave_) >> 10;
ESP_LOGVV(TAG, "Calc play time: wish: %" PRIu32 " gets: %" PRIu32 " (div: %d spw: %" PRIu32 ")", samples_wish,
this->samples_count_, division, this->samples_per_wave_);
}
// Convert from frequency in Hz to high and low samples in fixed point
}
#endif // USE_SPEAKER
this->last_note_ = millis();
this->last_note_start_time_ = millis();
}
void Rtttl::play(std::string rtttl) {
@@ -275,25 +280,28 @@ void Rtttl::play(std::string rtttl) {
this->rtttl_ = std::move(rtttl);
this->default_duration_ = 4;
this->default_octave_ = 6;
this->default_note_denominator_ = DEFAULT_NOTE_DENOMINATOR;
this->default_octave_ = DEFAULT_OCTAVE;
this->note_duration_ = 0;
int bpm = 63;
uint16_t num;
uint16_t bpm = DEFAULT_BPM;
uint16_t num; // Used for: default note-denominator, default octave, BPM
// Get name
this->position_ = this->rtttl_.find(':');
// it's somewhat documented to be up to 10 characters but let's be a bit flexible here
if (this->position_ == std::string::npos || this->position_ > 15) {
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Unable to determine name; missing ':'");
return;
}
if (this->position_ >= SONG_NAME_LENGTH_LIMIT) {
ESP_LOGE(TAG, "Name is too long: length=%u, limit=%u", static_cast<unsigned>(this->position_),
static_cast<unsigned>(SONG_NAME_LENGTH_LIMIT));
return;
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
// get default duration
// Get default duration
this->position_ = this->rtttl_.find("d=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'd='");
@@ -301,11 +309,14 @@ void Rtttl::play(std::string rtttl) {
}
this->position_ += 2;
num = this->get_integer_();
if (num > 0) {
this->default_duration_ = num;
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
this->default_note_denominator_ = num;
} else {
ESP_LOGE(TAG, "Invalid default duration: %d", num);
return;
}
// get default octave
// Get default octave
this->position_ = this->rtttl_.find("o=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'o=");
@@ -313,11 +324,14 @@ void Rtttl::play(std::string rtttl) {
}
this->position_ += 2;
num = this->get_integer_();
if (num >= 3 && num <= 7) {
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
this->default_octave_ = num;
} else {
ESP_LOGE(TAG, "Invalid default octave: %d", num);
return;
}
// get BPM
// Get BPM
this->position_ = this->rtttl_.find("b=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing b=");
@@ -325,8 +339,11 @@ void Rtttl::play(std::string rtttl) {
}
this->position_ += 2;
num = this->get_integer_();
if (num != 0) {
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
bpm = num;
} else {
ESP_LOGE(TAG, "Invalid BPM: %d", num);
return;
}
this->position_ = this->rtttl_.find(':', this->position_);
@@ -337,10 +354,10 @@ void Rtttl::play(std::string rtttl) {
this->position_++;
// BPM usually expresses the number of quarter notes per minute
this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds)
this->output_freq_ = 0;
this->last_note_ = millis();
this->last_note_start_time_ = millis();
this->note_duration_ = 1;
#ifdef USE_OUTPUT
+16 -15
View File
@@ -13,6 +13,10 @@
namespace esphome::rtttl {
inline constexpr uint8_t DEFAULT_NOTE_DENOMINATOR = 4; // Default note-denominator (quarter note)
inline constexpr uint8_t DEFAULT_OCTAVE =
6; // Default octave for a note (see: `MIN_OCTAVE`, `MAX_OCTAVE` in `rtttl.cpp`)
enum class State : uint8_t {
STOPPED = 0,
INIT,
@@ -67,19 +71,18 @@ class Rtttl : public Component {
std::string rtttl_{""};
/// The current position in the RTTTL string.
size_t position_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_;
/// The default duration of a note (e.g. 4 for a quarter note).
uint16_t default_duration_;
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
/// The default octave for a note.
uint16_t default_octave_;
/// The time the last note was started.
uint32_t last_note_;
uint8_t default_octave_{DEFAULT_OCTAVE};
/// The duration of the current note in milliseconds.
uint16_t note_duration_;
uint16_t note_duration_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_duration_;
/// The time in milliseconds since microcontroller boot when the last note was started.
uint32_t last_note_start_time_;
/// The frequency of the current note in Hz.
uint32_t output_freq_;
uint32_t output_freq_{0};
/// The gain of the output.
float gain_{0.6f};
/// The current state of the RTTTL player.
@@ -93,16 +96,14 @@ class Rtttl : public Component {
#ifdef USE_SPEAKER
/// The speaker to write the sound to.
speaker::Speaker *speaker_{nullptr};
/// The sample rate of the speaker.
int sample_rate_{16000};
/// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format.
int samples_per_wave_{0};
uint32_t samples_per_wave_{0};
/// The number of samples sent.
int samples_sent_{0};
uint32_t samples_sent_{0};
/// The total number of samples to send.
int samples_count_{0};
uint32_t samples_count_{0};
/// The number of samples for the gap between notes.
int samples_gap_{0};
uint32_t samples_gap_{0};
#endif // USE_SPEAKER
/// The callback to call when playback is finished.

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