按键系统待测

This commit is contained in:
CSBOOK\44744
2026-06-25 01:20:23 +08:00
parent 3cce1be162
commit 84ecf892b5
11 changed files with 1187 additions and 61 deletions
@@ -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;
}
+4
View File
@@ -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
}
+136
View File
@@ -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.
+85
View File
@@ -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"
+11
View File
@@ -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.