1853 lines
68 KiB
C++
1853 lines
68 KiB
C++
#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 <esp_log.h>
|
||
#include <esp_lcd_panel_vendor.h>
|
||
#include <driver/i2c_master.h>
|
||
#include <driver/spi_common.h>
|
||
#include <wifi_station.h>
|
||
#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 <driver/rtc_io.h>
|
||
#include <esp_sleep.h>
|
||
|
||
#include <string.h>
|
||
#include <sys/unistd.h>
|
||
#include <sys/stat.h>
|
||
#include "esp_vfs_fat.h"
|
||
#include "sdmmc_cmd.h"
|
||
#include "driver/sdmmc_host.h"
|
||
#include <dirent.h>
|
||
#include <nvs_flash.h> // 添加NVS头文件
|
||
#include <nvs.h> // 添加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<std::string> 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<std::string> GetAudioFiles(const char *mount_point)
|
||
{
|
||
std::vector<std::string> 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);
|