| | |
| | | #include "mqtt_protocol.h" |
| | | #include "websocket_protocol.h" |
| | | #include "boards/sening-xiaozhi-s3/mqtt.h" |
| | | #include "boards/sening-xiaozhi-s3/app_task.h" |
| | | #include "assets/lang_config.h" |
| | | #include "mcp_server.h" |
| | | #include "assets.h" |
| | |
| | | .arg = this, |
| | | .dispatch_method = ESP_TIMER_TASK, |
| | | .name = "clock_timer", |
| | | .skip_unhandled_events = true |
| | | .skip_unhandled_events = false |
| | | }; |
| | | esp_timer_create(&clock_timer_args, &clock_timer_handle_); |
| | | } |
| | |
| | | } |
| | | }; |
| | | callbacks.on_offline_command_detected = [this](const std::string& command) { |
| | | if (!power_off_standby_active_.load()) { |
| | | Board_Handle_Offline_Command(command.c_str()); |
| | | if (power_off_standby_active_.load()) { |
| | | return; |
| | | } |
| | | // AFE audio_detection only has a 4KB stack. Starting repeater (NVS, cJSON, |
| | | // SD list, MP3, JPG) on that task overflows and reboots. |
| | | Schedule([command]() { |
| | | Board_Handle_Offline_Command(command.c_str()); |
| | | }); |
| | | }; |
| | | callbacks.on_vad_change = [this](bool speaking) { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_VAD_CHANGE); |
| | |
| | | for (auto& task : tasks) { |
| | | task(); |
| | | } |
| | | if (GetDeviceState() == kDeviceStateSpeaking) { |
| | | MaybeFinishTtsAfterPlayback(); |
| | | } |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_CLOCK_TICK) { |
| | | clock_ticks_++; |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->UpdateStatusBar(); |
| | | |
| | | auto state = GetDeviceState(); |
| | | if (state == kDeviceStateConnecting || state == kDeviceStateSpeaking || |
| | | (state == kDeviceStateIdle && suppress_wake_until_listen_)) { |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | } |
| | | MaybeFinishTtsAfterPlayback(); |
| | | if (suppress_wake_until_listen_ && state == kDeviceStateIdle) { |
| | | if (protocol_ && protocol_->IsAudioChannelConnected()) { |
| | | boot_wake_restore_ticks_ = 0; |
| | | 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()); |
| | | } |
| | | } |
| | | } else if (++boot_wake_restore_ticks_ >= 20) { |
| | | ESP_LOGW(TAG, "boot websocket not opened, restore wake word"); |
| | | suppress_wake_until_listen_ = false; |
| | | boot_wake_restore_ticks_ = 0; |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | } |
| | | } else { |
| | | boot_wake_restore_ticks_ = 0; |
| | | } |
| | | if (state == kDeviceStateSpeaking && tts_audio_expected_ && !tts_audio_received_ && |
| | | 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); |
| | | } |
| | | } |
| | | if (protocol_ && IsSdCardMp3Playing() && protocol_->IsAudioChannelConnected()) { |
| | | protocol_->MarkIncomingActivity(); |
| | | } |
| | | if (!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_ = 25; |
| | | ESP_LOGW(TAG, "retry listen detect after handshake fail: %s left=%d", |
| | | pending_detect_text_.c_str(), pending_detect_retries_left_); |
| | | SendListenDetect(pending_detect_text_, true); |
| | | } else { |
| | | pending_detect_text_.clear(); |
| | | } |
| | | } |
| | | |
| | | // Print debug info every 10 seconds |
| | | if (clock_ticks_ % 10 == 0) { |
| | | SystemInfo::PrintHeapStats(); |
| | |
| | | ESP_LOGI(TAG, "Activation done"); |
| | | |
| | | SystemInfo::PrintHeapStats(); |
| | | // 开机马上要连语音 WebSocket。先不要拉起 AFE:内部 SRAM 被 WakeNet 占满时 |
| | | // 第一次握手会 10 秒超时,重试虽能连上,欢迎词 opus 也容易丢。 |
| | | suppress_wake_until_listen_ = true; |
| | | boot_wake_restore_ticks_ = 0; |
| | | SetDeviceState(kDeviceStateIdle); |
| | | |
| | | has_server_time_ = ota_->HasServerTime(); |
| | |
| | | // Release OTA object after activation is complete |
| | | ota_.reset(); |
| | | auto& board = Board::GetInstance(); |
| | | board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); |
| | | // 开机马上就要连语音 WebSocket 播欢迎词。这里切 LOW_POWER 会让随后的握手 |
| | | // 卡在 handshake timeout,欢迎词根本发不出来。保持高性能直到通道打开。 |
| | | board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | |
| | | Schedule([this]() { |
| | | // Play the success sound to indicate the device is ready |
| | |
| | | |
| | | protocol_->OnNetworkError([this](const std::string& message) { |
| | | last_error_message_ = message; |
| | | if (network_error_suppressed_) { |
| | | ESP_LOGW(TAG, "network error suppressed during retry: %s", message.c_str()); |
| | | return; |
| | | } |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_ERROR); |
| | | }); |
| | | |
| | |
| | | auto shared_packet = std::shared_ptr<AudioStreamPacket>(packet.release()); |
| | | Schedule([this, shared_packet]() { |
| | | auto state = GetDeviceState(); |
| | | if (state != kDeviceStateSpeaking && !tts_audio_expected_) { |
| | | // speak_txt 不发独立 tts start,首句 opus 可能早于 sentence_start。 |
| | | // 待命/聆听时不要丢包,交给 ScheduleTtsAudio 等到 speaking 就绪再播。 |
| | | if (state != kDeviceStateSpeaking && !tts_audio_expected_ && |
| | | state != kDeviceStateListening && state != kDeviceStateIdle) { |
| | | return; |
| | | } |
| | | if (state == kDeviceStateListening || state == kDeviceStateIdle) { |
| | | tts_audio_expected_ = true; |
| | | } |
| | | |
| | | tts_response_started_ = true; |
| | | ESP_LOGI(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, |
| | | static_cast<int>(state)); |
| | | |
| | | ScheduleTtsAudio(shared_packet); |
| | | if (state == kDeviceStateListening) { |
| | | if (state == kDeviceStateListening || state == kDeviceStateIdle) { |
| | | SetDeviceState(kDeviceStateSpeaking); |
| | | } |
| | | }); |
| | |
| | | }); |
| | | |
| | | protocol_->OnAudioChannelClosed([this, &board]() { |
| | | board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); |
| | | if (opening_audio_channel_) { |
| | | 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); |
| | | } |
| | | Schedule([this]() { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | opening_audio_channel_ = false; |
| | | if ((Board_IsShadowingTask() || Board_IsRepeaterTask()) && IsSdCardMp3Playing()) { |
| | | if (GetDeviceState() == kDeviceStateListening) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | return; |
| | | } |
| | | tts_audio_expected_ = false; |
| | | tts_response_started_ = false; |
| | | tts_audio_received_ = false; |
| | |
| | | tts_audio_expected_ = true; |
| | | tts_response_started_ = false; |
| | | tts_audio_received_ = false; |
| | | tts_stop_pending_ = false; |
| | | tts_stop_waiting_ = false; |
| | | tts_playback_empty_ticks_ = 0; |
| | | tts_stop_wait_ticks_ = 0; |
| | | tts_activity_generation_++; |
| | | audio_service_.SetAudioPowerInhibit(true); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | auto state = GetDeviceState(); |
| | | if (state == kDeviceStateListening || state == kDeviceStateIdle) { |
| | | SetDeviceState(kDeviceStateSpeaking); |
| | | } |
| | | ESP_LOGI(TAG, "TTS start output=%d", |
| | | Board::GetInstance().GetAudioCodec()->output_enabled() ? 1 : 0); |
| | | }); |
| | | } else if (strcmp(state->valuestring, "stop") == 0) { |
| | | Schedule([this]() { |
| | | if (!tts_response_started_ && !standby_requested_ && !power_off_requested_) { |
| | | tts_stop_pending_ = true; |
| | | return; |
| | | } |
| | | ESP_LOGI(TAG, "TTS stop started=%d received=%d", |
| | | tts_response_started_, tts_audio_received_); |
| | | tts_stop_pending_ = true; |
| | | FinishTtsResponseAfterPlayback(); |
| | | }); |
| | |
| | | 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()) { |
| | | PauseSdCardMp3(); |
| | | resume_sdcard_mp3_after_tts_ = true; |
| | | } |
| | | if (GetDeviceState() == kDeviceStateListening) { |
| | | auto state = GetDeviceState(); |
| | | if (state == kDeviceStateListening || state == kDeviceStateIdle) { |
| | | SetDeviceState(kDeviceStateSpeaking); |
| | | } |
| | | ScheduleTtsIdleFallback(); |
| | | display->SetChatMessage("assistant", message.c_str()); |
| | | }); |
| | | } |
| | |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_STOP_LISTENING); |
| | | } |
| | | |
| | | bool Application::SendListenDetectNow(const std::string& text) { |
| | | if (!protocol_ || text.empty()) { |
| | | return false; |
| | | } |
| | | ESP_LOGI(TAG, "Send listen detect now: %s connected=%d", |
| | | text.c_str(), protocol_->IsAudioChannelConnected() ? 1 : 0); |
| | | if (!protocol_->IsAudioChannelConnected()) { |
| | | return false; |
| | | } |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | protocol_->MarkIncomingActivity(); |
| | | return protocol_->SendWakeWordDetected(text); |
| | | } |
| | | |
| | | void Application::SendListenDetect(const std::string& text, bool open_if_idle) { |
| | | Schedule([this, text, open_if_idle]() { |
| | | if (!protocol_ || text.empty()) { |
| | | return; |
| | | } |
| | | if (protocol_->IsAudioChannelOpened()) { |
| | | protocol_->SendWakeWordDetected(text); |
| | | ESP_LOGI(TAG, "Send listen detect: %s connected=%d", |
| | | text.c_str(), protocol_->IsAudioChannelConnected() ? 1 : 0); |
| | | if (protocol_->IsAudioChannelConnected()) { |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | protocol_->MarkIncomingActivity(); |
| | | if (protocol_->SendWakeWordDetected(text)) { |
| | | return; |
| | | } |
| | | ESP_LOGW(TAG, "detect send failed, reopen audio channel"); |
| | | } |
| | | if (!open_if_idle) { |
| | | return; |
| | | } |
| | | if (open_if_idle) { |
| | | WakeWordInvoke(text); |
| | | if (!Board::GetInstance().IsNetworkReady()) { |
| | | ESP_LOGW(TAG, "skip listen detect, network not ready: %s", text.c_str()); |
| | | return; |
| | | } |
| | | auto state = GetDeviceState(); |
| | | // 开机/TTS/配网时不要 WakeWordInvoke:那会进 listening、开 AFE、ResetDecoder, |
| | | // 欢迎词被清掉,FEED 环缓冲也会立刻撑满。 |
| | | if (state == kDeviceStateSpeaking || state == kDeviceStateWifiConfiguring || |
| | | state == kDeviceStateAudioTesting || state == kDeviceStateConnecting || |
| | | state == kDeviceStateStarting || state == kDeviceStateActivating) { |
| | | ESP_LOGI(TAG, "defer listen detect until idle: state=%d", static_cast<int>(state)); |
| | | return; |
| | | } |
| | | if (state != kDeviceStateIdle) { |
| | | return; |
| | | } |
| | | SetDeviceState(kDeviceStateConnecting); |
| | | Schedule([this, text]() { |
| | | ContinueOpenAudioChannelForDetect(text); |
| | | }); |
| | | }); |
| | | } |
| | | |
| | |
| | | if (GetDeviceState() != kDeviceStateConnecting) { |
| | | return; |
| | | } |
| | | if (opening_audio_channel_) { |
| | | if (!OpenAudioChannelWithRetry(2)) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | return; |
| | | } |
| | | |
| | | SetListeningMode(mode); |
| | | } |
| | | |
| | | bool Application::OpenAudioChannelWithRetry(int attempts) { |
| | | if (!protocol_) { |
| | | return false; |
| | | } |
| | | if (protocol_->IsAudioChannelConnected()) { |
| | | protocol_->MarkIncomingActivity(); |
| | | return true; |
| | | } |
| | | if (opening_audio_channel_) { |
| | | return false; |
| | | } |
| | | if (attempts < 1) { |
| | | attempts = 1; |
| | | } |
| | | |
| | | opening_audio_channel_ = true; |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | if (!Board::GetInstance().EnsureNetworkReadyForWake(30000)) { |
| | | opening_audio_channel_ = false; |
| | | SetDeviceState(kDeviceStateIdle); |
| | | return; |
| | | // 握手期间停 AFE,避免和 Wi-Fi 抢内部 SRAM/CPU。不要另建任务: |
| | | // AFE 起来后内部堆只剩几 KB,xTaskCreate(8KB) 会直接失败。 |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | audio_service_.SetAudioPowerInhibit(true); |
| | | |
| | | bool ok = false; |
| | | for (int i = 0; i < attempts; ++i) { |
| | | if (protocol_->IsAudioChannelConnected()) { |
| | | ok = true; |
| | | break; |
| | | } |
| | | if (!protocol_->OpenAudioChannel()) { |
| | | opening_audio_channel_ = false; |
| | | SetDeviceState(kDeviceStateIdle); |
| | | return; |
| | | if (!Board::GetInstance().EnsureNetworkReadyForWake(30000)) { |
| | | ESP_LOGW(TAG, "OpenAudioChannel aborted, network not ready"); |
| | | break; |
| | | } |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | const bool last = (i == attempts - 1); |
| | | network_error_suppressed_ = !last; |
| | | ok = protocol_->OpenAudioChannel(); |
| | | network_error_suppressed_ = false; |
| | | if (ok) { |
| | | break; |
| | | } |
| | | ESP_LOGW(TAG, "OpenAudioChannel failed (%d/%d)", i + 1, attempts); |
| | | protocol_->CloseAudioChannel(false); |
| | | if (!last) { |
| | | ESP_LOGI(TAG, "Retry websocket handshake in 500ms"); |
| | | vTaskDelay(pdMS_TO_TICKS(500)); |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | } |
| | | } |
| | | opening_audio_channel_ = false; |
| | | |
| | | SetListeningMode(mode); |
| | | opening_audio_channel_ = false; |
| | | audio_service_.SetAudioPowerInhibit(false); |
| | | return ok; |
| | | } |
| | | |
| | | void Application::ContinueOpenAudioChannelForDetect(const std::string& detect_text) { |
| | | if (power_off_standby_active_.load()) { |
| | | return; |
| | | } |
| | | if (GetDeviceState() != kDeviceStateConnecting) { |
| | | return; |
| | | } |
| | | 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; |
| | | } |
| | | 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"); |
| | | return; |
| | | } |
| | | |
| | | pending_detect_text_.clear(); |
| | | pending_detect_retries_left_ = 0; |
| | | pending_detect_retry_ticks_ = 0; |
| | | greeting_listen_wait_ticks_ = 8; |
| | | if (!detect_text.empty()) { |
| | | protocol_->MarkIncomingActivity(); |
| | | protocol_->SendWakeWordDetected(detect_text); |
| | | } |
| | | // 保持待命且不开 AFE:欢迎词从 idle 进 speaking。说完再 listening 开麦。 |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | |
| | | void Application::PauseSdCardMp3ForVoice() { |
| | |
| | | if (resume_sdcard_mp3_after_voice_ && !sdcard_mp3_stop_requested_) { |
| | | resume_sdcard_mp3_after_voice_ = false; |
| | | ResumeSdCardMp3(); |
| | | ApplyMp3CommandOnlyMic(); |
| | | } else { |
| | | resume_sdcard_mp3_after_voice_ = false; |
| | | } |
| | | } |
| | | |
| | | void Application::ApplyMp3CommandOnlyMic() { |
| | | if (GetDeviceState() == kDeviceStateWifiConfiguring || |
| | | GetDeviceState() == kDeviceStateSpeaking) { |
| | | return; |
| | | } |
| | | if (!IsSdCardMp3Playing() || IsSdCardMp3Paused()) { |
| | | return; |
| | | } |
| | | // 音频在播:不上行录音/闲聊,只留离线口令(退出复读/跟读、我们聊天吧)。 |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | if (GetDeviceState() != kDeviceStateListening) { |
| | | return; |
| | | } |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | protocol_->SendStopListening(); |
| | | } |
| | | // 播课文时切待命:关掉 AFE/上行,只留离线口令。跟读句间由播完后再 ResumeListening。 |
| | | Schedule([this]() { |
| | | if (GetDeviceState() == kDeviceStateListening && |
| | | IsSdCardMp3Playing() && !IsSdCardMp3Paused()) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | void Application::ResumeListeningAfterMp3Clip() { |
| | | if (power_off_standby_active_.load()) { |
| | | return; |
| | | } |
| | | if (IsSdCardMp3Playing() && !IsSdCardMp3Paused() && !Board_IsShadowingTask()) { |
| | | return; |
| | | } |
| | | auto state = GetDeviceState(); |
| | | // 开机欢迎词 / 进场 TTS 期间禁止开麦:EnableVoiceProcessing 会 ResetDecoder 并喂 AFE。 |
| | | if (state == kDeviceStateSpeaking || state == kDeviceStateWifiConfiguring || |
| | | state == kDeviceStateAudioTesting || state == kDeviceStateConnecting || |
| | | state == kDeviceStateStarting || state == kDeviceStateActivating) { |
| | | return; |
| | | } |
| | | if (!protocol_) { |
| | | return; |
| | | } |
| | | ESP_LOGI(TAG, "Resume listening after MP3 clip"); |
| | | if (state == kDeviceStateListening) { |
| | | protocol_->SendStartListening(listening_mode_); |
| | | audio_service_.EnableVoiceProcessing(true); |
| | | #ifdef CONFIG_WAKE_WORD_DETECTION_IN_LISTENING |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | #else |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | #endif |
| | | return; |
| | | } |
| | | if (state == kDeviceStateIdle && !protocol_->IsAudioChannelConnected()) { |
| | | return; |
| | | } |
| | | skip_playback_wait_on_listen_ = true; |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | } |
| | | |
| | | void Application::CloseShadowingListenWindow() |
| | | { |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | while (auto packet = audio_service_.PopPacketFromSendQueue()) { |
| | | if (!protocol_->SendAudio(std::move(packet))) { |
| | | break; |
| | | } |
| | | } |
| | | protocol_->SendStopListening(); |
| | | } |
| | | if (GetDeviceState() == kDeviceStateListening) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | } |
| | | |
| | |
| | | return; |
| | | } |
| | | |
| | | opening_audio_channel_ = true; |
| | | if (!Board::GetInstance().EnsureNetworkReadyForWake(30000)) { |
| | | opening_audio_channel_ = false; |
| | | SetDeviceState(kDeviceStateIdle); |
| | | if (!OpenAudioChannelWithRetry(2)) { |
| | | ResumeSdCardMp3AfterVoice(); |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | SetDeviceState(kDeviceStateIdle); |
| | | 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()) { |
| | | if (!protocol_->SendAudio(std::move(packet))) { |
| | | break; |
| | | } |
| | | } |
| | | // Set the chat state to wake word detected |
| | | protocol_->SendWakeWordDetected(wake_word); |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | #else |
| | | // Set flag to play popup sound after state changes to listening |
| | | // (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing) |
| | | play_popup_on_listening_ = true; |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | #endif |
| | |
| | | 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_++; |
| | | if (!suppress_wake_until_listen_) { |
| | | 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) |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | if (suppress_wake_until_listen_) { |
| | | 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); |
| | | } |
| | | break; |
| | | case kDeviceStateConnecting: |
| | | display->SetStatus(Lang::Strings::CONNECTING); |
| | |
| | | display->SetStatus(Lang::Strings::LISTENING); |
| | | display->SetEmotion("neutral"); |
| | | |
| | | // Make sure the audio processor is running |
| | | if (play_popup_on_listening_ || !audio_service_.IsAudioProcessorRunning()) { |
| | | // For auto mode, wait for playback queue to be empty before enabling voice processing |
| | | // This prevents audio truncation when STOP arrives late due to network jitter |
| | | if (listening_mode_ == kListeningModeAutoStop) { |
| | | audio_service_.WaitForPlaybackQueueEmpty(); |
| | | if (IsSdCardMp3Playing() && !IsSdCardMp3Paused() && !Board_IsShadowingTask()) { |
| | | ApplyMp3CommandOnlyMic(); |
| | | } else { |
| | | if (play_popup_on_listening_ || !audio_service_.IsAudioProcessorRunning()) { |
| | | if (listening_mode_ == kListeningModeAutoStop && !skip_playback_wait_on_listen_) { |
| | | audio_service_.WaitForPlaybackQueueEmpty(); |
| | | } |
| | | protocol_->SendStartListening(listening_mode_); |
| | | } |
| | | |
| | | // Send the start listening command |
| | | protocol_->SendStartListening(listening_mode_); |
| | | audio_service_.EnableVoiceProcessing(true); |
| | | } |
| | | |
| | | #ifdef CONFIG_WAKE_WORD_DETECTION_IN_LISTENING |
| | | // Enable wake word detection in listening mode (configured via Kconfig) |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | #else |
| | | // Disable wake word detection in listening mode |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | #endif |
| | | |
| | | // Play popup sound after ResetDecoder (in EnableVoiceProcessing) has been called |
| | | } |
| | | audio_service_.SetAudioPowerInhibit(false); |
| | | suppress_wake_until_listen_ = false; |
| | | skip_playback_wait_on_listen_ = false; |
| | | if (play_popup_on_listening_) { |
| | | play_popup_on_listening_ = false; |
| | | audio_service_.PlaySound(Lang::Sounds::OGG_POPUP); |
| | |
| | | break; |
| | | case kDeviceStateSpeaking: |
| | | tts_speaking_state_ready_ = false; |
| | | tts_playback_empty_ticks_ = 0; |
| | | tts_stop_wait_ticks_ = 0; |
| | | greeting_listen_wait_ticks_ = 0; |
| | | speaking_started_us_ = esp_timer_get_time(); |
| | | display->SetStatus(Lang::Strings::SPEAKING); |
| | | |
| | | if (listening_mode_ != kListeningModeRealtime) { |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | // Only AFE wake word can be detected in speaking mode |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | // TTS 回声会把菜单里的「跟读/我们聊天吧」听成口令,而且 AFE 会挤爆内部 SRAM。 |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | } |
| | | audio_service_.ResetDecoder(); |
| | | // 欢迎词的 opus 可能已经在待命时入队。这里 ResetDecoder 会清掉刚到的 TTS, |
| | | // 听起来像没播,随后又等不到 tts stop,一直停在说话中。 |
| | | if (!tts_audio_expected_ && !tts_audio_received_ && audio_service_.IsPlaybackQueueEmpty()) { |
| | | audio_service_.ResetDecoder(); |
| | | } |
| | | audio_service_.SetAudioPowerInhibit(true); |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | ESP_LOGI(TAG, "Speaking: codec ready output=%d", |
| | | Board::GetInstance().GetAudioCodec()->output_enabled() ? 1 : 0); |
| | | tts_speaking_state_ready_ = true; |
| | | break; |
| | | case kDeviceStateWifiConfiguring: |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | // MultiNet/AFE wake word consumes internal SRAM. The captive portal HTML |
| | | // and /scan both fail when free SRAM drops to hundreds of bytes |
| | | // (httpd send EAGAIN, blank page, empty AP list). Keep it off until |
| | | // WiFi is configured; Idle re-enables detection afterwards. |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | break; |
| | | default: |
| | | // Do nothing |
| | |
| | | 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); |
| | | } |
| | | } |
| | | 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; |
| | | 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 (GetDeviceState() == kDeviceStateSpeaking) { |
| | | if (listening_mode_ == kListeningModeManualStop || |
| | | (IsSdCardMp3Playing() && !IsSdCardMp3Paused())) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } else { |
| | | listening_mode_ = GetDefaultListeningMode(); |
| | | SetListeningMode(listening_mode_); |
| | | } |
| | | } |
| | | if (IsSdCardMp3Playing() && !IsSdCardMp3Paused()) { |
| | | ApplyMp3CommandOnlyMic(); |
| | | } |
| | | if (power_off_requested_) { |
| | | power_off_requested_ = false; |
| | |
| | | |
| | | void Application::ScheduleTtsAudio(std::shared_ptr<AudioStreamPacket> packet) { |
| | | Schedule([this, packet]() { |
| | | if (GetDeviceState() != kDeviceStateSpeaking || !tts_audio_expected_) { |
| | | auto state = GetDeviceState(); |
| | | if (state != kDeviceStateSpeaking && state != kDeviceStateIdle && |
| | | state != kDeviceStateListening) { |
| | | ESP_LOGW(TAG, "TTS audio ignored state=%d expected=%d", |
| | | static_cast<int>(state), tts_audio_expected_ ? 1 : 0); |
| | | return; |
| | | } |
| | | if (!tts_speaking_state_ready_) { |
| | | ScheduleTtsAudio(packet); |
| | | return; |
| | | } |
| | | tts_audio_expected_ = true; |
| | | tts_audio_received_ = true; |
| | | tts_activity_generation_++; |
| | | tts_playback_empty_ticks_ = 0; |
| | | audio_service_.KeepCodecReadyForPlayback(); |
| | | auto audio_packet = std::make_unique<AudioStreamPacket>(std::move(*packet)); |
| | | audio_service_.PushPacketToDecodeQueue(std::move(audio_packet)); |
| | | ScheduleTtsIdleFallback(); |
| | | if (!audio_service_.PushPacketToDecodeQueue(std::move(audio_packet), true)) { |
| | | ESP_LOGW(TAG, "TTS audio dropped, decode queue full"); |
| | | } |
| | | if (tts_stop_pending_) { |
| | | FinishTtsResponseAfterPlayback(); |
| | | } |
| | |
| | | } |
| | | |
| | | void Application::FinishTtsResponseAfterPlayback() { |
| | | if (tts_stop_waiting_) { |
| | | return; |
| | | } |
| | | if (tts_audio_expected_ && !tts_audio_received_) { |
| | | ScheduleTtsIdleFallback(); |
| | | return; |
| | | } |
| | | tts_stop_pending_ = true; |
| | | 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); |
| | | tts_stop_wait_ticks_ = 0; |
| | | } |
| | | |
| | | void Application::ScheduleTtsIdleFallback() { |
| | | if (tts_idle_waiting_ || tts_stop_waiting_) { |
| | | void Application::MaybeFinishTtsAfterPlayback() { |
| | | if (GetDeviceState() != kDeviceStateSpeaking) { |
| | | tts_playback_empty_ticks_ = 0; |
| | | tts_stop_wait_ticks_ = 0; |
| | | 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); |
| | | |
| | | const bool playback_empty = audio_service_.IsPlaybackQueueEmpty(); |
| | | if (playback_empty) { |
| | | tts_playback_empty_ticks_++; |
| | | } else { |
| | | tts_playback_empty_ticks_ = 0; |
| | | } |
| | | |
| | | const int64_t elapsed_ms = speaking_started_us_ > 0 |
| | | ? (esp_timer_get_time() - speaking_started_us_) / 1000 |
| | | : 0; |
| | | |
| | | if (tts_stop_waiting_ || tts_stop_pending_) { |
| | | 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_); |
| | | FinishTtsResponse(); |
| | | } else if (!playback_empty && tts_stop_wait_ticks_ >= 30) { |
| | | ESP_LOGW(TAG, "TTS playback stuck after stop, return to listen"); |
| | | FinishTtsResponse(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | // 欢迎词/长回答: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_); |
| | | 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_); |
| | | FinishTtsResponse(); |
| | | } |
| | | } |
| | | |
| | | bool Application::CanEnterSleepMode() { |