From 70481429367ca00144b24f118cbfdfb28b02967d Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期三, 26 八月 2026 15:54:31 +0800
Subject: [PATCH] add sening functions

---
 xiaozhi-esp32-2.2.6/main/application.cc |  573 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 540 insertions(+), 33 deletions(-)

diff --git a/xiaozhi-esp32-2.2.6/main/application.cc b/xiaozhi-esp32-2.2.6/main/application.cc
index 0a809fa..e6e0d7d 100644
--- a/xiaozhi-esp32-2.2.6/main/application.cc
+++ b/xiaozhi-esp32-2.2.6/main/application.cc
@@ -5,12 +5,15 @@
 #include "audio_codec.h"
 #include "mqtt_protocol.h"
 #include "websocket_protocol.h"
+#include "boards/sening-xiaozhi-s3/mqtt.h"
 #include "assets/lang_config.h"
 #include "mcp_server.h"
 #include "assets.h"
 #include "settings.h"
+#include "boards/sening-xiaozhi-s3/system.h"
 
 #include <cstring>
+#include <memory>
 #include <esp_log.h>
 #include <cJSON.h>
 #include <driver/gpio.h>
@@ -19,6 +22,59 @@
 
 #define TAG "Application"
 
+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));
+
+static bool IsSdCardMp3Playing() {
+    return SDCard_Is_Playing && SDCard_Is_Playing();
+}
+
+static bool IsSdCardMp3Paused() {
+    return SDCard_Is_Paused && SDCard_Is_Paused();
+}
+
+static void PauseSdCardMp3() {
+    if (SDCard_Pause_Play) {
+        SDCard_Pause_Play();
+    }
+}
+
+static void ResumeSdCardMp3() {
+    if (SDCard_Resume_Play) {
+        SDCard_Resume_Play();
+    }
+}
+
+static void StopSdCardMp3() {
+    if (SDCard_Stop_Play) {
+        SDCard_Stop_Play();
+    }
+}
+
+static bool IsInternalToolSpeech(const char* text) {
+    if (!text) {
+        return false;
+    }
+    while (*text == ' ' || *text == '\t') {
+        ++text;
+    }
+    if (strncmp(text, "% self.", 7) == 0 || strncmp(text, "%self.", 6) == 0) {
+        return true;
+    }
+    return (strstr(text, "\"arguments\"") && strstr(text, "\"trigger_time\"")) || strstr(text, "\"label\"");
+}
+
+static bool IsStandbyRequestText(const char* text) {
+    if (!text) {
+        return false;
+    }
+    return strstr(text, "寰呭懡") || strstr(text, "杩涘叆寰呮満") || strstr(text, "杩涘叆寰呮満鐘舵��") ||
+        strstr(text, "杩涘叆浼戠湢") || strstr(text, "浼戠湢鐘舵��") || strstr(text, "鍋滄鐩戝惉") ||
+        strstr(text, "鍒惉浜�") || strstr(text, "涓嶈鍚簡");
+}
 
 Application::Application() {
     event_group_ = xEventGroupCreate();
@@ -60,11 +116,15 @@
 
 void Application::Initialize() {
     auto& board = Board::GetInstance();
+    ota_ = std::make_unique<Ota>();
+    ota_->MarkCurrentVersionValid();
+
     SetDeviceState(kDeviceStateStarting);
 
     // Setup the display
     auto display = board.GetDisplay();
     display->SetupUI();
+    ESP_LOGI(TAG, "Application firmware version: %s", APP_FIRMWARE_VERSION);
     // Print board name/version info
     display->SetChatMessage("system", SystemInfo::GetUserAgent().c_str());
 
@@ -78,7 +138,14 @@
         xEventGroupSetBits(event_group_, MAIN_EVENT_SEND_AUDIO);
     };
     callbacks.on_wake_word_detected = [this](const std::string& wake_word) {
-        xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED);
+        if (!power_off_standby_active_.load()) {
+            xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED);
+        }
+    };
+    callbacks.on_offline_command_detected = [this](const std::string& command) {
+        if (!power_off_standby_active_.load()) {
+            Board_Handle_Offline_Command(command.c_str());
+        }
     };
     callbacks.on_vad_change = [this](bool speaking) {
         xEventGroupSetBits(event_group_, MAIN_EVENT_VAD_CHANGE);
@@ -130,6 +197,8 @@
             case NetworkEvent::Disconnected:
                 xEventGroupSetBits(event_group_, MAIN_EVENT_NETWORK_DISCONNECTED);
                 break;
+            case NetworkEvent::ConnectionFailed:
+                break;
             case NetworkEvent::WifiConfigModeEnter:
                 // WiFi config mode enter is handled by WifiBoard internally
                 break;
@@ -154,6 +223,8 @@
                 break;
         }
     });
+
+    LoadLocalAssetsForWakeWord();
 
     // Start network asynchronously
     board.StartNetwork();
@@ -284,6 +355,11 @@
 }
 
 void Application::HandleNetworkDisconnectedEvent() {
+    auto& board = Board::GetInstance();
+    if (board.IsNetworkReady()) {
+        return;
+    }
+
     // Close current conversation when network disconnected
     auto state = GetDeviceState();
     if (state == kDeviceStateConnecting || state == kDeviceStateListening || state == kDeviceStateSpeaking) {
@@ -292,7 +368,7 @@
     }
 
     // Update the status bar immediately to show the network state
-    auto display = Board::GetInstance().GetDisplay();
+    auto display = board.GetDisplay();
     display->UpdateStatusBar(true);
 }
 
@@ -320,9 +396,19 @@
     });
 }
 
+void Application::LoadLocalAssetsForWakeWord() {
+    auto& assets = Assets::GetInstance();
+    if (!assets.partition_valid() || local_assets_applied_) {
+        return;
+    }
+
+    local_assets_applied_ = assets.Apply(false);
+}
+
 void Application::ActivationTask() {
-    // Create OTA object for activation process
-    ota_ = std::make_unique<Ota>();
+    if (!ota_) {
+        ota_ = std::make_unique<Ota>();
+    }
 
     // Check for new assets version
     CheckAssetsVersion();
@@ -372,7 +458,7 @@
 
         bool success = assets.Download(download_url, [this, display](int progress, size_t speed) -> void {
             char buffer[32];
-            snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
+            snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, static_cast<unsigned>(speed / 1024));
             Schedule([display, message = std::string(buffer)]() {
                 display->SetChatMessage("system", message.c_str());
             });
@@ -387,10 +473,13 @@
             SetDeviceState(kDeviceStateActivating);
             return;
         }
+        local_assets_applied_ = false;
     }
 
     // Apply assets
-    assets.Apply();
+    if (!local_assets_applied_) {
+        local_assets_applied_ = assets.Apply();
+    }
     display->SetChatMessage("system", "");
     display->SetEmotion("microchip_ai");
 }
@@ -496,9 +585,20 @@
     });
     
     protocol_->OnIncomingAudio([this](std::unique_ptr<AudioStreamPacket> packet) {
-        if (GetDeviceState() == kDeviceStateSpeaking) {
-            audio_service_.PushPacketToDecodeQueue(std::move(packet));
-        }
+        auto shared_packet = std::shared_ptr<AudioStreamPacket>(packet.release());
+        Schedule([this, shared_packet]() {
+            auto state = GetDeviceState();
+            if (state != kDeviceStateSpeaking && !tts_audio_expected_) {
+                return;
+            }
+
+            tts_response_started_ = true;
+
+            ScheduleTtsAudio(shared_packet);
+            if (state == kDeviceStateListening) {
+                SetDeviceState(kDeviceStateSpeaking);
+            }
+        });
     });
     
     protocol_->OnAudioChannelOpened([this, codec, &board]() {
@@ -513,8 +613,34 @@
         board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER);
         Schedule([this]() {
             auto display = Board::GetInstance().GetDisplay();
+            opening_audio_channel_ = false;
+            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_speaking_state_ready_ = false;
+            tts_activity_generation_++;
+            audio_service_.ResetDecoder();
             display->SetChatMessage("system", "");
             SetDeviceState(kDeviceStateIdle);
+            if (power_off_requested_) {
+                power_off_requested_ = false;
+                resume_sdcard_mp3_after_tts_ = false;
+                resume_sdcard_mp3_after_voice_ = false;
+                Board_Enter_Power_Off();
+                return;
+            }
+            if (standby_requested_) {
+                standby_requested_ = false;
+            }
+            if (resume_sdcard_mp3_after_tts_ && !sdcard_mp3_stop_requested_) {
+                resume_sdcard_mp3_after_tts_ = false;
+                ResumeSdCardMp3();
+            } else {
+                resume_sdcard_mp3_after_tts_ = false;
+            }
         });
     });
     
@@ -526,23 +652,44 @@
             if (strcmp(state->valuestring, "start") == 0) {
                 Schedule([this]() {
                     aborted_ = false;
-                    SetDeviceState(kDeviceStateSpeaking);
+                    tts_audio_expected_ = true;
+                    tts_response_started_ = false;
+                    tts_audio_received_ = false;
+                    tts_activity_generation_++;
                 });
             } else if (strcmp(state->valuestring, "stop") == 0) {
                 Schedule([this]() {
-                    if (GetDeviceState() == kDeviceStateSpeaking) {
-                        if (listening_mode_ == kListeningModeManualStop) {
-                            SetDeviceState(kDeviceStateIdle);
-                        } else {
-                            SetDeviceState(kDeviceStateListening);
-                        }
+                    if (!tts_response_started_ && !standby_requested_ && !power_off_requested_) {
+                        tts_stop_pending_ = true;
+                        return;
                     }
+                    tts_stop_pending_ = true;
+                    FinishTtsResponseAfterPlayback();
                 });
             } else if (strcmp(state->valuestring, "sentence_start") == 0) {
                 auto text = cJSON_GetObjectItem(root, "text");
                 if (cJSON_IsString(text)) {
                     ESP_LOGI(TAG, "<< %s", text->valuestring);
-                    Schedule([display, message = std::string(text->valuestring)]() {
+                    if (IsInternalToolSpeech(text->valuestring)) {
+                        ESP_LOGW(TAG, "Ignore internal tool speech: %s", text->valuestring);
+                        Schedule([this]() {
+                            tts_audio_expected_ = false;
+                            tts_response_started_ = false;
+                        });
+                        return;
+                    }
+                    Schedule([this, display, message = std::string(text->valuestring)]() {
+                        tts_audio_expected_ = true;
+                        tts_response_started_ = true;
+                        tts_activity_generation_++;
+                        if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
+                            PauseSdCardMp3();
+                            resume_sdcard_mp3_after_tts_ = true;
+                        }
+                        if (GetDeviceState() == kDeviceStateListening) {
+                            SetDeviceState(kDeviceStateSpeaking);
+                        }
+                        ScheduleTtsIdleFallback();
                         display->SetChatMessage("assistant", message.c_str());
                     });
                 }
@@ -550,7 +697,15 @@
         } else if (strcmp(type->valuestring, "stt") == 0) {
             auto text = cJSON_GetObjectItem(root, "text");
             if (cJSON_IsString(text)) {
+                if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
+                    PauseSdCardMp3();
+                    resume_sdcard_mp3_after_tts_ = true;
+                }
                 ESP_LOGI(TAG, ">> %s", text->valuestring);
+                BoardMqtt_PublishSttEvent(text->valuestring);
+                if (IsStandbyRequestText(text->valuestring)) {
+                    EnterStandby();
+                }
                 Schedule([display, message = std::string(text->valuestring)]() {
                     display->SetChatMessage("user", message.c_str());
                 });
@@ -565,6 +720,23 @@
         } else if (strcmp(type->valuestring, "mcp") == 0) {
             auto payload = cJSON_GetObjectItem(root, "payload");
             if (cJSON_IsObject(payload)) {
+                auto method = cJSON_GetObjectItem(payload, "method");
+                auto params = cJSON_GetObjectItem(payload, "params");
+                auto name = cJSON_IsObject(params) ? cJSON_GetObjectItem(params, "name") : nullptr;
+                if (cJSON_IsString(method) && strcmp(method->valuestring, "tools/call") == 0 && cJSON_IsString(name)) {
+                    if (strcmp(name->valuestring, "self.mp3.stop") == 0) {
+                        sdcard_mp3_stop_requested_ = true;
+                        BoardMqtt_StopPlaylistPlayback();
+                        resume_sdcard_mp3_after_tts_ = false;
+                        resume_sdcard_mp3_after_voice_ = false;
+                    } else if (strcmp(name->valuestring, "self.mp3.play") == 0) {
+                        sdcard_mp3_stop_requested_ = false;
+                        if (GetDeviceState() == kDeviceStateSpeaking) {
+                            PauseSdCardMp3();
+                            resume_sdcard_mp3_after_tts_ = true;
+                        }
+                    }
+                }
                 McpServer::GetInstance().ParseMessage(payload);
             }
         } else if (strcmp(type->valuestring, "system") == 0) {
@@ -672,6 +844,11 @@
 }
 
 void Application::HandleToggleChatEvent() {
+    if (power_off_standby_active_.load()) {
+        ESP_LOGI(TAG, "Ignore toggle chat while power-off standby is active");
+        return;
+    }
+
     auto state = GetDeviceState();
     
     if (state == kDeviceStateActivating) {
@@ -711,21 +888,59 @@
 }
 
 void Application::ContinueOpenAudioChannel(ListeningMode mode) {
+    if (power_off_standby_active_.load()) {
+        ESP_LOGI(TAG, "Ignore open audio channel while power-off standby is active");
+        return;
+    }
+
     // Check state again in case it was changed during scheduling
     if (GetDeviceState() != kDeviceStateConnecting) {
         return;
     }
+    if (opening_audio_channel_) {
+        return;
+    }
 
+    opening_audio_channel_ = true;
     if (!protocol_->IsAudioChannelOpened()) {
+        if (!Board::GetInstance().EnsureNetworkReadyForWake(30000)) {
+            opening_audio_channel_ = false;
+            SetDeviceState(kDeviceStateIdle);
+            return;
+        }
         if (!protocol_->OpenAudioChannel()) {
+            opening_audio_channel_ = false;
+            SetDeviceState(kDeviceStateIdle);
             return;
         }
     }
+    opening_audio_channel_ = false;
 
     SetListeningMode(mode);
 }
 
+void Application::PauseSdCardMp3ForVoice() {
+    if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) {
+        PauseSdCardMp3();
+        resume_sdcard_mp3_after_voice_ = true;
+    }
+}
+
+void Application::ResumeSdCardMp3AfterVoice() {
+    if (resume_sdcard_mp3_after_voice_ && !sdcard_mp3_stop_requested_) {
+        resume_sdcard_mp3_after_voice_ = false;
+        ResumeSdCardMp3();
+    } else {
+        resume_sdcard_mp3_after_voice_ = false;
+    }
+}
+
 void Application::HandleStartListeningEvent() {
+    if (power_off_standby_active_.load()) {
+        ESP_LOGI(TAG, "Ignore start listening while power-off standby is active");
+        return;
+    }
+
     auto state = GetDeviceState();
     
     if (state == kDeviceStateActivating) {
@@ -744,6 +959,7 @@
     
     if (state == kDeviceStateIdle) {
         if (!protocol_->IsAudioChannelOpened()) {
+            PauseSdCardMp3ForVoice();
             SetDeviceState(kDeviceStateConnecting);
             // Schedule to let the state change be processed first (UI update)
             Schedule([this]() {
@@ -751,6 +967,7 @@
             });
             return;
         }
+        PauseSdCardMp3ForVoice();
         SetListeningMode(kListeningModeManualStop);
     } else if (state == kDeviceStateSpeaking) {
         AbortSpeaking(kAbortReasonNone);
@@ -774,6 +991,11 @@
 }
 
 void Application::HandleWakeWordDetectedEvent() {
+    if (power_off_standby_active_.load()) {
+        ESP_LOGI(TAG, "Ignore wake word while power-off standby is active");
+        return;
+    }
+
     if (!protocol_) {
         return;
     }
@@ -783,6 +1005,7 @@
     ESP_LOGI(TAG, "Wake word detected: %s (state: %d)", wake_word.c_str(), (int)state);
 
     if (state == kDeviceStateIdle) {
+        PauseSdCardMp3ForVoice();
         audio_service_.EncodeWakeWord();
         auto wake_word = audio_service_.GetLastWakeWord();
 
@@ -795,8 +1018,9 @@
             });
             return;
         }
-        // Channel already opened, continue directly
-        ContinueWakeWordInvoke(wake_word);
+        Schedule([this, wake_word]() {
+            ContinueWakeWordInvoke(wake_word);
+        });
     } else if (state == kDeviceStateSpeaking || state == kDeviceStateListening) {
         AbortSpeaking(kAbortReasonWakeWordDetected);
         // Clear send queue to avoid sending residues to server
@@ -820,23 +1044,44 @@
 }
 
 void Application::ContinueWakeWordInvoke(const std::string& wake_word) {
-    // Check state again in case it was changed during scheduling
-    if (GetDeviceState() != kDeviceStateConnecting) {
+    if (power_off_standby_active_.load()) {
+        ESP_LOGI(TAG, "Ignore wake word invoke while power-off standby is active");
         return;
     }
 
+    // Check state again in case it was changed during scheduling
+    auto state = GetDeviceState();
+    if (state != kDeviceStateConnecting && state != kDeviceStateIdle) {
+        return;
+    }
+    if (opening_audio_channel_) {
+        return;
+    }
+
+    opening_audio_channel_ = true;
+    if (!Board::GetInstance().EnsureNetworkReadyForWake(30000)) {
+        opening_audio_channel_ = false;
+        SetDeviceState(kDeviceStateIdle);
+        audio_service_.EnableWakeWordDetection(true);
+        return;
+    }
     if (!protocol_->IsAudioChannelOpened()) {
         if (!protocol_->OpenAudioChannel()) {
+            opening_audio_channel_ = false;
+            ResumeSdCardMp3AfterVoice();
             audio_service_.EnableWakeWordDetection(true);
             return;
         }
     }
+    opening_audio_channel_ = false;
 
     ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
 #if CONFIG_SEND_WAKE_WORD_DATA
     // Encode and send the wake word data to the server
     while (auto packet = audio_service_.PopWakeWordPacket()) {
-        protocol_->SendAudio(std::move(packet));
+        if (!protocol_->SendAudio(std::move(packet))) {
+            break;
+        }
     }
     // Set the chat state to wake word detected
     protocol_->SendWakeWordDetected(wake_word);
@@ -857,10 +1102,29 @@
     auto display = board.GetDisplay();
     auto led = board.GetLed();
     led->OnStateChanged();
-    
+
+    if (power_off_standby_active_.load()) {
+        opening_audio_channel_ = false;
+        play_popup_on_listening_ = false;
+        audio_service_.EnableVoiceProcessing(false);
+        audio_service_.EnableWakeWordDetection(false);
+        audio_service_.ResetDecoder();
+        return;
+    }
+
     switch (new_state) {
         case kDeviceStateUnknown:
         case kDeviceStateIdle:
+            opening_audio_channel_ = false;
+            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_speaking_state_ready_ = false;
+            tts_activity_generation_++;
+            ResumeSdCardMp3AfterVoice();
             display->SetStatus(Lang::Strings::STANDBY);
             display->ClearChatMessages();  // Clear messages first
             display->SetEmotion("neutral"); // Then set emotion (wechat mode checks child count)
@@ -873,6 +1137,7 @@
             display->SetChatMessage("system", "");
             break;
         case kDeviceStateListening:
+            tts_speaking_state_ready_ = false;
             display->SetStatus(Lang::Strings::LISTENING);
             display->SetEmotion("neutral");
 
@@ -904,6 +1169,7 @@
             }
             break;
         case kDeviceStateSpeaking:
+            tts_speaking_state_ready_ = false;
             display->SetStatus(Lang::Strings::SPEAKING);
 
             if (listening_mode_ != kListeningModeRealtime) {
@@ -912,10 +1178,11 @@
                 audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord());
             }
             audio_service_.ResetDecoder();
+            tts_speaking_state_ready_ = true;
             break;
         case kDeviceStateWifiConfiguring:
             audio_service_.EnableVoiceProcessing(false);
-            audio_service_.EnableWakeWordDetection(false);
+            audio_service_.EnableWakeWordDetection(true);
             break;
         default:
             // Do nothing
@@ -961,7 +1228,9 @@
     esp_restart();
 }
 
-bool Application::UpgradeFirmware(const std::string& url, const std::string& version) {
+bool Application::UpgradeFirmware(const std::string& url, const std::string& version,
+                                  std::function<void(int progress, size_t speed)> progress_callback,
+                                  std::function<void(bool success)> complete_callback) {
     auto& board = Board::GetInstance();
     auto display = board.GetDisplay();
 
@@ -978,6 +1247,7 @@
     Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "download", Lang::Sounds::OGG_UPGRADE);
     vTaskDelay(pdMS_TO_TICKS(3000));
 
+    AcquireSleepInhibit();
     SetDeviceState(kDeviceStateUpgrading);
 
     std::string message = std::string(Lang::Strings::NEW_VERSION) + version_info;
@@ -987,12 +1257,15 @@
     audio_service_.Stop();
     vTaskDelay(pdMS_TO_TICKS(1000));
 
-    bool upgrade_success = Ota::Upgrade(upgrade_url, [this, display](int progress, size_t speed) {
+    bool upgrade_success = Ota::Upgrade(upgrade_url, [this, display, progress_callback](int progress, size_t speed) {
         char buffer[32];
-        snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
+        snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, static_cast<unsigned>(speed / 1024));
         Schedule([display, message = std::string(buffer)]() {
             display->SetChatMessage("system", message.c_str());
         });
+        if (progress_callback) {
+            progress_callback(progress, speed);
+        }
     });
 
     if (!upgrade_success) {
@@ -1000,6 +1273,10 @@
         ESP_LOGE(TAG, "Firmware upgrade failed, restarting audio service and continuing operation...");
         audio_service_.Start(); // Restart audio service
         board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); // Restore power save level
+        ReleaseSleepInhibit();
+        if (complete_callback) {
+            complete_callback(false);
+        }
         Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION);
         vTaskDelay(pdMS_TO_TICKS(3000));
         return false;
@@ -1008,12 +1285,21 @@
         ESP_LOGI(TAG, "Firmware upgrade successful, rebooting...");
         display->SetChatMessage("system", "Upgrade successful, rebooting...");
         vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message
+        ReleaseSleepInhibit();
+        if (complete_callback) {
+            complete_callback(true);
+        }
         Reboot();
         return true;
     }
 }
 
 void Application::WakeWordInvoke(const std::string& wake_word) {
+    if (power_off_standby_active_.load()) {
+        ESP_LOGI(TAG, "Ignore wake word invoke while power-off standby is active");
+        return;
+    }
+
     if (!protocol_) {
         return;
     }
@@ -1031,8 +1317,9 @@
             });
             return;
         }
-        // Channel already opened, continue directly
-        ContinueWakeWordInvoke(wake_word);
+        Schedule([this, wake_word]() {
+            ContinueWakeWordInvoke(wake_word);
+        });
     } else if (state == kDeviceStateSpeaking) {
         Schedule([this]() {
             AbortSpeaking(kAbortReasonNone);
@@ -1046,12 +1333,226 @@
     }
 }
 
+void Application::EnterStandby() {
+    Schedule([this]() {
+        standby_requested_ = true;
+        auto state = GetDeviceState();
+        if (state == kDeviceStateIdle) {
+            standby_requested_ = false;
+        } else if (state == kDeviceStateSpeaking && tts_stop_pending_) {
+            FinishTtsResponseAfterPlayback();
+        }
+    });
+}
+
+void Application::RequestPowerOffStandby() {
+    Schedule([this]() {
+        power_off_requested_ = true;
+        auto state = GetDeviceState();
+        if (state == kDeviceStateIdle) {
+            power_off_requested_ = false;
+            Board_Enter_Power_Off();
+        } else if (state == kDeviceStateSpeaking && tts_stop_pending_) {
+            FinishTtsResponseAfterPlayback();
+        }
+    });
+}
+
+void Application::EnterPowerOffStandby() {
+    power_off_standby_active_.store(true);
+    Schedule([this]() {
+        ESP_LOGI(TAG, "Entering power-off standby");
+
+        standby_requested_ = false;
+        power_off_requested_ = false;
+        opening_audio_channel_ = false;
+        play_popup_on_listening_ = false;
+        resume_sdcard_mp3_after_tts_ = false;
+        resume_sdcard_mp3_after_voice_ = false;
+        sdcard_mp3_stop_requested_ = false;
+        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_speaking_state_ready_ = false;
+        tts_activity_generation_++;
+
+        if (GetDeviceState() == kDeviceStateSpeaking) {
+            AbortSpeaking(kAbortReasonNone);
+        }
+        if (protocol_ && protocol_->IsAudioChannelOpened()) {
+            protocol_->CloseAudioChannel();
+        }
+
+        audio_service_.EnableVoiceProcessing(false);
+        audio_service_.EnableWakeWordDetection(false);
+        audio_service_.ResetDecoder();
+        while (audio_service_.PopPacketFromSendQueue()) {
+        }
+        BoardMqtt_StopPlaylistPlayback();
+
+        auto state = GetDeviceState();
+        if (state == kDeviceStateConnecting || state == kDeviceStateListening || state == kDeviceStateSpeaking) {
+            SetDeviceState(kDeviceStateIdle);
+        }
+    });
+}
+
+void Application::RestoreFromPowerOffStandby(bool network_ready) {
+    Schedule([this, network_ready]() {
+        ESP_LOGI(TAG, "Restoring from power-off standby, network_ready=%d", network_ready);
+
+        power_off_standby_active_.store(false);
+        standby_requested_ = false;
+        power_off_requested_ = false;
+        opening_audio_channel_ = false;
+        play_popup_on_listening_ = false;
+        tts_activity_generation_++;
+
+        audio_service_.EnableVoiceProcessing(false);
+        audio_service_.ResetDecoder();
+        if (GetDeviceState() != kDeviceStateIdle) {
+            SetDeviceState(kDeviceStateIdle);
+        }
+        audio_service_.EnableWakeWordDetection(true);
+        BoardMqtt_ResumePlaylistIfUnfinished();
+    });
+}
+
+void Application::AcquireSleepInhibit() {
+    std::lock_guard<std::mutex> lock(mutex_);
+    sleep_inhibit_count_++;
+}
+
+void Application::ReleaseSleepInhibit() {
+    std::lock_guard<std::mutex> lock(mutex_);
+    if (sleep_inhibit_count_ > 0) {
+        sleep_inhibit_count_--;
+    }
+}
+
+void Application::FinishTtsResponse() {
+    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_speaking_state_ready_ = false;
+    if (GetDeviceState() == kDeviceStateSpeaking) {
+        if (listening_mode_ == kListeningModeManualStop) {
+            SetDeviceState(kDeviceStateIdle);
+        } else {
+            SetDeviceState(kDeviceStateListening);
+        }
+    }
+    ResumeSdCardMp3AfterVoice();
+    if (resume_sdcard_mp3_after_tts_ && !sdcard_mp3_stop_requested_) {
+        resume_sdcard_mp3_after_tts_ = false;
+        ResumeSdCardMp3();
+    } else {
+        resume_sdcard_mp3_after_tts_ = false;
+    }
+    if (power_off_requested_) {
+        power_off_requested_ = false;
+        Board_Enter_Power_Off();
+        return;
+    }
+    if (standby_requested_) {
+        standby_requested_ = false;
+        SetDeviceState(kDeviceStateIdle);
+    }
+}
+
+void Application::ScheduleTtsAudio(std::shared_ptr<AudioStreamPacket> packet) {
+    Schedule([this, packet]() {
+        if (GetDeviceState() != kDeviceStateSpeaking || !tts_audio_expected_) {
+            return;
+        }
+        if (!tts_speaking_state_ready_) {
+            ScheduleTtsAudio(packet);
+            return;
+        }
+        tts_audio_received_ = true;
+        tts_activity_generation_++;
+        auto audio_packet = std::make_unique<AudioStreamPacket>(std::move(*packet));
+        audio_service_.PushPacketToDecodeQueue(std::move(audio_packet));
+        ScheduleTtsIdleFallback();
+        if (tts_stop_pending_) {
+            FinishTtsResponseAfterPlayback();
+        }
+    });
+}
+
+void Application::FinishTtsResponseAfterPlayback() {
+    if (tts_stop_waiting_) {
+        return;
+    }
+    if (tts_audio_expected_ && !tts_audio_received_) {
+        ScheduleTtsIdleFallback();
+        return;
+    }
+    tts_stop_waiting_ = true;
+    xTaskCreate([](void* arg) {
+        auto* app = static_cast<Application*>(arg);
+        uint32_t generation = app->tts_activity_generation_;
+        app->audio_service_.WaitForPlaybackQueueEmpty();
+        vTaskDelay(pdMS_TO_TICKS(800));
+        app->Schedule([app, generation]() {
+            if (app->tts_activity_generation_ == generation) {
+                app->FinishTtsResponse();
+            } else {
+                app->tts_stop_waiting_ = false;
+                if (app->GetDeviceState() == kDeviceStateSpeaking) {
+                    if (app->tts_stop_pending_) {
+                        app->FinishTtsResponseAfterPlayback();
+                    } else {
+                        app->ScheduleTtsIdleFallback();
+                    }
+                }
+            }
+        });
+        vTaskDelete(NULL);
+    }, "tts_finish", 4096, this, 2, NULL);
+}
+
+void Application::ScheduleTtsIdleFallback() {
+    if (tts_idle_waiting_ || tts_stop_waiting_) {
+        return;
+    }
+    tts_idle_waiting_ = true;
+    xTaskCreate([](void* arg) {
+        auto* app = static_cast<Application*>(arg);
+        uint32_t generation = app->tts_activity_generation_;
+        vTaskDelay(pdMS_TO_TICKS(1500));
+        app->Schedule([app, generation]() {
+            app->tts_idle_waiting_ = false;
+            if (app->tts_activity_generation_ != generation || app->GetDeviceState() != kDeviceStateSpeaking) {
+                return;
+            }
+            if (app->tts_audio_received_) {
+                app->FinishTtsResponseAfterPlayback();
+                return;
+            }
+            if (app->tts_stop_pending_ || app->tts_audio_expected_) {
+                app->FinishTtsResponse();
+            }
+        });
+        vTaskDelete(NULL);
+    }, "tts_idle", 4096, this, 2, NULL);
+}
+
 bool Application::CanEnterSleepMode() {
-    if (GetDeviceState() != kDeviceStateIdle) {
-        return false;
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        if (sleep_inhibit_count_ > 0) {
+            return false;
+        }
     }
 
-    if (protocol_ && protocol_->IsAudioChannelOpened()) {
+    if (GetDeviceState() != kDeviceStateIdle) {
         return false;
     }
 
@@ -1063,10 +1564,16 @@
     return true;
 }
 
+void Application::PrepareProtocolForNetworkStandby() {
+    if (protocol_) {
+        protocol_->PrepareForNetworkStandby();
+    }
+}
+
 void Application::SendMcpMessage(const std::string& payload) {
     // Always schedule to run in main task for thread safety
     Schedule([this, payload = std::move(payload)]() {
-        if (protocol_) {
+        if (protocol_ && !power_off_standby_active_.load()) {
             protocol_->SendMcpMessage(payload);
         }
     });

--
Gitblit v1.9.1