| | |
| | | #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> |
| | |
| | | |
| | | #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(); |
| | |
| | | |
| | | 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()); |
| | | |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | break; |
| | | } |
| | | }); |
| | | |
| | | LoadLocalAssetsForWakeWord(); |
| | | |
| | | // Start network asynchronously |
| | | board.StartNetwork(); |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | // Update the status bar immediately to show the network state |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | auto display = board.GetDisplay(); |
| | | display->UpdateStatusBar(true); |
| | | } |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | 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(); |
| | |
| | | |
| | | 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()); |
| | | }); |
| | |
| | | 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"); |
| | | } |
| | |
| | | }); |
| | | |
| | | 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]() { |
| | |
| | | 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; |
| | | } |
| | | }); |
| | | }); |
| | | |
| | |
| | | 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()); |
| | | }); |
| | | } |
| | |
| | | } 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()); |
| | | }); |
| | |
| | | } 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) { |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | |
| | | if (state == kDeviceStateIdle) { |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | PauseSdCardMp3ForVoice(); |
| | | SetDeviceState(kDeviceStateConnecting); |
| | | // Schedule to let the state change be processed first (UI update) |
| | | Schedule([this]() { |
| | |
| | | }); |
| | | return; |
| | | } |
| | | PauseSdCardMp3ForVoice(); |
| | | SetListeningMode(kListeningModeManualStop); |
| | | } else if (state == kDeviceStateSpeaking) { |
| | | AbortSpeaking(kAbortReasonNone); |
| | |
| | | } |
| | | |
| | | 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; |
| | | } |
| | |
| | | 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(); |
| | | |
| | |
| | | }); |
| | | 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 |
| | |
| | | } |
| | | |
| | | 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); |
| | |
| | | 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) |
| | |
| | | display->SetChatMessage("system", ""); |
| | | break; |
| | | case kDeviceStateListening: |
| | | tts_speaking_state_ready_ = false; |
| | | display->SetStatus(Lang::Strings::LISTENING); |
| | | display->SetEmotion("neutral"); |
| | | |
| | |
| | | } |
| | | break; |
| | | case kDeviceStateSpeaking: |
| | | tts_speaking_state_ready_ = false; |
| | | display->SetStatus(Lang::Strings::SPEAKING); |
| | | |
| | | if (listening_mode_ != kListeningModeRealtime) { |
| | |
| | | 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 |
| | |
| | | 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(); |
| | | |
| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | 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; |
| | |
| | | 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; |
| | | } |
| | |
| | | }); |
| | | return; |
| | | } |
| | | // Channel already opened, continue directly |
| | | ContinueWakeWordInvoke(wake_word); |
| | | Schedule([this, wake_word]() { |
| | | ContinueWakeWordInvoke(wake_word); |
| | | }); |
| | | } else if (state == kDeviceStateSpeaking) { |
| | | Schedule([this]() { |
| | | AbortSpeaking(kAbortReasonNone); |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | | }); |