#include "wifi_board.h" #include "audio_codecs/es8311_audio_codec.h" #include "display/lcd_display.h" #include "application.h" #include "button.h" #include "config.h" #include "i2c_device.h" #include "iot/thing_manager.h" #include "../../components/gbk_encoding/include/gbk_encoding.h" // 使用完整路径 #include #include #include #include #include #include "led/single_led.h" #include "assets/lang_config.h" #include "esp_lcd_panel_gc9301.h" #include "power_save_timer.h" #include "power_manager.h" #include #include #include #include #include #include "esp_vfs_fat.h" #include "sdmmc_cmd.h" #include "driver/sdmmc_host.h" #include #include // 添加NVS头文件 #include // 添加NVS头文件 #include "audio_player.h" // 音频播放器 #define TAG "JiuchuangDevBoard" #define __USER_GPIO_PWRDOWN__ LV_FONT_DECLARE(font_puhui_20_4); LV_FONT_DECLARE(font_awesome_20_4); // 前向声明 class JiuchuangDevBoard; // 音频播放器必须的回调函数 static esp_err_t audio_mute_callback(AUDIO_PLAYER_MUTE_SETTING setting); static esp_err_t audio_clk_set_callback(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch); static esp_err_t audio_write_callback(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms); static void audio_event_callback(audio_player_cb_ctx_t *ctx); static esp_err_t restore_device_status(); static void sd_card_detect_task(void *arg); static bool is_supported_audio_file(const char *filename) { int len = strlen(filename); if (len <= 4) return false; const char *ext = filename + len - 4; return (strcasecmp(ext, ".mp3") == 0 || strcasecmp(ext, ".wav") == 0); } // 添加一个辅助函数用于将二进制数据转换成十六进制字符串,方便调试 static std::string bytes_to_hex(const uint8_t* data, size_t len) { std::string result; result.reserve(len * 3); for (size_t i = 0; i < len; i++) { char buf[4]; snprintf(buf, sizeof(buf), "%02X ", data[i]); result += buf; } return result; } // 添加一个简单的GBK到UTF-8转换映射表,覆盖常用的中文字符 struct GbkUtf8Mapping { uint16_t gbk_code; const char *utf8_str; }; // 常用的中文字符GBK到UTF-8的映射,扩充更多常用字符 static const GbkUtf8Mapping gbk_utf8_map[] = { // 您的音乐文件名中出现的字符 {0xB6AA, "丢"}, {0xCAD6, "手"}, {0xBEEE, "绢"}, // 丢手绢 {0xD0A1, "小"}, {0xCFBC, "霞"}, // 小霞 {0xD7F9, "座"}, {0xCEBB, "位"}, // 座位 {0xB8E6, "告"}, {0xB0D7, "白"}, {0xC6F8, "气"}, {0xC7F2, "球"}, // 告白气球 {0xB0AE, "爱"}, {0xB4ED, "错"}, // 爱错 // 扩充更多常用汉字 // 数字相关 {0xD2BB, "一"}, {0xB6FE, "二"}, {0xC8FD, "三"}, {0xCBC4, "四"}, {0xCEE5, "五"}, {0xC1F9, "六"}, {0xC6DF, "七"}, {0xB0CB, "八"}, {0xBEC5, "九"}, {0xCAE5, "十"}, {0xB0D9, "百"}, {0xC7A7, "千"}, {0xCDF2, "万"}, {0xD2DA, "亿"}, // 常用形容词 {0xBAC3, "好"}, {0xBDD6, "快"}, {0xC2A5, "乐"}, {0xD0C2, "新"}, {0xC0CF, "老"}, {0xD0A1, "小"}, {0xB4F3, "大"}, {0xB8DF, "高"}, {0xB5CD, "低"}, {0xD1D5, "颜"}, {0xC9AB, "色"}, {0xBADA, "美"}, {0xB3C1, "沉"}, {0xCFE0, "箱"}, {0xB5E7, "电"}, // 常用名词 {0xC4EA, "年"}, {0xD4C2, "月"}, {0xC8D5, "日"}, {0xCEC4, "星"}, {0xC6DA, "期"}, {0xCAB1, "时"}, {0xBFE4, "刻"}, {0xB7D6, "分"}, {0xC3EB, "秒"}, {0xC4DA, "内"}, {0xBEA9, "京"}, {0xC9CF, "上"}, {0xBAA3, "海"}, {0xB9E3, "广"}, {0xD6DD, "州"}, {0xC7ED, "青"}, {0xB5BA, "岛"}, {0xCED2, "我"}, {0xC4E3, "你"}, {0xCBFB, "他"}, // 常用动词 {0xBFB4, "看"}, {0xCFB7, "玩"}, {0xCFDF, "走"}, {0xD7F7, "做"}, {0xCEC2, "写"}, {0xCBB5, "说"}, {0xCFD6, "想"}, {0xCFC2, "下"}, {0xC9CF, "上"}, {0xD7F8, "左"}, {0xD3D2, "右"}, {0xC7B0, "前"}, {0xBBA7, "户"}, {0xCDE2, "外"}, {0xCBF7, "室"}, // 音乐相关 {0xD2F4, "音"}, {0xC0D6, "乐"}, {0xB8E8, "歌"}, {0xB3CC, "程"}, {0xB5C6, "灯"}, {0xB9E2, "光"}, {0xCAD3, "视"}, {0xC6C1, "频"}, {0xBDA1, "舞"}, {0xC7FA, "曲"}, {0xC4DA, "内"}, {0xCDA8, "涨"}, {0xBCA3, "汪"}, {0xB7D2, "佳"}, {0xBBAA, "华"}, // 音乐人名 {0xCEB2, "沈"}, {0xD6A3, "郑"}, {0xC9A1, "秀"}, {0xCEB0, "薛"}, {0xD6EC, "之"}, {0xCFC9, "谦"}, {0xB8B7, "蔡"}, {0xD2AF, "依"}, {0xC1D5, "林"}, {0xD4AA, "元"}, {0xBAA3, "海"}, {0xC0BC, "蓝"}, {0xDEB9, "魏"}, {0xB4EF, "敖"}, // 常用标点符号 {0xA3BA, ":"}, {0xA3BB, ";"}, {0xA1A4, "。"}, {0xA3AC, ","}, {0xA1A2, "、"}, {0xA3BF, "?"}, {0xA3A1, "!"}, {0xA1B0, "—"}, {0xA1B1, "…"}, {0xA1F1, "·"}, // 更多可能的中文字符映射可以根据需要添加 }; // 更优化的字符编码转换函数,连续检测和转换GBK编码 static std::string gbk_to_utf8(const char* gbk_str) { std::string utf8_result; const unsigned char* p = (const unsigned char*)gbk_str; while (*p) { if (*p < 0x80) { // ASCII字符,直接复制 utf8_result += *p; p++; } else if (*p >= 0x81 && *p <= 0xFE && *(p+1) >= 0x40 && *(p+1) <= 0xFE) { // 可能是GBK编码的中文字符 uint16_t gbk_code = (*p << 8) | *(p + 1); bool found = false; // 查找映射表 for (const auto& mapping : gbk_utf8_map) { if (mapping.gbk_code == gbk_code) { utf8_result += mapping.utf8_str; found = true; break; } } if (!found) { // 如果找不到映射,使用占位符并记录未识别的编码 ESP_LOGW(TAG, "未识别的GBK编码: 0x%04X", gbk_code); utf8_result += "?"; } p += 2; // GBK编码是双字节 } else { // 不是有效的GBK编码,跳过 ESP_LOGW(TAG, "无效的GBK编码字节: 0x%02X", *p); p++; } } return utf8_result; } // 增强的自定义映射函数,先尝试使用硬编码映射,再尝试通用转换 static std::string map_filename_by_hex(const char* filename) { // 创建一个十六进制字符串用于比较 std::string hex_str = bytes_to_hex((const uint8_t*)filename, strlen(filename)); // 移除十六进制字符串中的空格 std::string clean_hex; for (char c : hex_str) { if (c != ' ') { clean_hex += c; } } // 特定文件的硬编码映射 if (clean_hex.find("B6AACAD6BEEE2E4D5033") != std::string::npos) { return "丢手绢.MP3"; } else if (clean_hex.find("D0A1CFBC2E4D5033") != std::string::npos) { return "小霞.MP3"; } else if (clean_hex.find("D7F9CEBB2E4D5033") != std::string::npos) { return "座位.MP3"; } else if (clean_hex.find("B8E6B0D7C6F8C7F22E4D5033") != std::string::npos) { return "告白气球.MP3"; } else if (clean_hex.find("B0AEB4ED2E4D5033") != std::string::npos) { return "爱错.MP3"; } // 添加日志中显示的特定文件名映射 else if (clean_hex.find("B1F0C8C3B0AE7E312E4D5033") != std::string::npos) { return "别让爱~1.MP3"; } else if (clean_hex.find("D7DFD4DAC0E47E312E4D5033") != std::string::npos) { return "走在冷~1.MP3"; } else if (clean_hex.find("B4BAB7E7D0ED7E312E4D5033") != std::string::npos) { return "春风许~1.MP3"; } // 添加新发现的特定文件名映射 else if (clean_hex.find("D0A6CBC0CED2C1CB2E4D5033") != std::string::npos) { return "笑死我了.MP3"; } else if (clean_hex.find("C4E3CAC7CBAD2E4D5033") != std::string::npos) { return "你是谁.MP3"; } else if (clean_hex.find("D4F5C3B4CBB52E4D5033") != std::string::npos) { return "怎么说.MP3"; } // 添加最新发现的文件名映射 else if (clean_hex.find("CDA6BAC3B5C4B0A12E4D5033") != std::string::npos) { return "哈哈好的啊.MP3"; } else if (clean_hex.find("BECDD5E2D1F9B0C92E4D5033") != std::string::npos) { return "就这样吧.MP3"; } else if (clean_hex.find("D7EEBDFCD4F57E312E4D5033") != std::string::npos) { return "最近怎~1.MP3"; } // 记录未硬编码映射的文件的十六进制表示,便于后续添加 ESP_LOGI(TAG, "未硬编码的文件十六进制表示: %s", clean_hex.c_str()); // 如果找不到硬编码映射,尝试通用转换,但已知这部分有问题 return gbk_to_utf8(filename); } // 添加辅助函数用于显示文件名的原始字节和显示形式 static void debug_filename(const char* filename) { size_t len = strlen(filename); ESP_LOGI(TAG, "文件名: [%s], 长度: %d", filename, len); ESP_LOGI(TAG, "十六进制: %s", bytes_to_hex((const uint8_t*)filename, len).c_str()); } // 专门用于处理SD卡文件名的函数,包含更详细的调试信息 static std::string process_sd_filename(const char* original_filename) { if (!original_filename || strlen(original_filename) == 0) { return ""; } // 打印原始文件名 ESP_LOGI(TAG, "处理SD卡文件名: [%s]", original_filename); // 获取十六进制表示 std::string hex_string = bytes_to_hex((const uint8_t*)original_filename, strlen(original_filename)); ESP_LOGI(TAG, "文件名十六进制: %s", hex_string.c_str()); // 检查文件名是否已经是UTF-8编码 bool is_utf8 = true; const uint8_t* str = (const uint8_t*)original_filename; size_t len = strlen(original_filename); for (size_t i = 0; i < len; i++) { if (str[i] < 0x80) { // ASCII字符,继续 continue; } else if ((str[i] & 0xE0) == 0xC0) { // 2字节UTF-8序列 if (i + 1 >= len || (str[i+1] & 0xC0) != 0x80) { is_utf8 = false; break; } i += 1; } else if ((str[i] & 0xF0) == 0xE0) { // 3字节UTF-8序列 if (i + 2 >= len || (str[i+1] & 0xC0) != 0x80 || (str[i+2] & 0xC0) != 0x80) { is_utf8 = false; break; } i += 2; } else if ((str[i] & 0xF8) == 0xF0) { // 4字节UTF-8序列 if (i + 3 >= len || (str[i+1] & 0xC0) != 0x80 || (str[i+2] & 0xC0) != 0x80 || (str[i+3] & 0xC0) != 0x80) { is_utf8 = false; break; } i += 3; } else { // 不是有效的UTF-8序列 is_utf8 = false; break; } } if (is_utf8) { ESP_LOGI(TAG, "文件名已经是UTF-8编码,无需转换: [%s]", original_filename); return original_filename; } // 如果不是UTF-8,则尝试从GBK转换 ESP_LOGI(TAG, "文件名不是UTF-8编码,尝试从GBK转换"); // 直接使用组件提供的GBK转换函数 char* output_buffer = (char*)malloc(strlen(original_filename) * 4 + 1); if (!output_buffer) { ESP_LOGE(TAG, "内存分配失败"); return original_filename; } char* temp_ptr = output_buffer; int out_len = gbk_to_utf8((void**)&temp_ptr, (void*)original_filename, strlen(original_filename)); if (out_len > 0) { std::string result = std::string(output_buffer); free(output_buffer); if (strcmp(result.c_str(), original_filename) != 0) { ESP_LOGI(TAG, "文件名转换结果(GBK库): [%s] -> [%s]", original_filename, result.c_str()); return result; } } else { free(output_buffer); } // 如果组件转换失败,尝试硬编码映射(作为备选) std::string mapped_name = map_filename_by_hex(original_filename); // 如果转换后的结果不同于原始文件名,显示转换结果 if (strcmp(mapped_name.c_str(), original_filename) != 0) { ESP_LOGI(TAG, "文件名转换结果(硬编码): [%s] -> [%s]", original_filename, mapped_name.c_str()); } else { ESP_LOGI(TAG, "文件名未发生变化: [%s]", original_filename); } return mapped_name; } class JiuchuangDevBoard : public WifiBoard { private: // 声明为友元函数,允许访问私有成员 friend void sd_card_detect_task(void *arg); friend void audio_event_callback(audio_player_cb_ctx_t *ctx); i2c_master_bus_handle_t codec_i2c_bus_; Button boot_button_; Button pwr_button_; Button wifi_button; Button cmd_button; LcdDisplay *display_; PowerSaveTimer *power_save_timer_; PowerManager *power_manager_; esp_lcd_panel_io_handle_t panel_io = NULL; esp_lcd_panel_handle_t panel = NULL; sdmmc_card_t *card = NULL; sdmmc_host_t host; sdmmc_slot_config_t slot_config; std::vector audio_files; bool card_mounted = false; bool audio_player_initialized = false; int current_volume = 80; // 添加当前音量存储变量,初始值设为80 bool is_playing = false; // 当前是否处于音乐播放状态 TaskHandle_t sd_card_detect_task_handle = NULL; // 音量映射函数:将内部音量(0-80)映射为显示音量(0-100%) int MapVolumeForDisplay(int internal_volume) { // 确保输入在有效范围内 if (internal_volume < 0) internal_volume = 0; if (internal_volume > 80) internal_volume = 80; // 将0-80映射到0-100 // 公式: 显示音量 = (内部音量 / 80) * 100 return (internal_volume * 100) / 80; } public: bool is_switching = false; // 防止快速连续切换音乐 - 移至公有部分供回调访问 // 保存音量到NVS void SaveVolumeToNVS(int volume) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open("storage", NVS_READWRITE, &nvs_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "Error opening NVS handle: %s", esp_err_to_name(err)); return; } err = nvs_set_i32(nvs_handle, "volume", volume); if (err != ESP_OK) { ESP_LOGE(TAG, "Error writing volume to NVS: %s", esp_err_to_name(err)); } err = nvs_commit(nvs_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "Error committing NVS: %s", esp_err_to_name(err)); } nvs_close(nvs_handle); } // 从NVS获取音量 int LoadVolumeFromNVS() { nvs_handle_t nvs_handle; esp_err_t err = nvs_open("storage", NVS_READONLY, &nvs_handle); if (err != ESP_OK) { ESP_LOGI(TAG, "NVS不存在,使用默认音量"); return 60; // 默认音量改为60(原来是80的75%) } int32_t volume = 60; // 默认音量改为60 err = nvs_get_i32(nvs_handle, "volume", &volume); if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) { ESP_LOGE(TAG, "Error reading volume from NVS: %s", esp_err_to_name(err)); } nvs_close(nvs_handle); // 确保音量在有效范围内 if (volume < 0) volume = 0; if (volume > 80) volume = 80; // 最大音量限制为80 return volume; } // 公共初始化方法 public: static JiuchuangDevBoard *audio_board_instance; std::string current_file; // 当前播放的文件路径 - 移至公有部分以便audio_event_callback访问 // 获取所有音频文件列表 std::vector GetAudioFiles(const char *mount_point) { std::vector files; DIR *dir = opendir(mount_point); if (!dir) { ESP_LOGE(TAG, "无法打开目录: %s", mount_point); return files; } ESP_LOGI(TAG, "扫描目录 %s 中的音频文件", mount_point); struct dirent *entry; while ((entry = readdir(dir)) != NULL) { // 使用d_name可能会有编码问题,尝试打印原始字节值 const char *filename = entry->d_name; if (filename[0] == 0) continue; // 跳过空文件名 // 使用辅助函数显示文件名 debug_filename(filename); // 尝试使用映射函数转换文件名 std::string mapped_name = process_sd_filename(filename); if (mapped_name != filename) { ESP_LOGI(TAG, "文件名映射: [%s] -> [%s]", filename, mapped_name.c_str()); } if (is_supported_audio_file(filename)) { char filepath[512]; snprintf(filepath, sizeof(filepath), "%s/%s", mount_point, filename); files.push_back(filepath); ESP_LOGI(TAG, "添加音频文件: %s", filepath); } } closedir(dir); // 按名称排序 std::sort(files.begin(), files.end()); // 打印找到的所有文件 ESP_LOGI(TAG, "找到 %d 个音频文件:", files.size()); for (size_t i = 0; i < files.size(); i++) { size_t pos = files[i].find_last_of("/\\"); std::string filename = (pos != std::string::npos) ? files[i].substr(pos + 1) : files[i]; std::string mapped_name = process_sd_filename(filename.c_str()); ESP_LOGI(TAG, "[%d] %s -> %s", i, files[i].c_str(), mapped_name.c_str()); } return files; } // 播放指定文件 bool PlayFile(const std::string &filepath) { // 首先检查是否处于播放模式 if (!IsPlaying()) { ESP_LOGI(TAG, "当前不处于音乐播放模式,不开始播放"); return false; } ESP_LOGI(TAG, "尝试播放: %s", filepath.c_str()); FILE *file = fopen(filepath.c_str(), "rb"); if (file) { // 确保应用状态正确 auto &app = Application::GetInstance(); auto current_state = app.GetDeviceState(); // 检查当前状态,如果不是音乐播放状态,更新状态 if (current_state != DeviceState::kDeviceStateMusicPlaying) { ESP_LOGI(TAG, "设置应用状态为音乐播放"); app.SetDeviceState(DeviceState::kDeviceStateMusicPlaying); // 确保已禁用语音功能 app.DisableVoiceFeatures(); } // 记录当前文件 current_file = filepath; // 更新用户界面 auto display = GetDisplay(); if (display) { // 提取文件名(不含路径) size_t pos = filepath.find_last_of("/\\"); std::string filename = (pos != std::string::npos) ? filepath.substr(pos + 1) : filepath; // 打印文件名的十六进制值,用于调试 std::string hex_str = ""; for (int i = 0; i < filename.length(); i++) { char buf[8]; snprintf(buf, sizeof(buf), "%02X ", (unsigned char)filename[i]); hex_str += buf; } ESP_LOGI(TAG, "显示文件名: [%s], 十六进制: %s", filename.c_str(), hex_str.c_str()); // 使用映射函数转换文件名 std::string displayName = process_sd_filename(filename.c_str()); ESP_LOGI(TAG, "转换后的文件名: [%s]", displayName.c_str()); std::string status_text = "正在播放: " + displayName; display->SetStatus(status_text.c_str()); display->SetEmotion("happy"); // 在聊天消息中也显示当前播放的文件名 display->SetChatMessage("system", status_text.c_str()); } // 开始播放 ESP_LOGI(TAG, "开始播放: %s", filepath.c_str()); audio_player_play(file); return true; } ESP_LOGE(TAG, "无法打开文件: %s", filepath.c_str()); return false; } // 播放下一首歌 bool PlayNextSong() { const char mount_point[] = "/sdcard"; ESP_LOGI(TAG, "=== 开始播放下一首歌曲 ==="); ESP_LOGI(TAG, "当前文件: %s", current_file.c_str()); // 检查SD卡状态 if (!card_mounted) { ESP_LOGE(TAG, "SD卡未挂载,无法播放下一首"); return false; } // 获取音频文件列表 audio_files = GetAudioFiles(mount_point); if (audio_files.empty()) { ESP_LOGE(TAG, "未找到音频文件"); return false; } ESP_LOGI(TAG, "找到 %d 个音频文件", audio_files.size()); // 找到当前文件的下一个文件 auto it = std::find(audio_files.begin(), audio_files.end(), current_file); std::string next_file; int current_index = -1; int next_index = 0; if (it == audio_files.end()) { // 当前文件未找到,播放第一个 next_file = audio_files.front(); next_index = 0; ESP_LOGW(TAG, "当前文件未找到,播放第一个文件 (索引: %d)", next_index); } else { // 找到当前文件的索引 current_index = std::distance(audio_files.begin(), it); ESP_LOGI(TAG, "当前文件索引: %d", current_index); // 计算下一个文件的索引 next_index = (current_index + 1) % audio_files.size(); next_file = audio_files[next_index]; if (next_index == 0) { ESP_LOGI(TAG, "已到最后一首,循环到第一首 (索引: %d)", next_index); } else { ESP_LOGI(TAG, "播放下一首 (索引: %d)", next_index); } } ESP_LOGI(TAG, "下一首文件: %s", next_file.c_str()); // 检查文件是否存在 FILE* test_file = fopen(next_file.c_str(), "rb"); if (!test_file) { ESP_LOGE(TAG, "下一首文件不存在: %s", next_file.c_str()); return false; } fclose(test_file); // 播放文件 ESP_LOGI(TAG, "开始播放下一首文件"); bool success = PlayFile(next_file); if (!success) { ESP_LOGE(TAG, "播放下一首失败: %s", next_file.c_str()); } else { ESP_LOGI(TAG, "成功开始播放下一首: %s", next_file.c_str()); } ESP_LOGI(TAG, "=== 播放下一首歌曲完成 ==="); return success; } // 播放上一首歌 bool PlayPreviousSong() { const char mount_point[] = "/sdcard"; ESP_LOGI(TAG, "=== 开始播放上一首歌曲 ==="); ESP_LOGI(TAG, "当前文件: %s", current_file.c_str()); // 检查SD卡状态 if (!card_mounted) { ESP_LOGE(TAG, "SD卡未挂载,无法播放上一首"); return false; } // 获取音频文件列表 audio_files = GetAudioFiles(mount_point); if (audio_files.empty()) { ESP_LOGE(TAG, "未找到音频文件"); return false; } ESP_LOGI(TAG, "找到 %d 个音频文件", audio_files.size()); // 找到当前文件的前一个文件 auto it = std::find(audio_files.begin(), audio_files.end(), current_file); std::string prev_file; int current_index = -1; int prev_index = 0; if (it == audio_files.end()) { // 当前文件未找到,播放最后一个 prev_index = audio_files.size() - 1; prev_file = audio_files.back(); ESP_LOGW(TAG, "当前文件未找到,播放最后一个文件 (索引: %d)", prev_index); } else { // 找到当前文件的索引 current_index = std::distance(audio_files.begin(), it); ESP_LOGI(TAG, "当前文件索引: %d", current_index); // 计算上一个文件的索引 prev_index = (current_index - 1 + audio_files.size()) % audio_files.size(); prev_file = audio_files[prev_index]; if (current_index == 0) { ESP_LOGI(TAG, "已到第一首,循环到最后一首 (索引: %d)", prev_index); } else { ESP_LOGI(TAG, "播放上一首 (索引: %d)", prev_index); } } ESP_LOGI(TAG, "上一首文件: %s", prev_file.c_str()); // 检查文件是否存在 FILE* test_file = fopen(prev_file.c_str(), "rb"); if (!test_file) { ESP_LOGE(TAG, "上一首文件不存在: %s", prev_file.c_str()); return false; } fclose(test_file); // 播放文件 ESP_LOGI(TAG, "开始播放上一首文件"); bool success = PlayFile(prev_file); if (!success) { ESP_LOGE(TAG, "播放上一首失败: %s", prev_file.c_str()); } else { ESP_LOGI(TAG, "成功开始播放上一首: %s", prev_file.c_str()); } ESP_LOGI(TAG, "=== 播放上一首歌曲完成 ==="); return success; } // 安全的切换到上一首 bool SwitchToPreviousSong() { ESP_LOGI(TAG, "*** 开始切换到上一首 ***"); ESP_LOGI(TAG, "当前播放状态: %s", is_playing ? "播放中" : "未播放"); ESP_LOGI(TAG, "当前切换状态: %s", is_switching ? "切换中" : "空闲"); if (!is_playing) { ESP_LOGW(TAG, "当前未在播放音乐,无法切换"); return false; } // 防抖:检查是否正在切换 if (is_switching) { ESP_LOGI(TAG, "正在切换中,忽略操作"); return false; } is_switching = true; ESP_LOGI(TAG, "设置切换状态为true"); // 显示切换状态 auto display = GetDisplay(); if (display) { display->ShowNotification("切换到上一首..."); ESP_LOGI(TAG, "显示切换通知"); } // 停止当前播放 ESP_LOGI(TAG, "停止当前播放,准备切换到上一首"); audio_player_state_t current_state = audio_player_get_state(); ESP_LOGI(TAG, "当前播放器状态: %d", current_state); audio_player_stop(); ESP_LOGI(TAG, "已调用audio_player_stop()"); // 等待播放器真正停止 int timeout = 50; // 5秒超时 ESP_LOGI(TAG, "等待播放器停止,超时时间: %d * 100ms", timeout); while (audio_player_get_state() != AUDIO_PLAYER_STATE_IDLE && timeout > 0) { current_state = audio_player_get_state(); ESP_LOGD(TAG, "等待停止中,当前状态: %d, 剩余超时: %d", current_state, timeout); vTaskDelay(pdMS_TO_TICKS(100)); timeout--; } current_state = audio_player_get_state(); ESP_LOGI(TAG, "等待结束,最终状态: %d, 剩余超时: %d", current_state, timeout); if (timeout <= 0) { ESP_LOGE(TAG, "停止播放超时,当前状态: %d", current_state); is_switching = false; if (display) { display->ShowNotification("切换超时"); } return false; } ESP_LOGI(TAG, "播放器已停止,开始播放上一首"); // 播放上一首 bool success = PlayPreviousSong(); // 如果播放成功,等待一小段时间确保播放稳定启动 if (success) { ESP_LOGI(TAG, "等待播放稳定启动..."); vTaskDelay(pdMS_TO_TICKS(200)); // 等待200ms } is_switching = false; ESP_LOGI(TAG, "重置切换状态为false"); if (!success) { ESP_LOGE(TAG, "播放上一首失败"); if (display) { display->ShowNotification("切换失败"); } } else { ESP_LOGI(TAG, "成功切换到上一首"); if (display) { display->ShowNotification("已切换到上一首"); } } ESP_LOGI(TAG, "*** 切换到上一首完成,结果: %s ***", success ? "成功" : "失败"); return success; } // 安全的切换到下一首 bool SwitchToNextSong() { ESP_LOGI(TAG, "*** 开始切换到下一首 ***"); ESP_LOGI(TAG, "当前播放状态: %s", is_playing ? "播放中" : "未播放"); ESP_LOGI(TAG, "当前切换状态: %s", is_switching ? "切换中" : "空闲"); if (!is_playing) { ESP_LOGW(TAG, "当前未在播放音乐,无法切换"); return false; } // 防抖:检查是否正在切换 if (is_switching) { ESP_LOGI(TAG, "正在切换中,忽略操作"); return false; } is_switching = true; ESP_LOGI(TAG, "设置切换状态为true"); // 显示切换状态 auto display = GetDisplay(); if (display) { display->ShowNotification("切换到下一首..."); ESP_LOGI(TAG, "显示切换通知"); } // 停止当前播放 ESP_LOGI(TAG, "停止当前播放,准备切换到下一首"); audio_player_state_t current_state = audio_player_get_state(); ESP_LOGI(TAG, "当前播放器状态: %d", current_state); audio_player_stop(); ESP_LOGI(TAG, "已调用audio_player_stop()"); // 等待播放器真正停止 int timeout = 50; // 5秒超时 ESP_LOGI(TAG, "等待播放器停止,超时时间: %d * 100ms", timeout); while (audio_player_get_state() != AUDIO_PLAYER_STATE_IDLE && timeout > 0) { current_state = audio_player_get_state(); ESP_LOGD(TAG, "等待停止中,当前状态: %d, 剩余超时: %d", current_state, timeout); vTaskDelay(pdMS_TO_TICKS(100)); timeout--; } current_state = audio_player_get_state(); ESP_LOGI(TAG, "等待结束,最终状态: %d, 剩余超时: %d", current_state, timeout); if (timeout <= 0) { ESP_LOGE(TAG, "停止播放超时,当前状态: %d", current_state); is_switching = false; if (display) { display->ShowNotification("切换超时"); } return false; } ESP_LOGI(TAG, "播放器已停止,开始播放下一首"); // 播放下一首 bool success = PlayNextSong(); // 如果播放成功,等待一小段时间确保播放稳定启动 if (success) { ESP_LOGI(TAG, "等待播放稳定启动..."); vTaskDelay(pdMS_TO_TICKS(200)); // 等待200ms } is_switching = false; ESP_LOGI(TAG, "重置切换状态为false"); if (!success) { ESP_LOGE(TAG, "播放下一首失败"); if (display) { display->ShowNotification("切换失败"); } } else { ESP_LOGI(TAG, "成功切换到下一首"); if (display) { display->ShowNotification("已切换到下一首"); } } ESP_LOGI(TAG, "*** 切换到下一首完成,结果: %s ***", success ? "成功" : "失败"); return success; } void InitializePowerManager() { power_manager_ = new PowerManager(PWR_ADC_GPIO); power_manager_->OnChargingStatusChanged([this](bool is_charging) { if (is_charging) { power_save_timer_->SetEnabled(false); } else { power_save_timer_->SetEnabled(true); } }); } void InitializeSdCard() { // 配置SD卡主机 host = SDMMC_HOST_DEFAULT(); // 配置SD卡插槽 slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); slot_config.width = 1; // 1位模式 slot_config.clk = SD_CARD_CLK_PIN; slot_config.cmd = SD_CARD_CMD_PIN; slot_config.d0 = SD_CARD_DAT0_PIN; slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; } void InitializePowerSaveTimer() { #ifndef __USER_GPIO_PWRDOWN__ RTC_DATA_ATTR static bool long_press_occurred = false; esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); if (cause == ESP_SLEEP_WAKEUP_EXT0) { ESP_LOGI(TAG, "Wake up by EXT0"); const int64_t start = esp_timer_get_time(); ESP_LOGI(TAG, "esp_sleep_get_wakeup_cause"); while (gpio_get_level(PWR_BUTTON_GPIO) == 0) { if (esp_timer_get_time() - start > 3000000) { long_press_occurred = true; break; } vTaskDelay(100 / portTICK_PERIOD_MS); } if (long_press_occurred) { ESP_LOGI(TAG, "Long press wakeup"); long_press_occurred = false; } else { ESP_LOGI(TAG, "Short press, return to sleep"); ESP_ERROR_CHECK(esp_sleep_enable_ext0_wakeup(PWR_BUTTON_GPIO, 0)); ESP_ERROR_CHECK(rtc_gpio_pullup_en(PWR_BUTTON_GPIO)); // 内部上拉 ESP_ERROR_CHECK(rtc_gpio_pulldown_dis(PWR_BUTTON_GPIO)); esp_deep_sleep_start(); } } #endif // 一分钟进入浅睡眠,5分钟进入深睡眠关机 power_save_timer_ = new PowerSaveTimer(-1, (60 * 10), (60 * 30)); // power_save_timer_ = new PowerSaveTimer(-1, 6, 10);//test power_save_timer_->OnEnterSleepMode([this]() { ESP_LOGI(TAG, "Enabling sleep mode"); display_->SetChatMessage("system", ""); display_->SetEmotion("sleepy"); GetBacklight()->SetBrightness(1); }); power_save_timer_->OnExitSleepMode([this]() { display_->SetChatMessage("system", ""); display_->SetEmotion("neutral"); GetBacklight()->RestoreBrightness(); }); power_save_timer_->OnShutdownRequest([this]() { ESP_LOGI(TAG, "Shutting down"); #ifndef __USER_GPIO_PWRDOWN__ ESP_ERROR_CHECK(esp_sleep_enable_ext0_wakeup(PWR_BUTTON_GPIO, 0)); ESP_ERROR_CHECK(rtc_gpio_pullup_en(PWR_BUTTON_GPIO)); // 内部上拉 ESP_ERROR_CHECK(rtc_gpio_pulldown_dis(PWR_BUTTON_GPIO)); esp_lcd_panel_disp_on_off(panel, false); // 关闭显示 esp_deep_sleep_start(); #else rtc_gpio_set_level(PWR_EN_GPIO, 0); rtc_gpio_hold_dis(PWR_EN_GPIO); #endif }); power_save_timer_->SetEnabled(true); } void InitializeI2c() { // Initialize I2C peripheral i2c_master_bus_config_t i2c_bus_cfg = { .i2c_port = (i2c_port_t)1, .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, .clk_source = I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt = 7, .intr_priority = 0, .trans_queue_depth = 0, .flags = { .enable_internal_pullup = 1, }, }; ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_)); } void InitializeSpi() { } void InitializeButtons() { static bool pwrbutton_unreleased = false; static int power_button_click_count = 0; static int64_t last_power_button_press_time = 0; if (gpio_get_level(GPIO_NUM_3) == 1) { pwrbutton_unreleased = true; } // 电源按钮按下和松开事件处理 pwr_button_.OnPressUp([this]() { ESP_LOGI(TAG, "电源按钮按下: %s %d", __FUNCTION__, __LINE__); pwrbutton_unreleased = false; int64_t current_time = esp_timer_get_time(); if (current_time - last_power_button_press_time < 1000000) { // 1秒内 power_button_click_count++; // 三击重置WiFi if (power_button_click_count >= 3) { ESP_LOGI(TAG, "三击重置WiFi"); rtc_gpio_set_level(PWR_EN_GPIO, 1); rtc_gpio_hold_en(PWR_EN_GPIO); ResetWifiConfiguration(); power_button_click_count = 0; return; } } else { power_button_click_count = 1; } last_power_button_press_time = current_time; // 获取当前应用实例和状态 auto &app = Application::GetInstance(); auto current_state = app.GetDeviceState(); // 如果正在播放音乐,退出音乐播放模式 if (IsPlaying() || current_state == kDeviceStateMusicPlaying) { ESP_LOGI(TAG, "检测到音乐播放状态,正在退出..."); // 先停止所有音频播放 ESP_LOGI(TAG, "停止音频播放"); audio_player_stop(); // 等待一小段时间确保停止处理完成 vTaskDelay(pdMS_TO_TICKS(100)); // 强制重置播放状态标志 ESP_LOGI(TAG, "强制重置播放状态标志"); SetPlaying(false); // 获取显示对象和音频编解码器 auto display = GetDisplay(); auto codec = GetAudioCodec(); // 重新配置音频编解码器 ESP_LOGI(TAG, "重新配置音频编解码器"); codec->SetSampleRate(24000, 1); codec->EnableOutput(false); codec->EnableInput(true); // 重新启用语音功能 ESP_LOGI(TAG, "重新启用语音功能"); app.EnableVoiceFeatures(); // 强制设置为待机状态 ESP_LOGI(TAG, "强制设置为待机状态"); app.SetDeviceState(DeviceState::kDeviceStateIdle); // 更新显示 ESP_LOGI(TAG, "更新显示状态"); display->SetStatus(Lang::Strings::STANDBY); display->SetEmotion("neutral"); ESP_LOGI(TAG, "成功退出音乐播放模式,切换到小智模式"); // 唤醒设备,防止立即进入睡眠 power_save_timer_->WakeUp(); } else { ESP_LOGI(TAG, "当前设备状态: %d", current_state); if (current_state == kDeviceStateIdle) { // 如果当前是待命状态,切换到聆听状态 ESP_LOGI(TAG, "从待命状态切换到聆听状态"); app.ToggleChatState(); // 切换到聆听状态 } else if (current_state == kDeviceStateListening) { // 如果当前是聆听状态,切换到待命状态 ESP_LOGI(TAG, "从聆听状态切换到待命状态"); app.ToggleChatState(); // 切换到待命状态 } else if (current_state == kDeviceStateSpeaking) { // 如果当前是说话状态,终止说话并切换到待命状态 ESP_LOGI(TAG, "从说话状态切换到待命状态"); app.ToggleChatState(); // 终止说话 } else { // 其他状态下只唤醒设备 ESP_LOGI(TAG, "唤醒设备"); power_save_timer_->WakeUp(); } } }); // 电源按钮长按事件 pwr_button_.OnLongPress([this]() { ESP_LOGI(TAG, "电源按钮长按"); if (pwrbutton_unreleased) return; // 如果在音乐播放模式,先停止播放 if (IsPlaying()) { ESP_LOGI(TAG, "从音乐播放模式退出并关机"); audio_player_stop(); SetPlaying(false); } // 长按前保存当前音量 ESP_LOGI(TAG, "保存音量设置: %d", current_volume); SaveVolumeToNVS(current_volume); // 长按进入深度睡眠模式(关机) #ifndef __USER_GPIO_PWRDOWN__ ESP_LOGI(TAG, "准备进入深度睡眠模式"); ESP_ERROR_CHECK(esp_sleep_enable_ext0_wakeup(PWR_BUTTON_GPIO, 0)); ESP_ERROR_CHECK(rtc_gpio_pullup_en(PWR_BUTTON_GPIO)); // 内部上拉 ESP_ERROR_CHECK(rtc_gpio_pulldown_dis(PWR_BUTTON_GPIO)); ESP_LOGI(TAG, "Enter deep sleep"); esp_deep_sleep_start(); #else ESP_LOGI(TAG, "准备进入深度睡眠模式"); ESP_ERROR_CHECK(esp_sleep_enable_ext0_wakeup(PWR_BUTTON_GPIO, 0)); ESP_ERROR_CHECK(rtc_gpio_pulldown_en(PWR_BUTTON_GPIO)); // 内部上拉 ESP_ERROR_CHECK(rtc_gpio_pullup_dis(PWR_BUTTON_GPIO)); ESP_LOGI(TAG, "Enter deep sleep"); rtc_gpio_set_level(PWR_EN_GPIO, 0); rtc_gpio_hold_dis(PWR_EN_GPIO); esp_deep_sleep_start(); #endif }); // WIFI按钮功能 wifi_button.OnClick([this]() { if (is_playing) { // 在音乐模式下:播放上一首 ESP_LOGI(TAG, "播放上一首"); // 使用安全的切换方法 SwitchToPreviousSong(); } else { // 在小智状态下:加音量 ESP_LOGI(TAG, "WIFI按钮:增加音量"); // 调整音量,每次增加8个内部音量单位(对应显示10%) current_volume = (current_volume + 8 > 80) ? 80 : current_volume + 8; auto codec = GetAudioCodec(); // 将0-80的音量映射到完整音量范围 int actual_volume = current_volume; codec->SetOutputVolume(actual_volume); ESP_LOGI(TAG, "当前音量: %d, 实际音量: %d", current_volume, actual_volume); // 保存新的音量设置 SaveVolumeToNVS(current_volume); power_save_timer_->WakeUp(); // 在屏幕上显示当前音量(使用映射后的显示音量) auto display = GetDisplay(); if (display) { int display_volume = MapVolumeForDisplay(current_volume); char volume_text[20]; snprintf(volume_text, sizeof(volume_text), "音量: %d%%", display_volume); display->ShowNotification(volume_text); ESP_LOGI(TAG, "显示音量: %d%% (内部音量: %d)", display_volume, current_volume); } } }); // CMD按钮功能 cmd_button.OnClick([this]() { if (is_playing) { // 在音乐模式下:播放下一首 ESP_LOGI(TAG, "播放下一首"); // 使用安全的切换方法 SwitchToNextSong(); } else { // 在小智状态下:减音量 ESP_LOGI(TAG, "CMD按钮:减少音量"); // 调整音量,每次减少8个内部音量单位(对应显示10%) current_volume = (current_volume - 8 < 0) ? 0 : current_volume - 8; auto codec = GetAudioCodec(); // 将0-80的音量映射到完整音量范围 int actual_volume = current_volume; codec->SetOutputVolume(actual_volume); ESP_LOGI(TAG, "当前音量: %d, 实际音量: %d", current_volume, actual_volume); // 保存新的音量设置 SaveVolumeToNVS(current_volume); power_save_timer_->WakeUp(); // 在屏幕上显示当前音量(使用映射后的显示音量) auto display = GetDisplay(); if (display) { int display_volume = MapVolumeForDisplay(current_volume); char volume_text[20]; snprintf(volume_text, sizeof(volume_text), "音量: %d%%", display_volume); display->ShowNotification(volume_text); ESP_LOGI(TAG, "显示音量: %d%% (内部音量: %d)", display_volume, current_volume); } } }); // BOOT按钮功能不变,仅保留唤醒设备的功能 boot_button_.OnClick([this]() { // 仅唤醒设备 power_save_timer_->WakeUp(); }); } void InitializeGC9301isplay() { // 液晶屏控制IO初始化 ESP_LOGI(TAG, "test Install panel IO"); spi_bus_config_t buscfg = {}; buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN; buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN; buscfg.miso_io_num = GPIO_NUM_NC; buscfg.quadwp_io_num = GPIO_NUM_NC; buscfg.quadhd_io_num = GPIO_NUM_NC; buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t); ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); // 初始化SPI总线 esp_lcd_panel_io_spi_config_t io_config = {}; io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN; io_config.dc_gpio_num = DISPLAY_DC_PIN; io_config.spi_mode = 3; io_config.pclk_hz = 80 * 1000 * 1000; io_config.trans_queue_depth = 10; io_config.lcd_cmd_bits = 8; io_config.lcd_param_bits = 8; esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io); // 初始化液晶屏驱动芯片9309 ESP_LOGI(TAG, "Install LCD driver"); esp_lcd_panel_dev_config_t panel_config = {}; panel_config.reset_gpio_num = GPIO_NUM_NC; panel_config.rgb_ele_order = LCD_RGB_ENDIAN_BGR; panel_config.bits_per_pixel = 16; esp_lcd_new_panel_gc9309na(panel_io, &panel_config, &panel); esp_lcd_panel_reset(panel); esp_lcd_panel_init(panel); esp_lcd_panel_invert_color(panel, false); esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); display_ = new SpiLcdDisplay(panel_io, panel, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, { .text_font = &font_puhui_20_4, .icon_font = &font_awesome_20_4, #if CONFIG_USE_WECHAT_MESSAGE_STYLE .emoji_font = font_emoji_32_init(), #else .emoji_font = font_emoji_64_init(), #endif }); } // 物联网初始化,添加对 AI 可见设备 void InitializeIot() { auto &thing_manager = iot::ThingManager::GetInstance(); thing_manager.AddThing(iot::CreateThing("Speaker")); thing_manager.AddThing(iot::CreateThing("Screen")); thing_manager.AddThing(iot::CreateThing("Battery")); } // 初始化mp3和wav播放器 void InitializeAudioPlayer() { if (!audio_player_initialized) { // 存储实例指针供全局回调使用 audio_board_instance = this; // 使用外部定义的回调函数 audio_player_config_t config = { .mute_fn = audio_mute_callback, .clk_set_fn = audio_clk_set_callback, .write_fn = audio_write_callback, .priority = 7, .coreID = 1}; esp_err_t ret = audio_player_new(config); if (ret == ESP_OK) { // 注册播放器状态变化回调 audio_player_callback_register(audio_event_callback, NULL); audio_player_initialized = true; ESP_LOGI(TAG, "音频播放器初始化成功"); } else { ESP_LOGE(TAG, "音频播放器初始化失败: %d", ret); } } } // 通过语音命令启动音乐播放 virtual bool StartMusicPlayback(const std::string& music_name = "") override { ESP_LOGI(TAG, "准备启动音乐播放..."); if (!card_mounted) { ESP_LOGE(TAG, "SD卡未挂载,无法播放音乐"); return false; } // 初始化音频播放器(如果尚未初始化) if (!audio_player_initialized) { ESP_LOGI(TAG, "初始化音频播放器"); InitializeAudioPlayer(); } // 停止任何当前播放 if (audio_player_get_state() != AUDIO_PLAYER_STATE_IDLE) { ESP_LOGI(TAG, "停止当前播放"); audio_player_stop(); vTaskDelay(pdMS_TO_TICKS(100)); // 等待停止完成 } // 设置音乐播放状态 ESP_LOGI(TAG, "设置音乐播放状态标志"); SetPlaying(true); // 禁用语音功能 ESP_LOGI(TAG, "禁用语音功能"); auto &app = Application::GetInstance(); app.DisableVoiceFeatures(); // 设置应用状态 ESP_LOGI(TAG, "设置应用状态为音乐播放"); app.SetDeviceState(DeviceState::kDeviceStateMusicPlaying); // 配置音频编解码器 ESP_LOGI(TAG, "配置音频编解码器"); auto codec = GetAudioCodec(); codec->EnableInput(false); codec->EnableOutput(true); // 更新显示状态 ESP_LOGI(TAG, "更新显示状态"); auto display = GetDisplay(); display->SetStatus("音乐播放模式"); display->SetEmotion("happy"); // 列出所有SD卡中的音频文件 ESP_LOGI(TAG, "扫描SD卡音频文件"); const char mount_point[] = "/sdcard"; audio_files = GetAudioFiles(mount_point); if (audio_files.empty()) { ESP_LOGI(TAG, "未找到音频文件"); SetPlaying(false); restore_device_status(); return false; } ESP_LOGI(TAG, "找到 %d 个音频文件", audio_files.size()); // 如果提供了音乐名称,尝试查找匹配的文件 if (!music_name.empty()) { ESP_LOGI(TAG, "尝试查找匹配音乐: %s", music_name.c_str()); for (const auto& file : audio_files) { // 提取文件名部分(不含路径) size_t pos = file.find_last_of("/\\"); std::string filename = (pos != std::string::npos) ? file.substr(pos + 1) : file; // 打印文件名的十六进制值和转换后的文件名 std::string hex_str = bytes_to_hex((const uint8_t*)filename.c_str(), filename.length()); std::string displayName = process_sd_filename(filename.c_str()); ESP_LOGI(TAG, "检查文件: [%s], 转换后: [%s]", filename.c_str(), displayName.c_str()); // 匹配逻辑:检查转换后的文件名是否包含要搜索的音乐名称 if (displayName.find(music_name) != std::string::npos) { ESP_LOGI(TAG, "找到匹配的音乐: %s -> %s", file.c_str(), displayName.c_str()); current_file = file; // 设置当前文件 FILE *fp = fopen(file.c_str(), "rb"); if (fp) { ESP_LOGI(TAG, "开始播放匹配的音乐"); audio_player_play(fp); return true; } ESP_LOGE(TAG, "无法打开文件: %s", file.c_str()); break; } } ESP_LOGI(TAG, "未找到匹配的音乐,将播放第一首"); } // 如果没有找到匹配的文件或没有提供音乐名称,播放第一首 ESP_LOGI(TAG, "播放第一首音乐"); if (!audio_files.empty()) { current_file = audio_files[0]; // 设置为第一首歌 FILE *fp = fopen(audio_files[0].c_str(), "rb"); if (fp) { ESP_LOGI(TAG, "开始播放第一首音乐: %s", audio_files[0].c_str()); audio_player_play(fp); return true; } ESP_LOGE(TAG, "无法打开文件: %s", audio_files[0].c_str()); } // 如果到这里,说明播放失败 ESP_LOGE(TAG, "播放失败,恢复设备状态"); SetPlaying(false); restore_device_status(); return false; } // 公共接口方法 public: JiuchuangDevBoard() : boot_button_(BOOT_BUTTON_GPIO), pwr_button_(PWR_BUTTON_GPIO, true), wifi_button(WIFI_BUTTON_GPIO), cmd_button(CMD_BUTTON_GPIO) { // 初始化NVS esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { // NVS分区已满或版本不匹配,擦除并重新初始化 ESP_ERROR_CHECK(nvs_flash_erase()); err = nvs_flash_init(); } ESP_ERROR_CHECK(err); // 初始化GBK编码表 ESP_LOGI(TAG, "初始化GBK编码表"); init_gbk_encoding(); // 从NVS加载保存的音量 current_volume = LoadVolumeFromNVS(); ESP_LOGI(TAG, "从NVS加载音量: %d", current_volume); InitializeI2c(); InitializePowerManager(); InitializePowerSaveTimer(); InitializeSpi(); InitializeButtons(); InitializeGC9301isplay(); InitializeIot(); InitializeSdCard(); InitializeAudioPlayer(); // 设置加载的音量 auto codec = GetAudioCodec(); // 应用音量值 int actual_volume = current_volume; codec->SetOutputVolume(actual_volume); ESP_LOGI(TAG, "设置初始音量: %d, 实际音量: %d", current_volume, actual_volume); GetBacklight()->RestoreBrightness(); } // 获取音乐播放状态 bool IsPlaying() const { return is_playing; } // 设置音乐播放状态 void SetPlaying(bool playing) { is_playing = playing; } // virtual Led* GetLed() override { // static SingleLed led(BUILTIN_LED_GPIO); // return &led; // } virtual AudioCodec *GetAudioCodec() override { static Es8311AudioCodec audio_codec( codec_i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN, AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR); return &audio_codec; } virtual Display *GetDisplay() override { return display_; } virtual Backlight *GetBacklight() override { static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); return &backlight; } virtual bool GetBatteryLevel(int &level, bool &charging, bool &discharging) override { static bool last_discharging = false; charging = power_manager_->IsCharging(); discharging = power_manager_->IsDischarging(); if (discharging != last_discharging) { power_save_timer_->SetEnabled(discharging); last_discharging = discharging; } level = power_manager_->GetBatteryLevel(); return true; } virtual void SetPowerSaveMode(bool enabled) override { if (!enabled) { power_save_timer_->WakeUp(); } WifiBoard::SetPowerSaveMode(enabled); } virtual void StartSdCardDetection() { if (sd_card_detect_task_handle == NULL) { xTaskCreate( sd_card_detect_task, "sd_card_detect", 8192, this, 6, &sd_card_detect_task_handle); } } }; // 在类定义外部初始化静态成员变量 JiuchuangDevBoard *JiuchuangDevBoard::audio_board_instance = nullptr; /** * @brief 列出SD卡中的文件 * * @param mount_point 挂载点路径 * @return esp_err_t ESP_OK表示成功 */ static esp_err_t list_sd_card_files(const char *mount_point) { char tmp_file_path[128]; snprintf(tmp_file_path, sizeof(tmp_file_path), "%s/d.tmp", mount_point); FILE *tmp_file = fopen(tmp_file_path, "w"); if (fclose(tmp_file) != 0) { ESP_LOGE(TAG, "SD卡已拔出"); return ESP_FAIL; }; unlink(tmp_file_path); // 继续列出SD卡中的文件 ESP_LOGI(TAG, "SD卡文件列表:"); DIR *dir = opendir(mount_point); if (!dir) { ESP_LOGE(TAG, "无法打开目录: %s", mount_point); return ESP_FAIL; } struct dirent *entry; int file_count = 0; int audio_file_count = 0; // 遍历并打印文件名 while ((entry = readdir(dir)) != NULL) { // 使用辅助函数调试文件名 const char* filename = entry->d_name; std::string mapped_name = process_sd_filename(filename); file_count++; // 检查是否是MP3文件 if (is_supported_audio_file(filename)) { ESP_LOGI(TAG, "找到音频文件: %s -> %s", filename, mapped_name.c_str()); audio_file_count++; } } closedir(dir); if (file_count == 0) { ESP_LOGI(TAG, "SD卡为空"); } else { ESP_LOGI(TAG, "共有 %d 个文件,其中 %d 个音频文件", file_count, audio_file_count); } return ESP_OK; } // 静音/取消静音控制回调 static esp_err_t audio_mute_callback(AUDIO_PLAYER_MUTE_SETTING setting) { ESP_LOGI(TAG, "mute setting %d", setting); return ESP_OK; } // 音频时钟设置回调 static esp_err_t audio_clk_set_callback(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch) { if (!JiuchuangDevBoard::audio_board_instance) return ESP_ERR_INVALID_STATE; auto &app = Application::GetInstance(); app.DisableVoiceFeatures(); app.SetDeviceState(DeviceState::kDeviceStateMusicPlaying); auto codec = JiuchuangDevBoard::audio_board_instance->GetAudioCodec(); codec->EnableInput(false); codec->SetSampleRate(rate, (int)ch); ESP_LOGI(TAG, "音频时钟设置: rate=%lu, bits_cfg=%lu, ch=%d", rate, bits_cfg, (int)ch); return ESP_OK; } // 音频数据写入回调 static esp_err_t audio_write_callback(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms) { if (!JiuchuangDevBoard::audio_board_instance) return ESP_ERR_INVALID_STATE; auto codec = JiuchuangDevBoard::audio_board_instance->GetAudioCodec(); int16_t *samples = (int16_t *)audio_buffer; int sample_count = len / sizeof(int16_t); // 调用我们的AudioCodec类的PlayAudio方法播放音频数据 int samples_played = codec->PlayAudio(samples, sample_count); *bytes_written = samples_played * sizeof(int16_t); return ESP_OK; } // 音频播放器状态变化回调 static void audio_event_callback(audio_player_cb_ctx_t *ctx) { if (!JiuchuangDevBoard::audio_board_instance) return; auto board = JiuchuangDevBoard::audio_board_instance; auto display = board->GetDisplay(); switch (ctx->audio_event) { case AUDIO_PLAYER_CALLBACK_EVENT_PLAYING: ESP_LOGI(TAG, "音频播放中..."); // 获取当前播放的文件名 if (!board->current_file.empty()) { size_t pos = board->current_file.find_last_of("/\\"); std::string filename = (pos != std::string::npos) ? board->current_file.substr(pos + 1) : board->current_file; std::string displayName = process_sd_filename(filename.c_str()); std::string status_text = "正在播放: " + displayName; display->SetStatus(status_text.c_str()); display->SetChatMessage("system", status_text.c_str()); } else { display->SetStatus("正在播放音乐"); } display->SetEmotion("happy"); break; case AUDIO_PLAYER_CALLBACK_EVENT_PAUSE: ESP_LOGI(TAG, "音频已暂停"); display->SetStatus("音乐已暂停"); break; case AUDIO_PLAYER_CALLBACK_EVENT_IDLE: ESP_LOGI(TAG, "音频播放结束"); // 检查是否正在切换中,如果是则不自动播放下一首 if (board->is_switching) { ESP_LOGI(TAG, "正在切换中,不自动播放下一首"); break; } // 只有在用户仍处于音乐播放模式时才自动播放下一首 if (board->IsPlaying()) { // 延迟一小段时间后自动播放下一首 vTaskDelay(pdMS_TO_TICKS(100)); // 再次检查是否仍处于播放模式和切换状态 if (board->IsPlaying() && !board->is_switching) { ESP_LOGI(TAG, "自动播放下一首歌曲"); board->PlayNextSong(); } else { ESP_LOGI(TAG, "播放模式已退出或正在切换,不自动播放下一首"); } } else { ESP_LOGI(TAG, "当前不处于音乐播放模式,不自动播放下一首"); } break; case AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT: break; case AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE: ESP_LOGE(TAG, "未知文件类型"); display->SetStatus("未知文件类型"); break; default: ESP_LOGI(TAG, "音频事件: %d", ctx->audio_event); break; } } static esp_err_t restore_device_status() { if (!JiuchuangDevBoard::audio_board_instance) return ESP_ERR_INVALID_STATE; ESP_LOGI(TAG, "开始恢复设备状态"); // 停止正在播放的音频 ESP_LOGI(TAG, "停止音频播放"); audio_player_stop(); // 等待确保音频停止完成 vTaskDelay(pdMS_TO_TICKS(100)); auto& board = *JiuchuangDevBoard::audio_board_instance; // 强制重置播放状态标志 ESP_LOGI(TAG, "强制重置播放状态标志"); board.SetPlaying(false); auto &app = Application::GetInstance(); // 重新启用语音功能 ESP_LOGI(TAG, "重新启用语音功能"); app.EnableVoiceFeatures(); // 强制设置为待机状态 ESP_LOGI(TAG, "强制设置应用状态为待机"); app.SetDeviceState(DeviceState::kDeviceStateIdle); // 重新配置音频编解码器 ESP_LOGI(TAG, "重新配置音频编解码器"); auto codec = board.GetAudioCodec(); codec->SetSampleRate(24000, (int)1); codec->EnableOutput(false); codec->EnableInput(true); // 更新用户界面 ESP_LOGI(TAG, "更新用户界面"); auto display = board.GetDisplay(); if (display) { display->SetStatus(Lang::Strings::STANDBY); display->SetEmotion("neutral"); } ESP_LOGI(TAG, "设备状态已完全恢复到小智模式"); return ESP_OK; } static void sd_card_detect_task(void *arg) { esp_log_level_set("sdmmc_common", ESP_LOG_NONE); // 完全禁用SDMMC公共日志 esp_log_level_set("vfs_fat_sdmmc", ESP_LOG_NONE); const char mount_point[] = "/sdcard"; esp_err_t ret; JiuchuangDevBoard *board = (JiuchuangDevBoard *)arg; ESP_LOGI(TAG, "SD卡检测任务启动"); // 配置挂载设置 esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 16, // 增加最大文件数以支持更多文件 .allocation_unit_size = 16 * 1024, }; // 设置文件系统字符编码为UTF-8 // 注意: ESP32上的文件系统API可能没有直接支持设置UTF-8编码的方法 // 我们尝试通过增加日志和调试来解决中文文件名问题 while (1) { // 检查SD卡是否已挂载 if (!board->card_mounted) { ESP_LOGI(TAG, "尝试挂载SD卡..."); ret = esp_vfs_fat_sdmmc_mount(mount_point, &board->host, &board->slot_config, &mount_config, &board->card); if (ret == ESP_OK) { ESP_LOGI(TAG, "SD卡挂载成功"); board->card_mounted = true; // 打印SD卡信息 sdmmc_card_print_info(stdout, board->card); // 列出SD卡文件 list_sd_card_files(mount_point); } else { ESP_LOGE(TAG, "SD卡挂载失败: %s", esp_err_to_name(ret)); } } else { // 仅检查SD卡是否存在,不进行任何自动播放操作 if (list_sd_card_files(mount_point) != ESP_OK) { ESP_LOGI(TAG, "SD卡已移除,执行卸载操作"); esp_vfs_fat_sdcard_unmount(mount_point, board->card); board->card_mounted = false; // 如果当前正在播放,停止播放 if (board->IsPlaying()) { ESP_LOGI(TAG, "SD卡已移除且正在播放,停止播放"); audio_player_stop(); board->SetPlaying(false); // 恢复正常模式 auto &app = Application::GetInstance(); app.EnableVoiceFeatures(); app.SetDeviceState(DeviceState::kDeviceStateIdle); // 更新显示 auto display = board->GetDisplay(); if (display) { display->SetStatus(Lang::Strings::STANDBY); display->SetEmotion("neutral"); } } } } vTaskDelay(pdMS_TO_TICKS(1000)); } } DECLARE_BOARD(JiuchuangDevBoard);