diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp deleted file mode 100644 index 366aa10697..0000000000 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ /dev/null @@ -1,355 +0,0 @@ -#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 { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -void ESP32TouchComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - touch_pad_init(); -// set up and enable/start filtering based on ESP32 variant -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - 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(); - } -#else - if (this->iir_filter_enabled_()) { - touch_pad_filter_start(this->iir_filter_); - } -#endif - -#if ESP_IDF_VERSION_MAJOR >= 5 && defined(USE_ESP32_VARIANT_ESP32) - 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_); - - for (auto *child : this->children_) { -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_pad_config(child->get_touch_pad()); -#else - // Disable interrupt threshold - touch_pad_config(child->get_touch_pad(), 0); -#endif - } -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - touch_pad_fsm_start(); -#endif -} - -void ESP32TouchComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Config for ESP32 Touch Hub:\n" - " Meas cycle: %.2fms\n" - " Sleep cycle: %.2fms", - this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f)); - - const char *lv_s; - switch (this->low_voltage_reference_) { - case TOUCH_LVOLT_0V5: - lv_s = "0.5V"; - break; - case TOUCH_LVOLT_0V6: - lv_s = "0.6V"; - break; - case TOUCH_LVOLT_0V7: - lv_s = "0.7V"; - break; - case TOUCH_LVOLT_0V8: - lv_s = "0.8V"; - break; - default: - lv_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Low Voltage Reference: %s", lv_s); - - const char *hv_s; - switch (this->high_voltage_reference_) { - case TOUCH_HVOLT_2V4: - hv_s = "2.4V"; - break; - case TOUCH_HVOLT_2V5: - hv_s = "2.5V"; - break; - case TOUCH_HVOLT_2V6: - hv_s = "2.6V"; - break; - case TOUCH_HVOLT_2V7: - hv_s = "2.7V"; - break; - default: - hv_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " High Voltage Reference: %s", hv_s); - - const char *atten_s; - switch (this->voltage_attenuation_) { - case TOUCH_HVOLT_ATTEN_1V5: - atten_s = "1.5V"; - break; - case TOUCH_HVOLT_ATTEN_1V: - atten_s = "1V"; - break; - case TOUCH_HVOLT_ATTEN_0V5: - atten_s = "0.5V"; - break; - case TOUCH_HVOLT_ATTEN_0V: - atten_s = "0V"; - break; - default: - atten_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Voltage Attenuation: %s", atten_s); - -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - 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); - } -#else - if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); - } else { - ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); - } -#endif - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - for (auto *child : this->children_) { - LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); - ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); - } -} - -uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) { -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - uint32_t value = 0; - if (this->filter_configured_()) { - touch_pad_filter_read_smooth(tp, &value); - } else { - touch_pad_read_raw_data(tp, &value); - } -#else - uint16_t value = 0; - if (this->iir_filter_enabled_()) { - touch_pad_read_filtered(tp, &value); - } else { - touch_pad_read(tp, &value); - } -#endif - return value; -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; - for (auto *child : this->children_) { - child->value_ = this->component_touch_pad_read(child->get_touch_pad()); -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - child->publish_state(child->value_ < child->get_threshold()); -#else - child->publish_state(child->value_ > child->get_threshold()); -#endif - - if (should_print) { - ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), - (uint32_t) child->get_touch_pad(), child->value_); - } - - App.feed_wdt(); - } - - if (should_print) { - // Avoid spamming logs - this->setup_mode_last_log_print_ = now; - } -} - -void ESP32TouchComponent::on_shutdown() { - bool is_wakeup_source = false; - -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - if (this->iir_filter_enabled_()) { - touch_pad_filter_stop(); - touch_pad_filter_delete(); - } -#endif - - for (auto *child : this->children_) { - if (child->get_wakeup_threshold() != 0) { - if (!is_wakeup_source) { - is_wakeup_source = true; - // Touch sensor FSM mode must be 'TOUCH_FSM_MODE_TIMER' to use it to wake-up. - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - } - -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - // No filter available when using as wake-up source. - touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); -#endif - } - } - - if (!is_wakeup_source) { - touch_pad_deinit(); - } -} - -ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) - : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} - -} // namespace esp32_touch -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 3fce8a7e18..576c1a5649 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -9,10 +9,26 @@ #include #include +#include +#include namespace esphome { namespace 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. +// +// 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; + class ESP32TouchBinarySensor; class ESP32TouchComponent : public Component { @@ -31,6 +47,14 @@ class ESP32TouchComponent : public Component { void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { this->voltage_attenuation_ = voltage_attenuation; } + + void setup() override; + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + 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; } @@ -47,16 +71,101 @@ class ESP32TouchComponent : public Component { void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } #endif - uint32_t component_touch_pad_read(touch_pad_t tp); + protected: + // Common helper methods + void dump_config_base_(); + void dump_config_sensors_(); + bool create_touch_queue_(); + void cleanup_touch_queue_(); + void configure_wakeup_pads_(); - void setup() override; - void dump_config() override; - void loop() override; + // 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_(); - void on_shutdown() override; + // 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}; + bool initial_state_published_[TOUCH_PAD_MAX] = {false}; + + // 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}; + + // Common constants + static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100; + + // ==================== PLATFORM SPECIFIC ==================== + +#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: -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + // 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_[TOUCH_PAD_MAX] = {0}; + 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}; + + 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; + }; + + // Track last touch time for timeout-based release detection + uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0}; + + protected: + // Filter configuration + touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + 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}; + + // 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); } @@ -67,43 +176,78 @@ class ESP32TouchComponent : public Component { return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); } -#else - bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } + + // 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 + void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched); + + // Helper to read touch value and update state for a given child + bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); #endif - std::vector children_; - bool setup_mode_{false}; - uint32_t setup_mode_last_log_print_{0}; - // common 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}; -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; - 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_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; - touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; - touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; - touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; -#else - uint32_t iir_filter_{0}; -#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"; + } + } }; /// 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); + ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) + : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} touch_pad_t get_touch_pad() const { return this->touch_pad_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } +#ifdef USE_ESP32_VARIANT_ESP32 uint32_t get_value() const { return this->value_; } +#endif uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } protected: @@ -111,7 +255,10 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t touch_pad_{TOUCH_PAD_MAX}; uint32_t threshold_{0}; +#ifdef USE_ESP32_VARIANT_ESP32 uint32_t value_{0}; +#endif + bool last_state_{false}; const uint32_t wakeup_threshold_{0}; }; diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp new file mode 100644 index 0000000000..fd2cdfcbad --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_common.cpp @@ -0,0 +1,159 @@ +#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", + this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s, + atten_s); +} + +void ESP32TouchComponent::dump_config_sensors_() { + for (auto *child : this->children_) { + LOG_BINARY_SENSOR(" ", "Touch Pad", child); + ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); + ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); + } +} + +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()); + ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value); +#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) { + touch_pad_t pad = child->get_touch_pad(); + if (!this->initial_state_published_[pad]) { + // Check if enough time has passed since startup + if (now > this->release_timeout_ms_) { + child->publish_initial_state(false); + this->initial_state_published_[pad] = 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 new file mode 100644 index 0000000000..a6d499e9fa --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -0,0 +1,240 @@ +#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"; + +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_) { + 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) { + this->last_touch_time_[event.pad] = now; + } + + // Only publish if state changed - this filters out repeated events + if (new_state != child->last_state_) { + 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: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", + child->get_name().c_str(), 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_) { + touch_pad_t pad = child->get_touch_pad(); + + // 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 - this->last_touch_time_[pad]; + + // 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); + + 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 + uint32_t value; + if (component->iir_filter_enabled_()) { + uint16_t temp_value = 0; + touch_pad_read_filtered(pad, &temp_value); + value = temp_value; + } else { + // Use low-level HAL function when filter is not enabled + value = touch_ll_read_raw_data(pad); + } + + // Skip pads with 0 value - they haven't been measured in this cycle + // This is important: not all pads are measured every interrupt cycle, + // only those that the hardware has updated + if (value == 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 new file mode 100644 index 0000000000..ad77881724 --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp @@ -0,0 +1,398 @@ +#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 +void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) { + // Always update timer when touched + if (is_touched) { + this->last_touch_time_[child->get_touch_pad()] = App.get_loop_component_start_time(); + } + + if (child->last_state_ != is_touched) { + // Read value for logging + uint32_t value = this->read_touch_value(child->get_touch_pad()); + + child->last_state_ = is_touched; + child->publish_state(is_touched); + if (is_touched) { + // ESP32-S2/S3 v2: touched when value > threshold + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), + value, child->get_threshold()); + } 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->get_touch_pad()); + + // ESP32-S2/S3 v2: Touch is detected when value > threshold + bool is_touched = value > child->get_threshold(); + + this->update_touch_state_(child, is_touched); + 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->get_touch_pad()); + if (config_err != ESP_OK) { + ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->get_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_); + // ESP32-S2/S3 always use the older API + touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); + + // Configure timeout if needed + touch_pad_timeout_set(true, 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->get_threshold() != 0) { + touch_pad_set_thresh(child->get_touch_pad(), child->get_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) { + // 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->get_touch_pad() != event.pad) { + continue; + } + + 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 + this->update_touch_state_(child, true); // 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_) { + touch_pad_t pad = child->get_touch_pad(); + + // 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 - this->last_touch_time_[pad]; + + // 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(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); + 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