From 659c470eba542383987aad227b7f918100f131fa Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期五, 11 九月 2026 15:09:05 +0800
Subject: [PATCH] 小智终端 5种 场景模式 OK
---
xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/app_task.cc | 513 +++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 477 insertions(+), 36 deletions(-)
diff --git a/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/app_task.cc b/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/app_task.cc
index b9dcc3d..df9e80e 100644
--- a/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/app_task.cc
+++ b/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/app_task.cc
@@ -1,6 +1,7 @@
#include "app_task.h"
#include "application.h"
+#include "board.h"
#include "mqtt.h"
#include "system.h"
@@ -15,6 +16,7 @@
#define TAG "app_task"
#define APP_TASK_NVS_NS "app_task"
#define APP_TASK_NVS_KEY "pending"
+#define APP_TASK_NVS_LAST_REP "last_rep"
#define APP_TASK_JSON_MAX 768
#define APP_TASK_ID_MAX 32
#define APP_TASK_SOURCE_MAX 24
@@ -23,6 +25,8 @@
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;
+static bool s_finish_task_after_mp3 = true;
+static float s_shadowing_listen_ratio = 1.2f;
typedef struct {
const char *app_id;
@@ -34,7 +38,9 @@
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_learning", "shadowing", {NULL, NULL, NULL, NULL}},
{"english_translation", "live_translate", {"translate", NULL, NULL, NULL}},
+ {"english_conversation", "bilingual_chat", {NULL, NULL, NULL, NULL}},
};
int Board_AppTask_SourcePriority(const char *source)
@@ -82,10 +88,292 @@
(!task_id || task_id[0] == '\0' || strcmp(task_id, "conversation") == 0);
}
+bool Board_IsShadowingTask(void)
+{
+ return strcmp(s_current_task, "shadowing") == 0;
+}
+
+bool Board_IsRepeaterTask(void)
+{
+ return strcmp(s_current_task, "repeater") == 0;
+}
+
+void Board_SetShadowingListenRatio(float ratio)
+{
+ if (ratio < 1.2f) {
+ ratio = 1.2f;
+ }
+ if (ratio > 1.5f) {
+ ratio = 1.5f;
+ }
+ s_shadowing_listen_ratio = ratio;
+}
+
+unsigned Board_ShadowingListenWindowMs(unsigned duration_ms)
+{
+ if (duration_ms == 0) {
+ return 800;
+ }
+ unsigned wait_ms = static_cast<unsigned>(static_cast<float>(duration_ms) * s_shadowing_listen_ratio);
+ if (wait_ms < 800) {
+ wait_ms = 800;
+ }
+ return wait_ms;
+}
+
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);
+}
+
+void Board_SetActiveTask(const char *app_id, const char *task_id)
+{
+ if (!app_id || app_id[0] == '\0' || !task_id || task_id[0] == '\0') {
+ return;
+ }
+ if (strcmp(s_current_app, app_id) == 0 && strcmp(s_current_task, task_id) == 0) {
+ return;
+ }
+ copy_id(s_current_app, sizeof(s_current_app), app_id, "chat");
+ copy_id(s_current_task, sizeof(s_current_task), task_id, "conversation");
+ ESP_LOGI(TAG, "set active task %s/%s", s_current_app, s_current_task);
+}
+
+static void normalize_media_path(char *path)
+{
+ if (!path) {
+ return;
+ }
+ for (char *p = path; *p; ++p) {
+ if (*p == '\\') {
+ *p = '/';
+ }
+ }
+}
+
+static const char *path_without_leading_slash(const char *path)
+{
+ if (!path) {
+ return "";
+ }
+ return path[0] == '/' ? path + 1 : path;
+}
+
+static bool path_has_folder_prefix(const char *path, const char *prefix)
+{
+ if (!path || !prefix || prefix[0] == '\0') {
+ return false;
+ }
+ char normalized[SDCARD_PATH_MAX_LEN];
+ char needle[96];
+ strlcpy(normalized, path, sizeof(normalized));
+ strlcpy(needle, prefix, sizeof(needle));
+ normalize_media_path(normalized);
+ normalize_media_path(needle);
+ const char *hay = path_without_leading_slash(normalized);
+ const char *pre = path_without_leading_slash(needle);
+ size_t n = strlen(pre);
+ if (n == 0 || strncmp(hay, pre, n) != 0) {
+ return false;
+ }
+ return hay[n] == '\0' || hay[n] == '/';
+}
+
+static bool path_belongs_to_current_task(const char *path)
+{
+ if (is_chat_task(s_current_app, s_current_task)) {
+ return true;
+ }
+ char prefix[96];
+ snprintf(prefix, sizeof(prefix), "/%s/%s/", s_current_app, s_current_task);
+ return path_has_folder_prefix(path, prefix);
+}
+
+static bool basename_is_full_mp3(const char *name)
+{
+ if (!name || name[0] == '\0') {
+ return false;
+ }
+ const char *base = strrchr(name, '/');
+ base = base ? base + 1 : name;
+ if (strcasecmp(base, "full.mp3") == 0) {
+ return true;
+ }
+ size_t n = strlen(base);
+ return n > 9 && strcasecmp(base + n - 9, "_full.mp3") == 0;
+}
+
+void Board_BindTaskFromMediaPath(const char *path)
+{
+ // 璇炬枃鏂囦欢鍙兘閮藉湪 /english_learning/shadowing/ 涓嬨�備换鍔″彧鐢� MCP app_id/task_id 鍐冲畾銆�
+ (void)path;
+}
+
+static bool task_owns_play_audio(const char *app_id, const char *task_id)
+{
+ const app_task_skill_map_t *spec = find_task_spec(app_id, task_id);
+ if (!spec) {
+ return false;
+ }
+ for (int i = 0; i < 4 && spec->skills[i]; ++i) {
+ if (strcmp(spec->skills[i], "self.mp3.play") == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool first_sd_mp3_name(char *name, size_t name_size, const char *prefix, bool prefer_full)
+{
+ if (!name || name_size == 0) {
+ return false;
+ }
+ name[0] = '\0';
+ const char *list = SDCard_Get_MP3_List();
+ if (!list || list[0] == '\0') {
+ return false;
+ }
+ cJSON *root = cJSON_Parse(list);
+ if (!root) {
+ return false;
+ }
+ const cJSON *files = cJSON_GetObjectItem(root, "files");
+ if (!cJSON_IsArray(files)) {
+ cJSON_Delete(root);
+ return false;
+ }
+ const cJSON *best = NULL;
+ bool best_full = false;
+ const cJSON *file = NULL;
+ cJSON_ArrayForEach(file, files) {
+ const cJSON *item = cJSON_GetObjectItem(file, "name");
+ if (!cJSON_IsString(item) || !item->valuestring || !item->valuestring[0]) {
+ continue;
+ }
+ if (prefix && prefix[0] && !path_has_folder_prefix(item->valuestring, prefix)) {
+ continue;
+ }
+ bool is_full = basename_is_full_mp3(item->valuestring);
+ if (!best) {
+ best = item;
+ best_full = is_full;
+ continue;
+ }
+ if (prefer_full && is_full && !best_full) {
+ best = item;
+ best_full = true;
+ continue;
+ }
+ if ((!prefer_full || is_full == best_full) &&
+ strcmp(item->valuestring, best->valuestring) < 0) {
+ best = item;
+ best_full = is_full;
+ }
+ }
+ bool ok = best != NULL;
+ if (ok) {
+ strlcpy(name, best->valuestring, name_size);
+ }
+ cJSON_Delete(root);
+ return ok;
+}
+
+void Board_SetFinishTaskAfterMp3(bool finish)
+{
+ s_finish_task_after_mp3 = finish;
+}
+
+void Board_RememberRepeaterPlayback(const char *name, float speed, int repeat, bool power_off_after)
+{
+ if (!name || name[0] == '\0') {
+ return;
+ }
+ if (strcmp(s_current_task, "repeater") != 0) {
+ return;
+ }
+ cJSON *root = cJSON_CreateObject();
+ if (!root) {
+ return;
+ }
+ cJSON_AddStringToObject(root, "name", name);
+ cJSON_AddNumberToObject(root, "speed", speed);
+ cJSON_AddNumberToObject(root, "repeat", repeat < 1 ? 1 : repeat);
+ cJSON_AddBoolToObject(root, "power_off_after", power_off_after);
+ char *printed = cJSON_PrintUnformatted(root);
+ cJSON_Delete(root);
+ if (!printed) {
+ return;
+ }
+ nvs_handle_t handle;
+ if (nvs_open(APP_TASK_NVS_NS, NVS_READWRITE, &handle) == ESP_OK) {
+ if (nvs_set_str(handle, APP_TASK_NVS_LAST_REP, printed) == ESP_OK) {
+ nvs_commit(handle);
+ }
+ nvs_close(handle);
+ }
+ free(printed);
+}
+
+bool Board_ResolveRepeaterPlayback(char *name, size_t name_size, float *speed, int *repeat, bool *power_off_after)
+{
+ if (!name || name_size == 0) {
+ return false;
+ }
+ if (name[0] != '\0') {
+ if (speed) {
+ *speed = SDCard_Clamp_MP3_Speed(*speed);
+ }
+ return true;
+ }
+
+ nvs_handle_t handle;
+ if (nvs_open(APP_TASK_NVS_NS, NVS_READONLY, &handle) == ESP_OK) {
+ char buf[APP_TASK_JSON_MAX];
+ size_t length = sizeof(buf);
+ if (nvs_get_str(handle, APP_TASK_NVS_LAST_REP, buf, &length) == ESP_OK) {
+ cJSON *root = cJSON_Parse(buf);
+ if (root) {
+ const cJSON *name_item = cJSON_GetObjectItem(root, "name");
+ const cJSON *repeat_item = cJSON_GetObjectItem(root, "repeat");
+ const cJSON *power_item = cJSON_GetObjectItem(root, "power_off_after");
+ if (cJSON_IsString(name_item) && name_item->valuestring && name_item->valuestring[0]) {
+ strlcpy(name, name_item->valuestring, name_size);
+ if (repeat && cJSON_IsNumber(repeat_item)) {
+ *repeat = repeat_item->valueint;
+ }
+ if (power_off_after) {
+ 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(root);
+ }
+ }
+ nvs_close(handle);
+ }
+ if (name[0] != '\0' && !path_belongs_to_current_task(name)) {
+ ESP_LOGW(TAG, "discard last_rep from other task: %s", name);
+ name[0] = '\0';
+ }
+
+ if (name[0] == '\0') {
+ char prefix[96];
+ snprintf(prefix, sizeof(prefix), "/%s/%s/lessons", s_current_app, s_current_task);
+ bool prefer_full = strcmp(s_current_task, "repeater") == 0;
+ if (!first_sd_mp3_name(name, name_size, prefix, prefer_full)) {
+ if (is_chat_task(s_current_app, s_current_task)) {
+ first_sd_mp3_name(name, name_size, NULL, false);
+ }
+ }
+ }
+ if (speed) {
+ *speed = SDCard_Clamp_MP3_Speed(*speed);
+ }
+ return name[0] != '\0';
}
esp_err_t Board_SavePendingTask(const char *app_id, const char *task_id, const char *params_json,
@@ -195,6 +483,7 @@
const char *params_json, const char *source)
{
if (!skill || skill[0] == '\0') {
+ // 涓婃姤 [app_task]銆係endListenDetect 浼氬紑閫氶亾浣嗕繚鎸� idle锛屼笉寮� listening/AFE銆�
notify_server(app_id, task_id, source, true);
return;
}
@@ -214,7 +503,7 @@
strlcpy(name, name_item->valuestring, sizeof(name));
}
if (cJSON_IsNumber(speed_item)) {
- speed = static_cast<float>(speed_item->valuedouble);
+ speed = SDCard_Clamp_MP3_Speed(static_cast<float>(speed_item->valuedouble));
}
if (cJSON_IsNumber(repeat_item)) {
repeat = repeat_item->valueint;
@@ -227,19 +516,29 @@
cJSON_Delete(params);
}
}
- if (name[0] == '\0') {
- ESP_LOGW(TAG, "self.mp3.play missing name");
+ if (!Board_ResolveRepeaterPlayback(name, sizeof(name), &speed, &repeat, &power_off_after)) {
+ ESP_LOGW(TAG, "self.mp3.play has no last lesson and no SD MP3");
+ notify_server(app_id, task_id, source, true);
return;
}
if (repeat < 1) {
repeat = 1;
}
+ speed = SDCard_Clamp_MP3_Speed(speed);
+ Board_SetFinishTaskAfterMp3(true);
+ // 鏈夌綉鐨勮闊冲叆鍙o細鍏堣 Server 鎾姤杩涘満璇嶏紝闊抽鐢� MCP 鍦� TTS 缁撴潫鍚庡啀鎾��
+ if (source && strcmp(source, "voice") == 0 && Board::GetInstance().IsNetworkReady()) {
+ Board_RememberRepeaterPlayback(name, speed, repeat, power_off_after);
+ notify_server(app_id, task_id, source, true);
+ return;
+ }
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);
+ Board_OnSkillPlaybackFinished(true, false, 0);
return;
}
+ Board_RememberRepeaterPlayback(name, speed, repeat, power_off_after);
notify_server(app_id, task_id, source, false);
return;
}
@@ -256,18 +555,31 @@
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 char *tid = (task_id && task_id[0]) ? task_id : NULL;
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);
+ ESP_LOGW(TAG, "unknown app/task: %s/%s", app_id, tid ? tid : "");
return ESP_ERR_NOT_FOUND;
}
+ tid = spec->task_id;
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;
+ }
+
+ if (strcmp(s_current_app, app_id) == 0 && strcmp(s_current_task, tid) == 0 &&
+ strcmp(app_id, "chat") == 0 && strcmp(src, "voice") == 0) {
+ if (SDCard_Is_Playing()) {
+ ESP_LOGI(TAG, "stop leftover MP3 while already in chat");
+ preempt_current_skills();
+ }
+ // 璺熻鐢� MCP 璧锋挱鏃舵湰鍦颁粛鍙兘鏄� chat銆傘�岄��鍑鸿窡璇汇�嶅繀椤婚�氱煡 Server 鍙栨秷鍚獥銆�
+ // 涓嶈鍦ㄨ繖閲� ResumeListening锛氫細 ResetDecoder銆佹彁鍓嶅紑 AFE锛屾杩庤瘝/缁撴潫鎾姤浼氳娓呮帀銆�
+ notify_server("chat", "conversation", "voice", true);
+ return ESP_OK;
}
preempt_current_skills();
@@ -288,15 +600,107 @@
return ESP_OK;
}
-void Board_OnSkillPlaybackFinished(bool stopped, bool power_off_requested)
+void Board_OnSkillPlaybackFinished(bool stopped, bool power_off_requested, unsigned duration_ms)
{
(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);
+ // MP3 浠诲姟鏍堝湪 PSRAM锛屼笉鑳藉湪杩欓噷 nvs_set_str / 鏀圭姸鎬佹満銆�
+ Application::GetInstance().Schedule([stopped, duration_ms]() {
+ ESP_LOGI(TAG, "skill playback %s duration_ms=%u task=%s/%s",
+ stopped ? "stopped" : "finished", duration_ms, s_current_app, s_current_task);
+
+ char app_id[APP_TASK_ID_MAX];
+ char task_id[APP_TASK_ID_MAX];
+ copy_id(app_id, sizeof(app_id), s_current_app, "chat");
+ copy_id(task_id, sizeof(task_id), s_current_task, "conversation");
+ const bool shadowing = strcmp(task_id, "shadowing") == 0;
+ const bool repeater = strcmp(task_id, "repeater") == 0;
+ // 澶嶈娓呭崟鏄惁缁撴潫鐢� Server 鍐冲畾銆傝澶囦笉瑕佸湪姝ゅ啓 NVS 鍒囧洖 chat銆�
+ const bool end_task = !shadowing && !repeater &&
+ task_owns_play_audio(app_id, task_id) && s_finish_task_after_mp3;
+
+ if (end_task) {
+ 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);
+ }
+
+ char detect[208];
+ snprintf(detect, sizeof(detect), "[skill_event]audio.%s|%s|%s|%u",
+ stopped ? "stopped" : "completed", app_id, task_id, duration_ms);
+ const bool open_mic = !stopped && shadowing;
+ // 鍚獥浠庤鏂囦竴鍋滃氨寮�濮嬨�備笉瑕佺瓑寮�楹�/AFE 鍒濆鍖栧悗鍐嶄笂鎶� completed銆�
+ if (!Application::GetInstance().SendListenDetectNow(detect)) {
+ Application::GetInstance().SendListenDetect(detect, false);
+ }
+ if (open_mic) {
+ Application::GetInstance().GetAudioService().ResetDecoder();
+ Application::GetInstance().ResumeListeningAfterMp3Clip();
+ } else if (repeater && !stopped) {
+ // 娓呭崟宸叉挱瀹岋紝椹笂瑕佹挱銆屽璇荤粨鏉熴�嶏細淇濇寔鍔熸斁锛屼笉瑕佸啀 ResetDecoder銆�
+ Application::GetInstance().GetAudioService().KeepCodecReadyForPlayback();
+ Application::GetInstance().GetAudioService().SetAudioPowerInhibit(true);
+ } else if (!repeater) {
+ Application::GetInstance().GetAudioService().ResetDecoder();
+ }
+ });
+}
+
+void Board_OnShadowingClipFinished(unsigned duration_ms)
+{
+ Application::GetInstance().Schedule([duration_ms]() {
+ ESP_LOGI(TAG, "shadowing clip finished duration_ms=%u task=%s/%s",
+ duration_ms, s_current_app, s_current_task);
+
+ char app_id[APP_TASK_ID_MAX];
+ char task_id[APP_TASK_ID_MAX];
+ copy_id(app_id, sizeof(app_id), s_current_app, "chat");
+ copy_id(task_id, sizeof(task_id), s_current_task, "shadowing");
+
+ char detect[208];
+ snprintf(detect, sizeof(detect), "[skill_event]audio.completed|%s|%s|%u",
+ app_id, task_id, duration_ms);
+ if (!Application::GetInstance().SendListenDetectNow(detect)) {
+ Application::GetInstance().SendListenDetect(detect, false);
+ }
+ Application::GetInstance().ResumeListeningAfterMp3Clip();
+ });
+}
+
+void Board_OnShadowingPlaylistFinished(void)
+{
+ Application::GetInstance().Schedule([]() {
+ ESP_LOGI(TAG, "shadowing playlist finished task=%s/%s", s_current_app, s_current_task);
+
+ char app_id[APP_TASK_ID_MAX];
+ char task_id[APP_TASK_ID_MAX];
+ copy_id(app_id, sizeof(app_id), s_current_app, "chat");
+ copy_id(task_id, sizeof(task_id), s_current_task, "shadowing");
+
+ char detect[208];
+ snprintf(detect, sizeof(detect), "[skill_event]audio.playlist_completed|%s|%s",
+ app_id, task_id);
+ if (!Application::GetInstance().SendListenDetectNow(detect)) {
+ ESP_LOGW(TAG, "playlist_completed not sent, return to chat locally");
+ 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);
+ auto &app = Application::GetInstance();
+ app.GetAudioService().SetAudioPowerInhibit(false);
+ app.GetAudioService().EnableVoiceProcessing(false);
+ app.GetAudioService().EnableWakeWordDetection(true);
+ if (app.GetDeviceState() == kDeviceStateListening ||
+ app.GetDeviceState() == kDeviceStateConnecting) {
+ app.SetDeviceState(kDeviceStateIdle);
+ }
+ return;
+ }
+ Application::GetInstance().GetAudioService().KeepCodecReadyForPlayback();
+ Application::GetInstance().GetAudioService().SetAudioPowerInhibit(true);
+ });
}
void Board_ResolveBootOrRestore(bool from_alarm_wake)
@@ -313,9 +717,12 @@
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");
+ const bool scheduled = strcmp(source, "schedule") == 0 || strcmp(source, "mqtt_immediate") == 0;
+ const bool offline = !Board::GetInstance().IsNetworkReady();
+ if (!is_chat_task(app_id, task_id) && (from_alarm_wake || scheduled || offline)) {
+ ESP_LOGI(TAG, "restore pending %s/%s source=%s offline=%d",
+ app_id, task_id, source, offline ? 1 : 0);
+ Board_StartAppTask(app_id, task_id, params, source);
return;
}
@@ -328,6 +735,10 @@
ESP_LOGI(TAG, "ignore voice_entry, current priority=%d", s_current_priority);
return;
}
+ if (!Board::GetInstance().IsNetworkReady()) {
+ ESP_LOGW(TAG, "voice_entry skipped, network not ready; say 澶嶈鏈� to play locally");
+ return;
+ }
Application::GetInstance().SendListenDetect("[voice_entry]", true);
}
@@ -336,36 +747,66 @@
if (!command || command[0] == '\0') {
return false;
}
- if (strcmp(command, "鍒囨崲妯″紡") == 0 || strcmp(command, "qie huan mo shi") == 0) {
+ const bool speaking = Application::GetInstance().GetDeviceState() == kDeviceStateSpeaking;
+ if (Board_Is_Mode_Switch_Command(command)) {
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;
- }
+ if (Board_Is_Chat_Return_Command(command)) {
+ s_current_priority = 0;
Board_StartAppTask("chat", "conversation", "{}", "voice");
return true;
}
- if (strstr(command, "鎵撳紑缈昏瘧") || strstr(command, "寮�濮嬬炕璇�")) {
+ if (speaking) {
+ if (Board_Is_App_Launch_Command(command)) {
+ ESP_LOGI(TAG, "ignore app launch while TTS is speaking: %s", command);
+ return true;
+ }
+ return false;
+ }
+ if (strcmp(command, OFFLINE_REPEATER_WORD) == 0 ||
+ strcmp(command, OFFLINE_START_REPEATER_WORD) == 0 ||
+ 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, "鍙g畻")) {
- ESP_LOGI(TAG, "reserved voice slot: %s", command);
- Board_EnterVoiceEntry();
+ Board_StartAppTask("english_learning", "repeater", "{}", "voice");
return true;
}
return false;
}
+
+static bool layout_id_is_safe(const char *id)
+{
+ if (!id || id[0] == '\0' || strlen(id) >= APP_TASK_ID_MAX) {
+ return false;
+ }
+ for (const char *p = id; *p; ++p) {
+ if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
+ *p == '_' || *p == '-')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+esp_err_t Board_EnsureAppLayout(const char *app_id, const char *task_id)
+{
+ if (!layout_id_is_safe(app_id)) {
+ return ESP_ERR_INVALID_ARG;
+ }
+ char path[SDCARD_PATH_MAX_LEN];
+ int written;
+ if (task_id && task_id[0]) {
+ if (!layout_id_is_safe(task_id)) {
+ return ESP_ERR_INVALID_ARG;
+ }
+ written = snprintf(path, sizeof(path), "/%s/%s/lessons", app_id, task_id);
+ } else {
+ written = snprintf(path, sizeof(path), "/%s", app_id);
+ }
+ if (written <= 0 || (size_t)written >= sizeof(path)) {
+ return ESP_ERR_INVALID_SIZE;
+ }
+ return SDCard_Ensure_Business_Dir(path);
+}
--
Gitblit v1.9.1