ESPHome 2025.7.3
Loading...
Searching...
No Matches
esp32_touch_v2.cpp
Go to the documentation of this file.
1#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
2
3#include "esp32_touch.h"
5#include "esphome/core/log.h"
6#include "esphome/core/hal.h"
7
8namespace esphome {
9namespace esp32_touch {
10
11static const char *const TAG = "esp32_touch";
12
13// Helper to update touch state with a known state
15 // Always update timer when touched
16 if (is_touched) {
18 }
19
20 if (child->last_state_ != is_touched) {
21 child->last_state_ = is_touched;
22 child->publish_state(is_touched);
23 if (is_touched) {
24 // ESP32-S2/S3 v2: touched when value > threshold
25 ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(),
26 this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_);
27 } else {
28 ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
29 }
30 }
31}
32
33// Helper to read touch value and update state for a given child (used for timeout events)
35 // Read current touch value
36 uint32_t value = this->read_touch_value(child->touch_pad_);
37
38 // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark
39 ESP_LOGV(TAG,
40 "Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32,
41 child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_);
42 bool is_touched = value > child->benchmark_ + child->threshold_;
43
44 this->update_touch_state_(child, is_touched);
45 return is_touched;
46}
47
49 // Create queue for touch events first
50 if (!this->create_touch_queue_()) {
51 return;
52 }
53
54 // Initialize touch pad peripheral
55 esp_err_t init_err = touch_pad_init();
56 if (init_err != ESP_OK) {
57 ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err));
58 this->mark_failed();
59 return;
60 }
61
62 // Configure each touch pad first
63 for (auto *child : this->children_) {
64 esp_err_t config_err = touch_pad_config(child->touch_pad_);
65 if (config_err != ESP_OK) {
66 ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err));
67 }
68 }
69
70 // Set up filtering if configured
71 if (this->filter_configured_()) {
72 touch_filter_config_t filter_info = {
73 .mode = this->filter_mode_,
74 .debounce_cnt = this->debounce_count_,
75 .noise_thr = this->noise_threshold_,
76 .jitter_step = this->jitter_step_,
77 .smh_lvl = this->smooth_level_,
78 };
79 touch_pad_filter_set_config(&filter_info);
80 touch_pad_filter_enable();
81 }
82
83 if (this->denoise_configured_()) {
84 touch_pad_denoise_t denoise = {
85 .grade = this->grade_,
86 .cap_level = this->cap_level_,
87 };
88 touch_pad_denoise_set_config(&denoise);
89 touch_pad_denoise_enable();
90 }
91
92 if (this->waterproof_configured_()) {
93 touch_pad_waterproof_t waterproof = {
94 .guard_ring_pad = this->waterproof_guard_ring_pad_,
95 .shield_driver = this->waterproof_shield_driver_,
96 };
97 touch_pad_waterproof_set_config(&waterproof);
98 touch_pad_waterproof_enable();
99 }
100
101 // Configure measurement parameters
102 touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
103 touch_pad_set_charge_discharge_times(this->meas_cycle_);
104 touch_pad_set_measurement_interval(this->sleep_cycle_);
105
106 // Configure timeout if needed
107 touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX);
108
109 // Register ISR handler with interrupt mask
110 esp_err_t err =
111 touch_pad_isr_register(touch_isr_handler, this, static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ALL));
112 if (err != ESP_OK) {
113 ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
114 this->cleanup_touch_queue_();
115 this->mark_failed();
116 return;
117 }
118
119 // Set thresholds for each pad BEFORE starting FSM
120 for (auto *child : this->children_) {
121 if (child->threshold_ != 0) {
122 touch_pad_set_thresh(child->touch_pad_, child->threshold_);
123 }
124 }
125
126 // Enable interrupts - only ACTIVE and TIMEOUT
127 // NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable
128 // on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based
129 // release detection with the ability to verify the actual state.
130 touch_pad_intr_enable(static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT));
131
132 // Set FSM mode before starting
133 touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
134
135 // Start FSM
136 touch_pad_fsm_start();
137
138 // Calculate release timeout based on sleep cycle
140}
141
143 this->dump_config_base_();
144
145 if (this->filter_configured_()) {
146 const char *filter_mode_s;
147 switch (this->filter_mode_) {
148 case TOUCH_PAD_FILTER_IIR_4:
149 filter_mode_s = "IIR_4";
150 break;
151 case TOUCH_PAD_FILTER_IIR_8:
152 filter_mode_s = "IIR_8";
153 break;
154 case TOUCH_PAD_FILTER_IIR_16:
155 filter_mode_s = "IIR_16";
156 break;
157 case TOUCH_PAD_FILTER_IIR_32:
158 filter_mode_s = "IIR_32";
159 break;
160 case TOUCH_PAD_FILTER_IIR_64:
161 filter_mode_s = "IIR_64";
162 break;
163 case TOUCH_PAD_FILTER_IIR_128:
164 filter_mode_s = "IIR_128";
165 break;
166 case TOUCH_PAD_FILTER_IIR_256:
167 filter_mode_s = "IIR_256";
168 break;
169 case TOUCH_PAD_FILTER_JITTER:
170 filter_mode_s = "JITTER";
171 break;
172 default:
173 filter_mode_s = "UNKNOWN";
174 break;
175 }
176 ESP_LOGCONFIG(TAG,
177 " Filter mode: %s\n"
178 " Debounce count: %" PRIu32 "\n"
179 " Noise threshold coefficient: %" PRIu32 "\n"
180 " Jitter filter step size: %" PRIu32,
181 filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_);
182 const char *smooth_level_s;
183 switch (this->smooth_level_) {
184 case TOUCH_PAD_SMOOTH_OFF:
185 smooth_level_s = "OFF";
186 break;
187 case TOUCH_PAD_SMOOTH_IIR_2:
188 smooth_level_s = "IIR_2";
189 break;
190 case TOUCH_PAD_SMOOTH_IIR_4:
191 smooth_level_s = "IIR_4";
192 break;
193 case TOUCH_PAD_SMOOTH_IIR_8:
194 smooth_level_s = "IIR_8";
195 break;
196 default:
197 smooth_level_s = "UNKNOWN";
198 break;
199 }
200 ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s);
201 }
202
203 if (this->denoise_configured_()) {
204 const char *grade_s;
205 switch (this->grade_) {
206 case TOUCH_PAD_DENOISE_BIT12:
207 grade_s = "BIT12";
208 break;
209 case TOUCH_PAD_DENOISE_BIT10:
210 grade_s = "BIT10";
211 break;
212 case TOUCH_PAD_DENOISE_BIT8:
213 grade_s = "BIT8";
214 break;
215 case TOUCH_PAD_DENOISE_BIT4:
216 grade_s = "BIT4";
217 break;
218 default:
219 grade_s = "UNKNOWN";
220 break;
221 }
222 ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s);
223
224 const char *cap_level_s;
225 switch (this->cap_level_) {
226 case TOUCH_PAD_DENOISE_CAP_L0:
227 cap_level_s = "L0";
228 break;
229 case TOUCH_PAD_DENOISE_CAP_L1:
230 cap_level_s = "L1";
231 break;
232 case TOUCH_PAD_DENOISE_CAP_L2:
233 cap_level_s = "L2";
234 break;
235 case TOUCH_PAD_DENOISE_CAP_L3:
236 cap_level_s = "L3";
237 break;
238 case TOUCH_PAD_DENOISE_CAP_L4:
239 cap_level_s = "L4";
240 break;
241 case TOUCH_PAD_DENOISE_CAP_L5:
242 cap_level_s = "L5";
243 break;
244 case TOUCH_PAD_DENOISE_CAP_L6:
245 cap_level_s = "L6";
246 break;
247 case TOUCH_PAD_DENOISE_CAP_L7:
248 cap_level_s = "L7";
249 break;
250 default:
251 cap_level_s = "UNKNOWN";
252 break;
253 }
254 ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s);
255 }
256
257 if (this->setup_mode_) {
258 ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
259 }
260
261 this->dump_config_sensors_();
262}
263
266
267 // V2 TOUCH HANDLING:
268 // Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach:
269 // 1. Process ACTIVE interrupts when pads are touched
270 // 2. Use timeout-based release detection (like v1)
271 // 3. But smarter than v1: verify actual state before releasing on timeout
272 // This prevents false releases if we missed interrupts
273
274 // In setup mode, periodically log all pad values
276
277 // Process any queued touch events from interrupts
278 TouchPadEventV2 event;
279 while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
280 ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad);
281 // Handle timeout events
282 if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
283 // Resume measurement after timeout
284 touch_pad_timeout_resume();
285 // For timeout events, always check the current state
286 } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) {
287 // Skip if not an active/timeout event
288 continue;
289 }
290
291 // Find the child for the pad that triggered the interrupt
292 for (auto *child : this->children_) {
293 if (child->touch_pad_ == event.pad) {
294 if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
295 // For timeout events, we need to read the value to determine state
297 } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
298 // We only get ACTIVE interrupts now, releases are detected by timeout
299 this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts
300 }
301 break;
302 }
303 }
304 }
305
306 // Check for released pads periodically (like v1)
307 if (!this->should_check_for_releases_(now)) {
308 return;
309 }
310
311 size_t pads_off = 0;
312 for (auto *child : this->children_) {
313 if (child->benchmark_ == 0)
314 touch_pad_read_benchmark(child->touch_pad_, &child->benchmark_);
315 // Handle initial state publication after startup
316 this->publish_initial_state_if_needed_(child, now);
317
318 if (child->last_state_) {
319 // Pad is currently in touched state - check for release timeout
320 // Using subtraction handles 32-bit rollover correctly
321 uint32_t time_diff = now - child->last_touch_time_;
322
323 // Check if we haven't seen this pad recently
324 if (time_diff > this->release_timeout_ms_) {
325 // Haven't seen this pad recently - verify actual state
326 // Unlike v1, v2 hardware allows us to read the current state anytime
327 // This makes v2 smarter: we can verify if it's actually released before
328 // declaring a timeout, preventing false releases if interrupts were missed
329 bool still_touched = this->check_and_update_touch_state_(child);
330
331 if (still_touched) {
332 // Still touched! Timer was reset in update_touch_state_
333 ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer",
334 child->get_name().c_str(), this->release_timeout_ms_);
335 } else {
336 // Actually released - already handled by check_and_update_touch_state_
337 pads_off++;
338 }
339 }
340 } else {
341 // Pad is already off
342 pads_off++;
343 }
344 }
345
346 // Disable the loop when all pads are off and not in setup mode (like v1)
347 // We need to keep checking for timeouts, so only disable when all pads are confirmed off
349}
350
352 // Disable interrupts
353 touch_pad_intr_disable(static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT));
354 touch_pad_isr_deregister(touch_isr_handler, this);
355 this->cleanup_touch_queue_();
356
357 // Configure wakeup pads if any are set
359}
360
361void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
362 ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
363 BaseType_t x_higher_priority_task_woken = pdFALSE;
364
365 // Read interrupt status
366 TouchPadEventV2 event;
367 event.intr_mask = touch_pad_read_intr_status_mask();
368 event.pad = touch_pad_get_current_meas_channel();
369
370 // Send event to queue for processing in main loop
371 xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
372 component->enable_loop_soon_any_context();
373
374 if (x_higher_priority_task_woken) {
375 portYIELD_FROM_ISR();
376 }
377}
378
379uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const {
380 // Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations.
381 // The hardware continuously samples in the background and we can read the
382 // latest value at any time without waiting.
383 uint32_t value = 0;
384 if (this->filter_configured_()) {
385 // Read filtered/smoothed value when filter is enabled
386 touch_pad_filter_read_smooth(pad, &value);
387 } else {
388 // Read raw value when filter is not configured
389 touch_pad_read_raw_data(pad, &value);
390 }
391 return value;
392}
393
394} // namespace esp32_touch
395} // namespace esphome
396
397#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
virtual void mark_failed()
Mark this component as failed.
const StringRef & get_name() const
constexpr const char * c_str() const
Definition string_ref.h:69
void publish_state(bool new_state)
Publish a new state to the front-end.
Simple helper class to expose a touch pad value as a binary sensor.
void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched)
void check_and_disable_loop_if_all_released_(size_t pads_off)
bool check_and_update_touch_state_(ESP32TouchBinarySensor *child)
void publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now)
touch_pad_shield_driver_t waterproof_shield_driver_
std::vector< ESP32TouchBinarySensor * > children_
Definition esp32_touch.h:90
uint32_t read_touch_value(touch_pad_t pad) const
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
Application App
Global storage of Application pointer - only one Application can exist.
uint8_t pad