Create v1.7.6 聆听空闲超时自动退出 + Bug修复

一、新增功能
1. 聆听状态空闲超时自动退出
   - 聆听状态下无用户交互(无语音对话、无按键操作、无音频播放)超过60秒后,
     设备自动关闭音频通道回到idle待命状态,行为等同于手动按下BOOT按键退出
   - 超时时间通过Kconfig CONFIG_LISTENING_IDLE_TIMEOUT_SECONDS可配置(范围30~300秒,默认60秒)
   - speaking状态期间暂停计时,回到listening后从0重新倒计时,确保用户有完整的思考时间

2. 聆听空闲计时器外部重置接口
   - 新增ResetListeningIdleTimer()公开方法,供板级按键/触摸回调调用
   - 重置触发点:触摸按键(摸脑袋等)、BOOT按键、故事按键、收到服务端stt/tts/music_control/story消息

二、Bug修复
3. 修复超时退出后待命音效无声
   - 原因:超时退出路径中audio_processor_.Stop()关闭了功放,之后才播放待命音效
   - 修复:在SetDeviceState(kDeviceStateIdle)播放待命音效前调用codec->EnableOutput(true)确保功放开启

4. 修复WebSocket断开与tts:stop竞态导致崩溃重启
   - 原因:tts:stop和WebSocket断开同时发生时,设备切换到listening触发SendStartListening失败,
     竞态导致WakeWordDetect堆损坏(StoreProhibited崩溃)
   - 修复:tts:stop处理中先检查IsAudioChannelOpened(),音频通道不可用时直接回退到idle

5. 修复listening状态音频通道不可用时逻辑错误
   - 原因:音频通道不可用时"保持在listening状态"导致后续状态混乱
   - 修复:改为直接回退到idle状态

三、优化调整
6. 版本号从1.7.5升级到1.7.6
7. ADC电量采样间隔从10ms缩短为5ms,提高采样效率
8. 日志配置调整:恢复日志输出能力用于调试
9. WiFi组件代码注释补充
10. 新增.gitignore,忽略build目录和.vscode/settings.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rdzleo 2026-04-01 11:17:37 +08:00
parent ad98cf4110
commit bc14e60836
13 changed files with 94 additions and 147 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# 忽略根目录下的 build 文件夹(包括其所有子文件/子文件夹)
/build
# 忽略 VSCode 本地配置
.vscode/settings.json

113
.vscode/settings.json vendored
View File

@ -1,113 +0,0 @@
{
"idf.openOcdConfigs": [
"board/esp32s3-bridge.cfg"
],
"idf.customExtraVars": {
"IDF_TARGET": "esp32s3"
},
"idf.flashType": "UART",
"idf.portWin": "COM9",
"files.associations": {
"algorithm": "cpp",
"atomic": "cpp",
"bit": "cpp",
"cctype": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"compare": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"exception": "cpp",
"format": "cpp",
"forward_list": "cpp",
"fstream": "cpp",
"functional": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"ios": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"iterator": "cpp",
"limits": "cpp",
"list": "cpp",
"locale": "cpp",
"map": "cpp",
"memory": "cpp",
"mutex": "cpp",
"new": "cpp",
"optional": "cpp",
"ostream": "cpp",
"ratio": "cpp",
"regex": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"string": "cpp",
"system_error": "cpp",
"thread": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp",
"unordered_map": "cpp",
"utility": "cpp",
"vector": "cpp",
"xfacet": "cpp",
"xhash": "cpp",
"xiosbase": "cpp",
"xlocale": "cpp",
"xlocbuf": "cpp",
"xlocinfo": "cpp",
"xlocmes": "cpp",
"xlocmon": "cpp",
"xlocnum": "cpp",
"xloctime": "cpp",
"xmemory": "cpp",
"xstddef": "cpp",
"xstring": "cpp",
"xtr1common": "cpp",
"xtree": "cpp",
"xutility": "cpp",
"__bit_reference": "cpp",
"__hash_table": "cpp",
"__locale": "cpp",
"__node_handle": "cpp",
"__split_buffer": "cpp",
"__tree": "cpp",
"__verbose_abort": "cpp",
"array": "cpp",
"bitset": "cpp",
"cstdarg": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"execution": "cpp",
"print": "cpp",
"queue": "cpp",
"stack": "cpp",
"string_view": "cpp",
"variant": "cpp",
"complex": "cpp",
"sdkconfig.h": "c",
"unordered_set": "cpp",
"cinttypes": "cpp",
"span": "cpp",
"osmemory": "cpp",
"osutility": "cpp",
"__config": "cpp"
},
"idf.port": "/dev/tty.usbmodem834401",
"idf.espIdfPath": "/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf",
"idf.toolsPath": "/Users/rdzleo/.espressif",
"idf.pythonInstallPath": "/opt/homebrew/bin/python3",
"git.ignoreLimitWarning": true
}

View File

@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16)
# 1.5.6
# OTA
set(PROJECT_VER "1.7.5")
set(PROJECT_VER "1.7.6")
# Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers)

View File

@ -285,6 +285,14 @@ config USE_REALTIME_CHAT
help
需要 ESP32 S3 与 AEC 开启,因为性能不够,不建议和微信聊天界面风格同时开启
config LISTENING_IDLE_TIMEOUT_SECONDS
int "聆听状态空闲超时时间(秒)"
default 60
range 30 300
help
聆听状态下无用户交互(无语音对话、无按键操作、无音频播放)超过此时间后,
设备自动退出聆听回到待命状态等同于手动按下BOOT按键退出。
endmenu
# 蓝牙配网功能配置选项

View File

@ -632,6 +632,16 @@ void Application::Start() {
protocol_->OnIncomingJson([this, display](const cJSON* root) {
// Parse JSON data
auto type = cJSON_GetObjectItem(root, "type");
// 收到服务端有效消息时重置聆听空闲计时器
// stt=用户说话被识别, tts=AI回复, music_control=故事/音乐, story=故事状态
if (strcmp(type->valuestring, "stt") == 0 ||
strcmp(type->valuestring, "tts") == 0 ||
strcmp(type->valuestring, "music_control") == 0 ||
strcmp(type->valuestring, "story") == 0) {
listening_idle_ticks_ = 0;
}
if (strcmp(type->valuestring, "tts") == 0) {
auto state = cJSON_GetObjectItem(root, "state");
if (strcmp(state->valuestring, "start") == 0) {
@ -645,7 +655,10 @@ void Application::Start() {
Schedule([this]() {
background_task_->WaitForCompletion();
if (device_state_ == kDeviceStateSpeaking) {
if (listening_mode_ == kListeningModeManualStop) {
// 切换状态前检查音频通道是否仍然可用防止在WebSocket已断开时
// 切到listening触发SendStartListening失败导致崩溃
if (listening_mode_ == kListeningModeManualStop ||
!protocol_ || !protocol_->IsAudioChannelOpened()) {
SetDeviceState(kDeviceStateIdle);
} else {
SetDeviceState(kDeviceStateListening);
@ -970,6 +983,23 @@ void Application::Start() {
void Application::OnClockTimer() {
clock_ticks_++;
// 聆听状态空闲超时检测:只在 listening 状态下递增speaking 时暂停
if (device_state_ == kDeviceStateListening) {
listening_idle_ticks_++;
if (listening_idle_ticks_ >= CONFIG_LISTENING_IDLE_TIMEOUT_SECONDS) {
ESP_LOGI(TAG, "聆听空闲超时 %d 秒,自动退出聆听状态", CONFIG_LISTENING_IDLE_TIMEOUT_SECONDS);
listening_idle_ticks_ = 0;
Schedule([this]() {
if (device_state_ == kDeviceStateListening && protocol_) {
protocol_->CloseAudioChannel();
}
});
}
} else {
// 非 listening 状态(含 speaking重置计时器
listening_idle_ticks_ = 0;
}
// Print the debug info every 10 seconds
if (clock_ticks_ % 10 == 0) {
int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
@ -1338,8 +1368,13 @@ void Application::SetDeviceState(DeviceState state) {
// }
// 开机后 进入待命状态 播报 卡卡正在待命(配网模式下不播报“卡卡正在待命”)-新增代码
//=====================================================================================
if (previous_state != kDeviceStateIdle && previous_state != kDeviceStateUnknown &&
if (previous_state != kDeviceStateIdle && previous_state != kDeviceStateUnknown &&
previous_state != kDeviceStateWifiConfiguring && !is_blufi_provisioning && !IsLowBatteryTransition()) {
// 确保功放开启,防止从聆听超时退出时功放已关闭导致待命音效无声
auto codec = board.GetAudioCodec();
if (codec) {
codec->EnableOutput(true);
}
ESP_LOGI(TAG, "Entering idle state, playing standby sound");
// PlaySound(Lang::Sounds::P3_DAIMING); 原有 待命 播报
if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){
@ -1364,6 +1399,8 @@ void Application::SetDeviceState(DeviceState state) {
display->SetChatMessage("system", "");
break;
case kDeviceStateListening:
// 进入聆听状态时无条件重置空闲计时器,避免残留倒计时
listening_idle_ticks_ = 0;
display->SetStatus(Lang::Strings::LISTENING);
display->SetEmotion("neutral");
@ -1410,9 +1447,8 @@ void Application::SetDeviceState(DeviceState state) {
audio_processor_.Start();
#endif
} else {
ESP_LOGW(TAG, "Audio channel not available, skipping SendStartListening");
// 保持在聆听状态不自动回退到idle状态
ESP_LOGI(TAG, "🔵 Staying in listening state despite audio channel unavailable");
ESP_LOGW(TAG, "Audio channel not available, falling back to idle");
SetDeviceState(kDeviceStateIdle);
}
}
break;
@ -2005,3 +2041,8 @@ void Application::SetLowBatteryTransition(bool value) {
bool Application::IsLowBatteryTransition() const {
return is_low_battery_transition_.load();// 获取低电量过渡状态
}
// 重置聆听空闲计时器
void Application::ResetListeningIdleTimer() {
listening_idle_ticks_ = 0;
}

View File

@ -90,6 +90,7 @@ public:
void SuppressNextIdleSound();
void SetLowBatteryTransition(bool value);
bool IsLowBatteryTransition() const;
void ResetListeningIdleTimer(); // 重置聆听空闲计时器(外部按键/触摸调用)
// 姿态传感器接口
bool IsImuSensorAvailable(); // 检查IMU传感器是否可用
@ -137,6 +138,7 @@ private:
std::chrono::time_point<std::chrono::steady_clock> last_audio_input_time_;
int clock_ticks_ = 0;
int listening_idle_ticks_ = 0; // 聆听状态空闲计时器(秒)
TaskHandle_t main_loop_task_handle_ = nullptr;
TaskHandle_t check_new_version_task_handle_ = nullptr;

View File

@ -250,6 +250,7 @@ public:
void SendTouchMessage(int touch_pad_num) {
const char* message = nullptr;
power_save_timer_->WakeUp();
Application::GetInstance().ResetListeningIdleTimer(); // 触摸操作重置聆听空闲计时
// 获取当前应用状态
auto& app = Application::GetInstance();
@ -607,6 +608,7 @@ public:
}
ESP_LOGI(TAG, "当前设备状态: %d", current_state);
app.ResetListeningIdleTimer(); // BOOT按键重置聆听空闲计时
if (current_state == kDeviceStateIdle) {
// 如果当前是待命状态,切换到聆听状态
@ -1128,7 +1130,7 @@ public:
return;
}
adc_samples.push_back(adc_value);
vTaskDelay(pdMS_TO_TICKS(10)); // 每次采样间隔10ms
vTaskDelay(pdMS_TO_TICKS(5)); // 每次采样间隔5ms
}
std::sort(adc_samples.begin(), adc_samples.end());
@ -1243,6 +1245,7 @@ public:
// 唤醒设备,防止立即进入睡眠
power_save_timer_->WakeUp();
app.ResetListeningIdleTimer(); // 故事按键重置聆听空闲计时
});
ESP_LOGI(TAG, "Story button initialized on GPIO%d", KEY4_GPIO);

View File

@ -11,8 +11,8 @@
#define TAG "main"
// 新增禁用日志配置(生产环境)
// 重定向printf到空函数彻底禁用所有输出 新增禁用日志配置
// // 新增禁用日志配置(生产环境)
// // 重定向printf到空函数彻底禁用所有输出 新增禁用日志配置
// // =======================禁用日志参输出=============================
// extern "C" {
// int printf(const char* format, ...) { return 0; }
@ -24,9 +24,9 @@
extern "C" void app_main(void)
{
// 新增禁用日志配置(生产环境)
// // 新增禁用日志配置(生产环境)
// // // 新增禁用日志配置(生产环境)
// // // ====================================================================================================
// // ====================================================================================================
// //全局禁用所有日志输出 - 必须在最开始就设置
// esp_log_level_set("*", ESP_LOG_NONE); // 全局禁用所有日志
// //特别禁用可能的残留日志组件

View File

@ -54,8 +54,8 @@ private:
std::function<void(const std::string& ssid)> on_connect_;// 连接开始时调用
std::function<void(const std::string& ssid)> on_connected_;// 连接成功时调用
std::function<void()> on_scan_begin_;
std::function<void()> on_no_candidates_;
std::function<void()> on_reconnect_timeout_;
std::function<void()> on_no_candidates_;// 没有候选AP时调用
std::function<void()> on_reconnect_timeout_;// 重新连接超时时调用
std::vector<WifiApRecord> connect_queue_;
esp_timer_handle_t reconnect_timer_handle_ = nullptr;

View File

@ -375,7 +375,7 @@ CONFIG_IDF_TOOLCHAIN_GCC=y
CONFIG_IDF_TARGET_ARCH_XTENSA=y
CONFIG_IDF_TARGET_ARCH="xtensa"
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_INIT_VERSION="5.4.2"
CONFIG_IDF_INIT_VERSION="$IDF_INIT_VERSION"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009
@ -563,15 +563,15 @@ CONFIG_PARTITION_TABLE_MD5=y
#
# Kapi Assistant
#
CONFIG_OTA_VERSION_URL="https://xiaozhi-dev-web.goods.fun/xiaozhi/ota/"
CONFIG_BATTERY_REPORT_URL="https://kapibala-ai.dev.goods.fun/api/v1/public/device/update-battery/"
CONFIG_OTA_VERSION_URL="https://xiaozhi-prod-web.goods.fun/xiaozhi/ota/"
CONFIG_BATTERY_REPORT_URL="https://kapibala-ai.goods.fun/api/v1/public/device/update-battery/"
CONFIG_LANGUAGE_ZH_CN=y
# CONFIG_LANGUAGE_ZH_TW is not set
# CONFIG_LANGUAGE_EN_US is not set
# CONFIG_LANGUAGE_JA_JP is not set
# CONFIG_CONNECTION_TYPE_MQTT_UDP is not set
CONFIG_CONNECTION_TYPE_WEBSOCKET=y
CONFIG_WEBSOCKET_URL="wss://xiaozhi-dev-api.goods.fun/xiaozhi/v1"
CONFIG_WEBSOCKET_URL="wss://xiaozhi-prod-api.goods.fun/xiaozhi/v1"
CONFIG_WEBSOCKET_ACCESS_TOKEN="test-token"
# CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI is not set
# CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD is not set
@ -629,6 +629,7 @@ CONFIG_USE_WAKE_WORD_DETECT=y
# CONFIG_USE_CUSTOM_WAKE_WORD is not set
CONFIG_USE_AUDIO_PROCESSOR=y
# CONFIG_USE_REALTIME_CHAT is not set
CONFIG_LISTENING_IDLE_TIMEOUT_SECONDS=60
# end of Kapi Assistant
#
@ -772,6 +773,7 @@ CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y
#
# CONFIG_APPTRACE_DEST_JTAG is not set
CONFIG_APPTRACE_DEST_NONE=y
# CONFIG_APPTRACE_DEST_UART0 is not set
# CONFIG_APPTRACE_DEST_UART1 is not set
# CONFIG_APPTRACE_DEST_UART2 is not set
# CONFIG_APPTRACE_DEST_USB_CDC is not set
@ -1695,18 +1697,16 @@ CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y
# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set
CONFIG_ESP_MAIN_TASK_AFFINITY=0x0
CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
# CONFIG_ESP_CONSOLE_USB_CDC is not set
# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG is not set
# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set
# CONFIG_ESP_CONSOLE_NONE is not set
CONFIG_ESP_CONSOLE_NONE=y
# CONFIG_ESP_CONSOLE_SECONDARY_NONE is not set
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y
CONFIG_ESP_CONSOLE_UART=y
CONFIG_ESP_CONSOLE_UART_NUM=0
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
CONFIG_ESP_CONSOLE_UART_NUM=-1
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=-1
CONFIG_ESP_INT_WDT=y
CONFIG_ESP_INT_WDT_TIMEOUT_MS=300
CONFIG_ESP_INT_WDT_CHECK_CPU1=y
@ -2985,13 +2985,11 @@ CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240
CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32
CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=4096
CONFIG_MAIN_TASK_STACK_SIZE=4096
CONFIG_CONSOLE_UART_DEFAULT=y
# CONFIG_CONSOLE_UART_DEFAULT is not set
# CONFIG_CONSOLE_UART_CUSTOM is not set
# CONFIG_CONSOLE_UART_NONE is not set
# CONFIG_ESP_CONSOLE_UART_NONE is not set
CONFIG_CONSOLE_UART=y
CONFIG_CONSOLE_UART_NUM=0
CONFIG_CONSOLE_UART_BAUDRATE=115200
CONFIG_CONSOLE_UART_NONE=y
CONFIG_ESP_CONSOLE_UART_NONE=y
CONFIG_CONSOLE_UART_NUM=-1
CONFIG_INT_WDT=y
CONFIG_INT_WDT_TIMEOUT_MS=300
CONFIG_INT_WDT_CHECK_CPU1=y

View File

@ -112,6 +112,7 @@ CONFIG_LV_BUILD_EXAMPLES=n
# Audio Processing Configuration for AEC+VAD Echo-Aware Optimization
CONFIG_USE_AUDIO_PROCESSOR=y
CONFIG_USE_REALTIME_CHAT=y
CONFIG_LISTENING_IDLE_TIMEOUT_SECONDS=60
# Standard ESP-IDF Audio and Logging Configuration
# CONFIG_LOG_DEFAULT_LEVEL_INFO=y 原有打印日志配置 恢复原有日志打印可以取消注释

View File

@ -113,6 +113,7 @@ CONFIG_LV_BUILD_EXAMPLES=n
# Audio Processing Configuration for AEC+VAD Echo-Aware Optimization
CONFIG_USE_AUDIO_PROCESSOR=y
CONFIG_USE_REALTIME_CHAT=y
CONFIG_LISTENING_IDLE_TIMEOUT_SECONDS=60
# Standard ESP-IDF Audio and Logging Configuration
# CONFIG_LOG_DEFAULT_LEVEL_INFO=y 原有打印日志配置 恢复原有日志打印可以取消注释

View File

@ -375,7 +375,7 @@ CONFIG_IDF_TOOLCHAIN_GCC=y
CONFIG_IDF_TARGET_ARCH_XTENSA=y
CONFIG_IDF_TARGET_ARCH="xtensa"
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_INIT_VERSION="$IDF_INIT_VERSION"
CONFIG_IDF_INIT_VERSION="5.4.2"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009
@ -563,15 +563,15 @@ CONFIG_PARTITION_TABLE_MD5=y
#
# Kapi Assistant
#
CONFIG_OTA_VERSION_URL="https://xiaozhi-dev-web.goods.fun/xiaozhi/ota/"
CONFIG_BATTERY_REPORT_URL="https://kapibala-ai.dev.goods.fun/api/v1/public/device/update-battery/"
CONFIG_OTA_VERSION_URL="https://xiaozhi-prod-web.goods.fun/xiaozhi/ota/"
CONFIG_BATTERY_REPORT_URL="https://kapibala-ai.goods.fun/api/v1/public/device/update-battery/"
CONFIG_LANGUAGE_ZH_CN=y
# CONFIG_LANGUAGE_ZH_TW is not set
# CONFIG_LANGUAGE_EN_US is not set
# CONFIG_LANGUAGE_JA_JP is not set
# CONFIG_CONNECTION_TYPE_MQTT_UDP is not set
CONFIG_CONNECTION_TYPE_WEBSOCKET=y
CONFIG_WEBSOCKET_URL="wss://xiaozhi-dev-api.goods.fun/xiaozhi/v1"
CONFIG_WEBSOCKET_URL="wss://xiaozhi-prod-api.goods.fun/xiaozhi/v1"
CONFIG_WEBSOCKET_ACCESS_TOKEN="test-token"
# CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI is not set
# CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD is not set
@ -629,6 +629,7 @@ CONFIG_USE_WAKE_WORD_DETECT=y
# CONFIG_USE_CUSTOM_WAKE_WORD is not set
CONFIG_USE_AUDIO_PROCESSOR=y
# CONFIG_USE_REALTIME_CHAT is not set
CONFIG_LISTENING_IDLE_TIMEOUT_SECONDS=60
# end of Kapi Assistant
#
@ -644,7 +645,7 @@ CONFIG_BLUETOOTH_PROVISIONING_WIFI_RETRY=5
# CONFIG_BLUETOOTH_PROVISIONING_VERBOSE_LOG is not set
# end of 蓝牙配网 (Bluetooth Provisioning)
CONFIG_DEVICE_ROLE=""
CONFIG_DEVICE_ROLE="KAKA"
#
# ESP Speech Recognition