#include "app_task.h" #include "application.h" #include "mqtt.h" #include "system.h" #include #include #include #include #include #include #include #define TAG "app_task" #define APP_TASK_NVS_NS "app_task" #define APP_TASK_NVS_KEY "pending" #define APP_TASK_JSON_MAX 768 #define APP_TASK_ID_MAX 32 #define APP_TASK_SOURCE_MAX 24 static char s_current_app[APP_TASK_ID_MAX] = "chat"; static char s_current_task[APP_TASK_ID_MAX] = "conversation"; static char s_current_source[APP_TASK_SOURCE_MAX] = "default_chat"; static int s_current_priority = 0; typedef struct { const char *app_id; const char *task_id; const char *skills[4]; } app_task_skill_map_t; /* Configuration table: channels only pass app_id/task_id; skills are looked up here. */ static const app_task_skill_map_t k_task_skills[] = { {"chat", "conversation", {NULL, NULL, NULL, NULL}}, {"english_learning", "repeater", {"self.mp3.play", NULL, NULL, NULL}}, {"english_translation", "live_translate", {"translate", NULL, NULL, NULL}}, }; int Board_AppTask_SourcePriority(const char *source) { if (!source) { return 1; } if (strcmp(source, "mqtt_immediate") == 0) { return 3; } if (strcmp(source, "schedule") == 0) { return 2; } if (strcmp(source, "voice") == 0) { return 1; } return 0; } int Board_AppTask_CurrentPriority(void) { return s_current_priority; } static const app_task_skill_map_t *find_task_spec(const char *app_id, const char *task_id) { if (!app_id) { return NULL; } const char *tid = (task_id && task_id[0]) ? task_id : NULL; for (size_t i = 0; i < sizeof(k_task_skills) / sizeof(k_task_skills[0]); ++i) { if (strcmp(k_task_skills[i].app_id, app_id) != 0) { continue; } if (!tid || strcmp(k_task_skills[i].task_id, tid) == 0) { return &k_task_skills[i]; } } return NULL; } static bool is_chat_task(const char *app_id, const char *task_id) { return app_id && strcmp(app_id, "chat") == 0 && (!task_id || task_id[0] == '\0' || strcmp(task_id, "conversation") == 0); } static void copy_id(char *dest, size_t dest_size, const char *src, const char *fallback) { const char *value = (src && src[0]) ? src : fallback; strlcpy(dest, value, dest_size); } esp_err_t Board_SavePendingTask(const char *app_id, const char *task_id, const char *params_json, const char *source, const char *alarm_id) { cJSON *root = cJSON_CreateObject(); if (!root) { return ESP_ERR_NO_MEM; } cJSON_AddStringToObject(root, "app_id", app_id && app_id[0] ? app_id : "chat"); cJSON_AddStringToObject(root, "task_id", task_id && task_id[0] ? task_id : "conversation"); cJSON_AddStringToObject(root, "source", source && source[0] ? source : "default_chat"); cJSON_AddStringToObject(root, "alarm_id", alarm_id ? alarm_id : ""); if (params_json && params_json[0]) { cJSON *params = cJSON_Parse(params_json); if (params) { cJSON_AddItemToObject(root, "params", params); } else { cJSON_AddRawToObject(root, "params", params_json); } } else { cJSON_AddItemToObject(root, "params", cJSON_CreateObject()); } char *printed = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (!printed) { return ESP_ERR_NO_MEM; } nvs_handle_t handle; esp_err_t err = nvs_open(APP_TASK_NVS_NS, NVS_READWRITE, &handle); if (err == ESP_OK) { err = nvs_set_str(handle, APP_TASK_NVS_KEY, printed); if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); } free(printed); return err; } static bool load_pending_fields(char *app_id, size_t app_size, char *task_id, size_t task_size, char *source, size_t source_size, char *params_json, size_t params_size) { nvs_handle_t handle; if (nvs_open(APP_TASK_NVS_NS, NVS_READONLY, &handle) != ESP_OK) { return false; } char buf[APP_TASK_JSON_MAX]; size_t length = sizeof(buf); esp_err_t err = nvs_get_str(handle, APP_TASK_NVS_KEY, buf, &length); nvs_close(handle); if (err != ESP_OK) { return false; } cJSON *root = cJSON_Parse(buf); if (!root) { return false; } const cJSON *app = cJSON_GetObjectItem(root, "app_id"); const cJSON *task = cJSON_GetObjectItem(root, "task_id"); const cJSON *src = cJSON_GetObjectItem(root, "source"); const cJSON *params = cJSON_GetObjectItem(root, "params"); copy_id(app_id, app_size, cJSON_IsString(app) ? app->valuestring : NULL, "chat"); copy_id(task_id, task_size, cJSON_IsString(task) ? task->valuestring : NULL, "conversation"); copy_id(source, source_size, cJSON_IsString(src) ? src->valuestring : NULL, "default_chat"); if (params_json && params_size > 0) { params_json[0] = '\0'; if (params) { char *printed = cJSON_PrintUnformatted(params); if (printed) { strlcpy(params_json, printed, params_size); free(printed); } } } cJSON_Delete(root); return app_id[0] != '\0'; } static void preempt_current_skills(void) { SDCard_Stop_Play(); BoardMqtt_StopPlaylistPlayback(); Application::GetInstance().Schedule([]() { auto state = Application::GetInstance().GetDeviceState(); if (state == kDeviceStateSpeaking) { Application::GetInstance().AbortSpeaking(kAbortReasonNone); } }); } static void notify_server(const char *app_id, const char *task_id, const char *source, bool open_if_idle) { char detect[160]; snprintf(detect, sizeof(detect), "[app_task]%s|%s|%s", app_id && app_id[0] ? app_id : "chat", task_id && task_id[0] ? task_id : "conversation", source && source[0] ? source : "default_chat"); Application::GetInstance().SendListenDetect(detect, open_if_idle); } static void dispatch_skill(const char *skill, const char *app_id, const char *task_id, const char *params_json, const char *source) { if (!skill || skill[0] == '\0') { notify_server(app_id, task_id, source, true); return; } if (strcmp(skill, "self.mp3.play") == 0) { char name[SDCARD_PATH_MAX_LEN] = {0}; float speed = 1.0f; int repeat = 1; bool power_off_after = false; if (params_json && params_json[0]) { cJSON *params = cJSON_Parse(params_json); if (params) { const cJSON *name_item = cJSON_GetObjectItem(params, "name"); const cJSON *speed_item = cJSON_GetObjectItem(params, "speed"); const cJSON *repeat_item = cJSON_GetObjectItem(params, "repeat"); const cJSON *power_item = cJSON_GetObjectItem(params, "power_off_after"); if (cJSON_IsString(name_item) && name_item->valuestring) { strlcpy(name, name_item->valuestring, sizeof(name)); } if (cJSON_IsNumber(speed_item)) { speed = static_cast(speed_item->valuedouble); } if (cJSON_IsNumber(repeat_item)) { repeat = repeat_item->valueint; } if (cJSON_IsBool(power_item)) { power_off_after = cJSON_IsTrue(power_item); } else if (cJSON_IsNumber(power_item)) { power_off_after = power_item->valueint != 0; } cJSON_Delete(params); } } if (name[0] == '\0') { ESP_LOGW(TAG, "self.mp3.play missing name"); return; } if (repeat < 1) { repeat = 1; } esp_err_t err = SDCard_Play_MP3_With_Speed_Repeat(name, speed, repeat, power_off_after); if (err != ESP_OK) { ESP_LOGW(TAG, "self.mp3.play failed: %s", esp_err_to_name(err)); Board_OnSkillPlaybackFinished(true, false); return; } notify_server(app_id, task_id, source, false); return; } if (strcmp(skill, "translate") == 0) { notify_server(app_id, task_id, source, true); return; } ESP_LOGW(TAG, "unsupported skill: %s", skill); } esp_err_t Board_StartAppTask(const char *app_id, const char *task_id, const char *params_json, const char *source) { if (!app_id || app_id[0] == '\0') { return ESP_ERR_INVALID_ARG; } const char *src = (source && source[0]) ? source : "mqtt_immediate"; const char *tid = (task_id && task_id[0]) ? task_id : "conversation"; const app_task_skill_map_t *spec = find_task_spec(app_id, tid); if (!spec) { ESP_LOGW(TAG, "unknown app/task: %s/%s", app_id, tid); return ESP_ERR_NOT_FOUND; } int new_priority = Board_AppTask_SourcePriority(src); if (new_priority < s_current_priority) { ESP_LOGW(TAG, "reject %s/%s source=%s prio %d < current %d", app_id, tid, src, new_priority, s_current_priority); return ESP_ERR_INVALID_STATE; } preempt_current_skills(); copy_id(s_current_app, sizeof(s_current_app), app_id, "chat"); copy_id(s_current_task, sizeof(s_current_task), tid, "conversation"); copy_id(s_current_source, sizeof(s_current_source), src, "default_chat"); s_current_priority = new_priority; Board_SavePendingTask(app_id, tid, params_json, src, NULL); ESP_LOGI(TAG, "start app_task %s/%s source=%s", app_id, tid, src); if (!spec->skills[0]) { dispatch_skill(NULL, app_id, tid, params_json, src); return ESP_OK; } for (int i = 0; i < 4 && spec->skills[i]; ++i) { dispatch_skill(spec->skills[i], app_id, tid, params_json, src); } return ESP_OK; } void Board_OnSkillPlaybackFinished(bool stopped, bool power_off_requested) { (void)power_off_requested; ESP_LOGI(TAG, "skill playback %s", stopped ? "stopped" : "finished"); copy_id(s_current_app, sizeof(s_current_app), "chat", "chat"); copy_id(s_current_task, sizeof(s_current_task), "conversation", "conversation"); copy_id(s_current_source, sizeof(s_current_source), "default_chat", "default_chat"); s_current_priority = 0; Board_SavePendingTask("chat", "conversation", "{}", "default_chat", NULL); } void Board_ResolveBootOrRestore(bool from_alarm_wake) { char app_id[APP_TASK_ID_MAX] = "chat"; char task_id[APP_TASK_ID_MAX] = "conversation"; char source[APP_TASK_SOURCE_MAX] = "default_chat"; char params[APP_TASK_JSON_MAX] = "{}"; bool has_pending = load_pending_fields(app_id, sizeof(app_id), task_id, sizeof(task_id), source, sizeof(source), params, sizeof(params)); if (!has_pending) { Board_SavePendingTask("chat", "conversation", "{}", "default_chat", NULL); Board_StartAppTask("chat", "conversation", "{}", "default_chat"); return; } if (from_alarm_wake && strcmp(source, "schedule") == 0 && !is_chat_task(app_id, task_id)) { ESP_LOGI(TAG, "alarm restore runs pending %s/%s", app_id, task_id); Board_StartAppTask(app_id, task_id, params, "schedule"); return; } Board_StartAppTask("chat", "conversation", "{}", "default_chat"); } void Board_EnterVoiceEntry(void) { if (s_current_priority >= 2) { ESP_LOGI(TAG, "ignore voice_entry, current priority=%d", s_current_priority); return; } Application::GetInstance().SendListenDetect("[voice_entry]", true); } bool Board_Handle_Voice_Command(const char *command) { if (!command || command[0] == '\0') { return false; } if (strcmp(command, "切换模式") == 0 || strcmp(command, "qie huan mo shi") == 0) { Board_EnterVoiceEntry(); return true; } if (strstr(command, "我们来聊天吧") || strstr(command, "聊会天吧")) { if (s_current_priority >= 2) { ESP_LOGI(TAG, "ignore voice chat switch, priority=%d", s_current_priority); return true; } Board_StartAppTask("chat", "conversation", "{}", "voice"); return true; } if (strstr(command, "打开翻译") || strstr(command, "开始翻译")) { if (s_current_priority >= 2) { return true; } Board_StartAppTask("english_translation", "live_translate", "{}", "voice"); return true; } if (strstr(command, "开始复读") || strstr(command, "打开复读")) { if (s_current_priority >= 2) { return true; } Board_EnterVoiceEntry(); return true; } if (strstr(command, "数字背诵") || strstr(command, "成语接龙") || strstr(command, "口算")) { ESP_LOGI(TAG, "reserved voice slot: %s", command); Board_EnterVoiceEntry(); return true; } return false; }