ESPHome 2026.1.5
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 // Disable hardware timeout - it causes continuous interrupts with high-capacitance
109 // setups (e.g., pressure sensors under cushions). The periodic release check in
110 // loop() handles state detection reliably without needing hardware timeout.
111 touch_pad_timeout_set(false, TOUCH_PAD_THRESHOLD_MAX);
112
113 // Register ISR handler with interrupt mask
114 esp_err_t err =
115 touch_pad_isr_register(touch_isr_handler, this, static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ALL));
116 if (err != ESP_OK) {
117 ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
118 this->cleanup_touch_queue_();
119 this->mark_failed();
120 return;
121 }
122
123 // Set thresholds for each pad BEFORE starting FSM
124 for (auto *child : this->children_) {
125 if (child->threshold_ != 0) {
126 touch_pad_set_thresh(child->touch_pad_, child->threshold_);
127 }
128 }
129
130 // Enable interrupts - only ACTIVE and TIMEOUT
131 // NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable
132 // on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based
133 // release detection with the ability to verify the actual state.
134 touch_pad_intr_enable(static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT));
135
136 // Set FSM mode before starting
137 touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
138
139 // Start FSM
140 touch_pad_fsm_start();
141
142 // Calculate release timeout based on sleep cycle
144}
145
147 this->dump_config_base_();
148
149 if (this->filter_configured_()) {
150 const char *filter_mode_s;
151 switch (this->filter_mode_) {
152 case TOUCH_PAD_FILTER_IIR_4:
153 filter_mode_s = "IIR_4";
154 break;
155 case TOUCH_PAD_FILTER_IIR_8:
156 filter_mode_s = "IIR_8";
157 break;
158 case TOUCH_PAD_FILTER_IIR_16:
159 filter_mode_s = "IIR_16";
160 break;
161 case TOUCH_PAD_FILTER_IIR_32:
162 filter_mode_s = "IIR_32";
163 break;
164 case TOUCH_PAD_FILTER_IIR_64:
165 filter_mode_s = "IIR_64";
166 break;
167 case TOUCH_PAD_FILTER_IIR_128:
168 filter_mode_s = "IIR_128";
169 break;
170 case TOUCH_PAD_FILTER_IIR_256:
171 filter_mode_s = "IIR_256";
172 break;
173 case TOUCH_PAD_FILTER_JITTER:
174 filter_mode_s = "JITTER";
175 break;
176 default:
177 filter_mode_s = "UNKNOWN";
178 break;
179 }
180 ESP_LOGCONFIG(TAG,
181 " Filter mode: %s\n"
182 " Debounce count: %" PRIu32 "\n"
183 " Noise threshold coefficient: %" PRIu32 "\n"
184 " Jitter filter step size: %" PRIu32,
185 filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_);
186 const char *smooth_level_s;
187 switch (this->smooth_level_) {
188 case TOUCH_PAD_SMOOTH_OFF:
189 smooth_level_s = "OFF";
190 break;
191 case TOUCH_PAD_SMOOTH_IIR_2:
192 smooth_level_s = "IIR_2";
193 break;
194 case TOUCH_PAD_SMOOTH_IIR_4:
195 smooth_level_s = "IIR_4";
196 break;
197 case TOUCH_PAD_SMOOTH_IIR_8:
198 smooth_level_s = "IIR_8";
199 break;
200 default:
201 smooth_level_s = "UNKNOWN";
202 break;
203 }
204 ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s);
205 }
206
207 if (this->denoise_configured_()) {
208 const char *grade_s;
209 switch (this->grade_) {
210 case TOUCH_PAD_DENOISE_BIT12:
211 grade_s = "BIT12";
212 break;
213 case TOUCH_PAD_DENOISE_BIT10:
214 grade_s = "BIT10";
215 break;
216 case TOUCH_PAD_DENOISE_BIT8:
217 grade_s = "BIT8";
218 break;
219 case TOUCH_PAD_DENOISE_BIT4:
220 grade_s = "BIT4";
221 break;
222 default:
223 grade_s = "UNKNOWN";
224 break;
225 }
226 ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s);
227
228 const char *cap_level_s;
229 switch (this->cap_level_) {
230 case TOUCH_PAD_DENOISE_CAP_L0:
231 cap_level_s = "L0";
232 break;
233 case TOUCH_PAD_DENOISE_CAP_L1:
234 cap_level_s = "L1";
235 break;
236 case TOUCH_PAD_DENOISE_CAP_L2:
237 cap_level_s = "L2";
238 break;
239 case TOUCH_PAD_DENOISE_CAP_L3:
240 cap_level_s = "L3";
241 break;
242 case TOUCH_PAD_DENOISE_CAP_L4:
243 cap_level_s = "L4";
244 break;
245 case TOUCH_PAD_DENOISE_CAP_L5:
246 cap_level_s = "L5";
247 break;
248 case TOUCH_PAD_DENOISE_CAP_L6:
249 cap_level_s = "L6";
250 break;
251 case TOUCH_PAD_DENOISE_CAP_L7:
252 cap_level_s = "L7";
253 break;
254 default:
255 cap_level_s = "UNKNOWN";
256 break;
257 }
258 ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s);
259 }
260
261 if (this->setup_mode_) {
262 ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
263 }
264
265 this->dump_config_sensors_();
266}
267
270
271 // V2 TOUCH HANDLING:
272 // Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach:
273 // 1. Process ACTIVE interrupts when pads are touched
274 // 2. Use timeout-based release detection (like v1)
275 // 3. But smarter than v1: verify actual state before releasing on timeout
276 // This prevents false releases if we missed interrupts
277
278 // In setup mode, periodically log all pad values
280
281 // Process any queued touch events from interrupts
282 TouchPadEventV2 event;
283 while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
284 ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad);
285 // Handle timeout events
286 if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
287 // Resume measurement after timeout
288 touch_pad_timeout_resume();
289 // For timeout events, always check the current state
290 } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) {
291 // Skip if not an active/timeout event
292 continue;
293 }
294
295 // Find the child for the pad that triggered the interrupt
296 for (auto *child : this->children_) {
297 if (child->touch_pad_ == event.pad) {
298 if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
299 // For timeout events, we need to read the value to determine state
301 } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
302 // We only get ACTIVE interrupts now, releases are detected by timeout
303 // Read the current value
304 uint32_t value = this->read_touch_value(child->touch_pad_);
305 this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts
306 }
307 break;
308 }
309 }
310 }
311
312 // Check for released pads periodically (like v1)
313 if (!this->should_check_for_releases_(now)) {
314 return;
315 }
316
317 size_t pads_off = 0;
318 for (auto *child : this->children_) {
319 child->ensure_benchmark_read();
320 // Handle initial state publication after startup
321 this->publish_initial_state_if_needed_(child, now);
322
323 if (child->last_state_) {
324 // Pad is currently in touched state - check for release timeout
325 // Using subtraction handles 32-bit rollover correctly
326 uint32_t time_diff = now - child->last_touch_time_;
327
328 // Check if we haven't seen this pad recently
329 if (time_diff > this->release_timeout_ms_) {
330 // Haven't seen this pad recently - verify actual state
331 // Unlike v1, v2 hardware allows us to read the current state anytime
332 // This makes v2 smarter: we can verify if it's actually released before
333 // declaring a timeout, preventing false releases if interrupts were missed
334 bool still_touched = this->check_and_update_touch_state_(child);
335
336 if (still_touched) {
337 // Still touched! Timer was reset in update_touch_state_
338 ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer",
339 child->get_name().c_str(), this->release_timeout_ms_);
340 } else {
341 // Actually released - already handled by check_and_update_touch_state_
342 pads_off++;
343 }
344 }
345 } else {
346 // Pad is already off
347 pads_off++;
348 }
349 }
350
351 // Disable the loop when all pads are off and not in setup mode (like v1)
352 // We need to keep checking for timeouts, so only disable when all pads are confirmed off
354}
355
357 // Disable interrupts
358 touch_pad_intr_disable(TOUCH_PAD_INTR_MASK_ACTIVE);
359 touch_pad_isr_deregister(touch_isr_handler, this);
360 this->cleanup_touch_queue_();
361
362 // Configure wakeup pads if any are set
364}
365
366void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
367 ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
368 BaseType_t x_higher_priority_task_woken = pdFALSE;
369
370 // Read interrupt status
371 TouchPadEventV2 event;
372 event.intr_mask = touch_pad_read_intr_status_mask();
373 event.pad = touch_pad_get_current_meas_channel();
374
375 // Send event to queue for processing in main loop
376 xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
377 component->enable_loop_soon_any_context();
378
379 if (x_higher_priority_task_woken) {
380 portYIELD_FROM_ISR();
381 }
382}
383
384uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const {
385 // Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations.
386 // The hardware continuously samples in the background and we can read the
387 // latest value at any time without waiting.
388 uint32_t value = 0;
389 if (this->filter_configured_()) {
390 // Read filtered/smoothed value when filter is enabled
391 touch_pad_filter_read_smooth(pad, &value);
392 } else {
393 // Read raw value when filter is not configured
394 touch_pad_read_raw_data(pad, &value);
395 }
396 return value;
397}
398
399} // namespace esp32_touch
400} // namespace esphome
401
402#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:73
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
const Component * component
Definition component.cpp:37
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