From 0df454481eb8654506a47d752e0f1edde47874c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jun 2025 14:15:26 -0500 Subject: [PATCH] safer --- esphome/components/esp32_touch/esp32_touch.h | 14 +- .../components/esp32_touch/esp32_touch_v2.cpp | 125 +++++++++++++----- 2 files changed, 104 insertions(+), 35 deletions(-) diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 70de25cdfa..27a18526c4 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; @@ -127,6 +129,10 @@ class ESP32TouchComponent : public Component { static void touch_isr_handler(void *arg); QueueHandle_t touch_queue_{nullptr}; + // Timeout-based release detection (like v1) + uint32_t release_timeout_ms_{1500}; + uint32_t release_check_interval_ms_{50}; + private: // Touch event structure for ESP32 v2 (S2/S3) // Contains touch pad and interrupt mask for queue communication @@ -135,6 +141,10 @@ class ESP32TouchComponent : public Component { uint32_t intr_mask; }; + // Track last touch time and initial state for timeout-based release detection + uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0}; + bool initial_state_published_[TOUCH_PAD_MAX] = {false}; + protected: // Filter configuration touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; @@ -171,7 +181,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_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp index b9e3da52c4..ee012bd878 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,15 @@ 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()); + // Initialize tracking arrays + for (size_t i = 0; i < TOUCH_PAD_MAX; i++) { + this->last_touch_time_[i] = 0; + this->initial_state_published_[i] = false; } + + // Mark initial states as not published yet (like v1) + // The actual initial state will be determined after release_timeout_ms_ in the loop + // This prevents false positives during startup when values may be unstable } void ESP32TouchComponent::dump_config() { @@ -262,6 +271,13 @@ void ESP32TouchComponent::dump_config() { 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 if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { for (auto *child : this->children_) { @@ -281,8 +297,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 +311,72 @@ 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 + + // Check for released pads periodically (like v1) + static uint32_t last_release_check = 0; + if (now - last_release_check < this->release_check_interval_ms_) { + 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_) { + // 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_LOGD(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 + if (pads_off == this->children_.size() && !this->setup_mode_) { this->disable_loop(); } } 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_();