[esp32_touch] Migrate to new unified touch sensor driver (esp_driver_touch_sens) (#14033)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Jonathan Swoboda
2026-02-27 01:38:36 -05:00
committed by GitHub
parent 656389f215
commit 4044520ccc
8 changed files with 800 additions and 1128 deletions

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

@@ -4,7 +4,6 @@ esp32_touch:
measurement_duration: 8ms
low_voltage_reference: 0.5V
high_voltage_reference: 2.7V
voltage_attenuation: 1.5V
binary_sensor:
- platform: esp32_touch

View File

@@ -0,0 +1,5 @@
substitutions:
pin: GPIO5
<<: !include common-variants.yaml
<<: !include common-get-value.yaml