From 220e4fab27b7f419b3e89e2f2d28ae789aac09d7 Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期四, 08 十月 2026 16:29:55 +0800
Subject: [PATCH] 5种场景模式家长端ok

---
 xiaozhi-esp32-2.2.6/main/application.cc |  359 +++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 308 insertions(+), 51 deletions(-)

diff --git a/xiaozhi-esp32-2.2.6/main/application.cc b/xiaozhi-esp32-2.2.6/main/application.cc
index f4cb306..ec1cdfb 100644
--- a/xiaozhi-esp32-2.2.6/main/application.cc
+++ b/xiaozhi-esp32-2.2.6/main/application.cc
@@ -16,6 +16,7 @@
 #include <cstring>
 #include <memory>
 #include <esp_log.h>
+#include <esp_heap_caps.h>
 #include <cJSON.h>
 #include <driver/gpio.h>
 #include <arpa/inet.h>
@@ -25,7 +26,6 @@
 
 extern "C" void SDCard_Pause_Play(void) __attribute__((weak));
 extern "C" void SDCard_Resume_Play(void) __attribute__((weak));
-extern "C" void SDCard_Stop_Play(void) __attribute__((weak));
 extern "C" bool SDCard_Is_Playing(void) __attribute__((weak));
 extern "C" bool SDCard_Is_Paused(void) __attribute__((weak));
 
@@ -37,6 +37,16 @@
     return SDCard_Is_Paused && SDCard_Is_Paused();
 }
 
+static bool ShouldHoldIdleForLessonWork() {
+    return Board_IsLessonSyncing() || Board_IsRepeaterTask() || Board_IsShadowingTask() ||
+           Board_IsWaitingStartPhrase();
+}
+
+static bool IsLessonDetectText(const std::string& text) {
+    return text.rfind("[lesson_sync]", 0) == 0 ||
+           text.rfind("[app_task]english_learning|", 0) == 0;
+}
+
 static void PauseSdCardMp3() {
     if (SDCard_Pause_Play) {
         SDCard_Pause_Play();
@@ -46,12 +56,6 @@
 static void ResumeSdCardMp3() {
     if (SDCard_Resume_Play) {
         SDCard_Resume_Play();
-    }
-}
-
-static void StopSdCardMp3() {
-    if (SDCard_Stop_Play) {
-        SDCard_Stop_Play();
     }
 }
 
@@ -240,6 +244,7 @@
 }
 
 void Application::Run() {
+    main_loop_task_ = xTaskGetCurrentTaskHandle();
     // Set the priority of the main task to 10
     vTaskPrioritySet(nullptr, 10);
 
@@ -331,6 +336,7 @@
             display->UpdateStatusBar();
             auto state = GetDeviceState();
             if (state == kDeviceStateConnecting || state == kDeviceStateSpeaking ||
+                state == kDeviceStateActivating ||
                 (state == kDeviceStateIdle && suppress_wake_until_listen_)) {
                 audio_service_.KeepCodecReadyForPlayback();
             }
@@ -341,10 +347,16 @@
                     if (greeting_listen_wait_ticks_ > 0) {
                         greeting_listen_wait_ticks_--;
                         if (greeting_listen_wait_ticks_ == 0) {
-                            ESP_LOGW(TAG, "greeting TTS missing, start listening");
-                            SetListeningMode(GetDefaultListeningMode());
+                            if (ShouldHoldIdleForLessonWork()) {
+                                ESP_LOGI(TAG, "skip greeting listen while lesson task");
+                            } else {
+                                ESP_LOGW(TAG, "greeting TTS missing, start listening");
+                                SetListeningMode(GetDefaultListeningMode());
+                            }
                         }
                     }
+                } else if (Board_IsLessonSyncing()) {
+                    boot_wake_restore_ticks_ = 0;
                 } else if (++boot_wake_restore_ticks_ >= 20) {
                     ESP_LOGW(TAG, "boot websocket not opened, restore wake word");
                     suppress_wake_until_listen_ = false;
@@ -358,7 +370,7 @@
                 speaking_started_us_ > 0) {
                 const int64_t wait_ms = (esp_timer_get_time() - speaking_started_us_) / 1000;
                 if (wait_ms >= 1000 && (wait_ms / 1000) <= 8) {
-                    ESP_LOGW(TAG, "TTS audio still missing after %lldms", (long long)wait_ms);
+                    ESP_LOGW(TAG, "TTS audio still missing after %dms", (int)wait_ms);
                 }
             }
             if (protocol_ && IsSdCardMp3Playing() && protocol_->IsAudioChannelConnected()) {
@@ -379,7 +391,33 @@
                     pending_detect_text_.clear();
                 }
             }
+            if (hold_idle_for_prompt_ && !pending_detect_text_.empty() &&
+                GetDeviceState() == kDeviceStateIdle &&
+                protocol_ && protocol_->IsAudioChannelConnected()) {
+                if (pending_detect_retry_ticks_ > 0) {
+                    pending_detect_retry_ticks_--;
+                } else if (pending_detect_retries_left_ > 0) {
+                    pending_detect_retries_left_--;
+                    pending_detect_retry_ticks_ = 2;
+                    ESP_LOGW(TAG, "retry app_task detect: %s left=%d",
+                             pending_detect_text_.c_str(), pending_detect_retries_left_);
+                    SendListenDetectNow(pending_detect_text_);
+                } else if (IsLessonDetectText(pending_detect_text_) || ShouldHoldIdleForLessonWork()) {
+                    ESP_LOGW(TAG, "lesson detect unanswered, keep idle: %s",
+                             pending_detect_text_.c_str());
+                    pending_detect_text_.clear();
+                    pending_detect_retries_left_ = 0;
+                    pending_detect_retry_ticks_ = 0;
+                } else {
+                    ESP_LOGW(TAG, "app_task detect unanswered, start listening");
+                    hold_idle_for_prompt_ = false;
+                    suppress_wake_until_listen_ = false;
+                    pending_detect_text_.clear();
+                    SetListeningMode(GetDefaultListeningMode());
+                }
+            }
 
+            Board_PollPendingLessonPlayback();
             // Print debug info every 10 seconds
             if (clock_ticks_ % 10 == 0) {
                 SystemInfo::PrintHeapStats();
@@ -621,6 +659,10 @@
             if (GetDeviceState() == kDeviceStateIdle) {
                 break;
             }
+            // 婵�娲昏姹傚け璐ユ椂鍐嶆挱涓�閬� 6 浣嶇爜锛岄伩鍏嶅彧鍦ㄥ紑鏈虹灛闂存挱涓�娆¤鍚紡銆�
+            if (ota_->HasActivationCode() && i < 9) {
+                ShowActivationCode(ota_->GetActivationCode(), ota_->GetActivationMessage());
+            }
         }
     }
 }
@@ -666,10 +708,13 @@
             }
             if (state == kDeviceStateListening || state == kDeviceStateIdle) {
                 tts_audio_expected_ = true;
+                if (!ShouldHoldIdleForLessonWork()) {
+                    hold_idle_for_prompt_ = false;
+                }
             }
 
             tts_response_started_ = true;
-            ESP_LOGI(TAG, "TTS audio packet bytes=%u expected=%d ready=%d state=%d",
+            ESP_LOGD(TAG, "TTS audio packet bytes=%u expected=%d ready=%d state=%d",
                      (unsigned)shared_packet->payload.size(),
                      tts_audio_expected_ ? 1 : 0,
                      tts_speaking_state_ready_ ? 1 : 0,
@@ -695,12 +740,10 @@
             ESP_LOGI(TAG, "Skip channel-closed teardown while opening/reusing websocket");
             return;
         }
-        // 璺熻/澶嶈杩樺湪鎾椂涓嶈鍒� LOW_POWER锛氬悗闈㈣ˉ鍙� playlist_completed 浼� handshake timeout銆�
-        if ((Board_IsShadowingTask() || Board_IsRepeaterTask()) && IsSdCardMp3Playing()) {
-            board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
-        } else {
-            board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER);
-        }
+        // 閫氶亾鍏虫帀鍚庝細绔嬪埢鍒� idle銆佸姞杞藉敜閱掕瘝銆傝繖鏃跺啀杩� MAX_MODEM锛�
+        // MQTT 閲嶈繛浠诲姟鍜� phy_track_pll 浼氭妸鍐呴儴 SRAM 鎵撶┛锛孍SP_ERROR_CHECK 鐩存帴閲嶅惎銆�
+        // 淇濇寔 PERFORMANCE锛岀瓑 PowerSaveTimer 瓒呮椂鍚庡啀鐪熸鏂綉寰呭懡銆�
+        board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
         Schedule([this]() {
             auto display = Board::GetInstance().GetDisplay();
             opening_audio_channel_ = false;
@@ -716,6 +759,7 @@
             tts_stop_pending_ = false;
             tts_stop_waiting_ = false;
             tts_idle_waiting_ = false;
+            tts_session_end_ = false;
             tts_speaking_state_ready_ = false;
             tts_activity_generation_++;
             audio_service_.ResetDecoder();
@@ -748,11 +792,18 @@
             if (strcmp(state->valuestring, "start") == 0) {
                 Schedule([this]() {
                     aborted_ = false;
+                    if (!ShouldHoldIdleForLessonWork()) {
+                        hold_idle_for_prompt_ = false;
+                    }
+                    pending_detect_text_.clear();
+                    pending_detect_retries_left_ = 0;
+                    pending_detect_retry_ticks_ = 0;
                     tts_audio_expected_ = true;
                     tts_response_started_ = false;
                     tts_audio_received_ = false;
                     tts_stop_pending_ = false;
                     tts_stop_waiting_ = false;
+                    tts_session_end_ = false;
                     tts_playback_empty_ticks_ = 0;
                     tts_stop_wait_ticks_ = 0;
                     tts_activity_generation_++;
@@ -766,9 +817,15 @@
                              Board::GetInstance().GetAudioCodec()->output_enabled() ? 1 : 0);
                 });
             } else if (strcmp(state->valuestring, "stop") == 0) {
-                Schedule([this]() {
-                    ESP_LOGI(TAG, "TTS stop started=%d received=%d",
-                             tts_response_started_, tts_audio_received_);
+                auto session_end_item = cJSON_GetObjectItem(root, "session_end");
+                const bool session_end = cJSON_IsTrue(session_end_item) ||
+                    (cJSON_IsNumber(session_end_item) && session_end_item->valuedouble != 0);
+                Schedule([this, session_end]() {
+                    ESP_LOGI(TAG, "TTS stop started=%d received=%d session_end=%d",
+                             tts_response_started_, tts_audio_received_, session_end ? 1 : 0);
+                    if (session_end) {
+                        tts_session_end_ = true;
+                    }
                     tts_stop_pending_ = true;
                     FinishTtsResponseAfterPlayback();
                 });
@@ -785,13 +842,22 @@
                         return;
                     }
                     Schedule([this, display, message = std::string(text->valuestring)]() {
+                        if (!ShouldHoldIdleForLessonWork()) {
+                            hold_idle_for_prompt_ = false;
+                        }
+                        pending_detect_text_.clear();
+                        pending_detect_retries_left_ = 0;
+                        pending_detect_retry_ticks_ = 0;
                         tts_audio_expected_ = true;
                         tts_response_started_ = true;
                         tts_activity_generation_++;
                         tts_playback_empty_ticks_ = 0;
                         audio_service_.SetAudioPowerInhibit(true);
                         audio_service_.KeepCodecReadyForPlayback();
-                        if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
+                        if (Board_OnStartPhraseSentence(message.c_str())) {
+                            resume_sdcard_mp3_after_tts_ = false;
+                            resume_sdcard_mp3_after_voice_ = false;
+                        } else if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
                             PauseSdCardMp3();
                             resume_sdcard_mp3_after_tts_ = true;
                         }
@@ -890,6 +956,18 @@
     protocol_->Start();
 }
 
+static void WaitUntilActivationPlaybackIdle(AudioService& audio) {
+    audio.KeepCodecReadyForPlayback();
+    for (int i = 0; i < 150; ++i) {
+        if (audio.IsPlaybackQueueEmpty()) {
+            vTaskDelay(pdMS_TO_TICKS(150));
+            return;
+        }
+        audio.KeepCodecReadyForPlayback();
+        vTaskDelay(pdMS_TO_TICKS(100));
+    }
+}
+
 void Application::ShowActivationCode(const std::string& code, const std::string& message) {
     struct digit_sound {
         char digit;
@@ -908,14 +986,24 @@
         digit_sound{'9', Lang::Sounds::OGG_9}
     }};
 
+    ESP_LOGI(TAG, "Speaking activation code: %s", code.c_str());
+    audio_service_.KeepCodecReadyForPlayback();
+    // 鍔熸斁鍒氭墦寮�鏃跺墠鍑犵櫨姣甯告槸闈欓煶锛屽厛绋充綇鍐嶆挱锛岄伩鍏嶅彧鍚埌鎻愮ず闊冲惉涓嶅埌鏁板瓧銆�
+    vTaskDelay(pdMS_TO_TICKS(400));
+
     // This sentence uses 9KB of SRAM, so we need to wait for it to finish
     Alert(Lang::Strings::ACTIVATION, message.c_str(), "link", Lang::Sounds::OGG_ACTIVATION);
+    WaitUntilActivationPlaybackIdle(audio_service_);
 
     for (const auto& digit : code) {
         auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(),
             [digit](const digit_sound& ds) { return ds.digit == digit; });
         if (it != digit_sounds.end()) {
+            ESP_LOGI(TAG, "Playing activation digit: %c", digit);
+            audio_service_.KeepCodecReadyForPlayback();
             audio_service_.PlaySound(it->sound);
+            WaitUntilActivationPlaybackIdle(audio_service_);
+            vTaskDelay(pdMS_TO_TICKS(250));
         }
     }
 }
@@ -966,6 +1054,87 @@
     return protocol_->SendWakeWordDetected(text);
 }
 
+void Application::HoldIdleForServerPrompt() {
+    hold_idle_for_prompt_ = true;
+    suppress_wake_until_listen_ = true;
+    auto state = GetDeviceState();
+    if (state == kDeviceStateListening) {
+        SetDeviceState(kDeviceStateIdle);
+    }
+}
+
+void Application::ReleaseHoldIdle() {
+    hold_idle_for_prompt_ = false;
+    suppress_wake_until_listen_ = false;
+    boot_wake_restore_ticks_ = 0;
+}
+
+void Application::StartListeningAfterPrompt() {
+    SetListeningMode(GetDefaultListeningMode());
+}
+
+void Application::ArmPendingDetectRetry(const std::string& text, int retries) {
+    if (text.empty() || retries <= 0) {
+        return;
+    }
+    pending_detect_text_ = text;
+    pending_detect_retries_left_ = retries;
+    pending_detect_retry_ticks_ = 2;
+}
+
+bool Application::IsAudioChannelConnected() const {
+    return protocol_ && protocol_->IsAudioChannelConnected();
+}
+
+void Application::AnnounceReturnedToChat() {
+    pending_detect_text_.clear();
+    pending_detect_retries_left_ = 0;
+    pending_detect_retry_ticks_ = 0;
+    suppress_wake_until_listen_ = false;
+    audio_service_.KeepCodecReadyForPlayback();
+    auto display = Board::GetInstance().GetDisplay();
+    display->SetStatus("瀵硅瘽妯″紡");
+    display->SetEmotion("happy");
+    display->SetChatMessage("assistant", "鎴戜滑鏉ヨ亰澶╁惂銆�");
+    audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS);
+    ESP_LOGI(TAG, "announced local return to chat");
+}
+
+void Application::ReconnectAfterLesson(const std::string& detect) {
+    // 璇炬枃缁撴潫 WS 宸叉柇锛氬厛閲婃斁 AFE锛屽啀鎻℃墜琛ュ彂 skill_event銆�
+    // Server 浼氭挱銆屾硾鍚�/璺熻缁撴潫銆傛垜浠潵鑱婂ぉ鍚с�傘�嶈瀹屽啀 listening銆�
+    pending_detect_text_.clear();
+    pending_detect_retries_left_ = 0;
+    pending_detect_retry_ticks_ = 0;
+    suppress_wake_until_listen_ = true;
+    audio_service_.SetAudioPowerInhibit(false);
+    audio_service_.KeepCodecReadyForPlayback();
+    audio_service_.EnableVoiceProcessing(false);
+    audio_service_.EnableWakeWordDetection(false);
+    audio_service_.ReleaseAfeRuntime();
+
+    auto state = GetDeviceState();
+    if (state == kDeviceStateListening || state == kDeviceStateSpeaking) {
+        SetDeviceState(kDeviceStateIdle);
+    }
+
+    if (detect.empty() || !protocol_) {
+        ESP_LOGW(TAG, "lesson reconnect skipped, stay local chat");
+        AnnounceReturnedToChat();
+        audio_service_.EnableWakeWordDetection(true);
+        return;
+    }
+
+    // 娉涘惉鏃堕暱浼氭妸 WiFi 寰呮満鍏虫帀锛堣窡璇绘湁鍚獥鎵�浠ヤ笉浼氾級銆備笉瑕佺敤 IsNetworkReady()
+    // 鐩存帴鏀惧純锛氬悗闈� OpenAudioChannel 浼� EnsureNetworkReadyForWake锛岃繛涓婂啀鎾粨鏉熻銆�
+    ESP_LOGI(TAG, "lesson reconnect for %s", detect.c_str());
+    Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
+    SetDeviceState(kDeviceStateConnecting);
+    Schedule([this, detect]() {
+        ContinueOpenAudioChannelForDetect(detect);
+    });
+}
+
 void Application::SendListenDetect(const std::string& text, bool open_if_idle) {
     Schedule([this, text, open_if_idle]() {
         if (!protocol_ || text.empty()) {
@@ -977,6 +1146,7 @@
             Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
             protocol_->MarkIncomingActivity();
             if (protocol_->SendWakeWordDetected(text)) {
+                Board_NoteLessonStartDetectSent();
                 return;
             }
             ESP_LOGW(TAG, "detect send failed, reopen audio channel");
@@ -1089,6 +1259,18 @@
     // AFE 璧锋潵鍚庡唴閮ㄥ爢鍙墿鍑� KB锛寈TaskCreate(8KB) 浼氱洿鎺ュけ璐ャ��
     audio_service_.EnableVoiceProcessing(false);
     audio_service_.EnableWakeWordDetection(false);
+    const size_t dma_before = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
+    ESP_LOGI(TAG, "OpenAudioChannel DMA largest=%u free=%u",
+             (unsigned)dma_before,
+             (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
+    if (dma_before < 8192) {
+        ESP_LOGW(TAG, "low DMA, release AFE before websocket");
+        audio_service_.ClearWakeWordBuffers();
+        audio_service_.ReleaseAfeRuntime();
+        vTaskDelay(pdMS_TO_TICKS(50));
+        ESP_LOGI(TAG, "OpenAudioChannel DMA after AFE release=%u",
+                 (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA));
+    }
     Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
     audio_service_.SetAudioPowerInhibit(true);
 
@@ -1136,25 +1318,45 @@
     listening_mode_ = GetDefaultListeningMode();
     suppress_wake_until_listen_ = true;
     if (!OpenAudioChannelWithRetry(2)) {
-        pending_detect_text_ = detect_text;
-        if (pending_detect_retries_left_ <= 0) {
-            pending_detect_retries_left_ = 3;
+        // 璇炬枃缁撴潫鍚庨�氶亾宸叉鏃朵笉瑕佸弽澶� handshake锛氫細鍗″湪銆岃繛鎺ヤ腑銆嶅苟鍏虫帀鍞ら啋璇嶃��
+        const bool skip_retry = detect_text.rfind("[skill_event]", 0) == 0;
+        if (skip_retry) {
+            pending_detect_text_.clear();
+            pending_detect_retries_left_ = 0;
+            pending_detect_retry_ticks_ = 0;
+        } else {
+            pending_detect_text_ = detect_text;
+            if (pending_detect_retries_left_ <= 0) {
+                pending_detect_retries_left_ = 3;
+            }
+            pending_detect_retry_ticks_ = 3;
         }
-        pending_detect_retry_ticks_ = 3;
         suppress_wake_until_listen_ = false;
-        audio_service_.EnableWakeWordDetection(true);
         SetDeviceState(kDeviceStateIdle);
-        ESP_LOGW(TAG, "boot websocket failed, will retry detect in 3s");
+        if (skip_retry) {
+            ESP_LOGW(TAG, "websocket failed after lesson, keep idle chat without retry");
+            AnnounceReturnedToChat();
+        } else {
+            ESP_LOGW(TAG, "boot websocket failed, will retry detect in 3s");
+        }
+        if (IsLessonDetectText(detect_text) || ShouldHoldIdleForLessonWork()) {
+            suppress_wake_until_listen_ = true;
+            audio_service_.EnableWakeWordDetection(false);
+        } else {
+            audio_service_.EnableWakeWordDetection(true);
+        }
         return;
     }
 
     pending_detect_text_.clear();
     pending_detect_retries_left_ = 0;
     pending_detect_retry_ticks_ = 0;
-    greeting_listen_wait_ticks_ = 8;
+    greeting_listen_wait_ticks_ = (IsLessonDetectText(detect_text) || ShouldHoldIdleForLessonWork()) ? 0 : 8;
     if (!detect_text.empty()) {
         protocol_->MarkIncomingActivity();
-        protocol_->SendWakeWordDetected(detect_text);
+        if (protocol_->SendWakeWordDetected(detect_text)) {
+            Board_NoteLessonStartDetectSent();
+        }
     }
     // 淇濇寔寰呭懡涓斾笉寮� AFE锛氭杩庤瘝浠� idle 杩� speaking銆傝瀹屽啀 listening 寮�楹︺��
     SetDeviceState(kDeviceStateIdle);
@@ -1185,9 +1387,20 @@
     if (!IsSdCardMp3Playing() || IsSdCardMp3Paused()) {
         return;
     }
-    // 闊抽鍦ㄦ挱锛氫笉涓婅褰曢煶/闂茶亰锛屽彧鐣欑绾垮彛浠わ紙閫�鍑哄璇�/璺熻銆佹垜浠亰澶╁惂锛夈��
+    // 闊抽鍦ㄦ挱锛氫笉涓婅褰曢煶/闂茶亰锛屽彧鐣欑绾垮彛浠わ紙閫�鍑烘硾鍚�/璺熻銆佹垜浠亰澶╁惂锛夈��
+    // 璇炬枃杩樺湪璇� SD 鏃剁姝㈡媺璧� AFE锛氫細鎶� SDMMC DMA 闇�瑕佺殑杩炵画鍐呴儴 SRAM 鍚冨厜锛宖open abort銆�
     audio_service_.EnableVoiceProcessing(false);
-    audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord());
+    const size_t largest_internal = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
+    if (!SDCard_Mp3OfflineWakeAllowed() || largest_internal < 32768) {
+        audio_service_.EnableWakeWordDetection(false);
+        if (!SDCard_Mp3OfflineWakeAllowed()) {
+            ESP_LOGD(TAG, "offline wake deferred until MP3 cached");
+        } else {
+            ESP_LOGW(TAG, "skip offline wake, internal largest=%u", (unsigned)largest_internal);
+        }
+    } else {
+        audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord());
+    }
     if (GetDeviceState() != kDeviceStateListening) {
         return;
     }
@@ -1241,7 +1454,9 @@
 void Application::CloseShadowingListenWindow()
 {
     audio_service_.EnableVoiceProcessing(false);
-    audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord());
+    // 涓嬩竴鍙ヨ繕瑕� fopen/瑙g爜锛屻�岃窡璇荤粨鏉熴�峊TS 杩樿 mbedtls AES銆傝繖閲屼笉瑕佺珛鍒绘媺璧�
+    // AFE 鍞ら啋璇嶏紝鍚﹀垯鍐呴儴 SRAM 鍙墿 1KB 杩炵画鍧楋紝SD DMA / SSL 閮戒細澶辫触銆�
+    audio_service_.EnableWakeWordDetection(false);
     if (protocol_ && protocol_->IsAudioChannelOpened()) {
         while (auto packet = audio_service_.PopPacketFromSendQueue()) {
             if (!protocol_->SendAudio(std::move(packet))) {
@@ -1326,7 +1541,21 @@
 
     if (state == kDeviceStateIdle) {
         PauseSdCardMp3ForVoice();
-        audio_service_.EncodeWakeWord();
+        size_t dma_largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
+        // 鍐嶈鍚庡敜閱掕瘝 AFE 杩樺崰鐫�鍐呴儴鍫嗭紝DMA 寰�寰�涓嶅缂栫爜 opus銆�
+        // 鏃ч�昏緫浼氳烦杩囩紪鐮佸嵈缁х画闃诲绛� opus锛屼富寰幆鍗℃锛�15s 鍚� codec 鎺夌數锛屽儚杞叧鏈恒��
+        if (dma_largest < 12288) {
+            ESP_LOGW(TAG, "low DMA before wake handshake, release AFE largest=%u",
+                     (unsigned)dma_largest);
+            audio_service_.ReleaseAfeRuntime();
+            dma_largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
+        }
+        if (dma_largest >= 8192) {
+            audio_service_.EncodeWakeWord();
+        } else {
+            ESP_LOGW(TAG, "skip wake opus encode, DMA largest=%u", (unsigned)dma_largest);
+            audio_service_.AbortWakeWordEncode();
+        }
         auto wake_word = audio_service_.GetLastWakeWord();
 
         if (!protocol_->IsAudioChannelOpened()) {
@@ -1429,6 +1658,7 @@
                 tts_stop_pending_ = false;
                 tts_stop_waiting_ = false;
                 tts_idle_waiting_ = false;
+                tts_session_end_ = false;
                 tts_speaking_state_ready_ = false;
                 tts_activity_generation_++;
             }
@@ -1437,16 +1667,18 @@
             display->ClearChatMessages();  // Clear messages first
             display->SetEmotion("neutral"); // Then set emotion (wechat mode checks child count)
             audio_service_.EnableVoiceProcessing(false);
-            if (suppress_wake_until_listen_) {
+            // 鍐嶈鏂嚎浼� speaking鈫抣istening鈫抜dle锛歱rocessor AFE 娌¢噴鏀惧氨鎶婂敜閱掕瘝 AFE 鎷夎捣鏉ワ紝
+            // audio_detection 寤轰笉璧锋潵锛宖eed 鍒峰睆鐩村埌鍐呴儴 SRAM 鎵撶┛銆�
+            if (!suppress_wake_until_listen_) {
+                audio_service_.ReleaseVoiceProcessorRuntime();
+            }
+            if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
+                ApplyMp3CommandOnlyMic();
+            } else if (suppress_wake_until_listen_ || Board_IsLessonSyncing() ||
+                       Board_IsRepeaterTask() || Board_IsShadowingTask()) {
                 audio_service_.EnableWakeWordDetection(false);
                 audio_service_.SetAudioPowerInhibit(true);
                 audio_service_.KeepCodecReadyForPlayback();
-            } else if (Board_IsRepeaterTask()) {
-                audio_service_.EnableVoiceProcessing(false);
-                audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord());
-                audio_service_.SetAudioPowerInhibit(false);
-            } else if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
-                ApplyMp3CommandOnlyMic();
             } else {
                 audio_service_.EnableWakeWordDetection(true);
                 audio_service_.SetAudioPowerInhibit(false);
@@ -1522,6 +1754,10 @@
             // Do nothing
             break;
     }
+}
+
+bool Application::IsOnMainLoop() const {
+    return main_loop_task_ != nullptr && xTaskGetCurrentTaskHandle() == main_loop_task_;
 }
 
 void Application::Schedule(std::function<void()>&& callback) {
@@ -1710,6 +1946,7 @@
         tts_stop_pending_ = false;
         tts_stop_waiting_ = false;
         tts_idle_waiting_ = false;
+        tts_session_end_ = false;
         tts_speaking_state_ready_ = false;
         tts_activity_generation_++;
 
@@ -1770,20 +2007,28 @@
 }
 
 void Application::FinishTtsResponse() {
+    const bool end_session = tts_session_end_;
+    const bool hold_idle = hold_idle_for_prompt_;
     tts_audio_expected_ = false;
     tts_response_started_ = false;
     tts_audio_received_ = false;
     tts_stop_pending_ = false;
     tts_stop_waiting_ = false;
     tts_idle_waiting_ = false;
+    tts_session_end_ = false;
     tts_speaking_state_ready_ = false;
     tts_playback_empty_ticks_ = 0;
     tts_stop_wait_ticks_ = 0;
     speaking_started_us_ = 0;
     audio_service_.SetAudioPowerInhibit(false);
-    suppress_wake_until_listen_ = false;
-    pending_detect_text_.clear();
-    pending_detect_retries_left_ = 0;
+    if (hold_idle || ShouldHoldIdleForLessonWork()) {
+        suppress_wake_until_listen_ = true;
+    } else {
+        suppress_wake_until_listen_ = false;
+        pending_detect_text_.clear();
+        pending_detect_retries_left_ = 0;
+        pending_detect_retry_ticks_ = 0;
+    }
     ResumeSdCardMp3AfterVoice();
     if (resume_sdcard_mp3_after_tts_ && !sdcard_mp3_stop_requested_) {
         resume_sdcard_mp3_after_tts_ = false;
@@ -1792,8 +2037,12 @@
         resume_sdcard_mp3_after_tts_ = false;
     }
     if (GetDeviceState() == kDeviceStateSpeaking) {
-        if (listening_mode_ == kListeningModeManualStop ||
+        if (end_session || hold_idle || ShouldHoldIdleForLessonWork() ||
+            listening_mode_ == kListeningModeManualStop ||
             (IsSdCardMp3Playing() && !IsSdCardMp3Paused())) {
+            ESP_LOGI(TAG, "TTS done, return to idle session_end=%d hold=%d lesson=%d",
+                     end_session ? 1 : 0, hold_idle ? 1 : 0,
+                     ShouldHoldIdleForLessonWork() ? 1 : 0);
             SetDeviceState(kDeviceStateIdle);
         } else {
             listening_mode_ = GetDefaultListeningMode();
@@ -1803,6 +2052,7 @@
     if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
         ApplyMp3CommandOnlyMic();
     }
+    Board_FlushPendingLessonPlayback();
     if (power_off_requested_) {
         power_off_requested_ = false;
         Board_Enter_Power_Off();
@@ -1866,8 +2116,9 @@
         tts_stop_wait_ticks_++;
         const int empty_needed = tts_audio_received_ ? 1 : 2;
         if (playback_empty && tts_playback_empty_ticks_ >= empty_needed) {
-            ESP_LOGI(TAG, "TTS done, return to listen empty=%d wait=%d received=%d",
-                     tts_playback_empty_ticks_, tts_stop_wait_ticks_, tts_audio_received_);
+            ESP_LOGI(TAG, "TTS done empty=%d wait=%d received=%d session_end=%d",
+                     tts_playback_empty_ticks_, tts_stop_wait_ticks_, tts_audio_received_,
+                     tts_session_end_ ? 1 : 0);
             FinishTtsResponse();
         } else if (!playback_empty && tts_stop_wait_ticks_ >= 30) {
             ESP_LOGW(TAG, "TTS playback stuck after stop, return to listen");
@@ -1878,13 +2129,13 @@
 
     // 娆㈣繋璇�/闀垮洖绛旓細stop 娌″埌鏃讹紝鍙湪銆屼竴鐩存病鏀跺埌 opus銆嶆垨銆屾挱瀹屽悗闀挎椂闂寸┖闃熷垪銆嶆墠鍥炲惉銆�
     // 涓嶈鐢ㄨ璇濆閽� 15s/闃熷垪绌� 2s锛氫腑鑻卞璇濋暱鍙ヤ細鍦ㄥ彞闂寸┖闅欐垨 15 绉掑琚帎鏂��
-    if (!tts_audio_received_ && elapsed_ms >= 5000) {
-        ESP_LOGW(TAG, "TTS stop missing, return to listen elapsed=%lldms received=0 empty=%d",
-                 (long long)elapsed_ms, tts_playback_empty_ticks_);
+    if (!tts_audio_received_ && elapsed_ms >= 10000) {
+        ESP_LOGW(TAG, "TTS stop missing, return to listen elapsed=%dms received=0 empty=%d",
+                 (int)elapsed_ms, tts_playback_empty_ticks_);
         FinishTtsResponse();
     } else if (playback_empty && tts_audio_received_ && tts_playback_empty_ticks_ >= 8) {
-        ESP_LOGW(TAG, "TTS stop missing after idle playback elapsed=%lldms empty=%d",
-                 (long long)elapsed_ms, tts_playback_empty_ticks_);
+        ESP_LOGW(TAG, "TTS stop missing after idle playback elapsed=%dms empty=%d",
+                 (int)elapsed_ms, tts_playback_empty_ticks_);
         FinishTtsResponse();
     }
 }
@@ -1901,6 +2152,12 @@
         return false;
     }
 
+    // 娉涘惉鏄� idle 鎾� SD MP3锛孉udioService 闃熷垪鏄┖鐨勶紝浼氳褰撴垚鍙紤鐪犮��
+    // 璺熻姣忓彞鏈夊惉绐楁墍浠ヤ笉浼氳繘 WiFi 寰呮満锛涙硾鍚細鎶婄綉鍏虫帀锛岀粨鏉熻灏卞彂涓嶅嚭鍘汇��
+    if (IsSdCardMp3Playing() || Board_IsRepeaterTask() || Board_IsShadowingTask()) {
+        return false;
+    }
+
     if (!audio_service_.IsIdle()) {
         return false;
     }

--
Gitblit v1.9.1