/** **************************************************************************************************** * @file app.c * @author HiFi Player Project * @version V1.2 * @date 2026-01-01 * @brief 应用管理器 + 全局按键服务 * * 模块职责: * 1. 唯一持有 XL9555 按键的扫描权(其它模块不再直接读按键,避免重复扫描/漏键)。 * 2. 按键去抖 + 区分短按/长按,统一投递到事件队列(消费者是 ui.c)。 * 3. 维护当前应用模式、主界面应用表与选择项,并提供“返回 / 切换应用”接口。 **************************************************************************************************** */ #include "app.h" #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "esp_log.h" #include "lvgl.h" #include "xl9555.h" #define TAG "app" /* ---------------- 按键服务参数 ---------------- */ #define KEY_SCAN_PERIOD_MS 10 /* 扫描周期 */ #define KEY_DEBOUNCE_COUNT 3 /* 连续 3 次一致才确认(约 30ms) */ #define KEY_LONG_PRESS_MS 700 /* 长按阈值 */ #define KEY_TASK_STACK (3 * 1024) #define KEY_TASK_PRIO 5 /* 低于播放器(6),高于 LVGL(4) */ #define EVENT_QUEUE_LEN 16 /* ---------------- 主界面应用表(新增应用只需在这里加一条) ---------------- */ static const app_entry_t s_apps[] = { { .icon = LV_SYMBOL_AUDIO, /* 音符:音乐播放器 */ .title = "MUSIC", .subtitle = "WAV files in SD /MUSIC", .mode = APP_MODE_PLAYER, .enabled = true, .color_from = 0x8E5BFF, .color_to = 0x3FB6F5, }, { .icon = LV_SYMBOL_DIRECTORY, /* 文件夹:SD 卡文件浏览 */ .title = "FILES", .subtitle = "Browse files on SD card", .mode = APP_MODE_NONE, .enabled = false, .color_from = 0x31C27C, .color_to = 0x1E9C6B, }, { .icon = LV_SYMBOL_SETTINGS, /* 齿轮:设置 */ .title = "SETTINGS", .subtitle = "Display, audio and keys", .mode = APP_MODE_NONE, .enabled = false, .color_from = 0x9AA3B5, .color_to = 0x6A7183, }, { .icon = LV_SYMBOL_LIST, /* 列表:关于 */ .title = "ABOUT", .subtitle = "HiFi Player V1.2", .mode = APP_MODE_NONE, .enabled = false, .color_from = 0xFFB020, .color_to = 0xF07C18, }, }; #define APP_COUNT ((int)(sizeof(s_apps) / sizeof(s_apps[0]))) /* ---------------- 内部状态 ---------------- */ typedef struct { uint8_t last_raw; /* 上次原始电平(1=按下) */ uint8_t stable; /* 已确认的稳定状态 */ uint8_t counter; /* 连续一致计数 */ TickType_t press_tick; /* 按下时刻 */ bool long_sent; /* 本次按住是否已上报长按 */ } key_state_t; static const uint16_t s_key_mask[APP_KEY_NUM] = { KEY0_IO, KEY1_IO, KEY2_IO, KEY3_IO }; static key_state_t s_keys[APP_KEY_NUM]; static QueueHandle_t s_event_queue = NULL; static app_mode_t s_mode = APP_MODE_HOME; static int s_selection = 0; /* ---------------- 按键事件投递 ---------------- */ static void key_post(uint8_t key, uint8_t type) { app_event_t ev = { .key = key, .type = type }; if (s_event_queue != NULL) { /* 队列满时丢弃新事件,绝不阻塞按键任务 */ (void)xQueueSend(s_event_queue, &ev, 0); } } /** * @brief 按键扫描任务(非阻塞去抖 + 长短按识别) */ static void key_task(void *arg) { (void)arg; memset(s_keys, 0, sizeof(s_keys)); while (1) { uint8_t data[2] = {0, 0}; if (xl9555_read_byte(data, 2) == ESP_OK) { uint16_t level = (uint16_t)(((uint16_t)data[1] << 8) | data[0]); TickType_t now = xTaskGetTickCount(); for (int i = 0; i < APP_KEY_NUM; i++) { key_state_t *k = &s_keys[i]; uint8_t raw = ((level & s_key_mask[i]) == 0) ? 1 : 0; /* 低电平有效 */ /* --- 1. 去抖计数 --- */ if (raw != k->last_raw) { k->last_raw = raw; k->counter = 0; } else if (k->counter < KEY_DEBOUNCE_COUNT) { k->counter++; } if (k->counter < KEY_DEBOUNCE_COUNT) { continue; /* 尚未稳定 */ } /* --- 2. 状态变化 --- */ if (raw != k->stable) { k->stable = raw; if (raw) /* 刚按下:记录时刻 */ { k->press_tick = now; k->long_sent = false; } else if (!k->long_sent) /* 松开且从未上报长按 → 短按 */ { key_post((uint8_t)i, APP_KEY_SHORT); } } /* --- 3. 按住不放 → 长按 --- */ else if (raw && !k->long_sent && ((now - k->press_tick) >= pdMS_TO_TICKS(KEY_LONG_PRESS_MS))) { k->long_sent = true; key_post((uint8_t)i, APP_KEY_LONG); } } } vTaskDelay(pdMS_TO_TICKS(KEY_SCAN_PERIOD_MS)); } } /* ---------------- 对外接口 ---------------- */ void app_start(void) { if (s_event_queue == NULL) { s_event_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(app_event_t)); if (s_event_queue == NULL) { ESP_LOGE(TAG, "create event queue failed"); return; } } s_mode = APP_MODE_HOME; s_selection = 0; if (xTaskCreate(key_task, "key_svc", KEY_TASK_STACK, NULL, KEY_TASK_PRIO, NULL) != pdPASS) { ESP_LOGE(TAG, "create key task failed"); } else { ESP_LOGI(TAG, "app manager started, %d app(s) in launcher", APP_COUNT); } } app_mode_t app_get_mode(void) { return s_mode; } void app_set_mode(app_mode_t mode) { if (mode == s_mode) { return; } s_mode = mode; /* 丢弃切换过程中的残留事件,避免“上一个界面的按键”触发新界面的动作 */ app_event_flush(); ESP_LOGI(TAG, "switch to mode %d", (int)mode); } void app_back(void) { if (s_mode != APP_MODE_HOME) { app_set_mode(APP_MODE_HOME); } } bool app_switch_next(void) { int count = 0; const app_entry_t *apps = app_get_entries(&count); if (count <= 0) { return false; } /* 找到当前模式的表项下标(找不到则视为 -1,从第 1 个开始找) */ int start = -1; for (int i = 0; i < count; i++) { if (apps[i].mode == s_mode) { start = i; break; } } /* 向后找下一个“可用且已实现”的应用 */ for (int step = 1; step <= count; step++) { int idx = (start + step + count) % count; if (apps[idx].enabled && (apps[idx].mode < APP_MODE_NUM)) { s_selection = idx; app_set_mode(apps[idx].mode); return true; } } return false; } const app_entry_t *app_get_entries(int *count) { if (count != NULL) { *count = APP_COUNT; } return s_apps; } int app_get_selection(void) { if ((s_selection < 0) || (s_selection >= APP_COUNT)) { s_selection = 0; } return s_selection; } void app_select_move(int delta) { if (APP_COUNT <= 0) { return; } s_selection = (app_get_selection() + delta) % APP_COUNT; if (s_selection < 0) { s_selection += APP_COUNT; } } bool app_event_poll(app_event_t *ev) { if ((s_event_queue == NULL) || (ev == NULL)) { return false; } return (xQueueReceive(s_event_queue, ev, 0) == pdTRUE); } bool app_event_wait(app_event_t *ev, uint32_t timeout_ms) { if ((s_event_queue == NULL) || (ev == NULL)) { return false; } TickType_t ticks = (timeout_ms == 0) ? 0 : pdMS_TO_TICKS(timeout_ms); return (xQueueReceive(s_event_queue, ev, ticks) == pdTRUE); } void app_event_flush(void) { if (s_event_queue != NULL) { (void)xQueueReset(s_event_queue); } }