Files
lucky-cat/components/control_plane/src/runtime/control_plane_key_input.c
T

593 lines
20 KiB
C

/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "control_plane_internal.h"
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#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
}