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