按键系统待测
This commit is contained in:
@@ -12,42 +12,99 @@
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifndef CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN
|
||||
#define CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN 1
|
||||
#endif
|
||||
|
||||
static const char *TAG = "app_comp_boot";
|
||||
|
||||
#if CONFIG_TQ_BOOT_TURNTABLE_CALIBRATION_ENABLE
|
||||
static void app_composition_run_boot_turntable_calibration(void)
|
||||
typedef enum {
|
||||
APP_COMPOSITION_BOOT_STAGE_POWER_HOLD = 1,
|
||||
APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT,
|
||||
APP_COMPOSITION_BOOT_STAGE_BOOT_SOUND,
|
||||
APP_COMPOSITION_BOOT_STAGE_AUDIO_RELEASE,
|
||||
APP_COMPOSITION_BOOT_STAGE_LCD_DEVICE,
|
||||
APP_COMPOSITION_BOOT_STAGE_LCD_RENDER_LOOP,
|
||||
APP_COMPOSITION_BOOT_STAGE_BOOT_EXPRESSION,
|
||||
APP_COMPOSITION_BOOT_STAGE_PLATFORM_DOMAIN,
|
||||
APP_COMPOSITION_BOOT_STAGE_TURNTABLE_CALIBRATION,
|
||||
APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE,
|
||||
APP_COMPOSITION_BOOT_STAGE_WIFI_BOOTSTRAP,
|
||||
APP_COMPOSITION_BOOT_STAGE_RUNTIME_WORKERS,
|
||||
APP_COMPOSITION_BOOT_STAGE_NETWORK_SERVICES,
|
||||
} app_composition_boot_stage_t;
|
||||
|
||||
static void app_composition_log_stage(app_composition_boot_stage_t stage,
|
||||
const char *name,
|
||||
const char *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starting boot turntable calibration");
|
||||
|
||||
esp_err_t err = control_plane_calibrate();
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Boot turntable calibration completed");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Boot turntable calibration failed: %s", esp_err_to_name(err));
|
||||
ESP_LOGI(TAG, "boot stage %02d %-24s event=%s", (int)stage, name, event);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void app_composition_connect_wifi_for_boot(void)
|
||||
static void app_composition_log_stage_dry_run(app_composition_boot_stage_t stage,
|
||||
const char *name,
|
||||
const char *event)
|
||||
{
|
||||
esp_err_t err = control_plane_bootstrap_connectivity(CONFIG_MULTIMODAL_WIFI_SSID,
|
||||
CONFIG_MULTIMODAL_WIFI_PASSWORD,
|
||||
CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS,
|
||||
"TQ-CAT");
|
||||
ESP_LOGI(TAG, "boot stage %02d %-24s would_trigger=%s", (int)stage, name, event);
|
||||
}
|
||||
|
||||
static esp_err_t app_composition_run_plan_a_boot_sequence(void)
|
||||
{
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_POWER_HOLD,
|
||||
"power_hold",
|
||||
"assert_power_hold_gpio");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT,
|
||||
"audio_init",
|
||||
"initialize_audio_path_with_timeout");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_BOOT_SOUND,
|
||||
"boot_sound",
|
||||
"play_boot_sound_with_timeout");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_AUDIO_RELEASE,
|
||||
"audio_release",
|
||||
"release_idle_audio_resources");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_LCD_DEVICE,
|
||||
"lcd_device",
|
||||
"initialize_lcd_device");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_LCD_RENDER_LOOP,
|
||||
"lcd_render_loop",
|
||||
"start_lcd_render_loop");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_BOOT_EXPRESSION,
|
||||
"boot_expression",
|
||||
"request_boot_default_expression");
|
||||
|
||||
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_PLATFORM_DOMAIN,
|
||||
"platform_domain",
|
||||
"control_plane_init");
|
||||
esp_err_t err = control_plane_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Connectivity bootstrap failed: %s", esp_err_to_name(err));
|
||||
ESP_LOGE(TAG, "control_plane_init failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
static void app_composition_run_boot_orchestration(void)
|
||||
{
|
||||
#if CONFIG_TQ_BOOT_TURNTABLE_CALIBRATION_ENABLE
|
||||
app_composition_run_boot_turntable_calibration();
|
||||
#else
|
||||
ESP_LOGI(TAG, "Boot turntable calibration disabled by configuration");
|
||||
#endif
|
||||
app_composition_connect_wifi_for_boot();
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_TURNTABLE_CALIBRATION,
|
||||
"turntable_calibration",
|
||||
"run_turntable_calibration_with_timeout");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE,
|
||||
"storage_prepare",
|
||||
"prepare_runtime_storage");
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_WIFI_BOOTSTRAP,
|
||||
"wifi_bootstrap",
|
||||
"start_wifi_sta_or_config_ap");
|
||||
|
||||
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_RUNTIME_WORKERS,
|
||||
"runtime_workers",
|
||||
"start_event_observer_runtime");
|
||||
err = control_plane_start_event_observer_runtime();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "event observer runtime failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
app_composition_log_stage_dry_run(APP_COMPOSITION_BOOT_STAGE_NETWORK_SERVICES,
|
||||
"network_services",
|
||||
"start_websocket_fortune_lan_http_after_network_ready");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t app_composition_start_default_runtime(void)
|
||||
@@ -57,42 +114,15 @@ esp_err_t app_composition_start_default_runtime(void)
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
|
||||
ESP_LOGI(TAG, "========================================");
|
||||
ESP_LOGI(TAG, " App Composition Boot");
|
||||
ESP_LOGI(TAG, " App Composition Boot Plan A");
|
||||
ESP_LOGI(TAG, " dry_run=%d", CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN);
|
||||
ESP_LOGI(TAG, "========================================");
|
||||
|
||||
#if CONFIG_TQ_SCREEN_ENABLE
|
||||
esp_err_t lcd_err = control_plane_init_lcd_device();
|
||||
if (lcd_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "LCD init failed: %s", esp_err_to_name(lcd_err));
|
||||
}
|
||||
#if CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN
|
||||
return app_composition_run_plan_a_boot_sequence();
|
||||
#else
|
||||
ESP_LOGI(TAG, "LCD init disabled by configuration");
|
||||
ESP_LOGE(TAG, "Non-dry-run Plan A boot is not enabled in this refactor phase");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#endif
|
||||
|
||||
esp_err_t err = control_plane_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Start default runtime failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t cli_err = control_plane_factory_cli_start();
|
||||
if (cli_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Factory serial CLI start failed: %s", esp_err_to_name(cli_err));
|
||||
}
|
||||
|
||||
err = control_plane_prepare_runtime_storage();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Start default runtime failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
app_composition_run_boot_orchestration();
|
||||
|
||||
err = control_plane_start_runtime_workers();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Start default runtime failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ idf_component_register(
|
||||
"src/multimodal/control_plane_multimodal_session_service.c"
|
||||
"src/runtime/control_plane_connectivity_runtime.c"
|
||||
"src/runtime/control_plane_fortune_service.c"
|
||||
"src/runtime/control_plane_system_events.c"
|
||||
"src/runtime/control_plane_key_input.c"
|
||||
"src/runtime/control_plane_runtime_workers.c"
|
||||
"src/runtime/control_plane_factory_cli.c"
|
||||
INCLUDE_DIRS
|
||||
@@ -19,6 +21,8 @@ idf_component_register(
|
||||
"internal"
|
||||
PRIV_REQUIRES
|
||||
domain
|
||||
esp_driver_gpio
|
||||
esp_timer
|
||||
esp_system
|
||||
freertos
|
||||
json
|
||||
|
||||
@@ -183,6 +183,16 @@ esp_err_t control_plane_multimodal_update_info(
|
||||
esp_err_t control_plane_start_runtime_workers(void);
|
||||
void control_plane_stop_runtime_workers(void);
|
||||
|
||||
/**
|
||||
* @brief Start the staged event observer runtime only.
|
||||
*
|
||||
* This starts the system event dispatcher and key input observer so module
|
||||
* events can be verified through logs. It intentionally does not start LCD,
|
||||
* Wi-Fi, WebSocket, fortune, LAN HTTP, motor, or audio business workers.
|
||||
*/
|
||||
esp_err_t control_plane_start_event_observer_runtime(void);
|
||||
void control_plane_stop_event_observer_runtime(void);
|
||||
|
||||
/**
|
||||
* @brief Daily fortune policy (control_plane-owned).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
|
||||
typedef enum {
|
||||
CONTROL_PLANE_EVENT_FAMILY_KEY = 0,
|
||||
CONTROL_PLANE_EVENT_FAMILY_WIFI,
|
||||
CONTROL_PLANE_EVENT_FAMILY_MULTIMODAL,
|
||||
CONTROL_PLANE_EVENT_FAMILY_LCD,
|
||||
CONTROL_PLANE_EVENT_FAMILY_DIRECTION,
|
||||
CONTROL_PLANE_EVENT_FAMILY_CALIBRATION,
|
||||
CONTROL_PLANE_EVENT_FAMILY_RUNTIME,
|
||||
} control_plane_event_family_t;
|
||||
|
||||
typedef enum {
|
||||
CONTROL_PLANE_KEY_ID_PTT = 0,
|
||||
CONTROL_PLANE_KEY_ID_SW1,
|
||||
CONTROL_PLANE_KEY_ID_SW2,
|
||||
} control_plane_key_id_t;
|
||||
|
||||
typedef enum {
|
||||
CONTROL_PLANE_KEY_EVENT_PRESSED = 0,
|
||||
CONTROL_PLANE_KEY_EVENT_RELEASED,
|
||||
CONTROL_PLANE_KEY_EVENT_SHORT_PRESS,
|
||||
CONTROL_PLANE_KEY_EVENT_LONG_PRESS_START,
|
||||
CONTROL_PLANE_KEY_EVENT_LONG_PRESS_RELEASE,
|
||||
CONTROL_PLANE_KEY_EVENT_DOUBLE_CLICK,
|
||||
CONTROL_PLANE_KEY_EVENT_REPEAT,
|
||||
CONTROL_PLANE_KEY_EVENT_CANCELLED,
|
||||
} control_plane_key_event_type_t;
|
||||
|
||||
typedef struct {
|
||||
control_plane_key_id_t key_id;
|
||||
control_plane_key_event_type_t type;
|
||||
gpio_num_t gpio_num;
|
||||
int level;
|
||||
int64_t timestamp_ms;
|
||||
uint32_t press_duration_ms;
|
||||
uint32_t click_count;
|
||||
} control_plane_key_event_t;
|
||||
|
||||
typedef struct {
|
||||
control_plane_event_family_t family;
|
||||
int64_t timestamp_ms;
|
||||
union {
|
||||
control_plane_key_event_t key;
|
||||
} data;
|
||||
} control_plane_system_event_t;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "control_plane.h"
|
||||
#include "domain.h"
|
||||
#include "esp_err.h"
|
||||
#include "control_plane_events.h"
|
||||
|
||||
control_plane_direction_command_t control_plane_direction_parse_command_internal(
|
||||
const char *input,
|
||||
@@ -112,3 +113,13 @@ bool control_plane_multimodal_is_uplink_running(void);
|
||||
esp_err_t control_plane_multimodal_play_downstream_audio(const uint8_t *data, size_t len);
|
||||
esp_err_t control_plane_multimodal_handle_ptt_press_step(void);
|
||||
esp_err_t control_plane_multimodal_handle_ptt_release(void);
|
||||
|
||||
esp_err_t control_plane_start_system_event_task(void);
|
||||
void control_plane_stop_system_event_task(void);
|
||||
esp_err_t control_plane_post_system_event(const control_plane_system_event_t *event, uint32_t timeout_ms);
|
||||
int64_t control_plane_event_timestamp_ms(void);
|
||||
const char *control_plane_key_id_name(control_plane_key_id_t key_id);
|
||||
const char *control_plane_key_event_type_name(control_plane_key_event_type_t type);
|
||||
|
||||
esp_err_t control_plane_start_key_input_task(void);
|
||||
void control_plane_stop_key_input_task(void);
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
/*
|
||||
* 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_INPUT_POLL_MS
|
||||
#define CONFIG_TQ_KEY_INPUT_POLL_MS 20
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN
|
||||
#define CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN
|
||||
#define CONFIG_TQ_CALIBRATION_SWITCH_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 long_press_sent;
|
||||
bool click_pending;
|
||||
int64_t debounce_deadline_ms;
|
||||
int64_t press_start_ms;
|
||||
int64_t click_deadline_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,
|
||||
.enable_short_press = false,
|
||||
.enable_long_press = false,
|
||||
.enable_double_click = false,
|
||||
},
|
||||
{
|
||||
.key_id = CONTROL_PLANE_KEY_ID_SW1,
|
||||
.gpio_num = CONFIG_TQ_CALIBRATION_SWITCH_GPIO,
|
||||
.active_level = CONFIG_TQ_CALIBRATION_SWITCH_ACTIVE_LEVEL,
|
||||
.pullup_enabled = CONFIG_TQ_CALIBRATION_SWITCH_PULLUP,
|
||||
.pulldown_enabled = CONFIG_TQ_CALIBRATION_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,
|
||||
},
|
||||
{
|
||||
.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 (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;
|
||||
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->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);
|
||||
s_key_states[i].debounced_pressed =
|
||||
control_plane_key_level_is_pressed(config, s_key_states[i].raw_level);
|
||||
|
||||
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];
|
||||
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 = xTaskCreate(control_plane_key_input_task,
|
||||
"cp_key_input",
|
||||
CONFIG_TQ_KEY_INPUT_TASK_STACK,
|
||||
NULL,
|
||||
5,
|
||||
&s_key_input_task);
|
||||
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
|
||||
}
|
||||
@@ -1675,6 +1675,12 @@ esp_err_t control_plane_start_runtime_workers(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
esp_err_t system_event_err = control_plane_start_system_event_task();
|
||||
if (system_event_err != ESP_OK) {
|
||||
control_plane_stop_runtime_workers();
|
||||
return system_event_err;
|
||||
}
|
||||
|
||||
if (control_plane_get_task_handle(&s_ptt_task) == NULL) {
|
||||
esp_err_t task_err = control_plane_create_task(control_plane_ptt_task,
|
||||
"cp_ptt",
|
||||
@@ -1703,6 +1709,12 @@ esp_err_t control_plane_start_runtime_workers(void)
|
||||
ESP_LOGI(TAG, "Slot switch worker disabled by configuration");
|
||||
#endif
|
||||
|
||||
esp_err_t key_input_err = control_plane_start_key_input_task();
|
||||
if (key_input_err != ESP_OK) {
|
||||
control_plane_stop_runtime_workers();
|
||||
return key_input_err;
|
||||
}
|
||||
|
||||
#if CONFIG_FORTUNE_ENABLE
|
||||
if (control_plane_get_task_handle(&s_daily_fortune_task) == NULL) {
|
||||
control_plane_request_fortune_refresh();
|
||||
@@ -1721,6 +1733,34 @@ esp_err_t control_plane_start_runtime_workers(void)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t control_plane_start_event_observer_runtime(void)
|
||||
{
|
||||
control_plane_set_runtime_stop_requested(false);
|
||||
|
||||
esp_err_t system_event_err = control_plane_start_system_event_task();
|
||||
if (system_event_err != ESP_OK) {
|
||||
control_plane_stop_event_observer_runtime();
|
||||
return system_event_err;
|
||||
}
|
||||
|
||||
esp_err_t key_input_err = control_plane_start_key_input_task();
|
||||
if (key_input_err != ESP_OK) {
|
||||
control_plane_stop_event_observer_runtime();
|
||||
return key_input_err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Event observer runtime started");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void control_plane_stop_event_observer_runtime(void)
|
||||
{
|
||||
control_plane_set_runtime_stop_requested(true);
|
||||
control_plane_stop_key_input_task();
|
||||
control_plane_stop_system_event_task();
|
||||
control_plane_set_runtime_stop_requested(false);
|
||||
}
|
||||
|
||||
void control_plane_stop_runtime_workers(void)
|
||||
{
|
||||
control_plane_set_runtime_stop_requested(true);
|
||||
@@ -1741,6 +1781,9 @@ void control_plane_stop_runtime_workers(void)
|
||||
(void)xQueueSend(queue, &stop_cmd, 0);
|
||||
}
|
||||
|
||||
control_plane_stop_key_input_task();
|
||||
control_plane_stop_system_event_task();
|
||||
|
||||
esp_err_t lcd_stop_err = control_plane_stop_lcd_render_loop(CONTROL_PLANE_TASK_STOP_WAIT_MS);
|
||||
if (lcd_stop_err == ESP_ERR_TIMEOUT) {
|
||||
control_plane_set_task_handle(&s_lcd_render_loop_task, NULL);
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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 "control_plane_events.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifndef CONFIG_TQ_SYSTEM_EVENT_ENABLE
|
||||
#define CONFIG_TQ_SYSTEM_EVENT_ENABLE 1
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_SYSTEM_EVENT_QUEUE_LEN
|
||||
#define CONFIG_TQ_SYSTEM_EVENT_QUEUE_LEN 32
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_SYSTEM_EVENT_TASK_STACK
|
||||
#define CONFIG_TQ_SYSTEM_EVENT_TASK_STACK 4096
|
||||
#endif
|
||||
|
||||
static const char *TAG = "cp_system_event";
|
||||
|
||||
static QueueHandle_t s_system_event_queue;
|
||||
static TaskHandle_t s_system_event_task;
|
||||
static volatile bool s_stop_requested;
|
||||
|
||||
static const char *control_plane_event_family_name(control_plane_event_family_t family)
|
||||
{
|
||||
switch (family) {
|
||||
case CONTROL_PLANE_EVENT_FAMILY_KEY:
|
||||
return "KEY";
|
||||
case CONTROL_PLANE_EVENT_FAMILY_WIFI:
|
||||
return "WIFI";
|
||||
case CONTROL_PLANE_EVENT_FAMILY_MULTIMODAL:
|
||||
return "MULTIMODAL";
|
||||
case CONTROL_PLANE_EVENT_FAMILY_LCD:
|
||||
return "LCD";
|
||||
case CONTROL_PLANE_EVENT_FAMILY_DIRECTION:
|
||||
return "DIRECTION";
|
||||
case CONTROL_PLANE_EVENT_FAMILY_CALIBRATION:
|
||||
return "CALIBRATION";
|
||||
case CONTROL_PLANE_EVENT_FAMILY_RUNTIME:
|
||||
return "RUNTIME";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const char *control_plane_key_id_name(control_plane_key_id_t key_id)
|
||||
{
|
||||
switch (key_id) {
|
||||
case CONTROL_PLANE_KEY_ID_PTT:
|
||||
return "PTT";
|
||||
case CONTROL_PLANE_KEY_ID_SW1:
|
||||
return "SW1";
|
||||
case CONTROL_PLANE_KEY_ID_SW2:
|
||||
return "SW2";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const char *control_plane_key_event_type_name(control_plane_key_event_type_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case CONTROL_PLANE_KEY_EVENT_PRESSED:
|
||||
return "PRESSED";
|
||||
case CONTROL_PLANE_KEY_EVENT_RELEASED:
|
||||
return "RELEASED";
|
||||
case CONTROL_PLANE_KEY_EVENT_SHORT_PRESS:
|
||||
return "SHORT_PRESS";
|
||||
case CONTROL_PLANE_KEY_EVENT_LONG_PRESS_START:
|
||||
return "LONG_PRESS_START";
|
||||
case CONTROL_PLANE_KEY_EVENT_LONG_PRESS_RELEASE:
|
||||
return "LONG_PRESS_RELEASE";
|
||||
case CONTROL_PLANE_KEY_EVENT_DOUBLE_CLICK:
|
||||
return "DOUBLE_CLICK";
|
||||
case CONTROL_PLANE_KEY_EVENT_REPEAT:
|
||||
return "REPEAT";
|
||||
case CONTROL_PLANE_KEY_EVENT_CANCELLED:
|
||||
return "CANCELLED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static void control_plane_dispatch_key_event(const control_plane_key_event_t *event)
|
||||
{
|
||||
if (event == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"key event key=%s type=%s gpio=%d level=%d duration=%" PRIu32 "ms clicks=%" PRIu32,
|
||||
control_plane_key_id_name(event->key_id),
|
||||
control_plane_key_event_type_name(event->type),
|
||||
(int)event->gpio_num,
|
||||
event->level,
|
||||
event->press_duration_ms,
|
||||
event->click_count);
|
||||
}
|
||||
|
||||
static void control_plane_dispatch_system_event(const control_plane_system_event_t *event)
|
||||
{
|
||||
if (event == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "dispatch family=%s", control_plane_event_family_name(event->family));
|
||||
|
||||
switch (event->family) {
|
||||
case CONTROL_PLANE_EVENT_FAMILY_KEY:
|
||||
control_plane_dispatch_key_event(&event->data.key);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "unhandled event family=%s", control_plane_event_family_name(event->family));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void control_plane_system_event_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_LOGI(TAG,
|
||||
"System event task started queue_len=%d",
|
||||
CONFIG_TQ_SYSTEM_EVENT_QUEUE_LEN);
|
||||
|
||||
while (!s_stop_requested) {
|
||||
control_plane_system_event_t event = {0};
|
||||
if (xQueueReceive(s_system_event_queue, &event, pdMS_TO_TICKS(200)) == pdTRUE) {
|
||||
control_plane_dispatch_system_event(&event);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "System event task stopped");
|
||||
s_system_event_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t control_plane_post_system_event(const control_plane_system_event_t *event, uint32_t timeout_ms)
|
||||
{
|
||||
#if CONFIG_TQ_SYSTEM_EVENT_ENABLE
|
||||
if (event == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_system_event_queue == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
|
||||
if (xQueueSend(s_system_event_queue, event, timeout_ticks) != pdTRUE) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
return ESP_OK;
|
||||
#else
|
||||
(void)event;
|
||||
(void)timeout_ms;
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
int64_t control_plane_event_timestamp_ms(void)
|
||||
{
|
||||
return esp_timer_get_time() / 1000;
|
||||
}
|
||||
|
||||
esp_err_t control_plane_start_system_event_task(void)
|
||||
{
|
||||
#if CONFIG_TQ_SYSTEM_EVENT_ENABLE
|
||||
if (s_system_event_task != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (s_system_event_queue == NULL) {
|
||||
s_system_event_queue = xQueueCreate(CONFIG_TQ_SYSTEM_EVENT_QUEUE_LEN,
|
||||
sizeof(control_plane_system_event_t));
|
||||
if (s_system_event_queue == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
s_stop_requested = false;
|
||||
BaseType_t created = xTaskCreate(control_plane_system_event_task,
|
||||
"cp_system_event",
|
||||
CONFIG_TQ_SYSTEM_EVENT_TASK_STACK,
|
||||
NULL,
|
||||
4,
|
||||
&s_system_event_task);
|
||||
if (created != pdPASS) {
|
||||
s_system_event_task = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
#else
|
||||
ESP_LOGI(TAG, "System event task disabled by configuration");
|
||||
return ESP_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
void control_plane_stop_system_event_task(void)
|
||||
{
|
||||
#if CONFIG_TQ_SYSTEM_EVENT_ENABLE
|
||||
s_stop_requested = true;
|
||||
|
||||
for (int i = 0; i < 20 && s_system_event_task != NULL; ++i) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
if (s_system_event_task != NULL) {
|
||||
vTaskDelete(s_system_event_task);
|
||||
s_system_event_task = NULL;
|
||||
}
|
||||
|
||||
if (s_system_event_queue != NULL) {
|
||||
vQueueDelete(s_system_event_queue);
|
||||
s_system_event_queue = NULL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
# Boot Sequence Plan A
|
||||
|
||||
This document captures the selected startup sequence for the "sound-first"
|
||||
boot experience.
|
||||
|
||||
## Product Goal
|
||||
|
||||
Plan A prioritizes immediate audible feedback after power-on. The device should
|
||||
play the boot sound before display startup, turntable calibration, Wi-Fi
|
||||
bootstrap, or network workers begin.
|
||||
|
||||
Expected user perception:
|
||||
|
||||
1. Power is applied.
|
||||
2. The boot sound starts as early as possible.
|
||||
3. The display comes up after the sound path has completed or timed out.
|
||||
4. Mechanical calibration and network startup happen after the audible boot
|
||||
feedback.
|
||||
|
||||
The tradeoff is that the user may hear the boot sound while the screen is still
|
||||
dark. This is accepted for this plan.
|
||||
|
||||
## Startup Order
|
||||
|
||||
```text
|
||||
power hold
|
||||
-> audio init
|
||||
-> play boot sound with timeout
|
||||
-> audio deinit or idle release
|
||||
-> LCD device init
|
||||
-> LCD render loop start
|
||||
-> request boot/default expression
|
||||
-> platform/domain init
|
||||
-> optional turntable calibration
|
||||
-> storage prepare
|
||||
-> Wi-Fi STA/AP bootstrap
|
||||
-> runtime workers
|
||||
-> WebSocket / fortune / LAN HTTP services
|
||||
```
|
||||
|
||||
## Stage Details
|
||||
|
||||
| Order | Stage | Wait policy | Failure policy | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Power hold GPIO | Wait | Continue only if board power remains stable | Configure the hold GPIO and assert the active level before any heavier work. |
|
||||
| 2 | Audio init | Wait with bounded timeout | Log and skip boot sound | Initializes the codec path needed for playback: I2C, I2S, codec, PA control, codec buffers, and decoder resources. |
|
||||
| 3 | Boot sound playback | Wait for completion or timeout | Stop playback and continue boot | The sound must not block startup indefinitely. Use a short product-defined timeout. |
|
||||
| 4 | Audio release | Wait for synchronous release | Continue if release reports a recoverable error | If Wi-Fi/TLS starts soon after, deinit or release idle audio resources to reduce DMA/internal heap pressure. |
|
||||
| 5 | LCD device init | Wait | Continue with display disabled if init fails | Initializes SPI, panel, LVGL, draw buffers, tick timer, and backlight. |
|
||||
| 6 | LCD render loop | Start and continue | Continue with display disabled if task creation fails | Needed for the default boot image or animation to appear. |
|
||||
| 7 | Boot/default expression | Do not wait for animation completion | Ignore request failure when LCD is unavailable | This is the first visible boot state. |
|
||||
| 8 | Platform/domain init | Wait | Stop boot on unrecoverable error | Performs stepper electrical initialization and domain direction state setup. |
|
||||
| 9 | Turntable calibration | Product decision; default wait with timeout | Continue with degraded direction accuracy on timeout/failure | This is the mechanical SW1 homing/calibration process, not the stepper electrical init. |
|
||||
| 10 | Storage prepare | Wait | Stop features that need persistent state | Initializes NVS before Wi-Fi credentials, identity, auth, and fortune state are read or written. |
|
||||
| 11 | Wi-Fi bootstrap | Wait until STA connected or AP config mode starts | Enter config AP or keep retry policy active | Must not overlap with boot sound playback or heavy audio initialization. |
|
||||
| 12 | Runtime workers | Start and continue | Stop only failed dependent feature | Starts PTT, slot switch, Wi-Fi state monitor, model command worker, LCD runtime worker, and WebSocket wait worker as configured. |
|
||||
| 13 | Network services | Async after network readiness | Retry using existing policy | Includes multimodal WebSocket, LAN HTTP config server, SNTP, HTTPS fortune refresh, and auth flows. |
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Display Startup
|
||||
|
||||
"LCD device init" means the screen hardware is available and the backlight can
|
||||
be enabled. It does not guarantee that a boot image is visible.
|
||||
|
||||
Showing a default boot image requires the render loop to be running and a
|
||||
presentation/expression request to be accepted.
|
||||
|
||||
### Stepper Startup
|
||||
|
||||
Stepper electrical initialization only configures the four motor phase GPIOs,
|
||||
creates the motion mutex, resets the internal phase state, and powers off the
|
||||
motor coils.
|
||||
|
||||
It is not the same as turntable calibration. Turntable calibration is the
|
||||
mechanical homing process that rotates until the SW1 calibration switch is
|
||||
detected, then updates the domain direction state.
|
||||
|
||||
### Boot Sound Placement
|
||||
|
||||
The boot sound can run before LCD and stepper initialization because it does
|
||||
not depend on display, turntable, Wi-Fi, or NVS when the sound asset is embedded
|
||||
or otherwise available without persistent storage.
|
||||
|
||||
If the selected sound asset is stored in NVS or another mounted filesystem, the
|
||||
storage/filesystem prerequisite must be moved before audio playback.
|
||||
|
||||
## Resource Rules
|
||||
|
||||
- Do not run audio init/playback concurrently with Wi-Fi STA connect, WebSocket
|
||||
TLS handshake, HTTPS requests, or fortune refresh.
|
||||
- Prefer releasing idle audio resources before Wi-Fi bootstrap to reduce
|
||||
internal heap and DMA-capable memory pressure.
|
||||
- Do not initialize LCD/LVGL draw buffers concurrently with audio init unless
|
||||
memory measurements prove it is stable.
|
||||
- Boot sound playback must have bounded completion semantics.
|
||||
- Audio startup failure must not prevent display, calibration, or Wi-Fi startup.
|
||||
|
||||
## Current Implementation Gap
|
||||
|
||||
The current `voice_prompt_play_file()` implementation is a stub that returns
|
||||
`ESP_ERR_NOT_SUPPORTED`. Plan A therefore requires an audio playback
|
||||
implementation before the boot sound can actually be heard.
|
||||
|
||||
Recommended ownership:
|
||||
|
||||
- `control_plane` owns the boot-sound startup policy and timeout.
|
||||
- `domain` owns the semantic boot prompt contract if the prompt is exposed
|
||||
across layers.
|
||||
- `platform` owns the concrete audio playback primitive and hardware resource
|
||||
lifecycle.
|
||||
- `app_composition` only sequences the startup stages.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The current startup path initializes LCD before `control_plane_init()`. Plan A
|
||||
requires moving boot audio ahead of LCD startup, while keeping the logic inside
|
||||
the existing layering rules:
|
||||
|
||||
```text
|
||||
app_composition
|
||||
-> control_plane boot sound policy
|
||||
-> domain semantic/runtime adapter
|
||||
-> platform audio playback primitive
|
||||
```
|
||||
|
||||
Avoid direct `platform_*` audio calls from `app_composition`.
|
||||
|
||||
The boot sound API should define:
|
||||
|
||||
- timeout behavior,
|
||||
- whether playback is synchronous or asynchronous,
|
||||
- what happens after timeout,
|
||||
- whether audio resources remain initialized after playback,
|
||||
- idempotent behavior for repeated boot calls.
|
||||
|
||||
@@ -47,6 +47,15 @@ menu "Example Configuration"
|
||||
|
||||
menu "Runtime Diagnostics"
|
||||
|
||||
config TQ_BOOT_SEQUENCE_DRY_RUN
|
||||
bool "Run Plan A boot sequence as event log only"
|
||||
default y
|
||||
help
|
||||
Use the Plan A startup order as a dry-run orchestration log.
|
||||
Only the event observer runtime is started. Business modules
|
||||
such as LCD, Wi-Fi, WebSocket, fortune, LAN HTTP, calibration,
|
||||
and boot audio are not actually triggered.
|
||||
|
||||
config TQ_TASK_MONITOR_ENABLE
|
||||
bool "Enable periodic FreeRTOS task monitor log"
|
||||
default y
|
||||
@@ -69,6 +78,82 @@ menu "Example Configuration"
|
||||
help
|
||||
Maximum FreeRTOS task entries captured per report.
|
||||
|
||||
config TQ_SYSTEM_EVENT_ENABLE
|
||||
bool "Enable control-plane system event task"
|
||||
default y
|
||||
help
|
||||
Start the staged system event dispatcher task.
|
||||
|
||||
config TQ_SYSTEM_EVENT_QUEUE_LEN
|
||||
int "System event queue length"
|
||||
default 32
|
||||
range 8 128
|
||||
depends on TQ_SYSTEM_EVENT_ENABLE
|
||||
help
|
||||
Maximum number of queued control-plane system events.
|
||||
|
||||
config TQ_SYSTEM_EVENT_TASK_STACK
|
||||
int "System event task stack size"
|
||||
default 4096
|
||||
range 2048 8192
|
||||
depends on TQ_SYSTEM_EVENT_ENABLE
|
||||
help
|
||||
Stack size in bytes for the control-plane system event task.
|
||||
|
||||
config TQ_KEY_INPUT_ENABLE
|
||||
bool "Enable interrupt-driven key input task"
|
||||
default y
|
||||
help
|
||||
Start the staged GPIO interrupt key input task for PTT, SW1, and SW2.
|
||||
|
||||
config TQ_KEY_INPUT_TASK_STACK
|
||||
int "Key input task stack size"
|
||||
default 4096
|
||||
range 2048 8192
|
||||
depends on TQ_KEY_INPUT_ENABLE
|
||||
help
|
||||
Stack size in bytes for the key input state-machine task.
|
||||
|
||||
config TQ_KEY_IRQ_QUEUE_LEN
|
||||
int "Key GPIO interrupt queue length"
|
||||
default 16
|
||||
range 4 64
|
||||
depends on TQ_KEY_INPUT_ENABLE
|
||||
help
|
||||
Maximum number of queued raw GPIO-change events from ISR context.
|
||||
|
||||
config TQ_KEY_INPUT_POLL_MS
|
||||
int "Key input timer poll interval (ms)"
|
||||
default 20
|
||||
range 5 100
|
||||
depends on TQ_KEY_INPUT_ENABLE
|
||||
help
|
||||
Maximum timer polling delay used for debounce, long press, and click decisions.
|
||||
|
||||
config TQ_KEY_DEBOUNCE_INTERVAL_MS
|
||||
int "Key debounce interval (ms)"
|
||||
default 20
|
||||
range 5 200
|
||||
depends on TQ_KEY_INPUT_ENABLE
|
||||
help
|
||||
Stable-level interval required before a raw GPIO change becomes a key event.
|
||||
|
||||
config TQ_KEY_LONG_PRESS_MS
|
||||
int "Key long-press threshold (ms)"
|
||||
default 800
|
||||
range 100 5000
|
||||
depends on TQ_KEY_INPUT_ENABLE
|
||||
help
|
||||
Press duration required before emitting a long-press-start event.
|
||||
|
||||
config TQ_KEY_DOUBLE_CLICK_MS
|
||||
int "Key double-click threshold (ms)"
|
||||
default 300
|
||||
range 100 1000
|
||||
depends on TQ_KEY_INPUT_ENABLE
|
||||
help
|
||||
Maximum gap between short clicks before emitting a double-click event.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "TQ Screen"
|
||||
|
||||
@@ -14,8 +14,19 @@ CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY=y
|
||||
CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y
|
||||
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
|
||||
CONFIG_TQ_TASK_MONITOR_ENABLE=y
|
||||
CONFIG_TQ_BOOT_SEQUENCE_DRY_RUN=y
|
||||
CONFIG_TQ_TASK_MONITOR_INTERVAL_MS=10000
|
||||
CONFIG_TQ_TASK_MONITOR_MAX_TASKS=64
|
||||
CONFIG_TQ_SYSTEM_EVENT_ENABLE=y
|
||||
CONFIG_TQ_SYSTEM_EVENT_QUEUE_LEN=32
|
||||
CONFIG_TQ_SYSTEM_EVENT_TASK_STACK=4096
|
||||
CONFIG_TQ_KEY_INPUT_ENABLE=y
|
||||
CONFIG_TQ_KEY_INPUT_TASK_STACK=4096
|
||||
CONFIG_TQ_KEY_IRQ_QUEUE_LEN=16
|
||||
CONFIG_TQ_KEY_INPUT_POLL_MS=20
|
||||
CONFIG_TQ_KEY_DEBOUNCE_INTERVAL_MS=20
|
||||
CONFIG_TQ_KEY_LONG_PRESS_MS=800
|
||||
CONFIG_TQ_KEY_DOUBLE_CLICK_MS=300
|
||||
|
||||
# Internal DMA-capable SRAM budget. TLS/auth/fortune flows, hardware AES DMA
|
||||
# bounce buffers, LCD DMA, and worker task stacks all need internal-only memory.
|
||||
|
||||
Reference in New Issue
Block a user