#include "app_task.h"
|
|
#include "application.h"
|
#include "mqtt.h"
|
#include "system.h"
|
|
#include <cJSON.h>
|
#include <esp_log.h>
|
#include <nvs.h>
|
#include <strings.h>
|
#include <string.h>
|
#include <stdio.h>
|
#include <stdlib.h>
|
|
#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<float>(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;
|
}
|