/* * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include "control_plane_internal.h" #include #include #include #include #include "control_plane_events.h" #include "driver/gpio.h" #include "esp_err.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" #include "sdkconfig.h" #ifndef CONFIG_TQ_KEY_INPUT_ENABLE #define CONFIG_TQ_KEY_INPUT_ENABLE 1 #endif #ifndef CONFIG_TQ_KEY_INPUT_TASK_STACK #define CONFIG_TQ_KEY_INPUT_TASK_STACK 4096 #endif #ifndef CONFIG_TQ_KEY_IRQ_QUEUE_LEN #define CONFIG_TQ_KEY_IRQ_QUEUE_LEN 16 #endif #ifndef CONFIG_TQ_KEY_DEBOUNCE_INTERVAL_MS #define CONFIG_TQ_KEY_DEBOUNCE_INTERVAL_MS 20 #endif #ifndef CONFIG_TQ_KEY_LONG_PRESS_MS #define CONFIG_TQ_KEY_LONG_PRESS_MS 800 #endif #ifndef CONFIG_TQ_KEY_DOUBLE_CLICK_MS #define CONFIG_TQ_KEY_DOUBLE_CLICK_MS 300 #endif #ifndef CONFIG_TQ_KEY_POST_DOUBLE_CLICK_SUPPRESS_MS #define CONFIG_TQ_KEY_POST_DOUBLE_CLICK_SUPPRESS_MS 450 #endif #ifndef CONFIG_TQ_KEY_INPUT_POLL_MS #define CONFIG_TQ_KEY_INPUT_POLL_MS 20 #endif #if !defined(CONFIG_TQ_PTT_MODE_HOLD_TO_TALK) && !defined(CONFIG_TQ_PTT_MODE_CLICK_TO_TALK) #define CONFIG_TQ_PTT_MODE_CLICK_TO_TALK 1 #endif #ifndef CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS #define CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS 120 #endif #ifndef CONFIG_TQ_LOCAL_TASK_CORE #define CONFIG_TQ_LOCAL_TASK_CORE 1 #endif #if CONFIG_TQ_LOCAL_TASK_CORE < 0 #define CONTROL_PLANE_KEY_INPUT_TASK_CORE tskNO_AFFINITY #else #define CONTROL_PLANE_KEY_INPUT_TASK_CORE ((BaseType_t)CONFIG_TQ_LOCAL_TASK_CORE) #endif #ifndef CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN #define CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN 0 #endif #ifndef CONFIG_TQ_SLOT_SWITCH_PULLDOWN #define CONFIG_TQ_SLOT_SWITCH_PULLDOWN 0 #endif static const char *TAG = "cp_key_input"; typedef struct { gpio_num_t gpio_num; int level; int64_t timestamp_ms; } control_plane_gpio_irq_event_t; typedef struct { control_plane_key_id_t key_id; gpio_num_t gpio_num; int active_level; bool pullup_enabled; bool pulldown_enabled; uint32_t debounce_ms; uint32_t long_press_ms; uint32_t double_click_ms; bool emit_press_release; bool enable_short_press; bool enable_long_press; bool enable_double_click; } control_plane_key_config_t; typedef struct { bool initialized; int raw_level; bool debounced_pressed; bool debounce_pending; bool wait_inactive_before_press; bool long_press_sent; bool click_pending; int64_t debounce_deadline_ms; int64_t inactive_since_ms; int64_t press_start_ms; int64_t click_deadline_ms; int64_t suppress_until_ms; } control_plane_key_state_t; static const control_plane_key_config_t s_key_configs[] = { { .key_id = CONTROL_PLANE_KEY_ID_PTT, .gpio_num = CONFIG_MULTIMODAL_PTT_BUTTON_GPIO, .active_level = CONFIG_MULTIMODAL_PTT_BUTTON_ACTIVE_LEVEL, .pullup_enabled = CONFIG_MULTIMODAL_PTT_BUTTON_PULLUP, .pulldown_enabled = CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN, .debounce_ms = CONFIG_TQ_KEY_DEBOUNCE_INTERVAL_MS, .long_press_ms = CONFIG_TQ_KEY_LONG_PRESS_MS, .double_click_ms = CONFIG_TQ_KEY_DOUBLE_CLICK_MS, .emit_press_release = true, #if CONFIG_TQ_PTT_MODE_CLICK_TO_TALK .enable_short_press = true, .enable_long_press = false, .enable_double_click = true, #else .enable_short_press = false, .enable_long_press = false, .enable_double_click = false, #endif }, { .key_id = CONTROL_PLANE_KEY_ID_SW2, .gpio_num = CONFIG_TQ_SLOT_SWITCH_GPIO, .active_level = CONFIG_TQ_SLOT_SWITCH_ACTIVE_LEVEL, .pullup_enabled = CONFIG_TQ_SLOT_SWITCH_PULLUP, .pulldown_enabled = CONFIG_TQ_SLOT_SWITCH_PULLDOWN, .debounce_ms = CONFIG_TQ_KEY_DEBOUNCE_INTERVAL_MS, .long_press_ms = CONFIG_TQ_KEY_LONG_PRESS_MS, .double_click_ms = CONFIG_TQ_KEY_DOUBLE_CLICK_MS, .emit_press_release = true, .enable_short_press = true, .enable_long_press = true, .enable_double_click = true, }, }; static QueueHandle_t s_gpio_irq_queue; static TaskHandle_t s_key_input_task; static volatile bool s_stop_requested; static control_plane_key_state_t s_key_states[sizeof(s_key_configs) / sizeof(s_key_configs[0])]; static const control_plane_key_config_t *control_plane_find_key_config_by_gpio(gpio_num_t gpio_num, size_t *out_index) { for (size_t i = 0; i < sizeof(s_key_configs) / sizeof(s_key_configs[0]); ++i) { if (s_key_configs[i].gpio_num == gpio_num) { if (out_index != NULL) { *out_index = i; } return &s_key_configs[i]; } } return NULL; } static bool control_plane_key_level_is_pressed(const control_plane_key_config_t *config, int level) { return config != NULL && level == config->active_level; } static void IRAM_ATTR control_plane_key_gpio_isr_handler(void *arg) { const gpio_num_t gpio_num = (gpio_num_t)(intptr_t)arg; QueueHandle_t queue = s_gpio_irq_queue; if (queue == NULL) { return; } control_plane_gpio_irq_event_t event = { .gpio_num = gpio_num, .level = gpio_get_level(gpio_num), .timestamp_ms = (int64_t)pdTICKS_TO_MS(xTaskGetTickCountFromISR()), }; BaseType_t higher_priority_task_woken = pdFALSE; (void)xQueueSendFromISR(queue, &event, &higher_priority_task_woken); if (higher_priority_task_woken == pdTRUE) { portYIELD_FROM_ISR(); } } static esp_err_t control_plane_configure_key_gpio(const control_plane_key_config_t *config) { if (config == NULL) { return ESP_ERR_INVALID_ARG; } gpio_config_t io_config = { .pin_bit_mask = 1ULL << config->gpio_num, .mode = GPIO_MODE_INPUT, .pull_up_en = config->pullup_enabled ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE, .pull_down_en = config->pulldown_enabled ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_ANYEDGE, }; esp_err_t err = gpio_config(&io_config); if (err != ESP_OK) { return err; } err = gpio_isr_handler_add(config->gpio_num, control_plane_key_gpio_isr_handler, (void *)(intptr_t)config->gpio_num); if (err == ESP_ERR_INVALID_STATE) { (void)gpio_isr_handler_remove(config->gpio_num); err = gpio_isr_handler_add(config->gpio_num, control_plane_key_gpio_isr_handler, (void *)(intptr_t)config->gpio_num); } if (err != ESP_OK) { return err; } return gpio_intr_enable(config->gpio_num); } static void control_plane_emit_key_event(const control_plane_key_config_t *config, control_plane_key_event_type_t type, int level, uint32_t duration_ms, uint32_t click_count) { control_plane_system_event_t event = { .family = CONTROL_PLANE_EVENT_FAMILY_KEY, .timestamp_ms = control_plane_event_timestamp_ms(), .data.key = { .key_id = config->key_id, .type = type, .gpio_num = config->gpio_num, .level = level, .timestamp_ms = control_plane_event_timestamp_ms(), .press_duration_ms = duration_ms, .click_count = click_count, }, }; ESP_LOGI(TAG, "emit key=%s type=%s gpio=%d level=%d duration=%" PRIu32 "ms clicks=%" PRIu32, control_plane_key_id_name(config->key_id), control_plane_key_event_type_name(type), (int)config->gpio_num, level, duration_ms, click_count); esp_err_t err = control_plane_post_system_event(&event, 0); if (err != ESP_OK) { ESP_LOGW(TAG, "failed to post key event: %s", esp_err_to_name(err)); } } static void control_plane_handle_debounced_transition(const control_plane_key_config_t *config, control_plane_key_state_t *state, bool pressed, int level, int64_t now_ms) { if (state->wait_inactive_before_press) { if (pressed) { state->debounced_pressed = false; state->inactive_since_ms = 0; return; } state->wait_inactive_before_press = false; state->inactive_since_ms = 0; ESP_LOGI(TAG, "key=%s initial active gate cleared by debounced inactive level", control_plane_key_id_name(config->key_id)); } if (pressed == state->debounced_pressed) { return; } state->debounced_pressed = pressed; if (pressed) { state->press_start_ms = now_ms; state->long_press_sent = false; if (config->emit_press_release) { control_plane_emit_key_event(config, CONTROL_PLANE_KEY_EVENT_PRESSED, level, 0, 0); } return; } uint32_t duration_ms = 0; if (state->press_start_ms > 0 && now_ms >= state->press_start_ms) { duration_ms = (uint32_t)(now_ms - state->press_start_ms); } if (config->emit_press_release) { control_plane_emit_key_event(config, CONTROL_PLANE_KEY_EVENT_RELEASED, level, duration_ms, 0); } if (state->long_press_sent) { if (config->enable_long_press) { control_plane_emit_key_event(config, CONTROL_PLANE_KEY_EVENT_LONG_PRESS_RELEASE, level, duration_ms, 0); } state->click_pending = false; return; } if (!config->enable_short_press) { return; } if (config->enable_double_click && state->click_pending && now_ms <= state->click_deadline_ms) { state->click_pending = false; state->debounce_pending = false; state->press_start_ms = 0; state->suppress_until_ms = now_ms + CONFIG_TQ_KEY_POST_DOUBLE_CLICK_SUPPRESS_MS; control_plane_emit_key_event(config, CONTROL_PLANE_KEY_EVENT_DOUBLE_CLICK, level, duration_ms, 2); return; } if (config->enable_double_click) { state->click_pending = true; state->click_deadline_ms = now_ms + config->double_click_ms; } else { control_plane_emit_key_event(config, CONTROL_PLANE_KEY_EVENT_SHORT_PRESS, level, duration_ms, 1); } } static void control_plane_process_key_timers(void) { const int64_t now_ms = control_plane_event_timestamp_ms(); for (size_t i = 0; i < sizeof(s_key_configs) / sizeof(s_key_configs[0]); ++i) { const control_plane_key_config_t *config = &s_key_configs[i]; control_plane_key_state_t *state = &s_key_states[i]; if (!state->initialized) { continue; } if (state->suppress_until_ms > 0 && now_ms < state->suppress_until_ms) { state->debounce_pending = false; continue; } if (state->wait_inactive_before_press) { const int level = gpio_get_level(config->gpio_num); const bool pressed = control_plane_key_level_is_pressed(config, level); if (!pressed) { if (state->inactive_since_ms == 0) { state->inactive_since_ms = now_ms; } uint32_t stable_ms = config->debounce_ms; if (stable_ms < CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS) { stable_ms = CONTROL_PLANE_PTT_INITIAL_INACTIVE_STABLE_MS; } if ((now_ms - state->inactive_since_ms) >= stable_ms) { state->wait_inactive_before_press = false; state->debounced_pressed = false; state->debounce_pending = false; state->press_start_ms = 0; state->long_press_sent = false; ESP_LOGI(TAG, "key=%s initial active gate cleared by stable inactive poll (%" PRId64 "ms)", control_plane_key_id_name(config->key_id), now_ms - state->inactive_since_ms); } } else { state->inactive_since_ms = 0; } } if (state->debounce_pending && now_ms >= state->debounce_deadline_ms) { state->debounce_pending = false; const int level = gpio_get_level(config->gpio_num); const bool pressed = control_plane_key_level_is_pressed(config, level); control_plane_handle_debounced_transition(config, state, pressed, level, now_ms); } if (config->enable_long_press && state->debounced_pressed && !state->long_press_sent && state->press_start_ms > 0 && now_ms - state->press_start_ms >= config->long_press_ms) { state->long_press_sent = true; control_plane_emit_key_event(config, CONTROL_PLANE_KEY_EVENT_LONG_PRESS_START, state->raw_level, (uint32_t)(now_ms - state->press_start_ms), 0); } if (state->click_pending && now_ms >= state->click_deadline_ms) { state->click_pending = false; control_plane_emit_key_event(config, CONTROL_PLANE_KEY_EVENT_SHORT_PRESS, state->raw_level, 0, 1); } } } static void control_plane_key_input_task(void *arg) { (void)arg; ESP_LOGI(TAG, "Key input task started irq_queue_len=%d debounce=%dms long=%dms double=%dms", CONFIG_TQ_KEY_IRQ_QUEUE_LEN, CONFIG_TQ_KEY_DEBOUNCE_INTERVAL_MS, CONFIG_TQ_KEY_LONG_PRESS_MS, CONFIG_TQ_KEY_DOUBLE_CLICK_MS); vTaskDelay(pdMS_TO_TICKS(500)); esp_err_t setup_err = ESP_OK; memset(s_key_states, 0, sizeof(s_key_states)); for (size_t i = 0; i < sizeof(s_key_configs) / sizeof(s_key_configs[0]); ++i) { const control_plane_key_config_t *config = &s_key_configs[i]; s_key_states[i].initialized = true; s_key_states[i].raw_level = gpio_get_level(config->gpio_num); bool initial_pressed = control_plane_key_level_is_pressed(config, s_key_states[i].raw_level); s_key_states[i].debounced_pressed = initial_pressed; if (config->key_id == CONTROL_PLANE_KEY_ID_PTT && initial_pressed) { ESP_LOGW(TAG, "PTT initial level is active; wait for next stable press before emitting press event"); s_key_states[i].debounced_pressed = false; s_key_states[i].wait_inactive_before_press = true; s_key_states[i].inactive_since_ms = 0; } setup_err = control_plane_configure_key_gpio(config); if (setup_err != ESP_OK) { ESP_LOGE(TAG, "failed to configure key=%s gpio=%d: %s", control_plane_key_id_name(config->key_id), (int)config->gpio_num, esp_err_to_name(setup_err)); break; } ESP_LOGI(TAG, "key config key=%s gpio=%d active_level=%d pullup=%d pulldown=%d initial_level=%d", control_plane_key_id_name(config->key_id), (int)config->gpio_num, config->active_level, config->pullup_enabled, config->pulldown_enabled, s_key_states[i].raw_level); } if (setup_err != ESP_OK) { s_key_input_task = NULL; vTaskDelete(NULL); } while (!s_stop_requested) { control_plane_gpio_irq_event_t irq_event = {0}; if (xQueueReceive(s_gpio_irq_queue, &irq_event, pdMS_TO_TICKS(CONFIG_TQ_KEY_INPUT_POLL_MS)) == pdTRUE) { size_t index = 0; const control_plane_key_config_t *config = control_plane_find_key_config_by_gpio(irq_event.gpio_num, &index); if (config == NULL) { ESP_LOGW(TAG, "ignored gpio change gpio=%d level=%d", (int)irq_event.gpio_num, irq_event.level); continue; } control_plane_key_state_t *state = &s_key_states[index]; if (state->suppress_until_ms > 0 && irq_event.timestamp_ms < state->suppress_until_ms) { const bool pressed = control_plane_key_level_is_pressed(config, irq_event.level); state->raw_level = irq_event.level; state->debounce_pending = false; if (!pressed) { state->debounced_pressed = false; } ESP_LOGD(TAG, "suppressed gpio change key=%s gpio=%d level=%d", control_plane_key_id_name(config->key_id), (int)irq_event.gpio_num, irq_event.level); continue; } state->raw_level = irq_event.level; state->debounce_pending = true; state->debounce_deadline_ms = irq_event.timestamp_ms + config->debounce_ms; ESP_LOGI(TAG, "gpio change key=%s gpio=%d level=%d", control_plane_key_id_name(config->key_id), (int)irq_event.gpio_num, irq_event.level); } control_plane_process_key_timers(); } ESP_LOGI(TAG, "Key input task stopped"); s_key_input_task = NULL; vTaskDelete(NULL); } esp_err_t control_plane_start_key_input_task(void) { #if CONFIG_TQ_KEY_INPUT_ENABLE if (s_key_input_task != NULL) { return ESP_OK; } esp_err_t err = gpio_install_isr_service(0); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { return err; } if (s_gpio_irq_queue == NULL) { s_gpio_irq_queue = xQueueCreate(CONFIG_TQ_KEY_IRQ_QUEUE_LEN, sizeof(control_plane_gpio_irq_event_t)); if (s_gpio_irq_queue == NULL) { return ESP_ERR_NO_MEM; } } s_stop_requested = false; BaseType_t created = xTaskCreatePinnedToCore(control_plane_key_input_task, "cp_key_input", CONFIG_TQ_KEY_INPUT_TASK_STACK, NULL, 5, &s_key_input_task, CONTROL_PLANE_KEY_INPUT_TASK_CORE); if (created != pdPASS) { s_key_input_task = NULL; return ESP_ERR_NO_MEM; } return ESP_OK; #else ESP_LOGI(TAG, "Key input task disabled by configuration"); return ESP_OK; #endif } void control_plane_stop_key_input_task(void) { #if CONFIG_TQ_KEY_INPUT_ENABLE s_stop_requested = true; for (int i = 0; i < 20 && s_key_input_task != NULL; ++i) { vTaskDelay(pdMS_TO_TICKS(50)); } if (s_key_input_task != NULL) { vTaskDelete(s_key_input_task); s_key_input_task = NULL; } for (size_t i = 0; i < sizeof(s_key_configs) / sizeof(s_key_configs[0]); ++i) { (void)gpio_intr_disable(s_key_configs[i].gpio_num); (void)gpio_isr_handler_remove(s_key_configs[i].gpio_num); } if (s_gpio_irq_queue != NULL) { vQueueDelete(s_gpio_irq_queue); s_gpio_irq_queue = NULL; } #endif }