From 84ecf892b532457f34c655cb8925eb9cedcfaaf1 Mon Sep 17 00:00:00 2001 From: "CSBOOK\\44744" <447449914@qq.com> Date: Thu, 25 Jun 2026 01:20:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E9=94=AE=E7=B3=BB=E7=BB=9F=E5=BE=85?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/app_composition_default_runtime.c | 152 +++--- components/control_plane/CMakeLists.txt | 4 + .../control_plane/include/control_plane.h | 10 + .../internal/control_plane_events.h | 57 ++ .../internal/control_plane_internal.h | 11 + .../src/runtime/control_plane_key_input.c | 508 ++++++++++++++++++ .../runtime/control_plane_runtime_workers.c | 43 ++ .../src/runtime/control_plane_system_events.c | 231 ++++++++ docs/boot-sequence-plan-a.md | 136 +++++ main/Kconfig.projbuild | 85 +++ sdkconfig.defaults | 11 + 11 files changed, 1187 insertions(+), 61 deletions(-) create mode 100644 components/control_plane/internal/control_plane_events.h create mode 100644 components/control_plane/src/runtime/control_plane_key_input.c create mode 100644 components/control_plane/src/runtime/control_plane_system_events.c create mode 100644 docs/boot-sequence-plan-a.md diff --git a/components/app_composition/src/runtime/app_composition_default_runtime.c b/components/app_composition/src/runtime/app_composition_default_runtime.c index 7d5dc55..3ce890e 100644 --- a/components/app_composition/src/runtime/app_composition_default_runtime.c +++ b/components/app_composition/src/runtime/app_composition_default_runtime.c @@ -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; } + diff --git a/components/control_plane/CMakeLists.txt b/components/control_plane/CMakeLists.txt index 233211f..1dd17b7 100644 --- a/components/control_plane/CMakeLists.txt +++ b/components/control_plane/CMakeLists.txt @@ -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 diff --git a/components/control_plane/include/control_plane.h b/components/control_plane/include/control_plane.h index d9da48e..2c3fca3 100644 --- a/components/control_plane/include/control_plane.h +++ b/components/control_plane/include/control_plane.h @@ -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). * diff --git a/components/control_plane/internal/control_plane_events.h b/components/control_plane/internal/control_plane_events.h new file mode 100644 index 0000000..288420a --- /dev/null +++ b/components/control_plane/internal/control_plane_events.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#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; + diff --git a/components/control_plane/internal/control_plane_internal.h b/components/control_plane/internal/control_plane_internal.h index 3719f28..ce0c3cb 100644 --- a/components/control_plane/internal/control_plane_internal.h +++ b/components/control_plane/internal/control_plane_internal.h @@ -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); diff --git a/components/control_plane/src/runtime/control_plane_key_input.c b/components/control_plane/src/runtime/control_plane_key_input.c new file mode 100644 index 0000000..50668cc --- /dev/null +++ b/components/control_plane/src/runtime/control_plane_key_input.c @@ -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 +#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_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 +} diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index 790d15d..8cd3d30 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -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); diff --git a/components/control_plane/src/runtime/control_plane_system_events.c b/components/control_plane/src/runtime/control_plane_system_events.c new file mode 100644 index 0000000..40d045d --- /dev/null +++ b/components/control_plane/src/runtime/control_plane_system_events.c @@ -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 +#include + +#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 +} diff --git a/docs/boot-sequence-plan-a.md b/docs/boot-sequence-plan-a.md new file mode 100644 index 0000000..0d4f831 --- /dev/null +++ b/docs/boot-sequence-plan-a.md @@ -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. + diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 7cfd86e..cffe16b 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -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" diff --git a/sdkconfig.defaults b/sdkconfig.defaults index d2dffcb..a20a4ff 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -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.