Merge remote-tracking branch 'upstream/dev' into pack-entity-strings

# Conflicts:
#	esphome/components/rp2040/core.cpp
This commit is contained in:
J. Nick Koston
2026-02-27 14:54:37 -10:00
364 changed files with 13145 additions and 7869 deletions

View File

@@ -1 +1 @@
5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6
b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052

View File

@@ -6,8 +6,9 @@
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change)
- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api)
- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other

View File

@@ -27,6 +27,7 @@ module.exports = {
'new-feature',
'breaking-change',
'developer-breaking-change',
'undocumented-api-change',
'code-quality',
'deprecated-component'
],

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;
}
@@ -238,6 +226,7 @@ async function detectPRTemplateCheckboxes(context) {
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];
@@ -258,7 +247,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
const { owner, repo } = context.repo;
// Compile regex once for better performance
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
const componentFileRegex = COMPONENT_REGEX;
// Get files that are modified or added in components directory
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));

66
.github/scripts/detect-tags.js vendored Normal file
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,
};

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: |

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

76
.github/workflows/pr-title-check.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
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;
// 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` +
`Example: [pn532] Add health checking and auto-reset`
);
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>`
);
}

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

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.2
rev: v0.15.3
hooks:
# Run the linter.
- id: ruff

View File

@@ -213,6 +213,7 @@ esphome/components/hbridge/light/* @DotNetDann
esphome/components/hbridge/switch/* @dwmw2
esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/hdc302x/* @joshuasing
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
@@ -428,6 +429,7 @@ esphome/components/select/* @esphome/core
esphome/components/sen0321/* @notjj
esphome/components/sen21231/* @shreyaskarnik
esphome/components/sen5x/* @martgras
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/sfa30/* @ghsensdev

View File

@@ -92,10 +92,7 @@ void AbsoluteHumidityComponent::loop() {
// Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k);
ESP_LOGD(TAG,
"Saturation vapor pressure %f kPa\n"
"Publishing absolute humidity %f g/m³",
es, absolute_humidity);
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
// Publish absolute humidity
this->status_clear_warning();

View File

@@ -199,12 +199,19 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
if (dimmer_timer == nullptr) {
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
if (dimmer_timer == nullptr) {
ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer");
this->mark_failed();
return;
}
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
}
#endif
}

View File

@@ -67,10 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG,
"[%s] No control service found at device, not an Anova..?\n"
"[%s] Note, this component does not currently support Anova Nano.",
this->get_name().c_str(), this->get_name().c_str());
ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
break;
}
this->char_handle_ = chr->handle;

View File

@@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
# API needs 1 listening socket + typically 3 concurrent client connections
# (not max_connections, which is the upper limit rarely reached)
sockets_needed = 1 + 3
socket.consume_sockets(sockets_needed, "api")(config)
socket.consume_sockets(3, "api")(config)
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
return config

View File

@@ -989,6 +989,7 @@ enum ClimateAction {
CLIMATE_ACTION_IDLE = 4;
CLIMATE_ACTION_DRYING = 5;
CLIMATE_ACTION_FAN = 6;
CLIMATE_ACTION_DEFROSTING = 7;
}
enum ClimatePreset {
CLIMATE_PRESET_NONE = 0;

View File

@@ -1346,9 +1346,8 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne
resp.target_temperature_low = wh->get_target_temperature_low();
resp.target_temperature_high = wh->get_target_temperature_high();
resp.state = wh->get_state();
resp.key = wh->get_object_id_hash();
return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size);
return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size);
}
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);

View File

@@ -2,7 +2,6 @@
// See script/api_protobuf/api_protobuf.py
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/string_ref.h"
#include "proto.h"
@@ -117,6 +116,7 @@ enum ClimateAction : uint32_t {
CLIMATE_ACTION_IDLE = 4,
CLIMATE_ACTION_DRYING = 5,
CLIMATE_ACTION_FAN = 6,
CLIMATE_ACTION_DEFROSTING = 7,
};
enum ClimatePreset : uint32_t {
CLIMATE_PRESET_NONE = 0,

View File

@@ -0,0 +1,12 @@
// This file was automatically generated with a tool.
// See script/api_protobuf/api_protobuf.py
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLUETOOTH_PROXY
#ifndef USE_API_VARINT64
#define USE_API_VARINT64
#endif
#endif
namespace esphome::api {} // namespace esphome::api

View File

@@ -321,6 +321,8 @@ template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::Climate
return "CLIMATE_ACTION_DRYING";
case enums::CLIMATE_ACTION_FAN:
return "CLIMATE_ACTION_FAN";
case enums::CLIMATE_ACTION_DEFROSTING:
return "CLIMATE_ACTION_DEFROSTING";
default:
return "UNKNOWN";
}

View File

@@ -433,8 +433,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once) {
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(StringRef)> &&f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
@@ -443,7 +443,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f, bool once) {
std::function<void(StringRef)> &&f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
@@ -463,29 +463,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
// Legacy helper: wraps std::string callback and delegates to StringRef version
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once) {
std::function<void(const std::string &)> &&f, bool once) {
// Wrap callback to convert StringRef -> std::string, then delegate
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
@@ -494,12 +494,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}

View File

@@ -201,20 +201,20 @@ class APIServer : public Component,
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
@@ -241,13 +241,13 @@ class APIServer : public Component,
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> &&f, bool once);
// Legacy helper: wraps std::string callback and delegates to StringRef version
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
std::function<void(const std::string &)> &&f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// No explicit close() needed — listen sockets have no active connections on
// failure/shutdown. Destructor handles fd cleanup (close or abort per platform).

View File

@@ -15,7 +15,7 @@ class APIConnection;
return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
}
class ListEntitiesIterator : public ComponentIterator {
class ListEntitiesIterator final : public ComponentIterator {
public:
ListEntitiesIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR

View File

@@ -7,6 +7,23 @@ namespace esphome::api {
static const char *const TAG = "api.proto";
#ifdef USE_API_VARINT64
optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed,
uint32_t result32) {
uint64_t result64 = result32;
uint32_t limit = std::min(len, uint32_t(10));
for (uint32_t i = 4; i < limit; i++) {
uint8_t val = buffer[i];
result64 |= uint64_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result64);
}
}
return {};
}
#endif
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
uint32_t count = 0;
const uint8_t *ptr = buffer;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "api_pb2_defines.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -110,59 +111,78 @@ class ProtoVarInt {
#endif
if (len == 0)
return {};
// Most common case: single-byte varint (values 0-127)
// Fast path: single-byte varints (0-127) are the most common case
// (booleans, small enums, field tags). Avoid loop overhead entirely.
if ((buffer[0] & 0x80) == 0) {
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
// General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
uint32_t max_len = std::min(len, uint32_t(10));
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < max_len; i++) {
// 32-bit phase: process remaining bytes with native 32-bit shifts.
// Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t
// shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values.
// With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles
// byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX).
uint32_t result32 = buffer[0] & 0x7F;
#ifdef USE_API_VARINT64
uint32_t limit = std::min(len, uint32_t(4));
#else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result);
return ProtoVarInt(result32);
}
}
return {}; // Incomplete or invalid varint
// 64-bit phase for remaining bytes (BLE addresses etc.)
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, consumed, result32);
#else
return {};
#endif
}
#ifdef USE_API_VARINT64
protected:
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
/// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path.
static optional<ProtoVarInt> parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32)
__attribute__((noinline));
public:
#endif
constexpr uint16_t as_uint16() const { return this->value_; }
constexpr uint32_t as_uint32() const { return this->value_; }
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr bool as_bool() const { return this->value_; }
constexpr int32_t as_int32() const {
// Not ZigZag encoded
return static_cast<int32_t>(this->as_int64());
}
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
return static_cast<int32_t>(this->value_);
}
constexpr int32_t as_sint32() const {
// with ZigZag encoding
return decode_zigzag32(static_cast<uint32_t>(this->value_));
}
#ifdef USE_API_VARINT64
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
}
constexpr int64_t as_sint64() const {
// with ZigZag encoding
return decode_zigzag64(this->value_);
}
#endif
protected:
#ifdef USE_API_VARINT64
uint64_t value_;
#else
uint32_t value_;
#endif
};
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32

View File

@@ -16,7 +16,7 @@ class APIConnection;
return this->client_->send_##entity_type##_state(entity); \
}
class InitialStateIterator : public ComponentIterator {
class InitialStateIterator final : public ComponentIterator {
public:
InitialStateIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR

View File

@@ -230,7 +230,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void set_data(std::function<void(Ts..., JsonObject)> func) {
void set_data(std::function<void(Ts..., JsonObject)> &&func) {
this->json_builder_ = std::move(func);
this->has_data_ = true;
}

View File

@@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() {
uint8_t tune_val = this->read_capacitance();
ESP_LOGI(TAG,
"Starting antenna tuning\n"
"Division Ratio is set to: %d\n"
"Internal Capacitor is set to: %d\n"
"Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
" Division Ratio is set to: %d\n"
" Internal Capacitor is set to: %d\n"
" Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
div_ratio, tune_val);
this->display_oscillator(true, ANTFREQ);
}

View File

@@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); }
bool AT581XComponent::i2c_write_config() {
ESP_LOGCONFIG(TAG,
"Writing new config for AT581X\n"
"Frequency: %dMHz\n"
"Sensing distance: %d\n"
"Power: %dµA\n"
"Gain: %d\n"
"Trigger base time: %dms\n"
"Trigger keep time: %dms\n"
"Protect time: %dms\n"
"Self check time: %dms",
" Frequency: %dMHz\n"
" Sensing distance: %d\n"
" Power: %dµA\n"
" Gain: %d\n"
" Trigger base time: %dms\n"
" Trigger keep time: %dms\n"
" Protect time: %dms\n"
" Self check time: %dms",
this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_,
this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_);

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")

View File

@@ -564,6 +564,7 @@ async def setup_binary_sensor_core_(var, config):
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))

View File

@@ -29,10 +29,8 @@ void MultiClickTrigger::on_state_(bool state) {
// Start matching
MultiClickTriggerEvent evt = this->timing_[0];
if (evt.state == state) {
ESP_LOGV(TAG,
"START min=%" PRIu32 " max=%" PRIu32 "\n"
"Multi Click: Starting multi click action!",
evt.min_length, evt.max_length);
ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length);
ESP_LOGV(TAG, "Multi Click: Starting multi click action!");
this->at_index_ = 1;
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });

View File

@@ -18,11 +18,15 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
}
void BinarySensor::publish_state(bool new_state) {
#ifdef USE_BINARY_SENSOR_FILTER
if (this->filter_list_ == nullptr) {
#endif
this->send_state_internal(new_state);
#ifdef USE_BINARY_SENSOR_FILTER
} else {
this->filter_list_->input(new_state);
}
#endif
}
void BinarySensor::publish_initial_state(bool new_state) {
this->invalidate_state();
@@ -47,6 +51,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) {
return false;
}
#ifdef USE_BINARY_SENSOR_FILTER
void BinarySensor::add_filter(Filter *filter) {
filter->parent_ = this;
if (this->filter_list_ == nullptr) {
@@ -63,6 +68,7 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
this->add_filter(filter);
}
}
#endif // USE_BINARY_SENSOR_FILTER
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace esphome::binary_sensor

View File

@@ -2,7 +2,9 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#ifdef USE_BINARY_SENSOR_FILTER
#include "esphome/components/binary_sensor/filter.h"
#endif
#include <initializer_list>
@@ -45,8 +47,10 @@ class BinarySensor : public StatefulEntityBase<bool> {
*/
void publish_initial_state(bool new_state);
#ifdef USE_BINARY_SENSOR_FILTER
void add_filter(Filter *filter);
void add_filters(std::initializer_list<Filter *> filters);
#endif
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@@ -60,7 +64,9 @@ class BinarySensor : public StatefulEntityBase<bool> {
bool state{};
protected:
#ifdef USE_BINARY_SENSOR_FILTER
Filter *filter_list_{nullptr};
#endif
bool set_new_state(const optional<bool> &new_state) override;
};

View File

@@ -1,3 +1,6 @@
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR_FILTER
#include "filter.h"
#include "binary_sensor.h"
@@ -142,3 +145,5 @@ optional<bool> SettleFilter::new_value(bool value) {
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace esphome::binary_sensor
#endif // USE_BINARY_SENSOR_FILTER

View File

@@ -1,5 +1,8 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR_FILTER
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
@@ -138,3 +141,5 @@ class SettleFilter : public Filter, public Component {
};
} // namespace esphome::binary_sensor
#endif // USE_BINARY_SENSOR_FILTER

View File

@@ -182,7 +182,10 @@ void BL0940::recalibrate_() {
ESP_LOGD(TAG,
"Recalibrated reference values:\n"
"Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n",
" Voltage: %f\n"
" Current: %f\n"
" Power: %f\n"
" Energy: %f",
this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_,
this->energy_reference_cal_);
}

View File

@@ -52,12 +52,12 @@ void BL0942::loop() {
return;
}
if (avail < sizeof(buffer)) {
if (!this->rx_start_) {
if (!this->rx_start_.has_value()) {
this->rx_start_ = millis();
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
} else if (millis() - *this->rx_start_ > PKT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
this->read_array((uint8_t *) &buffer, avail);
this->rx_start_ = 0;
this->rx_start_.reset();
}
return;
}
@@ -67,7 +67,7 @@ void BL0942::loop() {
this->received_package_(&buffer);
}
}
this->rx_start_ = 0;
this->rx_start_.reset();
}
bool BL0942::validate_checksum_(DataPacket *data) {

View File

@@ -140,7 +140,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
uint8_t address_ = 0;
bool reset_ = false;
LineFrequency line_freq_ = LINE_FREQUENCY_50HZ;
uint32_t rx_start_ = 0;
optional<uint32_t> rx_start_{};
uint32_t prev_cf_cnt_ = 0;
bool validate_checksum_(DataPacket *data);

View File

@@ -101,7 +101,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
}
void loop() override {
if (this->found_ && this->last_seen_ + this->timeout_ < millis())
if (this->found_ && millis() - this->last_seen_ > this->timeout_)
this->set_found_(false);
}
void dump_config() override;

View File

@@ -7,6 +7,7 @@
#include "esphome/core/preferences.h"
#include "esphome/core/defines.h"
#include <map>
#include <queue>
#ifdef USE_BSEC
#include <bsec.h>

View File

@@ -178,8 +178,11 @@ async def to_code_base(config):
bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs)))
# Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library
# The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and
# SPI.h in their source files, so these libraries must be available even though
# ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones.
if core.CORE.using_arduino:
cg.add_library("Wire", None)
cg.add_library("SPI", None)
cg.add_library(
"BME68x Sensor library",

View File

@@ -76,13 +76,15 @@ def _final_validate(config: ConfigType) -> ConfigType:
# Register socket needs for DNS server and additional HTTP connections
# - 1 UDP socket for DNS server
# - 3 additional TCP sockets for captive portal detection probes + configuration requests
# - 3 TCP sockets for captive portal detection probes + configuration requests
# OS captive portal detection makes multiple probe requests that stay in TIME_WAIT.
# Need headroom for actual user configuration requests.
# LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts.
# The listening socket is registered by web_server_base (shared HTTP server).
from esphome.components import socket
socket.consume_sockets(4, "captive_portal")(config)
socket.consume_sockets(3, "captive_portal")(config)
socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config)
return config

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_DATA,
CONF_FREQUENCY,
CONF_ID,
CONF_OUTPUT_POWER,
CONF_VALUE,
CONF_WAIT_TIME,
)
@@ -22,7 +23,6 @@ ns = cg.esphome_ns.namespace("cc1101")
CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice)
# Config keys
CONF_OUTPUT_POWER = "output_power"
CONF_RX_ATTENUATION = "rx_attenuation"
CONF_DC_BLOCKING_FILTER = "dc_blocking_filter"
CONF_IF_FREQUENCY = "if_frequency"

View File

@@ -242,6 +242,9 @@ void CC1101Component::begin_tx() {
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
}
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
// block TX entry when strobing from RX, and to ensure FS_AUTOCAL calibration
this->enter_idle_();
if (!this->enter_tx_()) {
ESP_LOGW(TAG, "Failed to enter TX state!");
}
@@ -252,6 +255,8 @@ void CC1101Component::begin_rx() {
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
}
// Transition through IDLE to ensure FS_AUTOCAL calibration occurs
this->enter_idle_();
if (!this->enter_rx_()) {
ESP_LOGW(TAG, "Failed to enter RX state!");
}

View File

@@ -124,9 +124,11 @@ bool CH422GComponent::write_outputs_() {
float CH422GComponent::get_setup_priority() const { return setup_priority::IO; }
#ifdef USE_LOOP_PRIORITY
// Run our loop() method very early in the loop, so that we cache read values
// before other components call our digital_read() method.
float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI
#endif
void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }

View File

@@ -23,7 +23,9 @@ class CH422GComponent : public Component, public i2c::I2CDevice {
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
#ifdef USE_LOOP_PRIORITY
float get_loop_priority() const override;
#endif
void dump_config() override;
protected:

View File

@@ -129,9 +129,11 @@ bool CH423Component::write_outputs_() {
float CH423Component::get_setup_priority() const { return setup_priority::IO; }
#ifdef USE_LOOP_PRIORITY
// Run our loop() method very early in the loop, so that we cache read values
// before other components call our digital_read() method.
float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI
#endif
void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }

View File

@@ -22,7 +22,9 @@ class CH423Component : public Component, public i2c::I2CDevice {
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
#ifdef USE_LOOP_PRIORITY
float get_loop_priority() const override;
#endif
void dump_config() override;
protected:

View File

@@ -10,8 +10,10 @@ const LogString *climate_mode_to_string(ClimateMode mode) {
return ClimateModeStrings::get_log_str(static_cast<uint8_t>(mode), ClimateModeStrings::LAST_INDEX);
}
// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN");
// Climate action strings indexed by ClimateAction enum (0,2-7): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN,
// DEFROSTING
PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN",
"DEFROSTING", "UNKNOWN");
const LogString *climate_action_to_string(ClimateAction action) {
return ClimateActionStrings::get_log_str(static_cast<uint8_t>(action), ClimateActionStrings::LAST_INDEX);

View File

@@ -41,6 +41,8 @@ enum ClimateAction : uint8_t {
CLIMATE_ACTION_DRYING = 5,
/// The climate device is in fan only mode
CLIMATE_ACTION_FAN = 6,
/// The climate device is defrosting
CLIMATE_ACTION_DEFROSTING = 7,
};
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value

View File

@@ -3,8 +3,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace cse7766 {
namespace esphome::cse7766 {
static const char *const TAG = "cse7766";
@@ -258,5 +257,4 @@ void CSE7766Component::dump_config() {
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
}
} // namespace cse7766
} // namespace esphome
} // namespace esphome::cse7766

View File

@@ -5,8 +5,7 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace cse7766 {
namespace esphome::cse7766 {
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
@@ -49,5 +48,4 @@ class CSE7766Component : public Component, public uart::UARTDevice {
uint16_t cf_pulses_last_{0};
};
} // namespace cse7766
} // namespace esphome
} // namespace esphome::cse7766

View File

@@ -148,14 +148,14 @@ void CurrentBasedCover::dump_config() {
}
ESP_LOGCONFIG(TAG,
" Close Duration: %.1fs\n"
"Obstacle Rollback: %.1f%%",
" Obstacle Rollback: %.1f%%",
this->close_duration_ / 1e3f, this->obstacle_rollback_ * 100);
if (this->max_duration_ != UINT32_MAX) {
ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f);
ESP_LOGCONFIG(TAG, " Maximum duration: %.1fs", this->max_duration_ / 1e3f);
}
ESP_LOGCONFIG(TAG,
"Start sensing delay: %.1fs\n"
"Malfunction detection: %s",
" Start sensing delay: %.1fs\n"
" Malfunction detection: %s",
this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_));
}

View File

@@ -79,7 +79,6 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
} else {
snprintf(buf, size, "unknown source");
}
ESP_LOGD(TAG, "Reset Reason: %s", buf);
return buf;
}
@@ -107,7 +106,6 @@ const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFE
} else {
wake_reason = "unknown source";
}
ESP_LOGD(TAG, "Wakeup Reason: %s", wake_reason);
// Return the static string directly - no need to copy to buffer
return wake_reason;
}
@@ -172,7 +170,6 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
#endif
@@ -194,39 +191,46 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
if (info.features != 0) {
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
}
ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision);
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
// Framework detection
#ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
#elif defined(USE_ESP32)
ESP_LOGD(TAG, "Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
#else
ESP_LOGW(TAG, "Framework: UNKNOWN");
pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN");
#endif
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
uint8_t mac[6];
get_mac_address_raw(mac);
ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
uint8_t mac[6];
get_mac_address_raw(mac);
ESP_LOGD(TAG,
"ESP32 debug info:\n"
" Chip: %s\n"
" Cores: %u\n"
" Revision: %u\n"
" CPU Frequency: %" PRIu32 " MHz\n"
" ESP-IDF Version: %s\n"
" EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X\n"
" Reset Reason: %s\n"
" Wakeup Cause: %s",
model, info.cores, info.revision, cpu_freq_mhz, esp_get_idf_version(), mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5], reset_reason, wakeup_cause);
#if defined(USE_ARDUINO)
ESP_LOGD(TAG, " Flash: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
#endif
// Framework detection
#ifdef USE_ARDUINO
ESP_LOGD(TAG, " Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
#else
ESP_LOGD(TAG, " Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
#endif
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos;

View File

@@ -128,14 +128,16 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
// NOLINTEND(readability-static-accessed-through-instance)
ESP_LOGD(TAG,
"Chip ID: 0x%08" PRIX32 "\n"
"SDK Version: %s\n"
"Core Version: %s\n"
"Boot Version=%u Mode=%u\n"
"CPU Frequency: %u\n"
"Flash Chip ID=0x%08" PRIX32 "\n"
"Reset Reason: %s\n"
"Reset Info: %s",
"ESP8266 debug info:\n"
" Chip ID: 0x%08" PRIX32 "\n"
" SDK Version: %s\n"
" Core Version: %s\n"
" Boot Version: %u\n"
" Boot Mode: %u\n"
" CPU Frequency: %u\n"
" Flash Chip ID: 0x%08" PRIX32 "\n"
" Reset Reason: %s\n"
" Reset Info: %s",
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));

View File

@@ -27,12 +27,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t mac_id = lt_cpu_get_mac_id();
ESP_LOGD(TAG,
"LibreTiny Version: %s\n"
"Chip: %s (%04x) @ %u MHz\n"
"Chip ID: 0x%06" PRIX32 "\n"
"Board: %s\n"
"Flash: %" PRIu32 " KiB / RAM: %" PRIu32 " KiB\n"
"Reset Reason: %s",
"LibreTiny debug info:\n"
" Version: %s\n"
" Chip: %s (%04x) @ %u MHz\n"
" Chip ID: 0x%06" PRIX32 "\n"
" Board: %s\n"
" Flash: %" PRIu32 " KiB\n"
" RAM: %" PRIu32 " KiB\n"
" Reset Reason: %s",
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason);

View File

@@ -79,13 +79,13 @@ static void fa_cb(const struct flash_area *fa, void *user_data) {
void DebugComponent::log_partition_info_() {
#if CONFIG_FLASH_MAP_LABELS
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Label | Offset | Size\n"
"--------------------------------------------"
"| Label | Offset | Size");
ESP_LOGCONFIG(TAG, "--------------------------------------------"
"-----------------------------------------------");
#else
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Offset | Size\n"
"-----------------------------------------"
"| Offset | Size");
ESP_LOGCONFIG(TAG, "-----------------------------------------"
"------------------------------");
#endif
flash_area_foreach(fa_cb, nullptr);
@@ -284,11 +284,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
get_mac_address_pretty_into_buffer(mac_pretty);
ESP_LOGD(TAG,
"Code page size: %u, code size: %u, device id: 0x%08x%08x\n"
"Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n"
"Device address type: %s, address: %s\n"
"Part code: nRF%x, version: %c%c%c%c, package: %s\n"
"RAM: %ukB, Flash: %ukB, production test: %sdone",
"nRF debug info:\n"
" Code page size: %u, code size: %u, device id: 0x%08x%08x\n"
" Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n"
" Device address type: %s, address: %s\n"
" Part code: nRF%x, version: %c%c%c%c, package: %s\n"
" RAM: %ukB, Flash: %ukB, production test: %sdone",
NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0],
NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2],
NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART,
@@ -299,23 +300,22 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
(NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected
<< UICR_PSELRESET_CONNECT_Pos;
ESP_LOGD(
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
TAG, " GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
YESNO(n_reset_enabled));
if (n_reset_enabled) {
uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos;
uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos;
ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin);
ESP_LOGD(TAG, " nRESET port P%u.%02u", port, pin);
}
#ifdef USE_BOOTLOADER_MCUBOOT
ESP_LOGD(TAG, "bootloader: mcuboot");
ESP_LOGD(TAG, " Bootloader: mcuboot");
#else
ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF,
ESP_LOGD(TAG, " Bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF,
(BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF);
ESP_LOGD(TAG,
"MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n"
"MBR param page addr 0x%08x, UICR param page addr 0x%08x",
read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR),
ESP_LOGD(TAG, " MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR),
NRF_UICR->NRFFW[0]);
ESP_LOGD(TAG, " MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
NRF_UICR->NRFFW[1]);
if (is_sd_present()) {
uint32_t const sd_id = sd_id_get();
@@ -326,7 +326,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
ver[1] = (sd_version - ver[0] * 1000000) / 1000;
ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000);
ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]);
ESP_LOGD(TAG, " SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]);
#ifdef USE_SOFTDEVICE_ID
#ifdef USE_SOFTDEVICE_VERSION
if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) {
@@ -352,10 +352,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
}
return res;
};
ESP_LOGD(TAG,
"NRFFW %s\n"
"NRFHW %s",
uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str());
ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str());
ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str());
return pos;
}

View File

@@ -40,9 +40,11 @@ void DeepSleepComponent::loop() {
this->begin_sleep();
}
#ifdef USE_LOOP_PRIORITY
float DeepSleepComponent::get_loop_priority() const {
return -100.0f; // run after everything else is ready
}
#endif
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }

View File

@@ -113,7 +113,9 @@ class DeepSleepComponent : public Component {
void setup() override;
void dump_config() override;
void loop() override;
#ifdef USE_LOOP_PRIORITY
float get_loop_priority() const override;
#endif
float get_setup_priority() const override;
/// Helper to enter deep sleep mode

View File

@@ -187,18 +187,18 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) {
} else if (message == "Done") {
ESP_LOGI(TAG,
"Updated detection area config:\n"
"Detection area 1 from %.02fm to %.02fm.",
" Detection area 1 from %.02fm to %.02fm.",
this->min1_, this->max1_);
if (this->min2_ >= 0 && this->max2_ >= 0) {
ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_);
ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_);
}
if (this->min3_ >= 0 && this->max3_ >= 0) {
ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_);
ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_);
}
if (this->min4_ >= 0 && this->max4_ >= 0) {
ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_);
ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_);
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet.
@@ -222,10 +222,10 @@ uint8_t SetLatencyCommand::on_message(std::string &message) {
} else if (message == "Done") {
ESP_LOGI(TAG,
"Updated output latency config:\n"
"Signal that someone was detected is delayed by %.03f s.\n"
"Signal that nobody is detected anymore is delayed by %.03f s.",
" Signal that someone was detected is delayed by %.03f s.\n"
" Signal that nobody is detected anymore is delayed by %.03f s.",
this->delay_after_detection_, this->delay_after_disappear_);
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet

View File

@@ -1,7 +1,5 @@
#include "dlms_meter.h"
#include <cmath>
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
#include <bearssl/bearssl.h>
#elif defined(USE_ESP32)
@@ -410,7 +408,7 @@ void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_lengt
if (current_position + 1 < message_length) {
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
if (scaler != 0) {
value *= powf(10.0f, scaler);
value *= pow10_int(scaler);
}
}

View File

@@ -153,10 +153,7 @@ void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::Remote
bool EmmetiClimate::check_checksum_(uint8_t checksum) {
uint8_t expected = this->gen_checksum_();
ESP_LOGV(TAG,
"Expected checksum: %X\n"
"Checksum received: %X",
expected, checksum);
ESP_LOGV(TAG, "Expected checksum: %X, Checksum received: %X", expected, checksum);
return checksum == expected;
}
@@ -266,10 +263,7 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) {
}
}
ESP_LOGD(TAG,
"Swing: %d\n"
"Sleep: %d",
(curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01);
ESP_LOGD(TAG, "Swing: %d, Sleep: %d", (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01);
for (size_t pos = 0; pos < 4; pos++) {
if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) {
@@ -295,13 +289,8 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) {
}
}
ESP_LOGD(TAG,
"Turbo: %d\n"
"Light: %d\n"
"Tree: %d\n"
"Blow: %d",
(curr_state.bitmap >> 3) & 0x01, (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01,
(curr_state.bitmap >> 6) & 0x01);
ESP_LOGD(TAG, "Turbo: %d, Light: %d, Tree: %d, Blow: %d", (curr_state.bitmap >> 3) & 0x01,
(curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, (curr_state.bitmap >> 6) & 0x01);
uint16_t control_data = 0;
for (size_t pos = 0; pos < 11; pos++) {

View File

@@ -152,12 +152,13 @@ void ENS160Component::update() {
// verbose status logging
ESP_LOGV(TAG,
"Status: ENS160 STATAS bit 0x%x\n"
"Status: ENS160 STATER bit 0x%x\n"
"Status: ENS160 VALIDITY FLAG 0x%02x\n"
"Status: ENS160 NEWDAT bit 0x%x\n"
"Status: ENS160 NEWGPR bit 0x%x",
(ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS,
"ENS160 Status Register: 0x%02x\n"
" STATAS bit 0x%x\n"
" STATER bit 0x%x\n"
" VALIDITY FLAG 0x%02x\n"
" NEWDAT bit 0x%x\n"
" NEWGPR bit 0x%x",
status_value, (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS,
(ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER,
(ENS160_DATA_STATUS_VALIDITY & status_value) >> 2,
(ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT,

View File

@@ -209,9 +209,10 @@ bool ES8388::set_dac_output(DacOutputLine line) {
};
ESP_LOGV(TAG,
"Setting ES8388_DACPOWER to 0x%02X\n"
"Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X\n"
"Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X",
"DAC output config:\n"
" DACPOWER: 0x%02X\n"
" DACCONTROL24/25: 0x%02X\n"
" DACCONTROL26/27: 0x%02X",
dac_power, reg_out1, reg_out2);
ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL24, reg_out1)); // LOUT1VOL

View File

@@ -14,6 +14,7 @@ from esphome.const import (
CONF_BOARD,
CONF_COMPONENTS,
CONF_DISABLED,
CONF_ENABLE_OTA_ROLLBACK,
CONF_ESPHOME,
CONF_FRAMEWORK,
CONF_IGNORE_EFUSE_CUSTOM_MAC,
@@ -90,7 +91,6 @@ CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_ENGINEERING_SAMPLE = "engineering_sample"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_RELEASE = "release"
@@ -790,8 +790,15 @@ def _detect_variant(value):
engineering_sample = value.get(CONF_ENGINEERING_SAMPLE)
if engineering_sample is None:
_LOGGER.warning(
"No board specified for ESP32-P4. Defaulting to production silicon (rev3). "
"If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'."
"No board specified for ESP32-P4. Defaulting to production silicon (rev3).\n"
"If you have an early engineering sample (pre-rev3), add this to your config:\n"
"\n"
" esp32:\n"
" engineering_sample: true\n"
"\n"
"To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n"
"Engineering samples will show a revision below v3.0.\n"
"The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)."
)
elif engineering_sample:
value[CONF_BOARD] = "esp32-p4-evboard"
@@ -883,10 +890,10 @@ def final_validate(config):
)
)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
if config[CONF_VARIANT] != VARIANT_ESP32S3:
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
errs.append(
cv.Invalid(
f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant",
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
)
)
@@ -1258,21 +1265,15 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
This function runs in to_code() after all components have registered their socket needs.
User-provided sdkconfig_options take precedence.
"""
from esphome.components.socket import KEY_SOCKET_CONSUMERS
from esphome.components.socket import get_socket_counts
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS")
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
total_sockets = sum(socket_consumers.values())
# Early return if no sockets registered and no user override
if total_sockets == 0 and user_max_sockets is None:
return
components_list = ", ".join(
f"{name}={count}" for name, count in sorted(socket_consumers.items())
)
# CONFIG_LWIP_MAX_SOCKETS is a single VFS socket pool shared by all socket
# types (TCP clients, TCP listeners, and UDP). Include all three counts.
sc = get_socket_counts()
total_sockets = sc.tcp + sc.udp + sc.tcp_listen
# User specified their own value - respect it but warn if insufficient
if user_max_sockets is not None:
@@ -1281,22 +1282,23 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
user_max_sockets,
)
# Warn if user's value is less than what components need
if total_sockets > 0:
user_sockets_int = 0
with contextlib.suppress(ValueError, TypeError):
user_sockets_int = int(user_max_sockets)
user_sockets_int = 0
with contextlib.suppress(ValueError, TypeError):
user_sockets_int = int(user_max_sockets)
if user_sockets_int < total_sockets:
_LOGGER.warning(
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
"needs %d sockets (registered: %s). You may experience socket "
"exhaustion errors. Consider increasing to at least %d.",
user_sockets_int,
total_sockets,
components_list,
total_sockets,
)
if user_sockets_int < total_sockets:
_LOGGER.warning(
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
"needs %d sockets (%d TCP + %d UDP + %d TCP_LISTEN). You may "
"experience socket exhaustion errors. Consider increasing to "
"at least %d.",
user_sockets_int,
total_sockets,
sc.tcp,
sc.udp,
sc.tcp_listen,
total_sockets,
)
# User's value already added via sdkconfig_options processing
return
@@ -1305,11 +1307,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
sock_min = " (min)" if max_sockets > total_sockets else ""
_LOGGER.log(
log_level,
"Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
"Setting CONFIG_LWIP_MAX_SOCKETS to %d%s "
"(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])",
max_sockets,
components_list,
sock_min,
sc.tcp,
sc.tcp_details,
sc.udp,
sc.udp_details,
sc.tcp_listen,
sc.tcp_listen_details,
)
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
@@ -1617,8 +1627,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

View File

@@ -23,6 +23,7 @@ namespace esphome {
void HOT yield() { vPortYield(); }
uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); }
uint64_t HOT millis_64() { return static_cast<uint64_t>(esp_timer_get_time()) / 1000ULL; }
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); }

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))

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");
}

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)

View File

@@ -423,10 +423,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
for (auto &svc : this->services_) {
char uuid_buf[espbt::UUID_STR_LEN];
svc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG,
"[%d] [%s] Service UUID: %s\n"
"[%d] [%s] start_handle: 0x%x end_handle: 0x%x",
this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_,
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
svc->start_handle, svc->end_handle);
}
#endif

View File

@@ -24,6 +24,7 @@ from esphome.const import (
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.schema_extractors import SCHEMA_EXTRACT
AUTO_LOAD = ["esp32_ble", "bytebuffer"]
@@ -42,6 +43,7 @@ CONF_FIRMWARE_VERSION = "firmware_version"
CONF_INDICATE = "indicate"
CONF_MANUFACTURER = "manufacturer"
CONF_MANUFACTURER_DATA = "manufacturer_data"
CONF_MAX_CLIENTS = "max_clients"
CONF_ON_WRITE = "on_write"
CONF_READ = "read"
CONF_STRING = "string"
@@ -287,6 +289,22 @@ def create_device_information_service(config):
def final_validate_config(config):
# Validate max_clients does not exceed esp32_ble max_connections
max_clients = config[CONF_MAX_CLIENTS]
if max_clients > 1:
full_config = fv.full_config.get()
ble_config = full_config.get("esp32_ble", {})
max_connections = ble_config.get(
"max_connections", esp32_ble.DEFAULT_MAX_CONNECTIONS
)
if max_clients > max_connections:
raise cv.Invalid(
f"'max_clients' ({max_clients}) cannot exceed esp32_ble "
f"'max_connections' ({max_connections}). "
f"Please set 'max_connections: {max_clients}' in the "
f"'esp32_ble' component."
)
# Check if all characteristics that require notifications have the notify property set
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()):
# Look for the characteristic in the configuration
@@ -428,6 +446,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_MODEL): value_schema("string", templatable=False),
cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False),
cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]),
cv.Optional(CONF_MAX_CLIENTS, default=1): cv.int_range(min=1, max=9),
cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA),
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
@@ -552,6 +571,7 @@ async def to_code(config):
esp32_ble.register_ble_status_event_handler(parent, var)
cg.add(var.set_parent(parent))
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
for service_config in config[CONF_SERVICES]:

View File

@@ -175,6 +175,10 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id);
// Resume advertising so additional clients can discover and connect
if (this->client_count_ < this->max_clients_) {
this->parent_->advertising_start();
}
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
break;
}
@@ -241,7 +245,12 @@ void BLEServer::ble_before_disabled_event_handler() {
float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; }
void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); }
void BLEServer::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 BLE Server:\n"
" Max clients: %u",
this->max_clients_);
}
BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -39,6 +39,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
this->restart_advertising_();
}
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
uint8_t get_max_clients() const { return this->max_clients_; }
BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15);
void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0);
BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0);
@@ -95,6 +98,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
uint8_t client_count_{0};
uint8_t max_clients_{1};
std::vector<ServiceEntry> services_{};
std::vector<BLEService *> services_to_start_{};
BLEService *device_information_service_{};

View File

@@ -20,8 +20,10 @@ def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
from esphome.components import socket
# Each camera web server instance needs 1 listening socket + 2 client connections
sockets_needed = 3
socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
socket.consume_sockets(2, "esp32_camera_web_server")(config)
socket.consume_sockets(1, "esp32_camera_web_server", socket.SocketType.TCP_LISTEN)(
config
)
return config

View File

@@ -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}"

View File

@@ -106,11 +106,12 @@ void Esp32HostedUpdate::setup() {
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) {
ESP_LOGD(TAG,
"Firmware version: %s\n"
"Project name: %s\n"
"Build date: %s\n"
"Build time: %s\n"
"IDF version: %s",
"ESP32 Hosted firmware:\n"
" Firmware version: %s\n"
" Project name: %s\n"
" Build date: %s\n"
" Build time: %s\n"
" IDF version: %s",
app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver);
this->update_info_.latest_version = app_desc->version;
if (this->update_info_.latest_version != this->update_info_.current_version) {

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:

View File

@@ -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

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

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"),
)

View File

@@ -3,6 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.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 App.scheduler.millis_64_impl_(::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); }

View File

@@ -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)

View File

@@ -13,22 +13,63 @@ esp_ldo_ns = cg.esphome_ns.namespace("esp_ldo")
EspLdo = esp_ldo_ns.class_("EspLdo", cg.Component)
AdjustAction = esp_ldo_ns.class_("AdjustAction", Action)
CHANNELS = (3, 4)
CHANNELS = (1, 2, 3, 4)
CHANNELS_INTERNAL = (1, 2)
CONF_ADJUSTABLE = "adjustable"
CONF_ALLOW_INTERNAL_CHANNEL = "allow_internal_channel"
CONF_PASSTHROUGH = "passthrough"
adjusted_ids = set()
def validate_ldo_voltage(value):
if isinstance(value, str) and value.lower() == CONF_PASSTHROUGH:
return CONF_PASSTHROUGH
value = cv.voltage(value)
if 0.5 <= value <= 2.7:
return value
raise cv.Invalid(
f"LDO voltage must be in range 0.5V-2.7V or 'passthrough' (bypass mode), got {value}V"
)
def validate_ldo_config(config):
channel = config[CONF_CHANNEL]
allow_internal = config[CONF_ALLOW_INTERNAL_CHANNEL]
if allow_internal and channel not in CHANNELS_INTERNAL:
raise cv.Invalid(
f"'{CONF_ALLOW_INTERNAL_CHANNEL}' is only valid for internal channels (1, 2). "
f"Channel {channel} is a user-configurable channel — its usage depends on your board schematic.",
path=[CONF_ALLOW_INTERNAL_CHANNEL],
)
if channel in CHANNELS_INTERNAL and not allow_internal:
raise cv.Invalid(
f"LDO channel {channel} is normally used internally by the chip (flash/PSRAM). "
f"Set '{CONF_ALLOW_INTERNAL_CHANNEL}: true' to confirm you know what you are doing.",
path=[CONF_CHANNEL],
)
if config[CONF_VOLTAGE] == CONF_PASSTHROUGH and config[CONF_ADJUSTABLE]:
raise cv.Invalid(
"Passthrough mode passes the supply voltage directly to the output and does not support "
"runtime voltage adjustment.",
path=[CONF_ADJUSTABLE],
)
return config
CONFIG_SCHEMA = cv.All(
cv.ensure_list(
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EspLdo),
cv.Required(CONF_VOLTAGE): cv.All(
cv.voltage, cv.float_range(min=0.5, max=2.7)
),
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
}
cv.All(
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EspLdo),
cv.Required(CONF_VOLTAGE): validate_ldo_voltage,
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
cv.Optional(CONF_ALLOW_INTERNAL_CHANNEL, default=False): cv.boolean,
}
),
validate_ldo_config,
)
),
cv.only_on_esp32,
@@ -40,7 +81,11 @@ async def to_code(configs):
for config in configs:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_CHANNEL])
await cg.register_component(var, config)
cg.add(var.set_voltage(config[CONF_VOLTAGE]))
voltage = config[CONF_VOLTAGE]
if voltage == CONF_PASSTHROUGH:
cg.add(var.set_voltage(3300))
else:
cg.add(var.set_voltage(int(round(voltage * 1000))))
cg.add(var.set_adjustable(config[CONF_ADJUSTABLE]))

View File

@@ -10,32 +10,34 @@ static const char *const TAG = "esp_ldo";
void EspLdo::setup() {
esp_ldo_channel_config_t config{};
config.chan_id = this->channel_;
config.voltage_mv = (int) (this->voltage_ * 1000.0f);
config.voltage_mv = this->voltage_mv_;
config.flags.adjustable = this->adjustable_;
auto err = esp_ldo_acquire_channel(&config, &this->handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_);
this->mark_failed(LOG_STR("Failed to acquire LDO channel"));
} else {
ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_);
ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_);
}
}
void EspLdo::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP LDO Channel %d:\n"
" Voltage: %fV\n"
" Voltage: %dmV\n"
" Adjustable: %s",
this->channel_, this->voltage_, YESNO(this->adjustable_));
this->channel_, this->voltage_mv_, YESNO(this->adjustable_));
}
void EspLdo::adjust_voltage(float voltage) {
if (!std::isfinite(voltage) || voltage < 0.5f || voltage > 2.7f) {
ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d", voltage, this->channel_);
ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d (must be 0.5V-2.7V)", voltage, this->channel_);
return;
}
auto erro = esp_ldo_channel_adjust_voltage(this->handle_, (int) (voltage * 1000.0f));
if (erro != ESP_OK) {
ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %fV: %s", this->channel_, voltage, esp_err_to_name(erro));
int voltage_mv = (int) roundf(voltage * 1000.0f);
auto err = esp_ldo_channel_adjust_voltage(this->handle_, voltage_mv);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %dmV: %s", this->channel_, voltage_mv,
esp_err_to_name(err));
}
}

View File

@@ -15,7 +15,7 @@ class EspLdo : public Component {
void dump_config() override;
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
void set_voltage(float voltage) { this->voltage_ = voltage; }
void set_voltage(int voltage_mv) { this->voltage_mv_ = voltage_mv; }
void adjust_voltage(float voltage);
float get_setup_priority() const override {
return setup_priority::BUS; // LDO setup should be done early
@@ -23,7 +23,7 @@ class EspLdo : public Component {
protected:
int channel_;
float voltage_{2.7};
int voltage_mv_{2700};
bool adjustable_{false};
esp_ldo_channel_handle_t handle_{};
};

View File

@@ -97,8 +97,9 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for OTA component."""
from esphome.components import socket
# OTA needs 1 listening socket (client connections are temporary during updates)
socket.consume_sockets(1, "ota")(config)
# OTA needs 1 listening socket. The active transfer connection during an update
# uses a TCP PCB from the general pool, covered by MIN_TCP_SOCKETS headroom.
socket.consume_sockets(1, "ota", socket.SocketType.TCP_LISTEN)(config)
return config

View File

@@ -12,7 +12,7 @@
namespace esphome {
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
class ESPHomeOTAComponent : public ota::OTAComponent {
class ESPHomeOTAComponent final : public ota::OTAComponent {
public:
enum class OTAState : uint8_t {
IDLE,

View File

@@ -21,11 +21,9 @@ void ESPNowTransport::setup() {
return;
}
ESP_LOGI(TAG,
"Registering ESP-NOW handlers\n"
"Peer address: %02X:%02X:%02X:%02X:%02X:%02X",
this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3],
this->peer_address_[4], this->peer_address_[5]);
ESP_LOGI(TAG, "Registering ESP-NOW handlers, peer: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0],
this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], this->peer_address_[4],
this->peer_address_[5]);
// Register received handler
this->parent_->register_received_handler(this);

View File

@@ -19,8 +19,7 @@
#include <driver/spi_master.h>
#endif
namespace esphome {
namespace ethernet {
namespace esphome::ethernet {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637
@@ -866,10 +865,7 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
}
#endif
ESP_LOGD(TAG,
"Writing to PHY Register Address: 0x%02" PRIX32 "\n"
"Writing to PHY Register Value: 0x%04" PRIX32,
register_data.address, register_data.value);
ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value);
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
@@ -884,7 +880,6 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
#endif
} // namespace ethernet
} // namespace esphome
} // namespace esphome::ethernet
#endif // USE_ESP32

View File

@@ -15,8 +15,7 @@
#include "esp_mac.h"
#include "esp_idf_version.h"
namespace esphome {
namespace ethernet {
namespace esphome::ethernet {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
/** Listener interface for Ethernet IP state changes.
@@ -218,7 +217,6 @@ extern EthernetComponent *global_eth_component;
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
#endif
} // namespace ethernet
} // namespace esphome
} // namespace esphome::ethernet
#endif // USE_ESP32

View File

@@ -150,9 +150,9 @@ void EzoPMP::read_command_result_() {
if (current_char == '\0') {
ESP_LOGV(TAG,
"Read Response from device: %s\n"
"First Component: %s\n"
"Second Component: %s\n"
"Third Component: %s",
" First Component: %s\n"
" Second Component: %s\n"
" Third Component: %s",
(char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer,
(char *) third_parameter_buffer);

View File

@@ -97,10 +97,10 @@ void GCJA5Component::parse_data_() {
ESP_LOGI(TAG,
"GCJA5 Status\n"
"Overall Status : %i\n"
"PD Status : %i\n"
"LD Status : %i\n"
"Fan Status : %i",
" Overall Status : %i\n"
" PD Status : %i\n"
" LD Status : %i\n"
" Fan Status : %i",
(status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03);
}
}

View File

@@ -6,12 +6,12 @@
namespace esphome {
namespace gp8403 {
enum GP8403Voltage {
enum GP8403Voltage : uint8_t {
GP8403_VOLTAGE_5V = 0x00,
GP8403_VOLTAGE_10V = 0x11,
};
enum GP8403Model {
enum GP8403Model : uint8_t {
GP8403,
GP8413,
};

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

View File

@@ -38,13 +38,13 @@ void GraphicalDisplayMenu::setup() {
void GraphicalDisplayMenu::dump_config() {
ESP_LOGCONFIG(TAG,
"Graphical Display Menu\n"
"Has Display: %s\n"
"Popup Mode: %s\n"
"Advanced Drawing Mode: %s\n"
"Has Font: %s\n"
"Mode: %s\n"
"Active: %s\n"
"Menu items:",
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_));

View File

@@ -1,6 +1,7 @@
#pragma once
#include <chrono>
#include <queue>
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif

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