From 886dc393a90403ea638c54e915e6d8fdafa77992 Mon Sep 17 00:00:00 2001 From: wurtz Date: Thu, 5 Feb 2026 08:58:56 -0500 Subject: [PATCH 1/2] feat: Add capacitive touch controls for Tidbyt Gen 2 Implements touch gestures on GPIO33 (TOUCH_PAD_NUM8): - TAP: Skip to next app - DOUBLE TAP: Reserved for future use - HOLD (2s): Toggle display on/off Features: - Adaptive baseline tracking to handle display EMI drift - Late tap detection to prevent accidental skips during double-tap - Fast warmup period (5s) for quicker calibration on boot - Debug logging (can be disabled via TOUCH_DEBUG_ENABLED) Co-Authored-By: Claude Opus 4.5 --- main/CMakeLists.txt | 1 + main/main.c | 81 +++++++++++- main/touch_control.c | 305 +++++++++++++++++++++++++++++++++++++++++++ main/touch_control.h | 59 +++++++++ 4 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 main/touch_control.c create mode 100644 main/touch_control.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ee5b9d2..e32e2a8 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -24,6 +24,7 @@ idf_component_register(SRCS "main.c" "dns_wrapper.c" "syslog.c" "sntp.c" + "touch_control.c" INCLUDE_DIRS ".") idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_getaddrinfo" APPEND) diff --git a/main/main.c b/main/main.c index 241e970..11476a7 100644 --- a/main/main.c +++ b/main/main.c @@ -22,6 +22,7 @@ #include "sdkconfig.h" #include "sntp.h" #include "syslog.h" +#include "touch_control.h" #include "version.h" #include "wifi.h" @@ -54,6 +55,13 @@ static bool button_boot = false; static bool first_ws_image_received = false; static bool config_received = false; +// Touch control state +static bool display_power_on = true; +static uint8_t saved_brightness = 30; + +// Touch control function declaration +static void handle_touch_event(touch_event_t event); + static void config_saved_callback(void) { config_received = true; ESP_LOGI(TAG, "Configuration saved - signaling main task"); @@ -496,6 +504,17 @@ void app_main(void) { } esp_register_shutdown_handler(&display_shutdown); + // Initialize touch controls (GPIO33 on Tidbyt Gen2) + ESP_LOGI(TAG, "Initializing touch control..."); + esp_err_t touch_ret = touch_control_init(); + if (touch_ret == ESP_OK) { + ESP_LOGI(TAG, "Touch control ready on GPIO33"); + touch_control_debug_all_pads(); + } else { + ESP_LOGW(TAG, "Touch control init failed: %s (continuing without touch)", + esp_err_to_name(touch_ret)); + } + // Start the AP web server now that display memory is allocated if (nvs_get_ap_mode()) { ESP_LOGI(TAG, "Starting AP Web Server..."); @@ -692,7 +711,16 @@ void app_main(void) { } wifi_health_check(); - vTaskDelay(pdMS_TO_TICKS(5000)); // check every 5s + + // Poll touch frequently for 5 seconds (50ms intervals = 100 checks) + // This allows proper gesture detection while keeping health checks at 5s + for (int i = 0; i < 100; i++) { + touch_event_t touch_event = touch_control_check(); + if (touch_event != TOUCH_EVENT_NONE) { + handle_touch_event(touch_event); + } + vTaskDelay(pdMS_TO_TICKS(50)); // 50ms = responsive touch + } } } else { // normal http @@ -778,7 +806,58 @@ void app_main(void) { isAnimating = 1; } } + + // Check for touch events + touch_event_t touch_event = touch_control_check(); + if (touch_event != TOUCH_EVENT_NONE) { + handle_touch_event(touch_event); + } + wifi_health_check(); } } } + +/** + * Handle touch events from the single touch pad + * TAP = skip to next app + * DOUBLE_TAP = (reserved for future use) + * HOLD = toggle display power on/off + */ +static void handle_touch_event(touch_event_t event) { + ESP_LOGI(TAG, "Touch event: %s", touch_event_to_string(event)); + + switch (event) { + case TOUCH_EVENT_TAP: + if (display_power_on) { + ESP_LOGI(TAG, "TAP - skip to next app"); + isAnimating = -1; + } else { + ESP_LOGI(TAG, "TAP ignored - display is off (hold to turn on)"); + } + break; + + case TOUCH_EVENT_DOUBLE_TAP: + // Reserved for future use + ESP_LOGI(TAG, "DOUBLE TAP - no action assigned"); + break; + + case TOUCH_EVENT_HOLD: + display_power_on = !display_power_on; + + if (display_power_on) { + ESP_LOGI(TAG, "HOLD - Display ON"); + display_set_brightness(saved_brightness); + isAnimating = 1; + } else { + ESP_LOGI(TAG, "HOLD - Display OFF"); + saved_brightness = 30; + display_set_brightness(0); + isAnimating = 0; + } + break; + + default: + break; + } +} diff --git a/main/touch_control.c b/main/touch_control.c new file mode 100644 index 0000000..565ec00 --- /dev/null +++ b/main/touch_control.c @@ -0,0 +1,305 @@ +/** + * touch_control.c + * + * Touch control for Tidbyt Gen 2 + * Single touch zone on GPIO33 (TOUCH_PAD_NUM8) + * + * Gestures: + * - Single tap: Next app + * - Double tap: Cycle brightness (10% -> 25% -> 50% -> 75%) + * - Long hold (2s): Toggle display on/off + */ + +#include "touch_control.h" + +#include + +static const char* TAG = "TouchControl"; + +#define TOUCH_HOLD_MS 2000 +#define DOUBLE_TAP_WINDOW_MS 500 // Window for second tap to count as double-tap +#define MIN_TAP_DURATION_MS 20 // Minimum to register as a tap at all + +typedef enum { + STATE_IDLE, + STATE_TOUCHING, + STATE_WAIT_FOR_DOUBLE_TAP, + STATE_HOLD_FIRED +} touch_fsm_state_t; + +// Adaptive baseline tracking parameters +#define BASELINE_UPDATE_INTERVAL_MS 200 // Update baseline every 200ms +#define BASELINE_ALPHA 0.15f // Faster adaptation (15% new, 85% old) +#define BASELINE_ALPHA_FAST 0.5f // Fast adaptation during warmup +#define WARMUP_PERIOD_MS 5000 // Use fast adaptation for first 5 seconds +#define TOUCH_DROP_THRESHOLD 35 // Touch must drop at least this much from baseline + +typedef struct { + uint16_t threshold; + uint32_t debounce_ms; + bool initialized; + uint16_t baseline; + float adaptive_baseline; // Floating point for smooth adaptation + uint32_t last_baseline_update; + uint32_t init_time; // Time of initialization for warmup period + touch_fsm_state_t state; + uint32_t touch_start_time; + uint32_t release_time; + uint32_t last_event_time; + bool is_late_tap; // True if current touch was too late for double-tap (will be swallowed) +} touch_state_t; + +static touch_state_t g_touch = {.threshold = TOUCH_THRESHOLD_DEFAULT, + .debounce_ms = TOUCH_DEBOUNCE_MS, + .initialized = false, + .baseline = 0, + .adaptive_baseline = 0, + .last_baseline_update = 0, + .init_time = 0, + .state = STATE_IDLE, + .touch_start_time = 0, + .release_time = 0, + .last_event_time = 0, + .is_late_tap = false}; + +static uint32_t get_time_ms(void) { + return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); +} + +static uint16_t read_touch_filtered(touch_pad_t pad) { + uint16_t value = 0; + esp_err_t ret = touch_pad_read_filtered(pad, &value); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to read pad %d: %s", pad, esp_err_to_name(ret)); + return 65535; + } + return value; +} + +// Touch pad on Tidbyt Gen 2: GPIO33 (TOUCH_PAD_NUM8) +// Based on ESPHome configuration: https://community.home-assistant.io/t/esphome-on-tidbyt-gen-2/830367 + +esp_err_t touch_control_init(void) { + ESP_LOGI(TAG, "Initializing touch control on GPIO33..."); + + esp_err_t ret = touch_pad_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to init touch pad: %s", esp_err_to_name(ret)); + return ret; + } + + touch_pad_set_voltage(TOUCH_HVOLT_2V7, TOUCH_LVOLT_0V5, TOUCH_HVOLT_ATTEN_1V); + + // Only configure the main touch pad (GPIO33) + touch_pad_config(TOUCH_PAD_MAIN, 0); + + ret = touch_pad_filter_start(10); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start filter: %s", esp_err_to_name(ret)); + } + + vTaskDelay(pdMS_TO_TICKS(100)); + + touch_control_calibrate(); + + g_touch.initialized = true; + g_touch.state = STATE_IDLE; + g_touch.init_time = get_time_ms(); + + ESP_LOGI(TAG, "Touch control ready (GPIO33)"); + ESP_LOGI(TAG, " TAP = Next app | DOUBLE-TAP = Brightness | HOLD 2s = Toggle display"); + + return ESP_OK; +} + +touch_event_t touch_control_check(void) { + if (!g_touch.initialized) { + return TOUCH_EVENT_NONE; + } + + uint32_t now = get_time_ms(); + uint16_t value = read_touch_filtered(TOUCH_PAD_MAIN); + + // Initialize adaptive baseline on first read + if (g_touch.adaptive_baseline == 0) { + g_touch.adaptive_baseline = (float)value; + } + + // Calculate delta from adaptive baseline (positive = finger touching = value dropped) + int16_t delta = (int16_t)g_touch.adaptive_baseline - (int16_t)value; + + // Touch detected if value dropped significantly from adaptive baseline + bool is_touched = (delta >= TOUCH_DROP_THRESHOLD); + + // Update adaptive baseline when NOT touching + // This handles drift from display EMI, temperature, etc. + // Use fast adaptation during warmup period to quickly track display EMI + if (!is_touched && (now - g_touch.last_baseline_update >= BASELINE_UPDATE_INTERVAL_MS)) { + bool in_warmup = (now - g_touch.init_time) < WARMUP_PERIOD_MS; + float alpha = in_warmup ? BASELINE_ALPHA_FAST : BASELINE_ALPHA; + g_touch.adaptive_baseline = (alpha * (float)value) + + ((1.0f - alpha) * g_touch.adaptive_baseline); + g_touch.last_baseline_update = now; + } + +#if TOUCH_DEBUG_ENABLED + static uint32_t last_debug = 0; + + // Every 5 seconds, show touch debug info + if (now - last_debug > 5000) { + ESP_LOGI(TAG, "=== TOUCH DEBUG (adaptive baseline) ==="); + ESP_LOGI(TAG, "Current: %d, Adaptive baseline: %.0f, Delta: %d", + value, g_touch.adaptive_baseline, delta); + ESP_LOGI(TAG, "Touch threshold: %d drop, Touched: %s", + TOUCH_DROP_THRESHOLD, is_touched ? "YES" : "NO"); + ESP_LOGI(TAG, "State: %d", g_touch.state); + ESP_LOGI(TAG, "========================================"); + last_debug = now; + } +#endif + + touch_event_t event = TOUCH_EVENT_NONE; + + switch (g_touch.state) { + case STATE_IDLE: + if (is_touched) { + g_touch.state = STATE_TOUCHING; + g_touch.touch_start_time = now; + g_touch.is_late_tap = false; // Fresh tap from idle + } + break; + + case STATE_TOUCHING: + if (!is_touched) { + uint32_t duration = now - g_touch.touch_start_time; + + if (duration >= TOUCH_HOLD_MS) { + g_touch.state = STATE_IDLE; + } else if (g_touch.is_late_tap) { + // Late tap (came after double-tap window expired) - swallow it + ESP_LOGI(TAG, "Late tap swallowed (%ldms) - no skip", duration); + g_touch.state = STATE_IDLE; + } else if (duration >= MIN_TAP_DURATION_MS) { + g_touch.release_time = now; + g_touch.state = STATE_WAIT_FOR_DOUBLE_TAP; + } else { + g_touch.state = STATE_IDLE; + } + } else { + uint32_t duration = now - g_touch.touch_start_time; + if (duration >= TOUCH_HOLD_MS) { + event = TOUCH_EVENT_HOLD; + g_touch.state = STATE_HOLD_FIRED; + g_touch.last_event_time = now; + ESP_LOGI(TAG, "HOLD detected"); + } + } + break; + + case STATE_WAIT_FOR_DOUBLE_TAP: + if (is_touched) { + uint32_t gap = now - g_touch.release_time; + if (gap <= DOUBLE_TAP_WINDOW_MS) { + // Second tap in time - double-tap! + event = TOUCH_EVENT_DOUBLE_TAP; + g_touch.last_event_time = now; + g_touch.is_late_tap = false; + ESP_LOGI(TAG, "DOUBLE-TAP detected"); + } else { + // Second tap came too late - mark it so it gets swallowed on release + g_touch.is_late_tap = true; + ESP_LOGI(TAG, "Late second tap (gap %ldms > %dms)", gap, DOUBLE_TAP_WINDOW_MS); + } + g_touch.state = STATE_TOUCHING; + g_touch.touch_start_time = now; + } else { + uint32_t wait_time = now - g_touch.release_time; + if (wait_time > DOUBLE_TAP_WINDOW_MS) { + // No second tap came - this was a single tap + event = TOUCH_EVENT_TAP; + g_touch.last_event_time = now; + g_touch.state = STATE_IDLE; + ESP_LOGI(TAG, "TAP detected (single)"); + } + } + break; + + case STATE_HOLD_FIRED: + if (!is_touched) { + g_touch.state = STATE_IDLE; + } + break; + } + + return event; +} + +void touch_control_calibrate(void) { + ESP_LOGI(TAG, "Calibrating (don't touch!)..."); + + // Match official Tidbyt HDK: use maximum of 3 readings + uint16_t max_value = 0; + const int samples = 3; + + for (int i = 0; i < samples; i++) { + uint16_t val = read_touch_filtered(TOUCH_PAD_MAIN); + if (val > max_value) { + max_value = val; + } + vTaskDelay(pdMS_TO_TICKS(100)); // 100ms between samples like HDK + } + + g_touch.baseline = max_value; + g_touch.adaptive_baseline = (float)max_value; + + ESP_LOGI(TAG, "Baseline (max of %d samples): %d", samples, g_touch.baseline); + ESP_LOGI(TAG, "Using adaptive tracking + delta threshold: %d", + TOUCH_DROP_THRESHOLD); +} + +void touch_control_debug_all_pads(void) { + ESP_LOGI(TAG, "=== Touch Control Debug ==="); + + uint16_t current = read_touch_filtered(TOUCH_PAD_MAIN); + int16_t delta = (int16_t)g_touch.adaptive_baseline - (int16_t)current; + + ESP_LOGI(TAG, "Main pad (GPIO33): TOUCH_PAD_NUM8"); + ESP_LOGI(TAG, "Current: %d, Adaptive baseline: %.0f", current, g_touch.adaptive_baseline); + ESP_LOGI(TAG, "Delta: %d (need %d+ for touch)", delta, TOUCH_DROP_THRESHOLD); + ESP_LOGI(TAG, "========================="); +} + +void touch_control_set_threshold(uint16_t threshold) { + g_touch.threshold = threshold; + ESP_LOGI(TAG, "Threshold set to: %d", threshold); +} + +uint16_t touch_control_get_threshold(void) { return g_touch.threshold; } + +void touch_control_set_debounce(uint32_t ms) { + g_touch.debounce_ms = ms; + ESP_LOGI(TAG, "Debounce set to: %d ms", ms); +} + +uint16_t touch_control_read_raw(touch_pad_t pad) { + uint16_t value = 0; + touch_pad_read(pad, &value); + return value; +} + +bool touch_control_is_initialized(void) { return g_touch.initialized; } + +const char* touch_event_to_string(touch_event_t event) { + switch (event) { + case TOUCH_EVENT_NONE: + return "NONE"; + case TOUCH_EVENT_TAP: + return "TAP"; + case TOUCH_EVENT_DOUBLE_TAP: + return "DOUBLE_TAP"; + case TOUCH_EVENT_HOLD: + return "HOLD"; + default: + return "UNKNOWN"; + } +} diff --git a/main/touch_control.h b/main/touch_control.h new file mode 100644 index 0000000..1c95d3b --- /dev/null +++ b/main/touch_control.h @@ -0,0 +1,59 @@ +/** + * touch_control.h + * + * Touch control interface for Tidbyt Gen 2 + * Single touch zone on GPIO33 (TOUCH_PAD_NUM8) + */ + +#ifndef TOUCH_CONTROL_H +#define TOUCH_CONTROL_H + +#include "driver/touch_pad.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Touch pad assignment for Tidbyt Gen 2 +#define TOUCH_PAD_MAIN TOUCH_PAD_NUM8 // GPIO33 + +// Touch threshold - based on ESPHome Tidbyt Gen2 config +// Untouched values are typically 900-1000, touched drops below this +// ESPHome uses 1200 as threshold (touch triggers when value < 1200) +// But we'll use adaptive calibration - this is just the starting point +#define TOUCH_THRESHOLD_DEFAULT 1200 + +// Debounce time (250ms matches official Tidbyt HDK) +#define TOUCH_DEBOUNCE_MS 250 + +// Debug logging - set to 1 to see touch values in serial monitor +#define TOUCH_DEBUG_ENABLED 1 + +typedef enum { + TOUCH_EVENT_NONE = 0, + TOUCH_EVENT_TAP, // Single tap - next app + TOUCH_EVENT_DOUBLE_TAP, // Double tap - cycle brightness + TOUCH_EVENT_HOLD // Long hold (2+ sec) - toggle display on/off +} touch_event_t; + +esp_err_t touch_control_init(void); +touch_event_t touch_control_check(void); +void touch_control_set_threshold(uint16_t threshold); +uint16_t touch_control_get_threshold(void); +void touch_control_set_debounce(uint32_t ms); +void touch_control_calibrate(void); +void touch_control_debug_all_pads(void); +uint16_t touch_control_read_raw(touch_pad_t pad); +bool touch_control_is_initialized(void); +const char* touch_event_to_string(touch_event_t event); + +#ifdef __cplusplus +} +#endif + +#endif // TOUCH_CONTROL_H From 32f2f4593298b7422066a548aa5fd3dd0b72765c Mon Sep 17 00:00:00 2001 From: wurtz Date: Fri, 6 Feb 2026 06:22:33 -0500 Subject: [PATCH 2/2] fix: Guard touch code for Gen2 only, fix display state sync - Add #ifdef CONFIG_BOARD_TIDBYT_GEN2 guards around all touch code - Conditionally compile touch_control.c only for Gen2 in CMakeLists - Non-Gen2 devices use original 5s delay instead of touch polling - Fix display_power_on sync when server sends brightness command Co-Authored-By: Claude Opus 4.5 --- main/CMakeLists.txt | 32 +++++++++++++++++++------------- main/main.c | 21 +++++++++++++++++++-- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index e32e2a8..2b1d141 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -12,19 +12,25 @@ if(NOT DEFINED VAL_REMOTE_URL) set(VAL_REMOTE_URL "XplaceholderREMOTEURL___________________________________________________________________________________________________________") endif() -idf_component_register(SRCS "main.c" - "display.cpp" - "flash.c" - "gfx.c" - "ota.c" - "remote.c" - "wifi.c" - "ap.c" - "nvs_settings.c" - "dns_wrapper.c" - "syslog.c" - "sntp.c" - "touch_control.c" +# Conditionally include touch_control.c only for Tidbyt Gen2 +set(SRCS "main.c" + "display.cpp" + "flash.c" + "gfx.c" + "ota.c" + "remote.c" + "wifi.c" + "ap.c" + "nvs_settings.c" + "dns_wrapper.c" + "syslog.c" + "sntp.c") + +if(CONFIG_BOARD_TIDBYT_GEN2) + list(APPEND SRCS "touch_control.c") +endif() + +idf_component_register(SRCS ${SRCS} INCLUDE_DIRS ".") idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_getaddrinfo" APPEND) diff --git a/main/main.c b/main/main.c index 11476a7..627aeab 100644 --- a/main/main.c +++ b/main/main.c @@ -22,7 +22,9 @@ #include "sdkconfig.h" #include "sntp.h" #include "syslog.h" +#ifdef CONFIG_BOARD_TIDBYT_GEN2 #include "touch_control.h" +#endif #include "version.h" #include "wifi.h" @@ -55,12 +57,12 @@ static bool button_boot = false; static bool first_ws_image_received = false; static bool config_received = false; +#ifdef CONFIG_BOARD_TIDBYT_GEN2 // Touch control state static bool display_power_on = true; static uint8_t saved_brightness = 30; - -// Touch control function declaration static void handle_touch_event(touch_event_t event); +#endif static void config_saved_callback(void) { config_received = true; @@ -209,6 +211,11 @@ static void websocket_event_handler(void* handler_args, esp_event_base_t base, brightness_value = DISPLAY_MAX_BRIGHTNESS; display_set_brightness((uint8_t)brightness_value); ESP_LOGI(TAG, "Updated brightness to %d", brightness_value); +#ifdef CONFIG_BOARD_TIDBYT_GEN2 + // Sync touch control state - server brightness command means display is on + display_power_on = true; + saved_brightness = (uint8_t)brightness_value; +#endif } // Check for "ota_url" @@ -504,6 +511,7 @@ void app_main(void) { } esp_register_shutdown_handler(&display_shutdown); +#ifdef CONFIG_BOARD_TIDBYT_GEN2 // Initialize touch controls (GPIO33 on Tidbyt Gen2) ESP_LOGI(TAG, "Initializing touch control..."); esp_err_t touch_ret = touch_control_init(); @@ -514,6 +522,7 @@ void app_main(void) { ESP_LOGW(TAG, "Touch control init failed: %s (continuing without touch)", esp_err_to_name(touch_ret)); } +#endif // Start the AP web server now that display memory is allocated if (nvs_get_ap_mode()) { @@ -712,6 +721,7 @@ void app_main(void) { wifi_health_check(); +#ifdef CONFIG_BOARD_TIDBYT_GEN2 // Poll touch frequently for 5 seconds (50ms intervals = 100 checks) // This allows proper gesture detection while keeping health checks at 5s for (int i = 0; i < 100; i++) { @@ -721,6 +731,9 @@ void app_main(void) { } vTaskDelay(pdMS_TO_TICKS(50)); // 50ms = responsive touch } +#else + vTaskDelay(pdMS_TO_TICKS(5000)); // 5 second health check interval +#endif } } else { // normal http @@ -807,17 +820,20 @@ void app_main(void) { } } +#ifdef CONFIG_BOARD_TIDBYT_GEN2 // Check for touch events touch_event_t touch_event = touch_control_check(); if (touch_event != TOUCH_EVENT_NONE) { handle_touch_event(touch_event); } +#endif wifi_health_check(); } } } +#ifdef CONFIG_BOARD_TIDBYT_GEN2 /** * Handle touch events from the single touch pad * TAP = skip to next app @@ -861,3 +877,4 @@ static void handle_touch_event(touch_event_t event) { break; } } +#endif