# Touch Sensor FSM
`touch_sensor_fsm` provides configurable touch-state detection for one to
three sample configurations per channel. It accepts samples from polling or
user-push applications and reports debounced active/inactive state changes.
The current source requires ESP-IDF 5.5, 6.0, or 6.1 and
`touch_sensor_lowlevel` 1.0.1 or later. ESP-IDF 5.5 and 6.0 cover ESP32-S2, ESP32-S3,
ESP32-P4, and Linux; ESP-IDF 6.1 also covers ESP32-H4 and ESP32-S31 when those
targets are available in the SDK.
## Features
- Single- and multi-frequency touch detection.
- Startup initialization from measured samples or optional preset values.
- Configurable active/inactive thresholds and debounce, with private idle-noise
monitoring policy.
- Polling callbacks and user-push input, including ISR-safe sample submission.
- Runtime state, sample, threshold, and signal-quality information.
- Deep copies of channel, active/inactive threshold, and optional initialization arrays.
## Add the Component
```bash
idf.py add-dependency "espressif/touch_sensor_fsm^0.9.0"
```
The component declares its compatible `touch_sensor_lowlevel` 1.x dependency.
## Polling Mode
Discover the effective sample count and scan period from lowlevel before
resolving the FSM defaults. On hardware, lowlevel `start()` performs its
best-effort startup alignment before the FSM is opened for events.
```c
#include "esp_check.h"
#include "touch_sensor_fsm.h"
#include "touch_sensor_lowlevel.h"
static void polling_cb(fsm_handle_t handle, uint32_t channel,
uint32_t *raw_data, void *user_data)
{
(void)handle;
(void)user_data;
ESP_ERROR_CHECK(touch_sensor_lowlevel_get_data(channel, raw_data));
}
void app_main(void)
{
uint32_t channels[] = {10};
uint32_t thresholds_active[] = {70};
uint32_t thresholds_inactive[] = {0}; /* 0 selects active / 2. */
touch_lowlevel_config_t lowlevel_config = {
.channel_num = 1,
.channel_list = channels,
.channel_type = NULL,
.sample_period_ms = 20,
.sample_cfg_num = 0,
};
ESP_ERROR_CHECK(touch_sensor_lowlevel_create(&lowlevel_config));
ESP_ERROR_CHECK(touch_sensor_lowlevel_start());
touch_lowlevel_info_t lowlevel_info = {0};
ESP_ERROR_CHECK(touch_sensor_lowlevel_get_info(&lowlevel_info));
fsm_config_t fsm_config;
ESP_ERROR_CHECK(touch_sensor_fsm_get_default_config(
lowlevel_info.sample_cfg_num, lowlevel_info.sample_period_ms,
&fsm_config));
fsm_config.channel_num = lowlevel_info.channel_num;
fsm_config.channel_list = (uint32_t *)lowlevel_info.channel_list;
fsm_config.freq_num = lowlevel_info.sample_cfg_num;
fsm_config.threshold_active = thresholds_active;
fsm_config.threshold_inactive = thresholds_inactive;
fsm_config.sample_period_ms = lowlevel_info.sample_period_ms;
fsm_config.polling_cb = polling_cb;
fsm_handle_t fsm = NULL;
ESP_ERROR_CHECK(touch_sensor_fsm_create(&fsm_config, &fsm));
ESP_ERROR_CHECK(touch_sensor_fsm_control(fsm, FSM_CTRL_START, NULL));
while (true) {
ESP_ERROR_CHECK(touch_sensor_fsm_handle_events(fsm));
vTaskDelay(pdMS_TO_TICKS(20));
}
}
```
Keep the first configured electrode untouched and the board stable while
lowlevel starts and FSM initialization collects its initial samples. A
lowlevel startup error is returned by `touch_sensor_lowlevel_start()`; do not
start the FSM until that call succeeds. The FSM also needs valid no-touch
samples before normal interaction.
See [`examples/fsm`](../../examples/fsm) for a complete project with channel,
threshold, and runtime diagnostic options.
## User-Push Mode
Use user-push mode when samples arrive from an application callback or ISR:
```c
fsm_config_t config = DEFAULTS_TOUCH_SENSOR_FSM_CONFIG();
config.mode = FSM_MODE_USER_PUSH;
config.channel_num = 1;
config.channel_list = (uint32_t[]){10};
config.freq_num = 3;
config.threshold_active = (uint32_t[]){70};
config.threshold_inactive = NULL; /* defaults to active / 2 */
fsm_handle_t fsm = NULL;
ESP_ERROR_CHECK(touch_sensor_fsm_create(&config, &fsm));
ESP_ERROR_CHECK(touch_sensor_fsm_control(fsm, FSM_CTRL_START, NULL));
uint32_t samples[] = {12300, 11800, 11500};
ESP_ERROR_CHECK(touch_sensor_fsm_update_data(fsm, 10, samples, 3, false));
ESP_ERROR_CHECK(touch_sensor_fsm_handle_events(fsm));
```
Pass `true` as the final argument of `touch_sensor_fsm_update_data()` only from
ISR context. Continue calling `touch_sensor_fsm_handle_events()` from a task.
Each update must contain exactly `freq_num` values for the configured channel.
## Configuration
Start with `DEFAULTS_TOUCH_SENSOR_FSM_CONFIG()` or
`touch_sensor_fsm_get_default_config()` and override only the values required
by the application.
- `channel_num`, `channel_list`, and `threshold_active` are required. Every
active threshold must be nonzero and below `UINT32_MAX`.
- `freq_num` must be in the range `1..FSM_MAX_FREQ`.
- Polling mode requires `polling_cb`.
- `threshold_active` and `threshold_inactive` are normalized fused
decision-domain counts. `threshold_inactive` is optional; NULL or a zero
entry resolves to `threshold_active / 2`. An inactive value equal to active
is valid and preserves the state until the strict release comparison is
crossed. Press uses `fused_filtered > active + noise`; release uses
`fused_filtered < inactive`.
- The smoothing coefficient uses integer permille values in the range
`0..1000`. Fields documented as automatic use their mode-specific default
when set to zero.
- `sample_period_ms` defaults to 20 ms. It is the polling rate limit in
polling mode and the time base for startup windows. Set it to zero when the
period is unknown.
- `debounce_active` and `debounce_inactive` control the number of consecutive
samples required for a state change. Both default to one frame; zero also
selects that one-frame default.
- Raw baselines follow every valid complete frame in both directions. The
private default rate is at most one raw count per 500 ms; there is no touch
lock or long-press absorption timer.
- Noise recovery waits for 100 ms of quiet data, then uses a fixed 20-frame
window. Its private noise base is `threshold_active / 5`, with a minimum of
one normalized count; noise window length and margin are not public fields.
- `freq_sensitivity` is optional per-channel/per-frequency sensitivity data in
the layout documented by `fsm_config_t`. Values are 100..1000; `NULL` uses
the default equal sensitivity.
- Configuration arrays are copied during creation and may be released after
`touch_sensor_fsm_create()` returns.
`touch_sensor_fsm_get_default_config(freq_num, sample_period_ms, &config)`
returns the scalar defaults for a topology. Set the channel and active
threshold arrays before passing that structure to `touch_sensor_fsm_create()`.
## Runtime Information
Call `touch_sensor_fsm_get_state()` for the current public state and
`touch_sensor_fsm_get_data()` for the signed fused-filtered amplitude in
normalized counts. Negative values are preserved by this FSM API; wrapper
components may clamp them when exposing a non-negative business amplitude.
The conversion saturates to the `int32_t` range at the API boundary.
Both functions remain available with diagnostics disabled.
Internal snapshots and effective-configuration queries are not public APIs.
They are declared in `private_include/touch_sensor_fsm_private.h` and compiled
only when `CONFIG_TOUCH_SENSOR_FSM_DEBUG=y` (disabled by default). Production
applications must not include this header or enable diagnostic queries.
The state callback receives `FSM_STATE_ACTIVE` or `FSM_STATE_INACTIVE` after
debounce. Its data argument is the current non-negative touch amplitude.
## Unreleased migration
Diagnostic types, queries and CSV macros have moved out of the public header.
Callers using `get_info()` only for amplitude must migrate to `get_data()`.
Internal diagnostic consumers must explicitly enable the debug option and add
the private include directory to their own target, not to a public include path.
This interface change requires callers to recompile. Details of internal
snapshot fields are documented with the private interface.
The removed `baseline_coef`, `baseline_recover_coef`, `long_press_samples`,
`baseline_hold`, `baseline_locked`, `press_timer`, `baseline_recovering`,
`long_press_absorb_count`, and outlier diagnostic fields have no replacement.
Delete reads and configuration assignments for those fields. Limiter overrides
remain in permille and are converted once from the measured startup baseline.
An explicit legacy per-frequency limiter is converted at
each frequency and the smallest normalized limit becomes the fused limit.
The old `threshold` plus `hysteresis` pair is not a field rename. For example,
old `threshold = 70, hysteresis = 10` compared at active 80 and inactive 60.
The new `threshold_active = 70` with a NULL inactive array compares at active
70 and inactive 35. To preserve the old comparison boundaries, configure
`threshold_active = 80` and `threshold_inactive = 60` explicitly. Noise
compensation raises only the active threshold and is capped below the positive
fused limiter; it never changes the inactive threshold.
The noise-window and noise-margin settings are no longer configurable. Remove
assignments to those former fields; noise collection follows the private fixed
policy described above.
## Lifecycle
```text
create -> start -> handle/update -> stop -> delete
```
Stop the FSM with `FSM_CTRL_STOP` before calling
`touch_sensor_fsm_delete()`. Stop and delete the lowlevel driver separately
after the FSM no longer uses it. If creation or startup fails, delete any
successfully created instance before retrying.
## Linux Replay
The Linux target accepts the same user-push API and can be paired with the
lowlevel replay providers. Use an explicit period and deterministic input in
automated tests. Linux replay leaves supplied raw values unchanged and does
not perform hardware startup alignment.
idf.py add-dependency "espressif/touch_sensor_fsm^0.9.0"