mirror of
https://github.com/esphome/esphome.git
synced 2026-02-28 18:04:19 -07:00
[esp32_touch] Migrate to new unified touch sensor driver (esp_driver_touch_sens) (#14033)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
@@ -21,6 +24,8 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["binary_sensor"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
@@ -37,135 +42,161 @@ CONF_WATERPROOF_SHIELD_DRIVER = "waterproof_shield_driver"
|
||||
esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch")
|
||||
ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component)
|
||||
|
||||
# Channel ID mappings: GPIO pin number -> integer channel ID
|
||||
# These are plain integers - the new unified API uses int chan_id directly.
|
||||
TOUCH_PADS = {
|
||||
VARIANT_ESP32: {
|
||||
4: cg.global_ns.TOUCH_PAD_NUM0,
|
||||
0: cg.global_ns.TOUCH_PAD_NUM1,
|
||||
2: cg.global_ns.TOUCH_PAD_NUM2,
|
||||
15: cg.global_ns.TOUCH_PAD_NUM3,
|
||||
13: cg.global_ns.TOUCH_PAD_NUM4,
|
||||
12: cg.global_ns.TOUCH_PAD_NUM5,
|
||||
14: cg.global_ns.TOUCH_PAD_NUM6,
|
||||
27: cg.global_ns.TOUCH_PAD_NUM7,
|
||||
33: cg.global_ns.TOUCH_PAD_NUM8,
|
||||
32: cg.global_ns.TOUCH_PAD_NUM9,
|
||||
4: 0,
|
||||
0: 1,
|
||||
2: 2,
|
||||
15: 3,
|
||||
13: 4,
|
||||
12: 5,
|
||||
14: 6,
|
||||
27: 7,
|
||||
33: 8,
|
||||
32: 9,
|
||||
},
|
||||
VARIANT_ESP32S2: {
|
||||
1: cg.global_ns.TOUCH_PAD_NUM1,
|
||||
2: cg.global_ns.TOUCH_PAD_NUM2,
|
||||
3: cg.global_ns.TOUCH_PAD_NUM3,
|
||||
4: cg.global_ns.TOUCH_PAD_NUM4,
|
||||
5: cg.global_ns.TOUCH_PAD_NUM5,
|
||||
6: cg.global_ns.TOUCH_PAD_NUM6,
|
||||
7: cg.global_ns.TOUCH_PAD_NUM7,
|
||||
8: cg.global_ns.TOUCH_PAD_NUM8,
|
||||
9: cg.global_ns.TOUCH_PAD_NUM9,
|
||||
10: cg.global_ns.TOUCH_PAD_NUM10,
|
||||
11: cg.global_ns.TOUCH_PAD_NUM11,
|
||||
12: cg.global_ns.TOUCH_PAD_NUM12,
|
||||
13: cg.global_ns.TOUCH_PAD_NUM13,
|
||||
14: cg.global_ns.TOUCH_PAD_NUM14,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 4,
|
||||
5: 5,
|
||||
6: 6,
|
||||
7: 7,
|
||||
8: 8,
|
||||
9: 9,
|
||||
10: 10,
|
||||
11: 11,
|
||||
12: 12,
|
||||
13: 13,
|
||||
14: 14,
|
||||
},
|
||||
VARIANT_ESP32S3: {
|
||||
1: cg.global_ns.TOUCH_PAD_NUM1,
|
||||
2: cg.global_ns.TOUCH_PAD_NUM2,
|
||||
3: cg.global_ns.TOUCH_PAD_NUM3,
|
||||
4: cg.global_ns.TOUCH_PAD_NUM4,
|
||||
5: cg.global_ns.TOUCH_PAD_NUM5,
|
||||
6: cg.global_ns.TOUCH_PAD_NUM6,
|
||||
7: cg.global_ns.TOUCH_PAD_NUM7,
|
||||
8: cg.global_ns.TOUCH_PAD_NUM8,
|
||||
9: cg.global_ns.TOUCH_PAD_NUM9,
|
||||
10: cg.global_ns.TOUCH_PAD_NUM10,
|
||||
11: cg.global_ns.TOUCH_PAD_NUM11,
|
||||
12: cg.global_ns.TOUCH_PAD_NUM12,
|
||||
13: cg.global_ns.TOUCH_PAD_NUM13,
|
||||
14: cg.global_ns.TOUCH_PAD_NUM14,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 4,
|
||||
5: 5,
|
||||
6: 6,
|
||||
7: 7,
|
||||
8: 8,
|
||||
9: 9,
|
||||
10: 10,
|
||||
11: 11,
|
||||
12: 12,
|
||||
13: 13,
|
||||
14: 14,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
2: 1,
|
||||
3: 2,
|
||||
4: 3,
|
||||
5: 4,
|
||||
6: 5,
|
||||
7: 6,
|
||||
8: 7,
|
||||
9: 8,
|
||||
10: 9,
|
||||
11: 10,
|
||||
12: 11,
|
||||
13: 12,
|
||||
14: 13,
|
||||
15: 14,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
TOUCH_PAD_DENOISE_GRADE = {
|
||||
"BIT12": cg.global_ns.TOUCH_PAD_DENOISE_BIT12,
|
||||
"BIT10": cg.global_ns.TOUCH_PAD_DENOISE_BIT10,
|
||||
"BIT8": cg.global_ns.TOUCH_PAD_DENOISE_BIT8,
|
||||
"BIT4": cg.global_ns.TOUCH_PAD_DENOISE_BIT4,
|
||||
"BIT12": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT12,
|
||||
"BIT10": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT10,
|
||||
"BIT8": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT8,
|
||||
"BIT4": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT4,
|
||||
}
|
||||
|
||||
TOUCH_PAD_DENOISE_CAP_LEVEL = {
|
||||
"L0": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L0,
|
||||
"L1": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L1,
|
||||
"L2": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L2,
|
||||
"L3": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L3,
|
||||
"L4": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L4,
|
||||
"L5": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L5,
|
||||
"L6": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L6,
|
||||
"L7": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L7,
|
||||
"L0": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_5PF,
|
||||
"L1": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_6PF,
|
||||
"L2": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_7PF,
|
||||
"L3": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_9PF,
|
||||
"L4": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_10PF,
|
||||
"L5": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_12PF,
|
||||
"L6": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_13PF,
|
||||
"L7": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_14PF,
|
||||
}
|
||||
|
||||
TOUCH_PAD_FILTER_MODE = {
|
||||
"IIR_4": cg.global_ns.TOUCH_PAD_FILTER_IIR_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_PAD_FILTER_IIR_8,
|
||||
"IIR_16": cg.global_ns.TOUCH_PAD_FILTER_IIR_16,
|
||||
"IIR_32": cg.global_ns.TOUCH_PAD_FILTER_IIR_32,
|
||||
"IIR_64": cg.global_ns.TOUCH_PAD_FILTER_IIR_64,
|
||||
"IIR_128": cg.global_ns.TOUCH_PAD_FILTER_IIR_128,
|
||||
"IIR_256": cg.global_ns.TOUCH_PAD_FILTER_IIR_256,
|
||||
"JITTER": cg.global_ns.TOUCH_PAD_FILTER_JITTER,
|
||||
"IIR_4": cg.global_ns.TOUCH_BM_IIR_FILTER_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_BM_IIR_FILTER_8,
|
||||
"IIR_16": cg.global_ns.TOUCH_BM_IIR_FILTER_16,
|
||||
"IIR_32": cg.global_ns.TOUCH_BM_IIR_FILTER_32,
|
||||
"IIR_64": cg.global_ns.TOUCH_BM_IIR_FILTER_64,
|
||||
"IIR_128": cg.global_ns.TOUCH_BM_IIR_FILTER_128,
|
||||
"IIR_256": cg.global_ns.TOUCH_BM_IIR_FILTER_256,
|
||||
"JITTER": cg.global_ns.TOUCH_BM_JITTER_FILTER,
|
||||
}
|
||||
|
||||
TOUCH_PAD_SMOOTH_MODE = {
|
||||
"OFF": cg.global_ns.TOUCH_PAD_SMOOTH_OFF,
|
||||
"IIR_2": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_2,
|
||||
"IIR_4": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_8,
|
||||
"OFF": cg.global_ns.TOUCH_SMOOTH_NO_FILTER,
|
||||
"IIR_2": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_2,
|
||||
"IIR_4": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_4,
|
||||
"IIR_8": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_8,
|
||||
}
|
||||
|
||||
LOW_VOLTAGE_REFERENCE = {
|
||||
"0.5V": cg.global_ns.TOUCH_LVOLT_0V5,
|
||||
"0.6V": cg.global_ns.TOUCH_LVOLT_0V6,
|
||||
"0.7V": cg.global_ns.TOUCH_LVOLT_0V7,
|
||||
"0.8V": cg.global_ns.TOUCH_LVOLT_0V8,
|
||||
"0.5V": cg.global_ns.TOUCH_VOLT_LIM_L_0V5,
|
||||
"0.6V": cg.global_ns.TOUCH_VOLT_LIM_L_0V6,
|
||||
"0.7V": cg.global_ns.TOUCH_VOLT_LIM_L_0V7,
|
||||
"0.8V": cg.global_ns.TOUCH_VOLT_LIM_L_0V8,
|
||||
}
|
||||
HIGH_VOLTAGE_REFERENCE = {
|
||||
"2.4V": cg.global_ns.TOUCH_HVOLT_2V4,
|
||||
"2.5V": cg.global_ns.TOUCH_HVOLT_2V5,
|
||||
"2.6V": cg.global_ns.TOUCH_HVOLT_2V6,
|
||||
"2.7V": cg.global_ns.TOUCH_HVOLT_2V7,
|
||||
"2.4V": cg.global_ns.TOUCH_VOLT_LIM_H_2V4,
|
||||
"2.5V": cg.global_ns.TOUCH_VOLT_LIM_H_2V5,
|
||||
"2.6V": cg.global_ns.TOUCH_VOLT_LIM_H_2V6,
|
||||
"2.7V": cg.global_ns.TOUCH_VOLT_LIM_H_2V7,
|
||||
}
|
||||
VOLTAGE_ATTENUATION = {
|
||||
"1.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V5,
|
||||
"1V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V,
|
||||
"0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5,
|
||||
"0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V,
|
||||
}
|
||||
TOUCH_PAD_WATERPROOF_SHIELD_DRIVER = {
|
||||
"L0": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L0,
|
||||
"L1": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L1,
|
||||
"L2": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L2,
|
||||
"L3": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L3,
|
||||
"L4": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L4,
|
||||
"L5": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L5,
|
||||
"L6": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L6,
|
||||
"L7": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L7,
|
||||
VOLTAGE_ATTENUATION = {"1.5V", "1V", "0.5V", "0V"}
|
||||
|
||||
# ESP32 V1: The new API's touch_volt_lim_h_t combines the old high_voltage_reference
|
||||
# and voltage_attenuation into a single enum representing the effective upper voltage.
|
||||
# Effective voltage = high_voltage_reference - voltage_attenuation
|
||||
EFFECTIVE_HIGH_VOLTAGE = {
|
||||
("2.4V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_0V9,
|
||||
("2.5V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V0,
|
||||
("2.6V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V1,
|
||||
("2.7V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V2,
|
||||
("2.4V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V4,
|
||||
("2.5V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V5,
|
||||
("2.6V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V6,
|
||||
("2.7V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V7,
|
||||
("2.4V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V9,
|
||||
("2.5V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V0,
|
||||
("2.6V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V1,
|
||||
("2.7V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V2,
|
||||
("2.4V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V4,
|
||||
("2.5V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V5,
|
||||
("2.6V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V6,
|
||||
("2.7V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V7,
|
||||
}
|
||||
|
||||
|
||||
def validate_touch_pad(value):
|
||||
value = gpio.gpio_pin_number_validator(value)
|
||||
variant = get_esp32_variant()
|
||||
if variant not in TOUCH_PADS:
|
||||
pads = TOUCH_PADS.get(variant)
|
||||
if pads is None:
|
||||
raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.")
|
||||
|
||||
pads = TOUCH_PADS[variant]
|
||||
if value not in pads:
|
||||
raise cv.Invalid(f"Pin {value} does not support touch pads.")
|
||||
return cv.enum(pads)(value)
|
||||
return pads[value] # Return integer channel ID
|
||||
|
||||
|
||||
def validate_variant_vars(config):
|
||||
if get_esp32_variant() == VARIANT_ESP32:
|
||||
variant_vars = {
|
||||
variant = get_esp32_variant()
|
||||
invalid_vars = set()
|
||||
if variant == VARIANT_ESP32:
|
||||
invalid_vars = {
|
||||
CONF_DEBOUNCE_COUNT,
|
||||
CONF_DENOISE_GRADE,
|
||||
CONF_DENOISE_CAP_LEVEL,
|
||||
@@ -176,15 +207,14 @@ def validate_variant_vars(config):
|
||||
CONF_WATERPROOF_GUARD_RING,
|
||||
CONF_WATERPROOF_SHIELD_DRIVER,
|
||||
}
|
||||
for vvar in variant_vars:
|
||||
if vvar in config:
|
||||
raise cv.Invalid(f"{vvar} is not valid on {VARIANT_ESP32}")
|
||||
elif (
|
||||
get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3
|
||||
) and CONF_IIR_FILTER in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_IIR_FILTER} is not valid on {VARIANT_ESP32S2} or {VARIANT_ESP32S3}"
|
||||
)
|
||||
elif variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4):
|
||||
invalid_vars = {CONF_IIR_FILTER}
|
||||
if variant == VARIANT_ESP32P4:
|
||||
invalid_vars |= {CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL}
|
||||
unsupported = invalid_vars.intersection(config)
|
||||
if unsupported:
|
||||
keys = ", ".join(sorted(f"'{k}'" for k in unsupported))
|
||||
raise cv.Invalid(f"{keys} not valid on {variant}")
|
||||
|
||||
return config
|
||||
|
||||
@@ -219,12 +249,17 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage(
|
||||
HIGH_VOLTAGE_REFERENCE
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage(
|
||||
VOLTAGE_ATTENUATION
|
||||
),
|
||||
# ESP32 V1 only: attenuates the high voltage reference
|
||||
cv.SplitDefault(
|
||||
CONF_VOLTAGE_ATTENUATION,
|
||||
esp32="0V",
|
||||
esp32_s2=cv.UNDEFINED,
|
||||
esp32_s3=cv.UNDEFINED,
|
||||
esp32_p4=cv.UNDEFINED,
|
||||
): validate_voltage(VOLTAGE_ATTENUATION),
|
||||
# ESP32 only
|
||||
cv.Optional(CONF_IIR_FILTER): cv.positive_time_period_milliseconds,
|
||||
# ESP32-S2/S3 only
|
||||
# ESP32-S2/S3/P4 only
|
||||
cv.Optional(CONF_DEBOUNCE_COUNT): cv.int_range(min=0, max=7),
|
||||
cv.Optional(CONF_FILTER_MODE): cv.enum(
|
||||
TOUCH_PAD_FILTER_MODE, upper=True, space="_"
|
||||
@@ -241,9 +276,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
TOUCH_PAD_DENOISE_CAP_LEVEL, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_WATERPROOF_GUARD_RING): validate_touch_pad,
|
||||
cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.enum(
|
||||
TOUCH_PAD_WATERPROOF_SHIELD_DRIVER, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.int_range(min=0, max=7),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL),
|
||||
@@ -260,6 +293,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp32.VARIANT_ESP32,
|
||||
esp32.VARIANT_ESP32S2,
|
||||
esp32.VARIANT_ESP32S3,
|
||||
esp32.VARIANT_ESP32P4,
|
||||
]
|
||||
),
|
||||
validate_variant_vars,
|
||||
@@ -267,44 +301,67 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
|
||||
# New unified touch sensor driver
|
||||
include_builtin_idf_component("esp_driver_touch_sens")
|
||||
# Legacy driver component provides driver/touch_sensor.h header
|
||||
include_builtin_idf_component("driver")
|
||||
|
||||
touch = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(touch, config)
|
||||
|
||||
cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE]))
|
||||
|
||||
sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15))
|
||||
cg.add(touch.set_sleep_duration(sleep_duration))
|
||||
# sleep_duration -> meas_interval_us (pass microseconds directly)
|
||||
cg.add(touch.set_meas_interval_us(config[CONF_SLEEP_DURATION].total_microseconds))
|
||||
|
||||
measurement_duration = int(
|
||||
round(config[CONF_MEASUREMENT_DURATION].total_microseconds * 7.99987793)
|
||||
)
|
||||
cg.add(touch.set_measurement_duration(measurement_duration))
|
||||
variant = get_esp32_variant()
|
||||
|
||||
cg.add(
|
||||
touch.set_low_voltage_reference(
|
||||
LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]]
|
||||
# measurement_duration handling differs per variant
|
||||
if variant == VARIANT_ESP32:
|
||||
# V1: charge_duration_ms (convert from microseconds to milliseconds)
|
||||
charge_duration_ms = (
|
||||
config[CONF_MEASUREMENT_DURATION].total_microseconds / 1000.0
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
touch.set_high_voltage_reference(
|
||||
HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]]
|
||||
cg.add(touch.set_charge_duration_ms(charge_duration_ms))
|
||||
else:
|
||||
# V2/V3: charge_times (approximate conversion from duration)
|
||||
# The old API used clock cycles; the new API uses charge_times count.
|
||||
# Default is 500 for V2/V3. Use measurement_duration as a rough scaling factor.
|
||||
# 65535 / 8192 ≈ 7.9999 maps the microsecond duration to charge_times.
|
||||
charge_times = int(
|
||||
round(config[CONF_MEASUREMENT_DURATION].total_microseconds * (65535 / 8192))
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
touch.set_voltage_attenuation(
|
||||
VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]]
|
||||
)
|
||||
)
|
||||
charge_times = max(charge_times, 1)
|
||||
cg.add(touch.set_charge_times(charge_times))
|
||||
|
||||
if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config:
|
||||
# Voltage references (not applicable to P4)
|
||||
if variant != VARIANT_ESP32P4:
|
||||
if CONF_LOW_VOLTAGE_REFERENCE in config:
|
||||
cg.add(
|
||||
touch.set_low_voltage_reference(
|
||||
LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]]
|
||||
)
|
||||
)
|
||||
if CONF_HIGH_VOLTAGE_REFERENCE in config:
|
||||
if variant == VARIANT_ESP32:
|
||||
# V1: combine high_voltage_reference with voltage_attenuation
|
||||
high_ref = config[CONF_HIGH_VOLTAGE_REFERENCE]
|
||||
atten = config[CONF_VOLTAGE_ATTENUATION]
|
||||
cg.add(
|
||||
touch.set_high_voltage_reference(
|
||||
EFFECTIVE_HIGH_VOLTAGE[(high_ref, atten)]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# V2/V3: no attenuation concept, use directly
|
||||
cg.add(
|
||||
touch.set_high_voltage_reference(
|
||||
HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]]
|
||||
)
|
||||
)
|
||||
|
||||
if variant == VARIANT_ESP32 and CONF_IIR_FILTER in config:
|
||||
cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
|
||||
if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3:
|
||||
if variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4):
|
||||
if CONF_FILTER_MODE in config:
|
||||
cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE]))
|
||||
if CONF_DEBOUNCE_COUNT in config:
|
||||
|
||||
500
esphome/components/esp32_touch/esp32_touch.cpp
Normal file
500
esphome/components/esp32_touch/esp32_touch.cpp
Normal file
@@ -0,0 +1,500 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::esp32_touch {
|
||||
|
||||
template<size_t N> static const char *lookup_str(const char *const (&table)[N], size_t index) {
|
||||
return (index < N) ? table[index] : "UNKNOWN";
|
||||
}
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
static constexpr uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250;
|
||||
static constexpr uint32_t INITIAL_STATE_DELAY_MS = 1500;
|
||||
static constexpr uint32_t ONESHOT_SCAN_COUNT = 3;
|
||||
static constexpr uint32_t ONESHOT_SCAN_TIMEOUT_MS = 2000;
|
||||
|
||||
// V1: called from esp_timer context (software filter)
|
||||
// V2/V3: called from ISR context
|
||||
// xQueueSendFromISR is safe from both contexts.
|
||||
|
||||
bool IRAM_ATTR ESP32TouchComponent::on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event,
|
||||
void *ctx) {
|
||||
auto *comp = static_cast<ESP32TouchComponent *>(ctx);
|
||||
TouchEvent te{event->chan_id, true};
|
||||
BaseType_t higher = pdFALSE;
|
||||
xQueueSendFromISR(comp->touch_queue_, &te, &higher);
|
||||
comp->enable_loop_soon_any_context();
|
||||
return higher == pdTRUE;
|
||||
}
|
||||
|
||||
bool IRAM_ATTR ESP32TouchComponent::on_inactive_cb(touch_sensor_handle_t handle,
|
||||
const touch_inactive_event_data_t *event, void *ctx) {
|
||||
auto *comp = static_cast<ESP32TouchComponent *>(ctx);
|
||||
TouchEvent te{event->chan_id, false};
|
||||
BaseType_t higher = pdFALSE;
|
||||
xQueueSendFromISR(comp->touch_queue_, &te, &higher);
|
||||
comp->enable_loop_soon_any_context();
|
||||
return higher == pdTRUE;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create sample config - differs per hardware version
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V1_DEFAULT_SAMPLE_CONFIG(
|
||||
this->charge_duration_ms_, this->low_voltage_reference_, this->high_voltage_reference_);
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
// div_num=8 (data scaling divisor), coarse_freq_tune=2, fine_freq_tune=2
|
||||
touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG(8, 2, 2);
|
||||
sample_cfg.charge_times = this->charge_times_;
|
||||
#else
|
||||
// ESP32-S2/S3 (V2)
|
||||
touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V2_DEFAULT_SAMPLE_CONFIG(
|
||||
this->charge_times_, this->low_voltage_reference_, this->high_voltage_reference_);
|
||||
#endif
|
||||
|
||||
// Create controller
|
||||
touch_sensor_config_t sens_cfg = TOUCH_SENSOR_DEFAULT_BASIC_CONFIG(1, &sample_cfg);
|
||||
sens_cfg.meas_interval_us = this->meas_interval_us_;
|
||||
#ifndef USE_ESP32_VARIANT_ESP32
|
||||
sens_cfg.max_meas_time_us = 0; // Disable measurement timeout (V2/V3 only)
|
||||
#endif
|
||||
|
||||
esp_err_t err = touch_sensor_new_controller(&sens_cfg, &this->sens_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create touch controller: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create channels for all children
|
||||
for (auto *child : this->children_) {
|
||||
touch_channel_config_t chan_cfg = {};
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
chan_cfg.abs_active_thresh[0] = child->get_threshold();
|
||||
chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
chan_cfg.group = TOUCH_CHAN_TRIG_GROUP_BOTH;
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
chan_cfg.active_thresh[0] = child->get_threshold();
|
||||
#else
|
||||
// ESP32-S2/S3 (V2)
|
||||
chan_cfg.active_thresh[0] = child->get_threshold();
|
||||
chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
#endif
|
||||
|
||||
err = touch_sensor_new_channel(this->sens_handle_, child->get_channel_id(), &chan_cfg, &child->chan_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create touch channel %d: %s", child->get_channel_id(), esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Configure filter
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// Software filter is REQUIRED for V1 on_active/on_inactive callbacks
|
||||
{
|
||||
touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG();
|
||||
if (this->iir_filter_enabled_()) {
|
||||
filter_cfg.interval_ms = this->iir_filter_;
|
||||
}
|
||||
err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to configure filter: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// V2/V3: Hardware benchmark filter
|
||||
{
|
||||
touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG();
|
||||
if (this->filter_configured_) {
|
||||
filter_cfg.benchmark.filter_mode = this->filter_mode_;
|
||||
filter_cfg.benchmark.jitter_step = this->jitter_step_;
|
||||
filter_cfg.benchmark.denoise_lvl = this->noise_threshold_;
|
||||
filter_cfg.data.smooth_filter = this->smooth_level_;
|
||||
filter_cfg.data.debounce_cnt = this->debounce_count_;
|
||||
}
|
||||
err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure filter: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
if (this->denoise_configured_) {
|
||||
touch_denoise_chan_config_t denoise_cfg = {};
|
||||
denoise_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
denoise_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
denoise_cfg.ref_cap = this->denoise_cap_level_;
|
||||
denoise_cfg.resolution = this->denoise_grade_;
|
||||
err = touch_sensor_config_denoise_channel(this->sens_handle_, &denoise_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure denoise: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_WATERPROOF
|
||||
if (this->waterproof_configured_) {
|
||||
touch_channel_handle_t guard_chan = nullptr;
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_channel_id() == this->waterproof_guard_ring_pad_) {
|
||||
guard_chan = child->chan_handle_;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
touch_channel_handle_t shield_chan = nullptr;
|
||||
touch_channel_config_t shield_cfg = {};
|
||||
#ifdef USE_ESP32_VARIANT_ESP32P4
|
||||
shield_cfg.active_thresh[0] = 0;
|
||||
err = touch_sensor_new_channel(this->sens_handle_, SOC_TOUCH_MAX_CHAN_ID, &shield_cfg, &shield_chan);
|
||||
#else
|
||||
shield_cfg.active_thresh[0] = 0;
|
||||
shield_cfg.charge_speed = TOUCH_CHARGE_SPEED_7;
|
||||
shield_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT;
|
||||
err = touch_sensor_new_channel(this->sens_handle_, TOUCH_SHIELD_CHAN_ID, &shield_cfg, &shield_chan);
|
||||
#endif
|
||||
if (err == ESP_OK) {
|
||||
touch_waterproof_config_t wp_cfg = {};
|
||||
wp_cfg.guard_chan = guard_chan;
|
||||
wp_cfg.shield_chan = shield_chan;
|
||||
wp_cfg.shield_drv = this->waterproof_shield_driver_;
|
||||
wp_cfg.flags.immersion_proof = 1;
|
||||
err = touch_sensor_config_waterproof(this->sens_handle_, &wp_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure waterproof: %s", esp_err_to_name(err));
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to create shield channel: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Configure wakeup pads before enabling (must be done in INIT state)
|
||||
this->configure_wakeup_pads_();
|
||||
|
||||
// Register callbacks
|
||||
touch_event_callbacks_t cbs = {};
|
||||
cbs.on_active = on_active_cb;
|
||||
cbs.on_inactive = on_inactive_cb;
|
||||
err = touch_sensor_register_callbacks(this->sens_handle_, &cbs, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register callbacks: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable and start scanning
|
||||
err = touch_sensor_enable(this->sens_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to enable touch sensor: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Do initial oneshot scans to populate baseline values
|
||||
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
|
||||
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Oneshot scan %d failed: %s", i, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
err = touch_sensor_start_continuous_scanning(this->sens_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start continuous scanning: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
static constexpr const char *LV_STRS[] = {"0.5V", "0.6V", "0.7V", "0.8V"};
|
||||
static constexpr const char *HV_STRS[] = {"0.9V", "1.0V", "1.1V", "1.2V", "1.4V", "1.5V", "1.6V", "1.7V",
|
||||
"1.9V", "2.0V", "2.1V", "2.2V", "2.4V", "2.5V", "2.6V", "2.7V"};
|
||||
const char *lv_s = lookup_str(LV_STRS, this->low_voltage_reference_);
|
||||
const char *hv_s = lookup_str(HV_STRS, this->high_voltage_reference_);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Measurement interval: %.1fus\n"
|
||||
" Low Voltage Reference: %s\n"
|
||||
" High Voltage Reference: %s",
|
||||
this->meas_interval_us_, lv_s, hv_s);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Measurement interval: %.1fus",
|
||||
this->meas_interval_us_);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
if (this->iir_filter_enabled_()) {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: 10ms (default)");
|
||||
}
|
||||
#else
|
||||
if (this->filter_configured_) {
|
||||
// TOUCH_BM_IIR_FILTER_256 only exists on V2, shifting JITTER's position
|
||||
static constexpr const char *FILTER_STRS[] = {
|
||||
"IIR_4",
|
||||
"IIR_8",
|
||||
"IIR_16",
|
||||
"IIR_32",
|
||||
"IIR_64",
|
||||
"IIR_128",
|
||||
#if SOC_TOUCH_SENSOR_VERSION == 2
|
||||
"IIR_256",
|
||||
#endif
|
||||
"JITTER",
|
||||
};
|
||||
static constexpr const char *SMOOTH_STRS[] = {"OFF", "IIR_2", "IIR_4", "IIR_8"};
|
||||
const char *filter_s = lookup_str(FILTER_STRS, this->filter_mode_);
|
||||
const char *smooth_s = lookup_str(SMOOTH_STRS, this->smooth_level_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Filter mode: %s\n"
|
||||
" Debounce count: %" PRIu32 "\n"
|
||||
" Noise threshold coefficient: %" PRIu32 "\n"
|
||||
" Jitter filter step size: %" PRIu32 "\n"
|
||||
" Smooth level: %s",
|
||||
filter_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_, smooth_s);
|
||||
}
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
if (this->denoise_configured_) {
|
||||
static constexpr const char *GRADE_STRS[] = {"BIT12", "BIT10", "BIT8", "BIT4"};
|
||||
static constexpr const char *CAP_STRS[] = {"5pF", "6.4pF", "7.8pF", "9.2pF", "10.6pF", "12pF", "13.4pF", "14.8pF"};
|
||||
const char *grade_s = lookup_str(GRADE_STRS, this->denoise_grade_);
|
||||
const char *cap_s = lookup_str(CAP_STRS, this->denoise_cap_level_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Denoise grade: %s\n"
|
||||
" Denoise capacitance level: %s",
|
||||
grade_s, cap_s);
|
||||
}
|
||||
#endif
|
||||
#endif // !USE_ESP32_VARIANT_ESP32
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
LOG_BINARY_SENSOR(" ", "Touch Pad", child);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Channel: %d\n"
|
||||
" Threshold: %" PRIu32 "\n"
|
||||
" Benchmark: %" PRIu32,
|
||||
child->channel_id_, child->threshold_, child->benchmark_);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// In setup mode, periodically log all pad values
|
||||
this->process_setup_mode_logging_(now);
|
||||
|
||||
// Process queued touch events from callbacks
|
||||
TouchEvent event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_channel_id() != event.chan_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read current smooth value
|
||||
uint32_t value = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &value);
|
||||
child->value_ = value;
|
||||
|
||||
#ifndef USE_ESP32_VARIANT_ESP32
|
||||
// V2/V3: also read benchmark
|
||||
uint32_t benchmark = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark);
|
||||
child->benchmark_ = benchmark;
|
||||
#endif
|
||||
|
||||
bool new_state = event.is_active;
|
||||
|
||||
if (new_state != child->last_state_) {
|
||||
child->initial_state_published_ = true;
|
||||
child->last_state_ = new_state;
|
||||
child->publish_state(new_state);
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 ", threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), ONOFF(new_state), value, child->get_threshold());
|
||||
#else
|
||||
if (new_state) {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 ", benchmark: %" PRIu32 ", threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), value, benchmark, child->get_threshold());
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish initial OFF state for sensors that haven't received events yet
|
||||
for (auto *child : this->children_) {
|
||||
this->publish_initial_state_if_needed_(child, now);
|
||||
}
|
||||
|
||||
if (!this->setup_mode_) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
if (this->sens_handle_ == nullptr)
|
||||
return;
|
||||
|
||||
touch_sensor_stop_continuous_scanning(this->sens_handle_);
|
||||
touch_sensor_disable(this->sens_handle_);
|
||||
|
||||
for (auto *child : this->children_) {
|
||||
if (child->chan_handle_ != nullptr) {
|
||||
touch_sensor_del_channel(child->chan_handle_);
|
||||
child->chan_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
touch_sensor_del_controller(this->sens_handle_);
|
||||
this->sens_handle_ = nullptr;
|
||||
|
||||
this->cleanup_touch_queue_();
|
||||
}
|
||||
|
||||
bool ESP32TouchComponent::create_touch_queue_() {
|
||||
size_t queue_size = this->children_.size() * 4;
|
||||
if (queue_size < 8)
|
||||
queue_size = 8;
|
||||
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchEvent));
|
||||
|
||||
if (this->touch_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::cleanup_touch_queue_() {
|
||||
if (this->touch_queue_) {
|
||||
vQueueDelete(this->touch_queue_);
|
||||
this->touch_queue_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::configure_wakeup_pads_() {
|
||||
#if SOC_TOUCH_SUPPORT_SLEEP_WAKEUP
|
||||
bool has_wakeup = false;
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
has_wakeup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_wakeup)
|
||||
return;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// V1: Simple sleep config - threshold is set via channel config's abs_active_thresh
|
||||
touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG();
|
||||
sleep_cfg.deep_slp_sens_cfg = nullptr;
|
||||
esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err));
|
||||
}
|
||||
#else
|
||||
// V2/V3: Need to specify a deep sleep channel and threshold
|
||||
touch_channel_handle_t wakeup_chan = nullptr;
|
||||
uint32_t wakeup_thresh = 0;
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
wakeup_chan = child->chan_handle_;
|
||||
wakeup_thresh = child->get_wakeup_threshold();
|
||||
break; // Only one deep sleep wakeup channel is supported
|
||||
}
|
||||
}
|
||||
|
||||
if (wakeup_chan != nullptr) {
|
||||
touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG();
|
||||
sleep_cfg.deep_slp_chan = wakeup_chan;
|
||||
sleep_cfg.deep_slp_thresh[0] = wakeup_thresh;
|
||||
sleep_cfg.deep_slp_sens_cfg = nullptr;
|
||||
esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif // SOC_TOUCH_SUPPORT_SLEEP_WAKEUP
|
||||
}
|
||||
|
||||
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_) {
|
||||
if (child->chan_handle_ == nullptr)
|
||||
continue;
|
||||
|
||||
uint32_t smooth_value = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &smooth_value);
|
||||
child->value_ = smooth_value;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (Ch%d): %" PRIu32, child->get_name().c_str(), child->channel_id_, smooth_value);
|
||||
#else
|
||||
uint32_t benchmark = 0;
|
||||
touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark);
|
||||
child->benchmark_ = benchmark;
|
||||
int32_t difference = static_cast<int32_t>(smooth_value) - static_cast<int32_t>(benchmark);
|
||||
ESP_LOGD(TAG,
|
||||
"Touch Pad '%s' (Ch%d): value=%" PRIu32 ", benchmark=%" PRIu32 ", difference=%" PRId32
|
||||
" (set threshold < %" PRId32 " to detect touch)",
|
||||
child->get_name().c_str(), child->channel_id_, smooth_value, benchmark, difference, difference);
|
||||
#endif
|
||||
}
|
||||
this->setup_mode_last_log_print_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) {
|
||||
if (!child->initial_state_published_) {
|
||||
if (now > INITIAL_STATE_DELAY_MS) {
|
||||
child->publish_initial_state(false);
|
||||
child->initial_state_published_ = true;
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::esp32_touch
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -4,49 +4,49 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <driver/touch_sensor.h>
|
||||
#include <driver/touch_sens.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
namespace esphome::esp32_touch {
|
||||
|
||||
// IMPORTANT: Touch detection logic differs between ESP32 variants:
|
||||
// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease)
|
||||
// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase)
|
||||
// This inversion is due to different hardware implementations between chip generations.
|
||||
// - ESP32 v1 (original): Touch detected when value < threshold (absolute threshold, capacitance increase causes
|
||||
// value decrease)
|
||||
// - ESP32-S2/S3 v2, ESP32-P4 v3: Touch detected when (smooth - benchmark) > threshold (relative threshold)
|
||||
//
|
||||
// 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: 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;
|
||||
// CALLBACK BEHAVIOR:
|
||||
// - ESP32 v1: on_active/on_inactive fire from a software filter timer (esp_timer context).
|
||||
// The software filter MUST be configured for these callbacks to fire.
|
||||
// - ESP32-S2/S3 v2, ESP32-P4 v3: on_active/on_inactive fire from hardware ISR context.
|
||||
// Release detection via on_inactive is used, with timeout as safety fallback.
|
||||
|
||||
class ESP32TouchBinarySensor;
|
||||
|
||||
class ESP32TouchComponent : public Component {
|
||||
class ESP32TouchComponent final : public Component {
|
||||
public:
|
||||
void register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); }
|
||||
|
||||
void set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; }
|
||||
void set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; }
|
||||
void set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; }
|
||||
void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) {
|
||||
void set_meas_interval_us(float meas_interval_us) { this->meas_interval_us_ = meas_interval_us; }
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
void set_charge_duration_ms(float charge_duration_ms) { this->charge_duration_ms_ = charge_duration_ms; }
|
||||
#else
|
||||
void set_charge_times(uint32_t charge_times) { this->charge_times_ = charge_times; }
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
void set_low_voltage_reference(touch_volt_lim_l_t low_voltage_reference) {
|
||||
this->low_voltage_reference_ = low_voltage_reference;
|
||||
}
|
||||
void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) {
|
||||
void set_high_voltage_reference(touch_volt_lim_h_t high_voltage_reference) {
|
||||
this->high_voltage_reference_ = high_voltage_reference;
|
||||
}
|
||||
void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) {
|
||||
this->voltage_attenuation_ = voltage_attenuation;
|
||||
}
|
||||
#endif
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -54,183 +54,130 @@ class ESP32TouchComponent : public Component {
|
||||
|
||||
void on_shutdown() override;
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; }
|
||||
void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; }
|
||||
void set_noise_threshold(uint32_t noise_threshold) { this->noise_threshold_ = noise_threshold; }
|
||||
void set_jitter_step(uint32_t jitter_step) { this->jitter_step_ = jitter_step; }
|
||||
void set_smooth_level(touch_smooth_mode_t smooth_level) { this->smooth_level_ = smooth_level; }
|
||||
void set_denoise_grade(touch_pad_denoise_grade_t denoise_grade) { this->grade_ = denoise_grade; }
|
||||
void set_denoise_cap(touch_pad_denoise_cap_t cap_level) { this->cap_level_ = cap_level; }
|
||||
void set_waterproof_guard_ring_pad(touch_pad_t pad) { this->waterproof_guard_ring_pad_ = pad; }
|
||||
void set_waterproof_shield_driver(touch_pad_shield_driver_t drive_capability) {
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
void set_filter_mode(touch_benchmark_filter_mode_t filter_mode) {
|
||||
this->filter_mode_ = filter_mode;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_debounce_count(uint32_t debounce_count) {
|
||||
this->debounce_count_ = debounce_count;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_noise_threshold(uint32_t noise_threshold) {
|
||||
this->noise_threshold_ = noise_threshold;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_jitter_step(uint32_t jitter_step) {
|
||||
this->jitter_step_ = jitter_step;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
void set_smooth_level(touch_smooth_filter_mode_t smooth_level) {
|
||||
this->smooth_level_ = smooth_level;
|
||||
this->filter_configured_ = true;
|
||||
}
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
void set_denoise_grade(touch_denoise_chan_resolution_t denoise_grade) {
|
||||
this->denoise_grade_ = denoise_grade;
|
||||
this->denoise_configured_ = true;
|
||||
}
|
||||
void set_denoise_cap(touch_denoise_chan_cap_t cap_level) {
|
||||
this->denoise_cap_level_ = cap_level;
|
||||
this->denoise_configured_ = true;
|
||||
}
|
||||
#endif
|
||||
void set_waterproof_guard_ring_pad(int channel_id) {
|
||||
this->waterproof_guard_ring_pad_ = channel_id;
|
||||
this->waterproof_configured_ = true;
|
||||
}
|
||||
void set_waterproof_shield_driver(uint32_t drive_capability) {
|
||||
this->waterproof_shield_driver_ = drive_capability;
|
||||
this->waterproof_configured_ = true;
|
||||
}
|
||||
#else
|
||||
void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Unified touch event for queue communication
|
||||
struct TouchEvent {
|
||||
int chan_id;
|
||||
bool is_active;
|
||||
};
|
||||
|
||||
// Common helper methods
|
||||
void dump_config_base_();
|
||||
void dump_config_sensors_();
|
||||
bool create_touch_queue_();
|
||||
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_();
|
||||
|
||||
// Unified callbacks for new API
|
||||
static bool on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, void *ctx);
|
||||
static bool on_inactive_cb(touch_sensor_handle_t handle, const touch_inactive_event_data_t *event, void *ctx);
|
||||
|
||||
// Common members
|
||||
std::vector<ESP32TouchBinarySensor *> 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};
|
||||
|
||||
// Controller handle (new API)
|
||||
touch_sensor_handle_t sens_handle_{nullptr};
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
|
||||
// Common configuration parameters
|
||||
uint16_t sleep_cycle_{4095};
|
||||
uint16_t meas_cycle_{65535};
|
||||
touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5};
|
||||
touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7};
|
||||
touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V};
|
||||
float meas_interval_us_{320.0f};
|
||||
|
||||
// Common constants
|
||||
static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100;
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
float charge_duration_ms_{1.0f};
|
||||
#else
|
||||
uint32_t charge_times_{500};
|
||||
#endif
|
||||
|
||||
// ==================== PLATFORM SPECIFIC ====================
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
touch_volt_lim_l_t low_voltage_reference_{TOUCH_VOLT_LIM_L_0V5};
|
||||
touch_volt_lim_h_t high_voltage_reference_{TOUCH_VOLT_LIM_H_2V7};
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// ESP32 v1 specific
|
||||
|
||||
static void touch_isr_handler(void *arg);
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
|
||||
private:
|
||||
// Touch event structure for ESP32 v1
|
||||
// Contains touch pad info, value, and touch state for queue communication
|
||||
struct TouchPadEventV1 {
|
||||
touch_pad_t pad;
|
||||
uint32_t value;
|
||||
bool is_touched;
|
||||
};
|
||||
|
||||
protected:
|
||||
uint32_t iir_filter_{0};
|
||||
|
||||
bool iir_filter_enabled_() const { return this->iir_filter_ > 0; }
|
||||
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// ESP32-S2/S3 v2 specific
|
||||
static void touch_isr_handler(void *arg);
|
||||
QueueHandle_t touch_queue_{nullptr};
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
// ESP32-S2/S3/P4 v2/v3 specific
|
||||
|
||||
private:
|
||||
// Touch event structure for ESP32 v2 (S2/S3)
|
||||
// Contains touch pad and interrupt mask for queue communication
|
||||
struct TouchPadEventV2 {
|
||||
touch_pad_t pad;
|
||||
uint32_t intr_mask;
|
||||
};
|
||||
|
||||
protected:
|
||||
// Filter configuration
|
||||
touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX};
|
||||
// Filter configuration - use sentinel values to detect "not configured"
|
||||
touch_benchmark_filter_mode_t filter_mode_{TOUCH_BM_JITTER_FILTER};
|
||||
uint32_t debounce_count_{0};
|
||||
uint32_t noise_threshold_{0};
|
||||
uint32_t jitter_step_{0};
|
||||
touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX};
|
||||
touch_smooth_filter_mode_t smooth_level_{TOUCH_SMOOTH_NO_FILTER};
|
||||
bool filter_configured_{false};
|
||||
|
||||
#if SOC_TOUCH_SUPPORT_DENOISE_CHAN
|
||||
// Denoise configuration
|
||||
touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX};
|
||||
touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX};
|
||||
|
||||
// Waterproof configuration
|
||||
touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX};
|
||||
touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX};
|
||||
|
||||
bool filter_configured_() const {
|
||||
return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX);
|
||||
}
|
||||
bool denoise_configured_() const {
|
||||
return (this->grade_ != TOUCH_PAD_DENOISE_MAX) && (this->cap_level_ != TOUCH_PAD_DENOISE_CAP_MAX);
|
||||
}
|
||||
bool waterproof_configured_() const {
|
||||
return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) &&
|
||||
(this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX);
|
||||
}
|
||||
|
||||
// Helper method to read touch values - non-blocking operation
|
||||
// Returns the current touch pad value using either filtered or raw reading
|
||||
// based on the filter configuration
|
||||
uint32_t read_touch_value(touch_pad_t pad) const;
|
||||
|
||||
// Helper to update touch state with a known state and value
|
||||
void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value);
|
||||
|
||||
// Helper to read touch value and update state for a given child
|
||||
bool check_and_update_touch_state_(ESP32TouchBinarySensor *child);
|
||||
touch_denoise_chan_resolution_t denoise_grade_{TOUCH_DENOISE_CHAN_RESOLUTION_BIT12};
|
||||
touch_denoise_chan_cap_t denoise_cap_level_{TOUCH_DENOISE_CHAN_CAP_5PF};
|
||||
bool denoise_configured_{false};
|
||||
#endif
|
||||
|
||||
// Helper functions for dump_config - common to both implementations
|
||||
static const char *get_low_voltage_reference_str(touch_low_volt_t ref) {
|
||||
switch (ref) {
|
||||
case TOUCH_LVOLT_0V5:
|
||||
return "0.5V";
|
||||
case TOUCH_LVOLT_0V6:
|
||||
return "0.6V";
|
||||
case TOUCH_LVOLT_0V7:
|
||||
return "0.7V";
|
||||
case TOUCH_LVOLT_0V8:
|
||||
return "0.8V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_high_voltage_reference_str(touch_high_volt_t ref) {
|
||||
switch (ref) {
|
||||
case TOUCH_HVOLT_2V4:
|
||||
return "2.4V";
|
||||
case TOUCH_HVOLT_2V5:
|
||||
return "2.5V";
|
||||
case TOUCH_HVOLT_2V6:
|
||||
return "2.6V";
|
||||
case TOUCH_HVOLT_2V7:
|
||||
return "2.7V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) {
|
||||
switch (atten) {
|
||||
case TOUCH_HVOLT_ATTEN_1V5:
|
||||
return "1.5V";
|
||||
case TOUCH_HVOLT_ATTEN_1V:
|
||||
return "1V";
|
||||
case TOUCH_HVOLT_ATTEN_0V5:
|
||||
return "0.5V";
|
||||
case TOUCH_HVOLT_ATTEN_0V:
|
||||
return "0V";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
// Waterproof configuration
|
||||
int waterproof_guard_ring_pad_{-1};
|
||||
uint32_t waterproof_shield_driver_{0};
|
||||
bool waterproof_configured_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Simple helper class to expose a touch pad value as a binary sensor.
|
||||
class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
public:
|
||||
ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold)
|
||||
: touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {}
|
||||
ESP32TouchBinarySensor(int channel_id, uint32_t threshold, uint32_t wakeup_threshold)
|
||||
: channel_id_(channel_id), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {}
|
||||
|
||||
touch_pad_t get_touch_pad() const { return this->touch_pad_; }
|
||||
int get_channel_id() const { return this->channel_id_; }
|
||||
uint32_t get_threshold() const { return this->threshold_; }
|
||||
void set_threshold(uint32_t threshold) { this->threshold_ = threshold; }
|
||||
|
||||
@@ -242,39 +189,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
|
||||
uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; }
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
/// Ensure benchmark value is read (v2 touch hardware only).
|
||||
/// Called from multiple places - kept as helper to document shared usage.
|
||||
void ensure_benchmark_read() {
|
||||
if (this->benchmark_ == 0) {
|
||||
touch_pad_read_benchmark(this->touch_pad_, &this->benchmark_);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
friend ESP32TouchComponent;
|
||||
|
||||
touch_pad_t touch_pad_{TOUCH_PAD_MAX};
|
||||
int channel_id_;
|
||||
touch_channel_handle_t chan_handle_{nullptr};
|
||||
uint32_t threshold_{0};
|
||||
uint32_t benchmark_{};
|
||||
uint32_t benchmark_{0};
|
||||
/// Stores the last raw touch measurement value.
|
||||
uint32_t value_{0};
|
||||
bool last_state_{false};
|
||||
const uint32_t wakeup_threshold_{0};
|
||||
|
||||
// Track last touch time for timeout-based release detection
|
||||
// Design note: last_touch_time_ does not require synchronization primitives because:
|
||||
// 1. ESP32 guarantees atomic 32-bit aligned reads/writes
|
||||
// 2. ISR only writes timestamps, main loop only reads
|
||||
// 3. Timing tolerance allows for occasional stale reads (50ms check interval)
|
||||
// 4. Queue operations provide implicit memory barriers
|
||||
// Using atomic/critical sections would add overhead without meaningful benefit
|
||||
uint32_t last_touch_time_{};
|
||||
bool initial_state_published_{};
|
||||
bool initial_state_published_{false};
|
||||
};
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_touch
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
#include "soc/rtc.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
void ESP32TouchComponent::dump_config_base_() {
|
||||
const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_);
|
||||
const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_);
|
||||
const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Config for ESP32 Touch Hub:\n"
|
||||
" Meas cycle: %.2fms\n"
|
||||
" Sleep cycle: %.2fms\n"
|
||||
" Low Voltage Reference: %s\n"
|
||||
" High Voltage Reference: %s\n"
|
||||
" Voltage Attenuation: %s\n"
|
||||
" Release Timeout: %" PRIu32 "ms\n",
|
||||
this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s,
|
||||
atten_s, this->release_timeout_ms_);
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config_sensors_() {
|
||||
for (auto *child : this->children_) {
|
||||
LOG_BINARY_SENSOR(" ", "Touch Pad", child);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Pad: T%u\n"
|
||||
" Threshold: %" PRIu32 "\n"
|
||||
" Benchmark: %" PRIu32,
|
||||
(unsigned) child->touch_pad_, child->threshold_, child->benchmark_);
|
||||
}
|
||||
}
|
||||
|
||||
bool ESP32TouchComponent::create_touch_queue_() {
|
||||
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
||||
// fires multiple times before main loop processes.
|
||||
size_t queue_size = this->children_.size() * 4;
|
||||
if (queue_size < 8)
|
||||
queue_size = 8;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1));
|
||||
#else
|
||||
this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2));
|
||||
#endif
|
||||
|
||||
if (this->touch_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::cleanup_touch_queue_() {
|
||||
if (this->touch_queue_) {
|
||||
vQueueDelete(this->touch_queue_);
|
||||
this->touch_queue_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::configure_wakeup_pads_() {
|
||||
bool is_wakeup_source = false;
|
||||
|
||||
// Check if any pad is configured for wakeup
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_wakeup_threshold() != 0) {
|
||||
is_wakeup_source = true;
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// ESP32 v1: No filter available when using as wake-up source.
|
||||
touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold());
|
||||
#else
|
||||
// ESP32-S2/S3 v2: Set threshold for wakeup
|
||||
touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_wakeup_source) {
|
||||
// If no pad is configured for wakeup, deinitialize touch pad
|
||||
touch_pad_deinit();
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
// Store the value for get_value() access in lambdas
|
||||
child->value_ = value;
|
||||
// Read benchmark if not already read
|
||||
child->ensure_benchmark_read();
|
||||
// Calculate difference to help user set threshold
|
||||
// For ESP32-S2/S3 v2: touch detected when value > benchmark + threshold
|
||||
// So threshold should be < (value - benchmark) when touched
|
||||
int32_t difference = static_cast<int32_t>(value) - static_cast<int32_t>(child->benchmark_);
|
||||
ESP_LOGD(TAG,
|
||||
"Touch Pad '%s' (T%d): value=%d, benchmark=%" PRIu32 ", difference=%" PRId32 " (set threshold < %" PRId32
|
||||
" to detect touch)",
|
||||
child->get_name().c_str(), child->get_touch_pad(), value, child->benchmark_, difference, difference);
|
||||
#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) {
|
||||
if (!child->initial_state_published_) {
|
||||
// Check if enough time has passed since startup
|
||||
if (now > this->release_timeout_ms_) {
|
||||
child->publish_initial_state(false);
|
||||
child->initial_state_published_ = 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
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,244 +0,0 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
// Include HAL for ISR-safe touch reading
|
||||
#include "hal/touch_sensor_ll.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
// Create queue for touch events
|
||||
// Queue size calculation: children * 4 allows for burst scenarios where ISR
|
||||
// fires multiple times before main loop processes. This is important because
|
||||
// ESP32 v1 scans all pads on each interrupt, potentially sending multiple events.
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
touch_pad_init();
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
|
||||
// Set up IIR filter if enabled
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_start(this->iir_filter_);
|
||||
}
|
||||
|
||||
// Configure measurement parameters
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
touch_pad_set_measurement_clock_cycles(this->meas_cycle_);
|
||||
touch_pad_set_measurement_interval(this->sleep_cycle_);
|
||||
#else
|
||||
touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_);
|
||||
#endif
|
||||
touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
|
||||
|
||||
// Configure each touch pad
|
||||
for (auto *child : this->children_) {
|
||||
if (this->setup_mode_) {
|
||||
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
|
||||
} else {
|
||||
touch_pad_config(child->get_touch_pad(), child->get_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
// Register ISR handler
|
||||
esp_err_t err = touch_pad_isr_register(touch_isr_handler, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate release timeout based on sleep cycle
|
||||
this->calculate_release_timeout_();
|
||||
|
||||
// Enable touch pad interrupt
|
||||
touch_pad_intr_enable();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
this->dump_config_base_();
|
||||
|
||||
if (this->iir_filter_enabled_()) {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter DISABLED");
|
||||
}
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
this->dump_config_sensors_();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Print debug info for all pads in setup mode
|
||||
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)
|
||||
// This is more efficient than sending all pad states every interrupt
|
||||
TouchPadEventV1 event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
// Find the corresponding sensor - O(n) search is acceptable since events are infrequent
|
||||
for (auto *child : this->children_) {
|
||||
if (child->get_touch_pad() != event.pad) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found matching pad - process it
|
||||
child->value_ = event.value;
|
||||
|
||||
// The interrupt gives us the touch state directly
|
||||
bool new_state = event.is_touched;
|
||||
|
||||
// Track when we last saw this pad as touched
|
||||
if (new_state) {
|
||||
child->last_touch_time_ = now;
|
||||
}
|
||||
|
||||
// Only publish if state changed - this filters out repeated events
|
||||
if (new_state != child->last_state_) {
|
||||
child->initial_state_published_ = true;
|
||||
child->last_state_ = new_state;
|
||||
child->publish_state(new_state);
|
||||
// Original ESP32: ISR only fires when touched, release is detected by timeout
|
||||
// Note: ESP32 v1 uses inverted logic - touched when value < threshold
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
|
||||
child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
|
||||
}
|
||||
break; // Exit inner loop after processing matching pad
|
||||
}
|
||||
}
|
||||
|
||||
// Check for released pads periodically
|
||||
if (!this->should_check_for_releases_(now)) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t pads_off = 0;
|
||||
for (auto *child : this->children_) {
|
||||
// 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 - child->last_touch_time_;
|
||||
|
||||
// Check if we haven't seen this pad recently
|
||||
if (time_diff > this->release_timeout_ms_) {
|
||||
// Haven't seen this pad recently, assume it's released
|
||||
child->last_state_ = false;
|
||||
child->publish_state(false);
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str());
|
||||
pads_off++;
|
||||
}
|
||||
} else {
|
||||
// Pad is already off
|
||||
pads_off++;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable the loop to save CPU cycles when all pads are off and not in setup mode.
|
||||
// The loop will be re-enabled by the ISR when any touch pad is touched.
|
||||
// v1 hardware limitations require us to check all pads are off because:
|
||||
// - 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
|
||||
this->check_and_disable_loop_if_all_released_(pads_off);
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
touch_pad_intr_disable();
|
||||
touch_pad_isr_deregister(touch_isr_handler, this);
|
||||
this->cleanup_touch_queue_();
|
||||
|
||||
if (this->iir_filter_enabled_()) {
|
||||
touch_pad_filter_stop();
|
||||
touch_pad_filter_delete();
|
||||
}
|
||||
|
||||
// Configure wakeup pads if any are set
|
||||
this->configure_wakeup_pads_();
|
||||
}
|
||||
|
||||
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
||||
|
||||
uint32_t mask = 0;
|
||||
touch_ll_read_trigger_status_mask(&mask);
|
||||
touch_ll_clear_trigger_status_mask();
|
||||
touch_pad_clear_status();
|
||||
|
||||
// INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured
|
||||
// touch pad detects a touch (value goes below threshold). The hardware does NOT
|
||||
// generate interrupts on release - only on touch events.
|
||||
// The interrupt will continue to fire periodically (based on sleep_cycle) as long
|
||||
// as any pad remains touched. This allows us to detect both new touches and
|
||||
// continued touches, but releases must be detected by timeout in the main loop.
|
||||
|
||||
// Process all configured pads to check their current state
|
||||
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
|
||||
// so we must scan all configured pads to find which ones were touched
|
||||
for (auto *child : component->children_) {
|
||||
touch_pad_t pad = child->get_touch_pad();
|
||||
|
||||
// Read current value using ISR-safe API
|
||||
// IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe
|
||||
// In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes:
|
||||
// "assert failed: xQueueSemaphoreTake queue.c:1718"
|
||||
// We must use raw values even when filter is enabled as a workaround.
|
||||
// Users should adjust thresholds to compensate for the lack of IIR filtering.
|
||||
// See: https://github.com/espressif/esp-idf/issues/17045
|
||||
uint32_t value = touch_ll_read_raw_data(pad);
|
||||
|
||||
// Skip pads that aren’t in the trigger mask
|
||||
if (((mask >> pad) & 1) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
||||
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
||||
// Therefore: touched = (value < threshold)
|
||||
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
||||
bool is_touched = value < child->get_threshold();
|
||||
|
||||
// Always send the current state - the main loop will filter for changes
|
||||
// We send both touched and untouched states because the ISR doesn't
|
||||
// track previous state (to keep ISR fast and simple)
|
||||
TouchPadEventV1 event;
|
||||
event.pad = pad;
|
||||
event.value = value;
|
||||
event.is_touched = is_touched;
|
||||
|
||||
// Send to queue from ISR - non-blocking, drops if queue full
|
||||
BaseType_t x_higher_priority_task_woken = pdFALSE;
|
||||
xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
|
||||
component->enable_loop_soon_any_context();
|
||||
if (x_higher_priority_task_woken) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32
|
||||
@@ -1,402 +0,0 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
|
||||
#include "esp32_touch.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
// Helper to update touch state with a known state and value
|
||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) {
|
||||
// Store the value for get_value() access in lambdas
|
||||
child->value_ = value;
|
||||
|
||||
// Always update timer when touched
|
||||
if (is_touched) {
|
||||
child->last_touch_time_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
if (child->last_state_ != is_touched) {
|
||||
child->last_state_ = is_touched;
|
||||
child->publish_state(is_touched);
|
||||
if (is_touched) {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(),
|
||||
value, child->threshold_ + child->benchmark_);
|
||||
} 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)
|
||||
bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) {
|
||||
// Read current touch value
|
||||
uint32_t value = this->read_touch_value(child->touch_pad_);
|
||||
|
||||
// ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark
|
||||
ESP_LOGV(TAG,
|
||||
"Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32,
|
||||
child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_);
|
||||
bool is_touched = value > child->benchmark_ + child->threshold_;
|
||||
|
||||
this->update_touch_state_(child, is_touched, value);
|
||||
return is_touched;
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::setup() {
|
||||
// Create queue for touch events first
|
||||
if (!this->create_touch_queue_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize touch pad peripheral
|
||||
esp_err_t init_err = touch_pad_init();
|
||||
if (init_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure each touch pad first
|
||||
for (auto *child : this->children_) {
|
||||
esp_err_t config_err = touch_pad_config(child->touch_pad_);
|
||||
if (config_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err));
|
||||
}
|
||||
}
|
||||
|
||||
// Set up filtering if configured
|
||||
if (this->filter_configured_()) {
|
||||
touch_filter_config_t filter_info = {
|
||||
.mode = this->filter_mode_,
|
||||
.debounce_cnt = this->debounce_count_,
|
||||
.noise_thr = this->noise_threshold_,
|
||||
.jitter_step = this->jitter_step_,
|
||||
.smh_lvl = this->smooth_level_,
|
||||
};
|
||||
touch_pad_filter_set_config(&filter_info);
|
||||
touch_pad_filter_enable();
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
touch_pad_denoise_t denoise = {
|
||||
.grade = this->grade_,
|
||||
.cap_level = this->cap_level_,
|
||||
};
|
||||
touch_pad_denoise_set_config(&denoise);
|
||||
touch_pad_denoise_enable();
|
||||
}
|
||||
|
||||
if (this->waterproof_configured_()) {
|
||||
touch_pad_waterproof_t waterproof = {
|
||||
.guard_ring_pad = this->waterproof_guard_ring_pad_,
|
||||
.shield_driver = this->waterproof_shield_driver_,
|
||||
};
|
||||
touch_pad_waterproof_set_config(&waterproof);
|
||||
touch_pad_waterproof_enable();
|
||||
}
|
||||
|
||||
// Configure measurement parameters
|
||||
touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
|
||||
touch_pad_set_charge_discharge_times(this->meas_cycle_);
|
||||
touch_pad_set_measurement_interval(this->sleep_cycle_);
|
||||
|
||||
// Disable hardware timeout - it causes continuous interrupts with high-capacitance
|
||||
// setups (e.g., pressure sensors under cushions). The periodic release check in
|
||||
// loop() handles state detection reliably without needing hardware timeout.
|
||||
touch_pad_timeout_set(false, TOUCH_PAD_THRESHOLD_MAX);
|
||||
|
||||
// Register ISR handler with interrupt mask
|
||||
esp_err_t err =
|
||||
touch_pad_isr_register(touch_isr_handler, this, static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ALL));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err));
|
||||
this->cleanup_touch_queue_();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set thresholds for each pad BEFORE starting FSM
|
||||
for (auto *child : this->children_) {
|
||||
if (child->threshold_ != 0) {
|
||||
touch_pad_set_thresh(child->touch_pad_, child->threshold_);
|
||||
}
|
||||
}
|
||||
|
||||
// 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_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT));
|
||||
|
||||
// Set FSM mode before starting
|
||||
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
|
||||
|
||||
// Start FSM
|
||||
touch_pad_fsm_start();
|
||||
|
||||
// Calculate release timeout based on sleep cycle
|
||||
this->calculate_release_timeout_();
|
||||
}
|
||||
|
||||
void ESP32TouchComponent::dump_config() {
|
||||
this->dump_config_base_();
|
||||
|
||||
if (this->filter_configured_()) {
|
||||
const char *filter_mode_s;
|
||||
switch (this->filter_mode_) {
|
||||
case TOUCH_PAD_FILTER_IIR_4:
|
||||
filter_mode_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_8:
|
||||
filter_mode_s = "IIR_8";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_16:
|
||||
filter_mode_s = "IIR_16";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_32:
|
||||
filter_mode_s = "IIR_32";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_64:
|
||||
filter_mode_s = "IIR_64";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_128:
|
||||
filter_mode_s = "IIR_128";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_IIR_256:
|
||||
filter_mode_s = "IIR_256";
|
||||
break;
|
||||
case TOUCH_PAD_FILTER_JITTER:
|
||||
filter_mode_s = "JITTER";
|
||||
break;
|
||||
default:
|
||||
filter_mode_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Filter mode: %s\n"
|
||||
" Debounce count: %" PRIu32 "\n"
|
||||
" Noise threshold coefficient: %" PRIu32 "\n"
|
||||
" Jitter filter step size: %" PRIu32,
|
||||
filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_);
|
||||
const char *smooth_level_s;
|
||||
switch (this->smooth_level_) {
|
||||
case TOUCH_PAD_SMOOTH_OFF:
|
||||
smooth_level_s = "OFF";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_2:
|
||||
smooth_level_s = "IIR_2";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_4:
|
||||
smooth_level_s = "IIR_4";
|
||||
break;
|
||||
case TOUCH_PAD_SMOOTH_IIR_8:
|
||||
smooth_level_s = "IIR_8";
|
||||
break;
|
||||
default:
|
||||
smooth_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s);
|
||||
}
|
||||
|
||||
if (this->denoise_configured_()) {
|
||||
const char *grade_s;
|
||||
switch (this->grade_) {
|
||||
case TOUCH_PAD_DENOISE_BIT12:
|
||||
grade_s = "BIT12";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT10:
|
||||
grade_s = "BIT10";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT8:
|
||||
grade_s = "BIT8";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_BIT4:
|
||||
grade_s = "BIT4";
|
||||
break;
|
||||
default:
|
||||
grade_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s);
|
||||
|
||||
const char *cap_level_s;
|
||||
switch (this->cap_level_) {
|
||||
case TOUCH_PAD_DENOISE_CAP_L0:
|
||||
cap_level_s = "L0";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L1:
|
||||
cap_level_s = "L1";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L2:
|
||||
cap_level_s = "L2";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L3:
|
||||
cap_level_s = "L3";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L4:
|
||||
cap_level_s = "L4";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L5:
|
||||
cap_level_s = "L5";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L6:
|
||||
cap_level_s = "L6";
|
||||
break;
|
||||
case TOUCH_PAD_DENOISE_CAP_L7:
|
||||
cap_level_s = "L7";
|
||||
break;
|
||||
default:
|
||||
cap_level_s = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s);
|
||||
}
|
||||
|
||||
if (this->setup_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup Mode ENABLED");
|
||||
}
|
||||
|
||||
this->dump_config_sensors_();
|
||||
}
|
||||
|
||||
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
|
||||
this->process_setup_mode_logging_(now);
|
||||
|
||||
// Process any queued touch events from interrupts
|
||||
TouchPadEventV2 event;
|
||||
while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
|
||||
ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad);
|
||||
// Handle timeout events
|
||||
if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
|
||||
// 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)) {
|
||||
// Skip if not an active/timeout event
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the child for the pad that triggered the interrupt
|
||||
for (auto *child : this->children_) {
|
||||
if (child->touch_pad_ == event.pad) {
|
||||
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 if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
|
||||
// We only get ACTIVE interrupts now, releases are detected by timeout
|
||||
// Read the current value
|
||||
uint32_t value = this->read_touch_value(child->touch_pad_);
|
||||
this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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_) {
|
||||
child->ensure_benchmark_read();
|
||||
// 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 - child->last_touch_time_;
|
||||
|
||||
// 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(TOUCH_PAD_INTR_MASK_ACTIVE);
|
||||
touch_pad_isr_deregister(touch_isr_handler, this);
|
||||
this->cleanup_touch_queue_();
|
||||
|
||||
// Configure wakeup pads if any are set
|
||||
this->configure_wakeup_pads_();
|
||||
}
|
||||
|
||||
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
||||
BaseType_t x_higher_priority_task_woken = pdFALSE;
|
||||
|
||||
// Read interrupt status
|
||||
TouchPadEventV2 event;
|
||||
event.intr_mask = touch_pad_read_intr_status_mask();
|
||||
event.pad = touch_pad_get_current_meas_channel();
|
||||
|
||||
// Send event to queue for processing in main loop
|
||||
xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken);
|
||||
component->enable_loop_soon_any_context();
|
||||
|
||||
if (x_higher_priority_task_woken) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const {
|
||||
// Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations.
|
||||
// The hardware continuously samples in the background and we can read the
|
||||
// latest value at any time without waiting.
|
||||
uint32_t value = 0;
|
||||
if (this->filter_configured_()) {
|
||||
// Read filtered/smoothed value when filter is enabled
|
||||
touch_pad_filter_read_smooth(pad, &value);
|
||||
} else {
|
||||
// Read raw value when filter is not configured
|
||||
touch_pad_read_raw_data(pad, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace esp32_touch
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
|
||||
@@ -4,7 +4,6 @@ esp32_touch:
|
||||
measurement_duration: 8ms
|
||||
low_voltage_reference: 0.5V
|
||||
high_voltage_reference: 2.7V
|
||||
voltage_attenuation: 1.5V
|
||||
|
||||
binary_sensor:
|
||||
- platform: esp32_touch
|
||||
|
||||
5
tests/components/esp32_touch/test.esp32-p4-idf.yaml
Normal file
5
tests/components/esp32_touch/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
|
||||
<<: !include common-variants.yaml
|
||||
<<: !include common-get-value.yaml
|
||||
Reference in New Issue
Block a user