diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index a02370a343..10ad339b12 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -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: diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp new file mode 100644 index 0000000000..e7124ce92f --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -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 + +namespace esphome::esp32_touch { + +template 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(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(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(smooth_value) - static_cast(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 diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 7f45f2ccb4..d51b2d4922 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -4,49 +4,49 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include #include -#include +#include #include #include -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 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 diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp deleted file mode 100644 index 429b5173be..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#ifdef USE_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/log.h" -#include - -#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(value) - static_cast(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 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp deleted file mode 100644 index ffb805e008..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ /dev/null @@ -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 -#include - -// 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(arg); - - uint32_t mask = 0; - touch_ll_read_trigger_status_mask(&mask); - touch_ll_clear_trigger_status_mask(); - touch_pad_clear_status(); - - // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured - // touch pad detects a touch (value goes below threshold). The hardware does NOT - // generate interrupts on release - only on touch events. - // The interrupt will continue to fire periodically (based on sleep_cycle) as long - // as any pad remains touched. This allows us to detect both new touches and - // continued touches, but releases must be detected by timeout in the main loop. - - // Process all configured pads to check their current state - // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, - // so we must scan all configured pads to find which ones were touched - for (auto *child : component->children_) { - touch_pad_t pad = child->get_touch_pad(); - - // Read current value using ISR-safe API - // IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe - // In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes: - // "assert failed: xQueueSemaphoreTake queue.c:1718" - // We must use raw values even when filter is enabled as a workaround. - // Users should adjust thresholds to compensate for the lack of IIR filtering. - // See: https://github.com/espressif/esp-idf/issues/17045 - uint32_t value = touch_ll_read_raw_data(pad); - - // Skip pads that aren’t in the trigger mask - if (((mask >> pad) & 1) == 0) { - continue; - } - - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - bool is_touched = value < child->get_threshold(); - - // Always send the current state - the main loop will filter for changes - // We send both touched and untouched states because the ISR doesn't - // track previous state (to keep ISR fast and simple) - TouchPadEventV1 event; - event.pad = pad; - event.value = value; - event.is_touched = is_touched; - - // Send to queue from ISR - non-blocking, drops if queue full - BaseType_t x_higher_priority_task_woken = pdFALSE; - xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); - component->enable_loop_soon_any_context(); - if (x_higher_priority_task_woken) { - portYIELD_FROM_ISR(); - } - } -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp deleted file mode 100644 index b34ca1abd3..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ /dev/null @@ -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_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_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(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 diff --git a/tests/components/esp32_touch/common-variants.yaml b/tests/components/esp32_touch/common-variants.yaml index 69a3dbd969..5d6d0bbd19 100644 --- a/tests/components/esp32_touch/common-variants.yaml +++ b/tests/components/esp32_touch/common-variants.yaml @@ -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 diff --git a/tests/components/esp32_touch/test.esp32-p4-idf.yaml b/tests/components/esp32_touch/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..ab5484af44 --- /dev/null +++ b/tests/components/esp32_touch/test.esp32-p4-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + pin: GPIO5 + +<<: !include common-variants.yaml +<<: !include common-get-value.yaml