diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 70de25cdfa..576c1a5649 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -23,7 +23,9 @@ namespace esp32_touch { // 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: Interrupts can be configured per-pad with both touch and release events. +// - 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; @@ -77,10 +79,21 @@ class ESP32TouchComponent : public Component { 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_(); + // 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}; @@ -89,11 +102,13 @@ class ESP32TouchComponent : public Component { 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 constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100; static void touch_isr_handler(void *arg); QueueHandle_t touch_queue_{nullptr}; @@ -115,9 +130,6 @@ class ESP32TouchComponent : public Component { // 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}; - bool initial_state_published_[TOUCH_PAD_MAX] = {false}; - uint32_t release_timeout_ms_{1500}; - uint32_t release_check_interval_ms_{50}; uint32_t iir_filter_{0}; bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } @@ -135,6 +147,9 @@ class ESP32TouchComponent : public Component { 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}; @@ -171,7 +186,7 @@ class ESP32TouchComponent : public Component { void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched); // Helper to read touch value and update state for a given child - void check_and_update_touch_state_(ESP32TouchBinarySensor *child); + bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); #endif // Helper functions for dump_config - common to both implementations diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp index 39769ed37a..fd2cdfcbad 100644 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ b/esphome/components/esp32_touch/esp32_touch_common.cpp @@ -4,6 +4,8 @@ #include "esphome/core/log.h" #include +#include "soc/rtc.h" + namespace esphome { namespace esp32_touch { @@ -85,6 +87,72 @@ void ESP32TouchComponent::configure_wakeup_pads_() { } } +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 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index 7b46cd9280..a6d499e9fa 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -10,8 +10,6 @@ // Include HAL for ISR-safe touch reading #include "hal/touch_sensor_ll.h" -// Include for RTC clock frequency -#include "soc/rtc.h" namespace esphome { namespace esp32_touch { @@ -59,20 +57,7 @@ void ESP32TouchComponent::setup() { } // Calculate release timeout based on sleep cycle - // Design note: ESP32 v1 hardware limitation - interrupts only fire 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 - // The division by 2 accounts for the fact that sleep_cycle is in half-cycles - uint32_t rtc_freq = rtc_clk_slow_freq_get_hz(); - this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / (rtc_freq * 2); - 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 the ESP32 v1 hardware doesn't generate 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; + this->calculate_release_timeout_(); // Enable touch pad interrupt touch_pad_intr_enable(); @@ -98,13 +83,7 @@ void ESP32TouchComponent::loop() { const uint32_t now = App.get_loop_component_start_time(); // Print debug info for all pads in setup mode - if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { - for (auto *child : this->children_) { - ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), - (uint32_t) child->get_touch_pad(), child->value_); - } - this->setup_mode_last_log_print_ = now; - } + 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) @@ -142,26 +121,18 @@ void ESP32TouchComponent::loop() { } // Check for released pads periodically - static uint32_t last_release_check = 0; - if (now - last_release_check < this->release_check_interval_ms_) { + if (!this->should_check_for_releases_(now)) { return; } - last_release_check = now; size_t pads_off = 0; for (auto *child : this->children_) { touch_pad_t pad = child->get_touch_pad(); // Handle initial state publication after startup - 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()); - pads_off++; - } - } else if (child->last_state_) { + 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]; @@ -186,9 +157,7 @@ void ESP32TouchComponent::loop() { // - 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 - if (pads_off == this->children_.size() && !this->setup_mode_) { - this->disable_loop(); - } + this->check_and_disable_loop_if_all_released_(pads_off); } void ESP32TouchComponent::on_shutdown() { diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp index b9e3da52c4..ad77881724 100644 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp @@ -12,19 +12,29 @@ 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); - ESP_LOGD(TAG, "Touch Pad '%s' %s (value: %" PRIu32 " %s threshold: %" PRIu32 ")", child->get_name().c_str(), - is_touched ? "touched" : "released", value, is_touched ? ">" : "<=", child->get_threshold()); + 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) -void ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { +bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { // Read current touch value uint32_t value = this->read_touch_value(child->get_touch_pad()); @@ -32,6 +42,7 @@ void ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor * bool is_touched = value > child->get_threshold(); this->update_touch_state_(child, is_touched); + return is_touched; } void ESP32TouchComponent::setup() { @@ -112,9 +123,11 @@ void ESP32TouchComponent::setup() { } } - // Enable interrupts - touch_pad_intr_enable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_INACTIVE | - TOUCH_PAD_INTR_MASK_TIMEOUT)); + // 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); @@ -122,19 +135,8 @@ void ESP32TouchComponent::setup() { // Start FSM touch_pad_fsm_start(); - // Read initial states after all hardware is initialized - for (auto *child : this->children_) { - // Read current value - uint32_t value = this->read_touch_value(child->get_touch_pad()); - - // Set initial state and publish - bool is_touched = value > child->get_threshold(); - child->last_state_ = is_touched; - child->publish_initial_state(is_touched); - - ESP_LOGD(TAG, "Touch Pad '%s' initial state: %s (value: %d %s threshold: %d)", child->get_name().c_str(), - is_touched ? "touched" : "released", value, is_touched ? ">" : "<=", child->get_threshold()); - } + // Calculate release timeout based on sleep cycle + this->calculate_release_timeout_(); } void ESP32TouchComponent::dump_config() { @@ -262,16 +264,15 @@ void ESP32TouchComponent::dump_config() { void ESP32TouchComponent::loop() { const uint32_t now = App.get_loop_component_start_time(); - // In setup mode, periodically log all pad values - if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { - for (auto *child : this->children_) { - // Read the value being used for touch detection - uint32_t value = this->read_touch_value(child->get_touch_pad()); + // 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 - ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value); - } - this->setup_mode_last_log_print_ = now; - } + // In setup mode, periodically log all pad values + this->process_setup_mode_logging_(now); // Process any queued touch events from interrupts TouchPadEventV2 event; @@ -281,8 +282,8 @@ void ESP32TouchComponent::loop() { // 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 | TOUCH_PAD_INTR_MASK_INACTIVE))) { - // Skip if not an active/inactive/timeout event + } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) { + // Skip if not an active/timeout event continue; } @@ -295,29 +296,62 @@ void ESP32TouchComponent::loop() { 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 { - // For ACTIVE/INACTIVE events, the interrupt tells us the state - bool is_touched = (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) != 0; - this->update_touch_state_(child, is_touched); + } 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; } } - if (!this->setup_mode_) { - // Disable the loop to save CPU cycles when not in setup mode. - // The loop will be re-enabled by the ISR when any touch event occurs. - // Unlike v1, we don't need to check if all pads are off because: - // - v2 hardware generates interrupts for both touch AND release events - // - We don't need to poll for timeouts or releases - // - All state changes are interrupt-driven - this->disable_loop(); + + // 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_INACTIVE | - TOUCH_PAD_INTR_MASK_TIMEOUT)); + 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_();