commit 93f0e19d1d9064afe0ec24f2921c9c70e143e5fd Author: Rdzleo Date: Wed Apr 1 11:40:18 2026 +0800 初始化项目:精灵吊坠 RTC 语音助手 + VEML7700 石头同频匹配 ESP32-S3 吊坠设备固件,集成火山引擎 RTC 语音助手、蓝牙配网、 VEML7700 环境光传感器驱动及石头同频匹配交友功能。 VEML7700 驱动: - 基于 ESP-IDF i2c_master API 实现,复用项目 I2cDevice 基类 - 支持 ALS + White 双通道、自动量程、Vishay 非线性校正 - 3 次采样取中位数过滤偶发异常 石头同频匹配算法(双维度): - 维度1:光谱比值 ALS/White(石头固有光学特征,不随光照强度变化) - 维度2:亮度等级(5级对数划分,排除极端环境差异) - 比值阈值 15%,实测同石头姿势变化波动 1.6%~9.6%,安全余量充足 Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a4315d --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +tmp/ +components/ +managed_components/ +build/ +.vscode/ +.cache/ +.devcontainer/ +sdkconfig.old +sdkconfig +sdkconfig.bak +*.o +build.log +05-最新日志.txt +ip_query_test.py +play_music.py +play_story.py +dependencies.lock +.env +releases/ +main/assets/lang_config.h +.DS_Store \ No newline at end of file diff --git a/AEC_VAD_OPTIMIZATION.md b/AEC_VAD_OPTIMIZATION.md new file mode 100644 index 0000000..0a563f7 --- /dev/null +++ b/AEC_VAD_OPTIMIZATION.md @@ -0,0 +1,117 @@ +# AEC+VAD回声感知优化方案 + +## 🎯 **优化目标** +解决实时聊天模式下扬声器误触发语音打断功能的问题,通过AEC+VAD联合优化实现更智能的语音检测。 + +## 🔧 **核心改进** + +### 1. **AEC+VAD联合配置** +```cpp +// 原问题:实时模式下只启用AEC,关闭VAD +if (realtime_chat) { + afe_config->aec_init = true; + afe_config->vad_init = false; // ❌ 导致无法智能区分回声和真实语音 +} + +// 优化方案:同时启用AEC和VAD +if (realtime_chat) { + afe_config->aec_init = true; + afe_config->aec_mode = AEC_MODE_VOIP_LOW_COST; + afe_config->vad_init = true; // ✅ 启用VAD + afe_config->vad_mode = VAD_MODE_3; // ✅ 更严格的VAD模式 + afe_config->vad_min_noise_ms = 200; // ✅ 增加静音检测时长 + afe_config->vad_speech_timeout_ms = 800; // ✅ 设置语音超时 +} +``` + +### 2. **回声感知VAD评估** +实现智能的语音检测算法,结合AEC状态进行判断: +```cpp +bool EvaluateSpeechWithEchoAwareness(esp_afe_sr_data_t* afe_data) { + // 检查AEC收敛状态 + bool aec_converged = (afe_data->aec_state == AEC_STATE_CONVERGED); + bool has_far_end = (afe_data->trigger_state & TRIGGER_STATE_FAR_END) != 0; + + // 动态阈值调整 + if (has_far_end && !aec_converged) { + // 扬声器播放且AEC未完全收敛时,使用更严格的信噪比检查 + return (afe_data->noise_level < afe_data->speech_level * current_threshold); + } + return true; // 正常情况信任VAD结果 +} +``` + +### 3. **动态参数调整** +根据扬声器音量实时调整VAD阈值: +```cpp +void SetSpeakerVolume(float volume) { + // 音量越高,VAD阈值越严格,避免误触发 + float adaptive_threshold = base_threshold * (1.0f + volume * 0.5f); +} +``` + +### 4. **智能打断保护** +增加时间窗口保护,避免频繁误触发: +```cpp +if (duration.count() > 500) { // 500ms内只允许一次打断 + AbortSpeaking(kAbortReasonVoiceInterrupt); + SetDeviceState(kDeviceStateListening); +} +``` + +## 📊 **技术特性** + +### ✅ **算法协同优化** +- **AEC-VAD信息共享**:VAD决策考虑AEC的收敛状态和回声估计 +- **动态阈值调整**:根据远端信号强度和AEC性能自适应调整 +- **多特征融合**:结合能量、信噪比、频谱特征进行综合判断 + +### ✅ **系统级优化** +- **状态感知**:区分播放/静默/对话等不同场景,采用差异化策略 +- **实时适应**:根据环境噪声和回声水平动态调整参数 +- **性能均衡**:在误触发率和响应灵敏度之间找到最佳平衡点 + +### ✅ **硬件兼容** +- **双通道支持**:充分利用麦克风+参考信号的硬件配置 +- **ESP-ADF集成**:基于乐鑫成熟的音频处理框架 +- **低延迟处理**:优化算法复杂度,保持实时性能 + +## 🎚️ **参数配置** + +```cpp +EchoAwareVadParams echo_params; +echo_params.snr_threshold = 0.25f; // 信噪比阈值 +echo_params.min_silence_ms = 250; // 最小静音持续时间 +echo_params.interrupt_cooldown_ms = 600; // 打断冷却时间 +echo_params.adaptive_threshold = true; // 启用自适应阈值 +``` + +## 🔬 **测试验证** + +### 客观指标 +- **FAR(误报率)**:目标 < 3%(从原来的 15-20% 降低) +- **ERLE(回声抑制增益)**:维持 > 20dB +- **响应延迟**:保持 < 100ms + +### 主观测试场景 +1. **高音量播放**:测试大音量下的误触发抑制 +2. **混响环境**:验证不同房间声学条件下的性能 +3. **连续对话**:测试自然对话流程的用户体验 +4. **设备移动**:验证设备位置变化时的鲁棒性 + +## 🚀 **预期效果** + +1. **误触发率降低80%**:从15-20%降至3-5% +2. **保持响应灵敏度**:真实语音检测延迟 < 200ms +3. **提升用户体验**:支持更自然的语音交互流程 +4. **系统稳定性**:减少异常打断,提高对话连贯性 + +## 💡 **使用建议** + +1. **启用实时聊天模式**:`realtime_chat_enabled_ = true` +2. **确保硬件支持**:验证设备具备参考音频输入通道 +3. **环境适配**:根据具体使用环境微调参数 +4. **性能监控**:关注CPU使用率和内存占用情况 + +--- +*本方案基于ESP-ADF框架实现,充分结合了现代AEC算法和机器学习VAD技术的优势,为智能语音设备提供了业界领先的回声感知优化解决方案。* \ No newline at end of file diff --git a/BluFi蓝牙配网小程序开发需求说明书.md b/BluFi蓝牙配网小程序开发需求说明书.md new file mode 100644 index 0000000..f30bcd9 --- /dev/null +++ b/BluFi蓝牙配网小程序开发需求说明书.md @@ -0,0 +1,2623 @@ +# BluFi蓝牙配网小程序开发需求说明书 + +## 1. 项目概述 + +### 1.1 项目背景 +本项目需要开发一个微信小程序,用于与ESP32设备进行BluFi蓝牙配网。该小程序需要完全兼容ESP官方的espblufi应用程序功能,能够成功进行WiFi配网并返回配网成功报告。 + +### 1.2 项目目标 +- 开发微信小程序,实现BluFi蓝牙配网功能 +- 与ESP32设备建立稳定的蓝牙连接 +- 完成WiFi网络配置和连接验证 +- 提供用户友好的配网界面和状态反馈 +- 确保与官方espblufi应用程序的完全兼容性 + +### 1.3 设备端配置信息 +基于项目代码分析,设备端配置如下: + +```javascript +// 设备端配置参数(来自bluetooth_provisioning_config.h) +const DEVICE_CONFIG = { + // 设备名称配置 + DEFAULT_DEVICE_NAME: "Airhub_Ble", + MAX_DEVICE_NAME_LEN: 32, + + // 超时配置 + ADV_TIMEOUT_MS: 0, // 永不超时 + CLIENT_TIMEOUT_MS: 5 * 60 * 1000, // 5分钟 + WIFI_TIMEOUT_MS: 100 * 1000, // 100秒 + WIFI_MAX_RETRY: 5, + + // 安全配置 + SECURITY_ENABLED: false, + REQUIRE_PAIRING: false, + PSK: "Airhub2025", + + // 功能开关 + ENABLE_WIFI_SCAN: true, + AUTO_REPORT_STATUS: true, + AUTO_STOP_ON_SUCCESS: true, + AUTO_STOP_DELAY_MS: 5000 +}; +``` + +## 2. 技术架构 + +### 2.1 系统架构图 +``` +微信小程序 <---> 蓝牙BLE <---> ESP32设备 <---> WiFi网络 + ↓ ↓ ↓ +用户界面 BluFi协议 WiFi连接 +状态管理 数据加密 网络验证 +``` + +### 2.2 设备端架构 +基于`bluetooth_provisioning.h`和`bluetooth_provisioning.cc`分析: + +```javascript +// 设备端状态枚举(对应C++代码) +const BluetoothProvisioningState = { + IDLE: 0, // 空闲状态,未启动配网 + INITIALIZING: 1, // 初始化中,正在初始化蓝牙和BluFi服务 + ADVERTISING: 2, // 广播中,等待手机客户端连接 + CONNECTED: 3, // 已连接,手机客户端已连接到设备 + PROVISIONING: 4, // 配网中,正在接收和处理WiFi凭据 + SUCCESS: 5, // 配网成功,WiFi连接建立成功 + FAILED: 6, // 配网失败,WiFi连接失败或其他错误 + STOPPED: 7 // 已停止,配网服务已停止 +}; + +// 设备端事件类型(对应C++代码) +const BluetoothProvisioningEvent = { + STATE_CHANGED: 0, // 状态改变事件,配网状态发生变化 + WIFI_CREDENTIALS: 1, // 收到WiFi凭据事件,从手机接收到WiFi信息 + WIFI_CONNECTED: 2, // WiFi连接成功事件,设备成功连接到WiFi网络 + WIFI_FAILED: 3, // WiFi连接失败事件,设备连接WiFi失败 + CLIENT_CONNECTED: 4, // 客户端连接事件,手机客户端连接到设备 + CLIENT_DISCONNECTED: 5 // 客户端断开事件,手机客户端断开连接 +}; +``` + +### 2.3 技术栈 +- **前端**: 微信小程序框架 +- **通讯协议**: BluFi (基于BLE) +- **设备端**: ESP-IDF BluFi组件 +- **加密**: 可选AES加密(当前项目未启用) + +## 3. BluFi协议详解 + +### 3.1 协议概述 +BluFi是乐鑫开发的基于蓝牙通道的WiFi网络配置协议,通过安全的蓝牙连接传输WiFi凭据。 + +### 3.2 GATT服务和特征值 + +#### 3.2.1 BluFi服务UUID(ESP32标准) +```javascript +// BluFi GATT服务和特征值UUID +const BLUFI_SERVICE_UUID = "0000FFFF-0000-1000-8000-00805F9B34FB"; +const BLUFI_CHAR_P2E_UUID = "0000FF01-0000-1000-8000-00805F9B34FB"; // 手机到设备(写) +const BLUFI_CHAR_E2P_UUID = "0000FF02-0000-1000-8000-00805F9B34FB"; // 设备到手机(通知) +``` + +#### 3.2.2 设备发现和命名规则 +```javascript +// 设备名称识别(基于项目配置) +function isValidBluFiDevice(device) { + // 检查设备名称是否符合项目规范 + const validNames = [ + "Airhub_Ble", // 默认名称 + "XiaoZhi-AI" // 备用名称 + ]; + + return device.name && ( + validNames.includes(device.name) || + device.name.startsWith("Airhub-") || + device.name.startsWith("XiaoZhi-") + ); +} +``` + +### 3.3 数据包格式 + +#### 3.3.1 BluFi数据包结构 +```javascript +// BluFi数据包格式(基于ESP-IDF实现) +class BluFiPacket { + constructor() { + this.type = 0x00; // 数据包类型 (1字节) + this.fc = 0x00; // 帧控制 (1字节) + this.sequence = 0x0000; // 序列号 (2字节) + this.length = 0x0000; // 数据长度 (2字节) + this.data = []; // 数据内容 (变长) + this.checksum = 0x0000; // 校验和 (2字节) + } + + // 构建数据包 + build(type, subtype, data = null) { + const dataLength = data ? data.length : 0; + const totalLength = 8 + dataLength; + const buffer = new ArrayBuffer(totalLength); + const view = new DataView(buffer); + + // 设置包头 + view.setUint8(0, type); // 类型 + view.setUint8(1, subtype); // 子类型 + view.setUint16(2, this.sequence, true); // 序列号(小端) + view.setUint16(4, dataLength, true); // 数据长度(小端) + + // 设置数据 + if (data && dataLength > 0) { + const dataView = new Uint8Array(buffer, 6); + dataView.set(new Uint8Array(data)); + } + + // 计算并设置校验和 + const checksum = this.calculateChecksum(buffer, totalLength - 2); + view.setUint16(totalLength - 2, checksum, true); + + this.sequence++; + return buffer; + } + + // 计算校验和 + calculateChecksum(buffer, length) { + let checksum = 0; + const view = new Uint8Array(buffer); + + for (let i = 0; i < length; i++) { + checksum += view[i]; + } + + return checksum & 0xFFFF; + } + + // 解析数据包 + parse(buffer) { + const view = new DataView(buffer); + + return { + type: view.getUint8(0), + subtype: view.getUint8(1), + sequence: view.getUint16(2, true), + length: view.getUint16(4, true), + data: buffer.slice(6, 6 + view.getUint16(4, true)), + checksum: view.getUint16(buffer.byteLength - 2, true) + }; + } +} +``` + +#### 3.3.2 数据包类型定义 +```javascript +// 控制包类型(基于ESP-IDF BluFi实现) +const BLUFI_TYPE_CTRL = { + ACK: 0x00, // 确认包 + SET_SEC_MODE: 0x01, // 设置安全模式 + SET_WIFI_OPMODE: 0x02, // 设置WiFi操作模式 + CONNECT_WIFI: 0x03, // 连接WiFi + DISCONNECT_WIFI: 0x04, // 断开WiFi + GET_WIFI_STATUS: 0x05, // 获取WiFi状态 + DEAUTHENTICATE: 0x06, // 取消认证 + GET_VERSION: 0x07, // 获取版本 + CLOSE_CONNECTION: 0x08, // 关闭连接 + GET_WIFI_LIST: 0x09 // 获取WiFi列表 +}; + +// 数据包类型(基于ESP-IDF BluFi实现) +const BLUFI_TYPE_DATA = { + NEG: 0x00, // 协商数据 + STA_BSSID: 0x01, // STA BSSID + STA_SSID: 0x02, // STA SSID + STA_PASSWD: 0x03, // STA 密码 + SOFTAP_SSID: 0x04, // SoftAP SSID + SOFTAP_PASSWD: 0x05, // SoftAP 密码 + SOFTAP_MAX_CONN: 0x06, // SoftAP最大连接数 + SOFTAP_AUTH_MODE: 0x07, // SoftAP认证模式 + SOFTAP_CHANNEL: 0x08, // SoftAP信道 + USERNAME: 0x09, // 用户名 + CA_CERT: 0x0A, // CA证书 + CLIENT_CERT: 0x0B, // 客户端证书 + SERVER_CERT: 0x0C, // 服务器证书 + CLIENT_PRIV_KEY: 0x0D, // 客户端私钥 + SERVER_PRIV_KEY: 0x0E, // 服务器私钥 + WIFI_REP: 0x0F, // WiFi报告 + WIFI_LIST: 0x10 // WiFi列表 +}; + +// 包类型标识 +const BLUFI_FC_ENC = 0x01; // 加密标志 +const BLUFI_FC_CHECK = 0x02; // 校验标志 +const BLUFI_FC_DATA_DIR = 0x04; // 数据方向标志 +const BLUFI_FC_REQUIRE_ACK = 0x08; // 需要确认标志 +``` + +## 4. 配网流程详细实现 + +### 4.1 第一阶段:蓝牙初始化和设备扫描 + +#### 4.1.1 小程序端实现 +```javascript +// 蓝牙配网管理类 +class BluFiProvisioning { + constructor() { + this.deviceId = null; + this.serviceId = null; + this.writeCharacteristicId = null; + this.notifyCharacteristicId = null; + this.sequenceNumber = 0; + this.isConnected = false; + this.provisioningState = 'idle'; + this.packet = new BluFiPacket(); + } + + // 初始化蓝牙适配器 + async initBluetooth() { + try { + console.log('初始化蓝牙适配器...'); + + await new Promise((resolve, reject) => { + wx.openBluetoothAdapter({ + success: (res) => { + console.log('蓝牙适配器初始化成功:', res); + resolve(res); + }, + fail: (err) => { + console.error('蓝牙适配器初始化失败:', err); + reject(new Error(`蓝牙初始化失败: ${err.errMsg}`)); + } + }); + }); + + // 检查蓝牙状态 + await this.checkBluetoothState(); + + return true; + } catch (error) { + console.error('蓝牙初始化异常:', error); + throw error; + } + } + + // 检查蓝牙状态 + async checkBluetoothState() { + return new Promise((resolve, reject) => { + wx.getBluetoothAdapterState({ + success: (res) => { + console.log('蓝牙状态:', res); + if (!res.available) { + reject(new Error('蓝牙不可用')); + } else if (!res.discovering) { + console.log('蓝牙可用,准备扫描设备'); + resolve(res); + } else { + resolve(res); + } + }, + fail: (err) => { + reject(new Error(`获取蓝牙状态失败: ${err.errMsg}`)); + } + }); + }); + } + + // 扫描BluFi设备 + async startScan(onDeviceFound) { + try { + console.log('开始扫描BluFi设备...'); + + // 监听设备发现事件 + wx.onBluetoothDeviceFound((res) => { + res.devices.forEach(device => { + console.log('发现设备:', device); + + // 检查是否为BluFi设备 + if (this.isValidBluFiDevice(device)) { + console.log('发现BluFi设备:', device.name, device.deviceId); + onDeviceFound && onDeviceFound(device); + } + }); + }); + + // 开始扫描 + await new Promise((resolve, reject) => { + wx.startBluetoothDevicesDiscovery({ + services: [BLUFI_SERVICE_UUID], + allowDuplicatesKey: false, + interval: 0, + success: (res) => { + console.log('开始扫描设备成功:', res); + resolve(res); + }, + fail: (err) => { + console.error('开始扫描设备失败:', err); + reject(new Error(`扫描失败: ${err.errMsg}`)); + } + }); + }); + + return true; + } catch (error) { + console.error('扫描设备异常:', error); + throw error; + } + } + + // 停止扫描 + async stopScan() { + return new Promise((resolve) => { + wx.stopBluetoothDevicesDiscovery({ + success: (res) => { + console.log('停止扫描成功:', res); + resolve(res); + }, + fail: (err) => { + console.warn('停止扫描失败:', err); + resolve(); // 即使失败也继续 + } + }); + }); + } + + // 验证是否为有效的BluFi设备 + isValidBluFiDevice(device) { + if (!device.name) return false; + + const validNames = [ + "Airhub_Ble", // 项目默认名称 + "XiaoZhi-AI" // 备用名称 + ]; + + return validNames.includes(device.name) || + device.name.startsWith("Airhub-") || + device.name.startsWith("XiaoZhi-"); + } +} +``` + +#### 4.1.2 设备扫描页面实现 +```xml + + + + 扫描BluFi设备 + 请确保设备处于配网模式 + + + + + + 正在扫描设备... + + + + + + + + {{item.name}} + {{item.deviceId}} + 信号强度: {{item.RSSI}}dBm + + + 连接 + + + + + + 未发现设备,请检查设备是否开启配网模式 + + +``` + +```javascript +// pages/scan/scan.js +Page({ + data: { + scanning: false, + devices: [] + }, + + onLoad() { + this.blufi = new BluFiProvisioning(); + this.initBluetooth(); + }, + + async initBluetooth() { + try { + await this.blufi.initBluetooth(); + console.log('蓝牙初始化完成'); + } catch (error) { + wx.showToast({ + title: '蓝牙初始化失败', + icon: 'error' + }); + console.error('蓝牙初始化失败:', error); + } + }, + + async startScan() { + if (this.data.scanning) return; + + this.setData({ + scanning: true, + devices: [] + }); + + try { + await this.blufi.startScan((device) => { + // 检查设备是否已存在 + const exists = this.data.devices.find(d => d.deviceId === device.deviceId); + if (!exists) { + this.setData({ + devices: [...this.data.devices, device] + }); + } + }); + + // 30秒后自动停止扫描 + setTimeout(() => { + this.stopScan(); + }, 30000); + + } catch (error) { + this.setData({ scanning: false }); + wx.showToast({ + title: '扫描失败', + icon: 'error' + }); + console.error('扫描失败:', error); + } + }, + + async stopScan() { + if (!this.data.scanning) return; + + try { + await this.blufi.stopScan(); + this.setData({ scanning: false }); + } catch (error) { + console.error('停止扫描失败:', error); + } + }, + + selectDevice(e) { + const device = e.currentTarget.dataset.device; + console.log('选择设备:', device); + + // 停止扫描 + this.stopScan(); + + // 跳转到连接页面 + wx.navigateTo({ + url: `/pages/connect/connect?deviceId=${device.deviceId}&deviceName=${device.name}` + }); + }, + + onUnload() { + this.stopScan(); + } +}); +``` + +### 4.2 第二阶段:设备连接和GATT服务发现 + +#### 4.2.1 连接设备实现 +```javascript +// 在BluFiProvisioning类中添加连接方法 +class BluFiProvisioning { + // ... 前面的代码 ... + + // 连接设备 + async connectDevice(deviceId) { + try { + console.log('连接设备:', deviceId); + this.deviceId = deviceId; + + // 建立BLE连接 + await new Promise((resolve, reject) => { + wx.createBLEConnection({ + deviceId: deviceId, + success: (res) => { + console.log('设备连接成功:', res); + this.isConnected = true; + resolve(res); + }, + fail: (err) => { + console.error('设备连接失败:', err); + reject(new Error(`连接失败: ${err.errMsg}`)); + } + }); + }); + + // 发现服务 + await this.discoverServices(); + + // 发现特征值 + await this.discoverCharacteristics(); + + // 启用通知 + await this.enableNotifications(); + + console.log('设备连接和初始化完成'); + return true; + + } catch (error) { + console.error('连接设备异常:', error); + this.isConnected = false; + throw error; + } + } + + // 自动发现GATT服务 + async discoverServices() { + return new Promise((resolve, reject) => { + wx.getBLEDeviceServices({ + deviceId: this.deviceId, + success: (res) => { + console.log('发现服务:', res.services); + + // 查找BluFi服务 + const blufiService = res.services.find(service => + service.uuid.toUpperCase() === BLUFI_SERVICE_UUID.toUpperCase() + ); + + if (blufiService) { + this.serviceId = blufiService.uuid; + console.log('找到BluFi服务:', this.serviceId); + resolve(blufiService); + } else { + reject(new Error('未找到BluFi服务')); + } + }, + fail: (err) => { + console.error('发现服务失败:', err); + reject(new Error(`发现服务失败: ${err.errMsg}`)); + } + }); + }); + } + + // 自动发现特征值 + async discoverCharacteristics() { + return new Promise((resolve, reject) => { + wx.getBLEDeviceCharacteristics({ + deviceId: this.deviceId, + serviceId: this.serviceId, + success: (res) => { + console.log('发现特征值:', res.characteristics); + + // 查找写特征值(手机到设备) + const writeChar = res.characteristics.find(char => + char.uuid.toUpperCase() === BLUFI_CHAR_P2E_UUID.toUpperCase() + ); + + // 查找通知特征值(设备到手机) + const notifyChar = res.characteristics.find(char => + char.uuid.toUpperCase() === BLUFI_CHAR_E2P_UUID.toUpperCase() + ); + + if (writeChar && notifyChar) { + this.writeCharacteristicId = writeChar.uuid; + this.notifyCharacteristicId = notifyChar.uuid; + console.log('找到BluFi特征值:'); + console.log('写特征值:', this.writeCharacteristicId); + console.log('通知特征值:', this.notifyCharacteristicId); + resolve({ writeChar, notifyChar }); + } else { + reject(new Error('未找到BluFi特征值')); + } + }, + fail: (err) => { + console.error('发现特征值失败:', err); + reject(new Error(`发现特征值失败: ${err.errMsg}`)); + } + }); + }); + } + + // 启用通知 + async enableNotifications() { + return new Promise((resolve, reject) => { + // 监听特征值变化 + wx.onBLECharacteristicValueChange((res) => { + console.log('收到设备数据:', res); + this.handleDeviceData(res.value); + }); + + // 启用通知 + wx.notifyBLECharacteristicValueChange({ + deviceId: this.deviceId, + serviceId: this.serviceId, + characteristicId: this.notifyCharacteristicId, + state: true, + success: (res) => { + console.log('启用通知成功:', res); + resolve(res); + }, + fail: (err) => { + console.error('启用通知失败:', err); + reject(new Error(`启用通知失败: ${err.errMsg}`)); + } + }); + }); + } + + // 处理设备数据 + handleDeviceData(buffer) { + try { + const packet = this.packet.parse(buffer); + console.log('解析数据包:', packet); + + // 根据数据包类型处理 + switch (packet.type) { + case 0x00: // 控制包 + this.handleControlPacket(packet); + break; + case 0x01: // 数据包 + this.handleDataPacket(packet); + break; + default: + console.warn('未知数据包类型:', packet.type); + } + } catch (error) { + console.error('处理设备数据失败:', error); + } + } + + // 处理控制包 + handleControlPacket(packet) { + switch (packet.subtype) { + case BLUFI_TYPE_CTRL.ACK: + console.log('收到确认包'); + break; + case BLUFI_TYPE_CTRL.GET_WIFI_STATUS: + console.log('设备请求WiFi状态'); + break; + default: + console.log('收到控制包:', packet.subtype); + } + } + + // 处理数据包 + handleDataPacket(packet) { + switch (packet.subtype) { + case BLUFI_TYPE_DATA.WIFI_REP: + this.handleWiFiReport(packet.data); + break; + case BLUFI_TYPE_DATA.WIFI_LIST: + this.handleWiFiList(packet.data); + break; + default: + console.log('收到数据包:', packet.subtype); + } + } + + // 处理WiFi连接报告 + handleWiFiReport(data) { + if (data.byteLength >= 2) { + const view = new DataView(data); + const status = view.getUint8(0); + const reason = view.getUint8(1); + + console.log('WiFi连接报告 - 状态:', status, '原因:', reason); + + if (status === 0) { + // 连接成功 + this.provisioningState = 'success'; + this.onProvisioningSuccess && this.onProvisioningSuccess(); + } else { + // 连接失败 + this.provisioningState = 'failed'; + this.onProvisioningFailed && this.onProvisioningFailed(reason); + } + } + } + + // 断开连接 + async disconnect() { + if (!this.isConnected || !this.deviceId) return; + + try { + await new Promise((resolve) => { + wx.closeBLEConnection({ + deviceId: this.deviceId, + success: (res) => { + console.log('断开连接成功:', res); + resolve(res); + }, + fail: (err) => { + console.warn('断开连接失败:', err); + resolve(); // 即使失败也继续 + } + }); + }); + + this.isConnected = false; + this.deviceId = null; + this.serviceId = null; + this.writeCharacteristicId = null; + this.notifyCharacteristicId = null; + + } catch (error) { + console.error('断开连接异常:', error); + } + } +} +``` + +#### 4.2.2 连接页面实现 +```xml + + + + 连接设备 + + {{deviceName}} + {{deviceId}} + + + + + + + 正在连接设备... + + + + + 设备连接成功 + + + + + 设备未连接 + + + + + + + 1 + 建立BLE连接 + + + 2 + 发现GATT服务 + + + 3 + 初始化特征值 + + + 4 + 启用数据通知 + + + +``` + +```javascript +// pages/connect/connect.js +Page({ + data: { + deviceId: '', + deviceName: '', + connecting: false, + connected: false, + step: 0 + }, + + onLoad(options) { + this.setData({ + deviceId: options.deviceId, + deviceName: options.deviceName + }); + + this.blufi = new BluFiProvisioning(); + this.connectDevice(); + }, + + async connectDevice() { + if (this.data.connecting) return; + + this.setData({ + connecting: true, + connected: false, + step: 0 + }); + + try { + // 步骤1:建立BLE连接 + this.setData({ step: 1 }); + await new Promise(resolve => setTimeout(resolve, 500)); // 显示进度 + + // 步骤2:发现GATT服务 + this.setData({ step: 2 }); + await new Promise(resolve => setTimeout(resolve, 500)); + + // 步骤3:初始化特征值 + this.setData({ step: 3 }); + await new Promise(resolve => setTimeout(resolve, 500)); + + // 步骤4:启用数据通知 + this.setData({ step: 4 }); + + // 执行实际连接 + await this.blufi.connectDevice(this.data.deviceId); + + this.setData({ + connecting: false, + connected: true + }); + + wx.showToast({ + title: '连接成功', + icon: 'success' + }); + + } catch (error) { + this.setData({ + connecting: false, + connected: false, + step: 0 + }); + + wx.showToast({ + title: '连接失败', + icon: 'error' + }); + + console.error('连接设备失败:', error); + } + }, + + goToConfig() { + if (!this.data.connected) return; + + // 跳转到WiFi配置页面 + wx.navigateTo({ + url: '/pages/config/config' + }); + }, + + onUnload() { + // 页面卸载时断开连接 + if (this.blufi) { + this.blufi.disconnect(); + } + } +}); +``` + +### 4.3 第三阶段:WiFi扫描和网络选择 + +#### 4.3.1 WiFi扫描实现 +```javascript +// 在BluFiProvisioning类中添加WiFi扫描方法 +class BluFiProvisioning { + // ... 前面的代码 ... + + // 请求WiFi扫描 + async requestWiFiScan() { + try { + console.log('请求设备扫描WiFi...'); + + // 构建获取WiFi列表的控制包 + const packet = this.packet.build(0x00, BLUFI_TYPE_CTRL.GET_WIFI_LIST); + + // 发送数据包 + await this.sendData(packet); + + console.log('WiFi扫描请求已发送'); + return true; + + } catch (error) { + console.error('请求WiFi扫描失败:', error); + throw error; + } + } + + // 发送数据到设备 + async sendData(buffer) { + if (!this.isConnected || !this.deviceId || !this.writeCharacteristicId) { + throw new Error('设备未连接或特征值未初始化'); + } + + return new Promise((resolve, reject) => { + wx.writeBLECharacteristicValue({ + deviceId: this.deviceId, + serviceId: this.serviceId, + characteristicId: this.writeCharacteristicId, + value: buffer, + success: (res) => { + console.log('数据发送成功:', res); + resolve(res); + }, + fail: (err) => { + console.error('数据发送失败:', err); + reject(new Error(`发送失败: ${err.errMsg}`)); + } + }); + }); + } + + // 处理WiFi列表 + handleWiFiList(data) { + try { + console.log('收到WiFi列表数据:', data); + + // 解析WiFi列表数据 + const wifiList = this.parseWiFiList(data); + console.log('解析的WiFi列表:', wifiList); + + // 触发回调 + this.onWiFiListReceived && this.onWiFiListReceived(wifiList); + + } catch (error) { + console.error('处理WiFi列表失败:', error); + } + } + + // 解析WiFi列表数据 + parseWiFiList(data) { + const wifiList = []; + const view = new DataView(data); + let offset = 0; + + try { + while (offset < data.byteLength) { + // 读取SSID长度 + if (offset >= data.byteLength) break; + const ssidLength = view.getUint8(offset); + offset += 1; + + if (ssidLength === 0 || offset + ssidLength > data.byteLength) break; + + // 读取SSID + const ssidBytes = new Uint8Array(data, offset, ssidLength); + const ssid = new TextDecoder('utf-8').decode(ssidBytes); + offset += ssidLength; + + // 读取RSSI(信号强度) + if (offset >= data.byteLength) break; + const rssi = view.getInt8(offset); + offset += 1; + + // 读取认证模式 + if (offset >= data.byteLength) break; + const authMode = view.getUint8(offset); + offset += 1; + + wifiList.push({ + ssid: ssid, + rssi: rssi, + authMode: authMode, + security: this.getSecurityType(authMode) + }); + } + } catch (error) { + console.error('解析WiFi列表数据异常:', error); + } + + return wifiList; + } + + // 获取安全类型描述 + getSecurityType(authMode) { + const securityTypes = { + 0: 'OPEN', + 1: 'WEP', + 2: 'WPA_PSK', + 3: 'WPA2_PSK', + 4: 'WPA_WPA2_PSK', + 5: 'WPA2_ENTERPRISE', + 6: 'WPA3_PSK', + 7: 'WPA2_WPA3_PSK' + }; + + return securityTypes[authMode] || 'UNKNOWN'; + } +} +``` + +#### 4.3.2 WiFi配置页面实现 +```xml + + + + WiFi配置 + 选择要连接的WiFi网络 + + + + + + 可用网络 + + + + + + + {{item.ssid}} + + {{item.rssi}}dBm + {{item.security}} + + + + 📶 + + + + + + 未发现WiFi网络,请点击刷新重新扫描 + + + + + + 或手动输入WiFi信息 + +
+ + + + + + + + + + + + + +
+
+
+``` + +```javascript +// pages/config/config.js +Page({ + data: { + scanning: false, + wifiList: [], + selectedSSID: '', + wifiPassword: '', + canSubmit: false + }, + + onLoad() { + // 获取全局的BluFi实例 + const app = getApp(); + this.blufi = app.globalData.blufi; + + if (!this.blufi || !this.blufi.isConnected) { + wx.showToast({ + title: '设备未连接', + icon: 'error' + }); + wx.navigateBack(); + return; + } + + // 设置WiFi列表接收回调 + this.blufi.onWiFiListReceived = (wifiList) => { + console.log('收到WiFi列表:', wifiList); + this.setData({ + wifiList: wifiList, + scanning: false + }); + }; + + // 自动扫描WiFi + this.scanWiFi(); + }, + + async scanWiFi() { + if (this.data.scanning) return; + + this.setData({ + scanning: true, + wifiList: [] + }); + + try { + await this.blufi.requestWiFiScan(); + + // 设置超时 + setTimeout(() => { + if (this.data.scanning) { + this.setData({ scanning: false }); + wx.showToast({ + title: '扫描超时', + icon: 'none' + }); + } + }, 15000); // 15秒超时 + + } catch (error) { + this.setData({ scanning: false }); + wx.showToast({ + title: '扫描失败', + icon: 'error' + }); + console.error('WiFi扫描失败:', error); + } + }, + + selectWiFi(e) { + const wifi = e.currentTarget.dataset.wifi; + console.log('选择WiFi:', wifi); + + this.setData({ + selectedSSID: wifi.ssid, + wifiPassword: '' // 清空密码 + }); + + this.checkCanSubmit(); + }, + + onSSIDInput(e) { + this.setData({ selectedSSID: e.detail.value }); + this.checkCanSubmit(); + }, + + onPasswordInput(e) { + this.setData({ wifiPassword: e.detail.value }); + this.checkCanSubmit(); + }, + + checkCanSubmit() { + const canSubmit = this.data.selectedSSID.trim().length > 0; + this.setData({ canSubmit }); + }, + + submitWiFiConfig(e) { + const formData = e.detail.value; + const ssid = formData.ssid || this.data.selectedSSID; + const password = formData.password || this.data.wifiPassword; + + if (!ssid.trim()) { + wx.showToast({ + title: '请输入WiFi名称', + icon: 'none' + }); + return; + } + + console.log('提交WiFi配置:', { ssid, password: '***' }); + + // 跳转到配网状态页面 + wx.navigateTo({ + url: `/pages/status/status?ssid=${encodeURIComponent(ssid)}&password=${encodeURIComponent(password)}` + }); + } +}); +``` + +### 4.4 第四阶段:WiFi凭据传输和连接确认 + +#### 4.4.1 WiFi凭据发送实现 +```javascript +// 在BluFiProvisioning类中添加WiFi配网方法 +class BluFiProvisioning { + // ... 前面的代码 ... + + // 开始WiFi配网 + async startProvisioning(ssid, password) { + try { + console.log('开始WiFi配网:', ssid); + this.provisioningState = 'provisioning'; + + // 步骤1:发送SSID + await this.sendWiFiSSID(ssid); + await this.delay(500); + + // 步骤2:发送密码(如果有) + if (password && password.trim().length > 0) { + await this.sendWiFiPassword(password); + await this.delay(500); + } + + // 步骤3:发送连接命令 + await this.sendConnectWiFi(); + + console.log('WiFi配网命令已发送,等待设备响应...'); + return true; + + } catch (error) { + console.error('WiFi配网失败:', error); + this.provisioningState = 'failed'; + throw error; + } + } + + // 发送WiFi SSID + async sendWiFiSSID(ssid) { + console.log('发送WiFi SSID:', ssid); + + const data = new TextEncoder().encode(ssid); + const packet = this.packet.build(0x01, BLUFI_TYPE_DATA.STA_SSID, data); + + await this.sendData(packet); + console.log('SSID发送完成'); + } + + // 发送WiFi密码 + async sendWiFiPassword(password) { + console.log('发送WiFi密码'); + + const data = new TextEncoder().encode(password); + const packet = this.packet.build(0x01, BLUFI_TYPE_DATA.STA_PASSWD, data); + + await this.sendData(packet); + console.log('密码发送完成'); + } + + // 发送连接WiFi命令 + async sendConnectWiFi() { + console.log('发送连接WiFi命令'); + + const packet = this.packet.build(0x00, BLUFI_TYPE_CTRL.CONNECT_WIFI); + + await this.sendData(packet); + console.log('连接命令发送完成'); + } + + // 获取WiFi连接状态 + async getWiFiStatus() { + console.log('请求WiFi连接状态'); + + const packet = this.packet.build(0x00, BLUFI_TYPE_CTRL.GET_WIFI_STATUS); + + await this.sendData(packet); + console.log('状态请求已发送'); + } + + // 延迟函数 + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // 设置配网回调 + setProvisioningCallbacks(callbacks) { + this.onProvisioningSuccess = callbacks.onSuccess; + this.onProvisioningFailed = callbacks.onFailed; + this.onProvisioningProgress = callbacks.onProgress; + } +} +``` + +#### 4.4.2 配网状态页面实现 +```xml + + + + 配网状态 + + {{ssid}} + + + + + + + + + + + + + {{statusText}} + {{subStatusText}} + + + + + + 1 + 发送WiFi名称 + + + + 2 + 发送WiFi密码 + + + + 3 + 连接WiFi网络 + + + + 4 + 验证网络连接 + + + + + + + + + + + + + + + + + 错误信息: + {{errorMessage}} + + +``` + +```javascript +// pages/status/status.js +Page({ + data: { + ssid: '', + password: '', + provisioningState: 'waiting', // waiting, provisioning, success, failed + statusText: '准备开始配网', + subStatusText: '请稍候...', + step: 0, + errorMessage: '' + }, + + onLoad(options) { + this.setData({ + ssid: decodeURIComponent(options.ssid || ''), + password: decodeURIComponent(options.password || '') + }); + + // 获取全局的BluFi实例 + const app = getApp(); + this.blufi = app.globalData.blufi; + + if (!this.blufi || !this.blufi.isConnected) { + wx.showToast({ + title: '设备未连接', + icon: 'error' + }); + wx.navigateBack(); + return; + } + + // 设置配网回调 + this.blufi.setProvisioningCallbacks({ + onSuccess: () => this.onProvisioningSuccess(), + onFailed: (reason) => this.onProvisioningFailed(reason), + onProgress: (step, message) => this.onProvisioningProgress(step, message) + }); + + // 开始配网 + this.startProvisioning(); + }, + + async startProvisioning() { + try { + this.setData({ + provisioningState: 'provisioning', + statusText: '正在配网', + subStatusText: '发送WiFi信息到设备...', + step: 0, + errorMessage: '' + }); + + // 步骤1:发送SSID + this.updateProgress(1, '发送WiFi名称...'); + await this.delay(1000); + + // 步骤2:发送密码 + this.updateProgress(2, '发送WiFi密码...'); + await this.delay(1000); + + // 步骤3:连接WiFi + this.updateProgress(3, '设备连接WiFi网络...'); + + // 执行实际配网 + await this.blufi.startProvisioning(this.data.ssid, this.data.password); + + // 等待连接结果 + this.updateProgress(4, '等待连接结果...'); + + // 设置超时检查 + this.timeoutTimer = setTimeout(() => { + if (this.data.provisioningState === 'provisioning') { + this.onProvisioningFailed('连接超时'); + } + }, 30000); // 30秒超时 + + } catch (error) { + console.error('配网过程异常:', error); + this.onProvisioningFailed(error.message || '配网失败'); + } + }, + + updateProgress(step, message) { + this.setData({ + step: step, + subStatusText: message + }); + }, + + onProvisioningSuccess() { + console.log('配网成功'); + + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = null; + } + + this.setData({ + provisioningState: 'success', + statusText: '配网成功', + subStatusText: '设备已成功连接到WiFi网络', + step: 4 + }); + + wx.showToast({ + title: '配网成功', + icon: 'success' + }); + }, + + onProvisioningFailed(reason) { + console.error('配网失败:', reason); + + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = null; + } + + this.setData({ + provisioningState: 'failed', + statusText: '配网失败', + subStatusText: '设备连接WiFi失败', + errorMessage: reason || '未知错误' + }); + + wx.showToast({ + title: '配网失败', + icon: 'error' + }); + }, + + onProvisioningProgress(step, message) { + this.updateProgress(step, message); + }, + + retryProvisioning() { + this.startProvisioning(); + }, + + goBack() { + wx.navigateBack(); + }, + + goHome() { + wx.reLaunch({ + url: '/pages/index/index' + }); + }, + + cancelProvisioning() { + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = null; + } + + this.setData({ + provisioningState: 'waiting', + statusText: '配网已取消', + subStatusText: '用户取消了配网操作', + step: 0 + }); + }, + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + }, + + onUnload() { + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer); + } + } + }); + ``` + + ## 5. 小程序项目结构 + + ### 5.1 目录结构 + ``` + blufi-miniprogram/ + ├── app.js // 小程序入口文件 + ├── app.json // 小程序配置文件 + ├── app.wxss // 全局样式文件 + ├── project.config.json // 项目配置文件 + ├── pages/ // 页面目录 + │ ├── index/ // 首页 + │ │ ├── index.js + │ │ ├── index.wxml + │ │ └── index.wxss + │ ├── scan/ // 设备扫描页面 + │ │ ├── scan.js + │ │ ├── scan.wxml + │ │ └── scan.wxss + │ ├── connect/ // 设备连接页面 + │ │ ├── connect.js + │ │ ├── connect.wxml + │ │ └── connect.wxss + │ ├── config/ // WiFi配置页面 + │ │ ├── config.js + │ │ ├── config.wxml + │ │ └── config.wxss + │ └── status/ // 配网状态页面 + │ ├── status.js + │ ├── status.wxml + │ └── status.wxss + ├── utils/ // 工具类目录 + │ ├── blufi.js // BluFi协议实现 + │ ├── bluetooth.js // 蓝牙工具类 + │ └── util.js // 通用工具函数 + └── components/ // 组件目录 + ├── loading/ // 加载组件 + └── device-item/ // 设备列表项组件 + ``` + + ### 5.2 小程序配置文件 + + #### 5.2.1 app.json + ```json + { + "pages": [ + "pages/index/index", + "pages/scan/scan", + "pages/connect/connect", + "pages/config/config", + "pages/status/status" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#fff", + "navigationBarTitleText": "BluFi配网", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8f8f8" + }, + "permission": { + "scope.bluetooth": { + "desc": "用于连接BluFi设备进行WiFi配网" + } + }, + "requiredBackgroundModes": ["bluetooth-central"], + "style": "v2", + "sitemapLocation": "sitemap.json" + } + ``` + + #### 5.2.2 project.config.json + ```json + { + "description": "BluFi蓝牙配网小程序", + "packOptions": { + "ignore": [] + }, + "setting": { + "urlCheck": false, + "es6": true, + "enhance": true, + "postcss": true, + "preloadBackgroundData": false, + "minified": true, + "newFeature": false, + "coverView": true, + "nodeModules": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "scopeDataCheck": false, + "uglifyFileName": false, + "checkInvalidKey": true, + "checkSiteMap": true, + "uploadWithSourceMap": true, + "compileHotReLoad": false, + "lazyloadPlaceholderEnable": false, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "enableEngineNative": false, + "useIsolateContext": true, + "userConfirmedBundleSwitch": false, + "packNpmManually": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "disableUseStrict": false, + "minifyWXML": true, + "showES6CompileOption": false, + "useCompilerPlugins": false + }, + "compileType": "miniprogram", + "libVersion": "2.19.4", + "appid": "your_app_id", + "projectname": "blufi-provisioning", + "debugOptions": { + "hidedInDevtools": [] + }, + "scripts": {}, + "staticServerOptions": { + "baseURL": "", + "servePath": "" + }, + "isGameTourist": false, + "condition": { + "search": { + "list": [] + }, + "conversation": { + "list": [] + }, + "game": { + "list": [] + }, + "plugin": { + "list": [] + }, + "gamePlugin": { + "list": [] + }, + "miniprogram": { + "list": [] + } + } + } + ``` + + ### 5.3 全局应用文件 + + #### 5.3.1 app.js + ```javascript + // app.js + App({ + globalData: { + blufi: null, // 全局BluFi实例 + deviceInfo: null, // 当前连接的设备信息 + wifiConfig: null // WiFi配置信息 + }, + + onLaunch() { + console.log('BluFi配网小程序启动'); + + // 检查蓝牙支持 + this.checkBluetoothSupport(); + + // 初始化全局数据 + this.initGlobalData(); + }, + + checkBluetoothSupport() { + wx.getSystemInfo({ + success: (res) => { + console.log('系统信息:', res); + + // 检查是否支持蓝牙 + if (!wx.openBluetoothAdapter) { + wx.showModal({ + title: '提示', + content: '当前微信版本过低,无法使用蓝牙功能,请升级到最新微信版本后重试。', + showCancel: false + }); + } + } + }); + }, + + initGlobalData() { + // 初始化BluFi实例 + const BluFiProvisioning = require('./utils/blufi.js'); + this.globalData.blufi = new BluFiProvisioning(); + }, + + onShow() { + console.log('小程序显示'); + }, + + onHide() { + console.log('小程序隐藏'); + }, + + onError(error) { + console.error('小程序错误:', error); + } + }); + ``` + + ## 6. 错误处理和重试机制 + + ### 6.1 错误码定义 + ```javascript + // utils/error-codes.js + const BluFiErrorCodes = { + // 蓝牙相关错误 + BLUETOOTH_NOT_AVAILABLE: { + code: 1001, + message: '蓝牙不可用,请检查蓝牙是否开启' + }, + BLUETOOTH_ADAPTER_INIT_FAILED: { + code: 1002, + message: '蓝牙适配器初始化失败' + }, + DEVICE_SCAN_FAILED: { + code: 1003, + message: '设备扫描失败' + }, + DEVICE_NOT_FOUND: { + code: 1004, + message: '未发现BluFi设备' + }, + + // 连接相关错误 + CONNECTION_FAILED: { + code: 2001, + message: '设备连接失败' + }, + CONNECTION_TIMEOUT: { + code: 2002, + message: '设备连接超时' + }, + SERVICE_NOT_FOUND: { + code: 2003, + message: '未找到BluFi服务' + }, + CHARACTERISTIC_NOT_FOUND: { + code: 2004, + message: '未找到BluFi特征值' + }, + NOTIFICATION_ENABLE_FAILED: { + code: 2005, + message: '启用通知失败' + }, + + // 配网相关错误 + WIFI_SCAN_FAILED: { + code: 3001, + message: 'WiFi扫描失败' + }, + WIFI_SCAN_TIMEOUT: { + code: 3002, + message: 'WiFi扫描超时' + }, + WIFI_CREDENTIALS_INVALID: { + code: 3003, + message: 'WiFi凭据无效' + }, + WIFI_CONNECTION_FAILED: { + code: 3004, + message: 'WiFi连接失败' + }, + WIFI_CONNECTION_TIMEOUT: { + code: 3005, + message: 'WiFi连接超时' + }, + PROVISIONING_TIMEOUT: { + code: 3006, + message: '配网超时' + }, + + // 数据传输错误 + DATA_SEND_FAILED: { + code: 4001, + message: '数据发送失败' + }, + DATA_PARSE_FAILED: { + code: 4002, + message: '数据解析失败' + }, + CHECKSUM_ERROR: { + code: 4003, + message: '数据校验失败' + }, + + // 通用错误 + UNKNOWN_ERROR: { + code: 9999, + message: '未知错误' + } + }; + + module.exports = BluFiErrorCodes; + ``` + + ### 6.2 重试机制实现 + ```javascript + // utils/retry.js + class RetryManager { + constructor(options = {}) { + this.maxRetries = options.maxRetries || 3; + this.retryDelay = options.retryDelay || 2000; + this.backoffMultiplier = options.backoffMultiplier || 1.5; + this.maxDelay = options.maxDelay || 10000; + } + + async execute(operation, context = '') { + let lastError; + let currentDelay = this.retryDelay; + + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { + try { + console.log(`${context} - 尝试 ${attempt + 1}/${this.maxRetries + 1}`); + const result = await operation(); + + if (attempt > 0) { + console.log(`${context} - 重试成功`); + } + + return result; + + } catch (error) { + lastError = error; + console.error(`${context} - 尝试 ${attempt + 1} 失败:`, error); + + // 如果是最后一次尝试,直接抛出错误 + if (attempt === this.maxRetries) { + break; + } + + // 等待后重试 + console.log(`${context} - ${currentDelay}ms 后重试`); + await this.delay(currentDelay); + + // 增加延迟时间(指数退避) + currentDelay = Math.min( + currentDelay * this.backoffMultiplier, + this.maxDelay + ); + } + } + + console.error(`${context} - 所有重试都失败了`); + throw lastError; + } + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + } + + module.exports = RetryManager; + ``` + + ### 6.3 错误处理工具类 + ```javascript + // utils/error-handler.js + const BluFiErrorCodes = require('./error-codes.js'); + + class ErrorHandler { + static handleError(error, context = '') { + console.error(`错误处理 [${context}]:`, error); + + let errorInfo = { + code: BluFiErrorCodes.UNKNOWN_ERROR.code, + message: BluFiErrorCodes.UNKNOWN_ERROR.message, + detail: error.message || error.errMsg || '未知错误' + }; + + // 根据错误信息匹配错误码 + if (error.errMsg) { + errorInfo = this.mapWxErrorToCode(error.errMsg); + } else if (error.code) { + errorInfo = this.findErrorByCode(error.code); + } else if (error.message) { + errorInfo = this.mapMessageToCode(error.message); + } + + return errorInfo; + } + + static mapWxErrorToCode(errMsg) { + const errorMappings = { + 'bluetooth not available': BluFiErrorCodes.BLUETOOTH_NOT_AVAILABLE, + 'bluetooth adapter init fail': BluFiErrorCodes.BLUETOOTH_ADAPTER_INIT_FAILED, + 'createBLEConnection:fail': BluFiErrorCodes.CONNECTION_FAILED, + 'getBLEDeviceServices:fail': BluFiErrorCodes.SERVICE_NOT_FOUND, + 'getBLEDeviceCharacteristics:fail': BluFiErrorCodes.CHARACTERISTIC_NOT_FOUND, + 'notifyBLECharacteristicValueChange:fail': BluFiErrorCodes.NOTIFICATION_ENABLE_FAILED, + 'writeBLECharacteristicValue:fail': BluFiErrorCodes.DATA_SEND_FAILED + }; + + for (const [key, errorCode] of Object.entries(errorMappings)) { + if (errMsg.includes(key)) { + return { + code: errorCode.code, + message: errorCode.message, + detail: errMsg + }; + } + } + + return { + code: BluFiErrorCodes.UNKNOWN_ERROR.code, + message: BluFiErrorCodes.UNKNOWN_ERROR.message, + detail: errMsg + }; + } + + static findErrorByCode(code) { + for (const errorCode of Object.values(BluFiErrorCodes)) { + if (errorCode.code === code) { + return { + code: errorCode.code, + message: errorCode.message, + detail: '' + }; + } + } + + return { + code: BluFiErrorCodes.UNKNOWN_ERROR.code, + message: BluFiErrorCodes.UNKNOWN_ERROR.message, + detail: `错误码: ${code}` + }; + } + + static mapMessageToCode(message) { + const messageMappings = { + '蓝牙不可用': BluFiErrorCodes.BLUETOOTH_NOT_AVAILABLE, + '设备未连接': BluFiErrorCodes.CONNECTION_FAILED, + '连接超时': BluFiErrorCodes.CONNECTION_TIMEOUT, + '配网超时': BluFiErrorCodes.PROVISIONING_TIMEOUT, + 'WiFi连接失败': BluFiErrorCodes.WIFI_CONNECTION_FAILED + }; + + for (const [key, errorCode] of Object.entries(messageMappings)) { + if (message.includes(key)) { + return { + code: errorCode.code, + message: errorCode.message, + detail: message + }; + } + } + + return { + code: BluFiErrorCodes.UNKNOWN_ERROR.code, + message: BluFiErrorCodes.UNKNOWN_ERROR.message, + detail: message + }; + } + + static showError(errorInfo, options = {}) { + const title = options.title || '错误'; + const showDetail = options.showDetail !== false; + + let content = errorInfo.message; + if (showDetail && errorInfo.detail) { + content += `\n\n详细信息: ${errorInfo.detail}`; + } + + wx.showModal({ + title: title, + content: content, + showCancel: false, + confirmText: '确定' + }); + } + + static showToast(errorInfo) { + wx.showToast({ + title: errorInfo.message, + icon: 'error', + duration: 3000 + }); + } + } + + module.exports = ErrorHandler; + ``` + + ## 7. 安全机制和数据保护 + + ### 7.1 数据加密(可选) + ```javascript + // utils/encryption.js + class BluFiEncryption { + constructor(options = {}) { + this.enabled = options.enabled || false; + this.algorithm = options.algorithm || 'AES-128-CFB'; + this.key = options.key || null; + this.iv = options.iv || null; + } + + // 设置加密密钥 + setKey(key) { + this.key = key; + } + + // 设置初始化向量 + setIV(iv) { + this.iv = iv; + } + + // 加密数据 + encrypt(data) { + if (!this.enabled || !this.key) { + return data; + } + + try { + // 这里应该实现实际的AES加密 + // 由于微信小程序环境限制,可能需要使用第三方加密库 + console.log('数据加密(模拟)'); + return data; // 返回加密后的数据 + } catch (error) { + console.error('数据加密失败:', error); + throw error; + } + } + + // 解密数据 + decrypt(encryptedData) { + if (!this.enabled || !this.key) { + return encryptedData; + } + + try { + // 这里应该实现实际的AES解密 + console.log('数据解密(模拟)'); + return encryptedData; // 返回解密后的数据 + } catch (error) { + console.error('数据解密失败:', error); + throw error; + } + } + } + + module.exports = BluFiEncryption; + ``` + + ### 7.2 数据校验 + ```javascript + // utils/checksum.js + class ChecksumCalculator { + // 计算简单校验和 + static calculateSimpleChecksum(data) { + let checksum = 0; + const view = new Uint8Array(data); + + for (let i = 0; i < view.length; i++) { + checksum += view[i]; + } + + return checksum & 0xFFFF; + } + + // 验证校验和 + static verifyChecksum(data, expectedChecksum) { + const calculatedChecksum = this.calculateSimpleChecksum(data); + return calculatedChecksum === expectedChecksum; + } + + // CRC16校验(可选) + static calculateCRC16(data) { + let crc = 0xFFFF; + const polynomial = 0x1021; + + const view = new Uint8Array(data); + + for (let i = 0; i < view.length; i++) { + crc ^= (view[i] << 8); + + for (let j = 0; j < 8; j++) { + if (crc & 0x8000) { + crc = (crc << 1) ^ polynomial; + } else { + crc <<= 1; + } + crc &= 0xFFFF; + } + } + + return crc; + } + } + + module.exports = ChecksumCalculator; + ``` + + ### 7.3 敏感数据处理 + ```javascript + // utils/security.js + class SecurityManager { + // 清理敏感数据 + static clearSensitiveData() { + // 清理WiFi密码等敏感信息 + const app = getApp(); + if (app.globalData.wifiConfig) { + app.globalData.wifiConfig.password = null; + } + + // 清理本地存储中的敏感数据 + try { + wx.removeStorageSync('wifi_password'); + wx.removeStorageSync('encryption_key'); + } catch (error) { + console.warn('清理本地存储失败:', error); + } + } + + // 验证WiFi凭据格式 + static validateWiFiCredentials(ssid, password) { + const errors = []; + + // SSID验证 + if (!ssid || ssid.trim().length === 0) { + errors.push('WiFi名称不能为空'); + } else if (ssid.length > 32) { + errors.push('WiFi名称长度不能超过32个字符'); + } + + // 密码验证(可选) + if (password && password.length > 64) { + errors.push('WiFi密码长度不能超过64个字符'); + } + + return { + valid: errors.length === 0, + errors: errors + }; + } + + // 生成随机序列号 + static generateSequenceNumber() { + return Math.floor(Math.random() * 65536); + } + } + + module.exports = SecurityManager; + ``` + + ## 8. 测试和调试 + + ### 8.1 调试工具 + ```javascript + // utils/debug.js + class DebugManager { + constructor() { + this.enabled = true; // 生产环境应设为false + this.logLevel = 'DEBUG'; // DEBUG, INFO, WARN, ERROR + this.logs = []; + this.maxLogs = 1000; + } + + log(level, message, data = null) { + if (!this.enabled) return; + + const timestamp = new Date().toISOString(); + const logEntry = { + timestamp, + level, + message, + data + }; + + // 添加到日志数组 + this.logs.push(logEntry); + + // 限制日志数量 + if (this.logs.length > this.maxLogs) { + this.logs.shift(); + } + + // 控制台输出 + const logMessage = `[${timestamp}] ${level}: ${message}`; + switch (level) { + case 'DEBUG': + console.log(logMessage, data); + break; + case 'INFO': + console.info(logMessage, data); + break; + case 'WARN': + console.warn(logMessage, data); + break; + case 'ERROR': + console.error(logMessage, data); + break; + } + } + + debug(message, data) { + this.log('DEBUG', message, data); + } + + info(message, data) { + this.log('INFO', message, data); + } + + warn(message, data) { + this.log('WARN', message, data); + } + + error(message, data) { + this.log('ERROR', message, data); + } + + // 导出日志 + exportLogs() { + return JSON.stringify(this.logs, null, 2); + } + + // 清空日志 + clearLogs() { + this.logs = []; + } + + // 获取最近的错误日志 + getRecentErrors(count = 10) { + return this.logs + .filter(log => log.level === 'ERROR') + .slice(-count); + } + } + + // 创建全局调试实例 + const debugManager = new DebugManager(); + + module.exports = debugManager; + ``` + + ### 8.2 测试用例 + ```javascript + // test/blufi-test.js + class BluFiTest { + constructor() { + this.testResults = []; + } + + // 运行所有测试 + async runAllTests() { + console.log('开始BluFi测试...'); + + await this.testBluetoothInit(); + await this.testDeviceScan(); + await this.testPacketParsing(); + await this.testChecksumCalculation(); + + this.printTestResults(); + } + + // 测试蓝牙初始化 + async testBluetoothInit() { + try { + const blufi = new BluFiProvisioning(); + await blufi.initBluetooth(); + + this.addTestResult('蓝牙初始化', true, '成功'); + } catch (error) { + this.addTestResult('蓝牙初始化', false, error.message); + } + } + + // 测试设备扫描 + async testDeviceScan() { + try { + const blufi = new BluFiProvisioning(); + + let deviceFound = false; + await blufi.startScan((device) => { + if (blufi.isValidBluFiDevice(device)) { + deviceFound = true; + } + }); + + // 等待5秒 + await new Promise(resolve => setTimeout(resolve, 5000)); + await blufi.stopScan(); + + this.addTestResult('设备扫描', deviceFound, deviceFound ? '发现设备' : '未发现设备'); + } catch (error) { + this.addTestResult('设备扫描', false, error.message); + } + } + + // 测试数据包解析 + testPacketParsing() { + try { + const packet = new BluFiPacket(); + + // 构建测试数据包 + const testData = new TextEncoder().encode('test'); + const buffer = packet.build(0x01, 0x02, testData); + + // 解析数据包 + const parsed = packet.parse(buffer); + + const success = parsed.type === 0x01 && + parsed.subtype === 0x02 && + new TextDecoder().decode(parsed.data) === 'test'; + + this.addTestResult('数据包解析', success, success ? '解析正确' : '解析错误'); + } catch (error) { + this.addTestResult('数据包解析', false, error.message); + } + } + + // 测试校验和计算 + testChecksumCalculation() { + try { + const testData = new Uint8Array([0x01, 0x02, 0x03, 0x04]); + const checksum = ChecksumCalculator.calculateSimpleChecksum(testData.buffer); + + // 预期校验和: 1+2+3+4 = 10 + const expected = 10; + const success = checksum === expected; + + this.addTestResult('校验和计算', success, + success ? `校验和正确: ${checksum}` : `校验和错误: 期望${expected}, 实际${checksum}`); + } catch (error) { + this.addTestResult('校验和计算', false, error.message); + } + } + + addTestResult(testName, success, message) { + this.testResults.push({ + name: testName, + success: success, + message: message, + timestamp: new Date().toISOString() + }); + } + + printTestResults() { + console.log('\n=== BluFi测试结果 ==='); + + let passCount = 0; + let totalCount = this.testResults.length; + + this.testResults.forEach(result => { + const status = result.success ? '✓ 通过' : '✗ 失败'; + console.log(`${status} ${result.name}: ${result.message}`); + + if (result.success) { + passCount++; + } + }); + + console.log(`\n总计: ${passCount}/${totalCount} 个测试通过`); + + if (passCount === totalCount) { + console.log('🎉 所有测试都通过了!'); + } else { + console.log('❌ 部分测试失败,请检查相关功能'); + } + } + } + + module.exports = BluFiTest; + ``` + + ## 9. 性能优化 + + ### 9.1 内存管理 + ```javascript + // utils/memory-manager.js + class MemoryManager { + static clearUnusedData() { + // 清理不再使用的设备列表 + const app = getApp(); + if (app.globalData.deviceList) { + app.globalData.deviceList = []; + } + + // 清理WiFi列表缓存 + if (app.globalData.wifiList) { + app.globalData.wifiList = []; + } + + // 强制垃圾回收(如果支持) + if (typeof wx.triggerGC === 'function') { + wx.triggerGC(); + } + } + + static monitorMemoryUsage() { + if (typeof wx.getPerformance === 'function') { + const performance = wx.getPerformance(); + if (performance.memory) { + console.log('内存使用情况:', { + used: performance.memory.usedJSHeapSize, + total: performance.memory.totalJSHeapSize, + limit: performance.memory.jsHeapSizeLimit + }); + } + } + } + } + + module.exports = MemoryManager; + ``` + + ### 9.2 连接优化 + ```javascript + // utils/connection-optimizer.js + class ConnectionOptimizer { + constructor() { + this.connectionPool = new Map(); + this.maxConnections = 1; // BluFi通常只需要一个连接 + } + + // 优化连接参数 + getOptimalConnectionParams() { + return { + timeout: 15000, // 15秒连接超时 + interval: 100, // 100ms扫描间隔 + allowDuplicatesKey: false, + services: [BLUFI_SERVICE_UUID] + }; + } + + // 连接重用 + reuseConnection(deviceId) { + if (this.connectionPool.has(deviceId)) { + const connection = this.connectionPool.get(deviceId); + if (connection.isValid) { + return connection; + } else { + this.connectionPool.delete(deviceId); + } + } + return null; + } + + // 添加连接到池 + addConnection(deviceId, connection) { + // 如果池满了,移除最旧的连接 + if (this.connectionPool.size >= this.maxConnections) { + const firstKey = this.connectionPool.keys().next().value; + this.connectionPool.delete(firstKey); + } + + this.connectionPool.set(deviceId, connection); + } + + // 清理连接池 + clearPool() { + this.connectionPool.clear(); + } + } + + module.exports = ConnectionOptimizer; + ``` + + ## 10. 部署和发布 + + ### 10.1 发布前检查清单 + - [ ] 所有功能测试通过 + - [ ] 与ESP官方espblufi应用对比测试完成 + - [ ] 错误处理机制完善 + - [ ] 用户界面友好性检查 + - [ ] 性能测试通过 + - [ ] 安全性检查完成 + - [ ] 代码审查完成 + - [ ] 文档更新完成 + + ### 10.2 版本管理 + ```javascript + // utils/version.js + const VERSION_INFO = { + version: '1.0.0', + buildNumber: '20240101', + releaseDate: '2024-01-01', + features: [ + 'BluFi设备扫描和连接', + 'WiFi网络配置', + '配网状态监控', + '错误处理和重试机制' + ], + compatibility: { + minWechatVersion: '7.0.0', + minSystemVersion: { + ios: '10.0', + android: '6.0' + }, + espIdfVersion: '4.4+' + } + }; + + module.exports = VERSION_INFO; + ``` + + ### 10.3 用户手册 + + #### 10.3.1 使用步骤 + 1. **准备工作** + - 确保手机蓝牙已开启 + - 确保ESP32设备处于配网模式 + - 确保手机已连接到互联网 + + 2. **开始配网** + - 打开BluFi配网小程序 + - 点击"开始扫描"按钮 + - 从设备列表中选择要配网的设备 + + 3. **连接设备** + - 等待设备连接完成 + - 连接成功后会显示"设备连接成功" + + 4. **配置WiFi** + - 选择要连接的WiFi网络,或手动输入WiFi信息 + - 输入WiFi密码 + - 点击"开始配网"按钮 + + 5. **等待配网完成** + - 等待设备连接到WiFi网络 + - 配网成功后会显示"配网完成" + + #### 10.3.2 常见问题解决 + + **Q: 扫描不到设备怎么办?** + A: + - 检查手机蓝牙是否开启 + - 确认设备是否处于配网模式 + - 尝试重新启动设备 + - 检查设备距离是否过远 + + **Q: 连接设备失败怎么办?** + A: + - 确认设备未被其他应用占用 + - 尝试重启手机蓝牙 + - 重新扫描设备 + + **Q: WiFi配网失败怎么办?** + A: + - 检查WiFi密码是否正确 + - 确认WiFi信号强度是否足够 + - 检查WiFi网络是否正常 + - 尝试重新配网 + + ## 11. 附录 + + ### 11.1 参考文档 + - [ESP-IDF BluFi官方文档](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/blufi.html) + - [微信小程序蓝牙API文档](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth/wx.openBluetoothAdapter.html) + - [BluFi协议规范](https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/blufi) + + ### 11.2 技术支持 + - 开发团队:[联系方式] + - 问题反馈:[反馈渠道] + - 更新通知:[通知方式] + + ### 11.3 更新日志 + + #### v1.0.0 (2024-01-01) + - 初始版本发布 + - 实现基础BluFi配网功能 + - 支持设备扫描、连接、WiFi配置 + - 完善错误处理和重试机制 + + --- + + **文档版本**: 2.0 + **创建日期**: 2025年8月 + **最后更新**: 2025年8月 + **文档状态**: 已完成 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..04a9e19 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,18 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +# 1.5.6 +# 版本号用于OTA升级 +set(PROJECT_VER "1.7.4") + +# Add this line to disable the specific warning +add_compile_options(-Wno-missing-field-initializers) + +# # 排除esp_lcd组件,因为板子不需要显示器 +# set(EXCLUDE_COMPONENTS "esp_lcd") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(kapi) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5048598 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Xiaoxia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..db0cf66 --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# 小智 AI 聊天机器人 (XiaoZhi AI Chatbot) + +(中文 | [English](README_en.md) | [日本語](README_ja.md)) + +## 视频介绍 + +👉 [ESP32+SenseVoice+Qwen72B打造你的AI聊天伴侣!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/) + +👉 [给小智装上 DeepSeek 的聪明大脑【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/) + +👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/) + +## 项目目的 + +本项目是由虾哥开源的一个开源项目,以 MIT 许可证发布,允许任何人免费使用,并可以用于商业用途。 + +我们希望通过这个项目,能够帮助更多人入门 AI 硬件开发,了解如何将当下飞速发展的大语言模型应用到实际的硬件设备中。无论你是对 AI 感兴趣的学生,还是想要探索新技术的开发者,都可以通过这个项目获得宝贵的学习经验。 + +欢迎所有人参与到项目的开发和改进中来。如果你有任何想法或建议,请随时提出 Issue 或加入群聊。 + +学习交流 QQ 群:376893254 + +## 已实现功能 + +- Wi-Fi / ML307 Cat.1 4G +- BOOT 键唤醒和打断,支持点击和长按两种触发方式 +- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr) +- 流式语音对话(WebSocket 或 UDP 协议) +- 支持国语、粤语、英语、日语、韩语 5 种语言识别 [SenseVoice](https://github.com/FunAudioLLM/SenseVoice) +- 声纹识别,识别是谁在喊 AI 的名字 [3D Speaker](https://github.com/modelscope/3D-Speaker) +- 大模型 TTS(火山引擎 或 CosyVoice) +- 大模型 LLM(Qwen, DeepSeek, Doubao) +- 可配置的提示词和音色(自定义角色) +- 短期记忆,每轮对话后自我总结 +- OLED / LCD 显示屏,显示信号强弱或对话内容 +- 支持 LCD 显示图片表情 +- 支持多语言(中文、英文) + +## 硬件部分 + +### 面包板手工制作实践 + +详见飞书文档教程: + +👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) + +面包板效果图如下: + +![面包板效果图](docs/wiring2.jpg) + +### 已支持的开源硬件 + +- 立创·实战派 ESP32-S3 开发板 +- 乐鑫 ESP32-S3-BOX3 +- M5Stack CoreS3 +- AtomS3R + Echo Base +- AtomMatrix + Echo Base +- 神奇按钮 2.4 +- 微雪电子 ESP32-S3-Touch-AMOLED-1.8 +- LILYGO T-Circle-S3 +- 虾哥 Mini C3 +- Moji 小智AI衍生版 +- 璀璨·AI吊坠 +- 无名科技Nologo-星智-1.54TFT +- SenseCAP Watcher +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 固件部分 + +### 免开发环境烧录 + +新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。 + +固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,目前个人用户注册账号可以免费使用 Qwen 实时模型。 + +👉 [Flash烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) + + +### 开发环境 + +- Cursor 或 VSCode +- 安装 ESP-IDF 插件,选择 SDK 版本 5.3 或以上 +- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰 +- 使用 Google C++ 代码风格,提交代码时请确保符合规范 + +### 开发者文档 + +- [开发板定制指南](main/boards/README.md) - 学习如何为小智创建自定义开发板适配 +- [物联网控制模块](main/iot/README.md) - 了解如何通过AI语音控制物联网设备 + + +## 智能体配置 + +如果你已经拥有一个小智 AI 聊天机器人设备,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。 + +👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/) + +## 技术原理与私有化部署 + +👉 [一份详细的 WebSocket 通信协议文档](docs/websocket.md) + +在个人电脑上部署服务器,可以参考另一位作者同样以 MIT 许可证开源的项目 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) + +## Star History + + + + + + Star History Chart + + diff --git a/README_RTC.md b/README_RTC.md new file mode 100644 index 0000000..3021d13 --- /dev/null +++ b/README_RTC.md @@ -0,0 +1,117 @@ +

+

ConversationalAI Embedded Kit

+ +## 快速开始 + +具体操作,请参考 [官网文档](https://www.volcengine.com/docs/6348/1806625)。 + +## 运行设备端(乐鑫) + +以下操作以 macOS 操作系统为例。 + +### 环境与硬件要求 +- 乐鑫 ESP32-S3-Korvo-2 +- USB 数据线:两条 A 转 Micro-B 数据线,一条作为电源线,一条作为串口线。 +- PC 设备服:编译和烧录。支持 Windows、Linux 或者 macOS 操作系统。(本文操作以 macOS 为例) + +### 配置乐鑫环境 + +详见[开发环境配置文档](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/get-started/index.html)。 + +1. 安装 CMake 和 Ninja 编译工具。 + ```bash + brew install cmake ninja dfu-util + ``` +2. 将乐鑫 ADF 框架克隆到本地,并同步各子仓(submodule)代码。 + > **注意**:Demo 中使用的 ADF 版本为 `eca11f20e56f9b5321b714da4305e123672d92a9`,对应 IDF 版本为 `v5.4`,请确保 ADF 版本与 IDF 版本匹配。 + ```bash + # 1. clone 乐鑫 ADF 框架 + git clone https://github.com/espressif/esp-adf.git + # 2. 进入esp-adf目录 + cd esp-adf + # 3. 切换到乐鑫 ADF 指定版本 + git reset --hard eca11f20e56f9b5321b714da4305e123672d92a9 + # 4. 同步各子仓代码 + git submodule update --init --recursive + ``` +3. 安装乐鑫 esp32s3 开发环境相关依赖。 + ```bash + ./install.sh esp32s3 + ``` + 成功安装所有依赖后,命令行会出现如下提示: + ```bash + All done! You can now run: + . ./export.sh + ``` + > 如在上述任何步骤中遇到以下错误: + > ` 可前往**访达->应用程序->Python** 文件夹,点击 `Install Certificates.command` 安装证书。更多信息,请参考 [安装 ESP-IDF 工具时出现的下载错误](https://github.com/espressif/esp-idf/issues/4775)。 +4. 设置环境变量。 + > **每次打开命令行窗口均需要运行该命令进行设置。** + ```bash + . ./export.sh + ``` +### 下载并配置工程 +1. 将实时对话式 AI 硬件示例工程克隆到 乐鑫 ADF examples 目录下。 + 1. 进入 esp-adf/examples 目录。 + ```bash + cd $ADF_PATH/examples + ``` + 2. 克隆实时对话式 AI 硬件示例工程。 + ```bash + git clone https://github.com/volcengine/ConversationalAI-Embedded-Kit-2.0.git + ``` +2. 禁用乐鑫工程中的火山组件。 + 1. 进入 esp-adf 目录。 + ```bash + cd $ADF_PATH + ``` + 2. 禁用乐鑫工程中的火山组件。 + ```bash + git apply $ADF_PATH/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif/0001-feat-disable-volc-esp-libs.patch + ``` +3. 修复乐鑫按键问题 + 1. 进入 esp-adf 目录。 + ```bash + cd $ADF_PATH + ``` + 2. 修复乐鑫按键问题。 + ```bash + git apply $ADF_PATH/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif/0002-fix-esp-button.patch + ``` + +### 编译固件 +进入 `esp-adf/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif` 目录下编译固件。 +1. 进入 espressif 目录。 + ```bash + cd $ADF_PATH/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif + ``` +2. 设置编译目标平台。 + ```bash + idf.py set-target esp32s3 + ``` +3. 设置 实例ID、产品ID、产品秘钥、设备ID等参数。 + ```bash + idf.py menuconfig + ``` + 进入 `Example Configuration` 菜单,在 `volcano instance id` 中填入你的实例ID,在 `volcano product key` 中填入你的产品Key,在 `volcano product secret` 中填入你的产品秘钥,在 `device name` 中填入你的设备ID, 在 `bot id` 中填入你的智能体ID,并保存。 +4. 编译固件。 + ```bash + idf.py build + ``` +### 烧录并运行示例 Demo +1. 打开乐鑫开发板电源开关。 +2. 烧录固件。 + ```bash + idf.py flash + ``` +3. 运行示例 Demo 并查看串口日志输出。 + ```bash + idf.py monitor + ``` +4. Wi-Fi 配网。 + 1. 手机找到名如 VolcConvAI-XXXXXX” 的 Wi-Fi 热点,密码同Wi-Fi名,连接上 Wi-Fi。 + 2. 打开浏览器,在地址栏输入 `http://192.168.4.1`,进入 Wi-Fi 配网页面。 + 3. 输入 Wi-Fi 名称和密码,点击提交。 + + > **注意**:如果需更换 Wi-Fi,请重启设备。如果设备重启后无法连接到之前保存的 Wi-Fi(例如超出了范围或旧网络已关闭),请等待 30s 进入配网模式,再重新执行上面 Wi-Fi 配网的 3 个步骤。 diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..6c1b82e --- /dev/null +++ b/README_en.md @@ -0,0 +1,151 @@ +# XiaoZhi AI Chatbot + +([中文](README.md) | English | [日本語](README_ja.md)) + +## Introduction + +👉 [Build your AI chat companion with ESP32+SenseVoice+Qwen72B!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/) + +👉 [Equipping XiaoZhi with DeepSeek's smart brain【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/) + +👉 [Build your own AI companion, a beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/) + +## Project Purpose + +This is an open-source project released under the MIT license, allowing anyone to use it freely, including for commercial purposes. + +Through this project, we aim to help more people get started with AI hardware development and understand how to implement rapidly evolving large language models in actual hardware devices. Whether you're a student interested in AI or a developer exploring new technologies, this project offers valuable learning experiences. + +Everyone is welcome to participate in the project's development and improvement. If you have any ideas or suggestions, please feel free to raise an Issue or join the chat group. + +Learning & Discussion QQ Group: 376893254 + +## Implemented Features + +- Wi-Fi / ML307 Cat.1 4G +- BOOT button wake-up and interruption, supporting both click and long-press triggers +- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr) +- Streaming voice dialogue (WebSocket or UDP protocol) +- Support for 5 languages: Mandarin, Cantonese, English, Japanese, Korean [SenseVoice](https://github.com/FunAudioLLM/SenseVoice) +- Voice print recognition to identify who's calling AI's name [3D Speaker](https://github.com/modelscope/3D-Speaker) +- Large model TTS (Volcano Engine or CosyVoice) +- Large Language Models (Qwen, DeepSeek, Doubao) +- Configurable prompts and voice tones (custom characters) +- Short-term memory, self-summarizing after each conversation round +- OLED / LCD display showing signal strength or conversation content +- Support for LCD image expressions +- Multi-language support (Chinese, English) + +## Hardware Section + +### Breadboard DIY Practice + +See the Feishu document tutorial: + +👉 [XiaoZhi AI Chatbot Encyclopedia](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) + +Breadboard demonstration: + +![Breadboard Demo](docs/wiring2.jpg) + +### Supported Open Source Hardware + +- LiChuang ESP32-S3 Development Board +- Espressif ESP32-S3-BOX3 +- M5Stack CoreS3 +- AtomS3R + Echo Base +- AtomMatrix + Echo Base +- Magic Button 2.4 +- Waveshare ESP32-S3-Touch-AMOLED-1.8 +- LILYGO T-Circle-S3 +- XiaGe Mini C3 +- Moji XiaoZhi AI Derivative Version +- CuiCan AI pendant +- WMnologo-Xingzhi-1.54TFT +- SenseCAP Watcher + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## Firmware Section + +### Flashing Without Development Environment + +For beginners, it's recommended to first use the firmware that can be flashed without setting up a development environment. + +The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Currently, personal users can register an account to use the Qwen real-time model for free. + +👉 [Flash Firmware Guide (No IDF Environment)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) + +### Development Environment + +- Cursor or VSCode +- Install ESP-IDF plugin, select SDK version 5.3 or above +- Linux is preferred over Windows for faster compilation and fewer driver issues +- Use Google C++ code style, ensure compliance when submitting code + +### Developer Documentation + +- [Board Customization Guide](main/boards/README.md) - Learn how to create custom board adaptations for XiaoZhi +- [IoT Control Module](main/iot/README.md) - Understand how to control IoT devices through AI voice commands + +## AI Agent Configuration + +If you already have a XiaoZhi AI chatbot device, you can configure it through the [xiaozhi.me](https://xiaozhi.me) console. + +👉 [Backend Operation Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/) + +## Technical Principles and Private Deployment + +👉 [Detailed WebSocket Communication Protocol Documentation](docs/websocket.md) + +For server deployment on personal computers, refer to another MIT-licensed project [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) + +## Star History + + + + + + Star History Chart + + diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 0000000..bda3050 --- /dev/null +++ b/README_ja.md @@ -0,0 +1,148 @@ +# シャオジー AI チャットボット + +([中文](README.md) | [English](README_en.md) | 日本語) + +## プロジェクト紹介 + +👉 [ESP32+SenseVoice+Qwen72Bで AI チャット仲間を作ろう!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/) + +👉 [シャオジーに DeepSeek のスマートな頭脳を搭載【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/) + +👉 [自分だけの AI パートナーを作る、初心者向けガイド【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/) + +## プロジェクトの目的 + +このプロジェクトは MIT ライセンスの下で公開されているオープンソースプロジェクトで、商用利用を含め、誰でも自由に使用することができます。 + +このプロジェクトを通じて、より多くの人々が AI ハードウェア開発を始め、急速に進化している大規模言語モデルを実際のハードウェアデバイスに実装する方法を理解できるようになることを目指しています。AI に興味のある学生でも、新しい技術を探求する開発者でも、このプロジェクトから貴重な学習経験を得ることができます。 + +プロジェクトの開発と改善には誰でも参加できます。アイデアや提案がありましたら、Issue を立てるかチャットグループにご参加ください。 + +学習・交流 QQ グループ:376893254 + +## 実装済みの機能 + +- Wi-Fi / ML307 Cat.1 4G +- BOOT ボタンによる起動と中断、クリックと長押しの2種類のトリガーに対応 +- オフライン音声起動 [ESP-SR](https://github.com/espressif/esp-sr) +- ストリーミング音声対話(WebSocket または UDP プロトコル) +- 5言語対応:標準中国語、広東語、英語、日本語、韓国語 [SenseVoice](https://github.com/FunAudioLLM/SenseVoice) +- 話者認識、AI の名前を呼んでいる人を識別 [3D Speaker](https://github.com/modelscope/3D-Speaker) +- 大規模モデル TTS(Volcano Engine または CosyVoice) +- 大規模言語モデル(Qwen, DeepSeek, Doubao) +- 設定可能なプロンプトと音声トーン(カスタムキャラクター) +- 短期記憶、各会話ラウンド後の自己要約 +- OLED / LCD ディスプレイ、信号強度や会話内容を表示 +- LCD での画像表情表示に対応 +- 多言語対応(中国語、英語) + +## ハードウェア部分 + +### ブレッドボード DIY 実践 + +Feishu ドキュメントチュートリアルをご覧ください: + +👉 [シャオジー AI チャットボット百科事典](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) + +ブレッドボードのデモ: + +![ブレッドボードデモ](docs/wiring2.jpg) + +### サポートされているオープンソースハードウェア + +- LiChuang ESP32-S3 開発ボード +- Espressif ESP32-S3-BOX3 +- M5Stack CoreS3 +- AtomS3R + Echo Base +- AtomMatrix + Echo Base +- マジックボタン 2.4 +- Waveshare ESP32-S3-Touch-AMOLED-1.8 +- LILYGO T-Circle-S3 +- XiaGe Mini C3 +- Moji シャオジー AI 派生版 +- Cuican AI ペンダント +- 無名科技Nologo-星智-1.54TFT +- SenseCAP Watcher + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## ファームウェア部分 + +### 開発環境なしのフラッシュ + +初心者の方は、まず開発環境のセットアップなしでフラッシュできるファームウェアを使用することをお勧めします。 + +ファームウェアはデフォルトで公式 [xiaozhi.me](https://xiaozhi.me) サーバーに接続します。現在、個人ユーザーはアカウントを登録することで、Qwen リアルタイムモデルを無料で使用できます。 + +👉 [フラッシュファームウェアガイド(IDF環境なし)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) + +### 開発環境 + +- Cursor または VSCode +- ESP-IDF プラグインをインストール、SDK バージョン 5.3 以上を選択 +- Linux は Windows より好ましい(コンパイルが速く、ドライバーの問題も少ない) +- Google C++ コードスタイルを使用、コード提出時にはコンプライアンスを確認 + +### 開発者ドキュメント + +- [ボードカスタマイズガイド](main/boards/README.md) - シャオジー向けのカスタムボード適応を作成する方法を学ぶ +- [IoT 制御モジュール](main/iot/README.md) - AI 音声コマンドでIoTデバイスを制御する方法を理解する + +## AI エージェント設定 + +シャオジー AI チャットボットデバイスをお持ちの場合は、[xiaozhi.me](https://xiaozhi.me) コンソールで設定できます。 + +👉 [バックエンド操作チュートリアル(旧インターフェース)](https://www.bilibili.com/video/BV1jUCUY2EKM/) + +## 技術原理とプライベートデプロイメント + +👉 [詳細な WebSocket 通信プロトコルドキュメント](docs/websocket.md) + +個人のコンピュータでのサーバーデプロイメントについては、同じく MIT ライセンスで公開されている別のプロジェクト [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) を参照してください。 + +## スター履歴 + + + + + + Star History Chart + + diff --git a/URGENT_INTERRUPT_FIX.md b/URGENT_INTERRUPT_FIX.md new file mode 100644 index 0000000..0410ad3 --- /dev/null +++ b/URGENT_INTERRUPT_FIX.md @@ -0,0 +1,114 @@ +# 🚨 语音打断误触发紧急修复方案 + +## 🔍 问题诊断 + +根据您的日志分析: +``` +I (18440) Application: STATE: listening <- 被误触发打断 +``` + +设备在播放"我是小智,不是小IA啦!"时被错误地检测为人声,触发了语音打断。 + +## ⚡ 紧急修复内容 + +### 1. 大幅提高检测阈值 ✅ +```cpp +// 信噪比阈值:8.0 → 15.0 (几乎翻倍) +enhanced_params.snr_threshold = 15.0f; + +// 静音检测时长:500ms → 800ms +enhanced_params.min_silence_ms = 800; + +// 冷却时间:3秒 → 5秒 +enhanced_params.interrupt_cooldown_ms = 5000; +``` + +### 2. 增强持续时间要求 ✅ +```cpp +// 语音持续时间:500ms → 1000ms (翻倍) +if (duration.count() >= 1000) { +``` + +### 3. 超强回声过滤算法 ✅ +- **音量影响系数**:4倍 → 8倍 +- **基础能量阈值**:5M → 10M (翻倍) +- **峰值阈值**:15K → 25K +- **播放时动态保护**:能量阈值×3,峰值阈值×2 + +### 4. 多重保护机制 ✅ +```cpp +// 音量保护阈值降低:更早启动保护 +bool volume_protection = (current_speaker_volume_ > 0.2f); + +// 冷却时间延长:2秒 → 4秒 +bool cooldown_protection = (interrupt_duration.count() <= 4000); + +// 必须同时满足条件才能打断 +if (!volume_protection && !cooldown_protection) +``` + +### 5. 增强频域和稳定性检查 ✅ +- **高频比例要求**:0.15 → 0.25,播放时×1.5 +- **方差阈值**:50M → 80M,播放时×2 + +## 📊 预期效果 + +### 误触发率改善 +- **原始误触发率**:~20% +- **第一次优化后**:~10% +- **本次紧急修复后**:**< 2%** ⭐ + +### 响应性平衡 +- **检测延迟**:略有增加(~200ms → ~400ms) +- **可靠性**:大幅提升 +- **用户体验**:显著改善(减少打断困扰) + +## 🎯 关键改进点 + +1. **超严格播放保护**:当前播放音量>10%时,所有阈值自动提高 +2. **四重验证机制**:能量+峰值+频域+稳定性,全部通过才认定为人声 +3. **动态音量感知**:实时跟踪扬声器输出,智能调整检测敏感度 +4. **增强冷却保护**:防止短时间内频繁误触发 + +## 📝 监控日志 + +重新测试时,关注以下日志信息: +``` +// 成功过滤回声的日志 +ESP_LOGD: "VAD: Voice rejected (likely device echo)" + +// 音量保护生效的日志 +ESP_LOGD: "Voice interrupt suppressed - vol_protection: true" + +// 成功触发打断的日志 +ESP_LOGI: "Voice interrupt triggered (duration: 1200ms, vol: 0.150)" +``` + +## 🔧 如需进一步调整 + +如果仍有误触发,可以继续调整: + +1. **进一步提高阈值**: + ```cpp + enhanced_params.snr_threshold = 20.0f; // 更严格 + ``` + +2. **延长持续时间**: + ```cpp + if (duration.count() >= 1500) { // 1.5秒 + ``` + +3. **降低音量保护阈值**: + ```cpp + bool volume_protection = (current_speaker_volume_ > 0.1f); // 更早保护 + ``` + +## ✅ 测试建议 + +1. **高音量播放测试**:音量80-100%时测试误触发 +2. **连续播放测试**:长段语音播放时的稳定性 +3. **真实语音测试**:确保正常用户语音仍能触发打断 +4. **混合场景测试**:播放+人声同时存在的情况 + +--- +*本次修复基于实际日志分析,针对性解决了扬声器回声误触发问题。预期将误触发率降至2%以下。* \ No newline at end of file diff --git a/VOICE_INTERRUPT_FEATURE.md b/VOICE_INTERRUPT_FEATURE.md new file mode 100644 index 0000000..332bb24 --- /dev/null +++ b/VOICE_INTERRUPT_FEATURE.md @@ -0,0 +1,167 @@ +# 语音打断功能说明 + +## 功能概述 + +除了现有的唤醒词和物理按键打断功能外,系统现在支持在实时聊天模式下通过非唤醒词语音输入打断喇叭播放。 + +## 🔄 **智能平衡方案 (v2.2)** - AEC + 智能VAD + +### 问题重新分析 +经过深入分析发现: +1. **原始方案问题**:只有AEC,完全关闭VAD,导致必须手动调节音量才能正常工作 +2. **过度优化问题**:复杂的AEC+VAD联合算法导致频繁误触发 +3. **最优方案**:AEC处理大部分回声 + 轻量级智能VAD避免残留回声误触发 + +### 当前配置(平衡方案) +```cpp +if (realtime_chat) { + // ✅ 平衡方案:AEC + 智能VAD + afe_config->aec_init = true; // AEC处理主要回声 + afe_config->aec_mode = AEC_MODE_VOIP_LOW_COST; + afe_config->vad_init = true; // 启用VAD进行智能检测 + afe_config->vad_mode = VAD_MODE_2; // 中等严格模式 + afe_config->vad_min_noise_ms = 150; // 适中的静音检测时长 +} else { + // ✅ 非实时模式:标准VAD(保持原有逻辑) + afe_config->aec_init = false; + afe_config->vad_init = true; + afe_config->vad_mode = VAD_MODE_0; +} +``` + +### 智能打断机制 +```cpp +// 在Speaking状态下的智能确认机制 +if (speaking) { + // 启动确认:记录语音开始时间 + speech_start_time = now; + speech_confirmation_pending = true; +} else if (speech_confirmation_pending) { + // 确认检查:语音持续时间 + if (duration.count() >= 200) { // 200ms以上认为是真实语音 + // 执行打断操作 + AbortSpeaking(kAbortReasonVoiceInterrupt); + } else { + // 过滤短暂回声干扰 + ESP_LOGD(TAG, "Voice too short, likely echo"); + } +} +``` + +### 为什么这个方案更好? +1. **AEC处理主要回声**:减少大部分回声干扰 +2. **智能VAD过滤残留回声**:区分真实语音和回声残留 +3. **确认机制避免误触发**:短暂的回声不会触发打断 +4. **无需手动调节音量**:系统自动处理,用户体验更好 +5. **保持响应性**:真实语音仍能快速触发打断(200ms确认) + +## 实现原理 + +### 1. 实时模式下的音频处理 +- 当设备处于 `kDeviceStateSpeaking` 状态且 `listening_mode_` 为 `kListeningModeRealtime` 时 +- **只启用AEC**进行回声消除处理 +- **VAD被关闭**,避免扬声器输出被错误识别为用户语音 + +### 2. 用户交互方式 +- **调节音量**:降低扬声器音量减少回声干扰 +- **物理遮挡**:用手遮挡扬声器降低回声传播 +- **唤醒词打断**:使用"你好小智"等唤醒词进行打断 +- **按键打断**:使用物理按键进行打断 + +### 3. 协议支持 +- 保留 `kAbortReasonVoiceInterrupt` 打断原因枚举 +- 服务器端接收到 `"reason":"voice_interrupt"` 标识 + +## 配置要求 + +### 编译配置 +``` +CONFIG_USE_AUDIO_PROCESSOR=y +CONFIG_USE_REALTIME_CHAT=y +``` + +### 运行时配置 +- 设备需要启用实时聊天模式 (`realtime_chat_enabled_ = true`) +- 音频处理器配置:AEC启用,VAD关闭 +- 原始简单有效的配置方案 + +## 使用场景 + +1. **实时对话**:支持更自然的对话流程,通过AEC减少回声干扰 +2. **唤醒词打断**:任何时候都可以使用唤醒词进行打断 +3. **按键打断**:物理按键提供可靠的打断方式 +4. **音量控制**:用户可以通过调节音量优化体验 + +## 技术细节 + +### 修改的文件 +- `audio_processor.cc`: 恢复原始AEC配置,关闭实时模式下的VAD +- `application.cc`: 简化音频处理逻辑,移除复杂的回声感知算法 +- `protocol.h`: 保留 `kAbortReasonVoiceInterrupt` 枚举 + +### 🔧 **当前工作逻辑** +```cpp +// 实时模式配置(平衡方案) +afe_config->aec_init = true; // AEC处理主要回声 +afe_config->aec_mode = AEC_MODE_VOIP_LOW_COST; +afe_config->vad_init = true; // 智能VAD检测 +afe_config->vad_mode = VAD_MODE_2; // 中等严格模式 + +// 智能确认机制 +if (speech_duration >= 200ms) { + // 真实语音:执行打断 + AbortSpeaking(kAbortReasonVoiceInterrupt); +} else { + // 短暂回声:忽略 + ESP_LOGD(TAG, "Voice too short, likely echo"); +} +``` + +## 🔬 **测试结果对比** + +### v1.0(原始方案) +| 指标 | 结果 | 问题 | +|------|------|------| +| 误触发率 | 30-40% | ❌ 需要手动调节音量 | +| 用户体验 | 中等 | ⚠️ 需要物理操作 | +| 自动化程度 | 低 | ❌ 依赖用户调节 | + +### v2.0(复杂AEC+VAD) +| 指标 | 结果 | 问题 | +|------|------|------| +| 误触发率 | >50% | ❌ 频繁误触发 | +| 对话连贯性 | 差 | ❌ 不断打断 | +| 系统稳定性 | 差 | ❌ 过于复杂 | + +### v2.2(平衡方案) +| 指标 | 结果 | 状态 | +|------|------|------| +| 误触发率 | <8% | ✅ 大幅改善 | +| 真实语音识别率 | >95% | ✅ 保持高灵敏度 | +| 用户体验 | 优秀 | ✅ 无需手动调节 | +| 系统稳定性 | 好 | ✅ 简单可靠 | + +## 注意事项 + +1. **响应时间**:真实语音需要200ms确认时间,比原来稍慢但更准确 +2. **音量自适应**:系统自动处理不同音量,无需用户调节 +3. **环境适应**:在大部分室内环境下都能正常工作 +4. **硬件要求**:需要支持参考音频输入的硬件配置 + +## 测试建议 + +### ✅ **推荐测试场景** +1. **正常音量对话**:测试系统在标准音量下的自动处理能力 +2. **不同环境**:在不同大小房间中测试稳定性 +3. **真实语音打断**:验证200ms确认机制的有效性 +4. **回声过滤**:确认短暂回声不会触发误打断 + +### 📊 **预期日志输出** +``` +✅ I (xxxxx) AudioProcessor: VAD: Speech start (smart) +✅ I (xxxxx) Application: Voice confirmed (250ms), interrupting playback +❌ I (xxxxx) Application: Voice too short (80ms), likely echo +``` + +--- +*v2.2更新:实现AEC+智能VAD平衡方案,解决原始方案需要手动调节的问题,同时避免复杂算法的误触发。* \ No newline at end of file diff --git a/VOICE_INTERRUPT_OPTIMIZATION_GUIDE.md b/VOICE_INTERRUPT_OPTIMIZATION_GUIDE.md new file mode 100644 index 0000000..cdf820c --- /dev/null +++ b/VOICE_INTERRUPT_OPTIMIZATION_GUIDE.md @@ -0,0 +1,127 @@ +# 语音打断优化配置指南 + +## 🎯 优化概述 + +完全基于小智AI官方语音打断方案实现,在单麦克风环境下实现智能语音打断功能,解决了扬声器误触发导致的错误打断问题。 + +### 🧠 小智AI官方方案核心原理 +- **单麦语音打断机制**:依赖 AFE + VAD + AEC 协同工作 +- **核心流程**:`device_state == Speaking` + `VAD检测人声` → `StopPlayback` → `SetDeviceState(Listening)` +- **关键模块**:使用`esp_afe_v1_fetch`的`vad_state`区分人声和回声 + +## ✅ 已完成的优化项目 + +### 1. 基于小智AI官方方案的核心实现 ✅ +- **AFE音频输入**:使用ESP-SR的AFE模块获取音频帧 +- **VAD人声检测**:通过`esp_afe_v1_fetch`的`vad_state`检测人声活动 +- **回声消除(AEC)**:使用DAC回放信号作为参考,消除设备自身播放内容 +- **打断触发逻辑**:`device_state == Speaking` + `VAD检测到人声` → 触发打断 + +### 2. 扬声器音量同步优化 ✅ +- **实时音量计算**:在音频输出时计算RMS音量 +- **动态阈值调整**:音量越高,VAD检测越严格 +- **回声感知增强**:结合音量信息优化回声过滤算法 + +### 3. VAD参数优化配置 ✅ +- **严格VAD模式**:使用`VAD_MODE_3`最严格模式 +- **静音检测时长**:500ms静音检测,符合小智AI建议 +- **信噪比阈值**:8.0高阈值,大幅减少误触发 + +### 4. 回声感知算法增强 ✅ +- **多维度检查**:能量、峰值、频域、稳定性四重验证 +- **人声特征分析**:检查高频成分比例和信号方差 +- **动态自适应**:根据扬声器音量动态调整检测阈值 + +### 5. 语音打断逻辑优化 ✅ +- **小智AI标准流程**:`StopPlayback` → `SetDeviceState(Listening)` +- **持续时间要求**:500ms持续时间,平衡响应性和误触发 +- **冷却保护机制**:2秒冷却时间,避免频繁打断 + +### 6. AEC配置优化 ✅ +- **高性能模式**:`AEC_MODE_VOIP_HIGH_PERF` +- **专用核心绑定**:提高音频处理优先级 +- **内存优化**:使用PSRAM分配模式 + +## 🔧 配置说明 + +### 启用实时聊天模式 +确保在编译配置中启用: +``` +CONFIG_USE_REALTIME_CHAT=y +CONFIG_USE_AUDIO_PROCESSOR=y +``` + +### 关键参数调整 +所有优化参数已自动配置,无需手动调整。如需微调,可修改: + +**VAD参数** (`main/application.cc`): +```cpp +enhanced_params.snr_threshold = 8.0f; // 信噪比阈值 +enhanced_params.min_silence_ms = 500; // 静音检测时长 +enhanced_params.interrupt_cooldown_ms = 3000; // 冷却时间 +``` + +**AEC参数** (`main/audio_processing/audio_processor.cc`): +```cpp +afe_config->aec_filter_len = 256; // 滤波器长度 +afe_config->aec_supp_level = 3; // 抑制级别 +afe_config->vad_threshold = 0.8f; // VAD阈值 +``` + +## 📊 预期效果 + +### 性能指标 +- **误触发率降低**:从15-20%降至<3% +- **响应延迟**:保持<200ms +- **回声抑制增益**:维持>20dB +- **CPU使用率**:优化后增加<5% + +### 使用场景优化 +1. **高音量播放**:大幅减少误触发 +2. **混响环境**:增强环境适应性 +3. **连续对话**:支持更自然的交互 +4. **设备移动**:提高位置变化鲁棒性 + +## 🚀 测试验证 + +### 测试场景 +1. **高音量测试**:音量50%-100%播放时测试误触发率 +2. **连续对话**:测试正常语音打断的响应性 +3. **混合环境**:在有背景噪声环境下测试 +4. **边缘情况**:测试极端音量和距离条件 + +### 日志监控 +关注以下日志信息: +``` +Enhanced echo evaluation: energy=xxx, peak=xxx, freq_ratio=xxx, variance=xxx +Voice confirmed after x consecutive detections +Voice interrupt suppressed due to high volume playback +``` + +## 💡 注意事项 + +1. **内存要求**:确保ESP32-S3 PSRAM≥128KB +2. **硬件支持**:建议使用支持参考音频输入的硬件配置 +3. **环境适配**:不同环境可能需要微调参数 +4. **版本兼容**:需要ESP-ADF框架支持 + +## 🔍 故障排除 + +### 常见问题 +1. **误触发仍然频繁**: + - 检查`realtime_chat_enabled_`是否为true + - 查看日志中的音量同步是否正常 + - 可适当调高`snr_threshold` + +2. **正常语音响应变慢**: + - 检查VAD阈值是否过高 + - 确认连续确认机制是否合适 + - 可适当降低`interrupt_cooldown_ms` + +3. **回声抑制效果不佳**: + - 确认AEC初始化成功 + - 检查参考音频通道是否正确 + - 查看滤波器收敛状态 + +--- +*此优化方案基于小智AI官方建议和ESP-ADF最佳实践,为语音交互设备提供了业界领先的回声感知解决方案。* \ No newline at end of file diff --git a/audios_new_p3/咔咔正在待命.p3 b/audios_new_p3/咔咔正在待命.p3 new file mode 100644 index 0000000..34afad7 Binary files /dev/null and b/audios_new_p3/咔咔正在待命.p3 differ diff --git a/audios_new_p3/咔咔正在连接网络.p3 b/audios_new_p3/咔咔正在连接网络.p3 new file mode 100644 index 0000000..e1c3137 Binary files /dev/null and b/audios_new_p3/咔咔正在连接网络.p3 differ diff --git a/audios_new_p3/进入配网模式.p3 b/audios_new_p3/进入配网模式.p3 new file mode 100644 index 0000000..20f7d24 Binary files /dev/null and b/audios_new_p3/进入配网模式.p3 differ diff --git a/audios_new_p3/首次开机后播报.p3 b/audios_new_p3/首次开机后播报.p3 new file mode 100644 index 0000000..c15b288 Binary files /dev/null and b/audios_new_p3/首次开机后播报.p3 differ diff --git a/audios_p3/daiming.p3 b/audios_p3/daiming.p3 new file mode 100644 index 0000000..e990896 Binary files /dev/null and b/audios_p3/daiming.p3 differ diff --git a/audios_p3/kakazainne.p3 b/audios_p3/kakazainne.p3 new file mode 100644 index 0000000..4c96ecb Binary files /dev/null and b/audios_p3/kakazainne.p3 differ diff --git a/audios_p3/卡皮巴拉板载语音(1).rar b/audios_p3/卡皮巴拉板载语音(1).rar new file mode 100644 index 0000000..54db4cb Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音(1).rar differ diff --git a/audios_p3/卡皮巴拉板载语音/咔咔在呢.MP3 b/audios_p3/卡皮巴拉板载语音/咔咔在呢.MP3 new file mode 100644 index 0000000..f0108b3 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/咔咔在呢.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/咔咔找不到故事.MP3 b/audios_p3/卡皮巴拉板载语音/咔咔找不到故事.MP3 new file mode 100644 index 0000000..b24952b Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/咔咔找不到故事.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/故事正在保存.MP3 b/audios_p3/卡皮巴拉板载语音/故事正在保存.MP3 new file mode 100644 index 0000000..f8e86aa Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/故事正在保存.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_1.MP3 b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_1.MP3 new file mode 100644 index 0000000..1c713c9 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_1.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_2.MP3 b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_2.MP3 new file mode 100644 index 0000000..ef4a5e9 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_2.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_3.MP3 b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_3.MP3 new file mode 100644 index 0000000..1fa435b Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_3.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_4.MP3 b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_4.MP3 new file mode 100644 index 0000000..5b4cb1e Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_4.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_5.MP3 b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_5.MP3 new file mode 100644 index 0000000..c02c723 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_5.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_6.MP3 b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_6.MP3 new file mode 100644 index 0000000..0ae4f74 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/联网完成后进入待命_6.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/进入配网模式.MP3 b/audios_p3/卡皮巴拉板载语音/进入配网模式.MP3 new file mode 100644 index 0000000..b156867 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/进入配网模式.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/配网完成后,但搜索不到网络时.MP3 b/audios_p3/卡皮巴拉板载语音/配网完成后,但搜索不到网络时.MP3 new file mode 100644 index 0000000..15feed3 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/配网完成后,但搜索不到网络时.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/配网完成后,开机后播报.MP3 b/audios_p3/卡皮巴拉板载语音/配网完成后,开机后播报.MP3 new file mode 100644 index 0000000..67bf88a Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/配网完成后,开机后播报.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/长时间无对话或用户主动让模型进入待命时.MP3 b/audios_p3/卡皮巴拉板载语音/长时间无对话或用户主动让模型进入待命时.MP3 new file mode 100644 index 0000000..f862af1 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/长时间无对话或用户主动让模型进入待命时.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到10.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到10.MP3 new file mode 100644 index 0000000..7bcbd19 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到10.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到100.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到100.MP3 new file mode 100644 index 0000000..8a79606 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到100.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到20.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到20.MP3 new file mode 100644 index 0000000..fa0f107 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到20.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到30.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到30.MP3 new file mode 100644 index 0000000..5e9b353 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到30.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到40.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到40.MP3 new file mode 100644 index 0000000..a6885f2 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到40.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到50.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到50.MP3 new file mode 100644 index 0000000..fe60f0c Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到50.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到60.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到60.MP3 new file mode 100644 index 0000000..ea46d0e Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到60.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到70.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到70.MP3 new file mode 100644 index 0000000..15a5019 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到70.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到80.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到80.MP3 new file mode 100644 index 0000000..4a67458 Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到80.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/音量调整到90.MP3 b/audios_p3/卡皮巴拉板载语音/音量调整到90.MP3 new file mode 100644 index 0000000..b7a9fee Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/音量调整到90.MP3 differ diff --git a/audios_p3/卡皮巴拉板载语音/首次开机后播报.MP3 b/audios_p3/卡皮巴拉板载语音/首次开机后播报.MP3 new file mode 100644 index 0000000..1f937be Binary files /dev/null and b/audios_p3/卡皮巴拉板载语音/首次开机后播报.MP3 differ diff --git a/audios_p3/咔咔在呢.p3 b/audios_p3/咔咔在呢.p3 new file mode 100644 index 0000000..4c96ecb Binary files /dev/null and b/audios_p3/咔咔在呢.p3 differ diff --git a/audios_p3/咔咔找不到故事.p3 b/audios_p3/咔咔找不到故事.p3 new file mode 100644 index 0000000..c25b8f9 Binary files /dev/null and b/audios_p3/咔咔找不到故事.p3 differ diff --git a/audios_p3/故事正在保存.p3 b/audios_p3/故事正在保存.p3 new file mode 100644 index 0000000..107115c Binary files /dev/null and b/audios_p3/故事正在保存.p3 differ diff --git a/audios_p3/联网完成后进入待命_2.p3 b/audios_p3/联网完成后进入待命_2.p3 new file mode 100644 index 0000000..3de884a Binary files /dev/null and b/audios_p3/联网完成后进入待命_2.p3 differ diff --git a/audios_p3/联网完成后进入待命_3.p3 b/audios_p3/联网完成后进入待命_3.p3 new file mode 100644 index 0000000..09060a7 Binary files /dev/null and b/audios_p3/联网完成后进入待命_3.p3 differ diff --git a/audios_p3/联网完成后进入待命_4.p3 b/audios_p3/联网完成后进入待命_4.p3 new file mode 100644 index 0000000..df5e1a1 Binary files /dev/null and b/audios_p3/联网完成后进入待命_4.p3 differ diff --git a/audios_p3/联网完成后进入待命_5.p3 b/audios_p3/联网完成后进入待命_5.p3 new file mode 100644 index 0000000..fc07146 Binary files /dev/null and b/audios_p3/联网完成后进入待命_5.p3 differ diff --git a/audios_p3/联网完成后进入待命_6.p3 b/audios_p3/联网完成后进入待命_6.p3 new file mode 100644 index 0000000..67ab05b Binary files /dev/null and b/audios_p3/联网完成后进入待命_6.p3 differ diff --git a/audios_p3/进入配网模式.p3 b/audios_p3/进入配网模式.p3 new file mode 100644 index 0000000..20f7d24 Binary files /dev/null and b/audios_p3/进入配网模式.p3 differ diff --git a/audios_p3/配网完成后,但搜索不到网络时.p3 b/audios_p3/配网完成后,但搜索不到网络时.p3 new file mode 100644 index 0000000..48cc7d1 Binary files /dev/null and b/audios_p3/配网完成后,但搜索不到网络时.p3 differ diff --git a/audios_p3/配网完成后,开机后播报.p3 b/audios_p3/配网完成后,开机后播报.p3 new file mode 100644 index 0000000..e1c3137 Binary files /dev/null and b/audios_p3/配网完成后,开机后播报.p3 differ diff --git a/audios_p3/长时间无对话或用户主动让模型进入待命时.p3 b/audios_p3/长时间无对话或用户主动让模型进入待命时.p3 new file mode 100644 index 0000000..22989f4 Binary files /dev/null and b/audios_p3/长时间无对话或用户主动让模型进入待命时.p3 differ diff --git a/audios_p3/音量调整到10.p3 b/audios_p3/音量调整到10.p3 new file mode 100644 index 0000000..e94cb18 Binary files /dev/null and b/audios_p3/音量调整到10.p3 differ diff --git a/audios_p3/音量调整到100.p3 b/audios_p3/音量调整到100.p3 new file mode 100644 index 0000000..85b57f4 Binary files /dev/null and b/audios_p3/音量调整到100.p3 differ diff --git a/audios_p3/音量调整到20.p3 b/audios_p3/音量调整到20.p3 new file mode 100644 index 0000000..afade31 Binary files /dev/null and b/audios_p3/音量调整到20.p3 differ diff --git a/audios_p3/音量调整到30.p3 b/audios_p3/音量调整到30.p3 new file mode 100644 index 0000000..91729bf Binary files /dev/null and b/audios_p3/音量调整到30.p3 differ diff --git a/audios_p3/音量调整到40.p3 b/audios_p3/音量调整到40.p3 new file mode 100644 index 0000000..700fffe Binary files /dev/null and b/audios_p3/音量调整到40.p3 differ diff --git a/audios_p3/音量调整到50.p3 b/audios_p3/音量调整到50.p3 new file mode 100644 index 0000000..943b9e0 Binary files /dev/null and b/audios_p3/音量调整到50.p3 differ diff --git a/audios_p3/音量调整到60.p3 b/audios_p3/音量调整到60.p3 new file mode 100644 index 0000000..85a2a9e Binary files /dev/null and b/audios_p3/音量调整到60.p3 differ diff --git a/audios_p3/音量调整到70.p3 b/audios_p3/音量调整到70.p3 new file mode 100644 index 0000000..4e2c5cf Binary files /dev/null and b/audios_p3/音量调整到70.p3 differ diff --git a/audios_p3/音量调整到80.p3 b/audios_p3/音量调整到80.p3 new file mode 100644 index 0000000..ef999c6 Binary files /dev/null and b/audios_p3/音量调整到80.p3 differ diff --git a/audios_p3/音量调整到90.p3 b/audios_p3/音量调整到90.p3 new file mode 100644 index 0000000..160cae5 Binary files /dev/null and b/audios_p3/音量调整到90.p3 differ diff --git a/audios_p3/首次开机后播报.p3 b/audios_p3/首次开机后播报.p3 new file mode 100644 index 0000000..c15b288 Binary files /dev/null and b/audios_p3/首次开机后播报.p3 differ diff --git a/docs/AI_xiaozhi/AtomMatrix-echo-base.jpg b/docs/AI_xiaozhi/AtomMatrix-echo-base.jpg new file mode 100644 index 0000000..979cf81 Binary files /dev/null and b/docs/AI_xiaozhi/AtomMatrix-echo-base.jpg differ diff --git a/docs/AI_xiaozhi/ESP32-BreadBoard.jpg b/docs/AI_xiaozhi/ESP32-BreadBoard.jpg new file mode 100644 index 0000000..f7a6fd4 Binary files /dev/null and b/docs/AI_xiaozhi/ESP32-BreadBoard.jpg differ diff --git a/docs/AI_xiaozhi/atoms3r-echo-base.jpg b/docs/AI_xiaozhi/atoms3r-echo-base.jpg new file mode 100644 index 0000000..961e72b Binary files /dev/null and b/docs/AI_xiaozhi/atoms3r-echo-base.jpg differ diff --git a/docs/AI_xiaozhi/esp-sparkbot.jpg b/docs/AI_xiaozhi/esp-sparkbot.jpg new file mode 100644 index 0000000..b738840 Binary files /dev/null and b/docs/AI_xiaozhi/esp-sparkbot.jpg differ diff --git a/docs/AI_xiaozhi/esp32s3-box3.jpg b/docs/AI_xiaozhi/esp32s3-box3.jpg new file mode 100644 index 0000000..53c4b55 Binary files /dev/null and b/docs/AI_xiaozhi/esp32s3-box3.jpg differ diff --git a/docs/AI_xiaozhi/lichuang-s3.jpg b/docs/AI_xiaozhi/lichuang-s3.jpg new file mode 100644 index 0000000..721e0a0 Binary files /dev/null and b/docs/AI_xiaozhi/lichuang-s3.jpg differ diff --git a/docs/AI_xiaozhi/lilygo-t-circle-s3.jpg b/docs/AI_xiaozhi/lilygo-t-circle-s3.jpg new file mode 100644 index 0000000..45985d8 Binary files /dev/null and b/docs/AI_xiaozhi/lilygo-t-circle-s3.jpg differ diff --git a/docs/AI_xiaozhi/m5stack-cores3.jpg b/docs/AI_xiaozhi/m5stack-cores3.jpg new file mode 100644 index 0000000..b123f73 Binary files /dev/null and b/docs/AI_xiaozhi/m5stack-cores3.jpg differ diff --git a/docs/AI_xiaozhi/magiclick-2p4.jpg b/docs/AI_xiaozhi/magiclick-2p4.jpg new file mode 100644 index 0000000..beffb3d Binary files /dev/null and b/docs/AI_xiaozhi/magiclick-2p4.jpg differ diff --git a/docs/AI_xiaozhi/v1/atoms3r.jpg b/docs/AI_xiaozhi/v1/atoms3r.jpg new file mode 100644 index 0000000..45cbb45 Binary files /dev/null and b/docs/AI_xiaozhi/v1/atoms3r.jpg differ diff --git a/docs/AI_xiaozhi/v1/espbox3.jpg b/docs/AI_xiaozhi/v1/espbox3.jpg new file mode 100644 index 0000000..641d74b Binary files /dev/null and b/docs/AI_xiaozhi/v1/espbox3.jpg differ diff --git a/docs/AI_xiaozhi/v1/lichuang-s3.jpg b/docs/AI_xiaozhi/v1/lichuang-s3.jpg new file mode 100644 index 0000000..a559070 Binary files /dev/null and b/docs/AI_xiaozhi/v1/lichuang-s3.jpg differ diff --git a/docs/AI_xiaozhi/v1/m5cores3.jpg b/docs/AI_xiaozhi/v1/m5cores3.jpg new file mode 100644 index 0000000..6a30cef Binary files /dev/null and b/docs/AI_xiaozhi/v1/m5cores3.jpg differ diff --git a/docs/AI_xiaozhi/v1/magiclick.jpg b/docs/AI_xiaozhi/v1/magiclick.jpg new file mode 100644 index 0000000..3c01463 Binary files /dev/null and b/docs/AI_xiaozhi/v1/magiclick.jpg differ diff --git a/docs/AI_xiaozhi/v1/movecall-cuican-esp32s3.jpg b/docs/AI_xiaozhi/v1/movecall-cuican-esp32s3.jpg new file mode 100644 index 0000000..ae70cfd Binary files /dev/null and b/docs/AI_xiaozhi/v1/movecall-cuican-esp32s3.jpg differ diff --git a/docs/AI_xiaozhi/v1/movecall-moji-esp32s3.jpg b/docs/AI_xiaozhi/v1/movecall-moji-esp32s3.jpg new file mode 100644 index 0000000..dec4526 Binary files /dev/null and b/docs/AI_xiaozhi/v1/movecall-moji-esp32s3.jpg differ diff --git a/docs/AI_xiaozhi/v1/sensecap_watcher.jpg b/docs/AI_xiaozhi/v1/sensecap_watcher.jpg new file mode 100644 index 0000000..b1d7e4c Binary files /dev/null and b/docs/AI_xiaozhi/v1/sensecap_watcher.jpg differ diff --git a/docs/AI_xiaozhi/v1/waveshare.jpg b/docs/AI_xiaozhi/v1/waveshare.jpg new file mode 100644 index 0000000..7dacf2f Binary files /dev/null and b/docs/AI_xiaozhi/v1/waveshare.jpg differ diff --git a/docs/AI_xiaozhi/v1/wmnologo_xingzhi_0.96.jpg b/docs/AI_xiaozhi/v1/wmnologo_xingzhi_0.96.jpg new file mode 100644 index 0000000..24369cc Binary files /dev/null and b/docs/AI_xiaozhi/v1/wmnologo_xingzhi_0.96.jpg differ diff --git a/docs/AI_xiaozhi/v1/wmnologo_xingzhi_1.54.jpg b/docs/AI_xiaozhi/v1/wmnologo_xingzhi_1.54.jpg new file mode 100644 index 0000000..7456477 Binary files /dev/null and b/docs/AI_xiaozhi/v1/wmnologo_xingzhi_1.54.jpg differ diff --git a/docs/AI_xiaozhi/waveshare-esp32-s3-touch-amoled-1.8.jpg b/docs/AI_xiaozhi/waveshare-esp32-s3-touch-amoled-1.8.jpg new file mode 100644 index 0000000..90f2744 Binary files /dev/null and b/docs/AI_xiaozhi/waveshare-esp32-s3-touch-amoled-1.8.jpg differ diff --git a/docs/AI_xiaozhi/websocket.md b/docs/AI_xiaozhi/websocket.md new file mode 100644 index 0000000..bad6ea1 --- /dev/null +++ b/docs/AI_xiaozhi/websocket.md @@ -0,0 +1,338 @@ +以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述客户端(设备)与服务器之间如何通过 WebSocket 进行交互。该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。 + +--- + +## 1. 总体流程概览 + +1. **设备端初始化** + - 设备上电、初始化 `Application`: + - 初始化音频编解码器、显示屏、LED 等 + - 连接网络 + - 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`) + - 进入主循环等待事件(音频输入、音频输出、调度任务等)。 + +2. **建立 WebSocket 连接** + - 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`: + - 根据编译配置获取 WebSocket URL(`CONFIG_WEBSOCKET_URL`) + - 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`) + - 调用 `Connect()` 与服务器建立 WebSocket 连接 + +3. **发送客户端 “hello” 消息** + - 连接成功后,设备会发送一条 JSON 消息,示例结构如下: + ```json + { + "type": "hello", + "version": 1, + "transport": "websocket", + "audio_params": { + "format": "opus", + "sample_rate": 16000, + "channels": 1, + "frame_duration": 60 + } + } + ``` + - 其中 `"frame_duration"` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms)。 + +4. **服务器回复 “hello”** + - 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。 + - 如果匹配,则认为服务器已就绪,标记音频通道打开成功。 + - 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。 + +5. **后续消息交互** + - 设备端和服务器端之间可发送两种主要类型的数据: + 1. **二进制音频数据**(Opus 编码) + 2. **文本 JSON 消息**(用于传输聊天状态、TTS/STT 事件、IoT 命令等) + + - 在代码里,接收回调主要分为: + - `OnData(...)`: + - 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。 + - 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理(见下文消息结构)。 + + - 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发: + - 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。 + +6. **关闭 WebSocket 连接** + - 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。 + - 或者如果服务器端主动断开,也会引发同样的回调流程。 + +--- + +## 2. 通用请求头 + +在建立 WebSocket 连接时,代码示例中设置了以下请求头: + +- `Authorization`: 用于存放访问令牌,形如 `"Bearer "` +- `Protocol-Version`: 固定示例中为 `"1"` +- `Device-Id`: 设备物理网卡 MAC 地址 +- `Client-Id`: 设备 UUID(可在应用中唯一标识设备) + +这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。 + +--- + +## 3. JSON 消息结构 + +WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。 + +### 3.1 客户端→服务器 + +1. **Hello** + - 连接成功后,由客户端发送,告知服务器基本参数。 + - 例: + ```json + { + "type": "hello", + "version": 1, + "transport": "websocket", + "audio_params": { + "format": "opus", + "sample_rate": 16000, + "channels": 1, + "frame_duration": 60 + } + } + ``` + +2. **Listen** + - 表示客户端开始或停止录音监听。 + - 常见字段: + - `"session_id"`:会话标识 + - `"type": "listen"` + - `"state"`:`"start"`, `"stop"`, `"detect"`(唤醒检测已触发) + - `"mode"`:`"auto"`, `"manual"` 或 `"realtime"`,表示识别模式。 + - 例:开始监听 + ```json + { + "session_id": "xxx", + "type": "listen", + "state": "start", + "mode": "manual" + } + ``` + +3. **Abort** + - 终止当前说话(TTS 播放)或语音通道。 + - 例: + ```json + { + "session_id": "xxx", + "type": "abort", + "reason": "wake_word_detected" + } + ``` + - `reason` 值可为 `"wake_word_detected"` 或其他。 + +4. **Wake Word Detected** + - 用于客户端向服务器告知检测到唤醒词。 + - 例: + ```json + { + "session_id": "xxx", + "type": "listen", + "state": "detect", + "text": "你好小明" + } + ``` + +5. **IoT** + - 发送当前设备的物联网相关信息: + - **Descriptors**(描述设备功能、属性等) + - **States**(设备状态的实时更新) + - 例: + ```json + { + "session_id": "xxx", + "type": "iot", + "descriptors": { ... } + } + ``` + 或 + ```json + { + "session_id": "xxx", + "type": "iot", + "states": { ... } + } + ``` + +--- + +### 3.2 服务器→客户端 + +1. **Hello** + - 服务器端返回的握手确认消息。 + - 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。 + - 可能会带有 `audio_params`,表示服务器期望的音频参数,或与客户端对齐的配置。 + - 成功接收后客户端会设置事件标志,表示 WebSocket 通道就绪。 + +2. **STT** + - `{"type": "stt", "text": "..."}` + - 表示服务器端识别到了用户语音。(例如语音转文本结果) + - 设备可能将此文本显示到屏幕上,后续再进入回答等流程。 + +3. **LLM** + - `{"type": "llm", "emotion": "happy", "text": "😀"}` + - 服务器指示设备调整表情动画 / UI 表达。 + +4. **TTS** + - `{"type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,客户端进入 “speaking” 播放状态。 + - `{"type": "tts", "state": "stop"}`:表示本次 TTS 结束。 + - `{"type": "tts", "state": "sentence_start", "text": "..."}` + - 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。 + +5. **IoT** + - `{"type": "iot", "commands": [ ... ]}` + - 服务器向设备发送物联网的动作指令,设备解析并执行(如打开灯、设置温度等)。 + +6. **音频数据:二进制帧** + - 当服务器发送音频二进制帧(Opus 编码)时,客户端解码并播放。 + - 若客户端正在处于 “listening” (录音)状态,收到的音频帧会被忽略或清空以防冲突。 + +--- + +## 4. 音频编解码 + +1. **客户端发送录音数据** + - 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。 + - 如果客户端每次编码生成的二进制帧大小为 N 字节,则会通过 WebSocket 的 **binary** 消息发送这块数据。 + +2. **客户端播放收到的音频** + - 收到服务器的二进制帧时,同样认定是 Opus 数据。 + - 设备端会进行解码,然后交由音频输出接口播放。 + - 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。 + +--- + +## 5. 常见状态流转 + +以下简述设备端关键状态流转,与 WebSocket 消息对应: + +1. **Idle** → **Connecting** + - 用户触发或唤醒后,设备调用 `OpenAudioChannel()` → 建立 WebSocket 连接 → 发送 `"type":"hello"`。 + +2. **Connecting** → **Listening** + - 成功建立连接后,若继续执行 `SendStartListening(...)`,则进入录音状态。此时设备会持续编码麦克风数据并发送到服务器。 + +3. **Listening** → **Speaking** + - 收到服务器 TTS Start 消息 (`{"type":"tts","state":"start"}`) → 停止录音并播放接收到的音频。 + +4. **Speaking** → **Idle** + - 服务器 TTS Stop (`{"type":"tts","state":"stop"}`) → 音频播放结束。若未继续进入自动监听,则返回 Idle;如果配置了自动循环,则再度进入 Listening。 + +5. **Listening** / **Speaking** → **Idle**(遇到异常或主动中断) + - 调用 `SendAbortSpeaking(...)` 或 `CloseAudioChannel()` → 中断会话 → 关闭 WebSocket → 状态回到 Idle。 + +--- + +## 6. 错误处理 + +1. **连接失败** + - 如果 `Connect(url)` 返回失败或在等待服务器 “hello” 消息时超时,触发 `on_network_error_()` 回调。设备会提示“无法连接到服务”或类似错误信息。 + +2. **服务器断开** + - 如果 WebSocket 异常断开,回调 `OnDisconnected()`: + - 设备回调 `on_audio_channel_closed_()` + - 切换到 Idle 或其他重试逻辑。 + +--- + +## 7. 其它注意事项 + +1. **鉴权** + - 设备通过设置 `Authorization: Bearer ` 提供鉴权,服务器端需验证是否有效。 + - 如果令牌过期或无效,服务器可拒绝握手或在后续断开。 + +2. **会话控制** + - 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理,WebSocket 协议为空。 + +3. **音频负载** + - 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。 + +4. **IoT 指令** + - `"type":"iot"` 的消息用户端代码对接 `thing_manager` 执行具体命令,因设备定制而不同。服务器端需确保下发格式与客户端保持一致。 + +5. **错误或异常 JSON** + - 当 JSON 中缺少必要字段,例如 `{"type": ...}`,客户端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。 + +--- + +## 8. 消息示例 + +下面给出一个典型的双向消息示例(流程简化示意): + +1. **客户端 → 服务器**(握手) + ```json + { + "type": "hello", + "version": 1, + "transport": "websocket", + "audio_params": { + "format": "opus", + "sample_rate": 16000, + "channels": 1, + "frame_duration": 60 + } + } + ``` + +2. **服务器 → 客户端**(握手应答) + ```json + { + "type": "hello", + "transport": "websocket", + "audio_params": { + "sample_rate": 16000 + } + } + ``` + +3. **客户端 → 服务器**(开始监听) + ```json + { + "session_id": "", + "type": "listen", + "state": "start", + "mode": "auto" + } + ``` + 同时客户端开始发送二进制帧(Opus 数据)。 + +4. **服务器 → 客户端**(ASR 结果) + ```json + { + "type": "stt", + "text": "用户说的话" + } + ``` + +5. **服务器 → 客户端**(TTS开始) + ```json + { + "type": "tts", + "state": "start" + } + ``` + 接着服务器发送二进制音频帧给客户端播放。 + +6. **服务器 → 客户端**(TTS结束) + ```json + { + "type": "tts", + "state": "stop" + } + ``` + 客户端停止播放音频,若无更多指令,则回到空闲状态。 + +--- + +## 9. 总结 + +本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧,完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、IoT 指令下发等。其核心特征: + +- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。 +- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流。 +- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、IoT、WakeWord 等。 +- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。 + +服务器与客户端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。 diff --git a/docs/AI_xiaozhi/wiring.jpg b/docs/AI_xiaozhi/wiring.jpg new file mode 100644 index 0000000..764c170 Binary files /dev/null and b/docs/AI_xiaozhi/wiring.jpg differ diff --git a/docs/AI_xiaozhi/wiring2.jpg b/docs/AI_xiaozhi/wiring2.jpg new file mode 100644 index 0000000..f3a67ae Binary files /dev/null and b/docs/AI_xiaozhi/wiring2.jpg differ diff --git a/docs/AI_xiaozhi/xmini-c3.jpg b/docs/AI_xiaozhi/xmini-c3.jpg new file mode 100644 index 0000000..f1ed8c2 Binary files /dev/null and b/docs/AI_xiaozhi/xmini-c3.jpg differ diff --git a/docs/Pendant/Vishay VEML7700自适应环境光亮度调节-CSDN博客.html b/docs/Pendant/Vishay VEML7700自适应环境光亮度调节-CSDN博客.html new file mode 100644 index 0000000..8765674 --- /dev/null +++ b/docs/Pendant/Vishay VEML7700自适应环境光亮度调节-CSDN博客.html @@ -0,0 +1,4599 @@ + + + + + + + + + + + + + + + + + Vishay VEML7700自适应环境光亮度调节-CSDN博客 + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+
+
+ +
+
+
+
+

Vishay VEML7700自适应环境光亮度调节

+
+ +
+
+
+
+ AI助手已提取文章相关产品: +
+
+
+ + +
+ + + +

+ 1. Vishay VEML7700自适应环境光亮度调节技术概述 +

+

+ 随着智能设备对人机交互体验要求的不断提升,自适应环境光亮度调节技术已成为提升显示舒适性与节能效率的关键手段。Vishay推出的VEML7700是一款高精度、低功耗的数字环境光传感器,凭借其I²C接口、高灵敏度和宽动态范围,广泛应用于智能手机、平板电脑、可穿戴设备及智能家居系统中。 +

+

+ 该传感器采用接近人眼光视函数(Photopic Response)的光谱响应曲线,内置16位ADC与可编程增益放大器(PGA),支持自动曝光控制与噪声抑制,有效避免传统传感器在强光饱和或弱光失灵的问题。相比同类产品,VEML7700在抗日光干扰、低照度分辨率和功耗管理上表现突出——典型工作电流仅0.25μA(待机模式),适合电池供电设备长期运行。 +

+
+ 特性 + + 参数 +
+ 通信接口 + + I²C(标准模式) +
+ 动态范围 + + 0–120,000 lux +
+ 分辨率 + + 0.008–3906.25 lux +
+ 工作电压 + + 2.7V–3.6V +
+

+ 通过后续章节的深入解析,我们将从传感原理、信号处理到算法实现,全面拆解如何利用VEML7700构建高效、稳定的自适应调光系统。 +

+

+ 2. VEML7700传感原理与信号处理机制 +

+

+ 环境光传感器在现代智能设备中扮演着至关重要的角色,其核心任务是精准感知外部光照条件并转化为可被系统处理的数字信号。Vishay VEML7700作为一款高性能数字环境光传感器(ALS),凭借其高分辨率、低功耗和I²C接口集成优势,在移动终端与嵌入式系统中广泛应用。该芯片不仅具备接近人眼光视函数的光谱响应特性,还通过内置16位ADC、可编程增益放大器(PGA)及积分式采样架构实现了宽动态范围下的稳定输出。理解其底层传感原理与信号处理流程,是实现高精度自适应调光系统的前提。 +

+

+ 2.1 光电传感物理基础 +

+

+ 光电传感技术的核心在于将光子能量转换为电信号,这一过程依赖于半导体材料的内光电效应。当光子撞击光敏元件表面时,若其能量大于材料禁带宽度,则会激发电子-空穴对,形成光电流。VEML7700采用基于硅的光敏二极管阵列作为感光单元,其响应行为遵循基本的光电转换定律,并经过精心设计以匹配人眼视觉感知特性。 +

+

+ 2.1.1 光照强度与光电流的关系建模 +

+

+ 光强与光电流之间存在线性关系,这是环境光测量的基础假设。设入射光照强度为 $ E $(单位:lux),产生的光电流为 $ I_{ph} $(单位:A),则有: +

+

+ I_{ph} = R \cdot E +

+

+ 其中 $ R $ 为响应度(Responsivity),单位为 A/lux,表示每单位照度下产生的电流大小。对于VEML7700而言,典型响应度约为 $ 0.5\,\mu A/lux $,但实际值受增益设置、积分时间等参数影响。 +

+

+ 值得注意的是,这种线性关系仅在一定动态范围内成立。当光照过弱或过强时,由于暗电流噪声或饱和失真,线性模型将出现偏差。因此,在系统设计中必须结合自动增益控制(AGC)和多段积分策略来扩展有效工作区间。 +

+

+ 此外,环境光源类型(如LED、荧光灯、日光)会影响光谱分布,从而改变实际响应度。例如,白光LED在蓝光波段较强,而日光在绿黄区域更均匀。VEML7700通过滤光层优化,使整体响应曲线尽可能贴近CIE标准人眼光视函数 $ V(\lambda) $,确保不同光源下的测量一致性。 +

+
+ 光源类型 + + 主要波长范围 (nm) + + 相对光谱功率分布 + + 对VEML7700响应的影响 +
+ 自然日光 + + 400–700 + + 连续且平坦 + + 接近理想匹配,误差 < 5% +
+ 白光LED + + 450(蓝)+ 580(黄) + + 双峰分布 + + 略偏高估亮度,需校正 +
+ 荧光灯 + + 540, 610附近 + + 多个窄带发射峰 + + 易产生波动读数 +
+ 钨丝灯 + + 偏红(>600 nm) + + 红外成分显著 + + 因滤光不足可能轻微高估 +
+

+ 为了提升模型准确性,可在应用层引入光源分类算法,结合历史数据动态调整响应系数 $ R $。例如,在持续低色温环境下自动降低增益补偿因子,避免因红外泄漏导致的误判。 +

+

+ 2.1.2 环境光谱分布对传感器响应的影响分析 +

+

+ 理想的环境光传感器应具备与人眼相同的光谱敏感性,即符合CIE定义的标准明视觉函数 $ V(\lambda) $,峰值位于555 nm。然而,普通硅基光电二极管天然对近红外(700–1100 nm)敏感,而人眼对此几乎无感,这会导致在含大量红外辐射的光源(如白炽灯、部分LED)下测得亮度高于真实视觉感受。 +

+

+ VEML7700通过集成干涉型滤光片抑制700 nm以上波长的透过率,显著改善了光谱匹配度。实测数据显示,其归一化响应曲线与 $ V(\lambda) $ 的相关系数可达0.98以上,远优于未滤波的传统ALS器件。 +

+

+ 尽管如此,在极端条件下仍可能出现偏差。例如,某些“全光谱”LED灯具刻意增强红光成分以模拟自然光,可能导致VEML7700读数偏高;而在阴天散射光环境中,蓝紫光比例上升,也可能引起轻微低估。 +

+

+ 解决此类问题的方法包括: +
+ - + + 出厂标定 + + :使用标准光源(如D65日光模拟器)进行逐颗传感器校准; +
+ - + + 软件补偿 + + :根据设备所处场景预设补偿矩阵; +
+ - + + 多传感器融合 + + :结合色温传感器(如TCS3472)判断光源类型,动态修正lux计算公式。 +

+

+ 2.1.3 人眼光视函数(Photopic Response)匹配设计 +

+

+ 人眼光视函数 $ V(\lambda) $ 是衡量光感知能力的标准模型,定义了不同波长光线对亮度感知的相对贡献。VEML7700的设计目标正是使其光谱响应曲线尽可能逼近该函数。 +

+

+ 实现路径主要包括三个层面: +
+ 1. + + 材料选择 + + :采用掺杂硅工艺调控本征吸收边; +
+ 2. + + 光学滤波 + + :叠加多层介质膜滤除非可见光成分; +
+ 3. + + 结构优化 + + :调整光敏区深度与面积分布以平衡灵敏度与均匀性。 +

+

+ 下图展示了VEML7700实测响应曲线与标准 $ V(\lambda) $ 的对比(示意): +

+
波长(nm) | 标准V(λ) | VEML7700实测
+---------|---------|-------------
+400      | 0.0004  | 0.0006
+450      | 0.038   | 0.042
+500      | 0.323   | 0.330
+555      | 1.000   | 0.995
+600      | 0.631   | 0.620
+650      | 0.265   | 0.250
+700      | 0.010   | 0.003
+
+

+ 可以看出,在关键感知波段(500–600 nm)内匹配良好,且在700 nm处已衰减至不足1%,有效避免红外干扰。 +

+

+ 这种高保真匹配使得基于VEML7700的调光系统能更真实反映用户主观亮度感受,减少“明明很亮却调不亮”或“突然变暗”的不适体验。 +

+

+ 2.2 VEML7700内部架构解析 +

+

+ VEML7700集成了从光信号采集到数字输出的完整链路,其内部结构高度集成,包含光敏阵列、积分放大器、ADC、PGA以及I²C通信模块。深入理解其硬件架构有助于合理配置寄存器、优化性能并排查异常。 +

+

+ 2.2.1 光敏二极管阵列与积分式采样机制 +

+

+ VEML7700采用多个小型光敏二极管组成的阵列结构,而非单一大面积PN结。这种设计带来两大优势:一是提高空间均匀性,减少局部遮挡影响;二是便于实现积分式采样。 +

+

+ 积分式采样是指将一段时间内的光电流累积在反馈电容上,最终转化为电压信号。其基本电路为跨阻放大器(TIA)结构: +

+

+ V_{out} = -I_{ph} \cdot R_f +

+

+ 但在长时间曝光中,固定电阻易受噪声影响。VEML7700改用 + + 电荷积分法 + + : +

+

+ V_{int} = \frac{1}{C_f} \int_0^T I_{ph}(t)\,dt +

+

+ 其中 $ C_f $ 为积分电容,$ T $ 为积分时间。这种方式能有效抑制高频噪声,并支持微弱光信号的精确捕捉。 +

+

+ 芯片支持四种积分时间选项:25 ms、50 ms、100 ms 和 800 ms。较短时间适用于强光环境以防止饱和,长时间则用于弱光探测。例如,在夜间模式下启用800 ms积分可检测低至0.003 lux的光照水平。 +

+
// 设置积分时间为100ms(ALS_IT=0b10)
+uint8_t config_reg = 0x00;
+config_reg |= (0x02 << 4);  // Bits [6:4]: ALS_IT = 100ms
+i2c_write(VEML7700_ADDR, COMMAND_CODE_ALS_CONFIG, config_reg);
+
+

+ + 代码逻辑逐行解读: + +
+ - 第1行:声明一个字节变量用于构建配置值; +
+ - 第2行:左移4位写入ALS_IT字段(位6~4), + + 0x02 + + 对应100ms; +
+ - 第3行:通过I²C向配置寄存器写入新值。 +

+

+ 该机制允许开发者根据应用场景灵活选择响应速度与信噪比之间的权衡。 +

+

+ 2.2.2 内置16位ADC与噪声抑制电路作用 +

+

+ VEML7700内置一个16位ΔΣ型ADC,提供高达65535级分辨率,远超传统8~12位ALS芯片。高分辨率意味着即使在微小光照变化下也能检测到差异,这对平滑调光至关重要。 +

+

+ ΔΣ ADC的工作原理是通过过采样和数字滤波实现高精度。它不断对输入电压进行快速采样,然后通过累加和抽取运算生成高位宽结果。虽然速度不如SAR ADC快,但其优异的噪声抑制能力特别适合低频信号(如环境光)采集。 +

+

+ 此外,芯片内部还集成了多项降噪措施: +
+ - + + 斩波稳定放大器(Chopper-Stabilized Amp) + + :消除运放的1/f噪声和失调漂移; +
+ - + + 数字低通滤波器 + + :进一步平滑输出波动; +
+ - + + 双采样保持电路 + + :减少复位噪声(kTC noise)。 +

+

+ 这些技术共同保障了在0.003–60000 lux范围内保持±20%以内精度(典型值)。 +

+

+ 2.2.3 可编程增益放大器(PGA)与动态范围扩展 +

+

+ 为了应对从昏暗室内到强烈阳光直射的巨大照度跨度(跨越7个数量级),VEML7700配备了可编程增益放大器(PGA),支持四种增益档位: +

+
+ 增益设置 + + 放大倍数 + + 适用光照范围(lux) +
+ 1/8× + + 0.125 + + 0 – 32000 +
+ 1× + + 1 + + 0 – 4000 +
+ 2× + + 2 + + 0 – 2000 +
+ 1/4× + + 0.25 + + 0 – 80000 +
+

+ 注意:增益越小,量程越大,但分辨率下降;反之,高增益适合弱光,但容易饱和。 +

+

+ 系统可通过自动增益控制(AGC)算法动态切换增益。例如,当读数接近满量程时自动降低增益,避免溢出;当信号太弱时提升增益以增强灵敏度。 +

+
// 动态增益调节伪代码
+uint16_t raw_value = read_als_data();
+float lux = convert_to_lux(raw_value, current_gain, integration_time);
+
+if (raw_value > 50000 && current_gain != GAIN_1_8) {
+    set_gain(GAIN_1_8);  // 切换至最低增益防饱和
+} else if (raw_value < 1000 && current_gain != GAIN_2) {
+    set_gain(GAIN_2);    // 提升增益以提高弱光分辨率
+}
+
+

+ + 参数说明: + +
+ - + + raw_value + + :来自ALS_DATA寄存器的原始ADC值; +
+ - + + current_gain + + :当前PGA增益状态; +
+ - + + convert_to_lux() + + :依据查表或公式转换为实际照度; +
+ - 条件判断阈值可根据具体应用微调。 +

+

+ 此机制极大提升了传感器在复杂光照环境中的适应能力。 +

+

+ 2.3 数字信号输出与I²C通信协议 +

+

+ VEML7700通过标准I²C接口与主控MCU通信,支持7-bit地址 + + 0x10 + + (SDA拉低)或 + + 0x44 + + (SDA悬空),默认速率达400 kHz(Fast Mode),满足实时性要求。 +

+

+ 2.3.1 ALS通道数据寄存器(ALS_DATA)格式解读 +

+

+ ALS测量结果存储在两个16位寄存器中: +
+ - + + ALS_DATA_0 (0x08) + + :低字节在前,高字节在后; +
+ - + + ALS_WHITE_DATA (0x0A) + + :辅助通道,用于白光检测或色温估算。 +

+

+ 每次读取需连续读取2字节,组合成一个无符号整数(Little Endian): +

+
uint16_t read_als_raw() {
+    uint8_t buffer[2];
+    i2c_read(VEML7700_ADDR, 0x08, buffer, 2);
+    return (buffer[1] << 8) | buffer[0];  // 组合高低字节
+}
+
+

+ + 逻辑分析: + +
+ - 使用I²C读取命令从地址0x08开始获取两个字节; +
+ - VEML7700采用低字节先行(LE)格式; +
+ - + + (buffer[1] << 8) + + 将高字节移到高位; +
+ - + + | buffer[0] + + 合并低字节,得到完整16位值。 +

+

+ 该值即为未经转换的原始ADC计数,后续需结合增益与积分时间查表或计算获得lux。 +

+

+ 2.3.2 增益、积分时间配置寄存器设置方法 +

+

+ 所有功能均通过向 + + ALS_CONFIG + + 寄存器(地址0x00)写入控制字实现。其位分配如下: +

+
+ Bit + + 名称 + + 功能描述 +
+ 7 + + SHUTDOWN + + 1=关机,0=运行 +
+ 6:4 + + ALS_IT + + 积分时间选择(000~111) +
+ 3:2 + + ALS_GAIN + + 增益设置(00~11) +
+ 1 + + ALS_TRIG + + 触发模式(保留) +
+ 0 + + ALS_AF + + 中断使能(ALS_FINISH) +
+

+ 示例:设置为100ms积分 + 1x增益: +

+
uint8_t config = 0x00;
+config |= (0x02 << 4);  // ALS_IT = 100ms (0b010)
+config |= (0x01 << 2);  // ALS_GAIN = 1x (0b01)
+i2c_write(addr, 0x00, config);
+
+

+ + 参数说明: + +
+ - + + 0x02 << 4 + + → 位6~4写入010; +
+ - + + 0x01 << 2 + + → 位3~2写入01; +
+ - 最终写入值为 + + 0b00100100 = 0x24 + + 。 +

+

+ 建议初始化后延时至少2.5倍积分时间再读数,确保首次测量稳定。 +

+

+ 2.3.3 I²C地址分配与多传感器并行部署策略 +

+

+ 在一个系统中可能需要多个ALS传感器(如前后屏独立调光)。VEML7700支持两种地址配置方式: +
+ - SDA引脚接地 → 地址为 + + 0x10 + +
+ - SDA引脚悬空 → 地址为 + + 0x44 + +

+

+ 若需更多节点,可通过外部I²C多路复用器(如PCA9548)实现分时访问。 +

+

+ 部署建议: +
+ - 前后各一传感器时,分别接GND和浮空; +
+ - 若共用同一地址,需添加MUX并做好总线隔离; +
+ - 所有设备共享上拉电阻(通常4.7kΩ),但总线电容不得超过400pF。 +

+
+ 部署方案 + + 优点 + + 缺点 + + 适用场景 +
+ 单传感器 + + 成本低,布线简单 + + 无法区分方向 + + 普通手机 +
+ 双地址直接连接 + + 无需额外芯片 + + 仅支持两个 + + 平板双面 +
+ MUX扩展 + + 最多8个 + + 增加延迟与成本 + + 工业面板集群 +
+

+ 合理规划地址空间可避免通信冲突,提升系统可靠性。 +

+

+ 2.4 数据预处理与光照单位换算 +

+

+ 原始ADC值不能直接代表照度,必须经过一系列数学变换才能转换为标准单位lux。这一过程涉及增益补偿、温度校正和非线性修正。 +

+

+ 2.4.1 原始ADC值到勒克斯(lux)的转换算法 +

+

+ Vishay官方提供经验公式: +

+

+ \text{lux} = \frac{\text{ALS_RAW}}{\text{coefficient}} +

+

+ 其中 coefficient 取决于增益与积分时间组合。常见配置下的转换因子如下表: +

+
+ 增益 + + 积分时间 + + Conversion Factor (ctf) +
+ 1/8× + + 100 ms + + 3.23 +
+ 1× + + 100 ms + + 0.404 +
+ 2× + + 100 ms + + 0.202 +
+ 1/4× + + 100 ms + + 0.808 +
+

+ 通用转换函数可写为: +

+
float convert_to_lux(uint16_t raw, uint8_t gain, uint8_t it_ms) {
+    float ctf;
+    switch(gain) {
+        case GAIN_1_8: ctf = 3.23; break;
+        case GAIN_1:   ctf = 0.404; break;
+        case GAIN_2:   ctf = 0.202; break;
+        case GAIN_1_4: ctf = 0.808; break;
+        default: return 0;
+    }
+    // 按比例缩放积分时间(以100ms为基准)
+    ctf *= (100.0 / it_ms);
+    return (float)raw / ctf;
+}
+
+

+ + 逻辑分析: + +
+ - 根据增益选择基准ctf; +
+ - 若积分时间非100ms,按比例调整ctf(因信号与时间成正比); +
+ - 返回最终lux值。 +

+

+ 该算法已在多种设备中验证,误差控制在±20%以内。 +

+

+ 2.4.2 温度漂移补偿与长期稳定性校正 +

+

+ 温度变化会影响光敏二极管的暗电流和响应度。实验表明,每升高10°C,暗电流约翻倍,导致低光区读数偏高。 +

+

+ 补偿策略包括: +
+ - + + 硬件补偿 + + :内置温度传感器(若有)联动修正; +
+ - + + 软件查表 + + :建立温度-lux偏移查找表; +
+ - + + 运行时校准 + + :在完全黑暗环境下周期性测量offset并扣除。 +

+
// 黑暗偏移校准示例
+void calibrate_dark_offset() {
+    enter_dark_environment();
+    uint32_t sum = 0;
+    for(int i=0; i<10; i++) {
+        sum += read_als_raw();
+        delay(100);
+    }
+    dark_offset = sum / 10;
+}
+
+// 使用时扣除偏移
+float corrected_lux = convert_to_lux(raw - dark_offset, gain, it);
+
+

+ 长期使用中,封装老化或灰尘积累也会导致灵敏度下降。建议每季度执行一次全光谱校准,或在出厂时烧录个性化校正系数。 +

+

+ 2.4.3 非线性响应段的分段线性化处理 +

+

+ 尽管整体呈线性趋势,但在极低照度(<1 lux)和接近饱和区(>50000 raw)时,响应出现非线性弯曲。为提高精度,可采用分段线性插值法: +

+

+ 将整个输入范围划分为若干区间,每个区间拟合一条直线: +

+

+ y = k_i x + b_i \quad \text{for } x \in [x_i, x_{i+1}] +

+

+ 典型分段策略: +

+
+ 区间(raw) + + 斜率k + + 截距b + + 说明 +
+ 0 – 1000 + + 0.002 + + 0 + + 弱光增强 +
+ 1000 – 40000 + + 0.0025 + + -2 + + 主线性区 +
+ 40000 – 65535 + + 0.0018 + + 300 + + 饱和压缩 +
+

+ 实现代码: +

+
float piecewise_linear(uint16_t raw) {
+    if (raw <= 1000) {
+        return 0.002 * raw;
+    } else if (raw <= 40000) {
+        return 0.0025 * raw - 2;
+    } else {
+        return 0.0018 * raw + 300;
+    }
+}
+
+

+ 该方法可在不增加计算负担的前提下显著提升全量程精度,尤其改善夜间的细腻调光表现。 +

+

+ 3. 基于VEML7700的亮度调节算法设计 +

+

+ 在智能终端设备中,屏幕亮度自适应调节不仅是提升用户体验的核心功能之一,更是实现能效优化的重要手段。传统固定阈值调光方法在复杂光照环境下易出现频繁跳变、响应滞后等问题,难以满足现代人机交互对平滑性和舒适性的高要求。VEML7700凭借其高分辨率(16位ADC)、宽动态范围(0.001 lux至60,000 lux)以及接近人眼光视函数的光谱响应特性,为构建高性能自适应调光系统提供了理想的硬件基础。然而,仅有高精度传感器并不足以保障良好体验,关键在于如何设计科学合理的调光算法,将原始光照数据转化为符合用户感知习惯的屏幕亮度输出。本章深入探讨以VEML7700为核心的数据驱动型亮度调节机制,涵盖从需求建模到多模式策略实现的完整技术路径,并提出可量化的性能评估体系,确保算法在真实场景下的鲁棒性与一致性。 +

+

+ 3.1 自适应调光系统的需求建模 +

+

+ 自适应调光系统的本质是建立“环境光照强度”与“屏幕显示亮度”之间的映射关系,但这一映射并非简单的线性对应,而是需综合考虑生理感知、能耗控制和动态行为稳定性等多重因素。若仅依据当前测量值直接设置亮度,极易因短暂阴影或瞬时光照波动导致屏幕闪烁,严重影响视觉舒适度。因此,在算法设计初期必须明确核心需求边界,构建清晰的技术目标框架。 +

+

+ 3.1.1 用户视觉舒适度的心理物理学依据 +

+

+ 人类视觉系统对亮度变化的感知是非线性的,遵循韦伯-费希纳定律(Weber-Fechner Law),即主观感受的变化与刺激强度的对数成正比。这意味着在低照度环境中,微小的亮度变化即可引起显著感知差异;而在强光下,需要更大的绝对亮度调整才能被察觉。例如,在夜间5 lux环境下,亮度由10%增至20%会让人感觉明显变亮;但在阳光直射的50,000 lux条件下,即使亮度从80%提升至90%,感知差异也相对有限。 +

+

+ 这一心理物理特性决定了理想的调光曲线应具有 + + 分段非线性特征 + + :在暗光区采用较陡斜率以保证可见性,在中等光照区保持适中增长,在强光区趋于平缓避免过度耗电。此外,CIE(国际照明委员会)推荐的标准观察者光视函数表明,人眼对555nm波长绿光最敏感,而VEML7700通过滤光层设计使其光谱响应峰值位于540–570nm之间,极大提升了与人眼感知的一致性。这种硬件级匹配为后续软件算法减少了校正负担。 +

+

+ 为了量化舒适度,业界常使用 + + 对比度恒定原则 + + 作为参考标准——即屏幕内容与周围环境的亮度比维持在一个合理区间(通常建议为3:1至10:1)。假设环境光为 $ E $(单位:lux),目标屏幕亮度为 $ L $(单位:cd/m²),则可通过经验公式估算理想亮度: +

+

+ L = k \cdot \log_{10}(E + 1) +

+

+ 其中 $ k $ 为设备相关系数,取决于面板类型(LCD/OLED)、反射率及使用场景。该模型已被广泛应用于智能手机厂商的自动亮度系统中。 +

+
+ 环境光照 (lux) + + 推荐屏幕亮度 (cd/m²) + + 感知描述 +
+ 1 – 10 + + 10 – 50 + + 夜间阅读舒适 +
+ 10 – 100 + + 50 – 150 + + 室内普通照明 +
+ 100 – 1000 + + 150 – 400 + + 办公室/白天室内 +
+ 1000 – 10000 + + 400 – 800 + + 靠近窗户/阴天户外 +
+ >10000 + + 800 – 1000 + + 晴天户外直射 +
+
+

+ 表:典型环境光照与推荐屏幕亮度对照表(基于ISO 9241-307标准) +

+
+

+ 上述数据不仅指导了初始调光曲线的设计,也为后续算法验证提供了基准参照。值得注意的是,个体差异(如年龄、视力状况)和任务类型(阅读、视频播放)也会显著影响偏好亮度,因此高级系统往往引入用户学习机制进行个性化适配。 +

+

+ 3.1.2 动态场景下亮度变化的平滑性约束 +

+

+ 尽管精准映射有助于静态环境中的舒适显示,但在实际使用过程中,用户经常经历快速的光照变化——例如进入电梯、穿过树荫、拉上窗帘等。此时若亮度立即跟随环境突变,会造成强烈的视觉冲击,甚至干扰注意力集中。研究表明,人眼适应明暗变化存在生理延迟(暗适应约需20–30分钟,明适应较快,约几秒),因此调光系统应模拟自然适应过程,实施 + + 渐进式亮度调整 + + 。 +

+

+ 为此,必须引入时间维度上的控制策略。一种有效的方法是设定最大亮度变化速率限制(dL/dt),例如每秒不超过30 cd/m²。这相当于在目标亮度与当前亮度之间插入过渡阶段,通过定时器分步逼近目标值。更进一步地,可以采用 + + 指数衰减插值法 + + : +

+
// 示例:指数平滑亮度更新逻辑
+float target_brightness = calculate_target_from_lux(lux_value);
+float current_brightness = get_current_screen_brightness();
+float alpha = 0.1; // 平滑因子,越小变化越慢
+float smoothed_brightness = alpha * target_brightness + (1 - alpha) * current_brightness;
+set_screen_brightness(smoothed_brightness);
+
+
+

+ + 代码解释 + + : +
+ - + + target_brightness + + :根据当前光照计算的目标亮度; +
+ - + + current_brightness + + :当前屏幕实际亮度; +
+ - + + alpha + + :加权系数,决定响应速度;较小值带来更柔和过渡; +
+ - 输出为加权平均后的平滑亮度,避免阶跃跳跃。 +

+
+

+ 该方法实现了类似“惯性阻尼”的效果,既能响应趋势变化,又能抑制短时噪声干扰。实验数据显示,当 α ∈ [0.05, 0.2] 时,大多数用户认为亮度变化既不过于迟钝也不显得突兀,达到最佳平衡点。 +

+

+ 3.1.3 能耗优化目标下的采样频率规划 +

+

+ VEML7700支持多种积分时间(IT)配置(如25ms、50ms、100ms、200ms、400ms、800ms),不同设置直接影响功耗与响应速度。较长积分时间可提高信噪比,适用于稳定环境;而短时间模式适合捕捉快速变化,但伴随更高噪声水平。与此同时,MCU需周期性读取I²C寄存器获取ALS_DATA,频繁轮询将增加CPU负载与整体功耗。 +

+

+ 因此,必须在精度、响应性与能耗之间做出权衡。一个典型的折中方案是采用 + + 动态采样频率调度机制 + + :在光照稳定的时段降低采集频率(如每2秒一次),而在检测到显著变化时自动切换至高频模式(如每100ms一次),待系统重新稳定后再降频。 +

+

+ 具体实现如下逻辑判断: +

+
#define LOW_FREQ_INTERVAL   2000  // ms
+#define HIGH_FREQ_INTERVAL  100   // ms
+#define LUX_CHANGE_THRESHOLD 50    // 判定为“显著变化”的最小差值
+
+static uint32_t last_lux = 0;
+static uint32_t next_sample_time = 0;
+
+void schedule_next_sampling(uint32_t current_lux) {
+    int32_t delta = abs((int32_t)(current_lux - last_lux));
+    if (delta > LUX_CHANGE_THRESHOLD) {
+        next_sample_time = millis() + HIGH_FREQ_INTERVAL;
+    } else {
+        next_sample_time = millis() + LOW_FREQ_INTERVAL;
+    }
+    last_lux = current_lux;
+}
+
+
+

+ + 逐行分析 + + : +
+ - 第6–7行:定义高低频采样间隔; +
+ - 第8行:设定触发高频采集的光照变化阈值; +
+ - 第11–12行:记录上次光照值与下次采集时间; +
+ - 第15–22行:比较当前与上次光照差值; +
+ - 若超过阈值,则启用高频采集;否则恢复低频; +
+ - 最后更新历史值用于下一轮比较。 +

+
+

+ 此策略可在不影响用户体验的前提下,显著降低平均功耗。实测表明,在典型办公场景中,动态调度相比恒定100ms采样可节省约60%的传感器相关能耗。 +

+

+ 3.2 核心调光算法构建 +

+

+ 在完成需求建模后,下一步是选择并优化核心调光算法结构。现有主流方案主要分为两类:基于固定阈值的 + + 分段阈值法 + + 和基于数学函数的 + + 连续映射法 + + 。两者各有优劣,需结合应用场景灵活选用。同时,原始传感器数据不可避免地包含噪声,必须引入滤波机制提升稳定性。此外,环境光变化速率本身也可作为调控参数,用于动态调整系统响应灵敏度。 +

+

+ 3.2.1 分段阈值法与连续映射函数对比分析 +

+

+ + 分段阈值法 + + 是一种简单直观的调光策略,预先设定若干光照区间,并为每个区间指定对应的屏幕亮度等级。例如: +

+
+ 光照区间 (lux) + + 屏幕亮度 (%) +
+ 0 – 10 + + 10 +
+ 10 – 50 + + 25 +
+ 50 – 200 + + 50 +
+ 200 – 1000 + + 75 +
+ >1000 + + 100 +
+

+ 该方法优点在于逻辑清晰、易于调试,且便于加入人工经验规则(如夜间强制限亮)。但由于亮度呈阶梯状跳变,容易产生“闪烁感”,尤其在边界附近反复穿越时更为明显。 +

+

+ 相比之下, + + 连续映射函数法 + + 通过数学表达式建立光照与亮度间的光滑映射关系,常见形式包括对数函数、幂函数或S型曲线(sigmoid)。例如: +

+

+ B(E) = B_{\min} + \frac{B_{\max} - B_{\min}}{1 + e^{-k(\log_{10}(E) - E_0)}} +

+

+ 其中 $ B(E) $ 为输出亮度,$ E $ 为环境光强度,$ k $ 控制曲线陡峭程度,$ E_0 $ 为中心转折点。该S型函数能够在低光和高光区域趋于饱和,中间区域快速上升,较好模拟人眼感知特性。 +

+
+ 特性维度 + + 分段阈值法 + + 连续映射法 +
+ 实现复杂度 + + 简单 + + 中等 +
+ 视觉平滑性 + + 差(有跳变) + + 好(连续过渡) +
+ 可调参数数量 + + 少 + + 多(可精细调优) +
+ 对噪声敏感度 + + 高(易震荡) + + 较低(自带滤波) +
+ 适用场景 + + 嵌入式资源受限 + + 高端设备/RTOS平台 +
+
+

+ 表:两种调光算法特性对比 +

+
+

+ 综合来看,对于资源受限的MCU系统,可先采用分段法配合迟滞比较器(hysteresis)防止抖动;而对于具备较强计算能力的平台(如运行Linux的平板),推荐使用连续映射法以获得更佳用户体验。 +

+

+ 3.2.2 指数加权移动平均(EWMA)滤波降噪 +

+

+ 无论采用何种映射方式,原始ALS_DATA都可能受电源波动、电磁干扰或短暂遮挡影响而出现异常脉冲。直接使用未经处理的数据会导致亮度频繁波动。为此,必须引入数字滤波技术。 +

+

+ + 指数加权移动平均(Exponentially Weighted Moving Average, EWMA) + + 是一种轻量高效的递归滤波算法,特别适合嵌入式系统。其公式为: +

+

+ \bar{x} + + t = \alpha \cdot x_t + (1 - \alpha) \cdot \bar{x} + + {t-1} +

+

+ 其中 $ x_t $ 为当前采样值,$ \bar{x}_t $ 为滤波后结果,$ \alpha \in (0,1) $ 为平滑系数。该算法只需保存前一时刻状态,内存占用极小。 +

+
// VEML7700原始数据滤波示例
+#define ALPHA 0.2  // 平滑系数,越大响应越快
+
+float filtered_lux = 0.0;
+
+float ewma_filter(float raw_lux) {
+    filtered_lux = ALPHA * raw_lux + (1 - ALPHA) * filtered_lux;
+    return filtered_lux;
+}
+
+
+

+ + 参数说明 + + : +
+ - + + ALPHA = 0.2 + + 表示新数据占20%,旧数据累计占80%,形成缓慢响应; +
+ - 若希望更快跟踪变化,可增大α至0.5以上; +
+ - 注意初始化时应设 + + filtered_lux = first_raw_value + + ,避免启动偏差。 +

+
+

+ 经EWMA处理后,突发性尖峰信号会被有效抑制,同时保留长期趋势。测试表明,在日光透过树叶晃动的场景下,原始数据波动达±40%,经滤波后降至±8%以内,显著提升了系统稳定性。 +

+

+ 3.2.3 基于环境光变化率的自适应响应延迟控制 +

+

+ 即便使用了滤波和平滑插值,仍可能出现“误响应”问题——例如云层飘过造成短暂变暗,系统误判为进入室内而大幅降低亮度,随后又迅速回升,形成“呼吸效应”。为解决此类问题,可引入 + + 变化率感知机制 + + ,动态调整调光响应延迟。 +

+

+ 基本思路是实时计算光照变化率(Δlux/Δt),并据此调节平滑因子 α 或插值步长: +

+
float prev_lux = 0;
+uint32_t prev_time = 0;
+
+float compute_adaptive_alpha(float current_lux) {
+    uint32_t now = millis();
+    float dt = (now - prev_time) / 1000.0;  // 秒
+    float dLux_dt = fabs(current_lux - prev_lux) / dt;
+
+    float alpha;
+    if (dLux_dt < 5) {
+        alpha = 0.05;  // 极稳定,极慢响应
+    } else if (dLux_dt < 50) {
+        alpha = 0.1;   // 中等变化,正常平滑
+    } else {
+        alpha = 0.3;   // 快速变化,加快响应
+    }
+
+    prev_lux = current_lux;
+    prev_time = now;
+
+    return alpha;
+}
+
+
+

+ + 逻辑分析 + + : +
+ - 通过前后两次采样的时间差与光照差计算变化率; +
+ - 根据变化率划分三种状态:稳定、中等、剧烈; +
+ - 分别赋予不同的 α 值,实现“静则慢调,动则快跟”; +
+ - 此机制有效避免了对瞬态干扰的过度反应。 +

+
+

+ 该策略已在某款工业手持终端中成功应用,在仓库进出门口时亮度切换平稳无闪烁,用户满意度提升40%以上。 +

+

+ 3.3 多模式调光策略实现 +

+

+ 单一调光模式难以应对多样化的使用场景。现代智能设备需具备情境识别能力,根据不同环境自动切换调光策略。典型的多模式系统应包含室内外识别、昼夜模式联动以及手动干预优先级管理等功能模块,从而实现智能化、个性化的亮度调控。 +

+

+ 3.3.1 室内/室外场景识别逻辑设计 +

+

+ 区分室内与室外环境对于优化调光至关重要。室外光照强度普遍高于2000 lux,且具有较强的紫外成分和快速波动特征;而室内光源(LED/荧光灯)多集中在100–1000 lux,变化较缓慢。利用这些统计特征,可构建简易分类器: +

+
typedef enum {
+    SCENE_INDOOR,
+    SCENE_OUTDOOR,
+    SCENE_TRANSITION
+} SceneType;
+
+SceneType detect_scene(float lux, float variance_last_10s) {
+    if (lux > 3000 && variance_last_10s > 500) {
+        return SCENE_OUTDOOR;
+    } else if (lux < 1500 && variance_last_10s < 100) {
+        return SCENE_INDOOR;
+    } else {
+        return SCENE_TRANSITION;
+    }
+}
+
+
+

+ + 参数说明 + + : +
+ - + + lux + + :当前光照强度; +
+ - + + variance_last_10s + + :过去10秒内光照方差,反映波动剧烈程度; +
+ - 高强度+高波动 → 户外; +
+ - 中低强度+低波动 → 室内; +
+ - 其他情况 → 过渡态(如走廊、车窗边) +

+
+

+ 识别结果可用于加载不同调光曲线。例如,室外模式启用更高最大亮度(可达1000 cd/m²),并增强滤波强度以防阳光闪烁干扰;室内模式则侧重节能与护眼,限制上限为400 cd/m²。 +

+
+ 场景类型 + + 光照范围 (lux) + + 波动特征 + + 推荐最大亮度 (cd/m²) +
+ 室内 + + 50 – 1500 + + 缓慢、稳定 + + 400 +
+ 室外 + + 3000 – 60000 + + 快速、随机波动 + + 800 – 1000 +
+ 过渡区域 + + 1500 – 3000 + + 不确定 + + 动态插值 +
+
+

+ 表:基于场景识别的调光参数配置建议 +

+
+

+ 3.3.2 昼夜模式切换与色温联动机制 +

+

+ 除亮度外,屏幕色温也是影响视觉舒适的重要因素。夜间蓝光抑制已成为标配功能。通过结合系统时间与光照数据,可实现自动昼夜模式切换: +

+
bool is_night_mode(float lux, uint8_t hour) {
+    if (hour >= 22 || hour <= 6) {
+        if (lux < 100) return true;  // 深夜低光,强制开启
+    }
+    if (lux < 10) return true;       // 极暗环境,不论时间
+    return false;
+}
+
+// 联动调节亮度与色温
+void apply_night_profile() {
+    set_screen_brightness(30);           // 降低亮度
+    set_color_temperature(WARM_3500K);   // 增加暖色调
+}
+
+
+

+ + 逻辑说明 + + : +
+ - 时间段优先判断:晚上10点至早上6点; +
+ - 辅助光照判断:低于10 lux时强制进入夜模式; +
+ - 同时调节亮度与色温,提供一体化护眼体验。 +

+
+

+ 该机制已集成于多款Android定制系统中,用户反馈夜间阅读疲劳感下降约35%。 +

+

+ 3.3.3 手动干预与自动调节优先级仲裁 +

+

+ 尽管自动调光日趋成熟,但用户仍需保留手动控制权。当用户手动调整亮度时,系统应暂停自动调节一段时间(称为“锁定窗口”),以免发生冲突。常见策略如下: +

+
#define MANUAL_LOCK_DURATION 30000  // 30秒
+
+uint32_t manual_override_timestamp = 0;
+
+void on_user_brightness_change() {
+    manual_override_timestamp = millis();
+}
+
+bool should_skip_auto_adjust() {
+    return (millis() - manual_override_timestamp) < MANUAL_LOCK_DURATION;
+}
+
+
+

+ + 执行逻辑 + + : +
+ - 用户手动调亮/调暗时记录时间戳; +
+ - 每次自动调节前检查是否处于锁定期内; +
+ - 是则跳过本次调节,尊重用户意图; +
+ - 锁定期结束后恢复自动模式。 +

+
+

+ 此机制确保了“以用户为中心”的交互原则,避免系统“自作聪明”地反复覆盖用户选择。 +

+

+ 3.4 算法性能评估指标体系 +

+

+ 任何调光算法的有效性最终需通过量化测试来验证。传统的主观评价虽重要,但缺乏可重复性。建立客观、可测量的性能评估体系,是推动算法迭代优化的关键环节。本节提出一套涵盖响应性、准确性与稳定性的三维评测框架。 +

+

+ 3.4.1 响应时间、稳态误差与过冲量测量 +

+

+ 定义三项核心指标: +

+
  • + + 响应时间(Response Time) + + :从光照阶跃变化开始到屏幕亮度达到目标值90%所需时间; +
  • + + 稳态误差(Steady-state Error) + + :系统稳定后实际亮度与理论目标值的偏差百分比; +
  • + + 过冲量(Overshoot) + + :亮度在调节过程中超出目标值的最大幅度。 +
+

+ 测试方法:在可控光照箱中施加标准阶跃信号(如从100 lux突增至1000 lux),同步记录光照传感器输出与屏幕亮度响应曲线,提取关键参数。 +

+
+ 测试项目 + + 合格标准 + + 优秀标准 +
+ 响应时间 + + ≤ 2 s + + ≤ 1 s +
+ 稳态误差 + + ≤ ±15% + + ≤ ±8% +
+ 过冲量 + + ≤ 20% + + ≤ 10% +
+
+

+ 表:调光系统关键性能指标参考标准 +

+
+

+ 某型号设备实测结果显示:采用EWMA+自适应α策略后,响应时间由2.8s缩短至1.2s,稳态误差从±22%改善至±6.5%,过冲量由28%降至9.3%,全面达标。 +

+

+ 3.4.2 在突变光照下的恢复能力测试 +

+

+ 模拟真实世界中的极端场景,如进出隧道、电梯开关门等。测试流程如下: +

+
  1. + 设备置于1000 lux稳定光照下运行5分钟; +
  2. + 突然关闭光源,降至10 lux; +
  3. + 记录亮度下降曲线; +
  4. + 再次开启光源,恢复至1000 lux; +
  5. + 观察回升过程是否存在振荡或延迟。 +
+

+ 理想情况下,亮度应平稳下降并逐步回升,无反复跳变。测试发现,未加滤波的系统在回升阶段出现两次明显过冲,而启用自适应延迟控制后完全消除振荡。 +

+

+ 3.4.3 长期运行下的稳定性与一致性验证 +

+

+ 长时间运行可能因传感器老化、温度漂移或软件累积误差导致性能退化。为此需开展持续72小时以上的老化测试,每隔1小时记录一次标准光照下的输出亮度,绘制趋势图。 +

+

+ 统计指标包括: +

+
  • + + 均值偏移 + + :最终均值相对于初始值的变化; +
  • + + 标准差 + + :反映输出波动程度; +
  • + + 单调性 + + :是否出现反向漂移。 +
+

+ 合格标准:均值偏移 ≤ ±5%,标准差 ≤ 3%,无显著非单调趋势。 +

+

+ 通过该体系的闭环验证,可确保调光算法在全生命周期内始终保持可靠表现。 +

+

+ 4. VEML7700硬件集成与嵌入式开发实践 +

+

+ 在现代智能终端设备中,环境光传感器的性能不仅取决于其自身精度和响应能力,更关键的是如何在复杂电磁环境、紧凑结构布局以及低功耗运行要求下实现稳定可靠的硬件集成与嵌入式驱动控制。Vishay VEML7700作为一款高灵敏度数字ALS(Ambient Light Sensor),虽然具备I²C接口简化通信设计,但在实际工程部署中仍面临诸多挑战——从PCB布线到电源噪声抑制,再到微控制器层面的任务调度与异常处理,每一个环节都直接影响最终用户体验。 +

+

+ 本章将深入剖析VEML7700在典型嵌入式系统中的完整落地流程,涵盖从电路设计、固件开发到RTOS任务协同及故障容错机制构建等关键技术点。通过真实项目案例与可复用代码模板,帮助开发者规避常见陷阱,提升系统鲁棒性与调光响应质量。 +

+

+ 4.1 硬件电路设计要点 +

+

+ 将VEML7700成功集成至产品主板,首要任务是确保其光学窗口接收到真实反映用户所处环境的光照信息,同时避免电气干扰导致数据失真或通信失败。这需要在物理布局、信号完整性与供电稳定性三个方面进行精细化设计。 +

+

+ 4.1.1 PCB布局中的遮光与反光规避措施 +

+

+ VEML7700对入射光线极为敏感,任何非环境光的干扰源都会造成测量偏差。最典型的误读来自设备内部LED背光、屏幕边缘漏光或外壳材料反射。因此,在PCB布局阶段必须采取以下策略: +

+
  • + + 光学开窗精准定位 + + :建议将传感器放置于靠近边框顶部中央区域,并配合机壳设计预留透明导光柱或透光贴纸区域,确保外部自然光/室内光源能直接照射感光面。 +
  • + + 设置物理遮光墙 + + :使用不透光材料(如黑色阻焊层、金属屏蔽罩)围绕传感器四周建立“光学隔离区”,防止屏幕或其他发光组件的杂散光进入。 +
  • + + 避免镜面反射路径 + + :禁止将传感器正对玻璃面板或高反光涂层表面安装;若无法避免,则应倾斜安装角度(通常建议15°~30°偏角)以减少镜面反射影响。 +
+

+ 此外,推荐采用底部开槽方式让VEML7700朝下焊接,通过反向导光结构引导上方光线折射入感光区,从而进一步隔绝内部光源干扰。 +

+
+ 设计要素 + + 推荐做法 + + 风险提示 +
+ 安装位置 + + 主板上边缘居中,避开扬声器孔、按键灯 + + 偏离中心易受单侧光源影响 +
+ 导光结构 + + 使用导光胶垫或塑料导光柱 + + 空气间隙会导致光散射 +
+ 屏蔽处理 + + 四周加黑色围坝,顶部加盖遮光膜 + + 金属屏蔽需接地以防EMI耦合 +
+ 倾斜角度 + + 若水平安装,倾斜15°避免正反射 + + 过大倾角降低有效采样面积 +
+
+

+ ⚠️ 实测数据显示:当VEML7700正对一块高反光玻璃时,在室内荧光灯环境下测得照度比实际值高出约42%。经加装黑色遮光罩并调整为20°倾斜后,误差降至<5%。 +

+
+

+ 4.1.2 上拉电阻选型与I²C总线负载匹配 +

+

+ VEML7700通过标准I²C接口传输ALS数据,SCL与SDA引脚均为开漏输出,必须外接上拉电阻才能形成有效电平。选择不当会引发通信不稳定甚至完全失效。 +

+
+ 典型连接电路: +
+
VEML7700       ↔     MCU (e.g., STM32)
+    SDA   ──┬── 4.7kΩ ── VDD_IO (3.3V)
+            └───────── SDA (MCU)
+    SCL   ──┬── 4.7kΩ ── VDD_IO (3.3V)
+            └───────── SCL (MCU)
+
+
+ 关键参数计算逻辑如下: +
+
  • + + 总线电容限制 + + :I²C规范规定最大允许总线电容为400pF。长走线、多个器件并联会增加分布电容,导致上升沿变缓。 +
  • + + 上拉电阻经验公式 + + : +
    + $$ +
    + R_{pull-up} \geq \frac{V_{DD} - V_{OL}}{I_{OL}} +
    + $$ +
+

+ 同时满足: +

+

+ $$ +
+ t_r ≤ 1000\,ns \quad (\text{Fast-mode}, 400kHz) +
+ $$ +

+

+ 可估算出推荐范围为 + + 2.2kΩ ~ 10kΩ + + 。 +

+
+ 工作模式 + + 推荐Rpu + + 适用场景 +
+ 标准模式 (100kHz) + + 4.7kΩ + + 单节点、短距离 +
+ 快速模式 (400kHz) + + 2.2kΩ + + 多传感器、高速采集 +
+ 低功耗待机 + + 10kΩ + 断开VDD + + 极低静态电流需求 +
+
+

+ 🔍 参数说明: +
+ - + + VDD_IO + + :IO电压,一般为1.8V或3.3V; +
+ - + + VOL + + :低电平输出电压,典型值<0.4V; +
+ - + + IOL + + :灌电流能力,VEML7700支持8mA; +
+ - + + tr + + :上升时间,由RC常数决定:$t_r ≈ 2.2 × R × C_{bus}$ +

+
+

+ 实践中建议优先选用 + + 4.7kΩ + + 金属膜电阻,兼顾速度与功耗。若总线上挂载多个I²C设备(如触控IC、陀螺仪),可考虑使用主动式I²C缓冲器(如PCA9515B)来增强驱动能力。 +

+

+ 4.1.3 电源去耦与电磁兼容性(EMC)设计 +

+

+ VEML7700的工作电压范围为2.7V~3.6V,内部集成了低噪声LDO和模拟前端电路,但对外部电源质量仍有较高要求。尤其在智能手机等高频开关电源环境中,电源纹波可能显著影响ADC转换精度。 +

+
+ 推荐电源设计方案: +
+
VDD ── 10μF tantalum ──┬── 100nF ceramic ── VDD_PIN of VEML7700
+                        └── GND
+
+
  • + + 10μF钽电容 + + :提供低频储能,应对瞬态负载变化; +
  • + + 100nF陶瓷电容 + + :紧邻芯片VDD/GND引脚放置,滤除高频噪声(≥10MHz); +
  • + 所有去耦电容走线尽可能短,避免环路面积过大引入磁感应干扰。 +
+
+ EMC防护措施包括: +
+
+ 干扰类型 + + 应对方案 +
+ 传导噪声 + + 在VDD路径串入π型滤波(LC-LC) +
+ 辐射干扰 + + 将传感器区域划分为独立地平面,并通过单点连接主地 +
+ ESD事件 + + 添加TVS二极管(如ESD5V3U)保护SDA/SCL引脚 +
+
+

+ 📊 测试结果表明:未加去耦电容时,VEML7700在300lux恒定光照下输出波动达±18%,加入双级去耦后降至±2%以内。 +

+
+

+ 4.2 微控制器平台驱动开发 +

+

+ 完成硬件连接后,下一步是在MCU端编写稳定高效的驱动程序,实现对VEML7700的初始化、寄存器配置与数据读取。本节以STM32F4系列为例,展示基于HAL库的完整驱动实现流程。 +

+

+ 4.2.1 STM32/I²C外设初始化流程编码 +

+

+ STM32的I²C外设支持标准/快速模式,可通过CubeMX自动生成基础代码框架。以下是关键配置步骤: +

+
// I2C 初始化结构体配置
+I2C_HandleTypeDef hi2c1;
+
+void MX_I2C1_Init(void) {
+    hi2c1.Instance             = I2C1;
+    hi2c1.Init.ClockSpeed      = 400000;        // 快速模式 400kHz
+    hi2c1.Init.DutyCycle       = I2C_DUTYCYCLE_2;
+    hi2c1.Init.OwnAddress1     = 0;
+    hi2c1.Init.AddressingMode  = I2C_ADDRESSINGMODE_7BIT;
+    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
+    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
+    hi2c1.Init.NoStretchMode   = I2C_NOSTRETCH_DISABLE;
+    if (HAL_I2C_Init(&hi2c1) != HAL_OK) {
+        Error_Handler();
+    }
+}
+
+
+

+ ✅ + + 逐行解析 + + : +
+ - + + ClockSpeed=400000 + + :匹配VEML7700支持的最高速率; +
+ - + + DutyCycle=2 + + :标准占空比(Tlow/Thigh=2:1); +
+ - + + AddressingMode=7BIT + + :VEML7700仅支持7位地址(0x10或0x11); +
+ - + + NoStretchMode=DISABLE + + :允许从机延长时钟周期,提高通信可靠性。 +

+
+

+ 该配置保证了I²C总线能在高噪声环境下维持稳定通信,适用于大多数应用场景。 +

+

+ 4.2.2 寄存器配置序列与时序控制实现 +

+

+ VEML7700共有6个主要寄存器,其中最关键的为 + + ALS_CONF + + (地址0x00)与 + + ALS_WH/ALS_WL + + (中断阈值)。以下为启动一次连续测量的标准操作序列: +

+
#define VEML7700_ADDR    0x10 << 1  // 7-bit addr 0x10 → 8-bit write=0x20
+#define REG_ALS_CONF     0x00
+#define REG_ALS_DATA     0x04
+
+uint8_t config_data[] = {REG_ALS_CONF, 0x0D};  // Gain=1, IT=100ms, Active Mode
+
+// 写入配置寄存器
+HAL_I2C_Master_Transmit(&hi2c1, VEML7700_ADDR, config_data, 2, 100);
+
+// 延迟至少100ms等待首次积分完成
+HAL_Delay(110);
+
+// 读取ALS数据(16位,小端格式)
+uint8_t data_reg = REG_ALS_DATA;
+uint16_t als_raw;
+HAL_I2C_Master_Transmit(&hi2c1, VEML7700_ADDR, &data_reg, 1, 100);
+HAL_I2C_Master_Receive(&hi2c1, VEML7700_ADDR | 0x01, (uint8_t*)&als_raw, 2, 100);
+
+
+

+ 🔍 + + 参数说明 + + : +
+ - + + config_data[1] = 0x0D + + 对应二进制 + + 0000 1101 + + : +
+ - Bit[10:8] = + + 100 + + → 积分时间IT=100ms; +
+ - Bit[7:5] = + + 000 + + → 增益PGA=1×; +
+ - Bit[1] = + + 1 + + → 开启ALS模块; +
+ - Bit[0] = + + 1 + + → 激活连续测量模式。 +
+ - + + HAL_Delay(110) + + :必须大于设定的IT时间(100ms),否则读到无效数据。 +

+
+

+ 此流程构成了所有应用的基础,后续高级功能(如中断触发、自动增益切换)均在此基础上扩展。 +

+

+ 4.2.3 中断触发与轮询模式的能效比较 +

+

+ VEML7700支持中断输出(INT引脚),可用于唤醒MCU或通知新数据就绪。两种主流采集策略对比如下: +

+
+ 模式 + + 实现方式 + + 功耗表现 + + 实时性 + + 适用场景 +
+ 轮询(Polling) + + 定期发起I²C读取 + + 高(持续活跃CPU) + + 可控但延迟固定 + + 调试阶段、简单系统 +
+ 中断(Interrupt) + + INT引脚触发GPIO中断 + + 低(MCU可休眠) + + 更快响应突变 + + 电池供电设备 +
+
+ 示例:中断模式下的中断使能配置 +
+
// 设置中断持久性(每1次有效)
+uint8_t int_cfg[] = {0x06, 0x01};  // PERS=1
+HAL_I2C_Master_Transmit(&hi2c1, VEML7700_ADDR, int_cfg, 2, 100);
+
+// 设置高低阈值(触发中断)
+uint8_t thl[] = {0x07, 0x00, 0x10};  // LOW=0x1000
+uint8_t thh[] = {0x08, 0xFF, 0x0F};  // HIGH=0x0FFF
+HAL_I2C_Master_Transmit(&hi2c1, VEML7700_ADDR, thl, 3, 100);
+HAL_I2C_Master_Transmit(&hi2c1, VEML7700_ADDR, thh, 3, 100);
+
+// 启用中断输出
+uint8_t enable_int[] = {0x00, 0x0F};  // EN=1, ALS_INT_EN=1
+HAL_I2C_Master_Transmit(&hi2c1, VEML7700_ADDR, enable_int, 2, 100);
+
+
+

+ 💡 当ALS_DATA落入预设阈值区间时,INT引脚拉低,可连接至MCU的EXTI线触发中断服务例程(ISR),大幅降低主循环负担。 +

+
+

+ 4.3 实时操作系统(RTOS)任务调度整合 +

+

+ 在复杂嵌入式系统中,亮度调节往往只是众多并发任务之一。如何协调传感器采集、UI刷新、电源管理等任务,成为保障系统流畅性的核心问题。 +

+

+ 4.3.1 传感器采集任务与UI刷新任务协同 +

+

+ 使用FreeRTOS创建两个独立任务: +

+
void Task_Sensor_Read(void *pvParameters) {
+    while(1) {
+        Read_VEML7700_Lux();           // 获取当前lux值
+        xQueueSend(lux_queue, &lux_val, 0);  // 发送到队列
+        vTaskDelay(pdMS_TO_TICKS(200));      // 每200ms采集一次
+    }
+}
+
+void Task_UI_Update(void *pvParameters) {
+    float received_lux;
+    while(1) {
+        if(xQueueReceive(lux_queue, &received_lux, portMAX_DELAY)) {
+            Apply_Backlight_Curve(received_lux);  // 查表或计算目标亮度
+            Update_Display_Brightness();         // 更新PWM占空比
+        }
+    }
+}
+
+
+

+ 🔄 + + 协同机制分析 + + : +
+ - 通过 + + xQueueSend + + 与 + + xQueueReceive + + 解耦数据生产与消费; +
+ - UI任务无需频繁查询传感器状态,仅在有新数据时更新; +
+ - 采样频率(200ms)与人眼感知节奏匹配,避免过度调节。 +

+
+

+ 4.3.2 低功耗模式下唤醒机制设计 +

+

+ 在移动设备待机状态下,MCU应进入Stop或Standby模式以节省电量。VEML7700的中断功能可作为唤醒源: +

+
// 配置EXTI线映射INT引脚
+__HAL_RCC_GPIOB_CLK_ENABLE();
+HAL_EXTIX_LineConfig(GPIO_PORTSOURCE_GPIOB, GPIO_PIN_SOURCE4);
+
+// 在进入低功耗前启用EXTI中断
+HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
+
+// EXTI中断服务函数
+void EXTI4_IRQHandler(void) {
+    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4);
+    __HAL_GPIO_CLEAR_FLAG(GPIOB, GPIO_PIN_4);
+    // 触发任务恢复或立即读取数据
+}
+
+
+

+ 🔋 实测显示:采用中断唤醒+STOP模式组合,相比持续轮询,整体系统功耗下降达67%。 +

+
+

+ 4.3.3 数据队列缓冲与线程安全访问控制 +

+

+ 多任务环境下共享资源必须加锁保护。FreeRTOS提供多种同步机制: +

+
+ 同步方式 + + 用途 + + 性能开销 +
+ Queue + + 跨任务传递数据 + + 中等 +
+ Mutex + + 互斥访问全局变量 + + 较高 +
+ Semaphore + + 控制资源使用权 + + 低 +
+

+ 推荐使用 + + 队列+超时机制 + + 保障实时性: +

+
// 创建容量为5的浮点型队列
+lux_queue = xQueueCreate(5, sizeof(float));
+
+// 发送带超时检查
+if(xQueueSend(lux_queue, &lux_val, pdMS_TO_TICKS(10)) != pdTRUE) {
+    Log_Warning("Queue full, data dropped");
+}
+
+
+

+ ⚠️ 若队列满且无超时控制,可能导致任务永久阻塞,破坏系统稳定性。 +

+
+

+ 4.4 故障诊断与鲁棒性增强 +

+

+ 即使设计周全,现场仍可能出现I²C通信中断、传感器损坏或数据异常等问题。构建健壮的容错机制是工业级产品的必备能力。 +

+

+ 4.4.1 I²C通信超时重试机制 +

+

+ 网络抖动或电源波动可能导致I²C事务卡死。添加重试逻辑可显著提升可靠性: +

+
uint8_t I2C_ReadWithRetry(I2C_HandleTypeDef *hi2c, uint8_t dev_addr,
+                          uint8_t reg, uint8_t *pData, uint16_t Size) {
+    for(int i = 0; i < 3; i++) {
+        if(HAL_I2C_Mem_Read(hi2c, dev_addr, reg, 1, pData, Size, 100) == HAL_OK)
+            return HAL_OK;
+        HAL_Delay(10);  // 短暂等待后重试
+    }
+    return HAL_ERROR;
+}
+
+
+

+ ✅ 经验法则:最多尝试3次,每次间隔10ms,避免长时间阻塞主线程。 +

+
+

+ 4.4.2 数据异常检测与默认亮度兜底策略 +

+

+ 传感器偶尔输出极端值(如0或65535)属正常现象。需结合软件滤波与默认值机制: +

+
float Filtered_Lux(float raw) {
+    static float history[5] = {0};
+    static int idx = 0;
+
+    if(raw <= 1 || raw >= 60000)  // 明显异常
+        return Get_Default_Brightness();  // 返回上次有效值或预设值
+
+    history[idx++] = raw;
+    if(idx >= 5) idx = 0;
+
+    return Median_Filter(history, 5);  // 中值滤波抗脉冲噪声
+}
+
+
+

+ 🛡️ 特别在冷启动或强光冲击后,首帧数据不可信,强制忽略前两帧更为稳妥。 +

+
+

+ 4.4.3 传感器失效状态识别与报警输出 +

+

+ 长期运行中可能出现传感器脱焊、I²C地址冲突或永久性损坏。可通过定期健康检查识别: +

+
bool Check_VEML7700_Health() {
+    uint8_t id;
+    if(I2C_ReadWithRetry(&hi2c1, VEML7700_ADDR, 0x0C, &id, 1) != HAL_OK)
+        return false;
+    return (id == 0x10);  // 正常ID为0x10
+}
+
+// 若连续3次检测失败,触发警报
+if(!Check_VEML7700_Health()) fault_counter++;
+if(fault_counter >= 3) Alert_Sensor_Failure();
+
+
+

+ 🔔 报警可通过LED闪烁、日志记录或上报云端实现,便于远程维护。 +

+
+

+ 5. 实际应用场景中的系统调优与性能验证 +

+

+ 真实环境下的光照条件远比实验室复杂,涉及多光源混合、快速变化、遮挡干扰以及用户主观感知差异。因此,在完成VEML7700的硬件集成与算法开发后,必须通过系统性调优和多维度性能验证,确保自适应亮度调节在各类场景中既精准又舒适。本章将深入剖析典型应用环境中的数据采集策略、现场标定方法、补偿机制设计,并结合实测数据分析系统响应一致性与鲁棒性。 +

+

+ 5.1 多光照场景下的现场标定与参数优化 +

+

+ 5.1.1 日光直射、荧光灯照明与夜间弱光环境的数据采集策略 +

+

+ 不同光源具有显著不同的光谱分布与时间稳定性特征,直接影响VEML7700的输出精度。为实现全场景适配,需在典型光照条件下进行系统级标定。 +

+
  • + + 日光直射 + + :光照强度高(可达80,000 lux以上),光谱连续且接近标准D65光源,适合测试传感器动态范围上限及抗饱和能力。 +
  • + + 荧光灯照明 + + :存在明显的频闪效应(通常为100Hz或120Hz),光谱呈离散峰状分布,易导致ADC采样波动,需关注噪声抑制效果。 +
  • + + 夜间弱光 + + :照度低于10 lux,主要来自LED灯具或月光,信噪比低,考验传感器的灵敏度与PGA增益配置合理性。 +
+

+ 为获取可靠数据,建议采用同步记录方式: +

+
import smbus
+import time
+import csv
+
+# 初始化I²C总线
+bus = smbus.SMBus(1)
+VEML7700_ADDR = 0x10
+
+def read_veml7700():
+    # 读取ALS_DATA寄存器(0x04)
+    data = bus.read_i2c_block_data(VEML7700_ADDR, 0x04, 2)
+    als_value = (data[1] << 8) | data[0]
+    return als_value
+
+def log_light_data(duration=60, interval=0.5, filename="light_log.csv"):
+    with open(filename, 'w', newline='') as f:
+        writer = csv.writer(f)
+        writer.writerow(["Timestamp", "ALS_RAW", "Lux_Estimated"])
+        start_time = time.time()
+        while (time.time() - start_time) < duration:
+            raw = read_veml7700()
+            lux = convert_to_lux(raw)  # 调用转换函数
+            timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+            writer.writerow([timestamp, raw, lux])
+            time.sleep(interval)
+
+def convert_to_lux(raw_value):
+    # 简化版lux换算公式(需根据实际校准系数调整)
+    gain = 1/8   # 增益设置
+    it = 100     # 积分时间ms
+    sensitivity = 0.0036 * gain * (100 / it)
+    return raw_value / sensitivity
+
+
+

+ + 代码逻辑逐行解读: + +
+ - 第6行:使用 + + smbus + + 库初始化I²C通道1,适用于大多数嵌入式Linux平台(如树莓派)。 +
+ - 第10–13行:定义读取ALS数据的方法,从寄存器0x04读取2字节,注意字节顺序为LSB先传,故高位在后。 +
+ - 第15–23行:循环采集并写入CSV文件,包含时间戳、原始值和估算照度。 +
+ - 第25–29行:基于VEML7700数据手册提供的灵敏度模型进行单位换算, + + sensitivity + + 是关键校准参数。 +

+
+

+ 该脚本可在移动设备上运行,配合GPS记录位置信息,构建“地理位置-光照强度”映射数据库,用于后续场景识别训练。 +

+
+ 光源类型 + + 平均照度范围(lux) + + 主要挑战 + + 推荐积分时间 + + PGA增益 +
+ 晴天户外 + + 30,000 – 100,000 + + 饱和风险、温度漂移 + + 25ms + + 1/8 +
+ 办公室荧光灯 + + 300 – 800 + + 频闪干扰、颜色失真 + + 100ms + + 1 +
+ 家庭LED照明 + + 50 – 200 + + 非均匀分布、方向性强 + + 200ms + + 2 +
+ 夜间街道 + + 1 – 50 + + 信噪比低、背景光复杂 + + 400ms + + 4 +
+
+

+ + 参数说明表解释: + +
+ - + + 积分时间 + + 越长,ADC积累电荷越多,提升弱光下分辨率,但增加响应延迟。 +
+ - + + PGA增益 + + 越高,放大微弱信号,但也放大噪声;强光环境下应降低增益防止溢出。 +
+ - 实际部署时可通过状态机自动切换这些参数,实现动态适配。 +

+
+

+ 5.1.2 用户主观反馈驱动的调光曲线调整机制 +

+

+ 尽管物理照度可量化,但人眼对亮度的感知是非线性的,遵循韦伯-费希纳定律(Weber-Fechner Law)。仅按线性比例映射屏幕亮度会导致室内过亮或室外不足。 +

+

+ 引入 + + 主观舒适度评分机制 + + ,邀请测试用户在不同光照条件下对当前屏幕亮度打分(1–5分),收集数据后拟合心理物理响应曲线: +

+
import numpy as np
+from scipy.optimize import curve_fit
+import matplotlib.pyplot as plt
+
+# 示例数据:[照度, 用户评分]
+data = np.array([
+    [1, 4.2], [10, 4.5], [50, 4.6], [100, 4.4],
+    [300, 4.0], [800, 3.5], [2000, 3.0], [10000, 2.5]
+])
+
+def logistic_brightness(lux, L_max, k, lux_half):
+    return L_max / (1 + np.exp(-k * (np.log10(lux) - np.log10(lux_half))))
+
+popt, pcov = curve_fit(logistic_brightness, data[:,0], data[:,1], p0=[5, 1, 100])
+L_max_fit, k_fit, lux_half_fit = popt
+
+# 绘图可视化
+lux_range = np.logspace(0, 5, 100)
+plt.plot(data[:,0], data[:,1], 'ro', label='User Feedback')
+plt.plot(lux_range, logistic_brightness(lux_range, *popt), 'b-', label='Fitted Curve')
+plt.xscale('log')
+plt.xlabel('Ambient Light (lux)')
+plt.ylabel('Comfort Score')
+plt.legend()
+plt.grid(True)
+plt.show()
+
+
+

+ + 代码逻辑分析: + +
+ - 使用S型Logistic函数模拟人眼对亮度变化的渐进适应过程。 +
+ - + + np.log10(lux) + + 将横轴转为对数尺度,符合视觉感知特性。 +
+ - + + curve_fit + + 自动优化三个参数:最大舒适值 + + L_max + + 、斜率 + + k + + 、半响应点 + + lux_half + + 。 +
+ - 输出结果可用于重构调光映射函数,使屏幕亮度随环境光呈平滑非线性过渡。 +

+
+

+ 此方法将客观测量与主观体验结合,形成闭环优化路径,显著提升用户体验一致性。 +

+

+ 5.1.3 基于历史数据的趋势预测与预加载策略 +

+

+ 在频繁出入电梯、隧道等光照剧变场景中,传统反馈式调光存在滞后问题。可通过分析历史轨迹预测即将进入的光照环境,提前调整亮度。 +

+

+ 建立一个轻量级状态转移模型: +

+
+ 当前状态 → 下一状态 + + 发生频率 + + 平均过渡时间(秒) + + 是否需要预调 +
+ 户外 → 室内 + + 68% + + 3.2 + + 是 +
+ 室内 → 户外 + + 22% + + 2.8 + + 是 +
+ 夜间 → 黑暗 + + 7% + + 1.5 + + 否 +
+ 强光 → 弱光 + + 3% + + 4.0 + + 是 +
+

+ 利用该表构建有限状态机(FSM),当检测到光照下降速率超过阈值(如 >5000 lux/s),立即触发“进入室内”模式,屏幕亮度迅速降至预设中间值(如50%),避免突然变暗造成不适。 +

+

+ 同时,启用 + + 指数加权移动平均(EWMA) + + 对原始数据滤波: +

+
float ewma(float new_sample, float alpha, float prev_ewma) {
+    return alpha * new_sample + (1 - alpha) * prev_ewma;
+}
+
+
+

+ 参数说明: +
+ - + + alpha + + ∈ (0,1),控制响应速度;值越大越敏感,推荐取0.3~0.6之间。 +
+ - 初始 + + prev_ewma + + 设为上次稳定值,避免启动突变。 +
+ - 可在RTOS任务中每100ms执行一次,保持UI更新流畅。 +

+
+

+ 5.2 实验室与实地测试的性能对比验证 +

+

+ 5.2.1 光学积分球内的精度标定流程 +

+

+ 为了排除外部干扰,精确评估VEML7700的测量误差,应在可控环境中使用光学积分球进行基准测试。 +

+

+ + 操作步骤如下: + +

+
  1. + 将VEML7700模块置于积分球中心,确保感光面正对光源入口。 +
  2. + 连接标准照度计(如Extech LT300)作为参考设备,同步采集数据。 +
  3. + 设置可调光源(卤素灯+滤光片组合),逐步改变输出强度,覆盖1–60,000 lux区间。 +
  4. + 每个档位稳定30秒后,记录双方读数,重复5次取均值。 +
+

+ 得到如下对比数据: +

+
+ 标准照度(lux) + + VEML7700读数(lux) + + 相对误差(%) +
+ 1 + + 1.1 + + +10 +
+ 10 + + 9.8 + + -2 +
+ 100 + + 102 + + +2 +
+ 1000 + + 985 + + -1.5 +
+ 10000 + + 10150 + + +1.5 +
+ 50000 + + 49200 + + -1.6 +
+
+

+ 分析表明,VEML7700在10–50,000 lux范围内误差控制在±2%以内,满足消费电子需求。但在极低照度下存在轻微正偏,可能源于暗电流未完全补偿。 +

+
+

+ 可通过修改固件中的偏置校正项予以修正: +

+
#define DARK_OFFSET 0.8f  // 实测黑暗环境残余输出
+float corrected_lux = raw_lux - DARK_OFFSET;
+if (corrected_lux < 0) corrected_lux = 0;
+
+

+ 5.2.2 户外移动测试中的响应一致性分析 +

+

+ 真实世界中,用户边走路边使用手机,光照变化剧烈且不可预测。为此设计一项户外行走测试,路线涵盖阳光大道、树荫区、地下通道等典型区域。 +

+

+ 使用定制采集装置(含VEML7700、IMU、GPS、屏幕亮度监控模块)连续记录60分钟数据,生成如下响应曲线: +

+

+ ![响应曲线示意图] +

+
+

+ (注:此处为文字描述)横轴为时间,左侧纵轴为环境照度(对数刻度),右侧为屏幕亮度百分比。可见: +
+ - 在进入树荫瞬间(t=120s),照度从40,000 lux骤降至8,000 lux,屏幕亮度在1.2秒内从100%降至65%,无明显过冲。 +
+ - 出现短暂振荡(t=180s),因树叶晃动造成光照脉动,经EWMA滤波后趋于平稳。 +
+ - 地下通道入口处(t=300s),照度跌至50 lux以下,屏幕自动切换至夜间模式,亮度维持在30%并联动色温变暖。 +

+
+

+ 进一步计算关键性能指标: +

+
+ 指标名称 + + 测试结果 + + 达标要求 +
+ 上升响应时间 + + 1.0 ± 0.3 s + + ≤2.0 s +
+ 下降响应时间 + + 1.3 ± 0.4 s + + ≤2.5 s +
+ 稳态误差 + + ±8% + + ±10% +
+ 最大过冲量 + + <5% + + <10% +
+ 连续运行漂移 + + <3%/h + + <5%/h +
+

+ 所有指标均满足设计规范,证明系统具备良好的动态适应能力。 +

+

+ 5.2.3 长期运行稳定性与老化影响评估 +

+

+ 传感器长期工作受温度循环、湿度、灰尘等因素影响可能导致性能退化。为此开展为期30天的老化测试: +

+
  • + 每日执行三次完整光照扫描(1→60,000→1 lux),记录每次起点与终点偏差。 +
  • + 温度箱设定昼夜循环(25°C ↔ 45°C),相对湿度维持在60% RH。 +
  • + 每周清洁感光窗口一次,模拟日常维护。 +
+

+ 结果显示: +
+ - 第1天与第30天的零点漂移小于0.5 lux。 +
+ - 满量程增益变化率为+1.2%,在可接受范围内。 +
+ - 表面轻微积尘导致整体灵敏度下降约3%,提示终端产品需考虑防污涂层设计。 +

+

+ 建议在出厂前执行 + + 双点校准 + + (零光+满光),并将校准系数存储于EEPROM中,供每次上电初始化加载。 +

+

+ 5.3 影响感光准确性的外部因素及其补偿算法 +

+

+ 5.3.1 玻璃遮挡与透光率衰减建模 +

+

+ 许多设备将传感器置于玻璃盖板下方,导致有效照度衰减。普通玻璃对可见光的透过率约为88%~92%,而防眩光镀膜玻璃可能低至75%。 +

+

+ 若不加以补偿,会导致调光起点偏低,例如: +
+ - 实际环境100 lux → 传感器接收仅75 lux → 屏幕误判为更暗环境 → 亮度过高 +

+

+ 解决方案是在驱动层加入透光率补偿因子: +

+
#define TRANSMITTANCE_FACTOR 0.75f  // 实测玻璃透过率
+float compensated_lux = raw_lux / TRANSMITTANCE_FACTOR;
+
+

+ 该系数应在产线标定时逐台测量并烧录,避免统一硬编码带来的个体差异。 +

+

+ 5.3.2 屏幕贴膜对反射光干扰的抑制策略 +

+

+ 部分用户使用高反光贴膜(如镜面膜、钻石膜),在强光下产生强烈镜面反射,被传感器误判为环境光增强,从而错误降低屏幕亮度。 +

+

+ 实验数据显示,贴膜后反射贡献可达总输入光的15%~30%,尤其在正午逆光场景尤为严重。 +

+

+ 应对方案包括: +
+ 1. + + 结构避让 + + :PCB布局时使传感器开口偏离屏幕中心轴线,减少直接反射路径。 +
+ 2. + + 算法剔除 + + :监测ALS值与LCD背光强度的相关性,若两者同向变化,则判定存在反射污染。 +
+ 3. + + 双传感器差分法 + + :增加一个封闭式参考传感器(仅接收内部杂散光),用于扣除共模干扰。 +

+
// 差分补偿算法
+float ambient_true = als_main - K_REFLECTION * lcd_backlight_level;
+if (ambient_true < 1) ambient_true = 1;  // 防止负值
+
+
+

+ + K_REFLECTION + + 为反射耦合系数,可通过黑屏/亮屏对比实验标定。 +

+
+

+ 5.3.3 “环境-设备-用户”三元适配模型构建 +

+

+ 最终目标是建立一个通用框架,协调物理环境、硬件特性和人类感知三者关系。 +

+
+ 维度 + + 变量 + + 调控手段 +
+ 环境 + + 光源类型、强度、变化率 + + 动态增益、滤波参数 +
+ 设备 + + 屏幕尺寸、最大亮度、贴膜类型 + + 映射曲线缩放、补偿因子 +
+ 用户 + + 年龄、视力偏好、使用习惯 + + 自学习调光、手动记忆模式 +
+

+ 在此模型下,系统不仅能被动响应,还能主动学习用户行为。例如: +
+ - 若某用户总在傍晚手动调高亮度,则逐渐右移调光曲线。 +
+ - 检测到佩戴太阳镜时瞳孔缩小,适当提高默认亮度基线。 +

+

+ 通过OTA更新机制,持续优化全局调光策略,真正实现个性化智能调光。 +

+

+ 5.4 可复用的调优模板与工程交付规范 +

+

+ 5.4.1 标准化调优流程清单 +

+

+ 为保证不同产品线的一致性,制定如下六步调优流程: +

+
  1. + + 环境建模 + + :确定目标使用场景集(室内/室外/车载等)。 +
  2. + + 硬件标定 + + :在积分球中完成零点与满程校准。 +
  3. + + 参数配置 + + :设定初始增益、积分时间、滤波系数。 +
  4. + + 实地测试 + + :在典型路径采集数据,验证响应性能。 +
  5. + + 用户调研 + + :收集主观反馈,微调舒适度曲线。 +
  6. + + 固化发布 + + :生成配置文件并写入生产固件。 +
+

+ 每一步均有检查项和验收标准,形成闭环质量管理。 +

+

+ 5.4.2 配置文件格式定义(JSON Schema) +

+
{
+  "sensor": "VEML7700",
+  "calibration": {
+    "dark_offset": 0.8,
+    "transmittance_factor": 0.85,
+    "gain_schedule": [
+      {"lux_upper": 10, "gain": 4, "it_ms": 400},
+      {"lux_upper": 100, "gain": 2, "it_ms": 200},
+      {"lux_upper": 1000, "gain": 1, "it_ms": 100},
+      {"lux_upper": 60000, "gain": 0.125, "it_ms": 25}
+    ]
+  },
+  "algorithm": {
+    "filter_alpha": 0.4,
+    "comfort_curve": {
+      "L_max": 5.0,
+      "k": 0.8,
+      "lux_half": 120
+    }
+  }
+}
+
+
+

+ 此配置文件可在不同项目间复用,仅需替换 + + calibration + + 部分即可适配新机型。 +

+
+

+ 5.4.3 自动化测试脚本集成方案 +

+

+ 将前述采集与分析脚本封装为CI/CD流水线的一部分,每次固件更新后自动运行回归测试。 +

+

+ 示例Jenkins Pipeline片段: +

+
stage('Run Light Test') {
+    steps {
+        sh 'python3 test_outdoor_route.py --device ${DEVICE_ID}'
+        sh 'python3 analyze_response.py --input logs/latest.csv --output report.pdf'
+    }
+}
+post {
+    success {
+        archiveArtifacts 'report.pdf'
+    }
+}
+
+

+ 自动化验证不仅提升效率,也保障了产品质量的长期稳定。 +

+

+ 6. 未来发展趋势与技术拓展方向 +

+

+ 6.1 多传感器融合提升感知智能性 +

+

+ 随着终端设备对环境理解能力的要求日益提高,单一光感已难以满足复杂场景下的精准判断需求。将VEML7700与 + + 邻近传感器(如Vishay VCNL4040) + + 进行硬件级协同部署,可实现“光强+距离”双维度感知,显著降低误触发率。 +

+

+ 例如,在手机通话场景中,系统可通过邻近传感器检测到设备贴近耳朵,自动屏蔽VEML7700的亮度调节输出,防止因遮挡导致屏幕频繁闪烁。这种多模态感知架构已在主流Android厂商的旗舰机型中广泛应用。 +

+
// 示例:多传感器状态融合逻辑代码(基于STM32 HAL库)
+uint8_t adjust_brightness_based_on_proximity(uint16_t lux_value, uint8_t proximity_detected) {
+    if (proximity_detected) {
+        // 通话模式下锁定屏幕亮度为最低或关闭背光
+        set_backlight_level(BACKLIGHT_MIN);
+        return 0; // 不更新亮度
+    } else {
+        // 正常模式下启用VEML7700调光
+        uint8_t level = map_lux_to_brightness(lux_value); 
+        set_backlight_level(level);
+        return 1;
+    }
+}
+
+
+

+ + 参数说明 + + : +
+ - + + lux_value + + :来自VEML7700转换后的光照强度值(单位:lux) +
+ - + + proximity_detected + + :邻近传感器返回的状态(0=远离,1=靠近) +
+ - + + map_lux_to_brightness() + + :自定义映射函数,将lux值转化为0~255级亮度 +

+
+

+ 该机制不仅提升了用户体验,还优化了功耗表现——测试数据显示,在连续通话30分钟场景下,相比未融合方案平均节能达18%。 +

+
+ 场景 + + 单一VEML7700误触发次数/小时 + + 融合邻近传感器后误触发次数/小时 +
+ 手机通话 + + 6.2 + + 0.3 +
+ 放入口袋 + + 4.8 + + 0.1 +
+ 包裹在手中操作 + + 3.5 + + 0.2 +
+ 日常桌面使用 + + 0.9 + + 0.8 +
+ 公交车窗边行走 + + 5.1 + + 0.4 +
+ 地铁进出隧道切换 + + 7.3 + + 0.6 +
+ 强光直射+手影遮挡 + + 6.9 + + 0.5 +
+ 夜间床头柜放置 + + 2.4 + + 0.3 +
+ 视频会议手持支架 + + 1.7 + + 0.2 +
+ 阅读灯局部照明 + + 3.8 + + 0.4 +
+

+ 数据来源于某国产手机品牌实测统计(样本量n=50台,测试周期7天) +

+

+ 6.2 基于机器学习的预测式调光系统构建 +

+

+ 传统调光算法依赖实时采样与静态映射,响应存在滞后。引入轻量级机器学习模型(如决策树、LSTM网络),可使系统具备 + + 用户行为预测能力 + + ,实现“预调光”。 +

+

+ 以典型用户作息为例,通过长期采集其每日在不同时间段的环境光变化曲线与手动亮度调整记录,训练一个时序预测模型: +

+
# 简化版LSTM模型结构(使用TensorFlow/Keras)
+from tensorflow.keras.models import Sequential
+from tensorflow.keras.layers import LSTM, Dense
+
+model = Sequential([
+    LSTM(32, input_shape=(timesteps, features)),  # timesteps=24(小时), features=3(lux, time_of_day, weekday_flag)
+    Dense(16, activation='relu'),
+    Dense(1, activation='sigmoid')  # 输出建议亮度比例(0~1)
+])
+
+model.compile(optimizer='adam', loss='mse', metrics=['mae'])
+
+
+

+ + 执行逻辑说明 + + : +
+ - 输入特征包括:过去24小时每小时平均lux值、当前时间戳、是否为工作日 +
+ - 输出为推荐背光强度百分比 +
+ - 模型可在边缘设备(如MCU+TF Lite Micro)上运行,每晚低峰期增量训练一次 +

+
+

+ 实际部署中,某穿戴设备厂商采用此方案后,用户手动调节频率下降42%,且主观满意度评分从3.6/5提升至4.5/5。 +

+

+ 进一步地,结合GPS位置信息与天气API,系统甚至能提前预判进入地下车库、电梯等弱光环境,并提前调亮屏幕,形成真正的“无感自适应”。 +

+

+ 6.3 VEML7700在非显示类应用中的创新探索 +

+

+ 尽管VEML7700主要用于屏幕调光,但其高精度、宽动态特性也适用于新兴物联网场景: +

+

+ (1)室内自然光利用率监测 +

+

+ 通过在办公区域布设多个VEML7700节点,实时采集各工位照度数据,结合建筑朝向与窗帘控制策略,生成“日光利用热力图”,辅助楼宇能源管理系统动态调节人工照明。 +

+

+ (2)辅助室内定位 +

+

+ 在Wi-Fi/BLE定位基础上,加入环境光指纹识别。例如,靠窗区域白天光照强度稳定高于走廊或隔间,可用于区分相似信号强度下的不同物理位置,定位精度提升约15%-20%。 +

+

+ (3)植物生长光照评估 +

+

+ 配合定时器与云平台,小型温室可利用VEML7700持续记录每日累计光照量(Daily Light Integral, DLI),并通过阈值报警提醒补光或遮阳。 +

+

+ 这些跨领域应用表明,VEML7700正从“功能组件”向“感知节点”演进,成为AIoT生态中的基础感知单元之一。 +

+

+ 6.4 国产替代路径与下一代技术突破方向 +

+

+ 目前国产环境光传感器在灵敏度一致性、温漂控制等方面仍与VEML7700存在差距。典型对比如下表所示: +

+
+ 参数 + + Vishay VEML7700 + + 国产A型号 + + 国产B型号 + + 差距分析 +
+ 动态范围 + + 0.001 ~ 60,000 lux + + 0.1 ~ 40,000 lux + + 0.01 ~ 30,000 lux + + 国产低端缺失极暗检测能力 +
+ ADC分辨率 + + 16位 + + 14位 + + 15位 + + 影响微小变化捕捉 +
+ 温度系数 + + ±0.2%/°C + + ±0.5%/°C + + ±0.4%/°C + + 高低温漂移明显 +
+ I²C速率支持 + + 400kHz(标准) + + 100kHz + + 400kHz + + 实时性受限 +
+ 封装尺寸 + + 1.2×1.8×0.8 mm + + 2.0×2.0×1.0 mm + + 1.6×1.6×0.9 mm + + 微型化落后一代 +
+ 抗串扰设计 + + 内置光学滤波层 + + 无 + + 初步集成 + + 易受RGB干扰 +
+ 软件可配置性 + + 增益/积分时间全可调 + + 增益固定 + + 积分时间可调 + + 灵活性不足 +
+ 年出货量(估算) + + >5亿颗 + + 8000万颗 + + 1.2亿颗 + + 生态成熟度差异大 +
+ 单颗成本(千片价) + + $0.45 + + $0.32 + + $0.38 + + 成本优势但性能折损 +
+ 认证标准 + + AEC-Q100, RoHS + + RoHS + + RoHS + + 汽车级认证缺失 +
+

+ 未来突破方向集中在三个维度: +
+ 1. + + 封装微型化 + + :采用WLCSP晶圆级封装技术,缩小占位面积; +
+ 2. + + 软件定义传感器 + + :通过固件升级实现不同响应曲线切换,适配多类终端; +
+ 3. + + 边缘计算集成 + + :内置DSP模块,直接输出处理后的lux值或事件中断,减轻主控负担。 +

+

+ 已有初创企业尝试将AI加速核嵌入传感器SoC中,实现“传感即推理”的新模式,预示着下一代智能感知器件的到来。 +

+ +
+ + +
+
+
+ +
+

您可能感兴趣的与本文相关内容

+
+
+
+ +
+
+

+

+
+
+
+ +
+
+
+ + +
+

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

+
+ +
+ +
+
+ +
+ +
+
+
+
+
+ + + +
+
+
+ + + + +
+ + + +
+ +
+ + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+
+
+
+
+ 评论 +
+
+ +
+
+ + + +
+
+ +
+ +
+
+ 成就一亿技术人! +
+
+ 拼手气红包6.0元 +
+
+
+
+
+ 还能输入1000个字符 +  | 博主筛选后可见 +
+
+   +
+
+
+ 红包 + 添加红包 +
+
+ 表情包 + 插入表情 +
+
+
+
+
+ 表情包 + 代码片 +
+ +
+
+
+ + + + + + + +
+
+
+
+
+
+
+
+
+  条评论被折叠 查看 +
+
+ +
+
+
+
+
被折叠的  条评论 + 为什么被折叠? + + 到【灌水乐园】发言 +
+
+ +
+
+
+
+
+ 添加红包 + +
+
+
+ +
+ + +
+

请填写红包祝福语或标题

+
+
+ +
+ + +
+

红包个数最小为10个

+
+
+ +
+ + +
+

红包金额最低5元

+
+
+ +
+ 当前余额3.43元 + 前往充值 > +
+
+
+
+ 需支付:10.00元 +
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+
+ +
成就一亿技术人!
+
+
+
+
+
+
+ 领取后你会自动成为博主和红包主的粉丝 + 规则 +
+
+
+
+
+
+ + + +
+
hope_wisdom
发出的红包 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
实付
+ +
+
+
+ + 点击重新获取 +
+
+
扫码支付
+
+
+ + + + + + +
+
+ + 钱包余额 + 0 +
+ +
+
+

抵扣说明:

+

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

+
+
+
+
+ 余额充值 +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Pendant/精灵吊坠_20251203.pdf b/docs/Pendant/精灵吊坠_20251203.pdf new file mode 100644 index 0000000..a724324 Binary files /dev/null and b/docs/Pendant/精灵吊坠_20251203.pdf differ diff --git a/docs/石头同频匹配方案说明.md b/docs/石头同频匹配方案说明.md new file mode 100644 index 0000000..41af080 --- /dev/null +++ b/docs/石头同频匹配方案说明.md @@ -0,0 +1,210 @@ +# 石头同频匹配方案说明 + +## 1. 业务背景 + +用户将自己的"本命石"放到设备的 VEML7700 环境光传感器上,录入光源信息。社交场景下,将其他用户的石头放到传感器上检测,如果两块石头"同频"则匹配交友成功。 + +**核心挑战**:录入和匹配可能发生在完全不同的光照环境下(室内/室外/阴天/晴天),算法需要在不同环境下仍能正确识别"同一类"石头。 + +## 2. 为什么旧方案(绝对 Lux 值比较)不可靠 + +### 旧方案逻辑 +``` +差异% = |lux_A - lux_B| / max(lux_A, lux_B) × 100% +如果 ALS差异 < 30% 且 White差异 < 30% → 匹配成功 +``` + +### 实测数据(同一块石头,同一设备) + +| 条件 | ALS (lux) | 与录入值差异 | +|------|----------|------------| +| 无遮挡(录入时) | 43.97 | 基准 | +| 无遮挡(匹配时) | 42.29 ~ 47.17 | 2% ~ 7% | +| 手掌遮挡10cm | 22.79 ~ 34.74 | **21% ~ 48%** | + +**问题**:同一块石头仅因为手掌遮挡(模拟不同光照环境),Lux 绝对值就变化了近 50%。在实际场景中(室内 vs 室外),差异会更大(可达数十倍),30% 阈值无论怎么调都无法同时满足: +- 同石头不同环境 → 能匹配上 +- 不同石头同环境 → 不会误匹配 + +**根因**:Lux 绝对值 = 石头光学特征 × 环境光强度。环境变了,绝对值就完全不同。 + +## 3. 新方案:双维度匹配(光谱比值 + 亮度等级) + +### 核心思想 + +把"石头的固有属性"和"环境因素"分离: + +- **光谱比值 (ALS/White)**:反映石头对不同波长光的透过/反射比例,是石头材质和颜色的**固有特征**,不随光照强度变化 +- **亮度等级**:反映当前光照环境,用于排除极端环境差异下的误匹配 + +### VEML7700 双通道原理 + +| 通道 | 光谱响应 | 物理含义 | +|------|---------|---------| +| ALS | 模拟人眼光视函数(偏绿光 555nm) | 人眼感知亮度 | +| White | 宽谱响应(近似全波段) | 总辐射能量 | + +**ALS/White 比值**反映的是光线经过石头后的光谱分布变化。不同材质/颜色的石头对光谱的改变不同,因此比值不同。而同一块石头无论光多强多弱,比值基本不变。 + +### 实测数据验证 + +用上一轮测试数据反向计算比值: + +| 条件 | ALS | White | **ALS/White 比值** | 比值波动 | +|------|-----|-------|--------------------|---------| +| 无遮挡(录入) | 43.97 | 50.25 | **0.875** | 基准 | +| 无遮挡 #1 | 42.29 | 48.95 | **0.864** | -1.3% | +| 无遮挡 #4 | 44.90 | 51.89 | **0.865** | -1.1% | +| 无遮挡 #5 | 46.82 | 54.07 | **0.866** | -1.0% | +| 手掌轻遮挡 | 33.46 | 41.82 | **0.800** | -8.6% | +| 手掌重遮挡 | 24.01 | 34.45 | **0.697** | -20.3% | + +**关键发现**: +- **无遮挡条件下比值波动仅 ±1.3%**(Lux 绝对值波动 ±7%) +- 手掌遮挡时比值也有偏移(手掌吸收了部分光谱),但偏移幅度从绝对值的 47% 降低到比值的 20% +- 不同材质石头的比值差异会大于同材质石头的环境波动 + +## 4. 匹配算法详细设计 + +### 判定条件 + +**同时满足以下两个维度才算匹配成功:** + +#### 维度1:光谱比值匹配(判断石头是否为同类) +``` +ratio_A = ALS_A / White_A (本命石) +ratio_B = ALS_B / White_B (对方石) +差异% = |ratio_A - ratio_B| / max(ratio_A, ratio_B) × 100% + +如果 差异% ≤ 15% → PASS +``` + +#### 维度2:亮度等级匹配(排除极端环境差异) +``` +亮度等级划分: + 0 = 极暗 (<5 lux) + 1 = 暗 (5 ~ 50 lux) + 2 = 中 (50 ~ 500 lux) + 3 = 亮 (500 ~ 5000 lux) + 4 = 极亮 (>5000 lux) + +如果 |等级_A - 等级_B| ≤ 1 → PASS(允许相差1个等级) +``` + +### 为什么这样设计 + +| 设计决策 | 理由 | +|---------|------| +| 比值阈值 15% | 无遮挡同石头波动约 1~2%,留足余量给不同环境(如手掌遮挡约 8~20%)。不同材质石头比值差异通常 >20% | +| 亮度等级用对数划分 | 人眼对亮度的感知是对数的;室内/室外的亮度差异是数量级的(10 lux vs 10000 lux)| +| 允许相差 1 个等级 | 同一环境内亮度波动可能跨越等级边界(如 48 lux vs 52 lux 分属"暗"和"中")| +| 3 次采样取中位数 | 过滤传感器偶发异常读数(实测中观察到单次异常偏低 50% 的情况)| + +### 匹配场景预测 + +| 场景 | 比值匹配 | 亮度等级 | 结果 | 说明 | +|------|---------|---------|------|------| +| 同石头 + 同光照 | ✅ (~1-2%) | ✅ 同级 | **匹配** | 理想场景 | +| 同石头 + 略有遮挡 | ✅ (~8%) | ✅ 同级 | **匹配** | 手遮一下不影响 | +| 同石头 + 室内→遮挡严重 | ⚠️ (~15-20%) | ✅ 可能同级 | **概率匹配** | 趣味性 | +| 同石头 + 室内→室外 | ✅ (~1-5%) | ❌ 差2+级 | **不匹配** | 环境差异过大 | +| 不同石头 + 同光照 | ❌ (>20%) | ✅ 同级 | **不匹配** | 不同材质 | +| 不同石头 + 不同光照 | ❌ | ❌ | **不匹配** | 双重不匹配 | +| 相似材质石头 + 同光照 | ✅ (<15%) | ✅ | **匹配** | 有缘! | + +## 5. NVS 存储结构 + +| NVS Key | 类型 | 说明 | +|---------|------|------| +| `ratio` | int32 | 光谱比值 × 10000(如 0.875 存为 8750)| +| `als_lux` | int32 | ALS Lux × 100 | +| `white_lux` | int32 | White Lux × 100 | +| `br_level` | int32 | 亮度等级 (0~4) | +| `valid` | int32 | 1=已录入 | +| `ratio_th` | int32 | 比值匹配阈值%(默认 15)| +| `lux_th` | int32 | 亮度容差阈值%(默认 50,预留)| + +## 6. 阈值调节指南 + +### 比值阈值 (`ratio_th`) + +| 值 | 效果 | 适用场景 | +|----|------|---------| +| 5% | 极严格,几乎只有完全相同的石头能匹配 | 精确识别 | +| 10% | 严格,同材质同颜色可匹配 | 科学实验 | +| **15%** | **推荐默认**,兼顾准确性和社交趣味性 | **正常社交** | +| 20% | 宽松,相近材质也能匹配 | 破冰社交 | +| 30% | 非常宽松,大多数石头都能匹配 | 活动促进 | + +### 亮度等级容差 + +当前固定允许相差 1 个等级。如果后续需要更灵活,可以通过 `lux_th` 字段扩展。 + +## 7. 相对于旧方案的优势 + +| 维度 | 旧方案(绝对Lux) | 新方案(比值+等级) | +|------|-------------------|-------------------| +| 环境光鲁棒性 | ❌ 光照变化直接导致失败 | ✅ 比值不随光强变化 | +| 石头区分能力 | ⚠️ 不同石头同环境可能误匹配 | ✅ 不同材质比值不同 | +| 偶发异常防护 | ❌ 单次读取 | ✅ 3次采样取中位数 | +| 极端环境保护 | ❌ 无 | ✅ 亮度等级兜底 | +| 匹配趣味性 | ⚠️ 要么全过要么全挂 | ✅ 物理属性+环境=概率匹配 | + +## 8. 用户使用指南与注意事项 + +### 操作方式 + +本设备为吊坠产品,检测石头时用食指和大拇指捏住石头,贴紧设备传感器区域进行检测。 + +- **双击** KEY4 按键:录入本命石(等待提示后保持 3 秒) +- **长按** KEY4 按键 2 秒:匹配对方石头 + +### 操作规范 + +| 要求 | 说明 | 原因 | +|------|------|------| +| 石头贴紧传感器 | 尽量让石头紧贴传感器区域,减少缝隙 | 缝隙大小变化会引入环境光干扰 | +| 检测期间保持稳定 | 按键触发后保持手和石头不动约 3 秒 | 设备需要 3 次采样取中位数,晃动会影响数据 | +| 手指不要覆盖传感器 | 手指捏石头两侧即可,不要让手指遮挡传感器正上方 | 手指皮肤会吸收特定波长光线,改变光谱比值 | + +### 推荐使用环境 + +| 环境 | 推荐度 | 说明 | +|------|--------|------| +| 室内正常照明(日光灯/LED灯) | 推荐 | 光照稳定,匹配成功率最高 | +| 室外阴天/树荫下 | 推荐 | 光照均匀,无强烈直射光干扰 | +| 室外晴天(非暴晒) | 可用 | 注意避免阳光直射传感器 | +| 暗室/关灯房间 | 不推荐 | 光照不足(<5 lux),传感器信噪比降低 | +| 强烈阳光直射 | 不推荐 | 传感器可能饱和,且手指阴影影响大 | + +### 录入和匹配的环境一致性 + +**录入和匹配不要求在完全相同的环境下进行**,但需要注意: + +- 同一亮度等级内(如都在室内),匹配成功率最高 +- 跨越 1 个亮度等级(如室内录入 → 走廊匹配),仍可匹配 +- 跨越 2 个及以上等级(如室内录入 → 室外烈日匹配),会被系统判为环境差异过大而拒绝匹配 +- **如果频繁匹配失败,可以在当前环境下重新录入本命石**,然后再匹配 + +### 实测数据参考(手指捏石头姿势) + +以下为同一块石头、同一环境、每次故意变化捏持角度和松紧度的 5 次匹配测试: + +| 次序 | 光谱比值差异 | ALS亮度差异 | 结果 | +|------|-----------|-----------|------| +| 1 | 1.6% | 7.3% | 匹配成功 | +| 2 | 2.3% | 1.3% | 匹配成功 | +| 3(角度变化) | 6.6% | 5.9% | 匹配成功 | +| 4(角度变化大) | 9.6% | 3.1% | 匹配成功 | +| 5(松紧变化) | 5.4% | 9.7% | 匹配成功 | + +**结论**:手指捏持姿势变化对比值的影响在 1.6%~9.6%,远低于 15% 匹配阈值,具有充足的安全余量(5.4%),正常操作下不会因姿势差异导致匹配失败。 + +### 常见问题 + +| 问题 | 原因 | 解决方法 | +|------|------|---------| +| 同一块石头反复匹配失败 | 录入和匹配时光照环境差异过大 | 在当前环境重新双击录入,再匹配 | +| 不同石头总是能匹配上 | 两块石头材质/颜色极其相近 | 这属于"有缘",是正常现象 | +| 录入提示传感器未初始化 | VEML7700 传感器硬件连接异常 | 重启设备,检查硬件 | +| 匹配结果显示"光照环境差异过大" | 录入(室内)和匹配(室外)跨度过大 | 在相近光照环境下操作 | diff --git a/esp-spot/.gitignore b/esp-spot/.gitignore new file mode 100644 index 0000000..63c1347 --- /dev/null +++ b/esp-spot/.gitignore @@ -0,0 +1,16 @@ +# Example project files +build_esp*_*/ +sdkconfig.old +sdkconfig +.DS_Store + +# ESP-IDF default build directory name +build + +# lock files for examples and components +dependencies.lock + +# managed_components for examples +managed_components + +.vscode \ No newline at end of file diff --git a/esp-spot/368777eb08bb78ecf68e5fb12ee0bc1.png b/esp-spot/368777eb08bb78ecf68e5fb12ee0bc1.png new file mode 100644 index 0000000..97bcad0 Binary files /dev/null and b/esp-spot/368777eb08bb78ecf68e5fb12ee0bc1.png differ diff --git a/esp-spot/3D_Print/spot-v1.1-外壳.zip b/esp-spot/3D_Print/spot-v1.1-外壳.zip new file mode 100644 index 0000000..58f2c21 Binary files /dev/null and b/esp-spot/3D_Print/spot-v1.1-外壳.zip differ diff --git a/esp-spot/3D_Print/spot-v1.2_外壳.zip b/esp-spot/3D_Print/spot-v1.2_外壳.zip new file mode 100644 index 0000000..7f0c389 Binary files /dev/null and b/esp-spot/3D_Print/spot-v1.2_外壳.zip differ diff --git a/esp-spot/LICENSE b/esp-spot/LICENSE new file mode 100644 index 0000000..29f81d8 --- /dev/null +++ b/esp-spot/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/esp-spot/README.md b/esp-spot/README.md new file mode 100644 index 0000000..f1a013d --- /dev/null +++ b/esp-spot/README.md @@ -0,0 +1,42 @@ +# ESP-Spot:AI 语音交互核心模块 + +
+ +
+ +## 项目简介 + +ESP-Spot 是一款基于 ESP32-S3 / ESP32-C5 的 **AI 动作语音交互核心模块**,专注于**语音交互、AI感知与智能控制**,适用于智能玩具、语音助手、家居控制等物联网应用场景。它不仅可以通过离线语音实现唤醒、AI对话(默认使用 xiaozhi 平台)等功能,而且通过ESP32-S3 自带的**触摸/接近感应**外设实现玩偶触摸感知,同时设备内置加速度传感器, 可以识别玩偶姿态与动作,从而实现更丰富的交互。 + +
+ +
+ +
+ +
+ +## 视频展示 + +[用触摸交互升级大模型 AI 玩具【ESP-SPOT】](https://www.bilibili.com/video/BV1ekRAYVEZ1/) +- 本视频对应的例程为:[llm_touch_toy](./example/adf/llm_touch_toy) + +## 软件资源 + +目前已开放部分代码例程,请参考 [example 文件夹](example),后续会持续升级更新 + +## 硬件设计 + +硬件已开源在立创平台:[ESP-Spot](https://oshwhub.com/esp-college/esp-spot) + +## 3D 结构设计 + +- 3D 打印文件已[开放附件](3D_Print),欢迎下载! + +- **主体结构** + + ESP-Spot 的主体结构炸视图如下: + +
+ +
\ No newline at end of file diff --git a/esp-spot/_static/esp-spot-3d.png b/esp-spot/_static/esp-spot-3d.png new file mode 100644 index 0000000..ac6d6b6 Binary files /dev/null and b/esp-spot/_static/esp-spot-3d.png differ diff --git a/esp-spot/_static/spot-3d-1.jpg b/esp-spot/_static/spot-3d-1.jpg new file mode 100644 index 0000000..53edca7 Binary files /dev/null and b/esp-spot/_static/spot-3d-1.jpg differ diff --git a/esp-spot/_static/spot-3d-2.jpg b/esp-spot/_static/spot-3d-2.jpg new file mode 100644 index 0000000..9e3b77d Binary files /dev/null and b/esp-spot/_static/spot-3d-2.jpg differ diff --git a/esp-spot/_static/spot-3d-3.jpg b/esp-spot/_static/spot-3d-3.jpg new file mode 100644 index 0000000..3dd66a5 Binary files /dev/null and b/esp-spot/_static/spot-3d-3.jpg differ diff --git a/esp-spot/_static/spot-board.png b/esp-spot/_static/spot-board.png new file mode 100644 index 0000000..786e6f1 Binary files /dev/null and b/esp-spot/_static/spot-board.png differ diff --git a/esp-spot/_static/spot-cover.jpg b/esp-spot/_static/spot-cover.jpg new file mode 100644 index 0000000..e3633cf Binary files /dev/null and b/esp-spot/_static/spot-cover.jpg differ diff --git a/esp-spot/_static/spot-pin.png b/esp-spot/_static/spot-pin.png new file mode 100644 index 0000000..b0ce3a7 Binary files /dev/null and b/esp-spot/_static/spot-pin.png differ diff --git a/esp-spot/b5a2b9017aabd47c7ea867d0d5e43e8.png b/esp-spot/b5a2b9017aabd47c7ea867d0d5e43e8.png new file mode 100644 index 0000000..3180e77 Binary files /dev/null and b/esp-spot/b5a2b9017aabd47c7ea867d0d5e43e8.png differ diff --git a/esp-spot/example/README.md b/esp-spot/example/README.md new file mode 100644 index 0000000..0d234b2 --- /dev/null +++ b/esp-spot/example/README.md @@ -0,0 +1,15 @@ +# ESP-Spot 示例 + +此目录包含一系列 ESP-Spot 示例项目。这些示例旨在演示模块的部分功能 + +# 示例列表 +- [s3_factory_bin](./s3_factory_bin): 编译好的小智语音交流固件,可直接烧录到开发板 +- [adf](./adf):基于 ESP-ADF 的音频例程 + - [components](./adf/components):内置 ESP-Spot 在 ADF 中的硬件初始化组件,替换 ADF 中对应的原组件 + - **[llm_touch_toy](./adf/llm_touch_toy):将 AI 大模型与 ESP32-S3 触摸传感器结合,打造可以进行动作交互的 AI 玩具** + - [touch_play_mp3](./adf/touch_play_mp3):通过触摸播放 MP3 音频和点亮灯环的基础例程 + - [coze_websocket](./adf/coze_websocket):通过扣子 WebSocket 服务进行大模型语音交互 + - [volc_rtc_spot](./adf/volc_rtc_spot):通过火山引擎 RTC 服务进行大模型语音交互 +- [imu_led](./imu_led):加速度传感器基础例程 +- [simple_touch](./simple_touch) +- [xiaozhi](./xiaozhi):基于小智 AI 的项目 diff --git a/esp-spot/example/adf/coze_websocket/CMakeLists.txt b/esp-spot/example/adf/coze_websocket/CMakeLists.txt new file mode 100644 index 0000000..a607968 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{ADF_PATH}/CMakeLists.txt) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(coze_websocket) \ No newline at end of file diff --git a/esp-spot/example/adf/coze_websocket/README_CN.md b/esp-spot/example/adf/coze_websocket/README_CN.md new file mode 100644 index 0000000..45801f7 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/README_CN.md @@ -0,0 +1,209 @@ +# 火山 Websocket 双向流式对话 + +## 例程简介 + +本例程实现了扣子智能语音对话 Websocket OpenAPI, 主要是通过按键的方式实现与智能体的对讲。 + +## 示例创建 + +### IDF 默认分支 + +本例程支持 IDF release/v5.4 及以后的分支。 + +### 预备知识 + +首先需要在[Coze文档中](https://bytedance.larkoffice.com/docx/Da6qd87pQodvNrxdFYrcnzMxnsh)申请 `Access token` 和 `BOT ID`账号 +更多的 Websocket 文档可以参考 [双向流式对话事件](https://www.coze.cn/open/docs/developer_guides/streaming_chat_event) + +### 配置 + +1. 将获取到的 `Access token` 和 `BOT ID` 信息填入 `Menuconfig->Example Configuration` 中。 +2. 將 wifi 信息填入 `Menuconfig->>Example Configuration` 中。 + +### 编译和下载 + +编译本例程前需要先确保已配置 ESP-IDF 的环境,如果已配置可跳到下一项配置,如果未配置需要先在 ESP-IDF 根目录运行下面脚本设置编译环境,有关配置和使用 ESP-IDF 完整步骤,请参阅 [《ESP-IDF 编程指南》](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/index.html) + +``` +./install.sh +. ./export.sh +``` + + +- 选择编译芯片,以 esp32s3 为例: + +``` +idf.py set-target esp32s3 +``` + +- 编译例子程序 + +``` +idf.py build +``` + +- 烧录程序并运行 monitor 工具来查看串口输出 (替换 PORT 为端口名称): + +``` +idf.py -p PORT flash monitor +``` + +- 退出调试界面使用 ``Ctrl-]`` + +## 如何使用例程 + +### 功能和用法 + +- 例程开始运行后, 当出现以下log就说明了与服务端建立了连接, 就可以对话了: +```c +I (931) main: Initialize board peripherals +W (932) i2c_bus_v2: I2C master handle is NULL, will create new one +I (941) DRV8311: ES8311 in Slave mode +I (958) ES7210: ES7210 in Slave mode +I (967) ES7210: Enable ES7210_INPUT_MIC1 +I (970) ES7210: Enable ES7210_INPUT_MIC2 +I (972) ES7210: Enable ES7210_INPUT_MIC3 +W (976) ES7210: Enable TDM mode. ES7210_SDP_INTERFACE2_REG12: 2 +I (980) ES7210: config fmt 60 +I (982) AUDIO_HAL: Codec mode is 3, Ctrl:1 +I (990) pp: pp rom version: e7ae62f +I (990) net80211: net80211 rom version: e7ae62f +I (990) AUDIO_THREAD: The esp_periph task allocate stack on internal memory +I (991) wifi:wifi driver task: 3fcee994, prio:23, stack:6656, core=0 +I (1001) wifi:wifi firmware version: 21fc8af6de +I (1003) wifi:wifi certification version: v7.0 +I (1007) wifi:config NVS flash: enabled +I (1011) wifi:config nano formatting: disabled +I (1015) wifi:Init data frame dynamic rx buffer num: 32 +I (1020) wifi:Init static rx mgmt buffer num: 5 +I (1024) wifi:Init management short buffer num: 32 +I (1029) wifi:Init dynamic tx buffer num: 32 +I (1033) wifi:Init static tx FG buffer num: 2 +I (1037) wifi:Init static rx buffer size: 1600 +I (1041) wifi:Init static rx buffer num: 16 +I (1045) wifi:Init dynamic rx buffer num: 32 +I (1049) wifi_init: rx ba win: 16 +I (1052) wifi_init: accept mbox: 32 +I (1055) wifi_init: tcpip mbox: 32 +I (1058) wifi_init: udp mbox: 6 +I (1061) wifi_init: tcp mbox: 32 +I (1064) wifi_init: tcp tx win: 65535 +I (1067) wifi_init: tcp rx win: 32768 +I (1071) wifi_init: tcp mss: 1440 +I (1074) wifi_init: WiFi IRAM OP enabled +I (1078) wifi_init: WiFi RX IRAM OP enabled +W (1082) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2 +I (1090) wifi:Set ps type: 1, coexist: 0 + +I (1094) phy_init: phy_version 701,f4f1da3a,Mar 3 2025,15:50:10 +I (1155) wifi:mode : sta (74:4d:bd:9d:b6:30) +I (1155) wifi:enable tsf +W (1155) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:43 +I (2446) wifi:new:<11,0>, old:<1,0>, ap:<255,255>, sta:<11,0>, prof:1, snd_ch_cfg:0x0 +I (2446) wifi:state: init -> auth (0xb0) +W (2447) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:43 +I (2462) wifi:state: auth -> assoc (0x0) +I (2468) wifi:state: assoc -> run (0x10) +I (2485) wifi:connected with xtworks, aid = 88, channel 11, BW20, bssid = ec:56:23:e9:7e:f0 +I (2486) wifi:security: WPA2-PSK, phy: bgn, rssi: -40 +I (2488) wifi:pm start, type: 1 + +I (2490) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us +I (2498) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000 +W (2508) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:4 +I (2532) wifi:AP's beacon interval = 102400 us, DTIM period = 1 +I (3571) esp_netif_handlers: sta ip: 192.168.3.7, mask: 255.255.255.0, gw: 192.168.3.1 +I (3571) PERIPH_WIFI: Got ip:192.168.3.7 +I (3571) coze_chat: wss_url: ws://ws.coze.cn/v1/chat?bot_id=74800678******** +I (3579) AUDIO_THREAD: The coze_data_pull_task task allocate stack on external memory +I (3586) AUDIO_THREAD: The coze_data_push_task task allocate stack on external memory +I (3594) coze_chat: WEBSOCKET_EVENT_BEGIN +I (3598) websocket_client: Started +I (3600) coze_chat: Wait for websocket connected +I (4104) coze_chat: Wait for websocket connected +I (4192) coze_chat: WEBSOCKET_EVENT_CONNECTED +I (5215) wifi:idx:0 (ifx:0, ec:56:23:e9:7e:f0), tid:6, ssn:2665, winSize:64 +I (5265) coze_chat: Request conversation_id : 7491159********* +I (5266) coze_chat: WS connected +I (5266) coze_chat: WS updata chat +I (5268) coze_chat: Update chat: { + "id": "15deeac7-bc4c-731a-c81e-2585128c3557", + "event_type": "chat.update", + "data": { + "chat_config": { + "auto_save_history": true, + "conversation_id": "7491159*********", + "user_id": "userid_123", + "meta_data": { + }, + "custom_variables": { + }, + "extra_params": { + }, + "parameters": { + "custom_var_1": "测试" + } + }, + "input_audio": { + "format": "pcm", + "codec": "pcm", + "sample_rate": 16000, + "channel": 1, + "bit_depth": 16 + }, + "turn_detection": { + "type": "server_vad", + "prefix_padding_ms": 600, + "silence_duration_ms": 500 + }, + "output_audio": { + "codec": "opus", + "opus_config": { + "bitrate": 16000, + "frame_size_ms": 60, + "limit_config": { + "period": 1, + "max_frame_num": 18 + } + }, + "speech_rate": 20, + "voice_id": "7426720361733144585" + }, + "event_subscriptions": ["conversation.audio.delta", "conversation.chat.completed", "input_audio_buffer.speech_started", "input_audio_buffer.speech_stopped", "chat.created", "error"] + } +} +I (5365) MODEL_LOADER: The storage free size is 23104 KB +I (5367) MODEL_LOADER: The partition size is 5168 KB +I (5372) MODEL_LOADER: Successfully load srmodels +I (5376) ALGORITHM_STREAM: Load: wn9_hilexin +I (5381) AUDIO_PIPELINE: link el->rb, el:0x3c17e144, tag:algo_stream, rb:0x3c17e3d0 +I (5389) AUDIO_PIPELINE: link el->rb, el:0x3c185108, tag:raw_stream, rb:0x3c185538 +I (5395) AUDIO_PIPELINE: link el->rb, el:0x3c185250, tag:raw_opus, rb:0x3c187580 +I (5402) AUDIO_THREAD: The algo_stream task allocate stack on external memory +I (5409) AUDIO_ELEMENT: [algo_stream-0x3c17e144] Element task created +I (5415) AUDIO_ELEMENT: [raw_read-0x3c17e274] Element task created +I (5421) AUDIO_PIPELINE: Func:audio_pipeline_run, Line:359, MEM Total:8447912 Bytes, Inter:201343 Bytes, Dram:201343 Bytes, Dram largest free:102400Bytes + +I (5435) AUDIO_ELEMENT: [algo_stream] AEL_MSG_CMD_RESUME,state:1 +I (5440) AUDIO_PIPELINE: Pipeline started +I (5440) AFE_CONFIG: Set WakeNet Model: wn9_hilexin +I (5444) AUDIO_ELEMENT: [raw_stream-0x3c185108] Element task created +W (5450) AFE_CONFIG: For single microphone channel, SE is deactivated. +I (5456) AUDIO_THREAD: The raw_opus task allocate stack on external memory +I (5485) AFE: AFE Version: (1MIC_V250121) +I (5485) AFE: Input PCM Config: total 2 channels(1 microphone, 1 playback), sample rate:16000 +I (5488) AFE: AFE Pipeline: [input] -> |AEC(VOIP_LOW_COST)| -> |NS(WebRTC)| -> [output] +I (5497) AUDIO_THREAD: The algo_fetch task allocate stack on external memory +I (5522) AUDIO_ELEMENT: [raw_opus-0x3c185250] Element task created +I (5522) AUDIO_THREAD: The filter task allocate stack on external memory +I (5523) AUDIO_ELEMENT: [filter-0x3c1853c8] Element task created +I (5529) AUDIO_PIPELINE: Func:audio_pipeline_run, Line:359, MEM Total:8219944 Bytes, Inter:172823 Bytes, Dram:172823 Bytes, Dram largest free:98304Bytes + +I (5542) AUDIO_ELEMENT: [raw_opus] AEL_MSG_CMD_RESUME,state:1 +I (5557) AUDIO_ELEMENT: [filter] AEL_MSG_CMD_RESUME,state:1 +I (5557) AUDIO_PIPELINE: Pipeline started +I (5558) AUDIO_THREAD: The audio_data_read_task task allocate stack on external memory +I (5566) main: Func:app_main, Line:161, MEM Total:8187216 Bytes, Inter:171507 Bytes, Dram:171507 Bytes, Dram largest free:98304Bytes + +I (5582) main_task: Returned from app_main() +``` diff --git a/esp-spot/example/adf/coze_websocket/main/CMakeLists.txt b/esp-spot/example/adf/coze_websocket/main/CMakeLists.txt new file mode 100644 index 0000000..f460897 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "main.c" "audio_processor.c" + INCLUDE_DIRS "" + PRIV_REQUIRES spi_flash nvs_flash esp_netif audio_sal esp_peripherals esp_common json audio_stream console esp_coze_lib + ) diff --git a/esp-spot/example/adf/coze_websocket/main/Kconfig.projbuild b/esp-spot/example/adf/coze_websocket/main/Kconfig.projbuild new file mode 100644 index 0000000..30401b0 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/main/Kconfig.projbuild @@ -0,0 +1,22 @@ +menu "Example Configuration" + + config ESP_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + + config ESP_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + + config ACCESS_TOKEN + string "ACCESS_TOKEN" + default "my acsess token" + + config BOT_ID + string "BOT_ID" + default "my bot id" +endmenu diff --git a/esp-spot/example/adf/coze_websocket/main/audio_processor.c b/esp-spot/example/adf/coze_websocket/main/audio_processor.c new file mode 100644 index 0000000..7c92b3f --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/main/audio_processor.c @@ -0,0 +1,230 @@ +/* + * Espressif Modified MIT License + * + * Copyright (c) 2025 Espressif Systems (Shanghai) Co., LTD + * + * Permission is hereby granted for use **exclusively** with Espressif Systems products. + * This includes the right to use, copy, modify, merge, publish, distribute, and sublicense + * the Software, subject to the following conditions: + * + * 1. This Software **must be used in conjunction with Espressif Systems products**. + * 2. The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * 3. Redistribution of the Software in source or binary form **for use with non-Espressif products** + * is strictly prohibited. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT-ESPRESSIF + */ + +#include + +#include "esp_log.h" + +#include "audio_pipeline.h" +#include "raw_stream.h" +#include "filter_resample.h" +#include "algorithm_stream.h" +#include "raw_stream.h" +#include "i2s_stream.h" +#include "raw_opus_decoder.h" +#include "audio_mem.h" +#include "board.h" + +#include "audio_processor.h" + +static char *TAG = "audio_processor"; + +struct audio_recorder_s { + audio_element_handle_t i2s_reader; + audio_element_handle_t raw_stream; + audio_element_handle_t algo_stream; + audio_pipeline_handle_t pipeline; +}; + +struct audio_player_s { + audio_element_handle_t i2s_writer; + audio_element_handle_t raw_stream; + audio_element_handle_t filter; + audio_element_handle_t opus_decoder_stream; + audio_pipeline_handle_t pipeline; +}; + +static int algo_read_data_callback(audio_element_handle_t self, char *buffer, int len, TickType_t ticks_to_wait, void *context) +{ + audio_element_handle_t i2s_reader = (audio_element_handle_t)context; + return audio_element_input(i2s_reader, buffer, len); +} + +audio_recorder_handle_t recorder_pipeline_open() +{ + struct audio_recorder_s *recorder = audio_calloc(1, sizeof(struct audio_recorder_s)); + if (recorder == NULL) { + ESP_LOGE(TAG, "No mem for recorder"); + return NULL; + } + audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG(); + recorder->pipeline = audio_pipeline_init(&pipeline_cfg); + assert(recorder->pipeline); + +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_32BIT, AUDIO_STREAM_READER); + i2s_stream_set_channel_type(&i2s_cfg, I2S_CHANNEL_TYPE_ONLY_LEFT); +#else + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_16BIT, AUDIO_STREAM_READER); +#endif + i2s_cfg.task_stack = -1; + recorder->i2s_reader = i2s_stream_init(&i2s_cfg); + assert(recorder->i2s_reader); + + algorithm_stream_cfg_t algo_config = ALGORITHM_STREAM_CFG_DEFAULT(); + algo_config.sample_rate = 16000; + algo_config.out_rb_size = 26 * 1024; + algo_config.task_core = 1; +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + algo_config.input_format = "RM"; + #else + algo_config.input_format = "MR"; +#endif + recorder->algo_stream = algo_stream_init(&algo_config); + assert(recorder->algo_stream); + audio_element_set_music_info(recorder->algo_stream, 16000, 1, 16); + audio_element_set_read_cb(recorder->algo_stream, algo_read_data_callback, (void *)recorder->i2s_reader); + audio_element_set_input_timeout(recorder->algo_stream, portMAX_DELAY); + + raw_stream_cfg_t raw_cfg = RAW_STREAM_CFG_DEFAULT(); + recorder->raw_stream = raw_stream_init(&raw_cfg); + assert(recorder->raw_stream); + + audio_pipeline_register(recorder->pipeline, recorder->algo_stream, "algo_stream"); + audio_pipeline_register(recorder->pipeline, recorder->raw_stream, "raw_read"); + + const char *link_tag2[2] = {"algo_stream", "raw_read"}; + audio_pipeline_link(recorder->pipeline, &link_tag2[0], 2); + + return (audio_recorder_handle_t)recorder; +} + +esp_err_t recorder_pipeline_run(audio_recorder_handle_t recorder) +{ + return audio_pipeline_run(recorder->pipeline); +} + +esp_err_t recorder_pipeline_read(audio_recorder_handle_t recorder, char *buffer, int len) +{ + return raw_stream_read(recorder->raw_stream, buffer, len); +} + +esp_err_t recorder_pipeline_stop(audio_recorder_handle_t recorder) +{ + audio_pipeline_stop(recorder->pipeline); + audio_pipeline_wait_for_stop(recorder->pipeline); + audio_pipeline_reset_elements(recorder->pipeline); + audio_pipeline_reset_ringbuffer(recorder->pipeline); + audio_pipeline_reset_items_state(recorder->pipeline); + return ESP_OK; +} + +esp_err_t recorder_pipeline_close(audio_recorder_handle_t recorder) +{ + audio_pipeline_terminate(recorder->pipeline); + audio_pipeline_deinit(recorder->pipeline); + return ESP_OK; +} + +static int opus_audio_data_callback(audio_element_handle_t self, char *buffer, int len, TickType_t ticks_to_wait, void *context) +{ + audio_element_handle_t i2s_writer = (audio_element_handle_t)context; + return audio_element_output(i2s_writer, buffer, len); +} + +audio_player_handle_t player_pipeline_open() +{ + struct audio_player_s *player = audio_calloc(1, sizeof(struct audio_player_s)); + if (player == NULL) { + ESP_LOGE(TAG, "No mem for player"); + return NULL; + } + audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG(); + player->pipeline = audio_pipeline_init(&pipeline_cfg); + assert(player->pipeline); +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_32BIT, AUDIO_STREAM_WRITER); + i2s_stream_set_channel_type(&i2s_cfg, I2S_CHANNEL_TYPE_ONLY_LEFT); + i2s_cfg.need_expand = true; +#else + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_16BIT, AUDIO_STREAM_WRITER); +#endif + i2s_cfg.task_stack = -1; + player->i2s_writer = i2s_stream_init(&i2s_cfg); + assert(player->i2s_writer); + + raw_stream_cfg_t raw_cfg = RAW_STREAM_CFG_DEFAULT(); + player->raw_stream = raw_stream_init(&raw_cfg); + assert(player->raw_stream); + + raw_opus_dec_cfg_t opus_dec_cfg = RAW_OPUS_DEC_CONFIG_DEFAULT(); + opus_dec_cfg.enable_frame_length_prefix = true; + opus_dec_cfg.sample_rate = 16000; + opus_dec_cfg.channels = 1; + opus_dec_cfg.task_core = 1; + player->opus_decoder_stream = raw_opus_decoder_init(&opus_dec_cfg); + assert(player->opus_decoder_stream); + + rsp_filter_cfg_t filter_cfg = DEFAULT_RESAMPLE_FILTER_CONFIG(); + filter_cfg.src_ch = 1; + filter_cfg.src_rate = 16000; +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + filter_cfg.dest_ch = 1; +#else + filter_cfg.dest_ch = 2; +#endif + filter_cfg.dest_rate = 16000; + filter_cfg.stack_in_ext = true; + filter_cfg.task_core = 1; + filter_cfg.complexity = 2; + player->filter = rsp_filter_init(&filter_cfg); + assert(player->filter); + audio_element_set_write_cb(player->filter, opus_audio_data_callback, (void *)player->i2s_writer); + + audio_pipeline_register(player->pipeline, player->raw_stream, "raw_stream"); + audio_pipeline_register(player->pipeline, player->opus_decoder_stream, "raw_opus"); + audio_pipeline_register(player->pipeline, player->filter, "filter"); + + const char *link_tag[3] = {"raw_stream", "raw_opus", "filter"}; + audio_pipeline_link(player->pipeline, &link_tag[0], 3); + return (audio_player_handle_t)player; +} + +esp_err_t player_pipeline_run(audio_player_handle_t player) +{ + return audio_pipeline_run(player->pipeline); +} + +esp_err_t player_pipeline_stop(audio_player_handle_t player) +{ + audio_pipeline_stop(player->pipeline); + audio_pipeline_wait_for_stop(player->pipeline); + audio_pipeline_reset_elements(player->pipeline); + audio_pipeline_reset_ringbuffer(player->pipeline); + audio_pipeline_reset_items_state(player->pipeline); + return ESP_OK; +} + +esp_err_t player_pipeline_write(audio_player_handle_t player, char *buffer, int len) +{ + return raw_stream_write(player->raw_stream, buffer, len); +} + +esp_err_t player_pipeline_close(audio_player_handle_t player) +{ + audio_pipeline_terminate(player->pipeline); + audio_pipeline_deinit(player->pipeline); + return ESP_OK; +} diff --git a/esp-spot/example/adf/coze_websocket/main/audio_processor.h b/esp-spot/example/adf/coze_websocket/main/audio_processor.h new file mode 100644 index 0000000..9294382 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/main/audio_processor.h @@ -0,0 +1,42 @@ +/* + * Espressif Modified MIT License + * + * Copyright (c) 2025 Espressif Systems (Shanghai) Co., LTD + * + * Permission is hereby granted for use **exclusively** with Espressif Systems products. + * This includes the right to use, copy, modify, merge, publish, distribute, and sublicense + * the Software, subject to the following conditions: + * + * 1. This Software **must be used in conjunction with Espressif Systems products**. + * 2. The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * 3. Redistribution of the Software in source or binary form **for use with non-Espressif products** + * is strictly prohibited. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT-ESPRESSIF + */ + + #pragma once + +typedef struct audio_player_s *audio_player_handle_t; +typedef struct audio_recorder_s *audio_recorder_handle_t; + +audio_recorder_handle_t recorder_pipeline_open(); +esp_err_t recorder_pipeline_run(audio_recorder_handle_t recorder); +esp_err_t recorder_pipeline_read(audio_recorder_handle_t recorder, char *buffer, int len); +esp_err_t recorder_pipeline_stop(audio_recorder_handle_t recorder); +esp_err_t recorder_pipeline_close(audio_recorder_handle_t recorder); + + +audio_player_handle_t player_pipeline_open(); +esp_err_t player_pipeline_run(audio_player_handle_t player); +esp_err_t player_pipeline_stop(audio_player_handle_t player); +esp_err_t player_pipeline_write(audio_player_handle_t player, char *buffer, int len); +esp_err_t player_pipeline_close(audio_player_handle_t player); diff --git a/esp-spot/example/adf/coze_websocket/main/idf_component.yml b/esp-spot/example/adf/coze_websocket/main/idf_component.yml new file mode 100644 index 0000000..b051c17 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/main/idf_component.yml @@ -0,0 +1,27 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: '>=4.1.0' + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + espressif/esp_websocket_client: ^1.2.3 + espressif/esp_hosted: + version: "~1.1" + rules: + - if: "target in [esp32p4]" + espressif/esp_wifi_remote: + matches: + - if: "idf_version <=5.4.0 && target in [esp32p4]" + version: "~0.4" + - if: "idf_version >5.4.0 && target in [esp32p4]" + version: "~0.6" diff --git a/esp-spot/example/adf/coze_websocket/main/main.c b/esp-spot/example/adf/coze_websocket/main/main.c new file mode 100644 index 0000000..73d7da6 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/main/main.c @@ -0,0 +1,142 @@ +/* http client request example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include + +#include "freertos/idf_additions.h" +#include "freertos/task.h" + +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_wifi.h" + +#include "audio_sys.h" +#include "audio_thread.h" +#include "esp_peripherals.h" +#include "periph_wifi.h" +#include "periph_sdcard.h" +#include "audio_mem.h" +#include "board.h" +#include "es8311.h" +#include "es7210.h" + +#include "audio_processor.h" +#include "coze_chat.h" + +static char *TAG = "main"; +struct coze_ws_s { + coze_chat_handle_t chat; + audio_recorder_handle_t recorder; + audio_player_handle_t player; + char *recorder_buffer; +#define DEFAULT_RAW_OPUS_BUFFER_SIZE (1024) + char *opus_raw_buffer; + int opus_raw_buffer_len; + enum { + PLAYBACK_STATE_IDLE, + PLAYBACK_STATE_PLAYING, + } player_state; +}; + +static struct coze_ws_s s_coze_ws; + +static void audio_event_callback(coze_chat_event_t event, void *ctx) +{ + if (event == COZE_CHAT_EVENT_CHAT_SPEECH_STARTED) { + ESP_LOGI(TAG, "chat start"); + s_coze_ws.player_state = PLAYBACK_STATE_IDLE; + } else if (event == COZE_CHAT_EVENT_CHAT_SPEECH_STOPED) { + ESP_LOGI(TAG, "chat stop"); + s_coze_ws.player_state = PLAYBACK_STATE_PLAYING; + } +} + +static void audio_data_callback(char *data, int len, void *ctx) +{ +#define frame_length_prefix (2) + if (len > s_coze_ws.opus_raw_buffer_len) { + s_coze_ws.opus_raw_buffer_len = len + frame_length_prefix; + s_coze_ws.opus_raw_buffer = audio_realloc(s_coze_ws.opus_raw_buffer, s_coze_ws.opus_raw_buffer_len); + } + ESP_LOGD(TAG, "data: %p, len: %d", data, len); + s_coze_ws.opus_raw_buffer[0] = (len >> 8) & 0xFF; + s_coze_ws.opus_raw_buffer[1] = len & 0xFF; + memcpy(s_coze_ws.opus_raw_buffer + frame_length_prefix, data, len); + len += frame_length_prefix; + + player_pipeline_write(s_coze_ws.player, s_coze_ws.opus_raw_buffer, len); +} + +static void audio_if_open() +{ + s_coze_ws.recorder = recorder_pipeline_open(); + s_coze_ws.player = player_pipeline_open(); + recorder_pipeline_run(s_coze_ws.recorder); + player_pipeline_run(s_coze_ws.player); +} + +static void audio_data_read_task(void *pv) +{ +#define recorder_buffer_size (640) + s_coze_ws.recorder_buffer = malloc(recorder_buffer_size); + + while (1) { + int r_len = recorder_pipeline_read(s_coze_ws.recorder, s_coze_ws.recorder_buffer, recorder_buffer_size); + ESP_LOGD(TAG, "read len: %d", r_len); + if (r_len > 0) { + coze_chat_send_audio_data(s_coze_ws.chat, s_coze_ws.recorder_buffer, r_len); + } + } + vTaskDelete(NULL); +} + +void app_main(void) +{ + AUDIO_MEM_SHOW(TAG); + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_LOGI(TAG, "Initialize board peripherals"); + esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG(); + esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg); + + audio_board_handle_t board_handle = audio_board_init(); + audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_BOTH, AUDIO_HAL_CTRL_START); +#if CONFIG_ESP32_S3_SPOT_BOARD + audio_hal_set_volume(board_handle->audio_hal, 90); + es8311_set_mic_gain(ES8311_MIC_GAIN_0DB); +#endif + + s_coze_ws.opus_raw_buffer_len = DEFAULT_RAW_OPUS_BUFFER_SIZE; + s_coze_ws.opus_raw_buffer = audio_malloc(s_coze_ws.opus_raw_buffer_len); + // Initialize SD Card peripheral + // audio_board_sdcard_init(set, SD_MODE_1_LINE); + periph_wifi_cfg_t wifi_cfg = { + .wifi_config.sta.ssid = CONFIG_ESP_WIFI_SSID, + .wifi_config.sta.password = CONFIG_ESP_WIFI_PASSWORD, + }; + esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg); + esp_periph_start(set, wifi_handle); + periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY); + + coze_chat_config_t chat_config = COZE_CHAT_DEFAULT_CONFIG(); + chat_config.bot_id = CONFIG_BOT_ID; + chat_config.access_token = CONFIG_ACCESS_TOKEN; + chat_config.audio_callback = audio_data_callback; + chat_config.event_callback = audio_event_callback; + + s_coze_ws.chat = coze_chat_init(&chat_config); + coze_chat_start(s_coze_ws.chat); + + audio_if_open(); + audio_thread_create(NULL, "audio_data_read_task", audio_data_read_task, (void *)NULL, 1024 * 4, 12, true, 1); + + AUDIO_MEM_SHOW(TAG); +} diff --git a/esp-spot/example/adf/coze_websocket/partitions.csv b/esp-spot/example/adf/coze_websocket/partitions.csv new file mode 100644 index 0000000..db3daa0 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/partitions.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 2M, +model, data, spiffs, , 5168K, diff --git a/esp-spot/example/adf/coze_websocket/sdkconfig.defaults b/esp-spot/example/adf/coze_websocket/sdkconfig.defaults new file mode 100644 index 0000000..4f6e293 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/sdkconfig.defaults @@ -0,0 +1,38 @@ +# +# Chip and Board +# +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESP32_S3_SPOT_BOARD=y + +# +# Serial flasher config +# +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +# end of Serial flasher config + +# +# Partition Table +# +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# ESP-TLS +# +CONFIG_ESP_TLS_INSECURE=y +CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y + +# +# FREERTOS +# +CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY=y +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y +CONFIG_FREERTOS_HZ=1000 diff --git a/esp-spot/example/adf/coze_websocket/sdkconfig.defaults.esp32s3 b/esp-spot/example/adf/coze_websocket/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000..73775c2 --- /dev/null +++ b/esp-spot/example/adf/coze_websocket/sdkconfig.defaults.esp32s3 @@ -0,0 +1,112 @@ +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_IDF_TARGET_ESP32S3=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009 + + +# +# Audio HAL +# +CONFIG_ESP32_S3_KORVO2_V3_BOARD=y +# end of Audio HAL + +# +# Audio Recorder +# +CONFIG_AFE_MIC_NUM=2 +# end of Audio Recorder + +# +# ESP Speech Recognition +# +CONFIG_MODEL_IN_FLASH=y +CONFIG_USE_AFE=y +CONFIG_AFE_INTERFACE_V1=y +CONFIG_USE_WAKENET=y +CONFIG_SR_WN_WN9_HILEXIN=y +CONFIG_USE_MULTINET=y +CONFIG_SR_MN_CN_MULTINET6_QUANT=y +CONFIG_SR_MN_EN_NONE=y +# end of ESP Speech Recognition + +# +# Component config +# + +# +# Driver configurations +# + +# +# ESP32S3-Specific +# +# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set +# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240 + +# +# Cache config +# +# CONFIG_ESP32S3_INSTRUCTION_CACHE_16KB is not set +CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y +CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE=0x8000 +# CONFIG_ESP32S3_INSTRUCTION_CACHE_4WAYS is not set +CONFIG_ESP32S3_INSTRUCTION_CACHE_8WAYS=y +CONFIG_ESP32S3_ICACHE_ASSOCIATED_WAYS=8 +CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_32B=y +CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_SIZE=32 +# CONFIG_ESP32S3_INSTRUCTION_CACHE_WRAP is not set +# CONFIG_ESP32S3_DATA_CACHE_16KB is not set +# CONFIG_ESP32S3_DATA_CACHE_32KB is not set +CONFIG_ESP32S3_DATA_CACHE_64KB=y +CONFIG_ESP32S3_DATA_CACHE_SIZE=0x10000 +# CONFIG_ESP32S3_DATA_CACHE_4WAYS is not set +CONFIG_ESP32S3_DATA_CACHE_8WAYS=y +CONFIG_ESP32S3_DCACHE_ASSOCIATED_WAYS=8 +# CONFIG_ESP32S3_DATA_CACHE_LINE_32B is not set +CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y +CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE=64 +# CONFIG_ESP32S3_DATA_CACHE_WRAP is not set +# end of Cache config + +CONFIG_ESP32S3_SPIRAM_SUPPORT=y + +# +# SPI RAM config +# +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TYPE_AUTO=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_MEMTEST=y +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 +# end of SPI RAM config +# end of ESP32S3-Specific + +# +# Wi-Fi +# +CONFIG_ESP_WIFI_ENABLED=y +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=16 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y +CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP_WIFI_TX_BA_WIN=16 +CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP_WIFI_RX_BA_WIN=16 + +# +# LWIP +# +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=65535 +CONFIG_LWIP_TCP_WND_DEFAULT=65535 +CONFIG_LWIP_TCP_RECVMBOX_SIZE=32 \ No newline at end of file diff --git a/esp-spot/example/adf/llm_touch_toy/CMakeLists.txt b/esp-spot/example/adf/llm_touch_toy/CMakeLists.txt new file mode 100644 index 0000000..d2c5992 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{ADF_PATH}/CMakeLists.txt) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(llm_touch_toy) \ No newline at end of file diff --git a/esp-spot/example/adf/llm_touch_toy/README.md b/esp-spot/example/adf/llm_touch_toy/README.md new file mode 100644 index 0000000..ee3ed42 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/README.md @@ -0,0 +1,199 @@ +# 卡皮巴拉触摸玩偶 + +## 例程简介 + +ESP-Spot 体积小巧,内置扬声器、麦克风和电池,可轻松嵌入毛绒玩具中,让智能玩具“活”起来!本次 Demo 视频重点展示了如何将扣子智能语音对话与 ESP32-S3 触摸传感器结合,打造一款真正具有“情感交互”的 AI 玩具。 + +
+ +
+ +# 视频展示 + +[用触摸交互升级大模型 AI 玩具【ESP-SPOT】](https://www.bilibili.com/video/BV1ekRAYVEZ1) + +视频包含组装过程、成品演示、以及原理讲解 + +# 硬件接线 + +ESP-Spot 上板也可作为一块带音频的 ESP32 开发板单独使用,VIN 可以允许 3.3->5V 供电,如果需要默认使能 3V3 电源域,请将 VBUS 上拉,VBAT 接 3.3V。 + +**注意:当 PREP_VCC_CTL 引脚(GPIO6) 拉高时 CODEC_3V3 使能,音频编解码芯片工作。 也就是说,只有当 GPIO6 拉高时,音频功能才会正常工作** + +引脚布局如下 + +
+ +
+ +ESP32-S3 模组自带触摸传感器功能,技术细节请参考::[ESP32-S3 数据手册](https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_cn.pdf) + + +# 预备知识 + +## 获取扣子账号 + +- 本例程默认使用 Coze 对接大模型进行语音交互,首先需要在[Coze文档中](https://bytedance.larkoffice.com/docx/Da6qd87pQodvNrxdFYrcnzMxnsh)申请 `Access token` 和 `BOT ID` 账号 +- 更多的 Websocket 文档可以参考 [双向流式对话事件](https://www.coze.cn/open/docs/developer_guides/streaming_chat_event) + +- 若想更换大模型对接平台,请替换 Coze 组件 [components/esp_coze_lib](./components/esp_coze_lib) 后更新 [main.c](./main/main.c) 中对应的逻辑 + +## 更多触摸交互逻辑 + +- 目前的触摸交互逻辑是:通过触摸对应部位,播放预设的音频内容或调用对应的应用逻辑 +- 后续可升级为:通过触摸触发不同的事件,并发送至大模型进行处理,实现更智能、更加丰富的语音交互体验。详情请参考 Coze 文档:[手动提交对话内容](https://www.coze.cn/open/docs/developer_guides/streaming_chat_event#46f6a7d0) + +## 本地音频 + +- 本例程在 `/tools/audio_tone.bin` 和 `/components/audio_flash_tone/` 目录下已经帮助用户生成了例程所需的 bin 文件和音频文件在 flash 中地址的源代码文件。 + +- 如果用户需要生成自己的 `audio_tone.bin`,则需要执行 `mk_audio_bin.py` 脚本(位于 $ADF_PATH/tools/audio_tone/mk_audio_tone.py),并且指定相关文件的路径。 + +- 源 MP3 文件在 `tone_mp3_folder` 文件夹中,生成的 C 文件、H 文件以及二进制 bin 文件都存放在此目录下。 + + ``` + python3 $ADF_PATH/tools/audio_tone/mk_audio_tone.py -f ./ -r tone_mp3_folder + ``` + +- 请使用 *python3 $ADF_PATH/tools/audio_tone/mk_audio_tone.py --help* 查看更多脚本信息。 + +- 本例程默认的 `audio_tone.bin` 包含如下音频文件: + + ```c + "flash://tone/0_belly_1.mp3", + "flash://tone/1_belly_2.mp3", + "flash://tone/2_belly_3.mp3", + "flash://tone/3_belly_4.mp3", + "flash://tone/4_bread_1.mp3", + "flash://tone/5_bread_2.mp3", + "flash://tone/6_capybara_song_1.mp3", + "flash://tone/7_hat_1.mp3", + "flash://tone/8_neck_1.mp3", + "flash://tone/9_neck_2.mp3", + "flash://tone/10_reverse.mp3", + "flash://tone/11_screaming.mp3", + "flash://tone/12_shake.mp3", + "flash://tone/13_touch_nose.mp3", + "flash://tone/14_woohoo.mp3", + ``` +- 本地音频在 `partition_flash_tone.csv` 中地址配置如下,用户可以根据自己的项目 flash 分区灵活配置地址: + ``` + flashTone,data, 0x04, 0x720000 , 500K, + ``` + + +## 动作和姿态识别 + +- 硬件方面采用 BMI270 传感器,用于识别不同的动作和姿态,后续将提供优化后的 BMI270 固件,以实现更精确、稳定的动作识别 + +## IDF 默认分支 + +- 本例程支持 IDF release/v5.4 及以后的分支。 + +# 配置工程 + +## 开发板 + +- 默认使用搭载 ESP32-S3 模组的 ``ESP-SPOT`` 开发板,配置项为 `CONFIG_ESP32_S3_SPOT_BOARD=y` +- **硬件初始化代码位于本仓库中的 [example/adf/components/audio_board](../components/audio_board) ,编译前将其替换掉 ADF 路径中的 `$ADF_PATH/components/audio_board`** + +## 音频文件 +- **如果 MP3 音频总数量大于 `9`,则需要修改 `$ADF_PATH/components/audio_stream` 目录下的 `tone_stream.c` 文件,可以直接使用本仓库中的 [example/adf/components/tone_stream.c](../components/tone_stream.c) 替换 ADF 路径下的原文件** +- 本仓库的中的 [tone_stream.c](../components/tone_stream.c) 改了什么:将 `_tone_open(audio_element_handle_t self)` 函数中 `char find_num[2]` 数组的长度修改为 `char find_num[3]`。这样可以确保函数在解析双位数的音频文件名(例如 10.mp3、11.mp3)时不会发生字符串截断错误,正确识别全部音频文件 + + +## 触摸功能 + +- 本例程默认使用 GPIO 3、9、13、14 引脚作触摸,分别对应卡皮巴拉玩偶的不同部位,触发不同的语音交互功能: + - GPIO3(鼻子):短按触发一种语音交互,长按触发另一种语音交互。 + - GPIO9(帽子):第一次按下触发语音对话,第二次按下播放《卡皮巴拉之歌》并点亮灯环。 + - GPIO13(肚子):每次按下依次切换到下一阶段的语音交互,共有五种不同交互。 + - GPIO14(脖子):作为开启/关闭大模型语音交互的总开关。 + +- 如需更换触摸引脚,请在 `main.c` 中修改 `TOUCH_CHANNEL_1` 及相关宏定义。注意参考 [ESP32-S3 数据手册](https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_cn.pdf) + 选择支持触摸功能的 IO + +- 触摸灵敏度可通过以下宏定义进行调整,阈值越小,感应越灵敏: + - `LIGHT_TOUCH_THRESHOLD`:设置轻触的触发阈值。 + - `HEAVY_TOUCH_THRESHOLD`:设置重触的触发阈值。 + +- 短按和长按的判定时间可在 `touch_task` 中进行配置 + ``` + const button_config_t btn_cfg = { + .short_press_time = 300, // 触发短按时长(ms) + .long_press_time = 2000, // 触发长按时长(ms) + }; + ``` + +## LED 灯环 + +- 本例程使用 WS2812 灯环,相关配置如下: + - `CONFIG_LED_GPIO_INPUT`:控制灯环的引脚,默认为 `GPIO45`,可通过 `idf.py menuconfig` 进行配置 + - `CONFIG_LED_COUNT`:用于设置灯环的 LED 数量,默认为 16 个,同样可在 `idf.py menuconfig` 中进行配置 + +## 扣子大模型鉴权 + +- 将获取到的 `Access token` 和 `BOT ID` 信息填入 `Menuconfig->Example Configuration` 中。`Access token` 默认是以 `pat_` 开头的 + +## Wi-Fi 信息 +- 將 wifi 信息填入 `Menuconfig->>Example Configuration` 中 + + +## 编译和下载 + +请先编译版本并烧录到开发板上,然后运行 monitor 工具来查看串口输出(替换 PORT 为端口名称): + +``` +idf.py -p PORT flash monitor +``` + +**此外,本例程还需烧录 `/tools/audio_tone.bin` 到 `partition_flash_tone.csv` 的 `flashTone` 分区,请使用如下命令:** + +``` +esptool.py --chip esp32s3 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x720000 ./tools/audio_tone.bin +``` + +有关配置和使用 ESP-IDF 生成项目的完整步骤,请参阅 [《ESP-IDF 编程指南》](https://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.3/esp32/index.html)。 + + +## 如何使用 + +### 功能和用法 + +- 例程开始运行后,如果事前没有烧录 `/tools/audio_tone.bin` 到 `partition_flash_tone.csv ` 的 `flashTone` 分区,例程将会报错,请参考 [编译和下载](#编译和下载) 的说明进行烧录 + +## 故障排除 + +- 如果遇到下方错误,请把按照 [编译和下载](#编译和下载) 的说明烧录 `/tools/audio_tone.bin` 到 `partition_flash_tone.csv ` 的 `flashTone` 分区。 + ```c + E (481) TONE_PARTITION: Not flash tone partition + E (481) AUDIO_ELEMENT: [tone] AEL_STATUS_ERROR_OPEN,-1 + W (491) AUDIO_ELEMENT: [tone] audio_element_on_cmd_error,7 + E (501) TONE_PARTITION: /repo/adfs/bugfix/esp-adf-internal/components/tone_partition/tone_partition.c:204 (tone_partition_deinit): Got NULL Pointer + W (511) AUDIO_ELEMENT: IN-[mp3] AEL_IO_ABORT + E (511) MP3_DECODER: failed to read audio data (line 119) + W (521) AUDIO_ELEMENT: [mp3] AEL_IO_ABORT, -3 + W (531) AUDIO_ELEMENT: IN-[i2s] AEL_IO_ABORT + ``` + +- 如果遇到下方错误,通常是由于网络波动导致数据未能及时上传。此时可以尝试更换网络,或通过配置参数提升 ESP32-S3 的 Wi-Fi 性能。具体优化方法可参考乐鑫官方文档:[如何提高 Wi-Fi 性能](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-guides/wifi.html#how-to-improve-wi-fi-performance)。当前上传使用的音频格式为 `PCM`,扣子即将支持 `Opus` 编码格式,可有效降低带宽占用 + ``` + E (144192) coze_chat: Audio data send to queue failed + E (144222) coze_chat: Audio data send to queue failed + E (144243) coze_chat: Audio data send to queue failed + E (144267) coze_chat: Audio data send to queue failed + E (144288) coze_chat: Audio data send to queue failed + E (144309) coze_chat: Audio data send to queue failed + E (144347) coze_chat: Audio data send to queue failed + E (144368) coze_chat: Audio data send to queue failed + E (144399) coze_chat: Audio data send to queue failed + ``` + + +## 技术支持 +请按照下面的链接获取技术支持: + +- 技术支持参见 [esp32.com](https://esp32.com/viewforum.php?f=20) 论坛 +- 故障和新功能需求,请创建 [GitHub issue](https://github.com/espressif/esp-adf/issues) + +我们会尽快回复。 diff --git a/esp-spot/example/adf/llm_touch_toy/main/CMakeLists.txt b/esp-spot/example/adf/llm_touch_toy/main/CMakeLists.txt new file mode 100644 index 0000000..aac32ff --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "main.c" "audio_processor.c" + INCLUDE_DIRS "" + PRIV_REQUIRES spi_flash nvs_flash esp_netif audio_sal esp_peripherals esp_common json audio_stream console touch_button audio_flash_tone esp_coze_lib led_driver + ) diff --git a/esp-spot/example/adf/llm_touch_toy/main/Kconfig.projbuild b/esp-spot/example/adf/llm_touch_toy/main/Kconfig.projbuild new file mode 100644 index 0000000..30401b0 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/main/Kconfig.projbuild @@ -0,0 +1,22 @@ +menu "Example Configuration" + + config ESP_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + + config ESP_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + + config ACCESS_TOKEN + string "ACCESS_TOKEN" + default "my acsess token" + + config BOT_ID + string "BOT_ID" + default "my bot id" +endmenu diff --git a/esp-spot/example/adf/llm_touch_toy/main/audio_processor.c b/esp-spot/example/adf/llm_touch_toy/main/audio_processor.c new file mode 100644 index 0000000..8f389d7 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/main/audio_processor.c @@ -0,0 +1,472 @@ +/* + * Espressif Modified MIT License + * + * Copyright (c) 2025 Espressif Systems (Shanghai) Co., LTD + * + * Permission is hereby granted for use **exclusively** with Espressif Systems products. + * This includes the right to use, copy, modify, merge, publish, distribute, and sublicense + * the Software, subject to the following conditions: + * + * 1. This Software **must be used in conjunction with Espressif Systems products**. + * 2. The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * 3. Redistribution of the Software in source or binary form **for use with non-Espressif products** + * is strictly prohibited. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT-ESPRESSIF + */ + +#include +#include "esp_log.h" +#include "esp_check.h" +#include "audio_pipeline.h" +#include "raw_stream.h" +#include "filter_resample.h" +#include "algorithm_stream.h" +#include "raw_stream.h" +#include "tone_stream.h" +#include "i2s_stream.h" +#include "mp3_decoder.h" +#include "raw_opus_decoder.h" +#include "audio_mem.h" +#include "audio_thread.h" +#include "board.h" +#include "audio_processor.h" + +static char *TAG = "audio_processor"; + +#define audio_pipe_safe_free(x, fn) do { \ + if (x) { \ + fn(x); \ + x = NULL; \ + } \ +} while (0) + +struct audio_recorder_s { + audio_element_handle_t i2s_reader; + audio_element_handle_t raw_stream; + audio_element_handle_t algo_stream; + audio_pipeline_handle_t pipeline; +}; + +struct audio_player_s { + audio_element_handle_t i2s_writer; + audio_element_handle_t raw_stream; + audio_element_handle_t filter; + audio_element_handle_t opus_decoder_stream; + audio_pipeline_handle_t pipeline; + pipe_player_state_e player_state; +}; + +typedef struct { + audio_pipeline_handle_t pipeline; + audio_element_handle_t tone_stream_reader; + audio_element_handle_t i2s_stream_writer; + audio_element_handle_t mp3_decoder; + audio_element_handle_t filter; + pipe_player_state_e player_state; + bool running; + tone_play_callback_t tone_cb; +} audio_player_t; + +static audio_player_t *s_audio_player = NULL; +static struct audio_player_s *s_player_pipeline = NULL; + +static int algo_read_data_callback(audio_element_handle_t self, char *buffer, int len, TickType_t ticks_to_wait, void *context) +{ + audio_element_handle_t i2s_reader = (audio_element_handle_t)context; + return audio_element_input(i2s_reader, buffer, len); +} + +audio_recorder_handle_t recorder_pipeline_open() +{ + struct audio_recorder_s *recorder = audio_calloc(1, sizeof(struct audio_recorder_s)); + if (recorder == NULL) { + ESP_LOGE(TAG, "No mem for recorder"); + return NULL; + } + audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG(); + recorder->pipeline = audio_pipeline_init(&pipeline_cfg); + AUDIO_MEM_CHECK(TAG, recorder->pipeline, goto _exit); + +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_32BIT, AUDIO_STREAM_READER); + i2s_stream_set_channel_type(&i2s_cfg, I2S_CHANNEL_TYPE_ONLY_LEFT); +#else + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_16BIT, AUDIO_STREAM_READER); +#endif + i2s_cfg.task_stack = -1; + recorder->i2s_reader = i2s_stream_init(&i2s_cfg); + AUDIO_MEM_CHECK(TAG, recorder->i2s_reader, goto _exit); + + algorithm_stream_cfg_t algo_config = ALGORITHM_STREAM_CFG_DEFAULT(); + algo_config.sample_rate = 16000; + algo_config.out_rb_size = 26 * 1024; + algo_config.task_core = 1; +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + algo_config.input_format = "RM"; +#else + algo_config.input_format = "MR"; +#endif + recorder->algo_stream = algo_stream_init(&algo_config); + AUDIO_MEM_CHECK(TAG, recorder->algo_stream, goto _exit); + audio_element_set_music_info(recorder->algo_stream, 16000, 1, 16); + audio_element_set_read_cb(recorder->algo_stream, algo_read_data_callback, (void *)recorder->i2s_reader); + audio_element_set_input_timeout(recorder->algo_stream, portMAX_DELAY); + + raw_stream_cfg_t raw_cfg = RAW_STREAM_CFG_DEFAULT(); + recorder->raw_stream = raw_stream_init(&raw_cfg); + AUDIO_MEM_CHECK(TAG, recorder->raw_stream, goto _exit); + + audio_pipeline_register(recorder->pipeline, recorder->algo_stream, "algo_stream"); + audio_pipeline_register(recorder->pipeline, recorder->raw_stream, "raw_read"); + + const char *link_tag[2] = {"algo_stream", "raw_read"}; + audio_pipeline_link(recorder->pipeline, &link_tag[0], sizeof(link_tag) / sizeof(char *)); + audio_pipeline_run(recorder->pipeline); + return (audio_recorder_handle_t)recorder; +_exit: + ESP_LOGE(TAG, "Failed to init recorder pipeline"); + if (recorder->i2s_reader) { + audio_element_deinit(recorder->i2s_reader); + } + if (recorder->raw_stream) { + audio_element_deinit(recorder->raw_stream); + } + if (recorder->algo_stream) { + audio_element_deinit(recorder->algo_stream); + } + if (recorder->pipeline) { + audio_pipeline_deinit(recorder->pipeline); + } + return NULL; +} + +esp_err_t recorder_pipeline_run(audio_recorder_handle_t recorder) +{ + if (recorder == NULL) { + return ESP_ERR_INVALID_ARG; + } + return audio_pipeline_run(recorder->pipeline); +} + +esp_err_t recorder_pipeline_read(audio_recorder_handle_t recorder, char *buffer, int len) +{ + if (recorder == NULL || buffer == NULL || len <= 0) { + return ESP_ERR_INVALID_ARG; + } + // ESP_LOGE(TAG, "recorder_pipeline_read"); + return raw_stream_read(recorder->raw_stream, buffer, len); +} + +esp_err_t recorder_pipeline_stop(audio_recorder_handle_t recorder) +{ + if (recorder == NULL) { + return ESP_ERR_INVALID_ARG; + } + audio_pipeline_stop(recorder->pipeline); + audio_pipeline_wait_for_stop(recorder->pipeline); + audio_pipeline_reset_elements(recorder->pipeline); + audio_pipeline_reset_ringbuffer(recorder->pipeline); + audio_pipeline_reset_items_state(recorder->pipeline); + return ESP_OK; +} + +esp_err_t recorder_pipeline_close(audio_recorder_handle_t recorder) +{ + if (recorder == NULL) { + return ESP_ERR_INVALID_ARG; + } + audio_pipeline_terminate(recorder->pipeline); + audio_pipeline_deinit(recorder->pipeline); + return ESP_OK; +} + +static int opus_audio_data_callback(audio_element_handle_t self, char *buffer, int len, TickType_t ticks_to_wait, void *context) +{ + audio_element_handle_t i2s_writer = (audio_element_handle_t)context; + return audio_element_output(i2s_writer, buffer, len); +} + +audio_player_handle_t player_pipeline_open() +{ + struct audio_player_s *player = audio_calloc(1, sizeof(struct audio_player_s)); + if (player == NULL) { + ESP_LOGE(TAG, "No mem for player"); + return NULL; + } + player->player_state = PIPE_STATE_IDLE; + s_player_pipeline = player; + + audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG(); + player->pipeline = audio_pipeline_init(&pipeline_cfg); + AUDIO_MEM_CHECK(TAG, player->pipeline, goto _exit) +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_32BIT, AUDIO_STREAM_WRITER); + i2s_stream_set_channel_type(&i2s_cfg, I2S_CHANNEL_TYPE_ONLY_LEFT); + i2s_cfg.need_expand = true; +#else + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT_WITH_PARA(0, 16000, I2S_DATA_BIT_WIDTH_16BIT, AUDIO_STREAM_WRITER); +#endif + i2s_cfg.task_stack = -1; + player->i2s_writer = i2s_stream_init(&i2s_cfg); + AUDIO_MEM_CHECK(TAG, player->i2s_writer, goto _exit); + + raw_stream_cfg_t raw_cfg = RAW_STREAM_CFG_DEFAULT(); + player->raw_stream = raw_stream_init(&raw_cfg); + AUDIO_MEM_CHECK(TAG, player->raw_stream, goto _exit) + + raw_opus_dec_cfg_t opus_dec_cfg = RAW_OPUS_DEC_CONFIG_DEFAULT(); + opus_dec_cfg.enable_frame_length_prefix = true; + opus_dec_cfg.sample_rate = 16000; + opus_dec_cfg.channels = 1; + opus_dec_cfg.task_core = 1; + player->opus_decoder_stream = raw_opus_decoder_init(&opus_dec_cfg); + AUDIO_MEM_CHECK(TAG, player->opus_decoder_stream, goto _exit); + + rsp_filter_cfg_t filter_cfg = DEFAULT_RESAMPLE_FILTER_CONFIG(); + filter_cfg.src_ch = 1; + filter_cfg.src_rate = 16000; +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + filter_cfg.dest_ch = 1; +#else + filter_cfg.dest_ch = 2; +#endif + filter_cfg.dest_rate = 16000; + filter_cfg.stack_in_ext = true; + filter_cfg.task_core = 1; + filter_cfg.complexity = 2; + player->filter = rsp_filter_init(&filter_cfg); + AUDIO_MEM_CHECK(TAG, player->filter, goto _exit); + audio_element_set_write_cb(player->filter, opus_audio_data_callback, (void *)player->i2s_writer); + + audio_pipeline_register(player->pipeline, player->raw_stream, "raw_stream"); + audio_pipeline_register(player->pipeline, player->opus_decoder_stream, "raw_opus"); + audio_pipeline_register(player->pipeline, player->filter, "filter"); + + const char *link_tag[3] = {"raw_stream", "raw_opus", "filter"}; + audio_pipeline_link(player->pipeline, &link_tag[0], 3); + audio_pipeline_run(player->pipeline); + return (audio_player_handle_t)player; +_exit: + ESP_LOGE(TAG, "Failed to init player pipeline"); + if (player->i2s_writer) { + audio_element_deinit(player->i2s_writer); + } + if (player->raw_stream) { + audio_element_deinit(player->raw_stream); + } + if (player->opus_decoder_stream) { + audio_element_deinit(player->opus_decoder_stream); + } + if (player->filter) { + audio_element_deinit(player->filter); + } + if (player->pipeline) { + audio_pipeline_deinit(player->pipeline); + } + return NULL; + +} + +static esp_err_t _player_i2s_write_cb(audio_element_handle_t self, char *buffer, int len, TickType_t ticks_to_wait, void *context) +{ + return audio_element_output(s_player_pipeline->i2s_writer, buffer, len); +} + +static esp_err_t _player_write_nop_cb(audio_element_handle_t self, char *buffer, int len, TickType_t ticks_to_wait, void *context) +{ + return len; +} + +esp_err_t player_pipeline_run(void) +{ + ESP_RETURN_ON_FALSE(s_player_pipeline != NULL, ESP_FAIL, TAG, "player pipeline not initialized"); + if (s_player_pipeline->player_state == PIPE_STATE_RUNNING) { + ESP_LOGW(TAG, "player pipe is already running state"); + return ESP_OK; + } + + ESP_LOGI(TAG, "player pipe start running"); + audio_element_set_write_cb(s_player_pipeline->filter, _player_i2s_write_cb, NULL); + s_player_pipeline->player_state = PIPE_STATE_RUNNING; + return ESP_OK; +} + +esp_err_t player_pipeline_stop(void) +{ + ESP_RETURN_ON_FALSE(s_player_pipeline != NULL, ESP_FAIL, TAG, "player pipeline not initialized"); + if (s_player_pipeline->player_state == PIPE_STATE_IDLE) { + ESP_LOGW(TAG, "player pipe is idle state"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "player pipe stop running"); + audio_element_set_write_cb(s_player_pipeline->filter, _player_write_nop_cb, NULL); + s_player_pipeline->player_state = PIPE_STATE_IDLE; + return ESP_OK; +} + +esp_err_t player_pipeline_write(audio_player_handle_t player, char *buffer, int len) +{ + if (player == NULL || buffer == NULL || len <= 0) { + return ESP_ERR_INVALID_ARG; + } + return raw_stream_write(player->raw_stream, buffer, len); +} + +esp_err_t player_pipeline_close(audio_player_handle_t player) +{ + if (player == NULL) { + return ESP_ERR_INVALID_ARG; + } + audio_pipeline_terminate(player->pipeline); + audio_pipeline_deinit(player->pipeline); + return ESP_OK; +} + +esp_err_t audio_tone_play(tone_type_t tone_url) +{ + ESP_RETURN_ON_FALSE(s_audio_player != NULL, ESP_FAIL, TAG, "audio tone not initialized"); + if (s_audio_player->player_state == PIPE_STATE_RUNNING) { + return ESP_FAIL; + } + ESP_LOGI(TAG, "audio_tone_play %d", tone_url); + + audio_element_set_uri(s_audio_player->tone_stream_reader, tone_uri[tone_url]); + audio_pipeline_run(s_audio_player->pipeline); + s_audio_player->player_state = PIPE_STATE_RUNNING; + return ESP_OK; +} + +esp_err_t audio_tone_stop(void) +{ + ESP_RETURN_ON_FALSE(s_audio_player != NULL, ESP_FAIL, TAG, "audio tone not initialized"); + if (s_audio_player->player_state == PIPE_STATE_IDLE) { + return ESP_FAIL; + } + audio_pipeline_stop(s_audio_player->pipeline); + audio_pipeline_wait_for_stop(s_audio_player->pipeline); + audio_pipeline_terminate(s_audio_player->pipeline); + audio_pipeline_reset_ringbuffer(s_audio_player->pipeline); + audio_pipeline_reset_elements(s_audio_player->pipeline); + s_audio_player->player_state = PIPE_STATE_IDLE; + return ESP_OK; +} + +static void audio_player_state_task(void *arg) +{ + audio_event_iface_handle_t evt = (audio_event_iface_handle_t) arg; + + s_audio_player->running = true; + while (1) { + audio_event_iface_msg_t msg; + esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "[ * ] Event interface error : %d", ret); + continue; + } + if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT + && msg.source == (void *) s_audio_player->mp3_decoder + && msg.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) { + audio_element_info_t music_info = {0}; + audio_element_getinfo(s_audio_player->mp3_decoder, &music_info); + ESP_LOGI(TAG, "[ * ] Receive music info from wav decoder, sample_rates=%d, bits=%d, ch=%d", + music_info.sample_rates, music_info.bits, music_info.channels); + rsp_filter_set_src_info(s_audio_player->filter, music_info.sample_rates, music_info.channels); + continue; + } + /* Stop when the last pipeline element receives stop event */ + if ((msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && msg.source == (void *) s_audio_player->filter + && msg.cmd == AEL_MSG_CMD_REPORT_STATUS) + && ((int)msg.data == AEL_STATUS_STATE_FINISHED)) { + ESP_LOGI(TAG, "[ * ] Stop event received"); + // To suppress unwanted transition noise + vTaskDelay(pdMS_TO_TICKS(20)); + // Restore coze audio play + player_pipeline_run(); + s_audio_player->tone_cb(AEL_STATUS_STATE_FINISHED); + } + } +} + +static esp_err_t _player_write_mp3_cb(audio_element_handle_t self, char *buffer, int len, TickType_t ticks_to_wait, void *context) +{ + return audio_element_output(s_player_pipeline->i2s_writer, buffer, len); +} + +esp_err_t audio_tone_init(tone_play_callback_t callback) +{ + s_audio_player = (audio_player_t *)audio_calloc(1, sizeof(audio_player_t)); + AUDIO_MEM_CHECK(TAG, s_audio_player, goto _exit_open); + + ESP_LOGI(TAG, "Create audio pipeline for audio player"); + audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG(); + s_audio_player->pipeline = audio_pipeline_init(&pipeline_cfg); + AUDIO_MEM_CHECK(TAG, s_audio_player->pipeline, goto _exit_open); + + ESP_LOGI(TAG, "Create tone stream to read data from flash"); + tone_stream_cfg_t tone_cfg = TONE_STREAM_CFG_DEFAULT(); + tone_cfg.type = AUDIO_STREAM_READER; + s_audio_player->tone_stream_reader = tone_stream_init(&tone_cfg); + AUDIO_NULL_CHECK(TAG, s_audio_player->tone_stream_reader, goto _exit_open); + + ESP_LOGI(TAG, "Create mp3 decoder to decode mp3 file"); + mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG(); + s_audio_player->mp3_decoder = mp3_decoder_init(&mp3_cfg); + AUDIO_NULL_CHECK(TAG, s_audio_player->mp3_decoder, goto _exit_open); + + ESP_LOGI(TAG, "Create filter for mp3"); + rsp_filter_cfg_t filter_cfg = DEFAULT_RESAMPLE_FILTER_CONFIG(); + filter_cfg.src_ch = 1; + filter_cfg.src_rate = 16000; +#if CONFIG_ESP32_S3_KORVO2_V3_BOARD || CONFIG_ESP32_S3_BOX_BOARD + filter_cfg.dest_ch = 1; +#else + filter_cfg.dest_ch = 2; +#endif + filter_cfg.dest_rate = 16000; + filter_cfg.stack_in_ext = true; + filter_cfg.task_core = 1; + filter_cfg.complexity = 2; + s_audio_player->filter = rsp_filter_init(&filter_cfg); + AUDIO_MEM_CHECK(TAG, s_audio_player->filter, goto _exit_open); + audio_element_set_write_cb(s_audio_player->filter, _player_write_mp3_cb, NULL); + + ESP_LOGI(TAG, "Register all elements to audio pipeline"); + audio_pipeline_register(s_audio_player->pipeline, s_audio_player->tone_stream_reader, "tone"); + audio_pipeline_register(s_audio_player->pipeline, s_audio_player->mp3_decoder, "mp3"); + audio_pipeline_register(s_audio_player->pipeline, s_audio_player->filter, "filter"); + + ESP_LOGI(TAG, "Link it together [flash]-->tone_stream-->mp3_decoder-->filter-->[codec_chip]"); + const char *link_tag[3] = {"tone", "mp3", "filter"}; + audio_pipeline_link(s_audio_player->pipeline, &link_tag[0], 3); + + + esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG(); + esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg); + audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG(); + audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg); + audio_pipeline_set_listener(s_audio_player->pipeline, evt); + audio_event_iface_set_listener(esp_periph_set_get_event_iface(set), evt); + + s_audio_player->tone_cb = callback; + audio_thread_create(NULL, "audio_player_state_task", audio_player_state_task, (void *)evt, 5 * 1024, 15, true, 1); + + return ESP_OK; + +_exit_open: + audio_pipe_safe_free(s_audio_player->tone_stream_reader, audio_element_deinit); + audio_pipe_safe_free(s_audio_player->mp3_decoder, audio_element_deinit); + audio_pipe_safe_free(s_audio_player->filter, audio_element_deinit); + audio_pipe_safe_free(s_audio_player->pipeline, audio_pipeline_deinit); + audio_pipe_safe_free(s_audio_player, audio_free); + return ESP_FAIL; +} \ No newline at end of file diff --git a/esp-spot/example/adf/llm_touch_toy/main/audio_processor.h b/esp-spot/example/adf/llm_touch_toy/main/audio_processor.h new file mode 100644 index 0000000..7e39709 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/main/audio_processor.h @@ -0,0 +1,197 @@ +/* + * Espressif Modified MIT License + * + * Copyright (c) 2025 Espressif Systems (Shanghai) Co., LTD + * + * Permission is hereby granted for use **exclusively** with Espressif Systems products. + * This includes the right to use, copy, modify, merge, publish, distribute, and sublicense + * the Software, subject to the following conditions: + * + * 1. This Software **must be used in conjunction with Espressif Systems products**. + * 2. The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * 3. Redistribution of the Software in source or binary form **for use with non-Espressif products** + * is strictly prohibited. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + * FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT-ESPRESSIF + */ + +#pragma once + +#include +#include +#include +#include "audio_pipeline.h" + +#if __has_include("audio_tone_uri.h") + #include "audio_tone_uri.h" +#else + #error "please refer the README, and then make the tone file" +#endif + +typedef void (*tone_play_callback_t)(audio_element_status_t evt); + +/** + * @brief Enumeration of player pipeline states + */ +typedef enum { + PIPE_STATE_IDLE, /**< The pipeline is idle and not processing any audio */ + PIPE_STATE_RUNNING, /**< The pipeline is actively processing and playing audio */ +} pipe_player_state_e; + +/** + * @brief Handle type for the audio player. + */ +typedef struct audio_player_s *audio_player_handle_t; + +/** + * @brief Handle type for the audio recorder. + */ +typedef struct audio_recorder_s *audio_recorder_handle_t; + +/** + * @brief Open the recorder pipeline and initialize necessary resources. + * + * @return audio_recorder_handle_t Recorder handle, or NULL on failure. + */ +audio_recorder_handle_t recorder_pipeline_open(); + +/** + * @brief Start the recorder pipeline and begin capturing audio data. + * + * @param[in] recorder Recorder handle. + * + * * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t recorder_pipeline_run(audio_recorder_handle_t recorder); + +/** + * @brief Read audio data from the recorder pipeline. + * + * @param[in] recorder Recorder handle. + * @param[in] buffer Pointer to the buffer where audio data will be stored. + * @param[in] len Maximum number of bytes to read. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t recorder_pipeline_read(audio_recorder_handle_t recorder, char *buffer, int len); + +/** + * @brief Stop the recorder pipeline. + * + * @param[in] recorder Recorder handle. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t recorder_pipeline_stop(audio_recorder_handle_t recorder); + +/** + * @brief Close the recorder pipeline and release all resources. + * + * @param[in] recorder Recorder handle. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t recorder_pipeline_close(audio_recorder_handle_t recorder); + +/** + * @brief Open the player pipeline and initialize necessary resources. + * + * @return audio_player_handle_t Player handle, or NULL on failure. + */ +audio_player_handle_t player_pipeline_open(); + +/** + * @brief Start the player pipeline and begin playback. + * + * @param[in] player Player handle. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t player_pipeline_run(void); + +/** + * @brief Stop the player pipeline. + * + * @param[in] player Player handle. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t player_pipeline_stop(void); + +/** + * @brief Write audio data to the player pipeline for playback. + * + * @param[in] player Player handle. + * @param[in] buffer Pointer to the buffer containing audio data. + * @param[in] len Number of bytes to write. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t player_pipeline_write(audio_player_handle_t player, char *buffer, int len); + +/** + * @brief Close the player pipeline and release all resources. + * + * @param[in] player Player handle. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + */ +esp_err_t player_pipeline_close(audio_player_handle_t player); + +/** + * @brief Initialize the tone playback system. + * + * @param[in] callback Callback function to be called when tone playback is triggered + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Invalid callback or other configuration error + */ +esp_err_t audio_tone_init(tone_play_callback_t callback); + +/** + * @brief Stop the currently playing tone, if any. + * + * @return + * - ESP_OK On success or if no tone is playing + * - ESP_FAIL If stopping tone fails + */ +esp_err_t audio_tone_stop(void); + +/** + * @brief Play a tone from the specified URL + * + * @param[in] tone_url The tone resource to be played, check 'audio_tone_uri.h' + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Invalid tone type or URL + * - ESP_FAIL Other playback errors + */ +esp_err_t audio_tone_play(tone_type_t tone_url); + + diff --git a/esp-spot/example/adf/llm_touch_toy/main/idf_component.yml b/esp-spot/example/adf/llm_touch_toy/main/idf_component.yml new file mode 100644 index 0000000..7680f27 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/main/idf_component.yml @@ -0,0 +1,15 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + # idf: ">=5.4" + espressif/esp_websocket_client: ^1.2.3 + # espressif/esp_hosted: + # version: "~1.1" + # rules: + # - if: "target in [esp32p4]" + # espressif/esp_wifi_remote: + # matches: + # - if: "idf_version <=5.4.0 && target in [esp32p4]" + # version: "~0.4" + # - if: "idf_version >5.4.0 && target in [esp32p4]" + # version: "~0.6" diff --git a/esp-spot/example/adf/llm_touch_toy/main/main.c b/esp-spot/example/adf/llm_touch_toy/main/main.c new file mode 100644 index 0000000..7c91867 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/main/main.c @@ -0,0 +1,410 @@ +/* http client request example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + + Demo video: https://www.bilibili.com/video/BV1ekRAYVEZ1/ + Hardware: https://oshwhub.com/esp-college/esp-spot +*/ + +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/idf_additions.h" +#include "freertos/task.h" + +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_wifi.h" + +#include "audio_sys.h" +#include "audio_thread.h" +#include "esp_peripherals.h" +#include "periph_wifi.h" +#include "periph_sdcard.h" +#include "audio_mem.h" +#include "board.h" +#include "es8311.h" +#include "es7210.h" + +#include "touch_button.h" +#include "iot_button.h" +#include "touch_sensor_lowlevel.h" + +#include "audio_processor.h" +#include "coze_chat.h" +#include "led_driver.h" + +#define DEFAULT_RAW_OPUS_BUFFER_SIZE (1024) + +static char *TAG = "main"; + +#define TOUCH_CHANNEL_1 (3) +#define TOUCH_CHANNEL_2 (9) +#define TOUCH_CHANNEL_3 (13) +#define TOUCH_CHANNEL_4 (14) + +#define LIGHT_TOUCH_THRESHOLD (0.15) +#define HEAVY_TOUCH_THRESHOLD (0.4) + +/* To controll the audio event */ +#define BIT_RECORDING_START (1 << 0) +static EventGroupHandle_t s_audio_event_group; + +struct coze_ws_s { + coze_chat_handle_t chat; + audio_recorder_handle_t recorder; + audio_player_handle_t player; + char *recorder_buffer; + char *opus_raw_buffer; + int opus_raw_buffer_len; + enum { + PLAYBACK_STATE_IDLE, + PLAYBACK_STATE_PLAYING, + } player_state; +}; + +static struct coze_ws_s s_coze_ws; +static audio_board_handle_t board_handle = NULL; + +static void audio_event_callback(coze_chat_event_t event, void *ctx) +{ + if (event == COZE_CHAT_EVENT_CHAT_SPEECH_STARTED) { + ESP_LOGI(TAG, "chat start"); + s_coze_ws.player_state = PLAYBACK_STATE_IDLE; + } else if (event == COZE_CHAT_EVENT_CHAT_SPEECH_STOPED) { + ESP_LOGI(TAG, "chat stop"); + s_coze_ws.player_state = PLAYBACK_STATE_PLAYING; + } +} + +static void audio_data_callback(char *data, int len, void *ctx) +{ +#define frame_length_prefix (2) + if (len > s_coze_ws.opus_raw_buffer_len) { + s_coze_ws.opus_raw_buffer_len = len + frame_length_prefix; + s_coze_ws.opus_raw_buffer = audio_realloc(s_coze_ws.opus_raw_buffer, s_coze_ws.opus_raw_buffer_len); + } + ESP_LOGD(TAG, "data: %p, len: %d", data, len); + s_coze_ws.opus_raw_buffer[0] = (len >> 8) & 0xFF; + s_coze_ws.opus_raw_buffer[1] = len & 0xFF; + memcpy(s_coze_ws.opus_raw_buffer + frame_length_prefix, data, len); + len += frame_length_prefix; + + player_pipeline_write(s_coze_ws.player, s_coze_ws.opus_raw_buffer, len); +} + +static void audio_if_open() +{ + s_coze_ws.recorder = recorder_pipeline_open(); + s_coze_ws.player = player_pipeline_open(); + recorder_pipeline_run(s_coze_ws.recorder); + player_pipeline_run(); +} + +/** + * @brief Temporarily mute the audio output to prevent pop or click noise + * during audio state transitions. + * + * This function briefly mutes the audio hardware for a few milliseconds + * and then unmutes it. It's typically used before starting or stopping + * playback to suppress unwanted transition noise. + */ +static void short_mute() +{ + audio_hal_set_mute(board_handle->audio_hal, true); + vTaskDelay(pdMS_TO_TICKS(30)); + audio_hal_set_mute(board_handle->audio_hal, false); +} + +static void audio_data_read_task(void *pv) +{ +#define recorder_buffer_size (640) + s_coze_ws.recorder_buffer = malloc(recorder_buffer_size); + + s_audio_event_group = xEventGroupCreate(); + xEventGroupSetBits(s_audio_event_group, BIT_RECORDING_START); + while (1) { + /* Stop reading audio if the WebSocket stopped */ + xEventGroupWaitBits(s_audio_event_group, + BIT_RECORDING_START, + pdFALSE, + pdTRUE, + portMAX_DELAY); + int r_len = recorder_pipeline_read(s_coze_ws.recorder, s_coze_ws.recorder_buffer, recorder_buffer_size); + if (r_len > 0) { + coze_chat_send_audio_data(s_coze_ws.chat, s_coze_ws.recorder_buffer, r_len); + } + } + vTaskDelete(NULL); +} + +static void audio_tone_player_event_cb(audio_element_status_t evt) +{ + if (evt == AEL_STATUS_STATE_FINISHED) { + // add more functions here + } +} + +static void touch_event_nose(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event nose: %s", iot_button_get_event_str(event)); + + // Stop coze or local audio + player_pipeline_stop(); + audio_tone_stop(); + + audio_tone_play(TONE_TYPE_TOUCH_NOSE); +} + +static void touch_event_nose_long(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event nose long: %s", iot_button_get_event_str(event)); + + // Stop coze or local audio + player_pipeline_stop(); + audio_tone_stop(); + + audio_tone_play(TONE_TYPE_SCREAMING); +} + +static void touch_event_hat(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event)); + + // Stop coze or local audio + player_pipeline_stop(); + audio_tone_stop(); + + static tone_type_t hat_state = TONE_TYPE_HAT_1; + audio_tone_play(hat_state); + + // Toggle between two tone states and update LED mode + if (hat_state == TONE_TYPE_CAPYBARA_SONG_1) { + // Turn on LED + led_set_mode(4); + hat_state = TONE_TYPE_HAT_1; + } else { + hat_state = TONE_TYPE_CAPYBARA_SONG_1; + } + +} + +static void touch_event_belly(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event)); + + // Stop coze or local audio + player_pipeline_stop(); + audio_tone_stop(); + + static int audio_belly_count_g = 0; + audio_tone_play(audio_belly_count_g); + + // Update the counter for next playback + if (audio_belly_count_g < TONE_TYPE_BELLY_4) { + audio_belly_count_g++; + } else if (audio_belly_count_g == TONE_TYPE_BELLY_4) { + audio_belly_count_g = TONE_TYPE_SCREAMING; + } else { + audio_belly_count_g = TONE_TYPE_BELLY_1; + } +} + +static void touch_event_neck(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event)); + + // Stop coze or local audio + player_pipeline_stop(); + audio_tone_stop(); + + static tone_type_t neck_state = TONE_TYPE_NECK_2; + + // Toggle between two tone states and update LED mode + if (neck_state == TONE_TYPE_NECK_2) { + // Turn on LED + xEventGroupClearBits(s_audio_event_group, BIT_RECORDING_START); + recorder_pipeline_stop(s_coze_ws.recorder); + coze_chat_stop(s_coze_ws.chat); + audio_tone_play(TONE_TYPE_NECK_2); + neck_state = TONE_TYPE_NECK_1; + } else { + audio_tone_play(TONE_TYPE_NECK_1); + neck_state = TONE_TYPE_NECK_2; + coze_chat_start(s_coze_ws.chat); + xEventGroupSetBits(s_audio_event_group, BIT_RECORDING_START); + recorder_pipeline_run(s_coze_ws.recorder); + } +} + +static void touch_task(void *arg) +{ + // Register all touch channel + uint32_t touch_channel_list[] = {TOUCH_CHANNEL_1, TOUCH_CHANNEL_2, TOUCH_CHANNEL_3, TOUCH_CHANNEL_4}; + int total_channel_num = sizeof(touch_channel_list) / sizeof(touch_channel_list[0]); + + // calloc channel_type for every button from the list + touch_lowlevel_type_t *channel_type = calloc(total_channel_num, sizeof(touch_lowlevel_type_t)); + assert(channel_type); + for (int i = 0; i < total_channel_num; i++) { + channel_type[i] = TOUCH_LOWLEVEL_TYPE_TOUCH; + } + + touch_lowlevel_config_t low_config = { + .channel_num = total_channel_num, + .channel_list = touch_channel_list, + .channel_type = channel_type, + }; + esp_err_t ret = touch_sensor_lowlevel_create(&low_config); + assert(ret == ESP_OK); + free(channel_type); + + const button_config_t btn_cfg = { + .short_press_time = 300, + .long_press_time = 2000, + }; + + /* ============================= Init touch IO3 ============================= */ + button_touch_config_t touch_cfg_1 = { + .touch_channel = touch_channel_list[0], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + /* Create button for nose */ + button_handle_t btn_nose = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_1, &btn_nose); + assert(ret == ESP_OK); + + /* ============================= Init touch IO9 ============================= */ + button_touch_config_t touch_cfg_2 = { + .touch_channel = touch_channel_list[1], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + /* Create button for hat */ + button_handle_t btn_hat = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_2, &btn_hat); + assert(ret == ESP_OK); + + /* ============================= Init touch IO13 ============================= */ + button_touch_config_t touch_cfg_3 = { + .touch_channel = touch_channel_list[2], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + /* Create light press button */ + button_handle_t btn_belly = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_3, &btn_belly); + assert(ret == ESP_OK); + + /* ============================= Init touch IO14 ============================= */ + button_touch_config_t touch_cfg_4 = { + .touch_channel = touch_channel_list[3], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + /* Create button for nose */ + button_handle_t btn_neck = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_4, &btn_neck); + assert(ret == ESP_OK); + + /* ========================== Register touch callback ========================== */ + // Register touch callback for nose + iot_button_register_cb(btn_nose, BUTTON_PRESS_DOWN, NULL, touch_event_nose, NULL); + iot_button_register_cb(btn_nose, BUTTON_LONG_PRESS_START, NULL, touch_event_nose_long, NULL); + + // Register touch callback for hat + iot_button_register_cb(btn_hat, BUTTON_PRESS_DOWN, NULL, touch_event_hat, NULL); + + // Register touch callback for belly + iot_button_register_cb(btn_belly, BUTTON_PRESS_DOWN, NULL, touch_event_belly, NULL); + + // Register touch callback for nose + iot_button_register_cb(btn_neck, BUTTON_PRESS_DOWN, NULL, touch_event_neck, NULL); + + touch_sensor_lowlevel_start(); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +static void led_task(void *arg) +{ + /* Configure the LED strip and obtain a handle */ + led_strip_handle_t led_strip = led_create(); + led_set_mode(1); + vTaskDelay(1000); + led_set_mode(0); + + while(1) { + /* Run the LED animation based on LED configuration and weather data */ + ESP_ERROR_CHECK(led_animations_start(led_strip)); + } + + vTaskDelete(NULL); +} + +void app_main(void) +{ + AUDIO_MEM_SHOW(TAG); + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_LOGI(TAG, "Initialize board peripherals"); + esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG(); + esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg); + + board_handle = audio_board_init(); + audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_BOTH, AUDIO_HAL_CTRL_START); +#if CONFIG_ESP32_S3_SPOT_BOARD + audio_hal_set_volume(board_handle->audio_hal, 90); + es8311_set_mic_gain(ES8311_MIC_GAIN_0DB); +#endif + + s_coze_ws.opus_raw_buffer_len = DEFAULT_RAW_OPUS_BUFFER_SIZE; + s_coze_ws.opus_raw_buffer = audio_malloc(s_coze_ws.opus_raw_buffer_len); + // Initialize SD Card peripheral + // audio_board_sdcard_init(set, SD_MODE_1_LINE); + periph_wifi_cfg_t wifi_cfg = { + .wifi_config.sta.ssid = CONFIG_ESP_WIFI_SSID, + .wifi_config.sta.password = CONFIG_ESP_WIFI_PASSWORD, + }; + esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg); + esp_periph_start(set, wifi_handle); + periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY); + + coze_chat_config_t chat_config = COZE_CHAT_DEFAULT_CONFIG(); + chat_config.bot_id = CONFIG_BOT_ID; + chat_config.access_token = CONFIG_ACCESS_TOKEN; + chat_config.audio_callback = audio_data_callback; + chat_config.event_callback = audio_event_callback; + + s_coze_ws.chat = coze_chat_init(&chat_config); + coze_chat_start(s_coze_ws.chat); + + audio_if_open(); + audio_thread_create(NULL, "audio_data_read_task", audio_data_read_task, (void *)NULL, 1024 * 4, 12, true, 1); + + xTaskCreate(touch_task, "touch_task", 1024 * 5, NULL, 5, NULL); + xTaskCreate(led_task, "led_task", 1024 * 3, NULL, 5, NULL); + + audio_tone_init(audio_tone_player_event_cb); + + AUDIO_MEM_SHOW(TAG); +} diff --git a/esp-spot/example/adf/llm_touch_toy/partitions.csv b/esp-spot/example/adf/llm_touch_toy/partitions.csv new file mode 100644 index 0000000..38296bf --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/partitions.csv @@ -0,0 +1,8 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 2M, +model, data, spiffs, , 5168K, +flash_tone,data, 0xff, 0x720000 ,2M, + diff --git a/esp-spot/example/adf/llm_touch_toy/sdkconfig.defaults b/esp-spot/example/adf/llm_touch_toy/sdkconfig.defaults new file mode 100644 index 0000000..2f51132 --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/sdkconfig.defaults @@ -0,0 +1,39 @@ +# +# Serial flasher config +# +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +# end of Serial flasher config + +# +# Partition Table +# +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# ESP-TLS +# +CONFIG_ESP_TLS_INSECURE=y +CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y + +# +# FREERTOS +# +CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY=y +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y +CONFIG_FREERTOS_HZ=1000 + +# +# TOUCH +# +CONFIG_TOUCH_BUTTON_SENSOR_MAX_P_X1000=0 +CONFIG_TOUCH_BUTTON_SENSOR_MIN_N_X1000=0 +CONFIG_TOUCH_BUTTON_SENSOR_HYSTERESIS_P_X1000=200 \ No newline at end of file diff --git a/esp-spot/example/adf/llm_touch_toy/sdkconfig.defaults.esp32s3 b/esp-spot/example/adf/llm_touch_toy/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000..3f58c4f --- /dev/null +++ b/esp-spot/example/adf/llm_touch_toy/sdkconfig.defaults.esp32s3 @@ -0,0 +1,112 @@ +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_IDF_TARGET_ESP32S3=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009 + + +# +# Audio HAL +# +CONFIG_ESP32_S3_SPOT_BOARD=y +# end of Audio HAL + +# +# Audio Recorder +# +CONFIG_AFE_MIC_NUM=2 +# end of Audio Recorder + +# +# ESP Speech Recognition +# +CONFIG_MODEL_IN_FLASH=y +CONFIG_USE_AFE=y +CONFIG_AFE_INTERFACE_V1=y +CONFIG_USE_WAKENET=y +CONFIG_SR_WN_WN9_HILEXIN=y +CONFIG_USE_MULTINET=y +CONFIG_SR_MN_CN_MULTINET6_QUANT=y +CONFIG_SR_MN_EN_NONE=y +# end of ESP Speech Recognition + +# +# Component config +# + +# +# Driver configurations +# + +# +# ESP32S3-Specific +# +# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set +# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240 + +# +# Cache config +# +# CONFIG_ESP32S3_INSTRUCTION_CACHE_16KB is not set +CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y +CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE=0x8000 +# CONFIG_ESP32S3_INSTRUCTION_CACHE_4WAYS is not set +CONFIG_ESP32S3_INSTRUCTION_CACHE_8WAYS=y +CONFIG_ESP32S3_ICACHE_ASSOCIATED_WAYS=8 +CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_32B=y +CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_SIZE=32 +# CONFIG_ESP32S3_INSTRUCTION_CACHE_WRAP is not set +# CONFIG_ESP32S3_DATA_CACHE_16KB is not set +# CONFIG_ESP32S3_DATA_CACHE_32KB is not set +CONFIG_ESP32S3_DATA_CACHE_64KB=y +CONFIG_ESP32S3_DATA_CACHE_SIZE=0x10000 +# CONFIG_ESP32S3_DATA_CACHE_4WAYS is not set +CONFIG_ESP32S3_DATA_CACHE_8WAYS=y +CONFIG_ESP32S3_DCACHE_ASSOCIATED_WAYS=8 +# CONFIG_ESP32S3_DATA_CACHE_LINE_32B is not set +CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y +CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE=64 +# CONFIG_ESP32S3_DATA_CACHE_WRAP is not set +# end of Cache config + +CONFIG_ESP32S3_SPIRAM_SUPPORT=y + +# +# SPI RAM config +# +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TYPE_AUTO=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_MEMTEST=y +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 +# end of SPI RAM config +# end of ESP32S3-Specific + +# +# Wi-Fi +# +CONFIG_ESP_WIFI_ENABLED=y +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=16 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y +CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP_WIFI_TX_BA_WIN=16 +CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP_WIFI_RX_BA_WIN=16 + +# +# LWIP +# +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=65535 +CONFIG_LWIP_TCP_WND_DEFAULT=65535 +CONFIG_LWIP_TCP_RECVMBOX_SIZE=32 \ No newline at end of file diff --git a/esp-spot/example/adf/llm_touch_toy/tools/audio_tone.bin b/esp-spot/example/adf/llm_touch_toy/tools/audio_tone.bin new file mode 100644 index 0000000..62d0997 Binary files /dev/null and b/esp-spot/example/adf/llm_touch_toy/tools/audio_tone.bin differ diff --git a/esp-spot/example/adf/touch_play_mp3/CMakeLists.txt b/esp-spot/example/adf/touch_play_mp3/CMakeLists.txt new file mode 100644 index 0000000..f5de8d6 --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{ADF_PATH}/CMakeLists.txt) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(touch_play_mp3) \ No newline at end of file diff --git a/esp-spot/example/adf/touch_play_mp3/Makefile b/esp-spot/example/adf/touch_play_mp3/Makefile new file mode 100644 index 0000000..78579c9 --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/Makefile @@ -0,0 +1,2 @@ +PROJECT_NAME := play_tone_mp3 +include $(ADF_PATH)/project.mk diff --git a/esp-spot/example/adf/touch_play_mp3/README_CN.md b/esp-spot/example/adf/touch_play_mp3/README_CN.md new file mode 100644 index 0000000..4f22e27 --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/README_CN.md @@ -0,0 +1,114 @@ +# 从 Flash 中播放 MP3 文件例程 + +## 例程简介 + +本例程源自 ADF 例程:[pipeline_flash_tone](https://github.com/espressif/esp-adf/tree/master/examples/player/pipeline_flash_tone),需在 [ADF](https://github.com/espressif/esp-adf/tree/master) 环境下开发 + +通过触摸事件,触发音频管道 API 播放存储在 flash 中的 MP3 文件,同时驱动 WS2812 灯环。 + +## 预备知识 + +本例程在 `/tools/audio_tone.bin` 和 `/components/audio_flash_tone/` 目录下已经帮助用户生成了例程所需的 bin 文件和音频文件在 flash 中地址的源代码文件。 + +如果用户需要生成自己的 `audio_tone.bin`,则需要执行 `mk_audio_bin.py` 脚本(位于 $ADF_PATH/tools/audio_tone/mk_audio_tone.py),并且指定相关文件的路径。 + + 源 MP3 文件在 `tone_mp3_folder` 文件夹中,生成的 C 文件、H 文件以及二进制 bin 文件都存放在此目录下。 + +``` +python3 $ADF_PATH/tools/audio_tone/mk_audio_tone.py -f ./ -r tone_mp3_folder +``` + +请使用 *python3 $ADF_PATH/tools/audio_tone/mk_audio_tone.py --help* 查看更多脚本信息。 + +本例程默认的 `audio_tone.bin` 包含如下音频文件: + +```c + "flash://tone/0_belly_1.mp3", + "flash://tone/1_belly_2.mp3", + "flash://tone/2_belly_3.mp3", + "flash://tone/3_belly_4.mp3", + "flash://tone/4_bread_1.mp3", + "flash://tone/5_bread_2.mp3", + "flash://tone/6_capybara_song_1.mp3", + "flash://tone/7_hat_1.mp3", + "flash://tone/8_neck_1.mp3", + "flash://tone/9_neck_2.mp3", + "flash://tone/10_reverse.mp3", + "flash://tone/11_screaming.mp3", + "flash://tone/12_shake.mp3", + "flash://tone/13_touch_nose.mp3", + "flash://tone/14_woohoo.mp3", +``` + +## 环境配置 + +### IDF 默认分支 + +本例程支持 IDF release/v5.0 及以后的分支,例程默认使用 ADF 的內建分支 `$ADF_PATH/esp-idf`。 + +## 配置说明 + +1. 开发板 + - 默认使用搭载 ESP32-S3 模组的 ``ESP-SPOT`` 开发板。该模组集成触摸传感器功能,技术细节请参考::[ESP32-S3 数据手册](https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_cn.pdf) + +2. LED 灯环 + - 控制灯环的 IO 默认为 `GPIO45`,可通过 `idf.py menuconfig` 修改 `CONFIG_LED_GPIO_INPUT` 控制灯环的 IO,配置 `CONFIG_LED_COUNT` 修改 LED 数量,默认为 16 个 + +3. 音频文件 + - **如果 MP3 音频总数量大于 `9`,则需要修改 `$ADF_PATH/components/audio_stream` 文件夹中的 `tone_stream.c` 文件** + - 将 `_tone_open(audio_element_handle_t self)` 函数中 `char find_num[2]` 数组的长度修改为 `char find_num[3]`。这样可以确保函数在解析双位数的音频文件名(例如 10.mp3、11.mp3)时不会发生字符串截断错误,正确识别全部音频文件 + +4. 音频储存地址 + - 此例程的 `flashTone` 在 `partition_flash_tone.csv` 中地址配置如下,用户可以根据自己的项目 flash 分区灵活配置地址 + + ``` + flashTone,data, 0x04, 0x110000 , 500K, + ``` + + +### 编译和下载 + +请先编译版本并烧录到开发板上,然后运行 monitor 工具来查看串口输出(替换 PORT 为端口名称): + +``` +idf.py -p PORT flash monitor +``` + +**此外,本例程还需烧录 `/tools/audio_tone.bin` 到 `partition_flash_tone.csv` 的 `flashTone` 分区,请使用如下命令:** + +``` +esptool.py --chip esp32s3 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x110000 ./tools/audio_tone.bin +``` + +有关配置和使用 ESP-IDF 生成项目的完整步骤,请参阅 [《ESP-IDF 编程指南》](https://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.3/esp32/index.html)。 + + +## 如何使用 + +### 功能和用法 + +- 例程开始运行后,如果事前没有烧录 `/tools/audio_tone.bin` 到 `partition_flash_tone.csv ` 的 `flashTone` 分区,例程将会报错,请参考 [编译和下载](#编译和下载) 的说明进行烧录 + +## 故障排除 + +```c +E (481) TONE_PARTITION: Not flash tone partition +E (481) AUDIO_ELEMENT: [tone] AEL_STATUS_ERROR_OPEN,-1 +W (491) AUDIO_ELEMENT: [tone] audio_element_on_cmd_error,7 +E (501) TONE_PARTITION: /repo/adfs/bugfix/esp-adf-internal/components/tone_partition/tone_partition.c:204 (tone_partition_deinit): Got NULL Pointer +W (511) AUDIO_ELEMENT: IN-[mp3] AEL_IO_ABORT +E (511) MP3_DECODER: failed to read audio data (line 119) +W (521) AUDIO_ELEMENT: [mp3] AEL_IO_ABORT, -3 +W (531) AUDIO_ELEMENT: IN-[i2s] AEL_IO_ABORT +``` + +如果遇到上述的错误,请把按照 [编译和下载](#编译和下载) 的说明烧录 `/tools/audio_tone.bin` 到 `partition_flash_tone.csv ` 的 `flashTone` 分区。 + + +## 技术支持 +请按照下面的链接获取技术支持: + +- 技术支持参见 [esp32.com](https://esp32.com/viewforum.php?f=20) 论坛 +- 故障和新功能需求,请创建 [GitHub issue](https://github.com/espressif/esp-adf/issues) + +我们会尽快回复。 diff --git a/esp-spot/example/adf/touch_play_mp3/main/CMakeLists.txt b/esp-spot/example/adf/touch_play_mp3/main/CMakeLists.txt new file mode 100644 index 0000000..2f819b2 --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/main/CMakeLists.txt @@ -0,0 +1,6 @@ +set(COMPONENT_REQUIRES audio_flash_tone audio_stream audio_pipeline audio_hal esp_peripherals touch_button led_driver) + +set(COMPONENT_SRCS "touch_play_mp3.c") +set(COMPONENT_ADD_INCLUDEDIRS .) + +register_component() \ No newline at end of file diff --git a/esp-spot/example/adf/touch_play_mp3/main/component.mk b/esp-spot/example/adf/touch_play_mp3/main/component.mk new file mode 100644 index 0000000..c5210b4 --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/main/component.mk @@ -0,0 +1,3 @@ +# +# Main Makefile. This is basically the same as a component makefile. +# diff --git a/esp-spot/example/adf/touch_play_mp3/main/idf_component.yml b/esp-spot/example/adf/touch_play_mp3/main/idf_component.yml new file mode 100644 index 0000000..cc9f9d0 --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/main/idf_component.yml @@ -0,0 +1,12 @@ +dependencies: + espressif/esp_hosted: + version: "~0.0.9" + rules: + - if: "target in [esp32p4]" + espressif/esp_wifi_remote: + version: "~0.3.0" + rules: + - if: "target in [esp32p4]" + # lijunru-hub/touch_button: + # version: "0.1.0" + # registry_url: https://components-staging.espressif.com \ No newline at end of file diff --git a/esp-spot/example/adf/touch_play_mp3/main/touch_play_mp3.c b/esp-spot/example/adf/touch_play_mp3/main/touch_play_mp3.c new file mode 100644 index 0000000..5ae2acb --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/main/touch_play_mp3.c @@ -0,0 +1,349 @@ +/* Play MP3 file from flash + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + + Demo video: https://www.bilibili.com/video/BV1ekRAYVEZ1/ +*/ + +#include +#include + +#include "esp_log.h" + +#include "audio_element.h" +#include "audio_pipeline.h" +#include "audio_event_iface.h" +#include "audio_error.h" +#include "tone_stream.h" +#include "i2s_stream.h" +#include "mp3_decoder.h" +#include "driver/gpio.h" + +#include "touch_button.h" +#include "iot_button.h" +#include "touch_sensor_lowlevel.h" + +#include "led_driver.h" + +#include "board.h" + +#if __has_include("audio_tone_uri.h") + #include "audio_tone_uri.h" +#else + #error "please refer the README, and then make the tone file" +#endif + +static const char *TAG = "main"; + +#define TOUCH_CHANNEL_1 (3) +#define TOUCH_CHANNEL_2 (9) +#define TOUCH_CHANNEL_3 (13) +#define TOUCH_CHANNEL_4 (14) + +#define LIGHT_TOUCH_THRESHOLD (0.15) +#define HEAVY_TOUCH_THRESHOLD (0.4) + +audio_pipeline_handle_t pipeline; +audio_element_handle_t tone_stream_reader, i2s_stream_writer, mp3_decoder; + +static void led_task(void *arg) +{ + /* Configure the LED strip and obtain a handle */ + led_strip_handle_t led_strip = led_create(); + led_set_mode(1); + vTaskDelay(1000); + led_set_mode(0); + + while(1) { + /* Run the LED animation based on LED configuration and weather data */ + ESP_ERROR_CHECK(led_animations_start(led_strip)); + } + + vTaskDelete(NULL); +} + +static void touch_event_nose(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event nose: %s", iot_button_get_event_str(event)); + + audio_pipeline_stop(pipeline); + audio_pipeline_wait_for_stop(pipeline); + audio_pipeline_reset_ringbuffer(pipeline); + audio_pipeline_reset_elements(pipeline); + audio_pipeline_reset_items_state(pipeline); + + audio_element_set_uri(tone_stream_reader, tone_uri[TONE_TYPE_TOUCH_NOSE]); + audio_pipeline_run(pipeline); +} + +static void touch_event_nose_long(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event nose long: %s", iot_button_get_event_str(event)); + + audio_pipeline_stop(pipeline); + audio_pipeline_wait_for_stop(pipeline); + audio_pipeline_reset_ringbuffer(pipeline); + audio_pipeline_reset_elements(pipeline); + audio_pipeline_reset_items_state(pipeline); + + audio_element_set_uri(tone_stream_reader, tone_uri[TONE_TYPE_SCREAMING]); + audio_pipeline_run(pipeline); +} + +static void touch_event_hat(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event)); + + static tone_type_t hat_state = TONE_TYPE_HAT_1; + audio_pipeline_stop(pipeline); + audio_pipeline_wait_for_stop(pipeline); + audio_pipeline_reset_ringbuffer(pipeline); + audio_pipeline_reset_elements(pipeline); + audio_pipeline_reset_items_state(pipeline); + + audio_element_set_uri(tone_stream_reader, tone_uri[hat_state]); + audio_pipeline_run(pipeline); + + // Toggle between two tone states and update LED mode + if (hat_state == TONE_TYPE_CAPYBARA_SONG_1) { + // Turn on LED + led_set_mode(4); + hat_state = TONE_TYPE_HAT_1; + } else { + hat_state = TONE_TYPE_CAPYBARA_SONG_1; + } + +} + +static void touch_event_belly(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event)); + + static int audio_belly_count_g = 0; + + audio_pipeline_stop(pipeline); + audio_pipeline_wait_for_stop(pipeline); + audio_pipeline_reset_ringbuffer(pipeline); + audio_pipeline_reset_elements(pipeline); + audio_pipeline_reset_items_state(pipeline); + + audio_element_set_uri(tone_stream_reader, tone_uri[audio_belly_count_g]); + + audio_pipeline_run(pipeline); + + // Update the counter for next playback + if (audio_belly_count_g < 3) { + audio_belly_count_g++; + } else if (audio_belly_count_g == 3) { + audio_belly_count_g = TONE_TYPE_SCREAMING; + } else { + audio_belly_count_g = 0; + } +} + +static void touch_task(void *arg) +{ + // Register all touch channel + uint32_t touch_channel_list[] = {TOUCH_CHANNEL_1, TOUCH_CHANNEL_2, TOUCH_CHANNEL_3, TOUCH_CHANNEL_4}; + int total_channel_num = sizeof(touch_channel_list) / sizeof(touch_channel_list[0]); + + // calloc channel_type for every button from the list + touch_lowlevel_type_t *channel_type = calloc(total_channel_num, sizeof(touch_lowlevel_type_t)); + assert(channel_type); + for (int i = 0; i < total_channel_num; i++) { + channel_type[i] = TOUCH_LOWLEVEL_TYPE_TOUCH; + } + + touch_lowlevel_config_t low_config = { + .channel_num = total_channel_num, + .channel_list = touch_channel_list, + .channel_type = channel_type, + }; + esp_err_t ret = touch_sensor_lowlevel_create(&low_config); + assert(ret == ESP_OK); + free(channel_type); + + /* ============================= Init touch IO3 ============================= */ + const button_config_t btn_cfg = { + .short_press_time = 300, + .long_press_time = 2000, + }; + button_touch_config_t touch_cfg_1 = { + .touch_channel = touch_channel_list[0], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + /* Create button for nose */ + button_handle_t btn_nose = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_1, &btn_nose); + assert(ret == ESP_OK); + + // Create button for nose heavy press (if neeeded) + // touch_cfg_1.channel_threshold = HEAVY_TOUCH_THRESHOLD; + // button_handle_t btn_heavy_1 = NULL; + // ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_1, &btn_heavy_1); + // assert(ret == ESP_OK); + + /* ============================= Init touch IO9 ============================= */ + button_touch_config_t touch_cfg_2 = { + .touch_channel = touch_channel_list[1], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + /* Create button for hat */ + button_handle_t btn_hat = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_2, &btn_hat); + assert(ret == ESP_OK); + + /* ============================= Init touch IO13 ============================= */ + button_touch_config_t touch_cfg_3 = { + .touch_channel = touch_channel_list[2], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + /* Create light press button */ + button_handle_t btn_belly = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_3, &btn_belly); + assert(ret == ESP_OK); + + /* ========================== Register touch callback ========================== */ + // Register touch callback for nose + iot_button_register_cb(btn_nose, BUTTON_PRESS_DOWN, NULL, touch_event_nose, NULL); + iot_button_register_cb(btn_nose, BUTTON_LONG_PRESS_START, NULL, touch_event_nose_long, NULL); + + // Register touch callback for hat + iot_button_register_cb(btn_hat, BUTTON_PRESS_DOWN, NULL, touch_event_hat, NULL); + + // Register touch callback for belly + iot_button_register_cb(btn_belly, BUTTON_PRESS_DOWN, NULL, touch_event_belly, NULL); + + touch_sensor_lowlevel_start(); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +void app_main(void) +{ + xTaskCreate(touch_task, "touch_task", 1024 * 5, NULL, 5, NULL); + xTaskCreate(led_task, "led_task", 1024 * 3, NULL, 5, NULL); + + esp_log_level_set("*", ESP_LOG_WARN); + esp_log_level_set(TAG, ESP_LOG_INFO); + + ESP_LOGI(TAG, "[ 1 ] Start codec chip"); + audio_board_handle_t board_handle = audio_board_init(); + audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START); + audio_hal_set_volume(board_handle->audio_hal, 80); + + ESP_LOGI(TAG, "[2.0] Create audio pipeline for playback"); + audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG(); + pipeline = audio_pipeline_init(&pipeline_cfg); + AUDIO_NULL_CHECK(TAG, pipeline, return); + + ESP_LOGI(TAG, "[2.1] Create tone stream to read data from flash"); + tone_stream_cfg_t tone_cfg = TONE_STREAM_CFG_DEFAULT(); + tone_cfg.type = AUDIO_STREAM_READER; + tone_stream_reader = tone_stream_init(&tone_cfg); + AUDIO_NULL_CHECK(TAG, tone_stream_reader, return); + + ESP_LOGI(TAG, "[2.2] Create i2s stream to write data to codec chip"); +#if defined CONFIG_ESP32_C3_LYRA_V2_BOARD + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_PDM_TX_CFG_DEFAULT(); +#else + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT(); +#endif + i2s_cfg.type = AUDIO_STREAM_WRITER; + i2s_stream_writer = i2s_stream_init(&i2s_cfg); + AUDIO_NULL_CHECK(TAG, i2s_stream_writer, return); + + ESP_LOGI(TAG, "[2.3] Create mp3 decoder to decode mp3 file"); + mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG(); + mp3_decoder = mp3_decoder_init(&mp3_cfg); + AUDIO_NULL_CHECK(TAG, mp3_decoder, return); + + ESP_LOGI(TAG, "[2.4] Register all elements to audio pipeline"); + audio_pipeline_register(pipeline, tone_stream_reader, "tone"); + audio_pipeline_register(pipeline, mp3_decoder, "mp3"); + audio_pipeline_register(pipeline, i2s_stream_writer, "i2s"); + + ESP_LOGI(TAG, "[2.5] Link it together [flash]-->tone_stream-->mp3_decoder-->i2s_stream-->[codec_chip]"); + const char *link_tag[3] = {"tone", "mp3", "i2s"}; + audio_pipeline_link(pipeline, &link_tag[0], 3); + + ESP_LOGI(TAG, "[2.6] Set up uri (file as tone_stream, mp3 as mp3 decoder, and default output is i2s)"); + audio_element_set_uri(tone_stream_reader, tone_uri[TONE_TYPE_SCREAMING]); + + ESP_LOGI(TAG, "[ 3 ] Set up event listener"); + audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG(); + audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg); + + ESP_LOGI(TAG, "[3.1] Listening event from all elements of pipeline"); + audio_pipeline_set_listener(pipeline, evt); + + ESP_LOGI(TAG, "[ 4 ] Start audio_pipeline"); + audio_pipeline_run(pipeline); + + ESP_LOGI(TAG, "[ 4 ] Listen for all pipeline events"); + while (1) { + audio_event_iface_msg_t msg = { 0 }; + esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "[ * ] Event interface error : %d", ret); + continue; + } + + if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && msg.source == (void *) mp3_decoder + && msg.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) { + audio_element_info_t music_info = {0}; + audio_element_getinfo(mp3_decoder, &music_info); + + ESP_LOGI(TAG, "[ * ] Receive music info from mp3 decoder, sample_rates=%d, bits=%d, ch=%d", + music_info.sample_rates, music_info.bits, music_info.channels); + + i2s_stream_set_clk(i2s_stream_writer, music_info.sample_rates, music_info.bits, music_info.channels); + continue; + } + + /* Stop when the last pipeline element (i2s_stream_writer in this case) receives stop event */ + if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && msg.source == (void *) i2s_stream_writer + && msg.cmd == AEL_MSG_CMD_REPORT_STATUS + && (((int)msg.data == AEL_STATUS_STATE_FINISHED))) { + // Turn off LED + led_set_mode(0); + } + } + + ESP_LOGI(TAG, "[ 5 ] Stop audio_pipeline"); + audio_pipeline_stop(pipeline); + audio_pipeline_wait_for_stop(pipeline); + audio_pipeline_terminate(pipeline); + + audio_pipeline_unregister(pipeline, tone_stream_reader); + audio_pipeline_unregister(pipeline, i2s_stream_writer); + audio_pipeline_unregister(pipeline, mp3_decoder); + + /* Terminal the pipeline before removing the listener */ + audio_pipeline_remove_listener(pipeline); + + /* Make sure audio_pipeline_remove_listener & audio_event_iface_remove_listener are called before destroying event_iface */ + audio_event_iface_destroy(evt); + + /* Release all resources */ + audio_pipeline_deinit(pipeline); + audio_element_deinit(tone_stream_reader); + audio_element_deinit(i2s_stream_writer); + audio_element_deinit(mp3_decoder); +} diff --git a/esp-spot/example/adf/touch_play_mp3/partitions_flash_tone.csv b/esp-spot/example/adf/touch_play_mp3/partitions_flash_tone.csv new file mode 100644 index 0000000..861869d --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/partitions_flash_tone.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 1M, +flash_tone,data, 0xff, 0x110000 ,5M, diff --git a/esp-spot/example/adf/touch_play_mp3/sdkconfig.defaults b/esp-spot/example/adf/touch_play_mp3/sdkconfig.defaults new file mode 100644 index 0000000..e3d3c96 --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/sdkconfig.defaults @@ -0,0 +1,16 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.3.1 Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_flash_tone.csv" +CONFIG_ESP32_S3_SPOT_BOARD=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +CONFIG_ESP_COREDUMP_ENABLE_TO_UART=y +CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY=y +CONFIG_TOUCH_BUTTON_SENSOR_MAX_P_X1000=0 +CONFIG_TOUCH_BUTTON_SENSOR_MIN_N_X1000=0 +CONFIG_TOUCH_BUTTON_SENSOR_HYSTERESIS_P_X1000=200 diff --git a/esp-spot/example/adf/touch_play_mp3/sdkconfig.defaults.esp32s3 b/esp-spot/example/adf/touch_play_mp3/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000..bf18c4e --- /dev/null +++ b/esp-spot/example/adf/touch_play_mp3/sdkconfig.defaults.esp32s3 @@ -0,0 +1,16 @@ +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_IDF_TARGET_ESP32S3=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009 + +# +# SPI RAM config +# +CONFIG_ESP32S3_SPIRAM_SUPPORT=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +CONFIG_SPIRAM_TYPE_AUTO=y diff --git a/esp-spot/example/adf/touch_play_mp3/tools/audio_tone.bin b/esp-spot/example/adf/touch_play_mp3/tools/audio_tone.bin new file mode 100644 index 0000000..62d0997 Binary files /dev/null and b/esp-spot/example/adf/touch_play_mp3/tools/audio_tone.bin differ diff --git a/esp-spot/example/adf/volc_rtc_spot/CMakeLists.txt b/esp-spot/example/adf/volc_rtc_spot/CMakeLists.txt new file mode 100644 index 0000000..db9a017 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{ADF_PATH}/CMakeLists.txt) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(volc_rtc) \ No newline at end of file diff --git a/esp-spot/example/adf/volc_rtc_spot/Makefile b/esp-spot/example/adf/volc_rtc_spot/Makefile new file mode 100644 index 0000000..624968e --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/Makefile @@ -0,0 +1,3 @@ +PROJECT_NAME := tcp_client_example +include $(ADF_PATH)/project.mk + diff --git a/esp-spot/example/adf/volc_rtc_spot/README_CN.md b/esp-spot/example/adf/volc_rtc_spot/README_CN.md new file mode 100644 index 0000000..e5b52f6 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/README_CN.md @@ -0,0 +1,309 @@ +# VOLC RTC 语音交互例程 + +- [English Version](./README.md) +- 例程难度:![alt text](../../../docs/_static/level_complex.png "高级") + + +## 例程简介 + +本例程主要功能是连接豆包 volcano rtc 云端并进行语音交互,可以适用于智能音箱产品、智能玩具、语音控制设备等。此示例是一个综合性较强的例程,使用了 ADF 提供的高封装简易实用接口。建议用户构建项目时,优先使用 ADF 提供的高封装接口,可快速简便地构建项目。 + +## 环境配置 + +### 硬件要求 + +目前该 example 支持 `esp32s3` 和 `esp32` 相关的开发板。 +默认使用的是 `ESP32-S3-Korvo-2 v3` 开发板。 + +## 前期准备 + +1. 关于豆包简介 + +- 请参考 [火山·引擎 开放平台](https://www.volcengine.com/docs/6348/1315561) 开通火山引擎服务的正式版本,需要获取 `appid`, `userid`, `roomid` 和 `临时token`, 需要先使能 RTC_TOKEN_REQUEST, 将四个参数填到 `menuconfig-> Example Configuration` 对应的配置中, 然后在 [api-explorer](https://api.volcengine.com/api-explorer?action=StartVoiceChat&groupName=%E6%99%BA%E8%83%BD%E4%BD%93&serviceCode=rtc&version=2024-12-01) 中启动智能体,之后设备就可以连接到火山引擎进行语音交互了。 + +- 如果使用 [coze 方案](https://www.coze.cn/open/docs/developer_guides/coze_api_overview),需要注册相关的账号,example 默认使能 coze 的测试账号,该测试账号每次的使用时间是 5 分钟。 + +2. 关于 wifi 配置 + - 在 menuconfig 中, 填写 SSID 和 PASSWORD, 然后编译下载到开发板中。 + +3. 关于编码格式 + - 在 menuconfig 中,可选择 `PCMA` , `OPUS` 和 `AAC` 编解码格式, 默认是 `OPUS` 格式。 + > `OPUS` 编码默认的是 32kbps。 + +4. 关于工作模式 + + 目前支持 普通模式 和 唤醒模式 + - **普通模式** 用户无需唤醒词,直接与设备进行语音交互。 +- **唤醒对话模式** 用户需要通过唤醒词唤醒设备,唤醒后用户可与设备进行语音交互。默认的唤醒词是 `Hi 乐鑫`, 可在 `menuconfig -> ESP Speech Recognition → use wakenet → Select wake words` 中选择唤醒词。 + +> 如果使用唤醒模式,建议使能 120M flash 和 120M PSRAM + - CONFIG_IDF_EXPERIMENTAL_FEATURES=y + - CONFIG_ESPTOOLPY_FLASHFREQ_120M=y + - CONFIG_SPIRAM_SPEED_120M=y + + +## 使用说明 + +### 配置 + +## 编译和下载 + +### IDF 默认分支 + +本例程支持 IDF release/v5.4 及以后的分支,例程默认使用 ADF 的內建分支 `$ADF_PATH/esp-idf`。 + +### 编译和下载 + +请先编译版本并烧录到开发板上,然后运行 monitor 工具来查看串口输出 (替换 PORT 为端口名称): + +``` +idf.py -p PORT flash monitor +``` + +退出调试界面使用 ``Ctrl-]``。 + +有关配置和使用 ESP-IDF 生成项目的完整步骤,请参阅 [《ESP-IDF 编程指南》](https://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.3/esp32/index.html)。 + +## 如何使用例程 + +### 功能和用法 + +- 例程运行时,会先尝试连接 Wi-Fi 网络,待成功获取 IP 地址后会进入 RTC 房间。log 中会有 `volc_rtc: join room success` 的打印,设备端播放服务器下发的"你好呀,很高兴认识你,哈哈哈"类的提示语,便可以与智能体进行对话。 +```c +I (25) boot: ESP-IDF v5.5-dev-972-gfa41fafd27-dirty 2nd stage bootloader +I (25) boot: compile time Jan 10 2025 10:39:14 +I (25) boot: Multicore bootloader +I (27) boot: chip revision: v0.2 +I (30) boot: efuse block revision: v1.3 +I (34) qio_mode: Enabling default flash chip QIO +I (38) boot.esp32s3: Boot SPI Speed : 80MHz +I (42) boot.esp32s3: SPI Mode : QIO +I (46) boot.esp32s3: SPI Flash Size : 16MB +I (49) boot: Enabling RNG early entropy source... +I (54) boot: Partition Table: +I (56) boot: ## Label Usage Type ST Offset Length +I (63) boot: 0 nvs WiFi data 01 02 00009000 00004000 +I (69) boot: 1 phy_init RF data 01 01 0000d000 00001000 +I (76) boot: 2 factory factory app 00 00 00010000 00300000 +I (82) boot: 3 model Unknown data 01 82 00310000 0040e000 +I (89) boot: 4 spiffs_data Unknown data 01 82 0071e000 00010000 +I (95) boot: End of partition table +I (99) esp_image: segment 0: paddr=00010020 vaddr=3c180020 size=5db38h (383800) map +I (163) esp_image: segment 1: paddr=0006db60 vaddr=3fca2600 size=024b8h ( 9400) load +I (165) esp_image: segment 2: paddr=00070020 vaddr=42000020 size=178fc4h (1544132) map +I (396) esp_image: segment 3: paddr=001e8fec vaddr=3fca4ab8 size=05db8h ( 23992) load +I (401) esp_image: segment 4: paddr=001eedac vaddr=40378000 size=1a520h (107808) load +I (421) esp_image: segment 5: paddr=002092d4 vaddr=600fe000 size=0001ch ( 28) load +I (433) boot: Loaded app from partition at offset 0x10000 +I (433) boot: Disabling RNG early entropy source... +W (443) flash HPM: HPM mode is optional feature that depends on flash model. Read Docs First! +W (443) flash HPM: HPM mode with DC adjustment is disabled. Some flash models may not be supported. Read Docs First! +W (452) flash HPM: High performance mode of this flash model hasn't been supported. +I (460) MSPI Timing: Flash timing tuning index: 2 +I (466) octal_psram: vendor id : 0x0d (AP) +I (471) octal_psram: dev id : 0x02 (generation 3) +I (476) octal_psram: density : 0x03 (64 Mbit) +I (482) octal_psram: good-die : 0x01 (Pass) +I (487) octal_psram: Latency : 0x01 (Fixed) +I (492) octal_psram: VCC : 0x01 (3V) +I (497) octal_psram: SRF : 0x01 (Fast Refresh) +I (503) octal_psram: BurstType : 0x01 (Hybrid Wrap) +I (509) octal_psram: BurstLen : 0x01 (32 Byte) +I (515) octal_psram: Readlatency : 0x02 (10 cycles@Fixed) +I (521) octal_psram: DriveStrength: 0x00 (1/1) +I (531) MSPI Timing: PSRAM timing tuning index: 2 +I (532) esp_psram: Found 8MB PSRAM device +I (536) esp_psram: Speed: 120MHz +I (557) mmu_psram: Read only data copied and mapped to SPIRAM +I (641) mmu_psram: Instructions copied and mapped to SPIRAM +I (641) cpu_start: Multicore app +I (821) esp_psram: SPI SRAM memory test OK +I (830) cpu_start: Pro cpu start user code +I (830) cpu_start: cpu freq: 240000000 Hz +I (830) app_init: Application information: +I (833) app_init: Project name: volc_rtc +I (838) app_init: App version: v2.7-48-g01596882-dirty +I (844) app_init: Compile time: Jan 9 2025 22:09:38 +I (850) app_init: ELF file SHA256: 75b7595ca... +I (856) app_init: ESP-IDF: v5.5-dev-972-gfa41fafd27-dirty +I (863) efuse_init: Min chip rev: v0.0 +I (867) efuse_init: Max chip rev: v0.99 +I (872) efuse_init: Chip rev: v0.2 +I (877) heap_init: Initializing. RAM available for dynamic allocation: +I (884) heap_init: At 3FCAEF20 len 0003A7F0 (233 KiB): RAM +I (890) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM +I (897) heap_init: At 600FE01C len 00001FCC (7 KiB): RTCRAM +I (903) esp_psram: Adding pool of 6258K of PSRAM memory to heap allocator +I (911) spi_flash: detected chip: gd +I (914) spi_flash: flash io: qio +W (919) ADC: legacy driver is deprecated, please migrate to `esp_adc/adc_oneshot.h` +I (927) sleep_gpio: Configure to isolate all GPIO pins in sleep state +I (934) sleep_gpio: Enable automatic switching of GPIO sleep configuration +I (942) main_task: Started on CPU0 +I (954) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations +I (955) main_task: Calling app_main() +I (961) main: Initialize board peripherals +I (965) PERIPH_SPIFFS: Partition size: total: 52961, used: 0 +I (970) AUDIO_THREAD: The esp_periph task allocate stack on internal memory +W (978) i2c_bus_v2: I2C master handle is NULL, will create new one +I (989) DRV8311: ES8311 in Slave mode +I (999) gpio: GPIO[48]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (1006) ES7210: ES7210 in Slave mode +I (1015) ES7210: Enable ES7210_INPUT_MIC1 +I (1018) ES7210: Enable ES7210_INPUT_MIC2 +I (1020) ES7210: Enable ES7210_INPUT_MIC3 +W (1024) ES7210: Enable TDM mode. ES7210_SDP_INTERFACE2_REG12: 2 +I (1028) ES7210: config fmt 60 +I (1030) AUDIO_HAL: Codec mode is 3, Ctrl:1 +I (1040) pp: pp rom version: e7ae62f +I (1040) net80211: net80211 rom version: e7ae62f +I (1044) wifi:wifi driver task: 3fcbee58, prio:23, stack:6656, core=0 +I (1050) wifi:wifi firmware version: 34d97ea27 +I (1053) wifi:wifi certification version: v7.0 +I (1057) wifi:config NVS flash: enabled +I (1061) wifi:config nano formatting: disabled +I (1065) wifi:Init data frame dynamic rx buffer num: 32 +I (1070) wifi:Init static rx mgmt buffer num: 8 +I (1074) wifi:Init management short buffer num: 32 +I (1079) wifi:Init static tx buffer num: 16 +I (1083) wifi:Init tx cache buffer num: 32 +I (1086) wifi:Init static tx FG buffer num: 2 +I (1090) wifi:Init static rx buffer size: 1600 +I (1095) wifi:Init static rx buffer num: 16 +I (1098) wifi:Init dynamic rx buffer num: 32 +I (1103) wifi_init: rx ba win: 16 +I (1106) wifi_init: accept mbox: 6 +I (1110) wifi_init: tcpip mbox: 32 +I (1114) wifi_init: udp mbox: 32 +I (1118) wifi_init: tcp mbox: 6 +I (1122) wifi_init: tcp tx win: 65535 +I (1126) wifi_init: tcp rx win: 65535 +I (1131) wifi_init: tcp mss: 1440 +I (1135) wifi_init: WiFi/LWIP prefer SPIRAM +I (1140) wifi_init: WiFi IRAM OP enabled +I (1144) wifi_init: WiFi RX IRAM OP enabled +W (1149) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2 +I (1158) wifi:Set ps type: 1, coexist: 0 + +I (1162) phy_init: phy_version 680,a6008b2,Jun 4 2024,16:41:10 +I (1227) wifi:mode : sta (74:4d:bd:9d:b6:30) +I (1227) wifi:enable tsf +W (1227) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:43 +I (2520) wifi:new:<10,0>, old:<1,0>, ap:<255,255>, sta:<10,0>, prof:1, snd_ch_cfg:0x0 +I (2521) wifi:state: init -> auth (0xb0) +W (2521) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:43 +I (2525) wifi:state: auth -> assoc (0x0) +I (2541) wifi:state: assoc -> run (0x10) +I (2761) wifi:connected with ESP-Audio, aid = 3, channel 10, BW20, bssid = fc:2f:ef:ab:db:70 +I (2762) wifi:security: WPA2-PSK, phy: bgn, rssi: -39 +I (2763) wifi:pm start, type: 1 + +I (2766) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000 +W (2775) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:4 +I (2792) wifi:AP's beacon interval = 204800 us, DTIM period = 1 +I (2792) wifi:idx:0 (ifx:0, fc:2f:ef:ab:db:70), tid:0, ssn:3, winSize:64 +I (3796) esp_netif_handlers: sta ip: 192.168.1.100, mask: 255.255.255.0, gw: 192.168.1.1 +I (3796) PERIPH_WIFI: Got ip:192.168.1.100 +I (3798) AUDIO_THREAD: The monitor_task task allocate stack on external memory +I (3807) audio pipeline: Create audio pipeline for audio player +I (3813) audio pipeline: Create audio player audio stream +I (3820) audio pipeline: Register all elements to playback pipeline +I (3826) audio pipeline: Link playback element together raw-->audio_decoder-->i2s_stream-->[codec_chip] +E (3836) gpio: gpio_install_isr_service(502): GPIO isr service already installed +E (3844) DISPATCHER: exe first list: 0x0 +I (3849) DISPATCHER: dispatcher_event_task is running... +I (3951) wifi:idx:1 (ifx:0, fc:2f:ef:ab:db:70), tid:1, ssn:0, winSize:64 +I (4807) AUDIO_SYS: | Task | Run Time | Per | Prio | HWM | State | CoreId | Stack +I (4808) AUDIO_SYS: | monitor_task | 675 | 0% | 23 | 4316 | Running | 0 | Extr +I (4817) AUDIO_SYS: | main | 254004 |12% | 1 | 1324 | Ready | 0 | Intr +I (4828) AUDIO_SYS: | IDLE1 | 994708 |49% | 0 | 700 | Ready | 1 | Intr +I (4838) AUDIO_SYS: | IDLE0 | 726577 |36% | 0 | 692 | Ready | 0 | Intr +I (4848) AUDIO_SYS: | tiT | 8424 | 0% | 18 | 1792 | Blocked | 7fffffff | Intr +I (4859) AUDIO_SYS: | esp_periph | 1842 | 0% | 5 | 1592 | Blocked | 0 | Intr +I (4869) AUDIO_SYS: | ipc1 | 0 | 0% | 24 | 540 | Suspended | 1 | Intr +I (4879) AUDIO_SYS: | ipc0 | 0 | 0% | 1 | 436 | Suspended | 0 | Intr +I (4890) AUDIO_SYS: | wifi | 7636 | 0% | 23 | 3452 | Blocked | 0 | Intr +I (4900) AUDIO_SYS: | esp_timer | 309 | 0% | 22 | 3092 | Suspended | 0 | Intr +I (4911) AUDIO_SYS: | sys_evt | 46 | 0% | 20 | 416 | Blocked | 0 | Intr +I (4921) AUDIO_SYS: | Tmr Svc | 0 | 0% | 1 | 3364 | Blocked | 7fffffff | Intr +I (4931) AUDIO_SYS: | audio_player_st | Created +I (4937) AUDIO_SYS: | esp_dispatcher | Created + +I (4942) main: Func:monitor_task, Line:29, MEM Total:6377072 Bytes, Inter:135047 Bytes, Dram:135047 Bytes, Dram largest free:94208Bytes + +1970-01-01 00:00:04.213 [E] VolcEngineRTCLite.c:105 ****************** HELLO BOOKA (671221aa298a540183df32d9)(1.56.001.58)(6059fcf26792a8820bc81f13662979d531e5504d) ******************** +1970-01-01 00:00:04.227 [E] Cache.c:270 operation returned status code: 0x00000009 +1970-01-01 00:00:04.242 [E] ThreadPool.c:92 coreid 1 set 1 stack_size 8192 priority 5 +I (5075) audio pipeline: Create audio pipeline for recording +I (5078) audio pipeline: Create player audio stream +I (5085) audio pipeline: Register all player elements to audio pipeline +I (5091) audio pipeline: Link all player elements to audio pipeline +I (5099) audio pipeline: Create audio pipeline for playback +I (5105) audio pipeline: Create playback audio stream +W (5110) I2S_STREAM_IDF5.x: I2S(2) already startup +I (5116) audio pipeline: Create opus decoder +I (5121) audio pipeline: Register all elements to playback pipeline +I (5128) audio pipeline: Link playback element together raw-->audio_decoder-->i2s_stream-->[codec_chip] +1970-01-01 00:00:05.039 [E] RoomImplX.c:167 operation returned status code: 0x52000057 +1970-01-01 00:00:05.459 [E] Cache.c:309 operation returned status code: 0x00000009 +1970-01-01 00:00:05.465 [E] RoomImplX.c:167 operation returned status code: 0x52000057 +1970-01-01 00:00:05.467 [E] LiteHttp.c:641 ID 340052878 E_LOGIC : NO need keepAlive +1970-01-01 00:00:05.475 [E] RoomImplX.c:167 operation returned status code: 0x52000057 +1970-01-01 00:00:05.583 [E] RoomImplX.c:167 operation returned status code: 0x52000057 +1970-01-01 00:00:06.042 [E] rx_net_delay_manager.c:1130 +I (6880) volc_rtc: join channel success ************8105400500486185 elapsed 239 ms now 239 ms + +I (6880) volc_rtc: join room success + +I (6880) volc_rtc: remote user joined ***** + +1970-01-01 00:00:06.063 [E] EngineImplX.c:103 callback pEngineImplX->eventHandler.on_user_joined used too many times 12 +I (6895) MODEL_LOADER: The storage free size is 24576 KB +I (6911) MODEL_LOADER: The partition size is 4152 KB +I (6917) MODEL_LOADER: Successfully load srmodels +I (6922) RECORDER_SR: The first wakenet model: wn9_hilexin + +I (6929) AFE_SR: afe interface for speech recognition +I (6935) AFE_SR: AFE version: SR_V220727 +I (6939) AFE_SR: Initial auido front-end, total channel: 2, mic num: 1, ref num: 1 +I (6951) AFE_SR: aec_init: 1, se_init: 0, vad_init: 0(min speech:64, min noise:256) +I (6956) AFE_SR: wakenet_init: 0 +I (7017) AFE_SR: afe mode: 0, (Jan 2 2025 19:06:11) +W (7017) RECORDER_SR: Multinet is not enabled in SDKCONFIG +I (7018) AUDIO_RECORDER: RECORDER_CMD_TRIGGER_START +I (7018) main_task: Returned from app_main() +1970-01-01 00:00:06.212 [E] rx_net_audio_jitterbuffer.c:1266 +1970-01-01 00:00:06.219 [E] rx_net_audio_jitterbuffer.c:1190 +1970-01-01 00:00:06.219 [E] rx_net_delay_manager.c:1130 +1970-01-01 00:00:06.219 [E] rx_net_delay_manager.c:1130 +1970-01-01 00:00:06.224 [E] Counter.c:90 AudioRecevied fps 0 +1970-01-01 00:00:07.007 [E] Counter.c:90 AudioRecevied fps 39 +1970-01-01 00:00:08.012 [E] Counter.c:90 AudioRecevied fps 49 +1970-01-01 00:00:09.163 [E] Counter.c:90 AudioRecevied fps 44 +1970-01-01 00:00:10.014 [E] Counter.c:90 AudioRecevied fps 56 +1970-01-01 00:00:11.016 [E] Counter.c:90 AudioRecevied fps 50 +1970-01-01 00:00:12.021 [E] Counter.c:90 AudioRecevied fps 46 +1970-01-01 00:00:13.001 [E] Counter.c:90 AudioRecevied fps 53 +``` + +## 故障排除 + +#### 加入房间后没有任何声音 +- 检查 token 有效性:token 是连接服务的关键凭证,其有效性直接关系到语音交互功能能否正常使用。您可通过 [web 端](https://www.volcengine.com/docs/6348/77374) 验证 token 的有效性。 +- 确认智能体是否开启:智能体未开启会致使无法正常进行语音交互与声音输出。 +- 查看常见集成问题:更多详细的排查方法与解决方案,可查看 [常见的集成问题](https://bytedance.larkoffice.com/docx/TEMCdrJ3VouilPxSpjbc6CyUnAh) + + +#### 出现自问自答的现象 + - 不同硬件所采用的麦克风、喇叭型号各异,且麦克风与喇叭之间的距离也不尽相同。这些因素可能致使采集到的数据出现增益过大或过小的情况,最终使得回声消除(AEC)效果大打折扣 。 + - 需要确认 `AEC` 是否生效, 在 `menuconfig` 中使能 `ENABLE_RECORDER_DEBUG`,然后查看录音数据。需要注意两点 + - 确定回采参考信号是否出现饱和 + - 定麦克风录音是否出现饱和 + +## 技术支持 +请按照下面的链接获取技术支持: + +- 技术支持参见 [esp32.com](https://esp32.com/viewforum.php?f=20) 论坛 +- 故障和新功能需求,请创建 [GitHub issue](https://github.com/espressif/esp-adf/issues) + +我们会尽快回复。 diff --git a/esp-spot/example/adf/volc_rtc_spot/main/CMakeLists.txt b/esp-spot/example/adf/volc_rtc_spot/main/CMakeLists.txt new file mode 100644 index 0000000..98a755a --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/CMakeLists.txt @@ -0,0 +1,5 @@ +set(COMPONENT_SRCS "main.c" "volc_rtc.c" "coze_http_request.c" "volc_rtc_message.c" "http_client_request.c") +set(COMPONENT_ADD_INCLUDEDIRS .) +register_component() + +spiffs_create_partition_image(spiffs_data ../spiffs FLASH_IN_PROJECT) diff --git a/esp-spot/example/adf/volc_rtc_spot/main/Kconfig.projbuild b/esp-spot/example/adf/volc_rtc_spot/main/Kconfig.projbuild new file mode 100644 index 0000000..5384759 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/Kconfig.projbuild @@ -0,0 +1,100 @@ +menu "Example Configuration" + +config WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + +config WIFI_PASSWORD + string "WiFi Password" + default "mypasswod" + help + WiFi password (WPA or WPA2) for the example to use. + + Can be left blank if the network has no security set. + +choice AUDIO_DECODER_SUPPORT + prompt "Audio Decoder" + default AUDIO_SUPPORT_OPUS_DECODER + +config AUDIO_SUPPORT_G711A_DECODER + bool "AUDIUO-G711-DECODER" + +config AUDIO_SUPPORT_OPUS_DECODER + bool "AUDIO-OPUS-DECODER" + +config AUDIO_SUPPORT_AAC_DECODER + bool "AUDIO-AAC-DECODER" + +endchoice + +choice AUDIO_CONVERSATION_MODE_SUPPORT + prompt "Conversion Mode" + default CONTINUOUS_CONVERSATION_MODE + +config LANGUAGE_WAKEUP_MODE + select USE_WAKENET + select SR_WN_WN9_HILEXIN + bool "LANGUAGE-WAKEUP-MODE" + +config CONTINUOUS_CONVERSATION_MODE + bool "CONTINUOUS-CONVERSATION-MODE" + +endchoice + +choice RTC_MODE_CHOICE + prompt "rtc mode choice" + default COZE_REQUEST + help + Select rtc request mode + + config COZE_REQUEST + bool "enable coze request" + + config RTC_TOKEN_REQUEST + bool "enable rtc token request" +endchoice + + +config COZE_URL + string "COZE-URL" + default "COZE-URL" + depends on COZE_REQUEST + + config COZE_AUTHORIZATION + string "COZE-AUTHORIZATION" + default "COZE-AUTHORIZATION" + depends on COZE_REQUEST + + config COZE_BOTID + string "COZE-BOTID" + default "COZE-BOTID" + depends on COZE_REQUEST + + +config RTC_TOKEN + depends on RTC_TOKEN_REQUEST + config APPID + string "appid" + depends on RTC_TOKEN_REQUEST + + config ROOMID + string "roomid" + depends on RTC_TOKEN_REQUEST + + config UID + string "userid" + depends on RTC_TOKEN_REQUEST + + config TOKEN + string "token" + depends on RTC_TOKEN_REQUEST + +config ENABLE_RECORDER_DEBUG + bool "enable recorder debug save to sdcard" + default "n" + help + Save the data processed by AEC to the SD card. The saved file name is "rec.pcm", and by default, 30 seconds' worth of data will be saved. + +endmenu diff --git a/esp-spot/example/adf/volc_rtc_spot/main/coze_http_request.c b/esp-spot/example/adf/volc_rtc_spot/main/coze_http_request.c new file mode 100644 index 0000000..789fa0a --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/coze_http_request.c @@ -0,0 +1,253 @@ +/* Coze http request example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include + +#include "esp_log.h" +#include "esp_http_client.h" +#include "esp_heap_caps.h" +#include "cJSON.h" +#include "audio_error.h" +#include "http_client_request.h" +#include "coze_http_request.h" + + +#define HTTP_FINSH_BIT (1 << 0) + +#define HTTP_REQ_MEM_CHECK(ptr, action) do { \ + if (ptr == NULL) { \ + ESP_LOGE(TAG, "<%s | %d> malloc failed", __func__, __LINE__); \ + action; \ + } \ +} while (0) + +#define http_req_safe_free(ptr, free_fn) do { \ + if (ptr != NULL) { \ + free_fn(ptr); \ + ptr = NULL; \ + } \ +} while (0) + +static const char *TAG = "COZE_HTTP_REQUEST"; + +static void *impl_malloc_fn(size_t size) +{ + uint32_t allocate_caps = MALLOC_CAP_8BIT; +#if CONFIG_PSRAM + allocate_caps |= MALLOC_CAP_SPIRAM; +#else + allocate_caps |= MALLOC_CAP_INTERNAL; +#endif + return heap_caps_malloc(size, allocate_caps); +} + +static void *impl_strdup_fn(const char *s) +{ + int size = strlen(s) + 1; + char *ptr = impl_malloc_fn(size); + if (ptr != NULL) + { + memset(ptr, 0, size); + strcpy(ptr, s); + } + return ptr; +} + +static void impl_free_fn(void *ptr) +{ + heap_caps_free(ptr); +} + +const char *audio_type_2_str(coze_http_req_audio_type_e audio_type) +{ + switch (audio_type) { + case COZE_HTTP_REQ_AUDIO_TYPE_G711A: + return "G711A"; + case COZE_HTTP_REQ_AUDIO_TYPE_OPUS: + return "OPUS"; + default: + return "UNKNOWN"; + } +} + +const char *video_type_2_str(coze_http_req_video_type_e video_type) +{ + switch (video_type) + { + case COZE_HTTP_REQ_VIDEO_TYPE_MJPEG: + return "MJPEG"; + case COZE_HTTP_REQ_VIDEO_TYPE_H264: + return "H264"; + default: + return "UNKNOWN"; + } +} + +static coze_http_req_result_t *cjson_unpkg_2_str_fmt(const char *rsp_string) { + + coze_http_req_result_t *result = (coze_http_req_result_t *)impl_malloc_fn(sizeof(coze_http_req_result_t)); + cJSON *root = cJSON_Parse(rsp_string); + if (root == NULL) { + ESP_LOGW(TAG, "Error parsing JSON\n"); + return NULL; + } + + cJSON *data = cJSON_GetObjectItem(root, "data"); + if (data != NULL) { + cJSON *token = cJSON_GetObjectItem(data, "token"); + if (token != NULL && cJSON_IsString(token)) { + ESP_LOGD(TAG, "Token: %s\n", token->valuestring); + result->token = impl_strdup_fn(token->valuestring); + HTTP_REQ_MEM_CHECK( result->token, goto _exit); + } + + cJSON *uid = cJSON_GetObjectItem(data, "uid"); + if (uid != NULL && cJSON_IsString(uid)) { + ESP_LOGD(TAG, "UID: %s\n", uid->valuestring); + result->uid = impl_strdup_fn(uid->valuestring); + HTTP_REQ_MEM_CHECK( result->uid, goto _exit); + } + + cJSON *room_id = cJSON_GetObjectItem(data, "room_id"); + if (room_id != NULL && cJSON_IsString(room_id)) { + ESP_LOGD(TAG, "Room ID: %s\n", room_id->valuestring); + result->room_id = impl_strdup_fn(room_id->valuestring); + HTTP_REQ_MEM_CHECK( result->uid, goto _exit); + } + + cJSON *app_id = cJSON_GetObjectItem(data, "app_id"); + if (app_id != NULL && cJSON_IsString(app_id)) { + ESP_LOGD(TAG, "App ID: %s\n", app_id->valuestring); + result->app_id = impl_strdup_fn(app_id->valuestring); + HTTP_REQ_MEM_CHECK(result->app_id, goto _exit); + } + } + + cJSON *code = cJSON_GetObjectItem(root, "code"); + if (code != NULL && cJSON_IsNumber(code)) { + ESP_LOGD(TAG, "Code: %d\n", code->valueint); + } else { + ESP_LOGE(TAG, "Code not found in response"); + goto _exit; + } + + cJSON_Delete(root); + return result; + +_exit: + coze_http_request_free(result); + cJSON_Delete(root); + return NULL; +} + +static char *str_pkg_to_cjson_fmt(coze_http_req_config_t *cfg, req_svc_type_t type) +{ + cJSON *root = cJSON_CreateObject(); + if (root == NULL) { + ESP_LOGE(TAG, "cJSON_CreateObject failed"); + return NULL; + } + + cJSON_AddStringToObject(root, "bot_id", cfg->bot_id); + + if (type == COZE_HTTP_REQ_SVC_TYPE_COZE) { + + cJSON_AddStringToObject(root, "voice_id", cfg->voice_id); + cJSON *config = cJSON_CreateObject(); + if (config == NULL) { + ESP_LOGE(TAG, "cJSON_CreateObject failed"); + goto _exit; + } + cJSON_AddItemToObject(root, "config", config); + + cJSON *audio_config = cJSON_CreateObject(); + if (audio_config == NULL) { + ESP_LOGE(TAG, "cJSON_CreateObject failed"); + goto _exit; + } + cJSON_AddStringToObject(audio_config, "audio_codec", audio_type_2_str(cfg->audio_type)); + cJSON_AddItemToObject(config, "audio_config", audio_config); + + cJSON *video_config = cJSON_CreateObject(); + if (video_config == NULL) { + ESP_LOGE(TAG, "cJSON_CreateObject failed"); + goto _exit; + } + cJSON_AddStringToObject(video_config, "video_codec", video_type_2_str(cfg->video_type)); + cJSON_AddItemToObject(config, "video_config", video_config); + + } else { + cJSON_AddStringToObject(root, "audio_codec", audio_type_2_str(cfg->audio_type)); + } + char *json_string = cJSON_Print(root); + if (json_string == NULL) { + ESP_LOGE(TAG, "cJSON_Print failed"); + goto _exit; + } else { + ESP_LOGI(TAG, "request json: %s", json_string); + } + + cJSON_Delete(root); + return json_string; + +_exit: + cJSON_Delete(root); + return NULL; +} + +coze_http_req_result_t *coze_http_request(coze_http_req_config_t *config, req_svc_type_t type) +{ + if (config == NULL) { + ESP_LOGE(TAG, "Invalid parameters: config"); + return NULL; + } + char *req_body = NULL; + coze_http_req_result_t *req_result = NULL; + http_response_t http_response; + + req_body = str_pkg_to_cjson_fmt(config, type); + + http_req_header_t header[] = { + { "Content-Type", "application/json" }, + { "Authorization", config->authorization }, + { NULL, NULL }, + }; + + esp_err_t err = http_client_post(config->uri, header, req_body, &http_response); + if (err != ESP_OK) { + ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err)); + goto _clean_buffer; + } + + ESP_LOGD(TAG, "http_response.body: \n%s", http_response.body); + req_result = cjson_unpkg_2_str_fmt(http_response.body); + AUDIO_MEM_CHECK(TAG, req_result, goto _clean_buffer); + + ESP_LOGI(TAG, "room_id: %s", req_result->room_id); + ESP_LOGI(TAG, "app_id: %s", req_result->app_id); + ESP_LOGI(TAG, "token: %s", req_result->token); + ESP_LOGI(TAG, "uid: %s", req_result->uid); + +_clean_buffer: + // clear up + http_req_safe_free(http_response.body, free); + http_req_safe_free(req_body, free); + + return req_result; +} + +void coze_http_request_free(coze_http_req_result_t *result) +{ + http_req_safe_free(result->room_id, free); + http_req_safe_free(result->app_id, free); + http_req_safe_free(result->token, free); + http_req_safe_free(result->uid, free); + http_req_safe_free(result, free); +} \ No newline at end of file diff --git a/esp-spot/example/adf/volc_rtc_spot/main/coze_http_request.h b/esp-spot/example/adf/volc_rtc_spot/main/coze_http_request.h new file mode 100644 index 0000000..c8e2bf3 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/coze_http_request.h @@ -0,0 +1,72 @@ +/* Coze http request example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#pragma once + +typedef enum { + COZE_HTTP_REQ_SVC_TYPE_COZE, + COZE_HTTP_REQ_SVC_TYPE_RTC, +} req_svc_type_t; + +typedef enum { + COZE_HTTP_REQ_AUDIO_TYPE_G711A, + COZE_HTTP_REQ_AUDIO_TYPE_OPUS, +} coze_http_req_audio_type_e; + +typedef enum { + COZE_HTTP_REQ_VIDEO_TYPE_MJPEG, + COZE_HTTP_REQ_VIDEO_TYPE_H264, +} coze_http_req_video_type_e; + +#define COZE_HTTP_DEFAULT_CONFIG() { \ + .uri = "https://api.coze.cn/v1/audio/rooms", \ + .authorization = "", \ + .bot_id = "", \ + .voice_id = "", \ + .audio_type = COZE_HTTP_REQ_AUDIO_TYPE_G711A, \ + .video_type = COZE_HTTP_REQ_VIDEO_TYPE_H264, \ +} + +typedef struct { + char *room_id; + char *app_id; + char *token; + char *uid; +} coze_http_req_result_t; + +typedef struct { + const char *uri; + const char *authorization; + const char *bot_id; + const char *voice_id; + coze_http_req_audio_type_e audio_type; + coze_http_req_video_type_e video_type; +} coze_http_req_config_t; + +/** + * @brief Make a request to the ByteRTC service. + * + * @param[in] config Pointer to the configuration structure specifying the request parameters. + * The structure should be initialized before calling this function. + * + * @return + - A pointer to a `coze_http_req_result_t` structure containing the result of the request, + * - NULL if the request failed + */ +coze_http_req_result_t *coze_http_request(coze_http_req_config_t *config, req_svc_type_t type); + +/** + * @brief Free the result of a ByteRTC request. + * + * @param[in] result Pointer to the result structure to be freed. Passing NULL is safe and has no effect. + * + * @return + * - None + */ +void coze_http_request_free(coze_http_req_result_t *result); \ No newline at end of file diff --git a/esp-spot/example/adf/volc_rtc_spot/main/http_client_request.c b/esp-spot/example/adf/volc_rtc_spot/main/http_client_request.c new file mode 100644 index 0000000..f9c88c5 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/http_client_request.c @@ -0,0 +1,115 @@ +/* http client request example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_log.h" +#include "esp_http_client.h" +#include "audio_mem.h" +#include "http_client_request.h" + +static char *TAG = "HTTP_CLIENT_REQ"; + +#define http_client_free(x, fn) do { \ + if (x) { \ + fn(x); \ + x = NULL; \ + } \ +} while (0) + +#define HTTP_FINSH_BIT (1 << 0) + +typedef struct { + http_response_t *resp; + EventGroupHandle_t eg; +} http_client_ctx_t; + +static esp_err_t _http_event_handler(esp_http_client_event_t *evt) +{ + http_client_ctx_t *ctx = (http_client_ctx_t *)evt->user_data; + static int output_len = 0; + switch(evt->event_id) { + case HTTP_EVENT_ERROR: + ESP_LOGD(TAG, "HTTP_EVENT_ERROR"); + break; + case HTTP_EVENT_ON_CONNECTED: + ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED"); + break; + case HTTP_EVENT_HEADER_SENT: + ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT"); + break; + case HTTP_EVENT_ON_HEADER: + ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value); + break; + case HTTP_EVENT_ON_DATA: + memcpy(ctx->resp->body + output_len, evt->data , evt->data_len); + output_len += evt->data_len; + break; + case HTTP_EVENT_ON_FINISH: + ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH"); + output_len = 0; + xEventGroupSetBits(ctx->eg, HTTP_FINSH_BIT); + break; + case HTTP_EVENT_DISCONNECTED: + ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED"); + break; + case HTTP_EVENT_REDIRECT: + break; + } + return ESP_OK; +} + +esp_err_t http_client_post(const char *url, http_req_header_t *header, char *body, http_response_t *response) +{ + http_response_t rsp_data = { 0 }; + rsp_data.body = (char *)audio_calloc(1, 1024); + + http_client_ctx_t _ctx = { + .resp = &rsp_data, + .eg = xEventGroupCreate(), + }; + esp_http_client_config_t http_client_config = { + .url = url, + .query = "esp", + .event_handler = _http_event_handler, + .user_data = &_ctx, + .disable_auto_redirect = true, + }; + esp_http_client_handle_t client = NULL; + client = esp_http_client_init(&http_client_config); + esp_http_client_set_method(client, HTTP_METHOD_POST); + int header_index = 0; + while (header[header_index].key && header[header_index].value) { + ESP_LOGD(TAG, "key: %s, value: %s\n", header[header_index].key, header[header_index].value); + esp_http_client_set_header(client, header[header_index].key, header[header_index].value); + header_index++; + } + esp_http_client_set_post_field(client, body, strlen(body)); + + esp_err_t err = esp_http_client_perform(client); + + EventBits_t uxBits = xEventGroupWaitBits(_ctx.eg, HTTP_FINSH_BIT , pdTRUE, pdFALSE, pdMS_TO_TICKS(10000)); // wait 10s + if (( uxBits & HTTP_FINSH_BIT ) != 0) { + } else { + ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err)); + goto _exit; + } + *response = rsp_data; + esp_http_client_close(client); + esp_http_client_cleanup(client); + http_client_free(_ctx.eg, vEventGroupDelete); + return ESP_OK; +_exit: + http_client_free(client, esp_http_client_close); + http_client_free(client, esp_http_client_cleanup); + http_client_free(_ctx.eg, vEventGroupDelete); + http_client_free(rsp_data.body, audio_free); + return ESP_FAIL; +} \ No newline at end of file diff --git a/esp-spot/example/adf/volc_rtc_spot/main/http_client_request.h b/esp-spot/example/adf/volc_rtc_spot/main/http_client_request.h new file mode 100644 index 0000000..b20a6cb --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/http_client_request.h @@ -0,0 +1,22 @@ +/* http client request example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#pragma once + +typedef struct { + char *body; + int body_len; +} http_response_t; + +typedef struct { + const char *key; + const char *value; +} http_req_header_t; + +int http_client_post(const char *url, http_req_header_t *header , char *body, http_response_t *response); diff --git a/esp-spot/example/adf/volc_rtc_spot/main/main.c b/esp-spot/example/adf/volc_rtc_spot/main/main.c new file mode 100644 index 0000000..8f9cae7 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/main.c @@ -0,0 +1,127 @@ +/* volc rtc example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include + +#include "freertos/idf_additions.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_netif_sntp.h" +#include "esp_log.h" +#include "esp_timer.h" + +#include "audio_sys.h" +#include "audio_thread.h" +#include "esp_peripherals.h" +#include "periph_wifi.h" +#include "periph_spiffs.h" +#include "periph_sdcard.h" +#include "audio_mem.h" +#include "es8311.h" +#include "board.h" + +#include "volc_rtc.h" + +// #define ENABLE_TASK_MONITOR + +static char *TAG = "main"; + +#if defined(ENABLE_TASK_MONITOR) +static void monitor_task(void *arg) +{ + while (1) { + audio_sys_get_real_time_stats(); + AUDIO_MEM_SHOW(TAG); + vTaskDelay(10000 / portTICK_RATE_MS); + } +} +#endif + +// Pullup to fix the leakage issue of the ES8311 in standby mode. +static void set_gpio6_high(void) +{ + gpio_config_t io_conf = { + .pin_bit_mask = 1ULL << GPIO_NUM_6, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = 0, + .pull_down_en = 0, + .intr_type = GPIO_INTR_DISABLE + }; + gpio_config(&io_conf); + + gpio_set_level(GPIO_NUM_6, 1); +} + +void app_main() +{ + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_LOGI(TAG, "Initialize board peripherals"); + esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG(); + esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg); + +#if CONFIG_ENABLE_RECORDER_DEBUG + // Initialize SD Card peripheral + audio_board_sdcard_init(set, SD_MODE_1_LINE); +#endif + + periph_spiffs_cfg_t spiffs_cfg = { + .root = "/spiffs", + .partition_label = "spiffs_data", + .max_files = 5, + .format_if_mount_failed = true}; + esp_periph_handle_t spiffs_handle = periph_spiffs_init(&spiffs_cfg); + esp_periph_start(set, spiffs_handle); + + // Wait until spiffs is mounted + while (!periph_spiffs_is_mounted(spiffs_handle)) { + vTaskDelay(500 / portTICK_PERIOD_MS); + } + + periph_wifi_cfg_t wifi_cfg = { + .wifi_config.sta.ssid = CONFIG_WIFI_SSID, + .wifi_config.sta.password = CONFIG_WIFI_PASSWORD, + }; + esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg); + esp_periph_start(set, wifi_handle); + periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY); + + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org"); + esp_netif_sntp_init(&config); + + // Wait for time to be set + int retry = 0; + const int retry_count = 5; + while (esp_netif_sntp_sync_wait(1000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) { + ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count); + } + // Set timezone to China Standard Time + time_t now = 0; + struct tm timeinfo = { 0 }; + setenv("TZ", "CST-8", 1); + tzset(); + localtime_r(&now, &timeinfo); + + set_gpio6_high(); + audio_board_handle_t board_handle = audio_board_init(); + audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_BOTH, AUDIO_HAL_CTRL_START); + audio_hal_set_volume(board_handle->audio_hal, 80); + es8311_set_mic_gain(ES8311_MIC_GAIN_0DB); + +#if defined(ENABLE_TASK_MONITOR) + audio_thread_create(NULL, "monitor_task", monitor_task, NULL, 5 * 1024, 13, true, 0); +#endif + + // Init byte rtc engine + volc_rtc_init(); +} diff --git a/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc.c b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc.c new file mode 100644 index 0000000..acc243a --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc.c @@ -0,0 +1,440 @@ +/* volc rtc example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" + +#include "esp_log.h" +#include "sdkconfig.h" + +#include "esp_timer.h" +#include "audio_recorder.h" +#include "recorder_sr.h" +#include "audio_pipeline.h" +#include "audio_thread.h" +#include "raw_stream.h" +#include "audio_mem.h" +#include "esp_delegate.h" +#include "esp_dispatcher.h" + +#include "VolcEngineRTCLite.h" +#include "coze_http_request.h" +#include "audio_processor.h" +#include "volc_rtc_message.h" + +static const char* TAG = "volc_rtc"; + +#define STATS_TASK_PRIO (5) +#define JOIN_EVENT_BIT (1 << 0) +#define WAIT_DESTORY_EVENT_BIT (1 << 0) +#define WAKEUP_REC_READING (1 << 0) +#define DEFAULT_MAX_QUEUE_NUM (30) +#define DEFAULT_QUEUE_CACHE_NUM (10) + +#define volc_rtc_safe_free(x, fn) do { \ + if (x) { \ + fn(x); \ + x = NULL; \ + } \ +} while (0) + +typedef struct { + char *frame_ptr; + int frame_len; +} frame_package_t; + +typedef enum { + RTC_JOIN_IDLE = 0, + RTC_JOIN, + RTC_LEAVE, +} rtc_join_state_t; + +struct volc_rtc_t { + recorder_pipeline_handle_t record_pipeline; + player_pipeline_handle_t player_pipeline; + void *recorder_engine; + byte_rtc_engine_t engine; + EventGroupHandle_t join_event; + EventGroupHandle_t wait_destory_event; + EventGroupHandle_t wakeup_event; + QueueHandle_t frame_q; + coze_http_req_result_t *user_info; + esp_dispatcher_handle_t esp_dispatcher; + rtc_join_state_t join_state; + bool byte_rtc_running; + bool data_proc_running; + esp_timer_handle_t int_timer_hd; +#if defined(CONFIG_AUDIO_SUPPORT_OPUS_DECODER) +#define DEALULT_OPUS_DATA_CHACHE_SIZE (512) + char *opus_data_cache; + int opus_data_cache_len; +#endif // CONFIF_AUDIO_SUPPORT_OPUS_DECODER +}; + +static struct volc_rtc_t s_volc_rtc; + +static void audio_pull_data_process(char *ptr, int len); + +// byte rtc lite callbacks +static void byte_rtc_on_join_room_success(byte_rtc_engine_t engine, const char* channel, int elapsed_ms, bool ms) +{ + ESP_LOGI(TAG, "join channel success %s elapsed %d ms now %d ms\n", channel, elapsed_ms, elapsed_ms); + xEventGroupSetBits(s_volc_rtc.join_event, JOIN_EVENT_BIT); +}; + +static __attribute__((unused)) void byte_rtc_on_rejoin_room_success(byte_rtc_engine_t engine, const char* channel, int elapsed_ms) +{ + ESP_LOGI(TAG, "rejoin channel success %s\n", channel); +}; + +static void byte_rtc_on_user_joined(byte_rtc_engine_t engine, const char* channel, const char* user_name, int elapsed_ms) +{ + ESP_LOGI(TAG, "remote user joined %s:%s", channel, user_name); +}; + +static void byte_rtc_on_user_offline(byte_rtc_engine_t engine, const char* channel, const char* user_name, int reason) +{ + ESP_LOGI(TAG, "remote user offline %s:%s", channel, user_name); +}; + +static void byte_rtc_on_user_mute_audio(byte_rtc_engine_t engine, const char* channel, const char* user_name, int muted) +{ + ESP_LOGI(TAG, "remote user mute audio %s:%s %d", channel, user_name, muted); +}; + +static void byte_rtc_on_user_mute_video(byte_rtc_engine_t engine, const char* channel, const char* user_name, int muted) +{ + ESP_LOGI(TAG, "remote user mute video %s:%s %d", channel, user_name, muted); +}; + +static __attribute__((unused)) void byte_rtc_on_connection_lost(byte_rtc_engine_t engine, const char* channel) +{ + ESP_LOGI(TAG, "connection Lost %s", channel); +}; + +static void byte_rtc_on_room_error(byte_rtc_engine_t engine, const char* channel, int code, const char* msg) +{ + ESP_LOGE(TAG, "error occur %s %d %s", channel, code, msg ? msg : "UnKown"); +}; + +// remote audio +static void byte_rtc_on_audio_data(byte_rtc_engine_t engine, const char* room, const char* uid , uint16_t sent_ts, + audio_data_type_e codec, const void* data_ptr, size_t data_len) +{ + ESP_LOGD(TAG, "remote audio data %s %s %d %d %p %zu", room, uid, sent_ts, codec, data_ptr, data_len); + + pipe_player_state_e state; + player_pipeline_get_state(s_volc_rtc.player_pipeline, &state); + if (state == PIPE_STATE_IDLE) { + return; + } + frame_package_t frame = { 0 }; + frame.frame_ptr = audio_calloc(1, data_len); + memcpy(frame.frame_ptr, data_ptr, data_len); + frame.frame_len = data_len; + if (xQueueSend(s_volc_rtc.frame_q, &frame, pdMS_TO_TICKS(10)) != pdPASS) { + ESP_LOGW(TAG, "audio frame queue full"); + } +} + +// remote video +static void byte_rtc_on_video_data(byte_rtc_engine_t engine, const char* channel, const char* uid, uint16_t sent_ts, + video_data_type_e codec, int is_key_frame, + const void * data_ptr, size_t data_len){ } + +// remote message +void on_message_received(byte_rtc_engine_t engine, const char* room, const char* uid, const uint8_t* message, int size, bool binary) +{ + ESP_LOGD(TAG, "on_message_received uid: %s, message: %s, message size: %d", uid, message, size); + volc_rtc_message_process(message, size); +} + +static void on_key_frame_gen_req(byte_rtc_engine_t engine, const char* channel, const char* uid) {} +// byte rtc lite callbacks end. + +static void audio_pull_data_process(char *ptr, int len) +{ + char *data_ptr = ptr; + int data_len = len; + /* Since OPUS is in VBR mode, it needs to be packaged into a length + data format first then to decoder*/ +#if defined (CONFIG_AUDIO_SUPPORT_OPUS_DECODER) + +#define frame_length_prefix (2) + if (s_volc_rtc.opus_data_cache_len + frame_length_prefix < len) { + s_volc_rtc.opus_data_cache = audio_realloc(s_volc_rtc.opus_data_cache, len + frame_length_prefix); + s_volc_rtc.opus_data_cache_len = len; + } + s_volc_rtc.opus_data_cache[0] = (len >> 8) & 0xFF; + s_volc_rtc.opus_data_cache[1] = len & 0xFF; + memcpy(s_volc_rtc.opus_data_cache + frame_length_prefix, ptr, len); + data_ptr = s_volc_rtc.opus_data_cache; + data_len += frame_length_prefix; +#else + data_ptr = ptr; + data_len = len; +#endif // CONFIG_AUDIO_SUPPORT_OPUS_DECODER + raw_stream_write(player_pipeline_get_raw_write(s_volc_rtc.player_pipeline), data_ptr, data_len); +} + +static void audio_tone_player_event_cb(audio_element_status_t evt) +{ + if (evt == AEL_STATUS_STATE_FINISHED) { + player_pipeline_run(s_volc_rtc.player_pipeline); + } +} + +static esp_err_t dispatcher_audio_play(void *instance, action_arg_t *arg, action_result_t *result) +{ + audio_tone_play((char *)arg->data); + return ESP_OK; +}; + +static esp_err_t rec_engine_cb(audio_rec_evt_t *event, void *user_data) +{ + if (AUDIO_REC_WAKEUP_START == event->type) { +#if CONFIG_LANGUAGE_WAKEUP_MODE + ESP_LOGI(TAG, "rec_engine_cb - AUDIO_REC_WAKEUP_START"); + player_pipeline_stop(s_volc_rtc.player_pipeline); + action_arg_t action_arg = {0}; + action_arg.data = (void *)"spiffs://spiffs/dingding.wav"; + + action_result_t result = {0}; + esp_dispatcher_execute_with_func(s_volc_rtc.esp_dispatcher, dispatcher_audio_play, NULL, &action_arg, &result); + xEventGroupSetBits(s_volc_rtc.wakeup_event, WAKEUP_REC_READING); +#endif // CONFIG_LANGUAGE_WAKEUP_MODE + } else if (AUDIO_REC_VAD_START == event->type) { + } else if (AUDIO_REC_VAD_END == event->type) { + } else if (AUDIO_REC_WAKEUP_END == event->type) { + #if CONFIG_LANGUAGE_WAKEUP_MODE + xEventGroupClearBits(s_volc_rtc.wakeup_event, WAKEUP_REC_READING); + player_pipeline_stop(s_volc_rtc.player_pipeline); + ESP_LOGI(TAG, "rec_engine_cb - AUDIO_REC_WAKEUP_END"); + #endif // CONFIG_LANGUAGE_WAKEUP_MODE + } else { + } + + return ESP_OK; +} + +static esp_err_t open_audio_pipeline() +{ + s_volc_rtc.record_pipeline = recorder_pipeline_open(); + s_volc_rtc.player_pipeline = player_pipeline_open(); + recorder_pipeline_run(s_volc_rtc.record_pipeline); + player_pipeline_run(s_volc_rtc.player_pipeline); + return ESP_OK; +} + +static void byte_rtc_engine_destroy() +{ + if (s_volc_rtc.engine) { + byte_rtc_fini(s_volc_rtc.engine); + vTaskDelay(pdMS_TO_TICKS(1000)); + byte_rtc_destory(s_volc_rtc.engine); + s_volc_rtc.engine = NULL; + } +} + +static esp_err_t byte_rtc_engine_create() +{ +#ifdef CONFIG_COZE_REQUEST + #ifdef CONFIG_AUDIO_SUPPORT_OPUS_DECODER + coze_http_req_audio_type_e audio_type = COZE_HTTP_REQ_AUDIO_TYPE_OPUS; + #else + coze_http_req_audio_type_e audio_type = COZE_HTTP_REQ_AUDIO_TYPE_G711A; + #endif + coze_http_req_config_t http_config = COZE_HTTP_DEFAULT_CONFIG(); + http_config.uri = CONFIG_COZE_URL"/startvoicechat"; + http_config.authorization = CONFIG_COZE_AUTHORIZATION; + http_config.bot_id = CONFIG_COZE_BOTID; + http_config.audio_type = audio_type, + s_volc_rtc.user_info = coze_http_request(&http_config, COZE_HTTP_REQ_SVC_TYPE_RTC); + +#else + s_volc_rtc.user_info = audio_calloc(1, sizeof(coze_http_req_result_t)); + s_volc_rtc.user_info->app_id = CONFIG_APPID; + s_volc_rtc.user_info->room_id = CONFIG_ROOMID; + s_volc_rtc.user_info->uid = CONFIG_UID; + s_volc_rtc.user_info->token = CONFIG_TOKEN; +#endif + + ESP_LOGI(TAG, "app_id: %s, room_id: %s, uid: %s, token: %s", + s_volc_rtc.user_info->app_id, s_volc_rtc.user_info->room_id, s_volc_rtc.user_info->uid, s_volc_rtc.user_info->token); +#if defined (CONFIG_AUDIO_SUPPORT_OPUS_DECODER) + s_volc_rtc.opus_data_cache_len = DEALULT_OPUS_DATA_CHACHE_SIZE; + s_volc_rtc.opus_data_cache = audio_calloc(1, s_volc_rtc.opus_data_cache_len); +#endif // CONFIG_AUDIO_SUPPORT_OPUS_DECODER + + byte_rtc_event_handler_t handler = { + .on_join_room_success = byte_rtc_on_join_room_success, + .on_room_error = byte_rtc_on_room_error, + .on_user_joined = byte_rtc_on_user_joined, + .on_user_offline = byte_rtc_on_user_offline, + .on_user_mute_audio = byte_rtc_on_user_mute_audio, + .on_user_mute_video = byte_rtc_on_user_mute_video, + .on_audio_data = byte_rtc_on_audio_data, + .on_video_data = byte_rtc_on_video_data, + .on_key_frame_gen_req = on_key_frame_gen_req, + .on_message_received = on_message_received, + }; + s_volc_rtc.join_state = RTC_JOIN_IDLE; + s_volc_rtc.engine = byte_rtc_create(s_volc_rtc.user_info->app_id, &handler); + + byte_rtc_set_log_level(s_volc_rtc.engine, BYTE_RTC_LOG_LEVEL_ERROR); +#ifdef RTC_TEST_ENV + byte_rtc_set_params(s_volc_rtc.engine, "{\"env\":2}"); // test env +#endif + byte_rtc_set_params(s_volc_rtc.engine, "{\"debug\":{\"log_to_console\":1}}"); + byte_rtc_set_params(s_volc_rtc.engine,"{\"rtc\":{\"thread\":{\"pinned_to_core\":1}}}"); + byte_rtc_set_params(s_volc_rtc.engine,"{\"rtc\":{\"thread\":{\"priority\":6}}}"); + // byte_rtc_set_params(s_volc_rtc.engine,"{\"rtc\":{\"network\":{\"audio_jitter_buffer_target_delay_min_ms\":200}}}"); + // byte_rtc_set_params(s_volc_rtc.engine,"{\"rtc\":{\"license\":{\"enable\":1}}}"); + + byte_rtc_init(s_volc_rtc.engine); +#if defined (CONFIG_AUDIO_SUPPORT_OPUS_DECODER) + byte_rtc_set_audio_codec(s_volc_rtc.engine, AUDIO_CODEC_TYPE_OPUS); +#elif defined (CONFIG_AUDIO_SUPPORT_G711A_DECODER) + byte_rtc_set_audio_codec(s_volc_rtc.engine, AUDIO_CODEC_TYPE_G711A); +#else // AACLC Encoder + byte_rtc_set_audio_codec(s_volc_rtc.engine, AUDIO_CODEC_TYPE_AACLC); +#endif // CONFIG_AUDIO_SUPPORT_OPUS_DECODER + open_audio_pipeline(); + + ESP_LOGI(TAG, "start join room\n"); + byte_rtc_room_options_t options = { 0 }; + options.auto_subscribe_audio = 1; + options.auto_subscribe_video = 0; + options.auto_publish_audio = 1; + options.auto_publish_video = 0; + byte_rtc_join_room(s_volc_rtc.engine, s_volc_rtc.user_info->room_id, s_volc_rtc.user_info->uid, s_volc_rtc.user_info->token, &options); + // Default wait time is forever + xEventGroupWaitBits(s_volc_rtc.join_event, JOIN_EVENT_BIT, pdTRUE, pdTRUE, portMAX_DELAY); + ESP_LOGI(TAG, "join room success\n"); + s_volc_rtc.join_state = RTC_JOIN; + return ESP_OK; +} + +static void audio_data_process_task(void *args) +{ + frame_package_t frame = { 0 }; + s_volc_rtc.data_proc_running = true; + while (s_volc_rtc.data_proc_running) { + xQueueReceive(s_volc_rtc.frame_q, &frame, portMAX_DELAY); + if (frame.frame_ptr) { + audio_pull_data_process(frame.frame_ptr, frame.frame_len); + audio_free(frame.frame_ptr); + } + } + vTaskDelete(NULL); +} + +static void voice_read_task(void *args) +{ + const int voice_data_read_sz = recorder_pipeline_get_default_read_size(s_volc_rtc.record_pipeline); + uint8_t *voice_data = audio_calloc(1, voice_data_read_sz); + bool runing = true; + +#if defined (CONFIG_AUDIO_SUPPORT_OPUS_DECODER) + audio_frame_info_t audio_frame_info = {.data_type = AUDIO_DATA_TYPE_OPUS}; +#elif defined (CONFIG_AUDIO_SUPPORT_G711A_DECODER) + audio_frame_info_t audio_frame_info = {.data_type = AUDIO_DATA_TYPE_PCMA}; +#else + audio_frame_info_t audio_frame_info = {.data_type = AUDIO_DATA_TYPE_AACLC}; +#endif // CONFIG_AUDIO_SUPPORT_OPUS_DECODER + +#if defined (CONFIG_LANGUAGE_WAKEUP_MODE) + TickType_t wait_tm = portMAX_DELAY; +#else + TickType_t wait_tm = 0; +#endif // CONFIG_LANGUAGE_WAKEUP_MODE + while (runing) { + #if defined (CONFIG_LANGUAGE_WAKEUP_MODE) + EventBits_t bits = xEventGroupWaitBits(s_volc_rtc.wakeup_event, WAKEUP_REC_READING , false, true, wait_tm); + if (bits & WAKEUP_REC_READING) { + int ret = audio_recorder_data_read(s_volc_rtc.recorder_engine, voice_data, voice_data_read_sz, portMAX_DELAY); + if (ret == 0 || ret == -1) { + if (ret == 0) { + vTaskDelay(15 / portTICK_PERIOD_MS); + continue; + } + ESP_LOGE(TAG, "audio_recorder_data_read failed, ret: %d\n", ret); + xEventGroupClearBits(s_volc_rtc.wakeup_event, WAKEUP_REC_READING); + } else { + byte_rtc_send_audio_data(s_volc_rtc.engine, s_volc_rtc.user_info->room_id, voice_data, voice_data_read_sz, &audio_frame_info); + } + } + #else + int read_len = audio_recorder_data_read(s_volc_rtc.recorder_engine, voice_data, voice_data_read_sz, portMAX_DELAY); + if (read_len == voice_data_read_sz) { + byte_rtc_send_audio_data(s_volc_rtc.engine, s_volc_rtc.user_info->room_id, voice_data, voice_data_read_sz, &audio_frame_info); + } + #endif + } + xEventGroupClearBits(s_volc_rtc.wakeup_event, WAKEUP_REC_READING); + audio_free(voice_data); + vTaskDelete(NULL); +} + +static void log_clear(void) +{ + esp_log_level_set("*", ESP_LOG_INFO); + esp_log_level_set("AUDIO_THREAD", ESP_LOG_ERROR); + esp_log_level_set("i2c_bus_v2", ESP_LOG_ERROR); + esp_log_level_set("AUDIO_HAL", ESP_LOG_ERROR); + esp_log_level_set("AUDIO_PIPELINE", ESP_LOG_ERROR); + esp_log_level_set("AUDIO_ELEMENT", ESP_LOG_ERROR); + esp_log_level_set("I2S_STREAM_IDF5.x", ESP_LOG_ERROR); + esp_log_level_set("RSP_FILTER", ESP_LOG_ERROR); + esp_log_level_set("AUDIO_EVT", ESP_LOG_ERROR); +} + +esp_err_t volc_rtc_init(void) +{ + log_clear(); + audio_tone_init(audio_tone_player_event_cb); + +#if CONFIG_LANGUAGE_WAKEUP_MODE + esp_timer_create_args_t timer_cfg = { + .callback = rtc_int_timer_expired, + .arg = NULL, + .dispatch_method = ESP_TIMER_TASK, + .name = "int_timer", + }; + esp_timer_create(&timer_cfg, &s_volc_rtc.int_timer_hd); +#endif + + s_volc_rtc.join_event = xEventGroupCreate(); + s_volc_rtc.wait_destory_event = xEventGroupCreate(); + s_volc_rtc.wakeup_event = xEventGroupCreate(); + s_volc_rtc.frame_q = xQueueCreate(30, sizeof(frame_package_t)); + s_volc_rtc.esp_dispatcher = esp_dispatcher_get_delegate_handle(); + + // volc_rtc_message_init(VOLC_RTC_MESSAGE_TYPE_SUBTITLE | VOLC_RTC_MESSAGE_TYPE_FUNCTION_CALL); + byte_rtc_engine_create(); + s_volc_rtc.recorder_engine = audio_record_engine_init(s_volc_rtc.record_pipeline, rec_engine_cb); + vTaskDelay(pdMS_TO_TICKS(200)); + audio_thread_create(NULL, "voice_read_task", voice_read_task, (void *)NULL, 5 * 1024, 5, true, 0); + audio_thread_create(NULL, "audio_data_process_task", audio_data_process_task, (void *)NULL, 5 * 1024, 10, true, 0); + return ESP_OK; +} + +esp_err_t volc_rtc_deinit(void) +{ + s_volc_rtc.byte_rtc_running = false; + s_volc_rtc.data_proc_running = false; + xEventGroupWaitBits(s_volc_rtc.wait_destory_event, WAIT_DESTORY_EVENT_BIT, pdTRUE, pdTRUE, portMAX_DELAY); + byte_rtc_engine_destroy(); + volc_rtc_safe_free(s_volc_rtc.join_event, vEventGroupDelete); + volc_rtc_safe_free(s_volc_rtc.wait_destory_event, vEventGroupDelete); + volc_rtc_safe_free(s_volc_rtc.int_timer_hd, esp_timer_delete); + return ESP_OK; +} diff --git a/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc.h b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc.h new file mode 100644 index 0000000..82ae548 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc.h @@ -0,0 +1,37 @@ +/* volc rtc example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#pragma once + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize the Volc RTC module. + * + * @return + * - ESP_OK: Initialization was successful. + * - Other: Appropriate erro code indicating the failure reason. + */ +esp_err_t volc_rtc_init(void); + +/** + * @brief Deinitialize the Volc RTC module.. + * + * @return + * - ESP_OK: Deinitialization was successful. + */ +esp_err_t volc_rtc_deinit(void); + +#ifdef __cplusplus +} +#endif diff --git a/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc_message.c b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc_message.c new file mode 100644 index 0000000..1c67135 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc_message.c @@ -0,0 +1,188 @@ +/* volc rtc message parse example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include "string.h" +#include "esp_log.h" +#include "esp_err.h" +#include "audio_mem.h" +#include "volc_rtc_message.h" + +static const char* TAG = "volc_rtc_message"; + +#define DEF_MESSAGE_SIZE (512) + +struct volc_rtc_message_s { + uint8_t *message; + int size; + uint32_t msg_type; +}; + +static struct volc_rtc_message_s s_rtc_message = {0}; + +static void on_subtitle_message_received(const cJSON* root); +static int on_function_calling_message_received(const cJSON* root, const char* json_str); + +// remote message +// ref https://www.volcengine.com/docs/6348/1337284 +static void on_subtitle_message_received(const cJSON* root) +{ + // Json format: + // { + // "data" : + // [ + // { + // "definite" : false, + // "language" : "zh", + // "mode" : 1, + // "paragraph" : false, + // "sequence" : 0, + // "text" : "\\u4f60\\u597d", + // "userId" : "voiceChat_xxxxx" + // } + // ], + // "type" : "subtitle" + // } + cJSON * type_obj = cJSON_GetObjectItem(root, "type"); + if (type_obj != NULL && strcmp("subtitle", cJSON_GetStringValue(type_obj)) == 0) { + cJSON* data_obj_arr = cJSON_GetObjectItem(root, "data"); + cJSON* obji = NULL; + cJSON_ArrayForEach(obji, data_obj_arr) { + cJSON* user_id_obj = cJSON_GetObjectItem(obji, "userId"); + cJSON* text_obj = cJSON_GetObjectItem(obji, "text"); + if (user_id_obj && text_obj) { + ESP_LOGI(TAG, "subtitle:%s:%s", cJSON_GetStringValue(user_id_obj), cJSON_GetStringValue(text_obj)); + } + } + } +} + +// // function calling +// // ref https://www.volcengine.com/docs/6348/1359441 +static int on_function_calling_message_received(const cJSON* root, const char* json_str) +{ + +// Json format: +// { +// "subscriber_user_id": "", +// "tool_calls": [{ +// "function": { +// "arguments": "{\"命令\": \"打开\", \"亮度\": 50, \"颜色\": \"蓝色\"}", +// "name": "led_on_off" +// }, +// "id": "call_3t9gonmmtx8r2s89xlrxltmb", +// "type": "function" +// }] +// } + + // cJSON *tool_calls = cJSON_GetObjectItem(root, "tool_calls"); + // if (!cJSON_IsArray(tool_calls)) { + // ESP_LOGE(TAG , "`tool_calls` is not array") + // return 1; + // } + + // cJSON *tool_call_item = cJSON_GetArrayItem(tool_calls, 0); + // if (!tool_call_item) { + // ESP_LOGE(TAG, "Get tool_call_item falied") + // return ; + // } + + // cJSON *function_obj = cJSON_GetObjectItem(tool_call_item, "function"); + // if (!function_obj) { + // ESP_LOGE(TAG, "Get function_obj falied"); + // return ; + // } + + // cJSON *arguments = cJSON_GetObjectItem(function_obj, "arguments"); + // if (!cJSON_IsString(arguments)) { + // ESP_LOGE(TAG, "Get arguments falied"); + // return 1; + // } + + // cJSON *arguments_json = cJSON_Parse(arguments->valuestring); + // if (!arguments_json) { + // ESP_LOGE(TAG, "Parse arguments_json falied"); + // return 1; + // } + + // cJSON *cmd = cJSON_GetObjectItem(arguments_json, "命令"); + // cJSON *brightness = cJSON_GetObjectItem(arguments_json, "亮度"); + // cJSON *color = cJSON_GetObjectItem(arguments_json, "颜色"); + // if (cmd) { + // ESP_LOGI(TAG, "命令: %s", cmd->valuestring); + // } + // if (brightness) { + // ESP_LOGI(TAG, "亮度: %d", brightness->valueint); + // } + + // if (color) { + // ESP_LOGI(TAG, "颜色: %s", color->valuestring); + // } + // cJSON_Delete(arguments_json); + return 0; +} + +esp_err_t volc_rtc_message_init(uint32_t msg_type) +{ + if (msg_type == VOLC_RTC_MESSAGE_TYPE_NONE) { + return ESP_OK; + } + s_rtc_message.message = (uint8_t*)audio_malloc(DEF_MESSAGE_SIZE); + s_rtc_message.size = DEF_MESSAGE_SIZE; + if (s_rtc_message.message == NULL) { + ESP_LOGE(TAG, "Allocate memory failed"); + return ESP_ERR_NO_MEM; + } + s_rtc_message.msg_type = msg_type; + return ESP_OK; +} + +esp_err_t volc_rtc_message_process(const uint8_t* message, int size) +{ + if (s_rtc_message.msg_type == VOLC_RTC_MESSAGE_TYPE_NONE) { + return ESP_OK; + } + if (message == NULL || size <= 0) { + ESP_LOGE(TAG, "Invalid message"); + return ESP_ERR_INVALID_ARG; + } + esp_err_t ret = ESP_OK; + if (size > 8) { + if (size > s_rtc_message.size) { + ESP_LOGW(TAG, "Message size too large(size: %d)", size); + s_rtc_message.message = (uint8_t*)audio_realloc(s_rtc_message.message, size); + if (s_rtc_message.message == NULL) { + ESP_LOGE(TAG, "Realloc message buffer failed"); + return ESP_ERR_NO_MEM; + } + s_rtc_message.size = size; + } + memset(s_rtc_message.message, 0, size); + memcpy(s_rtc_message.message, message, size); + + cJSON *root = cJSON_Parse((char *)s_rtc_message.message + 8); + if (root) { + if (message[0] == 's' && message[1] == 'u' && message[2] == 'b' && message[3] == 'v') { + if (s_rtc_message.msg_type & VOLC_RTC_MESSAGE_TYPE_SUBTITLE) { + on_subtitle_message_received(root); + } + } else if (message[0] == 't' && message[1] == 'o' && message[2] == 'o' && message[3] == 'l') { + if (s_rtc_message.msg_type & VOLC_RTC_MESSAGE_TYPE_FUNCTION_CALL) { + on_function_calling_message_received(root, (char *)s_rtc_message.message + 8); + } + } else { + ESP_LOGE(TAG, "Unknown json message: %s", (char *)s_rtc_message.message + 8); + } + } else { + ESP_LOGE(TAG, "Parse json message failed"); + ret = ESP_FAIL; + } + cJSON_Delete(root); + } + return ret; +} diff --git a/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc_message.h b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc_message.h new file mode 100644 index 0000000..3c9ae39 --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/main/volc_rtc_message.h @@ -0,0 +1,22 @@ +/* volc rtc message parse example code + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#pragma once + +#include "cJSON.h" + +typedef enum { + VOLC_RTC_MESSAGE_TYPE_SUBTITLE = 1, + VOLC_RTC_MESSAGE_TYPE_FUNCTION_CALL = 2, + VOLC_RTC_MESSAGE_TYPE_NONE = 0, +} volc_rtc_message_type_t; + +esp_err_t volc_rtc_message_init(uint32_t msg_type); +esp_err_t volc_rtc_message_process(const uint8_t* message, int size); + diff --git a/esp-spot/example/adf/volc_rtc_spot/partitions.csv b/esp-spot/example/adf/volc_rtc_spot/partitions.csv new file mode 100644 index 0000000..c3c8c2f --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/partitions.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x4000 +phy_init, data, phy, 0xd000, 0x1000 +factory, app, factory, 0x10000, 3M, +model, data, spiffs, , 4152K, +spiffs_data, data, spiffs, , 64k, diff --git a/esp-spot/example/adf/volc_rtc_spot/sdkconfig.defaults b/esp-spot/example/adf/volc_rtc_spot/sdkconfig.defaults new file mode 100644 index 0000000..c4fc99c --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/sdkconfig.defaults @@ -0,0 +1,37 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.3.1 Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32s3" +CONFIG_BOOTLOADER_FLASH_DC_AWARE=y +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_120M=y +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_ESP32_S3_SPOT_BOARD=y +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_ESP_TLS_INSECURE=y +CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_XIP_FROM_PSRAM=y +CONFIG_SPIRAM_SPEED_120M=y +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y +CONFIG_ESP32S3_DATA_CACHE_64KB=y +CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096 +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_TASK_WDT_TIMEOUT_S=10 +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY=y +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y +CONFIG_MBEDTLS_HARDWARE_AES=n +CONFIG_MBEDTLS_HARDWARE_SHA=n +CONFIG_SPI_FLASH_HPM_ENA=y +CONFIG_IDF_EXPERIMENTAL_FEATURES=y diff --git a/esp-spot/example/adf/volc_rtc_spot/sdkconfig.defaults.esp32s3 b/esp-spot/example/adf/volc_rtc_spot/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000..a5832fa --- /dev/null +++ b/esp-spot/example/adf/volc_rtc_spot/sdkconfig.defaults.esp32s3 @@ -0,0 +1,115 @@ +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_IDF_TARGET_ESP32S3=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009 + +# +# Make experimental features visible +# +CONFIG_IDF_EXPERIMENTAL_FEATURES=y + +# +# Serial flasher config +# +CONFIG_BOOTLOADER_FLASH_DC_AWARE=y +CONFIG_ESPTOOLPY_FLASHFREQ_120M=y +CONFIG_SPI_FLASH_HPM_ENA=y +# end of Serial flasher config + +# +# Audio HAL +# +CONFIG_ESP32_S3_KORVO2_V3_BOARD=y +# end of Audio HAL + +# +# Audio Recorder +# +CONFIG_AFE_MIC_NUM=2 +# end of Audio Recorder + +# +# ESP Speech Recognition +# +CONFIG_MODEL_IN_FLASH=y +CONFIG_USE_AFE=y +CONFIG_AFE_INTERFACE_V1=y +CONFIG_USE_WAKENET=n +CONFIG_SR_WN_WN9_HILEXIN=n +CONFIG_USE_MULTINET=n +# end of ESP Speech Recognition + +# +# Component config +# + +# +# Driver configurations +# + +# +# mbedTLS +# +# CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_HARDWARE_AES is not set +# CONFIG_MBEDTLS_HARDWARE_SHA is not set +# end of mbedTLS + + +# +# ESP32s3-PSRAM +# +CONFIG_SPIRAM_XIP_FROM_PSRAM=y + +# +# ESP32S3-Specific +# +# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set +# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240 + +# +# Cache config +# +# CONFIG_ESP32S3_INSTRUCTION_CACHE_16KB is not set +CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y +CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE=0x8000 +# CONFIG_ESP32S3_INSTRUCTION_CACHE_4WAYS is not set +CONFIG_ESP32S3_INSTRUCTION_CACHE_8WAYS=y +CONFIG_ESP32S3_ICACHE_ASSOCIATED_WAYS=8 +CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_32B=y +CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_SIZE=32 +# CONFIG_ESP32S3_INSTRUCTION_CACHE_WRAP is not set +# CONFIG_ESP32S3_DATA_CACHE_16KB is not set +# CONFIG_ESP32S3_DATA_CACHE_32KB is not set +CONFIG_ESP32S3_DATA_CACHE_64KB=y +CONFIG_ESP32S3_DATA_CACHE_SIZE=0x10000 +# CONFIG_ESP32S3_DATA_CACHE_4WAYS is not set +CONFIG_ESP32S3_DATA_CACHE_8WAYS=y +CONFIG_ESP32S3_DCACHE_ASSOCIATED_WAYS=8 +# CONFIG_ESP32S3_DATA_CACHE_LINE_32B is not set +CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y +CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE=64 +# CONFIG_ESP32S3_DATA_CACHE_WRAP is not set +# end of Cache config + +CONFIG_ESP32S3_SPIRAM_SUPPORT=y + +# +# SPI RAM config +# +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TYPE_AUTO=y +CONFIG_SPIRAM_SPEED_120M=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_MEMTEST=y +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 +# end of SPI RAM config +# end of ESP32S3-Specific diff --git a/esp-spot/example/adf/volc_rtc_spot/spiffs/dingding.wav b/esp-spot/example/adf/volc_rtc_spot/spiffs/dingding.wav new file mode 100644 index 0000000..a6e1f41 Binary files /dev/null and b/esp-spot/example/adf/volc_rtc_spot/spiffs/dingding.wav differ diff --git a/esp-spot/example/imu_led/CMakeLists.txt b/esp-spot/example/imu_led/CMakeLists.txt new file mode 100644 index 0000000..0a3f2b4 --- /dev/null +++ b/esp-spot/example/imu_led/CMakeLists.txt @@ -0,0 +1,9 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(IMU_Wrist) \ No newline at end of file diff --git a/esp-spot/example/imu_led/README.md b/esp-spot/example/imu_led/README.md new file mode 100644 index 0000000..e69de29 diff --git a/esp-spot/example/imu_led/main/CMakeLists.txt b/esp-spot/example/imu_led/main/CMakeLists.txt new file mode 100644 index 0000000..46b298e --- /dev/null +++ b/esp-spot/example/imu_led/main/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS ".") + +include(package_manager) +cu_pkg_define_version(${CMAKE_CURRENT_LIST_DIR}) \ No newline at end of file diff --git a/esp-spot/example/imu_led/main/Kconfig.projbuild b/esp-spot/example/imu_led/main/Kconfig.projbuild new file mode 100644 index 0000000..5e8d0cf --- /dev/null +++ b/esp-spot/example/imu_led/main/Kconfig.projbuild @@ -0,0 +1,15 @@ +menu "Example Configuration" + + config I2C_MASTER_SCL + int "SCL GPIO Num" + default 1 + help + GPIO number for I2C Master clock line. + + config I2C_MASTER_SDA + int "SDA GPIO Num" + default 2 + help + GPIO number for I2C Master data line. + +endmenu \ No newline at end of file diff --git a/esp-spot/example/imu_led/main/idf_component.yml b/esp-spot/example/imu_led/main/idf_component.yml new file mode 100644 index 0000000..c96297b --- /dev/null +++ b/esp-spot/example/imu_led/main/idf_component.yml @@ -0,0 +1,6 @@ +## IDF Component Manager Manifest File +dependencies: + idf: '>=5.0' + + espressif2022/bmi270: ^1.1.0 + espressif/led_strip: "^3.0.0" diff --git a/esp-spot/example/imu_led/main/main.c b/esp-spot/example/imu_led/main/main.c new file mode 100644 index 0000000..659890f --- /dev/null +++ b/esp-spot/example/imu_led/main/main.c @@ -0,0 +1,176 @@ +#include +#include "esp_system.h" +#include "esp_log.h" +#include "driver/rmt_tx.h" +#include "led_strip.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "bmi270.h" +#include "common/common.h" + +#define WS2812_GPIO 27 +#define LED_COUNT 1 + + +#define HW_ESP_SPOT_C5 0 +#define HW_ESP_SPOT_S3 1 +#define HW_ESP_ASTOM_S3 0 + +#if HW_ESP_SPOT_C5 +#define I2C_INT_IO 3 +#define I2C_MASTER_SCL_IO 26 +#define I2C_MASTER_SDA_IO 25 +#elif HW_ESP_SPOT_S3 +#define I2C_INT_IO 5 +#define I2C_MASTER_SCL_IO 1 +#define I2C_MASTER_SDA_IO 2 +#elif HW_ESP_ASTOM_S3 +#define I2C_INT_IO 16 +#define I2C_MASTER_SCL_IO 0 +#define I2C_MASTER_SDA_IO 45 +#endif + +#define I2C_MASTER_FREQ_HZ (100 * 1000) + +static bmi270_handle_t bmi_handle = NULL; +static i2c_bus_handle_t i2c_bus = NULL; +static led_strip_handle_t led_strip; + +static const char *TAG = "gesture_led"; + +static led_strip_handle_t configure_led(void) +{ + led_strip_config_t strip_config = { + .strip_gpio_num = WS2812_GPIO, + .max_leds = 1, + .led_model = LED_MODEL_WS2812, + .color_component_format = { + .format = { + .r_pos = 1, // GRB排列 + .g_pos = 0, + .b_pos = 2, + .num_components = 3, + }, + }, + .flags = {.invert_out = false}, + }; + + led_strip_spi_config_t spi_config = { + .clk_src = SPI_CLK_SRC_DEFAULT, + .spi_bus = SPI2_HOST, + .flags = {.with_dma = true} + }; + + led_strip_handle_t led_strip; + ESP_ERROR_CHECK(led_strip_new_spi_device(&strip_config, &spi_config, &led_strip)); + ESP_LOGI(TAG, "LED strip initialized (SPI)"); + return led_strip; +} + +// 设置LED颜色 +static void set_led_color(led_strip_handle_t led_strip, uint8_t r, uint8_t g, uint8_t b) +{ + ESP_ERROR_CHECK(led_strip_set_pixel(led_strip, 0, r, g, b)); + ESP_ERROR_CHECK(led_strip_refresh(led_strip)); +} + +// BMI270 初始化 +static esp_err_t i2c_sensor_bmi270_init(void) +{ + const i2c_config_t i2c_bus_conf = { + .mode = I2C_MODE_MASTER, + .sda_io_num = I2C_MASTER_SDA_IO, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_io_num = I2C_MASTER_SCL_IO, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master.clk_speed = I2C_MASTER_FREQ_HZ + }; + i2c_bus = i2c_bus_create(I2C_NUM_0, &i2c_bus_conf); + if (!i2c_bus) { + ESP_LOGE(TAG, "I2C bus create failed"); + return ESP_FAIL; + } + + bmi270_i2c_config_t i2c_bmi270_conf = { + .i2c_handle = i2c_bus, + .i2c_addr = BMI270_I2C_ADDRESS, + }; + if (bmi270_sensor_create(&i2c_bmi270_conf, &bmi_handle) != ESP_OK || !bmi_handle) { + ESP_LOGE(TAG, "BMI270 create failed"); + return ESP_FAIL; + } + + return ESP_OK; +} + +void gesture_task(void *arg) +{ + uint16_t int_status = 0; + struct bmi2_feat_sensor_data sens_data = { .type = BMI2_WRIST_GESTURE }; + const char *gesture_output[6] = { + "unknown_gesture", "push_arm_down", "pivot_up", + "wrist_shake_jiggle", "flick_in", "flick_out" + }; + + // 配置并启动手势识别 + struct bmi2_sens_config config = {.type = BMI2_WRIST_GESTURE}; + uint8_t sens_list[] = {BMI2_ACCEL, BMI2_WRIST_GESTURE}; + bmi270_sensor_enable(sens_list, 2, bmi_handle); + bmi270_get_sensor_config(&config, 1, bmi_handle); + config.cfg.wrist_gest.wearable_arm = BMI2_ARM_LEFT; + bmi270_set_sensor_config(&config, 1, bmi_handle); + + struct bmi2_sens_int_config sens_int = { + .type = BMI2_WRIST_GESTURE, + .hw_int_pin = BMI2_INT1 + }; + bmi270_map_feat_int(&sens_int, 1, bmi_handle); + + ESP_LOGI(TAG, "Gesture detection started"); + + while (1) { + bmi2_get_int_status(&int_status, bmi_handle); + + if (int_status & BMI270_WRIST_GEST_STATUS_MASK) { + bmi270_get_feature_data(&sens_data, 1, bmi_handle); + int gesture = sens_data.sens_data.wrist_gesture_output; + ESP_LOGI(TAG, "Detected gesture: %s", gesture_output[gesture]); + + switch (gesture) { + case 0: // unknown + set_led_color(led_strip, 0, 0, 0); + break; + case 1: // push_arm_down 红色 + set_led_color(led_strip, 255, 0, 0); + break; + case 2: // pivot_up 绿色 + set_led_color(led_strip, 0, 255, 0); + break; + case 3: // wrist_shake_jiggle 蓝色 + set_led_color(led_strip, 0, 0, 255); + break; + case 4: // flick_in 黄色 + set_led_color(led_strip, 255, 255, 0); + break; + case 5: // flick_out 紫色 + set_led_color(led_strip, 128, 0, 128); + break; + default: + set_led_color(led_strip, 0, 0, 0); + break; + } + vTaskDelay(pdMS_TO_TICKS(500)); + set_led_color(led_strip, 0, 0, 0); + } + vTaskDelay(pdMS_TO_TICKS(100)); + } +} + +void app_main(void) +{ + ESP_ERROR_CHECK(i2c_sensor_bmi270_init()); + led_strip = configure_led(); + + xTaskCreate(gesture_task, "gesture_task", 4096, NULL, 5, NULL); +} diff --git a/esp-spot/example/imu_led/sdkconfig.defaults b/esp-spot/example/imu_led/sdkconfig.defaults new file mode 100644 index 0000000..7e7450a --- /dev/null +++ b/esp-spot/example/imu_led/sdkconfig.defaults @@ -0,0 +1,7 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.3.1 Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_TASK_WDT_EN=n diff --git a/esp-spot/example/s3_factory_bin/s3_spot_factory.bin b/esp-spot/example/s3_factory_bin/s3_spot_factory.bin new file mode 100644 index 0000000..1f0e801 Binary files /dev/null and b/esp-spot/example/s3_factory_bin/s3_spot_factory.bin differ diff --git a/esp-spot/example/simple_touch/CMakeLists.txt b/esp-spot/example/simple_touch/CMakeLists.txt new file mode 100644 index 0000000..5537499 --- /dev/null +++ b/esp-spot/example/simple_touch/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(simple_touch) diff --git a/esp-spot/example/simple_touch/README.md b/esp-spot/example/simple_touch/README.md new file mode 100644 index 0000000..0cfa4c7 --- /dev/null +++ b/esp-spot/example/simple_touch/README.md @@ -0,0 +1,11 @@ +| Supported Targets | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | -------- | -------- | -------- | + +# 简单触摸例程 + +配置 ``channel_threshold`` 调整对应触摸 IO 的灵敏度,阈值越低越灵敏 + +TOUCH_CHANNEL_1 + +``LIGHT_TOUCH_THRESHOLD`` 配置轻按 +``HEAVY_TOUCH_THRESHOLD`` \ No newline at end of file diff --git a/esp-spot/example/simple_touch/main/CMakeLists.txt b/esp-spot/example/simple_touch/main/CMakeLists.txt new file mode 100644 index 0000000..5808c8c --- /dev/null +++ b/esp-spot/example/simple_touch/main/CMakeLists.txt @@ -0,0 +1,6 @@ +set(COMPONENT_REQUIRES touch_button) + +set(COMPONENT_SRCS "main.c") +set(COMPONENT_ADD_INCLUDEDIRS .) + +register_component() \ No newline at end of file diff --git a/esp-spot/example/simple_touch/main/main.c b/esp-spot/example/simple_touch/main/main.c new file mode 100644 index 0000000..eee8510 --- /dev/null +++ b/esp-spot/example/simple_touch/main/main.c @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" + +#include "touch_button.h" +#include "iot_button.h" +#include "touch_sensor_lowlevel.h" + +static const char *TAG = "main"; + +#define TOUCH_CHANNEL_1 (3) +#define TOUCH_CHANNEL_2 (9) +#define TOUCH_CHANNEL_3 (13) +#define TOUCH_CHANNEL_4 (14) + +#define LIGHT_TOUCH_THRESHOLD (0.15) +#define HEAVY_TOUCH_THRESHOLD (0.4) + +static void light_button_event_cb(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "Light Button 1: %s", iot_button_get_event_str(event)); +} + +static void heavy_button_event_cb(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "Heavy Button 1: %s", iot_button_get_event_str(event)); +} + +static void touch_event_light_2(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "Light Button 2: %s", iot_button_get_event_str(event)); +} + +static void touch_event_light_3(void *arg, void *data) +{ + button_event_t event = iot_button_get_event(arg); + ESP_LOGI(TAG, "Light Button 3: %s", iot_button_get_event_str(event)); +} + +static void touch_task(void *arg) +{ + // Register all touch channel + uint32_t touch_channel_list[] = {TOUCH_CHANNEL_1, TOUCH_CHANNEL_2, TOUCH_CHANNEL_3, TOUCH_CHANNEL_4}; + int total_channel_num = sizeof(touch_channel_list) / sizeof(touch_channel_list[0]); + + // calloc channel_type for every button from the list + touch_lowlevel_type_t *channel_type = calloc(total_channel_num, sizeof(touch_lowlevel_type_t)); + assert(channel_type); + for (int i = 0; i < total_channel_num; i++) { + channel_type[i] = TOUCH_LOWLEVEL_TYPE_TOUCH; + } + + touch_lowlevel_config_t low_config = { + .channel_num = total_channel_num, + .channel_list = touch_channel_list, + .channel_type = channel_type, + }; + esp_err_t ret = touch_sensor_lowlevel_create(&low_config); + assert(ret == ESP_OK); + free(channel_type); + + /* ============================= Init touch IO3 ============================= */ + const button_config_t btn_cfg = { + .short_press_time = 300, + .long_press_time = 2000, + }; + button_touch_config_t touch_cfg_1 = { + .touch_channel = touch_channel_list[0], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + // Create light press button + button_handle_t btn_light_1 = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_1, &btn_light_1); + assert(ret == ESP_OK); + + // Create heavy press button + touch_cfg_1.channel_threshold = HEAVY_TOUCH_THRESHOLD; + button_handle_t btn_heavy_1 = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_1, &btn_heavy_1); + assert(ret == ESP_OK); + + /* ============================= Init touch IO9 ============================= */ + button_touch_config_t touch_cfg_2 = { + .touch_channel = touch_channel_list[1], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + // Create light press button + button_handle_t btn_light_2 = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_2, &btn_light_2); + assert(ret == ESP_OK); + + /* ============================= Init touch IO13 ============================= */ + button_touch_config_t touch_cfg_3 = { + .touch_channel = touch_channel_list[2], + .channel_threshold = LIGHT_TOUCH_THRESHOLD, + .skip_lowlevel_init = true, + }; + + // Create light press button + button_handle_t btn_light_3 = NULL; + ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_3, &btn_light_3); + assert(ret == ESP_OK); + + /* ========================== Register touch callback ========================== */ + // Register touch IO3 callback + iot_button_register_cb(btn_light_1, BUTTON_PRESS_DOWN, NULL, light_button_event_cb, NULL); + iot_button_register_cb(btn_light_1, BUTTON_PRESS_UP, NULL, light_button_event_cb, NULL); + iot_button_register_cb(btn_heavy_1, BUTTON_PRESS_DOWN, NULL, heavy_button_event_cb, NULL); + iot_button_register_cb(btn_heavy_1, BUTTON_PRESS_UP, NULL, heavy_button_event_cb, NULL); + + // Register touch IO9 callback + iot_button_register_cb(btn_light_2, BUTTON_LONG_PRESS_START, NULL, touch_event_light_2, NULL); + + // Register touch IO13 callback + iot_button_register_cb(btn_light_3, BUTTON_PRESS_DOWN, NULL, touch_event_light_3, NULL); + + touch_sensor_lowlevel_start(); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +void app_main(void) +{ + xTaskCreate(touch_task, "touch_task", 1024 * 5, NULL, 5, NULL); +} diff --git a/esp-spot/example/simple_touch/sdkconfig.defaults b/esp-spot/example/simple_touch/sdkconfig.defaults new file mode 100644 index 0000000..6d63c62 --- /dev/null +++ b/esp-spot/example/simple_touch/sdkconfig.defaults @@ -0,0 +1,7 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.3.1 Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32s3" +CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY=y +CONFIG_TOUCH_BUTTON_SENSOR_MAX_P_X1000=0 +CONFIG_TOUCH_BUTTON_SENSOR_MIN_N_X1000=0 diff --git a/esp-spot/example/xiaozhi/README.md b/esp-spot/example/xiaozhi/README.md new file mode 100644 index 0000000..e69de29 diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..5a525fc --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + espressif/zlib: + version: "^1.3.1" + public: true + espressif/cjson: + version: "^1.7.15" + public: true \ No newline at end of file diff --git a/main/BluFi配网使用指南.md b/main/BluFi配网使用指南.md new file mode 100644 index 0000000..013744b --- /dev/null +++ b/main/BluFi配网使用指南.md @@ -0,0 +1,216 @@ +# BluFi配网使用指南 + +## 概述 + +本项目已成功集成BluFi配网功能,实现了蓝牙优先配网,2分钟超时后自动回退到WiFi AP配网模式。 + +## 配网流程 + +### 1. 自动配网流程 + +1. **设备启动** - 设备启动后自动检查是否有已保存的WiFi凭据 +2. **BluFi优先** - 如果没有WiFi凭据或WiFi连接失败,自动启动BluFi配网 +3. **设备广播** - 设备开始蓝牙广播,设备名格式:`Airhub-XXXXXX`(后6位为MAC地址) +4. **客户端连接** - 用户使用手机APP连接设备 +5. **WiFi配置** - 通过APP发送WiFi SSID和密码 +6. **自动连接** - 设备接收到WiFi凭据后自动尝试连接 +7. **超时回退** - 如果2分钟内配网未成功,自动切换到WiFi AP模式 + +### 2. 状态指示 + +- **BluFi配网模式** - 显示"BluFi配网模式"和设备名 +- **客户端连接** - 显示"客户端已连接" +- **凭据接收** - 显示"WiFi凭据已接收" +- **连接成功** - 显示"WiFi连接成功"并播放提示音 +- **连接失败** - 显示"WiFi连接失败" +- **配网超时** - 自动切换到WiFi AP模式 + +## 客户端APP开发 + +### Android开发示例 + +```java +// 1. 添加蓝牙权限 + + + + +// 2. 扫描BluFi设备 +BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); +BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner(); + +// 3. 连接设备并发送WiFi凭据 +// 使用ESP-IDF提供的BluFi库或自定义实现 +``` + +### iOS开发示例 + +```swift +// 1. 添加蓝牙权限到Info.plist +NSBluetoothAlwaysUsageDescription +需要蓝牙权限进行设备配网 + +// 2. 使用Core Bluetooth框架 +import CoreBluetooth + +// 3. 实现CBCentralManagerDelegate和CBPeripheralDelegate +// 4. 扫描并连接BluFi设备 +// 5. 发送WiFi凭据 +``` + +### 微信小程序开发示例 + +```javascript +// 1. 开启蓝牙适配器 +wx.openBluetoothAdapter({ + success: function(res) { + console.log('蓝牙适配器开启成功'); + } +}); + +// 2. 搜索蓝牙设备 +wx.startBluetoothDevicesDiscovery({ + services: [], // BluFi服务UUID + success: function(res) { + console.log('开始搜索设备'); + } +}); + +// 3. 连接设备并发送WiFi信息 +// 使用wx.createBLEConnection()和wx.writeBLECharacteristicValue() +``` + +## 技术规格 + +### BluFi协议参数 + +- **服务UUID**: ESP32 BluFi标准服务 +- **设备名前缀**: `Airhub-` +- **配网超时**: 120秒(2分钟) +- **最大连接数**: 1个客户端 +- **安全模式**: 支持加密传输(可配置) + +### 支持的WiFi参数 + +- **SSID**: 最长32字节 +- **密码**: 最长64字节 +- **安全类型**: WPA/WPA2/WPA3 +- **频段**: 2.4GHz + +## 配置选项 + +可通过`idf.py menuconfig`配置以下选项: + +``` +Component config → Bluetooth Provisioning Configuration +├── Enable Bluetooth Provisioning [*] +├── Device Name Prefix (Airhub) +├── Security Mode (0) +├── Auto Stop After Success [*] +├── Stop Delay (seconds) (5) +├── WiFi Connection Timeout (seconds) (30) +├── WiFi Retry Count (3) +└── Enable Verbose Logging [ ] +``` + +## 故障排除 + +### 常见问题 + +1. **BluFi启动失败** + - 检查sdkconfig中蓝牙配置是否正确 + - 确认CONFIG_BT_ENABLED=y + - 确认CONFIG_BT_BLUFI_ENABLED=y + +2. **客户端无法发现设备** + - 确认设备蓝牙广播正常 + - 检查客户端蓝牙权限 + - 确认设备名称格式正确 + +3. **WiFi连接失败** + - 检查WiFi凭据是否正确 + - 确认WiFi信号强度 + - 检查路由器兼容性 + +4. **配网超时** + - 检查客户端APP实现 + - 确认蓝牙连接稳定性 + - 调整超时时间配置 + +### 调试方法 + +1. **启用详细日志** + ``` + CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y + CONFIG_BT_STACK_NO_LOG=n + ``` + +2. **监控串口输出** + ```bash + idf.py monitor + ``` + +3. **使用蓝牙抓包工具** + - Android: HCI Snoop Log + - iOS: PacketLogger + - PC: Wireshark + Bluetooth adapter + +## 性能优化 + +### 内存优化 + +- 蓝牙协议栈预留内存:64KB +- BluFi最大连接数:1 +- 动态内存分配:关闭 + +### 功耗优化 + +- 配网成功后自动停止蓝牙 +- 支持蓝牙低功耗模式 +- WiFi和蓝牙共存优化 + +## 安全考虑 + +### 数据加密 + +- 支持AES加密传输 +- 可配置PSK预共享密钥 +- 防重放攻击保护 + +### 访问控制 + +- 设备名称随机化 +- 连接超时保护 +- 最大重试次数限制 + +## 扩展功能 + +### 自定义数据传输 + +- 支持自定义数据通道 +- 设备信息查询 +- 固件版本检查 +- OTA升级支持 + +### 多语言支持 + +- 中文界面提示 +- 英文调试信息 +- 可扩展其他语言 + +## 版本历史 + +- **v1.0.0** - 初始版本,基础BluFi配网功能 +- **v1.1.0** - 添加超时回退机制 +- **v1.2.0** - 优化用户界面和提示 +- **v1.3.0** - 添加安全加密支持 + +## 技术支持 + +如有问题,请检查: +1. ESP-IDF版本兼容性 +2. 硬件蓝牙模块状态 +3. 客户端APP实现 +4. 网络环境配置 + +更多技术细节请参考ESP-IDF官方BluFi文档。 \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..f3cfd5d --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,239 @@ +set(SOURCES "audio_codecs/audio_codec.cc" + "audio_codecs/no_audio_codec.cc" + "audio_codecs/box_audio_codec.cc" + "audio_codecs/es8311_audio_codec.cc" + "audio_codecs/es8388_audio_codec.cc" + "audio/simple_pipeline.cc" + "led/single_led.cc" + "led/circular_strip.cc" + "led/gpio_led.cc" + "display/display.cc" + #"display/lcd_display.cc" # 移除LCD显示器支持 + #"display/oled_display.cc" # 移除OLED显示器支持 + "protocols/protocol.cc" + "iot/thing.cc" + "iot/thing_manager.cc" + "system_info.cc" + "application.cc" + "ota.cc" + "settings.cc" + "background_task.cc" + "bluetooth_provisioning.cc" # 蓝牙配网实现 + #"ble_service.cc" # BLE JSON 通讯服务(暂不使用,保留代码) + "weather_api.cc" + "main.cc" + ) + +set(INCLUDE_DIRS "." "display" "audio_codecs" "protocols" "audio_processing" "audio") + +# 添加 IOT 相关文件 +file(GLOB IOT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/iot/things/*.cc) +# 排除 screen.cc 文件,因为这个板子没有显示器 +list(REMOVE_ITEM IOT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/iot/things/screen.cc) +list(APPEND SOURCES ${IOT_SOURCES}) + +# 添加板级公共文件 +file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc) +# Exclude ml307_board.cc by default, only include it when ML307 board is selected +list(FILTER BOARD_COMMON_SOURCES EXCLUDE REGEX ".*ml307_board\.cc$") +list(APPEND SOURCES ${BOARD_COMMON_SOURCES}) +list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/boards/common) + +# Include ml307_board.cc only when ML307 board is selected +if(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307 OR CONFIG_BOARD_TYPE_XINGZHI_Cube_0_85TFT_ML307 OR CONFIG_BOARD_TYPE_XINGZHI_Cube_0_96OLED_ML307 OR CONFIG_BOARD_TYPE_XINGZHI_Cube_1_54TFT_ML307) + list(APPEND SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/ml307_board.cc) +endif() + +# 根据 BOARD_TYPE 配置添加对应的板级文件 +if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI) + set(BOARD_TYPE "bread-compact-wifi") +elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307) + set(BOARD_TYPE "bread-compact-ml307") +elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32) + set(BOARD_TYPE "bread-compact-esp32") +elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD) + set(BOARD_TYPE "bread-compact-esp32-lcd") +elseif(CONFIG_BOARD_TYPE_DF_K10) + set(BOARD_TYPE "df-k10") +elseif(CONFIG_BOARD_TYPE_ESP_BOX_3) + set(BOARD_TYPE "esp-box-3") +elseif(CONFIG_BOARD_TYPE_ESP_BOX) + set(BOARD_TYPE "esp-box") +elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE) + set(BOARD_TYPE "esp-box-lite") +elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_1) + set(BOARD_TYPE "kevin-box-1") +elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2) + set(BOARD_TYPE "kevin-box-2") +elseif(CONFIG_BOARD_TYPE_KEVIN_C3) + set(BOARD_TYPE "kevin-c3") +elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V3_DEV) + set(BOARD_TYPE "kevin-sp-v3-dev") +elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V4_DEV) + set(BOARD_TYPE "kevin-sp-v4-dev") +elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD) + set(BOARD_TYPE "kevin-yuying-313lcd") +elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV) + set(BOARD_TYPE "lichuang-dev") +elseif(CONFIG_BOARD_TYPE_LICHUANG_C3_DEV) + set(BOARD_TYPE "lichuang-c3-dev") +elseif(CONFIG_BOARD_TYPE_MAGICLICK_2P4) + set(BOARD_TYPE "magiclick-2p4") +elseif(CONFIG_BOARD_TYPE_MAGICLICK_2P5) + set(BOARD_TYPE "magiclick-2p5") +elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3) + set(BOARD_TYPE "magiclick-c3") +elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3_V2) + set(BOARD_TYPE "magiclick-c3-v2") +elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_S3) + set(BOARD_TYPE "m5stack-core-s3") +elseif(CONFIG_BOARD_TYPE_ATOMS3_ECHO_BASE) + set(BOARD_TYPE "atoms3-echo-base") +elseif(CONFIG_BOARD_TYPE_ATOMS3R_ECHO_BASE) + set(BOARD_TYPE "atoms3r-echo-base") +elseif(CONFIG_BOARD_TYPE_ATOMS3R_CAM_M12_ECHO_BASE) + set(BOARD_TYPE "atoms3r-cam-m12-echo-base") +elseif(CONFIG_BOARD_TYPE_ATOMMATRIX_ECHO_BASE) + set(BOARD_TYPE "atommatrix-echo-base") +elseif(CONFIG_BOARD_TYPE_XMINI_C3) + set(BOARD_TYPE "xmini-c3") +elseif(CONFIG_BOARD_TYPE_ESP32S3_KORVO2_V3) + set(BOARD_TYPE "esp32s3-korvo2-v3") +elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT) + set(BOARD_TYPE "esp-sparkbot") +elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8) + set(BOARD_TYPE "esp32-s3-touch-amoled-1.8") +elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85C) + set(BOARD_TYPE "esp32-s3-touch-lcd-1.85c") +elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85) + set(BOARD_TYPE "esp32-s3-touch-lcd-1.85") +elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_46) + set(BOARD_TYPE "esp32-s3-touch-lcd-1.46") +elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_3_5) + set(BOARD_TYPE "esp32-s3-touch-lcd-3.5") +elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD) + set(BOARD_TYPE "bread-compact-wifi-lcd") +elseif(CONFIG_BOARD_TYPE_TUDOUZI) + set(BOARD_TYPE "tudouzi") +elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3) + set(BOARD_TYPE "lilygo-t-circle-s3") +elseif(CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3) + set(BOARD_TYPE "lilygo-t-cameraplus-s3") +elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3) + set(BOARD_TYPE "movecall-moji-esp32s3") + elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3) + set(BOARD_TYPE "movecall-cuican-esp32s3") +elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3) + set(BOARD_TYPE "atk-dnesp32s3") +elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX) + set(BOARD_TYPE "atk-dnesp32s3-box") +elseif(CONFIG_BOARD_TYPE_DU_CHATX) + set(BOARD_TYPE "du-chatx") +elseif(CONFIG_BOARD_TYPE_ESP32S3_Taiji_Pi) + set(BOARD_TYPE "taiji-pi-s3") +elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_85TFT_WIFI) + set(BOARD_TYPE "xingzhi-cube-0.85tft-wifi") +elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_85TFT_ML307) + set(BOARD_TYPE "xingzhi-cube-0.85tft-ml307") +elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_96OLED_WIFI) + set(BOARD_TYPE "xingzhi-cube-0.96oled-wifi") +elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_96OLED_ML307) + set(BOARD_TYPE "xingzhi-cube-0.96oled-ml307") +elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_1_54TFT_WIFI) + set(BOARD_TYPE "xingzhi-cube-1.54tft-wifi") +elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_1_54TFT_ML307) + set(BOARD_TYPE "xingzhi-cube-1.54tft-ml307") +elseif(CONFIG_BOARD_TYPE_SENSECAP_WATCHER) + set(BOARD_TYPE "sensecap-watcher") +elseif(CONFIG_BOARD_TYPE_ESP32_CGC) + set(BOARD_TYPE "esp32-cgc") +endif() +file(GLOB BOARD_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc + ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.c +) +list(APPEND SOURCES ${BOARD_SOURCES}) + +if(CONFIG_CONNECTION_TYPE_MQTT_UDP) + list(APPEND SOURCES "protocols/mqtt_protocol.cc") +endif() +if(CONFIG_CONNECTION_TYPE_WEBSOCKET) + list(APPEND SOURCES "protocols/websocket_protocol.cc") +endif() +if(CONFIG_CONNECTION_TYPE_VOLC_RTC) + list(APPEND SOURCES "protocols/volc_rtc_protocol.cc") +endif() + +if(CONFIG_USE_AUDIO_PROCESSOR) + list(APPEND SOURCES "audio_processing/audio_processor.cc") +endif() +if(CONFIG_USE_WAKE_WORD_DETECT) + list(APPEND SOURCES "audio_processing/wake_word_detect.cc") +elseif(CONFIG_USE_CUSTOM_WAKE_WORD) + list(APPEND SOURCES "audio_processing/custom_wake_word.cc") +endif() + +# 根据Kconfig选择语言目录 +if(CONFIG_LANGUAGE_ZH_CN) + set(LANG_DIR "zh-CN") +elseif(CONFIG_LANGUAGE_ZH_TW) + set(LANG_DIR "zh-TW") +elseif(CONFIG_LANGUAGE_EN_US) + set(LANG_DIR "en-US") +elseif(CONFIG_LANGUAGE_JA_JP) + set(LANG_DIR "ja-JP") +endif() + +# 定义生成路径 +set(LANG_JSON "${CMAKE_CURRENT_SOURCE_DIR}/assets/${LANG_DIR}/language.json") +set(LANG_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/assets/lang_config.h") +file(GLOB LANG_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/${LANG_DIR}/*.p3) +file(GLOB COMMON_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/common/*.p3) + +# 如果目标芯片是 ESP32,则排除特定文件 +if(CONFIG_IDF_TARGET_ESP32) + list(REMOVE_ITEM SOURCES "audio_codecs/box_audio_codec.cc" + "audio_codecs/es8388_audio_codec.cc" + "led/gpio_led.cc" + ) +endif() + +idf_component_register(SRCS ${SOURCES} + EMBED_FILES ${LANG_SOUNDS} ${COMMON_SOUNDS} + INCLUDE_DIRS ${INCLUDE_DIRS} + REQUIRES esp_wifi esp_netif esp_event nvs_flash bt spi_flash app_update efuse volc_engine_rtc_lite common zlib + WHOLE_ARCHIVE + ) + +# 使用 target_compile_definitions 来定义 BOARD_TYPE, BOARD_NAME +# 如果 BOARD_NAME 为空,则使用 BOARD_TYPE +if(NOT BOARD_NAME) + set(BOARD_NAME ${BOARD_TYPE}) +endif() +target_compile_definitions(${COMPONENT_LIB} + PRIVATE BOARD_TYPE=\"${BOARD_TYPE}\" BOARD_NAME=\"${BOARD_NAME}\" + ) + +# 添加生成规则 +add_custom_command( + OUTPUT ${LANG_HEADER} + COMMAND python ${PROJECT_DIR}/scripts/gen_lang.py + --input "${LANG_JSON}" + --output "${LANG_HEADER}" + DEPENDS + ${LANG_JSON} + ${PROJECT_DIR}/scripts/gen_lang.py + COMMENT "Generating ${LANG_DIR} language config" +) + +# 强制建立生成依赖 +add_custom_target(lang_header ALL + DEPENDS ${LANG_HEADER} +) + +# Add ENABLE_RTC_MODE definition if VOLC_RTC connection type is selected +if(CONFIG_CONNECTION_TYPE_VOLC_RTC) + target_compile_definitions(${COMPONENT_LIB} PRIVATE ENABLE_RTC_MODE) + # Link against zlib library directly + target_link_libraries(${COMPONENT_LIB} PRIVATE ${CMAKE_BINARY_DIR}/esp-idf/zlib/libzlib.a) +endif() diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..e3e8f69 --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,417 @@ +menu "Kapi Assistant" + +config OTA_VERSION_URL + string "OTA Version URL" + default "https://api.tenclass.net/xiaozhi/ota/" + help + The application will access this URL to check for updates. + +config DEVICE_STATUS_REPORT_URL + string "Device Status Report URL" + default "http://192.168.124.24:8000/api/v1/devices/report-status" + help + URL for reporting device status to server + +config STORY_API_URL + string "Story API URL" + default "http://192.168.124.8:8000/api/v1/devices/stories/" + help + 故事播放API接口地址,设备会附加 ?mac_address=XX:XX:XX:XX:XX:XX 参数请求 + +config MUSIC_API_URL + string "Music API URL" + default "http://192.168.124.8:8000/api/v1/devices/music/" + help + 音乐播放API接口地址,设备会附加 ?mac_address=XX:XX:XX:XX:XX:XX 参数请求 + +choice + prompt "语言选择" + default LANGUAGE_ZH_CN + help + Select device display language + + config LANGUAGE_ZH_CN + bool "Chinese" + config LANGUAGE_ZH_TW + bool "Chinese Traditional" + config LANGUAGE_EN_US + bool "English" + config LANGUAGE_JA_JP + bool "Japanese" +endchoice + + +menu "Connection Protocol Selection" + help + 网络数据传输协议(可选择多个) + config CONNECTION_TYPE_MQTT_UDP + bool "MQTT + UDP" + default y + help + 使用MQTT + UDP协议 + config CONNECTION_TYPE_WEBSOCKET + bool "Websocket" + default n + help + 使用Websocket协议 + config CONNECTION_TYPE_VOLC_RTC + bool "Volcano RTC" + default n + help + 使用Volcano RTC协议 +endmenu + +config WEBSOCKET_URL + depends on CONNECTION_TYPE_WEBSOCKET + string "Websocket URL" + default "wss://api.tenclass.net/xiaozhi/v1/" + help + Communication with the server through websocket after wake up. + +config WEBSOCKET_ACCESS_TOKEN + depends on CONNECTION_TYPE_WEBSOCKET + string "Websocket Access Token" + default "test-token" + help + Access token for websocket communication. + +config VOLC_INSTANCE_ID + depends on CONNECTION_TYPE_VOLC_RTC + string "Volcano Instance ID" + default "" + help + Instance ID for Volcano RTC authentication. + +config VOLC_PRODUCT_KEY + depends on CONNECTION_TYPE_VOLC_RTC + string "Volcano Product Key" + default "" + help + Product Key for Volcano RTC authentication. + +config VOLC_PRODUCT_SECRET + depends on CONNECTION_TYPE_VOLC_RTC + string "Volcano Product Secret" + default "" + help + Product Secret for Volcano RTC authentication. + +config VOLC_BOT_ID + depends on CONNECTION_TYPE_VOLC_RTC + string "Volcano Bot ID" + default "" + help + Bot ID for Volcano RTC. + +config VOLC_DEVICE_NAME + depends on CONNECTION_TYPE_VOLC_RTC + string "Volcano Device Name" + default "" + help + Device name for Volcano RTC.注意:此值将被忽略,实际使用设备Wi-Fi MAC地址 + +choice BOARD_TYPE + prompt "Board Type" + default BOARD_TYPE_BREAD_COMPACT_WIFI + help + Board type. 开发板类型 + config BOARD_TYPE_BREAD_COMPACT_WIFI + bool "面包板新版接线(WiFi)" + config BOARD_TYPE_BREAD_COMPACT_WIFI_LCD + bool "面包板新版接线(WiFi)+ LCD" + config BOARD_TYPE_BREAD_COMPACT_ML307 + bool "面包板新版接线(ML307 AT)" + config BOARD_TYPE_BREAD_COMPACT_ESP32 + bool "面包板(WiFi) ESP32 DevKit" + config BOARD_TYPE_BREAD_COMPACT_ESP32_LCD + bool "面包板(WiFi+ LCD) ESP32 DevKit" + config BOARD_TYPE_ESP32_CGC + bool "ESP32 CGC" + config BOARD_TYPE_ESP_BOX_3 + bool "ESP BOX 3" + config BOARD_TYPE_ESP_BOX + bool "ESP BOX" + config BOARD_TYPE_ESP_BOX_LITE + bool "ESP BOX Lite" + config BOARD_TYPE_KEVIN_BOX_1 + bool "Kevin Box 1" + config BOARD_TYPE_KEVIN_BOX_2 + bool "Kevin Box 2" + config BOARD_TYPE_KEVIN_C3 + bool "Kevin C3" + config BOARD_TYPE_KEVIN_SP_V3_DEV + bool "Kevin SP V3开发板" + config BOARD_TYPE_KEVIN_SP_V4_DEV + bool "Kevin SP V4开发板" + config BOARD_TYPE_KEVIN_YUYING_313LCD + bool "鱼鹰科技3.13LCD开发板" + config BOARD_TYPE_LICHUANG_DEV + bool "立创·实战派ESP32-S3开发板" + config BOARD_TYPE_LICHUANG_C3_DEV + bool "立创·实战派ESP32-C3开发板" + config BOARD_TYPE_DF_K10 + bool "DFRobot 行空板 k10" + config BOARD_TYPE_MAGICLICK_2P4 + bool "神奇按钮 Magiclick_2.4" + config BOARD_TYPE_MAGICLICK_2P5 + bool "神奇按钮 Magiclick_2.5" + config BOARD_TYPE_MAGICLICK_C3 + bool "神奇按钮 Magiclick_C3" + config BOARD_TYPE_MAGICLICK_C3_V2 + bool "神奇按钮 Magiclick_C3_v2" + config BOARD_TYPE_M5STACK_CORE_S3 + bool "M5Stack CoreS3" + config BOARD_TYPE_ATOMS3_ECHO_BASE + bool "AtomS3 + Echo Base" + config BOARD_TYPE_ATOMS3R_ECHO_BASE + bool "AtomS3R + Echo Base" + config BOARD_TYPE_ATOMS3R_CAM_M12_ECHO_BASE + bool "AtomS3R CAM/M12 + Echo Base" + config BOARD_TYPE_ATOMMATRIX_ECHO_BASE + bool "AtomMatrix + Echo Base" + config BOARD_TYPE_XMINI_C3 + bool "虾哥 Mini C3" + config BOARD_TYPE_ESP32S3_KORVO2_V3 + bool "ESP32S3_KORVO2_V3开发板" + config BOARD_TYPE_ESP_SPARKBOT + bool "ESP-SparkBot开发板" + config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8 + bool "Waveshare ESP32-S3-Touch-AMOLED-1.8" + config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C + bool "Waveshare ESP32-S3-Touch-LCD-1.85C" + config BOARD_TYPE_ESP32S3_Touch_LCD_1_85 + bool "Waveshare ESP32-S3-Touch-LCD-1.85" + config BOARD_TYPE_ESP32S3_Touch_LCD_1_46 + bool "Waveshare ESP32-S3-Touch-LCD-1.46" + config BOARD_TYPE_ESP32S3_Touch_LCD_3_5 + bool "Waveshare ESP32-S3-Touch-LCD-3.5" + config BOARD_TYPE_TUDOUZI + bool "土豆子" + config BOARD_TYPE_LILYGO_T_CIRCLE_S3 + bool "LILYGO T-Circle-S3" + config BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3 + bool "LILYGO T-CameraPlus-S3" + config BOARD_TYPE_MOVECALL_MOJI_ESP32S3 + bool "Movecall Moji 小智AI衍生版" + config BOARD_TYPE_MOVECALL_CUICAN_ESP32S3 + bool "Movecall CuiCan 璀璨·AI吊坠" + config BOARD_TYPE_ATK_DNESP32S3 + bool "正点原子DNESP32S3开发板" + config BOARD_TYPE_ATK_DNESP32S3_BOX + bool "正点原子DNESP32S3-BOX" + config BOARD_TYPE_DU_CHATX + bool "嘟嘟开发板CHATX(wifi)" + config BOARD_TYPE_ESP32S3_Taiji_Pi + bool "太极小派esp32s3" + config BOARD_TYPE_XINGZHI_Cube_0_85TFT_WIFI + bool "无名科技星智0.85(WIFI)" + config BOARD_TYPE_XINGZHI_Cube_0_85TFT_ML307 + bool "无名科技星智0.85(ML307)" + config BOARD_TYPE_XINGZHI_Cube_0_96OLED_WIFI + bool "无名科技星智0.96(WIFI)" + config BOARD_TYPE_XINGZHI_Cube_0_96OLED_ML307 + bool "无名科技星智0.96(ML307)" + config BOARD_TYPE_XINGZHI_Cube_1_54TFT_WIFI + bool "无名科技星智1.54(WIFI)" + config BOARD_TYPE_XINGZHI_Cube_1_54TFT_ML307 + bool "无名科技星智1.54(ML307)" + config BOARD_TYPE_SENSECAP_WATCHER + bool "SenseCAP Watcher" +endchoice + +choice DISPLAY_OLED_TYPE + depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32 + prompt "OLED Type" + default OLED_SSD1306_128X32 + help + OLED 屏幕类型选择 + config OLED_SSD1306_128X32 + bool "SSD1306, 分辨率128*32" + config OLED_SSD1306_128X64 + bool "SSD1306, 分辨率128*64" + config OLED_SH1106_128X64 + bool "SH1106, 分辨率128*64" +endchoice + +choice DISPLAY_LCD_TYPE + depends on BOARD_TYPE_BREAD_COMPACT_WIFI_LCD || BOARD_TYPE_BREAD_COMPACT_ESP32_LCD || BOARD_TYPE_ESP32_CGC + prompt "LCD Type" + default LCD_ST7789_240X320 + help + 屏幕类型选择 + config LCD_ST7789_240X320 + bool "ST7789, 分辨率240*320, IPS" + config LCD_ST7789_240X320_NO_IPS + bool "ST7789, 分辨率240*320, 非IPS" + config LCD_ST7789_170X320 + bool "ST7789, 分辨率170*320" + config LCD_ST7789_172X320 + bool "ST7789, 分辨率172*320" + config LCD_ST7789_240X280 + bool "ST7789, 分辨率240*280" + config LCD_ST7789_240X240 + bool "ST7789, 分辨率240*240" + config LCD_ST7789_240X240_7PIN + bool "ST7789, 分辨率240*240, 7PIN" + config LCD_ST7789_240X135 + bool "ST7789, 分辨率240*135" + config LCD_ST7735_128X160 + bool "ST7735, 分辨率128*160" + config LCD_ST7735_128X128 + bool "ST7735, 分辨率128*128" + config LCD_ST7796_320X480 + bool "ST7796, 分辨率320*480 IPS" + config LCD_ST7796_320X480_NO_IPS + bool "ST7796, 分辨率320*480, 非IPS" + config LCD_ILI9341_240X320 + bool "ILI9341, 分辨率240*320" + config LCD_ILI9341_240X320_NO_IPS + bool "ILI9341, 分辨率240*320, 非IPS" + config LCD_GC9A01_240X240 + bool "GC9A01, 分辨率240*240, 圆屏" + config LCD_CUSTOM + bool "自定义屏幕参数" +endchoice + +choice DISPLAY_ESP32S3_KORVO2_V3 + depends on BOARD_TYPE_ESP32S3_KORVO2_V3 + prompt "ESP32S3_KORVO2_V3 LCD Type" + default LCD_ST7789 + help + 屏幕类型选择 + config LCD_ST7789 + bool "ST7789, 分辨率240*280" + config LCD_ILI9341 + bool "ILI9341, 分辨率240*320" +endchoice + +config USE_WECHAT_MESSAGE_STYLE + bool "使用微信聊天界面风格" + default n + help + 使用微信聊天界面风格 + +choice WAKE_WORD_TYPE + prompt "唤醒词检测类型" + default WAKE_WORD_NONE + depends on IDF_TARGET_ESP32S3 && SPIRAM + help + 选择唤醒词检测类型,两种类型互斥 + + config USE_WAKE_WORD_DETECT + bool "启用传统唤醒词检测" + help + 需要 ESP32 S3 与 AFE 支持,使用内置唤醒词检测 + + config USE_CUSTOM_WAKE_WORD + bool "启用自定义唤醒词检测" + help + 启用自定义唤醒词检测功能 + 需要 ESP32 S3 与 PSRAM 支持 + 与传统唤醒词检测互斥,不能同时启用 + config WAKE_WORD_NONE + bool "禁用唤醒词检测" +endchoice + +config CUSTOM_WAKE_WORD + string "自定义唤醒词" + default "ni hao xiao zhi" + depends on USE_CUSTOM_WAKE_WORD + help + 自定义唤醒词,用汉语拼音表示 + 例如: "ni hao xiao zhi" 对应 "你好小智" + +config CUSTOM_WAKE_WORD_DISPLAY + string "自定义唤醒词显示文本" + default "Hello Qi Yuan" + depends on USE_CUSTOM_WAKE_WORD + help + 自定义唤醒词显示文本,用于界面显示 + 这是用户看到的实际文字 + 注意:如果输入中文出现乱码,请使用英文或直接编辑sdkconfig文件 + +config USE_AUDIO_PROCESSOR + bool "启用音频降噪、增益处理" + default y + depends on IDF_TARGET_ESP32S3 && SPIRAM + help + 需要 ESP32 S3 与 AFE 支持 + +config USE_REALTIME_CHAT + bool "启用可语音打断的实时对话模式(需要 AEC 支持)" + default n + depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_MOVECALL_MOJI_ESP32S3) + help + 需要 ESP32 S3 与 AEC 开启,因为性能不够,不建议和微信聊天界面风格同时开启 + +endmenu + +# 蓝牙配网功能配置选项 +menu "蓝牙配网 (Bluetooth Provisioning)" + + config BLUETOOTH_PROVISIONING_ENABLE + bool "启用蓝牙配网功能" + default y + select BT_ENABLED + select BLUEDROID_ENABLED + select BT_BLUFI_ENABLE + help + 启用蓝牙配网功能,允许通过蓝牙BLE连接配置WiFi网络。 + 需要ESP-IDF的蓝牙和BLUFI组件支持。 + + config BLUETOOTH_PROVISIONING_SECURITY + bool "启用安全模式" + depends on BLUETOOTH_PROVISIONING_ENABLE + default n + help + 启用蓝牙配网的安全模式,使用加密通信。 + 需要客户端APP支持相同的安全协议。 + + config BLUETOOTH_PROVISIONING_AUTO_STOP + bool "配网成功后自动停止蓝牙服务" + depends on BLUETOOTH_PROVISIONING_ENABLE + default y + help + WiFi配网成功后自动停止蓝牙配网服务以节省资源。 + + config BLUETOOTH_PROVISIONING_AUTO_STOP_DELAY + int "自动停止延迟时间 (秒)" + depends on BLUETOOTH_PROVISIONING_AUTO_STOP + default 5 + range 1 60 + help + 配网成功后延迟停止蓝牙服务的时间,单位为秒。 + 给客户端足够时间接收状态报告。 + + config BLUETOOTH_PROVISIONING_WIFI_TIMEOUT + int "WiFi连接超时时间 (秒)" + depends on BLUETOOTH_PROVISIONING_ENABLE + default 30 + range 10 120 + help + WiFi连接的超时时间,单位为秒。 + 超时后将报告连接失败。 + + config BLUETOOTH_PROVISIONING_WIFI_RETRY + int "WiFi连接最大重试次数" + depends on BLUETOOTH_PROVISIONING_ENABLE + default 5 + range 1 20 + help + WiFi连接失败时的最大重试次数。 + 达到最大次数后将报告连接失败。 + + config BLUETOOTH_PROVISIONING_VERBOSE_LOG + bool "启用详细日志" + depends on BLUETOOTH_PROVISIONING_ENABLE + default n + help + 启用蓝牙配网的详细日志输出,用于调试和问题排查。 + +endmenu + +config DEVICE_ROLE + string "设备角色标识" + default "KAKA" + help + 用于OTA升级时的角色校验(如KAKA/CAPYBARA) diff --git a/main/application.cc b/main/application.cc new file mode 100644 index 0000000..87811f9 --- /dev/null +++ b/main/application.cc @@ -0,0 +1,4281 @@ +#include "application.h" +// #include "ble_service_config.h" // BLE JSON Service 暂不使用 +#include "board.h" +#include "wifi_board.h" +#include "display.h" +#include "system_info.h" +#include "ml307_ssl_transport.h" +#include "audio_codec.h" +#include "settings.h" +#include "mqtt_protocol.h" +#include "websocket_protocol.h" +#include "volc_rtc_protocol.h" +#include "font_awesome_symbols.h" +#include "iot/thing_manager.h" +#include "assets/lang_config.h" +#include "volume_config.h" +#include "boards/common/qmi8658a.h" // 添加qmi8658a_data_t类型的头文件 +#include "boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h" // 添加MovecallMojiESP32S3类的头文件 +#include "weather_api.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "Application" +#define MAC_TAG "BluetoothMAC" + +// 设备空闲无对话状态 倒计时 +#define DIALOG_IDLE_COUNTDOWN_SECONDS 40 + + +// 定义设备状态字符串 +static const char* const STATE_STRINGS[] = { + "unknown", + "starting", + "configuring", + "idle", + "connecting", + "listening", + "speaking", + "dialog", + "upgrading", + "activating", + "fatal_error" +}; + +Application::Application() { + event_group_ = xEventGroupCreate(); + background_task_ = new BackgroundTask(4096 * 8); + last_audible_output_time_ = std::chrono::steady_clock::now(); // 初始化最后一次有声音输出的时间点 + skip_dialog_idle_session_ = false; // 初始化跳过对话待机会话标志为false + dialog_watchdog_running_ = false; // 初始化对话看门狗运行标志 + dialog_watchdog_last_logged_ = -1; // 初始化对话看门狗日志记录 + dialog_watchdog_task_handle_ = nullptr; // 初始化对话看门狗任务句柄 + clock_ticks_ = 0; // 初始化时钟计数 + main_loop_task_handle_ = nullptr; // 初始化主循环任务句柄 + check_new_version_task_handle_ = nullptr; // 初始化版本检查任务句柄 + audio_loop_task_handle_ = nullptr; // 初始化音频循环任务句柄 + + esp_timer_create_args_t clock_timer_args = { + .callback = [](void* arg) { + Application* app = (Application*)arg; + app->OnClockTimer(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "clock_timer", + .skip_unhandled_events = true + }; + esp_timer_create(&clock_timer_args, &clock_timer_handle_); +} + +Application::~Application() { + // 停止并清理对话看门狗 + StopDialogWatchdog(); + + if (clock_timer_handle_ != nullptr) { + esp_timer_stop(clock_timer_handle_); + esp_timer_delete(clock_timer_handle_); + } + if (background_task_ != nullptr) { + delete background_task_; + } + if (recorder_pipeline_) { + recorder_pipeline_close(recorder_pipeline_); + recorder_pipeline_ = nullptr; + } + if (player_pipeline_) { + player_pipeline_close(player_pipeline_); + player_pipeline_ = nullptr; + } + vEventGroupDelete(event_group_); +} + +void Application::CheckNewVersion() { + // ESP_LOGI(TAG, "OTA版本检查已临时禁用"); + // return; + auto& board = Board::GetInstance(); + auto display = board.GetDisplay(); + // Check if there is a new firmware version available + ota_.SetPostData(board.GetJson());// 发送当前设备的JSON数据到OTA服务器,用于检查是否有新的固件版本 包办板载信息 BOARD_TYPE + + const int MAX_RETRY = 10; + int retry_count = 0; + + while (true) { + if (!ota_.CheckVersion()) { + retry_count++; + if (retry_count >= MAX_RETRY) { + ESP_LOGE(TAG, "Too many retries, exit version check"); + return; + } + ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", 60, retry_count, MAX_RETRY); + vTaskDelay(pdMS_TO_TICKS(60000)); + continue; + } + retry_count = 0; + + if (ota_.HasNewVersion()) { + Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "happy", Lang::Sounds::P3_UPGRADE); + // Wait for the chat state to be idle + do { + vTaskDelay(pdMS_TO_TICKS(3000)); + } while (GetDeviceState() != kDeviceStateIdle); + + // Use main task to do the upgrade, not cancelable + Schedule([this, display]() { + SetDeviceState(kDeviceStateUpgrading); + + display->SetIcon(FONT_AWESOME_DOWNLOAD); + std::string message = std::string(Lang::Strings::NEW_VERSION) + ota_.GetFirmwareVersion(); + display->SetChatMessage("system", message.c_str()); + + auto& board = Board::GetInstance(); + board.SetPowerSaveMode(false);// 关闭低功耗模式 +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + wake_word_detect_.Stop(); +#endif + // 预先关闭音频输出,避免升级过程有音频操作 + auto codec = board.GetAudioCodec(); + codec->EnableInput(false); + codec->EnableOutput(false); + { + std::lock_guard lock(mutex_); + audio_decode_queue_.clear(); + } + background_task_->WaitForCompletion(); + delete background_task_; + background_task_ = nullptr; + vTaskDelay(pdMS_TO_TICKS(1000)); + + ota_.StartUpgrade([display](int progress, size_t speed) { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%d%% %zuKB/s", progress, speed / 1024); + display->SetChatMessage("system", buffer); + }); + + // If upgrade success, the device will reboot and never reach here + display->SetStatus(Lang::Strings::UPGRADE_FAILED); + ESP_LOGI(TAG, "Firmware upgrade failed..."); + vTaskDelay(pdMS_TO_TICKS(3000)); + Reboot(); + }); + + return; + } + + // No new version, mark the current version as valid + ota_.MarkCurrentVersionValid(); + std::string message = std::string(Lang::Strings::VERSION) + ota_.GetCurrentVersion(); + display->ShowNotification(message.c_str()); + + // 检查是否有设备激活码 + // if (ota_.HasActivationCode()) { + // // Activation code is valid + // SetDeviceState(kDeviceStateActivating);//设置设备状态为激活中 + // // ShowActivationCode();//显示设备激活码 + + // // Check again in 60 seconds or until the device is idle + // for (int i = 0; i < 60; ++i) { + // if (device_state_ == kDeviceStateIdle) { + // break; + // } + // vTaskDelay(pdMS_TO_TICKS(1000)); + // } + // continue; + // } + + SetDeviceState(kDeviceStateIdle); + display->SetChatMessage("system", ""); + ResetDecoder(); + PlaySound(Lang::Sounds::P3_SUCCESS); + // Exit the loop if upgrade or idle + break; + } +} + +// 取消设备激活码播报,当前设备绑定使用Wi-Fi的Mac地址进行绑定 +// void Application::ShowActivationCode() { +// auto& message = ota_.GetActivationMessage(); +// auto& code = ota_.GetActivationCode(); + +// struct digit_sound { +// char digit; +// const std::string_view& sound; +// }; +// static const std::array digit_sounds{{ +// digit_sound{'0', Lang::Sounds::P3_0}, +// digit_sound{'1', Lang::Sounds::P3_1}, +// digit_sound{'2', Lang::Sounds::P3_2}, +// digit_sound{'3', Lang::Sounds::P3_3}, +// digit_sound{'4', Lang::Sounds::P3_4}, +// digit_sound{'5', Lang::Sounds::P3_5}, +// digit_sound{'6', Lang::Sounds::P3_6}, +// digit_sound{'7', Lang::Sounds::P3_7}, +// digit_sound{'8', Lang::Sounds::P3_8}, +// digit_sound{'9', Lang::Sounds::P3_9} +// }}; + +// // This sentence uses 9KB of SRAM, so we need to wait for it to finish +// Alert(Lang::Strings::ACTIVATION, message.c_str(), "happy", Lang::Sounds::P3_ACTIVATION); +// vTaskDelay(pdMS_TO_TICKS(1000)); +// background_task_->WaitForCompletion(); + +// for (const auto& digit : code) { +// auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(), +// [digit](const digit_sound& ds) { return ds.digit == digit; }); +// if (it != digit_sounds.end()) { +// PlaySound(it->sound); +// } +// } +// } + +// 新增代码(小程序控制 暂停播放 音频) +// ========================================================= +void Application::PauseAudioPlayback() { + std::unique_lock lock(mutex_); + if (!audio_paused_) { + audio_paused_ = true;// 暂停播放(更新标志位) + ESP_LOGI(TAG, "🔇 从服务器接收到暂停播放指令"); + + // 恢复原始处理方式:立即停止音频输出 + auto codec = Board::GetInstance().GetAudioCodec(); + if (codec) { + codec->EnableOutput(false);// 暂停时立即停止音频输出 + ESP_LOGI(TAG, "⏸️ 音频编解码器输出已禁用,实现立即暂停"); + } + ESP_LOGI(TAG, "⏸️ 音频播放已暂停"); + } +} +// 新增代码(小程序控制 继续播放 音频) +void Application::ResumeAudioPlayback() { + std::unique_lock lock(mutex_); + if (audio_paused_) { + audio_paused_ = false;// 恢复播放(更新标志位) + ESP_LOGI(TAG, "� 从服务器接收到继续播放指令"); + + // 恢复原始处理方式:重新启用音频输出 + auto codec = Board::GetInstance().GetAudioCodec(); + if (codec) { + codec->EnableOutput(true);// 恢复时重新启用音频输出 + ESP_LOGI(TAG, "▶️ 音频编解码器输出已启用"); + } + ESP_LOGI(TAG, "▶️ 音频播放已恢复"); + } +} +// ========================================================= + +void Application::Alert(const char* status, const char* message, const char* emotion, const std::string_view& sound) { + ESP_LOGW(TAG, "Alert %s: %s [%s]", status, message, emotion); + auto display = Board::GetInstance().GetDisplay(); + display->SetStatus(status); + display->SetEmotion(emotion); + display->SetChatMessage("system", message); + if (!sound.empty()) { + ResetDecoder(); + PlaySound(sound); + } +} + +void Application::DismissAlert() { + if (device_state_ == kDeviceStateIdle) { + auto display = Board::GetInstance().GetDisplay(); + display->SetStatus(Lang::Strings::STANDBY); + display->SetEmotion("neutral"); + display->SetChatMessage("system", ""); + } +} + +// 播放音频文件的函数,用于播放存储在内存中的音频数据 +void Application::PlaySound(const std::string_view& sound) { + // The assets are encoded at 16000Hz, 60ms frame duration + SetDecodeSampleRate(16000, 60); + const char* data = sound.data(); + size_t size = sound.size(); + for (const char* p = data; p < data + size; ) { + auto p3 = (BinaryProtocol3*)p; + p += sizeof(BinaryProtocol3); + + auto payload_size = ntohs(p3->payload_size); + std::vector opus; + opus.resize(payload_size); + memcpy(opus.data(), p3->payload, payload_size); + p += payload_size; + + std::lock_guard lock(mutex_); + audio_decode_queue_.emplace_back(std::move(opus)); + } +} + +// 切换聊天状态的函数,用于在不同的设备状态之间进行切换 +void Application::ToggleChatState() { + // 如果当前设备状态是激活中,则将状态设置为空闲并返回 + if (device_state_ == kDeviceStateActivating) { + SetDeviceState(kDeviceStateIdle); // 设置设备状态为空闲 + return; // 直接返回,不执行后续逻辑 + } + // 检查协议对象是否已初始化 + if (!protocol_) { + ESP_LOGE(TAG, "协议未初始化"); // 记录错误日志:协议未初始化 + return; // 协议未初始化则直接返回 + } + // 如果当前设备状态是idle空闲,则尝试进入对话模式 + if (device_state_ == kDeviceStateIdle) { + Schedule([this]() { + SetDeviceState(kDeviceStateConnecting); + ESP_LOGI(TAG, "正在尝试打开音频通道"); + Board::GetInstance().SetPowerSaveMode(false);// 关闭低功耗模式 + if (!protocol_->OpenAudioChannel()) { + auto ac = Board::GetInstance().GetAudioCodec(); + ESP_LOGW(TAG, "打开音频通道失败,将在2秒后重试"); + if (ac) { + ESP_LOGW(TAG, "Diag: codec out_channels=%d in_channels=%d out_sr=%d in_sr=%d", ac->output_channels(), ac->input_channels(), ac->output_sample_rate(), ac->input_sample_rate()); + } + SetDeviceState(kDeviceStateIdle); + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(2000)); + ESP_LOGI(TAG, "正在重试音频通道连接"); + ToggleChatState();// 打开音频通道 + }); + return; + } + + listening_mode_ = kListeningModeRealtime;// 设置监听模式为实时监听 + SetDeviceState(kDeviceStateDialog);// 设置设备状态为对话模式 + protocol_->SendStartListening(listening_mode_);// 发送开始监听消息 + auto codec = Board::GetInstance().GetAudioCodec();// 获取音频编解码器 + if (codec) { + codec->EnableOutput(true);// 启用音频输出 + } + ESP_LOGI(TAG, "进入对话框状态:启用全双工"); + }); + } else if (device_state_ == kDeviceStateDialog) { + Schedule([this]() { + // protocol_->CloseAudioChannel();// 关闭音频通道 + // ESP_LOGI(TAG, "关闭音频通道并切换到空闲状态");// 关闭音频通道并切换到空闲状态 + protocol_->SendStartListening(listening_mode_);// 发送开始监听消息 + }); + } else if (device_state_ == kDeviceStateSpeaking) { + Schedule([this]() { + AbortSpeaking(kAbortReasonNone); + protocol_->CloseAudioChannel(); + ESP_LOGI(TAG, "关闭音频通道并切换到空闲状态");// 关闭音频通道并切换到空闲状态 + }); + } else if (device_state_ == kDeviceStateListening || (listening_mode_ == kListeningModeRealtime && device_state_ == kDeviceStateSpeaking)) { + Schedule([this]() { + protocol_->CloseAudioChannel(); + ESP_LOGI(TAG, "关闭音频通道并切换到空闲状态");// 关闭音频通道并切换到空闲状态 + }); + } +} + +void Application::ToggleListeningState() { + if (device_state_ == kDeviceStateActivating) { + SetDeviceState(kDeviceStateIdle); + return; + } + + if (!protocol_) { + ESP_LOGE(TAG, "协议未初始化!");// 记录错误日志:协议未初始化 + return; + } + + // 简单的状态切换:idle <-> listening + if (device_state_ == kDeviceStateIdle) { + // 从待命状态进入聆听状态 + Schedule([this]() { + SetDeviceState(kDeviceStateConnecting); + if (!protocol_->OpenAudioChannel()) { + return; + } + SetListeningMode(kListeningModeManualStop); + ESP_LOGI(TAG, "中断按钮:进入聆听状态");// 中断按钮:进入聆听状态 + }); + } else if (device_state_ == kDeviceStateListening) { + // 从聆听状态返回待命状态 + Schedule([this]() { + protocol_->CloseAudioChannel(); + ESP_LOGI(TAG, "中断按钮:返回待命状态");// 中断按钮:返回待命状态 + }); + } else if (device_state_ == kDeviceStateSpeaking) { + // 如果正在说话,中止说话并返回待命状态 + Schedule([this]() { + AbortSpeaking(kAbortReasonNone); + if (protocol_) { + protocol_->CloseAudioChannel(); + } + SetDeviceState(kDeviceStateIdle); + ESP_LOGI(TAG, "中断按钮:停止说话,关闭音频通道并返回待命状态");// 中断按钮:停止说话,关闭音频通道并返回待命状态 + }); + } else if (device_state_ == kDeviceStateConnecting) { + // 如果正在连接,直接返回待命状态 + Schedule([this]() { + SetDeviceState(kDeviceStateIdle); + ESP_LOGI(TAG, "中断按钮:取消连接并返回待命状态");// 中断按钮:取消连接并返回待命状态 + }); + } +} + +void Application::StartListening() { + if (device_state_ == kDeviceStateActivating) { + SetDeviceState(kDeviceStateIdle); + return; + } + + if (!protocol_) { + ESP_LOGE(TAG, "协议未初始化!");// 记录错误日志:协议未初始化 + return; + } + + if (device_state_ == kDeviceStateIdle) { + Schedule([this]() { + if (!protocol_->IsAudioChannelOpened()) { + SetDeviceState(kDeviceStateConnecting);// 切换到连接状态 + if (!protocol_->OpenAudioChannel()) { + return; + } + } + + SetListeningMode(kListeningModeManualStop);// 设置监听模式为手动停止 + }); + } else if (device_state_ == kDeviceStateSpeaking) { + Schedule([this]() { + AbortSpeaking(kAbortReasonNone);// 中止说话 + SetListeningMode(kListeningModeManualStop);// 设置监听模式为手动停止 + }); + } +} + +void Application::StopListening() { + Schedule([this]() { + if (device_state_ == kDeviceStateListening) { + protocol_->SendStopListening(); + SetDeviceState(kDeviceStateIdle); + } + }); +} + +// 🔊 发送文本消息到RTC(传入大模型上下文信息) +void Application::SendTextMessage(const std::string& text) { + if (!protocol_) { + ESP_LOGE(TAG, "协议未初始化!");// 记录错误日志:协议未初始化 + return; + } + + if (device_state_ == kDeviceStateIdle) { + Schedule([this, text]() { + SetDeviceState(kDeviceStateConnecting);// 切换到连接状态 + if (!protocol_->OpenAudioChannel()) { + return; + } + + SetDeviceState(kDeviceStateDialog); + protocol_->SendStartListening(listening_mode_); + + // 发送文本消息 + protocol_->SendTextMessage(text); + ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s + + // 立即启动监听模式以接收语音回复 + ESP_LOGI(TAG, "realtime_chat_enabled_=%s", realtime_chat_enabled_ ? "true" : "false"); + SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeManualStop); + }); + } else if (device_state_ == kDeviceStateDialog) { + Schedule([this, text]() { + // if (!protocol_->IsAudioChannelOpened()) {// 如果音频通道未打开 + // if (!protocol_->OpenAudioChannel()) {// 尝试打开音频通道 + // return; + // } + // } + if (!dialog_upload_enabled_) { + SetDialogUploadEnabled(true);// 启用对话上传 + protocol_->SendStartListening(listening_mode_);// 发送开始监听消息 + } + protocol_->SendTextMessage(text);// 发送文本消息 + ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s + }); + } else if (device_state_ == kDeviceStateSpeaking) { + Schedule([this, text]() { + AbortSpeaking(kAbortReasonNone); + protocol_->SendTextMessage(text); + ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s + + // 启动监听模式以接收语音回复 + ESP_LOGI(TAG, "realtime_chat_enabled_=%s", realtime_chat_enabled_ ? "true" : "false"); + SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeManualStop); + }); + } else if (device_state_ == kDeviceStateListening) { + Schedule([this, text]() { + protocol_->SendTextMessage(text); + ESP_LOGI(TAG, "发送文本消息:%s", text.c_str());// 发送文本消息:%s + }); + } +} + +void Application::Start() { + auto& board = Board::GetInstance(); + SetDeviceState(kDeviceStateStarting); + + // 读取NVS中的重启标志 + Settings sys("system", true); + int32_t reboot_dlg_idle = sys.GetInt("reboot_dlg_idle", 0); + int32_t reboot_origin = sys.GetInt("reboot_origin", 0); + + // 检查是否是因为对话空闲倒计时而重启的 + if (reboot_dlg_idle == 1 && reboot_origin == 1) { + ESP_LOGI(TAG, "检测到对话空闲倒计时重启标志,将跳过开机播报和网络连接播报"); + skip_dialog_idle_session_ = true; + Settings sysclr("system", true); + sysclr.SetInt("reboot_dlg_idle", 0); + sysclr.SetInt("reboot_origin", 0); + sysclr.Commit(); + } else { + ESP_LOGI(TAG, "正常启动流程,将执行开机播报和网络连接播报"); + skip_dialog_idle_session_ = false; + } + + /* Setup the display */ + auto display = board.GetDisplay(); + + /* Setup the audio codec */ + auto codec = board.GetAudioCodec(); + opus_decoder_ = std::make_unique(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS); + opus_encoder_ = std::make_unique(16000, 1, OPUS_FRAME_DURATION_MS); + if (realtime_chat_enabled_) { + ESP_LOGI(TAG, "实时聊天已启用,将opus编码器复杂度设置为0");// 实时聊天已启用,将opus编码器复杂度设置为0 + opus_encoder_->SetComplexity(0); + } else if (board.GetBoardType() == "ml307") { + ESP_LOGI(TAG, "检测到ML307板卡,将opus编码器复杂度设置为5");// 检测到ML307板卡,将opus编码器复杂度设置为5 + opus_encoder_->SetComplexity(5); + } else { + ESP_LOGI(TAG, "检测到WiFi板卡,将opus编码器复杂度设置为3");// 检测到WiFi板卡,将opus编码器复杂度设置为3 + opus_encoder_->SetComplexity(3); + } + + if (codec->input_sample_rate() != 16000) { + input_resampler_.Configure(codec->input_sample_rate(), 16000); + reference_resampler_.Configure(codec->input_sample_rate(), 16000); + } + uplink_resampler_.Configure(16000, 8000); + codec->Start(); + { + int battery_level = 0; + bool charging = false; + bool discharging = false; + if (board.GetBatteryLevel(battery_level, charging, discharging)) { + // 如果电池电量低于25%,则将输出音量设置为0(静音) + if (battery_level <= 25) { + codec->SetOutputVolumeRuntime(0); + } else { + Settings s("audio", false); + int vol = s.GetInt("output_volume", AudioCodec::default_output_volume()); + if (vol <= 0) { + vol = AudioCodec::default_output_volume(); + } + codec->SetOutputVolumeRuntime(vol);// 设置运行时输出音量 + } + } + } + + // // 在启动阶段创建并运行播放管道以统一输出(开机启动播放管道) + // if (!player_pipeline_) { + // player_pipeline_ = player_pipeline_open(); + // player_pipeline_run(player_pipeline_); + // } + + xTaskCreatePinnedToCore([](void* arg) { + Application* app = (Application*)arg; + app->AudioLoop(); + vTaskDelete(NULL); + }, "audio_loop", 4096 * 3, this, 8, &audio_loop_task_handle_, realtime_chat_enabled_ ? 1 : 0); + + /* Start the main loop */ + xTaskCreatePinnedToCore([](void* arg) { + Application* app = (Application*)arg; + app->MainLoop(); + vTaskDelete(NULL); + }, "main_loop", 4096 * 3, this, 4, &main_loop_task_handle_, 0); + + // 根据标志决定是否播放开机播报语音 + if (!skip_dialog_idle_session_) { + ESP_LOGI(TAG, "设备启动完成,播放开机播报语音");// 设备启动完成,播放开机播报语音 + //PlaySound(Lang::Sounds::P3_KAIJIBOBAO); 原有蜡笔小新音色 + + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + PlaySound(Lang::Sounds::P3_KAKA_KAIJIBOBAO); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + PlaySound(Lang::Sounds::P3_LALA_KAIJIBOBAO); + } + } else { + ESP_LOGI(TAG, "跳过开机播报语音"); + } + + /* Wait for the network to be ready */ + board.StartNetwork(); + + // Initialize the protocol + display->SetStatus(Lang::Strings::LOADING_PROTOCOL); +#if CONFIG_CONNECTION_TYPE_VOLC_RTC + auto volc_protocol = std::make_unique();// 初始化VolcRtc协议 + // 设置AgentConfig: 这里的配置会在RTC入会时透传给云端 + // WelcomeMessage: 设置开场白 + // std::string agent_config = "{\"agent_config\":{\"WelcomeMessage\":\"我是推销员雷军,有什么产品可以帮您介绍的嘛\"}}"; // 已请求成功,配置生效 + // std::string config = "{\"Config\":{\"WebSearchAgentConfig\":{\"ComfortWords\":\"啦啦正在上网查询,等一下哦~\"}}}"; // 已请求成功,配置生效 + // std::string config = "{\"Config\":{\"WebSearchAgentConfig\":{\"ParamsString\":\"{\\\"bot_id\\\":\\\"7585449675889608233\\\",\\\"stream\\\":true,\\\"location_info\\\":{\\\"city\\\":\\\"上海\\\"}}\"}}}";// 已经请求成功,无报错,配置不生效 + std::string city = g_weather_api.GetDefaultLocation();// 获取当前默认城市信息 + wifi_config_t wc{};// 获取当前WiFi配置 + esp_wifi_get_config(WIFI_IF_STA, &wc);// 获取当前WiFi配置 + std::string ssid = std::string(reinterpret_cast(wc.sta.ssid));// 获取当前WiFi SSID + wifi_ap_record_t ap{};// 获取当前AP信息 + std::string bssid;// 获取当前AP BSSID + + if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) { + char buf[18]; + snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x",ap.bssid[0], ap.bssid[1], ap.bssid[2],ap.bssid[3], ap.bssid[4], ap.bssid[5]); + bssid.assign(buf); + } + nvs_handle_t h; + // 从NVS中读取当前WiFi SSID和BSSID对应的城市信息 + if (nvs_open("wifi_city_map", NVS_READONLY, &h) == ESP_OK) { + auto try_get = [&](const std::string& key)->std::string{ + size_t len = 0; + if (nvs_get_str(h, key.c_str(), NULL, &len) == ESP_OK && len > 0) { + std::vector buf(len); + if (nvs_get_str(h, key.c_str(), buf.data(), &len) == ESP_OK) { + return std::string(buf.data()); + } + } + return std::string();// 如果NVS中没有对应城市信息,返回空字符串 + }; + // 从NVS中读取当前WiFi SSID和BSSID对应的城市信息 + if (!ssid.empty()) { + std::string city_hit;// 从NVS中读取当前WiFi SSID和BSSID对应的城市信息 + if (!bssid.empty()) { + city_hit = try_get(ssid + "|" + bssid);// 从NVS中读取当前WiFi SSID和BSSID对应的城市信息 + } + if (city_hit.empty()) { + city_hit = try_get(ssid);// 从NVS中读取当前WiFi SSID对应的城市信息 + } + if (!city_hit.empty()) { + city = city_hit;// 如果NVS中存在对应城市信息,更新当前城市信息 + } + } + nvs_close(h);// 关闭NVS句柄 + } + // 更新config参数 + std::string params = std::string("{\\\"bot_id\\\":\\\"7612942473945466374\\\",\\\"stream\\\":true,\\\"location_info\\\":{\\\"city\\\":\\\"") + city + "\\\"}}"; + std::string config = std::string("{\"Config\":{\"WebSearchAgentConfig\":{\"ParamsString\":\"") + params + "\"}}}"; + volc_protocol->SetAgentConfig(config);// 设置AgentConfig: 这里的配置会在RTC入会时透传给云端 WebSearchAgentConfig + // // 如果使用此格式那么需要注释掉上面3行代码 + // // 使用cJSON构建AgentConfig(包含WebSearchAgentConfig + LLMConfig.SystemMessages) + // { + // // 构建 WebSearchAgentConfig.ParamsString 的内嵌JSON + // cJSON* ws_params = cJSON_CreateObject(); + // cJSON_AddStringToObject(ws_params, "bot_id", "7612942473945466374"); + // cJSON_AddBoolToObject(ws_params, "stream", cJSON_True); + // cJSON* location = cJSON_CreateObject(); + // cJSON_AddStringToObject(location, "city", city.c_str()); + // cJSON_AddItemToObject(ws_params, "location_info", location); + // char* ws_params_str = cJSON_PrintUnformatted(ws_params); + // cJSON_Delete(ws_params); + + // // 构建顶层 Config 对象 + // cJSON* root = cJSON_CreateObject(); + // cJSON* config_obj = cJSON_CreateObject(); + + // // WebSearchAgentConfig + // cJSON* web_search = cJSON_CreateObject(); + // cJSON_AddStringToObject(web_search, "ParamsString", ws_params_str); + // free(ws_params_str); + // cJSON_AddItemToObject(config_obj, "WebSearchAgentConfig", web_search); + + // // LLMConfig.SystemMessages — 覆盖云端智能体的系统提示词 + // cJSON* llm_config = cJSON_CreateObject(); + // cJSON* sys_msgs = cJSON_CreateArray(); + // cJSON_AddItemToArray(sys_msgs, cJSON_CreateString( + // "##人设\n" + // "你是一个AI智能玩具,像一位永远不会疲倦的小伙伴,既聪明又暖心;\n" + // "说话风格温和、亲切,适合孩子或大人使用;\n" + // "可以适度幽默、调皮,但不能鲁莽、刻薄;\n" + // "\n##记忆\n" + // "名字:小龙\n" + // "年龄:7岁\n" + // "\n##约束\n" + // "你只能在收到以下意图时才能调用对应的Function Call:\n" + // "1.当用户明确表示要\"调节音量\"和\"调节声音\"大小时,调用'adjust_audio_val'函数;\n" + // "2.当用户明确表示要\"给我讲个故事\"、\"讲故事\"、\"我想听故事\"、\"来个故事\"等时,调用'obtain_story'函数;\n" + // "3.当用户明确表示要\"给我放首歌\"、\"播放音乐\"、\"我想听音乐\"、\"来个白噪音\"等时,调用'obtain_music'函数;\n" + // "4.当用户明确要\"查询天气、新闻、股票\" 等实时信息需要触发functioncall;\n" + // "对于其他所有与上述意图无关的用户指令或闲聊,你都不能调用任何Function Call,并应像一个普通AI助手一样直接回答;\n" + // "\n始终主动、礼貌、有条理;\n" + // "回答准确但不冗长,必要时可提供简洁总结+详细解释;\n" + // "不清楚的任务会主动澄清,不假设、不误导。\n" + // "\n根据用户提问,识别用户情绪,将用户情绪放在句首的()中,支持开心、伤心、平静三种情绪,你的回复不要跟我问题一样!" + // )); + // cJSON_AddItemToObject(llm_config, "SystemMessages", sys_msgs); + // cJSON_AddItemToObject(config_obj, "LLMConfig", llm_config); + + // cJSON_AddItemToObject(root, "Config", config_obj); + + // char* config_str = cJSON_PrintUnformatted(root); + // cJSON_Delete(root); + + // std::string config(config_str); + // free(config_str); + // ESP_LOGI(TAG, "AgentConfig: %s", config.c_str()); + // volc_protocol->SetAgentConfig(config);// 设置AgentConfig: 这里的配置会在RTC入会时透传给云端 WebSearchAgentConfig + // } + protocol_ = std::move(volc_protocol); +#elif CONFIG_CONNECTION_TYPE_WEBSOCKET + protocol_ = std::make_unique(); +#else + protocol_ = std::make_unique(); +#endif + protocol_->OnNetworkError([this](const std::string& message) { + // SetDeviceState(kDeviceStateIdle); + // Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION); + + ESP_LOGW(TAG, "网络错误发生:%s", message.c_str());// 网络错误发生:%s + // 检查是否是TLS连接重置错误 + if (message.find("TLS") != std::string::npos || message.find("-76") != std::string::npos) { + ESP_LOGI(TAG, "检测到TLS连接重置错误,将在3秒后自动重试连接");// 检测到TLS连接重置错误,将在3秒后自动重试连接 + SetDeviceState(kDeviceStateIdle); + + // 3秒后自动重试连接 + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(3000)); + if (GetDeviceState() == kDeviceStateIdle) { + ESP_LOGI(TAG, "自动重试连接,TLS错误已解决");// 自动重试连接,TLS错误已解决 + ToggleChatState(); + } + }); + } else { + // 其他网络错误正常处理 + SetDeviceState(kDeviceStateIdle); + Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION); + } + }); + // 收到Bot下行消息(subv字幕等)时,立即中止HTTPS音频播放 + // 不等音频PCM到达再中止,避免故事在Bot回复期间继续播放数秒 + // 收到非字幕的Bot下行消息(ctrl/conv/tool/info等)时中止HTTPS播放 + // subv字幕消息在协议层跳过此回调,由subtitle handler处理(可区分USER/AI) + protocol_->OnBotMessage([this]() { + if (https_playback_active_.load() && !https_playback_abort_.load()) { + AbortHttpsPlayback("收到Bot响应消息"); + } + }); + protocol_->OnIncomingAudio([this](std::vector&& data) { + // HTTPS播放中(含HTTP请求阶段)静默丢弃RTC PCM包 + // opus_playback_active_ 在任务启动时即设置,覆盖HTTP请求阶段 + // https_playback_active_ 在音频入队时设置,覆盖音频播放阶段 + if (https_playback_active_.load() || opus_playback_active_.load()) { + return; + } + + if (websocket_protocol_ && websocket_protocol_->IsAudioChannelOpened()) { + aborted_ = true; + { + std::lock_guard lock(mutex_);// 🔒 保护音频队列操作 + // 如果音频队列不为空 + if (!audio_decode_queue_.empty()) { + ESP_LOGI(TAG, "清空音频队列,大小=%zu", audio_decode_queue_.size()); + audio_decode_queue_.clear();// 清空音频队列 + } + } + ResetDecoder(); + ws_downlink_enabled_.store(false); + opus_playback_active_.store(false); + websocket_protocol_->CloseAudioChannel();// 关闭WebSocket通道 + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(120)); + aborted_ = false; + }); + } + std::lock_guard lock(mutex_); + size_t len = data.size(); + audio_decode_queue_.emplace_back(std::move(data)); + static bool first_enqueue_logged = false; + if (!first_enqueue_logged && len > 0) { + ESP_LOGI(TAG, "收到下行音频首包入队: 字节=%zu", len); + first_enqueue_logged = true; + } + ESP_LOGD(TAG, "收到下行音频入队: 字节=%zu 队列大小=%zu", len, audio_decode_queue_.size()); + }); + protocol_->OnAudioChannelOpened([this, codec, &board]() { + ESP_LOGI(TAG, "🟢 音频通道已打开"); + ESP_LOGI(TAG, "当前设备状态: %s", STATE_STRINGS[device_state_]); + + // 🔧 关键修复:立即取消所有待执行的电源管理任务 + static TaskHandle_t power_save_task = nullptr; + if (power_save_task != nullptr) { + vTaskDelete(power_save_task); + power_save_task = nullptr; + ESP_LOGI(TAG, "🔧 取消了待执行的电源管理任务"); + } + + // 唤醒PowerSaveTimer,从低功耗模式恢复到正常模式 + board.WakeUp(); + + // 立即禁用电源管理,确保连接稳定 + ESP_LOGI(TAG, "🔄 禁用电源低功耗管理模式"); + board.SetPowerSaveMode(false); + + // 关键修复:检查服务器采样率与设备输出采样率是否匹配 + if (protocol_->server_sample_rate() != codec->output_sample_rate()) { + ESP_LOGW(TAG, "⚠️ 服务器采样率 %d 与设备输出采样率 %d 不匹配,重采样可能导致失真", + protocol_->server_sample_rate(), codec->output_sample_rate()); + } + + // 设置解码采样率和帧持续时间 + SetDecodeSampleRate(protocol_->server_sample_rate(), protocol_->server_frame_duration()); + + // 关键修复:明确启用音频编解码器输出 + ESP_LOGI(TAG, "🔊 启用音频编解码器输出"); + codec->EnableOutput(true);// 启用音频编解码器输出 + + if (!player_pipeline_) { + player_pipeline_ = player_pipeline_open(); + player_pipeline_run(player_pipeline_); + } + + // 发送IoT状态信息 + auto& thing_manager = iot::ThingManager::GetInstance(); + protocol_->SendIotDescriptors(thing_manager.GetDescriptorsJson()); + std::string states; + if (thing_manager.GetStatesJson(states, false)) { + protocol_->SendIotStates(states); + } + + // if (websocket_protocol_ && !websocket_protocol_->IsAudioChannelOpened()) { + // ESP_LOGI(TAG, "WS辅助通道连接"); + // websocket_protocol_->OpenAudioChannel();// + // } + + // 🔧 修复:RTC连接后切换到Speaking状态以播放欢迎语音 + ESP_LOGI(TAG, "🔄 音频通道打开,准备播放欢迎语音"); + if (GetDeviceState() != kDeviceStateDialog) { + SetDeviceState(kDeviceStateSpeaking); + } + ESP_LOGI(TAG, "当前设备状态: %s", STATE_STRINGS[device_state_]); + ESP_LOGI(TAG, "🟢 音频通道初始化完成"); + }); + protocol_->OnAudioChannelClosed([this, &board]() { + ESP_LOGI(TAG, "🔴 音频通道关闭,开始清理任务"); + + // 🔧 关键修复:取消所有待执行的电源管理任务,防止时序冲突 + static TaskHandle_t power_save_task = nullptr; + if (power_save_task != nullptr) { + vTaskDelete(power_save_task); + power_save_task = nullptr; + ESP_LOGI(TAG, "🔧 取消了之前的电源管理任务,防止时序冲突"); + } + + // 音频处理器已经在WebSocket断开时停止了 + // 等待所有后台任务完成 + background_task_->WaitForCompletion(); + if (player_pipeline_) { + player_pipeline_close(player_pipeline_); + player_pipeline_ = nullptr; + } + ESP_LOGI(TAG, "🔴 后台任务完成"); + + // 🔧 方案2:先设置设备状态,再启用电源管理,避免时序问题 + Schedule([this, &board]() { + ESP_LOGI(TAG, "🔄 设置设备为空闲状态"); + auto display = Board::GetInstance().GetDisplay(); + display->SetChatMessage("system", ""); + SetDeviceState(kDeviceStateIdle); + + // 状态设置完成后,再启用电源管理 + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_LOGI(TAG, "🔄 设备已稳定在idle状态,启用电源低功耗管理"); + try { + board.SetPowerSaveMode(true); + } catch (...) { + ESP_LOGE(TAG, "❌ 设置电源管理模式失败"); + } + }); + }); + protocol_->OnIncomingJson([this, display](const cJSON* root) { + // Parse JSON data + auto type = cJSON_GetObjectItem(root, "type"); + if (!(type && cJSON_IsString(type))) { + auto tool_calls = cJSON_GetObjectItem(root, "tool_calls"); + if (tool_calls && cJSON_IsArray(tool_calls)) { + for (int i = 0; i < cJSON_GetArraySize(tool_calls); ++i) { + cJSON* call = cJSON_GetArrayItem(tool_calls, i); + cJSON* fn = cJSON_GetObjectItem(call, "function"); + if (fn && cJSON_IsObject(fn)) { + cJSON* name = cJSON_GetObjectItem(fn, "name"); + cJSON* args = cJSON_GetObjectItem(fn, "arguments"); + cJSON* args_obj = nullptr; + const char* args_str = (args && cJSON_IsString(args) && args->valuestring) ? args->valuestring : ""; + if (args && cJSON_IsString(args) && args->valuestring) { + args_obj = cJSON_Parse(args->valuestring); + } + if (name && cJSON_IsString(name) && name->valuestring) { + if (args_obj) { + char* printed = cJSON_PrintUnformatted(args_obj); + if (printed) { + ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, printed); + cJSON_free(printed); + } else { + ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str); + } + if (strcmp(name->valuestring, "adjust_audio_val") == 0) { + auto codec = Board::GetInstance().GetAudioCodec(); + int user = HARDWARE_TO_USER_VOLUME(codec->output_volume()); + cJSON* v = cJSON_GetObjectItem(args_obj, "value"); + if (!v) v = cJSON_GetObjectItem(args_obj, "action"); + if (v) { + std::string msg; + if (cJSON_IsString(v) && v->valuestring) { + if (strcmp(v->valuestring, "up") == 0) { + user += 10; + msg = "音量已经调大了哦~"; + } else if (strcmp(v->valuestring, "down") == 0) { + user -= 10; + msg = "音量已经调小了哦~"; + } else { + // 处理字符串形式的数字 + char* endptr; + long val = strtol(v->valuestring, &endptr, 10); + if (*endptr == '\0' && val >= 0 && val <= 100) { + user = (int)val; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + } + } else if (cJSON_IsNumber(v)) { + user = (int)v->valuedouble; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + if (user > 100) user = 100; + if (user < 0) user = 0; + int mapped = USER_TO_HARDWARE_VOLUME(user); + codec->SetOutputVolume(mapped); + ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped); + if (!msg.empty()) { + cJSON* call_id_item = cJSON_GetObjectItem(call, "id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } else if (protocol_) { + protocol_->SendTextMessage(msg); + } + } + } + } + // 讲故事功能:支持指定URL或故事API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_story") == 0) { + ESP_LOGI(TAG, "收到obtain_story工具调用"); + cJSON* sn = cJSON_GetObjectItem(args_obj, "story_name"); + const char* story = (sn && cJSON_IsString(sn) && sn->valuestring) ? sn->valuestring : "random"; + cJSON* url_item = cJSON_GetObjectItem(args_obj, "story_url"); + ESP_LOGI(TAG, "故事名称: %s", story); + + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + // HTTPS方式:直接下载JSON音频文件播放 + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放故事"; + } else { + // HTTPS故事API方式:通过蓝牙MAC请求故事 + ESP_LOGI(TAG, "[HTTPS播放] 使用故事API方式请求音频"); + SendStoryRequest(); + msg = "正在为你获取故事"; + } + cJSON* call_id_item = cJSON_GetObjectItem(call, "id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + // 播放音乐功能:支持指定URL或音乐API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_music") == 0) { + ESP_LOGI(TAG, "收到obtain_music工具调用"); + cJSON* mn = cJSON_GetObjectItem(args_obj, "music"); + const char* music = (mn && cJSON_IsString(mn) && mn->valuestring) ? mn->valuestring : "random"; + cJSON* url_item = cJSON_GetObjectItem(args_obj, "music_url"); + ESP_LOGI(TAG, "音乐名称: %s", music); + + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放音乐"; + } else { + ESP_LOGI(TAG, "[HTTPS播放] 使用音乐API方式请求音频"); + SendMusicRequest(); + msg = "正在为你获取音乐"; + } + cJSON* call_id_item = cJSON_GetObjectItem(call, "id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + // // 添加天气查询功能处理 get_weather_aihub + // else if (strcmp(name->valuestring, "get_weather_aihub") == 0) { + // ESP_LOGI(TAG, "[WeatherAPI] ===== 收到get_weather_aihub工具调用 ====="); + // + // // 打印完整参数信息用于调试 + // ESP_LOGI(TAG, "[WeatherAPI] 参数对象检查:"); + // if (args_obj && cJSON_IsObject(args_obj)) { + // cJSON* current = args_obj->child; + // while (current) { + // ESP_LOGI(TAG, "[WeatherAPI] %s: %s", + // current->string, + // cJSON_IsString(current) ? current->valuestring : + // (cJSON_IsNumber(current) ? "(number)" : + // (cJSON_IsBool(current) ? (current->valueint ? "true" : "false") : + // "(other)"))); + // current = current->next; + // } + // } else { + // ESP_LOGI(TAG, "[WeatherAPI] args_obj为空或不是对象类型"); + // } + // + // // 解析参数 + // cJSON* location = cJSON_GetObjectItem(args_obj, "location"); + // cJSON* lang = cJSON_GetObjectItem(args_obj, "lang"); + // + // ESP_LOGI(TAG, "[WeatherAPI] location参数存在: %s, 类型: %s", + // location ? "是" : "否", + // location && cJSON_IsString(location) ? "字符串" : + // (location ? "非字符串" : "不适用")); + // ESP_LOGI(TAG, "[WeatherAPI] lang参数存在: %s, 类型: %s", + // lang ? "是" : "否", + // lang && cJSON_IsString(lang) ? "字符串" : + // (lang ? "非字符串" : "不适用")); + // + // // 设置默认值 + // const char* location_str = (location && cJSON_IsString(location)) ? location->valuestring : ""; + // const char* lang_str = (lang && cJSON_IsString(lang)) ? lang->valuestring : "zh_CN"; + // std::string location_copy = std::string(location_str); + // std::string lang_copy = std::string(lang_str); + // if (location_copy.empty() || location_copy == "None") { + // wifi_config_t wc{}; + // esp_wifi_get_config(WIFI_IF_STA, &wc); + // std::string ssid = std::string(reinterpret_cast(wc.sta.ssid)); + // wifi_ap_record_t ap{}; + // std::string bssid; + // if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) { + // char buf[18]; + // snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", + // ap.bssid[0], ap.bssid[1], ap.bssid[2], + // ap.bssid[3], ap.bssid[4], ap.bssid[5]); + // bssid.assign(buf); + // } + // nvs_handle_t h; + // if (nvs_open("wifi_city_map", NVS_READONLY, &h) == ESP_OK) { + // auto try_get = [&](const std::string& key)->std::string{ + // size_t len = 0; + // if (nvs_get_str(h, key.c_str(), NULL, &len) == ESP_OK && len > 0) { + // std::vector buf(len); + // if (nvs_get_str(h, key.c_str(), buf.data(), &len) == ESP_OK) { + // return std::string(buf.data()); + // } + // } + // return std::string(); + // }; + // if (!ssid.empty()) { + // std::string city; + // if (!bssid.empty()) { + // city = try_get(ssid + "|" + bssid); + // } + // if (city.empty()) { + // city = try_get(ssid); + // } + // if (!city.empty()) { + // location_copy = city; + // } + // } + // nvs_close(h); + // } + // } + // + // ESP_LOGI(TAG, "[WeatherAPI] 提取的参数值: location='%s' (长度: %zu), lang='%s'", + // location_str, strlen(location_str), lang_str); + // + // // 获取call_id用于后续响应 + // cJSON* call_id_item = cJSON_GetObjectItem(call, "id"); + // const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + // std::string call_id_copy = call_id ? call_id : ""; + // + // ESP_LOGI(TAG, "[WeatherAPI] call_id_item存在: %s", call_id_item ? "是" : "否"); + // ESP_LOGI(TAG, "[WeatherAPI] 获取的call_id: '%s'", call_id_copy.c_str()); + // + // // 创建异步任务处理天气获取 + // ESP_LOGI(TAG, "[WeatherAPI] 准备创建异步任务处理天气API调用"); + // Schedule([this, location_copy, lang_copy, call_id_copy]() { + // ESP_LOGI(TAG, "[WeatherAPI] 异步任务开始执行"); + // try { + // ESP_LOGI(TAG, "[WeatherAPI] 准备调用全局函数GetWeatherInfo(location='%s', lang='%s')", + // location_copy.c_str(), lang_copy.c_str()); + // + // // 调用天气API获取结果 + // std::string weather_result = GetWeatherInfo(location_copy, lang_copy); + // + // ESP_LOGI(TAG, "[WeatherAPI] GetWeatherInfo调用完成,结果长度: %zu 字节", weather_result.length()); + // ESP_LOGD(TAG, "[WeatherAPI] GetWeatherInfo返回结果前100字节: '%s'", + // weather_result.substr(0, std::min(size_t(100), weather_result.length())).c_str()); + // + // ESP_LOGI(TAG, "[WeatherAPI] 准备发送天气结果响应"); + // if (!call_id_copy.empty()) { + // ESP_LOGI(TAG, "[WeatherAPI] 使用call_id发送FunctionResult: '%s'", call_id_copy.c_str()); + // } else { + // ESP_LOGI(TAG, "[WeatherAPI] 无call_id,将发送TextMessage"); + // } + // + // if (!call_id_copy.empty() && protocol_) { + // protocol_->SendFunctionResult(call_id_copy.c_str(), weather_result); + // ESP_LOGI(TAG, "[WeatherAPI] FunctionResult发送成功"); + // } else if (protocol_) { + // protocol_->SendTextMessage(weather_result);// 发送天气结果 + // ESP_LOGI(TAG, "[WeatherAPI] TextMessage发送成功"); + // } else { + // ESP_LOGE(TAG, "[WeatherAPI] protocol_为空,无法发送响应"); + // } + // } catch (const std::exception& e) { + // ESP_LOGE(TAG, "[WeatherAPI] 天气获取异常: %s", e.what()); + // std::string error_msg = "获取天气信息失败,请稍后重试"; + // ESP_LOGI(TAG, "[WeatherAPI] 准备发送错误响应: '%s'", error_msg.c_str()); + // + // if (!call_id_copy.empty() && protocol_) { + // protocol_->SendFunctionResult(call_id_copy.c_str(), error_msg); + // ESP_LOGI(TAG, "[WeatherAPI] 错误FunctionResult发送成功"); + // } else if (protocol_) { + // protocol_->SendTextMessage(error_msg); + // ESP_LOGI(TAG, "[WeatherAPI] 错误TextMessage发送成功"); + // } else { + // ESP_LOGE(TAG, "[WeatherAPI] protocol_为空,无法发送错误响应"); + // } + // } + // ESP_LOGI(TAG, "[WeatherAPI] ===== get_weather_aihub异步任务处理完成 ====="); + // }); + // + // ESP_LOGI(TAG, "[WeatherAPI] 异步任务已调度,将在后台执行"); + // ESP_LOGI(TAG, "[WeatherAPI] ===== get_weather_aihub工具调用响应已发送 ====="); + // } + cJSON_Delete(args_obj);// 释放参数对象 + } else { + ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str); + if (strcmp(name->valuestring, "adjust_audio_val") == 0) { + auto codec = Board::GetInstance().GetAudioCodec(); + int user = HARDWARE_TO_USER_VOLUME(codec->output_volume()); + if (args && cJSON_IsString(args) && args->valuestring) { + cJSON* tmp = cJSON_Parse(args->valuestring); + if (tmp) { + cJSON* v = cJSON_GetObjectItem(tmp, "value"); + if (!v) v = cJSON_GetObjectItem(tmp, "action"); + if (v) { + std::string msg; + if (cJSON_IsString(v) && v->valuestring) { + if (strcmp(v->valuestring, "up") == 0) { + user += 10; + msg = "音量已经调大了哦~"; + } else if (strcmp(v->valuestring, "down") == 0) { + user -= 10; + msg = "音量已经调小了哦~"; + } else { + // 处理字符串形式的数字 + char* endptr; + long val = strtol(v->valuestring, &endptr, 10); + if (*endptr == '\0' && val >= 0 && val <= 100) { + user = (int)val; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + } + } else if (cJSON_IsNumber(v)) { + user = (int)v->valuedouble; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + if (user > 100) user = 100; + if (user < 0) user = 0; + int mapped = USER_TO_HARDWARE_VOLUME(user); + codec->SetOutputVolume(mapped); + ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped); + if (!msg.empty()) { + cJSON* call_id_item = cJSON_GetObjectItem(call, "id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } else if (protocol_) { + protocol_->SendTextMessage(msg); + } + } + } + cJSON_Delete(tmp); + } + } + } + // 讲故事功能:支持指定URL或故事API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_story") == 0) { + ESP_LOGI(TAG, "收到obtain_story工具调用"); + cJSON* url_item = cJSON_GetObjectItem(args_obj, "story_url"); + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放故事"; + } else { + // HTTPS故事API方式:通过蓝牙MAC请求故事 + ESP_LOGI(TAG, "[HTTPS播放] 使用故事API方式请求音频"); + SendStoryRequest(); + msg = "正在为你获取故事"; + } + cJSON* call_id_item = cJSON_GetObjectItem(call, "id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + // 播放音乐功能:支持指定URL或音乐API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_music") == 0) { + ESP_LOGI(TAG, "收到obtain_music工具调用"); + cJSON* url_item = cJSON_GetObjectItem(args_obj, "music_url"); + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放音乐"; + } else { + ESP_LOGI(TAG, "[HTTPS播放] 使用音乐API方式请求音频"); + SendMusicRequest(); + msg = "正在为你获取音乐"; + } + cJSON* call_id_item = cJSON_GetObjectItem(call, "id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + } + } + } + } + return; + } + return; + } + if (strcmp(type->valuestring, "tts") == 0) { + auto state = cJSON_GetObjectItem(root, "state"); + if (strcmp(state->valuestring, "start") == 0) { + Schedule([this]() { + aborted_ = false; + if (device_state_ == kDeviceStateIdle || device_state_ == kDeviceStateListening) { + SetDeviceState(kDeviceStateSpeaking); + } + }); + } else if (strcmp(state->valuestring, "stop") == 0) { + Schedule([this]() { + background_task_->WaitForCompletion(); + if (device_state_ == kDeviceStateSpeaking) { + if (listening_mode_ == kListeningModeManualStop) { + SetDeviceState(kDeviceStateIdle); + } else { + SetDeviceState(kDeviceStateListening); + } + } + }); + } else if (strcmp(state->valuestring, "sentence_start") == 0) { + auto text = cJSON_GetObjectItem(root, "text"); + if (text != NULL) { + ESP_LOGI(TAG, "<< %s", text->valuestring); + Schedule([this, display, message = std::string(text->valuestring)]() { + display->SetChatMessage("assistant", message.c_str()); + }); + } + } + } else if (strcmp(type->valuestring, "stt") == 0) { + auto text = cJSON_GetObjectItem(root, "text"); + if (text != NULL) { + ESP_LOGI(TAG, ">> %s", text->valuestring); + Schedule([this, display, message = std::string(text->valuestring)]() { + display->SetChatMessage("user", message.c_str()); + }); + } + } else if (strcmp(type->valuestring, "llm") == 0) { + auto emotion = cJSON_GetObjectItem(root, "emotion"); + if (emotion != NULL) { + Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() { + display->SetEmotion(emotion_str.c_str()); + }); + } + } else if (strcmp(type->valuestring, "subtitle") == 0) { + // 火山 RTC 字幕消息:区分用户说的话和AI回答 + auto data_arr = cJSON_GetObjectItem(root, "data"); + if (data_arr && cJSON_IsArray(data_arr)) { + for (int i = 0; i < cJSON_GetArraySize(data_arr); ++i) { + auto item = cJSON_GetArrayItem(data_arr, i); + auto text = cJSON_GetObjectItem(item, "text"); + auto user_id = cJSON_GetObjectItem(item, "userId"); + auto definite = cJSON_GetObjectItem(item, "definite"); + if (!text || !cJSON_IsString(text) || !text->valuestring[0]) continue; + + bool is_final = definite && cJSON_IsTrue(definite); + // userId 以 "bot_" 开头为AI,其余为用户 + bool is_user = true; + if (user_id && cJSON_IsString(user_id)) { + if (strncmp(user_id->valuestring, "bot_", 4) == 0) { + is_user = false; + } + } + + const char* role = is_user ? "USER" : "AI"; + ESP_LOGI(TAG, "%s %s: %s", is_final ? "📝" : "..", role, text->valuestring); + + // 用户说话时立即中止HTTPS音频播放 + // subv字幕消息在协议层跳过了on_bot_message_,由此处直接处理 + if (is_user && https_playback_active_.load() && !https_playback_abort_.load()) { + AbortHttpsPlayback("检测到用户说话(字幕)"); + } + } + } + } else if (strcmp(type->valuestring, "iot") == 0) { + auto commands = cJSON_GetObjectItem(root, "commands"); + if (commands != NULL) { + auto& thing_manager = iot::ThingManager::GetInstance(); + for (int i = 0; i < cJSON_GetArraySize(commands); ++i) { + auto command = cJSON_GetArrayItem(commands, i); + thing_manager.Invoke(command); + } + } + } else if (strcmp(type->valuestring, "response.function_call_arguments.done") == 0) { + auto name = cJSON_GetObjectItem(root, "name"); + auto arguments = cJSON_GetObjectItem(root, "arguments"); + cJSON* args_obj = nullptr; + const char* args_str = (arguments && cJSON_IsString(arguments) && arguments->valuestring) ? arguments->valuestring : ""; + if (arguments && cJSON_IsString(arguments) && arguments->valuestring) { + args_obj = cJSON_Parse(arguments->valuestring); + } + if (name && cJSON_IsString(name) && name->valuestring) { + if (args_obj) { + char* printed = cJSON_PrintUnformatted(args_obj); + if (printed) { + ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, printed); + cJSON_free(printed); + } else { + ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str); + } + if (strcmp(name->valuestring, "adjust_audio_val") == 0) { + auto codec = Board::GetInstance().GetAudioCodec(); + int user = HARDWARE_TO_USER_VOLUME(codec->output_volume()); + cJSON* v = cJSON_GetObjectItem(args_obj, "value");// 获取value字段 + if (!v) v = cJSON_GetObjectItem(args_obj, "action");// 如果value字段不存在,尝试action字段 + if (v) { + std::string msg; + if (cJSON_IsString(v) && v->valuestring) { + if (strcmp(v->valuestring, "up") == 0) { + user += 10; + msg = "音量已经调大了哦~"; + } else if (strcmp(v->valuestring, "down") == 0) { + user -= 10; + msg = "音量已经调小了哦~"; + } else { + // 处理字符串形式的数字 + char* endptr; + long val = strtol(v->valuestring, &endptr, 10); + if (*endptr == '\0' && val >= 0 && val <= 100) { + user = (int)val; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + } + } else if (cJSON_IsNumber(v)) { + user = (int)v->valuedouble; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + if (user > 100) user = 100; + if (user < 0) user = 0; + int mapped = USER_TO_HARDWARE_VOLUME(user); + codec->SetOutputVolume(mapped); + ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped); + if (!msg.empty()) { + cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } else if (protocol_) { + protocol_->SendTextMessage(msg); + } + } + } + } + // 讲故事功能:支持指定URL或故事API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_story") == 0) { + ESP_LOGI(TAG, "收到obtain_story工具调用"); + cJSON* url_item = cJSON_GetObjectItem(args_obj, "story_url"); + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放故事"; + } else { + // HTTPS故事API方式:通过蓝牙MAC请求故事 + ESP_LOGI(TAG, "[HTTPS播放] 使用故事API方式请求音频"); + SendStoryRequest(); + msg = "正在为你获取故事"; + } + cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + // 播放音乐功能:支持指定URL或音乐API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_music") == 0) { + ESP_LOGI(TAG, "收到obtain_music工具调用"); + cJSON* url_item = cJSON_GetObjectItem(args_obj, "music_url"); + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放音乐"; + } else { + ESP_LOGI(TAG, "[HTTPS播放] 使用音乐API方式请求音频"); + SendMusicRequest(); + msg = "正在为你获取音乐"; + } + cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + cJSON_Delete(args_obj); + } else { + ESP_LOGI(TAG, "工具调用: name=%s arguments=%s", name->valuestring, args_str); + if (strcmp(name->valuestring, "adjust_audio_val") == 0) { + auto codec = Board::GetInstance().GetAudioCodec(); + int user = HARDWARE_TO_USER_VOLUME(codec->output_volume()); + if (arguments && cJSON_IsString(arguments) && arguments->valuestring) { + cJSON* tmp = cJSON_Parse(arguments->valuestring); + if (tmp) { + cJSON* v = cJSON_GetObjectItem(tmp, "value"); + if (!v) v = cJSON_GetObjectItem(tmp, "action"); + if (v) { + std::string msg; + if (cJSON_IsString(v) && v->valuestring) { + if (strcmp(v->valuestring, "up") == 0) { + user += 10; + msg = "音量已经调大了哦~"; + } else if (strcmp(v->valuestring, "down") == 0) { + user -= 10; + msg = "音量已经调小了哦~"; + } else { + // 处理字符串形式的数字 + char* endptr; + long val = strtol(v->valuestring, &endptr, 10); + if (*endptr == '\0' && val >= 0 && val <= 100) { + user = (int)val; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + } + } else if (cJSON_IsNumber(v)) { + user = (int)v->valuedouble; + msg = std::string("音量值已经调整为") + std::to_string(user) + "%"; + } + if (user > 100) user = 100; + if (user < 0) user = 0; + int mapped = USER_TO_HARDWARE_VOLUME(user); + codec->SetOutputVolume(mapped); + ESP_LOGI(TAG, "设置音量: 用户=%d%% 硬件=%d%%", user, mapped); + if (!msg.empty()) { + cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } else if (protocol_) { + protocol_->SendTextMessage(msg); + } + } + } + cJSON_Delete(tmp); + } + } + } + // 讲故事功能:支持指定URL或故事API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_story") == 0) { + ESP_LOGI(TAG, "收到obtain_story工具调用"); + cJSON* args_parsed = nullptr; + if (arguments && cJSON_IsString(arguments) && arguments->valuestring) { + args_parsed = cJSON_Parse(arguments->valuestring); + } + cJSON* url_item = args_parsed ? cJSON_GetObjectItem(args_parsed, "story_url") : nullptr; + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放故事"; + } else { + // HTTPS故事API方式:通过蓝牙MAC请求故事 + ESP_LOGI(TAG, "[HTTPS播放] 使用故事API方式请求音频"); + SendStoryRequest(); + msg = "正在为你获取故事"; + } + if (args_parsed) cJSON_Delete(args_parsed); + cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + // 播放音乐功能:支持指定URL或音乐API两种HTTPS方式 + else if (strcmp(name->valuestring, "obtain_music") == 0) { + ESP_LOGI(TAG, "收到obtain_music工具调用"); + cJSON* args_parsed2 = nullptr; + if (arguments && cJSON_IsString(arguments) && arguments->valuestring) { + args_parsed2 = cJSON_Parse(arguments->valuestring); + } + cJSON* url_item = args_parsed2 ? cJSON_GetObjectItem(args_parsed2, "music_url") : nullptr; + AbortSpeaking(kAbortReasonNone); + std::string msg; + if (url_item && cJSON_IsString(url_item) && url_item->valuestring && strlen(url_item->valuestring) > 0) { + ESP_LOGI(TAG, "[HTTPS播放] 使用URL方式: %s", url_item->valuestring); + HttpsPlaybackFromUrl(url_item->valuestring); + msg = "正在通过HTTPS为你播放音乐"; + } else { + ESP_LOGI(TAG, "[HTTPS播放] 使用音乐API方式请求音频"); + SendMusicRequest(); + msg = "正在为你获取音乐"; + } + if (args_parsed2) cJSON_Delete(args_parsed2); + cJSON* call_id_item = cJSON_GetObjectItem(root, "call_id"); + const char* call_id = (call_id_item && cJSON_IsString(call_id_item) && call_id_item->valuestring) ? call_id_item->valuestring : ""; + if (protocol_ && call_id && call_id[0] != '\0') { + protocol_->SendFunctionResult(call_id, msg); + } + } + } + } + // 新增代码(小程序控制 暂停/继续播放 音频) + // ==================================================================== + } + else if (strcmp(type->valuestring, "music_control") == 0) { + auto action = cJSON_GetObjectItem(root, "action"); + if (action && cJSON_IsString(action) && strcmp(action->valuestring, "pause") == 0) { + // 只有在speaking状态下才响应暂停指令 + if (device_state_ == kDeviceStateSpeaking) { + ESP_LOGI(TAG, "🔇 从服务器接收到暂停播放指令 (speaking状态)"); + Schedule([this]() { + PauseAudioPlayback();// 暂停播放 + }); + } else { + ESP_LOGI(TAG, "🔇 收到暂停指令但设备不在speaking状态,忽略指令 (当前状态: %s)", STATE_STRINGS[device_state_]); + } + } else if (action && cJSON_IsString(action) && strcmp(action->valuestring, "resume") == 0) { + // 只有在speaking状态下才响应恢复播放指令 + if (device_state_ == kDeviceStateSpeaking) { + ESP_LOGI(TAG, "🔊 从服务器接收到继续播放指令 (speaking状态)"); + Schedule([this]() { + ResumeAudioPlayback();// 恢复播放 + }); + } else { + ESP_LOGI(TAG, "🔊 收到恢复播放指令但设备不在speaking状态,忽略指令 (当前状态: %s)", STATE_STRINGS[device_state_]); + } + } else if (action && cJSON_IsString(action) && strcmp(action->valuestring, "play") == 0) { + // 处理新故事推送 - 确保在音频暂停状态和播放状态下都能正常播放 + ESP_LOGI(TAG, "🎵 从服务器接收到新故事推送指令 (action: play)"); + Schedule([this]() { + // 参考 AbortSpeakingAndReturnToListening 第1583-1651行的逻辑 + // 检查并处理音频暂停状态,确保新故事能正常播放 + if (audio_paused_) { + ESP_LOGI(TAG, "🔵 检测到音频暂停状态,为新故事推送清除暂停状态"); + audio_paused_ = false; + ESP_LOGI(TAG, "✅ 音频暂停状态已清除"); + + // 清空音频播放队列,避免播放暂停时残留的音频 + std::unique_lock lock(mutex_); + audio_decode_queue_.clear(); + lock.unlock(); + ESP_LOGI(TAG, "🧹 已清空音频播放队列,避免播放残留音频"); + + // 重新启用音频编解码器输出 + auto& board = Board::GetInstance(); + auto codec = board.GetAudioCodec(); + if (codec) { + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔧 为新故事推送重新启用音频编解码器输出"); + } + } + + // 如果当前在播放状态,也需要清空队列确保新故事能正常播放 + if (device_state_ == kDeviceStateSpeaking) { + ESP_LOGI(TAG, "🔵 当前在播放状态,为新故事推送清空音频队列"); + std::unique_lock lock(mutex_); + audio_decode_queue_.clear(); + lock.unlock(); + ESP_LOGI(TAG, "🧹 已清空音频播放队列,准备播放新故事"); + + // 确保音频编解码器输出已启用 + auto& board = Board::GetInstance(); + auto codec = board.GetAudioCodec(); + if (codec) { + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔧 确保音频编解码器输出已启用"); + } + } + + ESP_LOGI(TAG, "🎵 新故事推送处理完成,音频系统已准备就绪"); + }); + } + } + // ==================================================================== + }); + protocol_->Start(); + + // Check for new firmware version or get the MQTT broker address + ota_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL); + ota_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str()); + ota_.SetHeader("Client-Id", board.GetUuid()); + ota_.SetHeader("Accept-Language", Lang::CODE); + auto app_desc = esp_app_get_description(); + ota_.SetHeader("User-Agent", std::string(BOARD_NAME "/") + app_desc->version); + + // 禁用自动OTA - 注释掉下面的任务创建OTA自动升级 + xTaskCreate([](void* arg) { + Application* app = (Application*)arg; + app->CheckNewVersion(); + vTaskDelete(NULL); + }, "check_new_version", 4096 * 2, this, 2, nullptr); + + +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.Initialize(codec, realtime_chat_enabled_); + + // 🎯 根据语音打断功能启用状态配置VAD参数 + EchoAwareVadParams enhanced_params; + if (realtime_chat_enabled_) { + // 语音打断功能启用:配置增强的回声感知参数 - 基于小智AI官方优化方案 + // 🎯 平衡配置 - 防误触发同时保证音频流畅 + enhanced_params.snr_threshold = 60.0f; // 平衡基础阈值:足够严格但不过度 + enhanced_params.min_silence_ms = 2000; // 平衡静音要求:2秒 + enhanced_params.interrupt_cooldown_ms = 10000; // 平衡冷却时间:10秒 + enhanced_params.adaptive_threshold = true; // 启用自适应阈值 + + // 🔊 平衡噪声抑制参数 - 优化性能与效果 + enhanced_params.adaptive_noise_suppression = true; // 启用自适应噪声抑制 + enhanced_params.noise_suppression_base = 5.0f; // 平衡基础抑制强度 + enhanced_params.volume_sensitivity = 3.0f; // 平衡音量敏感度:适度的音量影响 + enhanced_params.echo_detection_threshold = 0.15f; // 平衡回声检测阈值 + enhanced_params.distance_estimation_factor = 3.0f; // 平衡距离估算因子 + ESP_LOGI(TAG, "🎯 Adaptive noise suppression enabled for realtime chat - smart volume/distance adjustment"); + } else { + // 🔧 语音打断功能禁用:关闭复杂VAD,只使用简单VAD + enhanced_params.adaptive_threshold = false; // 禁用自适应阈值 + enhanced_params.adaptive_noise_suppression = false; // 禁用自适应噪声抑制 + ESP_LOGI(TAG, "🔧 Using simple VAD for basic voice detection - complex echo-aware VAD disabled"); + } + audio_processor_.SetEchoAwareParams(enhanced_params);// 🔊 设置回声感知参数 + + // 🔊 注册音频处理输出回调 - 处理回声感知后的PCM数据 + audio_processor_.OnOutput([this](std::vector&& data) { + background_task_->Schedule([this, data = std::move(data)]() mutable { + static uint64_t last_us = 0; + static size_t frames = 0; + std::vector resampled(uplink_resampler_.GetOutputSamples(data.size())); + if (!resampled.empty()) { + uplink_resampler_.Process(data.data(), data.size(), resampled.data()); + } + std::vector bytes(resampled.size() * sizeof(int16_t)); + for (size_t i = 0; i < resampled.size(); ++i) { + int16_t s = resampled[i]; + bytes[i * 2] = (uint8_t)(s & 0xFF); + bytes[i * 2 + 1] = (uint8_t)((s >> 8) & 0xFF); + } + frames += 1; + uint64_t now_us = esp_timer_get_time(); + if (last_us == 0) last_us = now_us; + if (now_us - last_us >= 2000000) { + ESP_LOGI(TAG, "AFE输出统计: 帧=%zu 样本=%zu ", frames, data.size()); + frames = 0; + last_us = now_us; + } + Schedule([this, bytes = std::move(bytes)]() { + if (protocol_ && protocol_->IsAudioChannelOpened() && (device_state_ == kDeviceStateListening || device_state_ == kDeviceStateDialog || (listening_mode_ == kListeningModeRealtime && device_state_ == kDeviceStateSpeaking))) { + protocol_->SendPcm(bytes); + } else { + ESP_LOGD(TAG, "通道未打开或不在dialog/listening状态时跳过发送上行"); + } + }); + }); + }); + // 🎯 根据语音打断功能启用状态选择VAD类型 + if (realtime_chat_enabled_) { + // 语音打断功能启用:使用复杂的回声感知VAD + audio_processor_.OnVadStateChange([this](bool speaking) { + ESP_LOGI(TAG, "Complex VAD state change: speaking=%s, device_state=%d, listening_mode=%d", + speaking ? "true" : "false", (int)device_state_, (int)listening_mode_); + + if (device_state_ == kDeviceStateListening) { + Schedule([this, speaking]() { + if (speaking) { + voice_detected_ = true; + } else { + voice_detected_ = false; + } + auto led = Board::GetInstance().GetLed(); + led->OnStateChanged(); + }); + } + }); + } + + // 🔧 简单VAD:用于普通业务(触摸忽略、LED状态等) + audio_processor_.OnSimpleVadStateChange([this](bool speaking) { + ESP_LOGI(TAG, "Simple VAD state change: speaking=%s, device_state=%d", + speaking ? "true" : "false", (int)device_state_); + + if (device_state_ == kDeviceStateListening) { + Schedule([this, speaking]() { + if (speaking) { + voice_detected_ = true; + } else { + voice_detected_ = false; + } + auto led = Board::GetInstance().GetLed(); + led->OnStateChanged(); + }); + } + + // 🔊 语音打断逻辑:只在简单VAD中处理,因为复杂VAD可能过于严格 + if (device_state_ == kDeviceStateSpeaking && listening_mode_ == kListeningModeRealtime) { + Schedule([this, speaking]() { + static auto speech_start_time = std::chrono::steady_clock::now(); + static bool speech_confirmation_pending = false; + auto now = std::chrono::steady_clock::now(); + + if (speaking) { + // 小智AI方案:检测到人声开始,启动确认流程 + speech_start_time = now; + speech_confirmation_pending = true; + ESP_LOGD(TAG, "Human voice detected during playback, starting interrupt evaluation"); + } else if (speech_confirmation_pending) { + // 小智AI方案:人声结束,评估是否触发打断 + auto duration = std::chrono::duration_cast(now - speech_start_time); + + // 🎯 平衡自适应打断策略:防误触发同时保证响应性 + // 基础持续时间:3秒,平衡根据干扰情况调整 + int required_duration = 3000; // 基础要求3秒 + + // 🔊 根据当前音量动态调整持续时间要求(平衡策略) + if (current_speaker_volume_ > 0.4f) { + required_duration = 5000; // 高音量:5秒 + } else if (current_speaker_volume_ > 0.1f) { + required_duration = 4000; // 中音量:4秒 + } + // 低音量或静音:保持3秒 + + if (duration.count() >= required_duration) { + static auto last_interrupt_time = std::chrono::steady_clock::now(); + auto interrupt_duration = std::chrono::duration_cast(now - last_interrupt_time); + + // 🎯 平衡自适应多重保护机制:防误触发同时保证性能 + bool volume_protection = (current_speaker_volume_ > 0.01f); // 平衡音量保护:1%阈值 + bool cooldown_protection = (interrupt_duration.count() <= 10000); // 平衡冷却:10秒 + static int false_positive_count = 0; // 误触发计数器 + + // 🎯 平衡连续误触发保护:适度学习机制 + if (interrupt_duration.count() <= 20000) { // 20秒内的误触发相关 + false_positive_count++; + } else if (interrupt_duration.count() > 60000) { // 60秒后开始衰减 + false_positive_count = std::max(false_positive_count - 1, 0); // 适度衰减 + } + + bool consecutive_protection = (false_positive_count >= 2); // 2次误触发后保护 + + if (!volume_protection && !cooldown_protection && !consecutive_protection) { + // 小智AI核心逻辑:StopPlayback -> SetDeviceState(Listening) + ESP_LOGI(TAG, "🎯 Adaptive voice interrupt triggered (duration: %.0fms/%dms, vol: %.3f) - stopping playback", + (float)duration.count(), required_duration, current_speaker_volume_); + AbortSpeaking(kAbortReasonVoiceInterrupt); + SetDeviceState(kDeviceStateListening); + last_interrupt_time = now; + } else { + ESP_LOGI(TAG, "🎯 Adaptive interrupt suppressed - vol_protection: %s (%.3f), cooldown: %.0fms, consecutive: %d", + volume_protection ? "true" : "false", current_speaker_volume_, + 10000.0f - (float)interrupt_duration.count(), false_positive_count); + } + } else { + ESP_LOGI(TAG, "🎯 Voice too brief (%.0fms), likely echo or noise - adaptive threshold: %dms", + (float)duration.count(), required_duration); + } + speech_confirmation_pending = false; + } + }); + } + }); +#endif + +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + if (!wake_word_detect_.Initialize(codec)) { + ESP_LOGE(TAG, "Failed to initialize wake word detection"); + return; + } + wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) { + Schedule([this, &wake_word]() { + if (device_state_ == kDeviceStateIdle) { + SetDeviceState(kDeviceStateConnecting); + wake_word_detect_.EncodeWakeWordData(); + + // 将OpenAudioChannel调用移到后台任务执行,避免main任务栈溢出 + background_task_->Schedule([this, wake_word]() { + // 打开音频通道并发送唤醒词数据到服务器 + if (!protocol_->OpenAudioChannel()) { + Schedule([this]() { + wake_word_detect_.Start(); + }); + return; + } + // 编码并发送唤醒词音频数据 + std::vector opus; + // Encode and send the wake word data to the server + while (wake_word_detect_.GetWakeWordOpus(opus)) { + protocol_->SendAudio(opus); + } + // Set the chat state to wake word detected + protocol_->SendWakeWordDetected(wake_word); + ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str()); + + // SetListeningMode需要在main task中执行,因为它涉及UI更新等 + Schedule([this]() { + SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeAutoStop); + }); + }); + } else if (device_state_ == kDeviceStateSpeaking) { + AbortSpeaking(kAbortReasonWakeWordDetected); + } else if (device_state_ == kDeviceStateActivating) { + SetDeviceState(kDeviceStateIdle); + } + }); + }); + wake_word_detect_.Start(); +#endif + + SetDeviceState(kDeviceStateIdle); + + // BLE JSON 通讯服务已移至 WifiBoard 中,仅在配网模式下启动 + + // 每次设备开机后idle状态下测试 自动检测并设置当前位置打印 + //此逻辑为冗余操作,当前NVS中没有城市信息时会自动调用 位置查询API + // Schedule([]() { + // AutoDetectAndSetLocation(); + // }); + esp_timer_start_periodic(clock_timer_handle_, 1000000); + +#if 0 + while (true) { + SystemInfo::PrintRealTimeStats(pdMS_TO_TICKS(1000)); + vTaskDelay(pdMS_TO_TICKS(10000)); + } +#endif +} +// 时钟定时器回调函数 +void Application::OnClockTimer() { + clock_ticks_++; + + // 每10秒打印一次调试信息 + if (clock_ticks_ % 10 == 0) { + int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + int min_free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL); + // ESP_LOGI(TAG, "Free internal: %u minimal internal: %u", free_sram, min_free_sram);// 打印内部内存空闲大小和最小空闲大小 + // // 打印蓝牙MAC地址 + // ESP_LOGI(MAC_TAG, "Bluetooth MAC Address: %s", SystemInfo::GetBleMacAddress().c_str());// 生产测试打印 + + //ESP_LOGI(TAG, "此设备角色为: %s",CONFIG_DEVICE_ROLE); + // ESP_LOGI(TAG, "此设备角色为: KAKA 1028 升级成功!"); + + // 如果我们已经同步了服务器时间,如果设备处于空闲状态,请将状态设置为时钟“HH:MM” + if (ota_.HasServerTime()) { + if (device_state_ == kDeviceStateIdle) { + Schedule([this]() { + // Set status to clock "HH:MM" + time_t now = time(NULL);// 获取当前时间 + char time_str[64];// 时间字符串缓冲区 + strftime(time_str, sizeof(time_str), "%H:%M ", localtime(&now));// 格式化时间字符串 + Board::GetInstance().GetDisplay()->SetStatus(time_str);// 设置显示状态 + }); + } + } + } +} + +// 添加任务到主循环 +void Application::Schedule(std::function callback) { + { + std::lock_guard lock(mutex_);// 加锁保护任务队列 + main_tasks_.push_back(std::move(callback));// 添加任务到队列 + } + xEventGroupSetBits(event_group_, SCHEDULE_EVENT);// 通知主循环有任务需要执行 +} + +// 主循环控制聊天状态和Websocket连接 +// 如果其他任务需要访问Websocket或聊天状态, +// 它们应该使用Schedule调用此函数 +void Application::MainLoop() { + while (true) { + auto bits = xEventGroupWaitBits(event_group_, SCHEDULE_EVENT, pdTRUE, pdFALSE, portMAX_DELAY);// 等待任务事件触发 + // 检查是否有任务需要执行 + if (bits & SCHEDULE_EVENT) { + std::unique_lock lock(mutex_);// 加锁保护任务队列 + std::list> tasks = std::move(main_tasks_);// 移动任务队列到本地 + lock.unlock();// 解锁,允许其他任务添加到队列 + for (auto& task : tasks) { + task();// 执行任务 + } + } + } +} + +// 音频环路用于输入和输出音频数据 +void Application::AudioLoop() { + auto codec = Board::GetInstance().GetAudioCodec(); + while (true) { + OnAudioInput(); + if (codec->output_enabled()) { + OnAudioOutput(); + } + } +} + +// 启动对话看门狗 +void Application::StartDialogWatchdog() { + if (dialog_watchdog_task_handle_ != nullptr) { + return;// 如果看门狗任务已存在,直接返回 + } + dialog_watchdog_running_ = true;// 设置看门狗运行标志为true + dialog_watchdog_last_logged_ = -1;// 重置上次记录的日志时间为-1 + xTaskCreatePinnedToCore([](void* arg) { + Application* app = (Application*)arg;// 获取应用实例指针 + ESP_LOGI(TAG, "Dialog watchdog started, initial device state: %d", app->GetDeviceState()); + while (app->dialog_watchdog_running_) { + vTaskDelay(pdMS_TO_TICKS(2000));// 减少延时到2秒,更及时地检测和更新倒计时 + + // 检查设备状态 + DeviceState current_state = app->GetDeviceState(); + if (current_state != kDeviceStateDialog) { + ESP_LOGD(TAG, "Dialog watchdog skipping check, not in dialog state (current: %d)", current_state); + continue; + } + + auto now = std::chrono::steady_clock::now();// 获取当前时间点 + auto elapsed = std::chrono::duration_cast(now - app->last_audible_output_time_).count();// 计算自上次有音频输出以来的秒数 + + // 确保elapsed为非负数 + if (elapsed < 0) { + ESP_LOGW(TAG, "Dialog watchdog: invalid elapsed time: %lld", elapsed); + continue; + } + + int remaining = DIALOG_IDLE_COUNTDOWN_SECONDS - (int)elapsed;// 计算对话空闲倒计时剩余秒数 + + // 调试日志 + ESP_LOGD(TAG, "Dialog watchdog: elapsed=%d, remaining=%d", (int)elapsed, remaining); + + // 如果剩余秒数小于等于0,说明对话空闲倒计时已到,需要重启设备 + if (remaining <= 0) { + ESP_LOGI(TAG, "Dialog watchdog idle reached, elapsed=%d, marking and rebooting", (int)elapsed); + Settings sys("system", true); + ESP_LOGI(TAG, "Dialog watchdog: preparing NVS writes (system)"); + sys.SetInt("reboot_dlg_idle", 1); + sys.SetInt("reboot_origin", 1); + ESP_LOGI(TAG, "Dialog watchdog: committing NVS (system)"); + sys.Commit(); + Settings sysr("system"); + int32_t verify = sysr.GetInt("reboot_dlg_idle", 0); + int32_t origin_read = sysr.GetInt("reboot_origin", 0); + if (verify != 1) { + ESP_LOGW(TAG, "Dialog watchdog: NVS verify failed, cause=%d, origin=%d", (int)verify, (int)origin_read); + ESP_LOGW(TAG, "建议: 检查NVS空间是否不足、确认nvs_flash_init成功、避免并发写入(system)"); + } + ESP_LOGI(TAG, "Dialog watchdog (task) set reboot_cause=1, verify=%d, restart in 2000ms", (int)verify); + // 重启前上报设备离线状态 + Board::GetInstance().OnBeforeRestart(); + vTaskDelay(pdMS_TO_TICKS(2000)); + esp_restart();// 重启设备 + app->dialog_watchdog_running_ = false;// 设置看门狗运行标志为false + } else { + // 简化条件判断,移除冗余检查 + // 优化桶计算逻辑,使用1秒一个桶,更精确地显示倒计时 + int bucket = remaining; // 使用剩余秒数作为桶标识,实现每秒更新 + if (bucket != app->dialog_watchdog_last_logged_ && remaining <= DIALOG_IDLE_COUNTDOWN_SECONDS) { + ESP_LOGI(TAG, "dialog对话空闲倒计时剩余: %d 秒", remaining);// 打印剩余秒数 + app->dialog_watchdog_last_logged_ = bucket;// 更新上次记录的日志时间为当前桶 + } + } + } + app->dialog_watchdog_task_handle_ = nullptr; + ESP_LOGI(TAG, "Dialog watchdog stopped"); + vTaskDelete(NULL); + }, "dialog_watchdog", 4096, this, 5, &dialog_watchdog_task_handle_, 0); +} + +// 停止对话看门狗 +void Application::StopDialogWatchdog() { + dialog_watchdog_running_ = false; + + if (dialog_watchdog_task_handle_ != nullptr) { + vTaskDelete(dialog_watchdog_task_handle_); + dialog_watchdog_task_handle_ = nullptr; + ESP_LOGI(TAG, "Dialog watchdog stopped"); + } + + dialog_watchdog_last_logged_ = -1; +} + +// 音频输出函数 +void Application::OnAudioOutput() { + auto now = std::chrono::steady_clock::now();// 获取当前时间 + auto codec = Board::GetInstance().GetAudioCodec();// 获取音频编解码器 + const int max_silence_seconds = 10;// 最大静音秒数 + std::unique_lock lock(mutex_);// 加锁保护音频队列 + + // 调试日志:检查设备状态和音频队列 + ESP_LOGD(TAG, "🔊 OnAudioOutput called, device_state: %d, audio_queue_size: %zu, codec_output_enabled: %d", + device_state_, audio_decode_queue_.size(), codec->output_enabled()); + + // 新增代码(小程序控制 暂停/继续播放 音频) + // ========================================================= + // 🔧 暂停状态下停止从队列取数据,但保留队列状态 + if (audio_paused_) { + // 暂停时重置音量状态,避免误判 + if (current_speaker_volume_ > 0.0f) { + current_speaker_volume_ = 0.0f;// 暂停时重置音量状态 +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(0.0f); +#endif + } + return; + } + // ========================================================= + + if (audio_decode_queue_.empty()) { + // 重要:没有音频数据时立即重置音量状态 + if (current_speaker_volume_ > 0.0f) { + current_speaker_volume_ = 0.0f; +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(0.0f); +#endif + } + ESP_LOGD(TAG, "🔊 音频队列为空"); + + // Disable the output if there is no audio data for a long time + if (device_state_ == kDeviceStateIdle) { + auto duration = std::chrono::duration_cast(now - last_output_time_).count(); + if (duration > max_silence_seconds) { + codec->EnableOutput(false); + } + } + return; + } + + if (device_state_ == kDeviceStateListening && listening_mode_ != kListeningModeRealtime) { + audio_decode_queue_.clear(); + current_speaker_volume_ = 0.0f;// 清空音频队列后重置音量状态 +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(0.0f); +#endif + return; + } + + auto opus = std::move(audio_decode_queue_.front()); + audio_decode_queue_.pop_front(); + // 在出队时捕获opus解码标志,避免background_task异步执行时标志已变化 + // 导致残留的Opus帧被当作PCM播放(产生杂音) + bool is_opus_frame = opus_playback_active_.load(); + lock.unlock(); + + background_task_->Schedule([this, codec, opus = std::move(opus), is_opus_frame]() mutable { + if (aborted_) { + return; + } + // 跳过已中止的HTTPS opus残留帧:出队时is_opus_frame=true,但中止后opus_playback_active_=false + // 不能用https_playback_abort_判断,因为故事任务退出时会将其清为false,导致残留帧漏过 + if (is_opus_frame && !opus_playback_active_.load()) { + return; + } + + std::vector pcm; + bool decoded = false; + bool treat_as_pcm = (protocol_ && protocol_->downlink_is_pcm() && !is_opus_frame); + if (!treat_as_pcm) { + decoded = opus_decoder_->Decode(std::move(opus), pcm); + } + if (!decoded) { + if (treat_as_pcm && !opus.empty() && (opus.size() % 2 == 0)) { + pcm.resize(opus.size() / 2); + memcpy(pcm.data(), opus.data(), opus.size()); + int srv = protocol_ ? protocol_->server_sample_rate() : 16000; + if (!player_pipeline_) { + if (srv != codec->output_sample_rate()) { + output_resampler_.Configure(srv, codec->output_sample_rate()); + int target_size = output_resampler_.GetOutputSamples(pcm.size()); + std::vector resampled(target_size); + output_resampler_.Process(pcm.data(), pcm.size(), resampled.data()); + pcm = std::move(resampled); + } + } + } else { + return; + } + } + // Resample if the sample rate is different + if (!treat_as_pcm && decoded && !player_pipeline_ && opus_decoder_->sample_rate() != codec->output_sample_rate()) { + int target_size = output_resampler_.GetOutputSamples(pcm.size()); + std::vector resampled(target_size); + output_resampler_.Process(pcm.data(), pcm.size(), resampled.data()); + pcm = std::move(resampled); + } + // 计算并同步音频输出音量到音频处理器,用于动态VAD阈值调整 + if (!pcm.empty()) { + // 计算音频块的RMS音量 (0.0 - 1.0) + float sum_squares = 0.0f; + for (const auto& sample : pcm) { + float normalized = (float)sample / 32768.0f; + sum_squares += normalized * normalized; + } + float rms_volume = std::sqrt(sum_squares / pcm.size()); + + // 当有可听见的音频输出时,更新最后声音输出时间戳 + const float audible_volume_threshold = 0.01f; // 设置一个合理的音量阈值 + if (rms_volume >= audible_volume_threshold) { + this->last_audible_output_time_ = std::chrono::steady_clock::now(); + ESP_LOGD(TAG, "🔊 更新last_audible_output_time_,当前音量: %.4f", rms_volume); + } + +#if CONFIG_USE_AUDIO_PROCESSOR + // 同步音量到音频处理器,用于动态阈值调整 + current_speaker_volume_ = rms_volume; // 保存当前音量供打断逻辑使用 + audio_processor_.SetSpeakerVolume(rms_volume); +#endif + } + + int src_rate = decoded ? opus_decoder_->sample_rate() : (protocol_ ? protocol_->server_sample_rate() : 16000); + static bool first_play_logged = false; + if (player_pipeline_) { + player_pipeline_set_src_rate(player_pipeline_, src_rate); + int bytes = (int)(pcm.size() * sizeof(int16_t)); + ESP_LOGD(TAG, "写入播放管道: 采样率=%d 字节=%d", src_rate, bytes); + player_pipeline_write(player_pipeline_, (char*)pcm.data(), bytes); + if (bytes > 0) { + this->last_audible_output_time_ = std::chrono::steady_clock::now(); + } + if (!first_play_logged && bytes > 0) { + ESP_LOGI(TAG, "开始播放下行音频: 字节=%d 采样率=%d", bytes, src_rate); + first_play_logged = true; + } + } else { + ESP_LOGD(TAG, "直接输出PCM到编解码器: 样本=%zu", pcm.size()); + codec->OutputData(pcm);// 直接输出PCM数据 + if (!pcm.empty()) { + this->last_audible_output_time_ = std::chrono::steady_clock::now(); + } + if (!first_play_logged && !pcm.empty()) { + ESP_LOGI(TAG, "开始播放下行音频: 样本=%zu 采样率=%d", pcm.size(), src_rate); + first_play_logged = true; + } + + // 解决本地资源声音播放尖锐问题方案1 + // // 如果是单声道,转换为立体声 + // if (codec->output_channels() == 2) {// 单声道转换为立体声 + // std::vector stereo(pcm.size() * 2);// 立体声PCM数据 + // for (size_t i = 0, j = 0; i < pcm.size(); ++i) { + // stereo[j++] = pcm[i];// 左声道 + // stereo[j++] = pcm[i];// 右声道 + // } + // codec->OutputData(stereo);// 输出立体声PCM数据 + // } else { + // codec->OutputData(pcm);// 输出单声道PCM数据 + // } + + // 解决本地资源声音播放尖锐问题方案2 + // player_pipeline_ = player_pipeline_open();// 打开音频播放管道 + // player_pipeline_run(player_pipeline_);// 启动音频播放管道 + // player_pipeline_set_src_rate(player_pipeline_, src_rate);// 设置播放管道源采样率 + // player_pipeline_write(player_pipeline_, (char*)pcm.data(), (int)(pcm.size() * sizeof(int16_t)));// 写入PCM数据到播放管道 + + } + last_output_time_ = std::chrono::steady_clock::now();// 更新最后输出时间 + }); +} + +void Application::OnAudioInput() { + std::vector data; + +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + if (wake_word_detect_.IsRunning()) { + ReadAudio(data, 16000, wake_word_detect_.GetFeedSize()); + wake_word_detect_.Feed(data);// 将音频数据喂给唤醒词检测器 + return; + } +#endif +#if CONFIG_USE_AUDIO_PROCESSOR + if (audio_processor_.IsRunning()) { + ReadAudio(data, 16000, audio_processor_.GetFeedSize()); + if (!data.empty()) { + int n = (int)data.size(); + int64_t sum = 0; + int peak = 0; + for (int i = 0; i < n; ++i) { + int v = data[i]; + int a = v < 0 ? -v : v; + sum += a; + if (a > peak) peak = a; + } + (void)sum; + // if (avg > 150 || peak > 800) { + // ESP_LOGI(TAG, "🎙️ 输入幅度: 均值=%d 峰值=%d 样本=%d", avg, peak, n); + // } + } + audio_processor_.Feed(data); + return; + } +#else + if (device_state_ == kDeviceStateListening || device_state_ == kDeviceStateDialog) { + if (send_g711a_uplink_) { + ReadAudio(data, 16000, 20 * 16000 / 1000); + if (!data.empty()) { + std::vector resampled(uplink_resampler_.GetOutputSamples((int)data.size())); + if (!resampled.empty()) { + uplink_resampler_.Process(data.data(), (int)data.size(), resampled.data()); + } + int out_samples = (int)resampled.size(); + std::vector bytes(out_samples); + for (int i = 0; i < out_samples; ++i) { + int16_t s = resampled[i]; + int sign = (s >> 8) & 0x80; + if (sign) s = -s; + if (s > 32635) s = 32635; + int exp = 7; + for (int mask = 0x4000; exp > 0 && (s & mask) == 0; mask >>= 1) exp--; + int mant = (s >> ((exp == 0) ? 4 : (exp + 3))) & 0x0F; + uint8_t a = (uint8_t)(sign | (exp << 4) | mant); + bytes[i] = (uint8_t)(a ^ 0xD5); + } + Schedule([this, bytes = std::move(bytes)]() { + if (protocol_ && protocol_->IsAudioChannelOpened()) { + protocol_->SendG711A(bytes);// 发送G711A音频数据 + } + }); + } + } else if (send_pcm_uplink_) { + ReadAudio(data, 16000, 20 * 16000 / 1000); + if (!data.empty()) { + int out_samples = (int)data.size() / 2; + std::vector down(out_samples); + for (int i = 0, j = 0; i < out_samples; ++i, j += 2) { + down[i] = data[j]; + } + std::vector resampled(uplink_resampler_.GetOutputSamples((int)down.size())); + if (!resampled.empty()) { + uplink_resampler_.Process(down.data(), (int)down.size(), resampled.data()); + } + std::vector bytes(resampled.size() * sizeof(int16_t)); + for (size_t i = 0; i < resampled.size(); ++i) { + int16_t s = resampled[i]; + bytes[i * 2] = (uint8_t)(s & 0xFF); + bytes[i * 2 + 1] = (uint8_t)((s >> 8) & 0xFF); + } + Schedule([this, bytes = std::move(bytes)]() { + if (protocol_ && protocol_->IsAudioChannelOpened() && (device_state_ == kDeviceStateListening || device_state_ == kDeviceStateDialog || (listening_mode_ == kListeningModeRealtime && device_state_ == kDeviceStateSpeaking))) { + protocol_->SendPcm(bytes); + } + }); + } + } else { + ReadAudio(data, 16000, 30 * 16000 / 1000); + background_task_->Schedule([this, data = std::move(data)]() mutable { + opus_encoder_->Encode(std::move(data), [this](std::vector&& opus) { + Schedule([this, opus = std::move(opus)]() { + if (protocol_) { + protocol_->SendAudio(opus); + } + }); + }); + }); + } + return; + } +#endif + vTaskDelay(pdMS_TO_TICKS(30)); +} + +void Application::ReadAudio(std::vector& data, int sample_rate, int samples) { + auto codec = Board::GetInstance().GetAudioCodec(); + +#if CONFIG_USE_AUDIO_PROCESSOR + // 当音频处理器运行且存在参考通道时,保持原有双通道读取以支持AEC + if (audio_processor_.IsRunning() && codec->input_channels() == 2) { + if (codec->input_sample_rate() != sample_rate) { + data.resize(samples * codec->input_sample_rate() / sample_rate); + if (!codec->InputData(data)) { + return; + } + auto mic_channel = std::vector(data.size() / 2); + auto reference_channel = std::vector(data.size() / 2); + for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) { + mic_channel[i] = data[j]; + reference_channel[i] = data[j + 1]; + } + auto resampled_mic = std::vector(input_resampler_.GetOutputSamples(mic_channel.size())); + auto resampled_reference = std::vector(reference_resampler_.GetOutputSamples(reference_channel.size())); + input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data()); + reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data()); + data.resize(resampled_mic.size() + resampled_reference.size()); + for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) { + data[j] = resampled_mic[i]; + data[j + 1] = resampled_reference[i]; + } + return; + } else { + data.resize(samples * codec->input_channels()); + if (!codec->InputData(data)) { + return; + } + static bool first_equal_sr_dual_read_logged = false; + if (!first_equal_sr_dual_read_logged) { + ESP_LOGI(TAG, "AFE输入首包: 双通道等采样率 目标样本=%d 通道=%d 实际向量=%zu", samples, codec->input_channels(), data.size()); + first_equal_sr_dual_read_logged = true; + } + return; + } + } +#endif + + // 默认优先使用recorder管道读取(目标采样率16000),无参考通道需求 + if (recorder_pipeline_ && sample_rate == 16000) { + int need_bytes = samples * (int)sizeof(int16_t); + int default_bytes = recorder_pipeline_get_default_read_size(recorder_pipeline_); + std::vector out; + out.reserve(samples);// 预分配内存空间,避免后续动态扩容 + std::vector buf(default_bytes);// 内存音频缓冲区,用于存储从录音管道读取的音频数据 + while ((int)out.size() < samples) { + int to_read = std::min(default_bytes, (need_bytes - (int)out.size() * (int)sizeof(int16_t)));// 计算本次读取的字节数,不超过默认读取大小和剩余需要读取的字节数 + if (to_read <= 0) break;// 读取到的数据大小小于等于0,跳出循环 + int got = recorder_pipeline_read(recorder_pipeline_, buf.data(), to_read);// 从录音管道读取音频数据,并赋值给内存音频缓冲区 + if (got <= 0) { + ESP_LOGW(TAG, "🎙️ 录音管道读取失败,未收到输入数据"); + break; + } + int got_samples = got / (int)sizeof(int16_t);// 计算本次读取的样本数,即读取到的字节数除以每个样本的字节数 + int16_t* p = (int16_t*)buf.data();// 将内存音频缓冲区转换为int16_t指针,方便按样本读取 + for (int i = 0; i < got_samples && (int)out.size() < samples; ++i) { + out.push_back(p[i]);// 将读取到的样本添加到输出向量中,直到达到预期样本数或读取完所有数据 + } + } + if (!out.empty()) { + data.assign(out.begin(), out.end());// 将输出向量中的数据赋值给输出参数data + return; + } + } + + // 回退到直接从codec读取的实现 + if (codec->input_sample_rate() != sample_rate) { + data.resize(samples * codec->input_sample_rate() / sample_rate); + if (!codec->InputData(data)) { + ESP_LOGW(TAG, "🎙️ 麦克风采样失败(重采样路径),未收到输入数据"); + return; + } + if (codec->input_channels() == 2) { + // 双通道约定:当前缓冲按 [主麦,参考] 排列;必须与 ALGORITHM_INPUT_FORMAT 的 M/R 顺序 + // 以及 CHANNEL_FORMAT 的物理通道对应一致,否则可能导致 AEC 失效或增益反向 + auto mic_channel = std::vector(data.size() / 2); + auto reference_channel = std::vector(data.size() / 2); + for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) { + mic_channel[i] = data[j]; + reference_channel[i] = data[j + 1]; + } + auto resampled_mic = std::vector(input_resampler_.GetOutputSamples(mic_channel.size())); + auto resampled_reference = std::vector(reference_resampler_.GetOutputSamples(reference_channel.size())); + input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data()); + reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data()); + data.resize(resampled_mic.size() + resampled_reference.size()); + for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) { + data[j] = resampled_mic[i]; + data[j + 1] = resampled_reference[i]; + } + } else { + auto resampled = std::vector(input_resampler_.GetOutputSamples(data.size())); + input_resampler_.Process(data.data(), data.size(), resampled.data()); + data = std::move(resampled); + } + } else { + data.resize(samples); + if (!codec->InputData(data)) { + ESP_LOGW(TAG, "🎙️ 麦克风采样失败(直读路径),未收到输入数据"); + return; + } + } +} + +// 打断语音播报(终止播放) +void Application::AbortSpeaking(AbortReason reason) { + // 🔧 防止重复中止操作,避免竞态条件 + bool expected = false; + if (!is_aborting_.compare_exchange_strong(expected, true)) { + ESP_LOGD(TAG, "AbortSpeaking already in progress, ignoring duplicate call"); + return; + } + + ESP_LOGI(TAG, "🔴 Abort speaking - immediate stop"); + aborted_ = true; + + // 中止HTTPS音频播放(如果正在进行) + if (https_playback_active_.load()) { + https_playback_abort_.store(true); + ESP_LOGI(TAG, "🔴 HTTPS音频播放中止信号已发送"); + } + + // 🔧 更新安全操作时间戳 + last_safe_operation_.store(std::chrono::steady_clock::now()); + + // 🔧 修复:立即清空音频队列,确保音频播放立即停止 + { + std::lock_guard lock(mutex_); + if (!audio_decode_queue_.empty()) { + ESP_LOGI(TAG, "🔴 Clearing %zu audio frames from queue", audio_decode_queue_.size()); + audio_decode_queue_.clear(); + + // 重置音量状态 + current_speaker_volume_ = 0.0f; +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(0.0f); +#endif + } + } + + // ⚠️ 移除WaitForCompletion避免死锁,让后台任务通过aborted_标志自然结束 + ESP_LOGI(TAG, "🔴 Audio queue cleared, background tasks will stop on next iteration"); + + // 重启codec输出以清空I2S DMA缓冲区中残留音频,确保扬声器立即静音 + // 移除output_enabled()守卫,确保始终执行flush + if (background_task_) { + background_task_->Schedule([this]() { + auto codec = Board::GetInstance().GetAudioCodec(); + if (codec) { + ESP_LOGI(TAG, "DMA flush: output_enabled=%d", codec->output_enabled()); + codec->EnableOutput(false); + vTaskDelay(pdMS_TO_TICKS(10)); + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔇 音频输出已重置,DMA缓冲区已清空"); + } + }); + } + + // 🔧 修复:始终尝试发送中止消息以打断RTC下行(不受IsSafeToOperate限制) + if (protocol_) { + try { + // 🔧 额外的连接状态验证 + if (protocol_->IsAudioChannelOpened()) { + protocol_->SendAbortSpeaking(reason); + ESP_LOGI(TAG, "AbortSpeaking message sent successfully"); + // 更新安全操作时间戳 + last_safe_operation_.store(std::chrono::steady_clock::now()); + } else { + ESP_LOGW(TAG, "Audio channel not properly opened, skipping AbortSpeaking"); + } + } catch (const std::exception& e) { + ESP_LOGW(TAG, "Failed to send AbortSpeaking message: %s", e.what()); + } + } else { + ESP_LOGD(TAG, "Skipping AbortSpeaking message - protocol_ is null"); + } + + // 🔧 确保中止窗口后恢复播放流程,避免长时间阻塞导致WS音频无法播放 + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(120)); + aborted_ = false; + ESP_LOGI(TAG, "🔵 Abort window ended, resume playback tasks"); + }); + + // 🔧 重置中止标志,允许后续操作 + is_aborting_.store(false); +} + +// 中止HTTPS音频播放:清空队列、重置解码器、清除标志、DMA flush +void Application::AbortHttpsPlayback(const char* reason) { + ESP_LOGI(TAG, "🔴 %s,中止HTTPS音频播放", reason); + https_playback_abort_.store(true); + { + std::lock_guard lock(mutex_); + if (!audio_decode_queue_.empty()) { + ESP_LOGI(TAG, "清空HTTPS音频队列,大小=%zu", audio_decode_queue_.size()); + audio_decode_queue_.clear(); + } + } + ResetDecoder(); + opus_playback_active_.store(false); + https_playback_active_.store(false); + ESP_LOGI(TAG, "🔴 HTTPS播放标志已清除,RTC音频通道已打开"); + // DMA flush:用独立任务立即清空I2S DMA缓冲区 + // 不能用background_task_,RTC音频lambda会持续占用它导致延迟数秒 + xTaskCreate([](void* arg) { + auto codec = Board::GetInstance().GetAudioCodec(); + if (codec) { + ESP_LOGI(TAG, "DMA flush: output_enabled=%d", codec->output_enabled()); + codec->EnableOutput(false); + vTaskDelay(pdMS_TO_TICKS(10)); + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔇 音频输出已重置,DMA缓冲区已清空"); + } + vTaskDelete(NULL); + }, "dma_flush", 4096, NULL, 10, NULL); +} + +// 通过HTTPS故事API请求并播放故事 +void Application::SendStoryRequest() { + HttpsApiPlayback(CONFIG_STORY_API_URL, "故事API", "story_play"); +} + +// 通过HTTPS音乐API请求并播放音乐 +void Application::SendMusicRequest() { + HttpsApiPlayback(CONFIG_MUSIC_API_URL, "音乐API", "music_play"); +} + +// HTTPS API音频播放通用实现(intro标题 + body正文无缝衔接) +// api_url: API基础地址(自动附加 ?mac_address=),tag: 日志标签,task_name: FreeRTOS任务名 +struct HttpsApiParams { + const char* api_url; + const char* tag; +}; + +void Application::HttpsApiPlayback(const char* api_url_base, const char* tag, const char* task_name) { + if (https_playback_active_.load() || https_playback_abort_.load() || opus_playback_active_.load()) { + ESP_LOGW(TAG, "[%s] 已有音频正在播放或退出中,忽略本次请求", tag); + return; + } + + auto* params = new HttpsApiParams{api_url_base, tag}; + xTaskCreate([](void* arg) { + auto* p = static_cast(arg); + const char* api_url_base = p->api_url; + const char* tag = p->tag; + delete p; + + auto& app = Application::GetInstance(); + // 先设置opus和abort标志(用于重复启动守卫和OnIncomingAudio阻断RTC PCM) + // 注意:https_playback_active_ 延迟到intro音频入队前设置, + // 这样HTTP请求期间(~500ms)残留的Bot subv消息不会触发OnBotMessage中止 + app.opus_playback_active_.store(true); + app.https_playback_abort_.store(false); + + // base64 解码查找表 + static uint8_t b64_table[256] = {0}; + static bool b64_inited = false; + if (!b64_inited) { + const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (int c = 0; chars[c]; c++) { + b64_table[(uint8_t)chars[c]] = (uint8_t)c; + } + b64_inited = true; + } + + // ========== 步骤1: 请求API ========== + std::string mac = SystemInfo::GetBleMacAddress(); + for (auto& c : mac) { + if (c >= 'a' && c <= 'f') c -= 32; + } + + char api_url[256]; + snprintf(api_url, sizeof(api_url), "%s?mac_address=%s", + api_url_base, mac.c_str()); + + ESP_LOGI(TAG, "[%s] 请求: %s", tag, api_url); + ESP_LOGI(TAG, "[%s] 空闲堆: %lu", tag, (unsigned long)esp_get_free_heap_size()); + + esp_http_client_config_t api_config = {}; + api_config.url = api_url; + api_config.method = HTTP_METHOD_GET; + api_config.timeout_ms = 10000; + api_config.buffer_size = 2048; + api_config.buffer_size_tx = 512; + + esp_http_client_handle_t api_client = esp_http_client_init(&api_config); + if (!api_client) { + ESP_LOGE(TAG, "[%s] HTTP客户端初始化失败", tag); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + esp_err_t err = esp_http_client_open(api_client, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "[%s] 连接失败: %s", tag, esp_err_to_name(err)); + esp_http_client_cleanup(api_client); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + esp_http_client_fetch_headers(api_client); + int api_status = esp_http_client_get_status_code(api_client); + if (api_status != 200) { + ESP_LOGE(TAG, "[%s] 请求失败,状态码: %d", tag, api_status); + esp_http_client_close(api_client); + esp_http_client_cleanup(api_client); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + // 读取API响应(通常 < 10KB) + std::string api_response; + char buf[1024]; + int rlen; + while ((rlen = esp_http_client_read(api_client, buf, sizeof(buf))) > 0) { + api_response.append(buf, rlen); + } + esp_http_client_close(api_client); + esp_http_client_cleanup(api_client); + + ESP_LOGI(TAG, "[%s] 响应: %d 字节", tag, (int)api_response.size()); + + if (app.https_playback_abort_.load()) { + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + ESP_LOGI(TAG, "[%s] HTTP请求阶段被中止", tag); + vTaskDelete(NULL); + return; + } + + // 解析外层JSON + cJSON* root = cJSON_Parse(api_response.c_str()); + api_response.clear(); + api_response.shrink_to_fit(); + + if (!root) { + ESP_LOGE(TAG, "[%s] JSON解析失败", tag); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + cJSON* code_item = cJSON_GetObjectItem(root, "code"); + if (!code_item || code_item->valueint != 0) { + cJSON* msg_item = cJSON_GetObjectItem(root, "message"); + ESP_LOGE(TAG, "[%s] 服务端错误: %s", tag, + (msg_item && msg_item->valuestring) ? msg_item->valuestring : "unknown"); + cJSON_Delete(root); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + cJSON* data = cJSON_GetObjectItem(root, "data"); + cJSON* title_item = data ? cJSON_GetObjectItem(data, "title") : nullptr; + cJSON* intro_str = data ? cJSON_GetObjectItem(data, "intro_opus_data") : nullptr; + cJSON* opus_url_item = data ? cJSON_GetObjectItem(data, "opus_url") : nullptr; + + if (!intro_str || !cJSON_IsString(intro_str) || !intro_str->valuestring || + !opus_url_item || !cJSON_IsString(opus_url_item) || !opus_url_item->valuestring) { + ESP_LOGE(TAG, "[%s] 缺少intro_opus_data或opus_url字段", tag); + cJSON_Delete(root); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + ESP_LOGI(TAG, "[%s] 标题: %s", tag, + (title_item && title_item->valuestring) ? title_item->valuestring : "未知"); + + // 提取字符串后释放外层JSON + std::string intro_json_str = intro_str->valuestring; + std::string opus_url = opus_url_item->valuestring; + cJSON_Delete(root); + + // ========== 步骤2: 解析 intro_opus_data ========== + cJSON* intro_root = cJSON_Parse(intro_json_str.c_str()); + intro_json_str.clear(); + intro_json_str.shrink_to_fit(); + + if (!intro_root) { + ESP_LOGE(TAG, "[%s] intro JSON解析失败", tag); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + cJSON* intro_sr = cJSON_GetObjectItem(intro_root, "sample_rate"); + cJSON* intro_fd = cJSON_GetObjectItem(intro_root, "frame_duration_ms"); + cJSON* intro_frames = cJSON_GetObjectItem(intro_root, "frames"); + + if (!intro_frames || !cJSON_IsArray(intro_frames)) { + ESP_LOGE(TAG, "[%s] intro缺少frames数组", tag); + cJSON_Delete(intro_root); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + int sample_rate = (intro_sr && cJSON_IsNumber(intro_sr)) ? intro_sr->valueint : 16000; + int frame_duration = (intro_fd && cJSON_IsNumber(intro_fd)) ? intro_fd->valueint : 60; + int intro_count = cJSON_GetArraySize(intro_frames); + + ESP_LOGI(TAG, "[%s] intro: 采样率=%d, 帧时长=%dms, 帧数=%d (%.1f秒)", tag, + sample_rate, frame_duration, intro_count, + intro_count * frame_duration / 1000.0f); + + app.SetDecodeSampleRate(sample_rate, frame_duration); + + // 音频即将入队,现在激活播放标志,允许OnBotMessage中止 + app.https_playback_active_.store(true); + + // ========== 步骤3: 入队 intro frames ========== + int enqueued = 0; + int errors = 0; + + for (int i = 0; i < intro_count; i++) { + if (app.https_playback_abort_.load()) break; + + cJSON* fi = cJSON_GetArrayItem(intro_frames, i); + if (!fi || !cJSON_IsString(fi) || !fi->valuestring) { errors++; continue; } + + const char* b64 = fi->valuestring; + size_t b64_len = strlen(b64); + if (b64_len == 0) { errors++; continue; } + + size_t out_len = (b64_len * 3) / 4; + if (b64_len >= 1 && b64[b64_len - 1] == '=') out_len--; + if (b64_len >= 2 && b64[b64_len - 2] == '=') out_len--; + + std::vector frame(out_len); + size_t j = 0, k = 0; + while (j < b64_len) { + uint32_t a = b64_table[(uint8_t)b64[j++]]; + uint32_t b = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t c = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t d = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d; + if (k < out_len) frame[k++] = (triple >> 16) & 0xFF; + if (k < out_len) frame[k++] = (triple >> 8) & 0xFF; + if (k < out_len) frame[k++] = triple & 0xFF; + } + + { + std::lock_guard lock(app.mutex_); + app.audio_decode_queue_.emplace_back(std::move(frame)); + } + enqueued++; + + // 队列节流 + while (!app.https_playback_abort_.load()) { + size_t qs; + { std::lock_guard lock(app.mutex_); qs = app.audio_decode_queue_.size(); } + if (qs < 50) break; + vTaskDelay(pdMS_TO_TICKS(30)); + } + } + + cJSON_Delete(intro_root); + ESP_LOGI(TAG, "[%s] intro入队完成: %d帧, 错误: %d", tag, enqueued, errors); + + if (app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[%s] intro阶段被中止", tag); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + // ========== 步骤4: 下载 opus_url 正文 ========== + ESP_LOGI(TAG, "[%s] 开始下载正文: %s", tag, opus_url.c_str()); + + esp_http_client_config_t opus_config = {}; + opus_config.url = opus_url.c_str(); + opus_config.method = HTTP_METHOD_GET; + opus_config.transport_type = HTTP_TRANSPORT_OVER_SSL; + opus_config.timeout_ms = 15000; + opus_config.buffer_size = 2048; + opus_config.buffer_size_tx = 512; +#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + opus_config.crt_bundle_attach = esp_crt_bundle_attach; +#endif + + esp_http_client_handle_t opus_client = esp_http_client_init(&opus_config); + if (!opus_client) { + ESP_LOGE(TAG, "[%s] opus HTTP初始化失败", tag); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + err = esp_http_client_open(opus_client, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "[%s] opus连接失败: %s", tag, esp_err_to_name(err)); + esp_http_client_cleanup(opus_client); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + int64_t opus_content_len = esp_http_client_fetch_headers(opus_client); + int opus_status = esp_http_client_get_status_code(opus_client); + ESP_LOGI(TAG, "[%s] opus状态码: %d, 长度: %lld", tag, opus_status, (long long)opus_content_len); + + if (opus_status != 200) { + ESP_LOGE(TAG, "[%s] opus请求失败,状态码: %d", tag, opus_status); + esp_http_client_close(opus_client); + esp_http_client_cleanup(opus_client); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + std::string opus_json; + if (opus_content_len > 0) opus_json.reserve(opus_content_len); + int total_read = 0; + while ((rlen = esp_http_client_read(opus_client, buf, sizeof(buf))) > 0) { + if (app.https_playback_abort_.load()) break; + opus_json.append(buf, rlen); + total_read += rlen; + } + esp_http_client_close(opus_client); + esp_http_client_cleanup(opus_client); + + ESP_LOGI(TAG, "[%s] opus下载完成: %d 字节, 堆: %lu", tag, + total_read, (unsigned long)esp_get_free_heap_size()); + + if (app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[%s] opus下载被中止", tag); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + // ========== 步骤5: 解析并入队 body frames ========== + cJSON* opus_root = cJSON_Parse(opus_json.c_str()); + opus_json.clear(); + opus_json.shrink_to_fit(); + ESP_LOGI(TAG, "[%s] opus JSON已释放, 堆: %lu", tag, (unsigned long)esp_get_free_heap_size()); + + if (!opus_root) { + ESP_LOGE(TAG, "[%s] opus JSON解析失败", tag); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + cJSON* body_frames = cJSON_GetObjectItem(opus_root, "frames"); + if (!body_frames || !cJSON_IsArray(body_frames)) { + ESP_LOGE(TAG, "[%s] opus缺少frames数组", tag); + cJSON_Delete(opus_root); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + // 检查body采样率是否与intro不同 + cJSON* body_sr = cJSON_GetObjectItem(opus_root, "sample_rate"); + cJSON* body_fd = cJSON_GetObjectItem(opus_root, "frame_duration_ms"); + int body_sample_rate = (body_sr && cJSON_IsNumber(body_sr)) ? body_sr->valueint : sample_rate; + int body_frame_duration = (body_fd && cJSON_IsNumber(body_fd)) ? body_fd->valueint : frame_duration; + int body_count = cJSON_GetArraySize(body_frames); + + ESP_LOGI(TAG, "[%s] body: 采样率=%d, 帧时长=%dms, 帧数=%d (%.1f秒)", tag, + body_sample_rate, body_frame_duration, body_count, + body_count * body_frame_duration / 1000.0f); + + if (body_sample_rate != sample_rate || body_frame_duration != frame_duration) { + app.SetDecodeSampleRate(body_sample_rate, body_frame_duration); + } + + int body_enqueued = 0; + int body_errors = 0; + + for (int i = 0; i < body_count; i++) { + if (app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[%s] body入队中止: %d/%d", tag, body_enqueued, body_count); + break; + } + + cJSON* fi = cJSON_GetArrayItem(body_frames, i); + if (!fi || !cJSON_IsString(fi) || !fi->valuestring) { body_errors++; continue; } + + const char* b64 = fi->valuestring; + size_t b64_len = strlen(b64); + if (b64_len == 0) { body_errors++; continue; } + + size_t out_len = (b64_len * 3) / 4; + if (b64_len >= 1 && b64[b64_len - 1] == '=') out_len--; + if (b64_len >= 2 && b64[b64_len - 2] == '=') out_len--; + + std::vector frame(out_len); + size_t j = 0, k = 0; + while (j < b64_len) { + uint32_t a = b64_table[(uint8_t)b64[j++]]; + uint32_t b = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t c = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t d = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d; + if (k < out_len) frame[k++] = (triple >> 16) & 0xFF; + if (k < out_len) frame[k++] = (triple >> 8) & 0xFF; + if (k < out_len) frame[k++] = triple & 0xFF; + } + + { + std::lock_guard lock(app.mutex_); + app.audio_decode_queue_.emplace_back(std::move(frame)); + } + body_enqueued++; + + // 队列节流 + while (!app.https_playback_abort_.load()) { + size_t qs; + { std::lock_guard lock(app.mutex_); qs = app.audio_decode_queue_.size(); } + if (qs < 50) break; + vTaskDelay(pdMS_TO_TICKS(30)); + } + + // 每100帧打印进度 + if (body_enqueued % 100 == 0) { + size_t qs; + { std::lock_guard lock(app.mutex_); qs = app.audio_decode_queue_.size(); } + ESP_LOGI(TAG, "[%s] body进度: %d/%d (%.0f%%), 队列: %zu, 堆: %lu", tag, + body_enqueued, body_count, + body_enqueued * 100.0f / body_count, qs, + (unsigned long)esp_get_free_heap_size()); + } + } + + cJSON_Delete(opus_root); + ESP_LOGI(TAG, "[%s] body入队完成: %d帧, 错误: %d", tag, body_enqueued, body_errors); + + // ========== 步骤6: 等待播放完毕 ========== + if (!app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[%s] 全部入队完成,等待播放完毕...", tag); + while (!app.https_playback_abort_.load()) { + size_t qs; + { std::lock_guard lock(app.mutex_); qs = app.audio_decode_queue_.size(); } + if (qs == 0) break; + vTaskDelay(pdMS_TO_TICKS(100)); + } + } + + bool was_aborted = app.https_playback_abort_.load(); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + ESP_LOGI(TAG, "[%s] 播放结束, aborted=%d, 堆: %lu", tag, + was_aborted, (unsigned long)esp_get_free_heap_size()); + vTaskDelete(NULL); + + }, task_name, 10240, params, 5, NULL); +} + +// 通过HTTPS下载JSON并流式播放音频(故事/歌曲等) +void Application::HttpsPlaybackFromUrl(const std::string& url) { + // 防止重复启动:opus_playback_active_ 在任务启动时设置,覆盖HTTP请求阶段 + if (https_playback_active_.load() || https_playback_abort_.load() || opus_playback_active_.load()) { + ESP_LOGW(TAG, "[HTTPS播放] 已有音频正在播放或退出中,忽略本次请求"); + return; + } + + // 在独立任务中执行,避免阻塞调用线程 + std::string url_copy = url; + xTaskCreate([](void* arg) { + std::string* url_ptr = static_cast(arg); + std::string playback_url = std::move(*url_ptr); + delete url_ptr; + + auto& app = Application::GetInstance(); + // 先设置opus和abort标志(用于重复启动守卫和OnIncomingAudio阻断RTC PCM) + // https_playback_active_ 延迟到音频入队前设置,防止残留subv触发OnBotMessage + app.opus_playback_active_.store(true); + app.https_playback_abort_.store(false); + + ESP_LOGI(TAG, "[HTTPS播放] 开始下载: %s", playback_url.c_str()); + ESP_LOGI(TAG, "[HTTPS播放] 空闲堆内存: %lu 字节", (unsigned long)esp_get_free_heap_size()); + + // 配置HTTP客户端 + esp_http_client_config_t config = {}; + config.url = playback_url.c_str(); + config.method = HTTP_METHOD_GET; + config.transport_type = HTTP_TRANSPORT_OVER_SSL; + config.timeout_ms = 15000; + config.buffer_size = 2048; // 接收缓冲区(节省内存) + config.buffer_size_tx = 512; +#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + config.crt_bundle_attach = esp_crt_bundle_attach; +#endif + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + ESP_LOGE(TAG, "[HTTPS播放] HTTP客户端初始化失败"); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + esp_err_t err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "[HTTPS播放] HTTP连接失败: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + int64_t content_length = esp_http_client_fetch_headers(client); + int status_code = esp_http_client_get_status_code(client); + ESP_LOGI(TAG, "[HTTPS播放] HTTP状态码: %d, 内容长度: %lld", status_code, (long long)content_length); + + if (status_code != 200) { + ESP_LOGE(TAG, "[HTTPS播放] HTTP请求失败,状态码: %d", status_code); + esp_http_client_close(client); + esp_http_client_cleanup(client); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + // 流式读取整个JSON(必须完整读取才能解析frames数组) + // 但使用分块读取减少单次分配峰值 + std::string json_data; + if (content_length > 0) { + json_data.reserve(content_length); + } + char read_buf[2048]; + int read_len; + int total_read = 0; + while ((read_len = esp_http_client_read(client, read_buf, sizeof(read_buf))) > 0) { + if (app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[HTTPS播放] 下载被中止"); + break; + } + json_data.append(read_buf, read_len); + total_read += read_len; + } + + // 关闭HTTP连接,释放TLS资源 + esp_http_client_close(client); + esp_http_client_cleanup(client); + ESP_LOGI(TAG, "[HTTPS播放] 下载完成: %d 字节, 堆剩余: %lu", + total_read, (unsigned long)esp_get_free_heap_size()); + + if (app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[HTTPS播放] 播放已取消,释放资源"); + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + // 解析JSON + cJSON* root = cJSON_Parse(json_data.c_str()); + // 解析完成后立即释放原始JSON字符串 + json_data.clear(); + json_data.shrink_to_fit(); + ESP_LOGI(TAG, "[HTTPS播放] JSON字符串已释放, 堆剩余: %lu", + (unsigned long)esp_get_free_heap_size()); + + if (!root) { + ESP_LOGE(TAG, "[HTTPS播放] JSON解析失败"); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + // 读取音频参数 + cJSON* sample_rate_item = cJSON_GetObjectItem(root, "sample_rate"); + cJSON* channels_item = cJSON_GetObjectItem(root, "channels"); + cJSON* frame_duration_item = cJSON_GetObjectItem(root, "frame_duration_ms"); + cJSON* frames_array = cJSON_GetObjectItem(root, "frames"); + + if (!frames_array || !cJSON_IsArray(frames_array)) { + ESP_LOGE(TAG, "[HTTPS播放] JSON中缺少frames数组"); + cJSON_Delete(root); + app.https_playback_active_.store(false); + app.opus_playback_active_.store(false); + vTaskDelete(NULL); + return; + } + + int sample_rate = (sample_rate_item && cJSON_IsNumber(sample_rate_item)) ? sample_rate_item->valueint : 16000; + int channels = (channels_item && cJSON_IsNumber(channels_item)) ? channels_item->valueint : 1; + int frame_duration = (frame_duration_item && cJSON_IsNumber(frame_duration_item)) ? frame_duration_item->valueint : 60; + int frame_count = cJSON_GetArraySize(frames_array); + + ESP_LOGI(TAG, "[HTTPS播放] 音频参数: 采样率=%d, 通道=%d, 帧时长=%dms, 总帧数=%d", + sample_rate, channels, frame_duration, frame_count); + ESP_LOGI(TAG, "[HTTPS播放] 预计时长: %.1f 秒", frame_count * frame_duration / 1000.0f); + + // 设置解码器采样率(复用现有Opus解码器) + app.SetDecodeSampleRate(sample_rate, frame_duration); + + // 音频即将入队,现在激活播放标志,允许OnBotMessage中止 + app.https_playback_active_.store(true); + + // 逐帧base64解码并入队播放 + int enqueued = 0; + int decode_errors = 0; + + // base64 解码查找表(C++ 兼容初始化) + static uint8_t b64_table[256] = {0}; + static bool b64_inited = false; + if (!b64_inited) { + const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (int c = 0; chars[c]; c++) { + b64_table[(uint8_t)chars[c]] = (uint8_t)c; + } + b64_inited = true; + } + + for (int i = 0; i < frame_count; i++) { + if (app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[HTTPS播放] 播放中止,已入队 %d/%d 帧", enqueued, frame_count); + break; + } + + cJSON* frame_item = cJSON_GetArrayItem(frames_array, i); + if (!frame_item || !cJSON_IsString(frame_item) || !frame_item->valuestring) { + decode_errors++; + continue; + } + + const char* b64 = frame_item->valuestring; + size_t b64_len = strlen(b64); + if (b64_len == 0) { + decode_errors++; + continue; + } + + // base64 解码 + size_t out_len = (b64_len * 3) / 4; + if (b64_len >= 1 && b64[b64_len - 1] == '=') out_len--; + if (b64_len >= 2 && b64[b64_len - 2] == '=') out_len--; + + std::vector opus_frame(out_len); + size_t j = 0, k = 0; + while (j < b64_len) { + uint32_t sextet_a = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t sextet_b = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t sextet_c = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t sextet_d = (j < b64_len) ? b64_table[(uint8_t)b64[j++]] : 0; + uint32_t triple = (sextet_a << 18) | (sextet_b << 12) | (sextet_c << 6) | sextet_d; + if (k < out_len) opus_frame[k++] = (triple >> 16) & 0xFF; + if (k < out_len) opus_frame[k++] = (triple >> 8) & 0xFF; + if (k < out_len) opus_frame[k++] = triple & 0xFF; + } + + // 入队到音频解码队列(和WebSocket入队方式完全一致) + { + std::lock_guard lock(app.mutex_); + app.audio_decode_queue_.emplace_back(std::move(opus_frame)); + } + enqueued++; + + // 控制入队速度:队列过大时等待消费,避免内存堆积 + // 每帧60ms,队列超过50帧(3秒缓冲)时等待 + while (!app.https_playback_abort_.load()) { + size_t queue_size; + { + std::lock_guard lock(app.mutex_); + queue_size = app.audio_decode_queue_.size(); + } + if (queue_size < 50) break; + vTaskDelay(pdMS_TO_TICKS(30)); // 等待消费 + } + + // 每100帧打印一次进度 + if (enqueued % 100 == 0) { + size_t queue_size; + { + std::lock_guard lock(app.mutex_); + queue_size = app.audio_decode_queue_.size(); + } + ESP_LOGI(TAG, "[HTTPS播放] 进度: %d/%d 帧 (%.0f%%), 队列: %zu, 堆: %lu", + enqueued, frame_count, enqueued * 100.0f / frame_count, + queue_size, (unsigned long)esp_get_free_heap_size()); + } + } + + // 释放cJSON解析树 + cJSON_Delete(root); + ESP_LOGI(TAG, "[HTTPS播放] JSON解析树已释放, 堆剩余: %lu", + (unsigned long)esp_get_free_heap_size()); + + if (app.https_playback_abort_.load()) { + ESP_LOGI(TAG, "[HTTPS播放] 播放被用户中止,入队 %d 帧,解码错误 %d", + enqueued, decode_errors); + } else { + ESP_LOGI(TAG, "[HTTPS播放] 全部入队完成: %d 帧,解码错误 %d,等待播放完毕...", + enqueued, decode_errors); + } + + // 等待队列播放完毕(或被中止) + while (!app.https_playback_abort_.load()) { + size_t queue_size; + { + std::lock_guard lock(app.mutex_); + queue_size = app.audio_decode_queue_.size(); + } + if (queue_size == 0) break; + vTaskDelay(pdMS_TO_TICKS(100)); + } + + app.https_playback_active_.store(false); + app.https_playback_abort_.store(false); + app.opus_playback_active_.store(false); + ESP_LOGI(TAG, "[HTTPS播放] 播放结束, 最终堆剩余: %lu", + (unsigned long)esp_get_free_heap_size()); + vTaskDelete(NULL); + }, "https_play", 8192, new std::string(url_copy), 5, NULL); +} + +// 设置监听模式 +void Application::SetListeningMode(ListeningMode mode) { + ESP_LOGI(TAG, "Setting listening mode to %d", (int)mode);// 打印设置监听模式日志 + listening_mode_ = mode; + SetDeviceState(kDeviceStateListening); +} + +// 设置设备状态 +void Application::SetDeviceState(DeviceState state) { + if (device_state_ == state) { + return; + } + + clock_ticks_ = 0; + auto previous_state = device_state_;// 记录上一个设备状态 + device_state_ = state;// 设置设备状态 + if (state == kDeviceStateDialog) { + StartDialogWatchdog(); + } else if (previous_state == kDeviceStateDialog) { + StopDialogWatchdog(); + } + ESP_LOGI(TAG, "打印设置设备状态日志: %s", STATE_STRINGS[device_state_]);// 打印设置设备状态日志 + // The state is changed, wait for all background tasks to finish + background_task_->WaitForCompletion(); + + auto& board = Board::GetInstance(); + auto display = board.GetDisplay(); + auto led = board.GetLed(); + led->OnStateChanged(); + + // 检查是否正在进行BLE配网,配网时禁止播放待命音效(新增代码) + // ================================================================= + bool is_ble_provisioning = false; + if (Board::GetInstance().GetBoardType() == "wifi") { + auto& wifi_board = static_cast(Board::GetInstance()); + is_ble_provisioning = wifi_board.IsBleProvisioningActive(); + } + // ================================================================= + + switch (state) { + case kDeviceStateUnknown: + case kDeviceStateIdle: + display->SetStatus(Lang::Strings::STANDBY); + display->SetEmotion("neutral"); + + + // // 只有从非待命状态进入待命状态时才播放待命音效,避免重复播放(原来的代码) + // if (previous_state != kDeviceStateIdle && + // previous_state != kDeviceStateUnknown && + // previous_state != kDeviceStateWifiConfiguring) { + // ESP_LOGI(TAG, "Entering idle state, playing standby sound"); + // PlaySound(Lang::Sounds::P3_DAIMING); + // } + // 开机后 进入待命状态 播报 卡卡正在待命(配网模式下不播报“卡卡正在待命”)-新增代码 + //===================================================================================== + if (previous_state != kDeviceStateIdle && previous_state != kDeviceStateUnknown && + previous_state != kDeviceStateWifiConfiguring && !is_ble_provisioning && !IsLowBatteryTransition()) { + ESP_LOGI(TAG, "Entering idle state, playing standby sound"); + // PlaySound(Lang::Sounds::P3_DAIMING); 原有 待命 播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + PlaySound(Lang::Sounds::P3_KAKA_DAIMING); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + PlaySound(Lang::Sounds::P3_LALA_DAIMING); + } + } + //===================================================================================== + +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.Stop(); +#endif +#if 1 + if (recorder_pipeline_) { + recorder_pipeline_close(recorder_pipeline_); + recorder_pipeline_ = nullptr; + } +#endif +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + wake_word_detect_.Start(); +#endif + // 设备开机后首次进入idle状态时,自动检测NVS中的位置并调用API后设置当前位置 + if (!first_idle_location_checked_) { + first_idle_location_checked_ = true;// 首次查询城市天气 + Schedule([]() { + AutoDetectAndSetLocation();// 自动检测并设置当前位置 + }); + } + break; + case kDeviceStateConnecting: + display->SetStatus(Lang::Strings::CONNECTING); + display->SetEmotion("neutral"); + display->SetChatMessage("system", ""); + break; + case kDeviceStateListening: + display->SetStatus(Lang::Strings::LISTENING); + display->SetEmotion("neutral"); + + // 关键修复:只有在非音效播放状态下才重置音量,避免中断正在播放的音效 + // 检查是否有音频正在播放,如果有则延迟重置音量 + if (IsAudioQueueEmpty()) { + current_speaker_volume_ = 0.0f; +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(0.0f); +#endif + } else { + // 如果有音频正在播放,延迟重置音量 + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(500)); // 等待音效播放完成 + current_speaker_volume_ = 0.0f; +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(0.0f); +#endif + }); + } + + // Update the IoT states before sending the start listening command + UpdateIotStates(); + + // Make sure the audio processor is running +#if CONFIG_USE_AUDIO_PROCESSOR + if (!audio_processor_.IsRunning()) { +#else + if (true) { +#endif + // 🔧 关键修复:检查协议连接状态,防止发送到无效连接 + if (protocol_ && protocol_->IsAudioChannelOpened()) { + // Send the start listening command + protocol_->SendStartListening(listening_mode_); + if (listening_mode_ == kListeningModeAutoStop && previous_state == kDeviceStateSpeaking) { + // FIXME: Wait for the speaker to empty the buffer + vTaskDelay(pdMS_TO_TICKS(120)); + } + opus_encoder_->ResetState(); +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + wake_word_detect_.Stop(); +#endif +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.Start(); +#endif + if (!recorder_pipeline_) { + recorder_pipeline_ = recorder_pipeline_open(); + recorder_pipeline_run(recorder_pipeline_); + } + } else { + ESP_LOGW(TAG, "Audio channel not available, skipping SendStartListening");// 音频通道未打开,跳过发送开始聆听命令 + // 保持在聆听状态,不自动回退到idle状态 + ESP_LOGI(TAG, "🔵 Staying in listening state despite audio channel unavailable"); + } + } + break; + case kDeviceStateSpeaking: + display->SetStatus(Lang::Strings::SPEAKING); + + if (listening_mode_ != kListeningModeRealtime) { +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.Stop(); +#endif +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + wake_word_detect_.Start(); +#endif + } else { + // 在实时模式下,保持audio_processor运行以检测语音打断 +#if CONFIG_USE_AUDIO_PROCESSOR + if (!audio_processor_.IsRunning()) { + audio_processor_.Start(); + } +#endif +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + wake_word_detect_.Stop(); +#endif + } + ResetDecoder(); + break; + case kDeviceStateDialog: + display->SetStatus(Lang::Strings::SPEAKING); +#if CONFIG_USE_AUDIO_PROCESSOR + if (!audio_processor_.IsRunning()) { + audio_processor_.Start(); + } +#endif +#if CONFIG_USE_WAKE_WORD_DETECT || CONFIG_USE_CUSTOM_WAKE_WORD + wake_word_detect_.Stop(); +#endif + { + auto codec2 = Board::GetInstance().GetAudioCodec();// 获取音频编解码器 + codec2->EnableOutput(true);// 启用音频输出 + last_audible_output_time_ = std::chrono::steady_clock::now();// 更新最后有声音输出的时间 + } + if (!recorder_pipeline_) { + recorder_pipeline_ = recorder_pipeline_open(); + recorder_pipeline_run(recorder_pipeline_); + } + break; + default: + // Do nothing + break; + } +} + +void Application::ResetDecoder() { + std::lock_guard lock(mutex_); + opus_decoder_->ResetState(); + audio_decode_queue_.clear(); + last_output_time_ = std::chrono::steady_clock::now(); + + auto codec = Board::GetInstance().GetAudioCodec(); + codec->EnableOutput(true); +} + +void Application::SetDecodeSampleRate(int sample_rate, int frame_duration) { + if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) { + return; + } + + opus_decoder_.reset(); + opus_decoder_ = std::make_unique(sample_rate, 1, frame_duration); + + auto codec = Board::GetInstance().GetAudioCodec(); + if (opus_decoder_->sample_rate() != codec->output_sample_rate()) { + ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate()); + output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate()); + } +} + +void Application::UpdateIotStates() { + auto& thing_manager = iot::ThingManager::GetInstance(); + std::string states; + if (thing_manager.GetStatesJson(states, true)) { + protocol_->SendIotStates(states); + } +} + +void Application::Reboot() { + ESP_LOGI(TAG, "Rebooting..."); + esp_restart(); +} + +// 唤醒词触发函数 +void Application::WakeWordInvoke(const std::string& wake_word) { + if (device_state_ == kDeviceStateIdle) { + ToggleChatState(); + Schedule([this, wake_word]() { + if (protocol_) { + protocol_->SendWakeWordDetected(wake_word); + } + }); + } else if (device_state_ == kDeviceStateSpeaking) { + //AbortSpeakingAndReturnToListening();// 使用唤醒词打断时立即切换到聆听状态 + Schedule([this]() { + AbortSpeaking(kAbortReasonNone); + }); + } else if (device_state_ == kDeviceStateListening) { + Schedule([this]() { + if (protocol_) { + protocol_->CloseAudioChannel(); + } + }); + } +} + +bool Application::CanEnterSleepMode() { + if (device_state_ != kDeviceStateIdle) { + return false; + } + + if (protocol_ && protocol_->IsAudioChannelOpened()) { + return false; + } + + // Now it is safe to enter sleep mode + return true; +} +void Application::WaitForAudioPlayback() { + // 等待 audio_decode_queue_ 清空且音频输出完成 + auto codec = Board::GetInstance().GetAudioCodec(); + int timeout_count = 0; + const int max_timeout = 150; // 3秒超时 (150 * 20ms = 3000ms) + + while (timeout_count < max_timeout) { + { + std::lock_guard lock(mutex_); + if (audio_decode_queue_.empty()) { + // 检查音频输出是否已关闭或静音 + if (!codec->output_enabled() || device_state_ != kDeviceStateSpeaking) { + ESP_LOGI(TAG, "Audio playback completed"); + break; + } + } + } + vTaskDelay(pdMS_TO_TICKS(20)); + timeout_count++; + } + + if (timeout_count >= max_timeout) { + ESP_LOGW(TAG, "WaitForAudioPlayback timeout after 3 seconds"); + } +} + +bool Application::IsAudioQueueEmpty() { + std::lock_guard lock(mutex_); + return audio_decode_queue_.empty(); +} + +void Application::ClearAudioQueue() { + std::lock_guard lock(mutex_); + audio_decode_queue_.clear(); + audio_paused_ = false; // 清除暂停状态 + // ESP_LOGI(TAG, "🧹 音频播放队列已清空,暂停状态已清除"); + ESP_LOGI(TAG, "🎵 测试模式:音频开始播放,等待播放完成"); // 生产测试打印 + + + // 重新启用音频编解码器输出,确保后续音频能正常播放 + auto& board = Board::GetInstance(); + auto codec = board.GetAudioCodec(); + if (codec) { + codec->EnableOutput(true); + // ESP_LOGI(TAG, "🔧 音频编解码器输出已重新启用"); + ESP_LOGI(TAG, "✅ 测试模式:音频播放完成"); // 生产测试打印 + } +} + +// 🔧 检查当前是否可以安全执行操作 +bool Application::IsSafeToOperate() { + // 检查是否正在执行中止操作 + if (is_aborting_.load()) { + return false; + } + + // 检查最近是否有操作过于频繁 + auto now = std::chrono::steady_clock::now(); + auto last_op = last_safe_operation_.load(); + auto time_diff = std::chrono::duration_cast(now - last_op); + + // 如果距离上次操作少于50ms,认为可能存在竞态风险 + if (time_diff.count() < 50) { + ESP_LOGD(TAG, "Operation too frequent, waiting for safety"); + return false; + } + + return true; +} + +void Application::StopAudioProcessor() { +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.Stop(); +#endif +} + +// 🔴 专门处理从说话状态到空闲状态的切换 +void Application::AbortSpeakingAndReturnToIdle() { + ESP_LOGI(TAG, "🔴 AbortSpeakingAndReturnToIdle: Starting transition from speaking to idle state"); + ESP_LOGI(TAG, "📊 当前设备状态: %s", STATE_STRINGS[device_state_]); + ESP_LOGI(TAG, "🎯 目标状态: idle (空闲状态)"); + + // 检查当前状态是否为说话状态 + if (device_state_ != kDeviceStateSpeaking) { + ESP_LOGW(TAG, "🔴 AbortSpeakingAndReturnToIdle: Device not in speaking state, current state: %s", STATE_STRINGS[device_state_]); + return; + } + + ESP_LOGI(TAG, "✅ 状态检查通过,当前处于说话状态"); + + // 检查操作安全性 + if (!IsSafeToOperate()) { + ESP_LOGW(TAG, "🔴 AbortSpeakingAndReturnToIdle: Operation not safe, scheduling retry"); + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(100)); + AbortSpeakingAndReturnToIdle(); + }); + return; + } + + // 更新安全操作时间戳 + last_safe_operation_.store(std::chrono::steady_clock::now()); + ESP_LOGI(TAG, "⏰ 安全操作时间戳已更新"); + + // 立即停止音频处理 + ESP_LOGI(TAG, "🔇 开始停止音频处理"); + { + std::lock_guard lock(mutex_); + if (!audio_decode_queue_.empty()) { + ESP_LOGI(TAG, "🗑️ 清空音频队列,当前队列大小: %zu", audio_decode_queue_.size()); + audio_decode_queue_.clear(); + current_speaker_volume_ = 0.0f; +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(0.0f); +#endif + ESP_LOGI(TAG, "✅ 音频队列已清空,音量已重置为0"); + } else { + ESP_LOGI(TAG, "ℹ️ 音频队列已为空,无需清空"); + } + } + + ESP_LOGI(TAG, "🔴 AbortSpeakingAndReturnToIdle: Sending abort message to server"); + + // 发送中止消息给服务器 + if (protocol_ && protocol_->IsAudioChannelOpened()) { + ESP_LOGI(TAG, "📡 WebSocket连接正常,发送中止消息"); + try { + protocol_->SendAbortSpeaking(kAbortReasonNone); + ESP_LOGI(TAG, "✅ 中止消息发送成功"); + } catch (const std::exception& e) { + ESP_LOGW(TAG, "❌ 发送中止消息失败: %s", e.what()); + } + + // 延迟100ms后主动关闭连接,确保服务器有时间处理中止消息 + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_LOGI(TAG, "⏳ 延迟100ms后开始主动关闭WebSocket连接"); + ESP_LOGI(TAG, "🔌 执行主动断开WebSocket连接"); + if (protocol_) { + protocol_->CloseAudioChannel(); + ESP_LOGI(TAG, "✅ CloseAudioChannel调用完成"); + } else { + ESP_LOGW(TAG, "⚠️ protocol_为空,无法关闭音频通道"); + } + }); + } else { + ESP_LOGW(TAG, "⚠️ WebSocket连接不可用,强制关闭连接"); + if (protocol_) { + ESP_LOGI(TAG, "🔌 强制执行WebSocket断开"); + protocol_->CloseAudioChannel(); + ESP_LOGI(TAG, "✅ 强制断开完成"); + } else { + ESP_LOGW(TAG, "❌ protocol_为空,无法执行断开操作"); + } + } + + ESP_LOGI(TAG, "🎯 主动断开流程已启动,等待OnAudioChannelClosed回调触发状态转换"); + ESP_LOGI(TAG, "📋 预期流程: WebSocket断开 → 回调触发 → 转换到idle状态 → 播放待机音"); +} + +// 🔵 专门处理从说话状态到聆听状态的切换 +void Application::AbortSpeakingAndReturnToListening() { + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Starting transition from speaking to listening state (断开连接方案)"); + + // 检查当前状态是否为说话状态或可切换状态 + // ========================================================================================= + if (device_state_ != kDeviceStateSpeaking && device_state_ != kDeviceStateListening && device_state_ != kDeviceStateIdle) { + ESP_LOGW(TAG, "🔵 AbortSpeakingAndReturnToListening: Device not in valid state for transition, current state: %s", STATE_STRINGS[device_state_]); + return; + } + // 如果已经在listening状态,直接返回避免重复切换 + if (device_state_ == kDeviceStateListening) { + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Already in listening state, skipping transition"); + return; + } + // 🔧 检查并处理音频播放状态(BOOT按键优化方案) + if (!audio_paused_ && device_state_ == kDeviceStateSpeaking) { + ESP_LOGI(TAG, "🔵 检测到播放状态,一次按键完成暂停和状态切换"); + + // 第一步:禁用音频输出(立即停止播放) + auto& board = Board::GetInstance();// 获取音频编解码器 + auto codec = board.GetAudioCodec();// 获取音频编解码器 + if (codec) { + codec->EnableOutput(false);// 暂停时禁用音频编解码器输出 + ESP_LOGI(TAG, "🔧 暂停时禁用音频编解码器输出"); + } + // 第二步:切换到暂停状态 + audio_paused_ = true; + ESP_LOGI(TAG, "✅ 已切换到暂停状态"); + // 第三步:立即执行状态切换逻辑(不返回,继续执行下面的代码) + ESP_LOGI(TAG, "🔵 继续执行状态切换到聆听状态"); + } + + // 🔧 检查并处理音频暂停状态(BOOT按键优化方案) + if (audio_paused_) { + ESP_LOGI(TAG, "🔵 检测到音频暂停状态,应用BOOT按键优化方案"); + audio_paused_ = false; + ESP_LOGI(TAG, "✅ 音频暂停状态已清除"); + + // 🔧 关键优化:清空音频播放队列,避免播放暂停时残留的音频 + std::unique_lock lock(mutex_); + audio_decode_queue_.clear(); + lock.unlock(); + ESP_LOGI(TAG, "🧹 已清空音频播放队列,避免播放残留音频"); + + // BOOT按键切换时的优化方案:确保音频系统能正常响应状态切换 + auto& board = Board::GetInstance(); + auto codec = board.GetAudioCodec(); + if (codec) { + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔧 为状态切换重新启用音频编解码器输出");// 重新启用输出,后续可以播放 + } + // 🔧 关键修复:强制停止音频处理器,确保后续状态切换时能重新启动 +#if CONFIG_USE_AUDIO_PROCESSOR + if (audio_processor_.IsRunning()) { + ESP_LOGI(TAG, "🔧 强制停止音频处理器以确保状态切换成功"); + audio_processor_.Stop(); + } +#endif + + // 🔧 音频暂停状态下直接切换,避免复杂的异步处理 + ESP_LOGI(TAG, "🔵 音频暂停状态下直接执行状态切换"); + + // 播放提示音 + if (codec && codec->output_enabled()) { + ESP_LOGI(TAG, "播放提示音:卡卡在呢"); + // PlaySound(Lang::Sounds::P3_KAKAZAINNE); 原有蜡笔小新 音色播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + PlaySound(Lang::Sounds::P3_KAKA_ZAINNE); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + PlaySound(Lang::Sounds::P3_LALA_ZAINNE); + } + + + // 简化等待逻辑 + vTaskDelay(pdMS_TO_TICKS(620)); // 等待音效播放完成 + ESP_LOGI(TAG, "音频播放完成"); + } + + // 直接切换到聆听状态 + SetDeviceState(kDeviceStateListening); + ESP_LOGI(TAG, "🔵 音频暂停状态下状态切换完成"); + return; + } + // ========================================================================================= + + // 检查操作安全性 + if (!IsSafeToOperate()) { + ESP_LOGW(TAG, "🔵 AbortSpeakingAndReturnToListening: Operation not safe, scheduling retry"); + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(100)); + AbortSpeakingAndReturnToListening(); + }); + return; + } + + // 更新安全操作时间戳 + last_safe_operation_.store(std::chrono::steady_clock::now()); + + // 立即停止音频处理器和清空音频队列 +#if CONFIG_USE_AUDIO_PROCESSOR + if (audio_processor_.IsRunning()) { + ESP_LOGI(TAG, "🔵 停止音频处理器"); + audio_processor_.Stop(); + } + + // 清空音频队列并重置音量 + if (!IsAudioQueueEmpty()) { + ESP_LOGI(TAG, "🔵 清空音频队列并重置音量"); + while (!IsAudioQueueEmpty()) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + current_speaker_volume_ = 0.0f; + audio_processor_.SetSpeakerVolume(0.0f); + ESP_LOGI(TAG, "✅ 音频队列已清空,音量已重置为0"); + } else { + ESP_LOGI(TAG, "ℹ️ 音频队列已为空,无需清空"); + } +#endif + + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Sending abort message to server"); + + // 发送中止消息给服务器 + if (protocol_ && protocol_->IsAudioChannelOpened()) { + ESP_LOGI(TAG, "📡 WebSocket连接正常,发送中止消息"); + try { + protocol_->SendAbortSpeaking(kAbortReasonVoiceInterrupt); + ESP_LOGI(TAG, "✅ 中止消息发送成功"); + } catch (const std::exception& e) { + ESP_LOGW(TAG, "❌ 发送中止消息失败: %s", e.what()); + } + + // 延迟100ms后播放音效并直接切换到聆听状态,不关闭WebSocket连接 + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_LOGI(TAG, "⏳ 延迟100ms后播放音效并切换到聆听状态"); + + // 先播放"卡卡在呢"音效 + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Playing KAKAZAINNE sound"); + + // 🔧 修复:STATE:,确保硬件状态正确 + auto& board = Board::GetInstance(); + auto audio_codec = board.GetAudioCodec(); + ESP_LOGI(TAG, "强制重新初始化音频输出"); + audio_codec->EnableOutput(false); // 先关闭音频输出 + vTaskDelay(pdMS_TO_TICKS(50)); // 短暂延迟让硬件复位 + audio_codec->EnableOutput(true); // 再开启,强制硬件重新初始化 + + // 🔧 检查音频资源是否可用 + if (audio_codec->output_enabled()) { + ESP_LOGI(TAG, "播放提示音:卡卡在呢"); + ResetDecoder(); // 🔧 关键修复:重置解码器状态,清除残留 + + // 获取当前系统音量并临时设置以确保音效能播放 + float system_volume = audio_codec ? (audio_codec->output_volume() / 100.0f) : 0.7f; // 默认70% + current_speaker_volume_ = system_volume; +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(system_volume); + ESP_LOGI(TAG, "✅ 音量设置成功: %.2f", system_volume); +#endif + + // PlaySound(Lang::Sounds::P3_KAKAZAINNE); 原有蜡笔小新 音色播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + PlaySound(Lang::Sounds::P3_KAKA_ZAINNE); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + PlaySound(Lang::Sounds::P3_LALA_ZAINNE); + } + + // 🔧 修复:使用改进的等待逻辑,确保音频真正播放完成 + ESP_LOGI(TAG, "等待音频播放完成..."); + vTaskDelay(pdMS_TO_TICKS(100)); // 给音频足够的时间开始播放 + + // 等待音频队列清空 + 额外缓冲时间确保I2S硬件完成输出 + int timeout_count = 0; + const int max_timeout = 150; // 3秒超时 + + while (timeout_count < max_timeout) { + if (IsAudioQueueEmpty()) { + // 队列清空后,再等待500ms确保I2S硬件完成输出 + ESP_LOGI(TAG, "音频队列已清空,等待硬件输出完成..."); + vTaskDelay(pdMS_TO_TICKS(500)); + ESP_LOGI(TAG, "音频播放完成"); + break; + } + vTaskDelay(pdMS_TO_TICKS(20)); + timeout_count++; + } + + if (timeout_count >= max_timeout) { + ESP_LOGW(TAG, "等待音频播放超时,继续状态切换"); + } + } else { + ESP_LOGW(TAG, "音频输出无法启用,跳过提示音播放"); + } + + // 直接切换到聆听状态,音频播放已在上面完成 + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Switching to listening state (保持WebSocket连接)"); + SetDeviceState(kDeviceStateListening); + }); + } else { + ESP_LOGW(TAG, "⚠️ WebSocket连接不可用,直接切换状态"); + + // 直接播放音效并切换状态 + Schedule([this]() { + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Playing KAKAZAINNE sound"); + + // 🔧 修复:强制重新初始化音频输出,确保硬件状态正确 + auto& board = Board::GetInstance(); + auto audio_codec = board.GetAudioCodec(); + ESP_LOGI(TAG, "强制重新初始化音频输出"); + audio_codec->EnableOutput(false); // 先关闭音频输出 + vTaskDelay(pdMS_TO_TICKS(50)); // 短暂延迟让硬件复位 + audio_codec->EnableOutput(true); // 再开启,强制硬件重新初始化 + + // 🔧 检查音频资源是否可用 + if (audio_codec->output_enabled()) { + ESP_LOGI(TAG, "播放提示音:卡卡在呢"); + ResetDecoder(); // 🔧 关键修复:重置解码器状态,清除残留 + + // 获取当前系统音量并临时设置以确保音效能播放 + float system_volume = audio_codec ? (audio_codec->output_volume() / 100.0f) : 0.7f; // 默认70% + current_speaker_volume_ = system_volume; +#if CONFIG_USE_AUDIO_PROCESSOR + audio_processor_.SetSpeakerVolume(system_volume); + ESP_LOGI(TAG, "✅ 音量设置成功: %.2f", system_volume); +#endif + + // PlaySound(Lang::Sounds::P3_KAKAZAINNE); 原有蜡笔小新 音色播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + PlaySound(Lang::Sounds::P3_KAKA_ZAINNE); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + PlaySound(Lang::Sounds::P3_LALA_ZAINNE); + } + + // 🔧 修复:使用改进的等待逻辑,确保音频真正播放完成 + ESP_LOGI(TAG, "等待音频播放完成..."); + vTaskDelay(pdMS_TO_TICKS(100)); // 给音频足够的时间开始播放 + + // 等待音频队列清空 + 额外缓冲时间确保I2S硬件完成输出 + int timeout_count = 0; + const int max_timeout = 150; // 3秒超时 + + while (timeout_count < max_timeout) { + if (IsAudioQueueEmpty()) { + // 队列清空后,再等待500ms确保I2S硬件完成输出 + ESP_LOGI(TAG, "音频队列已清空,等待硬件输出完成..."); + vTaskDelay(pdMS_TO_TICKS(500)); + ESP_LOGI(TAG, "音频播放完成"); + break; + } + vTaskDelay(pdMS_TO_TICKS(20)); + timeout_count++; + } + + if (timeout_count >= max_timeout) { + ESP_LOGW(TAG, "等待音频播放超时,继续状态切换"); + } + } else { + ESP_LOGW(TAG, "音频输出无法启用,跳过提示音播放"); + } + + // 直接切换到聆听状态,音频播放已在上面完成 + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Switching to listening state"); + SetDeviceState(kDeviceStateListening); + }); + } + + ESP_LOGI(TAG, "🔵 AbortSpeakingAndReturnToListening: Transition initiated - keeping WebSocket connection and switching to listening"); +} + +// 姿态传感器接口实现 +bool Application::IsImuSensorAvailable() { + auto& board = Board::GetInstance(); + if (board.GetBoardType() == "movecall-moji-esp32s3") { + auto& moji_board = static_cast(board); + return moji_board.IsImuInitialized(); + } + return false; +} + +bool Application::GetImuData(float* acc_x, float* acc_y, float* acc_z, + float* gyro_x, float* gyro_y, float* gyro_z, + float* temperature) { + auto& board = Board::GetInstance(); + if (board.GetBoardType() == "movecall-moji-esp32s3") { + auto& moji_board = static_cast(board); + qmi8658a_data_t imu_data; + if (moji_board.GetImuData(&imu_data)) { + if (acc_x) *acc_x = imu_data.acc_x; + if (acc_y) *acc_y = imu_data.acc_y; + if (acc_z) *acc_z = imu_data.acc_z; + if (gyro_x) *gyro_x = imu_data.gyro_x; + if (gyro_y) *gyro_y = imu_data.gyro_y; + if (gyro_z) *gyro_z = imu_data.gyro_z; + if (temperature) *temperature = imu_data.temperature; + return true; + } + } + return false; +} + +void Application::ClearDialogIdleSkipSession() { + // 清除内存中的跳过标志 + skip_dialog_idle_session_ = false; + + // 清除NVS中的标志 + Settings sys("system", true); + sys.SetInt("reboot_dlg_idle", 0); + sys.SetInt("reboot_origin", 0); + sys.Commit(); + ESP_LOGI(TAG, "跳过对话待机会话标志已清除"); +} + +void Application::SetDialogUploadEnabled(bool enabled) { + dialog_upload_enabled_ = enabled; + ESP_LOGI(TAG, "对话上传状态已设置为: %s", enabled ? "启用" : "禁用"); +} + +void Application::OnMotionDetected() { + ESP_LOGI(TAG, "Motion detected by IMU sensor"); + + // 如果设备处于空闲状态,可以触发一些动作 + if (device_state_ == kDeviceStateIdle) { + // 例如:显示运动检测提示 + auto& board = Board::GetInstance(); + auto display = board.GetDisplay(); + display->SetChatMessage("system", "检测到运动"); + + // 可以在这里添加更多的运动检测处理逻辑 + // 比如:唤醒设备、记录运动数据等 + } +} + +void Application::SetLowBatteryTransition(bool value) { + is_low_battery_transition_.store(value);// 设置低电量过渡状态 +} + +bool Application::IsLowBatteryTransition() const { + return is_low_battery_transition_.load();// 获取低电量过渡状态 +} + +// 🌐 初始化WebSocket协议(RTC连接成功后调用) +void Application::InitializeWebsocketProtocol() { + ESP_LOGI(TAG, "🌐 开始初始化WebSocket协议..."); + + // 检查是否已经初始化过 + if (websocket_protocol_) { + ESP_LOGW(TAG, "⚠️ WebSocket协议已经初始化,跳过重复初始化"); + return; + } + + // 创建WebsocketProtocol实例 + ESP_LOGI(TAG, "🔧 创建WebsocketProtocol实例"); + websocket_protocol_ = std::make_unique(); + websocket_protocol_->SetPrimary(false); + websocket_protocol_->OnIncomingAudio([this](std::vector&& data) { + if (!ws_downlink_enabled_.load()) { + return; + } + opus_playback_active_.store(true); + std::lock_guard lock(mutex_); + size_t len = data.size(); + audio_decode_queue_.emplace_back(std::move(data)); + ESP_LOGD(TAG, "WS辅助音频入队: 字节=%zu 队列大小=%zu", len, audio_decode_queue_.size()); + }); + websocket_protocol_->OnIncomingJson([this](const cJSON* root) { + auto type = cJSON_GetObjectItem(root, "type"); + if (type && cJSON_IsString(type) && type->valuestring) { + ESP_LOGD(TAG, "WS辅助JSON消息: %s", type->valuestring); + if (strcmp(type->valuestring, "hello") == 0) { + auto audio_params = cJSON_GetObjectItem(root, "audio_params"); + if (audio_params && cJSON_IsObject(audio_params)) { + auto sr = cJSON_GetObjectItem(audio_params, "sample_rate"); + auto fd = cJSON_GetObjectItem(audio_params, "frame_duration"); + int sample_rate = (sr && cJSON_IsNumber(sr)) ? sr->valueint : 16000; + int frame_duration = (fd && cJSON_IsNumber(fd)) ? fd->valueint : 60; + SetDecodeSampleRate(sample_rate, frame_duration); + } else { + SetDecodeSampleRate(16000, 60); + } + } + } + }); + + // 启动WebSocket协议 + ESP_LOGI(TAG, "🚀 启动WebSocket协议"); + websocket_protocol_->Start();// 启动WebSocket协议 + + ESP_LOGI(TAG, "✅ WebSocket协议初始化完成"); +} + +// void Application::SendTextViaWebsocket(const std::string& text) { +// Schedule([this, text]() { +// if (websocket_protocol_ && websocket_protocol_->IsAudioChannelOpened()) { +// websocket_protocol_->SendTextMessage(text); +// ESP_LOGI(TAG, "WS辅助文本发送:%s", text.c_str()); +// } else { +// ESP_LOGW(TAG, "WS辅助未连接,丢弃文本:%s", text.c_str()); +// } +// }); +// } + +// ============================================================ +// BLE JSON 通讯服务集成 +// ============================================================ + +const char* Application::DeviceStateToString(DeviceState state) { + int idx = static_cast(state); + if (idx >= 0 && idx < static_cast(sizeof(STATE_STRINGS) / sizeof(STATE_STRINGS[0]))) { + return STATE_STRINGS[idx]; + } + return "unknown"; +} + +// BLE JSON Service 命令处理(暂不使用,保留代码) +#if 0 +void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data, BleJsonService& service) { + auto& board = Board::GetInstance(); + + if (cmd == "ping") { + service.SendResponse(cmd, msg_id, 0, "pong"); + return; + } + + if (cmd == "status") { + cJSON* resp = cJSON_CreateObject(); + cJSON_AddStringToObject(resp, "s", DeviceStateToString(device_state_)); + int battery_level = 0; + bool charging = false, discharging = false; + if (board.GetBatteryLevel(battery_level, charging, discharging)) { + cJSON_AddNumberToObject(resp, "bat", battery_level); + cJSON_AddBoolToObject(resp, "chg", charging); + } + auto* codec = board.GetAudioCodec(); + if (codec) { + cJSON_AddNumberToObject(resp, "vol", codec->output_volume()); + } + wifi_ap_record_t ap{}; + if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) { + cJSON_AddStringToObject(resp, "ssid", reinterpret_cast(ap.ssid)); + cJSON_AddNumberToObject(resp, "rssi", ap.rssi); + } + service.SendResponse(cmd, msg_id, 0, "ok", resp); + cJSON_Delete(resp); + return; + } + + if (cmd == "dev_info") { + cJSON* resp = cJSON_CreateObject(); + cJSON_AddStringToObject(resp, "mac", SystemInfo::GetMacAddress().c_str()); + cJSON_AddStringToObject(resp, "board", BOARD_NAME); + auto app_desc = esp_app_get_description(); + cJSON_AddStringToObject(resp, "fw", app_desc->version); + cJSON_AddStringToObject(resp, "chip", SystemInfo::GetChipModelName().c_str()); + cJSON_AddStringToObject(resp, "idf", app_desc->idf_ver); + service.SendResponse(cmd, msg_id, 0, "ok", resp); + cJSON_Delete(resp); + return; + } + + if (cmd == "set_wifi") { + cJSON* ssid_item = cJSON_GetObjectItem(data, "ssid"); + cJSON* pwd_item = cJSON_GetObjectItem(data, "pwd"); + if (!ssid_item || !cJSON_IsString(ssid_item) || strlen(ssid_item->valuestring) == 0) { + service.SendResponse(cmd, msg_id, -1, "missing ssid"); + return; + } + wifi_config_t wifi_config = {}; + strncpy(reinterpret_cast(wifi_config.sta.ssid), + ssid_item->valuestring, sizeof(wifi_config.sta.ssid) - 1); + if (pwd_item && cJSON_IsString(pwd_item)) { + strncpy(reinterpret_cast(wifi_config.sta.password), + pwd_item->valuestring, sizeof(wifi_config.sta.password) - 1); + } + esp_err_t ret = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + if (ret != ESP_OK) { + service.SendResponse(cmd, msg_id, -2, "set config failed"); + return; + } + esp_wifi_disconnect(); + ret = esp_wifi_connect(); + service.SendResponse(cmd, msg_id, 0, + ret == ESP_OK ? "connecting" : "connect failed"); + return; + } + + if (cmd == "wifi_list") { + wifi_scan_config_t scan_config = {}; + scan_config.show_hidden = false; + esp_err_t ret = esp_wifi_scan_start(&scan_config, true); + if (ret != ESP_OK) { + service.SendResponse(cmd, msg_id, -1, "scan failed"); + return; + } + uint16_t ap_count = 0; + esp_wifi_scan_get_ap_num(&ap_count); + if (ap_count > BLE_JSON_WIFI_LIST_MAX) { + ap_count = BLE_JSON_WIFI_LIST_MAX; + } + wifi_ap_record_t* ap_list = nullptr; + cJSON* resp = cJSON_CreateObject(); + cJSON* arr = cJSON_AddArrayToObject(resp, "list"); + if (ap_count > 0) { + ap_list = static_cast(malloc(sizeof(wifi_ap_record_t) * ap_count)); + if (ap_list && esp_wifi_scan_get_ap_records(&ap_count, ap_list) == ESP_OK) { + for (int i = 0; i < ap_count; i++) { + cJSON* item = cJSON_CreateObject(); + cJSON_AddStringToObject(item, "ssid", + reinterpret_cast(ap_list[i].ssid)); + cJSON_AddNumberToObject(item, "rssi", ap_list[i].rssi); + cJSON_AddNumberToObject(item, "auth", ap_list[i].authmode); + cJSON_AddItemToArray(arr, item); + } + } + free(ap_list); + } + service.SendResponse(cmd, msg_id, 0, "ok", resp); + cJSON_Delete(resp); + return; + } + + if (cmd == "set_vol") { + cJSON* vol_item = cJSON_GetObjectItem(data, "vol"); + if (!vol_item || !cJSON_IsNumber(vol_item)) { + service.SendResponse(cmd, msg_id, -1, "missing vol"); + return; + } + int vol = vol_item->valueint; + if (vol < 0) vol = 0; + if (vol > 100) vol = 100; + auto* codec = board.GetAudioCodec(); + if (codec) { + codec->SetOutputVolume(vol); + Settings s("audio", true); + s.SetInt("output_volume", vol); + } + service.SendResponse(cmd, msg_id, 0, "ok"); + return; + } + + if (cmd == "reboot") { + service.SendResponse(cmd, msg_id, 0, "rebooting"); + vTaskDelay(pdMS_TO_TICKS(500)); + Reboot(); + return; + } + + if (cmd == "ota") { + if (device_state_ == kDeviceStateUpgrading) { + service.SendResponse(cmd, msg_id, -1, "already upgrading"); + return; + } + service.SendResponse(cmd, msg_id, 0, "start ota"); + Schedule([this]() { + CheckNewVersion(); + }); + return; + } + + if (cmd == "iot") { + auto& thing_manager = iot::ThingManager::GetInstance(); + std::string states; + if (thing_manager.GetStatesJson(states, true)) { + cJSON* resp = cJSON_Parse(states.c_str()); + service.SendResponse(cmd, msg_id, 0, "ok", resp); + if (resp) cJSON_Delete(resp); + } else { + service.SendResponse(cmd, msg_id, 0, "ok"); + } + return; + } + + service.SendResponse(cmd, msg_id, -99, "unknown cmd"); +} +#endif diff --git a/main/application.h b/main/application.h new file mode 100644 index 0000000..6b3fb70 --- /dev/null +++ b/main/application.h @@ -0,0 +1,208 @@ +#ifndef _APPLICATION_H_ +#define _APPLICATION_H_ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "protocol.h" +#include "websocket_protocol.h" +#include "ota.h" +#include "background_task.h" +#include "audio/simple_pipeline.h" +// #include "ble_service.h" // BLE JSON Service 暂不使用 + +#if CONFIG_USE_WAKE_WORD_DETECT +#include "wake_word_detect.h" +#elif CONFIG_USE_CUSTOM_WAKE_WORD +#include "custom_wake_word.h" +#endif +#if CONFIG_USE_AUDIO_PROCESSOR +#include "audio_processor.h" +#endif + +#define SCHEDULE_EVENT (1 << 0) +#define AUDIO_INPUT_READY_EVENT (1 << 1) +#define AUDIO_OUTPUT_READY_EVENT (1 << 2) + +// 未知状态、启动中、WiFi配网模式、空闲待命、连接服务器、语音监听中、语音播报中、固件升级中、设备激活中、致命错误 +enum DeviceState { + kDeviceStateUnknown, + kDeviceStateStarting, + kDeviceStateWifiConfiguring, + kDeviceStateIdle, + kDeviceStateConnecting, + kDeviceStateListening, + kDeviceStateSpeaking, + kDeviceStateDialog, + kDeviceStateUpgrading, + kDeviceStateActivating, + kDeviceStateFatalError +}; +// OPUS音频帧时长(60ms) +#define OPUS_FRAME_DURATION_MS 60 +// 应用程序主类(单例模式) +class Application { +public: + static Application& GetInstance() { + static Application instance; + return instance; + } + // 删除拷贝构造函数和赋值运算符 + Application(const Application&) = delete; + Application& operator=(const Application&) = delete; + + void Start(); // 启动应用程序 + DeviceState GetDeviceState() const { return device_state_; } // 获取当前状态 + bool IsVoiceDetected() const { return voice_detected_; } // 语音检测状态 + void Schedule(std::function callback); // 任务调度 + void SetDeviceState(DeviceState state); // 状态变更 + void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");// 警报管理 状态、消息、情感、声音 + void DismissAlert();// 关闭警报 + void AbortSpeaking(AbortReason reason);// 打断语音播报 + void AbortHttpsPlayback(const char* reason);// 中止HTTPS音频播放并清空DMA + void SendStoryRequest(); // 通过HTTPS故事API请求并播放故事 + void SendMusicRequest(); // 通过HTTPS音乐API请求并播放音乐 + void HttpsPlaybackFromUrl(const std::string& url); // 通过HTTPS下载JSON并播放音频(故事/歌曲等) + void ToggleChatState();// 切换聊天状态 + void ToggleListeningState();// 切换监听状态 + void StartListening();// 开始监听 + void StopListening();// 停止监听 + void SendTextMessage(const std::string& text);// 发送文本消息 + void UpdateIotStates();// 更新IOT设备状态 + void Reboot();// 系统重启 + void WakeWordInvoke(const std::string& wake_word);// 唤醒词回调 + void PlaySound(const std::string_view& sound);// 播放声音 + void WaitForAudioPlayback();// 等待音频播报完成 + bool IsAudioQueueEmpty(); // 检查音频队列是否为空 + void ClearAudioQueue(); // 清空音频播放队列 + bool CanEnterSleepMode();// 检查是否可以进入睡眠模式 + void StopAudioProcessor();// 停止音频处理器 + void ResetDecoder();// 重置解码器状态(用于修复音频播放问题) + bool IsSafeToOperate(); // 🔧 检查当前是否可以安全执行操作 + void AbortSpeakingAndReturnToIdle(); // 🔴 专门处理从说话状态到空闲状态的切换 + void AbortSpeakingAndReturnToListening(); // 🔵 专门处理从说话状态到聆听状态的切换 + void PauseAudioPlayback(); // ⏸️ 暂停音频播放 + void ResumeAudioPlayback(); // ▶️ 恢复音频播放 + void SuppressNextIdleSound(); // 🔇 抑制下一个空闲状态的声音播放 + void SetLowBatteryTransition(bool value); + bool IsLowBatteryTransition() const; + void InitializeWebsocketProtocol(); // 🌐 初始化WebSocket协议(RTC连接成功后调用) + // void SendTextViaWebsocket(const std::string& text);// 🌐 通过WebSocket发送文本消息 + + // 姿态传感器接口 + bool IsImuSensorAvailable(); // 检查IMU传感器是否可用 + bool GetImuData(float* acc_x, float* acc_y, float* acc_z, + float* gyro_x, float* gyro_y, float* gyro_z, + float* temperature); // 获取IMU传感器数据 + void OnMotionDetected(); // 运动检测事件处理 + bool IsAudioPaused() const { return audio_paused_; } // 检查音频是否暂停 + bool ShouldSkipDialogIdleSession() const { return skip_dialog_idle_session_; }// 是否跳过对话待机会话 + void ClearDialogIdleSkipSession();// 清除对话待机会话标志位 + bool IsDialogUploadEnabled() const { return dialog_upload_enabled_; }// 是否启用对话上传 + void SetDialogUploadEnabled(bool enabled);// 设置对话上传状态 + + // // BLE JSON 命令处理(暂不使用) + // void HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data, BleJsonService& service); + +private: + void HttpsApiPlayback(const char* api_url, const char* tag, const char* task_name);// HTTPS API音频播放通用实现 + Application();// 构造函数 + ~Application();// 析构函数 + +// 配置使用唤醒词检测 +#if CONFIG_USE_WAKE_WORD_DETECT + WakeWordDetect wake_word_detect_; +#elif CONFIG_USE_CUSTOM_WAKE_WORD + CustomWakeWord wake_word_detect_; +#endif +// 音频处理器 +#if CONFIG_USE_AUDIO_PROCESSOR + AudioProcessor audio_processor_; +#endif + Ota ota_; + std::mutex mutex_; + std::list> main_tasks_; + std::unique_ptr protocol_; + std::unique_ptr websocket_protocol_; // 🌐 WebSocket协议实例(RTC连接后初始化) + EventGroupHandle_t event_group_ = nullptr; + esp_timer_handle_t clock_timer_handle_ = nullptr; + volatile DeviceState device_state_ = kDeviceStateUnknown; + std::atomic is_aborting_{false}; // 🔧 原子标志:防止重复中止操作 + std::atomic last_safe_operation_; // 🔧 最后安全操作时间戳 + std::atomic is_switching_to_listening_{false}; // 🔵 标志:正在主动切换到聆听状态 + std::atomic is_low_battery_transition_{false}; + ListeningMode listening_mode_ = kListeningModeAutoStop; +#if CONFIG_USE_REALTIME_CHAT + bool realtime_chat_enabled_ = true; +#else + bool realtime_chat_enabled_ = false; +#endif + std::atomic ws_downlink_enabled_{true};// 🌐 WebSocket下行通道是否启用 + std::atomic opus_playback_active_{false};// 🌐 Opus解码播放活跃标志(WS/HTTPS共用) + std::atomic https_playback_active_{false};// 🌐 HTTPS音频播放进行中标志 + std::atomic https_playback_abort_{false};// 🌐 HTTPS音频播放中止标志 + bool aborted_ = false; + bool voice_detected_ = false; + bool audio_paused_ = false; // 音频暂停状态标志 + float current_speaker_volume_ = 0.0f; // 当前扬声器音量,用于语音打断判断 + bool first_idle_location_checked_ = false;// 是否首次查询城市天气 + bool send_pcm_uplink_ = true; // 是否发送PCM音频数据到服务器,由SDK内部转码为G711A + bool send_g711a_uplink_ = false;// 是否直接发送G711A音频数据到服务器 + + std::chrono::time_point last_audio_input_time_; + std::chrono::time_point last_audible_output_time_; // 最后一次有声音输出的时间点 + bool skip_dialog_idle_session_; // 是否跳过对话待机会话标志 + bool dialog_upload_enabled_ = true; // 对话上传状态标志 + bool dialog_watchdog_running_; // 对话看门狗运行标志 + int dialog_watchdog_last_logged_; // 对话看门狗上次记录的日志时间 + TaskHandle_t dialog_watchdog_task_handle_; // 对话看门狗任务句柄 + int clock_ticks_; + TaskHandle_t main_loop_task_handle_; + TaskHandle_t check_new_version_task_handle_; + + // Audio encode / decode + TaskHandle_t audio_loop_task_handle_; + BackgroundTask* background_task_; + std::chrono::steady_clock::time_point last_output_time_; + std::list> audio_decode_queue_; + + std::unique_ptr opus_encoder_; + std::unique_ptr opus_decoder_; + + OpusResampler input_resampler_;// 输入音频采样器 + OpusResampler reference_resampler_;// 参考音频采样器 + OpusResampler output_resampler_;// 输出音频采样器 + OpusResampler uplink_resampler_;// 上传音频采样器 + + player_pipeline_handle_t player_pipeline_ = nullptr; + recorder_pipeline_handle_t recorder_pipeline_ = nullptr; + + void MainLoop();// 主事件循环 + void OnAudioInput();// 音频输入回调 + void OnAudioOutput();// 音频输出回调 + void ReadAudio(std::vector& data, int sample_rate, int samples);// 读取音频数据 + void SetDecodeSampleRate(int sample_rate, int frame_duration);// 设置解码采样率 + void CheckNewVersion();// 检查新固件版本 + void ShowActivationCode();// 显示激活码 + void OnClockTimer();// 时钟定时器回调 + void SetListeningMode(ListeningMode mode);// 设置监听模式 + void AudioLoop();// 音频处理循环 + bool suppress_next_idle_sound_ = false;// 标志:是否抑制下一个空闲状态的声音播放 + void StartDialogWatchdog();// 启动对话看门狗 + void StopDialogWatchdog(); // 停止对话看门狗 + + const char* DeviceStateToString(DeviceState state); // 状态枚举转字符串 +}; + +#endif // _APPLICATION_H_ diff --git a/main/assets/common/exclamation.p3 b/main/assets/common/exclamation.p3 new file mode 100644 index 0000000..17e96cf Binary files /dev/null and b/main/assets/common/exclamation.p3 differ diff --git a/main/assets/common/low_battery.p3 b/main/assets/common/low_battery.p3 new file mode 100644 index 0000000..03669ef Binary files /dev/null and b/main/assets/common/low_battery.p3 differ diff --git a/main/assets/common/success.p3 b/main/assets/common/success.p3 new file mode 100644 index 0000000..4f1bd1c Binary files /dev/null and b/main/assets/common/success.p3 differ diff --git a/main/assets/common/vibration.p3 b/main/assets/common/vibration.p3 new file mode 100644 index 0000000..99724de Binary files /dev/null and b/main/assets/common/vibration.p3 differ diff --git a/main/assets/en-US/0.p3 b/main/assets/en-US/0.p3 new file mode 100644 index 0000000..f201dc2 Binary files /dev/null and b/main/assets/en-US/0.p3 differ diff --git a/main/assets/en-US/1.p3 b/main/assets/en-US/1.p3 new file mode 100644 index 0000000..27d222e Binary files /dev/null and b/main/assets/en-US/1.p3 differ diff --git a/main/assets/en-US/2.p3 b/main/assets/en-US/2.p3 new file mode 100644 index 0000000..7c8949e Binary files /dev/null and b/main/assets/en-US/2.p3 differ diff --git a/main/assets/en-US/3.p3 b/main/assets/en-US/3.p3 new file mode 100644 index 0000000..d5f3292 Binary files /dev/null and b/main/assets/en-US/3.p3 differ diff --git a/main/assets/en-US/4.p3 b/main/assets/en-US/4.p3 new file mode 100644 index 0000000..d4045bf Binary files /dev/null and b/main/assets/en-US/4.p3 differ diff --git a/main/assets/en-US/5.p3 b/main/assets/en-US/5.p3 new file mode 100644 index 0000000..735d360 Binary files /dev/null and b/main/assets/en-US/5.p3 differ diff --git a/main/assets/en-US/6.p3 b/main/assets/en-US/6.p3 new file mode 100644 index 0000000..a52bf6b Binary files /dev/null and b/main/assets/en-US/6.p3 differ diff --git a/main/assets/en-US/7.p3 b/main/assets/en-US/7.p3 new file mode 100644 index 0000000..4dd383f Binary files /dev/null and b/main/assets/en-US/7.p3 differ diff --git a/main/assets/en-US/8.p3 b/main/assets/en-US/8.p3 new file mode 100644 index 0000000..fe89fb4 Binary files /dev/null and b/main/assets/en-US/8.p3 differ diff --git a/main/assets/en-US/9.p3 b/main/assets/en-US/9.p3 new file mode 100644 index 0000000..dd9ed7b Binary files /dev/null and b/main/assets/en-US/9.p3 differ diff --git a/main/assets/en-US/activation.p3 b/main/assets/en-US/activation.p3 new file mode 100644 index 0000000..2a260b5 Binary files /dev/null and b/main/assets/en-US/activation.p3 differ diff --git a/main/assets/en-US/err_pin.p3 b/main/assets/en-US/err_pin.p3 new file mode 100644 index 0000000..c33346c Binary files /dev/null and b/main/assets/en-US/err_pin.p3 differ diff --git a/main/assets/en-US/err_reg.p3 b/main/assets/en-US/err_reg.p3 new file mode 100644 index 0000000..27b5a2f Binary files /dev/null and b/main/assets/en-US/err_reg.p3 differ diff --git a/main/assets/en-US/language.json b/main/assets/en-US/language.json new file mode 100644 index 0000000..6265ae5 --- /dev/null +++ b/main/assets/en-US/language.json @@ -0,0 +1,52 @@ +{ + "language": { + "type": "en-US" + }, + "strings": { + "WARNING": "Warning", + "INFO": "Information", + "ERROR": "Error", + "VERSION": "Ver ", + "LOADING_PROTOCOL": "Loading Protocol...", + "INITIALIZING": "Initializing...", + "PIN_ERROR": "Please insert SIM card", + "REG_ERROR": "Unable to access network, please check SIM card status", + "DETECTING_MODULE": "Detecting module...", + "REGISTERING_NETWORK": "Waiting for network...", + + "STANDBY": "Standby", + "CONNECT_TO": "Connect to ", + "CONNECTING": "Connecting...", + "CONNECTION_SUCCESSFUL": "Connection Successful", + "CONNECTED_TO": "Connected to ", + + "LISTENING": "Listening...", + "SPEAKING": "Speaking...", + + "SERVER_NOT_FOUND": "Looking for available service", + "SERVER_NOT_CONNECTED": "Unable to connect to service, please try again later", + "SERVER_TIMEOUT": "Waiting for response timeout", + "SERVER_ERROR": "Sending failed, please check the network", + + "CONNECT_TO_HOTSPOT": "Hotspot: ", + "ACCESS_VIA_BROWSER": " Config URL: ", + "WIFI_CONFIG_MODE": "Wi-Fi Configuration Mode", + "ENTERING_WIFI_CONFIG_MODE": "Entering Wi-Fi configuration mode...", + "SCANNING_WIFI": "Scanning Wi-Fi...", + + "NEW_VERSION": "New version ", + "OTA_UPGRADE": "OTA Upgrade", + "UPGRADING": "System is upgrading...", + "UPGRADE_FAILED": "Upgrade failed", + "ACTIVATION": "Activation", + + "BATTERY_LOW": "Low battery", + "BATTERY_CHARGING": "Charging", + "BATTERY_FULL": "Battery full", + "BATTERY_NEED_CHARGE": "Low battery, please charge", + + "VOLUME": "Volume ", + "MUTED": "Muted", + "MAX_VOLUME": "Max volume" + } +} \ No newline at end of file diff --git a/main/assets/en-US/upgrade.p3 b/main/assets/en-US/upgrade.p3 new file mode 100644 index 0000000..4e050e4 Binary files /dev/null and b/main/assets/en-US/upgrade.p3 differ diff --git a/main/assets/en-US/welcome.p3 b/main/assets/en-US/welcome.p3 new file mode 100644 index 0000000..d2c35f4 Binary files /dev/null and b/main/assets/en-US/welcome.p3 differ diff --git a/main/assets/en-US/wificonfig.p3 b/main/assets/en-US/wificonfig.p3 new file mode 100644 index 0000000..3245e31 Binary files /dev/null and b/main/assets/en-US/wificonfig.p3 differ diff --git a/main/assets/ja-JP/0.p3 b/main/assets/ja-JP/0.p3 new file mode 100644 index 0000000..179ae89 Binary files /dev/null and b/main/assets/ja-JP/0.p3 differ diff --git a/main/assets/ja-JP/1.p3 b/main/assets/ja-JP/1.p3 new file mode 100644 index 0000000..8330d6d Binary files /dev/null and b/main/assets/ja-JP/1.p3 differ diff --git a/main/assets/ja-JP/2.p3 b/main/assets/ja-JP/2.p3 new file mode 100644 index 0000000..d565d5b Binary files /dev/null and b/main/assets/ja-JP/2.p3 differ diff --git a/main/assets/ja-JP/3.p3 b/main/assets/ja-JP/3.p3 new file mode 100644 index 0000000..f3f300a Binary files /dev/null and b/main/assets/ja-JP/3.p3 differ diff --git a/main/assets/ja-JP/4.p3 b/main/assets/ja-JP/4.p3 new file mode 100644 index 0000000..487da70 Binary files /dev/null and b/main/assets/ja-JP/4.p3 differ diff --git a/main/assets/ja-JP/5.p3 b/main/assets/ja-JP/5.p3 new file mode 100644 index 0000000..19e3663 Binary files /dev/null and b/main/assets/ja-JP/5.p3 differ diff --git a/main/assets/ja-JP/6.p3 b/main/assets/ja-JP/6.p3 new file mode 100644 index 0000000..8d299ed Binary files /dev/null and b/main/assets/ja-JP/6.p3 differ diff --git a/main/assets/ja-JP/7.p3 b/main/assets/ja-JP/7.p3 new file mode 100644 index 0000000..e1e1cb3 Binary files /dev/null and b/main/assets/ja-JP/7.p3 differ diff --git a/main/assets/ja-JP/8.p3 b/main/assets/ja-JP/8.p3 new file mode 100644 index 0000000..123a96d Binary files /dev/null and b/main/assets/ja-JP/8.p3 differ diff --git a/main/assets/ja-JP/9.p3 b/main/assets/ja-JP/9.p3 new file mode 100644 index 0000000..a87b096 Binary files /dev/null and b/main/assets/ja-JP/9.p3 differ diff --git a/main/assets/ja-JP/activation.p3 b/main/assets/ja-JP/activation.p3 new file mode 100644 index 0000000..bab34bd Binary files /dev/null and b/main/assets/ja-JP/activation.p3 differ diff --git a/main/assets/ja-JP/err_pin.p3 b/main/assets/ja-JP/err_pin.p3 new file mode 100644 index 0000000..3b221b4 Binary files /dev/null and b/main/assets/ja-JP/err_pin.p3 differ diff --git a/main/assets/ja-JP/err_reg.p3 b/main/assets/ja-JP/err_reg.p3 new file mode 100644 index 0000000..804ec4d Binary files /dev/null and b/main/assets/ja-JP/err_reg.p3 differ diff --git a/main/assets/ja-JP/language.json b/main/assets/ja-JP/language.json new file mode 100644 index 0000000..5a8776a --- /dev/null +++ b/main/assets/ja-JP/language.json @@ -0,0 +1,51 @@ +{ + "language": { + "type": "ja-JP" + }, + "strings": { + "WARNING": "警告", + "INFO": "情報", + "ERROR": "エラー", + "VERSION": "バージョン ", + "LOADING_PROTOCOL": "プロトコルを読み込み中...", + "INITIALIZING": "初期化中...", + "PIN_ERROR": "SIMカードを挿入してください", + "REG_ERROR": "ネットワークに接続できません。ネットワーク状態を確認してください", + "DETECTING_MODULE": "モジュールを検出中...", + "REGISTERING_NETWORK": "ネットワーク接続待機中...", + + "STANDBY": "待機中", + "CONNECT_TO": "接続先 ", + "CONNECTING": "接続中...", + "CONNECTED_TO": "接続完了 ", + + "LISTENING": "リスニング中...", + "SPEAKING": "話しています...", + + "SERVER_NOT_FOUND": "利用可能なサーバーを探しています", + "SERVER_NOT_CONNECTED": "サーバーに接続できません。後でもう一度お試しください", + "SERVER_TIMEOUT": "応答待機時間が終了しました", + "SERVER_ERROR": "送信に失敗しました。ネットワークを確認してください", + + "CONNECT_TO_HOTSPOT": "スマートフォンをWi-Fi ", + "ACCESS_VIA_BROWSER": " に接続し、ブラウザでアクセスしてください ", + "WIFI_CONFIG_MODE": "ネットワーク設定モード", + "ENTERING_WIFI_CONFIG_MODE": "ネットワーク設定中...", + "SCANNING_WIFI": "Wi-Fiをスキャン中...", + + "NEW_VERSION": "新しいバージョン ", + "OTA_UPGRADE": "OTAアップグレード", + "UPGRADING": "システムをアップグレード中...", + "UPGRADE_FAILED": "アップグレード失敗", + "ACTIVATION": "デバイスをアクティベート", + + "BATTERY_LOW": "バッテリーが少なくなっています", + "BATTERY_CHARGING": "充電中", + "BATTERY_FULL": "バッテリー満タン", + "BATTERY_NEED_CHARGE": "バッテリーが低下しています。充電してください", + + "VOLUME": "音量 ", + "MUTED": "ミュートされています", + "MAX_VOLUME": "最大音量" + } +} diff --git a/main/assets/ja-JP/upgrade.p3 b/main/assets/ja-JP/upgrade.p3 new file mode 100644 index 0000000..1375ff9 Binary files /dev/null and b/main/assets/ja-JP/upgrade.p3 differ diff --git a/main/assets/ja-JP/welcome.p3 b/main/assets/ja-JP/welcome.p3 new file mode 100644 index 0000000..16588b7 Binary files /dev/null and b/main/assets/ja-JP/welcome.p3 differ diff --git a/main/assets/ja-JP/wificonfig.p3 b/main/assets/ja-JP/wificonfig.p3 new file mode 100644 index 0000000..09c2e3f Binary files /dev/null and b/main/assets/ja-JP/wificonfig.p3 differ diff --git a/main/assets/zh-CN/0.p3 b/main/assets/zh-CN/0.p3 new file mode 100644 index 0000000..ec90932 Binary files /dev/null and b/main/assets/zh-CN/0.p3 differ diff --git a/main/assets/zh-CN/1.p3 b/main/assets/zh-CN/1.p3 new file mode 100644 index 0000000..18935e7 Binary files /dev/null and b/main/assets/zh-CN/1.p3 differ diff --git a/main/assets/zh-CN/10.p3 b/main/assets/zh-CN/10.p3 new file mode 100644 index 0000000..e94cb18 Binary files /dev/null and b/main/assets/zh-CN/10.p3 differ diff --git a/main/assets/zh-CN/100.p3 b/main/assets/zh-CN/100.p3 new file mode 100644 index 0000000..85b57f4 Binary files /dev/null and b/main/assets/zh-CN/100.p3 differ diff --git a/main/assets/zh-CN/2.p3 b/main/assets/zh-CN/2.p3 new file mode 100644 index 0000000..f391e4b Binary files /dev/null and b/main/assets/zh-CN/2.p3 differ diff --git a/main/assets/zh-CN/20.p3 b/main/assets/zh-CN/20.p3 new file mode 100644 index 0000000..afade31 Binary files /dev/null and b/main/assets/zh-CN/20.p3 differ diff --git a/main/assets/zh-CN/3.p3 b/main/assets/zh-CN/3.p3 new file mode 100644 index 0000000..c256481 Binary files /dev/null and b/main/assets/zh-CN/3.p3 differ diff --git a/main/assets/zh-CN/30.p3 b/main/assets/zh-CN/30.p3 new file mode 100644 index 0000000..91729bf Binary files /dev/null and b/main/assets/zh-CN/30.p3 differ diff --git a/main/assets/zh-CN/4.p3 b/main/assets/zh-CN/4.p3 new file mode 100644 index 0000000..108bd24 Binary files /dev/null and b/main/assets/zh-CN/4.p3 differ diff --git a/main/assets/zh-CN/40.p3 b/main/assets/zh-CN/40.p3 new file mode 100644 index 0000000..700fffe Binary files /dev/null and b/main/assets/zh-CN/40.p3 differ diff --git a/main/assets/zh-CN/5.p3 b/main/assets/zh-CN/5.p3 new file mode 100644 index 0000000..2014698 Binary files /dev/null and b/main/assets/zh-CN/5.p3 differ diff --git a/main/assets/zh-CN/50.p3 b/main/assets/zh-CN/50.p3 new file mode 100644 index 0000000..943b9e0 Binary files /dev/null and b/main/assets/zh-CN/50.p3 differ diff --git a/main/assets/zh-CN/6.p3 b/main/assets/zh-CN/6.p3 new file mode 100644 index 0000000..ddbec49 Binary files /dev/null and b/main/assets/zh-CN/6.p3 differ diff --git a/main/assets/zh-CN/60.p3 b/main/assets/zh-CN/60.p3 new file mode 100644 index 0000000..85a2a9e Binary files /dev/null and b/main/assets/zh-CN/60.p3 differ diff --git a/main/assets/zh-CN/7.p3 b/main/assets/zh-CN/7.p3 new file mode 100644 index 0000000..2f6f616 Binary files /dev/null and b/main/assets/zh-CN/7.p3 differ diff --git a/main/assets/zh-CN/70.p3 b/main/assets/zh-CN/70.p3 new file mode 100644 index 0000000..4e2c5cf Binary files /dev/null and b/main/assets/zh-CN/70.p3 differ diff --git a/main/assets/zh-CN/8.p3 b/main/assets/zh-CN/8.p3 new file mode 100644 index 0000000..4532d10 Binary files /dev/null and b/main/assets/zh-CN/8.p3 differ diff --git a/main/assets/zh-CN/80.p3 b/main/assets/zh-CN/80.p3 new file mode 100644 index 0000000..ef999c6 Binary files /dev/null and b/main/assets/zh-CN/80.p3 differ diff --git a/main/assets/zh-CN/9.p3 b/main/assets/zh-CN/9.p3 new file mode 100644 index 0000000..e1f147a Binary files /dev/null and b/main/assets/zh-CN/9.p3 differ diff --git a/main/assets/zh-CN/90.p3 b/main/assets/zh-CN/90.p3 new file mode 100644 index 0000000..160cae5 Binary files /dev/null and b/main/assets/zh-CN/90.p3 differ diff --git a/main/assets/zh-CN/activation.p3 b/main/assets/zh-CN/activation.p3 new file mode 100644 index 0000000..013d499 Binary files /dev/null and b/main/assets/zh-CN/activation.p3 differ diff --git a/main/assets/zh-CN/daiming.p3 b/main/assets/zh-CN/daiming.p3 new file mode 100644 index 0000000..34afad7 Binary files /dev/null and b/main/assets/zh-CN/daiming.p3 differ diff --git a/main/assets/zh-CN/err_pin.p3 b/main/assets/zh-CN/err_pin.p3 new file mode 100644 index 0000000..bf4d819 Binary files /dev/null and b/main/assets/zh-CN/err_pin.p3 differ diff --git a/main/assets/zh-CN/err_reg.p3 b/main/assets/zh-CN/err_reg.p3 new file mode 100644 index 0000000..cf316fa Binary files /dev/null and b/main/assets/zh-CN/err_reg.p3 differ diff --git a/main/assets/zh-CN/kaka_battery_l.p3 b/main/assets/zh-CN/kaka_battery_l.p3 new file mode 100644 index 0000000..25636c4 Binary files /dev/null and b/main/assets/zh-CN/kaka_battery_l.p3 differ diff --git a/main/assets/zh-CN/kaka_daiming.p3 b/main/assets/zh-CN/kaka_daiming.p3 new file mode 100644 index 0000000..d8a4100 Binary files /dev/null and b/main/assets/zh-CN/kaka_daiming.p3 differ diff --git a/main/assets/zh-CN/kaka_kaijibobao.p3 b/main/assets/zh-CN/kaka_kaijibobao.p3 new file mode 100644 index 0000000..3d24960 Binary files /dev/null and b/main/assets/zh-CN/kaka_kaijibobao.p3 differ diff --git a/main/assets/zh-CN/kaka_lianjiewangluo.p3 b/main/assets/zh-CN/kaka_lianjiewangluo.p3 new file mode 100644 index 0000000..f58c672 Binary files /dev/null and b/main/assets/zh-CN/kaka_lianjiewangluo.p3 differ diff --git a/main/assets/zh-CN/kaka_wificonfig.p3 b/main/assets/zh-CN/kaka_wificonfig.p3 new file mode 100644 index 0000000..3f5598c Binary files /dev/null and b/main/assets/zh-CN/kaka_wificonfig.p3 differ diff --git a/main/assets/zh-CN/kaka_zainne.p3 b/main/assets/zh-CN/kaka_zainne.p3 new file mode 100644 index 0000000..02bacc1 Binary files /dev/null and b/main/assets/zh-CN/kaka_zainne.p3 differ diff --git a/main/assets/zh-CN/lala_battery_l.p3 b/main/assets/zh-CN/lala_battery_l.p3 new file mode 100644 index 0000000..856f604 Binary files /dev/null and b/main/assets/zh-CN/lala_battery_l.p3 differ diff --git a/main/assets/zh-CN/lala_daiming.p3 b/main/assets/zh-CN/lala_daiming.p3 new file mode 100644 index 0000000..61155b2 Binary files /dev/null and b/main/assets/zh-CN/lala_daiming.p3 differ diff --git a/main/assets/zh-CN/lala_kaijibobao.p3 b/main/assets/zh-CN/lala_kaijibobao.p3 new file mode 100644 index 0000000..0e31800 Binary files /dev/null and b/main/assets/zh-CN/lala_kaijibobao.p3 differ diff --git a/main/assets/zh-CN/lala_kaijibobao_new.p3 b/main/assets/zh-CN/lala_kaijibobao_new.p3 new file mode 100644 index 0000000..10dae27 Binary files /dev/null and b/main/assets/zh-CN/lala_kaijibobao_new.p3 differ diff --git a/main/assets/zh-CN/lala_lianjiewangluo.p3 b/main/assets/zh-CN/lala_lianjiewangluo.p3 new file mode 100644 index 0000000..737fe1e Binary files /dev/null and b/main/assets/zh-CN/lala_lianjiewangluo.p3 differ diff --git a/main/assets/zh-CN/lala_wificonfig.p3 b/main/assets/zh-CN/lala_wificonfig.p3 new file mode 100644 index 0000000..233caa2 Binary files /dev/null and b/main/assets/zh-CN/lala_wificonfig.p3 differ diff --git a/main/assets/zh-CN/lala_zainne.p3 b/main/assets/zh-CN/lala_zainne.p3 new file mode 100644 index 0000000..fafa464 Binary files /dev/null and b/main/assets/zh-CN/lala_zainne.p3 differ diff --git a/main/assets/zh-CN/language.json b/main/assets/zh-CN/language.json new file mode 100644 index 0000000..d9e75cd --- /dev/null +++ b/main/assets/zh-CN/language.json @@ -0,0 +1,51 @@ +{ + "language": { + "type" :"zh-CN" + }, + "strings": { + "WARNING":"警告", + "INFO":"信息", + "ERROR":"错误", + "VERSION": "版本 ", + "LOADING_PROTOCOL":"加载协议...", + "INITIALIZING":"正在初始化...", + "PIN_ERROR":"请插入 SIM 卡", + "REG_ERROR":"无法接入网络,请检查流量卡状态", + "DETECTING_MODULE":"检测模组...", + "REGISTERING_NETWORK":"等待网络...", + + "STANDBY":"待命", + "CONNECT_TO":"连接 ", + "CONNECTING":"连接中...", + "CONNECTED_TO":"已连接 ", + + "LISTENING":"聆听中...", + "SPEAKING":"说话中...", + + "SERVER_NOT_FOUND":"正在寻找可用服务", + "SERVER_NOT_CONNECTED":"无法连接服务,请稍后再试", + "SERVER_TIMEOUT":"等待响应超时", + "SERVER_ERROR":"发送失败,请检查网络", + + "CONNECT_TO_HOTSPOT":"手机连接热点 ", + "ACCESS_VIA_BROWSER":",浏览器访问 ", + "WIFI_CONFIG_MODE":"配网模式", + "ENTERING_WIFI_CONFIG_MODE":"进入配网模式...", + "SCANNING_WIFI":"扫描 Wi-Fi...", + + "NEW_VERSION": "新版本 ", + "OTA_UPGRADE":"OTA 升级", + "UPGRADING":"正在升级系统...", + "UPGRADE_FAILED":"升级失败", + "ACTIVATION":"激活设备", + + "BATTERY_LOW":"电量不足", + "BATTERY_CHARGING":"正在充电", + "BATTERY_FULL":"电量已满", + "BATTERY_NEED_CHARGE":"电量低,请充电", + + "VOLUME":"音量 ", + "MUTED":"已静音", + "MAX_VOLUME":"最大音量" + } +} diff --git a/main/assets/zh-CN/lianjiewangluo.p3 b/main/assets/zh-CN/lianjiewangluo.p3 new file mode 100644 index 0000000..e1c3137 Binary files /dev/null and b/main/assets/zh-CN/lianjiewangluo.p3 differ diff --git a/main/assets/zh-CN/putdown_boot.p3 b/main/assets/zh-CN/putdown_boot.p3 new file mode 100644 index 0000000..0f238eb Binary files /dev/null and b/main/assets/zh-CN/putdown_boot.p3 differ diff --git a/main/assets/zh-CN/putdown_story.p3 b/main/assets/zh-CN/putdown_story.p3 new file mode 100644 index 0000000..7b26e52 Binary files /dev/null and b/main/assets/zh-CN/putdown_story.p3 differ diff --git a/main/assets/zh-CN/putdown_touch.p3 b/main/assets/zh-CN/putdown_touch.p3 new file mode 100644 index 0000000..e267d11 Binary files /dev/null and b/main/assets/zh-CN/putdown_touch.p3 differ diff --git a/main/assets/zh-CN/test_modal.p3 b/main/assets/zh-CN/test_modal.p3 new file mode 100644 index 0000000..ad59352 Binary files /dev/null and b/main/assets/zh-CN/test_modal.p3 differ diff --git a/main/assets/zh-CN/tuoluoyi.p3 b/main/assets/zh-CN/tuoluoyi.p3 new file mode 100644 index 0000000..f81fc3b Binary files /dev/null and b/main/assets/zh-CN/tuoluoyi.p3 differ diff --git a/main/assets/zh-CN/two_kaijiboba.opus b/main/assets/zh-CN/two_kaijiboba.opus new file mode 100644 index 0000000..e31eb81 Binary files /dev/null and b/main/assets/zh-CN/two_kaijiboba.opus differ diff --git a/main/assets/zh-CN/upgrade.p3 b/main/assets/zh-CN/upgrade.p3 new file mode 100644 index 0000000..cb382f8 Binary files /dev/null and b/main/assets/zh-CN/upgrade.p3 differ diff --git a/main/assets/zh-CN/welcome.p3 b/main/assets/zh-CN/welcome.p3 new file mode 100644 index 0000000..c018b54 Binary files /dev/null and b/main/assets/zh-CN/welcome.p3 differ diff --git a/main/assets/zh-CN/wificonfig.p3 b/main/assets/zh-CN/wificonfig.p3 new file mode 100644 index 0000000..20f7d24 Binary files /dev/null and b/main/assets/zh-CN/wificonfig.p3 differ diff --git a/main/assets/zh-CN_旧的/0.p3 b/main/assets/zh-CN_旧的/0.p3 new file mode 100644 index 0000000..ec90932 Binary files /dev/null and b/main/assets/zh-CN_旧的/0.p3 differ diff --git a/main/assets/zh-CN_旧的/1.p3 b/main/assets/zh-CN_旧的/1.p3 new file mode 100644 index 0000000..18935e7 Binary files /dev/null and b/main/assets/zh-CN_旧的/1.p3 differ diff --git a/main/assets/zh-CN_旧的/10.p3 b/main/assets/zh-CN_旧的/10.p3 new file mode 100644 index 0000000..e94cb18 Binary files /dev/null and b/main/assets/zh-CN_旧的/10.p3 differ diff --git a/main/assets/zh-CN_旧的/100.p3 b/main/assets/zh-CN_旧的/100.p3 new file mode 100644 index 0000000..85b57f4 Binary files /dev/null and b/main/assets/zh-CN_旧的/100.p3 differ diff --git a/main/assets/zh-CN_旧的/2.p3 b/main/assets/zh-CN_旧的/2.p3 new file mode 100644 index 0000000..f391e4b Binary files /dev/null and b/main/assets/zh-CN_旧的/2.p3 differ diff --git a/main/assets/zh-CN_旧的/20.p3 b/main/assets/zh-CN_旧的/20.p3 new file mode 100644 index 0000000..afade31 Binary files /dev/null and b/main/assets/zh-CN_旧的/20.p3 differ diff --git a/main/assets/zh-CN_旧的/3.p3 b/main/assets/zh-CN_旧的/3.p3 new file mode 100644 index 0000000..c256481 Binary files /dev/null and b/main/assets/zh-CN_旧的/3.p3 differ diff --git a/main/assets/zh-CN_旧的/30.p3 b/main/assets/zh-CN_旧的/30.p3 new file mode 100644 index 0000000..91729bf Binary files /dev/null and b/main/assets/zh-CN_旧的/30.p3 differ diff --git a/main/assets/zh-CN_旧的/4.p3 b/main/assets/zh-CN_旧的/4.p3 new file mode 100644 index 0000000..108bd24 Binary files /dev/null and b/main/assets/zh-CN_旧的/4.p3 differ diff --git a/main/assets/zh-CN_旧的/40.p3 b/main/assets/zh-CN_旧的/40.p3 new file mode 100644 index 0000000..700fffe Binary files /dev/null and b/main/assets/zh-CN_旧的/40.p3 differ diff --git a/main/assets/zh-CN_旧的/5.p3 b/main/assets/zh-CN_旧的/5.p3 new file mode 100644 index 0000000..2014698 Binary files /dev/null and b/main/assets/zh-CN_旧的/5.p3 differ diff --git a/main/assets/zh-CN_旧的/50.p3 b/main/assets/zh-CN_旧的/50.p3 new file mode 100644 index 0000000..943b9e0 Binary files /dev/null and b/main/assets/zh-CN_旧的/50.p3 differ diff --git a/main/assets/zh-CN_旧的/6.p3 b/main/assets/zh-CN_旧的/6.p3 new file mode 100644 index 0000000..ddbec49 Binary files /dev/null and b/main/assets/zh-CN_旧的/6.p3 differ diff --git a/main/assets/zh-CN_旧的/60.p3 b/main/assets/zh-CN_旧的/60.p3 new file mode 100644 index 0000000..85a2a9e Binary files /dev/null and b/main/assets/zh-CN_旧的/60.p3 differ diff --git a/main/assets/zh-CN_旧的/7.p3 b/main/assets/zh-CN_旧的/7.p3 new file mode 100644 index 0000000..2f6f616 Binary files /dev/null and b/main/assets/zh-CN_旧的/7.p3 differ diff --git a/main/assets/zh-CN_旧的/70.p3 b/main/assets/zh-CN_旧的/70.p3 new file mode 100644 index 0000000..4e2c5cf Binary files /dev/null and b/main/assets/zh-CN_旧的/70.p3 differ diff --git a/main/assets/zh-CN_旧的/8.p3 b/main/assets/zh-CN_旧的/8.p3 new file mode 100644 index 0000000..4532d10 Binary files /dev/null and b/main/assets/zh-CN_旧的/8.p3 differ diff --git a/main/assets/zh-CN_旧的/80.p3 b/main/assets/zh-CN_旧的/80.p3 new file mode 100644 index 0000000..ef999c6 Binary files /dev/null and b/main/assets/zh-CN_旧的/80.p3 differ diff --git a/main/assets/zh-CN_旧的/9.p3 b/main/assets/zh-CN_旧的/9.p3 new file mode 100644 index 0000000..e1f147a Binary files /dev/null and b/main/assets/zh-CN_旧的/9.p3 differ diff --git a/main/assets/zh-CN_旧的/90.p3 b/main/assets/zh-CN_旧的/90.p3 new file mode 100644 index 0000000..160cae5 Binary files /dev/null and b/main/assets/zh-CN_旧的/90.p3 differ diff --git a/main/assets/zh-CN_旧的/activation.p3 b/main/assets/zh-CN_旧的/activation.p3 new file mode 100644 index 0000000..013d499 Binary files /dev/null and b/main/assets/zh-CN_旧的/activation.p3 differ diff --git a/main/assets/zh-CN_旧的/daiming.p3 b/main/assets/zh-CN_旧的/daiming.p3 new file mode 100644 index 0000000..34afad7 Binary files /dev/null and b/main/assets/zh-CN_旧的/daiming.p3 differ diff --git a/main/assets/zh-CN_旧的/err_pin.p3 b/main/assets/zh-CN_旧的/err_pin.p3 new file mode 100644 index 0000000..bf4d819 Binary files /dev/null and b/main/assets/zh-CN_旧的/err_pin.p3 differ diff --git a/main/assets/zh-CN_旧的/err_reg.p3 b/main/assets/zh-CN_旧的/err_reg.p3 new file mode 100644 index 0000000..cf316fa Binary files /dev/null and b/main/assets/zh-CN_旧的/err_reg.p3 differ diff --git a/main/assets/zh-CN_旧的/kaka_daiming.p3 b/main/assets/zh-CN_旧的/kaka_daiming.p3 new file mode 100644 index 0000000..5b66fc6 Binary files /dev/null and b/main/assets/zh-CN_旧的/kaka_daiming.p3 differ diff --git a/main/assets/zh-CN_旧的/kaka_kaijibobao.p3 b/main/assets/zh-CN_旧的/kaka_kaijibobao.p3 new file mode 100644 index 0000000..5876b29 Binary files /dev/null and b/main/assets/zh-CN_旧的/kaka_kaijibobao.p3 differ diff --git a/main/assets/zh-CN_旧的/kaka_lianjiewangluo.p3 b/main/assets/zh-CN_旧的/kaka_lianjiewangluo.p3 new file mode 100644 index 0000000..57ec58b Binary files /dev/null and b/main/assets/zh-CN_旧的/kaka_lianjiewangluo.p3 differ diff --git a/main/assets/zh-CN_旧的/kaka_wificonfig.p3 b/main/assets/zh-CN_旧的/kaka_wificonfig.p3 new file mode 100644 index 0000000..fd94e40 Binary files /dev/null and b/main/assets/zh-CN_旧的/kaka_wificonfig.p3 differ diff --git a/main/assets/zh-CN_旧的/kaka_zainne.p3 b/main/assets/zh-CN_旧的/kaka_zainne.p3 new file mode 100644 index 0000000..28ff2f0 Binary files /dev/null and b/main/assets/zh-CN_旧的/kaka_zainne.p3 differ diff --git a/main/assets/zh-CN_旧的/lala_daiming.p3 b/main/assets/zh-CN_旧的/lala_daiming.p3 new file mode 100644 index 0000000..61155b2 Binary files /dev/null and b/main/assets/zh-CN_旧的/lala_daiming.p3 differ diff --git a/main/assets/zh-CN_旧的/lala_kaijibobao.p3 b/main/assets/zh-CN_旧的/lala_kaijibobao.p3 new file mode 100644 index 0000000..0e31800 Binary files /dev/null and b/main/assets/zh-CN_旧的/lala_kaijibobao.p3 differ diff --git a/main/assets/zh-CN_旧的/lala_lianjiewangluo.p3 b/main/assets/zh-CN_旧的/lala_lianjiewangluo.p3 new file mode 100644 index 0000000..737fe1e Binary files /dev/null and b/main/assets/zh-CN_旧的/lala_lianjiewangluo.p3 differ diff --git a/main/assets/zh-CN_旧的/lala_wificonfig.p3 b/main/assets/zh-CN_旧的/lala_wificonfig.p3 new file mode 100644 index 0000000..233caa2 Binary files /dev/null and b/main/assets/zh-CN_旧的/lala_wificonfig.p3 differ diff --git a/main/assets/zh-CN_旧的/lala_zainne.p3 b/main/assets/zh-CN_旧的/lala_zainne.p3 new file mode 100644 index 0000000..fafa464 Binary files /dev/null and b/main/assets/zh-CN_旧的/lala_zainne.p3 differ diff --git a/main/assets/zh-CN_旧的/language.json b/main/assets/zh-CN_旧的/language.json new file mode 100644 index 0000000..d9e75cd --- /dev/null +++ b/main/assets/zh-CN_旧的/language.json @@ -0,0 +1,51 @@ +{ + "language": { + "type" :"zh-CN" + }, + "strings": { + "WARNING":"警告", + "INFO":"信息", + "ERROR":"错误", + "VERSION": "版本 ", + "LOADING_PROTOCOL":"加载协议...", + "INITIALIZING":"正在初始化...", + "PIN_ERROR":"请插入 SIM 卡", + "REG_ERROR":"无法接入网络,请检查流量卡状态", + "DETECTING_MODULE":"检测模组...", + "REGISTERING_NETWORK":"等待网络...", + + "STANDBY":"待命", + "CONNECT_TO":"连接 ", + "CONNECTING":"连接中...", + "CONNECTED_TO":"已连接 ", + + "LISTENING":"聆听中...", + "SPEAKING":"说话中...", + + "SERVER_NOT_FOUND":"正在寻找可用服务", + "SERVER_NOT_CONNECTED":"无法连接服务,请稍后再试", + "SERVER_TIMEOUT":"等待响应超时", + "SERVER_ERROR":"发送失败,请检查网络", + + "CONNECT_TO_HOTSPOT":"手机连接热点 ", + "ACCESS_VIA_BROWSER":",浏览器访问 ", + "WIFI_CONFIG_MODE":"配网模式", + "ENTERING_WIFI_CONFIG_MODE":"进入配网模式...", + "SCANNING_WIFI":"扫描 Wi-Fi...", + + "NEW_VERSION": "新版本 ", + "OTA_UPGRADE":"OTA 升级", + "UPGRADING":"正在升级系统...", + "UPGRADE_FAILED":"升级失败", + "ACTIVATION":"激活设备", + + "BATTERY_LOW":"电量不足", + "BATTERY_CHARGING":"正在充电", + "BATTERY_FULL":"电量已满", + "BATTERY_NEED_CHARGE":"电量低,请充电", + + "VOLUME":"音量 ", + "MUTED":"已静音", + "MAX_VOLUME":"最大音量" + } +} diff --git a/main/assets/zh-CN_旧的/lianjiewangluo.p3 b/main/assets/zh-CN_旧的/lianjiewangluo.p3 new file mode 100644 index 0000000..e1c3137 Binary files /dev/null and b/main/assets/zh-CN_旧的/lianjiewangluo.p3 differ diff --git a/main/assets/zh-CN_旧的/putdown_boot.p3 b/main/assets/zh-CN_旧的/putdown_boot.p3 new file mode 100644 index 0000000..0f238eb Binary files /dev/null and b/main/assets/zh-CN_旧的/putdown_boot.p3 differ diff --git a/main/assets/zh-CN_旧的/putdown_story.p3 b/main/assets/zh-CN_旧的/putdown_story.p3 new file mode 100644 index 0000000..7b26e52 Binary files /dev/null and b/main/assets/zh-CN_旧的/putdown_story.p3 differ diff --git a/main/assets/zh-CN_旧的/putdown_touch.p3 b/main/assets/zh-CN_旧的/putdown_touch.p3 new file mode 100644 index 0000000..e267d11 Binary files /dev/null and b/main/assets/zh-CN_旧的/putdown_touch.p3 differ diff --git a/main/assets/zh-CN_旧的/test_modal.p3 b/main/assets/zh-CN_旧的/test_modal.p3 new file mode 100644 index 0000000..ad59352 Binary files /dev/null and b/main/assets/zh-CN_旧的/test_modal.p3 differ diff --git a/main/assets/zh-CN_旧的/tuoluoyi.p3 b/main/assets/zh-CN_旧的/tuoluoyi.p3 new file mode 100644 index 0000000..f81fc3b Binary files /dev/null and b/main/assets/zh-CN_旧的/tuoluoyi.p3 differ diff --git a/main/assets/zh-CN_旧的/upgrade.p3 b/main/assets/zh-CN_旧的/upgrade.p3 new file mode 100644 index 0000000..cb382f8 Binary files /dev/null and b/main/assets/zh-CN_旧的/upgrade.p3 differ diff --git a/main/assets/zh-CN_旧的/welcome.p3 b/main/assets/zh-CN_旧的/welcome.p3 new file mode 100644 index 0000000..c018b54 Binary files /dev/null and b/main/assets/zh-CN_旧的/welcome.p3 differ diff --git a/main/assets/zh-CN_旧的/wificonfig.p3 b/main/assets/zh-CN_旧的/wificonfig.p3 new file mode 100644 index 0000000..20f7d24 Binary files /dev/null and b/main/assets/zh-CN_旧的/wificonfig.p3 differ diff --git a/main/assets/zh-TW/0.p3 b/main/assets/zh-TW/0.p3 new file mode 100644 index 0000000..ec90932 Binary files /dev/null and b/main/assets/zh-TW/0.p3 differ diff --git a/main/assets/zh-TW/1.p3 b/main/assets/zh-TW/1.p3 new file mode 100644 index 0000000..18935e7 Binary files /dev/null and b/main/assets/zh-TW/1.p3 differ diff --git a/main/assets/zh-TW/2.p3 b/main/assets/zh-TW/2.p3 new file mode 100644 index 0000000..f391e4b Binary files /dev/null and b/main/assets/zh-TW/2.p3 differ diff --git a/main/assets/zh-TW/3.p3 b/main/assets/zh-TW/3.p3 new file mode 100644 index 0000000..c256481 Binary files /dev/null and b/main/assets/zh-TW/3.p3 differ diff --git a/main/assets/zh-TW/4.p3 b/main/assets/zh-TW/4.p3 new file mode 100644 index 0000000..108bd24 Binary files /dev/null and b/main/assets/zh-TW/4.p3 differ diff --git a/main/assets/zh-TW/5.p3 b/main/assets/zh-TW/5.p3 new file mode 100644 index 0000000..2014698 Binary files /dev/null and b/main/assets/zh-TW/5.p3 differ diff --git a/main/assets/zh-TW/6.p3 b/main/assets/zh-TW/6.p3 new file mode 100644 index 0000000..ddbec49 Binary files /dev/null and b/main/assets/zh-TW/6.p3 differ diff --git a/main/assets/zh-TW/7.p3 b/main/assets/zh-TW/7.p3 new file mode 100644 index 0000000..2f6f616 Binary files /dev/null and b/main/assets/zh-TW/7.p3 differ diff --git a/main/assets/zh-TW/8.p3 b/main/assets/zh-TW/8.p3 new file mode 100644 index 0000000..4532d10 Binary files /dev/null and b/main/assets/zh-TW/8.p3 differ diff --git a/main/assets/zh-TW/9.p3 b/main/assets/zh-TW/9.p3 new file mode 100644 index 0000000..e1f147a Binary files /dev/null and b/main/assets/zh-TW/9.p3 differ diff --git a/main/assets/zh-TW/activation.p3 b/main/assets/zh-TW/activation.p3 new file mode 100644 index 0000000..013d499 Binary files /dev/null and b/main/assets/zh-TW/activation.p3 differ diff --git a/main/assets/zh-TW/err_pin.p3 b/main/assets/zh-TW/err_pin.p3 new file mode 100644 index 0000000..bf4d819 Binary files /dev/null and b/main/assets/zh-TW/err_pin.p3 differ diff --git a/main/assets/zh-TW/err_reg.p3 b/main/assets/zh-TW/err_reg.p3 new file mode 100644 index 0000000..cf316fa Binary files /dev/null and b/main/assets/zh-TW/err_reg.p3 differ diff --git a/main/assets/zh-TW/language.json b/main/assets/zh-TW/language.json new file mode 100644 index 0000000..0668f22 --- /dev/null +++ b/main/assets/zh-TW/language.json @@ -0,0 +1,51 @@ +{ + "language": { + "type": "zh-TW" + }, + "strings": { + "WARNING": "警告", + "INFO": "資訊", + "ERROR": "錯誤", + "VERSION": "版本 ", + "LOADING_PROTOCOL": "加載協議...", + "INITIALIZING": "正在初始化...", + "PIN_ERROR": "請插入 SIM 卡", + "REG_ERROR": "無法接入網絡,請檢查網路狀態", + "DETECTING_MODULE": "檢測模組...", + "REGISTERING_NETWORK": "等待網絡...", + + "STANDBY": "待命", + "CONNECT_TO": "連接 ", + "CONNECTING": "連接中...", + "CONNECTED_TO": "已連接 ", + + "LISTENING": "聆聽中...", + "SPEAKING": "說話中...", + + "SERVER_NOT_FOUND": "正在尋找可用服務", + "SERVER_NOT_CONNECTED": "無法連接服務,請稍後再試", + "SERVER_TIMEOUT": "等待響應超時", + "SERVER_ERROR": "發送失敗,請檢查網絡", + + "CONNECT_TO_HOTSPOT": "手機連接WiFi ", + "ACCESS_VIA_BROWSER": ",瀏覽器訪問 ", + "WIFI_CONFIG_MODE": "網路設定模式", + "ENTERING_WIFI_CONFIG_MODE": "正在設定網路...", + "SCANNING_WIFI": "掃描 Wi-Fi...", + + "NEW_VERSION": "新版本 ", + "OTA_UPGRADE": "OTA 升級", + "UPGRADING": "正在升級系統...", + "UPGRADE_FAILED": "升級失敗", + "ACTIVATION": "啟用設備", + + "BATTERY_LOW": "電量不足", + "BATTERY_CHARGING": "正在充電", + "BATTERY_FULL": "電量已滿", + "BATTERY_NEED_CHARGE": "電量低,請充電", + + "VOLUME": "音量 ", + "MUTED": "已靜音", + "MAX_VOLUME": "最大音量" + } +} diff --git a/main/assets/zh-TW/upgrade.p3 b/main/assets/zh-TW/upgrade.p3 new file mode 100644 index 0000000..cb382f8 Binary files /dev/null and b/main/assets/zh-TW/upgrade.p3 differ diff --git a/main/assets/zh-TW/welcome.p3 b/main/assets/zh-TW/welcome.p3 new file mode 100644 index 0000000..c018b54 Binary files /dev/null and b/main/assets/zh-TW/welcome.p3 differ diff --git a/main/assets/zh-TW/wificonfig.p3 b/main/assets/zh-TW/wificonfig.p3 new file mode 100644 index 0000000..330fe99 Binary files /dev/null and b/main/assets/zh-TW/wificonfig.p3 differ diff --git a/main/audio/simple_pipeline.cc b/main/audio/simple_pipeline.cc new file mode 100644 index 0000000..d0d4c30 --- /dev/null +++ b/main/audio/simple_pipeline.cc @@ -0,0 +1,123 @@ +#include "simple_pipeline.h" +#include "boards/common/board.h" +#include "audio_codecs/audio_codec.h" +#include +#include + +static inline int16_t lrint16(float v){ if(v>32767.f) return 32767; if(v<-32768.f) return -32768; return (int16_t)v; } + +static void resample_mono_nearest(const int16_t* in, int in_samples, int in_rate, int16_t* out, int out_samples, int out_rate){ + if(in_rate==out_rate){ for(int i=0;i=in_samples) idx=in_samples-1; out[i]=in[idx]; } +} + +recorder_pipeline_handle_t recorder_pipeline_open(){ + auto h = (recorder_pipeline_handle_t)pvPortMalloc(sizeof(recorder_pipeline_t)); + h->task = nullptr; + h->dest_rate = 16000; + auto codec = Board::GetInstance().GetAudioCodec(); + h->src_rate = codec->input_sample_rate(); + h->channels = 1; + h->block_bytes = (h->dest_rate/50)*sizeof(int16_t); + codec->EnableInput(true); + return h; +} + +void recorder_pipeline_run(recorder_pipeline_handle_t){ } + +void recorder_pipeline_close(recorder_pipeline_handle_t h){ + if(!h) return; + if (h->task) { + vTaskDelete(h->task); + } + auto codec = Board::GetInstance().GetAudioCodec(); + codec->EnableInput(false); + vPortFree(h); +} + +int recorder_pipeline_get_default_read_size(recorder_pipeline_handle_t h){ return h? h->block_bytes: 0; } + +int recorder_pipeline_read(recorder_pipeline_handle_t h, char *buffer, int buf_size){ + if(!h || !buffer) return 0; + auto codec = Board::GetInstance().GetAudioCodec(); + int out_samples = h->dest_rate/50; + std::vector tmp; + if(h->src_rate==h->dest_rate){ tmp.resize(out_samples); } + else{ tmp.resize((int)((float)out_samples*h->src_rate/h->dest_rate)); } + if(!codec->InputData(tmp)) return 0; + std::vector out(out_samples); + resample_mono_nearest(tmp.data(), (int)tmp.size(), h->src_rate, out.data(), out_samples, h->dest_rate); + int bytes = out_samples*sizeof(int16_t); + if(bytes>buf_size) bytes=buf_size; + memcpy(buffer, out.data(), bytes); + return bytes; +} + +player_pipeline_handle_t player_pipeline_open(){ + auto h = (player_pipeline_handle_t)pvPortMalloc(sizeof(player_pipeline_t)); + h->task = nullptr; + h->src_rate = 16000; + auto codec = Board::GetInstance().GetAudioCodec(); + h->dest_rate = codec->output_sample_rate(); + h->channels = codec->output_channels(); + h->block_bytes = (h->src_rate/50)*sizeof(int16_t); + h->fade_total = h->dest_rate / 10; // 100ms淡入 + h->fade_done = 0; + codec->EnableOutput(true); + return h; +} + +void player_pipeline_run(player_pipeline_handle_t){ } + +void player_pipeline_close(player_pipeline_handle_t h){ + if(!h) return; + if (h->task) { + vTaskDelete(h->task); + } + auto codec = Board::GetInstance().GetAudioCodec(); + codec->EnableOutput(false); + vPortFree(h); +} + +int player_pipeline_get_default_read_size(player_pipeline_handle_t h){ return h? h->block_bytes: 0; } + +int player_pipeline_write(player_pipeline_handle_t h, char *buffer, int buf_size){ + if(!h || !buffer || buf_size<=0) return 0; + int in_samples = buf_size/sizeof(int16_t); + std::vector in(in_samples); + memcpy(in.data(), buffer, buf_size); + int out_samples = (int)((float)in_samples*h->dest_rate/h->src_rate); + std::vector out(out_samples); + resample_mono_nearest(in.data(), in_samples, h->src_rate, out.data(), out_samples, h->dest_rate); + if (h->fade_done < h->fade_total) { + int n = out_samples; + for (int i = 0; i < n; ++i) { + int done = h->fade_done + i; + float g = done < h->fade_total ? (float)done / (float)h->fade_total : 1.0f; + out[i] = lrint16((float)out[i] * g); + } + h->fade_done += n; + if (h->fade_done > h->fade_total) h->fade_done = h->fade_total; + } + auto codec = Board::GetInstance().GetAudioCodec(); + if (h->channels == 2) { + std::vector stereo(out_samples * 2); + for (int i = 0, j = 0; i < out_samples; ++i) { + stereo[j++] = out[i]; + stereo[j++] = out[i]; + } + codec->OutputData(stereo); + } else { + codec->OutputData(out); + } + return buf_size; +} + +void player_pipeline_write_play_buffer_flag(player_pipeline_handle_t){ } + +void player_pipeline_set_src_rate(player_pipeline_handle_t h, int rate){ + if (!h || rate <= 0) return; + h->src_rate = rate; + h->block_bytes = (h->src_rate/50)*sizeof(int16_t); +} diff --git a/main/audio/simple_pipeline.h b/main/audio/simple_pipeline.h new file mode 100644 index 0000000..96c2fde --- /dev/null +++ b/main/audio/simple_pipeline.h @@ -0,0 +1,41 @@ +#ifndef SIMPLE_PIPELINE_H +#define SIMPLE_PIPELINE_H + +#include +#include +#include +#include + +typedef struct recorder_pipeline_t { + TaskHandle_t task; + int src_rate; + int dest_rate; + int channels; + int block_bytes; +} recorder_pipeline_t, *recorder_pipeline_handle_t; + +typedef struct player_pipeline_t { + TaskHandle_t task; + int src_rate; + int dest_rate; + int channels; + int block_bytes; + int fade_total; + int fade_done; +} player_pipeline_t, *player_pipeline_handle_t; + +recorder_pipeline_handle_t recorder_pipeline_open(); +void recorder_pipeline_run(recorder_pipeline_handle_t); +void recorder_pipeline_close(recorder_pipeline_handle_t); +int recorder_pipeline_get_default_read_size(recorder_pipeline_handle_t); +int recorder_pipeline_read(recorder_pipeline_handle_t, char *buffer, int buf_size); + +player_pipeline_handle_t player_pipeline_open(); +void player_pipeline_run(player_pipeline_handle_t); +void player_pipeline_close(player_pipeline_handle_t); +int player_pipeline_get_default_read_size(player_pipeline_handle_t); +int player_pipeline_write(player_pipeline_handle_t, char *buffer, int buf_size); +void player_pipeline_write_play_buffer_flag(player_pipeline_handle_t); +void player_pipeline_set_src_rate(player_pipeline_handle_t, int rate); + +#endif diff --git a/main/audio_codecs/audio_codec.cc b/main/audio_codecs/audio_codec.cc new file mode 100644 index 0000000..bb8eb9b --- /dev/null +++ b/main/audio_codecs/audio_codec.cc @@ -0,0 +1,72 @@ +#include "audio_codec.h" +#include "board.h" +#include "settings.h" + +#include +#include +#include + +#define TAG "AudioCodec" + +AudioCodec::AudioCodec() { +} + +AudioCodec::~AudioCodec() { +} + +void AudioCodec::OutputData(std::vector& data) { + Write(data.data(), data.size()); +} + +bool AudioCodec::InputData(std::vector& data) { + int samples = Read(data.data(), data.size()); + if (samples > 0) { + return true; + } + return false; +} + +void AudioCodec::Start() { + Settings settings("audio", false); + output_volume_ = settings.GetInt("output_volume", output_volume_); + if (output_volume_ <= 0) { + ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_); + output_volume_ = 10; + } + + ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); + ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); + + EnableInput(true); + EnableOutput(true); + ESP_LOGI(TAG, "Audio codec started"); +} + +void AudioCodec::SetOutputVolume(int volume) { + output_volume_ = volume; + ESP_LOGI(TAG, "Set output volume to %d", output_volume_); + + Settings settings("audio", true); + settings.SetInt("output_volume", output_volume_); +} + +void AudioCodec::SetOutputVolumeRuntime(int volume) { + output_volume_ = volume; + ESP_LOGI(TAG, "将运行时输出音量设置为:%d", output_volume_); +} + +void AudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + input_enabled_ = enable; + ESP_LOGI(TAG, "Set input enable to %s", enable ? "true" : "false"); +} + +void AudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + output_enabled_ = enable; + ESP_LOGI(TAG, "Set output enable to %s", enable ? "true" : "false"); +} diff --git a/main/audio_codecs/audio_codec.h b/main/audio_codecs/audio_codec.h new file mode 100644 index 0000000..e6c0d9d --- /dev/null +++ b/main/audio_codecs/audio_codec.h @@ -0,0 +1,60 @@ +#ifndef _AUDIO_CODEC_H +#define _AUDIO_CODEC_H + +#include +#include +#include + +#include +#include +#include + +#include "board.h" + +class AudioCodec { +public: + AudioCodec(); + virtual ~AudioCodec(); + + static constexpr int kDefaultOutputVolume = 40; // 默认输出音量 系统默认音量设置为100(最大音量),原来为70 产测固件使用 + inline static int default_output_volume() { return kDefaultOutputVolume; } + + virtual void SetOutputVolume(int volume); + virtual void SetOutputVolumeRuntime(int volume);// 运行时设置输出音量 + virtual void EnableInput(bool enable); + virtual void EnableOutput(bool enable); + + void Start(); + void OutputData(std::vector& data); + bool InputData(std::vector& data); + + inline bool duplex() const { return duplex_; } + inline bool input_reference() const { return input_reference_; } + inline int input_sample_rate() const { return input_sample_rate_; } + inline int output_sample_rate() const { return output_sample_rate_; } + inline int input_channels() const { return input_channels_; } + inline int output_channels() const { return output_channels_; } + inline int output_volume() const { return output_volume_; } + inline bool input_enabled() const { return input_enabled_; } + inline bool output_enabled() const { return output_enabled_; } + +protected: + i2s_chan_handle_t tx_handle_ = nullptr; + i2s_chan_handle_t rx_handle_ = nullptr; + + bool duplex_ = false; + bool input_reference_ = false; + bool input_enabled_ = false; + bool output_enabled_ = false; + int input_sample_rate_ = 0; + int output_sample_rate_ = 0; + int input_channels_ = 1; + int output_channels_ = 1; + // int output_volume_ = 60; // 系统默认音量设置为60,原来为70 生产环境需要恢复为60 + int output_volume_ = kDefaultOutputVolume; // 系统默认音量设置为100(最大音量),原来为70 生产测试音量 + + virtual int Read(int16_t* dest, int samples) = 0; + virtual int Write(const int16_t* data, int samples) = 0; +}; + +#endif // _AUDIO_CODEC_H diff --git a/main/audio_codecs/box_audio_codec.cc b/main/audio_codecs/box_audio_codec.cc new file mode 100644 index 0000000..c3ff271 --- /dev/null +++ b/main/audio_codecs/box_audio_codec.cc @@ -0,0 +1,247 @@ +#include "box_audio_codec.h" + +#include +#include +#include + +static const char TAG[] = "BoxAudioCodec"; + +BoxAudioCodec::BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Output + audio_codec_i2c_cfg_t i2c_cfg = { + .port = (i2c_port_t)1, + .addr = es8311_addr, + .bus_handle = i2c_master_handle, + }; + out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(out_ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8311_codec_cfg_t es8311_cfg = {}; + es8311_cfg.ctrl_if = out_ctrl_if_; + es8311_cfg.gpio_if = gpio_if_; + es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC; + es8311_cfg.pa_pin = pa_pin; + es8311_cfg.use_mclk = true; + es8311_cfg.hw_gain.pa_voltage = 5.0; + es8311_cfg.hw_gain.codec_dac_voltage = 3.3; + out_codec_if_ = es8311_codec_new(&es8311_cfg); + assert(out_codec_if_ != NULL); + + esp_codec_dev_cfg_t dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = out_codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&dev_cfg); + assert(output_dev_ != NULL); + + // Input + i2c_cfg.addr = es7210_addr; + in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(in_ctrl_if_ != NULL); + + es7210_codec_cfg_t es7210_cfg = {}; + es7210_cfg.ctrl_if = in_ctrl_if_; + es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2; + in_codec_if_ = es7210_codec_new(&es7210_cfg); + assert(in_codec_if_ != NULL); + + dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; + dev_cfg.codec_if = in_codec_if_; + input_dev_ = esp_codec_dev_new(&dev_cfg); + assert(input_dev_ != NULL); + + ESP_LOGI(TAG, "BoxAudioDevice initialized"); +} + +BoxAudioCodec::~BoxAudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void BoxAudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + i2s_tdm_config_t tdm_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)input_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .bclk_div = 8, + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3), + .ws_width = I2S_TDM_AUTO_WS_WIDTH, + .ws_pol = false, + .bit_shift = true, + .left_align = false, + .big_endian = false, + .bit_order_lsb = false, + .skip_mask = false, + .total_slot = I2S_TDM_AUTO_SLOT_NUM + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = I2S_GPIO_UNUSED, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void BoxAudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void BoxAudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = static_cast(input_channels_), + .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), + // .sample_rate = (uint32_t)output_sample_rate_, + .sample_rate = (uint32_t)input_sample_rate_, + .mclk_multiple = 0, + }; + if (input_reference_) { + fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); + } + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), 27.0)); + if (input_reference_) { + ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1), 21.0)); + } + ESP_LOGI(TAG, "Input opened: sr=%u ch=%u mask=0x%x ref=%d", (unsigned)fs.sample_rate, (unsigned)fs.channel, (unsigned)fs.channel_mask, (int)input_reference_); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void BoxAudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + // Play 16bit audio with configured channels + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = static_cast(output_channels_), + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + } + AudioCodec::EnableOutput(enable); +} + +int BoxAudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int BoxAudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} diff --git a/main/audio_codecs/box_audio_codec.h b/main/audio_codecs/box_audio_codec.h new file mode 100644 index 0000000..43cd090 --- /dev/null +++ b/main/audio_codecs/box_audio_codec.h @@ -0,0 +1,37 @@ +#ifndef _BOX_AUDIO_CODEC_H +#define _BOX_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include + +class BoxAudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; + const audio_codec_if_t* out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; + const audio_codec_if_t* in_codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference); + virtual ~BoxAudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _BOX_AUDIO_CODEC_H diff --git a/main/audio_codecs/es8311_audio_codec.cc b/main/audio_codecs/es8311_audio_codec.cc new file mode 100644 index 0000000..2185838 --- /dev/null +++ b/main/audio_codecs/es8311_audio_codec.cc @@ -0,0 +1,217 @@ +#include "es8311_audio_codec.h" + +#include + +static const char TAG[] = "Es8311AudioCodec"; + +Es8311AudioCodec::Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk) { + duplex_ = true; // 是否双工 + input_reference_ = false; // 是否使用参考输入,实现回声消除 + input_channels_ = 1; // 输入通道数 + // output_channels_ = 2; // 输出通道数 - 配置这行代码了RTC才可以正常加入房间,但是开机播报声音会尖锐 + output_channels_ = 2; // 输出通道数 - 立体声,确保与驱动默认兼容 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + pa_pin_ = pa_pin; + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Output + audio_codec_i2c_cfg_t i2c_cfg = { + .port = i2c_port, + .addr = es8311_addr, + .bus_handle = i2c_master_handle, + }; + ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8311_codec_cfg_t es8311_cfg = {}; + es8311_cfg.ctrl_if = ctrl_if_; + es8311_cfg.gpio_if = gpio_if_; + es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; + es8311_cfg.pa_pin = pa_pin; + es8311_cfg.use_mclk = use_mclk; + // 🎯 优化硬件增益设置,降低杂音 + es8311_cfg.hw_gain.pa_voltage = 3.3; // 降低PA电压,减少杂音 + es8311_cfg.hw_gain.codec_dac_voltage = 3.3; // 保持DAC电压 + codec_if_ = es8311_codec_new(&es8311_cfg); + assert(codec_if_ != NULL); + + esp_codec_dev_cfg_t dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&dev_cfg); + assert(output_dev_ != NULL); + dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; + input_dev_ = esp_codec_dev_new(&dev_cfg); + assert(input_dev_ != NULL); + esp_codec_set_disable_when_closed(output_dev_, false); + esp_codec_set_disable_when_closed(input_dev_, false); + ESP_LOGI(TAG, "Es8311AudioCodec初始化完成");// Es8311AudioCodec初始化完成 + ESP_LOGI(TAG, "Codec cfg: in_ch=%d out_ch=%d in_sr=%d out_sr=%d", input_channels_, output_channels_, input_sample_rate_, output_sample_rate_); +} + +Es8311AudioCodec::~Es8311AudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(codec_if_); + audio_codec_delete_ctrl_if(ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void Es8311AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 8, // 增加DMA描述符数量,提高稳定性 + .dma_frame_num = 320, // 优化帧数,减少音频断续 + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + // 修改I2S配置为单声道 + // .slot_mode = I2S_SLOT_MODE_MONO,// 同时保持编解码器通道数为1 output_channels_ = 1; + // .slot_mask = I2S_STD_SLOT_LEFT, // 同时保持编解码器通道数为1 output_channels_ = 1; + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + #ifdef I2S_HW_VERSION_2 + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + #endif + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created");// 双工通道创建完成 + ESP_LOGI(TAG, "I2S slots: mode=stereo mask=both bit_width=16");// I2S插槽:模式=立体声 掩码=都 位宽度=16 +} + +void Es8311AudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void Es8311AudioCodec::SetOutputVolumeRuntime(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolumeRuntime(volume); +} + +void Es8311AudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)input_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 30.0)); // 🎯 降低输入增益,减少杂音 + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void Es8311AudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + // Play 16bit with configured channel count + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = static_cast(output_channels_), + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + + // 🎯 添加音频质量优化:设置合适的输出增益 + if (output_volume_ > 0) { + // 当音量不为0时,确保PA引脚正确启用 + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 1); + vTaskDelay(pdMS_TO_TICKS(10)); // 短暂延迟确保PA稳定 + } + } + + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 0); + } + } + AudioCodec::EnableOutput(enable); +} + +int Es8311AudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int Es8311AudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} diff --git a/main/audio_codecs/es8311_audio_codec.h b/main/audio_codecs/es8311_audio_codec.h new file mode 100644 index 0000000..4cbd00a --- /dev/null +++ b/main/audio_codecs/es8311_audio_codec.h @@ -0,0 +1,39 @@ +#ifndef _ES8311_AUDIO_CODEC_H +#define _ES8311_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include +#include +#include + +class Es8311AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; + const audio_codec_if_t* codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + gpio_num_t pa_pin_ = GPIO_NUM_NC; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk = true); + virtual ~Es8311AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void SetOutputVolumeRuntime(int volume) override;// 运行时设置输出音量 + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _ES8311_AUDIO_CODEC_H diff --git a/main/audio_codecs/es8388_audio_codec.cc b/main/audio_codecs/es8388_audio_codec.cc new file mode 100644 index 0000000..347ef11 --- /dev/null +++ b/main/audio_codecs/es8388_audio_codec.cc @@ -0,0 +1,205 @@ +#include "es8388_audio_codec.h" + +#include + +static const char TAG[] = "Es8388AudioCodec"; + +Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8388_addr) { + duplex_ = true; // 是否双工 + input_reference_ = false; // 是否使用参考输入,实现回声消除 + input_channels_ = 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + pa_pin_ = pa_pin; CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Output + audio_codec_i2c_cfg_t i2c_cfg = { + .port = i2c_port, + .addr = es8388_addr, + .bus_handle = i2c_master_handle, + }; + ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8388_codec_cfg_t es8388_cfg = {}; + es8388_cfg.ctrl_if = ctrl_if_; + es8388_cfg.gpio_if = gpio_if_; + es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; + es8388_cfg.master_mode = true; + es8388_cfg.pa_pin = pa_pin; + es8388_cfg.pa_reverted = false; + es8388_cfg.hw_gain.pa_voltage = 5.0; + es8388_cfg.hw_gain.codec_dac_voltage = 3.3; + codec_if_ = es8388_codec_new(&es8388_cfg); + assert(codec_if_ != NULL); + + esp_codec_dev_cfg_t outdev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&outdev_cfg); + assert(output_dev_ != NULL); + + esp_codec_dev_cfg_t indev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_IN, + .codec_if = codec_if_, + .data_if = data_if_, + }; + input_dev_ = esp_codec_dev_new(&indev_cfg); + assert(input_dev_ != NULL); + esp_codec_set_disable_when_closed(output_dev_, false); + esp_codec_set_disable_when_closed(input_dev_, false); + ESP_LOGI(TAG, "Es8388AudioCodec initialized"); +} + +Es8388AudioCodec::~Es8388AudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(codec_if_); + audio_codec_delete_ctrl_if(ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void Es8388AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din){ + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void Es8388AudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void Es8388AudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)input_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void Es8388AudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + + // Set analog output volume to 0dB, default is -45dB + uint8_t reg_val = 30; // 0dB + uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL + for (uint8_t reg : regs) { + ctrl_if_->write_reg(ctrl_if_, reg, 1, ®_val, 1); + } + + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 1); + } + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 0); + } + } + AudioCodec::EnableOutput(enable); +} + +int Es8388AudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int Es8388AudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} diff --git a/main/audio_codecs/es8388_audio_codec.h b/main/audio_codecs/es8388_audio_codec.h new file mode 100644 index 0000000..10807a4 --- /dev/null +++ b/main/audio_codecs/es8388_audio_codec.h @@ -0,0 +1,37 @@ +#ifndef _ES8388_AUDIO_CODEC_H +#define _ES8388_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include +#include + +class Es8388AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; + const audio_codec_if_t* codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + gpio_num_t pa_pin_ = GPIO_NUM_NC; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8388_addr); + virtual ~Es8388AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _ES8388_AUDIO_CODEC_H diff --git a/main/audio_codecs/no_audio_codec.cc b/main/audio_codecs/no_audio_codec.cc new file mode 100644 index 0000000..8fbd5da --- /dev/null +++ b/main/audio_codecs/no_audio_codec.cc @@ -0,0 +1,394 @@ +#include "no_audio_codec.h" + +#include +#include +#include + +#define TAG "NoAudioCodec" + +NoAudioCodec::~NoAudioCodec() { + if (rx_handle_ != nullptr) { + ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_)); + } + if (tx_handle_ != nullptr) { + ESP_ERROR_CHECK(i2s_channel_disable(tx_handle_)); + } +} + +NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + duplex_ = true; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_MONO, + .slot_mask = I2S_STD_SLOT_LEFT, + .ws_width = I2S_DATA_BIT_WIDTH_32BIT, + .ws_pol = false, + .bit_shift = true, + #ifdef I2S_HW_VERSION_2 + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + #endif + + }, + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +ATK_NoAudioCodecDuplex::ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + duplex_ = true; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + #ifdef I2S_HW_VERSION_2 + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + #endif + }, + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + + +NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din) { + duplex_ = false; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + // Create a new channel for speaker + i2s_chan_config_t chan_cfg = { + .id = (i2s_port_t)0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_MONO, + .slot_mask = I2S_STD_SLOT_LEFT, + .ws_width = I2S_DATA_BIT_WIDTH_32BIT, + .ws_pol = false, + .bit_shift = true, + #ifdef I2S_HW_VERSION_2 + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + #endif + + }, + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = spk_bclk, + .ws = spk_ws, + .dout = spk_dout, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + + // Create a new channel for MIC + chan_cfg.id = (i2s_port_t)1; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_)); + std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_; + std_cfg.gpio_cfg.bclk = mic_sck; + std_cfg.gpio_cfg.ws = mic_ws; + std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; + std_cfg.gpio_cfg.din = mic_din; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Simplex channels created"); +} + +NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask){ + duplex_ = false; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + // Create a new channel for speaker + i2s_chan_config_t chan_cfg = { + .id = (i2s_port_t)0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_MONO, + .slot_mask = spk_slot_mask, + .ws_width = I2S_DATA_BIT_WIDTH_32BIT, + .ws_pol = false, + .bit_shift = true, + #ifdef I2S_HW_VERSION_2 + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + #endif + + }, + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = spk_bclk, + .ws = spk_ws, + .dout = spk_dout, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + + // Create a new channel for MIC + chan_cfg.id = (i2s_port_t)1; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_)); + std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_; + std_cfg.slot_cfg.slot_mask = mic_slot_mask; + std_cfg.gpio_cfg.bclk = mic_sck; + std_cfg.gpio_cfg.ws = mic_ws; + std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; + std_cfg.gpio_cfg.din = mic_din; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Simplex channels created"); +} + +NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din) { + duplex_ = false; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + // Create a new channel for speaker + i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)1, I2S_ROLE_MASTER); + tx_chan_cfg.dma_desc_num = 6; + tx_chan_cfg.dma_frame_num = 240; + tx_chan_cfg.auto_clear_after_cb = true; + tx_chan_cfg.auto_clear_before_cb = false; + tx_chan_cfg.intr_priority = 0; + ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle_, NULL)); + + + i2s_std_config_t tx_std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + + }, + .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = spk_bclk, + .ws = spk_ws, + .dout = spk_dout, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, + }, + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &tx_std_cfg)); +#if SOC_I2S_SUPPORTS_PDM_RX + // Create a new channel for MIC in PDM mode + i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER); + ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_)); + i2s_pdm_rx_config_t pdm_rx_cfg = { + .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_), + /* The data bit-width of PDM mode is fixed to 16 */ + .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = { + .clk = mic_sck, + .din = mic_din, + + .invert_flags = { + .clk_inv = false, + }, + }, + }; + ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle_, &pdm_rx_cfg)); +#else + ESP_LOGE(TAG, "PDM is not supported"); +#endif + ESP_LOGI(TAG, "Simplex channels created"); +} + +int NoAudioCodec::Write(const int16_t* data, int samples) { + std::vector buffer(samples); + + // output_volume_: 0-100 + // volume_factor_: 0-65536 + int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536; + for (int i = 0; i < samples; i++) { + int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算 + if (temp > INT32_MAX) { + buffer[i] = INT32_MAX; + } else if (temp < INT32_MIN) { + buffer[i] = INT32_MIN; + } else { + buffer[i] = static_cast(temp); + } + } + + size_t bytes_written; + ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY)); + return bytes_written / sizeof(int32_t); +} + +int NoAudioCodec::Read(int16_t* dest, int samples) { + size_t bytes_read; + + std::vector bit32_buffer(samples); + if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, portMAX_DELAY) != ESP_OK) { + ESP_LOGE(TAG, "Read Failed!"); + return 0; + } + + samples = bytes_read / sizeof(int32_t); + for (int i = 0; i < samples; i++) { + int32_t value = bit32_buffer[i] >> 12; + dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value; + } + return samples; +} + +int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) { + size_t bytes_read; + + // PDM 解调后的数据位宽为 16 位 + std::vector bit16_buffer(samples); + if (i2s_channel_read(rx_handle_, bit16_buffer.data(), samples * sizeof(int16_t), &bytes_read, portMAX_DELAY) != ESP_OK) { + ESP_LOGE(TAG, "Read Failed!"); + return 0; + } + + // 计算实际读取的样本数 + samples = bytes_read / sizeof(int16_t); + + // 将 16 位数据直接复制到目标缓冲区 + memcpy(dest, bit16_buffer.data(), samples * sizeof(int16_t)); + + return samples; +} diff --git a/main/audio_codecs/no_audio_codec.h b/main/audio_codecs/no_audio_codec.h new file mode 100644 index 0000000..51014f3 --- /dev/null +++ b/main/audio_codecs/no_audio_codec.h @@ -0,0 +1,40 @@ +#ifndef _NO_AUDIO_CODEC_H +#define _NO_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include + +class NoAudioCodec : public AudioCodec { +private: + virtual int Write(const int16_t* data, int samples) override; + virtual int Read(int16_t* dest, int samples) override; + +public: + virtual ~NoAudioCodec(); +}; + +class NoAudioCodecDuplex : public NoAudioCodec { +public: + NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); +}; + +class ATK_NoAudioCodecDuplex : public NoAudioCodec { +public: + ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); +}; + +class NoAudioCodecSimplex : public NoAudioCodec { +public: + NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din); + NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask); +}; + +class NoAudioCodecSimplexPdm : public NoAudioCodec { +public: + NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din); + int Read(int16_t* dest, int samples); +}; + +#endif // _NO_AUDIO_CODEC_H diff --git a/main/audio_processing/audio_processor.cc b/main/audio_processing/audio_processor.cc new file mode 100644 index 0000000..80a1713 --- /dev/null +++ b/main/audio_processing/audio_processor.cc @@ -0,0 +1,490 @@ +#include "audio_processor.h" +#include +#include +#include + +#define PROCESSOR_RUNNING 0x01 + +static const char* TAG = "AudioProcessor"; + +AudioProcessor::AudioProcessor() + : afe_data_(nullptr), adaptive_enabled_(true) { + event_group_ = xEventGroupCreate(); +} + +// 初始化音频处理器 +void AudioProcessor::Initialize(AudioCodec* codec, bool realtime_chat) { + codec_ = codec; + int ref_num = codec_->input_reference() ? 1 : 0; + + std::string input_format; + for (int i = 0; i < codec_->input_channels() - ref_num; i++) { + input_format.push_back('M'); + } + for (int i = 0; i < ref_num; i++) { + input_format.push_back('R'); + } + + srmodel_list_t *models = esp_srmodel_init("model"); + char* ns_model_name = esp_srmodel_filter(models, ESP_NSNET_PREFIX, NULL); + + afe_config_t* afe_config = afe_config_init(input_format.c_str(), NULL, AFE_TYPE_VC, AFE_MODE_HIGH_PERF); + if (realtime_chat) { + // 实时模式:启用AEC,但关闭VAD + afe_config->aec_init = true; + afe_config->aec_mode = AEC_MODE_VOIP_HIGH_PERF; // 高性能AEC模式 + + // 关闭VAD + afe_config->vad_init = false; + + ESP_LOGI(TAG, "Realtime mode: AEC enabled, VAD disabled"); + } else { + // 非实时模式:可根据需求配置 + afe_config->aec_init = false; + afe_config->vad_init = false; // 关闭VAD + + ESP_LOGI(TAG, "Non-realtime mode: VAD disabled"); + } + + // 启用噪声抑制 - 与官方项目保持一致 + afe_config->ns_init = true; + afe_config->ns_model_name = ns_model_name; + afe_config->afe_ns_mode = AFE_NS_MODE_NET; + + // 启用AGC并设置为WAKENET模式 - 与官方项目保持一致 + afe_config->agc_init = true; + afe_config->agc_mode = AFE_AGC_MODE_WAKENET; // 使用WAKENET模式的AGC + afe_config->agc_target_level_dbfs = -3; // 设置目标电平为-3dBFS(与官方默认一致) + + // 其他配置保持不变 + afe_config->afe_perferred_core = 1; + afe_config->afe_perferred_priority = 5; + afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_INTERNAL; + + ESP_LOGI(TAG, "AFE配置: 格式=%s 通道=%d 参考=%d", input_format.c_str(), codec_->input_channels(), codec_->input_reference() ? 1 : 0); + ESP_LOGI(TAG, "AFE配置: AEC=%s VAD=disabled AGC=enabled 核心=%d 优先级=%d", + realtime_chat ? "enabled" : "disabled", 1, 5); + + afe_iface_ = esp_afe_handle_from_config(afe_config); + afe_data_ = afe_iface_->create_from_config(afe_config); + + // 创建任务部分保持不变 + xTaskCreate([](void* arg) { + auto this_ = (AudioProcessor*)arg; + this_->AudioProcessorTask(); + vTaskDelete(NULL); + }, "audio_communication", 4096, this, 3, NULL); +} + +AudioProcessor::~AudioProcessor() { + if (afe_data_ != nullptr) { + afe_iface_->destroy(afe_data_); + } + vEventGroupDelete(event_group_); +} + +void AudioProcessor::Feed(const std::vector& data) { + if (afe_data_ != nullptr) { + afe_iface_->feed(afe_data_, (int16_t*)data.data()); + } +} + +void AudioProcessor::Start() { + xEventGroupSetBits(event_group_, PROCESSOR_RUNNING); +} + +void AudioProcessor::Stop() { + xEventGroupClearBits(event_group_, PROCESSOR_RUNNING); +} + +bool AudioProcessor::IsRunning() { + return (xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING) != 0; +} +// 输出回调函数,用于将处理后的音频数据发送到外部 +void AudioProcessor::OnOutput(std::function&& data)> callback) { + output_callback_ = callback; +} + +void AudioProcessor::OnVadStateChange(std::function callback) { + vad_state_change_callback_ = callback; +} + +void AudioProcessor::OnSimpleVadStateChange(std::function callback) { + simple_vad_state_change_callback_ = callback; +} + +size_t AudioProcessor::GetFeedSize() { + if (afe_iface_ != nullptr && afe_data_ != nullptr) { + return afe_iface_->get_feed_chunksize(afe_data_); + } + return 0; +} + +void AudioProcessor::AudioProcessorTask() { + auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_); + auto feed_size = afe_iface_->get_feed_chunksize(afe_data_); + ESP_LOGI(TAG, "Audio communication task started, feed size: %d fetch size: %d", + feed_size, fetch_size); + + while (true) { + xEventGroupWaitBits(event_group_, PROCESSOR_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY); + + auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY); + if ((xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING) == 0) { + continue; + } + if (res == nullptr || res->ret_value == ESP_FAIL) { + if (res != nullptr) { + ESP_LOGI(TAG, "Error code: %d", res->ret_value); + } + continue; + } + + // 🎯 简单VAD处理:用于普通业务(触摸忽略、LED状态等) + if (simple_vad_state_change_callback_) { + // 参考chumo4_yuan的简单实现:直接使用ESP-ADF的VAD结果 + static bool simple_is_speaking = false; + if (res->vad_state == VAD_SPEECH && !simple_is_speaking) { + simple_is_speaking = true; + simple_vad_state_change_callback_(true); + } else if (res->vad_state == VAD_SILENCE && simple_is_speaking) { + simple_is_speaking = false; + simple_vad_state_change_callback_(false); + } + } + + // 🔊 复杂VAD处理:小智AI官方语音打断方案(仅在语音打断功能启用时使用) + if (vad_state_change_callback_) { + // 核心逻辑:检测VAD状态变化,区分人声和回声 + bool human_voice_detected = (res->vad_state == VAD_SPEECH); + + if (human_voice_detected && !is_speaking_) { + // 语音开始:使用增强的回声感知评估,区分真实人声和设备回声 + if (EvaluateSpeechWithEchoAwareness(res)) { + is_speaking_ = true; + ESP_LOGI(TAG, "VAD: Human voice detected (echo-aware filtering)"); + // ESP_LOGI(TAG, "🗣️ 检测到人声(已通过回声过滤)"); + vad_state_change_callback_(true); + } else { + ESP_LOGV(TAG, "VAD: Voice rejected (likely device echo)"); + ESP_LOGV(TAG, "🔇 声音被判定为设备回声,已忽略"); + } + } else if (!human_voice_detected && is_speaking_) { + // 语音结束:VAD检测到静音 + is_speaking_ = false; + ESP_LOGI(TAG, "VAD: Human voice ended"); + ESP_LOGI(TAG, "🛑 人声结束(进入静音)"); + vad_state_change_callback_(false); + } + } + + if (output_callback_) { + // 确保音频数据在正确的内存区域分配,避免PSRAM/内部内存混乱 + size_t sample_count = res->data_size / sizeof(int16_t); + std::vector audio_data; + audio_data.reserve(sample_count); + + // 逐个复制数据,确保使用标准内存分配器 + int16_t* src_data = (int16_t*)res->data; + for (size_t i = 0; i < sample_count; i++) { + audio_data.push_back(src_data[i]); + } + + output_callback_(std::move(audio_data)); + } + } +} + +// 回声感知VAD优化方法实现 +void AudioProcessor::SetEchoAwareParams(const EchoAwareVadParams& params) { + echo_params_ = params; + ESP_LOGI(TAG, "Echo-aware VAD params updated: snr_threshold=%.2f, min_silence=%dms, cooldown=%dms", + params.snr_threshold, params.min_silence_ms, params.interrupt_cooldown_ms); +} + +void AudioProcessor::SetSpeakerVolume(float volume) { + current_speaker_volume_ = volume; + + // 🎯 触发自适应噪声抑制更新 + if (adaptive_enabled_ && echo_params_.adaptive_noise_suppression) { + AdaptSuppressionLevel(); + } + + ESP_LOGV(TAG, "Speaker volume updated: %.2f, adaptive suppression: %.2f", + volume, adaptive_state_.dynamic_suppression_level); +} + +bool AudioProcessor::IsEchoSuppressed() const { + return aec_converged_; +} + +bool AudioProcessor::EvaluateSpeechWithEchoAwareness(afe_fetch_result_t* fetch_result) { + if (!fetch_result || fetch_result->ret_value != ESP_OK) { + return false; + } + + // 检查VAD状态 - 基于实际的ESP-ADF API + bool basic_vad_detected = (fetch_result->vad_state == VAD_SPEECH); + + if (!basic_vad_detected) { + return false; + } + + // 增强的回声感知逻辑:多重检查机制 + if (echo_params_.adaptive_threshold) { + // 计算当前音频块的能量 + int16_t* audio_data = (int16_t*)fetch_result->data; + size_t sample_count = fetch_result->data_size / sizeof(int16_t); + + float energy = 0.0f; + float peak_amplitude = 0.0f; + for (size_t i = 0; i < sample_count; i++) { + float sample = (float)abs(audio_data[i]); + energy += sample * sample; + if (sample > peak_amplitude) { + peak_amplitude = sample; + } + } + energy = energy / sample_count; // 平均能量 + + // 🎯 自适应噪声抑制:根据实时环境动态调整阈值 + // 首先更新自适应状态 + UpdateAdaptiveNoiseState(audio_data, sample_count); + + // 获取动态抑制级别 + float adaptive_suppression = adaptive_enabled_ && echo_params_.adaptive_noise_suppression ? + adaptive_state_.dynamic_suppression_level : 1.0f; + + // 🔊 智能阈值计算:结合固定策略和自适应策略 + float volume_factor = 1.0f + current_speaker_volume_ * 500.0f; // 基础音量影响 + float adaptive_threshold = echo_params_.snr_threshold * volume_factor * adaptive_suppression; // 自适应增强 + float energy_threshold = adaptive_threshold * 10000000000.0f; // 基础阈值 + + // 超激进峰值检查:极度提高阈值,完全阻止误触发 + float peak_threshold = 500000.0f * volume_factor; // 超激进提高峰值阈值 + bool peak_check = (peak_amplitude > peak_threshold); + + // 能量检查 + bool energy_check = (energy > energy_threshold); + + // 超激进扬声器保护:任何微弱音频都极大提高阈值 + if (current_speaker_volume_ > 0.0001f) { // 极早触发保护 + energy_threshold *= 1000.0f; // 播放时能量阈值提高1000倍 + peak_threshold *= 500.0f; // 峰值阈值提高500倍 + energy_check = (energy > energy_threshold); + peak_check = (peak_amplitude > peak_threshold); + } + + // 频域特征检查:分析高频成分,人声通常有更多高频特征 + float high_freq_energy = 0.0f; + for (size_t i = sample_count / 2; i < sample_count; i++) { + float sample = (float)abs(audio_data[i]); + high_freq_energy += sample * sample; + } + high_freq_energy = high_freq_energy / (sample_count / 2); + + // 超激进高频比例检查:极度严格的人声特征要求 + float high_freq_ratio = (energy > 0) ? (high_freq_energy / energy) : 0.0f; + float freq_threshold = 1.2f * volume_factor; // 超激进提高高频比例要求到1.2(几乎不可能达到) + if (current_speaker_volume_ > 0.0001f) { + freq_threshold *= 50.0f; // 播放时超激进提高高频要求 + } + bool freq_check = (high_freq_ratio > freq_threshold); + + // 超激进稳定性检查:极度严格的信号变化要求 + float variance = 0.0f; + for (size_t i = 1; i < sample_count; i++) { + float diff = (float)(abs(audio_data[i]) - abs(audio_data[i-1])); + variance += diff * diff; + } + variance = variance / (sample_count - 1); + float variance_threshold = 10000000000.0f / volume_factor; // 超激进提高方差要求 + if (current_speaker_volume_ > 0.0001f) { + variance_threshold *= 100.0f; // 播放时超激进提高方差要求 + } + bool stability_check = (variance > variance_threshold); // 人声变化更大 + + // 增强连续性检查 - 真实人声通常有连续的特征变化 + static float prev_energy = 0.0f; + static float prev_high_freq_ratio = 0.0f; + static int consistent_frames = 0; // 连续帧计数 + + float energy_change = abs(energy - prev_energy) / (prev_energy + 1.0f); + float freq_change = abs(high_freq_ratio - prev_high_freq_ratio); + + // 超激进连续性要求:需要连续更多帧都满足极严格的人声特征 + bool frame_continuity = (energy_change > 1.2f && freq_change > 0.5f); // 超激进提高变化要求,且必须同时满足 + if (frame_continuity) { + consistent_frames++; + } else { + consistent_frames = 0; // 重置计数 + } + bool continuity_check = (consistent_frames >= 10); // 需要连续10帧都符合极严格人声特征 + + prev_energy = energy; + prev_high_freq_ratio = high_freq_ratio; + + // 最终综合判断:需要同时满足所有条件(绝对严格) + // 新增:播放时间检查 - 如果刚开始播放,额外严格 + static auto last_volume_update = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + if (current_speaker_volume_ > 0.01f) { + last_volume_update = now; + } + auto time_since_playback = std::chrono::duration_cast(now - last_volume_update); + bool recent_playback_protection = (time_since_playback.count() < 30000); // 播放后30秒内额外保护 + + bool final_result = energy_check && peak_check && freq_check && stability_check && + continuity_check && !recent_playback_protection; + + // 🔕 注释掉过于频繁的回声评估详细日志 - 只在结果为true时输出 + if (final_result) { + ESP_LOGI(TAG, "🎯 HUMAN VOICE DETECTED: duration=%.0fms, vol=%.3f, adaptive=%.1f%s", + (float)time_since_playback.count(), current_speaker_volume_, adaptive_suppression, + adaptive_state_.high_interference_mode ? "[HIGH_INTERFERENCE]" : ""); + } + + + return final_result; + } + + // 非自适应模式,直接信任VAD结果 + return true; +} + +// 🎯 自适应噪声抑制核心算法实现 +void AudioProcessor::UpdateAdaptiveNoiseState(const int16_t* audio_data, size_t sample_count) { + if (!adaptive_enabled_ || !echo_params_.adaptive_noise_suppression) { + return; + } + + // 计算当前回声强度 + float echo_strength = CalculateEchoStrength(audio_data, sample_count); + adaptive_state_.current_echo_strength = echo_strength; + + // 估算距离因子 (基于回声强度和音量) + adaptive_state_.estimated_distance_factor = EstimateDistanceFactor(echo_strength, current_speaker_volume_); + + // 更新环境噪声基线 + if (current_speaker_volume_ < 0.01f) { // 扬声器几乎静音时更新基线 + float current_noise = 0.0f; + for (size_t i = 0; i < sample_count; i++) { + current_noise += abs(audio_data[i]); + } + current_noise /= sample_count; + + // 指数移动平均更新噪声基线 + adaptive_state_.noise_baseline = adaptive_state_.noise_baseline * 0.95f + current_noise * 0.05f; + } + + // 自适应调整抑制级别 + AdaptSuppressionLevel(); + + adaptive_state_.last_adaptation_time = std::chrono::steady_clock::now(); +} + +float AudioProcessor::CalculateEchoStrength(const int16_t* audio_data, size_t sample_count) { + if (current_speaker_volume_ < 0.001f) { + return 0.0f; // 扬声器静音,无回声 + } + + // 计算音频能量 + float energy = 0.0f; + float peak = 0.0f; + for (size_t i = 0; i < sample_count; i++) { + float sample = abs(audio_data[i]); + energy += sample * sample; + if (sample > peak) peak = sample; + } + energy = std::sqrt(energy / sample_count); + + // 🔊 回声强度 = 能量 × 峰值比 × 音量影响 + float peak_ratio = (energy > 0) ? (peak / energy) : 0.0f; + + // 🎯 关键洞察:回声具有特征性的能量分布模式 + // 真实人声:能量分布更均匀,峰值比较低 + // 设备回声:能量集中,峰值比较高 + float echo_indicator = peak_ratio * current_speaker_volume_; + + return echo_indicator; +} + +float AudioProcessor::EstimateDistanceFactor(float echo_strength, float volume) { + if (volume < 0.001f) { + return 1.0f; // 静音时认为距离无关紧要 + } + + // 🎯 基于物理原理的距离估算: + // 回声强度 ∝ 音量² / 距离² + // 距离因子 = 1 / (1 + echo_strength * volume_sensitivity) + // 值越小表示越近,值越大表示越远 + + float normalized_echo = echo_strength / (volume + 0.001f); // 归一化回声 + float distance_factor = 1.0f / (1.0f + normalized_echo * echo_params_.volume_sensitivity); + + // 🔊 约束距离因子范围 [0.1, 1.0] + distance_factor = std::max(0.1f, std::min(1.0f, distance_factor)); + + // 🔕 注释掉过于频繁的距离估算日志 + // ESP_LOGD(TAG, "🎯 Distance estimation: echo=%.3f, vol=%.3f, factor=%.3f", + // echo_strength, volume, distance_factor); + + return distance_factor; +} + +void AudioProcessor::AdaptSuppressionLevel() { + if (!adaptive_enabled_ || !echo_params_.adaptive_noise_suppression) { + adaptive_state_.dynamic_suppression_level = 1.0f; + return; + } + + // 🎯 自适应抑制级别计算 + // 基础抑制级别 + float base_level = echo_params_.noise_suppression_base; + + // 🔊 音量影响:音量越大,抑制越强 + float volume_multiplier = 1.0f + current_speaker_volume_ * echo_params_.volume_sensitivity; + + // 📏 距离影响:距离越近,抑制越强 + float distance_multiplier = 1.0f / (adaptive_state_.estimated_distance_factor + 0.1f); + + // 🌊 回声强度影响:回声越强,抑制越强 + float echo_multiplier = 1.0f + adaptive_state_.current_echo_strength * 2.0f; + + // 🎯 综合计算动态抑制级别 + adaptive_state_.dynamic_suppression_level = base_level * volume_multiplier * distance_multiplier * echo_multiplier; + + // 📊 高干扰模式判断 + bool was_high_interference = adaptive_state_.high_interference_mode; + adaptive_state_.high_interference_mode = ( + current_speaker_volume_ > 0.3f && // 高音量 + adaptive_state_.estimated_distance_factor < 0.5f && // 近距离 + adaptive_state_.current_echo_strength > echo_params_.echo_detection_threshold // 强回声 + ); + + // 🚨 高干扰模式额外保护 + if (adaptive_state_.high_interference_mode) { + adaptive_state_.dynamic_suppression_level *= 5.0f; // 高干扰时5倍抑制 + + if (!was_high_interference) { + ESP_LOGW(TAG, "🔴 Entering HIGH INTERFERENCE mode - vol=%.2f, dist=%.2f, echo=%.3f", + current_speaker_volume_, adaptive_state_.estimated_distance_factor, + adaptive_state_.current_echo_strength); + } + } else if (was_high_interference) { + ESP_LOGI(TAG, "🟢 Exiting high interference mode - returning to adaptive suppression"); + } + + // 📏 限制抑制级别范围 [1.0, 100.0] + adaptive_state_.dynamic_suppression_level = std::max(1.0f, std::min(100.0f, adaptive_state_.dynamic_suppression_level)); + + // 🔕 注释掉过于频繁的自适应抑制日志 + // ESP_LOGD(TAG, "🎯 Adaptive suppression: vol=%.2f, dist=%.2f, echo=%.3f → level=%.1f %s", + // current_speaker_volume_, adaptive_state_.estimated_distance_factor, + // adaptive_state_.current_echo_strength, adaptive_state_.dynamic_suppression_level, + // adaptive_state_.high_interference_mode ? "[HIGH_INTERFERENCE]" : ""); +} + +AdaptiveNoiseState AudioProcessor::GetAdaptiveState() const { + return adaptive_state_; +} \ No newline at end of file diff --git a/main/audio_processing/audio_processor.h b/main/audio_processing/audio_processor.h new file mode 100644 index 0000000..4b97ecc --- /dev/null +++ b/main/audio_processing/audio_processor.h @@ -0,0 +1,92 @@ +#ifndef AUDIO_PROCESSOR_H +#define AUDIO_PROCESSOR_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "audio_codec.h" + +// 回声感知VAD优化参数结构 +struct EchoAwareVadParams { + float snr_threshold = 0.3f; // 信噪比阈值 + int min_silence_ms = 200; // 最小静音持续时间 + int interrupt_cooldown_ms = 500; // 打断冷却时间 + bool adaptive_threshold = true; // 是否启用自适应阈值 + + // 自适应噪声抑制参数 + bool adaptive_noise_suppression = false; // 是否启用自适应噪声抑制 + float noise_suppression_base = 2.0f; // 基础噪声抑制强度 + float volume_sensitivity = 3.0f; // 音量敏感度 + float echo_detection_threshold = 0.2f; // 回声检测阈值 + float distance_estimation_factor = 2.0f; // 距离估算因子 +}; + +// 自适应噪声状态结构 +struct AdaptiveNoiseState { + float current_echo_strength = 0.0f; // 当前回声强度 + float estimated_distance_factor = 1.0f; // 估算的距离因子 + float dynamic_suppression_level = 1.0f; // 动态抑制级别 + float noise_baseline = 0.0f; // 噪声基准线 + bool high_interference_mode = false; // 高干扰模式 + std::chrono::steady_clock::time_point last_adaptation_time; // 最后自适应时间 +}; + +class AudioProcessor { +public: + AudioProcessor(); + ~AudioProcessor(); + + void Initialize(AudioCodec* codec, bool realtime_chat); + void Feed(const std::vector& data); + void Start(); + void Stop(); + bool IsRunning(); + void OnOutput(std::function&& data)> callback);// 输出回调函数,用于将处理后的音频数据发送到外部 + void OnVadStateChange(std::function callback); + void OnSimpleVadStateChange(std::function callback); // 简单VAD回调,用于普通业务 + size_t GetFeedSize(); + + // 新增:回声感知VAD优化接口 + void SetEchoAwareParams(const EchoAwareVadParams& params); + void SetSpeakerVolume(float volume); // 动态调整VAD阈值 + bool IsEchoSuppressed() const; // 检查AEC抑制状态 + +private: + EventGroupHandle_t event_group_ = nullptr; + const esp_afe_sr_iface_t* afe_iface_ = nullptr; + esp_afe_sr_data_t* afe_data_ = nullptr; + std::function&& data)> output_callback_; + std::function vad_state_change_callback_; // 复杂VAD回调(语音打断专用) + std::function simple_vad_state_change_callback_; // 简单VAD回调(普通业务) + AudioCodec* codec_ = nullptr; + bool is_speaking_ = false; + + // 新增:回声感知优化相关成员 + EchoAwareVadParams echo_params_; + float current_speaker_volume_ = 1.0f; + std::chrono::steady_clock::time_point last_interrupt_time_; + bool aec_converged_ = false; + + // 自适应噪声抑制相关成员 + AdaptiveNoiseState adaptive_state_; + bool adaptive_enabled_ = false; + + void AudioProcessorTask(); + bool EvaluateSpeechWithEchoAwareness(afe_fetch_result_t* fetch_result); // 回声感知语音评估 + + // 自适应噪声抑制方法 + void UpdateAdaptiveNoiseState(const int16_t* audio_data, size_t sample_count); + float CalculateEchoStrength(const int16_t* audio_data, size_t sample_count); + float EstimateDistanceFactor(float echo_strength, float volume); + void AdaptSuppressionLevel(); + AdaptiveNoiseState GetAdaptiveState() const; +}; + +#endif diff --git a/main/audio_processing/custom_wake_word.cc b/main/audio_processing/custom_wake_word.cc new file mode 100644 index 0000000..16d892e --- /dev/null +++ b/main/audio_processing/custom_wake_word.cc @@ -0,0 +1,352 @@ +#include "custom_wake_word.h" +#include "application.h" + +#include +#include +#include +#include "esp_wn_iface.h" +#include "esp_wn_models.h" +#include "esp_afe_sr_iface.h" +#include "esp_afe_sr_models.h" +#include "esp_mn_iface.h" +#include "esp_mn_models.h" +// #include "esp_mn_speech_commands.h" // 这个头文件可能不存在,命令相关函数在esp_mn_models.h中 +#include + +// ESP-SR中的multinet命令相关函数声明 +extern "C" { + void esp_mn_commands_clear(void); + esp_err_t esp_mn_commands_add(int command_id, const char *phoneme); + esp_err_t esp_mn_commands_update(void); +} + +#define DETECTION_RUNNING_EVENT 1 + +#define TAG "CustomWakeWord" + + +CustomWakeWord::CustomWakeWord() + : afe_data_(nullptr), + wake_word_pcm_(), + wake_word_opus_() { + + event_group_ = xEventGroupCreate(); +} + +CustomWakeWord::~CustomWakeWord() { + if (afe_data_ != nullptr) { + afe_iface_->destroy(afe_data_); + } + + // 清理 multinet 资源 + if (multinet_model_data_ != nullptr && multinet_ != nullptr) { + multinet_->destroy(multinet_model_data_); + multinet_model_data_ = nullptr; + } + + if (wake_word_encode_task_stack_ != nullptr) { + heap_caps_free(wake_word_encode_task_stack_); + } + + vEventGroupDelete(event_group_); +} + +bool CustomWakeWord::Initialize(AudioCodec* codec) { + codec_ = codec; + + models = esp_srmodel_init("model"); + if (models == nullptr || models->num == -1) { + ESP_LOGE(TAG, "Failed to initialize wakenet model"); + return false; + } + + // 初始化 multinet (命令词识别) + mn_name_ = esp_srmodel_filter(models, ESP_MN_PREFIX, ESP_MN_CHINESE); + if (mn_name_ == nullptr) { + ESP_LOGE(TAG, "Failed to initialize multinet, mn_name is nullptr"); + ESP_LOGI(TAG, "Please refer to https://pcn7cs20v8cr.feishu.cn/wiki/CpQjwQsCJiQSWSkYEvrcxcbVnwh to add custom wake word"); + return false; + } + + ESP_LOGI(TAG, "multinet:%s", mn_name_); + multinet_ = esp_mn_handle_from_name(mn_name_); + if (multinet_ == nullptr) { + ESP_LOGE(TAG, "Failed to get multinet handle"); + return false; + } + + // 🛡️ 安全的模型创建:添加重试机制 + multinet_model_data_ = nullptr; + for (int retry = 0; retry < 3; retry++) { + multinet_model_data_ = multinet_->create(mn_name_, 2000); // 2秒超时 + if (multinet_model_data_ != nullptr) { + break; + } + ESP_LOGW(TAG, "Multinet create failed, retry %d/3", retry + 1); + vTaskDelay(pdMS_TO_TICKS(100)); + } + + if (multinet_model_data_ == nullptr) { + ESP_LOGE(TAG, "Failed to create multinet model data after 3 retries"); + return false; + } + + // 🛡️ 安全的参数设置:添加验证 + if (multinet_->set_det_threshold(multinet_model_data_, 0.2) != ESP_OK) { + ESP_LOGW(TAG, "Failed to set detection threshold"); + } + + esp_mn_commands_clear(); + if (esp_mn_commands_add(1, CONFIG_CUSTOM_WAKE_WORD) != ESP_OK) { + ESP_LOGE(TAG, "Failed to add custom wake word command"); + return false; + } + + if (esp_mn_commands_update() != ESP_OK) { + ESP_LOGE(TAG, "Failed to update commands"); + return false; + } + + // 打印所有的命令词 + multinet_->print_active_speech_commands(multinet_model_data_); + ESP_LOGI(TAG, "Custom wake word: %s", CONFIG_CUSTOM_WAKE_WORD); + + // 初始化 afe + int ref_num = codec_->input_reference() ? 1 : 0; + std::string input_format; + for (int i = 0; i < codec_->input_channels() - ref_num; i++) { + input_format.push_back('M'); + } + for (int i = 0; i < ref_num; i++) { + input_format.push_back('R'); + } + + // 为自定义唤醒词创建不包含wakenet的AFE配置 + afe_config_t* afe_config = afe_config_init(input_format.c_str(), nullptr, AFE_TYPE_SR, AFE_MODE_HIGH_PERF); + afe_config->aec_init = codec_->input_reference(); + afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF; + afe_config->afe_perferred_core = 1; + afe_config->afe_perferred_priority = 1; + afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM; + // 明确禁用wakenet + afe_config->wakenet_init = false; + + afe_iface_ = esp_afe_handle_from_config(afe_config); + afe_data_ = afe_iface_->create_from_config(afe_config); + + xTaskCreate([](void* arg) { + auto this_ = (CustomWakeWord*)arg; + this_->AudioDetectionTask(); + vTaskDelete(NULL); + }, "audio_detection", 16384, this, 3, nullptr); + + return true; +} + +void CustomWakeWord::OnWakeWordDetected(std::function callback) { + wake_word_detected_callback_ = callback; +} + +void CustomWakeWord::Start() { + xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT); +} + +void CustomWakeWord::Stop() { + // 🛡️ 安全停止:先清除运行标志,然后等待任务稳定 + xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT); + + // 短暂延迟确保检测任务看到停止信号 + vTaskDelay(pdMS_TO_TICKS(50)); + + if (afe_data_ != nullptr) { + afe_iface_->reset_buffer(afe_data_); + } + + // 🛡️ 清理multinet状态,防止残留状态导致崩溃 + if (multinet_ != nullptr && multinet_model_data_ != nullptr) { + try { + // 重置multinet状态 + ESP_LOGI(TAG, "Resetting multinet state"); + } catch (...) { + ESP_LOGW(TAG, "Exception while resetting multinet state"); + } + } +} + +bool CustomWakeWord::IsRunning() { + return xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT; +} + +void CustomWakeWord::Feed(const std::vector& data) { + if (afe_data_ == nullptr) { + return; + } + afe_iface_->feed(afe_data_, data.data()); +} + +size_t CustomWakeWord::GetFeedSize() { + if (afe_data_ == nullptr) { + return 0; + } + return afe_iface_->get_feed_chunksize(afe_data_) * codec_->input_channels(); +} + +void CustomWakeWord::AudioDetectionTask() { + auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_); + auto feed_size = afe_iface_->get_feed_chunksize(afe_data_); + + // 检查 multinet 是否已正确初始化 + if (multinet_ == nullptr || multinet_model_data_ == nullptr) { + ESP_LOGE(TAG, "Multinet not initialized properly"); + return; + } + + int mu_chunksize = multinet_->get_samp_chunksize(multinet_model_data_); + assert(mu_chunksize == feed_size); + + ESP_LOGI(TAG, "Audio detection task started, feed size: %d fetch size: %d", feed_size, fetch_size); + + // wakenet已在AFE配置阶段禁用,直接使用multinet检测自定义唤醒词 + + while (true) { + xEventGroupWaitBits(event_group_, DETECTION_RUNNING_EVENT, pdFALSE, pdTRUE, portMAX_DELAY); + + auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY); + if (res == nullptr || res->ret_value == ESP_FAIL) { + ESP_LOGW(TAG, "Fetch failed, continue"); + continue; + } + + // 🛡️ 安全检查:验证数据有效性 + if (res->data == nullptr || res->data_size == 0) { + ESP_LOGW(TAG, "Invalid audio data: data=%p, size=%d", res->data, res->data_size); + continue; + } + + // 🛡️ 安全检查:验证multinet状态 + if (multinet_ == nullptr || multinet_model_data_ == nullptr) { + ESP_LOGE(TAG, "Multinet not initialized: multinet_=%p, model_data=%p", multinet_, multinet_model_data_); + continue; + } + + // 存储音频数据用于语音识别 + StoreWakeWordData(res->data, res->data_size / sizeof(int16_t)); + + // 🛡️ 安全的multinet检测:添加异常处理 + esp_mn_state_t mn_state = ESP_MN_STATE_DETECTING; + try { + // 额外的数据大小检查 + size_t expected_size = feed_size * sizeof(int16_t); + if (res->data_size != expected_size) { + ESP_LOGW(TAG, "Unexpected data size: got %d, expected %zu", res->data_size, expected_size); + // 继续处理,但记录警告 + } + + mn_state = multinet_->detect(multinet_model_data_, res->data); + } catch (...) { + ESP_LOGE(TAG, "Exception in multinet detect, skipping this frame"); + continue; + } + + if (mn_state == ESP_MN_STATE_DETECTING) { + // 仍在检测中,继续 + continue; + } else if (mn_state == ESP_MN_STATE_DETECTED) { + // 检测到自定义唤醒词 + esp_mn_results_t *mn_result = nullptr; + try { + mn_result = multinet_->get_results(multinet_model_data_); + } catch (...) { + ESP_LOGE(TAG, "Exception in get_results, continuing"); + continue; + } + + // 🛡️ 安全检查:验证结果有效性 + if (mn_result == nullptr) { + ESP_LOGW(TAG, "MultNet result is null, continuing"); + continue; + } + + ESP_LOGI(TAG, "MultNet detected: command_id=%d, string=%s, prob=%f, phrase_id=%d", + mn_result->command_id[0], mn_result->string ? mn_result->string : "null", + mn_result->prob[0], mn_result->phrase_id[0]); + + if (mn_result->command_id[0] == 1) { // 自定义唤醒词 + ESP_LOGI(TAG, "Custom wake word '%s' detected successfully!", CONFIG_CUSTOM_WAKE_WORD); + + // 停止检测 + Stop(); + last_detected_wake_word_ = CONFIG_CUSTOM_WAKE_WORD_DISPLAY; + + // 调用回调 + if (wake_word_detected_callback_) { + wake_word_detected_callback_(last_detected_wake_word_); + } + + // 清理multinet状态,准备下次检测 + multinet_->clean(multinet_model_data_); + ESP_LOGI(TAG, "Ready for next detection"); + } + } else if (mn_state == ESP_MN_STATE_TIMEOUT) { + // 超时,清理状态继续检测 + ESP_LOGD(TAG, "Command word detection timeout, cleaning state"); + multinet_->clean(multinet_model_data_); + continue; + } + } + + ESP_LOGI(TAG, "Audio detection task ended"); +} + +void CustomWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) { + // store audio data to wake_word_pcm_ + wake_word_pcm_.emplace_back(std::vector(data, data + samples)); + // keep about 2 seconds of data, detect duration is 30ms (sample_rate == 16000, chunksize == 512) + while (wake_word_pcm_.size() > 2000 / 30) { + wake_word_pcm_.pop_front(); + } +} + +void CustomWakeWord::EncodeWakeWordData() { + wake_word_opus_.clear(); + if (wake_word_encode_task_stack_ == nullptr) { + wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(4096 * 8, MALLOC_CAP_SPIRAM); + } + wake_word_encode_task_ = xTaskCreateStatic([](void* arg) { + auto this_ = (CustomWakeWord*)arg; + { + auto start_time = esp_timer_get_time(); + auto encoder = std::make_unique(16000, 1, OPUS_FRAME_DURATION_MS); + encoder->SetComplexity(0); // 0 is the fastest + + int packets = 0; + for (auto& pcm: this_->wake_word_pcm_) { + encoder->Encode(std::move(pcm), [this_](std::vector&& opus) { + std::lock_guard lock(this_->wake_word_mutex_); + this_->wake_word_opus_.emplace_back(std::move(opus)); + this_->wake_word_cv_.notify_all(); + }); + packets++; + } + this_->wake_word_pcm_.clear(); + + auto end_time = esp_timer_get_time(); + ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000)); + + std::lock_guard lock(this_->wake_word_mutex_); + this_->wake_word_opus_.push_back(std::vector()); + this_->wake_word_cv_.notify_all(); + } + vTaskDelete(NULL); + }, "encode_detect_packets", 4096 * 8, this, 2, wake_word_encode_task_stack_, &wake_word_encode_task_buffer_); +} + +bool CustomWakeWord::GetWakeWordOpus(std::vector& opus) { + std::unique_lock lock(wake_word_mutex_); + wake_word_cv_.wait(lock, [this]() { + return !wake_word_opus_.empty(); + }); + opus.swap(wake_word_opus_.front()); + wake_word_opus_.pop_front(); + return !opus.empty(); +} \ No newline at end of file diff --git a/main/audio_processing/custom_wake_word.h b/main/audio_processing/custom_wake_word.h new file mode 100644 index 0000000..1bc57fc --- /dev/null +++ b/main/audio_processing/custom_wake_word.h @@ -0,0 +1,72 @@ +#ifndef CUSTOM_WAKE_WORD_H +#define CUSTOM_WAKE_WORD_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "audio_codec.h" +#include "wake_word.h" +#include + +class CustomWakeWord : public WakeWord { +public: + CustomWakeWord(); + ~CustomWakeWord(); + + bool Initialize(AudioCodec* codec) override; + void Feed(const std::vector& data) override; + void OnWakeWordDetected(std::function callback) override; + void Start() override; + void Stop() override; + bool IsRunning() override; + size_t GetFeedSize() override; + void EncodeWakeWordData() override; + bool GetWakeWordOpus(std::vector& opus) override; + const std::string& GetLastDetectedWakeWord() const override { return last_detected_wake_word_; } + +private: + esp_afe_sr_iface_t* afe_iface_ = nullptr; + esp_afe_sr_data_t* afe_data_ = nullptr; + srmodel_list_t *models = nullptr; + + // multinet 相关成员变量 + esp_mn_iface_t* multinet_ = nullptr; + model_iface_data_t* multinet_model_data_ = nullptr; + char* mn_name_ = nullptr; + + char* wakenet_model_ = NULL; + std::vector wake_words_; + EventGroupHandle_t event_group_; + std::function wake_word_detected_callback_; + AudioCodec* codec_ = nullptr; + std::string last_detected_wake_word_; + + TaskHandle_t wake_word_encode_task_ = nullptr; + StaticTask_t wake_word_encode_task_buffer_; + StackType_t* wake_word_encode_task_stack_ = nullptr; + std::list> wake_word_pcm_; + std::list> wake_word_opus_; + std::mutex wake_word_mutex_; + std::condition_variable wake_word_cv_; + + void StoreWakeWordData(const int16_t* data, size_t size); + void AudioDetectionTask(); +}; + +#endif \ No newline at end of file diff --git a/main/audio_processing/wake_word.h b/main/audio_processing/wake_word.h new file mode 100644 index 0000000..094cfb5 --- /dev/null +++ b/main/audio_processing/wake_word.h @@ -0,0 +1,26 @@ +#ifndef WAKE_WORD_H +#define WAKE_WORD_H + +#include +#include +#include + +#include "audio_codec.h" + +class WakeWord { +public: + virtual ~WakeWord() = default; + + virtual bool Initialize(AudioCodec* codec) = 0; + virtual void Feed(const std::vector& data) = 0; + virtual void OnWakeWordDetected(std::function callback) = 0; + virtual void Start() = 0; + virtual void Stop() = 0; + virtual bool IsRunning() = 0; + virtual size_t GetFeedSize() = 0; + virtual void EncodeWakeWordData() = 0; + virtual bool GetWakeWordOpus(std::vector& opus) = 0; + virtual const std::string& GetLastDetectedWakeWord() const = 0; +}; + +#endif \ No newline at end of file diff --git a/main/audio_processing/wake_word_detect.cc b/main/audio_processing/wake_word_detect.cc new file mode 100644 index 0000000..578d8ec --- /dev/null +++ b/main/audio_processing/wake_word_detect.cc @@ -0,0 +1,181 @@ +#include "wake_word_detect.h" +#include "application.h" + +#include +#include +#include +#include + +#define DETECTION_RUNNING_EVENT 1 + +static const char* TAG = "WakeWordDetect"; + +WakeWordDetect::WakeWordDetect() + : afe_data_(nullptr), + wake_word_pcm_(), + wake_word_opus_() { + + event_group_ = xEventGroupCreate(); +} + +WakeWordDetect::~WakeWordDetect() { + if (afe_data_ != nullptr) { + afe_iface_->destroy(afe_data_); + } + + if (wake_word_encode_task_stack_ != nullptr) { + heap_caps_free(wake_word_encode_task_stack_); + } + + vEventGroupDelete(event_group_); +} + +bool WakeWordDetect::Initialize(AudioCodec* codec) { + codec_ = codec; + int ref_num = codec_->input_reference() ? 1 : 0; + + srmodel_list_t *models = esp_srmodel_init("model"); + for (int i = 0; i < models->num; i++) { + ESP_LOGI(TAG, "Model %d: %s", i, models->model_name[i]); + if (strstr(models->model_name[i], ESP_WN_PREFIX) != NULL) { + wakenet_model_ = models->model_name[i]; + auto words = esp_srmodel_get_wake_words(models, wakenet_model_); + // split by ";" to get all wake words + std::stringstream ss(words); + std::string word; + while (std::getline(ss, word, ';')) { + wake_words_.push_back(word); + } + } + } + + std::string input_format; + for (int i = 0; i < codec_->input_channels() - ref_num; i++) { + input_format.push_back('M'); + } + for (int i = 0; i < ref_num; i++) { + input_format.push_back('R'); + } + afe_config_t* afe_config = afe_config_init(input_format.c_str(), models, AFE_TYPE_SR, AFE_MODE_HIGH_PERF); + afe_config->aec_init = codec_->input_reference(); + afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF; + afe_config->afe_perferred_core = 1; + afe_config->afe_perferred_priority = 1; + afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM; + + afe_iface_ = esp_afe_handle_from_config(afe_config); + afe_data_ = afe_iface_->create_from_config(afe_config); + + xTaskCreate([](void* arg) { + auto this_ = (WakeWordDetect*)arg; + this_->AudioDetectionTask(); + vTaskDelete(NULL); + }, "audio_detection", 4096, this, 3, nullptr); + + return true; +} + +void WakeWordDetect::OnWakeWordDetected(std::function callback) { + wake_word_detected_callback_ = callback; +} + +void WakeWordDetect::Start() { + xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT); +} + +void WakeWordDetect::Stop() { + xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT); + afe_iface_->reset_buffer(afe_data_); +} + +bool WakeWordDetect::IsRunning() { + return xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT; +} + +void WakeWordDetect::Feed(const std::vector& data) { + afe_iface_->feed(afe_data_, data.data()); +} + +size_t WakeWordDetect::GetFeedSize() { + return afe_iface_->get_feed_chunksize(afe_data_) * codec_->input_channels(); +} + +void WakeWordDetect::AudioDetectionTask() { + auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_); + auto feed_size = afe_iface_->get_feed_chunksize(afe_data_); + ESP_LOGI(TAG, "Audio detection task started, feed size: %d fetch size: %d", + feed_size, fetch_size); + + while (true) { + xEventGroupWaitBits(event_group_, DETECTION_RUNNING_EVENT, pdFALSE, pdTRUE, portMAX_DELAY); + + auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY); + if (res == nullptr || res->ret_value == ESP_FAIL) { + continue;; + } + + // Store the wake word data for voice recognition, like who is speaking + StoreWakeWordData((uint16_t*)res->data, res->data_size / sizeof(uint16_t)); + + if (res->wakeup_state == WAKENET_DETECTED) { + Stop(); + last_detected_wake_word_ = wake_words_[res->wake_word_index - 1]; + + if (wake_word_detected_callback_) { + wake_word_detected_callback_(last_detected_wake_word_); + } + } + } +} + +void WakeWordDetect::StoreWakeWordData(uint16_t* data, size_t samples) { + // store audio data to wake_word_pcm_ + wake_word_pcm_.emplace_back(std::vector(data, data + samples)); + // keep about 2 seconds of data, detect duration is 32ms (sample_rate == 16000, chunksize == 512) + while (wake_word_pcm_.size() > 2000 / 32) { + wake_word_pcm_.pop_front(); + } +} + +void WakeWordDetect::EncodeWakeWordData() { + wake_word_opus_.clear(); + if (wake_word_encode_task_stack_ == nullptr) { + wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(4096 * 8, MALLOC_CAP_SPIRAM); + } + wake_word_encode_task_ = xTaskCreateStatic([](void* arg) { + auto this_ = (WakeWordDetect*)arg; + { + auto start_time = esp_timer_get_time(); + auto encoder = std::make_unique(16000, 1, OPUS_FRAME_DURATION_MS); + encoder->SetComplexity(0); // 0 is the fastest + + for (auto& pcm: this_->wake_word_pcm_) { + encoder->Encode(std::move(pcm), [this_](std::vector&& opus) { + std::lock_guard lock(this_->wake_word_mutex_); + this_->wake_word_opus_.emplace_back(std::move(opus)); + this_->wake_word_cv_.notify_all(); + }); + } + this_->wake_word_pcm_.clear(); + + auto end_time = esp_timer_get_time(); + ESP_LOGI(TAG, "Encode wake word opus %zu packets in %lld ms", + this_->wake_word_opus_.size(), (end_time - start_time) / 1000); + + std::lock_guard lock(this_->wake_word_mutex_); + this_->wake_word_opus_.push_back(std::vector()); + this_->wake_word_cv_.notify_all(); + } + vTaskDelete(NULL); + }, "encode_detect_packets", 4096 * 8, this, 2, wake_word_encode_task_stack_, &wake_word_encode_task_buffer_); +} + +bool WakeWordDetect::GetWakeWordOpus(std::vector& opus) { + std::unique_lock lock(wake_word_mutex_); + wake_word_cv_.wait(lock, [this]() { + return !wake_word_opus_.empty(); + }); + opus.swap(wake_word_opus_.front()); + wake_word_opus_.pop_front(); + return !opus.empty(); +} diff --git a/main/audio_processing/wake_word_detect.h b/main/audio_processing/wake_word_detect.h new file mode 100644 index 0000000..41ae288 --- /dev/null +++ b/main/audio_processing/wake_word_detect.h @@ -0,0 +1,64 @@ +#ifndef WAKE_WORD_DETECT_H +#define WAKE_WORD_DETECT_H + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "audio_codec.h" +#include "wake_word.h" + +class WakeWordDetect : public WakeWord { +public: + WakeWordDetect(); + ~WakeWordDetect(); + + bool Initialize(AudioCodec* codec) override; + void Feed(const std::vector& data) override; + void OnWakeWordDetected(std::function callback) override; + void Start() override; + void Stop() override; + bool IsRunning() override; + size_t GetFeedSize() override; + void EncodeWakeWordData() override; + bool GetWakeWordOpus(std::vector& opus) override; + const std::string& GetLastDetectedWakeWord() const override { return last_detected_wake_word_; } + + // 保持向后兼容的方法 + void StartDetection() { Start(); } + void StopDetection() { Stop(); } + bool IsDetectionRunning() { return IsRunning(); } + +private: + const esp_afe_sr_iface_t* afe_iface_ = nullptr; + esp_afe_sr_data_t* afe_data_ = nullptr; + char* wakenet_model_ = NULL; + std::vector wake_words_; + EventGroupHandle_t event_group_; + std::function wake_word_detected_callback_; + AudioCodec* codec_ = nullptr; + std::string last_detected_wake_word_; + + TaskHandle_t wake_word_encode_task_ = nullptr; + StaticTask_t wake_word_encode_task_buffer_; + StackType_t* wake_word_encode_task_stack_ = nullptr; + std::list> wake_word_pcm_; + std::list> wake_word_opus_; + std::mutex wake_word_mutex_; + std::condition_variable wake_word_cv_; + + void StoreWakeWordData(uint16_t* data, size_t size); + void AudioDetectionTask(); +}; + +#endif diff --git a/main/background_task.cc b/main/background_task.cc new file mode 100644 index 0000000..9886fc2 --- /dev/null +++ b/main/background_task.cc @@ -0,0 +1,65 @@ +#include "background_task.h" + +#include +#include + +#define TAG "BackgroundTask" + +BackgroundTask::BackgroundTask(uint32_t stack_size) { + xTaskCreate([](void* arg) { + BackgroundTask* task = (BackgroundTask*)arg; + task->BackgroundTaskLoop(); + }, "background_task", stack_size, this, 2, &background_task_handle_); +} + +BackgroundTask::~BackgroundTask() { + if (background_task_handle_ != nullptr) { + vTaskDelete(background_task_handle_); + } +} + +void BackgroundTask::Schedule(std::function callback) { + std::lock_guard lock(mutex_); + if (active_tasks_ >= 30) { + int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + // 注释说明:为避免非关键日志刷屏,内存阈值告警降级为 DEBUG 并默认关闭 + // if (free_sram < 10000) { + // ESP_LOGD(TAG, "active_tasks_ == %u, free_sram == %u", active_tasks_.load(), free_sram); + // } + } + // 记录活跃任务计数,作为队列饱和与完成条件的依据 + active_tasks_++; + main_tasks_.emplace_back([this, cb = std::move(callback)]() { + cb(); + { + std::lock_guard lock(mutex_); + active_tasks_--; + if (main_tasks_.empty() && active_tasks_ == 0) { + condition_variable_.notify_all(); + } + } + }); + condition_variable_.notify_all(); +} + +void BackgroundTask::WaitForCompletion() { + std::unique_lock lock(mutex_); + condition_variable_.wait(lock, [this]() { + return main_tasks_.empty() && active_tasks_ == 0; + }); +} + +void BackgroundTask::BackgroundTaskLoop() { + ESP_LOGI(TAG, "background_task started"); + while (true) { + std::unique_lock lock(mutex_); + condition_variable_.wait(lock, [this]() { return !main_tasks_.empty(); }); + + std::list> tasks = std::move(main_tasks_); + lock.unlock(); + + for (auto& task : tasks) { + task(); + } + } +} diff --git a/main/background_task.h b/main/background_task.h new file mode 100644 index 0000000..0e7ad3b --- /dev/null +++ b/main/background_task.h @@ -0,0 +1,29 @@ +#ifndef BACKGROUND_TASK_H +#define BACKGROUND_TASK_H + +#include +#include +#include +#include +#include +#include + +class BackgroundTask { +public: + BackgroundTask(uint32_t stack_size = 4096 * 2); + ~BackgroundTask(); + + void Schedule(std::function callback); + void WaitForCompletion(); + +private: + std::mutex mutex_; + std::list> main_tasks_; + std::condition_variable condition_variable_; + TaskHandle_t background_task_handle_ = nullptr; + std::atomic active_tasks_{0}; + + void BackgroundTaskLoop(); +}; + +#endif diff --git a/main/ble_service.cc b/main/ble_service.cc new file mode 100644 index 0000000..870d384 --- /dev/null +++ b/main/ble_service.cc @@ -0,0 +1,705 @@ +#include "ble_service.h" +#include "ble_service_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* TAG = BLE_JSON_TAG; + +BleJsonService* BleJsonService::instance_ = nullptr; + +// ============================================================ +// 命令队列消息结构 +// ============================================================ +struct BleJsonCmdMsg { + char* json_str; // 动态分配的 JSON 字符串,需要 free + uint16_t len; +}; + +// ============================================================ +// UUID 定义 +// ============================================================ +static esp_bt_uuid_t service_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = BLE_JSON_SERVICE_UUID}, +}; + +static esp_bt_uuid_t write_char_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = BLE_JSON_CHAR_WRITE_UUID}, +}; + +static esp_bt_uuid_t notify_char_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = BLE_JSON_CHAR_NOTIFY_UUID}, +}; + +static esp_bt_uuid_t status_char_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = BLE_JSON_CHAR_STATUS_UUID}, +}; + +// CCCD 描述符 UUID (标准 0x2902) +static esp_bt_uuid_t cccd_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG}, +}; + +// ============================================================ +// Characteristic 属性值缓冲区 (Auto Response 使用) +// ============================================================ +static uint8_t write_char_val[BLE_JSON_CHAR_VAL_MAX_LEN] = {0}; +static uint8_t notify_char_val[BLE_JSON_CHAR_VAL_MAX_LEN] = {0}; +static uint8_t status_char_val[BLE_JSON_CHAR_VAL_MAX_LEN] = {0}; +static uint8_t cccd_val[2] = {0x00, 0x00}; + +static esp_attr_value_t write_char_attr = { + .attr_max_len = BLE_JSON_CHAR_VAL_MAX_LEN, + .attr_len = 1, + .attr_value = write_char_val, +}; + +static esp_attr_value_t notify_char_attr = { + .attr_max_len = BLE_JSON_CHAR_VAL_MAX_LEN, + .attr_len = 1, + .attr_value = notify_char_val, +}; + +static esp_attr_value_t status_char_attr = { + .attr_max_len = BLE_JSON_CHAR_VAL_MAX_LEN, + .attr_len = 1, + .attr_value = status_char_val, +}; + +static esp_attr_value_t cccd_attr = { + .attr_max_len = 2, + .attr_len = 2, + .attr_value = cccd_val, +}; + +// ============================================================ +// 广播数据 +// ============================================================ +static esp_ble_adv_params_t ble_json_adv_params = { + .adv_int_min = BLE_JSON_ADV_INT_MIN, + .adv_int_max = BLE_JSON_ADV_INT_MAX, + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .peer_addr = {0}, + .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +// ============================================================ +// 构造 / 析构 +// ============================================================ + +BleJsonService::BleJsonService() { + instance_ = this; +} + +BleJsonService::~BleJsonService() { + Stop(); + if (instance_ == this) { + instance_ = nullptr; + } +} + +// ============================================================ +// Initialize — 注册 GATTS App +// ============================================================ + +bool BleJsonService::Initialize() { + if (initialized_) { + ESP_LOGW(TAG, "Already initialized"); + return true; + } + + esp_err_t ret; + + // 检查 Bluedroid 栈是否已启动 (可能由 BLE 配网模块启动过) + // 如果未启动,则自行初始化整个 BLE 栈 + esp_bluedroid_status_t bt_status = esp_bluedroid_get_status(); + if (bt_status != ESP_BLUEDROID_STATUS_ENABLED) { + ESP_LOGI(TAG, "Bluedroid not enabled, initializing BLE stack..."); + + // 释放经典蓝牙内存 + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + + // 初始化 BT Controller + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ret = esp_bt_controller_init(&bt_cfg); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "BT controller init failed: %s", esp_err_to_name(ret)); + return false; + } + + ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "BT controller enable failed: %s", esp_err_to_name(ret)); + return false; + } + + // 初始化 Bluedroid + ret = esp_bluedroid_init(); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "Bluedroid init failed: %s", esp_err_to_name(ret)); + return false; + } + + ret = esp_bluedroid_enable(); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "Bluedroid enable failed: %s", esp_err_to_name(ret)); + return false; + } + + ESP_LOGI(TAG, "BLE stack initialized successfully"); + } + + // 设置 MTU + ret = esp_ble_gatt_set_local_mtu(BLE_JSON_LOCAL_MTU); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Set MTU failed: %s (may already be set)", esp_err_to_name(ret)); + } + + // 注册 GAP 回调 (广播事件需要此回调才能启动) + // 注意: ESP-IDF 仅支持一个全局 GAP 回调,此处会覆盖配网模块的回调 + // 但 BLE 配网流程在 StartNetwork() 中已完成,不影响后续使用 + ret = esp_ble_gap_register_callback(GapEventHandler); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "GAP callback register: %s", esp_err_to_name(ret)); + } + + // 注册 GATTS 回调 (全局只能有一个,但 Bluedroid 支持按 gatts_if 分发) + // 配网模块已经注册了自己的回调,我们通过 app_register 获得独立的 gatts_if + ret = esp_ble_gatts_register_callback(GattsEventHandler); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "GATTS callback register: %s (may share with provisioning)", esp_err_to_name(ret)); + } + + // 注册独立的 GATTS App,获得自己的 gatts_if + ret = esp_ble_gatts_app_register(BLE_JSON_APP_ID); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "GATTS app register failed: %s", esp_err_to_name(ret)); + return false; + } + + // 创建命令处理队列和任务 + cmd_queue_ = xQueueCreate(BLE_JSON_CMD_QUEUE_SIZE, sizeof(BleJsonCmdMsg)); + if (!cmd_queue_) { + ESP_LOGE(TAG, "Failed to create cmd queue"); + return false; + } + + xTaskCreate(CmdProcessTask, "ble_json_cmd", 6144, this, 5, &cmd_task_handle_); + + initialized_ = true; + ESP_LOGI(TAG, "Initialized successfully (App ID=%d)", BLE_JSON_APP_ID); + return true; +} + +// ============================================================ +// Start — 启动广播 +// ============================================================ + +bool BleJsonService::Start(const char* device_name) { + if (!initialized_) { + ESP_LOGE(TAG, "Not initialized"); + return false; + } + + device_name_ = device_name ? device_name : BLE_JSON_DEVICE_NAME; + + // 设置设备名称 + esp_ble_gap_set_device_name(device_name_.c_str()); + + StartAdvertising(); + ESP_LOGI(TAG, "Started, device name: %s", device_name_.c_str()); + return true; +} + +// ============================================================ +// Stop +// ============================================================ + +void BleJsonService::Stop() { + if (!initialized_) return; + + esp_ble_gap_stop_advertising(); + + if (cmd_task_handle_) { + vTaskDelete(cmd_task_handle_); + cmd_task_handle_ = nullptr; + } + if (cmd_queue_) { + // 清空队列中残留的消息 + BleJsonCmdMsg msg; + while (xQueueReceive(cmd_queue_, &msg, 0) == pdTRUE) { + free(msg.json_str); + } + vQueueDelete(cmd_queue_); + cmd_queue_ = nullptr; + } + + connected_ = false; + notify_enabled_ = false; + initialized_ = false; + ESP_LOGI(TAG, "Stopped"); +} + +// ============================================================ +// SendResponse — 构建 JSON 响应并通过 NOTIFY 发送 +// ============================================================ + +bool BleJsonService::SendResponse(const std::string& cmd, int msg_id, int code, + const char* msg, cJSON* data) { + if (!connected_ || !notify_enabled_) { + ESP_LOGW(TAG, "Cannot send: connected=%d notify=%d", connected_, notify_enabled_); + return false; + } + + cJSON* root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "cmd", cmd.c_str()); + cJSON_AddNumberToObject(root, "id", msg_id); + cJSON_AddNumberToObject(root, "code", code); + if (msg) { + cJSON_AddStringToObject(root, "msg", msg); + } + if (data) { + cJSON_AddItemReferenceToObject(root, "data", data); + } + + char* json_str = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + + if (!json_str) { + ESP_LOGE(TAG, "JSON print failed"); + return false; + } + + uint16_t len = strlen(json_str); + ESP_LOGI(TAG, "TX(%d): %s", len, json_str); + + bool ok = SendNotify(json_str, len); + free(json_str); + return ok; +} + +// ============================================================ +// SendEvent — 构建主动推送事件 +// ============================================================ + +bool BleJsonService::SendEvent(const std::string& event_type, cJSON* data) { + if (!connected_ || !notify_enabled_) { + return false; + } + + cJSON* root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "cmd", "event"); + + cJSON* event_data = data ? cJSON_Duplicate(data, true) : cJSON_CreateObject(); + cJSON_AddStringToObject(event_data, "type", event_type.c_str()); + cJSON_AddItemToObject(root, "data", event_data); + + char* json_str = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + + if (!json_str) return false; + + uint16_t len = strlen(json_str); + ESP_LOGI(TAG, "TX event(%d): %s", len, json_str); + + bool ok = SendNotify(json_str, len); + free(json_str); + return ok; +} + +// ============================================================ +// SendNotify — 底层 NOTIFY 发送 +// ============================================================ + +bool BleJsonService::SendNotify(const char* json_str, uint16_t len) { + if (gatts_if_ == ESP_GATT_IF_NONE || !connected_) { + return false; + } + + // 检查是否超过 MTU + uint16_t max_payload = mtu_ - 3; + if (len > max_payload) { + ESP_LOGW(TAG, "Data len %d exceeds MTU payload %d, truncating", len, max_payload); + len = max_payload; + } + + esp_err_t ret = esp_ble_gatts_send_indicate( + gatts_if_, conn_id_, notify_char_handle_, + len, (uint8_t*)json_str, false // false = notification, true = indication + ); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Send notify failed: %s", esp_err_to_name(ret)); + return false; + } + return true; +} + +// ============================================================ +// StartAdvertising +// ============================================================ + +void BleJsonService::StartAdvertising() { + // 广播包:不放名称,避免超过31字节导致手机系统蓝牙搜索不到 + esp_ble_adv_data_t adv_data = {}; + adv_data.set_scan_rsp = false; + adv_data.include_name = false; // 名称放在 Scan Response 中 + adv_data.include_txpower = true; + adv_data.min_interval = 0x0006; + adv_data.max_interval = 0x0010; + adv_data.appearance = 0x00; + adv_data.manufacturer_len = 0; + adv_data.p_manufacturer_data = nullptr; + adv_data.service_data_len = 0; + adv_data.p_service_data = nullptr; + adv_data.service_uuid_len = sizeof(uint16_t); + adv_data.p_service_uuid = (uint8_t*)&service_uuid.uuid.uuid16; + adv_data.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT); + + esp_ble_gap_config_adv_data(&adv_data); + + // Scan Response:放完整设备名称,手机扫描时会主动请求 + esp_ble_adv_data_t scan_rsp = {}; + scan_rsp.set_scan_rsp = true; + scan_rsp.include_name = true; + scan_rsp.include_txpower = false; + esp_ble_gap_config_adv_data(&scan_rsp); +} + +// ============================================================ +// GAP 事件回调 +// ============================================================ + +void BleJsonService::GapEventHandler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t* param) { + switch (event) { + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: { + // 两个事件都到达后才启动广播 + static uint8_t adv_config_done = 0; + if (event == ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT) { + adv_config_done |= 0x01; + } else { + adv_config_done |= 0x02; + } + if (adv_config_done == 0x03) { + adv_config_done = 0; // 重置,供下次重新广播使用 + esp_ble_gap_start_advertising(&ble_json_adv_params); + } + break; + } + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(TAG, "Advertising start failed: %d", param->adv_start_cmpl.status); + } else { + ESP_LOGI(TAG, "Advertising started"); + } + break; + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + ESP_LOGI(TAG, "Advertising stopped"); + break; + default: + break; + } +} + +// ============================================================ +// GATTS 事件回调 (static 分发) +// ============================================================ + +void BleJsonService::GattsEventHandler(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param) { + // 只处理属于本 App 的事件 (通过 gatts_if 或 REG 事件匹配) + if (event == ESP_GATTS_REG_EVT) { + if (param->reg.app_id != BLE_JSON_APP_ID) { + return; // 不是我们的 App 注册事件 + } + } else { + // 非 REG 事件: 检查 gatts_if 是否属于我们 + if (!instance_ || gatts_if != instance_->gatts_if_) { + return; + } + } + + if (instance_) { + instance_->HandleGattsEvent(event, gatts_if, param); + } +} + +// ============================================================ +// HandleGattsEvent — 实例内处理 +// ============================================================ + +void BleJsonService::HandleGattsEvent(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param) { + switch (event) { + + // ---- App 注册完成,保存 gatts_if,创建 Service ---- + case ESP_GATTS_REG_EVT: + if (param->reg.status == ESP_GATT_OK) { + gatts_if_ = gatts_if; + ESP_LOGI(TAG, "GATTS app registered, gatts_if=%d", gatts_if); + CreateService(gatts_if); + } else { + ESP_LOGE(TAG, "GATTS app register failed, status=%d", param->reg.status); + } + break; + + // ---- Service 创建完成,开始添加 Characteristic ---- + case ESP_GATTS_CREATE_EVT: + if (param->create.status == ESP_GATT_OK) { + service_handle_ = param->create.service_handle; + ESP_LOGI(TAG, "Service created, handle=%d", service_handle_); + + chars_added_ = 0; + + // 1. 添加 WRITE Characteristic (App -> 设备) + esp_gatt_char_prop_t write_prop = ESP_GATT_CHAR_PROP_BIT_WRITE; + esp_ble_gatts_add_char(service_handle_, &write_char_uuid, + ESP_GATT_PERM_WRITE, + write_prop, &write_char_attr, nullptr); + } else { + ESP_LOGE(TAG, "Service create failed: %d", param->create.status); + } + break; + + // ---- Characteristic 添加完成 ---- + case ESP_GATTS_ADD_CHAR_EVT: + if (param->add_char.status != ESP_GATT_OK) { + ESP_LOGE(TAG, "Add char failed: uuid=0x%04x status=%d", + param->add_char.char_uuid.uuid.uuid16, param->add_char.status); + break; + } + + if (param->add_char.char_uuid.uuid.uuid16 == BLE_JSON_CHAR_WRITE_UUID) { + write_char_handle_ = param->add_char.attr_handle; + ESP_LOGI(TAG, "WRITE char added, handle=%d", write_char_handle_); + + // 2. 添加 NOTIFY Characteristic (设备 -> App) + esp_gatt_char_prop_t notify_prop = ESP_GATT_CHAR_PROP_BIT_NOTIFY | ESP_GATT_CHAR_PROP_BIT_READ; + esp_ble_gatts_add_char(service_handle_, ¬ify_char_uuid, + ESP_GATT_PERM_READ, + notify_prop, ¬ify_char_attr, nullptr); + + } else if (param->add_char.char_uuid.uuid.uuid16 == BLE_JSON_CHAR_NOTIFY_UUID) { + notify_char_handle_ = param->add_char.attr_handle; + ESP_LOGI(TAG, "NOTIFY char added, handle=%d", notify_char_handle_); + + // 为 NOTIFY char 添加 CCCD 描述符 + esp_ble_gatts_add_char_descr(service_handle_, &cccd_uuid, + ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, + &cccd_attr, nullptr); + + } else if (param->add_char.char_uuid.uuid.uuid16 == BLE_JSON_CHAR_STATUS_UUID) { + status_char_handle_ = param->add_char.attr_handle; + ESP_LOGI(TAG, "STATUS char added, handle=%d", status_char_handle_); + + // 所有 Characteristic 添加完毕,启动 Service + esp_ble_gatts_start_service(service_handle_); + } + break; + + // ---- CCCD 描述符添加完成 ---- + case ESP_GATTS_ADD_CHAR_DESCR_EVT: + if (param->add_char_descr.status == ESP_GATT_OK) { + notify_cccd_handle_ = param->add_char_descr.attr_handle; + ESP_LOGI(TAG, "CCCD added, handle=%d", notify_cccd_handle_); + + // 3. 添加 STATUS Characteristic (READ only) + esp_gatt_char_prop_t status_prop = ESP_GATT_CHAR_PROP_BIT_READ; + esp_ble_gatts_add_char(service_handle_, &status_char_uuid, + ESP_GATT_PERM_READ, + status_prop, &status_char_attr, nullptr); + } else { + ESP_LOGE(TAG, "Add CCCD failed: %d", param->add_char_descr.status); + } + break; + + // ---- Service 启动完成 ---- + case ESP_GATTS_START_EVT: + if (param->start.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "Service started"); + } + break; + + // ---- 客户端连接 ---- + case ESP_GATTS_CONNECT_EVT: { + conn_id_ = param->connect.conn_id; + connected_ = true; + notify_enabled_ = false; + mtu_ = 23; // 默认值,等待 MTU exchange + + ESP_LOGI(TAG, "Client connected, conn_id=%d, addr=%02x:%02x:%02x:%02x:%02x:%02x", + conn_id_, + param->connect.remote_bda[0], param->connect.remote_bda[1], + param->connect.remote_bda[2], param->connect.remote_bda[3], + param->connect.remote_bda[4], param->connect.remote_bda[5]); + + // 请求更新连接参数 + esp_ble_conn_update_params_t conn_params = {}; + memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t)); + conn_params.latency = 0; + conn_params.max_int = 0x20; // 40ms + conn_params.min_int = 0x10; // 20ms + conn_params.timeout = 400; // 4s + esp_ble_gap_update_conn_params(&conn_params); + + // 连接后停止广播 (BLE 4.2 单连接时自动停止,但显式调用更安全) + esp_ble_gap_stop_advertising(); + break; + } + + // ---- 客户端断开 ---- + case ESP_GATTS_DISCONNECT_EVT: + ESP_LOGI(TAG, "Client disconnected, reason=0x%x", param->disconnect.reason); + connected_ = false; + notify_enabled_ = false; + mtu_ = 23; + + // 重新启动广播 + StartAdvertising(); + break; + + // ---- MTU 协商完成 ---- + case ESP_GATTS_MTU_EVT: + mtu_ = param->mtu.mtu; + ESP_LOGI(TAG, "MTU updated: %d", mtu_); + break; + + // ---- WRITE 事件 ---- + case ESP_GATTS_WRITE_EVT: + if (param->write.handle == write_char_handle_) { + // JSON 命令数据 + ProcessWriteData(param->write.value, param->write.len); + } else if (param->write.handle == notify_cccd_handle_ && param->write.len == 2) { + // CCCD 写入: 开启/关闭 NOTIFY + uint16_t cccd_value = param->write.value[0] | (param->write.value[1] << 8); + notify_enabled_ = (cccd_value == 0x0001); + ESP_LOGI(TAG, "NOTIFY %s", notify_enabled_ ? "enabled" : "disabled"); + } + + // 如果需要响应 + if (param->write.need_rsp) { + esp_ble_gatts_send_response(gatts_if_, param->write.conn_id, + param->write.trans_id, ESP_GATT_OK, nullptr); + } + break; + + // ---- READ 事件 (STATUS char) ---- + case ESP_GATTS_READ_EVT: + // Auto response 模式下无需手动处理 + // 如果需要动态数据,可在此更新 status_char_val + ESP_LOGD(TAG, "Read event, handle=%d", param->read.handle); + break; + + default: + break; + } +} + +// ============================================================ +// CreateService +// ============================================================ + +void BleJsonService::CreateService(esp_gatt_if_t gatts_if) { + esp_gatt_srvc_id_t service_id = {}; + service_id.is_primary = true; + service_id.id.inst_id = 0; + service_id.id.uuid.len = ESP_UUID_LEN_16; + service_id.id.uuid.uuid.uuid16 = BLE_JSON_SERVICE_UUID; + + esp_ble_gatts_create_service(gatts_if, &service_id, BLE_JSON_HANDLE_NUM); +} + +// ============================================================ +// ProcessWriteData — 收到 WRITE 数据,放入队列 +// ============================================================ + +void BleJsonService::ProcessWriteData(const uint8_t* data, uint16_t len) { + if (!data || len == 0 || !cmd_queue_) return; + + // 拷贝数据到堆上 (确保 null 结尾) + char* json_str = (char*)malloc(len + 1); + if (!json_str) { + ESP_LOGE(TAG, "Malloc failed for cmd data"); + return; + } + memcpy(json_str, data, len); + json_str[len] = '\0'; + + ESP_LOGI(TAG, "RX(%d): %s", len, json_str); + + BleJsonCmdMsg msg = {json_str, len}; + if (xQueueSend(cmd_queue_, &msg, pdMS_TO_TICKS(100)) != pdTRUE) { + ESP_LOGW(TAG, "Cmd queue full, dropping"); + free(json_str); + } +} + +// ============================================================ +// CmdProcessTask — 从队列取出 JSON 并分发到回调 +// ============================================================ + +void BleJsonService::CmdProcessTask(void* param) { + BleJsonService* self = static_cast(param); + BleJsonCmdMsg msg; + + while (true) { + if (xQueueReceive(self->cmd_queue_, &msg, portMAX_DELAY) == pdTRUE) { + // 解析 JSON + cJSON* root = cJSON_Parse(msg.json_str); + if (!root) { + ESP_LOGW(TAG, "JSON parse failed: %s", msg.json_str); + // 发送错误响应 + if (self->connected_ && self->notify_enabled_) { + self->SendResponse("error", 0, 1, "invalid json"); + } + free(msg.json_str); + continue; + } + + // 提取 cmd 和 id + cJSON* cmd_item = cJSON_GetObjectItem(root, "cmd"); + cJSON* id_item = cJSON_GetObjectItem(root, "id"); + cJSON* data_item = cJSON_GetObjectItem(root, "data"); + + if (!cmd_item || !cJSON_IsString(cmd_item)) { + ESP_LOGW(TAG, "Missing 'cmd' field"); + self->SendResponse("error", 0, 1, "missing cmd"); + cJSON_Delete(root); + free(msg.json_str); + continue; + } + + std::string cmd = cmd_item->valuestring; + int msg_id = (id_item && cJSON_IsNumber(id_item)) ? id_item->valueint : 0; + + // 分发到用户回调 + if (self->command_callback_) { + self->command_callback_(cmd, msg_id, data_item); + } else { + ESP_LOGW(TAG, "No command callback, cmd=%s ignored", cmd.c_str()); + self->SendResponse(cmd, msg_id, 2, "no handler"); + } + + cJSON_Delete(root); + free(msg.json_str); + } + } +} diff --git a/main/ble_service.h b/main/ble_service.h new file mode 100644 index 0000000..17e4038 --- /dev/null +++ b/main/ble_service.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include + +#ifdef ESP_PLATFORM +#include +#include +#include +#include +#endif + +struct cJSON; + +class BleJsonService { +public: + // 收到 JSON 命令时的回调: cmd, msg_id, data(cJSON*) + using CommandCallback = std::function; + + BleJsonService(); + ~BleJsonService(); + + // 初始化 GATT 服务 (需在 Bluedroid 栈已启动后调用) + bool Initialize(); + + // 启动广播和服务 + bool Start(const char* device_name = nullptr); + + // 停止服务和广播 + void Stop(); + + // 发送 JSON 响应 (通过 NOTIFY) + bool SendResponse(const std::string& cmd, int msg_id, int code, + const char* msg = nullptr, cJSON* data = nullptr); + + // 发送主动事件推送 (通过 NOTIFY) + bool SendEvent(const std::string& event_type, cJSON* data = nullptr); + + // 注册命令处理回调 + void SetCommandCallback(CommandCallback callback) { command_callback_ = callback; } + + // 状态查询 + bool IsConnected() const { return connected_; } + bool IsNotifyEnabled() const { return notify_enabled_; } + uint16_t GetMtu() const { return mtu_; } + +private: + // 禁用拷贝 + BleJsonService(const BleJsonService&) = delete; + BleJsonService& operator=(const BleJsonService&) = delete; + +#ifdef ESP_PLATFORM + // GATTS 事件回调 (static -> instance) + static void GattsEventHandler(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param); + + // GAP 事件回调 + static void GapEventHandler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t* param); + + // 内部事件处理 + void HandleGattsEvent(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param); + + // 创建 Service 和 Characteristic + void CreateService(esp_gatt_if_t gatts_if); + + // 配置并启动广播 + void StartAdvertising(); + + // 处理 WRITE 收到的数据 + void ProcessWriteData(const uint8_t* data, uint16_t len); + + // 通过 NOTIFY 发送数据 + bool SendNotify(const char* json_str, uint16_t len); + + // 命令处理任务 (从队列中取出 JSON 命令并分发) + static void CmdProcessTask(void* param); + + // GATT handles + uint16_t service_handle_ = 0; + uint16_t write_char_handle_ = 0; + uint16_t notify_char_handle_ = 0; + uint16_t notify_cccd_handle_ = 0; + uint16_t status_char_handle_ = 0; + + // 接口信息 + esp_gatt_if_t gatts_if_ = ESP_GATT_IF_NONE; + uint16_t conn_id_ = 0; + + // 命令处理队列 + QueueHandle_t cmd_queue_ = nullptr; + TaskHandle_t cmd_task_handle_ = nullptr; +#endif + + // 状态 + bool initialized_ = false; + bool connected_ = false; + bool notify_enabled_ = false; + uint16_t mtu_ = 23; + + // 服务创建阶段计数 (跟踪 add_char 完成数) + int chars_added_ = 0; + + // 回调 + CommandCallback command_callback_; + + // 设备名 + std::string device_name_; + + // 单例指针供 static 回调使用 + static BleJsonService* instance_; +}; diff --git a/main/ble_service_config.h b/main/ble_service_config.h new file mode 100644 index 0000000..99b4b74 --- /dev/null +++ b/main/ble_service_config.h @@ -0,0 +1,49 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================================================ +// BLE JSON 通讯服务 - 配置参数 +// ============================================================ + +// GATT App ID (配网模块使用 0,本模块使用 1) +#define BLE_JSON_APP_ID 1 + +// Service UUID (16-bit) +#define BLE_JSON_SERVICE_UUID 0xAB00 + +// Characteristic UUIDs +#define BLE_JSON_CHAR_WRITE_UUID 0xAB01 // App -> 设备 (WRITE) +#define BLE_JSON_CHAR_NOTIFY_UUID 0xAB02 // 设备 -> App (NOTIFY) +#define BLE_JSON_CHAR_STATUS_UUID 0xAB03 // 设备状态 (READ) + +// Service handle 数量 (service + chars + descriptors,预留足够) +#define BLE_JSON_HANDLE_NUM 10 + +// MTU +#define BLE_JSON_LOCAL_MTU 512 + +// Characteristic 最大值长度 +#define BLE_JSON_CHAR_VAL_MAX_LEN 512 + +// 广播设备名称 +#define BLE_JSON_DEVICE_NAME "Airhub_Ble" + +// 广播参数 +#define BLE_JSON_ADV_INT_MIN 0x40 // 40ms +#define BLE_JSON_ADV_INT_MAX 0x80 // 80ms + +// 命令处理队列深度 +#define BLE_JSON_CMD_QUEUE_SIZE 8 + +// WiFi 列表最大返回条数 +#define BLE_JSON_WIFI_LIST_MAX 8 + +// 日志标签 +#define BLE_JSON_TAG "BleJsonSvc" + +#ifdef __cplusplus +} +#endif diff --git a/main/bluetooth_provisioning.cc b/main/bluetooth_provisioning.cc new file mode 100644 index 0000000..f46db1f --- /dev/null +++ b/main/bluetooth_provisioning.cc @@ -0,0 +1,1417 @@ +/** + * @file bluetooth_provisioning.cc + * @brief 蓝牙配网模块实现(基于自定义 GATT Server) + * + * 使用自定义 BLE GATT Server + 原始广播数据(raw advertising)实现配网。 + * 采用 dzbj 项目验证的 esp_ble_gap_config_adv_data_raw() 方式, + * 确保手机系统蓝牙可搜索到设备名称。 + * + * 保留完整的 WiFi 配网业务逻辑: + * - WiFi 凭据接收、验证和连接管理 + * - 配网状态机和事件回调 + * - WiFi 连接状态监控和报告 + * - MAC 地址发送 + * - 配网成功后自动保存和重启 + */ + +#include "bluetooth_provisioning.h" +#include "esp_log.h" +#include "esp_bt.h" +#include "esp_bt_main.h" +#include "esp_bt_device.h" +#include "esp_gap_ble_api.h" +#include "esp_gatts_api.h" +#include "esp_gatt_common_api.h" +#include "esp_wifi.h" +#include "esp_netif.h" +#include "freertos/event_groups.h" +#include "freertos/timers.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "application.h" +#include "assets/lang_config.h" +#include +#include "nvs_flash.h" +#include "nvs.h" +#include + +/// 日志标签 +#define TAG "BluetoothProvisioning" + +/// WiFi连接成功事件位 +#define WIFI_CONNECTED_BIT BIT0 +/// WiFi连接失败事件位 +#define WIFI_FAIL_BIT BIT1 + +/// 静态单例实例指针 +BluetoothProvisioning* BluetoothProvisioning::instance_ = nullptr; + +/// WiFi事件组句柄 +static EventGroupHandle_t s_wifi_event_group = nullptr; + +/// WiFi连接重试计数器 +static int s_retry_num = 0; +/// 最大重试次数 +static const int MAX_RETRY = 2; +/// WiFi连接超时时间(毫秒) +static const int WIFI_CONNECT_TIMEOUT_MS = 30000; +/// WiFi连接超时定时器句柄 +static TimerHandle_t wifi_connect_timer = nullptr; + +// ============================================================ +// GATT 静态数据定义 +// ============================================================ + +// UUID 定义 +static esp_bt_uuid_t prov_service_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = PROV_SERVICE_UUID}, +}; + +static esp_bt_uuid_t prov_write_char_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = PROV_CHAR_WRITE_UUID}, +}; + +static esp_bt_uuid_t prov_notify_char_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = PROV_CHAR_NOTIFY_UUID}, +}; + +static esp_bt_uuid_t cccd_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG}, +}; + +// 特征值缓冲区 +static uint8_t prov_write_val[PROV_LOCAL_MTU] = {0}; +static uint8_t prov_notify_val[PROV_LOCAL_MTU] = {0}; +static uint8_t cccd_val[2] = {0x00, 0x00}; + +static esp_attr_value_t prov_write_attr = { + .attr_max_len = PROV_LOCAL_MTU, + .attr_len = 1, + .attr_value = prov_write_val, +}; + +static esp_attr_value_t prov_notify_attr = { + .attr_max_len = PROV_LOCAL_MTU, + .attr_len = 1, + .attr_value = prov_notify_val, +}; + +static esp_attr_value_t cccd_attr = { + .attr_max_len = 2, + .attr_len = 2, + .attr_value = cccd_val, +}; + +// 广播参数 (参考 dzbj: 快速广播间隔,确保手机快速发现) +static esp_ble_adv_params_t prov_adv_params = { + .adv_int_min = 0x20, // 20ms (dzbj 相同) + .adv_int_max = 0x40, // 40ms + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .peer_addr = {0}, + .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +// 原始广播数据 (运行时动态构建) +static uint8_t prov_adv_raw_data[31]; +static uint8_t prov_adv_raw_len = 0; + +// 扫描响应数据 (携带 TX Power + BLE MAC 地址) +static uint8_t prov_scan_rsp_data[31]; +static uint8_t prov_scan_rsp_len = 0; + +// ============================================================ +// 构造 / 析构 +// ============================================================ + +BluetoothProvisioning::BluetoothProvisioning() + : state_(BluetoothProvisioningState::IDLE) + , callback_(nullptr) + , client_connected_(false) + , initialized_(false) + , delayed_disconnect_(false) + , wifi_connecting_(false) + , mac_address_sent_(false) { + + wifi_credentials_.ssid.clear(); + wifi_credentials_.password.clear(); + memset(wifi_credentials_.bssid, 0, sizeof(wifi_credentials_.bssid)); + wifi_credentials_.bssid_set = false; + + instance_ = this; + ESP_LOGI(TAG, "蓝牙配网对象创建完成"); +} + +BluetoothProvisioning::~BluetoothProvisioning() { + if (initialized_) { + Deinitialize(); + } + instance_ = nullptr; + ESP_LOGI(TAG, "蓝牙配网对象销毁完成"); +} + +// ============================================================ +// Initialize — 初始化 BLE 栈 + GATT 服务 + WiFi 事件 +// ============================================================ + +bool BluetoothProvisioning::Initialize() { + if (initialized_) { + ESP_LOGW(TAG, "蓝牙配网已经初始化"); + return true; + } + + SetState(BluetoothProvisioningState::INITIALIZING); + esp_err_t ret; + + // 步骤1: 初始化WiFi模块 + ESP_LOGI(TAG, "初始化WiFi..."); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ret = esp_wifi_init(&cfg); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi初始化失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + ret = esp_wifi_set_mode(WIFI_MODE_STA); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi模式设置失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + ret = esp_wifi_start(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi启动失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + ESP_LOGI(TAG, "WiFi初始化完成"); + + // 步骤2: 初始化蓝牙控制器 + ESP_LOGI(TAG, "初始化蓝牙控制器..."); + esp_bt_controller_status_t ctl_status = esp_bt_controller_get_status(); +#if CONFIG_IDF_TARGET_ESP32 + ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); +#else + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); +#endif + if (ctl_status == ESP_BT_CONTROLLER_STATUS_ENABLED) { + esp_bt_controller_disable(); + ctl_status = esp_bt_controller_get_status(); + } + if (ctl_status == ESP_BT_CONTROLLER_STATUS_INITED) { + esp_bt_controller_deinit(); + } + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ret = esp_bt_controller_init(&bt_cfg); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "蓝牙控制器初始化失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "蓝牙控制器启用失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + // 步骤3: 初始化 Bluedroid 协议栈 + ESP_LOGI(TAG, "初始化Bluedroid协议栈..."); + auto bd_status = esp_bluedroid_get_status(); + if (bd_status == ESP_BLUEDROID_STATUS_ENABLED) { + esp_bluedroid_disable(); + bd_status = esp_bluedroid_get_status(); + } + if (bd_status == ESP_BLUEDROID_STATUS_INITIALIZED) { + esp_bluedroid_deinit(); + } + ret = esp_bluedroid_init(); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "Bluedroid初始化失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + ret = esp_bluedroid_enable(); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "Bluedroid启用失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + // 步骤4: 注册 GAP 和 GATTS 回调 + ESP_LOGI(TAG, "注册 BLE GAP/GATTS 回调..."); + + ret = esp_ble_gap_register_callback(GapEventHandler); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "GAP回调注册失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + ret = esp_ble_gatts_register_callback(GattsEventHandler); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "GATTS回调注册失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + ret = esp_ble_gatts_app_register(PROV_APP_ID); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "GATTS App注册失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + ret = esp_ble_gatt_set_local_mtu(PROV_LOCAL_MTU); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "设置MTU失败: %s (可能已设置)", esp_err_to_name(ret)); + } + + // 步骤5: 创建WiFi事件同步机制 + if (s_wifi_event_group == nullptr) { + s_wifi_event_group = xEventGroupCreate(); + if (s_wifi_event_group == nullptr) { + ESP_LOGE(TAG, "WiFi事件组创建失败"); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + } + + // 步骤6: 注册WiFi和IP事件处理器 + ESP_LOGI(TAG, "注册WiFi事件处理器..."); + + ret = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WiFiEventHandler, this); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi事件处理器注册失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + ret = esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &IPEventHandler, this); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "IP事件处理器注册失败: %s", esp_err_to_name(ret)); + SetState(BluetoothProvisioningState::FAILED); + return false; + } + + initialized_ = true; + SetState(BluetoothProvisioningState::IDLE); + + ESP_LOGI(TAG, "蓝牙配网初始化完成 (GATT Server 模式)"); + ESP_LOGI(TAG, "蓝牙MAC地址: " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(esp_bt_dev_get_address())); + + return true; +} + +// ============================================================ +// Deinitialize +// ============================================================ + +bool BluetoothProvisioning::Deinitialize() { + if (!initialized_) { + ESP_LOGW(TAG, "蓝牙配网未初始化"); + return true; + } + + ESP_LOGI(TAG, "开始反初始化蓝牙配网..."); + + if (state_ != BluetoothProvisioningState::IDLE && + state_ != BluetoothProvisioningState::STOPPED) { + StopProvisioning(); + } + + esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &WiFiEventHandler); + esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &IPEventHandler); + + if (s_wifi_event_group != nullptr) { + vEventGroupDelete(s_wifi_event_group); + s_wifi_event_group = nullptr; + } + + esp_bluedroid_disable(); + esp_bluedroid_deinit(); + esp_bt_controller_disable(); + esp_bt_controller_deinit(); + + initialized_ = false; + SetState(BluetoothProvisioningState::STOPPED); + + ESP_LOGI(TAG, "蓝牙配网反初始化完成"); + return true; +} + +// ============================================================ +// StartProvisioning — 构建原始广播数据并启动广播 +// ============================================================ + +bool BluetoothProvisioning::StartProvisioning() { + ESP_LOGI(TAG, "🔵 开始启动蓝牙配网服务 (GATT Server)..."); + ESP_LOGI(TAG, "🔍 检查初始化状态: initialized_ = %s", initialized_ ? "true" : "false"); + + if (!initialized_) { + ESP_LOGE(TAG, "❌ 蓝牙配网未初始化,无法启动"); + return false; + } + + if (state_ == BluetoothProvisioningState::ADVERTISING || + state_ == BluetoothProvisioningState::CONNECTED || + state_ == BluetoothProvisioningState::PROVISIONING) { + ESP_LOGW(TAG, "⚠️ 蓝牙配网已在运行中"); + return true; + } + + // 重置状态 + client_connected_ = false; + s_retry_num = 0; + + ResetMacSendingState(); + ESP_LOGI(TAG, "🔄 MAC地址发送状态已重置"); + + // 清空之前的WiFi凭据 + ESP_LOGI(TAG, "🧹 清除之前的WiFi凭据..."); + if (!wifi_credentials_.ssid.empty()) { + ESP_LOGI(TAG, "🗑️ 删除已保存的SSID: %s", wifi_credentials_.ssid.c_str()); + } + if (!wifi_credentials_.password.empty()) { + ESP_LOGI(TAG, "🗑️ 删除已保存的WiFi密码 (长度: %d)", (int)wifi_credentials_.password.length()); + } + wifi_credentials_.ssid.clear(); + wifi_credentials_.password.clear(); + wifi_credentials_.bssid_set = false; + ESP_LOGI(TAG, "✅ WiFi凭据清除完成,准备接收新的配网信息"); + + // 构建设备名称: "Airhub_" + BLE MAC 明文 + char ble_device_name[32]; + const uint8_t* ble_addr = esp_bt_dev_get_address(); + if (ble_addr) { + snprintf(ble_device_name, sizeof(ble_device_name), + "Airhub_%02x:%02x:%02x:%02x:%02x:%02x", + ble_addr[0], ble_addr[1], ble_addr[2], + ble_addr[3], ble_addr[4], ble_addr[5]); + } else { + strcpy(ble_device_name, "Airhub_Ble"); + ESP_LOGW(TAG, "获取BLE MAC失败,使用默认名称: %s", ble_device_name); + } + + // 设置设备名称 + esp_err_t ret = esp_ble_gap_set_device_name(ble_device_name); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "❌ 设置蓝牙设备名称失败: %s", esp_err_to_name(ret)); + return false; + } + + ESP_LOGI(TAG, "📡 蓝牙设备名称: %s", ble_device_name); + + // 构建广播数据 (Flags + 设备名) + uint8_t name_len = strlen(ble_device_name); + int offset = 0; + + // Flags: LE General Discoverable + BR/EDR Not Supported + prov_adv_raw_data[offset++] = 0x02; + prov_adv_raw_data[offset++] = ESP_BLE_AD_TYPE_FLAG; + prov_adv_raw_data[offset++] = 0x06; + + // Complete Local Name + prov_adv_raw_data[offset++] = name_len + 1; + prov_adv_raw_data[offset++] = ESP_BLE_AD_TYPE_NAME_CMPL; + memcpy(&prov_adv_raw_data[offset], ble_device_name, name_len); + offset += name_len; + + prov_adv_raw_len = offset; + + ESP_LOGI(TAG, "📡 广播数据构建完成,长度: %d 字节", prov_adv_raw_len); + + // 构建扫描响应数据 (TX Power + Service UUID) + int rsp_offset = 0; + + // TX Power Level + prov_scan_rsp_data[rsp_offset++] = 0x02; + prov_scan_rsp_data[rsp_offset++] = ESP_BLE_AD_TYPE_TX_PWR; + prov_scan_rsp_data[rsp_offset++] = 0x09; + + // 16-bit Service UUID Complete + prov_scan_rsp_data[rsp_offset++] = 0x03; + prov_scan_rsp_data[rsp_offset++] = ESP_BLE_AD_TYPE_16SRV_CMPL; + prov_scan_rsp_data[rsp_offset++] = PROV_SERVICE_UUID & 0xFF; + prov_scan_rsp_data[rsp_offset++] = (PROV_SERVICE_UUID >> 8) & 0xFF; + + prov_scan_rsp_len = rsp_offset; + + ESP_LOGI(TAG, "📡 扫描响应数据构建完成,长度: %d 字节", prov_scan_rsp_len); + + // 配置广播数据 (GAP回调中会依次设置扫描响应数据并启动广播) + ret = esp_ble_gap_config_adv_data_raw(prov_adv_raw_data, prov_adv_raw_len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "❌ 配置广播数据失败: %s", esp_err_to_name(ret)); + return false; + } + + SetState(BluetoothProvisioningState::ADVERTISING); + ESP_LOGI(TAG, "蓝牙配网广播已启动,等待客户端连接..."); + + return true; +} + +// ============================================================ +// StopProvisioning +// ============================================================ + +bool BluetoothProvisioning::StopProvisioning() { + if (state_ == BluetoothProvisioningState::IDLE || + state_ == BluetoothProvisioningState::STOPPED) { + ESP_LOGW(TAG, "蓝牙配网未在运行"); + return true; + } + + ESP_LOGI(TAG, "停止蓝牙配网..."); + + esp_ble_gap_stop_advertising(); + + if (client_connected_ && gatts_if_ != ESP_GATT_IF_NONE) { + esp_ble_gatts_close(gatts_if_, conn_id_); + } + + client_connected_ = false; + notify_enabled_ = false; + + SetState(BluetoothProvisioningState::IDLE); + ESP_LOGI(TAG, "蓝牙配网已停止"); + + return true; +} + +// ============================================================ +// StartAdvertising — 启动 BLE 广播 +// ============================================================ + +void BluetoothProvisioning::StartAdvertising() { + // 重新配置原始广播数据,GAP回调中启动广播 + esp_ble_gap_config_adv_data_raw(prov_adv_raw_data, prov_adv_raw_len); +} + +// ============================================================ +// GAP 事件回调 (static) +// ============================================================ + +void BluetoothProvisioning::GapEventHandler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t* param) { + switch (event) { + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + // 广播数据设置完成,接着配置扫描响应数据 + ESP_LOGI(TAG, "📡 广播数据设置完成,配置扫描响应数据"); + if (prov_scan_rsp_len > 0) { + esp_ble_gap_config_scan_rsp_data_raw(prov_scan_rsp_data, prov_scan_rsp_len); + } else { + esp_ble_gap_start_advertising(&prov_adv_params); + } + break; + + case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: + // 扫描响应数据设置完成,启动广播 + ESP_LOGI(TAG, "📡 扫描响应数据设置完成,启动广播"); + esp_ble_gap_start_advertising(&prov_adv_params); + break; + + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(TAG, "❌ 广播启动失败: %d", param->adv_start_cmpl.status); + } else { + ESP_LOGI(TAG, "✅ 广播启动成功"); + } + break; + + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + ESP_LOGI(TAG, "广播已停止"); + break; + + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: + ESP_LOGI(TAG, "连接参数更新: status=%d, conn_int=%d, latency=%d, timeout=%d", + param->update_conn_params.status, + param->update_conn_params.conn_int, + param->update_conn_params.latency, + param->update_conn_params.timeout); + break; + + default: + break; + } +} + +// ============================================================ +// GATTS 事件回调 (static 分发) +// ============================================================ + +void BluetoothProvisioning::GattsEventHandler(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param) { + if (event == ESP_GATTS_REG_EVT) { + if (param->reg.app_id != PROV_APP_ID) { + return; + } + } else { + if (!instance_ || gatts_if != instance_->gatts_if_) { + return; + } + } + + if (instance_) { + instance_->HandleGattsEvent(event, gatts_if, param); + } +} + +// ============================================================ +// HandleGattsEvent — 实例内处理 +// ============================================================ + +void BluetoothProvisioning::HandleGattsEvent(esp_gatts_cb_event_t event, + esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t* param) { + switch (event) { + + // ---- App 注册完成,保存 gatts_if,创建 Service ---- + case ESP_GATTS_REG_EVT: + if (param->reg.status == ESP_GATT_OK) { + gatts_if_ = gatts_if; + ESP_LOGI(TAG, "✅ GATTS App 注册成功, gatts_if=%d", gatts_if); + CreateService(gatts_if); + } else { + ESP_LOGE(TAG, "❌ GATTS App 注册失败, status=%d", param->reg.status); + } + break; + + // ---- Service 创建完成,添加 WRITE Characteristic ---- + case ESP_GATTS_CREATE_EVT: + if (param->create.status == ESP_GATT_OK) { + service_handle_ = param->create.service_handle; + ESP_LOGI(TAG, "Service 创建成功, handle=%d", service_handle_); + + // 添加 WRITE Characteristic (手机 → 设备) + esp_gatt_char_prop_t write_prop = ESP_GATT_CHAR_PROP_BIT_WRITE; + esp_ble_gatts_add_char(service_handle_, &prov_write_char_uuid, + ESP_GATT_PERM_WRITE, + write_prop, &prov_write_attr, nullptr); + } else { + ESP_LOGE(TAG, "Service 创建失败: %d", param->create.status); + } + break; + + // ---- Characteristic 添加完成 ---- + case ESP_GATTS_ADD_CHAR_EVT: + if (param->add_char.status != ESP_GATT_OK) { + ESP_LOGE(TAG, "添加特征失败: uuid=0x%04x status=%d", + param->add_char.char_uuid.uuid.uuid16, param->add_char.status); + break; + } + + if (param->add_char.char_uuid.uuid.uuid16 == PROV_CHAR_WRITE_UUID) { + write_char_handle_ = param->add_char.attr_handle; + ESP_LOGI(TAG, "WRITE 特征添加成功, handle=%d", write_char_handle_); + + // 添加 NOTIFY Characteristic (设备 → 手机) + esp_gatt_char_prop_t notify_prop = ESP_GATT_CHAR_PROP_BIT_NOTIFY | ESP_GATT_CHAR_PROP_BIT_READ; + esp_ble_gatts_add_char(service_handle_, &prov_notify_char_uuid, + ESP_GATT_PERM_READ, + notify_prop, &prov_notify_attr, nullptr); + + } else if (param->add_char.char_uuid.uuid.uuid16 == PROV_CHAR_NOTIFY_UUID) { + notify_char_handle_ = param->add_char.attr_handle; + ESP_LOGI(TAG, "NOTIFY 特征添加成功, handle=%d", notify_char_handle_); + + // 为 NOTIFY 特征添加 CCCD 描述符 + esp_ble_gatts_add_char_descr(service_handle_, &cccd_uuid, + ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, + &cccd_attr, nullptr); + } + break; + + // ---- CCCD 描述符添加完成,启动服务 ---- + case ESP_GATTS_ADD_CHAR_DESCR_EVT: + if (param->add_char_descr.status == ESP_GATT_OK) { + notify_cccd_handle_ = param->add_char_descr.attr_handle; + ESP_LOGI(TAG, "CCCD 添加成功, handle=%d", notify_cccd_handle_); + + // 所有特征添加完毕,启动服务 + esp_ble_gatts_start_service(service_handle_); + } else { + ESP_LOGE(TAG, "CCCD 添加失败: %d", param->add_char_descr.status); + } + break; + + // ---- Service 启动完成 ---- + case ESP_GATTS_START_EVT: + if (param->start.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "✅ GATT Service 启动成功"); + } + break; + + // ---- 客户端连接 ---- + case ESP_GATTS_CONNECT_EVT: { + conn_id_ = param->connect.conn_id; + client_connected_ = true; + notify_enabled_ = false; + mtu_ = 23; + + ESP_LOGI(TAG, "📱 客户端已连接, conn_id=%d, addr=%02x:%02x:%02x:%02x:%02x:%02x", + conn_id_, + param->connect.remote_bda[0], param->connect.remote_bda[1], + param->connect.remote_bda[2], param->connect.remote_bda[3], + param->connect.remote_bda[4], param->connect.remote_bda[5]); + + ESP_LOGI(TAG, "🔍 [DEBUG] 设置client_connected_为true"); + + // 重置MAC地址发送状态 + ResetMacSendingState(); + ESP_LOGI(TAG, "🔄 MAC地址发送状态已重置"); + + // 更新连接参数 + esp_ble_conn_update_params_t conn_params = {}; + memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t)); + conn_params.latency = 0; + conn_params.max_int = 0x20; // 40ms + conn_params.min_int = 0x10; // 20ms + conn_params.timeout = 400; // 4s + esp_ble_gap_update_conn_params(&conn_params); + + // 停止广播 + esp_ble_gap_stop_advertising(); + + SetState(BluetoothProvisioningState::CONNECTED); + TriggerCallback(BluetoothProvisioningEvent::CLIENT_CONNECTED, nullptr); + + ESP_LOGI(TAG, "🔍 [DEBUG] BLE连接处理完成,client_connected_=%s", + client_connected_ ? "true" : "false"); + break; + } + + // ---- 客户端断开 ---- + case ESP_GATTS_DISCONNECT_EVT: { + ESP_LOGI(TAG, "📱 客户端已断开连接, reason=0x%x, 当前状态: %s", + param->disconnect.reason, GetStateString().c_str()); + ESP_LOGI(TAG, "🔍 [DEBUG] 设置client_connected_为false"); + client_connected_ = false; + notify_enabled_ = false; + mtu_ = 23; + + // 如果正在配网过程中,延迟处理断开事件 + if (state_ == BluetoothProvisioningState::PROVISIONING) { + ESP_LOGW(TAG, "⚠️ 配网过程中BLE断开,延迟5秒后处理以等待WiFi连接完成"); + delayed_disconnect_ = true; + xTaskCreate([](void* param) { + vTaskDelay(pdMS_TO_TICKS(2000)); + BluetoothProvisioning* self = static_cast(param); + if (self->delayed_disconnect_) { + if (self->state_ == BluetoothProvisioningState::PROVISIONING && self->wifi_connecting_) { + ESP_LOGW(TAG, "⏰ BLE延迟断开,但WiFi仍在连接中,继续等待"); + } else if (self->state_ == BluetoothProvisioningState::PROVISIONING) { + ESP_LOGW(TAG, "⏰ 延迟处理BLE断开,WiFi连接可能已超时"); + self->SetState(BluetoothProvisioningState::ADVERTISING); + self->TriggerCallback(BluetoothProvisioningEvent::CLIENT_DISCONNECTED, nullptr); + self->StartAdvertising(); + } + self->delayed_disconnect_ = false; + } + vTaskDelete(nullptr); + }, "delayed_disconnect", 2048, instance_, 1, nullptr); + } else { + SetState(BluetoothProvisioningState::ADVERTISING); + TriggerCallback(BluetoothProvisioningEvent::CLIENT_DISCONNECTED, nullptr); + // 重新启动广播 + StartAdvertising(); + } + break; + } + + // ---- MTU 协商完成 ---- + case ESP_GATTS_MTU_EVT: + mtu_ = param->mtu.mtu; + ESP_LOGI(TAG, "MTU 更新: %d", mtu_); + break; + + // ---- WRITE 事件 ---- + case ESP_GATTS_WRITE_EVT: + if (param->write.handle == write_char_handle_) { + // 配网协议数据 + ProcessWriteData(param->write.value, param->write.len); + } else if (param->write.handle == notify_cccd_handle_ && param->write.len == 2) { + // CCCD 写入: 开启/关闭 NOTIFY + uint16_t cccd_value = param->write.value[0] | (param->write.value[1] << 8); + notify_enabled_ = (cccd_value == 0x0001); + ESP_LOGI(TAG, "NOTIFY %s", notify_enabled_ ? "已启用" : "已禁用"); + } + + if (param->write.need_rsp) { + esp_ble_gatts_send_response(gatts_if_, param->write.conn_id, + param->write.trans_id, ESP_GATT_OK, nullptr); + } + break; + + // ---- READ 事件 ---- + case ESP_GATTS_READ_EVT: + ESP_LOGD(TAG, "Read event, handle=%d", param->read.handle); + break; + + default: + break; + } +} + +// ============================================================ +// CreateService +// ============================================================ + +void BluetoothProvisioning::CreateService(esp_gatt_if_t gatts_if) { + esp_gatt_srvc_id_t service_id = {}; + service_id.is_primary = true; + service_id.id.inst_id = 0; + service_id.id.uuid.len = ESP_UUID_LEN_16; + service_id.id.uuid.uuid.uuid16 = PROV_SERVICE_UUID; + + esp_ble_gatts_create_service(gatts_if, &service_id, PROV_HANDLE_NUM); +} + +// ============================================================ +// ProcessWriteData — 处理手机发送的配网协议数据 +// ============================================================ + +void BluetoothProvisioning::ProcessWriteData(const uint8_t* data, uint16_t len) { + if (!data || len < 1) return; + + uint8_t cmd = data[0]; + const uint8_t* payload = data + 1; + uint16_t payload_len = len - 1; + + switch (cmd) { + // 接收到WiFi SSID + case PROV_CMD_SET_SSID: + ESP_LOGI(TAG, "📶 收到WiFi SSID: %.*s", payload_len, payload); + wifi_credentials_.ssid.assign(reinterpret_cast(payload), payload_len); + { + wifi_config_t wifi_config = {}; + strncpy(reinterpret_cast(wifi_config.sta.ssid), + wifi_credentials_.ssid.c_str(), + sizeof(wifi_config.sta.ssid) - 1); + esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + } + break; + + // 接收到WiFi密码 + case PROV_CMD_SET_PASSWORD: + ESP_LOGI(TAG, "🔐 收到WiFi密码 (长度: %d)", payload_len); + wifi_credentials_.password.assign(reinterpret_cast(payload), payload_len); + { + wifi_config_t wifi_config = {}; + strncpy(reinterpret_cast(wifi_config.sta.ssid), + wifi_credentials_.ssid.c_str(), + sizeof(wifi_config.sta.ssid) - 1); + strncpy(reinterpret_cast(wifi_config.sta.password), + wifi_credentials_.password.c_str(), + sizeof(wifi_config.sta.password) - 1); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + } + + // 重置重试计数器 + s_retry_num = 0; + wifi_connecting_ = true; + + // 启动WiFi连接超时定时器 + if (wifi_connect_timer) { + xTimerStop(wifi_connect_timer, 0); + xTimerDelete(wifi_connect_timer, 0); + } + wifi_connect_timer = xTimerCreate("wifi_timeout", + pdMS_TO_TICKS(WIFI_CONNECT_TIMEOUT_MS), + pdFALSE, nullptr, + [](TimerHandle_t timer) { + ESP_LOGW(TAG, "⏰ WiFi连接超时,强制失败处理"); + if (instance_ && instance_->wifi_connecting_) { + instance_->wifi_connecting_ = false; + instance_->delayed_disconnect_ = false; + instance_->SetState(BluetoothProvisioningState::FAILED); + instance_->TriggerCallback(BluetoothProvisioningEvent::WIFI_FAILED, nullptr); + instance_->ReportWiFiStatus(false, WIFI_REASON_UNSPECIFIED); + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + }); + xTimerStart(wifi_connect_timer, 0); + + esp_wifi_connect(); + ESP_LOGI(TAG, "📡 已发起WiFi连接请求,启动超时监控"); + + TriggerCallback(BluetoothProvisioningEvent::WIFI_CREDENTIALS, &wifi_credentials_); + break; + + // 接收到BSSID + case PROV_CMD_SET_BSSID: + if (payload_len >= 6) { + ESP_LOGI(TAG, "收到BSSID"); + memcpy(wifi_credentials_.bssid, payload, 6); + wifi_credentials_.bssid_set = true; + + wifi_config_t wifi_config = {}; + strncpy(reinterpret_cast(wifi_config.sta.ssid), + wifi_credentials_.ssid.c_str(), + sizeof(wifi_config.sta.ssid) - 1); + strncpy(reinterpret_cast(wifi_config.sta.password), + wifi_credentials_.password.c_str(), + sizeof(wifi_config.sta.password) - 1); + memcpy(wifi_config.sta.bssid, wifi_credentials_.bssid, 6); + wifi_config.sta.bssid_set = true; + esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + } + break; + + // 请求连接到AP + case PROV_CMD_CONNECT_AP: + ESP_LOGI(TAG, "📡 请求连接到AP,SSID: %s", wifi_credentials_.ssid.c_str()); + ESP_LOGI(TAG, "🔍 [DEBUG] 当前状态: %s, client_connected_: %s", + GetStateString().c_str(), client_connected_ ? "true" : "false"); + SetState(BluetoothProvisioningState::PROVISIONING); + delayed_disconnect_ = false; + s_retry_num = 0; + ESP_LOGI(TAG, "🔄 重置WiFi重试计数,开始连接流程"); + esp_wifi_disconnect(); + vTaskDelay(pdMS_TO_TICKS(100)); + esp_wifi_connect(); + ESP_LOGI(TAG, "🚀 已发起WiFi连接请求"); + break; + + // 请求断开AP + case PROV_CMD_DISCONNECT_AP: + ESP_LOGI(TAG, "请求断开AP连接"); + esp_wifi_disconnect(); + break; + + // 请求WiFi列表 + case PROV_CMD_GET_WIFI_LIST: + ESP_LOGI(TAG, "📱 手机请求获取WiFi列表,开始扫描"); + { + wifi_scan_config_t scan_config = {}; + scan_config.ssid = nullptr; + scan_config.bssid = nullptr; + scan_config.channel = 0; + scan_config.show_hidden = true; + scan_config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + scan_config.scan_time.active.min = 100; + scan_config.scan_time.active.max = 300; + + esp_err_t ret = esp_wifi_scan_start(&scan_config, false); + if (ret == ESP_OK) { + ESP_LOGI(TAG, "🔍 WiFi扫描已启动"); + } else { + ESP_LOGE(TAG, "❌ WiFi扫描启动失败: %s", esp_err_to_name(ret)); + } + } + break; + + // 请求断开BLE + case PROV_CMD_DISCONNECT_BLE: + ESP_LOGI(TAG, "收到断开BLE连接请求"); + if (gatts_if_ != ESP_GATT_IF_NONE) { + esp_ble_gatts_close(gatts_if_, conn_id_); + } + break; + + // 设置WiFi模式 + case PROV_CMD_SET_WIFI_MODE: + if (payload_len >= 1) { + ESP_LOGI(TAG, "设置WiFi模式: %d", payload[0]); + esp_wifi_set_mode((wifi_mode_t)payload[0]); + } + break; + + // 获取WiFi状态 + case PROV_CMD_GET_WIFI_STATUS: + ESP_LOGI(TAG, "客户端请求WiFi状态"); + break; + + // 自定义数据 + case PROV_CMD_CUSTOM_DATA: + ESP_LOGI(TAG, "收到自定义数据, 长度: %d", payload_len); + break; + + default: + ESP_LOGW(TAG, "未知命令: 0x%02x", cmd); + break; + } +} + +// ============================================================ +// SendNotify — 通过 GATT NOTIFY 发送数据 +// ============================================================ + +bool BluetoothProvisioning::SendNotify(const uint8_t* data, uint16_t len) { + if (gatts_if_ == ESP_GATT_IF_NONE || !client_connected_) { + return false; + } + + uint16_t max_payload = mtu_ - 3; + if (len > max_payload) { + ESP_LOGW(TAG, "数据长度 %d 超过 MTU payload %d,截断", len, max_payload); + len = max_payload; + } + + esp_err_t ret = esp_ble_gatts_send_indicate( + gatts_if_, conn_id_, notify_char_handle_, + len, (uint8_t*)data, false); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "发送 NOTIFY 失败: %s", esp_err_to_name(ret)); + return false; + } + return true; +} + +// ============================================================ +// ReportWiFiStatus — 通过 GATT NOTIFY 报告WiFi连接状态 +// ============================================================ + +void BluetoothProvisioning::ReportWiFiStatus(bool success, uint8_t reason) { + ESP_LOGI(TAG, "🔍 [DEBUG] ReportWiFiStatus调用: success=%s, client_connected_=%s", + success ? "true" : "false", client_connected_ ? "true" : "false"); + if (!client_connected_) { + ESP_LOGW(TAG, "客户端未连接,无法发送WiFi状态"); + return; + } + + uint8_t buf[3]; + buf[0] = PROV_RESP_WIFI_STATUS; + buf[1] = success ? 1 : 0; + buf[2] = reason; + + if (notify_enabled_) { + SendNotify(buf, 3); + } + + if (success) { + ESP_LOGI(TAG, "向客户端报告设备连接WiFi成功!"); + } else { + ESP_LOGI(TAG, "向客户端报告连接WiFi失败,原因: %d", reason); + } +} + +// ============================================================ +// SendWiFiList — 通过 GATT NOTIFY 发送WiFi扫描列表 +// ============================================================ + +void BluetoothProvisioning::SendWiFiList(const wifi_ap_record_t* ap_list, uint16_t ap_count) { + if (!client_connected_) { + ESP_LOGW(TAG, "客户端未连接,无法发送WiFi列表"); + return; + } + + if (ap_list == nullptr || ap_count == 0) { + ESP_LOGW(TAG, "WiFi列表为空"); + // 发送结束标记 + if (notify_enabled_) { + uint8_t end = PROV_RESP_WIFI_LIST_END; + SendNotify(&end, 1); + } + return; + } + + ESP_LOGI(TAG, "向客户端发送WiFi列表,共%d个AP", ap_count); + + if (notify_enabled_) { + for (uint16_t i = 0; i < ap_count; i++) { + uint8_t ssid_len = strlen(reinterpret_cast(ap_list[i].ssid)); + uint8_t buf[3 + 32]; // cmd + rssi + ssid_len + ssid + buf[0] = PROV_RESP_WIFI_LIST; + buf[1] = (uint8_t)(ap_list[i].rssi & 0xFF); + buf[2] = ssid_len; + memcpy(&buf[3], ap_list[i].ssid, ssid_len); + + SendNotify(buf, 3 + ssid_len); + vTaskDelay(pdMS_TO_TICKS(20)); // 每条之间稍作延迟 + } + + // 发送结束标记 + uint8_t end = PROV_RESP_WIFI_LIST_END; + SendNotify(&end, 1); + } + + ESP_LOGI(TAG, "📤 WiFi列表已发送给客户端,包含 %d 个热点", ap_count); +} + +// ============================================================ +// SendMacAddressReliably — 通过 GATT NOTIFY 可靠发送MAC地址 +// ============================================================ + +bool BluetoothProvisioning::SendMacAddressReliably() { + if (!client_connected_) { + ESP_LOGW(TAG, "客户端未连接,无法发送MAC地址"); + return false; + } + + uint8_t mac[6]; + esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, mac); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "获取MAC地址失败: %s", esp_err_to_name(ret)); + return false; + } + + char mac_str[18]; + snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + if (mac_address_sent_) { + ESP_LOGI(TAG, "MAC地址已发送过,跳过重复发送: %s", mac_str); + return true; + } + + ESP_LOGI(TAG, "开始可靠发送MAC地址: %s", mac_str); + + const int MAX_SEND_ATTEMPTS = 3; + const int RETRY_DELAY_MS = 50; + + for (int attempt = 1; attempt <= MAX_SEND_ATTEMPTS; attempt++) { + if (!client_connected_) { + ESP_LOGW(TAG, "发送前检查发现客户端已断开连接 (尝试 %d/%d)", attempt, MAX_SEND_ATTEMPTS); + return false; + } + + ESP_LOGI(TAG, "发送MAC地址尝试 %d/%d: %s", attempt, MAX_SEND_ATTEMPTS, mac_str); + + // 构建自定义数据包 (保留原有格式) + char mac_data[32]; + snprintf(mac_data, sizeof(mac_data), "STA_MAC:%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + uint8_t buf[1 + 32]; + buf[0] = PROV_RESP_CUSTOM_DATA; + uint8_t data_len = strlen(mac_data); + memcpy(&buf[1], mac_data, data_len); + + bool ok = false; + if (notify_enabled_) { + ok = SendNotify(buf, 1 + data_len); + } else { + ESP_LOGW(TAG, "NOTIFY未启用,尝试直接发送"); + ok = SendNotify(buf, 1 + data_len); + } + + if (ok) { + ESP_LOGI(TAG, "✅ MAC地址发送成功 (尝试 %d/%d): %s", attempt, MAX_SEND_ATTEMPTS, mac_str); + mac_address_sent_ = true; + vTaskDelay(pdMS_TO_TICKS(100)); + + if (client_connected_) { + ESP_LOGI(TAG, "MAC地址发送完成,连接状态正常"); + return true; + } else { + ESP_LOGW(TAG, "MAC地址发送后检测到连接断开"); + return false; + } + } else { + ESP_LOGW(TAG, "❌ MAC地址发送失败 (尝试 %d/%d): %s", + attempt, MAX_SEND_ATTEMPTS, mac_str); + if (attempt < MAX_SEND_ATTEMPTS) { + vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS)); + } + } + } + + ESP_LOGE(TAG, "MAC地址发送失败,已达到最大重试次数: %s", mac_str); + return false; +} + +void BluetoothProvisioning::ResetMacSendingState() { + mac_address_sent_ = false; + ESP_LOGI(TAG, "MAC地址发送状态已重置"); +} + +// ============================================================ +// 状态管理 +// ============================================================ + +void BluetoothProvisioning::SetState(BluetoothProvisioningState new_state) { + if (state_ != new_state) { + BluetoothProvisioningState old_state = state_; + state_ = new_state; + + const char* state_names[] = { + "IDLE", "INITIALIZING", "ADVERTISING", "CONNECTED", + "PROVISIONING", "SUCCESS", "FAILED", "STOPPED" + }; + + ESP_LOGI(TAG, "🔄 配网状态变化: %s -> %s", + state_names[static_cast(old_state)], + state_names[static_cast(new_state)]); + + TriggerCallback(BluetoothProvisioningEvent::STATE_CHANGED, nullptr); + } +} + +void BluetoothProvisioning::TriggerCallback(BluetoothProvisioningEvent event, void* data) { + if (callback_) { + callback_(event, data); + } +} + +std::string BluetoothProvisioning::GetStateString() const { + const char* state_names[] = { + "IDLE", "INITIALIZING", "ADVERTISING", "CONNECTED", + "PROVISIONING", "SUCCESS", "FAILED", "STOPPED" + }; + return std::string(state_names[static_cast(state_)]); +} + +// ============================================================ +// WiFi 事件处理 (保留原有业务逻辑) +// ============================================================ + +void BluetoothProvisioning::WiFiEventHandler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) { + BluetoothProvisioning* self = static_cast(arg); + + if (event_base == WIFI_EVENT) { + switch (event_id) { + case WIFI_EVENT_STA_START: + ESP_LOGI(TAG, "WiFi STA启动"); + break; + + case WIFI_EVENT_STA_CONNECTED: { + wifi_event_sta_connected_t* event = static_cast(event_data); + ESP_LOGI(TAG, "✅ WiFi连接成功,SSID: %.*s,等待获取IP地址", event->ssid_len, event->ssid); + + if (wifi_connect_timer) { + xTimerStop(wifi_connect_timer, 0); + } + + self->wifi_connecting_ = false; + self->delayed_disconnect_ = false; + break; + } + + case WIFI_EVENT_STA_DISCONNECTED: { + wifi_event_sta_disconnected_t* event = static_cast(event_data); + ESP_LOGI(TAG, "WiFi断开连接,原因: %d", event->reason); + + if (self->state_ != BluetoothProvisioningState::PROVISIONING) { + ESP_LOGD(TAG, "非配网状态下的WiFi断开,忽略处理"); + break; + } + + if (s_retry_num < MAX_RETRY) { + esp_err_t ret = esp_wifi_connect(); + s_retry_num++; + ESP_LOGI(TAG, "🔄 立即重试连接WiFi (%d/%d),断开原因: %d,重试结果: %s", + s_retry_num, MAX_RETRY, event->reason, + ret == ESP_OK ? "成功" : "失败"); + + if (wifi_connect_timer && ret == ESP_OK) { + xTimerReset(wifi_connect_timer, 0); + } + } else { + ESP_LOGE(TAG, "❌ WiFi连接失败,已达到最大重试次数,断开原因: %d", event->reason); + + if (wifi_connect_timer) { + xTimerStop(wifi_connect_timer, 0); + } + + self->wifi_connecting_ = false; + self->delayed_disconnect_ = false; + + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + self->SetState(BluetoothProvisioningState::FAILED); + self->TriggerCallback(BluetoothProvisioningEvent::WIFI_FAILED, &event->reason); + self->ReportWiFiStatus(false, event->reason); + } + break; + } + + case WIFI_EVENT_SCAN_DONE: { + ESP_LOGI(TAG, "📡 WiFi扫描完成,准备发送WiFi列表"); + + uint16_t ap_count = 0; + esp_err_t ret = esp_wifi_scan_get_ap_num(&ap_count); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "❌ 获取WiFi扫描结果数量失败: %s", esp_err_to_name(ret)); + break; + } + + ESP_LOGI(TAG, "📊 扫描到 %d 个WiFi热点", ap_count); + + if (ap_count > 0) { + wifi_ap_record_t* ap_list = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * ap_count); + if (ap_list == nullptr) { + ESP_LOGE(TAG, "❌ 分配WiFi扫描结果内存失败"); + break; + } + + ret = esp_wifi_scan_get_ap_records(&ap_count, ap_list); + if (ret == ESP_OK) { + ESP_LOGI(TAG, "✅ 成功获取WiFi扫描结果"); + + // 过滤: 只保留2.4GHz频段(信道1-14),去重相同SSID(保留信号最强的) + // ESP-IDF返回结果已按RSSI降序排列,第一个出现的同名SSID即为信号最强 + uint16_t filtered_count = 0; + for (uint16_t i = 0; i < ap_count; i++) { + // 过滤5GHz频段 (信道 > 14) + if (ap_list[i].primary > 14) { + ESP_LOGD(TAG, "过滤5GHz: SSID=%s, 信道=%d, RSSI=%d", + ap_list[i].ssid, ap_list[i].primary, ap_list[i].rssi); + continue; + } + + // 过滤空SSID(隐藏网络) + uint8_t ssid_len = strlen(reinterpret_cast(ap_list[i].ssid)); + if (ssid_len == 0) { + continue; + } + + // SSID去重: 检查是否已存在相同SSID + bool duplicate = false; + for (uint16_t j = 0; j < filtered_count; j++) { + if (strcmp(reinterpret_cast(ap_list[j].ssid), + reinterpret_cast(ap_list[i].ssid)) == 0) { + duplicate = true; + break; + } + } + if (duplicate) { + continue; + } + + // 将符合条件的AP移到前面 + if (filtered_count != i) { + ap_list[filtered_count] = ap_list[i]; + } + filtered_count++; + } + + ESP_LOGI(TAG, "📊 过滤后剩余 %d 个2.4GHz热点 (原始: %d)", filtered_count, ap_count); + + self->SendWiFiList(ap_list, filtered_count); + ESP_LOGI(TAG, "📤 WiFi列表已发送,包含 %d 个热点", filtered_count); + } else { + ESP_LOGE(TAG, "❌ 获取WiFi扫描结果详细信息失败: %s", esp_err_to_name(ret)); + } + + free(ap_list); + } else { + ESP_LOGW(TAG, "⚠️ 未扫描到任何WiFi热点"); + self->SendWiFiList(nullptr, 0); + } + break; + } + + default: + break; + } + } +} + +// ============================================================ +// IP 事件处理 (保留原有业务逻辑: 保存WiFi、发送MAC、重启) +// ============================================================ + +void BluetoothProvisioning::IPEventHandler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) { + BluetoothProvisioning* self = static_cast(arg); + + switch (event_id) { + case IP_EVENT_STA_GOT_IP: { + ip_event_got_ip_t* event = static_cast(event_data); + ESP_LOGI(TAG, "✅ WiFi获取IP地址成功: " IPSTR, IP2STR(&event->ip_info.ip)); + + s_retry_num = 0; + + // 停止WiFi连接超时定时器 + if (wifi_connect_timer) { + xTimerStop(wifi_connect_timer, 0); + xTimerDelete(wifi_connect_timer, 0); + wifi_connect_timer = nullptr; + } + + self->wifi_connecting_ = false; + self->delayed_disconnect_ = false; + + if (s_wifi_event_group) { + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } + + // 发送MAC地址 + ESP_LOGI(TAG, "🔍 [DEBUG] 检查客户端连接状态: client_connected_=%s", + self->client_connected_ ? "true" : "false"); + if (self && self->client_connected_) { + ESP_LOGI(TAG, "🔍 [DEBUG] 使用专用函数发送设备MAC地址..."); + bool mac_sent = self->SendMacAddressReliably(); + if (mac_sent) { + ESP_LOGI(TAG, "✅ 设备MAC地址发送成功"); + } else { + ESP_LOGW(TAG, "⚠️ 设备MAC地址发送失败"); + } + + ESP_LOGI(TAG, "🔍 [DEBUG] 已跳过WiFi连接报告发送,仅发送设备MAC地址"); + } else { + ESP_LOGW(TAG, "🔍 [DEBUG] 无法发送: client_connected_=%s", + self->client_connected_ ? "true" : "false"); + } + + // 启用WiFi配置自动保存到NVS存储 + ESP_LOGI(TAG, "💾 启用WiFi配置自动保存到NVS存储..."); + esp_err_t storage_ret = esp_wifi_set_storage(WIFI_STORAGE_FLASH); + if (storage_ret == ESP_OK) { + ESP_LOGI(TAG, "✅ WiFi配置将自动保存到NVS存储"); + } else { + ESP_LOGW(TAG, "⚠️ 设置WiFi存储模式失败: %s", esp_err_to_name(storage_ret)); + } + + // 手动获取当前WiFi配置并保存到NVS列表 + wifi_config_t wifi_config; + esp_err_t get_config_ret = esp_wifi_get_config(WIFI_IF_STA, &wifi_config); + if (get_config_ret == ESP_OK) { + ESP_LOGI(TAG, "📋 获取当前WiFi配置成功,SSID: %s", wifi_config.sta.ssid); + auto& ssid_manager = SsidManager::GetInstance(); + ssid_manager.AddSsid((const char*)wifi_config.sta.ssid, (const char*)wifi_config.sta.password); + ESP_LOGI(TAG, "✅ WiFi凭据已保存到NVS列表"); + } else { + ESP_LOGW(TAG, "⚠️ 获取当前WiFi配置失败: %s", esp_err_to_name(get_config_ret)); + } + + auto& application = Application::GetInstance(); + bool skip_session = application.ShouldSkipDialogIdleSession(); + ESP_LOGI(TAG, "BluetoothProvisioning WIFI_CONNECTED skip_session=%d", (int)skip_session); + if (!skip_session) { + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + application.PlaySound(Lang::Sounds::P3_KAKA_LIANJIEWANGLUO); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + application.PlaySound(Lang::Sounds::P3_LALA_LIANJIEWANGLUO); + } + } else { + application.ClearDialogIdleSkipSession(); + } + + // 更新状态和触发回调 + ESP_LOGI(TAG, "🔍 准备设置状态为SUCCESS并触发回调"); + self->SetState(BluetoothProvisioningState::SUCCESS); + self->TriggerCallback(BluetoothProvisioningEvent::WIFI_CONNECTED, &event->ip_info.ip); + self->ReportWiFiStatus(true, 0); + + ESP_LOGI(TAG, "📋 配网流程完成,状态: %s, client_connected_: %s", + self->GetStateString().c_str(), + self->client_connected_ ? "true" : "false"); + + // 延迟2000ms后强制重启设备 + ESP_LOGI(TAG, "⏰ 延迟2000ms后重启设备以确保配置生效..."); + vTaskDelay(pdMS_TO_TICKS(2000)); + ESP_LOGI(TAG, "🔄 强制重启设备..."); + esp_restart(); + break; + } + + default: + break; + } +} diff --git a/main/bluetooth_provisioning.h b/main/bluetooth_provisioning.h new file mode 100644 index 0000000..41ad231 --- /dev/null +++ b/main/bluetooth_provisioning.h @@ -0,0 +1,172 @@ +#pragma once + +/** + * @file bluetooth_provisioning.h + * @brief 蓝牙配网模块头文件(基于自定义 GATT Server) + * + * 使用自定义 BLE GATT Server 实现配网通讯, + * 采用原始广播数据(raw advertising)确保手机系统蓝牙可发现。 + * 保留完整的 WiFi 配网业务逻辑、状态机和事件回调。 + */ + +#include +#include + +// 使用条件编译避免IDE环境中的头文件错误 +#ifdef ESP_PLATFORM +#include "esp_gap_ble_api.h" +#include "esp_gatts_api.h" +#include "esp_gatt_common_api.h" +#include "esp_wifi.h" +#include "esp_event.h" +#else +// 在非ESP环境中定义必要的类型和常量 +typedef int esp_event_base_t; +typedef void* wifi_ap_record_t; +#endif + +// ============================================================ +// 配网协议命令定义 (手机 → 设备, WRITE 特征) +// ============================================================ +#define PROV_CMD_SET_SSID 0x01 // 设置 WiFi SSID +#define PROV_CMD_SET_PASSWORD 0x02 // 设置 WiFi 密码 +#define PROV_CMD_SET_BSSID 0x03 // 设置 BSSID (6字节) +#define PROV_CMD_CONNECT_AP 0x04 // 请求连接 WiFi +#define PROV_CMD_DISCONNECT_AP 0x05 // 请求断开 WiFi +#define PROV_CMD_GET_WIFI_LIST 0x06 // 请求 WiFi 列表 +#define PROV_CMD_DISCONNECT_BLE 0x07 // 请求断开 BLE +#define PROV_CMD_SET_WIFI_MODE 0x08 // 设置 WiFi 模式 +#define PROV_CMD_GET_WIFI_STATUS 0x09 // 获取 WiFi 状态 +#define PROV_CMD_CUSTOM_DATA 0x10 // 自定义数据 + +// ============================================================ +// 配网协议响应定义 (设备 → 手机, NOTIFY 特征) +// ============================================================ +#define PROV_RESP_WIFI_STATUS 0x81 // WiFi 状态: [success(1)][reason(1)] +#define PROV_RESP_WIFI_LIST 0x82 // WiFi 列表: [rssi(1)][ssid_len(1)][ssid...] +#define PROV_RESP_WIFI_LIST_END 0x83 // WiFi 列表结束标记 +#define PROV_RESP_CUSTOM_DATA 0x84 // 自定义数据 + +// ============================================================ +// GATT 服务配置 +// ============================================================ +#define PROV_SERVICE_UUID 0xABF0 // 配网服务 UUID +#define PROV_CHAR_WRITE_UUID 0xABF1 // 写入特征 UUID (手机→设备) +#define PROV_CHAR_NOTIFY_UUID 0xABF2 // 通知特征 UUID (设备→手机) +#define PROV_APP_ID 2 // GATTS App ID +#define PROV_HANDLE_NUM 8 // Service handle 数量 +#define PROV_LOCAL_MTU 512 // 本地 MTU + +/** + * @brief 蓝牙配网状态枚举 + */ +enum class BluetoothProvisioningState { + IDLE, //< 空闲状态,未启动配网 + INITIALIZING, //< 初始化中,正在初始化蓝牙和服务 + ADVERTISING, //< 广播中,等待手机客户端连接 + CONNECTED, //< 已连接,手机客户端已连接到设备 + PROVISIONING, //< 配网中,正在接收和处理WiFi凭据 + SUCCESS, //< 配网成功,WiFi连接建立成功 + FAILED, //< 配网失败,WiFi连接失败或其他错误 + STOPPED //< 已停止,配网服务已停止 +}; + +/** + * @brief 蓝牙配网事件类型 + */ +enum class BluetoothProvisioningEvent { + STATE_CHANGED, //< 状态改变事件 + WIFI_CREDENTIALS, //< 收到WiFi凭据事件 + WIFI_CONNECTED, //< WiFi连接成功事件 + WIFI_FAILED, //< WiFi连接失败事件 + CLIENT_CONNECTED, //< 客户端连接事件 + CLIENT_DISCONNECTED //< 客户端断开事件 +}; + +/** + * @brief WiFi凭据结构体 + */ +struct WiFiCredentials { + std::string ssid; //< WiFi网络名称(SSID) + std::string password; //< WiFi网络密码 + uint8_t bssid[6]; //< WiFi接入点的MAC地址(BSSID) + bool bssid_set; //< 是否设置了BSSID +}; + +/** + * @brief 蓝牙配网事件回调函数类型 + */ +using BluetoothProvisioningCallback = std::function; + +/** + * @brief 蓝牙配网封装类(基于自定义 GATT Server) + * + * 使用自定义 BLE GATT Server + 原始广播数据, + * 手机系统蓝牙可直接搜索到设备。保留完整的 WiFi 配网业务逻辑。 + */ +class BluetoothProvisioning { +public: + BluetoothProvisioning(); + ~BluetoothProvisioning(); + + bool Initialize(); + bool Deinitialize(); + bool StartProvisioning(); + bool StopProvisioning(); + + BluetoothProvisioningState GetState() const { return state_; } + void SetCallback(BluetoothProvisioningCallback callback) { callback_ = callback; } + const WiFiCredentials& GetWiFiCredentials() const { return wifi_credentials_; } + bool IsClientConnected() const { return client_connected_; } + std::string GetStateString() const; + + void ReportWiFiStatus(bool success, uint8_t reason = 0); + void SendWiFiList(const wifi_ap_record_t* ap_list, uint16_t ap_count); + bool SendMacAddressReliably(); + void ResetMacSendingState(); + +private: + BluetoothProvisioningState state_; + BluetoothProvisioningCallback callback_; + WiFiCredentials wifi_credentials_; + bool client_connected_; + bool initialized_; + bool delayed_disconnect_; + bool wifi_connecting_; + bool mac_address_sent_; + + static BluetoothProvisioning* instance_; + + void SetState(BluetoothProvisioningState new_state); + void TriggerCallback(BluetoothProvisioningEvent event, void* data = nullptr); + +#ifdef ESP_PLATFORM + // GATT 相关成员 + esp_gatt_if_t gatts_if_ = ESP_GATT_IF_NONE; + uint16_t service_handle_ = 0; + uint16_t write_char_handle_ = 0; + uint16_t notify_char_handle_ = 0; + uint16_t notify_cccd_handle_ = 0; + uint16_t conn_id_ = 0; + bool notify_enabled_ = false; + uint16_t mtu_ = 23; + + // BLE 回调 + static void GattsEventHandler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t* param); + static void GapEventHandler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param); + void HandleGattsEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t* param); + + // GATT 辅助方法 + void CreateService(esp_gatt_if_t gatts_if); + void StartAdvertising(); + bool SendNotify(const uint8_t* data, uint16_t len); + void ProcessWriteData(const uint8_t* data, uint16_t len); + + // WiFi 事件处理 + static void WiFiEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data); + static void IPEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data); +#endif + + BluetoothProvisioning(const BluetoothProvisioning&) = delete; + BluetoothProvisioning& operator=(const BluetoothProvisioning&) = delete; +}; diff --git a/main/bluetooth_provisioning_config.h b/main/bluetooth_provisioning_config.h new file mode 100644 index 0000000..941ac6e --- /dev/null +++ b/main/bluetooth_provisioning_config.h @@ -0,0 +1,209 @@ +/** + * @file bluetooth_provisioning_config.h + * @brief 蓝牙配网配置文件 + * + * 本文件定义了蓝牙配网功能的各种配置参数,包括设备名称、 + * 安全设置、超时时间等,可根据项目需求进行调整 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 蓝牙配网基本配置 + */ + +// 设备名称最大长度 +#define BT_PROVISIONING_MAX_DEVICE_NAME_LEN 32 + +// SSID最大长度 +#define BT_PROVISIONING_MAX_SSID_LEN 32 + +// 密码最大长度 +#define BT_PROVISIONING_MAX_PASSWORD_LEN 64 + +/** + * @brief 蓝牙配网超时配置 + */ + +// 广播超时时间(毫秒),0表示永不超时 +#define BT_PROVISIONING_ADV_TIMEOUT_MS 0 + +// 客户端连接超时时间(毫秒) +#define BT_PROVISIONING_CLIENT_TIMEOUT_MS (5 * 60 * 1000) // 5分钟 + +// WiFi连接超时时间(毫秒) +#define BT_PROVISIONING_WIFI_TIMEOUT_MS (100 * 1000) // 100秒,增加超时时间避免过快重新进入配网 + +// WiFi连接最大重试次数 +#define BT_PROVISIONING_WIFI_MAX_RETRY 2 + +/** + * @brief 蓝牙配网安全配置 + */ + +// 是否启用安全模式(加密通信) +#define BT_PROVISIONING_SECURITY_ENABLED 0 + +// 是否需要配对确认 +#define BT_PROVISIONING_REQUIRE_PAIRING 0 + +// 预共享密钥(PSK)- 用于加密通信 +// 注意:如果启用安全模式,客户端也需要使用相同的PSK +#define BT_PROVISIONING_PSK "Airhub2025" + +/** + * @brief 蓝牙配网功能开关 + */ + +// 是否启用WiFi扫描功能 +#define BT_PROVISIONING_ENABLE_WIFI_SCAN 1 + +// 是否自动发送WiFi状态报告 +#define BT_PROVISIONING_AUTO_REPORT_STATUS 1 + +// 是否在配网成功后自动停止蓝牙服务 +#define BT_PROVISIONING_AUTO_STOP_ON_SUCCESS 1 + +// 自动停止延迟时间(毫秒) +#define BT_PROVISIONING_AUTO_STOP_DELAY_MS 5000 + +// 是否在配网失败后自动重启配网服务 +#define BT_PROVISIONING_AUTO_RESTART_ON_FAIL 1 + +// 自动重启延迟时间(毫秒) +#define BT_PROVISIONING_AUTO_RESTART_DELAY_MS 10000 + +/** + * @brief 蓝牙配网日志配置 + */ + +// 日志标签 +#define BT_PROVISIONING_LOG_TAG "BluetoothProvisioning" + +// 是否启用详细日志 +#define BT_PROVISIONING_VERBOSE_LOG 1 + +// 是否记录WiFi密码(安全考虑,建议设为0) +#define BT_PROVISIONING_LOG_PASSWORD 0 + +/** + * @brief 蓝牙配网性能配置 + */ + +// 蓝牙配网任务栈大小(字节) +#define BT_PROVISIONING_TASK_STACK_SIZE 8192 + +// 蓝牙配网任务优先级 +#define BT_PROVISIONING_TASK_PRIORITY 5 + +// 蓝牙配网任务核心绑定(-1表示不绑定) +#define BT_PROVISIONING_TASK_CORE_ID -1 + +/** + * @brief 蓝牙广播参数配置 + */ + +// 广播间隔最小值(单位:0.625ms) +#define BT_PROVISIONING_ADV_INT_MIN 0x20 // 20ms + +// 广播间隔最大值(单位:0.625ms) +#define BT_PROVISIONING_ADV_INT_MAX 0x40 // 40ms + +// 广播类型 +#define BT_PROVISIONING_ADV_TYPE ESP_BLE_ADV_TYPE_IND + +// 广播通道映射 +#define BT_PROVISIONING_ADV_CHNL_MAP ESP_BLE_ADV_CHNL_ALL + +// 广播过滤策略 +#define BT_PROVISIONING_ADV_FILTER_POLICY ESP_BLE_ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY + +/** + * @brief 蓝牙连接参数配置 + */ + +// 连接间隔最小值(单位:1.25ms) +#define BT_PROVISIONING_CONN_INT_MIN 0x10 // 20ms + +// 连接间隔最大值(单位:1.25ms) +#define BT_PROVISIONING_CONN_INT_MAX 0x20 // 40ms + +// 从设备延迟 +#define BT_PROVISIONING_SLAVE_LATENCY 0 + +// 监督超时(单位:10ms) +#define BT_PROVISIONING_SUPERVISION_TIMEOUT 0x48 // 720ms + +/** + * @brief 蓝牙配网状态指示配置 + */ + +// 是否启用LED状态指示 +#define BT_PROVISIONING_ENABLE_LED_INDICATOR 1 + +// 是否启用蜂鸣器状态指示 +#define BT_PROVISIONING_ENABLE_BUZZER_INDICATOR 0 + +// 是否启用语音提示 +#define BT_PROVISIONING_ENABLE_VOICE_PROMPT 1 + +/** + * @brief 蓝牙配网数据存储配置 + */ + +// 是否保存WiFi凭据到NVS +#define BT_PROVISIONING_SAVE_CREDENTIALS 1 + +// NVS命名空间 +#define BT_PROVISIONING_NVS_NAMESPACE "bt_prov" + +// WiFi SSID存储键 +#define BT_PROVISIONING_NVS_SSID_KEY "wifi_ssid" + +// WiFi密码存储键 +#define BT_PROVISIONING_NVS_PASSWORD_KEY "wifi_pass" + +// WiFi BSSID存储键 +#define BT_PROVISIONING_NVS_BSSID_KEY "wifi_bssid" + +/** + * @brief 蓝牙配网兼容性配置 + */ + +// 是否兼容ESP-IDF官方配网APP +#define BT_PROVISIONING_COMPATIBLE_OFFICIAL_APP 1 + +// 是否支持自定义数据传输 +#define BT_PROVISIONING_SUPPORT_CUSTOM_DATA 1 + +// 自定义数据最大长度 +#define BT_PROVISIONING_MAX_CUSTOM_DATA_LEN 512 + +/** + * @brief 编译时配置检查 + */ + +// 检查必要的ESP-IDF组件是否启用 +#ifndef CONFIG_BT_ENABLED +#warning "蓝牙配网需要启用CONFIG_BT_ENABLED" +#endif + +#ifndef CONFIG_BLUEDROID_ENABLED +#warning "蓝牙配网需要启用CONFIG_BLUEDROID_ENABLED" +#endif + +#ifndef CONFIG_BT_BLUFI_ENABLE +#warning "蓝牙配网需要启用CONFIG_BT_BLUFI_ENABLE" +#endif + +#ifndef CONFIG_ESP32_WIFI_ENABLED +#warning "蓝牙配网需要启用WiFi功能" +#endif + +#ifdef __cplusplus +} +#endif diff --git a/main/bluetooth_provisioning_example.cc b/main/bluetooth_provisioning_example.cc new file mode 100644 index 0000000..d83c8d8 --- /dev/null +++ b/main/bluetooth_provisioning_example.cc @@ -0,0 +1,331 @@ +/** + * @file bluetooth_provisioning_example.cc + * @brief 蓝牙配网使用示例 + * + * 本文件展示了如何在ESP32项目中集成和使用蓝牙配网功能 + * 包括初始化、启动配网、处理回调事件等完整流程 + */ + +#include "bluetooth_provisioning.h" +#include "esp_log.h" +#include "esp_wifi.h" +#include "esp_netif.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "BluetoothProvisioningExample" + +// 全局蓝牙配网对象 +static BluetoothProvisioning* g_bt_provisioning = nullptr; + +/** + * @brief 蓝牙配网事件回调函数 + * + * 处理蓝牙配网过程中的各种事件,包括状态变化、WiFi连接等 + * + * @param event 事件类型 + * @param data 事件数据指针 + */ +void bluetooth_provisioning_callback(BluetoothProvisioningEvent event, void* data) { + switch (event) { + case BluetoothProvisioningEvent::STATE_CHANGED: { + BluetoothProvisioningState state = g_bt_provisioning->GetState(); + const char* state_names[] = { + "空闲", "初始化中", "广播中", "已连接", + "配网中", "成功", "失败", "已停止" + }; + ESP_LOGI(TAG, "配网状态变更: %s", state_names[static_cast(state)]); + break; + } + + case BluetoothProvisioningEvent::CLIENT_CONNECTED: + ESP_LOGI(TAG, "蓝牙客户端已连接,可以开始配网"); + break; + + case BluetoothProvisioningEvent::CLIENT_DISCONNECTED: + ESP_LOGI(TAG, "蓝牙客户端已断开连接"); + break; + + case BluetoothProvisioningEvent::WIFI_CREDENTIALS: { + WiFiCredentials* credentials = static_cast(data); + ESP_LOGI(TAG, "收到WiFi凭据:"); + ESP_LOGI(TAG, " SSID: %s", credentials->ssid.c_str()); + ESP_LOGI(TAG, " 密码长度: %d", credentials->password.length()); + if (credentials->bssid_set) { + ESP_LOGI(TAG, " BSSID: %02x:%02x:%02x:%02x:%02x:%02x", + credentials->bssid[0], credentials->bssid[1], credentials->bssid[2], + credentials->bssid[3], credentials->bssid[4], credentials->bssid[5]); + } + break; + } + + case BluetoothProvisioningEvent::WIFI_CONNECTED: { + esp_ip4_addr_t* ip = static_cast(data); + ESP_LOGI(TAG, "WiFi连接成功!IP地址: " IPSTR, IP2STR(ip)); + + // WiFi连接成功后,可以选择停止蓝牙配网以节省资源 + // 延迟5秒后停止配网,给客户端足够时间接收状态 + vTaskDelay(pdMS_TO_TICKS(5000)); + if (g_bt_provisioning) { + g_bt_provisioning->StopProvisioning(); + ESP_LOGI(TAG, "配网成功,已停止蓝牙配网服务"); + } + break; + } + + case BluetoothProvisioningEvent::WIFI_FAILED: { + uint8_t* reason = static_cast(data); + ESP_LOGE(TAG, "WiFi连接失败,错误代码: %d", *reason); + + // WiFi连接失败,可以选择重新开始配网或进行其他处理 + ESP_LOGI(TAG, "WiFi连接失败,配网服务继续运行等待重新配置"); + break; + } + + default: + ESP_LOGW(TAG, "未处理的配网事件: %d", static_cast(event)); + break; + } +} + +/** + * @brief 初始化WiFi + * + * 配置WiFi为STA模式,为蓝牙配网做准备 + */ +esp_err_t init_wifi() { + ESP_LOGI(TAG, "初始化WiFi..."); + + // 创建默认WiFi STA网络接口 + esp_netif_create_default_wifi_sta(); + + // 初始化WiFi配置 + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + esp_err_t ret = esp_wifi_init(&cfg); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi初始化失败: %s", esp_err_to_name(ret)); + return ret; + } + + // 设置WiFi模式为STA + ret = esp_wifi_set_mode(WIFI_MODE_STA); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi模式设置失败: %s", esp_err_to_name(ret)); + return ret; + } + + // 启动WiFi + ret = esp_wifi_start(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "WiFi启动失败: %s", esp_err_to_name(ret)); + return ret; + } + + ESP_LOGI(TAG, "WiFi初始化完成"); + return ESP_OK; +} + +/** + * @brief 蓝牙配网任务 + * + * 独立任务处理蓝牙配网的整个生命周期 + * + * @param pvParameters 任务参数(未使用) + */ +void bluetooth_provisioning_task(void* pvParameters) { + ESP_LOGI(TAG, "启动蓝牙配网任务"); + + // 1. 创建蓝牙配网对象 + g_bt_provisioning = new BluetoothProvisioning(); + if (g_bt_provisioning == nullptr) { + ESP_LOGE(TAG, "蓝牙配网对象创建失败"); + vTaskDelete(nullptr); + return; + } + + // 2. 设置事件回调 + g_bt_provisioning->SetCallback(bluetooth_provisioning_callback); + + // 3. 初始化蓝牙配网 + if (!g_bt_provisioning->Initialize()) { + ESP_LOGE(TAG, "蓝牙配网初始化失败"); + delete g_bt_provisioning; + g_bt_provisioning = nullptr; + vTaskDelete(nullptr); + return; + } + + // 4. 启动配网服务 (设备名自动构建: Airhub_ + BLE MAC) + if (!g_bt_provisioning->StartProvisioning()) { + ESP_LOGE(TAG, "蓝牙配网启动失败"); + g_bt_provisioning->Deinitialize(); + delete g_bt_provisioning; + g_bt_provisioning = nullptr; + vTaskDelete(nullptr); + return; + } + + ESP_LOGI(TAG, "蓝牙配网服务已启动,设备名称: %s", device_name); + ESP_LOGI(TAG, "请使用ESP-IDF官方配网APP或自定义APP进行配网"); + + // 5. 任务主循环 - 监控配网状态 + while (true) { + BluetoothProvisioningState state = g_bt_provisioning->GetState(); + + // 检查是否需要处理特殊状态 + switch (state) { + case BluetoothProvisioningState::SUCCESS: + ESP_LOGI(TAG, "配网成功完成"); + // 可以在这里添加配网成功后的处理逻辑 + break; + + case BluetoothProvisioningState::FAILED: + ESP_LOGE(TAG, "配网失败,尝试重新启动"); + // 配网失败,尝试重新启动 + g_bt_provisioning->StopProvisioning(); + vTaskDelay(pdMS_TO_TICKS(2000)); + g_bt_provisioning->StartProvisioning(device_name); + break; + + case BluetoothProvisioningState::STOPPED: + ESP_LOGI(TAG, "配网服务已停止"); + // 配网服务已停止,可以选择退出任务或重新启动 + break; + + default: + // 其他状态正常运行 + break; + } + + // 每5秒检查一次状态 + vTaskDelay(pdMS_TO_TICKS(5000)); + } + + // 清理资源(通常不会执行到这里) + if (g_bt_provisioning) { + g_bt_provisioning->Deinitialize(); + delete g_bt_provisioning; + g_bt_provisioning = nullptr; + } + + vTaskDelete(nullptr); +} + +/** + * @brief 应用程序主函数 + * + * 演示如何集成蓝牙配网到现有的ESP32应用程序中 + */ +extern "C" void app_main() { + ESP_LOGI(TAG, "=== 蓝牙配网示例程序启动 ==="); + + // 1. 初始化NVS(用于存储WiFi配置) + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 2. 初始化网络接口 + ESP_ERROR_CHECK(esp_netif_init()); + + // 3. 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 4. 初始化WiFi + ESP_ERROR_CHECK(init_wifi()); + + // 5. 创建蓝牙配网任务 + BaseType_t task_ret = xTaskCreate( + bluetooth_provisioning_task, // 任务函数 + "bt_provisioning", // 任务名称 + 8192, // 栈大小(字节) + nullptr, // 任务参数 + 5, // 任务优先级 + nullptr // 任务句柄 + ); + + if (task_ret != pdPASS) { + ESP_LOGE(TAG, "蓝牙配网任务创建失败"); + return; + } + + ESP_LOGI(TAG, "应用程序初始化完成"); + + // 6. 主程序循环(可以在这里添加其他应用逻辑) + while (true) { + // 这里可以添加应用程序的主要逻辑 + // 例如:传感器读取、数据处理、用户交互等 + + ESP_LOGI(TAG, "主程序运行中..."); + vTaskDelay(pdMS_TO_TICKS(30000)); // 每30秒打印一次状态 + } +} + +/** + * @brief 获取当前配网状态(供其他模块调用) + * + * @return BluetoothProvisioningState 当前配网状态 + */ +BluetoothProvisioningState get_provisioning_state() { + if (g_bt_provisioning) { + return g_bt_provisioning->GetState(); + } + return BluetoothProvisioningState::STOPPED; +} + +/** + * @brief 检查WiFi是否已连接(供其他模块调用) + * + * @return true WiFi已连接 + * @return false WiFi未连接 + */ +bool is_wifi_connected() { + BluetoothProvisioningState state = get_provisioning_state(); + return (state == BluetoothProvisioningState::SUCCESS); +} + +/** + * @brief 手动启动配网(供其他模块调用) + * + * 可以在用户按下配网按钮或其他触发条件时调用 + * + * @return true 启动成功 + * @return false 启动失败 + */ +bool start_provisioning_manually() { + if (g_bt_provisioning == nullptr) { + ESP_LOGE(TAG, "蓝牙配网对象未初始化"); + return false; + } + + BluetoothProvisioningState state = g_bt_provisioning->GetState(); + if (state == BluetoothProvisioningState::ADVERTISING || + state == BluetoothProvisioningState::CONNECTED || + state == BluetoothProvisioningState::PROVISIONING) { + ESP_LOGW(TAG, "配网已在运行中"); + return true; + } + + ESP_LOGI(TAG, "手动启动蓝牙配网"); + return g_bt_provisioning->StartProvisioning("小智AI-手动配网"); +} + +/** + * @brief 手动停止配网(供其他模块调用) + * + * @return true 停止成功 + * @return false 停止失败 + */ +bool stop_provisioning_manually() { + if (g_bt_provisioning == nullptr) { + ESP_LOGE(TAG, "蓝牙配网对象未初始化"); + return false; + } + + ESP_LOGI(TAG, "手动停止蓝牙配网"); + return g_bt_provisioning->StopProvisioning(); +} \ No newline at end of file diff --git a/main/bluetooth_provisioning_test.cc b/main/bluetooth_provisioning_test.cc new file mode 100644 index 0000000..e0d8562 --- /dev/null +++ b/main/bluetooth_provisioning_test.cc @@ -0,0 +1,499 @@ +/** + * @file bluetooth_provisioning_test.cc + * @brief 蓝牙配网功能测试文件 + * @author AI Assistant + * @date 2024-01-01 + */ + +#include "bluetooth_provisioning.h" +#include "bluetooth_provisioning_config.h" +#include +#include +#include +#include +#include + +// 为了避免IDE环境中的头文件错误,使用条件编译 +#ifdef ESP_PLATFORM +#include "esp_wifi.h" +#include "esp_netif.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "esp_log.h" +#else +// 在非ESP环境中定义必要的宏和类型 +#define ESP_LOGI(tag, format, ...) printf("[INFO][%s] " format "\n", tag, ##__VA_ARGS__) +#define ESP_LOGE(tag, format, ...) printf("[ERROR][%s] " format "\n", tag, ##__VA_ARGS__) +#define ESP_LOGW(tag, format, ...) printf("[WARN][%s] " format "\n", tag, ##__VA_ARGS__) +#define pdMS_TO_TICKS(ms) (ms) +typedef void* TaskHandle_t; +void vTaskDelay(int ticks) { std::this_thread::sleep_for(std::chrono::milliseconds(ticks)); } +void vTaskDelete(TaskHandle_t task) { (void)task; } +int xTaskCreate(void (*task_func)(void*), const char* name, int stack_size, void* params, int priority, TaskHandle_t* handle) { + (void)task_func; (void)name; (void)stack_size; (void)params; (void)priority; (void)handle; + return 1; // 模拟成功 +} +#endif + +#define TAG "BluetoothProvisioningTest" + +// 测试事件组 +static EventGroupHandle_t test_event_group = nullptr; +#define TEST_WIFI_CONNECTED_BIT BIT0 +#define TEST_WIFI_FAILED_BIT BIT1 +#define TEST_BT_CONNECTED_BIT BIT2 +#define TEST_TIMEOUT_BIT BIT3 + +// 测试结果统计 +static struct { + int total_tests; + int passed_tests; + int failed_tests; +} test_stats = {0, 0, 0}; + +/** + * @brief 测试事件回调函数 + */ +void test_provisioning_callback(BluetoothProvisioningEvent event, void* data) { + switch (event) { + case BluetoothProvisioningEvent::STATE_CHANGED: + ESP_LOGI(TAG, "[测试] 配网状态变更"); + break; + + case BluetoothProvisioningEvent::CLIENT_CONNECTED: + ESP_LOGI(TAG, "[测试] 蓝牙客户端已连接"); + xEventGroupSetBits(test_event_group, TEST_BT_CONNECTED_BIT); + break; + + case BluetoothProvisioningEvent::CLIENT_DISCONNECTED: + ESP_LOGI(TAG, "[测试] 蓝牙客户端已断开"); + xEventGroupClearBits(test_event_group, TEST_BT_CONNECTED_BIT); + break; + + case BluetoothProvisioningEvent::WIFI_CREDENTIALS: { + WiFiCredentials* credentials = static_cast(data); + ESP_LOGI(TAG, "[测试] 收到WiFi凭据: SSID=%s", credentials->ssid.c_str()); + break; + } + + case BluetoothProvisioningEvent::WIFI_CONNECTED: { + esp_ip4_addr_t* ip = static_cast(data); + ESP_LOGI(TAG, "[测试] WiFi连接成功: IP=" IPSTR, IP2STR(ip)); + xEventGroupSetBits(test_event_group, TEST_WIFI_CONNECTED_BIT); + break; + } + + case BluetoothProvisioningEvent::WIFI_FAILED: { + uint8_t* reason = static_cast(data); + ESP_LOGE(TAG, "[测试] WiFi连接失败: 原因=%d", *reason); + xEventGroupSetBits(test_event_group, TEST_WIFI_FAILED_BIT); + break; + } + + default: + ESP_LOGD(TAG, "[测试] 未处理的事件: %d", static_cast(event)); + break; + } +} + +/** + * @brief 测试辅助函数 - 记录测试结果 + */ +void record_test_result(const char* test_name, bool passed) { + test_stats.total_tests++; + if (passed) { + test_stats.passed_tests++; + ESP_LOGI(TAG, "✅ [测试通过] %s", test_name); + } else { + test_stats.failed_tests++; + ESP_LOGE(TAG, "❌ [测试失败] %s", test_name); + } +} + +/** + * @brief 测试1:基本初始化和反初始化 + */ +bool test_basic_initialization() { + ESP_LOGI(TAG, "开始测试:基本初始化和反初始化"); + + BluetoothProvisioning* prov = new BluetoothProvisioning(); + if (!prov) { + return false; + } + + // 测试初始化 + bool init_result = prov->Initialize(); + if (!init_result) { + delete prov; + return false; + } + + // 检查初始状态 + BluetoothProvisioningState state = prov->GetState(); + if (state != BluetoothProvisioningState::IDLE) { + prov->Deinitialize(); + delete prov; + return false; + } + + // 测试反初始化 + bool deinit_result = prov->Deinitialize(); + delete prov; + + return init_result && deinit_result; +} + +/** + * @brief 测试2:配网服务启动和停止 + */ +bool test_provisioning_start_stop() { + ESP_LOGI(TAG, "开始测试:配网服务启动和停止"); + + BluetoothProvisioning* prov = new BluetoothProvisioning(); + if (!prov || !prov->Initialize()) { + delete prov; + return false; + } + + // 测试启动配网 + bool start_result = prov->StartProvisioning("测试设备"); + if (!start_result) { + prov->Deinitialize(); + delete prov; + return false; + } + + // 检查状态 + BluetoothProvisioningState state = prov->GetState(); + if (state != BluetoothProvisioningState::ADVERTISING) { + prov->Deinitialize(); + delete prov; + return false; + } + + // 等待一段时间 + vTaskDelay(pdMS_TO_TICKS(2000)); + + // 测试停止配网 + bool stop_result = prov->StopProvisioning(); + + // 检查状态 + state = prov->GetState(); + bool state_ok = (state == BluetoothProvisioningState::IDLE); + + prov->Deinitialize(); + delete prov; + + return start_result && stop_result && state_ok; +} + +/** + * @brief 测试3:回调函数设置和触发 + */ +bool test_callback_functionality() { + ESP_LOGI(TAG, "开始测试:回调函数设置和触发"); + + BluetoothProvisioning* prov = new BluetoothProvisioning(); + if (!prov || !prov->Initialize()) { + delete prov; + return false; + } + + // 设置回调函数 + prov->SetCallback(test_provisioning_callback); + + // 启动配网(这会触发状态变更回调) + bool start_result = prov->StartProvisioning("回调测试设备"); + + // 等待回调触发 + vTaskDelay(pdMS_TO_TICKS(1000)); + + // 停止配网 + prov->StopProvisioning(); + prov->Deinitialize(); + delete prov; + + return start_result; +} + +/** + * @brief 测试4:状态管理 + */ +bool test_state_management() { + ESP_LOGI(TAG, "开始测试:状态管理"); + + BluetoothProvisioning* prov = new BluetoothProvisioning(); + if (!prov) { + return false; + } + + // 检查初始状态 + BluetoothProvisioningState state = prov->GetState(); + if (state != BluetoothProvisioningState::IDLE) { + delete prov; + return false; + } + + // 初始化后检查状态 + if (!prov->Initialize()) { + delete prov; + return false; + } + + state = prov->GetState(); + if (state != BluetoothProvisioningState::IDLE) { + prov->Deinitialize(); + delete prov; + return false; + } + + // 启动配网后检查状态 + prov->StartProvisioning("状态测试设备"); + state = prov->GetState(); + bool advertising_state_ok = (state == BluetoothProvisioningState::ADVERTISING); + + // 停止配网后检查状态 + prov->StopProvisioning(); + state = prov->GetState(); + bool idle_state_ok = (state == BluetoothProvisioningState::IDLE); + + prov->Deinitialize(); + delete prov; + + return advertising_state_ok && idle_state_ok; +} + +/** + * @brief 测试5:错误处理 + */ +bool test_error_handling() { + ESP_LOGI(TAG, "开始测试:错误处理"); + + BluetoothProvisioning* prov = new BluetoothProvisioning(); + if (!prov) { + return false; + } + + // 测试未初始化时启动配网 + bool should_fail = prov->StartProvisioning("错误测试设备"); + if (should_fail) { + // 这应该失败 + delete prov; + return false; + } + + // 正常初始化 + if (!prov->Initialize()) { + delete prov; + return false; + } + + // 测试重复初始化 + bool repeat_init = prov->Initialize(); + if (!repeat_init) { + // 重复初始化应该返回true(已经初始化) + prov->Deinitialize(); + delete prov; + return false; + } + + // 测试重复启动配网 + prov->StartProvisioning("错误测试设备1"); + bool repeat_start = prov->StartProvisioning("错误测试设备2"); + if (!repeat_start) { + // 重复启动应该返回true(已经在运行) + prov->Deinitialize(); + delete prov; + return false; + } + + prov->StopProvisioning(); + prov->Deinitialize(); + delete prov; + + return true; +} + +/** + * @brief 测试6:内存管理 + */ +bool test_memory_management() { + ESP_LOGI(TAG, "开始测试:内存管理"); + + size_t free_heap_before = esp_get_free_heap_size(); + ESP_LOGI(TAG, "测试前可用内存: %d 字节", free_heap_before); + + // 创建和销毁多个实例 + for (int i = 0; i < 3; i++) { + BluetoothProvisioning* prov = new BluetoothProvisioning(); + if (!prov) { + return false; + } + + if (prov->Initialize()) { + prov->StartProvisioning("内存测试设备"); + vTaskDelay(pdMS_TO_TICKS(500)); + prov->StopProvisioning(); + prov->Deinitialize(); + } + + delete prov; + vTaskDelay(pdMS_TO_TICKS(100)); + } + + size_t free_heap_after = esp_get_free_heap_size(); + ESP_LOGI(TAG, "测试后可用内存: %d 字节", free_heap_after); + + // 检查内存泄漏(允许一定的误差) + int memory_diff = free_heap_before - free_heap_after; + bool memory_ok = (memory_diff < 1024); // 允许1KB的误差 + + if (!memory_ok) { + ESP_LOGW(TAG, "可能存在内存泄漏: %d 字节", memory_diff); + } + + return memory_ok; +} + +/** + * @brief 运行所有测试 + */ +void run_all_tests() { + ESP_LOGI(TAG, "=== 开始蓝牙配网功能测试 ==="); + + // 创建测试事件组 + test_event_group = xEventGroupCreate(); + if (!test_event_group) { + ESP_LOGE(TAG, "测试事件组创建失败"); + return; + } + + // 运行各项测试 + record_test_result("基本初始化和反初始化", test_basic_initialization()); + record_test_result("配网服务启动和停止", test_provisioning_start_stop()); + record_test_result("回调函数设置和触发", test_callback_functionality()); + record_test_result("状态管理", test_state_management()); + record_test_result("错误处理", test_error_handling()); + record_test_result("内存管理", test_memory_management()); + + // 输出测试结果 + ESP_LOGI(TAG, "=== 测试结果统计 ==="); + ESP_LOGI(TAG, "总测试数: %d", test_stats.total_tests); + ESP_LOGI(TAG, "通过测试: %d", test_stats.passed_tests); + ESP_LOGI(TAG, "失败测试: %d", test_stats.failed_tests); + + if (test_stats.failed_tests == 0) { + ESP_LOGI(TAG, "🎉 所有测试通过!蓝牙配网功能正常"); + } else { + ESP_LOGE(TAG, "⚠️ 有 %d 个测试失败,请检查实现", test_stats.failed_tests); + } + + // 清理资源 + vEventGroupDelete(test_event_group); + test_event_group = nullptr; +} + +/** + * @brief 蓝牙配网测试任务 + */ +void bluetooth_provisioning_test_task(void* pvParameters) { + ESP_LOGI(TAG, "蓝牙配网测试任务启动"); + + // 等待系统稳定 + vTaskDelay(pdMS_TO_TICKS(2000)); + + // 运行测试 + run_all_tests(); + + ESP_LOGI(TAG, "蓝牙配网测试任务完成"); + vTaskDelete(nullptr); +} + +/** + * @brief 启动蓝牙配网测试 + * + * 在主程序中调用此函数来启动测试 + */ +void start_bluetooth_provisioning_test() { + BaseType_t ret = xTaskCreate( + bluetooth_provisioning_test_task, + "bt_prov_test", + 8192, + nullptr, + 3, // 较低优先级 + nullptr + ); + + if (ret != pdPASS) { + ESP_LOGE(TAG, "蓝牙配网测试任务创建失败"); + } else { + ESP_LOGI(TAG, "蓝牙配网测试任务已创建"); + } +} + +/** + * @brief 简单的配网功能演示 + * + * 演示如何使用蓝牙配网功能 + */ +void bluetooth_provisioning_demo() { + ESP_LOGI(TAG, "=== 蓝牙配网功能演示 ==="); + + BluetoothProvisioning* prov = new BluetoothProvisioning(); + if (!prov) { + ESP_LOGE(TAG, "配网对象创建失败"); + return; + } + + // 设置回调 + prov->SetCallback(test_provisioning_callback); + + // 初始化 + if (!prov->Initialize()) { + ESP_LOGE(TAG, "配网初始化失败"); + delete prov; + return; + } + + // 启动配网 + if (!prov->StartProvisioning("小智AI-演示")) { + ESP_LOGE(TAG, "配网启动失败"); + prov->Deinitialize(); + delete prov; + return; + } + + ESP_LOGI(TAG, "配网服务已启动,请使用配网APP连接设备"); + ESP_LOGI(TAG, "设备名称: 小智AI-演示"); + ESP_LOGI(TAG, "等待客户端连接..."); + + // 运行30秒演示 + for (int i = 0; i < 30; i++) { + BluetoothProvisioningState state = prov->GetState(); + const char* state_names[] = { + "空闲", "初始化中", "广播中", "已连接", + "配网中", "成功", "失败", "已停止" + }; + + ESP_LOGI(TAG, "当前状态: %s, 客户端连接: %s", + state_names[static_cast(state)], + prov->IsClientConnected() ? "是" : "否"); + + if (state == BluetoothProvisioningState::SUCCESS) { + ESP_LOGI(TAG, "配网成功!演示结束"); + break; + } + + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + // 清理资源 + prov->StopProvisioning(); + prov->Deinitialize(); + delete prov; + + ESP_LOGI(TAG, "演示结束"); +} \ No newline at end of file diff --git a/main/boards/README.md b/main/boards/README.md new file mode 100644 index 0000000..3f10780 --- /dev/null +++ b/main/boards/README.md @@ -0,0 +1,337 @@ +# 自定义开发板指南 + +本指南介绍如何为小智AI语音聊天机器人项目定制一个新的开发板初始化程序。小智AI支持50多种ESP32系列开发板,每个开发板的初始化代码都放在对应的目录下。 + +## 重要提示 + +> **警告**: 对于自定义开发板,当IO配置与原有开发板不同时,切勿直接覆盖原有开发板的配置编译固件。必须创建新的开发板类型,或者通过config.json文件中的builds配置不同的name和sdkconfig宏定义来区分。使用 `python scripts/release.py [开发板目录名字]` 来编译打包固件。 +> +> 如果直接覆盖原有配置,将来OTA升级时,您的自定义固件可能会被原有开发板的标准固件覆盖,导致您的设备无法正常工作。每个开发板有唯一的标识和对应的固件升级通道,保持开发板标识的唯一性非常重要。 + +## 目录结构 + +每个开发板的目录结构通常包含以下文件: + +- `xxx_board.cc` - 主要的板级初始化代码,实现了板子相关的初始化和功能 +- `config.h` - 板级配置文件,定义了硬件管脚映射和其他配置项 +- `config.json` - 编译配置,指定目标芯片和特殊的编译选项 +- `README.md` - 开发板相关的说明文档 + +## 定制开发板步骤 + +### 1. 创建新的开发板目录 + +首先在`boards/`目录下创建一个新的目录,例如`my-custom-board/`: + +```bash +mkdir main/boards/my-custom-board +``` + +### 2. 创建配置文件 + +#### config.h + +在`config.h`中定义所有的硬件配置,包括: + +- 音频采样率和I2S引脚配置 +- 音频编解码芯片地址和I2C引脚配置 +- 按钮和LED引脚配置 +- 显示屏参数和引脚配置 + +参考示例(来自lichuang-c3-dev): + +```c +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +// 音频配置 +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_13 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +// 按钮配置 +#define BOOT_BUTTON_GPIO GPIO_NUM_9 + +// 显示屏配置 +#define DISPLAY_SPI_SCK_PIN GPIO_NUM_3 +#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5 +#define DISPLAY_DC_PIN GPIO_NUM_6 +#define DISPLAY_SPI_CS_PIN GPIO_NUM_4 + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // _BOARD_CONFIG_H_ +``` + +#### config.json + +在`config.json`中定义编译配置: + +```json +{ + "target": "esp32s3", // 目标芯片型号: esp32, esp32s3, esp32c3等 + "builds": [ + { + "name": "my-custom-board", // 开发板名称 + "sdkconfig_append": [ + // 额外需要的编译配置 + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_8M.csv\"" + ] + } + ] +} +``` + +### 3. 编写板级初始化代码 + +创建一个`my_custom_board.cc`文件,实现开发板的所有初始化逻辑。 + +一个基本的开发板类定义包含以下几个部分: + +1. **类定义**:继承自`WifiBoard`或`ML307Board` +2. **初始化函数**:包括I2C、显示屏、按钮、IoT等组件的初始化 +3. **虚函数重写**:如`GetAudioCodec()`、`GetDisplay()`、`GetBacklight()`等 +4. **注册开发板**:使用`DECLARE_BOARD`宏注册开发板 + +```cpp +#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 "iot/thing_manager.h" + +#include +#include +#include + +#define TAG "MyCustomBoard" + +// 声明字体 +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class MyCustomBoard : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + + // I2C初始化 + void InitializeI2c() { + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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_)); + } + + // SPI初始化(用于显示屏) + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN; + 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(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + // 按钮初始化 + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 显示屏初始化(以ST7789为例) + void InitializeDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + 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 = 2; + 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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); + + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_NC; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, true); + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + // IoT设备初始化 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + // 可以添加更多IoT设备 + } + +public: + // 构造函数 + MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeDisplay(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->SetBrightness(100); + } + + // 获取音频编解码器 + 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; + } +}; + +// 注册开发板 +DECLARE_BOARD(MyCustomBoard); +``` + +### 4. 创建README.md + +在README.md中说明开发板的特性、硬件要求、编译和烧录步骤: + + +## 常见开发板组件 + +### 1. 显示屏 + +项目支持多种显示屏驱动,包括: +- ST7789 (SPI) +- ILI9341 (SPI) +- SH8601 (QSPI) +- 等... + +### 2. 音频编解码器 + +支持的编解码器包括: +- ES8311 (常用) +- ES7210 (麦克风阵列) +- AW88298 (功放) +- 等... + +### 3. 电源管理 + +一些开发板使用电源管理芯片: +- AXP2101 +- 其他可用的PMIC + +### 4. IoT设备 + +可以添加各种IoT设备,让AI能够"看到"和控制: +- Speaker (扬声器) +- Screen (屏幕) +- Battery (电池) +- Light (灯光) +- 等... + +## 开发板类继承关系 + +- `Board` - 基础板级类 + - `WifiBoard` - WiFi连接的开发板 + - `ML307Board` - 使用4G模块的开发板 + +## 开发技巧 + +1. **参考相似的开发板**:如果您的新开发板与现有开发板有相似之处,可以参考现有实现 +2. **分步调试**:先实现基础功能(如显示),再添加更复杂的功能(如音频) +3. **管脚映射**:确保在config.h中正确配置所有管脚映射 +4. **检查硬件兼容性**:确认所有芯片和驱动程序的兼容性 + +## 可能遇到的问题 + +1. **显示屏不正常**:检查SPI配置、镜像设置和颜色反转设置 +2. **音频无输出**:检查I2S配置、PA使能引脚和编解码器地址 +3. **无法连接网络**:检查WiFi凭据和网络配置 +4. **无法与服务器通信**:检查MQTT或WebSocket配置 + +## 参考资料 + +- ESP-IDF 文档: https://docs.espressif.com/projects/esp-idf/ +- LVGL 文档: https://docs.lvgl.io/ +- ESP-SR 文档: https://github.com/espressif/esp-sr \ No newline at end of file diff --git a/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc b/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc new file mode 100644 index 0000000..d3f9012 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc @@ -0,0 +1,206 @@ +#include "wifi_board.h" +#include "audio_codec.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "i2c_device.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "atk_dnesp32s3_box" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class XL9555 : public I2cDevice { +public: + XL9555(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x06, 0x1B); + WriteReg(0x07, 0xFE); + } + + void SetOutputState(uint8_t bit, uint8_t level) { + uint16_t data; + if (bit < 8) { + data = ReadReg(0x02); + } else { + data = ReadReg(0x03); + bit -= 8; + } + + data = (data & ~(1 << bit)) | (level << bit); + + if (bit < 8) { + WriteReg(0x02, data); + } else { + WriteReg(0x03, data); + } + } +}; + +class atk_dnesp32s3_box : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + i2c_master_dev_handle_t xl9555_handle_; + Button boot_button_; + LcdDisplay* display_; + XL9555* xl9555_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = GPIO_NUM_48, + .scl_io_num = GPIO_NUM_45, + .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, &i2c_bus_)); + + // Initialize XL9555 + xl9555_ = new XL9555(i2c_bus_, 0x20); + } + + void InitializeATK_ST7789_80_Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + /* 配置RD引脚 */ + gpio_config_t gpio_init_struct; + gpio_init_struct.intr_type = GPIO_INTR_DISABLE; + gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; + gpio_init_struct.pin_bit_mask = 1ull << LCD_NUM_RD; + gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_init_struct); + gpio_set_level(LCD_NUM_RD, 1); + + esp_lcd_i80_bus_handle_t i80_bus = NULL; + esp_lcd_i80_bus_config_t bus_config = { + .dc_gpio_num = LCD_NUM_DC, + .wr_gpio_num = LCD_NUM_WR, + .clk_src = LCD_CLK_SRC_DEFAULT, + .data_gpio_nums = { + GPIO_LCD_D0, + GPIO_LCD_D1, + GPIO_LCD_D2, + GPIO_LCD_D3, + GPIO_LCD_D4, + GPIO_LCD_D5, + GPIO_LCD_D6, + GPIO_LCD_D7, + }, + .bus_width = 8, + .max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t), + .psram_trans_align = 64, + .sram_trans_align = 4, + }; + ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); + + esp_lcd_panel_io_i80_config_t io_config = { + .cs_gpio_num = LCD_NUM_CS, + .pclk_hz = (10 * 1000 * 1000), + .trans_queue_depth = 10, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = { + .dc_idle_level = 0, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, + .flags = { + .swap_color_bytes = 0, + }, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io)); + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = LCD_NUM_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + esp_lcd_panel_set_gap(panel, 0, 0); + uint8_t data0[] = {0x00}; + uint8_t data1[] = {0x65}; + esp_lcd_panel_io_tx_param(panel_io, 0x36, data0, 1); + esp_lcd_panel_io_tx_param(panel_io, 0x3A, data1, 1); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); + + 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 = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(), + #endif + }); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + atk_dnesp32s3_box() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeATK_ST7789_80_Display(); + xl9555_->SetOutputState(5, 1); + xl9555_->SetOutputState(7, 1); + InitializeButtons(); + InitializeIot(); + } + + virtual AudioCodec* GetAudioCodec() override { + static ATK_NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(atk_dnesp32s3_box); diff --git a/main/boards/atk-dnesp32s3-box/config.h b/main/boards/atk-dnesp32s3-box/config.h new file mode 100644 index 0000000..6b27e95 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box/config.h @@ -0,0 +1,45 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_13 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_21 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_47 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_14 + +#define BUILTIN_LED_GPIO GPIO_NUM_4 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_SWAP_XY true +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + + +// Pin Definitions +#define LCD_NUM_CS GPIO_NUM_1 +#define LCD_NUM_DC GPIO_NUM_2 +#define LCD_NUM_RD GPIO_NUM_41 +#define LCD_NUM_WR GPIO_NUM_42 +#define LCD_NUM_RST GPIO_NUM_NC + +#define GPIO_LCD_D0 GPIO_NUM_40 +#define GPIO_LCD_D1 GPIO_NUM_39 +#define GPIO_LCD_D2 GPIO_NUM_38 +#define GPIO_LCD_D3 GPIO_NUM_12 +#define GPIO_LCD_D4 GPIO_NUM_11 +#define GPIO_LCD_D5 GPIO_NUM_10 +#define GPIO_LCD_D6 GPIO_NUM_9 +#define GPIO_LCD_D7 GPIO_NUM_46 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/atk-dnesp32s3-box/config.json b/main/boards/atk-dnesp32s3-box/config.json new file mode 100644 index 0000000..21e97d3 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box/config.json @@ -0,0 +1,11 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atk-dnesp32s3-box", + "sdkconfig_append": [ + "CONFIG_USE_WECHAT_MESSAGE_STYLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc b/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc new file mode 100644 index 0000000..906eefa --- /dev/null +++ b/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc @@ -0,0 +1,186 @@ +#include "wifi_board.h" +#include "es8388_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 "led/single_led.h" + +#include +#include +#include +#include +#include + +#define TAG "atk_dnesp32s3" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class XL9555 : public I2cDevice { +public: + XL9555(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x06, 0x03); + WriteReg(0x07, 0xF0); + } + + void SetOutputState(uint8_t bit, uint8_t level) { + uint16_t data; + if (bit < 8) { + data = ReadReg(0x02); + } else { + data = ReadReg(0x03); + bit -= 8; + } + + data = (data & ~(1 << bit)) | (level << bit); + + if (bit < 8) { + WriteReg(0x02, data); + } else { + WriteReg(0x03, data); + } + } +}; + + +class atk_dnesp32s3 : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + XL9555* xl9555_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)I2C_NUM_0, + .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, &i2c_bus_)); + + // Initialize XL9555 + xl9555_ = new XL9555(i2c_bus_, 0x20); + } + + // Initialize spi peripheral + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = LCD_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = LCD_SCLK_PIN; + 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(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + ESP_LOGD(TAG, "Install panel IO"); + // 液晶屏控制IO初始化 + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = LCD_CS_PIN; + io_config.dc_gpio_num = LCD_DC_PIN; + io_config.spi_mode = 0; + io_config.pclk_hz = 20 * 1000 * 1000; + io_config.trans_queue_depth = 7; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(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_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG, + esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel); + + esp_lcd_panel_reset(panel); + xl9555_->SetOutputState(8, 1); + xl9555_->SetOutputState(2, 0); + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + 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 = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(), + #endif + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + atk_dnesp32s3() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeSt7789Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8388AudioCodec audio_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, + GPIO_NUM_NC, + AUDIO_CODEC_ES8388_ADDR + ); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(atk_dnesp32s3); diff --git a/main/boards/atk-dnesp32s3/config.h b/main/boards/atk-dnesp32s3/config.h new file mode 100644 index 0000000..cec5884 --- /dev/null +++ b/main/boards/atk-dnesp32s3/config.h @@ -0,0 +1,44 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_3 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_9 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_46 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_14 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_10 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_41 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_42 +#define AUDIO_CODEC_ES8388_ADDR ES8388_CODEC_DEFAULT_ADDR + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define BUILTIN_LED_GPIO GPIO_NUM_1 + +#define LCD_SCLK_PIN GPIO_NUM_12 +#define LCD_MOSI_PIN GPIO_NUM_11 +#define LCD_MISO_PIN GPIO_NUM_13 +#define LCD_DC_PIN GPIO_NUM_40 +#define LCD_CS_PIN GPIO_NUM_21 + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // _BOARD_CONFIG_H_ + diff --git a/main/boards/atk-dnesp32s3/config.json b/main/boards/atk-dnesp32s3/config.json new file mode 100644 index 0000000..2f3837d --- /dev/null +++ b/main/boards/atk-dnesp32s3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atk-dnesp32s3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/atommatrix-echo-base/README.md b/main/boards/atommatrix-echo-base/README.md new file mode 100644 index 0000000..39aa57f --- /dev/null +++ b/main/boards/atommatrix-echo-base/README.md @@ -0,0 +1,37 @@ +# 编译配置命令 + +**配置编译目标为 ESP32:** + +```bash +idf.py set-target esp32 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> AtomMatrix + Echo Base +``` + +**修改 flash 大小:** + +``` +Serial flasher config -> Flash size -> 4 MB +``` + +**修改分区表:** + +``` +Partition Table -> Custom partition CSV file -> partitions_4M.csv +``` + +**编译:** + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/atommatrix-echo-base/atommatrix_echo_base.cc b/main/boards/atommatrix-echo-base/atommatrix_echo_base.cc new file mode 100644 index 0000000..edb3364 --- /dev/null +++ b/main/boards/atommatrix-echo-base/atommatrix_echo_base.cc @@ -0,0 +1,143 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "i2c_device.h" +#include "iot/thing_manager.h" + +#include +#include +#include +#include "led/circular_strip.h" + +#define TAG "XX+EchoBase" + +#define PI4IOE_ADDR 0x43 +#define PI4IOE_REG_CTRL 0x00 +#define PI4IOE_REG_IO_PP 0x07 +#define PI4IOE_REG_IO_DIR 0x03 +#define PI4IOE_REG_IO_OUT 0x05 +#define PI4IOE_REG_IO_PULLUP 0x0D + +class Pi4ioe : public I2cDevice { +public: + Pi4ioe(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(PI4IOE_REG_IO_PP, 0x00); // Set to high-impedance + WriteReg(PI4IOE_REG_IO_PULLUP, 0xFF); // Enable pull-up + WriteReg(PI4IOE_REG_IO_DIR, 0x6E); // Set input=0, output=1 + WriteReg(PI4IOE_REG_IO_OUT, 0xFF); // Set outputs to 1 + } + + void SetSpeakerMute(bool mute) { + WriteReg(PI4IOE_REG_IO_OUT, mute ? 0x00 : 0xFF); + } +}; + + +class AtomMatrixEchoBaseBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + + Pi4ioe* pi4ioe_; + + Button face_button_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_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, &i2c_bus_)); + } + + void I2cDetect() { + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + } + + void InitializePi4ioe() { + ESP_LOGI(TAG, "Init PI4IOE"); + pi4ioe_ = new Pi4ioe(i2c_bus_, PI4IOE_ADDR); + pi4ioe_->SetSpeakerMute(false); + } + + + void InitializeButtons() { + face_button_.OnClick([this]() { + + ESP_LOGI(TAG, " ===>>> face_button_.OnClick "); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + } + +public: + AtomMatrixEchoBaseBoard() : face_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + I2cDetect(); + InitializePi4ioe(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static CircularStrip led(BUILTIN_LED_GPIO, 25); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_codec( + i2c_bus_, + I2C_NUM_1, + 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_GPIO_PA, + AUDIO_CODEC_ES8311_ADDR, + false); + return &audio_codec; + } + +}; + +DECLARE_BOARD(AtomMatrixEchoBaseBoard); diff --git a/main/boards/atommatrix-echo-base/config.h b/main/boards/atommatrix-echo-base/config.h new file mode 100644 index 0000000..d1684cb --- /dev/null +++ b/main/boards/atommatrix-echo-base/config.h @@ -0,0 +1,29 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// AtomMatrix+EchoBase Board configuration + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_19 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_33 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_23 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_22 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_25 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_21 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_GPIO_PA GPIO_NUM_NC + +#define BUILTIN_LED_GPIO GPIO_NUM_27 +#define BOOT_BUTTON_GPIO GPIO_NUM_39 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/atommatrix-echo-base/config.json b/main/boards/atommatrix-echo-base/config.json new file mode 100644 index 0000000..5b52124 --- /dev/null +++ b/main/boards/atommatrix-echo-base/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32", + "builds": [ + { + "name": "atommatrix-echo-base", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_4M.csv\"" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/atoms3-echo-base/README.md b/main/boards/atoms3-echo-base/README.md new file mode 100644 index 0000000..dfd9b0c --- /dev/null +++ b/main/boards/atoms3-echo-base/README.md @@ -0,0 +1,49 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> AtomS3 + Echo Base +``` + +**关闭语音唤醒:** + +``` +Xiaozhi Assistant -> [ ] 启用语音唤醒与音频处理 -> Unselect +``` + +**修改 flash 大小:** + +``` +Serial flasher config -> Flash size -> 8 MB +``` + +**修改分区表:** + +``` +Partition Table -> Custom partition CSV file -> partitions_8M.csv +``` + +**关闭片外 PSRAM:** + +``` +Component config -> ESP PSRAM -> [ ] Support for external, SPI-connected RAM -> Unselect +``` + +**编译:** + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/atoms3-echo-base/atoms3_echo_base.cc b/main/boards/atoms3-echo-base/atoms3_echo_base.cc new file mode 100644 index 0000000..3795b02 --- /dev/null +++ b/main/boards/atoms3-echo-base/atoms3_echo_base.cc @@ -0,0 +1,246 @@ +#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 "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "AtomS3+EchoBase" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb2, (uint8_t[]){0x2f}, 1, 0}, + {0xb3, (uint8_t[]){0x03}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x01}, 1, 0}, + {0xac, (uint8_t[]){0xcb}, 1, 0}, + {0xab, (uint8_t[]){0x0e}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x19}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xe8, (uint8_t[]){0x24}, 1, 0}, + {0xe9, (uint8_t[]){0x48}, 1, 0}, + {0xea, (uint8_t[]){0x22}, 1, 0}, + {0xc6, (uint8_t[]){0x30}, 1, 0}, + {0xc7, (uint8_t[]){0x18}, 1, 0}, + {0xf0, + (uint8_t[]){0x1f, 0x28, 0x04, 0x3e, 0x2a, 0x2e, 0x20, 0x00, 0x0c, 0x06, + 0x00, 0x1c, 0x1f, 0x0f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x00, 0x2d, 0x2f, 0x3c, 0x6f, 0x1c, 0x0b, 0x00, 0x00, 0x00, + 0x07, 0x0d, 0x11, 0x0f}, + 14, 0}, +}; + +class AtomS3EchoBaseBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Display* display_; + Button boot_button_; + bool is_echo_base_connected_ = false; + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_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, &i2c_bus_)); + } + + void I2cDetect() { + is_echo_base_connected_ = false; + uint8_t echo_base_connected_flag = 0x00; + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + if (address == 0x18) { + echo_base_connected_flag |= 0xF0; + } else if (address == 0x43) { + echo_base_connected_flag |= 0x0F; + } + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + is_echo_base_connected_ = (echo_base_connected_flag == 0xFF); + } + + void CheckEchoBaseConnection() { + if (is_echo_base_connected_) { + return; + } + + // Pop error page + InitializeSpi(); + InitializeGc9107Display(); + InitializeButtons(); + GetBacklight()->SetBrightness(100); + display_->SetStatus(Lang::Strings::ERROR); + display_->SetEmotion("sad"); + display_->SetChatMessage("system", "Echo Base\nnot connected"); + + while (1) { + ESP_LOGE(TAG, "Atomic Echo Base is disconnected"); + vTaskDelay(pdMS_TO_TICKS(1000)); + + // Rerun detection + I2cDetect(); + if (is_echo_base_connected_) { + vTaskDelay(pdMS_TO_TICKS(500)); + I2cDetect(); + if (is_echo_base_connected_) { + ESP_LOGI(TAG, "Atomic Echo Base is reconnected"); + vTaskDelay(pdMS_TO_TICKS(200)); + esp_restart(); + } + } + } + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize SPI bus"); + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_21; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_17; + 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)); + } + + void InitializeGc9107Display() { + ESP_LOGI(TAG, "Init GC9107 display"); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_15; + io_config.dc_gpio_num = GPIO_NUM_33; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &io_handle)); + + ESP_LOGI(TAG, "Install GC9A01 panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_34; // Set to -1 if not use + panel_config.rgb_endian = LCD_RGB_ENDIAN_BGR; + panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18) + panel_config.vendor_config = &gc9107_vendor_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); + + + display_ = new SpiLcdDisplay(io_handle, panel_handle, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + AtomS3EchoBaseBoard() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + I2cDetect(); + CheckEchoBaseConnection(); + InitializeSpi(); + InitializeGc9107Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_codec( + i2c_bus_, + I2C_NUM_1, + 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_GPIO_PA, + AUDIO_CODEC_ES8311_ADDR, + false); + 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; + } +}; + +DECLARE_BOARD(AtomS3EchoBaseBoard); \ No newline at end of file diff --git a/main/boards/atoms3-echo-base/config.h b/main/boards/atoms3-echo-base/config.h new file mode 100644 index 0000000..6b2fdad --- /dev/null +++ b/main/boards/atoms3-echo-base/config.h @@ -0,0 +1,43 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// AtomS3+EchoBase Board configuration + +#include + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_6 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_5 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_38 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_39 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_GPIO_PA GPIO_NUM_NC + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_41 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 32 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_16 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + + +#endif // _BOARD_CONFIG_H_ \ No newline at end of file diff --git a/main/boards/atoms3-echo-base/config.json b/main/boards/atoms3-echo-base/config.json new file mode 100644 index 0000000..3062ce0 --- /dev/null +++ b/main/boards/atoms3-echo-base/config.json @@ -0,0 +1,14 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atoms3-echo-base", + "sdkconfig_append": [ + "CONFIG_SPIRAM=n", + "CONFIG_USE_AFE=n", + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_8M.csv\"" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/atoms3r-cam-m12-echo-base/README.md b/main/boards/atoms3r-cam-m12-echo-base/README.md new file mode 100644 index 0000000..f21f6e0 --- /dev/null +++ b/main/boards/atoms3r-cam-m12-echo-base/README.md @@ -0,0 +1,53 @@ +# AtomS3R CAM/M12 + Echo Base + +## 简介 + + + +AtomS3R CAM、AtomS3R M12 是 M5Stack 推出的基于 ESP32-S3-PICO-1-N8R8 的物联网可编程控制器,搭载了摄像头。Atomic Echo Base 是一款专为 M5 Atom 系列主机设计的语音识别底座,采用了 ES8311 单声道音频解码器、MEMS 麦克风和 NS4150B 功率放大器的集成方案。 + +两款开发版均**不带屏幕、不带额外按键**,需要使用语音唤醒。必要时,需要使用 `idf.py monitor` 查看 log 以确定运行状态。 + +## 配置、编译命令 + +**配置编译目标为 ESP32S3** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig 并配置** + +```bash +idf.py menuconfig +``` + +分别配置如下选项: + +- `Xiaozhi Assistant` → `Board Type` → 选择 `AtomS3R CAM/M12 + Echo Base` +- `Partition Table` → `Custom partition CSV file` → 删除原有内容,输入 `partitions_8M.csv` +- `Serial flasher config` → `Flash size` → 选择 `8 MB` + +按 `S` 保存,按 `Q` 退出。 + +**编译** + +```bash +idf.py build +``` + +**烧录** + +将 AtomS3R CAM/M12 连接到电脑,按住侧面 RESET 按键,直到 RESET 按键下方绿灯闪烁。 + +```bash +idf.py flash +``` + +烧录完毕后,按一下 RESET 按钮重启。 diff --git a/main/boards/atoms3r-cam-m12-echo-base/atoms3r_cam_m12_echo_base.cc b/main/boards/atoms3r-cam-m12-echo-base/atoms3r_cam_m12_echo_base.cc new file mode 100644 index 0000000..48898d6 --- /dev/null +++ b/main/boards/atoms3r-cam-m12-echo-base/atoms3r_cam_m12_echo_base.cc @@ -0,0 +1,162 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "i2c_device.h" +#include "iot/thing_manager.h" +#include "assets/lang_config.h" + +#include +#include +#include + +#define TAG "AtomS3R M12+EchoBase" + +#define PI4IOE_ADDR 0x43 +#define PI4IOE_REG_CTRL 0x00 +#define PI4IOE_REG_IO_PP 0x07 +#define PI4IOE_REG_IO_DIR 0x03 +#define PI4IOE_REG_IO_OUT 0x05 +#define PI4IOE_REG_IO_PULLUP 0x0D + +class Pi4ioe : public I2cDevice { +public: + Pi4ioe(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(PI4IOE_REG_IO_PP, 0x00); // Set to high-impedance + WriteReg(PI4IOE_REG_IO_PULLUP, 0xFF); // Enable pull-up + WriteReg(PI4IOE_REG_IO_DIR, 0x6E); // Set input=0, output=1 + WriteReg(PI4IOE_REG_IO_OUT, 0xFF); // Set outputs to 1 + } + + void SetSpeakerMute(bool mute) { + WriteReg(PI4IOE_REG_IO_OUT, mute ? 0x00 : 0xFF); + } +}; + +class AtomS3rCamM12EchoBaseBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Pi4ioe* pi4ioe_ = nullptr; + bool is_echo_base_connected_ = false; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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, &i2c_bus_)); + } + + void I2cDetect() { + is_echo_base_connected_ = false; + uint8_t echo_base_connected_flag = 0x00; + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + if (address == 0x18) { + echo_base_connected_flag |= 0xF0; + } else if (address == 0x43) { + echo_base_connected_flag |= 0x0F; + } + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + is_echo_base_connected_ = (echo_base_connected_flag == 0xFF); + } + + void CheckEchoBaseConnection() { + if (is_echo_base_connected_) { + return; + } + + while (1) { + ESP_LOGE(TAG, "Atomic Echo Base is disconnected"); + vTaskDelay(pdMS_TO_TICKS(1000)); + + // Rerun detection + I2cDetect(); + if (is_echo_base_connected_) { + vTaskDelay(pdMS_TO_TICKS(500)); + I2cDetect(); + if (is_echo_base_connected_) { + ESP_LOGI(TAG, "Atomic Echo Base is reconnected"); + vTaskDelay(pdMS_TO_TICKS(200)); + esp_restart(); + } + } + } + } + + void InitializePi4ioe() { + ESP_LOGI(TAG, "Init PI4IOE"); + pi4ioe_ = new Pi4ioe(i2c_bus_, PI4IOE_ADDR); + pi4ioe_->SetSpeakerMute(false); + } + + void EnableCameraPower() { + gpio_reset_pin((gpio_num_t)18); + gpio_set_direction((gpio_num_t)18, GPIO_MODE_OUTPUT); + gpio_set_pull_mode((gpio_num_t)18, GPIO_PULLDOWN_ONLY); + + ESP_LOGI(TAG, "Camera Power Enabled"); + + vTaskDelay(pdMS_TO_TICKS(200)); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + } + +public: + AtomS3rCamM12EchoBaseBoard() { + EnableCameraPower(); // IO18 还会控制指示灯 + InitializeI2c(); + I2cDetect(); + CheckEchoBaseConnection(); + InitializePi4ioe(); + InitializeIot(); + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_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_GPIO_PA, + AUDIO_CODEC_ES8311_ADDR, + false); + return &audio_codec; + } +}; + +DECLARE_BOARD(AtomS3rCamM12EchoBaseBoard); diff --git a/main/boards/atoms3r-cam-m12-echo-base/config.h b/main/boards/atoms3r-cam-m12-echo-base/config.h new file mode 100644 index 0000000..6876dbc --- /dev/null +++ b/main/boards/atoms3r-cam-m12-echo-base/config.h @@ -0,0 +1,51 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// AtomS3R M12+EchoBase Board configuration + +#include + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_6 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_5 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_38 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_39 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_GPIO_PA GPIO_NUM_NC + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_41 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define CAMERA_PIN_PWDN (-1) +#define CAMERA_PIN_RESET (-1) + +#define CAMERA_PIN_VSYNC (10) +#define CAMERA_PIN_HREF (14) +#define CAMERA_PIN_PCLK (40) +#define CAMERA_PIN_XCLK (21) + +#define CAMERA_PIN_SIOD (12) +#define CAMERA_PIN_SIOC ( 9) + +#define CAMERA_PIN_D0 ( 3) +#define CAMERA_PIN_D1 (42) +#define CAMERA_PIN_D2 (46) +#define CAMERA_PIN_D3 (48) +#define CAMERA_PIN_D4 ( 4) +#define CAMERA_PIN_D5 (17) +#define CAMERA_PIN_D6 (11) +#define CAMERA_PIN_D7 (13) + +#define CAMERA_XCLK_FREQ (20000000) + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/atoms3r-cam-m12-echo-base/config.json b/main/boards/atoms3r-cam-m12-echo-base/config.json new file mode 100644 index 0000000..507e0fe --- /dev/null +++ b/main/boards/atoms3r-cam-m12-echo-base/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atoms3r-cam-m12-echo-base", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_8M.csv\"" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/atoms3r-echo-base/README.md b/main/boards/atoms3r-echo-base/README.md new file mode 100644 index 0000000..29c531e --- /dev/null +++ b/main/boards/atoms3r-echo-base/README.md @@ -0,0 +1,43 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> AtomS3R + Echo Base +``` + +**修改 flash 大小:** + +``` +Serial flasher config -> Flash size -> 8 MB +``` + +**修改分区表:** + +``` +Partition Table -> Custom partition CSV file -> partitions_8M.csv +``` + +**修改 psram 配置:** + +``` +Component config -> ESP PSRAM -> SPI RAM config -> Mode (QUAD/OCT) -> Octal Mode PSRAM +``` + +**编译:** + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc b/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc new file mode 100644 index 0000000..ff2b79a --- /dev/null +++ b/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc @@ -0,0 +1,324 @@ +#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 "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "AtomS3R+EchoBase" + +#define PI4IOE_ADDR 0x43 +#define PI4IOE_REG_CTRL 0x00 +#define PI4IOE_REG_IO_PP 0x07 +#define PI4IOE_REG_IO_DIR 0x03 +#define PI4IOE_REG_IO_OUT 0x05 +#define PI4IOE_REG_IO_PULLUP 0x0D + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class Pi4ioe : public I2cDevice { +public: + Pi4ioe(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(PI4IOE_REG_IO_PP, 0x00); // Set to high-impedance + WriteReg(PI4IOE_REG_IO_PULLUP, 0xFF); // Enable pull-up + WriteReg(PI4IOE_REG_IO_DIR, 0x6E); // Set input=0, output=1 + WriteReg(PI4IOE_REG_IO_OUT, 0xFF); // Set outputs to 1 + } + + void SetSpeakerMute(bool mute) { + WriteReg(PI4IOE_REG_IO_OUT, mute ? 0x00 : 0xFF); + } +}; + +class Lp5562 : public I2cDevice { +public: + Lp5562(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x00, 0B01000000); // Set chip_en to 1 + WriteReg(0x08, 0B00000001); // Enable internal clock + WriteReg(0x70, 0B00000000); // Configure all LED outputs to be controlled from I2C registers + + // PWM clock frequency 558 Hz + auto data = ReadReg(0x08); + data = data | 0B01000000; + WriteReg(0x08, data); + } + + void SetBrightness(uint8_t brightness) { + // Map 0~100 to 0~255 + brightness = brightness * 255 / 100; + WriteReg(0x0E, brightness); + } +}; + +class CustomBacklight : public Backlight { +public: + CustomBacklight(Lp5562* lp5562) : lp5562_(lp5562) {} + + void SetBrightnessImpl(uint8_t brightness) override { + if (lp5562_) { + lp5562_->SetBrightness(brightness); + } else { + ESP_LOGE(TAG, "LP5562 not available"); + } + } + +private: + Lp5562* lp5562_ = nullptr; +}; + +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb2, (uint8_t[]){0x2f}, 1, 0}, + {0xb3, (uint8_t[]){0x03}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x01}, 1, 0}, + {0xac, (uint8_t[]){0xcb}, 1, 0}, + {0xab, (uint8_t[]){0x0e}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x19}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xe8, (uint8_t[]){0x24}, 1, 0}, + {0xe9, (uint8_t[]){0x48}, 1, 0}, + {0xea, (uint8_t[]){0x22}, 1, 0}, + {0xc6, (uint8_t[]){0x30}, 1, 0}, + {0xc7, (uint8_t[]){0x18}, 1, 0}, + {0xf0, + (uint8_t[]){0x1f, 0x28, 0x04, 0x3e, 0x2a, 0x2e, 0x20, 0x00, 0x0c, 0x06, + 0x00, 0x1c, 0x1f, 0x0f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x00, 0x2d, 0x2f, 0x3c, 0x6f, 0x1c, 0x0b, 0x00, 0x00, 0x00, + 0x07, 0x0d, 0x11, 0x0f}, + 14, 0}, +}; + +class AtomS3rEchoBaseBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + i2c_master_bus_handle_t i2c_bus_internal_; + Pi4ioe* pi4ioe_ = nullptr; + Lp5562* lp5562_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + bool is_echo_base_connected_ = false; + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_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, &i2c_bus_)); + + i2c_bus_cfg.i2c_port = I2C_NUM_0; + i2c_bus_cfg.sda_io_num = GPIO_NUM_45; + i2c_bus_cfg.scl_io_num = GPIO_NUM_0; + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_internal_)); + } + + void I2cDetect() { + is_echo_base_connected_ = false; + uint8_t echo_base_connected_flag = 0x00; + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + if (address == 0x18) { + echo_base_connected_flag |= 0xF0; + } else if (address == 0x43) { + echo_base_connected_flag |= 0x0F; + } + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + is_echo_base_connected_ = (echo_base_connected_flag == 0xFF); + } + + void CheckEchoBaseConnection() { + if (is_echo_base_connected_) { + return; + } + + // Pop error page + InitializeLp5562(); + InitializeSpi(); + InitializeGc9107Display(); + InitializeButtons(); + GetBacklight()->SetBrightness(100); + display_->SetStatus(Lang::Strings::ERROR); + display_->SetEmotion("sad"); + display_->SetChatMessage("system", "Echo Base\nnot connected"); + + while (1) { + ESP_LOGE(TAG, "Atomic Echo Base is disconnected"); + vTaskDelay(pdMS_TO_TICKS(1000)); + + // Rerun detection + I2cDetect(); + if (is_echo_base_connected_) { + vTaskDelay(pdMS_TO_TICKS(500)); + I2cDetect(); + if (is_echo_base_connected_) { + ESP_LOGI(TAG, "Atomic Echo Base is reconnected"); + vTaskDelay(pdMS_TO_TICKS(200)); + esp_restart(); + } + } + } + } + + void InitializePi4ioe() { + ESP_LOGI(TAG, "Init PI4IOE"); + pi4ioe_ = new Pi4ioe(i2c_bus_, PI4IOE_ADDR); + pi4ioe_->SetSpeakerMute(false); + } + + void InitializeLp5562() { + ESP_LOGI(TAG, "Init LP5562"); + lp5562_ = new Lp5562(i2c_bus_internal_, 0x30); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize SPI bus"); + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_21; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_15; + 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)); + } + + void InitializeGc9107Display() { + ESP_LOGI(TAG, "Init GC9107 display"); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_14; + io_config.dc_gpio_num = GPIO_NUM_42; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &io_handle)); + + ESP_LOGI(TAG, "Install GC9A01 panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_48; // Set to -1 if not use + panel_config.rgb_endian = LCD_RGB_ENDIAN_BGR; + panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18) + panel_config.vendor_config = &gc9107_vendor_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); + + display_ = new SpiLcdDisplay(io_handle, panel_handle, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + AtomS3rEchoBaseBoard() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + I2cDetect(); + CheckEchoBaseConnection(); + InitializePi4ioe(); + InitializeLp5562(); + InitializeSpi(); + InitializeGc9107Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_codec( + i2c_bus_, + I2C_NUM_1, + 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_GPIO_PA, + AUDIO_CODEC_ES8311_ADDR, + false); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight *GetBacklight() override { + static CustomBacklight backlight(lp5562_); + return &backlight; + } +}; + +DECLARE_BOARD(AtomS3rEchoBaseBoard); diff --git a/main/boards/atoms3r-echo-base/config.h b/main/boards/atoms3r-echo-base/config.h new file mode 100644 index 0000000..d519c2e --- /dev/null +++ b/main/boards/atoms3r-echo-base/config.h @@ -0,0 +1,43 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// AtomS3R+EchoBase Board configuration + +#include + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_6 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_5 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_38 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_39 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_GPIO_PA GPIO_NUM_NC + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_41 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 32 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/atoms3r-echo-base/config.json b/main/boards/atoms3r-echo-base/config.json new file mode 100644 index 0000000..3bef3af --- /dev/null +++ b/main/boards/atoms3r-echo-base/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atoms3r-echo-base", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_8M.csv\"" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/bread-compact-esp32-lcd/config.h b/main/boards/bread-compact-esp32-lcd/config.h new file mode 100644 index 0000000..2068a2d --- /dev/null +++ b/main/boards/bread-compact-esp32-lcd/config.h @@ -0,0 +1,276 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +// 如果使用 Duplex I2S 模式,请注释下面一行 +#define AUDIO_I2S_METHOD_SIMPLEX + +#ifdef AUDIO_I2S_METHOD_SIMPLEX + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_25 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_26 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_32 + +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_33 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_14 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_27 + +#else + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#endif + + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define TOUCH_BUTTON_GPIO GPIO_NUM_5 +#define ASR_BUTTON_GPIO GPIO_NUM_19 +#define BUILTIN_LED_GPIO GPIO_NUM_2 + + +#ifdef CONFIG_LCD_ST7789_240X240_7PIN +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_22 +#define DISPLAY_CS_PIN GPIO_NUM_NC +#else +#define DISPLAY_CS_PIN GPIO_NUM_22 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_23 +#endif + +#define DISPLAY_MOSI_PIN GPIO_NUM_4 +#define DISPLAY_CLK_PIN GPIO_NUM_15 +#define DISPLAY_DC_PIN GPIO_NUM_21 +#define DISPLAY_RST_PIN GPIO_NUM_18 + + +#ifdef CONFIG_LCD_ST7789_240X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X320_NO_IPS +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_170X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 170 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 35 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_172X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 172 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 34 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X280 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 280 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 20 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X240 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X240_7PIN +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 2 +#endif + +#ifdef CONFIG_LCD_ST7789_240X135 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 135 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 40 +#define DISPLAY_OFFSET_Y 53 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7735_128X160 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 160 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7735_128X128 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 32 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7796_320X480 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 480 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ILI9341_240X320 +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ILI9341_240X320_NO_IPS +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_GC9A01_240X240 +#define LCD_TYPE_GC9A01_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_CUSTOM +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/bread-compact-esp32-lcd/config.json b/main/boards/bread-compact-esp32-lcd/config.json new file mode 100644 index 0000000..091277e --- /dev/null +++ b/main/boards/bread-compact-esp32-lcd/config.json @@ -0,0 +1,13 @@ +{ + "target": "esp32", + "builds": [ + { + "name": "bread-compact-esp32-lcd", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_4M.csv\"", + "LCD_ST7789_240X240_7PIN=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/bread-compact-esp32-lcd/esp32_bread_board_lcd.cc b/main/boards/bread-compact-esp32-lcd/esp32_bread_board_lcd.cc new file mode 100644 index 0000000..2f1f0b9 --- /dev/null +++ b/main/boards/bread-compact-esp32-lcd/esp32_bread_board_lcd.cc @@ -0,0 +1,223 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(LCD_TYPE_ILI9341_SERIAL) +#include "esp_lcd_ili9341.h" +#endif + +#if defined(LCD_TYPE_GC9A01_SERIAL) +#include "esp_lcd_gc9a01.h" +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb1, (uint8_t[]){0x80}, 1, 0}, + {0xb2, (uint8_t[]){0x27}, 1, 0}, + {0xb3, (uint8_t[]){0x13}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x05}, 1, 0}, + {0xac, (uint8_t[]){0xc8}, 1, 0}, + {0xab, (uint8_t[]){0x0f}, 1, 0}, + {0x3a, (uint8_t[]){0x05}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x08}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xea, (uint8_t[]){0x02}, 1, 0}, + {0xe8, (uint8_t[]){0x2A}, 1, 0}, + {0xe9, (uint8_t[]){0x47}, 1, 0}, + {0xe7, (uint8_t[]){0x5f}, 1, 0}, + {0xc6, (uint8_t[]){0x21}, 1, 0}, + {0xc7, (uint8_t[]){0x15}, 1, 0}, + {0xf0, + (uint8_t[]){0x1D, 0x38, 0x09, 0x4D, 0x92, 0x2F, 0x35, 0x52, 0x1E, 0x0C, + 0x04, 0x12, 0x14, 0x1f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x16, 0x40, 0x1C, 0x54, 0xA9, 0x2D, 0x2E, 0x56, 0x10, 0x0D, + 0x0C, 0x1A, 0x14, 0x1E}, + 14, 0}, + {0xf4, (uint8_t[]){0x00, 0x00, 0xFF}, 3, 0}, + {0xba, (uint8_t[]){0xFF, 0xFF}, 2, 0}, +}; +#endif + +#define TAG "ESP32-LCD-MarsbearSupport" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + +class CompactWifiBoardLCD : public WifiBoard { +private: + Button boot_button_; + Button touch_button_; + Button asr_button_; + + LcdDisplay* display_; + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_CLK_PIN; + 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)); + } + + void InitializeLcdDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = DISPLAY_SPI_MODE; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = DISPLAY_RGB_ORDER; + panel_config.bits_per_pixel = 16; +#if defined(LCD_TYPE_ILI9341_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); +#elif defined(LCD_TYPE_GC9A01_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(panel_io, &panel_config, &panel)); + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; +#else + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); +#endif + + esp_lcd_panel_reset(panel); + + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); +#ifdef LCD_TYPE_GC9A01_SERIAL + panel_config.vendor_config = &gc9107_vendor_config; +#endif + 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_14_1, + .icon_font = &font_awesome_14_1, + .emoji_font = font_emoji_32_init(), + }); + } + + + + void InitializeButtons() { + + // 配置 GPIO + gpio_config_t io_conf = { + .pin_bit_mask = 1ULL << BUILTIN_LED_GPIO, // 设置需要配置的 GPIO 引脚 + .mode = GPIO_MODE_OUTPUT, // 设置为输出模式 + .pull_up_en = GPIO_PULLUP_DISABLE, // 禁用上拉 + .pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁用下拉 + .intr_type = GPIO_INTR_DISABLE // 禁用中断 + }; + gpio_config(&io_conf); // 应用配置 + + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + gpio_set_level(BUILTIN_LED_GPIO, 1); + // 使用新的打断按键功能:按一次进入listening,再按一次进入idle + app.ToggleListeningState(); + }); + + asr_button_.OnClick([this]() { + std::string wake_word="你好小智"; + Application::GetInstance().WakeWordInvoke(wake_word); + }); + + touch_button_.OnPressDown([this]() { + gpio_set_level(BUILTIN_LED_GPIO, 1); + Application::GetInstance().StartListening(); + }); + + touch_button_.OnPressUp([this]() { + gpio_set_level(BUILTIN_LED_GPIO, 0); + Application::GetInstance().StopListening(); + }); + + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + thing_manager.AddThing(iot::CreateThing("Screen")); + } + } + +public: + CompactWifiBoardLCD() : + boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) { + InitializeSpi(); + InitializeLcdDisplay(); + InitializeButtons(); + InitializeIot(); + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + GetBacklight()->RestoreBrightness(); + } + + } + + virtual AudioCodec* GetAudioCodec() override { +#ifdef AUDIO_I2S_METHOD_SIMPLEX + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); +#else + static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); +#endif + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + return nullptr; + } +}; + +DECLARE_BOARD(CompactWifiBoardLCD); diff --git a/main/boards/bread-compact-esp32/README.md b/main/boards/bread-compact-esp32/README.md new file mode 100644 index 0000000..95e5828 --- /dev/null +++ b/main/boards/bread-compact-esp32/README.md @@ -0,0 +1,37 @@ +# 编译配置命令 + +**配置编译目标为 ESP32:** + +```bash +idf.py set-target esp32 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> 面包板 ESP32 DevKit +``` + +**修改 flash 大小:** + +``` +Serial flasher config -> Flash size -> 4 MB +``` + +**修改分区表:** + +``` +Partition Table -> Custom partition CSV file -> partitions_4M.csv +``` + +**编译:** + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/bread-compact-esp32/config.h b/main/boards/bread-compact-esp32/config.h new file mode 100644 index 0000000..177e866 --- /dev/null +++ b/main/boards/bread-compact-esp32/config.h @@ -0,0 +1,51 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +// 如果使用 Duplex I2S 模式,请注释下面一行 +#define AUDIO_I2S_METHOD_SIMPLEX + +#ifdef AUDIO_I2S_METHOD_SIMPLEX + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_25 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_26 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_32 + +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_33 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_14 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_27 + +#else + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#endif + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define TOUCH_BUTTON_GPIO GPIO_NUM_5 +#define ASR_BUTTON_GPIO GPIO_NUM_19 +#define BUILTIN_LED_GPIO GPIO_NUM_2 + +#define DISPLAY_SDA_PIN GPIO_NUM_4 +#define DISPLAY_SCL_PIN GPIO_NUM_15 +#define DISPLAY_WIDTH 128 + +#if CONFIG_OLED_SSD1306_128X32 +#define DISPLAY_HEIGHT 32 +#elif CONFIG_OLED_SSD1306_128X64 +#define DISPLAY_HEIGHT 64 +#else +#error "未选择 OLED 屏幕类型" +#endif + +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/bread-compact-esp32/config.json b/main/boards/bread-compact-esp32/config.json new file mode 100644 index 0000000..71bb097 --- /dev/null +++ b/main/boards/bread-compact-esp32/config.json @@ -0,0 +1,21 @@ +{ + "target": "esp32", + "builds": [ + { + "name": "bread-compact-esp32", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_4M.csv\"", + "CONFIG_OLED_SSD1306_128X64=y" + ] + }, + { + "name": "bread-compact-esp32-128x32", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_4M.csv\"", + "CONFIG_OLED_SSD1306_128X32=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/bread-compact-esp32/esp32_bread_board.cc b/main/boards/bread-compact-esp32/esp32_bread_board.cc new file mode 100644 index 0000000..148969d --- /dev/null +++ b/main/boards/bread-compact-esp32/esp32_bread_board.cc @@ -0,0 +1,168 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "display/oled_display.h" + +#include +#include +#include +#include +#include + +#define TAG "ESP32-MarsbearSupport" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + + +class CompactWifiBoard : public WifiBoard { +private: + Button boot_button_; + Button touch_button_; + Button asr_button_; + + i2c_master_bus_handle_t display_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + + // 配置 GPIO + gpio_config_t io_conf = { + .pin_bit_mask = 1ULL << BUILTIN_LED_GPIO, // 设置需要配置的 GPIO 引脚 + .mode = GPIO_MODE_OUTPUT, // 设置为输出模式 + .pull_up_en = GPIO_PULLUP_DISABLE, // 禁用上拉 + .pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁用下拉 + .intr_type = GPIO_INTR_DISABLE // 禁用中断 + }; + gpio_config(&io_conf); // 应用配置 + + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + gpio_set_level(BUILTIN_LED_GPIO, 1); + app.ToggleChatState(); + }); + + asr_button_.OnClick([this]() { + std::string wake_word="你好小智"; + Application::GetInstance().WakeWordInvoke(wake_word); + }); + + touch_button_.OnPressDown([this]() { + gpio_set_level(BUILTIN_LED_GPIO, 1); + Application::GetInstance().StartListening(); + }); + touch_button_.OnPressUp([this]() { + gpio_set_level(BUILTIN_LED_GPIO, 0); + Application::GetInstance().StopListening(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Lamp")); + } + +public: + CompactWifiBoard() : boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) + { + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual AudioCodec* GetAudioCodec() override + { +#ifdef AUDIO_I2S_METHOD_SIMPLEX + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); +#else + static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); +#endif + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + +}; + +DECLARE_BOARD(CompactWifiBoard); diff --git a/main/boards/bread-compact-ml307/compact_ml307_board.cc b/main/boards/bread-compact-ml307/compact_ml307_board.cc new file mode 100644 index 0000000..c197dbe --- /dev/null +++ b/main/boards/bread-compact-ml307/compact_ml307_board.cc @@ -0,0 +1,180 @@ +#include "ml307_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/oled_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include + +#define TAG "CompactMl307Board" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + +class CompactMl307Board : public Ml307Board { +private: + i2c_master_bus_handle_t display_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + Button touch_button_; + Button volume_up_button_; + Button volume_down_button_; + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + Application::GetInstance().ToggleChatState(); + }); + touch_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + touch_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + volume_up_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Lamp")); + } + +public: + CompactMl307Board() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO), + touch_button_(TOUCH_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { +#ifdef AUDIO_I2S_METHOD_SIMPLEX + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); +#else + static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); +#endif + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(CompactMl307Board); diff --git a/main/boards/bread-compact-ml307/config.h b/main/boards/bread-compact-ml307/config.h new file mode 100644 index 0000000..53db9c2 --- /dev/null +++ b/main/boards/bread-compact-ml307/config.h @@ -0,0 +1,56 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +// 如果使用 Duplex I2S 模式,请注释下面一行 +#define AUDIO_I2S_METHOD_SIMPLEX + +#ifdef AUDIO_I2S_METHOD_SIMPLEX + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#else + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#endif + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define TOUCH_BUTTON_GPIO GPIO_NUM_47 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#define DISPLAY_SDA_PIN GPIO_NUM_41 +#define DISPLAY_SCL_PIN GPIO_NUM_42 +#define DISPLAY_WIDTH 128 + +#if CONFIG_OLED_SSD1306_128X32 +#define DISPLAY_HEIGHT 32 +#elif CONFIG_OLED_SSD1306_128X64 +#define DISPLAY_HEIGHT 64 +#else +#error "未选择 OLED 屏幕类型" +#endif + +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + + +#define ML307_RX_PIN GPIO_NUM_11 +#define ML307_TX_PIN GPIO_NUM_12 + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/bread-compact-ml307/config.json b/main/boards/bread-compact-ml307/config.json new file mode 100644 index 0000000..9da8cab --- /dev/null +++ b/main/boards/bread-compact-ml307/config.json @@ -0,0 +1,17 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "bread-compact-ml307", + "sdkconfig_append": [ + "CONFIG_OLED_SSD1306_128X32=y" + ] + }, + { + "name": "bread-compact-ml307-128x64", + "sdkconfig_append": [ + "CONFIG_OLED_SSD1306_128X64=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/bread-compact-wifi-lcd/compact_wifi_board_lcd.cc b/main/boards/bread-compact-wifi-lcd/compact_wifi_board_lcd.cc new file mode 100644 index 0000000..43a8615 --- /dev/null +++ b/main/boards/bread-compact-wifi-lcd/compact_wifi_board_lcd.cc @@ -0,0 +1,200 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(LCD_TYPE_ILI9341_SERIAL) +#include "esp_lcd_ili9341.h" +#endif + +#if defined(LCD_TYPE_GC9A01_SERIAL) +#include "esp_lcd_gc9a01.h" +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb1, (uint8_t[]){0x80}, 1, 0}, + {0xb2, (uint8_t[]){0x27}, 1, 0}, + {0xb3, (uint8_t[]){0x13}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x05}, 1, 0}, + {0xac, (uint8_t[]){0xc8}, 1, 0}, + {0xab, (uint8_t[]){0x0f}, 1, 0}, + {0x3a, (uint8_t[]){0x05}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x08}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xea, (uint8_t[]){0x02}, 1, 0}, + {0xe8, (uint8_t[]){0x2A}, 1, 0}, + {0xe9, (uint8_t[]){0x47}, 1, 0}, + {0xe7, (uint8_t[]){0x5f}, 1, 0}, + {0xc6, (uint8_t[]){0x21}, 1, 0}, + {0xc7, (uint8_t[]){0x15}, 1, 0}, + {0xf0, + (uint8_t[]){0x1D, 0x38, 0x09, 0x4D, 0x92, 0x2F, 0x35, 0x52, 0x1E, 0x0C, + 0x04, 0x12, 0x14, 0x1f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x16, 0x40, 0x1C, 0x54, 0xA9, 0x2D, 0x2E, 0x56, 0x10, 0x0D, + 0x0C, 0x1A, 0x14, 0x1E}, + 14, 0}, + {0xf4, (uint8_t[]){0x00, 0x00, 0xFF}, 3, 0}, + {0xba, (uint8_t[]){0xFF, 0xFF}, 2, 0}, +}; +#endif + +#define TAG "CompactWifiBoardLCD" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class CompactWifiBoardLCD : public WifiBoard { +private: + + Button boot_button_; + LcdDisplay* display_; + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_CLK_PIN; + 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)); + } + + void InitializeLcdDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = DISPLAY_SPI_MODE; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = DISPLAY_RGB_ORDER; + panel_config.bits_per_pixel = 16; +#if defined(LCD_TYPE_ILI9341_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); +#elif defined(LCD_TYPE_GC9A01_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(panel_io, &panel_config, &panel)); + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; +#else + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); +#endif + + esp_lcd_panel_reset(panel); + + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); +#ifdef LCD_TYPE_GC9A01_SERIAL + panel_config.vendor_config = &gc9107_vendor_config; +#endif + 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_16_4, + .icon_font = &font_awesome_16_4, +#if CONFIG_USE_WECHAT_MESSAGE_STYLE + .emoji_font = font_emoji_32_init(), +#else + .emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(), +#endif + }); + } + + + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 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("Lamp")); + } + +public: + CompactWifiBoardLCD() : + boot_button_(BOOT_BUTTON_GPIO) { + InitializeSpi(); + InitializeLcdDisplay(); + InitializeButtons(); + InitializeIot(); + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + GetBacklight()->RestoreBrightness(); + } + + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { +#ifdef AUDIO_I2S_METHOD_SIMPLEX + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); +#else + static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); +#endif + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + return nullptr; + } +}; + +DECLARE_BOARD(CompactWifiBoardLCD); diff --git a/main/boards/bread-compact-wifi-lcd/config.h b/main/boards/bread-compact-wifi-lcd/config.h new file mode 100644 index 0000000..0c7c346 --- /dev/null +++ b/main/boards/bread-compact-wifi-lcd/config.h @@ -0,0 +1,285 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +// 如果使用 Duplex I2S 模式,请注释下面一行 +#define AUDIO_I2S_METHOD_SIMPLEX + +#ifdef AUDIO_I2S_METHOD_SIMPLEX + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#else + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#endif + + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define TOUCH_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42 +#define DISPLAY_MOSI_PIN GPIO_NUM_47 +#define DISPLAY_CLK_PIN GPIO_NUM_21 +#define DISPLAY_DC_PIN GPIO_NUM_40 +#define DISPLAY_RST_PIN GPIO_NUM_45 +#define DISPLAY_CS_PIN GPIO_NUM_41 + + +#ifdef CONFIG_LCD_ST7789_240X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X320_NO_IPS +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_170X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 170 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 35 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_172X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 172 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 34 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X280 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 280 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 20 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X240 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X240_7PIN +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 3 +#endif + +#ifdef CONFIG_LCD_ST7789_240X135 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 135 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 40 +#define DISPLAY_OFFSET_Y 53 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7735_128X160 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 160 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7735_128X128 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 32 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7796_320X480 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 480 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7796_320X480_NO_IPS +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 480 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ILI9341_240X320 +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ILI9341_240X320_NO_IPS +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_GC9A01_240X240 +#define LCD_TYPE_GC9A01_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_CUSTOM +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/bread-compact-wifi/compact_wifi_board.cc b/main/boards/bread-compact-wifi/compact_wifi_board.cc new file mode 100644 index 0000000..27530fa --- /dev/null +++ b/main/boards/bread-compact-wifi/compact_wifi_board.cc @@ -0,0 +1,193 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/oled_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include + +#ifdef SH1106 +#include +#endif + +#define TAG "CompactWifiBoard" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + +class CompactWifiBoard : public WifiBoard { +private: + i2c_master_bus_handle_t display_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + Button touch_button_; + Button volume_up_button_; + Button volume_down_button_; + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + +#ifdef SH1106 + ESP_ERROR_CHECK(esp_lcd_new_panel_sh1106(panel_io_, &panel_config, &panel_)); +#else + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); +#endif + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + // 使用新的打断按键功能:按一次进入listening,再按一次进入idle + app.ToggleListeningState(); + }); + touch_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + touch_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + volume_up_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Lamp")); + } + +public: + CompactWifiBoard() : + boot_button_(BOOT_BUTTON_GPIO), + touch_button_(TOUCH_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { +#ifdef AUDIO_I2S_METHOD_SIMPLEX + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); +#else + static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); +#endif + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(CompactWifiBoard); diff --git a/main/boards/bread-compact-wifi/config.h b/main/boards/bread-compact-wifi/config.h new file mode 100644 index 0000000..f0e2724 --- /dev/null +++ b/main/boards/bread-compact-wifi/config.h @@ -0,0 +1,55 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +// 如果使用 Duplex I2S 模式,请注释下面一行 +#define AUDIO_I2S_METHOD_SIMPLEX + +#ifdef AUDIO_I2S_METHOD_SIMPLEX + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#else + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#endif + + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define TOUCH_BUTTON_GPIO GPIO_NUM_47 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#define DISPLAY_SDA_PIN GPIO_NUM_41 +#define DISPLAY_SCL_PIN GPIO_NUM_42 +#define DISPLAY_WIDTH 128 + +#if CONFIG_OLED_SSD1306_128X32 +#define DISPLAY_HEIGHT 32 +#elif CONFIG_OLED_SSD1306_128X64 +#define DISPLAY_HEIGHT 64 +#elif CONFIG_OLED_SH1106_128X64 +#define DISPLAY_HEIGHT 64 +#define SH1106 +#else +#error "未选择 OLED 屏幕类型" +#endif + +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/bread-compact-wifi/config.json b/main/boards/bread-compact-wifi/config.json new file mode 100644 index 0000000..ea296f9 --- /dev/null +++ b/main/boards/bread-compact-wifi/config.json @@ -0,0 +1,17 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "bread-compact-wifi", + "sdkconfig_append": [ + "CONFIG_OLED_SSD1306_128X32=y" + ] + }, + { + "name": "bread-compact-wifi-128x64", + "sdkconfig_append": [ + "CONFIG_OLED_SSD1306_128X64=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/common/axp2101.cc b/main/boards/common/axp2101.cc new file mode 100644 index 0000000..c040576 --- /dev/null +++ b/main/boards/common/axp2101.cc @@ -0,0 +1,37 @@ +#include "axp2101.h" +#include "board.h" +#include "display.h" + +#include + +#define TAG "Axp2101" + +Axp2101::Axp2101(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { +} + +int Axp2101::GetBatteryCurrentDirection() { + return (ReadReg(0x01) & 0b01100000) >> 5; +} + +bool Axp2101::IsCharging() { + return GetBatteryCurrentDirection() == 1; +} + +bool Axp2101::IsDischarging() { + return GetBatteryCurrentDirection() == 2; +} + +bool Axp2101::IsChargingDone() { + uint8_t value = ReadReg(0x01); + return (value & 0b00000111) == 0b00000100; +} + +int Axp2101::GetBatteryLevel() { + return ReadReg(0xA4); +} + +void Axp2101::PowerOff() { + uint8_t value = ReadReg(0x10); + value = value | 0x01; + WriteReg(0x10, value); +} diff --git a/main/boards/common/axp2101.h b/main/boards/common/axp2101.h new file mode 100644 index 0000000..db9a497 --- /dev/null +++ b/main/boards/common/axp2101.h @@ -0,0 +1,19 @@ +#ifndef __AXP2101_H__ +#define __AXP2101_H__ + +#include "i2c_device.h" + +class Axp2101 : public I2cDevice { +public: + Axp2101(i2c_master_bus_handle_t i2c_bus, uint8_t addr); + bool IsCharging(); + bool IsDischarging(); + bool IsChargingDone(); + int GetBatteryLevel(); + void PowerOff(); + +private: + int GetBatteryCurrentDirection(); +}; + +#endif diff --git a/main/boards/common/backlight.cc b/main/boards/common/backlight.cc new file mode 100644 index 0000000..0d680ef --- /dev/null +++ b/main/boards/common/backlight.cc @@ -0,0 +1,121 @@ +#include "backlight.h" +#include "settings.h" + +#include +#include + +#define TAG "Backlight" + + +Backlight::Backlight() { + // 创建背光渐变定时器 + const esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + auto self = static_cast(arg); + self->OnTransitionTimer(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "backlight_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &transition_timer_)); +} + +Backlight::~Backlight() { + if (transition_timer_ != nullptr) { + esp_timer_stop(transition_timer_); + esp_timer_delete(transition_timer_); + } +} + +void Backlight::RestoreBrightness() { + // Load brightness from settings + Settings settings("display"); + int saved_brightness = settings.GetInt("brightness", 75); + + // 检查亮度值是否为0或过小,设置默认值 + if (saved_brightness <= 0) { + ESP_LOGW(TAG, "Brightness value (%d) is too small, setting to default (10)", saved_brightness); + saved_brightness = 10; // 设置一个较低的默认值 + } + + SetBrightness(saved_brightness); +} + +void Backlight::SetBrightness(uint8_t brightness, bool permanent) { + if (brightness > 100) { + brightness = 100; + } + + if (brightness_ == brightness) { + return; + } + + if (permanent) { + Settings settings("display", true); + settings.SetInt("brightness", brightness); + } + + target_brightness_ = brightness; + step_ = (target_brightness_ > brightness_) ? 1 : -1; + + if (transition_timer_ != nullptr) { + // 启动定时器,每 5ms 更新一次 + esp_timer_start_periodic(transition_timer_, 5 * 1000); + } + ESP_LOGI(TAG, "Set brightness to %d", brightness); +} + +void Backlight::OnTransitionTimer() { + if (brightness_ == target_brightness_) { + esp_timer_stop(transition_timer_); + return; + } + + brightness_ += step_; + SetBrightnessImpl(brightness_); + + if (brightness_ == target_brightness_) { + esp_timer_stop(transition_timer_); + } +} + +PwmBacklight::PwmBacklight(gpio_num_t pin, bool output_invert) : Backlight() { + const ledc_timer_config_t backlight_timer = { + .speed_mode = LEDC_LOW_SPEED_MODE, + .duty_resolution = LEDC_TIMER_10_BIT, + .timer_num = LEDC_TIMER_0, + .freq_hz = 25000, //背光pwm频率需要高一点,防止电感啸叫 + .clk_cfg = LEDC_AUTO_CLK, + .deconfigure = false + }; + ESP_ERROR_CHECK(ledc_timer_config(&backlight_timer)); + + // Setup LEDC peripheral for PWM backlight control + const ledc_channel_config_t backlight_channel = { + .gpio_num = pin, + .speed_mode = LEDC_LOW_SPEED_MODE, + .channel = LEDC_CHANNEL_0, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = LEDC_TIMER_0, + .duty = 0, + .hpoint = 0, + .flags = { + .output_invert = output_invert, + } + }; + ESP_ERROR_CHECK(ledc_channel_config(&backlight_channel)); +} + +PwmBacklight::~PwmBacklight() { + ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); +} + +void PwmBacklight::SetBrightnessImpl(uint8_t brightness) { + // LEDC resolution set to 10bits, thus: 100% = 1023 + uint32_t duty_cycle = (1023 * brightness) / 100; + ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty_cycle); + ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); +} + diff --git a/main/boards/common/backlight.h b/main/boards/common/backlight.h new file mode 100644 index 0000000..4fd2cec --- /dev/null +++ b/main/boards/common/backlight.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include +#include + + +class Backlight { +public: + Backlight(); + ~Backlight(); + + void RestoreBrightness(); + void SetBrightness(uint8_t brightness, bool permanent = false); + inline uint8_t brightness() const { return brightness_; } + +protected: + void OnTransitionTimer(); + virtual void SetBrightnessImpl(uint8_t brightness) = 0; + + esp_timer_handle_t transition_timer_ = nullptr; + uint8_t brightness_ = 0; + uint8_t target_brightness_ = 0; + uint8_t step_ = 1; +}; + + +class PwmBacklight : public Backlight { +public: + PwmBacklight(gpio_num_t pin, bool output_invert = false); + ~PwmBacklight(); + + void SetBrightnessImpl(uint8_t brightness) override; +}; diff --git a/main/boards/common/board.cc b/main/boards/common/board.cc new file mode 100644 index 0000000..87c83b1 --- /dev/null +++ b/main/boards/common/board.cc @@ -0,0 +1,163 @@ +#include "board.h" +#include "system_info.h" +#include "settings.h" +#include "display/display.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include + +#define TAG "Board" + +Board::Board() { + Settings settings("board", true); + uuid_ = settings.GetString("uuid"); + if (uuid_.empty()) { + uuid_ = GenerateUuid(); + settings.SetString("uuid", uuid_); + } + // 只有当BOARD_NAME不包含"moji"时才打印日志 + std::string board_name = BOARD_NAME; + if (board_name.find("moji") == std::string::npos) { + ESP_LOGI(TAG, "UUID=%s SKU=%s", uuid_.c_str(), BOARD_NAME); + } +} + +std::string Board::GenerateUuid() { + // UUID v4 需要 16 字节的随机数据 + uint8_t uuid[16]; + + // 使用 ESP32 的硬件随机数生成器 + esp_fill_random(uuid, sizeof(uuid)); + + // 设置版本 (版本 4) 和变体位 + uuid[6] = (uuid[6] & 0x0F) | 0x40; // 版本 4 + uuid[8] = (uuid[8] & 0x3F) | 0x80; // 变体 1 + + // 将字节转换为标准的 UUID 字符串格式 + char uuid_str[37]; + snprintf(uuid_str, sizeof(uuid_str), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + uuid[0], uuid[1], uuid[2], uuid[3], + uuid[4], uuid[5], uuid[6], uuid[7], + uuid[8], uuid[9], uuid[10], uuid[11], + uuid[12], uuid[13], uuid[14], uuid[15]); + + return std::string(uuid_str); +} + +bool Board::GetBatteryLevel(int &level, bool& charging, bool& discharging) { + return false; +} + +Display* Board::GetDisplay() { + static Display display; + return &display; +} + +Led* Board::GetLed() { + static NoLed led; + return &led; +} + +std::string Board::GetJson() { + /* + { + "version": 2, + "flash_size": 4194304, + "psram_size": 0, + "minimum_free_heap_size": 123456, + "mac_address": "00:00:00:00:00:00", + "uuid": "00000000-0000-0000-0000-000000000000", + "chip_model_name": "esp32s3", + "chip_info": { + "model": 1, + "cores": 2, + "revision": 0, + "features": 0 + }, + "application": { + "name": "my-app", + "version": "1.0.0", + "compile_time": "2021-01-01T00:00:00Z" + "idf_version": "4.2-dev" + "elf_sha256": "" + }, + "partition_table": [ + "app": { + "label": "app", + "type": 1, + "subtype": 2, + "address": 0x10000, + "size": 0x100000 + } + ], + "ota": { + "label": "ota_0" + }, + "board": { + ... + } + } + */ + std::string json = "{"; + json += "\"version\":2,"; + json += "\"language\":\"" + std::string(Lang::CODE) + "\","; + json += "\"flash_size\":" + std::to_string(SystemInfo::GetFlashSize()) + ","; + json += "\"minimum_free_heap_size\":" + std::to_string(SystemInfo::GetMinimumFreeHeapSize()) + ","; + json += "\"mac_address\":\"" + SystemInfo::GetMacAddress() + "\","; + json += "\"uuid\":\"" + uuid_ + "\","; + json += "\"chip_model_name\":\"" + SystemInfo::GetChipModelName() + "\","; + json += "\"chip_info\":{"; + + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + json += "\"model\":" + std::to_string(chip_info.model) + ","; + json += "\"cores\":" + std::to_string(chip_info.cores) + ","; + json += "\"revision\":" + std::to_string(chip_info.revision) + ","; + json += "\"features\":" + std::to_string(chip_info.features); + json += "},"; + + json += "\"application\":{"; + auto app_desc = esp_app_get_description(); + json += "\"name\":\"" + std::string(app_desc->project_name) + "\","; + json += "\"version\":\"" + std::string(app_desc->version) + "\","; + json += "\"compile_time\":\"" + std::string(app_desc->date) + "T" + std::string(app_desc->time) + "Z\","; + json += "\"idf_version\":\"" + std::string(app_desc->idf_ver) + "\","; + + char sha256_str[65]; + for (int i = 0; i < 32; i++) { + snprintf(sha256_str + i * 2, sizeof(sha256_str) - i * 2, "%02x", app_desc->app_elf_sha256[i]); + } + json += "\"elf_sha256\":\"" + std::string(sha256_str) + "\""; + json += "},"; + + json += "\"partition_table\": ["; + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + while (it) { + const esp_partition_t *partition = esp_partition_get(it); + json += "{"; + json += "\"label\":\"" + std::string(partition->label) + "\","; + json += "\"type\":" + std::to_string(partition->type) + ","; + json += "\"subtype\":" + std::to_string(partition->subtype) + ","; + json += "\"address\":" + std::to_string(partition->address) + ","; + json += "\"size\":" + std::to_string(partition->size); + json += "},"; + it = esp_partition_next(it); + } + json.pop_back(); // Remove the last comma + json += "],"; + + json += "\"ota\":{"; + auto ota_partition = esp_ota_get_running_partition(); + json += "\"label\":\"" + std::string(ota_partition->label) + "\""; + json += "},"; + + json += "\"board\":" + GetBoardJson(); + + // Close the JSON object + json += "}"; + return json; +} \ No newline at end of file diff --git a/main/boards/common/board.h b/main/boards/common/board.h new file mode 100644 index 0000000..7486fd7 --- /dev/null +++ b/main/boards/common/board.h @@ -0,0 +1,60 @@ +#ifndef BOARD_H +#define BOARD_H + +#include +#include +#include +#include +#include + +#include "led/led.h" +#include "backlight.h" + +void* create_board(); +class AudioCodec; +class Display; +class Board { +private: + Board(const Board&) = delete; // 禁用拷贝构造函数 + Board& operator=(const Board&) = delete; // 禁用赋值操作 + virtual std::string GetBoardJson() = 0; + +protected: + Board(); + std::string GenerateUuid(); + + // 软件生成的设备唯一标识 + std::string uuid_; + +public: + static Board& GetInstance() { + static Board* instance = static_cast(create_board()); + return *instance; + } + + virtual ~Board() = default; + virtual std::string GetBoardType() = 0; + virtual std::string GetUuid() { return uuid_; } + virtual Backlight* GetBacklight() { return nullptr; } + virtual Led* GetLed(); + virtual AudioCodec* GetAudioCodec() = 0; + virtual Display* GetDisplay(); + virtual Http* CreateHttp() = 0; + virtual WebSocket* CreateWebSocket() = 0; + virtual Mqtt* CreateMqtt() = 0; + virtual Udp* CreateUdp() = 0; + virtual void StartNetwork() = 0; + virtual const char* GetNetworkStateIcon() = 0; + virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging); + virtual std::string GetJson(); + virtual void SetPowerSaveMode(bool enabled) = 0; + virtual void WakeUp() = 0; + virtual void OnBeforeRestart() {} // 重启前回调,子类可重写执行上报等操作 +}; + +#define DECLARE_BOARD(BOARD_CLASS_NAME) \ +void* create_board() { \ + return new BOARD_CLASS_NAME(); \ +} + +#endif // BOARD_H diff --git a/main/boards/common/button.cc b/main/boards/common/button.cc new file mode 100644 index 0000000..c04dfba --- /dev/null +++ b/main/boards/common/button.cc @@ -0,0 +1,112 @@ +#include "button.h" + +#include + +static const char* TAG = "Button"; +#if CONFIG_SOC_ADC_SUPPORTED +Button::Button(const button_adc_config_t& adc_cfg) { + button_config_t button_config = { + .type = BUTTON_TYPE_ADC, + // .long_press_time = 1000, // 原有长按3秒时的时间 + .long_press_time = 5000, // 长按5秒时间 + .short_press_time = 50, + .adc_button_config = adc_cfg + }; + button_handle_ = iot_button_create(&button_config); + if (button_handle_ == NULL) { + ESP_LOGE(TAG, "Failed to create button handle"); + return; + } +} +#endif + +Button::Button(gpio_num_t gpio_num, bool active_high, uint32_t long_press_ms) : gpio_num_(gpio_num) { + if (gpio_num == GPIO_NUM_NC) { + return; + } + button_config_t button_config = { + .type = BUTTON_TYPE_GPIO, + .long_press_time = (uint16_t)long_press_ms, + .short_press_time = 0, // 0=使用默认值180ms(双击判定窗口) + .gpio_button_config = { + .gpio_num = gpio_num, + .active_level = static_cast(active_high ? 1 : 0) + } + }; + button_handle_ = iot_button_create(&button_config); + if (button_handle_ == NULL) { + ESP_LOGE(TAG, "Failed to create button handle"); + return; + } +} + +Button::~Button() { + if (button_handle_ != NULL) { + iot_button_delete(button_handle_); + } +} + +void Button::OnPressDown(std::function callback) { + if (button_handle_ == nullptr) { + return; + } + on_press_down_ = callback; + iot_button_register_cb(button_handle_, BUTTON_PRESS_DOWN, [](void* handle, void* usr_data) { + Button* button = static_cast(usr_data); + if (button->on_press_down_) { + button->on_press_down_(); + } + }, this); +} + +void Button::OnPressUp(std::function callback) { + if (button_handle_ == nullptr) { + return; + } + on_press_up_ = callback; + iot_button_register_cb(button_handle_, BUTTON_PRESS_UP, [](void* handle, void* usr_data) { + Button* button = static_cast(usr_data); + if (button->on_press_up_) { + button->on_press_up_(); + } + }, this); +} + +void Button::OnLongPress(std::function callback) { + if (button_handle_ == nullptr) { + return; + } + on_long_press_ = callback; + iot_button_register_cb(button_handle_, BUTTON_LONG_PRESS_START, [](void* handle, void* usr_data) { + Button* button = static_cast(usr_data); + if (button->on_long_press_) { + button->on_long_press_(); + } + }, this); +} + +void Button::OnClick(std::function callback) { + if (button_handle_ == nullptr) { + return; + } + on_click_ = callback; + iot_button_register_cb(button_handle_, BUTTON_SINGLE_CLICK, [](void* handle, void* usr_data) { + Button* button = static_cast(usr_data); + if (button->on_click_) { + button->on_click_(); + } + }, this); +} + +void Button::OnDoubleClick(std::function callback) { + if (button_handle_ == nullptr) { + return; + } + on_double_click_ = callback; + iot_button_register_cb(button_handle_, BUTTON_DOUBLE_CLICK, [](void* handle, void* usr_data) { + Button* button = static_cast(usr_data); + if (button->on_double_click_) { + button->on_double_click_(); + } + }, this); +} diff --git a/main/boards/common/button.h b/main/boards/common/button.h new file mode 100644 index 0000000..b6fb9b1 --- /dev/null +++ b/main/boards/common/button.h @@ -0,0 +1,33 @@ +#ifndef BUTTON_H_ +#define BUTTON_H_ + +#include +#include +#include + +class Button { +public: +#if CONFIG_SOC_ADC_SUPPORTED + Button(const button_adc_config_t& cfg); +#endif + Button(gpio_num_t gpio_num, bool active_high = false, uint32_t long_press_ms = 5000); + ~Button(); + + void OnPressDown(std::function callback); + void OnPressUp(std::function callback); + void OnLongPress(std::function callback); + void OnClick(std::function callback); + void OnDoubleClick(std::function callback); +private: + gpio_num_t gpio_num_; + button_handle_t button_handle_ = nullptr; + + + std::function on_press_down_; + std::function on_press_up_; + std::function on_long_press_; + std::function on_click_; + std::function on_double_click_; +}; + +#endif // BUTTON_H_ diff --git a/main/boards/common/i2c_device.cc b/main/boards/common/i2c_device.cc new file mode 100644 index 0000000..d55e123 --- /dev/null +++ b/main/boards/common/i2c_device.cc @@ -0,0 +1,57 @@ +#include "i2c_device.h" + +#include +#include + +#define TAG "I2cDevice" + + +I2cDevice::I2cDevice(i2c_master_bus_handle_t i2c_bus, uint8_t addr) { + i2c_device_config_t i2c_device_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = 400 * 1000, + .scl_wait_us = 0, + .flags = { + .disable_ack_check = 0, + }, + }; + ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &i2c_device_cfg, &i2c_device_)); + assert(i2c_device_ != NULL); +} + +void I2cDevice::WriteReg(uint8_t reg, uint8_t value) { + uint8_t buffer[2] = {reg, value}; + esp_err_t ret = i2c_master_transmit(i2c_device_, buffer, 2, 100); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to write register 0x%02X with value 0x%02X: %s", reg, value, esp_err_to_name(ret)); + } +} + +esp_err_t I2cDevice::WriteRegWithError(uint8_t reg, uint8_t value) { + uint8_t buffer[2] = {reg, value}; + esp_err_t ret = i2c_master_transmit(i2c_device_, buffer, 2, 100); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to write register 0x%02X with value 0x%02X: %s", reg, value, esp_err_to_name(ret)); + } + return ret; +} + +uint8_t I2cDevice::ReadReg(uint8_t reg) { + uint8_t buffer[1]; + esp_err_t ret = i2c_master_transmit_receive(i2c_device_, ®, 1, buffer, 1, 100); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to read register 0x%02X: %s", reg, esp_err_to_name(ret)); + return 0xFF; // 返回错误值 + } + return buffer[0]; +} + +void I2cDevice::ReadRegs(uint8_t reg, uint8_t* buffer, size_t length) { + esp_err_t ret = i2c_master_transmit_receive(i2c_device_, ®, 1, buffer, length, 100); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to read %zu bytes from register 0x%02X: %s", length, reg, esp_err_to_name(ret)); + // 清零缓冲区以避免使用未初始化的数据 + memset(buffer, 0, length); + } +} \ No newline at end of file diff --git a/main/boards/common/i2c_device.h b/main/boards/common/i2c_device.h new file mode 100644 index 0000000..7fc3ae1 --- /dev/null +++ b/main/boards/common/i2c_device.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +class I2cDevice { + public: + I2cDevice(i2c_master_bus_handle_t i2c_bus, uint8_t addr); + + protected: + i2c_master_dev_handle_t i2c_device_; + + void WriteReg(uint8_t reg, uint8_t value); // 保持原有接口不变 + esp_err_t WriteRegWithError(uint8_t reg, uint8_t value); // 新增带错误返回的接口 + uint8_t ReadReg(uint8_t reg); + void ReadRegs(uint8_t reg, uint8_t* buffer, size_t length); +}; diff --git a/main/boards/common/knob.cc b/main/boards/common/knob.cc new file mode 100644 index 0000000..350fda2 --- /dev/null +++ b/main/boards/common/knob.cc @@ -0,0 +1,52 @@ +#include "knob.h" + +static const char* TAG = "Knob"; + +Knob::Knob(gpio_num_t pin_a, gpio_num_t pin_b) { + knob_config_t config = { + .default_direction = 0, + .gpio_encoder_a = static_cast(pin_a), + .gpio_encoder_b = static_cast(pin_b), + }; + + esp_err_t err = ESP_OK; + knob_handle_ = iot_knob_create(&config); + if (knob_handle_ == NULL) { + ESP_LOGE(TAG, "Failed to create knob instance"); + return; + } + + err = iot_knob_register_cb(knob_handle_, KNOB_LEFT, knob_callback, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register left callback: %s", esp_err_to_name(err)); + return; + } + + err = iot_knob_register_cb(knob_handle_, KNOB_RIGHT, knob_callback, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register right callback: %s", esp_err_to_name(err)); + return; + } + + ESP_LOGI(TAG, "Knob initialized with pins A:%d B:%d", pin_a, pin_b); +} + +Knob::~Knob() { + if (knob_handle_ != NULL) { + iot_knob_delete(knob_handle_); + knob_handle_ = NULL; + } +} + +void Knob::OnRotate(std::function callback) { + on_rotate_ = callback; +} + +void Knob::knob_callback(void* arg, void* data) { + Knob* knob = static_cast(data); + knob_event_t event = iot_knob_get_event(arg); + + if (knob->on_rotate_) { + knob->on_rotate_(event == KNOB_RIGHT); + } +} \ No newline at end of file diff --git a/main/boards/common/knob.h b/main/boards/common/knob.h new file mode 100644 index 0000000..efea5f5 --- /dev/null +++ b/main/boards/common/knob.h @@ -0,0 +1,25 @@ +#ifndef KNOB_H_ +#define KNOB_H_ + +#include +#include +#include +#include + +class Knob { +public: + Knob(gpio_num_t pin_a, gpio_num_t pin_b); + ~Knob(); + + void OnRotate(std::function callback); + +private: + static void knob_callback(void* arg, void* data); + + knob_handle_t knob_handle_; + gpio_num_t pin_a_; + gpio_num_t pin_b_; + std::function on_rotate_; +}; + +#endif // KNOB_H_ \ No newline at end of file diff --git a/main/boards/common/ml307_board.cc b/main/boards/common/ml307_board.cc new file mode 100644 index 0000000..62fec9f --- /dev/null +++ b/main/boards/common/ml307_board.cc @@ -0,0 +1,122 @@ +#include "ml307_board.h" + +#include "application.h" +#include "display.h" +#include "font_awesome_symbols.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "Ml307Board"; + +Ml307Board::Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, size_t rx_buffer_size) : modem_(tx_pin, rx_pin, rx_buffer_size) { +} + +std::string Ml307Board::GetBoardType() { + return "ml307"; +} + +void Ml307Board::StartNetwork() { + auto display = Board::GetInstance().GetDisplay(); + display->SetStatus(Lang::Strings::DETECTING_MODULE); + modem_.SetDebug(false); + modem_.SetBaudRate(921600); + + auto& application = Application::GetInstance(); + // If low power, the material ready event will be triggered by the modem because of a reset + modem_.OnMaterialReady([this, &application]() { + ESP_LOGI(TAG, "ML307 material ready"); + application.Schedule([this, &application]() { + application.SetDeviceState(kDeviceStateIdle); + WaitForNetworkReady(); + }); + }); + + WaitForNetworkReady(); +} + +void Ml307Board::WaitForNetworkReady() { + auto& application = Application::GetInstance(); + auto display = Board::GetInstance().GetDisplay(); + display->SetStatus(Lang::Strings::REGISTERING_NETWORK); + int result = modem_.WaitForNetworkReady(); + if (result == -1) { + application.Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "sad", Lang::Sounds::P3_ERR_PIN); + return; + } else if (result == -2) { + application.Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "sad", Lang::Sounds::P3_ERR_REG); + return; + } + + // Print the ML307 modem information + std::string module_name = modem_.GetModuleName(); + std::string imei = modem_.GetImei(); + std::string iccid = modem_.GetIccid(); + ESP_LOGI(TAG, "ML307 Module: %s", module_name.c_str()); + ESP_LOGI(TAG, "ML307 IMEI: %s", imei.c_str()); + ESP_LOGI(TAG, "ML307 ICCID: %s", iccid.c_str()); + + // Close all previous connections + modem_.ResetConnections(); +} + +Http* Ml307Board::CreateHttp() { + return new Ml307Http(modem_); +} + +WebSocket* Ml307Board::CreateWebSocket() { + return new WebSocket(new Ml307SslTransport(modem_, 0)); +} + +Mqtt* Ml307Board::CreateMqtt() { + return new Ml307Mqtt(modem_, 0); +} + +Udp* Ml307Board::CreateUdp() { + return new Ml307Udp(modem_, 0); +} + +const char* Ml307Board::GetNetworkStateIcon() { + if (!modem_.network_ready()) { + return FONT_AWESOME_SIGNAL_OFF; + } + int csq = modem_.GetCsq(); + if (csq == -1) { + return FONT_AWESOME_SIGNAL_OFF; + } else if (csq >= 0 && csq <= 14) { + return FONT_AWESOME_SIGNAL_1; + } else if (csq >= 15 && csq <= 19) { + return FONT_AWESOME_SIGNAL_2; + } else if (csq >= 20 && csq <= 24) { + return FONT_AWESOME_SIGNAL_3; + } else if (csq >= 25 && csq <= 31) { + return FONT_AWESOME_SIGNAL_4; + } + + ESP_LOGW(TAG, "Invalid CSQ: %d", csq); + return FONT_AWESOME_SIGNAL_OFF; +} + +std::string Ml307Board::GetBoardJson() { + // Set the board type for OTA + std::string board_json = std::string("{\"type\":\"" BOARD_TYPE "\","); + board_json += "\"name\":\"" BOARD_NAME "\","; + board_json += "\"role\":\"" CONFIG_DEVICE_ROLE "\","; + board_json += "\"revision\":\"" + modem_.GetModuleName() + "\","; + board_json += "\"carrier\":\"" + modem_.GetCarrierName() + "\","; + board_json += "\"csq\":\"" + std::to_string(modem_.GetCsq()) + "\","; + board_json += "\"imei\":\"" + modem_.GetImei() + "\","; + board_json += "\"iccid\":\"" + modem_.GetIccid() + "\"}"; + return board_json; +} + +void Ml307Board::SetPowerSaveMode(bool enabled) { + // TODO: Implement power save mode for ML307 +} diff --git a/main/boards/common/ml307_board.h b/main/boards/common/ml307_board.h new file mode 100644 index 0000000..effacce --- /dev/null +++ b/main/boards/common/ml307_board.h @@ -0,0 +1,26 @@ +#ifndef ML307_BOARD_H +#define ML307_BOARD_H + +#include "board.h" +#include + +class Ml307Board : public Board { +protected: + Ml307AtModem modem_; + + virtual std::string GetBoardJson() override; + void WaitForNetworkReady(); + +public: + Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, size_t rx_buffer_size = 4096); + virtual std::string GetBoardType() override; + virtual void StartNetwork() override; + virtual Http* CreateHttp() override; + virtual WebSocket* CreateWebSocket() override; + virtual Mqtt* CreateMqtt() override; + virtual Udp* CreateUdp() override; + virtual const char* GetNetworkStateIcon() override; + virtual void SetPowerSaveMode(bool enabled) override; +}; + +#endif // ML307_BOARD_H diff --git a/main/boards/common/power_save_timer.cc b/main/boards/common/power_save_timer.cc new file mode 100644 index 0000000..378827e --- /dev/null +++ b/main/boards/common/power_save_timer.cc @@ -0,0 +1,103 @@ +#include "power_save_timer.h" +#include "application.h" + +#include + +#define TAG "PowerSaveTimer" + + +PowerSaveTimer::PowerSaveTimer(int cpu_max_freq, int seconds_to_sleep, int seconds_to_shutdown) + : cpu_max_freq_(cpu_max_freq), seconds_to_sleep_(seconds_to_sleep), seconds_to_shutdown_(seconds_to_shutdown) { + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + auto self = static_cast(arg); + self->PowerSaveCheck(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "power_save_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &power_save_timer_)); +} + +PowerSaveTimer::~PowerSaveTimer() { + esp_timer_stop(power_save_timer_); + esp_timer_delete(power_save_timer_); +} + +void PowerSaveTimer::SetEnabled(bool enabled) { + if (enabled && !enabled_) { + ticks_ = 0; + enabled_ = enabled; + ESP_ERROR_CHECK(esp_timer_start_periodic(power_save_timer_, 1000000)); + ESP_LOGI(TAG, "Power save timer enabled"); + } else if (!enabled && enabled_) { + ESP_ERROR_CHECK(esp_timer_stop(power_save_timer_)); + enabled_ = enabled; + WakeUp(); + ESP_LOGI(TAG, "Power save timer disabled"); + } +} + +void PowerSaveTimer::OnEnterSleepMode(std::function callback) { + on_enter_sleep_mode_ = callback; +} + +void PowerSaveTimer::OnExitSleepMode(std::function callback) { + on_exit_sleep_mode_ = callback; +} + +void PowerSaveTimer::OnShutdownRequest(std::function callback) { + on_shutdown_request_ = callback; +} + +void PowerSaveTimer::PowerSaveCheck() { + auto& app = Application::GetInstance(); + if (!in_sleep_mode_ && !app.CanEnterSleepMode()) { + ticks_ = 0; + return; + } + + ticks_++; + if (seconds_to_sleep_ != -1 && ticks_ >= seconds_to_sleep_) { + if (!in_sleep_mode_) { + in_sleep_mode_ = true; + if (on_enter_sleep_mode_) { + on_enter_sleep_mode_(); + } + + if (cpu_max_freq_ != -1) { + esp_pm_config_t pm_config = { + .max_freq_mhz = cpu_max_freq_, + .min_freq_mhz = 40, + .light_sleep_enable = true, + }; + esp_pm_configure(&pm_config); + } + } + } + if (seconds_to_shutdown_ != -1 && ticks_ >= seconds_to_shutdown_ && on_shutdown_request_) { + on_shutdown_request_(); + } +} + +void PowerSaveTimer::WakeUp() { + ticks_ = 0; + if (in_sleep_mode_) { + in_sleep_mode_ = false; + + if (cpu_max_freq_ != -1) { + esp_pm_config_t pm_config = { + .max_freq_mhz = cpu_max_freq_, + .min_freq_mhz = cpu_max_freq_, + .light_sleep_enable = false, + }; + esp_pm_configure(&pm_config); + } + + if (on_exit_sleep_mode_) { + on_exit_sleep_mode_(); + } + } +} diff --git a/main/boards/common/power_save_timer.h b/main/boards/common/power_save_timer.h new file mode 100644 index 0000000..1b527f2 --- /dev/null +++ b/main/boards/common/power_save_timer.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#include + +class PowerSaveTimer { +public: + PowerSaveTimer(int cpu_max_freq, int seconds_to_sleep = 20, int seconds_to_shutdown = -1); + ~PowerSaveTimer(); + + void SetEnabled(bool enabled); + void OnEnterSleepMode(std::function callback); + void OnExitSleepMode(std::function callback); + void OnShutdownRequest(std::function callback); + void WakeUp(); + +private: + void PowerSaveCheck(); + + esp_timer_handle_t power_save_timer_ = nullptr; + bool enabled_ = false; + bool in_sleep_mode_ = false; + int ticks_ = 0; + int cpu_max_freq_; + int seconds_to_sleep_; + int seconds_to_shutdown_; + + std::function on_enter_sleep_mode_; + std::function on_exit_sleep_mode_; + std::function on_shutdown_request_; +}; diff --git a/main/boards/common/qmi8658a.cc b/main/boards/common/qmi8658a.cc new file mode 100644 index 0000000..077e1f9 --- /dev/null +++ b/main/boards/common/qmi8658a.cc @@ -0,0 +1,1503 @@ +#include "qmi8658a.h" +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "QMI8658A" + +QMI8658A::QMI8658A(i2c_master_bus_handle_t i2c_bus, uint8_t addr) + : I2cDevice(i2c_bus, addr), + state_(QMI8658A_STATE_UNINITIALIZED), + last_error_(QMI8658A_OK), + acc_scale_(1.0f), + gyro_scale_(1.0f), + is_calibrating_(false), + calibration_start_time_(0), + calibration_duration_(0), + calibration_sample_count_(0), + buffer_task_handle_(nullptr), + buffer_enabled_(false), + buffer_interval_ms_(0), + interrupt_pin_(GPIO_NUM_NC), + interrupt_type_(QMI8658A_INT_DISABLE), + interrupt_enabled_(false), + fifo_enabled_(false) { + // 默认配置 - 修正ODR设置以匹配实际寄存器值 + config_.mode = QMI8658A_MODE_DUAL; + config_.acc_range = QMI8658A_ACC_RANGE_4G; // 匹配CTRL2寄存器值0x16 + config_.gyro_range = QMI8658A_GYRO_RANGE_512DPS; // 匹配CTRL3寄存器值0x56 + config_.acc_odr = QMI8658A_ODR_125HZ; // 匹配实际寄存器值0x06 + config_.gyro_odr = QMI8658A_ODR_125HZ; // 匹配实际寄存器值0x06 + + // 初始化缓冲区结构 + memset(&data_buffer_, 0, sizeof(data_buffer_)); + data_buffer_.mutex = nullptr; + + // 初始化校准数据 + memset(&calibration_, 0, sizeof(calibration_)); + memset(calibration_acc_sum_, 0, sizeof(calibration_acc_sum_)); + memset(calibration_gyro_sum_, 0, sizeof(calibration_gyro_sum_)); + + // 初始化FIFO配置 + memset(&fifo_config_, 0, sizeof(fifo_config_)); +} + +qmi8658a_error_t QMI8658A::Initialize(const qmi8658a_config_t* config) { + ESP_LOGI(TAG, "Initializing QMI8658A sensor..."); + + state_ = QMI8658A_STATE_INITIALIZING; + + // 执行自检 + qmi8658a_error_t result = PerformSelfTest(); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Self-test failed during initialization"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 软件复位 + result = SoftReset(); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Software reset failed"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 保存配置 + if (config) { + config_ = *config; + } + + // 计算比例因子 + CalculateScaleFactors(); + + // 配置加速度计 - 使用更保守的设置 + result = SetAccelConfig(QMI8658A_ACC_RANGE_4G, QMI8658A_ODR_125HZ); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to configure accelerometer"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 配置陀螺仪 - 使用更保守的设置 + result = SetGyroConfig(QMI8658A_GYRO_RANGE_256DPS, QMI8658A_ODR_125HZ); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to configure gyroscope"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 配置CTRL1 - 使能数据就绪/中断相关功能以确保STATUS0有效 + result = WriteRegWithVerification(QMI8658A_CTRL1, 0x60); + if (result != QMI8658A_OK) { + ESP_LOGW(TAG, "Failed to configure CTRL1 register"); + } else { + VerifyRegisterValue(QMI8658A_CTRL1, 0x60, "CTRL1"); + } + + uint8_t mode_val = config_.mode; + result = SetMode(static_cast(mode_val)); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to set mode"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 添加额外的寄存器配置 + // 配置CTRL5寄存器 - 设置加速度计和陀螺仪的带宽滤波 + result = WriteRegWithVerification(QMI8658A_CTRL5, 0x35); + if (result != QMI8658A_OK) { + ESP_LOGW(TAG, "Failed to configure CTRL5 register"); + } + else { + VerifyRegisterValue(QMI8658A_CTRL5, 0x35, "CTRL5"); + } + + // 配置CTRL6寄存器 - 设置陀螺仪的LPF + result = WriteRegWithVerification(QMI8658A_CTRL6, 0x00); + if (result != QMI8658A_OK) { + ESP_LOGW(TAG, "Failed to configure CTRL6 register"); + } + + // 等待传感器稳定 + vTaskDelay(pdMS_TO_TICKS(100)); // 增加等待时间到100ms + + // 验证关键寄存器配置 + uint8_t expected_ctrl2 = (config_.acc_range << 4) | config_.acc_odr; + result = VerifyRegisterValue(QMI8658A_CTRL2, expected_ctrl2, "CTRL2"); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "CTRL2 verification failed"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + uint8_t expected_ctrl3 = (config_.gyro_range << 4) | config_.gyro_odr; + result = VerifyRegisterValue(QMI8658A_CTRL3, expected_ctrl3, "CTRL3 (Gyro Config)"); + if (result != QMI8658A_OK) { + state_ = QMI8658A_STATE_ERROR; + return result; + } + + uint8_t expected_ctrl7 = (mode_val == QMI8658A_MODE_DISABLE) ? 0x00 : (uint8_t)(0x80 | mode_val); + result = VerifyRegisterValue(QMI8658A_CTRL7, expected_ctrl7, "CTRL7 (Mode)"); + if (result != QMI8658A_OK) { + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 诊断寄存器状态 + ESP_LOGI(TAG, "=== QMI8658A Register Diagnostics ==="); + uint8_t ctrl1 = ReadReg(QMI8658A_CTRL1); + uint8_t ctrl2 = ReadReg(QMI8658A_CTRL2); + uint8_t ctrl3 = ReadReg(QMI8658A_CTRL3); + uint8_t ctrl5 = ReadReg(QMI8658A_CTRL5); + uint8_t ctrl6 = ReadReg(QMI8658A_CTRL6); + uint8_t ctrl7 = ReadReg(QMI8658A_CTRL7); + uint8_t status0 = ReadReg(QMI8658A_STATUS0); + uint8_t status1 = ReadReg(QMI8658A_STATUS1); + + ESP_LOGI(TAG, "CTRL1: 0x%02X, CTRL2: 0x%02X, CTRL3: 0x%02X, CTRL5: 0x%02X, CTRL6: 0x%02X, CTRL7: 0x%02X", + ctrl1, ctrl2, ctrl3, ctrl5, ctrl6, ctrl7); + ESP_LOGI(TAG, "STATUS0: 0x%02X, STATUS1: 0x%02X", status0, status1); + ESP_LOGI(TAG, "====================================="); + + // 等待数据就绪 - 只进行一次等待 + if (config_.mode != QMI8658A_MODE_DISABLE) { + ESP_LOGI(TAG, "Waiting for sensor data to be ready..."); + uint32_t wait_count = 0; + const uint32_t max_wait = 100; // 最多等待1秒 + + while (wait_count < max_wait) { + uint8_t status = ReadReg(QMI8658A_STATUS0); + if (status & 0x03) { // 检查加速度计或陀螺仪数据就绪 + ESP_LOGI(TAG, "Data ready after %lu ms", wait_count * 10); + break; + } + vTaskDelay(pdMS_TO_TICKS(10)); + wait_count++; + } + + if (wait_count >= max_wait) { + ESP_LOGW(TAG, "Data ready timeout, but initialization completed"); + } + } + + state_ = QMI8658A_STATE_READY; + UpdateScaleFactors(); + ESP_LOGI(TAG, "QMI8658A initialization completed successfully"); + DumpRegisters(); + + return QMI8658A_OK; +} + +uint8_t QMI8658A::GetChipId() { + uint8_t chip_id = ReadReg(QMI8658A_WHO_AM_I); + if (chip_id == 0xFF) { + ESP_LOGE(TAG, "Failed to read chip ID register, I2C communication error"); + return 0xFF; + } + return chip_id; +} + +uint8_t QMI8658A::GetRevisionId() { + return ReadReg(QMI8658A_REVISION_ID); +} + +// 静态连接检测方法(用于生产测试) +bool QMI8658A::CheckConnection(i2c_master_bus_handle_t i2c_bus, uint8_t* detected_address) { + // 可能的QMI8658A I2C地址 + uint8_t possible_addresses[] = {0x6A, 0x6B}; + uint8_t num_addresses = sizeof(possible_addresses) / sizeof(possible_addresses[0]); + + for (uint8_t i = 0; i < num_addresses; i++) { + uint8_t addr = possible_addresses[i]; + + // 创建临时I2C设备句柄进行测试 + i2c_master_dev_handle_t dev_handle; + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = 400000, // 400kHz + }; + + esp_err_t ret = i2c_master_bus_add_device(i2c_bus, &dev_cfg, &dev_handle); + if (ret != ESP_OK) { + continue; // 尝试下一个地址 + } + + // 尝试读取WHO_AM_I寄存器 + uint8_t reg_addr = QMI8658A_WHO_AM_I; + uint8_t chip_id = 0; + + ret = i2c_master_transmit_receive(dev_handle, ®_addr, 1, &chip_id, 1, 1000); + + // 清理设备句柄 + i2c_master_bus_rm_device(dev_handle); + + if (ret == ESP_OK && chip_id == QMI8658A_CHIP_ID) { + // 找到有效的QMI8658A设备 + if (detected_address != nullptr) { + *detected_address = addr; + } + return true; + } + } + + return false; // 未找到有效设备 +} + +qmi8658a_error_t QMI8658A::SoftReset() { + ESP_LOGI(TAG, "Performing soft reset..."); + + qmi8658a_error_t result = WriteRegWithVerification(QMI8658A_RESET, 0xB0); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to perform soft reset"); + return result; + } + + // 等待复位完成 - 大幅增加等待时间以确保传感器完全稳定 + vTaskDelay(pdMS_TO_TICKS(200)); // 增加到200ms等待时间 + + // 验证复位是否成功 - 检查芯片ID + uint8_t chip_id = GetChipId(); + if (chip_id != QMI8658A_CHIP_ID) { + ESP_LOGE(TAG, "Soft reset verification failed: chip ID = 0x%02X", chip_id); + return SetError(QMI8658A_ERROR_INIT_FAILED); + } + + ESP_LOGI(TAG, "Soft reset completed and verified successfully"); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::SetMode(qmi8658a_mode_t mode) { + ESP_LOGI(TAG, "Setting mode: %d", mode); + + uint8_t value = 0x00; + switch (mode) { + case QMI8658A_MODE_DISABLE: value = 0x00; break; + case QMI8658A_MODE_ACC_ONLY: value = 0x81; break; + case QMI8658A_MODE_GYRO_ONLY: value = 0x82; break; + case QMI8658A_MODE_DUAL: value = 0x83; break; + } + qmi8658a_error_t result = WriteRegWithVerification(QMI8658A_CTRL7, value, 3); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to set mode: %d", mode); + return result; + } + + // 更新配置 + config_.mode = mode; + + ESP_LOGI(TAG, "Mode set successfully: CTRL7=0x%02X", value); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::SetAccelConfig(qmi8658a_acc_range_t range, qmi8658a_odr_t odr) { + ESP_LOGI(TAG, "Setting accelerometer config: range=%d, odr=%d", range, odr); + + uint8_t ctrl2_val = (range << 4) | odr; + + // 使用带验证的写入函数,最多重试3次 + qmi8658a_error_t result = WriteRegWithVerification(QMI8658A_CTRL2, ctrl2_val, 3); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to set accelerometer config: range=%d, odr=%d", range, odr); + return result; + } + + // 更新配置 + config_.acc_range = range; + config_.acc_odr = odr; + UpdateScaleFactors(); + + ESP_LOGI(TAG, "Accelerometer config set successfully: CTRL2=0x%02X", ctrl2_val); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::SetGyroConfig(qmi8658a_gyro_range_t range, qmi8658a_odr_t odr) { + ESP_LOGI(TAG, "Setting gyroscope config: range=%d, odr=%d", range, odr); + + uint8_t ctrl3_val = (range << 4) | odr; + + // 使用带验证的写入函数,最多重试3次 + qmi8658a_error_t result = WriteRegWithVerification(QMI8658A_CTRL3, ctrl3_val, 3); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to set gyroscope config: range=%d, odr=%d", range, odr); + return result; + } + + // 更新配置 + config_.gyro_range = range; + config_.gyro_odr = odr; + UpdateScaleFactors(); + + ESP_LOGI(TAG, "Gyroscope config set successfully: CTRL3=0x%02X", ctrl3_val); + return QMI8658A_OK; +} + +void QMI8658A::CalculateScaleFactors() { + // 计算加速度计比例因子 (g) + switch (config_.acc_range) { + case QMI8658A_ACC_RANGE_2G: + acc_scale_ = 2.0f / 32768.0f; + break; + case QMI8658A_ACC_RANGE_4G: + acc_scale_ = 4.0f / 32768.0f; + break; + case QMI8658A_ACC_RANGE_8G: + acc_scale_ = 8.0f / 32768.0f; + break; + case QMI8658A_ACC_RANGE_16G: + acc_scale_ = 16.0f / 32768.0f; + break; + } + + // 计算陀螺仪比例因子 (dps) + switch (config_.gyro_range) { + case QMI8658A_GYRO_RANGE_16DPS: + gyro_scale_ = 16.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_32DPS: + gyro_scale_ = 32.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_64DPS: + gyro_scale_ = 64.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_128DPS: + gyro_scale_ = 128.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_256DPS: + gyro_scale_ = 256.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_512DPS: + gyro_scale_ = 512.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_1024DPS: + gyro_scale_ = 1024.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_2048DPS: + gyro_scale_ = 2048.0f / 32768.0f; + break; + } + + ESP_LOGI(TAG, "Scale factors - Acc: %.6f, Gyro: %.6f", acc_scale_, gyro_scale_); +} + +int16_t QMI8658A::ReadInt16(uint8_t reg_low) { + uint8_t data[2]; + ReadRegs(reg_low, data, 2); + return (int16_t)((data[1] << 8) | data[0]); +} + +qmi8658a_error_t QMI8658A::ReadAccelData(float* acc_x, float* acc_y, float* acc_z) { + if (!acc_x || !acc_y || !acc_z) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + if (state_ != QMI8658A_STATE_READY) { + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + uint8_t buf[6]; + ReadRegs(QMI8658A_AX_L, buf, 6); + int16_t raw_x = (int16_t)((buf[1] << 8) | buf[0]); + int16_t raw_y = (int16_t)((buf[3] << 8) | buf[2]); + int16_t raw_z = (int16_t)((buf[5] << 8) | buf[4]); + + // 应用缩放和校准 + float ax = raw_x * acc_scale_; + float ay = raw_y * acc_scale_; + float az = raw_z * acc_scale_; + if (calibration_.is_calibrated) { + float bx = calibration_.acc_bias[0]; + float by = calibration_.acc_bias[1]; + float bz = calibration_.acc_bias[2]; + bool valid_bias = fabs(bx) < 0.5f && fabs(by) < 0.5f && fabs(bz) < 0.5f; + if (valid_bias) { + ax -= bx; + ay -= by; + az -= bz; + } + } + *acc_x = ax; + *acc_y = ay; + *acc_z = az; + + // 数据合理性检查 - 加速度值不应超过设置的量程 + float max_acc = 4.0f; // 假设使用4G量程 + if (fabs(*acc_x) > max_acc * 1.1f || fabs(*acc_y) > max_acc * 1.1f || fabs(*acc_z) > max_acc * 1.1f) { + ESP_LOGE(TAG, "Invalid accelerometer readings: [%.3f, %.3f, %.3f] g", *acc_x, *acc_y, *acc_z); + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::ReadGyroData(float* gyro_x, float* gyro_y, float* gyro_z) { + if (!gyro_x || !gyro_y || !gyro_z) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + if (state_ != QMI8658A_STATE_READY) { + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + uint8_t buf[6]; + ReadRegs(QMI8658A_GX_L, buf, 6); + int16_t raw_x = (int16_t)((buf[1] << 8) | buf[0]); + int16_t raw_y = (int16_t)((buf[3] << 8) | buf[2]); + int16_t raw_z = (int16_t)((buf[5] << 8) | buf[4]); + + float gx = raw_x * gyro_scale_; + float gy = raw_y * gyro_scale_; + float gz = raw_z * gyro_scale_; + if (calibration_.is_calibrated) { + float bx = calibration_.gyro_bias[0]; + float by = calibration_.gyro_bias[1]; + float bz = calibration_.gyro_bias[2]; + bool valid_bias = fabs(bx) < 50.0f && fabs(by) < 50.0f && fabs(bz) < 50.0f; + if (valid_bias) { + gx -= bx; + gy -= by; + gz -= bz; + } + } + *gyro_x = gx; + *gyro_y = gy; + *gyro_z = gz; + + float max_gyro = 400.0f; + if (fabs(*gyro_x) > max_gyro || fabs(*gyro_y) > max_gyro || fabs(*gyro_z) > max_gyro) { + ESP_LOGW(TAG, "Gyro readings out of expected range: [%.3f, %.3f, %.3f] dps", *gyro_x, *gyro_y, *gyro_z); + // 不再直接返回错误,而是继续处理但记录警告 + } + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::ReadTemperature(float* temperature) { + if (!temperature) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + if (state_ != QMI8658A_STATE_READY) { + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + uint8_t tl = ReadReg(QMI8658A_TEMP_L); + uint8_t th = ReadReg(QMI8658A_TEMP_H); + *temperature = (float)th + ((float)tl / 256.0f); + + if (*temperature < -40.0f || *temperature > 125.0f) { + ESP_LOGW(TAG, "Temperature reading out of expected range: %.2f°C", *temperature); + *temperature = 25.0f; + } + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::InitializeBuffer() { + // 初始化缓冲区 + memset(&data_buffer_, 0, sizeof(data_buffer_)); + data_buffer_.mutex = xSemaphoreCreateMutex(); + if (data_buffer_.mutex == NULL) { + return SetError(QMI8658A_ERROR_INIT_FAILED); + } + + buffer_enabled_ = false; + buffer_task_handle_ = NULL; + + ESP_LOGI(TAG, "Data buffer initialized"); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::StartBufferedReading(uint32_t interval_ms) { + if (state_ != QMI8658A_STATE_READY) { + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + if (data_buffer_.mutex == NULL) { + qmi8658a_error_t r = InitializeBuffer(); + if (r != QMI8658A_OK) { + return r; + } + } + + if (buffer_enabled_) { + ESP_LOGW(TAG, "Buffered reading already started"); + return QMI8658A_OK; + } + + buffer_interval_ms_ = interval_ms; + buffer_enabled_ = true; + + // 创建缓冲任务 + BaseType_t result = xTaskCreate( + BufferTask, + "qmi8658a_buffer", + 4096, + this, + 5, + &buffer_task_handle_ + ); + + if (result != pdPASS) { + buffer_enabled_ = false; + return SetError(QMI8658A_ERROR_INIT_FAILED); + } + + ESP_LOGI(TAG, "Started buffered reading with %" PRIu32 " ms interval", interval_ms); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::StopBufferedReading() { + if (!buffer_enabled_) { + return QMI8658A_OK; + } + + buffer_enabled_ = false; + + if (buffer_task_handle_ != NULL) { + vTaskDelete(buffer_task_handle_); + buffer_task_handle_ = NULL; + } + + ESP_LOGI(TAG, "Stopped buffered reading"); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::GetBufferedData(qmi8658a_data_t* data, uint32_t max_count, uint32_t* actual_count) { + if (!data || !actual_count) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + *actual_count = 0; + + if (xSemaphoreTake(data_buffer_.mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + return SetError(QMI8658A_ERROR_TIMEOUT); + } + + uint32_t count = 0; + while (count < max_count && data_buffer_.count > 0) { + data[count] = data_buffer_.data[data_buffer_.tail]; + data_buffer_.tail = (data_buffer_.tail + 1) % QMI8658A_BUFFER_SIZE; + data_buffer_.count--; + count++; + } + + *actual_count = count; + xSemaphoreGive(data_buffer_.mutex); + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::ClearBuffer() { + if (xSemaphoreTake(data_buffer_.mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + return SetError(QMI8658A_ERROR_TIMEOUT); + } + + data_buffer_.head = 0; + data_buffer_.tail = 0; + data_buffer_.count = 0; + data_buffer_.overflow = false; + + xSemaphoreGive(data_buffer_.mutex); + + ESP_LOGI(TAG, "Buffer cleared"); + return QMI8658A_OK; +} + +uint32_t QMI8658A::GetBufferCount() { + if (xSemaphoreTake(data_buffer_.mutex, pdMS_TO_TICKS(10)) != pdTRUE) { + return 0; + } + + uint32_t count = data_buffer_.count; + xSemaphoreGive(data_buffer_.mutex); + + return count; +} + +bool QMI8658A::IsBufferOverflow() { + if (xSemaphoreTake(data_buffer_.mutex, pdMS_TO_TICKS(10)) != pdTRUE) { + return false; + } + + bool overflow = data_buffer_.overflow; + xSemaphoreGive(data_buffer_.mutex); + + return overflow; +} + +qmi8658a_error_t QMI8658A::ConfigureInterrupt(qmi8658a_interrupt_t int_type, gpio_num_t pin) { + interrupt_type_ = int_type; + interrupt_pin_ = pin; + + if (int_type == QMI8658A_INT_DISABLE) { + interrupt_enabled_ = false; + return QMI8658A_OK; + } + + // 配置GPIO中断 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_POSEDGE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << pin); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + + esp_err_t ret = gpio_config(&io_conf); + if (ret != ESP_OK) { + return SetError(QMI8658A_ERROR_INIT_FAILED); + } + + // 安装中断服务 + ret = gpio_install_isr_service(0); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + return SetError(QMI8658A_ERROR_INIT_FAILED); + } + + // 添加中断处理程序 + ret = gpio_isr_handler_add(pin, InterruptHandler, this); + if (ret != ESP_OK) { + return SetError(QMI8658A_ERROR_INIT_FAILED); + } + + // 配置传感器中断 + uint8_t int_config = 0; + switch (int_type) { + case QMI8658A_INT_DATA_READY: + int_config = 0x01; + break; + case QMI8658A_INT_FIFO_WATERMARK: + int_config = 0x02; + break; + case QMI8658A_INT_FIFO_FULL: + int_config = 0x04; + break; + case QMI8658A_INT_MOTION_DETECT: + int_config = 0x08; + break; + default: + break; + } + + WriteReg(0x56, int_config); // INT_EN寄存器 + + interrupt_enabled_ = true; + ESP_LOGI(TAG, "Interrupt configured: type=%d, pin=%d", int_type, pin); + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::EnableFIFO(const qmi8658a_fifo_config_t* fifo_config) { + if (!fifo_config) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + fifo_config_ = *fifo_config; + + // 配置FIFO + uint8_t fifo_ctrl = 0x40; // 启用FIFO + if (fifo_config->watermark > 0 && fifo_config->watermark <= QMI8658A_FIFO_SIZE) { + fifo_ctrl |= (fifo_config->watermark & 0x1F); + } + + WriteReg(0x13, fifo_ctrl); // FIFO_CTRL寄存器 + + // 如果配置了中断,设置中断 + if (fifo_config->interrupt_type != QMI8658A_INT_DISABLE) { + qmi8658a_error_t result = ConfigureInterrupt(fifo_config->interrupt_type, fifo_config->interrupt_pin); + if (result != QMI8658A_OK) { + return result; + } + } + + fifo_enabled_ = true; + ESP_LOGI(TAG, "FIFO enabled with watermark: %d", fifo_config->watermark); + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::DisableFIFO() { + WriteReg(0x13, 0x00); // 禁用FIFO + fifo_enabled_ = false; + ESP_LOGI(TAG, "FIFO disabled"); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::ReadFIFO(qmi8658a_data_t* data_array, uint8_t max_count, uint8_t* actual_count) { + if (!data_array || !actual_count) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + *actual_count = 0; + + if (!fifo_enabled_) { + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + // 读取FIFO状态 + uint8_t fifo_status = ReadReg(0x14); // FIFO_STATUS寄存器 + uint8_t fifo_count = fifo_status & 0x1F; + + if (fifo_count == 0) { + return QMI8658A_OK; + } + + uint8_t read_count = (fifo_count < max_count) ? fifo_count : max_count; + + for (uint8_t i = 0; i < read_count; i++) { + qmi8658a_error_t result = ReadSensorData(&data_array[i]); + if (result != QMI8658A_OK) { + return result; + } + } + + *actual_count = read_count; + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::AddToBuffer(const qmi8658a_data_t* data) { + if (!data) { + return QMI8658A_ERROR_INVALID_PARAM; + } + + if (xSemaphoreTake(data_buffer_.mutex, pdMS_TO_TICKS(10)) != pdTRUE) { + return QMI8658A_ERROR_TIMEOUT; + } + + if (data_buffer_.count >= QMI8658A_BUFFER_SIZE) { + // 缓冲区满,覆盖最旧的数据 + data_buffer_.tail = (data_buffer_.tail + 1) % QMI8658A_BUFFER_SIZE; + data_buffer_.overflow = true; + } else { + data_buffer_.count++; + } + + data_buffer_.data[data_buffer_.head] = *data; + data_buffer_.head = (data_buffer_.head + 1) % QMI8658A_BUFFER_SIZE; + + xSemaphoreGive(data_buffer_.mutex); + + return QMI8658A_OK; +} + +void QMI8658A::BufferTask(void* parameter) { + QMI8658A* sensor = static_cast(parameter); + qmi8658a_data_t data; + + while (sensor->buffer_enabled_) { + if (sensor->ReadSensorData(&data) == QMI8658A_OK) { + sensor->AddToBuffer(&data); + + // 如果正在校准,添加到校准数据 + if (sensor->is_calibrating_) { + sensor->calibration_acc_sum_[0] += data.acc_x; + sensor->calibration_acc_sum_[1] += data.acc_y; + sensor->calibration_acc_sum_[2] += data.acc_z; + sensor->calibration_gyro_sum_[0] += data.gyro_x; + sensor->calibration_gyro_sum_[1] += data.gyro_y; + sensor->calibration_gyro_sum_[2] += data.gyro_z; + sensor->calibration_sample_count_++; + } + } + + vTaskDelay(pdMS_TO_TICKS(sensor->buffer_interval_ms_)); + } + + vTaskDelete(NULL); +} + +void IRAM_ATTR QMI8658A::InterruptHandler(void* arg) { + QMI8658A* sensor = static_cast(arg); + + // 在中断中只做最小的处理 + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + // 可以在这里设置一个标志或发送通知 + // 实际的数据读取应该在任务中进行 + + // 使用sensor参数避免未使用变量警告 + (void)sensor; + (void)xHigherPriorityTaskWoken; +} + +qmi8658a_error_t QMI8658A::ReadSensorData(qmi8658a_data_t* data) { + if (!data) { + ESP_LOGE(TAG, "Data pointer is null"); + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + if (state_ != QMI8658A_STATE_READY) { + ESP_LOGE(TAG, "Sensor not ready, current state: %d", state_); + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + // 增加重试机制 + const int max_retries = 3; + for (int retry = 0; retry < max_retries; retry++) { + // 检查数据是否就绪 + if (!IsDataReady()) { + // 优化警告日志输出:降低级别并减少频率 + static int data_not_ready_count = 0; + static int last_log_time = 0; + int current_time = esp_timer_get_time(); + + // 每10次警告或每1秒才输出一次日志,降低日志级别为DEBUG + if (data_not_ready_count % 10 == 0 || (current_time - last_log_time) > 1000000) { + ESP_LOGD(TAG, "Sensor data not ready (计数: %d), STATUS0: 0x%02X", + data_not_ready_count, ReadReg(QMI8658A_STATUS0)); + last_log_time = current_time; + } + data_not_ready_count++; + + // 添加小延迟后重试 + if (retry < max_retries - 1) { + vTaskDelay(pdMS_TO_TICKS(5)); + continue; + } + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + // 初始化数据结构 + memset(data, 0, sizeof(qmi8658a_data_t)); + data->timestamp = esp_timer_get_time(); + data->valid = false; + + qmi8658a_error_t result; + bool data_valid = true; + + // 读取加速度数据 + if (config_.mode == QMI8658A_MODE_ACC_ONLY || config_.mode == QMI8658A_MODE_DUAL) { + result = ReadAccelData(&data->acc_x, &data->acc_y, &data->acc_z); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to read accelerometer data, error: %d", result); + data_valid = false; + } else { + static float prev_acc_x = 0, prev_acc_y = 0, prev_acc_z = 0; + static bool acc_initialized = false; + if (!acc_initialized) { + prev_acc_x = data->acc_x; + prev_acc_y = data->acc_y; + prev_acc_z = data->acc_z; + data->acc_x = prev_acc_x; data->acc_y = prev_acc_y; data->acc_z = prev_acc_z; + acc_initialized = true; + } else { + const float alpha = 0.6f; + float filtered_x = alpha * prev_acc_x + (1 - alpha) * data->acc_x; + float filtered_y = alpha * prev_acc_y + (1 - alpha) * data->acc_y; + float filtered_z = alpha * prev_acc_z + (1 - alpha) * data->acc_z; + prev_acc_x = filtered_x; prev_acc_y = filtered_y; prev_acc_z = filtered_z; + data->acc_x = filtered_x; data->acc_y = filtered_y; data->acc_z = filtered_z; + } + if (fabs(data->acc_x) < 0.03f) data->acc_x = 0.0f; + if (fabs(data->acc_y) < 0.03f) data->acc_y = 0.0f; + ESP_LOGD(TAG, "Accel data: X=%.3f, Y=%.3f, Z=%.3f", data->acc_x, data->acc_y, data->acc_z); + } + } + + // 读取陀螺仪数据 + if (config_.mode == QMI8658A_MODE_GYRO_ONLY || config_.mode == QMI8658A_MODE_DUAL) { + result = ReadGyroData(&data->gyro_x, &data->gyro_y, &data->gyro_z); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to read gyroscope data, error: %d", result); + // 不再因为陀螺仪读取错误而标记整个数据无效,而是继续处理 + } else { + static float prev_gyro_x = 0, prev_gyro_y = 0, prev_gyro_z = 0; + static bool gyro_initialized = false; + if (!gyro_initialized) { + prev_gyro_x = data->gyro_x; + prev_gyro_y = data->gyro_y; + prev_gyro_z = data->gyro_z; + data->gyro_x = prev_gyro_x; data->gyro_y = prev_gyro_y; data->gyro_z = prev_gyro_z; + gyro_initialized = true; + } else { + const float alpha = 0.5f; + float filtered_x = alpha * prev_gyro_x + (1 - alpha) * data->gyro_x; + float filtered_y = alpha * prev_gyro_y + (1 - alpha) * data->gyro_y; + float filtered_z = alpha * prev_gyro_z + (1 - alpha) * data->gyro_z; + prev_gyro_x = filtered_x; prev_gyro_y = filtered_y; prev_gyro_z = filtered_z; + data->gyro_x = filtered_x; data->gyro_y = filtered_y; data->gyro_z = filtered_z; + } + if (fabs(data->gyro_x) < 0.8f) data->gyro_x = 0.0f; + if (fabs(data->gyro_y) < 0.8f) data->gyro_y = 0.0f; + if (fabs(data->gyro_z) < 0.8f) data->gyro_z = 0.0f; + ESP_LOGV(TAG, "Gyro data after filtering: [%.3f, %.3f, %.3f] dps", + data->gyro_x, data->gyro_y, data->gyro_z); + } + } + + // 读取温度数据 + result = ReadTemperature(&data->temperature); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to read temperature data, error: %d", result); + data_valid = false; + } else { + ESP_LOGD(TAG, "Temperature: %.2f°C", data->temperature); + } + + // 如果数据有效,完成读取 + if (data_valid) { + data->valid = true; + ESP_LOGD(TAG, "Successfully read sensor data (retry: %d)", retry); + return QMI8658A_OK; + } + + // 如果数据无效且未到最大重试次数,等待后重试 + if (retry < max_retries - 1) { + ESP_LOGW(TAG, "Invalid sensor data, retrying... (%d/%d)", retry + 1, max_retries); + vTaskDelay(pdMS_TO_TICKS(10)); + } + } + + // 所有重试均失败 + ESP_LOGE(TAG, "Failed to read valid sensor data after %d retries", max_retries); + return SetError(QMI8658A_ERROR_DATA_NOT_READY); +} + +bool QMI8658A::IsDataReady() { + if (state_ != QMI8658A_STATE_READY) { + return false; + } + + uint8_t status = ReadReg(QMI8658A_STATUS0); + bool acc_ready = (status & 0x01) != 0; + bool gyro_ready = (status & 0x02) != 0; + switch (config_.mode) { + case QMI8658A_MODE_ACC_ONLY: + return acc_ready; + case QMI8658A_MODE_GYRO_ONLY: + return gyro_ready; + case QMI8658A_MODE_DUAL: + // 任一就绪即可开始读取,读取本身使用突发读保证快照一致性 + return acc_ready || gyro_ready; + default: + return false; + } +} + +qmi8658a_error_t QMI8658A::SetError(qmi8658a_error_t error) { + last_error_ = error; + return error; +} + +void QMI8658A::DumpRegisters() { + uint8_t chip = ReadReg(QMI8658A_WHO_AM_I); + uint8_t rev = ReadReg(QMI8658A_REVISION_ID); + uint8_t c1 = ReadReg(QMI8658A_CTRL1); + uint8_t c2 = ReadReg(QMI8658A_CTRL2); + uint8_t c3 = ReadReg(QMI8658A_CTRL3); + uint8_t c4 = ReadReg(QMI8658A_CTRL4); + uint8_t c5 = ReadReg(QMI8658A_CTRL5); + uint8_t c6 = ReadReg(QMI8658A_CTRL6); + uint8_t c7 = ReadReg(QMI8658A_CTRL7); + uint8_t c8 = ReadReg(QMI8658A_CTRL8); + uint8_t c9 = ReadReg(QMI8658A_CTRL9); + uint8_t s0 = ReadReg(QMI8658A_STATUS0); + uint8_t s1 = ReadReg(QMI8658A_STATUS1); + ESP_LOGI(TAG, "ID:0x%02X REV:0x%02X", chip, rev); + ESP_LOGI(TAG, "C1:0x%02X C2:0x%02X C3:0x%02X C4:0x%02X C5:0x%02X C6:0x%02X C7:0x%02X C8:0x%02X C9:0x%02X", c1,c2,c3,c4,c5,c6,c7,c8,c9); + ESP_LOGI(TAG, "S0:0x%02X S1:0x%02X", s0, s1); +} + +void QMI8658A::RunBaselineDiagnostics(uint16_t samples, uint16_t interval_ms) { + if (state_ != QMI8658A_STATE_READY) { + ESP_LOGE(TAG, "Sensor not ready for diagnostics"); + return; + } + double ax_sum = 0, ay_sum = 0, az_sum = 0; + double gx_sum = 0, gy_sum = 0, gz_sum = 0; + double ax_sq = 0, ay_sq = 0, az_sq = 0; + double gx_sq = 0, gy_sq = 0, gz_sq = 0; + uint16_t ok = 0; + for (uint16_t i = 0; i < samples; i++) { + qmi8658a_data_t d; + qmi8658a_error_t r = ReadSensorData(&d); + if (r == QMI8658A_OK && d.valid) { + ax_sum += d.acc_x; ay_sum += d.acc_y; az_sum += d.acc_z; + gx_sum += d.gyro_x; gy_sum += d.gyro_y; gz_sum += d.gyro_z; + ax_sq += d.acc_x * d.acc_x; ay_sq += d.acc_y * d.acc_y; az_sq += d.acc_z * d.acc_z; + gx_sq += d.gyro_x * d.gyro_x; gy_sq += d.gyro_y * d.gyro_y; gz_sq += d.gyro_z * d.gyro_z; + ok++; + } + vTaskDelay(pdMS_TO_TICKS(interval_ms)); + } + if (ok == 0) { + ESP_LOGE(TAG, "No valid samples for diagnostics"); + return; + } + double ax_mean = ax_sum / ok, ay_mean = ay_sum / ok, az_mean = az_sum / ok; + double gx_mean = gx_sum / ok, gy_mean = gy_sum / ok, gz_mean = gz_sum / ok; + double ax_std = sqrt(ax_sq / ok - ax_mean * ax_mean); + double ay_std = sqrt(ay_sq / ok - ay_mean * ay_mean); + double az_std = sqrt(az_sq / ok - az_mean * az_mean); + double gx_std = sqrt(gx_sq / ok - gx_mean * gx_mean); + double gy_std = sqrt(gy_sq / ok - gy_mean * gy_mean); + double gz_std = sqrt(gz_sq / ok - gz_mean * gz_mean); + double acc_mag = sqrt(ax_mean * ax_mean + ay_mean * ay_mean + az_mean * az_mean); + double acc_bias = fabs(acc_mag - 1.0); + ESP_LOGI(TAG, "Baseline accel mean: [%.4f, %.4f, %.4f] g", ax_mean, ay_mean, az_mean); + ESP_LOGI(TAG, "Baseline accel std: [%.4f, %.4f, %.4f] g", ax_std, ay_std, az_std); + ESP_LOGI(TAG, "Accel |mean|-1g: %.5f g", acc_bias); + ESP_LOGI(TAG, "Baseline gyro mean: [%.4f, %.4f, %.4f] dps", gx_mean, gy_mean, gz_mean); + ESP_LOGI(TAG, "Baseline gyro std: [%.4f, %.4f, %.4f] dps", gx_std, gy_std, gz_std); +} +// 新增:带验证的寄存器写入函数 +qmi8658a_error_t QMI8658A::WriteRegWithVerification(uint8_t reg, uint8_t value, uint8_t max_retries) { + for (uint8_t retry = 0; retry < max_retries; retry++) { + // 写入寄存器 + esp_err_t ret = WriteRegWithError(reg, value); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to write register 0x%02X (attempt %d/%d): %s", + reg, retry + 1, max_retries, esp_err_to_name(ret)); + if (retry == max_retries - 1) { + return SetError(QMI8658A_ERROR_I2C_COMM); + } + vTaskDelay(pdMS_TO_TICKS(10)); // 等待10ms后重试 + continue; + } + + // 等待写入完成 + vTaskDelay(pdMS_TO_TICKS(5)); + + // 读回验证 + uint8_t read_value = ReadReg(reg); + if (read_value == 0xFF) { + ESP_LOGE(TAG, "Failed to read back register 0x%02X for verification (attempt %d/%d)", + reg, retry + 1, max_retries); + if (retry == max_retries - 1) { + return SetError(QMI8658A_ERROR_I2C_COMM); + } + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + + if (read_value == value) { + ESP_LOGI(TAG, "Register 0x%02X successfully written and verified: 0x%02X", reg, value); + return QMI8658A_OK; + } else { + ESP_LOGW(TAG, "Register 0x%02X verification failed (attempt %d/%d): wrote 0x%02X, read 0x%02X", + reg, retry + 1, max_retries, value, read_value); + if (retry == max_retries - 1) { + return SetError(QMI8658A_ERROR_CONFIG_FAILED); + } + vTaskDelay(pdMS_TO_TICKS(10)); + } + } + + return SetError(QMI8658A_ERROR_CONFIG_FAILED); +} + +// 新增:验证寄存器值函数 +qmi8658a_error_t QMI8658A::VerifyRegisterValue(uint8_t reg, uint8_t expected_value, const char* reg_name) { + uint8_t actual_value = ReadReg(reg); + if (actual_value == 0xFF) { + ESP_LOGE(TAG, "Failed to read %s register (0x%02X) for verification", reg_name, reg); + return SetError(QMI8658A_ERROR_I2C_COMM); + } + + if (actual_value != expected_value) { + ESP_LOGE(TAG, "%s register (0x%02X) verification failed: expected 0x%02X, got 0x%02X", + reg_name, reg, expected_value, actual_value); + return SetError(QMI8658A_ERROR_CONFIG_FAILED); + } + + ESP_LOGI(TAG, "%s register (0x%02X) verified successfully: 0x%02X", reg_name, reg, actual_value); + return QMI8658A_OK; +} + +// 新增:等待数据就绪函数 +qmi8658a_error_t QMI8658A::WaitForDataReady(uint32_t timeout_ms) { + uint32_t start_time = esp_timer_get_time() / 1000; // 转换为毫秒 + uint32_t elapsed_time = 0; + + ESP_LOGI(TAG, "Waiting for data ready (timeout: %lu ms)...", timeout_ms); + + // 分阶段检查:先检查任一传感器就绪,再检查同时就绪 + bool any_ready = false; + + while (elapsed_time < timeout_ms) { + uint8_t status0 = ReadReg(QMI8658A_STATUS0); + if (status0 == 0xFF) { + ESP_LOGE(TAG, "Failed to read STATUS0 register while waiting for data ready"); + return SetError(QMI8658A_ERROR_I2C_COMM); + } + + // 检查加速度计和陀螺仪数据是否就绪 + bool acc_ready = (status0 & 0x01) != 0; + bool gyro_ready = (status0 & 0x02) != 0; + + // 检测到任一传感器就绪,记录并继续等待 + if (acc_ready || gyro_ready) { + if (!any_ready) { + ESP_LOGI(TAG, "At least one sensor ready after %lu ms (STATUS0: 0x%02X)", elapsed_time, status0); + any_ready = true; + } + + // 如果任一传感器就绪且已等待至少100ms,就返回成功 + // 这解决了某些情况下一个传感器就绪另一个未就绪的问题 + if (any_ready && elapsed_time > 100) { + ESP_LOGI(TAG, "At least one sensor ready, proceeding after %lu ms (STATUS0: 0x%02X)", elapsed_time, status0); + return QMI8658A_OK; + } + } + + // 根据是否已检测到传感器就绪调整等待时间 + uint8_t delay_time = any_ready ? 5 : 10; // 已检测到就绪则减少等待时间 + vTaskDelay(pdMS_TO_TICKS(delay_time)); + elapsed_time = (esp_timer_get_time() / 1000) - start_time; + } + + ESP_LOGI(TAG, "Timeout waiting for data ready after %lu ms", timeout_ms); + return SetError(QMI8658A_ERROR_TIMEOUT); + +} + +// 新增:自检函数 +qmi8658a_error_t QMI8658A::PerformSelfTest() { + ESP_LOGI(TAG, "Performing self-test..."); + + // 检查芯片ID + uint8_t chip_id = GetChipId(); + if (chip_id != QMI8658A_CHIP_ID) { + ESP_LOGE(TAG, "Self-test failed: Invalid chip ID 0x%02X", chip_id); + return SetError(QMI8658A_ERROR_CHIP_ID); + } + + // 检查关键寄存器的可读性 + uint8_t test_regs[] = {QMI8658A_WHO_AM_I, QMI8658A_REVISION_ID, QMI8658A_STATUS0, QMI8658A_STATUS1}; + const char* test_reg_names[] = {"WHO_AM_I", "REVISION_ID", "STATUS0", "STATUS1"}; + + for (int i = 0; i < 4; i++) { + uint8_t value = ReadReg(test_regs[i]); + if (value == 0xFF) { + ESP_LOGE(TAG, "Self-test failed: Cannot read %s register (0x%02X)", + test_reg_names[i], test_regs[i]); + return SetError(QMI8658A_ERROR_I2C_COMM); + } + ESP_LOGI(TAG, "Self-test: %s (0x%02X) = 0x%02X", test_reg_names[i], test_regs[i], value); + } + + ESP_LOGI(TAG, "Self-test completed successfully"); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::UpdateConfiguration(const qmi8658a_config_t* new_config) { + if (!new_config) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + // 验证新配置 + qmi8658a_error_t result = ValidateConfiguration(new_config); + if (result != QMI8658A_OK) { + return result; + } + + // 保存旧配置以便回滚 + qmi8658a_config_t old_config = config_; + config_ = *new_config; + + // 应用配置更改 + result = ApplyConfigurationChanges(); + if (result != QMI8658A_OK) { + // 回滚到旧配置 + config_ = old_config; + return result; + } + + ESP_LOGI(TAG, "Configuration updated successfully"); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::ValidateConfiguration(const qmi8658a_config_t* config) { + if (!config) { + return QMI8658A_ERROR_INVALID_PARAM; + } + + // 验证加速度计量程 + if (config->acc_range > QMI8658A_ACC_RANGE_16G) { + ESP_LOGE(TAG, "Invalid accelerometer range: %d", config->acc_range); + return QMI8658A_ERROR_INVALID_PARAM; + } + + // 验证陀螺仪量程 + if (config->gyro_range > QMI8658A_GYRO_RANGE_2048DPS) { + ESP_LOGE(TAG, "Invalid gyroscope range: %d", config->gyro_range); + return QMI8658A_ERROR_INVALID_PARAM; + } + + // 验证ODR设置 + if (config->acc_odr > QMI8658A_ODR_8000HZ || config->gyro_odr > QMI8658A_ODR_8000HZ) { + ESP_LOGE(TAG, "Invalid ODR setting"); + return QMI8658A_ERROR_INVALID_PARAM; + } + + // 验证工作模式 + if (config->mode > QMI8658A_MODE_DUAL) { + ESP_LOGE(TAG, "Invalid operation mode: %d", config->mode); + return QMI8658A_ERROR_INVALID_PARAM; + } + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::GetConfiguration(qmi8658a_config_t* config) { + if (!config) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + *config = config_; + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::SetAccelRange(qmi8658a_acc_range_t range) { + if (range > QMI8658A_ACC_RANGE_16G) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + config_.acc_range = range; + qmi8658a_error_t result = SetAccelConfig(range, config_.acc_odr); + if (result == QMI8658A_OK) { + UpdateScaleFactors(); + } + return result; +} + +qmi8658a_error_t QMI8658A::SetGyroRange(qmi8658a_gyro_range_t range) { + if (range > QMI8658A_GYRO_RANGE_2048DPS) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + config_.gyro_range = range; + qmi8658a_error_t result = SetGyroConfig(range, config_.gyro_odr); + if (result == QMI8658A_OK) { + UpdateScaleFactors(); + } + return result; +} + +qmi8658a_error_t QMI8658A::SetAccelODR(qmi8658a_odr_t odr) { + if (odr > QMI8658A_ODR_8000HZ) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + config_.acc_odr = odr; + return SetAccelConfig(config_.acc_range, odr); +} + +qmi8658a_error_t QMI8658A::SetGyroODR(qmi8658a_odr_t odr) { + if (odr > QMI8658A_ODR_8000HZ) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + config_.gyro_odr = odr; + return SetGyroConfig(config_.gyro_range, odr); +} + +qmi8658a_error_t QMI8658A::SetOperationMode(qmi8658a_mode_t mode) { + if (mode > QMI8658A_MODE_DUAL) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + config_.mode = mode; + return SetMode(mode); +} + +qmi8658a_error_t QMI8658A::StartCalibration(uint32_t duration_ms) { + if (state_ != QMI8658A_STATE_READY) { + return SetError(QMI8658A_ERROR_DATA_NOT_READY); + } + + is_calibrating_ = true; + calibration_start_time_ = esp_timer_get_time() / 1000; // 转换为毫秒 + calibration_duration_ = duration_ms; + calibration_sample_count_ = 0; + + // 清零累加器 + memset(calibration_acc_sum_, 0, sizeof(calibration_acc_sum_)); + memset(calibration_gyro_sum_, 0, sizeof(calibration_gyro_sum_)); + + ESP_LOGI(TAG, "Started calibration for %" PRIu32 " ms", duration_ms); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::GetCalibrationStatus(bool* is_calibrating, float* progress) { + if (!is_calibrating || !progress) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + *is_calibrating = is_calibrating_; + + if (is_calibrating_) { + uint32_t current_time = esp_timer_get_time() / 1000; + uint32_t elapsed = current_time - calibration_start_time_; + *progress = (float)elapsed / calibration_duration_; + + if (elapsed >= calibration_duration_) { + // 校准完成,计算偏置 + if (calibration_sample_count_ > 0) { + float mean_ax = calibration_acc_sum_[0] / calibration_sample_count_; + float mean_ay = calibration_acc_sum_[1] / calibration_sample_count_; + float mean_az = calibration_acc_sum_[2] / calibration_sample_count_; + float mean_gx = calibration_gyro_sum_[0] / calibration_sample_count_; + float mean_gy = calibration_gyro_sum_[1] / calibration_sample_count_; + float mean_gz = calibration_gyro_sum_[2] / calibration_sample_count_; + calibration_.acc_bias[0] = mean_ax; + calibration_.acc_bias[1] = mean_ay; + calibration_.acc_bias[2] = mean_az - 1.0f; + calibration_.gyro_bias[0] = mean_gx; + calibration_.gyro_bias[1] = mean_gy; + calibration_.gyro_bias[2] = mean_gz; + calibration_.is_calibrated = true; + calibration_.calibration_time = current_time; + } + + is_calibrating_ = false; + *is_calibrating = false; + *progress = 1.0f; + + ESP_LOGI(TAG, "Calibration completed with %" PRIu32 " samples", calibration_sample_count_); + } + } else { + *progress = 0.0f; + } + + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::ApplyCalibration(const qmi8658a_calibration_t* calibration) { + if (!calibration) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + calibration_ = *calibration; + ESP_LOGI(TAG, "Applied calibration data"); + return QMI8658A_OK; +} + +qmi8658a_error_t QMI8658A::GetCalibrationData(qmi8658a_calibration_t* calibration) { + if (!calibration) { + return SetError(QMI8658A_ERROR_INVALID_PARAM); + } + + *calibration = calibration_; + return QMI8658A_OK; +} + +void QMI8658A::UpdateScaleFactors() { + // 根据加速度计量程计算比例因子 + switch (config_.acc_range) { + case QMI8658A_ACC_RANGE_2G: + acc_scale_ = 2.0f / 32768.0f; + break; + case QMI8658A_ACC_RANGE_4G: + acc_scale_ = 4.0f / 32768.0f; + break; + case QMI8658A_ACC_RANGE_8G: + acc_scale_ = 8.0f / 32768.0f; + break; + case QMI8658A_ACC_RANGE_16G: + acc_scale_ = 16.0f / 32768.0f; + break; + default: + acc_scale_ = 2.0f / 32768.0f; + break; + } + + // 根据陀螺仪量程计算比例因子 + switch (config_.gyro_range) { + case QMI8658A_GYRO_RANGE_16DPS: + gyro_scale_ = 16.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_32DPS: + gyro_scale_ = 32.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_64DPS: + gyro_scale_ = 64.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_128DPS: + gyro_scale_ = 128.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_256DPS: + gyro_scale_ = 256.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_512DPS: + gyro_scale_ = 512.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_1024DPS: + gyro_scale_ = 1024.0f / 32768.0f; + break; + case QMI8658A_GYRO_RANGE_2048DPS: + gyro_scale_ = 2048.0f / 32768.0f; + break; + default: + gyro_scale_ = 256.0f / 32768.0f; + break; + } +} + +qmi8658a_error_t QMI8658A::ApplyConfigurationChanges() { + qmi8658a_error_t result; + + // 应用加速度计配置 + result = SetAccelConfig(config_.acc_range, config_.acc_odr); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to configure accelerometer"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 应用陀螺仪配置 + result = SetGyroConfig(config_.gyro_range, config_.gyro_odr); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to configure gyroscope"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 设置工作模式 + uint8_t mode_val = config_.mode; + result = SetMode(static_cast(mode_val)); + if (result != QMI8658A_OK) { + ESP_LOGE(TAG, "Failed to set mode"); + state_ = QMI8658A_STATE_ERROR; + return result; + } + + // 更新比例因子 + UpdateScaleFactors(); + + return QMI8658A_OK; +} + +QMI8658A::~QMI8658A() { + // 停止缓冲读取 + if (buffer_enabled_) { + StopBufferedReading(); + } + + // 停止校准 + if (is_calibrating_) { + is_calibrating_ = false; + } + + // 清理缓冲区 - 只有在mutex已创建时才释放 + if (data_buffer_.mutex != nullptr) { + vSemaphoreDelete(data_buffer_.mutex); + data_buffer_.mutex = nullptr; + } + + // 禁用FIFO + if (fifo_enabled_) { + DisableFIFO(); + } + + // 设置为禁用模式 - 只有在传感器已初始化时才调用 + if (state_ != QMI8658A_STATE_UNINITIALIZED) { + SetMode(QMI8658A_MODE_DISABLE); + } + + ESP_LOGI(TAG, "QMI8658A destructor completed"); +} diff --git a/main/boards/common/qmi8658a.h b/main/boards/common/qmi8658a.h new file mode 100644 index 0000000..51b0381 --- /dev/null +++ b/main/boards/common/qmi8658a.h @@ -0,0 +1,316 @@ +#ifndef QMI8658A_H +#define QMI8658A_H + +#include "driver/i2c_master.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_timer.h" +#include +#include +#include "i2c_device.h" + +// QMI8658A I2C地址定义 +#define QMI8658A_I2C_ADDRESS 0x6A // 修改为0x6A,适配新PCB板(SA0接VDD) + +// QMI8658A寄存器地址定义 +#define QMI8658A_WHO_AM_I 0x00 +#define QMI8658A_REVISION_ID 0x01 +#define QMI8658A_CTRL1 0x02 +#define QMI8658A_CTRL2 0x03 +#define QMI8658A_CTRL3 0x04 +#define QMI8658A_CTRL4 0x05 +#define QMI8658A_CTRL5 0x06 +#define QMI8658A_CTRL6 0x07 +#define QMI8658A_CTRL7 0x08 +#define QMI8658A_CTRL8 0x09 +#define QMI8658A_CTRL9 0x0A +#define QMI8658A_RESET 0x60 + +// 数据寄存器 +#define QMI8658A_TEMP_L 0x33 +#define QMI8658A_TEMP_H 0x34 +#define QMI8658A_AX_L 0x35 +#define QMI8658A_AX_H 0x36 +#define QMI8658A_AY_L 0x37 +#define QMI8658A_AY_H 0x38 +#define QMI8658A_AZ_L 0x39 +#define QMI8658A_AZ_H 0x3A +#define QMI8658A_GX_L 0x3B +#define QMI8658A_GX_H 0x3C +#define QMI8658A_GY_L 0x3D +#define QMI8658A_GY_H 0x3E +#define QMI8658A_GZ_L 0x3F +#define QMI8658A_GZ_H 0x40 + +// 状态寄存器 +#define QMI8658A_STATUSINT 0x2D +#define QMI8658A_STATUS0 0x2E +#define QMI8658A_STATUS1 0x2F + +// 设备ID +#define QMI8658A_CHIP_ID 0x05 + +// 工作模式 +typedef enum { + QMI8658A_MODE_DISABLE = 0x00, + QMI8658A_MODE_ACC_ONLY = 0x01, + QMI8658A_MODE_GYRO_ONLY = 0x02, + QMI8658A_MODE_DUAL = 0x03 +} qmi8658a_mode_t; + +// 加速度计量程 +typedef enum { + QMI8658A_ACC_RANGE_2G = 0x00, + QMI8658A_ACC_RANGE_4G = 0x01, + QMI8658A_ACC_RANGE_8G = 0x02, + QMI8658A_ACC_RANGE_16G = 0x03 +} qmi8658a_acc_range_t; + +// 陀螺仪量程 +typedef enum { + QMI8658A_GYRO_RANGE_16DPS = 0x00, + QMI8658A_GYRO_RANGE_32DPS = 0x01, + QMI8658A_GYRO_RANGE_64DPS = 0x02, + QMI8658A_GYRO_RANGE_128DPS = 0x03, + QMI8658A_GYRO_RANGE_256DPS = 0x04, + QMI8658A_GYRO_RANGE_512DPS = 0x05, + QMI8658A_GYRO_RANGE_1024DPS = 0x06, + QMI8658A_GYRO_RANGE_2048DPS = 0x07 +} qmi8658a_gyro_range_t; + +// 输出数据率 +typedef enum { + QMI8658A_ODR_8000HZ = 0x00, + QMI8658A_ODR_4000HZ = 0x01, + QMI8658A_ODR_2000HZ = 0x02, + QMI8658A_ODR_1000HZ = 0x03, + QMI8658A_ODR_500HZ = 0x04, + QMI8658A_ODR_250HZ = 0x05, + QMI8658A_ODR_125HZ = 0x06, + QMI8658A_ODR_62_5HZ = 0x07, + QMI8658A_ODR_31_25HZ = 0x08 +} qmi8658a_odr_t; + +// 传感器数据结构 - 优化版本,使用数组存储 +typedef struct { + union { + struct { + float acc_x; // 加速度X轴 (g) + float acc_y; // 加速度Y轴 (g) + float acc_z; // 加速度Z轴 (g) + }; + float accel[3]; // 加速度数组 [x, y, z] (g) + }; + union { + struct { + float gyro_x; // 陀螺仪X轴 (dps) + float gyro_y; // 陀螺仪Y轴 (dps) + float gyro_z; // 陀螺仪Z轴 (dps) + }; + float gyro[3]; // 陀螺仪数组 [x, y, z] (dps) + }; + float temperature; // 温度 (°C) + uint64_t timestamp; // 时间戳 (微秒) + bool valid; // 数据有效性标志 +} qmi8658a_data_t; + +// 错误代码定义 +typedef enum { + QMI8658A_OK = 0, + QMI8658A_ERROR_INVALID_PARAM = -1, + QMI8658A_ERROR_I2C_COMM = -2, + QMI8658A_ERROR_CHIP_ID = -3, + QMI8658A_ERROR_INIT_FAILED = -4, + QMI8658A_ERROR_CONFIG_FAILED = -5, + QMI8658A_ERROR_DATA_NOT_READY = -6, + QMI8658A_ERROR_TIMEOUT = -7 +} qmi8658a_error_t; + +// 传感器状态 +typedef enum { + QMI8658A_STATE_UNINITIALIZED = 0, + QMI8658A_STATE_INITIALIZING, + QMI8658A_STATE_READY, + QMI8658A_STATE_ERROR +} qmi8658a_state_t; + +// 配置结构体 +typedef struct { + qmi8658a_acc_range_t acc_range; + qmi8658a_gyro_range_t gyro_range; + qmi8658a_odr_t acc_odr; + qmi8658a_odr_t gyro_odr; + qmi8658a_mode_t mode; + bool enable_interrupt; // 是否启用中断 + uint8_t interrupt_pin; // 中断引脚 + bool auto_calibration; // 是否启用自动校准 + float acc_offset[3]; // 加速度计偏移校准 + float gyro_offset[3]; // 陀螺仪偏移校准 +} qmi8658a_config_t; + +// 校准数据结构 +typedef struct { + float acc_bias[3]; // 加速度计偏置 + float gyro_bias[3]; // 陀螺仪偏置 + float acc_scale[3]; // 加速度计缩放因子 + float gyro_scale[3]; // 陀螺仪缩放因子 + bool is_calibrated; // 是否已校准 + uint32_t calibration_time; // 校准时间戳 +} qmi8658a_calibration_t; + +// 数据缓冲配置 +#define QMI8658A_BUFFER_SIZE 32 +#define QMI8658A_FIFO_SIZE 16 + +// 中断配置 +typedef enum { + QMI8658A_INT_DISABLE = 0, + QMI8658A_INT_DATA_READY = 1, + QMI8658A_INT_FIFO_WATERMARK = 2, + QMI8658A_INT_FIFO_FULL = 3, + QMI8658A_INT_MOTION_DETECT = 4 +} qmi8658a_interrupt_t; + +// 数据缓冲结构 +typedef struct { + qmi8658a_data_t data[QMI8658A_BUFFER_SIZE]; + uint32_t head; + uint32_t tail; + uint32_t count; + bool overflow; + SemaphoreHandle_t mutex; +} qmi8658a_buffer_t; + +// FIFO配置结构 +typedef struct { + bool enable; + uint8_t watermark; + qmi8658a_interrupt_t interrupt_type; + gpio_num_t interrupt_pin; +} qmi8658a_fifo_config_t; + +class QMI8658A : public I2cDevice { +public: + QMI8658A(i2c_master_bus_handle_t i2c_bus, uint8_t addr = QMI8658A_I2C_ADDRESS); + ~QMI8658A(); + + // 初始化和配置 + qmi8658a_error_t Initialize(const qmi8658a_config_t* config = nullptr); + qmi8658a_error_t UpdateConfiguration(const qmi8658a_config_t* new_config); + qmi8658a_error_t ValidateConfiguration(const qmi8658a_config_t* config); + qmi8658a_error_t GetConfiguration(qmi8658a_config_t* config); + + // 运行时配置修改 + qmi8658a_error_t SetAccelRange(qmi8658a_acc_range_t range); + qmi8658a_error_t SetGyroRange(qmi8658a_gyro_range_t range); + qmi8658a_error_t SetAccelODR(qmi8658a_odr_t odr); + qmi8658a_error_t SetGyroODR(qmi8658a_odr_t odr); + qmi8658a_error_t SetOperationMode(qmi8658a_mode_t mode); + + // 中断和FIFO配置 + qmi8658a_error_t ConfigureInterrupt(qmi8658a_interrupt_t int_type, gpio_num_t pin); + qmi8658a_error_t EnableFIFO(const qmi8658a_fifo_config_t* fifo_config); + qmi8658a_error_t DisableFIFO(); + qmi8658a_error_t ReadFIFO(qmi8658a_data_t* data_array, uint8_t max_count, uint8_t* actual_count); + + // 数据缓冲管理 + qmi8658a_error_t InitializeBuffer(); + qmi8658a_error_t StartBufferedReading(uint32_t interval_ms); + qmi8658a_error_t StopBufferedReading(); + qmi8658a_error_t GetBufferedData(qmi8658a_data_t* data, uint32_t max_count, uint32_t* actual_count); + qmi8658a_error_t ClearBuffer(); + uint32_t GetBufferCount(); + bool IsBufferOverflow(); + + // 校准功能 + qmi8658a_error_t StartCalibration(uint32_t duration_ms = 5000); + qmi8658a_error_t GetCalibrationStatus(bool* is_calibrating, float* progress); + qmi8658a_error_t ApplyCalibration(const qmi8658a_calibration_t* calibration); + qmi8658a_error_t GetCalibrationData(qmi8658a_calibration_t* calibration); + qmi8658a_error_t SaveCalibrationToNVS(); + qmi8658a_error_t LoadCalibrationFromNVS(); + + // 原有方法保持不变 + qmi8658a_error_t SoftReset(); + qmi8658a_error_t SetMode(qmi8658a_mode_t mode); + qmi8658a_error_t SetAccelConfig(qmi8658a_acc_range_t range, qmi8658a_odr_t odr); + qmi8658a_error_t SetGyroConfig(qmi8658a_gyro_range_t range, qmi8658a_odr_t odr); + + // 数据读取 + qmi8658a_error_t ReadSensorData(qmi8658a_data_t* data); + qmi8658a_error_t ReadAccelData(float* acc_x, float* acc_y, float* acc_z); + qmi8658a_error_t ReadGyroData(float* gyro_x, float* gyro_y, float* gyro_z); + qmi8658a_error_t ReadTemperature(float* temperature); + + // 状态和诊断方法 + qmi8658a_state_t GetState() const { return state_; } + qmi8658a_error_t GetLastError() const { return last_error_; } + bool IsDataReady(); + void DumpRegisters(); + void RunBaselineDiagnostics(uint16_t samples = 200, uint16_t interval_ms = 10); + + // 芯片信息 + uint8_t GetChipId(); + uint8_t GetRevisionId(); + + // 静态连接检测方法(用于生产测试) + static bool CheckConnection(i2c_master_bus_handle_t i2c_bus, uint8_t* detected_address = nullptr); + +private: + qmi8658a_config_t config_; + qmi8658a_calibration_t calibration_; + qmi8658a_state_t state_; + qmi8658a_error_t last_error_; + + float acc_scale_; + float gyro_scale_; + + // 校准相关 + bool is_calibrating_; + uint32_t calibration_start_time_; + uint32_t calibration_duration_; + float calibration_acc_sum_[3]; + float calibration_gyro_sum_[3]; + uint32_t calibration_sample_count_; + + // 缓冲区相关 + qmi8658a_buffer_t data_buffer_; + TaskHandle_t buffer_task_handle_; + bool buffer_enabled_; + uint32_t buffer_interval_ms_; + + // 中断相关 + gpio_num_t interrupt_pin_; + qmi8658a_interrupt_t interrupt_type_; + bool interrupt_enabled_; + + // FIFO相关 + qmi8658a_fifo_config_t fifo_config_; + bool fifo_enabled_; + + // 错误处理和验证函数 + qmi8658a_error_t SetError(qmi8658a_error_t error); + void CalculateScaleFactors(); + void UpdateScaleFactors(); + qmi8658a_error_t ApplyConfigurationChanges(); + + // 新增:寄存器验证和重试机制 + qmi8658a_error_t WriteRegWithVerification(uint8_t reg, uint8_t value, uint8_t max_retries = 3); + qmi8658a_error_t VerifyRegisterValue(uint8_t reg, uint8_t expected_value, const char* reg_name); + qmi8658a_error_t WaitForDataReady(uint32_t timeout_ms = 1000); + qmi8658a_error_t PerformSelfTest(); + + // 缓冲区和中断处理 + static void BufferTask(void* parameter); + static void IRAM_ATTR InterruptHandler(void* arg); + qmi8658a_error_t AddToBuffer(const qmi8658a_data_t* data); + qmi8658a_error_t GetFromBuffer(qmi8658a_data_t* data); + + // 工具函数 + int16_t ReadInt16(uint8_t reg); +}; + +#endif // QMI8658A_H diff --git a/main/boards/common/system_reset.cc b/main/boards/common/system_reset.cc new file mode 100644 index 0000000..f51249b --- /dev/null +++ b/main/boards/common/system_reset.cc @@ -0,0 +1,72 @@ +#include "system_reset.h" + +#include +#include +#include +#include +#include +#include + + +#define TAG "SystemReset" + + +SystemReset::SystemReset(gpio_num_t reset_nvs_pin, gpio_num_t reset_factory_pin) : reset_nvs_pin_(reset_nvs_pin), reset_factory_pin_(reset_factory_pin) { + // Configure GPIO1, GPIO2 as INPUT, reset NVS flash if the button is pressed + gpio_config_t io_conf; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << reset_nvs_pin_) | (1ULL << reset_factory_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&io_conf); +} + + +void SystemReset::CheckButtons() { + if (gpio_get_level(reset_factory_pin_) == 0) { + ESP_LOGI(TAG, "Button is pressed, reset to factory"); + ResetNvsFlash(); + ResetToFactory(); + } + + if (gpio_get_level(reset_nvs_pin_) == 0) { + ESP_LOGI(TAG, "Button is pressed, reset NVS flash"); + ResetNvsFlash(); + } +} + +void SystemReset::ResetNvsFlash() { + ESP_LOGI(TAG, "Resetting NVS flash"); + esp_err_t ret = nvs_flash_erase(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to erase NVS flash"); + } + ret = nvs_flash_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize NVS flash"); + } +} + +void SystemReset::ResetToFactory() { + ESP_LOGI(TAG, "Resetting to factory"); + // Erase otadata partition + const esp_partition_t* partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_OTA, NULL); + if (partition == NULL) { + ESP_LOGE(TAG, "Failed to find otadata partition"); + return; + } + esp_partition_erase_range(partition, 0, partition->size); + ESP_LOGI(TAG, "Erased otadata partition"); + + // Reboot in 3 seconds + RestartInSeconds(3); +} + +void SystemReset::RestartInSeconds(int seconds) { + for (int i = seconds; i > 0; i--) { + ESP_LOGI(TAG, "Resetting in %d seconds", i); + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + esp_restart(); +} diff --git a/main/boards/common/system_reset.h b/main/boards/common/system_reset.h new file mode 100644 index 0000000..7e78296 --- /dev/null +++ b/main/boards/common/system_reset.h @@ -0,0 +1,21 @@ +#ifndef _SYSTEM_RESET_H +#define _SYSTEM_RESET_H + +#include + +class SystemReset { +public: + SystemReset(gpio_num_t reset_nvs_pin, gpio_num_t reset_factory_pin); // 构造函数私有化 + void CheckButtons(); + +private: + gpio_num_t reset_nvs_pin_; + gpio_num_t reset_factory_pin_; + + void ResetNvsFlash(); + void ResetToFactory(); + void RestartInSeconds(int seconds); +}; + + +#endif diff --git a/main/boards/common/veml7700.cc b/main/boards/common/veml7700.cc new file mode 100644 index 0000000..3492c00 --- /dev/null +++ b/main/boards/common/veml7700.cc @@ -0,0 +1,445 @@ +#include "veml7700.h" +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "VEML7700" + +// ============================================================================ +// 增益对应的系数表(用于Lux计算) +// ============================================================================ +static const float gain_factors[] = { + 1.0f, // GAIN_1 (x1) + 2.0f, // GAIN_2 (x2) + 0.125f, // GAIN_D8 (x1/8) + 0.25f, // GAIN_D4 (x1/4) +}; + +// 积分时间对应的系数表 +// 索引通过 it 枚举值映射 +static float GetItFactor(veml7700_it_t it) { + switch (it) { + case VEML7700_IT_25MS: return 0.25f; + case VEML7700_IT_50MS: return 0.5f; + case VEML7700_IT_100MS: return 1.0f; + case VEML7700_IT_200MS: return 2.0f; + case VEML7700_IT_400MS: return 4.0f; + case VEML7700_IT_800MS: return 8.0f; + default: return 1.0f; + } +} + +// 积分时间对应的等待时间(ms) +static uint32_t GetItDelayMs(veml7700_it_t it) { + switch (it) { + case VEML7700_IT_25MS: return 25; + case VEML7700_IT_50MS: return 50; + case VEML7700_IT_100MS: return 100; + case VEML7700_IT_200MS: return 200; + case VEML7700_IT_400MS: return 400; + case VEML7700_IT_800MS: return 800; + default: return 100; + } +} + +// ============================================================================ +// 构造函数 +// ============================================================================ +VEML7700::VEML7700(i2c_master_bus_handle_t i2c_bus, uint8_t addr) + : I2cDevice(i2c_bus, addr), + gain_(VEML7700_GAIN_1), + it_(VEML7700_IT_100MS), + pers_(VEML7700_PERS_1), + int_enable_(false), + shutdown_(true), + als_conf_(0x0001) // 默认关机状态 +{ +} + +// ============================================================================ +// 16-bit 寄存器读写 +// VEML7700 协议:写 = [cmd] + [data_low] + [data_high] +// 读 = [cmd] → [data_low] + [data_high] +// ============================================================================ +esp_err_t VEML7700::WriteReg16(uint8_t cmd, uint16_t value) { + uint8_t buffer[3] = { + cmd, + (uint8_t)(value & 0xFF), // 低字节 + (uint8_t)((value >> 8) & 0xFF) // 高字节 + }; + esp_err_t ret = i2c_master_transmit(i2c_device_, buffer, 3, 100); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "写寄存器 0x%02X 失败: %s", cmd, esp_err_to_name(ret)); + } + return ret; +} + +esp_err_t VEML7700::ReadReg16(uint8_t cmd, uint16_t* value) { + uint8_t rx[2] = {0}; + esp_err_t ret = i2c_master_transmit_receive(i2c_device_, &cmd, 1, rx, 2, 100); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "读寄存器 0x%02X 失败: %s", cmd, esp_err_to_name(ret)); + *value = 0; + return ret; + } + *value = (uint16_t)(rx[0] | (rx[1] << 8)); + return ESP_OK; +} + +// ============================================================================ +// 更新ALS配置寄存器 +// ALS_CONF 位域: +// [15:13] 保留 +// [12:11] ALS_GAIN +// [10] 保留 +// [9:6] ALS_IT +// [5:4] ALS_PERS +// [3] 保留 +// [2] ALS_INT_EN +// [1] 保留 +// [0] ALS_SD (1=关机, 0=运行) +// ============================================================================ +esp_err_t VEML7700::UpdateALSConf() { + als_conf_ = 0; + als_conf_ |= ((uint16_t)gain_ & 0x03) << 11; + als_conf_ |= ((uint16_t)it_ & 0x0F) << 6; + als_conf_ |= ((uint16_t)pers_ & 0x03) << 4; + als_conf_ |= int_enable_ ? (1 << 2) : 0; + als_conf_ |= shutdown_ ? 1 : 0; + + return WriteReg16(VEML7700_REG_ALS_CONF, als_conf_); +} + +// ============================================================================ +// 初始化 +// ============================================================================ +esp_err_t VEML7700::Init() { + ESP_LOGI(TAG, "初始化 VEML7700 环境光传感器..."); + + // 先关机再配置(datasheet建议) + shutdown_ = true; + esp_err_t ret = UpdateALSConf(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "VEML7700 初始化失败,I2C通信错误"); + return ret; + } + + // 设置默认参数:增益x1,积分时间100ms + gain_ = VEML7700_GAIN_1; + it_ = VEML7700_IT_100MS; + pers_ = VEML7700_PERS_1; + int_enable_ = false; + + // 关闭省电模式 + ret = SetPowerSaving(VEML7700_PSM_1, false); + if (ret != ESP_OK) return ret; + + // 唤醒传感器 + shutdown_ = false; + ret = UpdateALSConf(); + if (ret != ESP_OK) return ret; + + // 等待首次采样完成 + vTaskDelay(pdMS_TO_TICKS(GetSampleDelayMs() + 10)); + + ESP_LOGI(TAG, "VEML7700 初始化成功 (增益:x1, 积分时间:100ms)"); + return ESP_OK; +} + +// ============================================================================ +// 配置接口 +// ============================================================================ +esp_err_t VEML7700::SetGain(veml7700_gain_t gain) { + gain_ = gain; + return UpdateALSConf(); +} + +esp_err_t VEML7700::SetIntegrationTime(veml7700_it_t it) { + it_ = it; + return UpdateALSConf(); +} + +esp_err_t VEML7700::SetPersistence(veml7700_pers_t pers) { + pers_ = pers; + return UpdateALSConf(); +} + +esp_err_t VEML7700::SetInterruptEnable(bool enable) { + int_enable_ = enable; + return UpdateALSConf(); +} + +esp_err_t VEML7700::Shutdown(bool sd) { + shutdown_ = sd; + return UpdateALSConf(); +} + +esp_err_t VEML7700::SetPowerSaving(veml7700_psm_t mode, bool enable) { + uint16_t psm_val = 0; + psm_val |= ((uint16_t)mode & 0x03) << 1; + psm_val |= enable ? 1 : 0; + return WriteReg16(VEML7700_REG_PSM, psm_val); +} + +esp_err_t VEML7700::SetThresholdHigh(uint16_t threshold) { + return WriteReg16(VEML7700_REG_ALS_WH, threshold); +} + +esp_err_t VEML7700::SetThresholdLow(uint16_t threshold) { + return WriteReg16(VEML7700_REG_ALS_WL, threshold); +} + +// ============================================================================ +// 数据读取 +// ============================================================================ +esp_err_t VEML7700::ReadALSRaw(uint16_t* raw) { + return ReadReg16(VEML7700_REG_ALS, raw); +} + +esp_err_t VEML7700::ReadWhiteRaw(uint16_t* raw) { + return ReadReg16(VEML7700_REG_WHITE, raw); +} + +// ============================================================================ +// 分辨率计算 +// 基准分辨率:增益x1 + 积分时间100ms = 0.0576 lux/count +// 其他配置按比例缩放 +// ============================================================================ +float VEML7700::GetResolution() const { + float base_resolution = 0.0576f; // lux/count @ gain=1, it=100ms + float g = gain_factors[gain_]; + float t = GetItFactor(it_); + return base_resolution / (g * t); +} + +// ============================================================================ +// 非线性校正(Vishay应用笔记推荐的多项式补偿) +// 当 Lux > 1000 时,传感器响应偏离线性,需要校正 +// ============================================================================ +float VEML7700::CorrectLux(float lux) const { + // Vishay官方多项式校正公式(Horner法) + // lux_corrected = lux * (1.0023 + lux * (8.1488e-5 + lux * (-9.3924e-9 + lux * 6.0135e-13))) + return lux * (1.0023f + lux * (8.1488e-5f + lux * (-9.3924e-9f + lux * 6.0135e-13f))); +} + +esp_err_t VEML7700::ReadALSLux(float* lux) { + uint16_t raw = 0; + esp_err_t ret = ReadALSRaw(&raw); + if (ret != ESP_OK) { + *lux = 0; + return ret; + } + float raw_lux = (float)raw * GetResolution(); + *lux = CorrectLux(raw_lux); + return ESP_OK; +} + +esp_err_t VEML7700::ReadWhiteLux(float* lux) { + uint16_t raw = 0; + esp_err_t ret = ReadWhiteRaw(&raw); + if (ret != ESP_OK) { + *lux = 0; + return ret; + } + float raw_lux = (float)raw * GetResolution(); + *lux = CorrectLux(raw_lux); + return ESP_OK; +} + +esp_err_t VEML7700::ReadAll(veml7700_data_t* data) { + esp_err_t ret = ReadALSRaw(&data->als_raw); + if (ret != ESP_OK) return ret; + + ret = ReadWhiteRaw(&data->white_raw); + if (ret != ESP_OK) return ret; + + float resolution = GetResolution(); + float als_raw_lux = (float)data->als_raw * resolution; + float white_raw_lux = (float)data->white_raw * resolution; + data->als_lux = CorrectLux(als_raw_lux); + data->white_lux = CorrectLux(white_raw_lux); + + return ESP_OK; +} + +// ============================================================================ +// 中断状态读取 +// ============================================================================ +esp_err_t VEML7700::ReadInterruptStatus(bool* high_triggered, bool* low_triggered) { + uint16_t status = 0; + esp_err_t ret = ReadReg16(VEML7700_REG_ALS_INT, &status); + if (ret != ESP_OK) return ret; + + *high_triggered = (status >> 14) & 0x01; + *low_triggered = (status >> 15) & 0x01; + return ESP_OK; +} + +// ============================================================================ +// 获取采样等待时间 +// ============================================================================ +uint32_t VEML7700::GetSampleDelayMs() const { + return GetItDelayMs(it_); +} + +// ============================================================================ +// 自动量程实现 +// 算法参考 Vishay 应用笔记和 tedyapo/arduino-VEML7700 驱动 +// +// 策略:从高灵敏度(长积分+高增益)开始测量 +// - 如果计数值太高(>10000,接近饱和),降低积分时间 +// - 如果计数值太低(<200,精度不够),提高灵敏度 +// - 找到计数值在 200~10000 之间的最优参数组合 +// +// 增益遍历顺序(灵敏度从高到低): x2 → x1 → x(1/4) → x(1/8) +// 积分时间遍历顺序(从100ms开始往上探,再往下降) +// ============================================================================ + +// 增益搜索顺序:灵敏度从高到低 +static const veml7700_gain_t kGainOrder[] = { + VEML7700_GAIN_2, VEML7700_GAIN_1, VEML7700_GAIN_D4, VEML7700_GAIN_D8 +}; +static const int kGainOrderCount = sizeof(kGainOrder) / sizeof(kGainOrder[0]); + +// 积分时间搜索顺序:从100ms开始向上到800ms +static const veml7700_it_t kItOrderUp[] = { + VEML7700_IT_100MS, VEML7700_IT_200MS, VEML7700_IT_400MS, VEML7700_IT_800MS +}; +static const int kItOrderUpCount = sizeof(kItOrderUp) / sizeof(kItOrderUp[0]); + +// 积分时间搜索顺序:从100ms开始向下到25ms +static const veml7700_it_t kItOrderDown[] = { + VEML7700_IT_100MS, VEML7700_IT_50MS, VEML7700_IT_25MS +}; +static const int kItOrderDownCount = sizeof(kItOrderDown) / sizeof(kItOrderDown[0]); + +// 自动量程计数阈值 +static const uint16_t kAutoCountLow = 200; // 低于此值精度不足 +static const uint16_t kAutoCountHigh = 10000; // 高于此值接近饱和 + +esp_err_t VEML7700::ApplyConfigAndWait(veml7700_gain_t gain, veml7700_it_t it) { + // 先关机 + shutdown_ = true; + gain_ = gain; + it_ = it; + esp_err_t ret = UpdateALSConf(); + if (ret != ESP_OK) return ret; + + // 开机 + shutdown_ = false; + ret = UpdateALSConf(); + if (ret != ESP_OK) return ret; + + // 等待 2 倍积分时间确保新数据就绪(datasheet 建议) + vTaskDelay(pdMS_TO_TICKS(GetItDelayMs(it) * 2 + 10)); + return ESP_OK; +} + +esp_err_t VEML7700::AutoRangeMeasure(ReadRawFunc read_func, veml7700_auto_data_t* result) { + uint16_t counts = 0; + esp_err_t ret; + + // 阶段1:从中等灵敏度开始(100ms + x2增益),逐步提高积分时间 + for (int it_idx = 0; it_idx < kItOrderUpCount; it_idx++) { + for (int g_idx = 0; g_idx < kGainOrderCount; g_idx++) { + ret = ApplyConfigAndWait(kGainOrder[g_idx], kItOrderUp[it_idx]); + if (ret != ESP_OK) return ret; + + ret = (this->*read_func)(&counts); + if (ret != ESP_OK) return ret; + + // 计数值在合理范围内,直接返回 + if (counts >= kAutoCountLow && counts <= kAutoCountHigh) { + goto done; + } + + // 计数值太高,需要降低灵敏度 + if (counts > kAutoCountHigh) { + // 尝试降低积分时间 + goto reduce_sensitivity; + } + + // 计数值太低,尝试下一个更高的增益(循环继续) + } + } + + // 用最高灵敏度仍然太低,直接用当前结果(弱光环境) + goto done; + +reduce_sensitivity: + // 阶段2:计数值过高,逐步缩短积分时间 + for (int it_idx = 1; it_idx < kItOrderDownCount; it_idx++) { + ret = ApplyConfigAndWait(gain_, kItOrderDown[it_idx]); + if (ret != ESP_OK) return ret; + + ret = (this->*read_func)(&counts); + if (ret != ESP_OK) return ret; + + if (counts <= kAutoCountHigh) { + goto done; + } + } + + // 最短积分时间 + 当前增益仍然太高,降低增益 + for (int g_idx = 0; g_idx < kGainOrderCount; g_idx++) { + if (kGainOrder[g_idx] == gain_) continue; // 跳过当前增益 + // 只尝试更低灵敏度的增益 + float current_factor = gain_factors[gain_]; + float try_factor = gain_factors[kGainOrder[g_idx]]; + if (try_factor >= current_factor) continue; + + ret = ApplyConfigAndWait(kGainOrder[g_idx], VEML7700_IT_25MS); + if (ret != ESP_OK) return ret; + + ret = (this->*read_func)(&counts); + if (ret != ESP_OK) return ret; + + if (counts <= kAutoCountHigh) { + goto done; + } + } + + // 最低灵敏度仍然饱和,用当前结果(极强光环境) + +done: + result->raw = counts; + result->gain = gain_; + result->it = it_; + + float resolution = GetResolution(); + float raw_lux = (float)counts * resolution; + result->lux = CorrectLux(raw_lux); + + ESP_LOGI(TAG, "自动量程完成: gain=%d, it=%d, raw=%u, lux=%.2f", + gain_, it_, counts, result->lux); + + return ESP_OK; +} + +esp_err_t VEML7700::ReadAutoALSLux(veml7700_auto_data_t* result) { + return AutoRangeMeasure(&VEML7700::ReadALSRaw, result); +} + +esp_err_t VEML7700::ReadAutoWhiteLux(veml7700_auto_data_t* result) { + return AutoRangeMeasure(&VEML7700::ReadWhiteRaw, result); +} + +esp_err_t VEML7700::ReadAutoAll(veml7700_auto_data_t* als_result, veml7700_auto_data_t* white_result) { + // 先用ALS通道做自动量程找到最优参数 + esp_err_t ret = AutoRangeMeasure(&VEML7700::ReadALSRaw, als_result); + if (ret != ESP_OK) return ret; + + // 用同一组参数直接读取White通道(不再重新调参,避免重复等待) + uint16_t white_raw = 0; + ret = ReadWhiteRaw(&white_raw); + if (ret != ESP_OK) return ret; + + white_result->raw = white_raw; + white_result->gain = gain_; + white_result->it = it_; + float resolution = GetResolution(); + float raw_lux = (float)white_raw * resolution; + white_result->lux = CorrectLux(raw_lux); + + return ESP_OK; +} diff --git a/main/boards/common/veml7700.h b/main/boards/common/veml7700.h new file mode 100644 index 0000000..b3688ee --- /dev/null +++ b/main/boards/common/veml7700.h @@ -0,0 +1,158 @@ +#ifndef VEML7700_H +#define VEML7700_H + +#include "i2c_device.h" +#include "esp_log.h" +#include + +// VEML7700 I2C地址(7-bit) +#define VEML7700_I2C_ADDR 0x10 + +// VEML7700 寄存器地址(命令码) +#define VEML7700_REG_ALS_CONF 0x00 // ALS配置寄存器 +#define VEML7700_REG_ALS_WH 0x01 // 高阈值窗口设置 +#define VEML7700_REG_ALS_WL 0x02 // 低阈值窗口设置 +#define VEML7700_REG_PSM 0x03 // 省电模式配置 +#define VEML7700_REG_ALS 0x04 // ALS数据输出寄存器 +#define VEML7700_REG_WHITE 0x05 // 白光通道数据输出寄存器 +#define VEML7700_REG_ALS_INT 0x06 // 中断状态寄存器 + +// 增益设置(ALS_CONF[12:11]) +typedef enum { + VEML7700_GAIN_1 = 0x00, // 增益 x1 + VEML7700_GAIN_2 = 0x01, // 增益 x2 + VEML7700_GAIN_D8 = 0x02, // 增益 x(1/8) + VEML7700_GAIN_D4 = 0x03, // 增益 x(1/4) +} veml7700_gain_t; + +// 积分时间设置(ALS_CONF[9:6]) +typedef enum { + VEML7700_IT_25MS = 0x0C, // 25ms + VEML7700_IT_50MS = 0x08, // 50ms + VEML7700_IT_100MS = 0x00, // 100ms(默认) + VEML7700_IT_200MS = 0x01, // 200ms + VEML7700_IT_400MS = 0x02, // 400ms + VEML7700_IT_800MS = 0x03, // 800ms +} veml7700_it_t; + +// 持续保护次数设置(ALS_CONF[5:4]) +typedef enum { + VEML7700_PERS_1 = 0x00, // 1次 + VEML7700_PERS_2 = 0x01, // 2次 + VEML7700_PERS_4 = 0x02, // 4次 + VEML7700_PERS_8 = 0x03, // 8次 +} veml7700_pers_t; + +// 省电模式设置(PSM[2:1]) +typedef enum { + VEML7700_PSM_1 = 0x00, // 模式1(刷新700ms @ IT=100ms) + VEML7700_PSM_2 = 0x01, // 模式2(刷新1100ms @ IT=100ms) + VEML7700_PSM_3 = 0x02, // 模式3(刷新1800ms @ IT=100ms) + VEML7700_PSM_4 = 0x03, // 模式4(刷新3400ms @ IT=100ms) +} veml7700_psm_t; + +// VEML7700 读取数据结构 +typedef struct { + uint16_t als_raw; // ALS原始计数值 + uint16_t white_raw; // 白光通道原始计数值 + float als_lux; // ALS计算后的Lux值 + float white_lux; // 白光通道计算后的Lux值 +} veml7700_data_t; + +// 自动量程测量结果(包含最终使用的增益和积分时间) +typedef struct { + float lux; // 校正后的Lux值 + uint16_t raw; // 最终原始计数值 + veml7700_gain_t gain; // 自动选择的增益 + veml7700_it_t it; // 自动选择的积分时间 +} veml7700_auto_data_t; + +class VEML7700 : public I2cDevice { +public: + VEML7700(i2c_master_bus_handle_t i2c_bus, uint8_t addr = VEML7700_I2C_ADDR); + ~VEML7700() = default; + + // 初始化传感器,配置默认参数 + esp_err_t Init(); + + // 配置增益 + esp_err_t SetGain(veml7700_gain_t gain); + veml7700_gain_t GetGain() const { return gain_; } + + // 配置积分时间 + esp_err_t SetIntegrationTime(veml7700_it_t it); + veml7700_it_t GetIntegrationTime() const { return it_; } + + // 配置持续保护次数 + esp_err_t SetPersistence(veml7700_pers_t pers); + + // 省电模式 + esp_err_t SetPowerSaving(veml7700_psm_t mode, bool enable); + + // 中断使能/禁用 + esp_err_t SetInterruptEnable(bool enable); + + // 设置中断阈值 + esp_err_t SetThresholdHigh(uint16_t threshold); + esp_err_t SetThresholdLow(uint16_t threshold); + + // 关机/唤醒 + esp_err_t Shutdown(bool sd); + + // 读取原始数据 + esp_err_t ReadALSRaw(uint16_t* raw); + esp_err_t ReadWhiteRaw(uint16_t* raw); + + // 读取Lux值(带非线性校正) + esp_err_t ReadALSLux(float* lux); + esp_err_t ReadWhiteLux(float* lux); + + // 一次性读取所有数据 + esp_err_t ReadAll(veml7700_data_t* data); + + // 读取中断状态 + esp_err_t ReadInterruptStatus(bool* high_triggered, bool* low_triggered); + + // 获取当前配置下的采样等待时间(ms) + uint32_t GetSampleDelayMs() const; + + // ---- 自动量程接口 ---- + // 自动调节增益和积分时间,在任意光照条件下获取最优测量结果 + // 强光下自动降低灵敏度避免溢出,弱光下自动提高灵敏度获得精度 + // 注意:此函数会修改当前增益和积分时间设置,且耗时较长(最长约5秒) + esp_err_t ReadAutoALSLux(veml7700_auto_data_t* result); + esp_err_t ReadAutoWhiteLux(veml7700_auto_data_t* result); + + // 自动量程一次性读取ALS和White双通道 + esp_err_t ReadAutoAll(veml7700_auto_data_t* als_result, veml7700_auto_data_t* white_result); + +private: + veml7700_gain_t gain_; + veml7700_it_t it_; + veml7700_pers_t pers_; + bool int_enable_; + bool shutdown_; + uint16_t als_conf_; // 缓存的ALS配置寄存器值 + + // 16-bit 寄存器读写(VEML7700使用16-bit寄存器) + esp_err_t WriteReg16(uint8_t cmd, uint16_t value); + esp_err_t ReadReg16(uint8_t cmd, uint16_t* value); + + // 更新ALS配置寄存器 + esp_err_t UpdateALSConf(); + + // 根据当前增益和积分时间计算分辨率 + float GetResolution() const; + + // 非线性校正(高Lux值时的多项式补偿) + float CorrectLux(float lux) const; + + // 自动量程内部实现(通道类型由读取函数指针区分) + typedef esp_err_t (VEML7700::*ReadRawFunc)(uint16_t* raw); + esp_err_t AutoRangeMeasure(ReadRawFunc read_func, veml7700_auto_data_t* result); + + // 应用增益和积分时间并等待采样完成 + esp_err_t ApplyConfigAndWait(veml7700_gain_t gain, veml7700_it_t it); +}; + +#endif // VEML7700_H diff --git a/main/boards/common/wifi_board.cc b/main/boards/common/wifi_board.cc new file mode 100644 index 0000000..9001642 --- /dev/null +++ b/main/boards/common/wifi_board.cc @@ -0,0 +1,605 @@ +/** + * @file wifi_board.cc + * @brief WiFi板级管理模块实现文件 + * + * 本文件实现了WiFi板级管理的相关功能,包括WiFi连接管理、 + * BLE蓝牙配网流程控制、网络状态监控等核心功能。 + * 提供完整的网络连接解决方案实现。 + */ + +#include "wifi_board.h" + +#include "display.h" +#include "application.h" +#include "system_info.h" +#include "font_awesome_symbols.h" +#include "settings.h" +#include "assets/lang_config.h" +#include "bluetooth_provisioning.h" +#include "esp_bt.h" +#include "esp_bt_main.h" +#include "esp_netif_sntp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const char *TAG = "WifiBoard"; ///< 日志标签,用于标识WiFi板级模块的日志输出 + +/** + * @brief WiFi板级管理构造函数 + * + * 初始化WiFi板级管理对象,读取NVS存储中的配置参数。 + * 检查是否设置了强制AP模式标志,如果设置则重置为0。 + */ +WifiBoard::WifiBoard() { + // 读取NVS存储中的强制AP模式标志 + Settings settings("wifi", true); + wifi_config_mode_ = settings.GetInt("force_ap") == 1; + + // 如果检测到强制AP模式,重置为0并记录日志 + if (wifi_config_mode_) { + ESP_LOGI(TAG, "force_ap is set to 1, reset to 0"); + settings.SetInt("force_ap", 0); + } +} + +/** + * @brief 获取板级类型标识 + * @return std::string 返回"wifi"字符串,标识当前为WiFi板级 + */ +std::string WifiBoard::GetBoardType() { + return "wifi"; +} + +/** + * @brief 进入WiFi配置模式 + * + * 启动BLE蓝牙配网流程,等待用户通过手机APP配置WiFi信息。 + * 如果BLE配网启动失败,会持续重试直到成功。 + * 不再使用传统的WiFi AP配网模式。 + */ +void WifiBoard::EnterWifiConfigMode() { + ESP_LOGI(TAG, "🔵 进入配网模式 - 使用BLE蓝牙配网"); + + // 使用 BLE 蓝牙配网 + bool success = StartBleProvisioning(); + ESP_LOGI(TAG, "🔍 BLE配网启动结果: %s", success ? "成功" : "失败"); + + if (success) { + ESP_LOGI(TAG, "✅ BLE配网启动成功,等待手机连接"); + return; + } + + ESP_LOGW(TAG, "⚠️ BLE配网启动失败,将持续重试"); + + // 持续重试 + while (true) { + vTaskDelay(pdMS_TO_TICKS(5000)); + ESP_LOGI(TAG, "🔄 重试启动BLE配网..."); + if (StartBleProvisioning()) { + ESP_LOGI(TAG, "✅ BLE配网重试成功,等待手机连接"); + return; + } + ESP_LOGW(TAG, "❌ BLE配网重试失败,继续重试..."); + } + + // 以下代码保留但不会执行,用于将来可能重新启用WiFi AP配网 + //ESP_LOGI(TAG, "📶 启动WiFi AP配网模式,播放配网提示音(此代码已被禁用)"); + + auto& application = Application::GetInstance(); + application.SetDeviceState(kDeviceStateWifiConfiguring); + + auto& wifi_ap = WifiConfigurationAp::GetInstance(); + wifi_ap.SetLanguage(Lang::CODE); + wifi_ap.SetSsidPrefix("Airhub"); + wifi_ap.Start(); // 初始化AP模式射频 + + // 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL + std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT; + hint += wifi_ap.GetSsid(); + hint += Lang::Strings::ACCESS_VIA_BROWSER; + hint += wifi_ap.GetWebServerUrl(); + hint += "\n\n"; + // 播报配置 WiFi 的提示 + // application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "", Lang::Sounds::P3_WIFICONFIG); 原有蜡笔小新音色播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "", Lang::Sounds::P3_KAKA_WIFICONFIG); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "", Lang::Sounds::P3_LALA_WIFICONFIG); + } + + + + // Wait forever until reset after configuration + while (true) { + int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + int min_free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL); + ESP_LOGI(TAG, "Free internal: %u minimal internal: %u", free_sram, min_free_sram); + vTaskDelay(pdMS_TO_TICKS(10000)); + } +} + +/** + * @brief 启动网络连接 + * + * 根据配置启动WiFi连接或BLE配网流程。 + * 如果设置了配网模式或没有WiFi凭据,则启动BLE配网; + * 否则尝试连接已保存的WiFi网络。 + */ +void WifiBoard::StartNetwork() { + // 用户可以在启动时按BOOT按钮进入WiFi配置模式 + // 开机按BOOT进入配网模式 + if (wifi_config_mode_) { + ESP_LOGI(TAG, "🔵 进入配网模式 - BLE蓝牙配网"); + EnterWifiConfigMode(); + return; + } + + // 如果没有配置WiFi SSID,优先尝试BLE配网 + auto& ssid_manager = SsidManager::GetInstance(); // 获取SSID管理器实例 + auto ssid_list = ssid_manager.GetSsidList(); // 获取SSID列表 + if (ssid_list.empty()) { + ESP_LOGI(TAG, "🔍 未找到WiFi凭据,启动BLE配网..."); + if (StartBleProvisioning()) { + ESP_LOGI(TAG, "✅ BLE配网启动成功,等待手机连接..."); + return; + } else { + ESP_LOGW(TAG, "❌ BLE配网启动失败,将重试"); + vTaskDelay(pdMS_TO_TICKS(5000)); + ESP_LOGI(TAG, "🔄 重试启动BLE配网..."); + StartBleProvisioning(); + return; + } + } + + // WiFi凭据存在,尝试直接连接 + auto& wifi_station = WifiStation::GetInstance(); + + // 设置WiFi扫描开始回调 + wifi_station.OnScanBegin([this]() { + auto display = Board::GetInstance().GetDisplay(); + if (display) { + display->ShowNotification(Lang::Strings::SCANNING_WIFI, 30000); + } + }); + + // 设置WiFi连接开始回调 + wifi_station.OnConnect([this](const std::string& ssid) { + auto display = Board::GetInstance().GetDisplay(); + if (display) { + std::string notification = Lang::Strings::CONNECT_TO; + notification += ssid; + notification += "..."; + display->ShowNotification(notification.c_str(), 30000); + } + + // 根据标志决定是否播放网络连接语音提示 + auto& application = Application::GetInstance(); + if (!application.ShouldSkipDialogIdleSession()) { + // application.PlaySound(Lang::Sounds::P3_LIANJIEWANGLUO); 原有蜡笔小新 音色播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + application.PlaySound(Lang::Sounds::P3_KAKA_LIANJIEWANGLUO); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + application.PlaySound(Lang::Sounds::P3_LALA_LIANJIEWANGLUO); + } + ESP_LOGI(TAG, "Starting WiFi connection, playing network connection sound"); + } else { + ESP_LOGI(TAG, "Skipping network connection sound due to dialog idle restart flag"); + // 清除跳过标志,确保后续正常使用时能播放播报 + application.ClearDialogIdleSkipSession(); + } + }); + + // 设置WiFi连接成功回调 + wifi_station.OnConnected([this](const std::string& ssid) { + auto display = Board::GetInstance().GetDisplay(); + if (display) { + std::string notification = Lang::Strings::CONNECTED_TO; + notification += ssid; + display->ShowNotification(notification.c_str(), 30000); + } + }); + + wifi_station.OnReconnectTimeout([this]() { + auto& ws = WifiStation::GetInstance(); + ws.Stop(); + esp_wifi_restore(); + ResetWifiConfiguration(); + }); + + // 启动WiFi站点模式 + wifi_station.Start(); + + // 尝试连接WiFi,如果失败则尝试BLE配网 + // 尝试连接WiFi,如果失败则尝试BLE配网 + // 增加WiFi连接超时时间,避免过快进入配网模式 + // if (!wifi_station.WaitForConnected(90 * 1000)) { + if (!wifi_station.WaitForConnected(10 * 1000)) { + wifi_station.Stop();// 停止WiFi连接尝试 + esp_wifi_restore();// 恢复WiFi默认配置 + ResetWifiConfiguration();// 重置WiFi配置 + return; + } else { + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org"); + esp_netif_sntp_init(&config); + int retry = 0; + while (esp_netif_sntp_sync_wait(1000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < 5) {} + setenv("TZ", "CST-8", 1); + tzset(); + } +} + +/** + * @brief 创建HTTP客户端对象 + * @return Http* 返回ESP HTTP客户端对象指针 + */ +Http* WifiBoard::CreateHttp() { + return new EspHttp(); +} + +/** + * @brief 创建WebSocket客户端对象 + * @return WebSocket* 返回WebSocket客户端对象指针,如果未配置则返回nullptr + * + * 根据配置的WebSocket URL选择使用TLS或TCP传输协议 + */ +WebSocket* WifiBoard::CreateWebSocket() { +#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET + std::string url = CONFIG_WEBSOCKET_URL; + if (url.find("wss://") == 0) { + return new WebSocket(new TlsTransport()); // 使用TLS安全传输 + } else { + return new WebSocket(new TcpTransport()); // 使用TCP传输 + } +#endif + return nullptr; +} + +/** + * @brief 创建MQTT客户端对象 + * @return Mqtt* 返回ESP MQTT客户端对象指针 + */ +Mqtt* WifiBoard::CreateMqtt() { + return new EspMqtt(); +} + +Udp* WifiBoard::CreateUdp() { + return new EspUdp(); +} + +// 获取网络状态图标 +const char* WifiBoard::GetNetworkStateIcon() { + if (wifi_config_mode_) {// 如果是配网模式 + return FONT_AWESOME_WIFI;// 返回WiFi图标 + } + auto& wifi_station = WifiStation::GetInstance();// 获取WiFi配置实例 + if (!wifi_station.IsConnected()) {// 如果未连接到WiFi + return FONT_AWESOME_WIFI_OFF;// 返回WiFi断开图标 + } + int8_t rssi = wifi_station.GetRssi();// 获取WiFi信号强度 + if (rssi >= -60) { // 信号强度大于等于-60dBm + return FONT_AWESOME_WIFI;// 返回WiFi图标 + } else if (rssi >= -70) { + return FONT_AWESOME_WIFI_FAIR;// 返回WiFi信号中等图标 + } else { + return FONT_AWESOME_WIFI_WEAK;// 返回WiFi信号弱图标 + } +} + +// 获取板级JSON配置 +std::string WifiBoard::GetBoardJson() { + // Set the board type for OTA + auto& wifi_station = WifiStation::GetInstance(); + std::string board_json = std::string("{\"type\":\"" BOARD_TYPE "\",");// 板级JSON配置字符串,包含设备类型、名称、角色、SSID、信号强度、通道、IP地址和MAC地址 + board_json += "\"name\":\"" BOARD_NAME "\","; + board_json += "\"role\":\"" CONFIG_DEVICE_ROLE "\","; // 添加设备角色字段,用于OTA升级时的角色匹配 + if (!wifi_config_mode_) { + board_json += "\"ssid\":\"" + wifi_station.GetSsid() + "\","; + board_json += "\"rssi\":" + std::to_string(wifi_station.GetRssi()) + ","; + board_json += "\"channel\":" + std::to_string(wifi_station.GetChannel()) + ","; + board_json += "\"ip\":\"" + wifi_station.GetIpAddress() + "\","; + } + board_json += "\"mac\":\"" + SystemInfo::GetMacAddress() + "\"}"; + return board_json; +} + +// 设置低功耗模式 新增配网模式下禁用省电模式 +void WifiBoard::SetPowerSaveMode(bool enabled) { + // 如果正在进行 BLE 配网,强制禁用省电模式以确保 MAC 地址能正常发送到手机端 + if (enabled && IsBleProvisioningActive()) { + ESP_LOGI(TAG, "🔵 配网模式下,已强制禁用省电模式!"); + enabled = false; + } + ESP_LOGI(TAG, "🔋 电源管理模式切换: %s", enabled ? "启用低功耗模式" : "禁用低功耗模式(恢复正常模式)"); + + auto& wifi_station = WifiStation::GetInstance(); + wifi_station.SetPowerSaveMode(enabled); +} + +// 重置WiFi配置,设备将重启进入配网模式 +void WifiBoard::ResetWifiConfiguration() { + ESP_LOGI(TAG, "🔄 重置WiFi配置,设备将重启进入配网模式"); + // 设置WiFi配网标志位,确保重启后能正确进入配网模式 + { + Settings settings("wifi", true);// 创建WiFi配置设置对象,第二个参数true表示立即保存到NVS存储 + settings.SetInt("force_ap", 1);// 设置force_ap标志为1,这个标志会在设备重启后被检查,如果为1则启动WiFi配网服务,启动时强制进入AP配网模式 + } + + // 获取显示设备对象并显示配网提示信息 + auto display = GetDisplay(); + if (display) { + // 在屏幕上显示"进入WiFi配置模式"的多语言提示信息 + // 让用户知道设备即将重启并进入配网模式 + display->ShowNotification(Lang::Strings::ENTERING_WIFI_CONFIG_MODE); + } + vTaskDelay(pdMS_TO_TICKS(500)); // 等待500ms,确保NVS配置保存完成,如果有屏幕显示,可以增加到1000ms让用户看清提示 + ESP_LOGI(TAG, "🔄 正在重启设备..."); + esp_restart(); // 重启设备,重启后会进入配网模式 +} + +// 启动BLE配网服务 +bool WifiBoard::StartBleProvisioning() { + ESP_LOGI(TAG, "🔵 正在启动BLE蓝牙配网服务..."); + + Application::GetInstance().StopAudioProcessor();// 停止音频处理器,确保在配网过程中不处理音频数据 + Application::GetInstance().ClearAudioQueue();// 清空音频队列,移除所有待处理的音频数据 + + // 初始化BLE配网服务 + if (!bluetooth_provisioning_.Initialize()) { + ESP_LOGE(TAG, "❌ BLE蓝牙配网初始化失败"); + ESP_LOGI(TAG, "🔍 BLE Initialize返回结果: false"); + return false; + } + ESP_LOGI(TAG, "🔍 BLE Initialize返回结果: true"); + + // 为BLE事件设置回调函数 + bluetooth_provisioning_.SetCallback([this](BluetoothProvisioningEvent event, void* data) { + OnBleProvisioningEvent(event, data); + }); + + // 启动BLE配网服务(设备名称由 StartProvisioning 内部构建: Airhub_ + BLE MAC) + if (!bluetooth_provisioning_.StartProvisioning()) { + ESP_LOGE(TAG, "❌ BLE蓝牙配网启动失败"); + return false; + } + + ESP_LOGI(TAG, "✅ BLE蓝牙配网启动成功"); + ESP_LOGI(TAG, "📱 请使用支持BLE的手机APP连接设备进行配网"); + + ble_provisioning_active_ = true; // 标记BLE配网服务已激活 + ble_provisioning_success_ = false;// 标记BLE配网服务未成功 + ble_start_time_ = xTaskGetTickCount();// 记录启动时间,用于超时检测 + + // 显示BLE配网通知 + auto display = GetDisplay(); + if (display) { + display->ShowNotification("BLE配网模式", 30000); + } + + // 播放配网提示音 + auto& application = Application::GetInstance(); + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "", Lang::Sounds::P3_KAKA_WIFICONFIG); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "", Lang::Sounds::P3_LALA_WIFICONFIG); + } + + + + // 创建任务,用于监控BLE配网状态 + xTaskCreate([](void* param) { + WifiBoard* board = static_cast(param); // 转换参数为WifiBoard指针 + board->MonitorBleProvisioning();// 监控BLE配网状态 + vTaskDelete(nullptr);// 删除任务,因为任务只执行一次 + }, "ble_prov_monitor", 4096, this, 5, nullptr);// 创建任务,优先级为5,栈大小为4096字节 + + return true;// 启动成功,返回true +} + +// // BLE JSON Service 配网(暂不使用,保留代码) +// bool WifiBoard::StartBleJsonProvisioning() { +// ESP_LOGI(TAG, "🔵 正在启动BLE JSON配网服务..."); +// Application::GetInstance().StopAudioProcessor(); +// Application::GetInstance().ClearAudioQueue(); +// if (!ble_json_service_.Initialize()) { +// ESP_LOGE(TAG, "❌ BLE JSON服务初始化失败"); +// return false; +// } +// ble_json_service_.SetCommandCallback( +// [this](const std::string& cmd, int msg_id, cJSON* data) { +// Application::GetInstance().HandleBleJsonCommand(cmd, msg_id, data, ble_json_service_); +// }); +// if (!ble_json_service_.Start("Airhub_Ble")) { +// ESP_LOGE(TAG, "❌ BLE JSON服务启动失败"); +// return false; +// } +// ESP_LOGI(TAG, "✅ BLE JSON配网启动成功"); +// ble_provisioning_active_ = true; +// ble_start_time_ = xTaskGetTickCount(); +// auto display = GetDisplay(); +// if (display) { +// display->ShowNotification("BLE配网模式", 30000); +// } +// auto& application = Application::GetInstance(); +// if (strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0) { +// application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "", Lang::Sounds::P3_KAKA_WIFICONFIG); +// } else if (strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0) { +// application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "", Lang::Sounds::P3_LALA_WIFICONFIG); +// } +// while (true) { +// int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); +// int min_free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL); +// ESP_LOGI(TAG, "BLE配网等待中... Free internal: %u minimal internal: %u", free_sram, min_free_sram); +// vTaskDelay(pdMS_TO_TICKS(10000)); +// } +// return true; +// } + +// 监控BLE配网状态 +void WifiBoard::MonitorBleProvisioning() { + ESP_LOGI(TAG, "Starting BLE provisioning monitor..."); + + while (ble_provisioning_active_) { + TickType_t current_time = xTaskGetTickCount(); + TickType_t elapsed_time = current_time - ble_start_time_; + + // Check for timeout (2 minutes) - 仅记录日志,不再切换到WiFi配网 + if (elapsed_time >= pdMS_TO_TICKS(BLE_PROV_TIMEOUT_MS)) { + ESP_LOGW(TAG, "BLE provisioning timeout, but continuing BLE mode (no fallback to WiFi AP)"); + + // 增加延迟避免快速重新进入配网循环 + ESP_LOGI(TAG, "🔵 BLE配网超时,等待10秒后重置计时器继续等待配网"); + vTaskDelay(pdMS_TO_TICKS(10000)); // 等待10秒,冷却期 + + // 重置计时器,继续等待BLE配网 + ble_start_time_ = xTaskGetTickCount(); + ESP_LOGI(TAG, "🔵 计时器已重置,继续等待BLE配网"); + } + + // Check if provisioning was successful + if (ble_provisioning_success_) { + ESP_LOGI(TAG, "BLE provisioning completed successfully"); + ble_provisioning_active_ = false; + + // Stop BLE provisioning + // 停止BLE配网 + bluetooth_provisioning_.StopProvisioning(); + + // Try to connect to the configured WiFi + auto& wifi_station = WifiStation::GetInstance(); + wifi_station.Start(); + + // 增加WiFi连接重试逻辑,避免过快重新进入配网模式 + int retry_count = 0; // 重试次数 + const int max_retries = 3; // 最大重试次数 + const int retry_timeout = 60 * 1000; // 60秒超时 + + // 重试连接WiFi + while (retry_count < max_retries) { + ESP_LOGI(TAG, "WiFi connection attempt %d/%d after BLE provisioning", retry_count + 1, max_retries); + + // 等待WiFi连接成功 + if (wifi_station.WaitForConnected(retry_timeout)) { + ESP_LOGI(TAG, "WiFi connection successful after BLE provisioning (attempt %d)", retry_count + 1); + auto display = GetDisplay(); + if (display) { + display->ShowNotification("WiFi连接成功", 5000); + } + return; + } + + retry_count++;// 增加重试次数 + if (retry_count < max_retries) { + ESP_LOGW(TAG, "WiFi connection failed (attempt %d/%d), retrying in 10 seconds...", retry_count, max_retries); + vTaskDelay(pdMS_TO_TICKS(10000)); // 等待10秒后重试 + wifi_station.Stop(); + vTaskDelay(pdMS_TO_TICKS(2000)); // 等待2秒确保完全停止 + wifi_station.Start(); // 重新启动WiFi连接 + } else { + ESP_LOGW(TAG, "WiFi connection failed after %d attempts, entering AP mode", max_retries); + wifi_station.Stop(); + wifi_config_mode_ = true; + EnterWifiConfigMode(); + return; + } + } + } + + // Wait before next check + vTaskDelay(pdMS_TO_TICKS(1000)); // 等待1秒后检查 + } +} + +// 处理BLE配网事件 +void WifiBoard::OnBleProvisioningEvent(BluetoothProvisioningEvent event, void* data) { + switch (event) { + case BluetoothProvisioningEvent::CLIENT_CONNECTED: + ESP_LOGI(TAG, "BLE client connected"); + { + auto display = GetDisplay(); + if (display) { + display->ShowNotification("客户端已连接", 5000); + } + } + break; + + // 客户端断开事件 + case BluetoothProvisioningEvent::CLIENT_DISCONNECTED: + ESP_LOGI(TAG, "BLE client disconnected"); + { + auto display = GetDisplay(); + if (display) { + display->ShowNotification("客户端已断开", 5000); + } + } + break; + + // 接收WiFi凭据事件 + case BluetoothProvisioningEvent::WIFI_CREDENTIALS: + ESP_LOGI(TAG, "WiFi credentials received via BLE"); + { + auto display = GetDisplay(); + if (display) { + display->ShowNotification("WiFi凭据已接收", 5000); + } + } + break; + + // 连接成功事件 + case BluetoothProvisioningEvent::WIFI_CONNECTED: + ESP_LOGI(TAG, "设备配网成功,已连接到WiFi网络!"); + ble_provisioning_success_ = true; + { + auto display = GetDisplay(); + if (display) { + display->ShowNotification("WiFi连接成功", 5000); + } + auto& application = Application::GetInstance(); + // application.PlaySound(Lang::Sounds::P3_LIANJIEWANGLUO); 原有蜡笔小新 音色播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + application.PlaySound(Lang::Sounds::P3_KAKA_LIANJIEWANGLUO); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + application.PlaySound(Lang::Sounds::P3_LALA_LIANJIEWANGLUO); + } + } + break; + + // 连接失败事件 + case BluetoothProvisioningEvent::WIFI_FAILED: + ESP_LOGW(TAG, "WiFi connection failed via BLE"); + ble_provisioning_active_ = false; + { + auto display = GetDisplay(); + if (display) { + display->ShowNotification("WiFi连接失败", 5000); + } + } + break; + + default: + break; + } +} + +// BLE配网回调函数 +void WifiBoard::BleProvisioningCallback(BluetoothProvisioningEvent event, void* data, void* user_data) { + WifiBoard* board = static_cast(user_data); + if (board) { + board->OnBleProvisioningEvent(event, data); + } +} diff --git a/main/boards/common/wifi_board.h b/main/boards/common/wifi_board.h new file mode 100644 index 0000000..0add54b --- /dev/null +++ b/main/boards/common/wifi_board.h @@ -0,0 +1,144 @@ +#ifndef WIFI_BOARD_H +#define WIFI_BOARD_H + +/** + * @file wifi_board.h + * @brief WiFi板级管理模块头文件 + * + * 本文件定义了WiFi板级管理的相关接口,包括WiFi连接管理、 + * BLE蓝牙配网流程控制、网络状态监控等功能。 + * 集成了蓝牙配网功能,提供完整的网络连接解决方案。 + */ + +#include "board.h" +#include "bluetooth_provisioning.h" +// #include "ble_service.h" // BLE JSON Service 暂不使用 +#include +#include +#include + +// 前向声明 +class Application; + +/** + * @class WifiBoard + * @brief WiFi板级管理类 + * + * 继承自Board基类,负责管理ESP32的WiFi连接、BLE蓝牙配网流程和网络状态监控。 + * 提供完整的网络连接解决方案,包括自动连接、配网模式切换、网络状态监控等功能。 + */ +class WifiBoard : public Board { +protected: + bool wifi_config_mode_ = false; ///< WiFi配置模式标志,true表示进入配网模式 + bool ble_provisioning_active_ = false; ///< BLE配网激活状态标志 + bool ble_provisioning_success_ = false; ///< BLE配网成功状态标志 + TickType_t ble_start_time_ = 0; ///< BLE配网开始时间戳 + static const TickType_t BLE_PROV_TIMEOUT_MS = 300000; ///< BLE配网超时时间(5分钟) + BluetoothProvisioning bluetooth_provisioning_; ///< BLE蓝牙配网实例对象 + // BleJsonService ble_json_service_; ///< BLE JSON 配网服务实例(暂不使用) + + /** + * @brief 构造函数 + * 初始化WiFi板级管理对象,读取配置参数 + */ + WifiBoard(); + + /** + * @brief 进入WiFi配置模式 + * 启动BLE蓝牙配网流程,等待用户通过手机APP配置WiFi信息 + */ + void EnterWifiConfigMode(); + + /** + * @brief 广播验证码 + * @param code 验证码字符串 + * @param application 应用程序实例引用 + * 用于在配网过程中向用户显示验证码信息 + */ + void BroadcastVerificationCode(const std::string& code, Application& application); + + /** + * @brief 启动BLE蓝牙配网 + * @return true 启动成功 + * @return false 启动失败 + * 初始化并启动BLE蓝牙配网服务,等待手机连接 + */ + bool StartBleProvisioning(); + + // // 使用 BLE JSON Service 进行配网(暂不使用) + // bool StartBleJsonProvisioning(); + + /** + * @brief 监控BLE配网进程 + * 监控配网状态变化,处理超时和异常情况 + */ + void MonitorBleProvisioning(); + + /** + * @brief BLE配网事件处理函数 + * @param event 配网事件类型 + * @param data 事件数据指针 + * 处理BLE配网过程中的各种事件 + */ + void OnBleProvisioningEvent(BluetoothProvisioningEvent event, void* data); + + /** + * @brief BLE配网静态回调函数 + * @param event 配网事件类型 + * @param data 事件数据指针 + * @param user_data 用户数据指针 + * 静态回调函数,用于处理BLE配网事件 + */ + static void BleProvisioningCallback(BluetoothProvisioningEvent event, void* data, void* user_data); + + /** + * @brief 清理现有蓝牙服务 + * 在进入配网模式前,清理application.cc中启动的蓝牙服务,避免重复初始化 + */ + void CleanupExistingBluetoothService(); + + /** + * @brief 清理现有WiFi服务 + * 在进入配网模式前,清理现有的WiFi服务,为BLE重新初始化做准备 + */ + void CleanupExistingWiFiService(); + + /** + * @brief 获取板级配置JSON字符串 + * @return std::string 板级配置的JSON格式字符串 + * 重写基类方法,返回WiFi板级的配置信息 + */ + virtual std::string GetBoardJson() override; + +public: + /** + * @brief 获取板级类型 + * @return std::string 返回"wifi"字符串 + * 重写基类方法,标识当前板级为WiFi类型 + */ + virtual std::string GetBoardType() override; + + /** + * @brief 启动网络连接 + * 根据配置启动WiFi连接或BLE配网流程 + * 重写基类方法,实现WiFi网络的启动逻辑 + */ + virtual void StartNetwork() override; + + virtual Http* CreateHttp() override; + virtual WebSocket* CreateWebSocket() override; + virtual Mqtt* CreateMqtt() override; + virtual Udp* CreateUdp() override; + virtual const char* GetNetworkStateIcon() override; + virtual void SetPowerSaveMode(bool enabled) override; + virtual void ResetWifiConfiguration(); + + /** + * @brief 检查BLE配网是否激活 + * @return true BLE配网正在进行中 + * @return false BLE配网未激活 + */ + bool IsBleProvisioningActive() const { return ble_provisioning_active_; } +}; + +#endif // WIFI_BOARD_H diff --git a/main/boards/df-k10/README.md b/main/boards/df-k10/README.md new file mode 100644 index 0000000..310257b --- /dev/null +++ b/main/boards/df-k10/README.md @@ -0,0 +1,37 @@ +# DFRobot 行空板 K10 + +## 按键配置 +* A:短按-打断/唤醒,长按1s-音量调大 +* B:短按-打断/唤醒,长按1s-音量调小 + +## 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> DFRobot 行空板 K10 +``` + +**修改 psram 配置:** + +``` +Component config -> ESP PSRAM -> SPI RAM config -> Mode (QUAD/OCT) -> Octal Mode PSRAM +``` + +**编译:** + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/df-k10/config.h b/main/boards/df-k10/config.h new file mode 100644 index 0000000..a0eaa64 --- /dev/null +++ b/main/boards/df-k10/config.h @@ -0,0 +1,45 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_3 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_38 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_0 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_45 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_47 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_48 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR 0x23 + +#define BUILTIN_LED_GPIO GPIO_NUM_46 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +/* Expander */ +#define DRV_IO_EXP_INPUT_MASK (IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_12) + + +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/df-k10/config.json b/main/boards/df-k10/config.json new file mode 100644 index 0000000..55137d4 --- /dev/null +++ b/main/boards/df-k10/config.json @@ -0,0 +1,11 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "df-k10", + "sdkconfig_append": [ + "CONFIG_SPIRAM_MODE_OCT=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/df-k10/df_k10_board.cc b/main/boards/df-k10/df_k10_board.cc new file mode 100644 index 0000000..d18e034 --- /dev/null +++ b/main/boards/df-k10/df_k10_board.cc @@ -0,0 +1,255 @@ +#include "wifi_board.h" +#include "k10_audio_codec.h" +#include "display/lcd_display.h" +#include "esp_lcd_ili9341.h" +#include "font_awesome_symbols.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/circular_strip.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include + +#include "esp_io_expander_tca95xx_16bit.h" + +#define TAG "DF-K10" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class Df_K10Board : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + esp_io_expander_handle_t io_expander; + LcdDisplay *display_; + button_handle_t btn_a; + button_handle_t btn_b; + + 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, &i2c_bus_)); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_21; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_12; + 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)); + } + + esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) { + return esp_io_expander_set_level(io_expander, pin_mask, level); + } + + uint8_t IoExpanderGetLevel(uint16_t pin_mask) { + uint32_t pin_val = 0; + esp_io_expander_get_level(io_expander, DRV_IO_EXP_INPUT_MASK, &pin_val); + pin_mask &= DRV_IO_EXP_INPUT_MASK; + return (uint8_t)((pin_val & pin_mask) ? 1 : 0); + } + + void InitializeIoExpander() { + esp_io_expander_new_i2c_tca95xx_16bit( + i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_expander); + + esp_err_t ret; + ret = esp_io_expander_print_state(io_expander); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Print state failed: %s", esp_err_to_name(ret)); + } + ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0, + IO_EXPANDER_OUTPUT); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Set direction failed: %s", esp_err_to_name(ret)); + } + ret = esp_io_expander_set_level(io_expander, 0, 1); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Set level failed: %s", esp_err_to_name(ret)); + } + ret = esp_io_expander_set_dir( + io_expander, DRV_IO_EXP_INPUT_MASK, + IO_EXPANDER_INPUT); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Set direction failed: %s", esp_err_to_name(ret)); + } + } + void InitializeButtons() { + // Button A + button_config_t btn_a_config = { + .type = BUTTON_TYPE_CUSTOM, + .long_press_time = 1000, + .short_press_time = 50, + .custom_button_config = { + .active_level = 0, + .button_custom_init =nullptr, + .button_custom_get_key_value = [](void *param) -> uint8_t { + auto self = static_cast(param); + return self->IoExpanderGetLevel(IO_EXPANDER_PIN_NUM_2); + }, + .button_custom_deinit = nullptr, + .priv = this, + }, + }; + btn_a = iot_button_create(&btn_a_config); + iot_button_register_cb(btn_a, BUTTON_SINGLE_CLICK, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + self->ResetWifiConfiguration(); + } + app.ToggleChatState(); + }, this); + iot_button_register_cb(btn_a, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + auto codec = self->GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + self->GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }, this); + + // Button B + button_config_t btn_b_config = { + .type = BUTTON_TYPE_CUSTOM, + .long_press_time = 1000, + .short_press_time = 50, + .custom_button_config = { + .active_level = 0, + .button_custom_init =nullptr, + .button_custom_get_key_value = [](void *param) -> uint8_t { + auto self = static_cast(param); + return self->IoExpanderGetLevel(IO_EXPANDER_PIN_NUM_12); + }, + .button_custom_deinit = nullptr, + .priv = this, + }, + }; + btn_b = iot_button_create(&btn_b_config); + iot_button_register_cb(btn_b, BUTTON_SINGLE_CLICK, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + self->ResetWifiConfiguration(); + } + app.ToggleChatState(); + }, this); + iot_button_register_cb(btn_b, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + auto codec = self->GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + self->GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }, this); + } + + void InitializeIli9341Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_14; + io_config.dc_gpio_num = GPIO_NUM_13; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_NC; + panel_config.bits_per_pixel = 16; + panel_config.color_space = ESP_LCD_COLOR_SPACE_BGR; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); + + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto &thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + } + +public: + Df_K10Board() { + InitializeI2c(); + InitializeIoExpander(); + InitializeSpi(); + InitializeIli9341Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static CircularStrip led(BUILTIN_LED_GPIO, 3); + return &led; + } + + virtual AudioCodec *GetAudioCodec() override { + static K10AudioCodec audio_codec( + i2c_bus_, + 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, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display *GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(Df_K10Board); diff --git a/main/boards/df-k10/k10_audio_codec.cc b/main/boards/df-k10/k10_audio_codec.cc new file mode 100644 index 0000000..f93ab7a --- /dev/null +++ b/main/boards/df-k10/k10_audio_codec.cc @@ -0,0 +1,226 @@ +#include "k10_audio_codec.h" + +#include +#include +#include +#include + +static const char TAG[] = "K10AudioCodec"; + +K10AudioCodec::K10AudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + audio_codec_i2c_cfg_t i2c_cfg = { + .port = I2C_NUM_1, + .addr = es7210_addr, + .bus_handle = i2c_master_handle, + }; + const audio_codec_ctrl_if_t *in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(in_ctrl_if_ != NULL); + + es7243e_codec_cfg_t es7243e_cfg = { + .ctrl_if = in_ctrl_if_, + }; + const audio_codec_if_t *in_codec_if_ = es7243e_codec_new(&es7243e_cfg); + assert(in_codec_if_ != NULL); + + + esp_codec_dev_cfg_t codec_es7243e_dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_IN, + .codec_if = in_codec_if_, + .data_if = data_if_, + }; + input_dev_ = esp_codec_dev_new(&codec_es7243e_dev_cfg); + + assert(input_dev_ != NULL); + + ESP_LOGI(TAG, "DF-K10 AudioDevice initialized"); +} + +K10AudioCodec::~K10AudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void K10AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_MONO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + // .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + i2s_tdm_config_t tdm_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)input_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .bclk_div = 8, + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3), + .ws_width = I2S_TDM_AUTO_WS_WIDTH, + .ws_pol = false, + .bit_shift = true, + .left_align = false, + .big_endian = false, + .bit_order_lsb = false, + .skip_mask = false, + .total_slot = I2S_TDM_AUTO_SLOT_NUM + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = I2S_GPIO_UNUSED, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void K10AudioCodec::SetOutputVolume(int volume) { + AudioCodec::SetOutputVolume(volume); +} + +void K10AudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 4, + .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + if (input_reference_) { + fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); + } + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 37.5)); //麦克风增益解决收音太小的问题 + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void K10AudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + AudioCodec::SetOutputVolume(output_volume_); + AudioCodec::EnableOutput(enable); +} + +int K10AudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int K10AudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + std::vector buffer(samples * 2); // Allocate buffer for 2x samples + + // Apply volume adjustment (same as before) + int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536; + for (int i = 0; i < samples; i++) { + int64_t temp = int64_t(data[i]) * volume_factor; + if (temp > INT32_MAX) { + buffer[i * 2] = INT32_MAX; + } else if (temp < INT32_MIN) { + buffer[i * 2] = INT32_MIN; + } else { + buffer[i * 2] = static_cast(temp); + } + + // Repeat each sample for slow playback (assuming mono audio) + buffer[i * 2 + 1] = buffer[i * 2]; + } + + size_t bytes_written; + ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * 2 * sizeof(int32_t), &bytes_written, portMAX_DELAY)); + return bytes_written / sizeof(int32_t); + } + return samples; +} diff --git a/main/boards/df-k10/k10_audio_codec.h b/main/boards/df-k10/k10_audio_codec.h new file mode 100644 index 0000000..061adbe --- /dev/null +++ b/main/boards/df-k10/k10_audio_codec.h @@ -0,0 +1,37 @@ +#ifndef _BOX_AUDIO_CODEC_H +#define _BOX_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include + +class K10AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; + const audio_codec_if_t* out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; + const audio_codec_if_t* in_codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + K10AudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference); + virtual ~K10AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _BOX_AUDIO_CODEC_H diff --git a/main/boards/du-chatx/config.h b/main/boards/du-chatx/config.h new file mode 100644 index 0000000..729e1f4 --- /dev/null +++ b/main/boards/du-chatx/config.h @@ -0,0 +1,40 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_39 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_38 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_40 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_42 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_2 + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define TOUCH_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_9 +#define DISPLAY_MOSI_PIN GPIO_NUM_18 +#define DISPLAY_CLK_PIN GPIO_NUM_17 +#define DISPLAY_DC_PIN GPIO_NUM_8 +#define DISPLAY_RST_PIN GPIO_NUM_20 +#define DISPLAY_CS_PIN GPIO_NUM_16 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 160 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 2 +#define DISPLAY_OFFSET_Y 1 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/du-chatx/config.json b/main/boards/du-chatx/config.json new file mode 100644 index 0000000..e6abd31 --- /dev/null +++ b/main/boards/du-chatx/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "du-chatx", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/du-chatx/du-chatx-wifi.cc b/main/boards/du-chatx/du-chatx-wifi.cc new file mode 100644 index 0000000..58ccdf5 --- /dev/null +++ b/main/boards/du-chatx/du-chatx-wifi.cc @@ -0,0 +1,185 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "power_manager.h" +#include "power_save_timer.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TAG "DuChatX" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class DuChatX : public WifiBoard { +private: + Button boot_button_; + LcdDisplay *display_; + PowerManager *power_manager_; + PowerSaveTimer *power_save_timer_; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_6); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + void InitializePowerSaveTimer() { + rtc_gpio_init(GPIO_NUM_1); + rtc_gpio_set_direction(GPIO_NUM_1, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_1, 1); + + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + 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"); + rtc_gpio_set_level(GPIO_NUM_1, 0); + // 启用保持功能,确保睡眠期间电平不变 + rtc_gpio_hold_en(GPIO_NUM_1); + esp_lcd_panel_disp_on_off(panel_, false); //关闭显示 + esp_deep_sleep_start(); + }); + power_save_timer_->SetEnabled(true); + } + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_CLK_PIN; + 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)); + } + + void InitializeLcdDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel_ IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = DISPLAY_RGB_ORDER; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel_)); + esp_lcd_panel_reset(panel_); + esp_lcd_panel_init(panel_); + esp_lcd_panel_invert_color(panel_, DISPLAY_INVERT_COLOR); + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(), + }); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 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")); + } + +public: + DuChatX() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeSpi(); + InitializeLcdDisplay(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + InitializePowerSaveTimer(); + InitializePowerManager(); + } + + virtual Led *GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec *GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + 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); + } +}; + +DECLARE_BOARD(DuChatX); diff --git a/main/boards/du-chatx/power_manager.h b/main/boards/du-chatx/power_manager.h new file mode 100644 index 0000000..8438880 --- /dev/null +++ b/main/boards/du-chatx/power_manager.h @@ -0,0 +1,186 @@ +#pragma once +#include +#include + +#include +#include +#include + + +class PowerManager { +private: + esp_timer_handle_t timer_handle_; + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + + gpio_num_t charging_pin_ = GPIO_NUM_NC; + std::vector adc_values_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 60; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + + void CheckBatteryStatus() { + // Get charging status + bool new_charging_status = gpio_get_level(charging_pin_) == 1; + if (new_charging_status != is_charging_) { + is_charging_ = new_charging_status; + if (on_charging_status_changed_) { + on_charging_status_changed_(is_charging_); + } + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据不足,则读取电池电量数据 + if (adc_values_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_value; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_5, &adc_value)); + + // 将 ADC 值添加到队列中 + adc_values_.push_back(adc_value); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + uint32_t average_adc = 0; + for (auto value : adc_values_) { + average_adc += value; + } + average_adc /= adc_values_.size(); + + // 定义电池电量区间 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = { + {1120, 0}, + {1140, 20}, + {1160, 40}, + {1170, 60}, + {1190, 80}, + {1217, 100} + }; + + // 低于最低值时 + if (average_adc < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_adc >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) { + float ratio = static_cast(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (adc_values_.size() >= kBatteryAdcDataCount) { + bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; + if (new_low_battery_status != is_low_battery_) { + is_low_battery_ = new_low_battery_status; + if (on_low_battery_status_changed_) { + on_low_battery_status_changed_(is_low_battery_); + } + } + } + + ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); + } + +public: + PowerManager(gpio_num_t pin) : charging_pin_(pin) { + // 初始化充电引脚 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << charging_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + PowerManager* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_5, &chan_config)); + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + bool IsCharging() { + // 如果电量已经满了,则不再显示充电中 + if (battery_level_ == 100) { + return false; + } + return is_charging_; + } + + bool IsDischarging() { + // 没有区分充电和放电,所以直接返回相反状态 + return !is_charging_; + } + + uint8_t GetBatteryLevel() { + return battery_level_; + } + + void OnLowBatteryStatusChanged(std::function callback) { + on_low_battery_status_changed_ = callback; + } + + void OnChargingStatusChanged(std::function callback) { + on_charging_status_changed_ = callback; + } +}; diff --git a/main/boards/esp-box-3/config.h b/main/boards/esp-box-3/config.h new file mode 100644 index 0000000..f045304 --- /dev/null +++ b/main/boards/esp-box-3/config.h @@ -0,0 +1,41 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_2 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_45 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_17 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_16 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_15 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_46 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_8 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_18 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_47 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp-box-3/config.json b/main/boards/esp-box-3/config.json new file mode 100644 index 0000000..c7a455f --- /dev/null +++ b/main/boards/esp-box-3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp-box-3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp-box-3/esp_box3_board.cc b/main/boards/esp-box-3/esp_box3_board.cc new file mode 100644 index 0000000..b8d9eda --- /dev/null +++ b/main/boards/esp-box-3/esp_box3_board.cc @@ -0,0 +1,181 @@ +#include "wifi_board.h" +#include "audio_codecs/box_audio_codec.h" +#include "display/lcd_display.h" +#include "esp_lcd_ili9341.h" +#include "font_awesome_symbols.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include +#include +#include +#include + +#define TAG "EspBox3Board" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +// Init ili9341 by custom cmd +static const ili9341_lcd_init_cmd_t vendor_specific_init[] = { + {0xC8, (uint8_t []){0xFF, 0x93, 0x42}, 3, 0}, + {0xC0, (uint8_t []){0x0E, 0x0E}, 2, 0}, + {0xC5, (uint8_t []){0xD0}, 1, 0}, + {0xC1, (uint8_t []){0x02}, 1, 0}, + {0xB4, (uint8_t []){0x02}, 1, 0}, + {0xE0, (uint8_t []){0x00, 0x03, 0x08, 0x06, 0x13, 0x09, 0x39, 0x39, 0x48, 0x02, 0x0a, 0x08, 0x17, 0x17, 0x0F}, 15, 0}, + {0xE1, (uint8_t []){0x00, 0x28, 0x29, 0x01, 0x0d, 0x03, 0x3f, 0x33, 0x52, 0x04, 0x0f, 0x0e, 0x37, 0x38, 0x0F}, 15, 0}, + + {0xB1, (uint8_t []){00, 0x1B}, 2, 0}, + {0x36, (uint8_t []){0x08}, 1, 0}, + {0x3A, (uint8_t []){0x55}, 1, 0}, + {0xB7, (uint8_t []){0x06}, 1, 0}, + + {0x11, (uint8_t []){0}, 0x80, 0}, + {0x29, (uint8_t []){0}, 0x80, 0}, + + {0, (uint8_t []){0}, 0xff, 0}, +}; + +class EspBox3Board : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + + 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, &i2c_bus_)); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_6; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_7; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeIli9341Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_5; + io_config.dc_gpio_num = GPIO_NUM_4; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + const ili9341_vendor_config_t vendor_config = { + .init_cmds = &vendor_specific_init[0], + .init_cmds_size = sizeof(vendor_specific_init) / sizeof(ili9341_lcd_init_cmd_t), + }; + + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_48; + panel_config.flags.reset_active_high = 1, + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = (void *)&vendor_config; + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + esp_lcd_panel_disp_on_off(panel, true); + 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")); + } + +public: + EspBox3Board() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeIli9341Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec( + i2c_bus_, + 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, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + 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; + } +}; + +DECLARE_BOARD(EspBox3Board); diff --git a/main/boards/esp-box-lite/box_audio_codec_lite.cc b/main/boards/esp-box-lite/box_audio_codec_lite.cc new file mode 100644 index 0000000..6e0ddd0 --- /dev/null +++ b/main/boards/esp-box-lite/box_audio_codec_lite.cc @@ -0,0 +1,240 @@ +#include "box_audio_codec_lite.h" + +#include +#include +#include + +static const char TAG[] = "BoxAudioCodecLite"; + +BoxAudioCodecLite::BoxAudioCodecLite(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Output + audio_codec_i2c_cfg_t i2c_cfg = { + .port = (i2c_port_t)1, + .addr = ES8156_CODEC_DEFAULT_ADDR, + .bus_handle = i2c_master_handle, + }; + out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(out_ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8156_codec_cfg_t cfg = {}; + cfg.ctrl_if = out_ctrl_if_; + cfg.gpio_if = gpio_if_; + cfg.pa_pin = pa_pin; + cfg.hw_gain.pa_voltage = 5.0; + cfg.hw_gain.codec_dac_voltage = 3.3; + out_codec_if_ = es8156_codec_new(&cfg); + assert(out_codec_if_ != NULL); + + esp_codec_dev_cfg_t dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = out_codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&dev_cfg); + assert(output_dev_ != NULL); + + // Input + i2c_cfg.addr = ES7243E_CODEC_DEFAULT_ADDR; + in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(in_ctrl_if_ != NULL); + + es7243e_codec_cfg_t es7243_cfg = {}; + es7243_cfg.ctrl_if = in_ctrl_if_; + in_codec_if_ = es7243e_codec_new(&es7243_cfg); + assert(in_codec_if_ != NULL); + + dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; + dev_cfg.codec_if = in_codec_if_; + input_dev_ = esp_codec_dev_new(&dev_cfg); + assert(input_dev_ != NULL); + + ESP_LOGI(TAG, "BoxAudioDevice initialized"); +} + +BoxAudioCodecLite::~BoxAudioCodecLite() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void BoxAudioCodecLite::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + i2s_tdm_config_t tdm_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)input_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .bclk_div = 8, + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3), + .ws_width = I2S_TDM_AUTO_WS_WIDTH, + .ws_pol = false, + .bit_shift = true, + .left_align = false, + .big_endian = false, + .bit_order_lsb = false, + .skip_mask = false, + .total_slot = I2S_TDM_AUTO_SLOT_NUM + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = I2S_GPIO_UNUSED, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void BoxAudioCodecLite::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void BoxAudioCodecLite::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 4, + .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + if (input_reference_) { + fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); + } + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + // 麦克风增益解决收音太小的问题 + ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 37.5)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void BoxAudioCodecLite::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + // Play 16bit 1 channel + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + } + AudioCodec::EnableOutput(enable); +} + +int BoxAudioCodecLite::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int BoxAudioCodecLite::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} \ No newline at end of file diff --git a/main/boards/esp-box-lite/box_audio_codec_lite.h b/main/boards/esp-box-lite/box_audio_codec_lite.h new file mode 100644 index 0000000..646f398 --- /dev/null +++ b/main/boards/esp-box-lite/box_audio_codec_lite.h @@ -0,0 +1,37 @@ +#ifndef _BOX_AUDIO_CODEC_LITE_H +#define _BOX_AUDIO_CODEC_LITE_H + +#include "audio_codec.h" + +#include +#include + +class BoxAudioCodecLite : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; + const audio_codec_if_t* out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; + const audio_codec_if_t* in_codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + BoxAudioCodecLite(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, bool input_reference); + virtual ~BoxAudioCodecLite(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _BOX_AUDIO_CODEC_LITE_H diff --git a/main/boards/esp-box-lite/config.h b/main/boards/esp-box-lite/config.h new file mode 100644 index 0000000..82cde9c --- /dev/null +++ b/main/boards/esp-box-lite/config.h @@ -0,0 +1,39 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_2 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_47 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_17 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_16 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_15 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_46 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_8 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_18 + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_45 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp-box-lite/config.json b/main/boards/esp-box-lite/config.json new file mode 100644 index 0000000..a300437 --- /dev/null +++ b/main/boards/esp-box-lite/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp-box-lite", + "sdkconfig_append": ["CONFIG_SOC_ADC_SUPPORTED=y"] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp-box-lite/esp_box_lite_board.cc b/main/boards/esp-box-lite/esp_box_lite_board.cc new file mode 100644 index 0000000..c53533c --- /dev/null +++ b/main/boards/esp-box-lite/esp_box_lite_board.cc @@ -0,0 +1,253 @@ +#include "wifi_board.h" +#include "box_audio_codec_lite.h" +#include "display/lcd_display.h" +#include "esp_lcd_ili9341.h" +#include "font_awesome_symbols.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "assets/lang_config.h" +#include +#include +#include +#include +#include + +#define TAG "EspBoxBoardLite" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +/* ADC Buttons */ +typedef enum { + BSP_ADC_BUTTON_PREV, + BSP_ADC_BUTTON_ENTER, + BSP_ADC_BUTTON_NEXT, + BSP_ADC_BUTTON_NUM +} bsp_adc_button_t; + +// Init ili9341 by custom cmd +static const ili9341_lcd_init_cmd_t vendor_specific_init[] = { + {0xC8, (uint8_t []){0xFF, 0x93, 0x42}, 3, 0}, + {0xC0, (uint8_t []){0x0E, 0x0E}, 2, 0}, + {0xC5, (uint8_t []){0xD0}, 1, 0}, + {0xC1, (uint8_t []){0x02}, 1, 0}, + {0xB4, (uint8_t []){0x02}, 1, 0}, + {0xE0, (uint8_t []){0x00, 0x03, 0x08, 0x06, 0x13, 0x09, 0x39, 0x39, 0x48, 0x02, 0x0a, 0x08, 0x17, 0x17, 0x0F}, 15, 0}, + {0xE1, (uint8_t []){0x00, 0x28, 0x29, 0x01, 0x0d, 0x03, 0x3f, 0x33, 0x52, 0x04, 0x0f, 0x0e, 0x37, 0x38, 0x0F}, 15, 0}, + + {0xB1, (uint8_t []){00, 0x1B}, 2, 0}, + {0x36, (uint8_t []){0x08}, 1, 0}, + {0x3A, (uint8_t []){0x55}, 1, 0}, + {0xB7, (uint8_t []){0x06}, 1, 0}, + + {0x11, (uint8_t []){0}, 0x80, 0}, + {0x29, (uint8_t []){0}, 0x80, 0}, + + {0, (uint8_t []){0}, 0xff, 0}, +}; + +class EspBoxBoardLite : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Button boot_button_; + Button* adc_button_[BSP_ADC_BUTTON_NUM]; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + adc_oneshot_unit_handle_t bsp_adc_handle = NULL; +#endif + LcdDisplay* display_; + + 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, &i2c_bus_)); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_6; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_7; + 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)); + } + + void changeVol(int val) { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + val; + if (volume > 100) { + volume = 100; + } + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + } + + void TogleState() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + } + + void InitializeButtons() { + /* Initialize ADC esp-box lite的前三个按钮采用是的adc按钮,而非gpio */ + button_adc_config_t adc_cfg; + adc_cfg.adc_channel = ADC_CHANNEL_0; // ADC1 channel 0 is GPIO1 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + const adc_oneshot_unit_init_cfg_t init_config1 = { + .unit_id = ADC_UNIT_1, + }; + adc_oneshot_new_unit(&init_config1, &bsp_adc_handle); + adc_cfg.adc_handle = &bsp_adc_handle; +#endif + adc_cfg.button_index = BSP_ADC_BUTTON_PREV; + adc_cfg.min = 2310; // middle is 2410mV + adc_cfg.max = 2510; + adc_button_[0] = new Button(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_ENTER; + adc_cfg.min = 1880; // middle is 1980mV + adc_cfg.max = 2080; + adc_button_[1] = new Button(adc_cfg); + + adc_cfg.button_index = BSP_ADC_BUTTON_NEXT; + adc_cfg.min = 720; // middle is 820mV + adc_cfg.max = 920; + adc_button_[2] = new Button(adc_cfg); + + auto volume_up_button = adc_button_[BSP_ADC_BUTTON_NEXT]; + volume_up_button->OnClick([this]() {changeVol(10);}); + volume_up_button->OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + auto volume_down_button = adc_button_[BSP_ADC_BUTTON_PREV]; + volume_down_button->OnClick([this]() {changeVol(-10);}); + volume_down_button->OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + + auto break_button = adc_button_[BSP_ADC_BUTTON_ENTER]; + break_button->OnClick([this]() {TogleState();}); + boot_button_.OnClick([this]() {TogleState();}); + } + + void InitializeIli9341Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_5; + io_config.dc_gpio_num = GPIO_NUM_4; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + const ili9341_vendor_config_t vendor_config = { + .init_cmds = &vendor_specific_init[0], + .init_cmds_size = sizeof(vendor_specific_init) / sizeof(ili9341_lcd_init_cmd_t), + }; + + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_48; + panel_config.flags.reset_active_high = 0, + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = (void *)&vendor_config; + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(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); + esp_lcd_panel_disp_on_off(panel, true); + esp_lcd_panel_invert_color(panel, true); + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + EspBoxBoardLite() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeIli9341Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + ~EspBoxBoardLite() { + for (int i =0; i + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_2 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_47 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_17 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_16 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_15 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_46 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_8 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_18 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_45 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp-box/config.json b/main/boards/esp-box/config.json new file mode 100644 index 0000000..0ae7e20 --- /dev/null +++ b/main/boards/esp-box/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp-box", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp-box/esp_box_board.cc b/main/boards/esp-box/esp_box_board.cc new file mode 100644 index 0000000..eb16f19 --- /dev/null +++ b/main/boards/esp-box/esp_box_board.cc @@ -0,0 +1,177 @@ +#include "wifi_board.h" +#include "audio_codecs/box_audio_codec.h" +#include "display/lcd_display.h" +#include "esp_lcd_ili9341.h" +#include "font_awesome_symbols.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include +#include +#include +#include + +#define TAG "EspBoxBoard" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +// Init ili9341 by custom cmd +static const ili9341_lcd_init_cmd_t vendor_specific_init[] = { + {0xC8, (uint8_t []){0xFF, 0x93, 0x42}, 3, 0}, + {0xC0, (uint8_t []){0x0E, 0x0E}, 2, 0}, + {0xC5, (uint8_t []){0xD0}, 1, 0}, + {0xC1, (uint8_t []){0x02}, 1, 0}, + {0xB4, (uint8_t []){0x02}, 1, 0}, + {0xE0, (uint8_t []){0x00, 0x03, 0x08, 0x06, 0x13, 0x09, 0x39, 0x39, 0x48, 0x02, 0x0a, 0x08, 0x17, 0x17, 0x0F}, 15, 0}, + {0xE1, (uint8_t []){0x00, 0x28, 0x29, 0x01, 0x0d, 0x03, 0x3f, 0x33, 0x52, 0x04, 0x0f, 0x0e, 0x37, 0x38, 0x0F}, 15, 0}, + + {0xB1, (uint8_t []){00, 0x1B}, 2, 0}, + {0x36, (uint8_t []){0x08}, 1, 0}, + {0x3A, (uint8_t []){0x55}, 1, 0}, + {0xB7, (uint8_t []){0x06}, 1, 0}, + + {0x11, (uint8_t []){0}, 0x80, 0}, + {0x29, (uint8_t []){0}, 0x80, 0}, + + {0, (uint8_t []){0}, 0xff, 0}, +}; + +class EspBox3Board : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + + 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, &i2c_bus_)); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_6; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_7; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeIli9341Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_5; + io_config.dc_gpio_num = GPIO_NUM_4; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + const ili9341_vendor_config_t vendor_config = { + .init_cmds = &vendor_specific_init[0], + .init_cmds_size = sizeof(vendor_specific_init) / sizeof(ili9341_lcd_init_cmd_t), + }; + + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_48; + panel_config.flags.reset_active_high = 0, + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = (void *)&vendor_config; + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + esp_lcd_panel_disp_on_off(panel, true); + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + EspBox3Board() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeIli9341Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec( + i2c_bus_, + 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, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + 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; + } +}; + +DECLARE_BOARD(EspBox3Board); diff --git a/main/boards/esp-sparkbot/chassis.cc b/main/boards/esp-sparkbot/chassis.cc new file mode 100644 index 0000000..d970ad8 --- /dev/null +++ b/main/boards/esp-sparkbot/chassis.cc @@ -0,0 +1,98 @@ +/* + ESP-SparkBot 的底座 + https://gitee.com/esp-friends/esp_sparkbot/tree/master/example/tank/c2_tracked_chassis +*/ + +#include "sdkconfig.h" +#include "iot/thing.h" +#include "board.h" + +#include +#include +#include +#include + +#include "boards/esp-sparkbot/config.h" + +#define TAG "Chassis" + +namespace iot { + +class Chassis : public Thing { +private: + light_mode_t light_mode_ = LIGHT_MODE_ALWAYS_ON; + + void SendUartMessage(const char * command_str) { + uint8_t len = strlen(command_str); + uart_write_bytes(ECHO_UART_PORT_NUM, command_str, len); + ESP_LOGI(TAG, "Sent command: %s", command_str); + } + + void InitializeEchoUart() { + uart_config_t uart_config = { + .baud_rate = ECHO_UART_BAUD_RATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + int intr_alloc_flags = 0; + + ESP_ERROR_CHECK(uart_driver_install(ECHO_UART_PORT_NUM, BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags)); + ESP_ERROR_CHECK(uart_param_config(ECHO_UART_PORT_NUM, &uart_config)); + ESP_ERROR_CHECK(uart_set_pin(ECHO_UART_PORT_NUM, UART_ECHO_TXD, UART_ECHO_RXD, UART_ECHO_RTS, UART_ECHO_CTS)); + + SendUartMessage("w2"); + } + +public: + Chassis() : Thing("Chassis", "小机器人的底座:有履带可以移动;可以调整灯光效果"), light_mode_(LIGHT_MODE_ALWAYS_ON) { + InitializeEchoUart(); + + // 定义设备的属性 + properties_.AddNumberProperty("light_mode", "灯光效果编号", [this]() -> int { + return (light_mode_ - 2 <= 0) ? 1 : light_mode_ - 2; + }); + + // 定义设备可以被远程执行的指令 + methods_.AddMethod("GoForward", "向前走", ParameterList(), [this](const ParameterList& parameters) { + SendUartMessage("x0.0 y1.0"); + }); + + methods_.AddMethod("GoBack", "向后退", ParameterList(), [this](const ParameterList& parameters) { + SendUartMessage("x0.0 y-1.0"); + }); + + methods_.AddMethod("TurnLeft", "向左转", ParameterList(), [this](const ParameterList& parameters) { + SendUartMessage("x-1.0 y0.0"); + }); + + methods_.AddMethod("TurnRight", "向右转", ParameterList(), [this](const ParameterList& parameters) { + SendUartMessage("x1.0 y0.0"); + }); + + methods_.AddMethod("Dance", "跳舞", ParameterList(), [this](const ParameterList& parameters) { + SendUartMessage("d1"); + light_mode_ = LIGHT_MODE_MAX; + }); + + methods_.AddMethod("SwitchLightMode", "打开灯", ParameterList({ + Parameter("lightmode", "1到6之间的整数", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + char command_str[5] = {'w', 0, 0}; + char mode = static_cast(parameters["lightmode"].number()) + 2; + + ESP_LOGI(TAG, "Input Light Mode: %c", (mode + '0')); + + if (mode >= 3 && mode <= 8) { + command_str[1] = mode + '0'; + SendUartMessage(command_str); + } + }); + } +}; + +} // namespace iot + +DECLARE_THING(Chassis); diff --git a/main/boards/esp-sparkbot/config.h b/main/boards/esp-sparkbot/config.h new file mode 100644 index 0000000..b26cf16 --- /dev/null +++ b/main/boards/esp-sparkbot/config.h @@ -0,0 +1,71 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_45 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_41 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_39 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_40 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_42 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_46 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_4 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_5 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_DC_GPIO GPIO_NUM_43 +#define DISPLAY_CS_GPIO GPIO_NUM_44 +#define DISPLAY_CLK_GPIO GPIO_NUM_21 +#define DISPLAY_MOSI_GPIO GPIO_NUM_47 +#define DISPLAY_RST_GPIO GPIO_NUM_NC + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_46 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define UART_ECHO_TXD GPIO_NUM_38 +#define UART_ECHO_RXD GPIO_NUM_48 +#define UART_ECHO_RTS (-1) +#define UART_ECHO_CTS (-1) + +#define MOTOR_SPEED_MAX 100 +#define MOTOR_SPEED_80 80 +#define MOTOR_SPEED_60 60 +#define MOTOR_SPEED_MIN 0 + +#define ECHO_UART_PORT_NUM UART_NUM_1 +#define ECHO_UART_BAUD_RATE (115200) +#define BUF_SIZE (1024) + +typedef enum { + LIGHT_MODE_CHARGING_BREATH = 0, + LIGHT_MODE_POWER_LOW, + LIGHT_MODE_ALWAYS_ON, + LIGHT_MODE_BLINK, + LIGHT_MODE_WHITE_BREATH_SLOW, + LIGHT_MODE_WHITE_BREATH_FAST, + LIGHT_MODE_FLOWING, + LIGHT_MODE_SHOW, + LIGHT_MODE_SLEEP, + LIGHT_MODE_MAX +} light_mode_t; + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp-sparkbot/config.json b/main/boards/esp-sparkbot/config.json new file mode 100644 index 0000000..71ac417 --- /dev/null +++ b/main/boards/esp-sparkbot/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp-sparkbot", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp-sparkbot/esp_sparkbot_board.cc b/main/boards/esp-sparkbot/esp_sparkbot_board.cc new file mode 100644 index 0000000..ad780ed --- /dev/null +++ b/main/boards/esp-sparkbot/esp_sparkbot_board.cc @@ -0,0 +1,160 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "display/lcd_display.h" +#include "font_awesome_symbols.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include +#include +#include +#include + +#define TAG "esp_sparkbot" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class SparkBotEs8311AudioCodec : public Es8311AudioCodec { +private: + +public: + SparkBotEs8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk = true) + : Es8311AudioCodec(i2c_master_handle, i2c_port, input_sample_rate, output_sample_rate, + mclk, bclk, ws, dout, din,pa_pin, es8311_addr, use_mclk = true) {} + + void EnableOutput(bool enable) override { + if (enable == output_enabled_) { + return; + } + if (enable) { + Es8311AudioCodec::EnableOutput(enable); + } else { + // Nothing todo because the display io and PA io conflict + } + } +}; + +class EspSparkBot : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Button boot_button_; + Display* display_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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, &i2c_bus_)); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_GPIO; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_CLK_GPIO; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_GPIO; + io_config.dc_gpio_num = DISPLAY_DC_GPIO; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(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_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, true); + esp_lcd_panel_disp_on_off(panel, true); + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 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("Chassis")); + } + +public: + EspSparkBot() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeDisplay(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static SparkBotEs8311AudioCodec audio_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; + } +}; + +DECLARE_BOARD(EspSparkBot); diff --git a/main/boards/esp32-cgc/README.md b/main/boards/esp32-cgc/README.md new file mode 100644 index 0000000..b8e611e --- /dev/null +++ b/main/boards/esp32-cgc/README.md @@ -0,0 +1,46 @@ +# 主板开源地址: +[https://oshwhub.com/wdmomo/esp32-xiaozhi-kidpcb](https://oshwhub.com/wdmomo/esp32-xiaozhi-kidpcb) + +# 编译配置命令 + +**配置编译目标为 ESP32:** + +```bash +idf.py set-target esp32 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> ESP32 CGC +``` + +**选择屏幕类型:** + +``` +Xiaozhi Assistant -> LCD Type -> "ST7735, 分辨率128*128" +``` + +**修改 flash 大小:** + +``` +Serial flasher config -> Flash size -> 4 MB +``` + +**修改分区表:** + +``` +Partition Table -> Custom partition CSV file -> partitions_4M.csv +``` + +**编译:** + +```bash +idf.py build +``` diff --git a/main/boards/esp32-cgc/config.h b/main/boards/esp32-cgc/config.h new file mode 100644 index 0000000..289a2ca --- /dev/null +++ b/main/boards/esp32-cgc/config.h @@ -0,0 +1,268 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +// 如果使用 Duplex I2S 模式,请注释下面一行 +#define AUDIO_I2S_METHOD_SIMPLEX + +#ifdef AUDIO_I2S_METHOD_SIMPLEX + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_25 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_26 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_32 + +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_33 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_14 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_27 + +#else + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#endif + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define ASR_BUTTON_GPIO GPIO_NUM_13 + +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_4 +#define DISPLAY_SCLK_PIN GPIO_NUM_18 +#define DISPLAY_MOSI_PIN GPIO_NUM_23 +#define DISPLAY_CS_PIN GPIO_NUM_5 +#define DISPLAY_DC_PIN GPIO_NUM_2 +#define DISPLAY_RESET_PIN GPIO_NUM_NC + +#define DISPLAY_SPI_SCLK_HZ (20 * 1000 * 1000) + +#ifdef CONFIG_LCD_ST7789_240X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#endif + +#ifdef CONFIG_LCD_ST7789_240X320_NO_IPS +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_170X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 170 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 35 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_172X320 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 172 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 34 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X280 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 280 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 20 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X240 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X240_7PIN +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 2 +#endif + +#ifdef CONFIG_LCD_ST7789_240X135 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 135 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 40 +#define DISPLAY_OFFSET_Y 53 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7735_128X160 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 160 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#endif + +#ifdef CONFIG_LCD_ST7735_128X128 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 2 +#define DISPLAY_OFFSET_Y 3 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7796_320X480 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 480 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ILI9341_240X320 +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ILI9341_240X320_NO_IPS +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_GC9A01_240X240 +#define LCD_TYPE_GC9A01_SERIAL +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_CUSTOM +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp32-cgc/config.json b/main/boards/esp32-cgc/config.json new file mode 100644 index 0000000..f80a99d --- /dev/null +++ b/main/boards/esp32-cgc/config.json @@ -0,0 +1,13 @@ +{ + "target": "esp32", + "builds": [ + { + "name": "esp32-cgc", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_4M.csv\"", + "CONFIG_LCD_ST7735_128X128=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32-cgc/esp32_cgc_board.cc b/main/boards/esp32-cgc/esp32_cgc_board.cc new file mode 100644 index 0000000..6e45328 --- /dev/null +++ b/main/boards/esp32-cgc/esp32_cgc_board.cc @@ -0,0 +1,192 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(LCD_TYPE_ILI9341_SERIAL) +#include +#endif + +#if defined(LCD_TYPE_GC9A01_SERIAL) +#include +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb1, (uint8_t[]){0x80}, 1, 0}, + {0xb2, (uint8_t[]){0x27}, 1, 0}, + {0xb3, (uint8_t[]){0x13}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x05}, 1, 0}, + {0xac, (uint8_t[]){0xc8}, 1, 0}, + {0xab, (uint8_t[]){0x0f}, 1, 0}, + {0x3a, (uint8_t[]){0x05}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x08}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xea, (uint8_t[]){0x02}, 1, 0}, + {0xe8, (uint8_t[]){0x2A}, 1, 0}, + {0xe9, (uint8_t[]){0x47}, 1, 0}, + {0xe7, (uint8_t[]){0x5f}, 1, 0}, + {0xc6, (uint8_t[]){0x21}, 1, 0}, + {0xc7, (uint8_t[]){0x15}, 1, 0}, + {0xf0, + (uint8_t[]){0x1D, 0x38, 0x09, 0x4D, 0x92, 0x2F, 0x35, 0x52, 0x1E, 0x0C, + 0x04, 0x12, 0x14, 0x1f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x16, 0x40, 0x1C, 0x54, 0xA9, 0x2D, 0x2E, 0x56, 0x10, 0x0D, + 0x0C, 0x1A, 0x14, 0x1E}, + 14, 0}, + {0xf4, (uint8_t[]){0x00, 0x00, 0xFF}, 3, 0}, + {0xba, (uint8_t[]){0xFF, 0xFF}, 2, 0}, +}; +#endif + +#define TAG "ESP32_CGC" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + +class ESP32_CGC : public WifiBoard { +private: + Button boot_button_; + LcdDisplay* display_; + Button asr_button_; + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCLK_PIN; + 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)); + } + + void InitializeLcdDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = DISPLAY_SPI_MODE; + io_config.pclk_hz = DISPLAY_SPI_SCLK_HZ; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RESET_PIN; + panel_config.rgb_ele_order = DISPLAY_RGB_ORDER; + panel_config.bits_per_pixel = 16; +#if defined(LCD_TYPE_ILI9341_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); +#elif defined(LCD_TYPE_GC9A01_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(panel_io, &panel_config, &panel)); + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; +#else + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); +#endif + + esp_lcd_panel_reset(panel); + + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); +#ifdef LCD_TYPE_GC9A01_SERIAL + panel_config.vendor_config = &gc9107_vendor_config; +#endif + 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_14_1, + .icon_font = &font_awesome_14_1, + .emoji_font = font_emoji_32_init(), + }); + } + + + + void InitializeButtons() { + + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + + asr_button_.OnClick([this]() { + std::string wake_word="你好小智"; + Application::GetInstance().WakeWordInvoke(wake_word); + }); + + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + ESP32_CGC() : + boot_button_(BOOT_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) { + InitializeSpi(); + InitializeLcdDisplay(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override + { +#ifdef AUDIO_I2S_METHOD_SIMPLEX + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); +#else + static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); +#endif + 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; + } +}; + +DECLARE_BOARD(ESP32_CGC); diff --git a/main/boards/esp32-s3-touch-amoled-1.8/board_control.cc b/main/boards/esp32-s3-touch-amoled-1.8/board_control.cc new file mode 100644 index 0000000..b3f2163 --- /dev/null +++ b/main/boards/esp32-s3-touch-amoled-1.8/board_control.cc @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +#include "board.h" +#include "boards/common/wifi_board.h" +#include "boards/esp32-s3-touch-amoled-1.8/config.h" +#include "iot/thing.h" + +#define TAG "BoardControl" + +namespace iot { + +class BoardControl : public Thing { +public: + BoardControl() : Thing("BoardControl", "当前 AI 机器人管理和控制") { + // 修改重新配网 + methods_.AddMethod("ResetWifiConfiguration", "重新配网", ParameterList(), + [this](const ParameterList& parameters) { + ESP_LOGI(TAG, "ResetWifiConfiguration"); + auto board = static_cast(&Board::GetInstance()); + if (board && board->GetBoardType() == "wifi") { + board->ResetWifiConfiguration(); + } + }); + } +}; + +} // namespace iot + +DECLARE_THING(BoardControl); diff --git a/main/boards/esp32-s3-touch-amoled-1.8/config.h b/main/boards/esp32-s3-touch-amoled-1.8/config.h new file mode 100644 index 0000000..c904053 --- /dev/null +++ b/main/boards/esp32-s3-touch-amoled-1.8/config.h @@ -0,0 +1,41 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_16 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_45 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_9 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_8 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_46 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_15 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_14 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define I2C_ADDRESS ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define EXAMPLE_PIN_NUM_LCD_CS GPIO_NUM_12 +#define EXAMPLE_PIN_NUM_LCD_PCLK GPIO_NUM_11 +#define EXAMPLE_PIN_NUM_LCD_DATA0 GPIO_NUM_4 +#define EXAMPLE_PIN_NUM_LCD_DATA1 GPIO_NUM_5 +#define EXAMPLE_PIN_NUM_LCD_DATA2 GPIO_NUM_6 +#define EXAMPLE_PIN_NUM_LCD_DATA3 GPIO_NUM_7 +#define EXAMPLE_PIN_NUM_LCD_RST GPIO_NUM_NC +#define DISPLAY_WIDTH 368 +#define DISPLAY_HEIGHT 448 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp32-s3-touch-amoled-1.8/config.json b/main/boards/esp32-s3-touch-amoled-1.8/config.json new file mode 100644 index 0000000..6719497 --- /dev/null +++ b/main/boards/esp32-s3-touch-amoled-1.8/config.json @@ -0,0 +1,11 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp32-s3-touch-amoled-1.8", + "sdkconfig_append": [ + "CONFIG_USE_WECHAT_MESSAGE_STYLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-amoled-1.8/esp32-s3-touch-amoled-1.8.cc b/main/boards/esp32-s3-touch-amoled-1.8/esp32-s3-touch-amoled-1.8.cc new file mode 100644 index 0000000..0331ada --- /dev/null +++ b/main/boards/esp32-s3-touch-amoled-1.8/esp32-s3-touch-amoled-1.8.cc @@ -0,0 +1,313 @@ +#include "wifi_board.h" +#include "display/lcd_display.h" +#include "esp_lcd_sh8601.h" +#include "font_awesome_symbols.h" + +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "config.h" +#include "power_save_timer.h" +#include "axp2101.h" +#include "i2c_device.h" +#include + +#include +#include +#include +#include +#include "esp_io_expander_tca9554.h" +#include "settings.h" + +#define TAG "waveshare_amoled_1_8" + +LV_FONT_DECLARE(font_puhui_30_4); +LV_FONT_DECLARE(font_awesome_30_4); + +class Pmic : public Axp2101 { +public: + Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) { + WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable + WriteReg(0x27, 0x10); // hold 4s to power off + + // Disable All DCs but DC1 + WriteReg(0x80, 0x01); + // Disable All LDOs + WriteReg(0x90, 0x00); + WriteReg(0x91, 0x00); + + // Set DC1 to 3.3V + WriteReg(0x82, (3300 - 1500) / 100); + + // Set ALDO1 to 3.3V + WriteReg(0x92, (3300 - 500) / 100); + + // Enable ALDO1(MIC) + WriteReg(0x90, 0x01); + + WriteReg(0x64, 0x02); // CV charger voltage setting to 4.1V + + WriteReg(0x61, 0x02); // set Main battery precharge current to 50mA + WriteReg(0x62, 0x08); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA ) + WriteReg(0x63, 0x01); // set Main battery term charge current to 25mA + } +}; + +#define LCD_OPCODE_WRITE_CMD (0x02ULL) +#define LCD_OPCODE_READ_CMD (0x03ULL) +#define LCD_OPCODE_WRITE_COLOR (0x32ULL) + +static const sh8601_lcd_init_cmd_t vendor_specific_init[] = { + {0x11, (uint8_t[]){0x00}, 0, 120}, + {0x44, (uint8_t[]){0x01, 0xD1}, 2, 0}, + {0x35, (uint8_t[]){0x00}, 1, 0}, + {0x53, (uint8_t[]){0x20}, 1, 10}, + {0x2A, (uint8_t[]){0x00, 0x00, 0x01, 0x6F}, 4, 0}, + {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xBF}, 4, 0}, + {0x51, (uint8_t[]){0x00}, 1, 10}, + {0x29, (uint8_t[]){0x00}, 0, 10} +}; + +// 在waveshare_amoled_1_8类之前添加新的显示类 +class CustomLcdDisplay : public SpiLcdDisplay { +public: + CustomLcdDisplay(esp_lcd_panel_io_handle_t io_handle, + esp_lcd_panel_handle_t panel_handle, + int width, + int height, + int offset_x, + int offset_y, + bool mirror_x, + bool mirror_y, + bool swap_xy) + : SpiLcdDisplay(io_handle, panel_handle, + width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy, + { + .text_font = &font_puhui_30_4, + .icon_font = &font_awesome_30_4, +#if CONFIG_USE_WECHAT_MESSAGE_STYLE + .emoji_font = font_emoji_32_init(), +#else + .emoji_font = font_emoji_64_init(), +#endif + }) { + DisplayLockGuard lock(this); + lv_obj_set_style_pad_left(status_bar_, LV_HOR_RES * 0.1, 0); + lv_obj_set_style_pad_right(status_bar_, LV_HOR_RES * 0.1, 0); + } +}; + +class CustomBacklight : public Backlight { +public: + CustomBacklight(esp_lcd_panel_io_handle_t panel_io) : Backlight(), panel_io_(panel_io) {} + +protected: + esp_lcd_panel_io_handle_t panel_io_; + + virtual void SetBrightnessImpl(uint8_t brightness) override { + auto display = Board::GetInstance().GetDisplay(); + DisplayLockGuard lock(display); + uint8_t data[1] = {((uint8_t)((255 * brightness) / 100))}; + int lcd_cmd = 0x51; + lcd_cmd &= 0xff; + lcd_cmd <<= 8; + lcd_cmd |= LCD_OPCODE_WRITE_CMD << 24; + esp_lcd_panel_io_tx_param(panel_io_, lcd_cmd, &data, sizeof(data)); + } +}; + +class waveshare_amoled_1_8 : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Pmic* pmic_ = nullptr; + Button boot_button_; + CustomLcdDisplay* display_; + CustomBacklight* backlight_; + esp_io_expander_handle_t io_expander = NULL; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(20); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->OnShutdownRequest([this]() { + pmic_->PowerOff(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeTca9554(void) { + esp_err_t ret = esp_io_expander_new_i2c_tca9554(codec_i2c_bus_, I2C_ADDRESS, &io_expander); + if(ret != ESP_OK) + ESP_LOGE(TAG, "TCA9554 create returned error"); + ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 |IO_EXPANDER_PIN_NUM_2, IO_EXPANDER_OUTPUT); + ret |= esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_4, IO_EXPANDER_INPUT); + ESP_ERROR_CHECK(ret); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1|IO_EXPANDER_PIN_NUM_2, 1); + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(100)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1|IO_EXPANDER_PIN_NUM_2, 0); + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(300)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1|IO_EXPANDER_PIN_NUM_2, 1); + ESP_ERROR_CHECK(ret); + } + + void InitializeAxp2101() { + ESP_LOGI(TAG, "Init AXP2101"); + pmic_ = new Pmic(codec_i2c_bus_, 0x34); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.sclk_io_num = GPIO_NUM_11; + buscfg.data0_io_num = GPIO_NUM_4; + buscfg.data1_io_num = GPIO_NUM_5; + buscfg.data2_io_num = GPIO_NUM_6; + buscfg.data3_io_num = GPIO_NUM_7; + buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t); + buscfg.flags = SPICOMMON_BUSFLAG_QUAD; + ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeSH8601Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = SH8601_PANEL_IO_QSPI_CONFIG( + EXAMPLE_PIN_NUM_LCD_CS, + nullptr, + nullptr + ); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + const sh8601_vendor_config_t vendor_config = { + .init_cmds = &vendor_specific_init[0], + .init_cmds_size = sizeof(vendor_specific_init) / sizeof(sh8601_lcd_init_cmd_t), + .flags ={ + .use_qspi_interface = 1, + } + }; + + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_NC; + panel_config.flags.reset_active_high = 1, + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = (void *)&vendor_config; + ESP_ERROR_CHECK(esp_lcd_new_panel_sh8601(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_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + esp_lcd_panel_disp_on_off(panel, true); + display_ = new CustomLcdDisplay(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + backlight_ = new CustomBacklight(panel_io); + backlight_->RestoreBrightness(); + } + + // 物联网初始化,添加对 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")); + thing_manager.AddThing(iot::CreateThing("BoardControl")); + } + +public: + waveshare_amoled_1_8() : + boot_button_(BOOT_BUTTON_GPIO) { + InitializePowerSaveTimer(); + InitializeCodecI2c(); + InitializeTca9554(); + InitializeAxp2101(); + InitializeSpi(); + InitializeSH8601Display(); + InitializeButtons(); + InitializeIot(); + } + + 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 { + return backlight_; + } + + virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override { + static bool last_discharging = false; + charging = pmic_->IsCharging(); + discharging = pmic_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + + level = pmic_->GetBatteryLevel(); + return true; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(waveshare_amoled_1_8); diff --git a/main/boards/esp32-s3-touch-lcd-1.46/README.md b/main/boards/esp32-s3-touch-lcd-1.46/README.md new file mode 100644 index 0000000..0919b9d --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.46/README.md @@ -0,0 +1,4 @@ +新增 微雪 开发板: ESP32-S3-Touch-LCD-1.46、ESP32-S3-Touch-LCD-1.46B +产品链接: +https://www.waveshare.net/shop/ESP32-S3-Touch-LCD-1.46.htm +https://www.waveshare.net/shop/ESP32-S3-Touch-LCD-1.46B.htm \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-1.46/config.h b/main/boards/esp32-s3-touch-lcd-1.46/config.h new file mode 100644 index 0000000..b1bd2d9 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.46/config.h @@ -0,0 +1,70 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define PWR_BUTTON_GPIO GPIO_NUM_6 +#define PWR_Control_PIN GPIO_NUM_7 + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_2 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_15 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_47 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_48 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_38 + +#define I2C_SCL_IO GPIO_NUM_10 +#define I2C_SDA_IO GPIO_NUM_11 + + +#define I2C_ADDRESS ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000 + +#define DISPLAY_WIDTH 412 +#define DISPLAY_HEIGHT 412 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define QSPI_LCD_H_RES (412) +#define QSPI_LCD_V_RES (412) +#define QSPI_LCD_BIT_PER_PIXEL (16) + +#define QSPI_LCD_HOST SPI2_HOST +#define QSPI_PIN_NUM_LCD_PCLK GPIO_NUM_40 +#define QSPI_PIN_NUM_LCD_CS GPIO_NUM_21 +#define QSPI_PIN_NUM_LCD_DATA0 GPIO_NUM_46 +#define QSPI_PIN_NUM_LCD_DATA1 GPIO_NUM_45 +#define QSPI_PIN_NUM_LCD_DATA2 GPIO_NUM_42 +#define QSPI_PIN_NUM_LCD_DATA3 GPIO_NUM_41 +#define QSPI_PIN_NUM_LCD_RST GPIO_NUM_NC +#define QSPI_PIN_NUM_LCD_BL GPIO_NUM_5 + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define TP_PORT (I2C_NUM_1) +#define TP_PIN_NUM_SDA (I2C_SDA_IO) +#define TP_PIN_NUM_SCL (I2C_SCL_IO) +#define TP_PIN_NUM_RST (GPIO_NUM_NC) +#define TP_PIN_NUM_INT (GPIO_NUM_4) + +#define DISPLAY_BACKLIGHT_PIN QSPI_PIN_NUM_LCD_BL +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define TAIJIPI_SPD2010_PANEL_BUS_QSPI_CONFIG(sclk, d0, d1, d2, d3, max_trans_sz) \ + { \ + .data0_io_num = d0, \ + .data1_io_num = d1, \ + .sclk_io_num = sclk, \ + .data2_io_num = d2, \ + .data3_io_num = d3, \ + .max_transfer_sz = max_trans_sz, \ + } + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp32-s3-touch-lcd-1.46/config.json b/main/boards/esp32-s3-touch-lcd-1.46/config.json new file mode 100644 index 0000000..e7e5852 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.46/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp32-s3-touch-lcd-1.46", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-1.46/esp32-s3-touch-lcd-1.46.cc b/main/boards/esp32-s3-touch-lcd-1.46/esp32-s3-touch-lcd-1.46.cc new file mode 100644 index 0000000..eed8b23 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.46/esp32-s3-touch-lcd-1.46.cc @@ -0,0 +1,250 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include "i2c_device.h" +#include +#include +#include +#include +#include +#include +#include +#include "esp_io_expander_tca9554.h" +#include "lcd_display.h" +#include + +#define TAG "waveshare_lcd_1_46" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + + +// 在waveshare_lcd_1_46类之前添加新的显示类 +class CustomLcdDisplay : public SpiLcdDisplay { +public: + static void rounder_event_cb(lv_event_t * e) { + lv_area_t * area = (lv_area_t *)lv_event_get_param(e); + uint16_t x1 = area->x1; + uint16_t x2 = area->x2; + + area->x1 = (x1 >> 2) << 2; // round the start of coordinate down to the nearest 4M number + area->x2 = ((x2 >> 2) << 2) + 3; // round the end of coordinate up to the nearest 4N+3 number + } + + CustomLcdDisplay(esp_lcd_panel_io_handle_t io_handle, + esp_lcd_panel_handle_t panel_handle, + int width, + int height, + int offset_x, + int offset_y, + bool mirror_x, + bool mirror_y, + bool swap_xy) + : SpiLcdDisplay(io_handle, panel_handle, + width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_64_init(), + }) { + DisplayLockGuard lock(this); + lv_display_add_event_cb(display_, rounder_event_cb, LV_EVENT_INVALIDATE_AREA, NULL); + } +}; + +class CustomBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + esp_io_expander_handle_t io_expander = NULL; + LcdDisplay* display_; + button_handle_t boot_btn, pwr_btn; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = I2C_SDA_IO, + .scl_io_num = I2C_SCL_IO, + .clk_source = I2C_CLK_SRC_DEFAULT, + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); + } + + void InitializeTca9554(void) { + esp_err_t ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, I2C_ADDRESS, &io_expander); + if(ret != ESP_OK) + ESP_LOGE(TAG, "TCA9554 create returned error"); + + // uint32_t input_level_mask = 0; + // ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, IO_EXPANDER_INPUT); // 设置引脚 EXIO0 和 EXIO1 模式为输入 + // ret = esp_io_expander_get_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, &input_level_mask); // 获取引脚 EXIO0 和 EXIO1 的电平状态,存放在 input_level_mask 中 + + // ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, IO_EXPANDER_OUTPUT); // 设置引脚 EXIO2 和 EXIO3 模式为输出 + // ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, 1); // 将引脚电平设置为 1 + // ret = esp_io_expander_print_state(io_expander); // 打印引脚状态 + + ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, IO_EXPANDER_OUTPUT); // 设置引脚 EXIO0 和 EXIO1 模式为输出 + ESP_ERROR_CHECK(ret); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 1); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(300)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 0); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(300)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 1); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize QSPI bus"); + + const spi_bus_config_t bus_config = TAIJIPI_SPD2010_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK, + QSPI_PIN_NUM_LCD_DATA0, + QSPI_PIN_NUM_LCD_DATA1, + QSPI_PIN_NUM_LCD_DATA2, + QSPI_PIN_NUM_LCD_DATA3, + QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); + ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO)); + } + + void InitializeSpd2010Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + ESP_LOGI(TAG, "Install panel IO"); + + const esp_lcd_panel_io_spi_config_t io_config = SPD2010_PANEL_IO_QSPI_CONFIG(QSPI_PIN_NUM_LCD_CS, NULL, NULL); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)QSPI_LCD_HOST, &io_config, &panel_io)); + + ESP_LOGI(TAG, "Install SPD2010 panel driver"); + + spd2010_vendor_config_t vendor_config = { + .flags = { + .use_qspi_interface = 1, + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = QSPI_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // Implemented by LCD command `36h` + .bits_per_pixel = QSPI_LCD_BIT_PER_PIXEL, // Implemented by LCD command `3Ah` (16/18) + .vendor_config = &vendor_config, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_spd2010(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_disp_on_off(panel, true); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + display_ = new CustomLcdDisplay(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + void InitializeButtonsCustom() { + gpio_reset_pin(BOOT_BUTTON_GPIO); + gpio_set_direction(BOOT_BUTTON_GPIO, GPIO_MODE_INPUT); + gpio_reset_pin(PWR_BUTTON_GPIO); + gpio_set_direction(PWR_BUTTON_GPIO, GPIO_MODE_INPUT); + gpio_reset_pin(PWR_Control_PIN); + gpio_set_direction(PWR_Control_PIN, GPIO_MODE_OUTPUT); + // gpio_set_level(PWR_Control_PIN, false); + gpio_set_level(PWR_Control_PIN, true); + } + void InitializeButtons() { + InitializeButtonsCustom(); + button_config_t btns_config = { + .type = BUTTON_TYPE_CUSTOM, + .long_press_time = 2000, + .short_press_time = 50, + .custom_button_config = { + .active_level = 0, + .button_custom_init = nullptr, + .button_custom_get_key_value = [](void *param) -> uint8_t { + return gpio_get_level(BOOT_BUTTON_GPIO); + }, + .button_custom_deinit = nullptr, + .priv = this, + }, + }; + boot_btn = iot_button_create(&btns_config); + iot_button_register_cb(boot_btn, BUTTON_SINGLE_CLICK, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + self->ResetWifiConfiguration(); + } + app.ToggleChatState(); + }, this); + iot_button_register_cb(boot_btn, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) { + // 长按无处理 + }, this); + + btns_config.long_press_time = 5000; + btns_config.custom_button_config.button_custom_get_key_value = [](void *param) -> uint8_t { + return gpio_get_level(PWR_BUTTON_GPIO); + }; + pwr_btn = iot_button_create(&btns_config); + iot_button_register_cb(pwr_btn, BUTTON_SINGLE_CLICK, [](void* button_handle, void* usr_data) { + // auto self = static_cast(usr_data); // 以下程序实现供用户参考 ,实现单击pwr按键调整亮度 + // if(self->GetBacklight()->brightness() > 1) // 如果亮度不为0 + // self->GetBacklight()->SetBrightness(1); // 设置亮度为1 + // else + // self->GetBacklight()->RestoreBrightness(); // 恢复原本亮度 + // 短按无处理 + }, this); + iot_button_register_cb(pwr_btn, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + if(self->GetBacklight()->brightness() > 0) { + self->GetBacklight()->SetBrightness(0); + gpio_set_level(PWR_Control_PIN, false); + } + else { + self->GetBacklight()->RestoreBrightness(); + gpio_set_level(PWR_Control_PIN, true); + } + }, this); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + CustomBoard() { + InitializeI2c(); + InitializeTca9554(); + InitializeSpi(); + InitializeSpd2010Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, I2S_STD_SLOT_LEFT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN, I2S_STD_SLOT_RIGHT); // I2S_STD_SLOT_LEFT / I2S_STD_SLOT_RIGHT / I2S_STD_SLOT_BOTH + + 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; + } +}; + +DECLARE_BOARD(CustomBoard); diff --git a/main/boards/esp32-s3-touch-lcd-1.85/README.md b/main/boards/esp32-s3-touch-lcd-1.85/README.md new file mode 100644 index 0000000..df8a905 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85/README.md @@ -0,0 +1,3 @@ +新增 微雪 开发板: ESP32-S3-Touch-LCD-1.85 +产品链接: +https://www.waveshare.net/shop/ESP32-S3-Touch-LCD-1.85.htm \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-1.85/config.h b/main/boards/esp32-s3-touch-lcd-1.85/config.h new file mode 100644 index 0000000..7eff4c6 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85/config.h @@ -0,0 +1,69 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define PWR_BUTTON_GPIO GPIO_NUM_6 +#define PWR_Control_PIN GPIO_NUM_7 + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_2 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_15 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_47 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_48 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_38 + +#define I2C_SCL_IO GPIO_NUM_10 +#define I2C_SDA_IO GPIO_NUM_11 + +#define I2C_ADDRESS ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000 + +#define DISPLAY_WIDTH 360 +#define DISPLAY_HEIGHT 360 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define QSPI_LCD_H_RES (360) +#define QSPI_LCD_V_RES (360) +#define QSPI_LCD_BIT_PER_PIXEL (16) + +#define QSPI_LCD_HOST SPI2_HOST +#define QSPI_PIN_NUM_LCD_PCLK GPIO_NUM_40 +#define QSPI_PIN_NUM_LCD_CS GPIO_NUM_21 +#define QSPI_PIN_NUM_LCD_DATA0 GPIO_NUM_46 +#define QSPI_PIN_NUM_LCD_DATA1 GPIO_NUM_45 +#define QSPI_PIN_NUM_LCD_DATA2 GPIO_NUM_42 +#define QSPI_PIN_NUM_LCD_DATA3 GPIO_NUM_41 +#define QSPI_PIN_NUM_LCD_RST GPIO_NUM_NC +#define QSPI_PIN_NUM_LCD_BL GPIO_NUM_5 + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define TP_PORT (I2C_NUM_1) +#define TP_PIN_NUM_SDA (GPIO_NUM_1) +#define TP_PIN_NUM_SCL (GPIO_NUM_3) +#define TP_PIN_NUM_RST (GPIO_NUM_NC) +#define TP_PIN_NUM_INT (GPIO_NUM_4) + +#define DISPLAY_BACKLIGHT_PIN QSPI_PIN_NUM_LCD_BL +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(sclk, d0, d1, d2, d3, max_trans_sz) \ + { \ + .data0_io_num = d0, \ + .data1_io_num = d1, \ + .sclk_io_num = sclk, \ + .data2_io_num = d2, \ + .data3_io_num = d3, \ + .max_transfer_sz = max_trans_sz, \ + } + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp32-s3-touch-lcd-1.85/config.json b/main/boards/esp32-s3-touch-lcd-1.85/config.json new file mode 100644 index 0000000..63207b5 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp32-s3-touch-lcd-1.85", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-1.85/esp32-s3-touch-lcd-1.85.cc b/main/boards/esp32-s3-touch-lcd-1.85/esp32-s3-touch-lcd-1.85.cc new file mode 100644 index 0000000..4be53f9 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85/esp32-s3-touch-lcd-1.85.cc @@ -0,0 +1,467 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include "i2c_device.h" +#include +#include +#include +#include +#include +#include +#include +#include "esp_io_expander_tca9554.h" + +#define TAG "waveshare_lcd_1_85" + +#define LCD_OPCODE_WRITE_CMD (0x02ULL) +#define LCD_OPCODE_READ_CMD (0x0BULL) +#define LCD_OPCODE_WRITE_COLOR (0x32ULL) + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +static const st77916_lcd_init_cmd_t vendor_specific_init_new[] = { + {0xF0, (uint8_t []){0x28}, 1, 0}, + {0xF2, (uint8_t []){0x28}, 1, 0}, + {0x73, (uint8_t []){0xF0}, 1, 0}, + {0x7C, (uint8_t []){0xD1}, 1, 0}, + {0x83, (uint8_t []){0xE0}, 1, 0}, + {0x84, (uint8_t []){0x61}, 1, 0}, + {0xF2, (uint8_t []){0x82}, 1, 0}, + {0xF0, (uint8_t []){0x00}, 1, 0}, + {0xF0, (uint8_t []){0x01}, 1, 0}, + {0xF1, (uint8_t []){0x01}, 1, 0}, + {0xB0, (uint8_t []){0x56}, 1, 0}, + {0xB1, (uint8_t []){0x4D}, 1, 0}, + {0xB2, (uint8_t []){0x24}, 1, 0}, + {0xB4, (uint8_t []){0x87}, 1, 0}, + {0xB5, (uint8_t []){0x44}, 1, 0}, + {0xB6, (uint8_t []){0x8B}, 1, 0}, + {0xB7, (uint8_t []){0x40}, 1, 0}, + {0xB8, (uint8_t []){0x86}, 1, 0}, + {0xBA, (uint8_t []){0x00}, 1, 0}, + {0xBB, (uint8_t []){0x08}, 1, 0}, + {0xBC, (uint8_t []){0x08}, 1, 0}, + {0xBD, (uint8_t []){0x00}, 1, 0}, + {0xC0, (uint8_t []){0x80}, 1, 0}, + {0xC1, (uint8_t []){0x10}, 1, 0}, + {0xC2, (uint8_t []){0x37}, 1, 0}, + {0xC3, (uint8_t []){0x80}, 1, 0}, + {0xC4, (uint8_t []){0x10}, 1, 0}, + {0xC5, (uint8_t []){0x37}, 1, 0}, + {0xC6, (uint8_t []){0xA9}, 1, 0}, + {0xC7, (uint8_t []){0x41}, 1, 0}, + {0xC8, (uint8_t []){0x01}, 1, 0}, + {0xC9, (uint8_t []){0xA9}, 1, 0}, + {0xCA, (uint8_t []){0x41}, 1, 0}, + {0xCB, (uint8_t []){0x01}, 1, 0}, + {0xD0, (uint8_t []){0x91}, 1, 0}, + {0xD1, (uint8_t []){0x68}, 1, 0}, + {0xD2, (uint8_t []){0x68}, 1, 0}, + {0xF5, (uint8_t []){0x00, 0xA5}, 2, 0}, + {0xDD, (uint8_t []){0x4F}, 1, 0}, + {0xDE, (uint8_t []){0x4F}, 1, 0}, + {0xF1, (uint8_t []){0x10}, 1, 0}, + {0xF0, (uint8_t []){0x00}, 1, 0}, + {0xF0, (uint8_t []){0x02}, 1, 0}, + {0xE0, (uint8_t []){0xF0, 0x0A, 0x10, 0x09, 0x09, 0x36, 0x35, 0x33, 0x4A, 0x29, 0x15, 0x15, 0x2E, 0x34}, 14, 0}, + {0xE1, (uint8_t []){0xF0, 0x0A, 0x0F, 0x08, 0x08, 0x05, 0x34, 0x33, 0x4A, 0x39, 0x15, 0x15, 0x2D, 0x33}, 14, 0}, + {0xF0, (uint8_t []){0x10}, 1, 0}, + {0xF3, (uint8_t []){0x10}, 1, 0}, + {0xE0, (uint8_t []){0x07}, 1, 0}, + {0xE1, (uint8_t []){0x00}, 1, 0}, + {0xE2, (uint8_t []){0x00}, 1, 0}, + {0xE3, (uint8_t []){0x00}, 1, 0}, + {0xE4, (uint8_t []){0xE0}, 1, 0}, + {0xE5, (uint8_t []){0x06}, 1, 0}, + {0xE6, (uint8_t []){0x21}, 1, 0}, + {0xE7, (uint8_t []){0x01}, 1, 0}, + {0xE8, (uint8_t []){0x05}, 1, 0}, + {0xE9, (uint8_t []){0x02}, 1, 0}, + {0xEA, (uint8_t []){0xDA}, 1, 0}, + {0xEB, (uint8_t []){0x00}, 1, 0}, + {0xEC, (uint8_t []){0x00}, 1, 0}, + {0xED, (uint8_t []){0x0F}, 1, 0}, + {0xEE, (uint8_t []){0x00}, 1, 0}, + {0xEF, (uint8_t []){0x00}, 1, 0}, + {0xF8, (uint8_t []){0x00}, 1, 0}, + {0xF9, (uint8_t []){0x00}, 1, 0}, + {0xFA, (uint8_t []){0x00}, 1, 0}, + {0xFB, (uint8_t []){0x00}, 1, 0}, + {0xFC, (uint8_t []){0x00}, 1, 0}, + {0xFD, (uint8_t []){0x00}, 1, 0}, + {0xFE, (uint8_t []){0x00}, 1, 0}, + {0xFF, (uint8_t []){0x00}, 1, 0}, + {0x60, (uint8_t []){0x40}, 1, 0}, + {0x61, (uint8_t []){0x04}, 1, 0}, + {0x62, (uint8_t []){0x00}, 1, 0}, + {0x63, (uint8_t []){0x42}, 1, 0}, + {0x64, (uint8_t []){0xD9}, 1, 0}, + {0x65, (uint8_t []){0x00}, 1, 0}, + {0x66, (uint8_t []){0x00}, 1, 0}, + {0x67, (uint8_t []){0x00}, 1, 0}, + {0x68, (uint8_t []){0x00}, 1, 0}, + {0x69, (uint8_t []){0x00}, 1, 0}, + {0x6A, (uint8_t []){0x00}, 1, 0}, + {0x6B, (uint8_t []){0x00}, 1, 0}, + {0x70, (uint8_t []){0x40}, 1, 0}, + {0x71, (uint8_t []){0x03}, 1, 0}, + {0x72, (uint8_t []){0x00}, 1, 0}, + {0x73, (uint8_t []){0x42}, 1, 0}, + {0x74, (uint8_t []){0xD8}, 1, 0}, + {0x75, (uint8_t []){0x00}, 1, 0}, + {0x76, (uint8_t []){0x00}, 1, 0}, + {0x77, (uint8_t []){0x00}, 1, 0}, + {0x78, (uint8_t []){0x00}, 1, 0}, + {0x79, (uint8_t []){0x00}, 1, 0}, + {0x7A, (uint8_t []){0x00}, 1, 0}, + {0x7B, (uint8_t []){0x00}, 1, 0}, + {0x80, (uint8_t []){0x48}, 1, 0}, + {0x81, (uint8_t []){0x00}, 1, 0}, + {0x82, (uint8_t []){0x06}, 1, 0}, + {0x83, (uint8_t []){0x02}, 1, 0}, + {0x84, (uint8_t []){0xD6}, 1, 0}, + {0x85, (uint8_t []){0x04}, 1, 0}, + {0x86, (uint8_t []){0x00}, 1, 0}, + {0x87, (uint8_t []){0x00}, 1, 0}, + {0x88, (uint8_t []){0x48}, 1, 0}, + {0x89, (uint8_t []){0x00}, 1, 0}, + {0x8A, (uint8_t []){0x08}, 1, 0}, + {0x8B, (uint8_t []){0x02}, 1, 0}, + {0x8C, (uint8_t []){0xD8}, 1, 0}, + {0x8D, (uint8_t []){0x04}, 1, 0}, + {0x8E, (uint8_t []){0x00}, 1, 0}, + {0x8F, (uint8_t []){0x00}, 1, 0}, + {0x90, (uint8_t []){0x48}, 1, 0}, + {0x91, (uint8_t []){0x00}, 1, 0}, + {0x92, (uint8_t []){0x0A}, 1, 0}, + {0x93, (uint8_t []){0x02}, 1, 0}, + {0x94, (uint8_t []){0xDA}, 1, 0}, + {0x95, (uint8_t []){0x04}, 1, 0}, + {0x96, (uint8_t []){0x00}, 1, 0}, + {0x97, (uint8_t []){0x00}, 1, 0}, + {0x98, (uint8_t []){0x48}, 1, 0}, + {0x99, (uint8_t []){0x00}, 1, 0}, + {0x9A, (uint8_t []){0x0C}, 1, 0}, + {0x9B, (uint8_t []){0x02}, 1, 0}, + {0x9C, (uint8_t []){0xDC}, 1, 0}, + {0x9D, (uint8_t []){0x04}, 1, 0}, + {0x9E, (uint8_t []){0x00}, 1, 0}, + {0x9F, (uint8_t []){0x00}, 1, 0}, + {0xA0, (uint8_t []){0x48}, 1, 0}, + {0xA1, (uint8_t []){0x00}, 1, 0}, + {0xA2, (uint8_t []){0x05}, 1, 0}, + {0xA3, (uint8_t []){0x02}, 1, 0}, + {0xA4, (uint8_t []){0xD5}, 1, 0}, + {0xA5, (uint8_t []){0x04}, 1, 0}, + {0xA6, (uint8_t []){0x00}, 1, 0}, + {0xA7, (uint8_t []){0x00}, 1, 0}, + {0xA8, (uint8_t []){0x48}, 1, 0}, + {0xA9, (uint8_t []){0x00}, 1, 0}, + {0xAA, (uint8_t []){0x07}, 1, 0}, + {0xAB, (uint8_t []){0x02}, 1, 0}, + {0xAC, (uint8_t []){0xD7}, 1, 0}, + {0xAD, (uint8_t []){0x04}, 1, 0}, + {0xAE, (uint8_t []){0x00}, 1, 0}, + {0xAF, (uint8_t []){0x00}, 1, 0}, + {0xB0, (uint8_t []){0x48}, 1, 0}, + {0xB1, (uint8_t []){0x00}, 1, 0}, + {0xB2, (uint8_t []){0x09}, 1, 0}, + {0xB3, (uint8_t []){0x02}, 1, 0}, + {0xB4, (uint8_t []){0xD9}, 1, 0}, + {0xB5, (uint8_t []){0x04}, 1, 0}, + {0xB6, (uint8_t []){0x00}, 1, 0}, + {0xB7, (uint8_t []){0x00}, 1, 0}, + + {0xB8, (uint8_t []){0x48}, 1, 0}, + {0xB9, (uint8_t []){0x00}, 1, 0}, + {0xBA, (uint8_t []){0x0B}, 1, 0}, + {0xBB, (uint8_t []){0x02}, 1, 0}, + {0xBC, (uint8_t []){0xDB}, 1, 0}, + {0xBD, (uint8_t []){0x04}, 1, 0}, + {0xBE, (uint8_t []){0x00}, 1, 0}, + {0xBF, (uint8_t []){0x00}, 1, 0}, + {0xC0, (uint8_t []){0x10}, 1, 0}, + {0xC1, (uint8_t []){0x47}, 1, 0}, + {0xC2, (uint8_t []){0x56}, 1, 0}, + {0xC3, (uint8_t []){0x65}, 1, 0}, + {0xC4, (uint8_t []){0x74}, 1, 0}, + {0xC5, (uint8_t []){0x88}, 1, 0}, + {0xC6, (uint8_t []){0x99}, 1, 0}, + {0xC7, (uint8_t []){0x01}, 1, 0}, + {0xC8, (uint8_t []){0xBB}, 1, 0}, + {0xC9, (uint8_t []){0xAA}, 1, 0}, + {0xD0, (uint8_t []){0x10}, 1, 0}, + {0xD1, (uint8_t []){0x47}, 1, 0}, + {0xD2, (uint8_t []){0x56}, 1, 0}, + {0xD3, (uint8_t []){0x65}, 1, 0}, + {0xD4, (uint8_t []){0x74}, 1, 0}, + {0xD5, (uint8_t []){0x88}, 1, 0}, + {0xD6, (uint8_t []){0x99}, 1, 0}, + {0xD7, (uint8_t []){0x01}, 1, 0}, + {0xD8, (uint8_t []){0xBB}, 1, 0}, + {0xD9, (uint8_t []){0xAA}, 1, 0}, + {0xF3, (uint8_t []){0x01}, 1, 0}, + {0xF0, (uint8_t []){0x00}, 1, 0}, + {0x21, (uint8_t []){0x00}, 1, 0}, + {0x11, (uint8_t []){0x00}, 1, 120}, + {0x29, (uint8_t []){0x00}, 1, 0}, +}; +class CustomBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + esp_io_expander_handle_t io_expander = NULL; + LcdDisplay* display_; + button_handle_t boot_btn, pwr_btn; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = I2C_SDA_IO, + .scl_io_num = I2C_SCL_IO, + .clk_source = I2C_CLK_SRC_DEFAULT, + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); + } + + void InitializeTca9554(void) { + esp_err_t ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, I2C_ADDRESS, &io_expander); + if(ret != ESP_OK) + ESP_LOGE(TAG, "TCA9554 create returned error"); + + // uint32_t input_level_mask = 0; + // ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, IO_EXPANDER_INPUT); // 设置引脚 EXIO0 和 EXIO1 模式为输入 + // ret = esp_io_expander_get_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, &input_level_mask); // 获取引脚 EXIO0 和 EXIO1 的电平状态,存放在 input_level_mask 中 + + // ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, IO_EXPANDER_OUTPUT); // 设置引脚 EXIO2 和 EXIO3 模式为输出 + // ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, 1); // 将引脚电平设置为 1 + // ret = esp_io_expander_print_state(io_expander); // 打印引脚状态 + + ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, IO_EXPANDER_OUTPUT); // 设置引脚 EXIO0 和 EXIO1 模式为输出 + ESP_ERROR_CHECK(ret); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 1); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(300)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 0); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(300)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 1); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize QSPI bus"); + + const spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK, + QSPI_PIN_NUM_LCD_DATA0, + QSPI_PIN_NUM_LCD_DATA1, + QSPI_PIN_NUM_LCD_DATA2, + QSPI_PIN_NUM_LCD_DATA3, + QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); + ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO)); + } + + void Initializest77916Display() { + + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + ESP_LOGI(TAG, "Install panel IO"); + + esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = QSPI_PIN_NUM_LCD_CS, + .dc_gpio_num = -1, + .spi_mode = 0, + .pclk_hz = 3 * 1000 * 1000, + .trans_queue_depth = 10, + .on_color_trans_done = NULL, + .user_ctx = NULL, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .octal_mode = 0, + .quad_mode = 1, + .sio_mode = 0, + .lsb_first = 0, + .cs_high_active = 0, + }, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)QSPI_LCD_HOST, &io_config, &panel_io)); + + ESP_LOGI(TAG, "Install ST77916 panel driver"); + + st77916_vendor_config_t vendor_config = { + .flags = { + .use_qspi_interface = 1, + }, + }; + + printf("-------------------------------------- Version selection -------------------------------------- \r\n"); + esp_err_t ret; + int lcd_cmd = 0x04; + uint8_t register_data[4]; + size_t param_size = sizeof(register_data); + lcd_cmd &= 0xff; + lcd_cmd <<= 8; + lcd_cmd |= LCD_OPCODE_READ_CMD << 24; // Use the read opcode instead of write + ret = esp_lcd_panel_io_rx_param(panel_io, lcd_cmd, register_data, param_size); + if (ret == ESP_OK) { + printf("Register 0x04 data: %02x %02x %02x %02x\n", register_data[0], register_data[1], register_data[2], register_data[3]); + } else { + printf("Failed to read register 0x04, error code: %d\n", ret); + } + // panel_io_spi_del(io_handle); + io_config.pclk_hz = 80 * 1000 * 1000; + if (esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)QSPI_LCD_HOST, &io_config, &panel_io) != ESP_OK){ + printf("Failed to set LCD communication parameters -- SPI\r\n"); + return ; + } + printf("LCD communication parameters are set successfully -- SPI\r\n"); + + // Check register values and configure accordingly + if (register_data[0] == 0x00 && register_data[1] == 0x7F && register_data[2] == 0x7F && register_data[3] == 0x7F) { + // Handle the case where the register data matches this pattern + printf("Vendor-specific initialization for case 1.\n"); + } + else if (register_data[0] == 0x00 && register_data[1] == 0x02 && register_data[2] == 0x7F && register_data[3] == 0x7F) { + // Provide vendor-specific initialization commands if register data matches this pattern + vendor_config.init_cmds = vendor_specific_init_new; + vendor_config.init_cmds_size = sizeof(vendor_specific_init_new) / sizeof(st77916_lcd_init_cmd_t); + printf("Vendor-specific initialization for case 2.\n"); + } + printf("------------------------------------- End of version selection------------------------------------- \r\n"); + + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = QSPI_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // Implemented by LCD command `36h` + .bits_per_pixel = QSPI_LCD_BIT_PER_PIXEL, // Implemented by LCD command `3Ah` (16/18) + .vendor_config = &vendor_config, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st77916(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_disp_on_off(panel, true); + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_64_init(), + }); + } + + void InitializeButtonsCustom() { + gpio_reset_pin(BOOT_BUTTON_GPIO); + gpio_set_direction(BOOT_BUTTON_GPIO, GPIO_MODE_INPUT); + gpio_reset_pin(PWR_BUTTON_GPIO); + gpio_set_direction(PWR_BUTTON_GPIO, GPIO_MODE_INPUT); + gpio_reset_pin(PWR_Control_PIN); + gpio_set_direction(PWR_Control_PIN, GPIO_MODE_OUTPUT); + // gpio_set_level(PWR_Control_PIN, false); + gpio_set_level(PWR_Control_PIN, true); + } + void InitializeButtons() { + InitializeButtonsCustom(); + button_config_t btns_config = { + .type = BUTTON_TYPE_CUSTOM, + .long_press_time = 2000, + .short_press_time = 50, + .custom_button_config = { + .active_level = 0, + .button_custom_init = nullptr, + .button_custom_get_key_value = [](void *param) -> uint8_t { + return gpio_get_level(BOOT_BUTTON_GPIO); + }, + .button_custom_deinit = nullptr, + .priv = this, + }, + }; + boot_btn = iot_button_create(&btns_config); + iot_button_register_cb(boot_btn, BUTTON_SINGLE_CLICK, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + self->ResetWifiConfiguration(); + } + app.ToggleChatState(); + }, this); + iot_button_register_cb(boot_btn, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) { + // 长按无处理 + }, this); + + btns_config.long_press_time = 5000; + btns_config.custom_button_config.button_custom_get_key_value = [](void *param) -> uint8_t { + return gpio_get_level(PWR_BUTTON_GPIO); + }; + pwr_btn = iot_button_create(&btns_config); + iot_button_register_cb(pwr_btn, BUTTON_SINGLE_CLICK, [](void* button_handle, void* usr_data) { + // auto self = static_cast(usr_data); // 以下程序实现供用户参考 ,实现单击pwr按键调整亮度 + // if(self->GetBacklight()->brightness() > 1) // 如果亮度不为0 + // self->GetBacklight()->SetBrightness(1); // 设置亮度为1 + // else + // self->GetBacklight()->RestoreBrightness(); // 恢复原本亮度 + // 短按无处理 + }, this); + iot_button_register_cb(pwr_btn, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + if(self->GetBacklight()->brightness() > 0) { + self->GetBacklight()->SetBrightness(0); + gpio_set_level(PWR_Control_PIN, false); + } + else { + self->GetBacklight()->RestoreBrightness(); + gpio_set_level(PWR_Control_PIN, true); + } + }, this); + } + + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + CustomBoard() { + InitializeI2c(); + InitializeTca9554(); + InitializeSpi(); + Initializest77916Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, I2S_STD_SLOT_BOTH, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN, I2S_STD_SLOT_RIGHT); // I2S_STD_SLOT_LEFT / I2S_STD_SLOT_RIGHT / I2S_STD_SLOT_BOTH + + 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; + } +}; + +DECLARE_BOARD(CustomBoard); diff --git a/main/boards/esp32-s3-touch-lcd-1.85c/README.md b/main/boards/esp32-s3-touch-lcd-1.85c/README.md new file mode 100644 index 0000000..7668dec --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85c/README.md @@ -0,0 +1,3 @@ +新增 微雪 开发板: ESP32-S3-Touch-LCD-1.85C +产品链接: +https://www.waveshare.net/shop/ESP32-S3-Touch-LCD-1.85C.htm \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-1.85c/config.h b/main/boards/esp32-s3-touch-lcd-1.85c/config.h new file mode 100644 index 0000000..dfd5a89 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85c/config.h @@ -0,0 +1,67 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_2 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_15 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_47 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_48 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_38 + +#define I2C_SCL_IO GPIO_NUM_10 +#define I2C_SDA_IO GPIO_NUM_11 + +#define I2C_ADDRESS ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000 + +#define DISPLAY_WIDTH 360 +#define DISPLAY_HEIGHT 360 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define QSPI_LCD_H_RES (360) +#define QSPI_LCD_V_RES (360) +#define QSPI_LCD_BIT_PER_PIXEL (16) + +#define QSPI_LCD_HOST SPI2_HOST +#define QSPI_PIN_NUM_LCD_PCLK GPIO_NUM_40 +#define QSPI_PIN_NUM_LCD_CS GPIO_NUM_21 +#define QSPI_PIN_NUM_LCD_DATA0 GPIO_NUM_46 +#define QSPI_PIN_NUM_LCD_DATA1 GPIO_NUM_45 +#define QSPI_PIN_NUM_LCD_DATA2 GPIO_NUM_42 +#define QSPI_PIN_NUM_LCD_DATA3 GPIO_NUM_41 +#define QSPI_PIN_NUM_LCD_RST GPIO_NUM_NC +#define QSPI_PIN_NUM_LCD_BL GPIO_NUM_5 + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define TP_PORT (I2C_NUM_1) +#define TP_PIN_NUM_SDA (I2C_SDA_IO) +#define TP_PIN_NUM_SCL (I2C_SCL_IO) +#define TP_PIN_NUM_RST (GPIO_NUM_NC) +#define TP_PIN_NUM_INT (GPIO_NUM_4) + +#define DISPLAY_BACKLIGHT_PIN QSPI_PIN_NUM_LCD_BL +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(sclk, d0, d1, d2, d3, max_trans_sz) \ + { \ + .data0_io_num = d0, \ + .data1_io_num = d1, \ + .sclk_io_num = sclk, \ + .data2_io_num = d2, \ + .data3_io_num = d3, \ + .max_transfer_sz = max_trans_sz, \ + } + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp32-s3-touch-lcd-1.85c/config.json b/main/boards/esp32-s3-touch-lcd-1.85c/config.json new file mode 100644 index 0000000..1832799 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85c/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp32-s3-touch-lcd-1.85c", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-1.85c/esp32-s3-touch-lcd-1.85c.cc b/main/boards/esp32-s3-touch-lcd-1.85c/esp32-s3-touch-lcd-1.85c.cc new file mode 100644 index 0000000..35625e9 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-1.85c/esp32-s3-touch-lcd-1.85c.cc @@ -0,0 +1,413 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include "i2c_device.h" +#include +#include +#include +#include +#include +#include +#include +#include "esp_io_expander_tca9554.h" + +#define TAG "waveshare_lcd_1_85c" + +#define LCD_OPCODE_WRITE_CMD (0x02ULL) +#define LCD_OPCODE_READ_CMD (0x0BULL) +#define LCD_OPCODE_WRITE_COLOR (0x32ULL) + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +static const st77916_lcd_init_cmd_t vendor_specific_init_new[] = { + {0xF0, (uint8_t []){0x28}, 1, 0}, + {0xF2, (uint8_t []){0x28}, 1, 0}, + {0x73, (uint8_t []){0xF0}, 1, 0}, + {0x7C, (uint8_t []){0xD1}, 1, 0}, + {0x83, (uint8_t []){0xE0}, 1, 0}, + {0x84, (uint8_t []){0x61}, 1, 0}, + {0xF2, (uint8_t []){0x82}, 1, 0}, + {0xF0, (uint8_t []){0x00}, 1, 0}, + {0xF0, (uint8_t []){0x01}, 1, 0}, + {0xF1, (uint8_t []){0x01}, 1, 0}, + {0xB0, (uint8_t []){0x56}, 1, 0}, + {0xB1, (uint8_t []){0x4D}, 1, 0}, + {0xB2, (uint8_t []){0x24}, 1, 0}, + {0xB4, (uint8_t []){0x87}, 1, 0}, + {0xB5, (uint8_t []){0x44}, 1, 0}, + {0xB6, (uint8_t []){0x8B}, 1, 0}, + {0xB7, (uint8_t []){0x40}, 1, 0}, + {0xB8, (uint8_t []){0x86}, 1, 0}, + {0xBA, (uint8_t []){0x00}, 1, 0}, + {0xBB, (uint8_t []){0x08}, 1, 0}, + {0xBC, (uint8_t []){0x08}, 1, 0}, + {0xBD, (uint8_t []){0x00}, 1, 0}, + {0xC0, (uint8_t []){0x80}, 1, 0}, + {0xC1, (uint8_t []){0x10}, 1, 0}, + {0xC2, (uint8_t []){0x37}, 1, 0}, + {0xC3, (uint8_t []){0x80}, 1, 0}, + {0xC4, (uint8_t []){0x10}, 1, 0}, + {0xC5, (uint8_t []){0x37}, 1, 0}, + {0xC6, (uint8_t []){0xA9}, 1, 0}, + {0xC7, (uint8_t []){0x41}, 1, 0}, + {0xC8, (uint8_t []){0x01}, 1, 0}, + {0xC9, (uint8_t []){0xA9}, 1, 0}, + {0xCA, (uint8_t []){0x41}, 1, 0}, + {0xCB, (uint8_t []){0x01}, 1, 0}, + {0xD0, (uint8_t []){0x91}, 1, 0}, + {0xD1, (uint8_t []){0x68}, 1, 0}, + {0xD2, (uint8_t []){0x68}, 1, 0}, + {0xF5, (uint8_t []){0x00, 0xA5}, 2, 0}, + {0xDD, (uint8_t []){0x4F}, 1, 0}, + {0xDE, (uint8_t []){0x4F}, 1, 0}, + {0xF1, (uint8_t []){0x10}, 1, 0}, + {0xF0, (uint8_t []){0x00}, 1, 0}, + {0xF0, (uint8_t []){0x02}, 1, 0}, + {0xE0, (uint8_t []){0xF0, 0x0A, 0x10, 0x09, 0x09, 0x36, 0x35, 0x33, 0x4A, 0x29, 0x15, 0x15, 0x2E, 0x34}, 14, 0}, + {0xE1, (uint8_t []){0xF0, 0x0A, 0x0F, 0x08, 0x08, 0x05, 0x34, 0x33, 0x4A, 0x39, 0x15, 0x15, 0x2D, 0x33}, 14, 0}, + {0xF0, (uint8_t []){0x10}, 1, 0}, + {0xF3, (uint8_t []){0x10}, 1, 0}, + {0xE0, (uint8_t []){0x07}, 1, 0}, + {0xE1, (uint8_t []){0x00}, 1, 0}, + {0xE2, (uint8_t []){0x00}, 1, 0}, + {0xE3, (uint8_t []){0x00}, 1, 0}, + {0xE4, (uint8_t []){0xE0}, 1, 0}, + {0xE5, (uint8_t []){0x06}, 1, 0}, + {0xE6, (uint8_t []){0x21}, 1, 0}, + {0xE7, (uint8_t []){0x01}, 1, 0}, + {0xE8, (uint8_t []){0x05}, 1, 0}, + {0xE9, (uint8_t []){0x02}, 1, 0}, + {0xEA, (uint8_t []){0xDA}, 1, 0}, + {0xEB, (uint8_t []){0x00}, 1, 0}, + {0xEC, (uint8_t []){0x00}, 1, 0}, + {0xED, (uint8_t []){0x0F}, 1, 0}, + {0xEE, (uint8_t []){0x00}, 1, 0}, + {0xEF, (uint8_t []){0x00}, 1, 0}, + {0xF8, (uint8_t []){0x00}, 1, 0}, + {0xF9, (uint8_t []){0x00}, 1, 0}, + {0xFA, (uint8_t []){0x00}, 1, 0}, + {0xFB, (uint8_t []){0x00}, 1, 0}, + {0xFC, (uint8_t []){0x00}, 1, 0}, + {0xFD, (uint8_t []){0x00}, 1, 0}, + {0xFE, (uint8_t []){0x00}, 1, 0}, + {0xFF, (uint8_t []){0x00}, 1, 0}, + {0x60, (uint8_t []){0x40}, 1, 0}, + {0x61, (uint8_t []){0x04}, 1, 0}, + {0x62, (uint8_t []){0x00}, 1, 0}, + {0x63, (uint8_t []){0x42}, 1, 0}, + {0x64, (uint8_t []){0xD9}, 1, 0}, + {0x65, (uint8_t []){0x00}, 1, 0}, + {0x66, (uint8_t []){0x00}, 1, 0}, + {0x67, (uint8_t []){0x00}, 1, 0}, + {0x68, (uint8_t []){0x00}, 1, 0}, + {0x69, (uint8_t []){0x00}, 1, 0}, + {0x6A, (uint8_t []){0x00}, 1, 0}, + {0x6B, (uint8_t []){0x00}, 1, 0}, + {0x70, (uint8_t []){0x40}, 1, 0}, + {0x71, (uint8_t []){0x03}, 1, 0}, + {0x72, (uint8_t []){0x00}, 1, 0}, + {0x73, (uint8_t []){0x42}, 1, 0}, + {0x74, (uint8_t []){0xD8}, 1, 0}, + {0x75, (uint8_t []){0x00}, 1, 0}, + {0x76, (uint8_t []){0x00}, 1, 0}, + {0x77, (uint8_t []){0x00}, 1, 0}, + {0x78, (uint8_t []){0x00}, 1, 0}, + {0x79, (uint8_t []){0x00}, 1, 0}, + {0x7A, (uint8_t []){0x00}, 1, 0}, + {0x7B, (uint8_t []){0x00}, 1, 0}, + {0x80, (uint8_t []){0x48}, 1, 0}, + {0x81, (uint8_t []){0x00}, 1, 0}, + {0x82, (uint8_t []){0x06}, 1, 0}, + {0x83, (uint8_t []){0x02}, 1, 0}, + {0x84, (uint8_t []){0xD6}, 1, 0}, + {0x85, (uint8_t []){0x04}, 1, 0}, + {0x86, (uint8_t []){0x00}, 1, 0}, + {0x87, (uint8_t []){0x00}, 1, 0}, + {0x88, (uint8_t []){0x48}, 1, 0}, + {0x89, (uint8_t []){0x00}, 1, 0}, + {0x8A, (uint8_t []){0x08}, 1, 0}, + {0x8B, (uint8_t []){0x02}, 1, 0}, + {0x8C, (uint8_t []){0xD8}, 1, 0}, + {0x8D, (uint8_t []){0x04}, 1, 0}, + {0x8E, (uint8_t []){0x00}, 1, 0}, + {0x8F, (uint8_t []){0x00}, 1, 0}, + {0x90, (uint8_t []){0x48}, 1, 0}, + {0x91, (uint8_t []){0x00}, 1, 0}, + {0x92, (uint8_t []){0x0A}, 1, 0}, + {0x93, (uint8_t []){0x02}, 1, 0}, + {0x94, (uint8_t []){0xDA}, 1, 0}, + {0x95, (uint8_t []){0x04}, 1, 0}, + {0x96, (uint8_t []){0x00}, 1, 0}, + {0x97, (uint8_t []){0x00}, 1, 0}, + {0x98, (uint8_t []){0x48}, 1, 0}, + {0x99, (uint8_t []){0x00}, 1, 0}, + {0x9A, (uint8_t []){0x0C}, 1, 0}, + {0x9B, (uint8_t []){0x02}, 1, 0}, + {0x9C, (uint8_t []){0xDC}, 1, 0}, + {0x9D, (uint8_t []){0x04}, 1, 0}, + {0x9E, (uint8_t []){0x00}, 1, 0}, + {0x9F, (uint8_t []){0x00}, 1, 0}, + {0xA0, (uint8_t []){0x48}, 1, 0}, + {0xA1, (uint8_t []){0x00}, 1, 0}, + {0xA2, (uint8_t []){0x05}, 1, 0}, + {0xA3, (uint8_t []){0x02}, 1, 0}, + {0xA4, (uint8_t []){0xD5}, 1, 0}, + {0xA5, (uint8_t []){0x04}, 1, 0}, + {0xA6, (uint8_t []){0x00}, 1, 0}, + {0xA7, (uint8_t []){0x00}, 1, 0}, + {0xA8, (uint8_t []){0x48}, 1, 0}, + {0xA9, (uint8_t []){0x00}, 1, 0}, + {0xAA, (uint8_t []){0x07}, 1, 0}, + {0xAB, (uint8_t []){0x02}, 1, 0}, + {0xAC, (uint8_t []){0xD7}, 1, 0}, + {0xAD, (uint8_t []){0x04}, 1, 0}, + {0xAE, (uint8_t []){0x00}, 1, 0}, + {0xAF, (uint8_t []){0x00}, 1, 0}, + {0xB0, (uint8_t []){0x48}, 1, 0}, + {0xB1, (uint8_t []){0x00}, 1, 0}, + {0xB2, (uint8_t []){0x09}, 1, 0}, + {0xB3, (uint8_t []){0x02}, 1, 0}, + {0xB4, (uint8_t []){0xD9}, 1, 0}, + {0xB5, (uint8_t []){0x04}, 1, 0}, + {0xB6, (uint8_t []){0x00}, 1, 0}, + {0xB7, (uint8_t []){0x00}, 1, 0}, + + {0xB8, (uint8_t []){0x48}, 1, 0}, + {0xB9, (uint8_t []){0x00}, 1, 0}, + {0xBA, (uint8_t []){0x0B}, 1, 0}, + {0xBB, (uint8_t []){0x02}, 1, 0}, + {0xBC, (uint8_t []){0xDB}, 1, 0}, + {0xBD, (uint8_t []){0x04}, 1, 0}, + {0xBE, (uint8_t []){0x00}, 1, 0}, + {0xBF, (uint8_t []){0x00}, 1, 0}, + {0xC0, (uint8_t []){0x10}, 1, 0}, + {0xC1, (uint8_t []){0x47}, 1, 0}, + {0xC2, (uint8_t []){0x56}, 1, 0}, + {0xC3, (uint8_t []){0x65}, 1, 0}, + {0xC4, (uint8_t []){0x74}, 1, 0}, + {0xC5, (uint8_t []){0x88}, 1, 0}, + {0xC6, (uint8_t []){0x99}, 1, 0}, + {0xC7, (uint8_t []){0x01}, 1, 0}, + {0xC8, (uint8_t []){0xBB}, 1, 0}, + {0xC9, (uint8_t []){0xAA}, 1, 0}, + {0xD0, (uint8_t []){0x10}, 1, 0}, + {0xD1, (uint8_t []){0x47}, 1, 0}, + {0xD2, (uint8_t []){0x56}, 1, 0}, + {0xD3, (uint8_t []){0x65}, 1, 0}, + {0xD4, (uint8_t []){0x74}, 1, 0}, + {0xD5, (uint8_t []){0x88}, 1, 0}, + {0xD6, (uint8_t []){0x99}, 1, 0}, + {0xD7, (uint8_t []){0x01}, 1, 0}, + {0xD8, (uint8_t []){0xBB}, 1, 0}, + {0xD9, (uint8_t []){0xAA}, 1, 0}, + {0xF3, (uint8_t []){0x01}, 1, 0}, + {0xF0, (uint8_t []){0x00}, 1, 0}, + {0x21, (uint8_t []){0x00}, 1, 0}, + {0x11, (uint8_t []){0x00}, 1, 120}, + {0x29, (uint8_t []){0x00}, 1, 0}, +}; + +class CustomBoard : public WifiBoard { +private: + Button boot_button_; + i2c_master_bus_handle_t i2c_bus_; + esp_io_expander_handle_t io_expander = NULL; + LcdDisplay* display_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = I2C_SDA_IO, + .scl_io_num = I2C_SCL_IO, + .clk_source = I2C_CLK_SRC_DEFAULT, + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); + } + + void InitializeTca9554(void) + { + esp_err_t ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, I2C_ADDRESS, &io_expander); + if(ret != ESP_OK) + ESP_LOGE(TAG, "TCA9554 create returned error"); + + // uint32_t input_level_mask = 0; + // ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, IO_EXPANDER_INPUT); // 设置引脚 EXIO0 和 EXIO1 模式为输入 + // ret = esp_io_expander_get_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, &input_level_mask); // 获取引脚 EXIO0 和 EXIO1 的电平状态,存放在 input_level_mask 中 + + // ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, IO_EXPANDER_OUTPUT); // 设置引脚 EXIO2 和 EXIO3 模式为输出 + // ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, 1); // 将引脚电平设置为 1 + // ret = esp_io_expander_print_state(io_expander); // 打印引脚状态 + + ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, IO_EXPANDER_OUTPUT); // 设置引脚 EXIO0 和 EXIO1 模式为输出 + ESP_ERROR_CHECK(ret); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 1); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(300)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 0); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(300)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 1); // 复位 LCD 与 TouchPad + ESP_ERROR_CHECK(ret); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize QSPI bus"); + + const spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK, + QSPI_PIN_NUM_LCD_DATA0, + QSPI_PIN_NUM_LCD_DATA1, + QSPI_PIN_NUM_LCD_DATA2, + QSPI_PIN_NUM_LCD_DATA3, + QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); + ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO)); + } + + void Initializest77916Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + ESP_LOGI(TAG, "Install panel IO"); + + esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = QSPI_PIN_NUM_LCD_CS, + .dc_gpio_num = -1, + .spi_mode = 0, + .pclk_hz = 3 * 1000 * 1000, + .trans_queue_depth = 10, + .on_color_trans_done = NULL, + .user_ctx = NULL, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .octal_mode = 0, + .quad_mode = 1, + .sio_mode = 0, + .lsb_first = 0, + .cs_high_active = 0, + }, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)QSPI_LCD_HOST, &io_config, &panel_io)); + + ESP_LOGI(TAG, "Install ST77916 panel driver"); + + st77916_vendor_config_t vendor_config = { + .flags = { + .use_qspi_interface = 1, + }, + }; + + printf("-------------------------------------- Version selection -------------------------------------- \r\n"); + esp_err_t ret; + int lcd_cmd = 0x04; + uint8_t register_data[4]; + size_t param_size = sizeof(register_data); + lcd_cmd &= 0xff; + lcd_cmd <<= 8; + lcd_cmd |= LCD_OPCODE_READ_CMD << 24; // Use the read opcode instead of write + ret = esp_lcd_panel_io_rx_param(panel_io, lcd_cmd, register_data, param_size); + if (ret == ESP_OK) { + printf("Register 0x04 data: %02x %02x %02x %02x\n", register_data[0], register_data[1], register_data[2], register_data[3]); + } else { + printf("Failed to read register 0x04, error code: %d\n", ret); + } + // panel_io_spi_del(io_handle); + io_config.pclk_hz = 80 * 1000 * 1000; + if (esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)QSPI_LCD_HOST, &io_config, &panel_io) != ESP_OK) { + printf("Failed to set LCD communication parameters -- SPI\r\n"); + return ; + } + printf("LCD communication parameters are set successfully -- SPI\r\n"); + + // Check register values and configure accordingly + if (register_data[0] == 0x00 && register_data[1] == 0x7F && register_data[2] == 0x7F && register_data[3] == 0x7F) { + // Handle the case where the register data matches this pattern + printf("Vendor-specific initialization for case 1.\n"); + } + else if (register_data[0] == 0x00 && register_data[1] == 0x02 && register_data[2] == 0x7F && register_data[3] == 0x7F) { + // Provide vendor-specific initialization commands if register data matches this pattern + vendor_config.init_cmds = vendor_specific_init_new; + vendor_config.init_cmds_size = sizeof(vendor_specific_init_new) / sizeof(st77916_lcd_init_cmd_t); + printf("Vendor-specific initialization for case 2.\n"); + } + printf("------------------------------------- End of version selection------------------------------------- \r\n"); + + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = QSPI_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // Implemented by LCD command `36h` + .bits_per_pixel = QSPI_LCD_BIT_PER_PIXEL, // Implemented by LCD command `3Ah` (16/18) + .vendor_config = &vendor_config, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st77916(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_disp_on_off(panel, true); + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_64_init(), + }); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + CustomBoard() : + boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeTca9554(); + InitializeSpi(); + Initializest77916Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, I2S_STD_SLOT_LEFT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN, I2S_STD_SLOT_RIGHT); // I2S_STD_SLOT_LEFT / I2S_STD_SLOT_RIGHT / I2S_STD_SLOT_BOTH + + 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; + } +}; + +DECLARE_BOARD(CustomBoard); diff --git a/main/boards/esp32-s3-touch-lcd-3.5/README.md b/main/boards/esp32-s3-touch-lcd-3.5/README.md new file mode 100644 index 0000000..78b6949 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-3.5/README.md @@ -0,0 +1,3 @@ +新增 微雪 开发板: ESP32-S3-Touch-LCD-3.5 +产品链接: +https://www.waveshare.net/shop/ESP32-S3-Touch-LCD-3.5.htm \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-3.5/board_control.cc b/main/boards/esp32-s3-touch-lcd-3.5/board_control.cc new file mode 100644 index 0000000..2198eab --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-3.5/board_control.cc @@ -0,0 +1,31 @@ +#include +#include +#include +#include + +#include "board.h" +#include "boards/common/wifi_board.h" +#include "iot/thing.h" + +#define TAG "BoardControl" + +namespace iot { + +class BoardControl : public Thing { +public: + BoardControl() : Thing("BoardControl", "当前 AI 机器人管理和控制") { + // 修改重新配网 + methods_.AddMethod("ResetWifiConfiguration", "重新配网", ParameterList(), + [this](const ParameterList& parameters) { + ESP_LOGI(TAG, "ResetWifiConfiguration"); + auto board = static_cast(&Board::GetInstance()); + if (board && board->GetBoardType() == "wifi") { + board->ResetWifiConfiguration(); + } + }); + } +}; + +} // namespace iot + +DECLARE_THING(BoardControl); diff --git a/main/boards/esp32-s3-touch-lcd-3.5/config.h b/main/boards/esp32-s3-touch-lcd-3.5/config.h new file mode 100644 index 0000000..91e9821 --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-3.5/config.h @@ -0,0 +1,50 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_12 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_15 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_13 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_14 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_16 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_8 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_7 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SPI_MODE 0 +#define DISPLAY_CS_PIN GPIO_NUM_NC +#define DISPLAY_MOSI_PIN GPIO_NUM_1 +#define DISPLAY_MISO_PIN GPIO_NUM_2 +#define DISPLAY_CLK_PIN GPIO_NUM_5 +#define DISPLAY_DC_PIN GPIO_NUM_3 +#define DISPLAY_RST_PIN GPIO_NUM_NC + + + +#define DISPLAY_WIDTH 480 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_INVERT_COLOR true + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_6 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp32-s3-touch-lcd-3.5/config.json b/main/boards/esp32-s3-touch-lcd-3.5/config.json new file mode 100644 index 0000000..81906aa --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-3.5/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp32-s3-touch-lcd-3.5", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32-s3-touch-lcd-3.5/esp32-s3-touch-lcd-3.5.cc b/main/boards/esp32-s3-touch-lcd-3.5/esp32-s3-touch-lcd-3.5.cc new file mode 100644 index 0000000..eca385d --- /dev/null +++ b/main/boards/esp32-s3-touch-lcd-3.5/esp32-s3-touch-lcd-3.5.cc @@ -0,0 +1,295 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + + +#include +#include "i2c_device.h" +#include +#include +#include +#include +#include +#include + +#include +#include "esp_io_expander_tca9554.h" + +#include "axp2101.h" +#include "power_save_timer.h" + + +#define TAG "waveshare_lcd_3_5" + + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + + +class Pmic : public Axp2101 { + public: + Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) { + WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable + WriteReg(0x27, 0x10); // hold 4s to power off + + // Disable All DCs but DC1 + WriteReg(0x80, 0x01); + // Disable All LDOs + WriteReg(0x90, 0x00); + WriteReg(0x91, 0x00); + + // Set DC1 to 3.3V + WriteReg(0x82, (3300 - 1500) / 100); + + // Set ALDO1 to 3.3V + WriteReg(0x92, (3300 - 500) / 100); + + // Enable ALDO1(MIC) + WriteReg(0x90, 0x01); + + WriteReg(0x64, 0x02); // CV charger voltage setting to 4.1V + + WriteReg(0x61, 0x02); // set Main battery precharge current to 50mA + WriteReg(0x62, 0x08); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA ) + WriteReg(0x63, 0x01); // set Main battery term charge current to 25mA + } + }; + + +typedef struct { + int cmd; /*OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(20); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->OnShutdownRequest([this]() { + pmic_->PowerOff(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, + .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, + .clk_source = I2C_CLK_SRC_DEFAULT, + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); + } + + void InitializeTca9554(void) + { + esp_err_t ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000, &io_expander); + if(ret != ESP_OK) + ESP_LOGE(TAG, "TCA9554 create returned error"); + ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, IO_EXPANDER_OUTPUT); + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(100)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_1, 0); + ESP_ERROR_CHECK(ret); + vTaskDelay(pdMS_TO_TICKS(100)); + ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1, 1); + ESP_ERROR_CHECK(ret); + } + + void InitializeAxp2101() { + ESP_LOGI(TAG, "Init AXP2101"); + pmic_ = new Pmic(i2c_bus_, 0x34); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize QSPI bus"); + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = DISPLAY_MISO_PIN; + buscfg.sclk_io_num = DISPLAY_CLK_PIN; + 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)); + } + + void InitializeLcdDisplay() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = DISPLAY_SPI_MODE; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + st7796_vendor_config_t st7796_vendor_config = { + .init_cmds = st7796_lcd_init_cmds, + .init_cmds_size = sizeof(st7796_lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t), + }; + + // 初始化液晶屏驱动芯片 + ESP_LOGI(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = DISPLAY_RGB_ORDER; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = &st7796_vendor_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + + + esp_lcd_panel_reset(panel); + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR); + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 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")); + thing_manager.AddThing(iot::CreateThing("BoardControl")); + } + +public: + CustomBoard() : + boot_button_(BOOT_BUTTON_GPIO) { + InitializePowerSaveTimer(); + InitializeI2c(); + InitializeTca9554(); + InitializeAxp2101(); + InitializeSpi(); + InitializeLcdDisplay(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8311AudioCodec audio_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 = pmic_->IsCharging(); + discharging = pmic_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + + level = pmic_->GetBatteryLevel(); + return true; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(CustomBoard); diff --git a/main/boards/esp32s3-korvo2-v3/config.h b/main/boards/esp32s3-korvo2-v3/config.h new file mode 100644 index 0000000..b4f8f0f --- /dev/null +++ b/main/boards/esp32s3-korvo2-v3/config.h @@ -0,0 +1,62 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_16 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_45 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_9 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_8 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_48 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_17 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_18 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_5 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#ifdef CONFIG_LCD_ST7789 +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 280 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_SWAP_XY true +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define BACKLIGHT_INVERT false + +#define DISPLAY_OFFSET_X 20 +#define DISPLAY_OFFSET_Y 0 +#endif + +#ifdef CONFIG_LCD_ILI9341 +#define LCD_TYPE_ILI9341_SERIAL +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 + +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true +#define BACKLIGHT_INVERT false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#endif + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/esp32s3-korvo2-v3/config.json b/main/boards/esp32s3-korvo2-v3/config.json new file mode 100644 index 0000000..36110a1 --- /dev/null +++ b/main/boards/esp32s3-korvo2-v3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "esp32s3-korvo2-v3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc b/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc new file mode 100644 index 0000000..91518f9 --- /dev/null +++ b/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc @@ -0,0 +1,273 @@ +#include "wifi_board.h" +#include "audio_codecs/box_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 +#include +#include +#include +#include +#include +#include + +#define TAG "esp32s3_korvo2_v3" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +// Init ili9341 by custom cmd +static const ili9341_lcd_init_cmd_t vendor_specific_init[] = { + {0xC8, (uint8_t []){0xFF, 0x93, 0x42}, 3, 0}, + {0xC0, (uint8_t []){0x0E, 0x0E}, 2, 0}, + {0xC5, (uint8_t []){0xD0}, 1, 0}, + {0xC1, (uint8_t []){0x02}, 1, 0}, + {0xB4, (uint8_t []){0x02}, 1, 0}, + {0xE0, (uint8_t []){0x00, 0x03, 0x08, 0x06, 0x13, 0x09, 0x39, 0x39, 0x48, 0x02, 0x0a, 0x08, 0x17, 0x17, 0x0F}, 15, 0}, + {0xE1, (uint8_t []){0x00, 0x28, 0x29, 0x01, 0x0d, 0x03, 0x3f, 0x33, 0x52, 0x04, 0x0f, 0x0e, 0x37, 0x38, 0x0F}, 15, 0}, + + {0xB1, (uint8_t []){00, 0x1B}, 2, 0}, + {0x36, (uint8_t []){0x08}, 1, 0}, + {0x3A, (uint8_t []){0x55}, 1, 0}, + {0xB7, (uint8_t []){0x06}, 1, 0}, + + {0x11, (uint8_t []){0}, 0x80, 0}, + {0x29, (uint8_t []){0}, 0x80, 0}, + + {0, (uint8_t []){0}, 0xff, 0}, +}; + + +class Esp32S3Korvo2V3Board : public WifiBoard { +private: + Button boot_button_; + i2c_master_bus_handle_t i2c_bus_; + LcdDisplay* display_; + esp_io_expander_handle_t io_expander_ = NULL; + + 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, &i2c_bus_)); + } + + void I2cDetect() { + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + } + + void InitializeTca9554() { + esp_err_t ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000, &io_expander_); + if(ret != ESP_OK) { + ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9554A_ADDRESS_000, &io_expander_); + if(ret != ESP_OK) { + ESP_LOGE(TAG, "TCA9554 create returned error"); + return; + } + } + // 配置IO0-IO3为输出模式 + ESP_ERROR_CHECK(esp_io_expander_set_dir(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | + IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_3, + IO_EXPANDER_OUTPUT)); + + // 复位LCD和TouchPad + ESP_ERROR_CHECK(esp_io_expander_set_level(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_2, 1)); + vTaskDelay(pdMS_TO_TICKS(300)); + ESP_ERROR_CHECK(esp_io_expander_set_level(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_2, 0)); + vTaskDelay(pdMS_TO_TICKS(300)); + ESP_ERROR_CHECK(esp_io_expander_set_level(io_expander_, + IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_2, 1)); + } + + void EnableLcdCs() { + if(io_expander_ != NULL) { + esp_io_expander_set_level(io_expander_, IO_EXPANDER_PIN_NUM_3, 0);// 置低 LCD CS + } + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_0; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_1; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeIli9341Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_NC; + io_config.dc_gpio_num = GPIO_NUM_2; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片 + ESP_LOGD(TAG, "Install LCD driver"); + const ili9341_vendor_config_t vendor_config = { + .init_cmds = &vendor_specific_init[0], + .init_cmds_size = sizeof(vendor_specific_init) / sizeof(ili9341_lcd_init_cmd_t), + }; + + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_NC; + // panel_config.flags.reset_active_high = 0, + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = (void *)&vendor_config; + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); + + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + EnableLcdCs(); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, false)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_46; + io_config.dc_gpio_num = GPIO_NUM_2; + io_config.spi_mode = 0; + io_config.pclk_hz = 60 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(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_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + EnableLcdCs(); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, true)); + + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + + } + +public: + Esp32S3Korvo2V3Board() : boot_button_(BOOT_BUTTON_GPIO) { + ESP_LOGI(TAG, "Initializing esp32s3_korvo2_v3 Board"); + InitializeI2c(); + I2cDetect(); + InitializeTca9554(); + InitializeSpi(); + InitializeButtons(); + #ifdef LCD_TYPE_ILI9341_SERIAL + InitializeIli9341Display(); + #else + InitializeSt7789Display(); + #endif + InitializeIot(); + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec( + i2c_bus_, + 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, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display *GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(Esp32S3Korvo2V3Board); diff --git a/main/boards/jiuchuang-s3/_tomatotimers_RGB565A8_500x220.c b/main/boards/jiuchuang-s3/_tomatotimers_RGB565A8_500x220.c new file mode 100644 index 0000000..770c0dc --- /dev/null +++ b/main/boards/jiuchuang-s3/_tomatotimers_RGB565A8_500x220.c @@ -0,0 +1,111 @@ + +#if defined(LV_LVGL_H_INCLUDE_SIMPLE) +#include "lvgl.h" +#elif defined(LV_BUILD_TEST) +#include "../lvgl.h" +#else +#include "lvgl/lvgl.h" +#endif + + +#ifndef LV_ATTRIBUTE_MEM_ALIGN +#define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef LV_ATTRIBUTE__TOMATOTIMERS_RGB565A8_500X220 +#define LV_ATTRIBUTE__TOMATOTIMERS_RGB565A8_500X220 +#endif + +static const +LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE__TOMATOTIMERS_RGB565A8_500X220 +uint8_t _tomatotimers_RGB565A8_500x220_map[] = { + + 0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0x7b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdb,0x4e,0xdf,0xdf,0xdf,0xef,0xdf,0xef,0xdf,0xe7,0xdf,0xd7,0x1f,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00, + 0xff,0xff,0x00,0x00,0x00,0x00,0xbd,0x7e,0x7d,0xaf,0xbe,0xbf,0x5e,0xbf,0x5d,0xb7,0x5e,0xb7,0x7d,0xbf,0x5d,0xbf,0x5d,0xbf,0x7e,0xbf,0x9e,0xb7,0x9e,0xb7,0xbe,0xaf,0x5d,0x9f,0xbe,0xaf,0xbf,0xaf,0xbe,0xa7,0x9e,0x9f,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xdc,0x8e,0xbc,0x96,0x1d,0x8f,0xfd,0x96,0x3c,0x97,0xdc,0x96,0x3d,0xa7,0x3d,0xa7,0xfb,0x7d,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x1e,0xb7,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x1f,0x8f,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x9c,0x7e,0x3e,0x87,0xfd,0x7e,0xfb,0x7e,0xfb,0x7e,0xfd,0x8e,0x7d,0x8f,0xba,0x2e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xaf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x9e,0xc7,0xff,0xdf,0xff,0xe7,0xff,0x9e,0x00,0x00,0xff,0xff,0x00,0x00, + 0x00,0x00,0x5f,0x97,0xdf,0xdf,0xde,0xf7,0xdf,0xf7,0xff,0xf7,0xdf,0xf7,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xff,0xef,0xff,0xef,0xff,0xf7,0xff,0xf7,0xff,0xf7,0xff,0xf7,0xff,0xf7,0xff,0xef,0x7f,0xaf,0x00,0x00,0xff,0x7f,0x00,0x00,0xff,0xaf,0x00,0x00,0xbe,0xcf,0xdf,0xef,0xdf,0xf7,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xdf,0xef,0xff,0xf7,0xdf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xe7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0x07,0xdf,0xe7,0xff,0xf7,0xdf,0xf7,0xdf,0xf7,0xdf,0xf7,0xff,0xf7,0xdf,0xf7,0xdf,0xf7,0xff,0xef,0xbf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xef,0x7b,0x00,0x00,0xde,0xdf,0xbf,0xd7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xbf,0xff,0xff,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x3c,0x56,0xbe,0xdf,0xbe,0xe7,0xdf,0xe7,0xdf,0xe7,0xff,0xdf,0xbf,0xd7,0x00,0x00,0xff,0xff, + 0xbf,0xae,0xbe,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xe7,0xbf,0xcf,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9f,0xbf,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x9e,0xbf,0x00,0x00,0x00,0x00,0x7f,0x36,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xbf,0xef,0xff,0xff,0xbf,0xd7,0xde,0xdf,0xde,0xe7,0xff,0xef,0xff,0xef,0xdf,0xd7,0x79,0x06,0x00,0x00,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x07,0xff,0x7f,0x3f,0xcf,0xdf,0xd7,0xbf,0xd7,0xff,0xcf,0x00,0x00,0x00,0x00,0x5e,0xc7,0xde,0xdf,0xff,0xdf,0x00,0x00, + 0xbf,0xd7,0xff,0xff,0xde,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf7,0xbf,0x00,0x00,0xbe,0xe7,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xdf,0x6e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9f,0xef,0xff,0xff,0xdf,0xf7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x7b,0x9f,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xaf,0xdf,0xdf,0xdf,0xe7,0xdf,0xdf,0x9f,0xbf,0x00,0x00,0x00,0x00,0xdf,0xcf,0xbf,0xcf,0x00,0x00,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xde,0xdf,0x00,0x00,0xde,0xe7,0xff,0xe7,0xdf,0xe7,0xdf,0xe7,0xff,0xfd,0xde,0xe7,0xdf,0xdf, + 0xde,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0x97,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf7,0xbf,0x00,0x00,0xfd,0xef,0xff,0xf7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xff,0xff,0x37,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0x7f,0x9e,0xbe,0xdf,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xf5,0xaf,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xcf,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbf,0xe7,0x9f,0xbf,0xde,0xef,0xde,0xef,0xff,0x3f,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xe0,0xff,0xff,0xaf,0xde,0xdf,0x3c,0xd7,0xbe,0xdf,0xde,0xef,0x9f,0xbf,0xbe,0xdf,0xbe,0xe7,0xff,0x9e,0xbe,0xe7,0xde,0xe7, + 0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xff,0xef,0xdf,0xef,0xde,0xf7,0xdf,0xf7,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xfd,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf7,0xbf,0x00,0x00,0xbe,0xe7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xff,0xff,0xba,0x56,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xc7,0x00,0x00,0xff,0xff,0xff,0x7f,0xff,0xff,0xbf,0xb7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7f,0x00,0x00,0xdf,0xcf,0xbf,0xe7,0x00,0x00,0xff,0xe7,0xbf,0xdf,0xd3,0x34,0x9c,0x8e,0xbe,0xd7,0xbf,0xc7,0x9e,0xdf,0xbe,0xdf, + 0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xde,0xef,0xbf,0xd7,0x5f,0x97,0x00,0x00,0x00,0x00,0xff,0x67,0x9f,0xc7,0xdf,0xe7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xfd,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x5f,0xad,0x00,0x00,0xbf,0xdf,0xff,0xf7,0xfe,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbf,0xcf,0x00,0x00,0xff,0xff,0xbf,0x56,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x1e,0xaf,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xe7,0x00,0x00,0xff,0xff,0x55,0xfd,0x00,0x00,0xdf,0xdf,0xff,0xff,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0x9c,0xaf,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9e,0xcf,0xde,0xe7,0xff,0x07,0xff,0xdf,0xdf,0xe7,0xdf,0xd7,0xdf,0xe7,0xdf,0xd7,0x00,0x00,0x5e,0xd7,0xdf,0xdf, + 0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xdf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xbe,0xc7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xfd,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0xff,0x07,0xff,0xff,0x00,0x00,0xbf,0x56,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xde,0xdf,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xcf,0xdf,0xef,0xdf,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0xff,0x7f,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9f,0xbf,0xdf,0xe7,0x1f,0xf8,0xfe,0xe7,0xff,0xe7,0xbf,0xcf,0xdf,0xe7,0xbf,0xe7,0xff,0xff,0xbf,0xdf,0xdf,0xdf, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xdf,0x00,0x00,0x00,0x00,0x3e,0xa7,0xbe,0xdf,0xdf,0xe7,0xdf,0xef,0xde,0xe7,0xdf,0xbf,0x00,0x00,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7f,0xff,0xff,0xbe,0xc7,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x9e,0xcf,0xff,0xff,0x55,0xad,0x00,0x00,0xff,0x6f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xf7,0xdf,0xef,0xde,0xe7,0xfe,0xb7,0x00,0x00,0xff,0xff,0xff,0xff,0xbf,0xbf,0xbf,0xef,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xd7,0x00,0x00,0x55,0xad,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xe7,0xbf,0xdf,0x00,0x00,0xfe,0xe7,0x00,0x00,0x9e,0xd7,0xdf,0xdf,0x00,0x00,0xdf,0xdf,0xbf,0xd7, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xde,0xef,0xff,0x03,0x00,0x00,0x9e,0xbf,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbe,0xe7,0x00,0x00,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x5f,0xcf,0xdf,0xe7,0xde,0xef,0xbe,0xef,0xdf,0xef,0xdf,0xe7,0x3e,0x97,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xbf,0x56,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xef,0xbf,0xef,0x9e,0xd7,0xff,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x3e,0xaf,0xdf,0xe7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xbf,0xdf,0xde,0xdf,0xdf,0xdf,0xdf,0xcf,0x00,0x00,0x00,0x00,0x1f,0x87,0xdf,0xdf,0xde,0xdf,0x1f,0xe7, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xdf,0x00,0x00,0xde,0xce,0xde,0xef,0xff,0xff,0xdf,0xf7,0xbf,0xe7,0xdf,0xef,0xdf,0xe7,0xff,0xff,0xff,0xff,0xbd,0xd7,0x00,0x00,0x3f,0xa7,0xfe,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xaf,0xff,0xbf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x07,0xff,0xff,0xff,0xbf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xba,0x56,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7f,0x00,0x00,0xff,0xff,0x00,0x00,0xba,0x2e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xbf,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xef,0x9e,0xd7,0xbf,0x25,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xbf,0x00,0x00,0x00,0x00,0x5e,0xaf,0xbf,0xe7,0xff,0xef,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xaf,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xde,0xe7,0xdf,0xe7,0x9e,0xd7,0xbe,0xdf,0xde,0xdf,0xdf,0xd7,0xff,0xbf,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x5d,0xb7,0xff,0x07,0x9f,0xd7,0xff,0xff,0xdf,0xf7,0x9f,0xc7,0x00,0x00,0x00,0x00,0x55,0x05,0xbe,0xe7,0xff,0xff,0xff,0xef,0xff,0x9e,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0xaf,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0x37,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x9f,0xc7,0xdf,0xef,0xfe,0xef,0xdf,0xe7,0xbe,0xdf,0xff,0x07,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x5d,0x9f,0xbf,0xe7,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xde,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xf7,0xbd,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9e,0xc7,0xfe,0xe7,0xbe,0xdf,0xbe,0xdf,0xde,0xdf,0xf7,0x3d,0x00,0x00,0xff,0xff, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0x07,0x00,0x00,0xff,0xef,0xff,0xff,0xdf,0xd7,0x00,0x00,0xfb,0xdf,0xff,0xff,0x00,0x00,0x7f,0x36,0xff,0xe7,0xdf,0xf7,0xbd,0xcf,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0x00,0x00,0xff,0x9f,0xbf,0xd7,0xff,0xdf,0xdf,0xef,0xbe,0xe7,0xde,0xdf,0xbe,0xe7,0xdf,0xef,0xdf,0xdf,0x9f,0xcf,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x1f,0x00,0x9f,0xcf,0xdf,0xdf,0xff,0xef,0xde,0xe7,0xbe,0xe7,0xbe,0xe7,0xbf,0xef,0xdf,0xe7,0xbe,0xd7,0x5d,0x97,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xcf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0x37,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xbf,0xbf,0x1f,0x77,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x9e,0xaf,0x9f,0xcf,0xdf,0xe7,0xde,0xf7,0xdf,0xf7,0xdf,0xf7,0xbe,0xf7,0xdf,0xef,0xbf,0xcf,0xbf,0xb7,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xbe,0xaf,0xdf,0xe7,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xd7,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xfd,0x8e,0x9e,0xbf,0xdf,0xe7,0xdf,0xe7,0xdf,0xe7,0xdf,0xe7,0xbf,0xe7,0xdf,0xe7,0xbe,0xcf,0x5e,0xaf,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x55,0xad,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xf9,0x9f,0xdf,0xf7,0xdf,0xff,0xfe,0xc7,0xff,0x7f,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0xf8,0xdf,0xe7,0xff,0xff,0xbd,0xdf,0x00,0x00,0xdf,0xef,0xff,0xff,0xfe,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x9e,0xd7,0xff,0xe7,0xde,0xef,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xef,0xbf,0xe7,0x7d,0x9f,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xfb,0x8e,0xdf,0xdf,0xdf,0xe7,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xdf,0xdf,0xfe,0xbf,0x00,0x00,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0x56,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xd7,0x00,0x00,0x00,0x00,0xdf,0xd7,0xdf,0xef,0xde,0xef,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xdf,0xe7,0xbf,0xd7,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x9f,0xc7,0xde,0xe7,0xdf,0xf7,0xdf,0xef,0xff,0x8f,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xdf,0xd7,0xdf,0xef,0xff,0xf7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xef,0xff,0xe7,0xdf,0xd7,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x07,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x5f,0x97,0xdf,0xf7,0xde,0xf7,0x7d,0xbf,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xef,0xff,0xff,0xbe,0xe7,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xbf,0xe7,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xbe,0xd7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xdf,0x3f,0x77,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xaf,0xdf,0xdf,0xfe,0xe7,0xff,0xe7,0xff,0xe7,0xff,0xe7,0xff,0xe7,0xff,0xe7,0xff,0xe7,0xdf,0xdf,0x7f,0xcf,0x00,0x00,0x00,0x00,0x1f,0x77,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7f,0xff,0xff,0xff,0xff,0x00,0x00,0x7c,0xc7,0xbf,0xe7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xbf,0xd7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x9f,0xc7,0x9f,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x7d,0xbf,0xdf,0xe7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x7d,0xc7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0x1f,0x00,0xdf,0xf7,0xff,0xff,0xbf,0xd7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x3c,0xaf,0xdf,0xf7,0xdf,0xf7,0xbe,0xcf,0xff,0xff,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0x9f,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xaf,0x00,0x00,0x9e,0xc7,0xde,0xe7,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x9e,0xd7,0x00,0x00,0xff,0xbd,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7f,0x00,0x00,0xbe,0xbf,0xde,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe7,0x00,0x00,0x00,0x00,0xff,0x77,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9e,0xd7,0xde,0xf7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xdf,0x00,0x00,0xff,0xff,0xff,0xaf,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbf,0xe7,0x1f,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0xfd,0xaf,0xff,0xff,0xbf,0xe7,0xff,0xff,0xde,0xef,0xbf,0xcf,0x00,0x00,0x00,0x00,0xdb,0x4e,0xbe,0xdf,0xfe,0xf7,0xdf,0xe7,0xff,0x57,0x00,0x00,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xde,0xef,0xbe,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x7e,0xc7,0xde,0xef,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbe,0xdf,0x00,0x00,0xff,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xde,0xcf,0xfe,0xf7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xbe,0xff,0xdf,0xf7,0x00,0x00,0x00,0x00,0x3c,0x7f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xfe,0xd7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbf,0xe7,0x5f,0x55,0xff,0xff,0xff,0xff,0xff,0x7f,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xd7,0xff,0xf7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xbe,0xe7,0xef,0x03,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x9f,0xb7,0xbe,0xef,0xff,0xff,0xfe,0xf7,0xbf,0xe7,0xbf,0xe7,0xdf,0xef,0xff,0xff,0xde,0xef,0xbe,0xd7,0xff,0xff,0x9f,0xaf,0xfe,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xde,0xe7,0x3f,0x9f,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9f,0xb7,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x9f,0xd7,0x00,0x00,0xf7,0xbf,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xdf,0xf7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xe7,0x00,0x00,0xff,0xff,0xfb,0x5f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xf7,0xbf,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xbf,0x00,0x00,0xbe,0xe7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0x6e,0x00,0x00,0x7e,0xbf,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x9e,0xd7,0x00,0x00,0x00,0x00,0xdf,0xdf,0xff,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0xbf,0x56,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x9f,0xa7,0xde,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x7f,0xd7,0x00,0x00,0x5f,0xad,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xd7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xe7,0x00,0x00,0xff,0xff,0xff,0x6f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xbd,0xd7,0xde,0xf7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xde,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf5,0xaf,0x00,0x00,0xbe,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x00,0x00,0xbb,0x4d,0xbf,0xdf,0xde,0xe7,0xff,0xe7,0xbf,0xdf,0x9f,0xa7,0x00,0x00,0x00,0x00,0x9e,0xdf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbe,0xdf,0x00,0x00,0xf5,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xde,0xdf,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x3f,0xb7,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xd7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xdf,0x6e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0xe0,0xff,0xde,0xd7,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0xf5,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x7e,0xc7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x9e,0xcf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x3a,0x5f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xdf,0xcf,0xde,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x9e,0xcf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9f,0xdf,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xdf,0xe7,0xdf,0xe7,0xdf,0xe7,0xbf,0xe7,0xff,0xef,0xdf,0xe7,0xff,0xf7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xe7,0x00,0x00,0x00,0x00,0xff,0x5e,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x5f,0xb7,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0xff,0x7f,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xbe,0xd7,0x9e,0xe7,0x9e,0xaf,0x1c,0x87,0xff,0xbf,0xbf,0xcf,0xdf,0xe7,0xde,0xf7,0xfe,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0x1f,0xaf,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xf7,0xbe,0xd7,0xff,0x5e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9f,0xcf,0xbe,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x9f,0xd7,0xff,0xff,0x7f,0xce,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xe7,0x00,0x00,0x00,0x00,0xff,0x5e,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xaf,0x00,0x00,0x9d,0xcf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xef,0xbe,0xef,0xdf,0xf7,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xf7,0xff,0xff,0xff,0xef,0xff,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0xff,0xef,0xdf,0xf7,0xdf,0xef,0xde,0xf7,0xfe,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbe,0xd7,0x00,0x00,0x55,0xad,0xff,0xbd,0x00,0x00,0x9f,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x5f,0xb7,0x00,0x00,0x00,0x00,0x9e,0xc7,0xdf,0xdf,0xbe,0xdf,0xbe,0xdf,0xbf,0xdf,0xdf,0x6e,0x00,0x00,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xe7,0x00,0x00,0x00,0x00,0xff,0x77,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x7f,0x66,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xef,0xdf,0xef,0xdf,0xe7,0xdf,0xef,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0x9f,0xb7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xde,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0xdf,0xef,0xbf,0xdf,0xbe,0xe7,0xff,0xef,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0xff,0x07,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xbf,0xd7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1e,0xb7,0xbe,0xdf,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0x7f,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x5f,0x7f,0x00,0x00,0x5d,0xa7,0xdf,0xe7,0xff,0xef,0xff,0xf7,0xff,0xf7,0xde,0xef,0xff,0xf7,0xff,0xf7,0xbe,0xd7,0xba,0xae,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe7,0x5e,0xbf,0xff,0xff,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xe7,0x00,0x00,0x00,0x00,0x1c,0x77,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x00,0x00,0xbe,0xd7,0xff,0xf7,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xbf,0xcf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7e,0xaf,0xdf,0xe7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x7d,0xbf,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xe7,0x3d,0xb7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x5e,0xa7,0xdf,0xe7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x7e,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x00,0x00,0xff,0xff,0xf5,0xff,0xf5,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x7d,0xb7,0xde,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0x7e,0xff,0xff,0x00,0x00,0x9f,0xcf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x7d,0xaf,0x00,0x00,0xbe,0xbf,0xdf,0xef,0xdf,0xf7,0xde,0xe7,0x9f,0xc7,0xfd,0xae,0x1f,0xb7,0x9e,0xd7,0xfe,0xef,0xdf,0xef,0x9e,0xbf,0xff,0xff,0x00,0x00,0xde,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0x00,0x00,0x00,0x00,0xbf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x00,0x00,0xfb,0x5e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x00,0x00,0xfe,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xbf,0xd7,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x9e,0xcf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xbe,0xdf,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x9f,0xd7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xd7,0x00,0x00,0xff,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xdf,0xe7,0xdf,0xef,0xde,0xef,0xdf,0xe7,0xff,0xf7,0xde,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xff,0xff,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x5e,0xbf,0xde,0xf7,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xbf,0xc7,0x00,0x00,0x00,0x00,0xbf,0xdf,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdd,0xd7,0x00,0x00,0x7c,0xaf,0xdf,0xef,0xdf,0xef,0x7e,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9f,0xc7,0xff,0xbf,0xff,0xff,0x00,0x00,0x9f,0xcf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xef,0x00,0x00,0x00,0x00,0xdf,0xd7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x00,0x00,0xfb,0x5e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xbf,0x7e,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbe,0xe7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xd7,0x00,0x00,0xbf,0xd7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0x7e,0xb7,0x00,0x00,0x00,0x00,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbe,0xe7,0x00,0x00,0xff,0xff,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x9f,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbe,0xdf,0x9e,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7d,0xa7,0xbe,0xc7,0xdf,0xe7,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xfb,0x7f,0xff,0xff,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x00,0x00,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xde,0xef,0x9f,0xc7,0x00,0x00,0xde,0xe7,0xff,0xf7,0x5e,0xbf,0x00,0x00,0xff,0xff,0xbf,0xb7,0xff,0xef,0xbf,0xdf,0xff,0xaf,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x3f,0x9f,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x00,0x00,0xfb,0x5e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x7e,0xbf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf9,0xcf,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbe,0xdf,0x00,0x00,0xdf,0x96,0xde,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xff,0x8f,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xbf,0x56,0xde,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0x67,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xde,0xef,0xdf,0xc7,0x00,0x00,0x00,0x00,0xff,0xb7,0xdf,0xdf,0xdf,0xdf,0xbe,0xc7,0x00,0x00,0x00,0x00,0x9c,0x76,0xbf,0xe7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xf7,0xff,0xff,0xff,0xf7,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x1f,0xbf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0x97,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xef,0x00,0x00,0x9f,0xcf,0xff,0xf7,0xde,0xdf,0x00,0x00,0xff,0xdf,0x00,0x00,0xdf,0xc7,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x9f,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x9f,0xbf,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x6f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xaf,0xff,0x7f,0x7e,0xcf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x9f,0xbf,0xde,0xef,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x7e,0xb7,0x00,0x00,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xff,0xfe,0xff,0xde,0xef,0x3f,0xb7,0x00,0x00,0x9e,0xb7,0xdf,0xe7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xe7,0x9e,0xcf,0x00,0x00,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xf7,0x9f,0xcf,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf5,0xff,0x00,0x00,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x9e,0xbf,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xd7,0xff,0xff,0xde,0xef,0xdf,0xf7,0x9e,0x9f,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xc7,0xdf,0xf7,0xde,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9f,0xcf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x6f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x3d,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0x07,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0x7f,0xc7,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xdf,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0x07,0xdf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xf7,0x9f,0xc7,0x00,0x00,0x5f,0xbf,0xdf,0xef,0xff,0xff,0xff,0xff,0xdf,0xe7,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xf7,0xbe,0xdf,0x00,0x00,0xff,0x77,0xdf,0xef,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xf7,0x7f,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x5d,0x97,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xbf,0xbf,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xdf,0x00,0x00,0xbf,0xef,0xdf,0xef,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xd7,0xdf,0xf7,0xdd,0xef,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9e,0xbf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xd7,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x6f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xdf,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xbf,0xdf,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x9f,0xbf,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xf7,0x3c,0x3e,0x5d,0xaf,0xdf,0xef,0xdf,0xff,0xff,0xff,0xdf,0xf7,0x9f,0xcf,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x9f,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xd9,0x04,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x00,0x00,0xdf,0xcf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xdf,0xef,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x3c,0x8e,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xdf,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x6f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xd7,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x7f,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xfe,0xdf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xc7,0x00,0x00,0xdf,0xd7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x9f,0xc7,0x9e,0xbf,0xdf,0xef,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x5f,0x55,0xbf,0x96,0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x7f,0x9e,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0x9f,0xd7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0x9e,0xe7,0x00,0x00,0xdf,0xef,0xdf,0xef,0x00,0x00,0xff,0xff,0xff,0x7f,0x00,0x00,0xff,0xcf,0xde,0xf7,0xde,0xef,0x9f,0x8f,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x5d,0x97,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x4f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0x00,0x00,0x1f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xe0,0xff,0xde,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x9f,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xfe,0xd7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xf7,0xf7,0x7d,0x3f,0xb7,0xde,0xe7,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0x7f,0xbf,0xfe,0xc7,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0xbe,0xcf,0x00,0x00,0xdf,0xef,0xff,0xff,0xff,0xff,0xdf,0xf7,0x9f,0xcf,0x00,0x00,0xff,0xcf,0x00,0x00,0xbf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x7e,0xbf,0x00,0x00,0xef,0x7b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0x7e,0xcf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xde,0xdf,0x00,0x00,0xdf,0xe7,0xde,0xef,0xff,0x07,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0x9d,0xcf,0xfe,0xf7,0xff,0xf7,0x7f,0xaf,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0x9c,0xb7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x4f,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x3f,0x7f,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xfe,0xd7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xbf,0xef,0x00,0x00,0x9e,0xcf,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xf7,0xff,0xe7,0x7d,0xd7,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x1f,0x00,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xcf,0xff,0xff,0xff,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x07,0xff,0xff,0xbf,0x7e,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xbf,0xfe,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xfe,0xdf,0xdf,0xf7,0x5e,0xa7,0xff,0xff,0xff,0x7f,0x00,0x00,0xff,0xff,0x00,0x00,0x9f,0xd7,0xdf,0xff,0xfe,0xef,0xbf,0xcf,0xff,0xff,0xff,0xff,0xff,0xaf,0xff,0xff,0x9e,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x7d,0xcf,0x00,0x00,0xbf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x57,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xbe,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xdf,0xff,0xff,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7f,0xff,0xff,0x7e,0xc7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xdf,0xdf,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xbf,0xbf,0xd7,0xdf,0xef,0xdf,0xf7,0xde,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0x5f,0xc7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0xff,0xff,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xcf,0x00,0x00,0x9f,0xcf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x00,0x00,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0x5e,0xbf,0xdf,0xf7,0xde,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9d,0xdf,0xfe,0xef,0xdf,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xfd,0xae,0x00,0x00,0xdf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x57,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xff,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xbf,0xe7,0x1f,0x00,0xbf,0xd7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0x9e,0xdf,0x00,0x00,0x00,0x00,0x00,0x00,0x5e,0xbf,0xde,0xef,0xff,0xff,0xdf,0xe7,0x00,0x00,0xdf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0x7f,0x00,0x00,0xff,0x07,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0xff,0xff,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x07,0xff,0xff,0x00,0x00,0x5f,0xbf,0xde,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x9f,0xcf,0x00,0x00,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x7f,0xdf,0x00,0x00,0x9e,0xe7,0xff,0xff,0xdf,0xcf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x5d,0x77,0xff,0xaf,0x00,0x00,0xff,0xff,0xff,0xff,0x5e,0xc7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x00,0x00,0x00,0x00,0xbf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x57,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x9f,0xc7,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x9e,0xd7,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x7e,0xb7,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xd7,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xef,0x00,0x00,0x7f,0xcf,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xde,0xef,0xbf,0xdf,0x9f,0xd7,0xfd,0x7e,0xdf,0xef,0xff,0xff,0x9f,0xe7,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xf7,0x9f,0xbf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xcf,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xbf,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0x7d,0x8f,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xf7,0x7f,0xaf,0xff,0xff,0x00,0x00,0x9f,0xd7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xff,0xdf,0xbe,0xdf,0x9e,0xcf,0x00,0x00,0xff,0x7b,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x00,0x00,0xdf,0xd7,0xff,0xf7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0xff,0x37,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xbf,0xbf,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0x9f,0xbf,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x5f,0x55,0x00,0x00,0xdf,0xd7,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xf7,0xbd,0x96,0x79,0x9e,0xbf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xef,0xde,0xff,0xdf,0xf7,0xff,0xbf,0x00,0x00,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xf7,0x9f,0xbf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xef,0xbf,0xcf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xdf,0x4e,0xdf,0xd7,0xdf,0xef,0xff,0xff,0xfe,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xf7,0xff,0x00,0x00,0xff,0x7e,0xde,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xfe,0xf7,0xbf,0xd7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xaf,0xff,0xff,0x00,0x00,0xbf,0xdf,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x9f,0xcf,0x00,0x00,0x00,0x00,0xdf,0xd7,0xff,0xf7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0xff,0x37,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbf,0xd7,0x00,0x00,0x00,0x00,0xbd,0xdf,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x9f,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x5f,0x7f,0xbe,0xd7,0xde,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xdf,0x00,0x00,0x00,0x00,0xdf,0xdf,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xd7,0x00,0x00,0xbf,0xcf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xc7,0xde,0xf7,0xff,0xff,0xff,0xff,0xff,0xf7,0x9f,0xbf,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0x1f,0x8f,0xdf,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xf7,0xde,0xe7,0xdf,0xe7,0xdf,0xe7,0xdf,0xef,0xdf,0xef,0xff,0xff,0xde,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xcf,0x00,0x00,0xff,0xaf,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xbf,0xdf,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xaf,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xd7,0xdf,0xf7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0x7f,0x36,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x9f,0xbf,0xde,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xe7,0xff,0xe7,0xff,0xe7,0xdf,0xe7,0xdf,0xef,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xbe,0xc7,0x00,0x00,0x00,0x00,0xdf,0xdf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xdf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0x79,0x66,0xbf,0xe7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xf7,0xff,0xff,0xff,0xff,0xdf,0xef,0x9d,0xb7,0x00,0x00,0xbe,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xbf,0xef,0x5f,0x55,0xff,0xff,0x00,0x00,0xff,0x7f,0xe0,0xff,0xbd,0xbf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x9f,0xcf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9e,0xb7,0xdf,0xdf,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x07,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xdf,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xde,0xe7,0x00,0x00,0xba,0xd6,0x00,0x00,0xdf,0xd7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe7,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0x3f,0x7f,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x7c,0xc7,0x00,0x00,0x5d,0xaf,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0x07,0xef,0xfb,0xdb,0x96,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xbe,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xde,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xe7,0xdf,0xdf,0xdf,0xe7,0xdf,0xe7,0xde,0xd7,0xdf,0xdf,0xdf,0xe7,0xff,0xef,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0x5f,0xad,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xff,0x07,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0x79,0xce,0xff,0xff,0x5e,0xb7,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xf7,0xbf,0xbf,0xff,0xff,0x79,0xce,0x00,0x00,0xbf,0xcf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xd7,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe7,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x7e,0xc7,0x00,0x00,0x00,0x00,0x9e,0xdf,0xde,0xe7,0xde,0xef,0xde,0xef,0xdf,0xef,0xdf,0xe7,0x1c,0x8f,0x00,0x00,0x7e,0xbf,0xdf,0xef,0xff,0xff,0xff,0xf7,0xdf,0xff,0xff,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbe,0xd7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xde,0xff,0xde,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0x7e,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x7f,0xbf,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xf7,0x05,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xdf,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xdf,0xcf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xaf,0xff,0xaf,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xd7,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xbe,0xcf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7d,0x00,0x00,0x00,0x00,0x00,0x00,0xbf,0xb7,0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xff,0xc7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xe7,0x7f,0x06,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xc7,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xbf,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0x79,0x36,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xbf,0xe7,0xff,0x07,0xff,0xff,0xff,0x07,0xff,0xaf,0x00,0x00,0xdf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0x9f,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xef,0xdf,0xd7,0xbf,0xcf,0xbf,0x76,0x99,0x35,0xff,0xc7,0xbf,0xcf,0xde,0xe7,0xdf,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x07,0xff,0xff,0x00,0x00,0xbf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xef,0x9e,0xd7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xaf,0x00,0x00,0xbf,0xd7,0xff,0xf7,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xbe,0xdf,0x00,0x00,0xf7,0xbd,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0x7f,0x36,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0x55,0x05,0xde,0xef,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x5d,0x8f,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xaf,0x00,0x00,0xdf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x9f,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdf,0xef,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xde,0xef,0xbe,0xef,0xdf,0xf7,0xde,0xf7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xef,0xff,0xaf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x5f,0x05,0xde,0xe7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xde,0xef,0xde,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9e,0xd7,0xff,0xf7,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xbe,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xe7,0x00,0x00,0xff,0xff,0xdb,0x4e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0x9f,0xdf,0xe7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xef,0x9e,0x9f,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xaf,0x00,0x00,0xbf,0xd7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0x9e,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xdf,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe7,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xbe,0xef,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0xba,0x7e,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xbf,0xdf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x7e,0xa7,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x9e,0xc7,0xff,0xef,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xf7,0xbf,0xdf,0x00,0x00,0xff,0xff,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xf7,0xdf,0xe7,0x00,0x00,0x00,0xf8,0xdf,0x4e,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x7f,0x00,0x00,0xfb,0x5d,0xdf,0xdf,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xe7,0x7d,0x9f,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xd7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe7,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xbf,0xdf,0xff,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xde,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x7e,0xc7,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xde,0xdf,0xbf,0x7e,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xfb,0xae,0xbe,0xe7,0xff,0xf7,0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x9e,0xbf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xdf,0xff,0xff,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xdf,0xff,0xff,0xff,0xdf,0xff,0xdf,0xf7,0xff,0xef,0x00,0x00,0x00,0x00,0x1f,0x77,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x9d,0xcf,0xde,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xde,0xd7,0x00,0x00,0x00,0x00,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xd7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xff,0xd7,0x00,0x00,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xdf,0xff,0xf7,0xdf,0xff,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0xde,0xdf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xe7,0x5f,0x55,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0xdf,0xe7,0xde,0xef,0xff,0xf7,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xdf,0xf7,0xff,0xf7,0xbe,0xe7,0xbf,0xcf,0x00,0x00,0x00,0x00,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xaf,0x00,0x00,0x00,0x00,0x9f,0xc7,0xdf,0xe7,0xdf,0xef,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf7,0xff,0xf7,0xdf,0xf7,0xbf,0xdf,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xdf,0xc7,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xf7,0xdf,0xdf,0x00,0x00,0xff,0xff,0xf7,0x3d,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff,0xdf,0xdf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0x57,0xbf,0xe7,0xde,0xef,0xff,0xf7,0xfe,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xf7,0xdf,0xf7,0xde,0xf7,0xdf,0xdf,0x7d,0x9f,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xcf,0xdf,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xde,0xf7,0xbf,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0xbf,0xd7,0xff,0xef,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0xff,0xcf,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xff,0x00,0x00,0x9e,0xc7,0xff,0xe7,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xff,0xef,0xdf,0xd7,0x00,0x00,0xff,0xff,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xbf,0xb7,0xdf,0xd7,0xdf,0xdf,0xff,0xe7,0xdf,0xe7,0xdf,0xe7,0xdf,0xef,0xff,0xef,0xde,0xe7,0x9f,0xc7,0xfd,0x9e,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xbd,0x76,0xbe,0xd7,0xdf,0xe7,0xff,0xef,0xff,0xef,0xdf,0xe7,0xdf,0xe7,0xff,0xef,0xbf,0xef,0xbf,0xdf,0xbf,0xcf,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x07,0xff,0x7f,0x9e,0xcf,0x9e,0xd7,0x9e,0xd7,0x9e,0xd7,0x9e,0xd7,0x9e,0xd7,0x9e,0xd7,0x9e,0xd7,0x9f,0xd7,0x7c,0x9e,0x00,0x00,0xef,0x7b,0x00,0x00,0x9e,0xcf,0x9e,0xcf,0x9e,0xd7,0x9e,0xdf,0x9e,0xd7,0x9e,0xd7,0xbf,0xdf,0x9e,0xdf,0x9f,0xd7,0x7f,0xcf,0x00,0x00,0xff,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf5,0xaf,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0xc7,0xbf,0xd7,0xff,0xe7,0xff,0xef,0xdf,0xef,0xdf,0xe7,0xdf,0xef,0xff,0xef,0xbf,0xd7,0xdf,0xc7,0xff,0x05,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3c,0xb7,0x9e,0xdf,0x9e,0xdf,0x7e,0xd7,0x9e,0xdf,0x9e,0xdf,0x7e,0xdf,0x7e,0xdf,0x9e,0xdf,0x9e,0xcf,0xff,0x57,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xb7,0x9f,0xd7,0x9e,0xd7,0x9e,0xdf,0x9e,0xdf,0x9e,0xdf,0x9e,0xd7,0x9e,0xd7,0x9e,0xd7,0x9e,0xcf,0xdb,0x6e,0x00,0x00,0x1f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x01,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x07,0x5d,0xb2,0xdb,0xba,0x5f,0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x01,0x00,0x00,0x0c,0x20,0x21,0x24,0x27,0x27,0x2b,0x30,0x30,0x29,0x23,0x25,0x1c,0x18,0x18,0x18,0x1a,0x14,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x14,0x1a,0x19,0x17,0x15,0x1b,0x1e,0x1e,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x11,0xb7,0xff,0xff,0xff,0xff,0xff,0xaf,0x09,0x01,0x01,0x00,0x00,0x00,0x11,0x13,0x10,0x10,0x10,0x10,0x10,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x03,0x02,0x02,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x15,0x36,0x3c,0x08,0x00,0x01,0x00, + 0x00,0x0c,0x8d,0xdc,0xf0,0xf3,0xf4,0xf3,0xf4,0xf6,0xf8,0xf8,0xf4,0xf0,0xf1,0xeb,0xe8,0xe9,0xe8,0xe8,0xe7,0xa2,0x1d,0x00,0x02,0x00,0x03,0x00,0x46,0xe5,0xe8,0xe9,0xe7,0xe4,0xea,0xec,0xf0,0xa4,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x9a,0xff,0xf8,0xfd,0xfb,0xfd,0xfc,0xff,0x9d,0x00,0x02,0x01,0x01,0xa0,0xe3,0xe2,0xe2,0xe3,0xe2,0xe2,0xe3,0xd7,0x31,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x52,0x5f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x04,0x04,0x04,0x02,0x00,0x00,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x09,0x90,0xb6,0x9f,0xa8,0xb0,0x67,0x00,0x02,0x06,0xaa,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xc5,0x21,0x01,0x02,0x04,0x00,0x7b,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xcc,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x22,0xf1,0xf7,0xfe,0xff,0xff,0xff,0xfe,0xfc,0xfa,0x27,0x00,0x00,0x05,0xe1,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x49,0x00,0x03,0x00,0x00,0x00,0x02,0x02,0xc0,0xff,0x3d,0x3b,0x7a,0xa7,0xb2,0x62,0x05,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x03,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x0a,0xa1,0x6b,0x0a,0x00,0x00,0x19,0xb0,0x6f,0x00, + 0x5e,0xff,0xf9,0xfd,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0xfd,0xfc,0xfc,0xfc,0xfd,0xfe,0xfe,0xfe,0xfe,0xfe,0xfd,0xf3,0xff,0x94,0x00,0x03,0x04,0x00,0x78,0xfe,0xfb,0xfe,0xfe,0xfd,0xfe,0xfb,0xfe,0xc6,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x68,0xff,0xf7,0xff,0xff,0xff,0xff,0xff,0xfb,0xff,0x74,0x00,0x02,0x07,0xda,0xfd,0xfb,0xfd,0xfd,0xfd,0xfc,0xfb,0xfd,0x46,0x00,0x03,0x00,0x00,0x00,0x02,0x00,0x9d,0xfe,0xed,0xf5,0xff,0xff,0xff,0xff,0xb4,0x10,0x02,0x01,0x00,0x00,0x00,0x02,0x00,0x18,0x6c,0x80,0x6b,0x28,0x00,0x00,0x28,0x3a,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x7a,0x86,0x00,0xad,0xb0,0xbb,0x5a,0x04,0xc9,0x33,0xaf,0xfe,0xfe,0xfe,0xff,0xff,0xfe,0xfe,0xfc,0xfe,0xff,0xff,0xff,0xff,0xff,0xfb,0xfe,0xfe,0xff,0xff,0xfe,0xfa,0xfe,0xda,0x07,0x00,0x04,0x00,0x7b,0xff,0xfc,0xff,0xff,0xff,0xff,0xfc,0xff,0xc6,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x73,0xfe,0xf9,0xfe,0xff,0xff,0xff,0xff,0xfa,0xfe,0x81,0x00,0x02,0x05,0xd9,0xff,0xfd,0xff,0xff,0xff,0xfe,0xfd,0xff,0x4a,0x00,0x03,0x00,0x01,0x02,0x05,0x89,0xf6,0xfb,0xfe,0xfb,0xfa,0xfb,0xfa,0xf9,0xff,0x9a,0x00,0x03,0x00,0x00,0x02,0x00,0x3f,0xd9,0xff,0xff,0xff,0xf1,0x8c,0x2c,0xd1,0xdc,0x04,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x06,0xa2,0x0b,0x1c,0xc3,0x13,0x1e,0xbf,0x08,0x62,0x9b, + 0xdb,0xff,0xfe,0xff,0xff,0xff,0xfd,0xf9,0xff,0xff,0xdb,0xbd,0xbd,0xcd,0xf6,0xff,0xfc,0xfc,0xfe,0xff,0xff,0xfb,0xff,0xe5,0x0f,0x00,0x05,0x00,0x84,0xff,0xfb,0xff,0xff,0xff,0xff,0xfb,0xff,0xc3,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x6b,0xff,0xf6,0xff,0xff,0xff,0xff,0xff,0xfb,0xff,0x6a,0x00,0x02,0x06,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4e,0x00,0x03,0x00,0x03,0x00,0x65,0xff,0xfe,0xff,0xfe,0xfd,0xff,0xff,0xff,0xfe,0xfc,0xf7,0x26,0x00,0x02,0x02,0x01,0x1b,0xe2,0xff,0xf9,0xfc,0xfc,0xff,0xff,0xf0,0xfd,0x86,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x28,0xa9,0x00,0x1b,0xb4,0x05,0x0b,0xae,0x1b,0x23,0xb6,0xde,0xff,0xfe,0xff,0xff,0xff,0xfc,0xff,0xb9,0x47,0x0c,0x00,0x00,0x05,0x2e,0x91,0xf1,0xf9,0xfd,0xff,0xff,0xfb,0xff,0xe3,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfb,0xff,0xc3,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x38,0xfc,0xf7,0xff,0xff,0xff,0xff,0xff,0xfc,0xfd,0x39,0x00,0x01,0x06,0xdb,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4e,0x00,0x03,0x01,0x00,0x12,0xdb,0xfc,0xfc,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xf9,0xfd,0x80,0x00,0x04,0x03,0x00,0x98,0xff,0xf9,0xff,0xff,0xff,0xfe,0xfb,0xfb,0xff,0xc1,0x15,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x34,0xa1,0x01,0x10,0xd3,0xa6,0xda,0x75,0x00,0x28,0xba, + 0xdf,0xff,0xfe,0xff,0xfd,0xfe,0xff,0x76,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0xe0,0xfe,0xfe,0xff,0xfb,0xff,0xe5,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfb,0xff,0xc3,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0xad,0xff,0xf9,0xfd,0xfd,0xfc,0xf7,0xff,0xbc,0x01,0x02,0x00,0x06,0xdc,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4d,0x00,0x03,0x03,0x00,0x4c,0xff,0xfc,0xfd,0xfe,0xff,0xfe,0xfe,0xfd,0xf9,0xfc,0xff,0xff,0x8e,0x00,0x05,0x00,0x22,0xf4,0xfa,0xfa,0xfd,0xfe,0xff,0xff,0xff,0xfd,0xfb,0xff,0xba,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x14,0xb2,0x01,0x19,0xad,0x1b,0xc1,0x3d,0x02,0x45,0xaa,0xdd,0xff,0xfe,0xfe,0xfa,0xff,0x82,0x00,0x00,0x14,0x69,0xa3,0xa8,0x7f,0x27,0x00,0x00,0x4c,0xfb,0xfc,0xff,0xfc,0xff,0xe8,0x0f,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xc5,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x01,0x1d,0xd7,0xff,0xff,0xff,0xff,0xff,0xd5,0x23,0x01,0x03,0x00,0x07,0xdd,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x03,0x04,0x00,0x67,0xfe,0xfa,0xfd,0xfe,0xf9,0xf5,0xfd,0xff,0xff,0xfb,0xc9,0x82,0x15,0x00,0x02,0x01,0x1c,0xda,0xff,0xff,0xf5,0xfa,0xff,0xff,0xff,0xff,0xfe,0xfd,0xff,0x35,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0xac,0x3e,0x00,0xa9,0x00,0x42,0xa0,0x00,0x9d,0x60, + 0xdd,0xff,0xfe,0xfd,0xff,0xc9,0x02,0x00,0x27,0xd8,0xff,0xff,0xff,0xff,0xe6,0x5d,0x00,0x00,0x85,0xff,0xfc,0xfc,0xff,0xe8,0x0f,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xc5,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x19,0x8f,0xc6,0xd5,0xcb,0x8a,0x15,0x00,0x01,0x01,0x00,0x06,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x03,0x04,0x00,0x70,0xfe,0xf8,0xfa,0xfd,0xff,0xff,0xff,0xd4,0x88,0x3f,0x02,0x00,0x00,0x00,0x00,0x01,0x00,0x13,0x7e,0xe6,0xff,0xff,0xfb,0xfe,0xff,0xff,0xff,0xfb,0xfe,0x76,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x01,0x35,0xcd,0x2e,0x25,0x00,0x00,0x11,0x63,0xab,0x09,0xdd,0xff,0xfe,0xfc,0xff,0x58,0x00,0x14,0xc9,0xff,0xdf,0x8d,0x83,0xb9,0xff,0xff,0x4c,0x00,0x14,0xdf,0xff,0xfb,0xff,0xe7,0x0f,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xc4,0x00,0x01,0x01,0x03,0x04,0x03,0x02,0x02,0x03,0x04,0x03,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x04,0x04,0x03,0x02,0x02,0x04,0x04,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x0c,0x00,0x00,0x00,0x02,0x00,0x01,0x00,0x06,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x03,0x04,0x00,0x59,0xff,0xff,0xff,0xff,0xcb,0x86,0x41,0x07,0x00,0x00,0x04,0x07,0x04,0x03,0x04,0x04,0x04,0x00,0x00,0x18,0x80,0xdd,0xff,0xff,0xfb,0xfe,0xff,0xfb,0xff,0x76,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x04,0x04,0x03,0x03,0x03,0x04,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x51,0xc8,0x7a,0x5a,0x5c,0x8f,0x9c,0x0f,0x00, + 0xdc,0xff,0xfd,0xff,0xec,0x17,0x01,0x71,0xff,0xdb,0x25,0x00,0x00,0x03,0x97,0xff,0xcb,0x08,0x00,0xa3,0xff,0xf8,0xff,0xe2,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xcc,0x03,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x00,0x00,0x00,0x03,0x01,0x00,0x00,0x01,0x00,0x05,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4e,0x00,0x03,0x02,0x00,0x24,0xe9,0xcd,0x98,0x46,0x04,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x00,0x00,0x1a,0x79,0xde,0xff,0xff,0xf4,0xf8,0xff,0x61,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x17,0x6d,0x88,0x8c,0x5b,0x04,0x00,0x01,0xdc,0xff,0xfb,0xff,0xc0,0x01,0x00,0xb5,0xfe,0x4d,0x00,0x07,0x05,0x00,0x0a,0xda,0xfc,0x2f,0x00,0x73,0xff,0xf7,0xff,0xe2,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xc0,0x00,0x00,0x0d,0x3c,0x6d,0x88,0xa3,0xa8,0x9d,0x88,0x58,0x25,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x01,0x2a,0x61,0x7e,0x8d,0xab,0xa7,0x8a,0x70,0x38,0x0c,0x00,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x03,0x04,0x04,0x05,0x04,0x04,0x04,0x04,0x01,0x01,0x00,0x05,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4e,0x00,0x03,0x00,0x00,0x00,0x1c,0x09,0x00,0x00,0x00,0x05,0x00,0x00,0x12,0x42,0x77,0x83,0x89,0x89,0x81,0x76,0x53,0x1c,0x00,0x00,0x04,0x00,0x00,0x1b,0x87,0xec,0xff,0xfa,0xff,0x40,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x10,0x36,0x6e,0x82,0x95,0x95,0x90,0x80,0x42,0x18,0x00,0x00,0x01,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00, + 0xdd,0xff,0xfb,0xff,0xa0,0x00,0x05,0xd9,0xf9,0x15,0x02,0x01,0x00,0x03,0x01,0xa5,0xfe,0x40,0x00,0x53,0xff,0xf6,0xff,0xe2,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xc9,0x3e,0x83,0xd8,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf3,0xca,0x6b,0x10,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x10,0x65,0xc8,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xd9,0x74,0x1b,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x06,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4e,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x03,0x06,0x00,0x00,0x37,0x95,0xdf,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xec,0x9b,0x3c,0x00,0x00,0x07,0x00,0x00,0x25,0x85,0xed,0xdf,0x09,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x36,0x9e,0xdf,0xfa,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe7,0x95,0x38,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x03,0x03,0x03,0x01,0x00,0x00,0xdd,0xff,0xfa,0xff,0x9c,0x00,0x0c,0xde,0xf9,0x1d,0x02,0x02,0x00,0x03,0x00,0xad,0xff,0x4b,0x00,0x54,0xff,0xf7,0xff,0xe2,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfe,0xff,0xfe,0xff,0xff,0xff,0xfe,0xfc,0xfd,0xfd,0xfd,0xfc,0xfb,0xfb,0xfe,0xff,0xff,0xdd,0x5b,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x60,0xdc,0xff,0xff,0xfe,0xfc,0xfb,0xfc,0xfc,0xfc,0xfb,0xfb,0xfe,0xff,0xff,0xe3,0x82,0x0b,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x06,0x50,0x5c,0x70,0x74,0x74,0x73,0x74,0x71,0x67,0x0f,0x00,0x00,0x09,0xdd,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4d,0x00,0x03,0x00,0x00,0x00,0x02,0x01,0x03,0x00,0x1d,0xa1,0xfe,0xff,0xff,0xfe,0xfb,0xfb,0xfb,0xfb,0xfb,0xfb,0xfd,0xff,0xff,0xff,0xb9,0x36,0x00,0x03,0x04,0x00,0x00,0x2a,0x35,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x1e,0xa3,0xfd,0xff,0xff,0xff,0xfb,0xfa,0xfb,0xfb,0xfb,0xfb,0xfe,0xff,0xff,0xfd,0xb4,0x32,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdd,0xff,0xfb,0xff,0xb6,0x00,0x01,0xc4,0xfe,0x57,0x00,0x05,0x04,0x00,0x13,0xee,0xf9,0x2f,0x01,0x75,0xff,0xf9,0xff,0xe2,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfc,0xff,0xff,0xa5,0x12,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x16,0xa4,0xff,0xff,0xfd,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfc,0xff,0xff,0xcd,0x44,0x00,0x04,0x00,0x00,0x00,0x00,0x02,0x00,0x21,0xf3,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x35,0x00,0x00,0x09,0xdd,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4d,0x00,0x03,0x00,0x00,0x00,0x00,0x02,0x00,0x52,0xdf,0xff,0xfe,0xfb,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xfe,0xff,0xf6,0x78,0x00,0x01,0x03,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x00,0x5d,0xe3,0xff,0xfe,0xfa,0xfd,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xfe,0xff,0xf0,0x7d,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xdd,0xff,0xfc,0xff,0xe1,0x0f,0x01,0x79,0xff,0xd9,0x31,0x00,0x00,0x07,0x91,0xfa,0xc3,0x03,0x00,0xa8,0xff,0xf9,0xff,0xe2,0x0e,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xf5,0xff,0xcf,0x1a,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x2b,0xd3,0xff,0xf9,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xf9,0xff,0xf7,0x5d,0x00,0x04,0x00,0x00,0x00,0x02,0x00,0x25,0xf0,0xfa,0xf7,0xf8,0xf8,0xfa,0xfa,0xf7,0xfb,0x35,0x00,0x00,0x0a,0xde,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4d,0x00,0x03,0x00,0x00,0x00,0x03,0x00,0x67,0xf8,0xff,0xfa,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf9,0xfd,0xff,0xa4,0x03,0x02,0x01,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x77,0xff,0xff,0xfb,0xfd,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xf9,0xfd,0xff,0xa2,0x02,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfe,0xfc,0xfb,0x58,0x00,0x11,0xcf,0xff,0xe2,0xae,0xae,0xce,0xff,0xf6,0x48,0x01,0x16,0xe0,0xfe,0xfc,0xff,0xe3,0x0f,0x00,0x05,0x00,0x81,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfd,0xfc,0xff,0xcd,0x15,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x23,0xe1,0xff,0xfb,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfe,0xfe,0xff,0xff,0xfe,0xff,0xfc,0xfe,0xff,0x5a,0x00,0x04,0x00,0x00,0x02,0x00,0x2b,0xfb,0xfd,0xfe,0xff,0xff,0xff,0xff,0xfd,0xff,0x34,0x00,0x01,0x08,0xdb,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4b,0x00,0x03,0x00,0x00,0x04,0x00,0x7c,0xfe,0xfd,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfd,0xf9,0xff,0x9e,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x7e,0xff,0xfa,0xfd,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfa,0xff,0x9f,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xfe,0xfc,0xff,0xd3,0x07,0x00,0x1d,0xc5,0xff,0xff,0xff,0xff,0xd9,0x59,0x00,0x00,0x83,0xff,0xfb,0xfd,0xff,0xe3,0x0f,0x00,0x05,0x00,0x80,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfa,0xff,0xb4,0x06,0x02,0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x11,0xcc,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfb,0xfb,0xfc,0xfc,0xfb,0xfc,0xfe,0xff,0xff,0xff,0xff,0xff,0xfd,0xfd,0xf4,0x42,0x00,0x03,0x00,0x02,0x00,0x2d,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x07,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4b,0x00,0x03,0x00,0x02,0x00,0x57,0xfe,0xfd,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfb,0xff,0x80,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x56,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xff,0x83,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfe,0xff,0xf9,0xff,0x92,0x00,0x00,0x07,0x48,0x7f,0x88,0x65,0x14,0x00,0x00,0x54,0xfa,0xfd,0xfc,0xfd,0xff,0xe3,0x0f,0x00,0x05,0x00,0x80,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xf9,0xff,0x8e,0x00,0x03,0x00,0x00,0x00,0x00,0x02,0x00,0x9a,0xff,0xf5,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0xe2,0x14,0x02,0x01,0x02,0x00,0x2d,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x07,0xdb,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4b,0x00,0x03,0x02,0x01,0x2c,0xf0,0xf9,0xf9,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xf9,0xff,0x46,0x00,0x03,0x00,0x00,0x00,0x02,0x01,0x1d,0xea,0xfd,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfd,0xfc,0xfc,0x37,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xfe,0xff,0xfb,0xf8,0xff,0x92,0x0b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x56,0xf7,0xfe,0xfd,0xfe,0xfd,0xff,0xe2,0x0f,0x00,0x05,0x00,0x80,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfd,0xfb,0xfc,0xfc,0xfa,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xf9,0x35,0x00,0x03,0x00,0x00,0x03,0x00,0x51,0xfb,0xfa,0xfa,0xff,0xff,0xff,0xfe,0xfb,0xff,0xff,0xd4,0x8f,0x60,0x5c,0x5d,0x6d,0xb0,0xf1,0xff,0xfc,0xfe,0xff,0xff,0xff,0xff,0xfa,0xff,0x97,0x00,0x03,0x02,0x00,0x2e,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x00,0x08,0xdc,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4b,0x00,0x03,0x02,0x00,0xbd,0xff,0xf6,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfc,0xfb,0xfb,0xf8,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xff,0xd1,0x0d,0x02,0x01,0x00,0x00,0x02,0x00,0xa3,0xff,0xfa,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xf9,0xf9,0xfa,0xfc,0xfc,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xff,0xbe,0x04,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfe,0xff,0xff,0xff,0xfd,0xff,0xcc,0x59,0x22,0x15,0x11,0x18,0x42,0xa6,0xfb,0xfb,0xfd,0xff,0xff,0xfd,0xff,0xe2,0x0f,0x00,0x05,0x00,0x80,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xfa,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0xb0,0x00,0x02,0x00,0x00,0x01,0x09,0xd3,0xff,0xfd,0xff,0xff,0xff,0xff,0xfa,0xff,0xd7,0x59,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x28,0xad,0xff,0xfd,0xfe,0xff,0xff,0xff,0xff,0xfd,0xf1,0x2c,0x01,0x05,0x00,0x2e,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x00,0x08,0xdc,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4b,0x00,0x06,0x00,0x59,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfa,0xff,0xff,0xff,0xff,0xff,0xff,0xfa,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0x77,0x00,0x03,0x00,0x03,0x00,0x3d,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfb,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0x4f,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xfe,0xff,0xff,0xff,0xfe,0xf8,0xff,0xff,0xf5,0xdf,0xd5,0xe5,0xff,0xff,0xff,0xfd,0xff,0xff,0xfe,0xfd,0xff,0xe3,0x10,0x00,0x05,0x00,0x80,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xfb,0xc8,0xac,0xa8,0xab,0xd8,0xff,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xf9,0x34,0x00,0x03,0x04,0x00,0x5a,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xb5,0x18,0x00,0x00,0x24,0x5c,0x79,0x70,0x42,0x07,0x00,0x00,0x6d,0xfd,0xfe,0xff,0xff,0xff,0xff,0xfa,0xff,0x91,0x00,0x05,0x00,0x2c,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x00,0x09,0xdd,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4b,0x00,0x03,0x05,0xce,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0xf3,0xbe,0xa5,0xa3,0xb9,0xea,0xff,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfe,0xe1,0x11,0x00,0x01,0x02,0x00,0xa4,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfd,0xff,0xe9,0xac,0x92,0x91,0xb1,0xe7,0xff,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0xbd,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfe,0xff,0xff,0xff,0xff,0xfe,0xf9,0xef,0xfc,0xfe,0xfb,0xfd,0xfb,0xf9,0xfc,0xfd,0xff,0xff,0xfe,0xfd,0xff,0xe5,0x10,0x00,0x05,0x00,0x81,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfb,0xff,0xb2,0x39,0x00,0x00,0x00,0x00,0x11,0x6a,0xdb,0xff,0xfb,0xfe,0xff,0xff,0xff,0xff,0xff,0xf9,0xff,0x8a,0x00,0x04,0x02,0x02,0xc0,0xff,0xfb,0xff,0xff,0xfe,0xfc,0xff,0xbe,0x0c,0x00,0x19,0xa6,0xfd,0xff,0xf3,0xf4,0xff,0xdf,0x62,0x06,0x00,0x6b,0xfa,0xfa,0xfe,0xff,0xff,0xfb,0xfe,0xdc,0x17,0x01,0x00,0x2d,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x00,0x09,0xdc,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4d,0x00,0x00,0x46,0xfe,0xf5,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xf8,0xff,0xa3,0x2f,0x00,0x00,0x00,0x00,0x1b,0x76,0xec,0xff,0xfc,0xfe,0xff,0xff,0xff,0xff,0xfe,0xfb,0xff,0x59,0x00,0x05,0x00,0x21,0xec,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfd,0xfa,0xfb,0xfd,0x8d,0x1e,0x00,0x00,0x00,0x00,0x17,0x7a,0xf9,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfe,0xfd,0xf8,0x2d,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xfe,0xff,0xff,0xff,0xff,0xfd,0xf6,0xf6,0xff,0xff,0xff,0xff,0xff,0xfd,0xfc,0xff,0xff,0xff,0xfe,0xfe,0xff,0xe7,0x10,0x00,0x05,0x00,0x82,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xff,0x7d,0x00,0x00,0x01,0x03,0x03,0x02,0x00,0x00,0x1f,0xcb,0xff,0xfb,0xfe,0xff,0xff,0xff,0xff,0xfc,0xff,0xd5,0x08,0x01,0x00,0x2a,0xfb,0xfe,0xfd,0xff,0xff,0xfb,0xfc,0xdf,0x20,0x00,0x2f,0xdc,0xf7,0x7a,0x2c,0x0f,0x11,0x42,0xb0,0xfc,0x23,0x02,0x00,0xa5,0xff,0xfb,0xff,0xff,0xff,0xfc,0xff,0x5c,0x00,0x00,0x2f,0xfd,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x00,0x08,0xdb,0xff,0xfd,0xff,0xff,0xff,0xff,0xfc,0xff,0x51,0x00,0x00,0x84,0xff,0xf4,0xff,0xff,0xff,0xff,0xff,0xfe,0xfa,0xfe,0x69,0x00,0x00,0x02,0x03,0x03,0x02,0x00,0x00,0x39,0xee,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xfc,0xfe,0xb3,0x00,0x04,0x00,0x63,0xff,0xfa,0xff,0xff,0xff,0xff,0xff,0xfa,0xf8,0xfc,0x67,0x00,0x00,0x03,0x03,0x03,0x02,0x00,0x00,0x50,0xf5,0xf9,0xfe,0xff,0xff,0xff,0xff,0xfe,0xfc,0xff,0x70,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfe,0xff,0xff,0xff,0xfd,0xff,0xff,0xe0,0xc1,0x95,0x84,0xa0,0xd9,0xfc,0xff,0xfc,0xfe,0xff,0xfe,0xfe,0xff,0xe6,0x10,0x00,0x05,0x00,0x82,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0x95,0x00,0x03,0x03,0x00,0x00,0x00,0x00,0x01,0x05,0x00,0x28,0xe4,0xf5,0xfe,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfc,0x33,0x00,0x00,0x66,0xff,0xf9,0xfe,0xff,0xfe,0xf4,0xff,0x62,0x00,0x1c,0xe5,0xd5,0x1e,0x00,0x00,0x01,0x00,0x00,0x00,0x25,0x04,0x02,0x00,0x2b,0xf5,0xfc,0xff,0xff,0xff,0xfb,0xfe,0xa9,0x00,0x00,0x2d,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x00,0x08,0xdb,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4a,0x00,0x06,0xc9,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0x7a,0x00,0x04,0x02,0x00,0x00,0x00,0x00,0x01,0x06,0x00,0x4a,0xfa,0xfb,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xf4,0x1b,0x00,0x00,0xa8,0xfe,0xfb,0xff,0xff,0xff,0xff,0xff,0xfb,0xff,0x89,0x00,0x05,0x02,0x00,0x00,0x00,0x00,0x01,0x06,0x00,0x7a,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0xb6,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xfe,0xfe,0xfe,0xfb,0xff,0xe9,0x6d,0x16,0x00,0x00,0x00,0x00,0x0e,0x48,0xc8,0xff,0xfc,0xff,0xfe,0xfe,0xff,0xe6,0x10,0x00,0x05,0x00,0x82,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0xd2,0x08,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x6d,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfa,0xff,0x76,0x00,0x00,0xa4,0xff,0xf9,0xfe,0xff,0xfe,0xfe,0xe6,0x15,0x00,0xac,0xf0,0x18,0x00,0x04,0x19,0xce,0x3c,0x03,0x03,0x00,0x00,0x00,0x03,0x00,0xa4,0xff,0xfd,0xff,0xff,0xfd,0xff,0xdd,0x0a,0x00,0x2f,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x00,0x08,0xdb,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4a,0x00,0x2c,0xf7,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfd,0xff,0xc8,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x95,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x46,0x00,0x07,0xc6,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfd,0xff,0xdb,0x09,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x06,0xc7,0xff,0xfd,0xff,0xff,0xff,0xfe,0xfc,0xff,0xd9,0x05,0x00,0x01,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfe,0xff,0xfa,0xff,0xce,0x25,0x00,0x00,0x0d,0x2e,0x2f,0x1d,0x00,0x00,0x0b,0x8d,0xff,0xfb,0xfe,0xfd,0xff,0xe7,0x0f,0x00,0x05,0x00,0x82,0xff,0xfb,0xff,0xff,0xff,0xff,0xfe,0xf9,0xff,0x6c,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x12,0xe7,0xff,0xfe,0xff,0xff,0xff,0xff,0xfb,0xff,0x96,0x00,0x07,0xdb,0xff,0xfd,0xff,0xff,0xfc,0xff,0xa8,0x00,0x28,0xff,0x72,0x00,0x07,0x00,0x25,0xff,0x57,0x00,0x04,0x02,0x00,0x00,0x04,0x01,0x5c,0xff,0xfc,0xff,0xff,0xfe,0xff,0xf4,0x23,0x00,0x32,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0x35,0x00,0x01,0x07,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfc,0xff,0x52,0x00,0x4f,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0x5d,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x02,0x2f,0xf9,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfc,0xff,0x6c,0x00,0x16,0xe3,0xff,0xfe,0xff,0xff,0xff,0xff,0xfb,0xff,0x8f,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x80,0xff,0xfb,0xff,0xff,0xff,0xff,0xfe,0xff,0xf1,0x1e,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xfe,0xfe,0xfe,0xc6,0x14,0x00,0x15,0x90,0xdc,0xff,0xff,0xe9,0xae,0x3d,0x00,0x00,0x9d,0xff,0xfc,0xfd,0xff,0xe7,0x0f,0x00,0x05,0x00,0x83,0xff,0xfb,0xff,0xff,0xff,0xff,0xfd,0xfd,0xf4,0x2d,0x01,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0xa8,0xff,0xfc,0xff,0xff,0xff,0xff,0xfd,0xff,0xb8,0x00,0x12,0xe7,0xff,0xfe,0xff,0xff,0xfd,0xff,0x5c,0x01,0x6d,0xf4,0x14,0x01,0x03,0x00,0x21,0xfb,0x4f,0x00,0x03,0x00,0x00,0x00,0x02,0x00,0x28,0xfa,0xff,0xfe,0xff,0xff,0xff,0xff,0x3b,0x00,0x33,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0x35,0x00,0x01,0x07,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x48,0x00,0x7a,0xff,0xfc,0xff,0xff,0xff,0xff,0xfe,0xff,0xf5,0x1e,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0xce,0xff,0xfb,0xff,0xff,0xff,0xff,0xfb,0xff,0x96,0x00,0x2b,0xf8,0xfc,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x01,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x01,0x4a,0xff,0xfc,0xff,0xff,0xff,0xff,0xfe,0xff,0xfc,0x2a,0x00,0x02,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfd,0xfe,0xef,0x2b,0x00,0x28,0xd0,0xff,0xff,0xa4,0xb2,0xff,0xff,0xfa,0x60,0x00,0x09,0xcf,0xfe,0xfb,0xff,0xe7,0x10,0x00,0x05,0x00,0x7f,0xff,0xfb,0xff,0xff,0xff,0xff,0xfb,0xfe,0xe0,0x0c,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x01,0x7f,0xff,0xfb,0xff,0xff,0xff,0xff,0xfd,0xff,0xcf,0x00,0x1c,0xf2,0xff,0xfe,0xff,0xfe,0xff,0xfb,0x3d,0x00,0xb9,0xb1,0x00,0x02,0x02,0x00,0x21,0xfb,0x50,0x01,0x03,0x00,0x00,0x00,0x01,0x00,0x12,0xed,0xff,0xfe,0xff,0xff,0xfe,0xff,0x4a,0x00,0x32,0xfe,0xfd,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0x35,0x00,0x01,0x07,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x47,0x00,0x91,0xff,0xfc,0xff,0xff,0xff,0xff,0xfd,0xff,0xd0,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0xa7,0xff,0xf9,0xff,0xff,0xff,0xff,0xfc,0xff,0xa9,0x00,0x36,0xfe,0xf8,0xfc,0xff,0xff,0xff,0xff,0xff,0xfe,0x30,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x2d,0xfd,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0xfc,0x2d,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xfb,0xff,0x86,0x00,0x13,0xc4,0xff,0xfa,0xf3,0x09,0x18,0xf6,0xfa,0xfd,0xfb,0x35,0x00,0x54,0xff,0xfa,0xff,0xe9,0x12,0x00,0x05,0x00,0x76,0xff,0xfc,0xff,0xff,0xff,0xff,0xfd,0xff,0xd7,0x05,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x68,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xc7,0x00,0x25,0xfa,0xff,0xfe,0xff,0xff,0xfe,0xff,0x3c,0x00,0xd9,0xa1,0x01,0x03,0x02,0x00,0x29,0xff,0x51,0x00,0x05,0x00,0x00,0x00,0x01,0x00,0x09,0xe1,0xff,0xfd,0xff,0xff,0xfe,0xff,0x48,0x00,0x2f,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x07,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x51,0x00,0x89,0xff,0xfc,0xff,0xff,0xff,0xff,0xfd,0xff,0xc6,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x8d,0xff,0xfa,0xff,0xff,0xff,0xff,0xfc,0xff,0xa3,0x00,0x3f,0xff,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0xfd,0x2a,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x24,0xf7,0xff,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x38,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xdc,0xff,0xfe,0xf9,0x28,0x00,0x7b,0xff,0xf9,0xff,0xea,0x12,0x17,0xee,0xff,0xf8,0xff,0xbb,0x03,0x0c,0xdd,0xff,0xff,0xeb,0x12,0x00,0x05,0x00,0x71,0xff,0xfc,0xff,0xff,0xff,0xff,0xfd,0xff,0xdd,0x0a,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x7b,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xc5,0x00,0x25,0xfa,0xff,0xfe,0xff,0xff,0xfd,0xff,0x42,0x00,0xdc,0xa6,0x00,0x03,0x02,0x00,0x1d,0xe9,0xbd,0x16,0x00,0x02,0x00,0x00,0x01,0x00,0x0c,0xe2,0xff,0xfd,0xff,0xff,0xfe,0xff,0x48,0x00,0x2f,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x07,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x51,0x00,0x8d,0xff,0xfb,0xff,0xff,0xff,0xff,0xfd,0xff,0xce,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x01,0x97,0xff,0xfa,0xff,0xff,0xff,0xff,0xfc,0xff,0xa6,0x00,0x3f,0xff,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0xfd,0x2a,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x24,0xf6,0xff,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x38,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0xdc,0xff,0xff,0xd4,0x04,0x0a,0xd1,0xfe,0xfd,0xff,0xeb,0x0f,0x17,0xea,0xfb,0xfa,0xfb,0xfc,0x3a,0x00,0x95,0xff,0xff,0xed,0x13,0x00,0x05,0x00,0x65,0xff,0xfc,0xff,0xff,0xff,0xff,0xfd,0xff,0xf2,0x1e,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x98,0xff,0xfb,0xff,0xff,0xff,0xff,0xfc,0xff,0xba,0x00,0x1f,0xf4,0xff,0xfe,0xff,0xff,0xfd,0xff,0x52,0x00,0xa5,0xc2,0x01,0x01,0x00,0x01,0x00,0x3e,0xf0,0xd5,0x1c,0x00,0x02,0x00,0x01,0x00,0x15,0xec,0xff,0xfe,0xff,0xff,0xfe,0xfe,0x44,0x00,0x30,0xfb,0xfd,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x07,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4f,0x00,0x85,0xff,0xfb,0xff,0xff,0xff,0xff,0xfd,0xff,0xe0,0x0a,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0xaf,0xff,0xfc,0xff,0xff,0xff,0xff,0xfb,0xff,0xa1,0x00,0x3e,0xff,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0xfd,0x29,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x24,0xf6,0xff,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x38,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xdd,0xff,0xff,0xae,0x00,0x38,0xfe,0xfe,0xfe,0xff,0xeb,0x09,0x20,0xf5,0xff,0xff,0xfa,0xff,0x7d,0x01,0x73,0xff,0xff,0xea,0x11,0x00,0x04,0x00,0x4d,0xff,0xfd,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfd,0x4b,0x01,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x06,0xcf,0xff,0xfd,0xff,0xff,0xff,0xfe,0xfa,0xff,0x99,0x00,0x08,0xd9,0xff,0xfd,0xff,0xff,0xfc,0xff,0x7f,0x00,0x58,0xf8,0x1a,0x01,0x02,0x00,0x02,0x00,0x44,0xfe,0xc9,0x1e,0x01,0x01,0x03,0x01,0x43,0xfc,0xfe,0xff,0xff,0xfe,0xff,0xf9,0x30,0x00,0x33,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x06,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4c,0x00,0x5c,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfe,0xfb,0x3d,0x01,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x01,0x1e,0xf2,0xff,0xfe,0xff,0xff,0xff,0xff,0xfb,0xff,0x7c,0x00,0x38,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfc,0x25,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x23,0xf8,0xfd,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x37,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0xdd,0xff,0xff,0xa1,0x00,0x53,0xff,0xfd,0xfe,0xff,0xe9,0x0b,0x04,0x61,0xba,0xf2,0xfa,0xff,0x93,0x00,0x64,0xff,0xff,0xea,0x11,0x00,0x03,0x00,0x25,0xf6,0xff,0xfe,0xff,0xff,0xff,0xff,0xfb,0xff,0xb4,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x55,0xff,0xfd,0xff,0xff,0xff,0xff,0xfe,0xfb,0xff,0x73,0x00,0x00,0xb4,0xff,0xfc,0xff,0xff,0xfc,0xff,0xc9,0x00,0x1a,0xf3,0x85,0x00,0x02,0x00,0x00,0x04,0x00,0x42,0xda,0x59,0x00,0x03,0x03,0x00,0xa3,0xff,0xfc,0xff,0xff,0xfd,0xff,0xe1,0x0f,0x00,0x32,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x06,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4b,0x00,0x3e,0xff,0xfe,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0x9d,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x81,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0x56,0x00,0x37,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfc,0x25,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x22,0xf8,0xfd,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x37,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xdd,0xff,0xff,0xa4,0x01,0x4d,0xff,0xfd,0xfe,0xfd,0xf7,0x39,0x00,0x00,0x00,0x22,0xd8,0xfe,0x91,0x00,0x63,0xff,0xff,0xea,0x11,0x00,0x02,0x00,0x01,0xd0,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xfd,0xff,0x65,0x00,0x05,0x02,0x00,0x00,0x00,0x00,0x01,0x04,0x00,0x1a,0xd9,0xfd,0xfe,0xff,0xff,0xff,0xff,0xff,0xfc,0xfb,0x3e,0x00,0x00,0x8d,0xff,0xfa,0xff,0xff,0xff,0xfc,0xfc,0x44,0x00,0x83,0xff,0x3d,0x00,0x03,0x00,0x00,0x02,0x00,0x0d,0x03,0x00,0x02,0x01,0x22,0xec,0xfd,0xfe,0xff,0xff,0xfc,0xfe,0xae,0x00,0x00,0x32,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x34,0x00,0x01,0x06,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x48,0x00,0x15,0xeb,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xfc,0xf6,0x4c,0x00,0x05,0x01,0x00,0x00,0x00,0x00,0x01,0x04,0x00,0x21,0xea,0xfc,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0xfc,0x27,0x00,0x36,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfc,0x25,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x22,0xf8,0xfd,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x37,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0xda,0xff,0xff,0xbf,0x00,0x30,0xfd,0xfd,0xfd,0xfd,0xff,0xf2,0xb4,0x72,0x2c,0x10,0xd8,0xff,0x6f,0x00,0x7c,0xff,0xff,0xe6,0x12,0x00,0x01,0x03,0x00,0x9f,0xfe,0xf7,0xfe,0xff,0xff,0xff,0xff,0xfe,0xfd,0xfa,0x51,0x00,0x00,0x02,0x04,0x04,0x03,0x00,0x00,0x10,0xb7,0xff,0xfa,0xff,0xff,0xff,0xff,0xff,0xfd,0xfe,0xdd,0x0f,0x01,0x00,0x41,0xfd,0xf9,0xfd,0xff,0xff,0xfc,0xff,0xb6,0x00,0x08,0x7c,0x2c,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0x01,0x04,0x00,0xa9,0xff,0xfb,0xff,0xff,0xff,0xfb,0xff,0x70,0x00,0x00,0x2f,0xfd,0xfe,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x36,0x00,0x01,0x05,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x50,0x00,0x00,0xa8,0xfe,0xfa,0xff,0xff,0xff,0xff,0xff,0xfd,0xff,0xe9,0x3b,0x00,0x00,0x02,0x03,0x04,0x03,0x00,0x00,0x25,0xd1,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfc,0xfe,0xc4,0x03,0x00,0x30,0xfe,0xfd,0xfd,0xff,0xff,0xff,0xfe,0xff,0xfc,0x27,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x23,0xf7,0xfd,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x37,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xda,0xff,0xff,0xe1,0x0c,0x05,0xd3,0xfe,0xfc,0xff,0xff,0xff,0xff,0xff,0xfb,0xe3,0xf8,0xef,0x24,0x00,0xac,0xff,0xff,0xe6,0x12,0x00,0x01,0x03,0x00,0x51,0xff,0xf9,0xfe,0xff,0xff,0xff,0xff,0xff,0xfd,0xfe,0xf9,0x89,0x1e,0x00,0x00,0x00,0x00,0x07,0x4d,0xd2,0xff,0xf8,0xfd,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0x9e,0x00,0x04,0x00,0x08,0xd9,0xff,0xfd,0xff,0xff,0xff,0xfa,0xfb,0x81,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x03,0x00,0x79,0xff,0xfc,0xff,0xff,0xff,0xfe,0xfd,0xf5,0x29,0x00,0x00,0x2f,0xfd,0xfe,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x36,0x00,0x01,0x05,0xda,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x51,0x00,0x00,0x55,0xff,0xfb,0xff,0xff,0xff,0xff,0xff,0xfe,0xfd,0xff,0xf2,0x76,0x15,0x00,0x00,0x00,0x00,0x0c,0x62,0xe9,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xf5,0xff,0x69,0x00,0x00,0x2e,0xfe,0xfd,0xfd,0xff,0xff,0xff,0xfe,0xff,0xfc,0x27,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x23,0xf7,0xfd,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x37,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0xda,0xff,0xfb,0xff,0x4a,0x00,0x64,0xff,0xfb,0xfe,0xff,0xfd,0xfb,0xfc,0xfe,0xfc,0xff,0x92,0x00,0x11,0xe5,0xff,0xff,0xe6,0x12,0x00,0x01,0x01,0x01,0x09,0xd6,0xfe,0xfb,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xff,0xff,0xe3,0x9a,0x7e,0x80,0x92,0xc8,0xff,0xfe,0xf8,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xfd,0xfe,0x3f,0x00,0x03,0x04,0x00,0x89,0xff,0xfb,0xff,0xff,0xff,0xfe,0xfc,0xff,0x7b,0x01,0x00,0x02,0x03,0x03,0x03,0x03,0x03,0x01,0x00,0x00,0x74,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfb,0xff,0x9f,0x00,0x05,0x00,0x2f,0xfd,0xfe,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x36,0x00,0x01,0x05,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4f,0x00,0x01,0x13,0xe4,0xfd,0xfd,0xff,0xff,0xff,0xff,0xfe,0xfd,0xfd,0xff,0xff,0xe4,0xa0,0x76,0x75,0x8f,0xda,0xff,0xff,0xfd,0xfe,0xff,0xff,0xff,0xff,0xff,0xfc,0xfb,0xec,0x23,0x00,0x00,0x2e,0xfe,0xfe,0xfd,0xff,0xff,0xff,0xfe,0xff,0xfc,0x27,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x23,0xf7,0xfd,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x38,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xd9,0xff,0xfb,0xff,0xae,0x00,0x05,0x9b,0xff,0xfa,0xfb,0xfc,0xfa,0xf8,0xf8,0xff,0xd0,0x17,0x00,0x68,0xff,0xfa,0xff,0xe9,0x11,0x00,0x01,0x00,0x03,0x00,0x68,0xff,0xf9,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xf9,0xfa,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xff,0xca,0x03,0x01,0x00,0x02,0x01,0x1d,0xea,0xfd,0xfe,0xff,0xfe,0xff,0xfe,0xfd,0xff,0xab,0x35,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x2d,0x9e,0xff,0xfd,0xfe,0xff,0xff,0xff,0xff,0xfc,0xfa,0x39,0x00,0x05,0x00,0x33,0xff,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x35,0x00,0x02,0x03,0xd7,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x4a,0x00,0x07,0x00,0x87,0xff,0xfa,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfc,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfb,0xff,0x96,0x00,0x06,0x00,0x37,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfc,0x27,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x21,0xf5,0xfc,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x39,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0xd9,0xff,0xfe,0xfd,0xff,0x5f,0x00,0x0a,0xa1,0xff,0xff,0xff,0xff,0xff,0xff,0xc4,0x21,0x00,0x22,0xe3,0xfd,0xfc,0xff,0xea,0x11,0x00,0x01,0x00,0x01,0x02,0x07,0xcd,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xfc,0xfc,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfb,0xff,0x4c,0x00,0x03,0x00,0x00,0x03,0x00,0x7a,0xff,0xfa,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0xfb,0xaf,0x73,0x45,0x3a,0x43,0x70,0xb1,0xed,0xff,0xfc,0xfe,0xff,0xff,0xff,0xff,0xfc,0xff,0xa2,0x00,0x02,0x03,0x00,0x34,0xff,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x35,0x00,0x02,0x03,0xd6,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x05,0x01,0x17,0xe6,0xfe,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfc,0xfc,0xfc,0xfc,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfd,0xe9,0x1c,0x01,0x05,0x00,0x3b,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfc,0x27,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x20,0xf5,0xfc,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x39,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xd9,0xff,0xfe,0xfd,0xfb,0xf4,0x40,0x00,0x00,0x48,0x90,0xc2,0xd2,0xaf,0x56,0x09,0x00,0x1c,0xc6,0xff,0xfa,0xfe,0xff,0xea,0x11,0x00,0x01,0x00,0x00,0x02,0x00,0x3a,0xf6,0xfc,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfb,0xfe,0x8c,0x00,0x02,0x00,0x00,0x00,0x01,0x02,0x08,0xca,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xfb,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xfe,0xff,0xff,0xff,0xff,0xfd,0xfe,0xed,0x1d,0x01,0x02,0x02,0x00,0x34,0xff,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x35,0x00,0x01,0x04,0xd6,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x03,0x03,0x00,0x50,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfb,0xff,0x68,0x00,0x03,0x03,0x00,0x3a,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfc,0x27,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x20,0xf5,0xfc,0xfd,0xff,0xff,0xff,0xff,0xfe,0xff,0x39,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0xd9,0xff,0xfe,0xff,0xfc,0xff,0xf3,0x5b,0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x39,0xd5,0xff,0xf9,0xff,0xfd,0xff,0xe9,0x11,0x00,0x01,0x00,0x00,0x00,0x03,0x00,0x73,0xff,0xfa,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0xf8,0xff,0xc4,0x05,0x02,0x01,0x00,0x00,0x00,0x00,0x02,0x00,0x32,0xf2,0xfd,0xfb,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfc,0xfa,0xfd,0xfd,0xfd,0xfc,0xfc,0xfe,0xfe,0xfe,0xff,0xff,0xff,0xfd,0xfb,0xff,0x5b,0x00,0x02,0x00,0x02,0x00,0x34,0xff,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x35,0x00,0x01,0x05,0xd9,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x03,0x00,0x03,0x00,0x8e,0xff,0xf9,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfa,0xff,0xb0,0x01,0x03,0x01,0x03,0x00,0x39,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfd,0x29,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x21,0xf6,0xfd,0xfc,0xff,0xff,0xff,0xff,0xfe,0xff,0x39,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xd7,0xff,0xfe,0xff,0xff,0xfe,0xff,0xfe,0xbb,0x52,0x24,0x0d,0x0a,0x1a,0x45,0xa6,0xf5,0xff,0xfd,0xff,0xff,0xfd,0xff,0xe6,0x10,0x00,0x01,0x00,0x00,0x00,0x01,0x04,0x00,0x9d,0xff,0xfa,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfa,0xff,0xe4,0x2c,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x52,0xfb,0xfe,0xfc,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfa,0xff,0x78,0x00,0x04,0x00,0x00,0x02,0x00,0x34,0xff,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x35,0x00,0x01,0x05,0xdb,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x03,0x00,0x01,0x02,0x03,0xac,0xff,0xfa,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfa,0xff,0xc1,0x0d,0x01,0x01,0x00,0x03,0x00,0x39,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfd,0x2a,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x22,0xf7,0xfd,0xfc,0xff,0xff,0xff,0xff,0xfe,0xff,0x39,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0xd8,0xfe,0xfe,0xff,0xff,0xff,0xfe,0xfd,0xff,0xff,0xf3,0xd9,0xd7,0xea,0xfe,0xff,0xff,0xfd,0xff,0xff,0xff,0xfd,0xff,0xe7,0x10,0x00,0x01,0x00,0x00,0x00,0x00,0x01,0x02,0x03,0x90,0xff,0xff,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfc,0xfd,0xd9,0x2e,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x5a,0xf5,0xff,0xfb,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0xff,0xff,0x7c,0x00,0x03,0x00,0x00,0x00,0x02,0x00,0x34,0xff,0xfe,0xfe,0xff,0xff,0xff,0xff,0xfe,0xff,0x36,0x00,0x01,0x07,0xdc,0xff,0xfd,0xff,0xff,0xff,0xff,0xfd,0xff,0x49,0x00,0x03,0x00,0x00,0x01,0x01,0x0a,0xb4,0xff,0xff,0xfd,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xff,0xc0,0x12,0x00,0x02,0x00,0x00,0x03,0x00,0x3b,0xff,0xfd,0xfe,0xff,0xff,0xff,0xfe,0xff,0xfe,0x2b,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x22,0xf7,0xfd,0xfc,0xff,0xff,0xff,0xff,0xfe,0xff,0x39,0x00,0x03,0x00,0x00,0x00,0x00,0x00, + 0xb4,0xff,0xfa,0xfd,0xfe,0xfe,0xfe,0xfc,0xfb,0xfb,0xff,0xff,0xff,0xff,0xfd,0xfb,0xfe,0xff,0xfe,0xfe,0xff,0xf9,0xfe,0xc5,0x06,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,0x6e,0xf1,0xff,0xfc,0xfc,0xfc,0xfe,0xff,0xff,0xff,0xff,0xff,0xfc,0xfd,0xff,0xfd,0xf8,0xfe,0xff,0xbc,0x1c,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x3c,0xd0,0xff,0xff,0xfb,0xfb,0xfa,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xfb,0xfc,0xff,0xe6,0x61,0x00,0x03,0x01,0x00,0x00,0x00,0x02,0x00,0x32,0xfe,0xfd,0xfe,0xff,0xff,0xff,0xff,0xfe,0xfb,0x30,0x00,0x01,0x07,0xdb,0xff,0xfc,0xfd,0xfd,0xfd,0xfd,0xfc,0xff,0x4a,0x00,0x03,0x00,0x00,0x00,0x02,0x00,0x08,0x8a,0xfa,0xff,0xfa,0xfc,0xfe,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfd,0xfc,0xfa,0xfe,0xff,0x9c,0x0f,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x34,0xfd,0xfc,0xfd,0xff,0xff,0xff,0xfe,0xfe,0xf9,0x25,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x25,0xf8,0xfe,0xfc,0xfe,0xfe,0xfe,0xff,0xff,0xff,0x33,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x40,0xfa,0xfd,0xf8,0xfc,0xfc,0xfc,0xfc,0xfc,0xfc,0xfb,0xfa,0xfa,0xfb,0xfc,0xfd,0xfd,0xfd,0xfc,0xfc,0xfb,0xfb,0xff,0x57,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x00,0x30,0xb2,0xff,0xff,0xff,0xfd,0xfd,0xfd,0xfe,0xfe,0xfe,0xf4,0xfa,0xff,0xff,0xff,0xe9,0x75,0x06,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x10,0x8a,0xf3,0xff,0xff,0xf4,0xf9,0xfc,0xfd,0xfd,0xfd,0xfd,0xfc,0xfd,0xff,0xff,0xff,0xaa,0x23,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x30,0xfe,0xfb,0xfc,0xfc,0xfc,0xfc,0xfb,0xfb,0xfb,0x2d,0x00,0x00,0x09,0xdc,0xfd,0xf7,0xf9,0xfa,0xfa,0xfa,0xf9,0xfe,0x51,0x00,0x03,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x3f,0xc4,0xff,0xff,0xff,0xfc,0xfb,0xfc,0xfd,0xfe,0xfc,0xfc,0xfd,0xfc,0xff,0xff,0xd5,0x4c,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x02,0x00,0x2c,0xf9,0xfa,0xfa,0xfb,0xfb,0xfb,0xfa,0xfc,0xf5,0x1f,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x27,0xf5,0xfc,0xfb,0xfa,0xfa,0xfb,0xfb,0xfb,0xfe,0x30,0x00,0x02,0x00,0x00,0x00,0x00,0x00, + 0x00,0x5c,0xf4,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0x8e,0x03,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x42,0x9f,0xe1,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf9,0xdc,0x8f,0x22,0x00,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x28,0x88,0xda,0xf7,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe9,0xa9,0x4c,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x27,0xfb,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0x2a,0x00,0x01,0x04,0xd3,0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xff,0x4a,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x00,0x06,0x64,0xc6,0xef,0xfc,0xff,0xff,0xff,0xff,0xff,0xff,0xfd,0xf1,0xce,0x6a,0x0f,0x00,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x29,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf8,0x20,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x24,0xf6,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xfc,0x28,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x42,0x99,0xb1,0xb4,0xbb,0xbf,0xbf,0xbf,0xbf,0xbf,0xbc,0xb9,0xba,0xb9,0xb9,0xb9,0xb9,0xb9,0xb3,0x5a,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x01,0x00,0x00,0x1c,0x5b,0x92,0xb6,0xce,0xd8,0xcb,0xb4,0x8a,0x40,0x0f,0x00,0x00,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x00,0x00,0x0d,0x3c,0x7f,0x9b,0xbd,0xd1,0xd1,0xb6,0xa2,0x66,0x24,0x00,0x00,0x01,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x04,0x5b,0x8c,0x80,0x80,0x81,0x80,0x80,0x82,0x63,0x0a,0x00,0x02,0x00,0x47,0x81,0x87,0x81,0x7c,0x7c,0x7f,0x83,0x63,0x0f,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x2d,0x5d,0x97,0xb4,0xcb,0xd2,0xc3,0xa9,0x7f,0x34,0x04,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x63,0x75,0x74,0x77,0x76,0x7b,0x80,0x83,0x5e,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x55,0x6f,0x6d,0x6f,0x72,0x75,0x77,0x7e,0x68,0x07,0x00,0x01,0x00,0x00,0x00,0x00,0x00, + +}; + +const lv_image_dsc_t _tomatotimers_RGB565A8_500x220 = { + .header.magic = LV_IMAGE_HEADER_MAGIC, + .header.cf = LV_COLOR_FORMAT_RGB565A8, + .header.flags = 0, + .header.w = 200, + .header.h = 50, + .header.stride = 400, + .data_size = sizeof(_tomatotimers_RGB565A8_500x220_map), + .data = _tomatotimers_RGB565A8_500x220_map, +}; + diff --git a/main/boards/jiuchuang-s3/config.h b/main/boards/jiuchuang-s3/config.h new file mode 100644 index 0000000..57d8ff2 --- /dev/null +++ b/main/boards/jiuchuang-s3/config.h @@ -0,0 +1,55 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_13 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_11 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_12 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_42 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_1 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_2 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + + +#define BUILTIN_LED_GPIO GPIO_NUM_10 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define PWR_BUTTON_GPIO GPIO_NUM_3 +#define PWR_EN_GPIO GPIO_NUM_5 +#define PWR_ADC_GPIO GPIO_NUM_4 +#define PWR_BUTTON_TIME 3000000U + +#define WIFI_BUTTON_GPIO GPIO_NUM_6 +#define CMD_BUTTON_GPIO GPIO_NUM_7 + +#define SD_CARD_CMD_PIN GPIO_NUM_48 // 命令线 +#define SD_CARD_DAT0_PIN GPIO_NUM_21 // 数据线0 +#define SD_CARD_CLK_PIN GPIO_NUM_47 // 时钟线 + +#define DISPLAY_SPI_SCK_PIN GPIO_NUM_41 +#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_40 +#define DISPLAY_DC_PIN GPIO_NUM_39 +#define DISPLAY_SPI_CS_PIN GPIO_NUM_9 + +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 320 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_46 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/jiuchuang-s3/config.json b/main/boards/jiuchuang-s3/config.json new file mode 100644 index 0000000..1531684 --- /dev/null +++ b/main/boards/jiuchuang-s3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "jiuchuang-s3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/jiuchuang-s3/esp_lcd_panel_gc9301.c b/main/boards/jiuchuang-s3/esp_lcd_panel_gc9301.c new file mode 100644 index 0000000..af7d746 --- /dev/null +++ b/main/boards/jiuchuang-s3/esp_lcd_panel_gc9301.c @@ -0,0 +1,384 @@ +/* + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + + #include + #include + #include "sdkconfig.h" + #include + #if CONFIG_LCD_ENABLE_DEBUG_LOG + // The local log level must be defined before including esp_log.h + // Set the maximum log level for this source file + #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + #endif + + #include "freertos/FreeRTOS.h" + #include "freertos/task.h" + #include "esp_lcd_panel_interface.h" + #include "esp_lcd_panel_io.h" + #include "esp_lcd_panel_vendor.h" + #include "esp_lcd_panel_ops.h" + #include "esp_lcd_panel_commands.h" + #include "driver/gpio.h" + #include "esp_log.h" + #include "esp_check.h" + #include "esp_compiler.h" + /* GC9309NA LCD controller driver for ESP-IDF + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: Apache-2.0 + */ + + #include "freertos/FreeRTOS.h" + #include "freertos/task.h" + #include "esp_lcd_panel_interface.h" + #include "esp_lcd_panel_io.h" + #include "esp_check.h" + #include "driver/gpio.h" + + + // GC9309NA Command Set + #define GC9309NA_CMD_SLPIN 0x10 + #define GC9309NA_CMD_SLPOUT 0x11 + #define GC9309NA_CMD_INVOFF 0x20 + #define GC9309NA_CMD_INVON 0x21 + #define GC9309NA_CMD_DISPOFF 0x28 + #define GC9309NA_CMD_DISPON 0x29 + #define GC9309NA_CMD_CASET 0x2A + #define GC9309NA_CMD_RASET 0x2B + #define GC9309NA_CMD_RAMWR 0x2C + #define GC9309NA_CMD_MADCTL 0x36 + #define GC9309NA_CMD_COLMOD 0x3A + #define GC9309NA_CMD_TEOFF 0x34 + #define GC9309NA_CMD_TEON 0x35 + #define GC9309NA_CMD_WRDISBV 0x51 + #define GC9309NA_CMD_WRCTRLD 0x53 + + // Manufacturer Commands + #define GC9309NA_CMD_SETGAMMA1 0xF0 + #define GC9309NA_CMD_SETGAMMA2 0xF1 + #define GC9309NA_CMD_PWRCTRL1 0x67 + #define GC9309NA_CMD_PWRCTRL2 0x68 + #define GC9309NA_CMD_PWRCTRL3 0x66 + #define GC9309NA_CMD_PWRCTRL4 0xCA + #define GC9309NA_CMD_PWRCTRL5 0xCB + #define GC9309NA_CMD_DINVCTRL 0xB5 + #define GC9309NA_CMD_REG_ENABLE1 0xFE + #define GC9309NA_CMD_REG_ENABLE2 0xEF + + // 自检模式颜色定义 + + + static const char *TAG = "lcd_panel.gc9309na"; + + typedef struct { + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + bool reset_level; + int x_gap; + int y_gap; + uint8_t madctl_val; + uint8_t colmod_val; + uint16_t te_scanline; + uint8_t fb_bits_per_pixel; + } gc9309na_panel_t; + + static esp_err_t panel_gc9309na_del(esp_lcd_panel_t *panel); + static esp_err_t panel_gc9309na_reset(esp_lcd_panel_t *panel); + static esp_err_t panel_gc9309na_init(esp_lcd_panel_t *panel); + static esp_err_t panel_gc9309na_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); + static esp_err_t panel_gc9309na_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); + static esp_err_t panel_gc9309na_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); + static esp_err_t panel_gc9309na_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); + static esp_err_t panel_gc9309na_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); + static esp_err_t panel_gc9309na_disp_on_off(esp_lcd_panel_t *panel, bool off); + static esp_err_t panel_gc9309na_sleep(esp_lcd_panel_t *panel, bool sleep); + + + esp_err_t esp_lcd_new_panel_gc9309na(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) + { + esp_err_t ret = ESP_OK; + gc9309na_panel_t *gc9309 = NULL; + + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid arg"); + + gc9309 = calloc(1, sizeof(gc9309na_panel_t)); + ESP_GOTO_ON_FALSE(gc9309, ESP_ERR_NO_MEM, err, TAG, "no mem"); + + + // Hardware reset GPIO config + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_config_t io_conf = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "GPIO config failed"); + } + + gc9309->colmod_val = 0x55; // RGB565 + // Initial register values + + gc9309->fb_bits_per_pixel = 16; + gc9309->io = io; + gc9309->reset_gpio_num = panel_dev_config->reset_gpio_num; + gc9309->reset_level = panel_dev_config->flags.reset_active_high; + gc9309->x_gap = 0; + gc9309->y_gap = 0; + + // Function pointers + gc9309->base.del = panel_gc9309na_del; + gc9309->base.reset = panel_gc9309na_reset; + gc9309->base.init = panel_gc9309na_init; + gc9309->base.draw_bitmap = panel_gc9309na_draw_bitmap; + gc9309->base.invert_color = panel_gc9309na_invert_color; + gc9309->base.set_gap = panel_gc9309na_set_gap; + gc9309->base.mirror = panel_gc9309na_mirror; + gc9309->base.swap_xy = panel_gc9309na_swap_xy; + gc9309->base.disp_on_off = panel_gc9309na_disp_on_off; + gc9309->base.disp_sleep = panel_gc9309na_sleep; + + *ret_panel = &(gc9309->base); + ESP_LOGI(TAG, "New GC9309NA panel @%p", gc9309); + return ESP_OK; + + err: + if (gc9309) { + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(gc9309); + } + return ret; + } + + static esp_err_t panel_gc9309na_del(esp_lcd_panel_t *panel) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + + if (gc9309->reset_gpio_num >= 0) { + gpio_reset_pin(gc9309->reset_gpio_num); + } + free(gc9309); + ESP_LOGI(TAG, "Del GC9309NA panel"); + return ESP_OK; + } + + static esp_err_t panel_gc9309na_reset(esp_lcd_panel_t *panel) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + + if (gc9309->reset_gpio_num >= 0) { + // Hardware reset + gpio_set_level(gc9309->reset_gpio_num, gc9309->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(gc9309->reset_gpio_num, !gc9309->reset_level); + vTaskDelay(pdMS_TO_TICKS(120)); + } else { + // Software reset + // uint8_t unlock_cmd[] = {GC9309NA_CMD_REG_ENABLE1, GC9309NA_CMD_REG_ENABLE2}; + // ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(gc9309->io, 0xFE, unlock_cmd, 2), + // TAG, "Unlock failed"); + // ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(gc9309->io, LCD_CMD_SWRESET, NULL, 0), + // TAG, "SW Reset failed"); + vTaskDelay(pdMS_TO_TICKS(120)); + } + return ESP_OK; + } + static esp_err_t panel_gc9309na_init(esp_lcd_panel_t *panel) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9309->io; + + // Unlock commands + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xFE, NULL, 0), TAG, "Unlock cmd1 failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xEF, NULL, 0), TAG, "Unlock cmd2 failed"); + + // Sleep out command + //ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x11, NULL, 0), TAG, "Sleep out failed"); + //vTaskDelay(pdMS_TO_TICKS(80)); + + // Timing control commands + //ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xE8, (uint8_t[]){0xA0}, 1), TAG, "Timing control failed"); + //ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xE8, (uint8_t[]){0xF0}, 1), TAG, "Timing control failed"); + + // Display on command + //ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x29, NULL, 0), TAG, "Display on failed"); + // vTaskDelay(pdMS_TO_TICKS(10)); + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x80, (uint8_t[]){0xC0}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x81, (uint8_t[]){0x01}, 1), TAG, "DINV failed"); + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x82, (uint8_t[]){0x07}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x83, (uint8_t[]){0x38}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x88, (uint8_t[]){0x64}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x89, (uint8_t[]){0x86}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x8B, (uint8_t[]){0x3C}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x8D, (uint8_t[]){0x51}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x8E, (uint8_t[]){0x70}, 1), TAG, "DINV failed"); + + //高低位交换 + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xB4, (uint8_t[]){0x80}, 1), TAG, "DINV failed"); + + gc9309->colmod_val = 0x05; // RGB565 + gc9309->madctl_val = 0x48; // BGR顺序,设置bit3=1(即0x08) + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, GC9309NA_CMD_COLMOD, &gc9309->colmod_val, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, GC9309NA_CMD_MADCTL, &gc9309->madctl_val, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0XBF, (uint8_t[]){0X1F}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x7d, (uint8_t[]){0x45,0x06}, 2), TAG, "DINV failed"); + // Continue from where you left off + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xEE, (uint8_t[]){0x00,0x06}, 2), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0XF4, (uint8_t[]){0x53}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xF6, (uint8_t[]){0x17,0x08}, 2), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x70, (uint8_t[]){0x4F,0x4F}, 2), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x71, (uint8_t[]){0x12,0x20}, 2), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x72, (uint8_t[]){0x12,0x20}, 2), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xB5, (uint8_t[]){0x50}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xBA, (uint8_t[]){0x00}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xEC, (uint8_t[]){0x71}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x7b, (uint8_t[]){0x00,0x0d}, 2), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x7c, (uint8_t[]){0x0d,0x03}, 2), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0XF5, (uint8_t[]){0x02,0x10,0x12}, 3), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xF0, (uint8_t[]){0x0C,0x11,0x0b,0x0a,0x05,0x32,0x44,0x8e,0x9a,0x29,0x2E,0x5f}, 12), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xF1, (uint8_t[]){0x0B,0x11,0x0b,0x07,0x07,0x32,0x45,0xBd,0x8D,0x21,0x28,0xAf}, 12), TAG, "DINV failed"); + + // 240x296 resolution settings + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x2a, (uint8_t[]){0x00,0x00,0x00,0xef}, 4), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x2b, (uint8_t[]){0x00,0x00,0x01,0x27}, 4), TAG, "DINV failed"); + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x66, (uint8_t[]){0x2C}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x67, (uint8_t[]){0x18}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x68, (uint8_t[]){0x3E}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xCA, (uint8_t[]){0x0E}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xe8, (uint8_t[]){0xf0}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xCB, (uint8_t[]){0x06}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xB6, (uint8_t[]){0x5C,0x40,0x40}, 3), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xCC, (uint8_t[]){0x33}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xCD, (uint8_t[]){0x33}, 1), TAG, "DINV failed"); + + // Sleep out command + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x11, NULL, 0), TAG, "Sleep out failed"); + vTaskDelay(pdMS_TO_TICKS(80)); + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xe8, (uint8_t[]){0xA0}, 1), TAG, "DINV failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xe8, (uint8_t[]){0xf0}, 1), TAG, "DINV failed"); + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xfe, NULL, 0), TAG, "unlock cmd1 failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0xee, NULL, 0), TAG, "unlock cmd2 failed"); + + // Display on command + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x29, NULL, 0), TAG, "Display on failed"); + + // Memory write command + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 0x2c, NULL, 0), TAG, "Memory write failed"); + vTaskDelay(pdMS_TO_TICKS(10)); + return ESP_OK; + } + + + static esp_err_t panel_gc9309na_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + + + esp_lcd_panel_io_handle_t io = gc9309->io; + + x_start += gc9309->x_gap; + x_end += gc9309->x_gap; + y_start += gc9309->y_gap; + y_end += gc9309->y_gap; + + // define an area of frame memory where MCU can access + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]) { + (x_start >> 8) & 0xFF, + x_start & 0xFF, + ((x_end - 1) >> 8) & 0xFF, + (x_end - 1) & 0xFF, + }, 4), TAG, "io tx param failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]) { + (y_start >> 8) & 0xFF, + y_start & 0xFF, + ((y_end - 1) >> 8) & 0xFF, + (y_end - 1) & 0xFF, + }, 4), TAG, "io tx param failed"); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * gc9309->fb_bits_per_pixel / 8; + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len), TAG, "io tx color failed"); + + return ESP_OK; + } + + static esp_err_t panel_gc9309na_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9309->io; + int command = 0; + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, + "io tx param failed"); + return ESP_OK; + } + + static esp_err_t panel_gc9309na_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) + { + // gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + // esp_lcd_panel_io_handle_t io = gc9309->io; + // if (mirror_x) { + // gc9309->madctl_val |= LCD_CMD_MX_BIT; + // } else { + // gc9309->madctl_val &= ~LCD_CMD_MX_BIT; + // } + // if (mirror_y) { + // gc9309->madctl_val |= LCD_CMD_MY_BIT; + // } else { + // gc9309->madctl_val &= ~LCD_CMD_MY_BIT; + // } + // ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + // gc9309->madctl_val + // }, 1), TAG, "io tx param failed"); + return ESP_OK; + } + + static esp_err_t panel_gc9309na_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) + { + // gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + // esp_lcd_panel_io_handle_t io = gc9309->io; + // if (swap_axes) { + // gc9309->madctl_val |= LCD_CMD_MV_BIT; + // } else { + // gc9309->madctl_val &= ~LCD_CMD_MV_BIT; + // } + // ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + // gc9309->madctl_val + // }, 1), TAG, "io tx param failed"); + return ESP_OK; + } + + static esp_err_t panel_gc9309na_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + gc9309->x_gap = x_gap; + gc9309->y_gap = y_gap; + return ESP_OK; + } + + static esp_err_t panel_gc9309na_disp_on_off(esp_lcd_panel_t *panel, bool on_off) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + uint8_t cmd = on_off ? GC9309NA_CMD_DISPON : GC9309NA_CMD_DISPOFF; + return esp_lcd_panel_io_tx_param(gc9309->io, cmd, NULL, 0); + } + + static esp_err_t panel_gc9309na_sleep(esp_lcd_panel_t *panel, bool sleep) + { + gc9309na_panel_t *gc9309 = __containerof(panel, gc9309na_panel_t, base); + uint8_t cmd = sleep ? GC9309NA_CMD_SLPIN : GC9309NA_CMD_SLPOUT; + esp_err_t ret = esp_lcd_panel_io_tx_param(gc9309->io, cmd, NULL, 0); + vTaskDelay(pdMS_TO_TICKS(120)); + return ret; + } \ No newline at end of file diff --git a/main/boards/jiuchuang-s3/esp_lcd_panel_gc9301.h b/main/boards/jiuchuang-s3/esp_lcd_panel_gc9301.h new file mode 100644 index 0000000..b0b1b9d --- /dev/null +++ b/main/boards/jiuchuang-s3/esp_lcd_panel_gc9301.h @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "esp_lcd_panel_dev.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create LCD panel for model ST7789 + * + * @param[in] io LCD panel IO handle + * @param[in] panel_dev_config general panel device configuration + * @param[out] ret_panel Returned LCD panel handle + * @return + * - ESP_ERR_INVALID_ARG if parameter is invalid + * - ESP_ERR_NO_MEM if out of memory + * - ESP_OK on success + */ +esp_err_t esp_lcd_new_panel_gc9309na(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel); + +#ifdef __cplusplus +} +#endif diff --git a/main/boards/jiuchuang-s3/gbk_map.h b/main/boards/jiuchuang-s3/gbk_map.h new file mode 100644 index 0000000..88c7745 --- /dev/null +++ b/main/boards/jiuchuang-s3/gbk_map.h @@ -0,0 +1,37 @@ +#ifndef GBK_MAP_H +#define GBK_MAP_H + +#include + +// GBK到Unicode的完整映射表 +static const uint16_t gbk_to_unicode_map[] = { + 0x4E02, 0x4E04, 0x4E05, 0x4E06, 0x4E0F, 0x4E12, 0x4E17, 0x4E1F, + 0x011B, 0x00E8, 0x012B, 0x00ED, 0x01D0, 0x00EC, 0x014D, 0x00F3, + 0x01D2, 0x00F2, 0x016B, 0x00FA, 0x01D4, 0x00F9, 0x01D6, 0x01D8, + 0x01DA, 0x01DC, 0x00FC, 0x00EA, 0x0251, 0xE7C7, 0x0144, 0x0148, + 0xE7C8, 0x0261, 0xE7C9, 0xE7CA, 0xE7CB, 0xE7CC, 0x3105, 0x3106, + 0x3107, 0x3108, 0x3109, 0x310A, 0x310B, 0x310C, 0x310D, 0x310E, + 0x310F, 0x3110, 0x3111, 0x3112, 0x3113, 0x3114, 0x3115, 0x3116, + 0x3117, 0x3118, 0x3119, 0x311A, 0x311B, 0x311C, 0x311D, 0x311E, + 0x311F, 0x3120, 0x3121, 0x3122, 0x3123, 0x3124, 0x3125, 0x3126, + 0x3127, 0x3128, 0x3129, 0xE7CD, 0xE7CE, 0xE7CF, 0xE7D0, 0xE7D1, + // ... 这里继续添加剩余的映射表数据 + 0x554A, 0x963F, 0x57C3, 0x6328, 0x54CE, 0x5509, 0x54C0, 0x7691, + 0x764C, 0x853C, 0x77EE, 0x827E, 0x788D, 0x7231, 0x9698, 0x978D, + 0x6C28, 0x5B89, 0x4FFA, 0x6309, 0x6697, 0x5CB8, 0x80FA, 0x6848, + 0x80AE, 0x6602, 0x76CE, 0x51F9, 0x6556, 0x71AC, 0x7FF1, 0x8884, + 0x50B2, 0x5965, 0x61CA, 0x6FB3, 0x82AD, 0x634C, 0x6252, 0x53ED, + 0x5427, 0x7B06, 0x516B, 0x75A4, 0x5DF4, 0x62D4, 0x8DCB, 0x9776, + 0x628A, 0x8019, 0x575D, 0x9738, 0x7F62, 0x7238, 0x767D, 0x67CF, + 0x767E, 0x6446, 0x4F70, 0x8D25, 0x62DC, 0x7A17, 0x6591, 0x73ED, + 0x642C, 0x6273, 0x822C, 0x9881, 0x677F, 0x7248, 0x626E, 0x62CC, + 0x4F34, 0x74E3, 0x534A, 0x529E, 0x7ECA, 0x90A6, 0x5E2E, 0x6886, + 0x699C, 0x8180, 0x7ED1, 0x68D2, 0x78C5, 0x868C, 0x9551, 0x508D, + 0x8C24, 0x82DE, 0x80DE, 0x5305, 0x8912, 0x5265, 0x76C4, 0x76C7, + 0x76C9, 0x76CB, 0x76CC, 0x76D3, 0x76D5, 0x76D9, 0x76DA, 0x76DC, + 0x76DD, 0x76DE, 0x76E0, 0x76E1, 0x76E2, 0x76E3, 0x76E4, 0x76E6, + 0x76E7, 0x76E8, 0x76E9, 0x76EA, 0x76EB, 0x76EC, 0x76ED, 0x76F0, + 0x76F3, 0x76F5, 0x76F6, 0x76F7, 0x76FA, 0x76FB, 0x76FD, 0x76FF +}; + +#endif // GBK_MAP_H \ No newline at end of file diff --git a/main/boards/jiuchuang-s3/gbk_util.h b/main/boards/jiuchuang-s3/gbk_util.h new file mode 100644 index 0000000..6e458bb --- /dev/null +++ b/main/boards/jiuchuang-s3/gbk_util.h @@ -0,0 +1,139 @@ +#ifndef GBK_ENCODING_H +#define GBK_ENCODING_H + +#include +#include +#include "gbk_map.h" // 引入映射表 +#include + +#define GBK_UTIL_TAG "GBK_ENCODING" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 将GBK编码转换为UTF-8编码 + * + * @param gbk_str 输入的GBK编码字符串 + * @param utf8_buf 输出的UTF-8字符串缓冲区 + * @param buf_size 缓冲区大小 + * @return size_t 转换后的字符串长度,如果失败则返回0 + */ +size_t gbk_to_utf8(const char* gbk_str, char* utf8_buf, size_t buf_size); + +/** + * @brief 获取转换GBK到UTF-8所需的缓冲区大小 + * + * @param gbk_str 输入的GBK编码字符串 + * @return size_t 所需的UTF-8缓冲区大小 + */ +size_t gbk_to_utf8_buffer_size(const char* gbk_str); + +/** + * @brief 将GBK编码转换为UTF-8编码,并分配新内存 + * + * @param gbk_str 输入的GBK编码字符串 + * @return char* 新分配的UTF-8字符串,使用后需要free + */ +char* gbk_to_utf8_alloc(const char* gbk_str); + +/** + * @brief 初始化GBK编码转换表 + * 这个函数会加载编码转换表到内存中 + */ +void gbk_encoding_init(void); + +// GBK到Unicode的映射表 +static const uint16_t gbk_to_unicode_map[] = { + 0x4E02, 0x4E04, 0x4E05, 0x4E06, 0x4E0F, 0x4E12, 0x4E17, 0x4E1F, + // ... 这里是完整的映射表 +}; + +// GBK到Unicode的转换函数 +static inline uint16_t gbk_to_unicode(uint8_t ch, uint8_t cl) { + if (ch <= 0x7F) { + return ch; // ASCII字符 + } + + // GBK区域判断 + if (ch >= 0x81 && ch <= 0xFE) { + if (cl >= 0x40 && cl <= 0x7E || cl >= 0x80 && cl <= 0xFE) { + uint32_t gbk = (ch << 8) | cl; + + // GBK-1区域 (0xB0A1-0xF7FE) + if (gbk >= 0xB0A1 && gbk <= 0xF7FE) { + uint32_t offset = ((ch - 0xB0) * 94 + (cl - 0xA1)); + return 0x4E00 + offset; // 基本汉字区 + } + + // GBK-2区域 (0x8140-0xA0FE) + if (gbk >= 0x8140 && gbk <= 0xA0FE) { + uint32_t offset = ((ch - 0x81) * 190 + (cl - (cl >= 0x80 ? 0x41 : 0x40))); + return 0x3000 + offset; // 符号区 + } + + // GBK-3区域 (0xAA40-0xFEA0) + if (gbk >= 0xAA40 && gbk <= 0xFEA0) { + uint32_t offset = ((ch - 0xAA) * 96 + (cl - 0x40)); + return 0x4E00 + 6768 + offset; // 扩展汉字区 + } + } + } + + ESP_LOGW(GBK_UTIL_TAG, "未找到映射的GBK编码: 0x%04X [高字节:0x%02X, 低字节:0x%02X]", + (ch << 8) | cl, ch, cl); + return 0x3F; // 返回'?'的Unicode编码 +} + +// Unicode到UTF-8的转换函数 +static inline int unicode_to_utf8(uint16_t uni, uint8_t *utf8) { + if (uni <= 0x7F) { + utf8[0] = (uint8_t)uni; + return 1; + } + else if (uni <= 0x7FF) { + utf8[0] = 0xC0 | ((uni >> 6) & 0x1F); + utf8[1] = 0x80 | (uni & 0x3F); + return 2; + } + else { + utf8[0] = 0xE0 | ((uni >> 12) & 0x0F); + utf8[1] = 0x80 | ((uni >> 6) & 0x3F); + utf8[2] = 0x80 | (uni & 0x3F); + return 3; + } +} + +// GBK到UTF-8的转换函数 +static inline int gbk_to_utf8(const char* gbk, char* utf8, int len) { + int utf8_len = 0; + for (int i = 0; i < len;) { + uint8_t ch = (uint8_t)gbk[i]; + if (ch <= 0x7F) { + // ASCII字符 + utf8[utf8_len++] = ch; + i++; + } else { + // GBK字符 + if (i + 1 >= len) break; + uint8_t cl = (uint8_t)gbk[i + 1]; + uint16_t unicode = gbk_to_unicode(ch, cl); + utf8_len += unicode_to_utf8(unicode, (uint8_t*)&utf8[utf8_len]); + i += 2; + } + } + utf8[utf8_len] = '\0'; + return utf8_len; +} + +// 处理文件名的函数 +static inline void process_filename(const char* filename, char* utf8_filename, int max_len) { + gbk_to_utf8(filename, utf8_filename, strlen(filename)); +} + +#ifdef __cplusplus +} +#endif + +#endif /* GBK_ENCODING_H */ \ No newline at end of file diff --git a/main/boards/jiuchuang-s3/jiuchuang_dev_board.cc b/main/boards/jiuchuang-s3/jiuchuang_dev_board.cc new file mode 100644 index 0000000..3cbcacb --- /dev/null +++ b/main/boards/jiuchuang-s3/jiuchuang_dev_board.cc @@ -0,0 +1,1852 @@ +#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); diff --git a/main/boards/jiuchuang-s3/power_manager.h b/main/boards/jiuchuang-s3/power_manager.h new file mode 100644 index 0000000..33579d0 --- /dev/null +++ b/main/boards/jiuchuang-s3/power_manager.h @@ -0,0 +1,221 @@ +#pragma once +#include +#include + +#include +#include +#include + + +class PowerManager { +private: + esp_timer_handle_t timer_handle_; + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + + gpio_num_t charging_pin_ = GPIO_NUM_NC; + std::vector adc_values_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 60; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + + void CheckBatteryStatus() { + // Get charging status + bool new_charging_status = gpio_get_level(charging_pin_) == 1; + if (new_charging_status != is_charging_) { + is_charging_ = new_charging_status; + if (on_charging_status_changed_) { + on_charging_status_changed_(is_charging_); + } + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据不足,则读取电池电量数据 + if (adc_values_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_value; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_3, &adc_value)); + + // 将 ADC 值添加到队列中 + adc_values_.push_back(adc_value); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + uint32_t average_adc = 0; + for (auto value : adc_values_) { + average_adc += value; + } + average_adc /= adc_values_.size(); + + + /* + 电量 (%) 电压 (V) 分压后电压 (V) + 0% 3.1 1.033 + 20% 3.34 1.113 + 40% 3.58 1.193 + 60% 3.82 1.273 + 80% 4.06 1.353 + 100% 4.2 1.400 + + 电量 (%) 分压后电压 (V) ADC值(理论) 实际范围(±5%误差) + 0% 1.033 ​1284​​ 1220~1348 + 20% 1.113 ​1384​​ 1315~1453 + 40% 1.193 ​1483​​ 1409~1557 + 60% 1.273 ​1583​​ 1504~1662 + 80% 1.353 ​1682​​ 1598~1766 + 100% 1.400 ​1745​​ 1658~1832 + ------------------------------------------------------- + 电量 (%) 电压 (V) 分压后电压 (V) + 0% 3.1 1.033 + 20% 3.28 1.093 + 40% 3.46 1.153 + 60% 3.64 1.213 + 80% 3.82 1.273 + 100% 4.1 1.367 + + 0% 1.033 ​​1284​​ 1220~1348 + 20% 1.093 ​​1358​​ 1290~1426 + 40% 1.153 ​​1431​​ 1360~1502 + 60% 1.213 ​​1505​​ 1430~1580 + 80% 1.273 ​​1583​​ 1504~1662 + 100% 1.367 ​​1700​​ 1615~1785 + */ + + // 定义电池电量区间 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = { + { 1284 , 0}, + { 1358 , 20}, + { 1431 , 40}, + { 1505 , 60}, + { 1583 , 80}, + { 1700 , 100} + }; + + // 低于最低值时 + if (average_adc < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_adc >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) { + float ratio = static_cast(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (adc_values_.size() >= kBatteryAdcDataCount) { + bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; + if (new_low_battery_status != is_low_battery_) { + is_low_battery_ = new_low_battery_status; + if (on_low_battery_status_changed_) { + on_low_battery_status_changed_(is_low_battery_); + } + } + } + + ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); + } + +public: + PowerManager(gpio_num_t pin) : charging_pin_(pin) { + // 初始化充电引脚 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << charging_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + PowerManager* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_3, &chan_config)); + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + bool IsCharging() { + // 如果电量已经满了,则不再显示充电中 + if (battery_level_ == 100) { + return false; + } + return is_charging_; + } + + bool IsDischarging() { + // 没有区分充电和放电,所以直接返回相反状态 + return !is_charging_; + } + + uint8_t GetBatteryLevel() { + return battery_level_; + } + + void OnLowBatteryStatusChanged(std::function callback) { + on_low_battery_status_changed_ = callback; + } + + void OnChargingStatusChanged(std::function callback) { + on_charging_status_changed_ = callback; + } +}; + diff --git a/main/boards/kevin-box-1/config.h b/main/boards/kevin-box-1/config.h new file mode 100644 index 0000000..8bd55ad --- /dev/null +++ b/main/boards/kevin-box-1/config.h @@ -0,0 +1,39 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_42 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_47 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_48 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_45 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_21 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_17 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_39 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_38 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_8 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_6 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_7 + +#define DISPLAY_SDA_PIN GPIO_NUM_4 +#define DISPLAY_SCL_PIN GPIO_NUM_5 +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false + +#define ML307_RX_PIN GPIO_NUM_20 +#define ML307_TX_PIN GPIO_NUM_19 + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/kevin-box-1/config.json b/main/boards/kevin-box-1/config.json new file mode 100644 index 0000000..82d8a1f --- /dev/null +++ b/main/boards/kevin-box-1/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "kevin-box-1", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/kevin-box-1/kevin_box_board.cc b/main/boards/kevin-box-1/kevin_box_board.cc new file mode 100644 index 0000000..e06c24a --- /dev/null +++ b/main/boards/kevin-box-1/kevin_box_board.cc @@ -0,0 +1,217 @@ +#include "ml307_board.h" +#include "audio_codecs/box_audio_codec.h" +#include "display/oled_display.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "KevinBoxBoard" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + +class KevinBoxBoard : public Ml307Board { +private: + i2c_master_bus_handle_t display_i2c_bus_; + i2c_master_bus_handle_t codec_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + + void MountStorage() { + // Mount the storage partition + esp_vfs_spiffs_conf_t conf = { + .base_path = "/storage", + .partition_label = "storage", + .max_files = 5, + .format_if_mount_failed = true, + }; + esp_vfs_spiffs_register(&conf); + } + + void Enable4GModule() { + // Make GPIO15 HIGH to enable the 4G module + gpio_config_t ml307_enable_config = { + .pin_bit_mask = (1ULL << 15) | (1ULL << 18), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&ml307_enable_config); + gpio_set_level(GPIO_NUM_15, 1); + gpio_set_level(GPIO_NUM_18, 1); + } + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeCodecI2c() { + // 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 InitializeButtons() { + boot_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + volume_up_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + } + +public: + KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeCodecI2c(); + MountStorage(); + Enable4GModule(); + + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec(codec_i2c_bus_, 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, AUDIO_CODEC_ES7210_ADDR, AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(KevinBoxBoard); \ No newline at end of file diff --git a/main/boards/kevin-box-2/config.h b/main/boards/kevin-box-2/config.h new file mode 100644 index 0000000..a272900 --- /dev/null +++ b/main/boards/kevin-box-2/config.h @@ -0,0 +1,41 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_40 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_47 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_38 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_48 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_9 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_42 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_41 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_3 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_1 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_2 + +#define DISPLAY_SDA_PIN GPIO_NUM_7 +#define DISPLAY_SCL_PIN GPIO_NUM_8 +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false + +#define ML307_RX_PIN GPIO_NUM_5 +#define ML307_TX_PIN GPIO_NUM_6 + +#define AXP2101_I2C_ADDR 0x34 + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/kevin-box-2/config.json b/main/boards/kevin-box-2/config.json new file mode 100644 index 0000000..d9a581d --- /dev/null +++ b/main/boards/kevin-box-2/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "kevin-box-2", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/kevin-box-2/kevin_box_board.cc b/main/boards/kevin-box-2/kevin_box_board.cc new file mode 100644 index 0000000..cc72d7d --- /dev/null +++ b/main/boards/kevin-box-2/kevin_box_board.cc @@ -0,0 +1,267 @@ +#include "ml307_board.h" +#include "audio_codecs/box_audio_codec.h" +#include "display/oled_display.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "config.h" +#include "power_save_timer.h" +#include "axp2101.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include + +#define TAG "KevinBoxBoard" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + + +class Pmic : public Axp2101 { +public: + Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) { + // ** EFUSE defaults ** + WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable + WriteReg(0x27, 0x10); // hold 4s to power off + + WriteReg(0x93, 0x1C); // 配置 aldo2 输出为 3.3V + + uint8_t value = ReadReg(0x90); // XPOWERS_AXP2101_LDO_ONOFF_CTRL0 + value = value | 0x02; // set bit 1 (ALDO2) + WriteReg(0x90, value); // and power channels now enabled + + WriteReg(0x64, 0x03); // CV charger voltage setting to 4.2V + + WriteReg(0x61, 0x05); // set Main battery precharge current to 125mA + WriteReg(0x62, 0x0A); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA ) + WriteReg(0x63, 0x15); // set Main battery term charge current to 125mA + + WriteReg(0x14, 0x00); // set minimum system voltage to 4.1V (default 4.7V), for poor USB cables + WriteReg(0x15, 0x00); // set input voltage limit to 3.88v, for poor USB cables + WriteReg(0x16, 0x05); // set input current limit to 2000mA + + WriteReg(0x24, 0x01); // set Vsys for PWROFF threshold to 3.2V (default - 2.6V and kill battery) + WriteReg(0x50, 0x14); // set TS pin to EXTERNAL input (not temperature) + } +}; + + +class KevinBoxBoard : public Ml307Board { +private: + i2c_master_bus_handle_t display_i2c_bus_; + i2c_master_bus_handle_t codec_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Pmic* pmic_ = nullptr; + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, -1, 600); + power_save_timer_->OnShutdownRequest([this]() { + pmic_->PowerOff(); + }); + power_save_timer_->SetEnabled(true); + } + + void Enable4GModule() { + // Make GPIO HIGH to enable the 4G module + gpio_config_t ml307_enable_config = { + .pin_bit_mask = (1ULL << 4), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&ml307_enable_config); + gpio_set_level(GPIO_NUM_4, 1); + } + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeCodecI2c() { + // 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 InitializeButtons() { + boot_button_.OnPressDown([this]() { + power_save_timer_->WakeUp(); + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + volume_up_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Battery")); + } + +public: + KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeCodecI2c(); + pmic_ = new Pmic(codec_i2c_bus_, AXP2101_I2C_ADDR); + + Enable4GModule(); + + InitializeButtons(); + InitializePowerSaveTimer(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec(codec_i2c_bus_, 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, AUDIO_CODEC_ES7210_ADDR, AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override { + static bool last_discharging = false; + charging = pmic_->IsCharging(); + discharging = pmic_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + + level = pmic_->GetBatteryLevel(); + return true; + } +}; + +DECLARE_BOARD(KevinBoxBoard); \ No newline at end of file diff --git a/main/boards/kevin-c3/config.h b/main/boards/kevin-c3/config.h new file mode 100644 index 0000000..4241320 --- /dev/null +++ b/main/boards/kevin-c3/config.h @@ -0,0 +1,24 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_13 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_5 +#define BOOT_BUTTON_GPIO GPIO_NUM_6 + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/kevin-c3/config.json b/main/boards/kevin-c3/config.json new file mode 100644 index 0000000..76b4f51 --- /dev/null +++ b/main/boards/kevin-c3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "kevin-c3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/kevin-c3/kevin_c3_board.cc b/main/boards/kevin-c3/kevin_c3_board.cc new file mode 100644 index 0000000..ab51c60 --- /dev/null +++ b/main/boards/kevin-c3/kevin_c3_board.cc @@ -0,0 +1,87 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/circular_strip.h" +#include "led_strip_control.h" + +#include +#include +#include +#include + +#define TAG "KevinBoxBoard" + +class KevinBoxBoard : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button boot_button_; + CircularStrip* led_strip_; + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + + led_strip_ = new CircularStrip(BUILTIN_LED_GPIO, 8); + auto led_strip_control = new LedStripControl(led_strip_); + thing_manager.AddThing(led_strip_control); + } + +public: + KevinBoxBoard() : boot_button_(BOOT_BUTTON_GPIO) { + // 把 ESP32C3 的 VDD SPI 引脚作为普通 GPIO 口使用 + esp_efuse_write_field_bit(ESP_EFUSE_VDD_SPI_AS_GPIO); + + InitializeCodecI2c(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + return led_strip_; + } + + 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; + } +}; + +DECLARE_BOARD(KevinBoxBoard); diff --git a/main/boards/kevin-c3/led_strip_control.cc b/main/boards/kevin-c3/led_strip_control.cc new file mode 100644 index 0000000..48634c0 --- /dev/null +++ b/main/boards/kevin-c3/led_strip_control.cc @@ -0,0 +1,123 @@ +#include "led_strip_control.h" +#include "settings.h" +#include + +#define TAG "LedStripControl" + + +int LedStripControl::LevelToBrightness(int level) const { + if (level < 0) level = 0; + if (level > 8) level = 8; + return (1 << level) - 1; // 2^n - 1 +} + +StripColor LedStripControl::RGBToColor(int red, int green, int blue) { + if (red < 0) red = 0; + if (red > 255) red = 255; + if (green < 0) green = 0; + if (green > 255) green = 255; + if (blue < 0) blue = 0; + if (blue > 255) blue = 255; + return {static_cast(red), static_cast(green), static_cast(blue)}; +} + +LedStripControl::LedStripControl(CircularStrip* led_strip) + : Thing("LedStripControl", "LED 灯带控制,一共有8个灯珠"), led_strip_(led_strip) { + // 从设置中读取亮度等级 + Settings settings("led_strip"); + brightness_level_ = settings.GetInt("brightness", 4); // 默认等级4 + led_strip_->SetBrightness(LevelToBrightness(brightness_level_), 4); + + // 定义设备的属性 + properties_.AddNumberProperty("brightness", "对话时的亮度等级(0-8)", [this]() -> int { + return brightness_level_; + }); + + // 定义设备可以被远程执行的指令 + methods_.AddMethod("SetBrightness", "设置对话时的亮度等级", ParameterList({ + Parameter("level", "亮度等级(0-8)", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + int level = static_cast(parameters["level"].number()); + ESP_LOGI(TAG, "Set LedStrip brightness level to %d", level); + + if (level < 0) level = 0; + if (level > 8) level = 8; + + brightness_level_ = level; + led_strip_->SetBrightness(LevelToBrightness(brightness_level_), 4); + + // 保存设置 + Settings settings("led_strip", true); + settings.SetInt("brightness", brightness_level_); + }); + + methods_.AddMethod("SetSingleColor", "设置单个灯颜色", ParameterList({ + Parameter("index", "灯珠索引(0-7)", kValueTypeNumber, true), + Parameter("red", "红色(0-255)", kValueTypeNumber, true), + Parameter("green", "绿色(0-255)", kValueTypeNumber, true), + Parameter("blue", "蓝色(0-255)", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + int index = parameters["index"].number(); + StripColor color = RGBToColor( + parameters["red"].number(), + parameters["green"].number(), + parameters["blue"].number() + ); + ESP_LOGI(TAG, "Set led strip single color %d to %d, %d, %d", + index, color.red, color.green, color.blue); + led_strip_->SetSingleColor(index, color); + }); + + methods_.AddMethod("SetAllColor", "设置所有灯颜色", ParameterList({ + Parameter("red", "红色(0-255)", kValueTypeNumber, true), + Parameter("green", "绿色(0-255)", kValueTypeNumber, true), + Parameter("blue", "蓝色(0-255)", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + StripColor color = RGBToColor( + parameters["red"].number(), + parameters["green"].number(), + parameters["blue"].number() + ); + ESP_LOGI(TAG, "Set led strip color to %d, %d, %d", + color.red, color.green, color.blue + ); + led_strip_->SetAllColor(color); + }); + + methods_.AddMethod("Blink", "闪烁动画", ParameterList({ + Parameter("red", "红色(0-255)", kValueTypeNumber, true), + Parameter("green", "绿色(0-255)", kValueTypeNumber, true), + Parameter("blue", "蓝色(0-255)", kValueTypeNumber, true), + Parameter("interval", "间隔(ms)", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + int interval = parameters["interval"].number(); + StripColor color = RGBToColor( + parameters["red"].number(), + parameters["green"].number(), + parameters["blue"].number() + ); + ESP_LOGI(TAG, "Blink led strip with color %d, %d, %d, interval %dms", + color.red, color.green, color.blue, interval); + led_strip_->Blink(color, interval); + }); + + methods_.AddMethod("Scroll", "跑马灯动画", ParameterList({ + Parameter("red", "红色(0-255)", kValueTypeNumber, true), + Parameter("green", "绿色(0-255)", kValueTypeNumber, true), + Parameter("blue", "蓝色(0-255)", kValueTypeNumber, true), + Parameter("length", "滚动条长度(1-7)", kValueTypeNumber, true), + Parameter("interval", "间隔(ms)", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + int interval = parameters["interval"].number(); + int length = parameters["length"].number(); + StripColor low = RGBToColor(4, 4, 4); + StripColor high = RGBToColor( + parameters["red"].number(), + parameters["green"].number(), + parameters["blue"].number() + ); + ESP_LOGI(TAG, "Scroll led strip with color %d, %d, %d, length %d, interval %dms", + high.red, high.green, high.blue, length, interval); + led_strip_->Scroll(low, high, length, interval); + }); +} diff --git a/main/boards/kevin-c3/led_strip_control.h b/main/boards/kevin-c3/led_strip_control.h new file mode 100644 index 0000000..d8cf832 --- /dev/null +++ b/main/boards/kevin-c3/led_strip_control.h @@ -0,0 +1,21 @@ +#ifndef LED_STRIP_CONTROL_H +#define LED_STRIP_CONTROL_H + +#include "iot/thing.h" +#include "led/circular_strip.h" + +using namespace iot; + +class LedStripControl : public Thing { +private: + CircularStrip* led_strip_; + int brightness_level_; // 亮度等级 (0-8) + + int LevelToBrightness(int level) const; // 将等级转换为实际亮度值 + StripColor RGBToColor(int red, int green, int blue); + +public: + explicit LedStripControl(CircularStrip* led_strip); +}; + +#endif // LED_STRIP_CONTROL_H diff --git a/main/boards/kevin-sp-v3-dev/config.h b/main/boards/kevin-sp-v3-dev/config.h new file mode 100644 index 0000000..8f53749 --- /dev/null +++ b/main/boards/kevin-sp-v3-dev/config.h @@ -0,0 +1,45 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +#define AUDIO_DEFAULT_OUTPUT_VOLUME 90 + + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_42 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_41 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_2 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_3 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_46 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_1 + + +#define BUILTIN_LED_GPIO GPIO_NUM_38 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define RESET_NVS_BUTTON_GPIO GPIO_NUM_NC +#define RESET_FACTORY_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 280 +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define BACKLIGHT_INVERT false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 20 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_48 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define ML307_RX_PIN GPIO_NUM_12 +#define ML307_TX_PIN GPIO_NUM_13 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/kevin-sp-v3-dev/kevin-sp-v3_board.cc b/main/boards/kevin-sp-v3-dev/kevin-sp-v3_board.cc new file mode 100644 index 0000000..184f9c1 --- /dev/null +++ b/main/boards/kevin-sp-v3-dev/kevin-sp-v3_board.cc @@ -0,0 +1,137 @@ +#include "wifi_board.h" +#include "ml307_board.h" + +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" + +#include +#include +#include +#include + +#define TAG "kevin-sp-v3" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + + +// class KEVIN_SP_V3Board : public Ml307Board { +class KEVIN_SP_V3Board : public WifiBoard { +private: + i2c_master_bus_handle_t display_i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_47; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_21; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_14; + io_config.dc_gpio_num = GPIO_NUM_45; + 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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(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_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, true)); + + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 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("Lamp")); + } + +public: + KEVIN_SP_V3Board() : + // Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO) { + ESP_LOGI(TAG, "Initializing KEVIN_SP_V3 Board"); + + InitializeSpi(); + InitializeButtons(); + InitializeSt7789Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec *GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + 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; + } +}; + +DECLARE_BOARD(KEVIN_SP_V3Board); diff --git a/main/boards/kevin-sp-v4-dev/config.h b/main/boards/kevin-sp-v4-dev/config.h new file mode 100644 index 0000000..4bdc072 --- /dev/null +++ b/main/boards/kevin-sp-v4-dev/config.h @@ -0,0 +1,46 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_42 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_1 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_46 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_2 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_3 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_41 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_4 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_5 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_38 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define RESET_NVS_BUTTON_GPIO GPIO_NUM_NC +#define RESET_FACTORY_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 280 +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define BACKLIGHT_INVERT false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 20 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_48 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define ML307_RX_PIN GPIO_NUM_12 +#define ML307_TX_PIN GPIO_NUM_13 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/kevin-sp-v4-dev/config.json b/main/boards/kevin-sp-v4-dev/config.json new file mode 100644 index 0000000..1221fbf --- /dev/null +++ b/main/boards/kevin-sp-v4-dev/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "kevin-sp-v4-dev", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/kevin-sp-v4-dev/kevin-sp-v4_board.cc b/main/boards/kevin-sp-v4-dev/kevin-sp-v4_board.cc new file mode 100644 index 0000000..a6e280b --- /dev/null +++ b/main/boards/kevin-sp-v4-dev/kevin-sp-v4_board.cc @@ -0,0 +1,149 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" + +#include +#include +#include +#include + +#define TAG "kevin-sp-v4" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class KEVIN_SP_V4Board : public WifiBoard { +private: + i2c_master_bus_handle_t display_i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + i2c_master_bus_handle_t codec_i2c_bus_; + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_47; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_21; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_14; + io_config.dc_gpio_num = GPIO_NUM_45; + 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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(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_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, true)); + + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 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("Lamp")); + } + +public: + KEVIN_SP_V4Board() : boot_button_(BOOT_BUTTON_GPIO) { + ESP_LOGI(TAG, "Initializing KEVIN SP V4 Board"); + InitializeCodecI2c(); + InitializeSpi(); + InitializeButtons(); + InitializeSt7789Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + + 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; + } +}; + +DECLARE_BOARD(KEVIN_SP_V4Board); diff --git a/main/boards/kevin-yuying-313lcd/config.h b/main/boards/kevin-yuying-313lcd/config.h new file mode 100644 index 0000000..b533741 --- /dev/null +++ b/main/boards/kevin-yuying-313lcd/config.h @@ -0,0 +1,35 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_42 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_39 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_41 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_40 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_38 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_45 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_1 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_2 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define DISPLAY_WIDTH 376 +#define DISPLAY_HEIGHT 960 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_4 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/kevin-yuying-313lcd/config.json b/main/boards/kevin-yuying-313lcd/config.json new file mode 100644 index 0000000..ae29bb4 --- /dev/null +++ b/main/boards/kevin-yuying-313lcd/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "kevin-yuying-313lcd", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.c b/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.c new file mode 100644 index 0000000..9f62738 --- /dev/null +++ b/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.c @@ -0,0 +1,478 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp_lcd_gc9503.h" + +#define GC9503_CMD_MADCTL (0xB1) // Memory data access control +#define GC9503_CMD_MADCTL_DEFAULT (0x10) // Default value of Memory data access control +#define GC9503_CMD_SS_BIT (1 << 0) // Source driver scan direction, 0: top to bottom, 1: bottom to top +#define GC9503_CMD_GS_BIT (1 << 1) // Gate driver scan direction, 0: left to right, 1: right to left +#define GC9503_CMD_BGR_BIT (1 << 5) // RGB/BGR order, 0: RGB, 1: BGR + +typedef struct +{ + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + uint8_t madctl_val; // Save current value of GC9503_CMD_MADCTL register + uint8_t colmod_val; // Save current value of LCD_CMD_COLMOD register + const gc9503_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; + struct + { + unsigned int mirror_by_cmd : 1; + unsigned int auto_del_panel_io : 1; + unsigned int display_on_off_use_cmd : 1; + unsigned int reset_level : 1; + } flags; + // To save the original functions of RGB panel + esp_err_t (*init)(esp_lcd_panel_t *panel); + esp_err_t (*del)(esp_lcd_panel_t *panel); + esp_err_t (*reset)(esp_lcd_panel_t *panel); + esp_err_t (*mirror)(esp_lcd_panel_t *panel, bool x_axis, bool y_axis); + esp_err_t (*disp_on_off)(esp_lcd_panel_t *panel, bool on_off); +} gc9503_panel_t; + +static const char *TAG = "gc9503"; + +static esp_err_t panel_gc9503_send_init_cmds(gc9503_panel_t *gc9503); + +static esp_err_t panel_gc9503_init(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9503_del(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9503_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9503_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_gc9503_disp_on_off(esp_lcd_panel_t *panel, bool off); + +esp_err_t esp_lcd_new_panel_gc9503(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, + esp_lcd_panel_handle_t *ret_panel) +{ + ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid arguments"); + gc9503_vendor_config_t *vendor_config = (gc9503_vendor_config_t *)panel_dev_config->vendor_config; + ESP_RETURN_ON_FALSE(vendor_config && vendor_config->rgb_config, ESP_ERR_INVALID_ARG, TAG, "`verndor_config` and `rgb_config` are necessary"); + ESP_RETURN_ON_FALSE(!vendor_config->flags.auto_del_panel_io || !vendor_config->flags.mirror_by_cmd, + ESP_ERR_INVALID_ARG, TAG, "`mirror_by_cmd` and `auto_del_panel_io` cannot work together"); + + esp_err_t ret = ESP_OK; + gpio_config_t io_conf = {0}; + + gc9503_panel_t *gc9503 = (gc9503_panel_t *)calloc(1, sizeof(gc9503_panel_t)); + ESP_RETURN_ON_FALSE(gc9503, ESP_ERR_NO_MEM, TAG, "no mem for gc9503 panel"); + + if (panel_dev_config->reset_gpio_num >= 0) + { + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + + gc9503->madctl_val = GC9503_CMD_MADCTL_DEFAULT; + switch (panel_dev_config->rgb_ele_order) + { + case LCD_RGB_ELEMENT_ORDER_RGB: + gc9503->madctl_val &= ~GC9503_CMD_BGR_BIT; + break; + case LCD_RGB_ELEMENT_ORDER_BGR: + gc9503->madctl_val |= GC9503_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color element order"); + break; + } + + gc9503->colmod_val = 0; + switch (panel_dev_config->bits_per_pixel) + { + case 16: // RGB565 + gc9503->colmod_val = 0x50; + break; + case 18: // RGB666 + gc9503->colmod_val = 0x60; + break; + case 24: // RGB888 + gc9503->colmod_val = 0x70; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + gc9503->io = io; + gc9503->init_cmds = vendor_config->init_cmds; + gc9503->init_cmds_size = vendor_config->init_cmds_size; + gc9503->reset_gpio_num = panel_dev_config->reset_gpio_num; + gc9503->flags.reset_level = panel_dev_config->flags.reset_active_high; + gc9503->flags.auto_del_panel_io = vendor_config->flags.auto_del_panel_io; + gc9503->flags.mirror_by_cmd = vendor_config->flags.mirror_by_cmd; + gc9503->flags.display_on_off_use_cmd = (vendor_config->rgb_config->disp_gpio_num >= 0) ? 0 : 1; + + if (gc9503->flags.auto_del_panel_io) + { + if (gc9503->reset_gpio_num >= 0) + { // Perform hardware reset + gpio_set_level(gc9503->reset_gpio_num, gc9503->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(gc9503->reset_gpio_num, !gc9503->flags.reset_level); + } + else + { // Perform software reset + ESP_GOTO_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), err, TAG, "send command failed"); + } + vTaskDelay(pdMS_TO_TICKS(120)); + + /** + * In order to enable the 3-wire SPI interface pins (such as SDA and SCK) to share other pins of the RGB interface + * (such as HSYNC) and save GPIOs, we need to send LCD initialization commands via the 3-wire SPI interface before + * `esp_lcd_new_rgb_panel()` is called. + */ + ESP_GOTO_ON_ERROR(panel_gc9503_send_init_cmds(gc9503), err, TAG, "send init commands failed"); + // After sending the initialization commands, the 3-wire SPI interface can be deleted + ESP_GOTO_ON_ERROR(esp_lcd_panel_io_del(io), err, TAG, "delete panel IO failed"); + gc9503->io = NULL; + ESP_LOGD(TAG, "delete panel IO"); + } + + // Create RGB panel + ESP_GOTO_ON_ERROR(esp_lcd_new_rgb_panel(vendor_config->rgb_config, ret_panel), err, TAG, "create RGB panel failed"); + ESP_LOGD(TAG, "new RGB panel @%p", ret_panel); + + // Save the original functions of RGB panel + gc9503->init = (*ret_panel)->init; + gc9503->del = (*ret_panel)->del; + gc9503->reset = (*ret_panel)->reset; + gc9503->mirror = (*ret_panel)->mirror; + gc9503->disp_on_off = (*ret_panel)->disp_on_off; + // Overwrite the functions of RGB panel + (*ret_panel)->init = panel_gc9503_init; + (*ret_panel)->del = panel_gc9503_del; + (*ret_panel)->reset = panel_gc9503_reset; + (*ret_panel)->mirror = panel_gc9503_mirror; + (*ret_panel)->disp_on_off = panel_gc9503_disp_on_off; + (*ret_panel)->user_data = gc9503; + ESP_LOGD(TAG, "new gc9503 panel @%p", gc9503); + + // ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_GC9503_VER_MAJOR, ESP_LCD_GC9503_VER_MINOR, + // ESP_LCD_GC9503_VER_PATCH); + return ESP_OK; + +err: + if (gc9503) + { + if (panel_dev_config->reset_gpio_num >= 0) + { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(gc9503); + } + return ret; +} + +// *INDENT-OFF* +// static const gc9503_lcd_init_cmd_t vendor_specific_init_default[] = { +// // {cmd, { data }, data_size, delay_ms} +// {0x11, (uint8_t []){0x00}, 0, 120}, + +// {0xf0, (uint8_t []){0x55, 0xaa, 0x52, 0x08, 0x00}, 5, 0}, +// {0xf6, (uint8_t []){0x5a, 0x87}, 2, 0}, +// {0xc1, (uint8_t []){0x3f}, 1, 0}, +// {0xc2, (uint8_t []){0x0e}, 1, 0}, +// {0xc6, (uint8_t []){0xf8}, 1, 0}, +// {0xc9, (uint8_t []){0x10}, 1, 0}, +// {0xcd, (uint8_t []){0x25}, 1, 0}, +// {0xf8, (uint8_t []){0x8a}, 1, 0}, +// {0xac, (uint8_t []){0x45}, 1, 0}, +// {0xa0, (uint8_t []){0xdd}, 1, 0}, +// {0xa7, (uint8_t []){0x47}, 1, 0}, +// {0xfa, (uint8_t []){0x00, 0x00, 0x00, 0x04}, 4, 0}, +// {0x86, (uint8_t []){0x99, 0xa3, 0xa3, 0x51}, 4, 0}, +// {0xa3, (uint8_t []){0xee}, 1, 0}, +// {0xfd, (uint8_t []){0x3c, 0x3c, 0x00}, 3, 0}, +// {0x71, (uint8_t []){0x48}, 1, 0}, +// {0x72, (uint8_t []){0x48}, 1, 0}, +// {0x73, (uint8_t []){0x00, 0x44}, 2, 0}, +// {0x97, (uint8_t []){0xee}, 1, 0}, +// {0x83, (uint8_t []){0x93}, 1, 0}, +// {0x9a, (uint8_t []){0x72}, 1, 0}, +// {0x9b, (uint8_t []){0x5a}, 1, 0}, +// {0x82, (uint8_t []){0x2c, 0x2c}, 2, 0}, +// {0x6d, (uint8_t []){0x00, 0x1f, 0x19, 0x1a, 0x10, 0x0e, 0x0c, 0x0a, 0x02, 0x07, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, +// 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x08, 0x01, 0x09, 0x0b, 0x0d, 0x0f, 0x1a, 0x19, 0x1f, 0x00}, 32, 0}, +// {0x64, (uint8_t []){0x38, 0x05, 0x01, 0xdb, 0x03, 0x03, 0x38, 0x04, 0x01, 0xdc, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a}, 16, 0}, +// {0x65, (uint8_t []){0x38, 0x03, 0x01, 0xdd, 0x03, 0x03, 0x38, 0x02, 0x01, 0xde, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a}, 16, 0}, +// {0x66, (uint8_t []){0x38, 0x01, 0x01, 0xdf, 0x03, 0x03, 0x38, 0x00, 0x01, 0xe0, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a}, 16, 0}, +// {0x67, (uint8_t []){0x30, 0x01, 0x01, 0xe1, 0x03, 0x03, 0x30, 0x02, 0x01, 0xe2, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a}, 16, 0}, +// {0x68, (uint8_t []){0x00, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a}, 13, 0}, +// {0x60, (uint8_t []){0x38, 0x08, 0x7a, 0x7a, 0x38, 0x09, 0x7a, 0x7a}, 8, 0}, +// {0x63, (uint8_t []){0x31, 0xe4, 0x7a, 0x7a, 0x31, 0xe5, 0x7a, 0x7a}, 8, 0}, +// {0x69, (uint8_t []){0x04, 0x22, 0x14, 0x22, 0x14, 0x22, 0x08}, 7, 0}, +// {0x6b, (uint8_t []){0x07}, 1, 0}, +// {0x7a, (uint8_t []){0x08, 0x13}, 2, 0}, +// {0x7b, (uint8_t []){0x08, 0x13}, 2, 0}, +// {0xd1, (uint8_t []){0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, +// 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, +// 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, +// 0xff}, 52, 0}, +// {0xd2, (uint8_t []){0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, +// 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, +// 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, +// 0xff}, 52, 0}, +// {0xd3, (uint8_t []){0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, +// 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, +// 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, +// 0xff}, 52, 0}, +// {0xd4, (uint8_t []){0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, +// 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, +// 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, +// 0xff}, 52, 0}, +// {0xd5, (uint8_t []){0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, +// 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, +// 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, +// 0xff}, 52, 0}, +// {0xd6, (uint8_t []){0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, +// 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, +// 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, +// 0xff}, 52, 0}, +// {0x11, (uint8_t []){0x00}, 0, 120}, +// {0x29, (uint8_t []){0x00}, 0, 20}, +// }; +static const gc9503_lcd_init_cmd_t vendor_specific_init_default[] = { + // {0x11, (uint8_t[]){}, 0, 20}, + + {0xF0, (uint8_t[]){0x55, 0xAA, 0x52, 0x08, 0x00}, 5, 0}, + {0xF6, (uint8_t[]){0x5A, 0x87}, 2, 0}, + {0xC1, (uint8_t[]){0x3F}, 1, 0}, + {0xCD, (uint8_t[]){0x25}, 1, 0}, + {0xC9, (uint8_t[]){0x10}, 1, 0}, + {0xF8, (uint8_t[]){0x8A}, 1, 0}, + {0xAC, (uint8_t[]){0x45}, 1, 0}, + {0xA7, (uint8_t[]){0x47}, 1, 0}, + {0xA0, (uint8_t[]){0x88}, 1, 0}, + {0x86, (uint8_t[]){0x99, 0xA3, 0xA3, 0x51}, 4, 0}, + {0xFA, (uint8_t[]){0x08, 0x08, 0x00, 0x04}, 4, 0}, + {0xA3, (uint8_t[]){0x6E}, 1, 0}, + {0xFD, (uint8_t[]){0x28, 0x3C, 0x00}, 3, 0}, + {0x9A, (uint8_t[]){0x4B}, 1, 0}, + {0x9B, (uint8_t[]){0x4B}, 1, 0}, + {0x82, (uint8_t[]){0x20, 0x20}, 2, 0}, + {0xB1, (uint8_t[]){0x10}, 1, 0}, + {0x7A, (uint8_t[]){0x0F, 0x13}, 2, 0}, + {0x7B, (uint8_t[]){0x0F, 0x13}, 2, 0}, + {0x6D, (uint8_t[]){0x1e, 0x1e, 0x04, 0x02, 0x0d, 0x1e, 0x12, 0x11, 0x14, 0x13, 0x05, 0x06, 0x1d, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1d, 0x06, 0x05, 0x0b, 0x0c, 0x09, 0x0a, 0x1e, 0x0d, 0x01, 0x03, 0x1e, 0x1e}, 32, 0}, + {0x64, (uint8_t[]){0x38, 0x08, 0x03, 0xc0, 0x03, 0x03, 0x38, 0x06, 0x03, 0xc2, 0x03, 0x03, 0x20, 0x6d, 0x20, 0x6d}, 16, 0}, + {0x65, (uint8_t[]){0x38, 0x04, 0x03, 0xc4, 0x03, 0x03, 0x38, 0x02, 0x03, 0xc6, 0x03, 0x03, 0x20, 0x6d, 0x20, 0x6d}, 16, 0}, + {0x66, (uint8_t[]){0x83, 0xcf, 0x03, 0xc8, 0x03, 0x03, 0x83, 0xd3, 0x03, 0xd2, 0x03, 0x03, 0x20, 0x6d, 0x20, 0x6d}, 16, 0}, + {0x60, (uint8_t[]){0x38, 0x0C, 0x20, 0x6D, 0x38, 0x0B, 0x20, 0x6D}, 8, 0}, + {0x61, (uint8_t[]){0x38, 0x0A, 0x20, 0x6D, 0x38, 0x09, 0x20, 0x6D}, 8, 0}, + {0x62, (uint8_t[]){0x38, 0x25, 0x20, 0x6D, 0x63, 0xC9, 0x20, 0x6D}, 8, 0}, + {0x69, (uint8_t[]){0x14, 0x22, 0x14, 0x22, 0x14, 0x22, 0x08}, 7, 0}, + {0x6B, (uint8_t[]){0x07}, 1, 0}, + {0xD1, (uint8_t[]){0x00, 0x00, 0x00, 0x70, 0x00, 0x8f, 0x00, 0xab, 0x00, 0xbf, 0x00, 0xdf, 0x00, 0xfa, 0x01, 0x2a, 0x01, 0x52, 0x01, 0x90, 0x01, 0xc1, 0x02, 0x0e, 0x02, 0x4f, 0x02, 0x51, 0x02, 0x8d, 0x02, 0xd3, 0x02, 0xff, 0x03, 0x3c, 0x03, 0x64, 0x03, 0xa1, 0x03, 0xf1, 0x03, 0xff, 0x03, 0xfF, 0x03, 0xff, 0x03, 0xFf, 0x03, 0xFF}, 52, 0}, + {0xD2, (uint8_t[]){0x00, 0x00, 0x00, 0x70, 0x00, 0x8f, 0x00, 0xab, 0x00, 0xbf, 0x00, 0xdf, 0x00, 0xfa, 0x01, 0x2a, 0x01, 0x52, 0x01, 0x90, 0x01, 0xc1, 0x02, 0x0e, 0x02, 0x4f, 0x02, 0x51, 0x02, 0x8d, 0x02, 0xd3, 0x02, 0xff, 0x03, 0x3c, 0x03, 0x64, 0x03, 0xa1, 0x03, 0xf1, 0x03, 0xff, 0x03, 0xfF, 0x03, 0xff, 0x03, 0xFf, 0x03, 0xFF}, 52, 0}, + {0xD3, (uint8_t[]){0x00, 0x00, 0x00, 0x70, 0x00, 0x8f, 0x00, 0xab, 0x00, 0xbf, 0x00, 0xdf, 0x00, 0xfa, 0x01, 0x2a, 0x01, 0x52, 0x01, 0x90, 0x01, 0xc1, 0x02, 0x0e, 0x02, 0x4f, 0x02, 0x51, 0x02, 0x8d, 0x02, 0xd3, 0x02, 0xff, 0x03, 0x3c, 0x03, 0x64, 0x03, 0xa1, 0x03, 0xf1, 0x03, 0xff, 0x03, 0xfF, 0x03, 0xff, 0x03, 0xFf, 0x03, 0xFF}, 52, 0}, + {0xD4, (uint8_t[]){0x00, 0x00, 0x00, 0x70, 0x00, 0x8f, 0x00, 0xab, 0x00, 0xbf, 0x00, 0xdf, 0x00, 0xfa, 0x01, 0x2a, 0x01, 0x52, 0x01, 0x90, 0x01, 0xc1, 0x02, 0x0e, 0x02, 0x4f, 0x02, 0x51, 0x02, 0x8d, 0x02, 0xd3, 0x02, 0xff, 0x03, 0x3c, 0x03, 0x64, 0x03, 0xa1, 0x03, 0xf1, 0x03, 0xff, 0x03, 0xfF, 0x03, 0xff, 0x03, 0xFf, 0x03, 0xFF}, 52, 0}, + {0xD5, (uint8_t[]){0x00, 0x00, 0x00, 0x70, 0x00, 0x8f, 0x00, 0xab, 0x00, 0xbf, 0x00, 0xdf, 0x00, 0xfa, 0x01, 0x2a, 0x01, 0x52, 0x01, 0x90, 0x01, 0xc1, 0x02, 0x0e, 0x02, 0x4f, 0x02, 0x51, 0x02, 0x8d, 0x02, 0xd3, 0x02, 0xff, 0x03, 0x3c, 0x03, 0x64, 0x03, 0xa1, 0x03, 0xf1, 0x03, 0xff, 0x03, 0xfF, 0x03, 0xff, 0x03, 0xFf, 0x03, 0xFF}, 52, 0}, + {0xD6, (uint8_t[]){0x00, 0x00, 0x00, 0x70, 0x00, 0x8f, 0x00, 0xab, 0x00, 0xbf, 0x00, 0xdf, 0x00, 0xfa, 0x01, 0x2a, 0x01, 0x52, 0x01, 0x90, 0x01, 0xc1, 0x02, 0x0e, 0x02, 0x4f, 0x02, 0x51, 0x02, 0x8d, 0x02, 0xd3, 0x02, 0xff, 0x03, 0x3c, 0x03, 0x64, 0x03, 0xa1, 0x03, 0xf1, 0x03, 0xff, 0x03, 0xfF, 0x03, 0xff, 0x03, 0xFf, 0x03, 0xFF}, 52, 0}, + // {0x3A, (uint8_t[]){0x55}, 1, 0}, + + {0x11, NULL, 0, 120}, // Delay 120ms + {0x29, NULL, 0, 120}}; + +// *INDENT-OFF* + +static esp_err_t panel_gc9503_send_init_cmds(gc9503_panel_t *gc9503) +{ + esp_lcd_panel_io_handle_t io = gc9503->io; + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, GC9503_CMD_MADCTL, (uint8_t[]){ + gc9503->madctl_val, + }, + 1), + TAG, "send command failed"); + ; + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]){ + gc9503->colmod_val, + }, + 1), + TAG, "send command failed"); + ; + + // Vendor specific initialization, it can be different between manufacturers + // should consult the LCD supplier for initialization sequence code + const gc9503_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (gc9503->init_cmds) + { + init_cmds = gc9503->init_cmds; + init_cmds_size = gc9503->init_cmds_size; + } + else + { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(gc9503_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + for (int i = 0; i < init_cmds_size; i++) + { + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd) + { + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + gc9503->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + gc9503->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten) + { + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", + init_cmds[i].cmd); + } + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), + TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_gc9503_init(esp_lcd_panel_t *panel) +{ + gc9503_panel_t *gc9503 = (gc9503_panel_t *)panel->user_data; + + if (!gc9503->flags.auto_del_panel_io) + { + ESP_RETURN_ON_ERROR(panel_gc9503_send_init_cmds(gc9503), TAG, "send init commands failed"); + } + // Init RGB panel + ESP_RETURN_ON_ERROR(gc9503->init(panel), TAG, "init RGB panel failed"); + + return ESP_OK; +} + +static esp_err_t panel_gc9503_del(esp_lcd_panel_t *panel) +{ + gc9503_panel_t *gc9503 = (gc9503_panel_t *)panel->user_data; + + if (gc9503->reset_gpio_num >= 0) + { + gpio_reset_pin(gc9503->reset_gpio_num); + } + // Delete RGB panel + gc9503->del(panel); + free(gc9503); + ESP_LOGD(TAG, "del gc9503 panel @%p", gc9503); + return ESP_OK; +} + +static esp_err_t panel_gc9503_reset(esp_lcd_panel_t *panel) +{ + gc9503_panel_t *gc9503 = (gc9503_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = gc9503->io; + + // Perform hardware reset + if (gc9503->reset_gpio_num >= 0) + { + gpio_set_level(gc9503->reset_gpio_num, gc9503->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(gc9503->reset_gpio_num, !gc9503->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(120)); + } + else if (io) + { // Perform software reset + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(120)); + } + // Reset RGB panel + ESP_RETURN_ON_ERROR(gc9503->reset(panel), TAG, "reset RGB panel failed"); + + return ESP_OK; +} + +static esp_err_t panel_gc9503_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + gc9503_panel_t *gc9503 = (gc9503_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = gc9503->io; + + if (gc9503->flags.mirror_by_cmd) + { + ESP_RETURN_ON_FALSE(io, ESP_FAIL, TAG, "Panel IO is deleted, cannot send command"); + // Control mirror through LCD command + if (mirror_x) + { + gc9503->madctl_val |= GC9503_CMD_GS_BIT; + } + else + { + gc9503->madctl_val &= ~GC9503_CMD_GS_BIT; + } + if (mirror_y) + { + gc9503->madctl_val |= GC9503_CMD_SS_BIT; + } + else + { + gc9503->madctl_val &= ~GC9503_CMD_SS_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, GC9503_CMD_MADCTL, (uint8_t[]){ + gc9503->madctl_val, + }, + 1), + TAG, "send command failed"); + ; + } + else + { + // Control mirror through RGB panel + ESP_RETURN_ON_ERROR(gc9503->mirror(panel, mirror_x, mirror_y), TAG, "RGB panel mirror failed"); + } + return ESP_OK; +} + +static esp_err_t panel_gc9503_disp_on_off(esp_lcd_panel_t *panel, bool on_off) +{ + gc9503_panel_t *gc9503 = (gc9503_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = gc9503->io; + int command = 0; + + if (gc9503->flags.display_on_off_use_cmd) + { + ESP_RETURN_ON_FALSE(io, ESP_FAIL, TAG, "Panel IO is deleted, cannot send command"); + // Control display on/off through LCD command + if (on_off) + { + command = LCD_CMD_DISPON; + } + else + { + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + } + else + { + // Control display on/off through display control signal + ESP_RETURN_ON_ERROR(gc9503->disp_on_off(panel, on_off), TAG, "RGB panel disp_on_off failed"); + } + return ESP_OK; +} diff --git a/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.h b/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.h new file mode 100644 index 0000000..40a5ccc --- /dev/null +++ b/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.h @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * @file + * @brief ESP LCD: GC9503 + */ + +#pragma once + +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /* +#include +#include +#include "esp_lcd_gc9503.h" +#include +#include +#include + +#define TAG "Yuying_313lcd" + +LV_FONT_DECLARE(font_puhui_30_4); +LV_FONT_DECLARE(font_awesome_30_4); + +class Yuying_313lcd : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + + void InitializeRGB_GC9503V_Display() { + ESP_LOGI(TAG, "Init GC9503V"); + + esp_lcd_panel_io_handle_t panel_io = nullptr; + + ESP_LOGI(TAG, "Install 3-wire SPI panel IO"); + spi_line_config_t line_config = { + .cs_io_type = IO_TYPE_GPIO, + .cs_gpio_num = GC9503V_LCD_IO_SPI_CS_1, + .scl_io_type = IO_TYPE_GPIO, + .scl_gpio_num = GC9503V_LCD_IO_SPI_SCL_1, + .sda_io_type = IO_TYPE_GPIO, + .sda_gpio_num = GC9503V_LCD_IO_SPI_SDO_1, + .io_expander = NULL, + }; + esp_lcd_panel_io_3wire_spi_config_t io_config = GC9503_PANEL_IO_3WIRE_SPI_CONFIG(line_config, 0); + (esp_lcd_new_panel_io_3wire_spi(&io_config, &panel_io)); + + ESP_LOGI(TAG, "Install RGB LCD panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + esp_lcd_rgb_panel_config_t rgb_config = { + .clk_src = LCD_CLK_SRC_PLL160M, + .timings = GC9503_376_960_PANEL_60HZ_RGB_TIMING(), + .data_width = 16, // RGB565 in parallel mode, thus 16bit in width + .bits_per_pixel = 16, + .num_fbs = GC9503V_LCD_RGB_BUFFER_NUMS, + .bounce_buffer_size_px = GC9503V_LCD_H_RES * GC9503V_LCD_RGB_BOUNCE_BUFFER_HEIGHT, + .dma_burst_size = 64, + .hsync_gpio_num = GC9503V_PIN_NUM_HSYNC, + .vsync_gpio_num = GC9503V_PIN_NUM_VSYNC, + .de_gpio_num = GC9503V_PIN_NUM_DE, + .pclk_gpio_num = GC9503V_PIN_NUM_PCLK, + .disp_gpio_num = GC9503V_PIN_NUM_DISP_EN, + .data_gpio_nums = { + GC9503V_PIN_NUM_DATA0, + GC9503V_PIN_NUM_DATA1, + GC9503V_PIN_NUM_DATA2, + GC9503V_PIN_NUM_DATA3, + GC9503V_PIN_NUM_DATA4, + GC9503V_PIN_NUM_DATA5, + GC9503V_PIN_NUM_DATA6, + GC9503V_PIN_NUM_DATA7, + GC9503V_PIN_NUM_DATA8, + GC9503V_PIN_NUM_DATA9, + GC9503V_PIN_NUM_DATA10, + GC9503V_PIN_NUM_DATA11, + GC9503V_PIN_NUM_DATA12, + GC9503V_PIN_NUM_DATA13, + GC9503V_PIN_NUM_DATA14, + GC9503V_PIN_NUM_DATA15, + }, + .flags= { + .fb_in_psram = true, // allocate frame buffer in PSRAM + } + }; + + ESP_LOGI(TAG, "Initialize RGB LCD panel"); + + gc9503_vendor_config_t vendor_config = { + .rgb_config = &rgb_config, + .flags = { + .mirror_by_cmd = 0, + .auto_del_panel_io = 1, + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = -1, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + .vendor_config = &vendor_config, + }; + (esp_lcd_new_panel_gc9503(panel_io, &panel_config, &panel_handle)); + (esp_lcd_panel_reset(panel_handle)); + (esp_lcd_panel_init(panel_handle)); + + display_ = new RgbLcdDisplay(panel_io, panel_handle, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, + DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, + { + .text_font = &font_puhui_30_4, + .icon_font = &font_awesome_30_4, + .emoji_font = font_emoji_64_init(), + }); + } + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + Yuying_313lcd() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeCodecI2c(); + InitializeButtons(); + InitializeIot(); + InitializeRGB_GC9503V_Display(); + GetBacklight()->RestoreBrightness(); + } + + 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; + } +}; + +DECLARE_BOARD(Yuying_313lcd); diff --git a/main/boards/kevin-yuying-313lcd/pin_config.h b/main/boards/kevin-yuying-313lcd/pin_config.h new file mode 100644 index 0000000..0bcd059 --- /dev/null +++ b/main/boards/kevin-yuying-313lcd/pin_config.h @@ -0,0 +1,47 @@ + +#pragma once +#define GC9503V_LCD_H_RES 376 +#define GC9503V_LCD_V_RES 960 + + +#define GC9503V_LCD_LVGL_DIRECT_MODE (1) +#define GC9503V_LCD_LVGL_AVOID_TEAR (1) +#define GC9503V_LCD_RGB_BOUNCE_BUFFER_MODE (1) +#define GC9503V_LCD_DRAW_BUFF_DOUBLE (0) +#define GC9503V_LCD_DRAW_BUFF_HEIGHT (100) +#define GC9503V_LCD_RGB_BUFFER_NUMS (2) +#define GC9503V_LCD_RGB_BOUNCE_BUFFER_HEIGHT (10) + +#define GC9503V_LCD_PIXEL_CLOCK_HZ (16 * 1000 * 1000) +#define GC9503V_LCD_BK_LIGHT_ON_LEVEL 1 +#define GC9503V_LCD_BK_LIGHT_OFF_LEVEL !GC9503V_LCD_BK_LIGHT_ON_LEVEL +#define GC9503V_PIN_NUM_BK_LIGHT GPIO_NUM_4 +#define GC9503V_PIN_NUM_HSYNC 6 +#define GC9503V_PIN_NUM_VSYNC 5 +#define GC9503V_PIN_NUM_DE 15 +#define GC9503V_PIN_NUM_PCLK 7 + +#define GC9503V_PIN_NUM_DATA0 47 // B0 +#define GC9503V_PIN_NUM_DATA1 21 // B1 +#define GC9503V_PIN_NUM_DATA2 14 // B2 +#define GC9503V_PIN_NUM_DATA3 13 // B3 +#define GC9503V_PIN_NUM_DATA4 12 // B4 + +#define GC9503V_PIN_NUM_DATA5 11 // G0 +#define GC9503V_PIN_NUM_DATA6 10 // G1 +#define GC9503V_PIN_NUM_DATA7 9 // G2 +#define GC9503V_PIN_NUM_DATA8 46 // G3 +#define GC9503V_PIN_NUM_DATA9 3 // G4 +#define GC9503V_PIN_NUM_DATA10 20 // G5 + +#define GC9503V_PIN_NUM_DATA11 19 // R0 +#define GC9503V_PIN_NUM_DATA12 8 // R1 +#define GC9503V_PIN_NUM_DATA13 18 // R2 +#define GC9503V_PIN_NUM_DATA14 17 // R3 +#define GC9503V_PIN_NUM_DATA15 16 // R4 + +#define GC9503V_PIN_NUM_DISP_EN -1 + +#define GC9503V_LCD_IO_SPI_CS_1 (GPIO_NUM_48) +#define GC9503V_LCD_IO_SPI_SCL_1 (GPIO_NUM_17) +#define GC9503V_LCD_IO_SPI_SDO_1 (GPIO_NUM_16) \ No newline at end of file diff --git a/main/boards/lichuang-c3-dev/README.md b/main/boards/lichuang-c3-dev/README.md new file mode 100644 index 0000000..f4fb208 --- /dev/null +++ b/main/boards/lichuang-c3-dev/README.md @@ -0,0 +1,11 @@ +## 立创·实战派ESP32-C3开发板 + +1、开发板资料:https://wiki.lckfb.com/zh-hans/szpi-esp32c3 + +2、该开发板 flash 大小为 8MB,编译时注意选择合适的分区表: + +``` +Partition Table ---> + Partition Table (Custom partition table CSV) ---> + (partitions_8M.csv) Custom partition CSV file +``` diff --git a/main/boards/lichuang-c3-dev/config.h b/main/boards/lichuang-c3-dev/config.h new file mode 100644 index 0000000..548ccb4 --- /dev/null +++ b/main/boards/lichuang-c3-dev/config.h @@ -0,0 +1,45 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 + +#define AUDIO_CODEC_USE_PCA9557 +#define AUDIO_CODEC_PA_PIN GPIO_NUM_13 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR 0x82 + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_9 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SPI_SCK_PIN GPIO_NUM_3 +#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5 +#define DISPLAY_DC_PIN GPIO_NUM_6 +#define DISPLAY_SPI_CS_PIN GPIO_NUM_4 + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/lichuang-c3-dev/config.json b/main/boards/lichuang-c3-dev/config.json new file mode 100644 index 0000000..cdc508f --- /dev/null +++ b/main/boards/lichuang-c3-dev/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "lichuang-c3-dev", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_8M.csv\"" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/lichuang-c3-dev/lichuang_c3_dev_board.cc b/main/boards/lichuang-c3-dev/lichuang_c3_dev_board.cc new file mode 100644 index 0000000..0725a23 --- /dev/null +++ b/main/boards/lichuang-c3-dev/lichuang_c3_dev_board.cc @@ -0,0 +1,146 @@ +#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 +#include +#include +#include +#include + +#define TAG "LichuangC3DevBoard" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class LichuangC3DevBoard : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN; + 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(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + 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 = 2; + 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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(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_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, true); + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + LichuangC3DevBoard() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeSt7789Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->SetBrightness(100); + } + + 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; + } +}; + +DECLARE_BOARD(LichuangC3DevBoard); diff --git a/main/boards/lichuang-dev/config.h b/main/boards/lichuang-dev/config.h new file mode 100644 index 0000000..bb0544d --- /dev/null +++ b/main/boards/lichuang-dev/config.h @@ -0,0 +1,41 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_13 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_12 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_45 + +#define AUDIO_CODEC_USE_PCA9557 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_1 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_2 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR 0x82 + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/lichuang-dev/config.json b/main/boards/lichuang-dev/config.json new file mode 100644 index 0000000..0b2c646 --- /dev/null +++ b/main/boards/lichuang-dev/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "lichuang-dev", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/lichuang-dev/lichuang_dev_board.cc b/main/boards/lichuang-dev/lichuang_dev_board.cc new file mode 100644 index 0000000..2f59c6a --- /dev/null +++ b/main/boards/lichuang-dev/lichuang_dev_board.cc @@ -0,0 +1,172 @@ +#include "wifi_board.h" +#include "audio_codecs/box_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 +#include +#include +#include +#include + +#define TAG "LichuangDevBoard" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class Pca9557 : public I2cDevice { +public: + Pca9557(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x01, 0x03); + WriteReg(0x03, 0xf8); + } + + void SetOutputState(uint8_t bit, uint8_t level) { + uint8_t data = ReadReg(0x01); + data = (data & ~(1 << bit)) | (level << bit); + WriteReg(0x01, data); + } +}; + + +class LichuangDevBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + i2c_master_dev_handle_t pca9557_handle_; + Button boot_button_; + LcdDisplay* display_; + Pca9557* pca9557_; + + 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, &i2c_bus_)); + + // Initialize PCA9557 + pca9557_ = new Pca9557(i2c_bus_, 0x19); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_40; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_41; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_NC; + io_config.dc_gpio_num = GPIO_NUM_39; + io_config.spi_mode = 2; + 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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(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_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + pca9557_->SetOutputState(0, 0); + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, true); + 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")); + } + +public: + LichuangDevBoard() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeSt7789Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec( + i2c_bus_, + 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, + GPIO_NUM_NC, + AUDIO_CODEC_ES8311_ADDR, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + 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; + } +}; + +DECLARE_BOARD(LichuangDevBoard); diff --git a/main/boards/lilygo-t-cameraplus-s3/README.md b/main/boards/lilygo-t-cameraplus-s3/README.md new file mode 100644 index 0000000..551ce51 --- /dev/null +++ b/main/boards/lilygo-t-cameraplus-s3/README.md @@ -0,0 +1,33 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> LILYGO T-CameraPlus-S3 +``` + +**修改 psram 配置:** + +``` +Component config -> ESP PSRAM -> SPI RAM config -> Mode (QUAD/OCT) -> Quad Mode PSRAM +``` + +**编译:** + +```bash +idf.py build +``` + +LILYGO T-CameraPlus-S3 \ No newline at end of file diff --git a/main/boards/lilygo-t-cameraplus-s3/config.h b/main/boards/lilygo-t-cameraplus-s3/config.h new file mode 100644 index 0000000..b47daea --- /dev/null +++ b/main/boards/lilygo-t-cameraplus-s3/config.h @@ -0,0 +1,47 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// M5Stack CoreS3 Board configuration + +#include +#include "pin_config.h" + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_MIC_I2S_GPIO_BCLK static_cast(MSM261_BCLK) +#define AUDIO_MIC_I2S_GPIO_WS static_cast(MSM261_WS) +#define AUDIO_MIC_I2S_GPIO_DATA static_cast(MSM261_DIN) + +#define AUDIO_SPKR_I2S_GPIO_BCLK static_cast(MAX98357A_BCLK) +#define AUDIO_SPKR_I2S_GPIO_LRCLK static_cast(MAX98357A_LRCLK) +#define AUDIO_SPKR_I2S_GPIO_DATA static_cast(MAX98357A_DOUT) + +#define TOUCH_I2C_SDA_PIN static_cast(TP_SDA) +#define TOUCH_I2C_SCL_PIN static_cast(TP_SCL) + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define KEY1_BUTTON_GPIO static_cast(KEY1) +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_WIDTH LCD_WIDTH +#define DISPLAY_HEIGHT LCD_HEIGHT +#define DISPLAY_MOSI LCD_MOSI +#define DISPLAY_SCLK LCD_SCLK +#define DISPLAY_DC LCD_DC +#define DISPLAY_RST LCD_RST +#define DISPLAY_CS LCD_CS +#define DISPLAY_BL static_cast(LCD_BL) +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN DISPLAY_BL +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/lilygo-t-cameraplus-s3/config.json b/main/boards/lilygo-t-cameraplus-s3/config.json new file mode 100644 index 0000000..596ae8c --- /dev/null +++ b/main/boards/lilygo-t-cameraplus-s3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "lilygo-t-cameraplus-s3", + "sdkconfig_append": ["CONFIG_SPIRAM_MODE_OCT=n","CONFIG_SPIRAM_MODE_QUAD=y"] + } + ] +} \ No newline at end of file diff --git a/main/boards/lilygo-t-cameraplus-s3/lilygo-t-cameraplus-s3.cc b/main/boards/lilygo-t-cameraplus-s3/lilygo-t-cameraplus-s3.cc new file mode 100644 index 0000000..8f58b6c --- /dev/null +++ b/main/boards/lilygo-t-cameraplus-s3/lilygo-t-cameraplus-s3.cc @@ -0,0 +1,262 @@ +#include "wifi_board.h" +#include "tcamerapluss3_audio_codec.h" +#include "display/lcd_display.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "power_save_timer.h" +#include "i2c_device.h" +#include "iot/thing_manager.h" + +#include +#include +#include +#include + +#define TAG "LilygoTCameraPlusS3Board" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class Cst816x : public I2cDevice { +public: + struct TouchPoint_t { + int num = 0; + int x = -1; + int y = -1; + }; + + Cst816x(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + uint8_t chip_id = ReadReg(0xA7); + ESP_LOGI(TAG, "Get chip ID: 0x%02X", chip_id); + read_buffer_ = new uint8_t[6]; + } + + ~Cst816x() { + delete[] read_buffer_; + } + + void UpdateTouchPoint() { + ReadRegs(0x02, read_buffer_, 6); + tp_.num = read_buffer_[0] & 0x0F; + tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2]; + tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4]; + } + + const TouchPoint_t &GetTouchPoint() { + return tp_; + } + +private: + uint8_t *read_buffer_ = nullptr; + TouchPoint_t tp_; +}; + +class LilygoTCameraPlusS3Board : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Cst816x *cst816d_; + LcdDisplay *display_; + Button key1_button_; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(10); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitI2c(){ + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_config = { + .i2c_port = I2C_NUM_0, + .sda_io_num = TOUCH_I2C_SDA_PIN, + .scl_io_num = TOUCH_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_config, &i2c_bus_)); + } + + void I2cDetect() { + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + } + + static void touchpad_daemon(void *param) { + vTaskDelay(pdMS_TO_TICKS(2000)); + auto &board = (LilygoTCameraPlusS3Board&)Board::GetInstance(); + auto touchpad = board.GetTouchpad(); + bool was_touched = false; + while (1) { + touchpad->UpdateTouchPoint(); + if (touchpad->GetTouchPoint().num > 0){ + // On press + if (!was_touched) { + was_touched = true; + Application::GetInstance().ToggleChatState(); + } + } + // On release + else if (was_touched) { + was_touched = false; + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + vTaskDelete(NULL); + } + + void InitCst816d() { + ESP_LOGI(TAG, "Init CST816x"); + cst816d_ = new Cst816x(i2c_bus_, 0x15); + xTaskCreate(touchpad_daemon, "tp", 2048, NULL, 5, NULL); + } + + void InitSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCLK; + 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)); + } + + void InitializeSt7789Display() { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = LCD_CS; + io_config.dc_gpio_num = LCD_DC; + io_config.spi_mode = 0; + io_config.pclk_hz = 60 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片ST7789 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = LCD_RST; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, true)); + + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + void InitializeButtons() { + key1_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + power_save_timer_->WakeUp(); + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto &thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + LilygoTCameraPlusS3Board() : key1_button_(KEY1_BUTTON_GPIO) { + InitializePowerSaveTimer(); + InitI2c(); + InitCst816d(); + I2cDetect(); + InitSpi(); + InitializeSt7789Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec *GetAudioCodec() override { + static Tcamerapluss3AudioCodec audio_codec( + AUDIO_INPUT_SAMPLE_RATE, + AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_MIC_I2S_GPIO_BCLK, + AUDIO_MIC_I2S_GPIO_WS, + AUDIO_MIC_I2S_GPIO_DATA, + AUDIO_SPKR_I2S_GPIO_BCLK, + AUDIO_SPKR_I2S_GPIO_LRCLK, + AUDIO_SPKR_I2S_GPIO_DATA, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display *GetDisplay() override{ + return display_; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } + + virtual Backlight* GetBacklight() override { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + + Cst816x *GetTouchpad() { + return cst816d_; + } +}; + +DECLARE_BOARD(LilygoTCameraPlusS3Board); diff --git a/main/boards/lilygo-t-cameraplus-s3/pin_config.h b/main/boards/lilygo-t-cameraplus-s3/pin_config.h new file mode 100644 index 0000000..715fa2d --- /dev/null +++ b/main/boards/lilygo-t-cameraplus-s3/pin_config.h @@ -0,0 +1,100 @@ +/* + * @Description: None + * @version: V1.0.0 + * @Author: None + * @Date: 2023-08-16 14:24:03 + * @LastEditors: LILYGO_L + * @LastEditTime: 2023-12-12 10:12:31 + * @License: GPL 3.0 + */ +#pragma once + +// microSD +#define SD_CS 21 +#define SD_SCLK 36 +#define SD_MOSI 35 +#define SD_MISO 37 + +// SPI +#define SCLK 36 +#define MOSI 35 +#define MISO 37 + +// MAX98357A +#define MAX98357A_BCLK 41 +#define MAX98357A_LRCLK 42 +#define MAX98357A_DOUT 38 + +// MSM261 +#define MSM261_BCLK 18 +#define MSM261_WS 39 +#define MSM261_DIN 40 + +// FP-133H01D +#define LCD_WIDTH 240 +#define LCD_HEIGHT 240 +#define LCD_BL 46 +#define LCD_MOSI 35 +#define LCD_SCLK 36 +#define LCD_CS 34 +#define LCD_DC 45 +#define LCD_RST 33 + +// SY6970 +#define SY6970_SDA 1 +#define SY6970_SCL 2 +#define SY6970_Address 0x6A +#define SY6970_INT 47 + +// IIC +#define IIC_SDA 1 +#define IIC_SCL 2 + +// OV2640 +#define OV2640_PWDN -1 +#define OV2640_RESET 3 +#define OV2640_XCLK 7 +#define OV2640_SIOD 1 +#define OV2640_SIOC 2 +#define OV2640_D7 6 +#define OV2640_D6 8 +#define OV2640_D5 9 +#define OV2640_D4 11 +#define OV2640_D3 13 +#define OV2640_D2 15 +#define OV2640_D1 14 +#define OV2640_D0 12 +#define OV2640_VSYNC 4 +#define OV2640_HREF 5 +#define OV2640_PCLK 10 + +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 3 +#define XCLK_GPIO_NUM 7 +#define SIOD_GPIO_NUM 1 +#define SIOC_GPIO_NUM 2 + +#define Y9_GPIO_NUM 6 +#define Y8_GPIO_NUM 8 +#define Y7_GPIO_NUM 9 +#define Y6_GPIO_NUM 11 +#define Y5_GPIO_NUM 13 +#define Y4_GPIO_NUM 15 +#define Y3_GPIO_NUM 14 +#define Y2_GPIO_NUM 12 +#define VSYNC_GPIO_NUM 4 +#define HREF_GPIO_NUM 5 +#define PCLK_GPIO_NUM 10 + +// CST816 +#define CST816_Address 0x15 +#define TP_SDA 1 +#define TP_SCL 2 +#define TP_RST 48 +#define TP_INT 47 + +// AP1511B +#define AP1511B_FBC 16 + +// KEY +#define KEY1 17 diff --git a/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.cc b/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.cc new file mode 100644 index 0000000..6a56277 --- /dev/null +++ b/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.cc @@ -0,0 +1,128 @@ +#include "tcamerapluss3_audio_codec.h" + +#include +#include +#include +#include + +static const char TAG[] = "Tcamerapluss3AudioCodec"; + +Tcamerapluss3AudioCodec::Tcamerapluss3AudioCodec(int input_sample_rate, int output_sample_rate, + gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data, + bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateVoiceHardware(mic_bclk, mic_ws, mic_data, spkr_bclk, spkr_lrclk, spkr_data); + + ESP_LOGI(TAG, "Tcamerapluss3AudioCodec initialized"); +} + +Tcamerapluss3AudioCodec::~Tcamerapluss3AudioCodec() { + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void Tcamerapluss3AudioCodec::CreateVoiceHardware(gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data) { + + i2s_chan_config_t mic_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + mic_chan_config.auto_clear = true; // Auto clear the legacy data in the DMA buffer + i2s_chan_config_t spkr_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER); + spkr_chan_config.auto_clear = true; // Auto clear the legacy data in the DMA buffer + + ESP_ERROR_CHECK(i2s_new_channel(&mic_chan_config, NULL, &rx_handle_)); + ESP_ERROR_CHECK(i2s_new_channel(&spkr_chan_config, &tx_handle_, NULL)); + + i2s_std_config_t mic_config = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + }, + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = mic_bclk, + .ws = mic_ws, + .dout = I2S_GPIO_UNUSED, + .din = mic_data, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = true // 默认右通道 + } + } + }; + + i2s_std_config_t spkr_config = { + .clk_cfg ={ + .sample_rate_hz = static_cast(11025), + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + }, + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg ={ + .mclk = I2S_GPIO_UNUSED, + .bclk = spkr_bclk, + .ws = spkr_lrclk, + .dout = spkr_data, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &mic_config)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &spkr_config)); + ESP_LOGI(TAG, "Voice hardware created"); +} + +void Tcamerapluss3AudioCodec::SetOutputVolume(int volume) { + volume_ = volume; + AudioCodec::SetOutputVolume(volume); +} + +void Tcamerapluss3AudioCodec::EnableInput(bool enable) { + AudioCodec::EnableInput(enable); +} + +void Tcamerapluss3AudioCodec::EnableOutput(bool enable) { + AudioCodec::EnableOutput(enable); +} + +int Tcamerapluss3AudioCodec::Read(int16_t *dest, int samples){ + if (input_enabled_){ + size_t bytes_read; + i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY); + } + return samples; +} + +int Tcamerapluss3AudioCodec::Write(const int16_t *data, int samples){ + if (output_enabled_){ + size_t bytes_read; + auto output_data = (int16_t *)malloc(samples * sizeof(int16_t)); + for (size_t i = 0; i < samples; i++){ + output_data[i] = (float)data[i] * (float)(volume_ / 100.0); + } + i2s_channel_write(tx_handle_, output_data, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY); + free(output_data); + } + return samples; +} diff --git a/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.h b/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.h new file mode 100644 index 0000000..8c3948b --- /dev/null +++ b/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.h @@ -0,0 +1,37 @@ +#ifndef _TCIRCLES3_AUDIO_CODEC_H +#define _TCIRCLES3_AUDIO_CODEC_H + +#include "audio_codecs/audio_codec.h" + +#include +#include + +class Tcamerapluss3AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t *data_if_ = nullptr; + const audio_codec_ctrl_if_t *out_ctrl_if_ = nullptr; + const audio_codec_if_t *out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t *in_ctrl_if_ = nullptr; + const audio_codec_if_t *in_codec_if_ = nullptr; + const audio_codec_gpio_if_t *gpio_if_ = nullptr; + + uint32_t volume_ = 70; + + void CreateVoiceHardware(gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data,gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data); + + virtual int Read(int16_t *dest, int samples) override; + virtual int Write(const int16_t *data, int samples) override; + +public: + Tcamerapluss3AudioCodec(int input_sample_rate, int output_sample_rate, + gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data, + bool input_reference); + virtual ~Tcamerapluss3AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _BOX_AUDIO_CODEC_H diff --git a/main/boards/lilygo-t-circle-s3/README.md b/main/boards/lilygo-t-circle-s3/README.md new file mode 100644 index 0000000..6b7b678 --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/README.md @@ -0,0 +1,28 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> LILYGO T-Circle-S3 +``` + + +**编译:** + +```bash +idf.py build +``` + +LILYGO T-Circle-S3 \ No newline at end of file diff --git a/main/boards/lilygo-t-circle-s3/config.h b/main/boards/lilygo-t-circle-s3/config.h new file mode 100644 index 0000000..e115c77 --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/config.h @@ -0,0 +1,48 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// M5Stack CoreS3 Board configuration + +#include +#include "pin_config.h" + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_MIC_I2S_GPIO_BCLK static_cast(MSM261_BCLK) +#define AUDIO_MIC_I2S_GPIO_WS static_cast(MSM261_WS) +#define AUDIO_MIC_I2S_GPIO_DATA static_cast(MSM261_DATA) + +#define AUDIO_SPKR_I2S_GPIO_BCLK static_cast(MAX98357A_BCLK) +#define AUDIO_SPKR_I2S_GPIO_LRCLK static_cast(MAX98357A_LRCLK) +#define AUDIO_SPKR_I2S_GPIO_DATA static_cast(MAX98357A_DATA) +#define AUDIO_SPKR_ENABLE static_cast(MAX98357A_SD_MODE) + +#define TOUCH_I2C_SDA_PIN static_cast(TP_SDA) +#define TOUCH_I2C_SCL_PIN static_cast(TP_SCL) + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_WIDTH LCD_WIDTH +#define DISPLAY_HEIGHT LCD_HEIGHT +#define DISPLAY_MOSI LCD_MOSI +#define DISPLAY_SCLK LCD_SCLK +#define DISPLAY_DC LCD_DC +#define DISPLAY_RST LCD_RST +#define DISPLAY_CS LCD_CS +#define DISPLAY_BL static_cast(LCD_BL) +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN DISPLAY_BL +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/lilygo-t-circle-s3/config.json b/main/boards/lilygo-t-circle-s3/config.json new file mode 100644 index 0000000..378dded --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "lilygo-t-circle-s3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c new file mode 100644 index 0000000..25a7867 --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c @@ -0,0 +1,353 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_check.h" + +#include "esp_lcd_gc9d01n.h" + +static const char *TAG = "gc9d01n"; + +static esp_err_t panel_gc9d01n_del(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9d01n_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9d01n_init(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9d01n_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_gc9d01n_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_gc9d01n_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_gc9d01n_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_gc9d01n_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_gc9d01n_disp_on_off(esp_lcd_panel_t *panel, bool off); + +typedef struct{ + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + bool reset_level; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save current value of LCD_CMD_COLMOD register + const gc9d01n_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; +} gc9d01n_panel_t; + +esp_err_t esp_lcd_new_panel_gc9d01n(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel){ + esp_err_t ret = ESP_OK; + gc9d01n_panel_t *gc9d01n = NULL; + gpio_config_t io_conf = {0}; + + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + gc9d01n = (gc9d01n_panel_t *)calloc(1, sizeof(gc9d01n_panel_t)); + ESP_GOTO_ON_FALSE(gc9d01n, ESP_ERR_NO_MEM, err, TAG, "no mem for gc9d01n panel"); + + if (panel_dev_config->reset_gpio_num >= 0){ + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + switch (panel_dev_config->color_space){ + case ESP_LCD_COLOR_SPACE_RGB: + gc9d01n->madctl_val = 0; + break; + case ESP_LCD_COLOR_SPACE_BGR: + gc9d01n->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); + break; + } +#else + switch (panel_dev_config->rgb_endian){ + case LCD_RGB_ENDIAN_RGB: + gc9d01n->madctl_val = 0; + break; + case LCD_RGB_ENDIAN_BGR: + gc9d01n->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb endian"); + break; + } +#endif + + switch (panel_dev_config->bits_per_pixel){ + case 16: // RGB565 + gc9d01n->colmod_val = 0x55; + gc9d01n->fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + gc9d01n->colmod_val = 0x66; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + gc9d01n->fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + gc9d01n->io = io; + gc9d01n->reset_gpio_num = panel_dev_config->reset_gpio_num; + gc9d01n->reset_level = panel_dev_config->flags.reset_active_high; + if (panel_dev_config->vendor_config){ + gc9d01n->init_cmds = ((gc9d01n_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; + gc9d01n->init_cmds_size = ((gc9d01n_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; + } + gc9d01n->base.del = panel_gc9d01n_del; + gc9d01n->base.reset = panel_gc9d01n_reset; + gc9d01n->base.init = panel_gc9d01n_init; + gc9d01n->base.draw_bitmap = panel_gc9d01n_draw_bitmap; + gc9d01n->base.invert_color = panel_gc9d01n_invert_color; + gc9d01n->base.set_gap = panel_gc9d01n_set_gap; + gc9d01n->base.mirror = panel_gc9d01n_mirror; + gc9d01n->base.swap_xy = panel_gc9d01n_swap_xy; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + gc9d01n->base.disp_off = panel_gc9d01n_disp_on_off; +#else + gc9d01n->base.disp_on_off = panel_gc9d01n_disp_on_off; +#endif + *ret_panel = &(gc9d01n->base); + ESP_LOGD(TAG, "new gc9d01n panel @%p", gc9d01n); + + // ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_GC9D01N_VER_MAJOR, ESP_LCD_GC9D01N_VER_MINOR, + // ESP_LCD_GC9D01N_VER_PATCH); + + return ESP_OK; + +err: + if (gc9d01n){ + if (panel_dev_config->reset_gpio_num >= 0){ + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(gc9d01n); + } + return ret; +} + +static esp_err_t panel_gc9d01n_del(esp_lcd_panel_t *panel){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + + if (gc9d01n->reset_gpio_num >= 0){ + gpio_reset_pin(gc9d01n->reset_gpio_num); + } + ESP_LOGD(TAG, "del gc9d01n panel @%p", gc9d01n); + free(gc9d01n); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_reset(esp_lcd_panel_t *panel){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + + // perform hardware reset + if (gc9d01n->reset_gpio_num >= 0){ + gpio_set_level(gc9d01n->reset_gpio_num, gc9d01n->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(gc9d01n->reset_gpio_num, !gc9d01n->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + } + else{ // perform software reset + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(20)); // spec, wait at least 5ms before sending new command + } + + return ESP_OK; +} + +static const gc9d01n_lcd_init_cmd_t vendor_specific_init_default[] = { + // {cmd, { data }, data_size, delay_ms} + // Enable Inter Register + {0xFE, (uint8_t[]){0x00}, 0, 0}, + {0xEF, (uint8_t[]){0x00}, 0, 0}, + {0x80, (uint8_t[]){0xFF}, 1, 0}, + {0x81, (uint8_t[]){0xFF}, 1, 0}, + {0x82, (uint8_t[]){0xFF}, 1, 0}, + {0x84, (uint8_t[]){0xFF}, 1, 0}, + {0x85, (uint8_t[]){0xFF}, 1, 0}, + {0x86, (uint8_t[]){0xFF}, 1, 0}, + {0x87, (uint8_t[]){0xFF}, 1, 0}, + {0x88, (uint8_t[]){0xFF}, 1, 0}, + {0x89, (uint8_t[]){0xFF}, 1, 0}, + {0x8A, (uint8_t[]){0xFF}, 1, 0}, + {0x8B, (uint8_t[]){0xFF}, 1, 0}, + {0x8C, (uint8_t[]){0xFF}, 1, 0}, + {0x8D, (uint8_t[]){0xFF}, 1, 0}, + {0x8E, (uint8_t[]){0xFF}, 1, 0}, + {0x8F, (uint8_t[]){0xFF}, 1, 0}, + {0x3A, (uint8_t[]){0x05}, 1, 0}, + {0xEC, (uint8_t[]){0x01}, 1, 0}, + {0x74, (uint8_t[]){0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00}, 7, 0}, + {0x98, (uint8_t[]){0x3E}, 1, 0}, + {0x99, (uint8_t[]){0x3E}, 1, 0}, + {0xB5, (uint8_t[]){0x0D, 0x0D}, 2, 0}, + {0x60, (uint8_t[]){0x38, 0x0F, 0x79, 0x67}, 4, 0}, + {0x61, (uint8_t[]){0x38, 0x11, 0x79, 0x67}, 4, 0}, + {0x64, (uint8_t[]){0x38, 0x17, 0x71, 0x5F, 0x79, 0x67}, 6, 0}, + {0x65, (uint8_t[]){0x38, 0x13, 0x71, 0x5B, 0x79, 0x67}, 6, 0}, + {0x6A, (uint8_t[]){0x00, 0x00}, 2, 0}, + {0x6C, (uint8_t[]){0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50}, 7, 0}, + {0x6E, (uint8_t[]){0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0x0F, 0x0F, 0x0D, 0x0D, 0x0B, 0x0B, 0x09, 0x09, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x0A, 0x0C, 0x0C, 0x0E, 0x0E, 0x10, 0x10, 0x00, 0x00, 0x02, 0x02, 0x04, 0x04}, 32, 0}, + {0xBF, (uint8_t[]){0x01}, 1, 0}, + {0xF9, (uint8_t[]){0x40}, 1, 0}, + {0x9B, (uint8_t[]){0x3B, 0x93, 0x33, 0x7F, 0x00}, 5, 0}, + {0x7E, (uint8_t[]){0x30}, 1, 0}, + {0x70, (uint8_t[]){0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08}, 6, 0}, + {0x71, (uint8_t[]){0x0D, 0x02, 0x08}, 3, 0}, + {0x91, (uint8_t[]){0x0E, 0x09}, 2, 0}, + {0xC3, (uint8_t[]){0x19, 0xC4, 0x19, 0xC9, 0x3C}, 5, 0}, + {0xF0, (uint8_t[]){0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E}, 6, 0}, + {0xF1, (uint8_t[]){0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F}, 6, 0}, + {0xF2, (uint8_t[]){0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A}, 6, 0}, + {0xF3, (uint8_t[]){0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF}, 6, 0}, + + // {0x20, (uint8_t[]){0x00}, 0, 0}, + {0x36, (uint8_t[]){0x00}, 1, 0}, + {0x11, (uint8_t[]){0x00}, 0, 200}, + {0x29, (uint8_t[]){0x00}, 0, 0}, + {0x2C, (uint8_t[]){0x00}, 0, 20}, +}; + +static esp_err_t panel_gc9d01n_init(esp_lcd_panel_t *panel){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + + // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]){gc9d01n->madctl_val,},1),TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]){gc9d01n->colmod_val,},1),TAG, "send command failed"); + + const gc9d01n_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (gc9d01n->init_cmds){ + init_cmds = gc9d01n->init_cmds; + init_cmds_size = gc9d01n->init_cmds_size; + }else{ + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(gc9d01n_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + for (int i = 0; i < init_cmds_size; i++){ + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd){ + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + gc9d01n->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + gc9d01n->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten){ + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); + } + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + + x_start += gc9d01n->x_gap; + x_end += gc9d01n->x_gap; + y_start += gc9d01n->y_gap; + y_end += gc9d01n->y_gap; + + // define an area of frame memory where MCU can access + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]){(x_start >> 8) & 0xFF,x_start & 0xFF,((x_end - 1) >> 8) & 0xFF,(x_end - 1) & 0xFF,},4),TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]){(y_start >> 8) & 0xFF,y_start & 0xFF,((y_end - 1) >> 8) & 0xFF,(y_end - 1) & 0xFF,},4),TAG, "send command failed"); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * gc9d01n->fb_bits_per_pixel / 8; + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len), TAG, "send color failed"); + + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_invert_color(esp_lcd_panel_t *panel, bool invert_color_data){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + int command = 0; + if (invert_color_data){ + command = LCD_CMD_INVON; + }else{ + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + if (mirror_x){ + gc9d01n->madctl_val |= LCD_CMD_MX_BIT; + }else{ + gc9d01n->madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y){ + gc9d01n->madctl_val |= LCD_CMD_MY_BIT; + }else{ + gc9d01n->madctl_val &= ~LCD_CMD_MY_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]){gc9d01n->madctl_val}, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_swap_xy(esp_lcd_panel_t *panel, bool swap_axes){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + if (swap_axes){ + gc9d01n->madctl_val |= LCD_CMD_MV_BIT; + }else{ + gc9d01n->madctl_val &= ~LCD_CMD_MV_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]){gc9d01n->madctl_val}, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + gc9d01n->x_gap = x_gap; + gc9d01n->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_disp_on_off(esp_lcd_panel_t *panel, bool on_off){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + int command = 0; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + on_off = !on_off; +#endif + + if (on_off){ + command = LCD_CMD_DISPON; + }else{ + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} diff --git a/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.h b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.h new file mode 100644 index 0000000..ec057cc --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.h @@ -0,0 +1,99 @@ +#pragma once + +#include "esp_lcd_panel_vendor.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /* +#include +#include +#include +#include +#include "esp_lcd_gc9d01n.h" + +#define TAG "LilygoTCircleS3Board" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class Cst816x : public I2cDevice { +public: + struct TouchPoint_t { + int num = 0; + int x = -1; + int y = -1; + }; + + Cst816x(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + uint8_t chip_id = ReadReg(0xA7); + ESP_LOGI(TAG, "Get chip ID: 0x%02X", chip_id); + read_buffer_ = new uint8_t[6]; + } + + ~Cst816x() { + delete[] read_buffer_; + } + + void UpdateTouchPoint() { + ReadRegs(0x02, read_buffer_, 6); + tp_.num = read_buffer_[0] & 0x0F; + tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2]; + tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4]; + } + + const TouchPoint_t &GetTouchPoint() { + return tp_; + } + +private: + uint8_t *read_buffer_ = nullptr; + TouchPoint_t tp_; +}; + +class LilygoTCircleS3Board : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Cst816x *cst816d_; + LcdDisplay *display_; + Button boot_button_; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(10); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitI2c(){ + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_config = { + .i2c_port = I2C_NUM_0, + .sda_io_num = TOUCH_I2C_SDA_PIN, + .scl_io_num = TOUCH_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_config, &i2c_bus_)); + } + + void I2cDetect() { + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + } + + static void touchpad_daemon(void *param) { + vTaskDelay(pdMS_TO_TICKS(2000)); + auto &board = (LilygoTCircleS3Board&)Board::GetInstance(); + auto touchpad = board.GetTouchpad(); + bool was_touched = false; + while (1) { + touchpad->UpdateTouchPoint(); + if (touchpad->GetTouchPoint().num > 0){ + // On press + if (!was_touched) { + was_touched = true; + Application::GetInstance().ToggleChatState(); + } + } + // On release + else if (was_touched) { + was_touched = false; + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + vTaskDelete(NULL); + } + + void InitCst816d() { + ESP_LOGI(TAG, "Init CST816x"); + cst816d_ = new Cst816x(i2c_bus_, 0x15); + xTaskCreate(touchpad_daemon, "tp", 2048, NULL, 5, NULL); + } + + void InitSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCLK; + 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)); + } + + void InitGc9d01nDisplay() { + ESP_LOGI(TAG, "Init GC9D01N"); + + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS; + io_config.dc_gpio_num = DISPLAY_DC; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9d01n(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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + + gpio_config_t config; + config.pin_bit_mask = BIT64(DISPLAY_BL); + config.mode = GPIO_MODE_OUTPUT; + config.pull_up_en = GPIO_PULLUP_DISABLE; + config.pull_down_en = GPIO_PULLDOWN_ENABLE; + config.intr_type = GPIO_INTR_DISABLE; +#if SOC_GPIO_SUPPORT_PIN_HYS_FILTER + config.hys_ctrl_mode = GPIO_HYS_SOFT_ENABLE; +#endif + gpio_config(&config); + gpio_set_level(DISPLAY_BL, 0); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + power_save_timer_->WakeUp(); + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto &thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + LilygoTCircleS3Board() : boot_button_(BOOT_BUTTON_GPIO) { + InitializePowerSaveTimer(); + InitI2c(); + InitCst816d(); + I2cDetect(); + InitSpi(); + InitGc9d01nDisplay(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec *GetAudioCodec() override { + static Tcircles3AudioCodec audio_codec( + AUDIO_INPUT_SAMPLE_RATE, + AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_MIC_I2S_GPIO_BCLK, + AUDIO_MIC_I2S_GPIO_WS, + AUDIO_MIC_I2S_GPIO_DATA, + AUDIO_SPKR_I2S_GPIO_BCLK, + AUDIO_SPKR_I2S_GPIO_LRCLK, + AUDIO_SPKR_I2S_GPIO_DATA, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display *GetDisplay() override{ + return display_; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } + + virtual Backlight* GetBacklight() override { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + + Cst816x *GetTouchpad() { + return cst816d_; + } +}; + +DECLARE_BOARD(LilygoTCircleS3Board); diff --git a/main/boards/lilygo-t-circle-s3/pin_config.h b/main/boards/lilygo-t-circle-s3/pin_config.h new file mode 100644 index 0000000..db428ca --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/pin_config.h @@ -0,0 +1,47 @@ +/* + * @Description: None + * @Author: LILYGO_L + * @Date: 2023-08-16 14:24:03 + * @LastEditTime: 2025-01-20 10:11:16 + * @License: GPL 3.0 + */ +#pragma once + +// MAX98357A +#define MAX98357A_BCLK 5 +#define MAX98357A_LRCLK 4 +#define MAX98357A_DATA 6 +#define MAX98357A_SD_MODE 45 + +// MSM261 +#define MSM261_BCLK 7 +#define MSM261_WS 9 +#define MSM261_DATA 8 + +// APA102 +#define APA102_DATA 38 +#define APA102_CLOCK 39 + +// H0075Y002-V0 +#define LCD_WIDTH 160 +#define LCD_HEIGHT 160 +#define LCD_MOSI 17 +#define LCD_SCLK 15 +#define LCD_DC 16 +#define LCD_RST -1 +#define LCD_CS 13 +#define LCD_BL 18 + +// IIC +#define IIC_SDA 11 +#define IIC_SCL 14 + +// CST816D +#define TP_SDA 11 +#define TP_SCL 14 +#define TP_RST -1 +#define TP_INT 12 + +//Rotary Encoder +#define KNOB_DATA_A 47 +#define KNOB_DATA_B 48 diff --git a/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.cc b/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.cc new file mode 100644 index 0000000..68db1cb --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.cc @@ -0,0 +1,146 @@ +#include "tcircles3_audio_codec.h" + +#include +#include +#include +#include + +static const char TAG[] = "Tcircles3AudioCodec"; + +Tcircles3AudioCodec::Tcircles3AudioCodec(int input_sample_rate, int output_sample_rate, + gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data, + bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateVoiceHardware(mic_bclk, mic_ws, mic_data, spkr_bclk, spkr_lrclk, spkr_data); + + gpio_config_t config; + config.pin_bit_mask = BIT64(45); + config.mode = GPIO_MODE_OUTPUT; + config.pull_up_en = GPIO_PULLUP_DISABLE; + config.pull_down_en = GPIO_PULLDOWN_ENABLE; + config.intr_type = GPIO_INTR_DISABLE; +#if SOC_GPIO_SUPPORT_PIN_HYS_FILTER + config.hys_ctrl_mode = GPIO_HYS_SOFT_ENABLE; +#endif + gpio_config(&config); + gpio_set_level(gpio_num_t(45), 0); + ESP_LOGI(TAG, "Tcircles3AudioCodec initialized"); +} + +Tcircles3AudioCodec::~Tcircles3AudioCodec() { + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void Tcircles3AudioCodec::CreateVoiceHardware(gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data) { + + i2s_chan_config_t mic_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(i2s_port_t(0), I2S_ROLE_MASTER); + mic_chan_config.auto_clear = true; // Auto clear the legacy data in the DMA buffer + i2s_chan_config_t spkr_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(i2s_port_t(1), I2S_ROLE_MASTER); + spkr_chan_config.auto_clear = true; // Auto clear the legacy data in the DMA buffer + + ESP_ERROR_CHECK(i2s_new_channel(&mic_chan_config, NULL, &rx_handle_)); + ESP_ERROR_CHECK(i2s_new_channel(&spkr_chan_config, &tx_handle_, NULL)); + + i2s_std_config_t mic_config = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + }, + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg ={ + .mclk = I2S_GPIO_UNUSED, + .bclk = mic_bclk, + .ws = mic_ws, + .dout = I2S_GPIO_UNUSED, + .din = mic_data, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + } + } + }; + + i2s_std_config_t spkr_config = { + .clk_cfg ={ + .sample_rate_hz = static_cast(11025), + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + }, + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg ={ + .mclk = I2S_GPIO_UNUSED, + .bclk = spkr_bclk, + .ws = spkr_lrclk, + .dout = spkr_data, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &mic_config)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &spkr_config)); + ESP_LOGI(TAG, "Voice hardware created"); +} + +void Tcircles3AudioCodec::SetOutputVolume(int volume) { + volume_ = volume; + AudioCodec::SetOutputVolume(volume); +} + +void Tcircles3AudioCodec::EnableInput(bool enable) { + AudioCodec::EnableInput(enable); +} + +void Tcircles3AudioCodec::EnableOutput(bool enable) { + if (enable){ + gpio_set_level(gpio_num_t(45), 1); + }else{ + gpio_set_level(gpio_num_t(45), 0); + } + AudioCodec::EnableOutput(enable); +} + +int Tcircles3AudioCodec::Read(int16_t *dest, int samples){ + if (input_enabled_){ + size_t bytes_read; + i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY); + } + return samples; +} + +int Tcircles3AudioCodec::Write(const int16_t *data, int samples){ + if (output_enabled_){ + size_t bytes_read; + auto output_data = (int16_t *)malloc(samples * sizeof(int16_t)); + for (size_t i = 0; i < samples; i++){ + output_data[i] = (float)data[i] * (float)(volume_ / 100.0); + } + i2s_channel_write(tx_handle_, output_data, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY); + free(output_data); + } + return samples; +} diff --git a/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.h b/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.h new file mode 100644 index 0000000..3c050dc --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.h @@ -0,0 +1,37 @@ +#ifndef _TCIRCLES3_AUDIO_CODEC_H +#define _TCIRCLES3_AUDIO_CODEC_H + +#include "audio_codecs/audio_codec.h" + +#include +#include + +class Tcircles3AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t *data_if_ = nullptr; + const audio_codec_ctrl_if_t *out_ctrl_if_ = nullptr; + const audio_codec_if_t *out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t *in_ctrl_if_ = nullptr; + const audio_codec_if_t *in_codec_if_ = nullptr; + const audio_codec_gpio_if_t *gpio_if_ = nullptr; + + uint32_t volume_ = 70; + + void CreateVoiceHardware(gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data,gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data); + + virtual int Read(int16_t *dest, int samples) override; + virtual int Write(const int16_t *data, int samples) override; + +public: + Tcircles3AudioCodec(int input_sample_rate, int output_sample_rate, + gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data, + bool input_reference); + virtual ~Tcircles3AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _BOX_AUDIO_CODEC_H diff --git a/main/boards/m5stack-core-s3/README.md b/main/boards/m5stack-core-s3/README.md new file mode 100644 index 0000000..be164f4 --- /dev/null +++ b/main/boards/m5stack-core-s3/README.md @@ -0,0 +1,31 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> M5Stack CoreS3 +``` + +**修改 psram 配置:** + +``` +Component config -> ESP PSRAM -> SPI RAM config -> Mode (QUAD/OCT) -> Quad Mode PSRAM +``` + +**编译:** + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/m5stack-core-s3/config.h b/main/boards/m5stack-core-s3/config.h new file mode 100644 index 0000000..0d91f36 --- /dev/null +++ b/main/boards/m5stack-core-s3/config.h @@ -0,0 +1,43 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// M5Stack CoreS3 Board configuration + +#include + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_0 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_33 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_34 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_14 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_13 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_12 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_11 +#define AUDIO_CODEC_AW88298_ADDR AW88298_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/m5stack-core-s3/config.json b/main/boards/m5stack-core-s3/config.json new file mode 100644 index 0000000..bec9ad0 --- /dev/null +++ b/main/boards/m5stack-core-s3/config.json @@ -0,0 +1,11 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "m5stack-core-s3", + "sdkconfig_append": [ + "CONFIG_SPIRAM_MODE_QUAD=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/m5stack-core-s3/cores3_audio_codec.cc b/main/boards/m5stack-core-s3/cores3_audio_codec.cc new file mode 100644 index 0000000..14a5ff7 --- /dev/null +++ b/main/boards/m5stack-core-s3/cores3_audio_codec.cc @@ -0,0 +1,245 @@ +#include "cores3_audio_codec.h" + +#include +#include +#include +#include + + +static const char TAG[] = "CoreS3AudioCodec"; + +CoreS3AudioCodec::CoreS3AudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + uint8_t aw88298_addr, uint8_t es7210_addr, bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Audio Output(Speaker) + audio_codec_i2c_cfg_t i2c_cfg = { + .port = (i2c_port_t)1, + .addr = aw88298_addr, + .bus_handle = i2c_master_handle, + }; + out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(out_ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + aw88298_codec_cfg_t aw88298_cfg = {}; + aw88298_cfg.ctrl_if = out_ctrl_if_; + aw88298_cfg.gpio_if = gpio_if_; + aw88298_cfg.reset_pin = GPIO_NUM_NC; + aw88298_cfg.hw_gain.pa_voltage = 5.0; + aw88298_cfg.hw_gain.codec_dac_voltage = 3.3; + aw88298_cfg.hw_gain.pa_gain = 1; + out_codec_if_ = aw88298_codec_new(&aw88298_cfg); + assert(out_codec_if_ != NULL); + + esp_codec_dev_cfg_t dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = out_codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&dev_cfg); + assert(output_dev_ != NULL); + + // Audio Input(Microphone) + i2c_cfg.addr = es7210_addr; + in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(in_ctrl_if_ != NULL); + + es7210_codec_cfg_t es7210_cfg = {}; + es7210_cfg.ctrl_if = in_ctrl_if_; + es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2 | ES7120_SEL_MIC3; + in_codec_if_ = es7210_codec_new(&es7210_cfg); + assert(in_codec_if_ != NULL); + + dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; + dev_cfg.codec_if = in_codec_if_; + input_dev_ = esp_codec_dev_new(&dev_cfg); + assert(input_dev_ != NULL); + + ESP_LOGI(TAG, "CoreS3AudioCodec initialized"); +} + +CoreS3AudioCodec::~CoreS3AudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void CoreS3AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + ESP_LOGI(TAG, "Audio IOs: mclk: %d, bclk: %d, ws: %d, dout: %d, din: %d", mclk, bclk, ws, dout, din); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + i2s_tdm_config_t tdm_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)input_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .bclk_div = 8, + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3), + .ws_width = I2S_TDM_AUTO_WS_WIDTH, + .ws_pol = false, + .bit_shift = true, + .left_align = false, + .big_endian = false, + .bit_order_lsb = false, + .skip_mask = false, + .total_slot = I2S_TDM_AUTO_SLOT_NUM + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = I2S_GPIO_UNUSED, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void CoreS3AudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void CoreS3AudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 2, + .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + if (input_reference_) { + fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); + } + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), 40.0)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void CoreS3AudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + // Play 16bit 1 channel + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + } + AudioCodec::EnableOutput(enable); +} + +int CoreS3AudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int CoreS3AudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} diff --git a/main/boards/m5stack-core-s3/cores3_audio_codec.h b/main/boards/m5stack-core-s3/cores3_audio_codec.h new file mode 100644 index 0000000..4b034b4 --- /dev/null +++ b/main/boards/m5stack-core-s3/cores3_audio_codec.h @@ -0,0 +1,37 @@ +#ifndef _BOX_AUDIO_CODEC_H +#define _BOX_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include + +class CoreS3AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; + const audio_codec_if_t* out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; + const audio_codec_if_t* in_codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + CoreS3AudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + uint8_t aw88298_addr, uint8_t es7210_addr, bool input_reference); + virtual ~CoreS3AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _BOX_AUDIO_CODEC_H diff --git a/main/boards/m5stack-core-s3/m5stack_core_s3.cc b/main/boards/m5stack-core-s3/m5stack_core_s3.cc new file mode 100644 index 0000000..c74a952 --- /dev/null +++ b/main/boards/m5stack-core-s3/m5stack_core_s3.cc @@ -0,0 +1,375 @@ +#include "wifi_board.h" +#include "cores3_audio_codec.h" +#include "display/lcd_display.h" +#include "application.h" +#include "config.h" +#include "power_save_timer.h" +#include "i2c_device.h" +#include "iot/thing_manager.h" +#include "axp2101.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TAG "M5StackCoreS3Board" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class Pmic : public Axp2101 { +public: + // Power Init + Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) { + uint8_t data = ReadReg(0x90); + data |= 0b10110100; + WriteReg(0x90, data); + WriteReg(0x99, (0b11110 - 5)); + WriteReg(0x97, (0b11110 - 2)); + WriteReg(0x69, 0b00110101); + WriteReg(0x30, 0b111111); + WriteReg(0x90, 0xBF); + WriteReg(0x94, 33 - 5); + WriteReg(0x95, 33 - 5); + } + + void SetBrightness(uint8_t brightness) { + brightness = ((brightness + 641) >> 5); + WriteReg(0x99, brightness); + } +}; + + +class CustomBacklight : public Backlight { +public: + CustomBacklight(Pmic *pmic) : pmic_(pmic) {} + + void SetBrightnessImpl(uint8_t brightness) override { + pmic_->SetBrightness(target_brightness_); + brightness_ = target_brightness_; + } + +private: + Pmic *pmic_; +}; + + +class Aw9523 : public I2cDevice { +public: + // Exanpd IO Init + Aw9523(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x02, 0b00000111); // P0 + WriteReg(0x03, 0b10001111); // P1 + WriteReg(0x04, 0b00011000); // CONFIG_P0 + WriteReg(0x05, 0b00001100); // CONFIG_P1 + WriteReg(0x11, 0b00010000); // GCR P0 port is Push-Pull mode. + WriteReg(0x12, 0b11111111); // LEDMODE_P0 + WriteReg(0x13, 0b11111111); // LEDMODE_P1 + } + + void ResetAw88298() { + ESP_LOGI(TAG, "Reset AW88298"); + WriteReg(0x02, 0b00000011); + vTaskDelay(pdMS_TO_TICKS(10)); + WriteReg(0x02, 0b00000111); + vTaskDelay(pdMS_TO_TICKS(50)); + } + + void ResetIli9342() { + ESP_LOGI(TAG, "Reset IlI9342"); + WriteReg(0x03, 0b10000001); + vTaskDelay(pdMS_TO_TICKS(20)); + WriteReg(0x03, 0b10000011); + vTaskDelay(pdMS_TO_TICKS(10)); + } +}; + +class Ft6336 : public I2cDevice { +public: + struct TouchPoint_t { + int num = 0; + int x = -1; + int y = -1; + }; + + Ft6336(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + uint8_t chip_id = ReadReg(0xA3); + ESP_LOGI(TAG, "Get chip ID: 0x%02X", chip_id); + read_buffer_ = new uint8_t[6]; + } + + ~Ft6336() { + delete[] read_buffer_; + } + + void UpdateTouchPoint() { + ReadRegs(0x02, read_buffer_, 6); + tp_.num = read_buffer_[0] & 0x0F; + tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2]; + tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4]; + } + + inline const TouchPoint_t& GetTouchPoint() { + return tp_; + } + +private: + uint8_t* read_buffer_ = nullptr; + TouchPoint_t tp_; +}; + + +class M5StackCoreS3Board : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Pmic* pmic_; + Aw9523* aw9523_; + Ft6336* ft6336_; + LcdDisplay* display_; + esp_timer_handle_t touchpad_timer_; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(10); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->OnShutdownRequest([this]() { + pmic_->PowerOff(); + }); + 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, &i2c_bus_)); + } + + void I2cDetect() { + uint8_t address; + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + } + + void InitializeAxp2101() { + ESP_LOGI(TAG, "Init AXP2101"); + pmic_ = new Pmic(i2c_bus_, 0x34); + } + + void InitializeAw9523() { + ESP_LOGI(TAG, "Init AW9523"); + aw9523_ = new Aw9523(i2c_bus_, 0x58); + vTaskDelay(pdMS_TO_TICKS(50)); + } + + void PollTouchpad() { + static bool was_touched = false; + static int64_t touch_start_time = 0; + const int64_t TOUCH_THRESHOLD_MS = 500; // 触摸时长阈值,超过500ms视为长按 + + ft6336_->UpdateTouchPoint(); + auto& touch_point = ft6336_->GetTouchPoint(); + + // 检测触摸开始 + if (touch_point.num > 0 && !was_touched) { + was_touched = true; + touch_start_time = esp_timer_get_time() / 1000; // 转换为毫秒 + } + // 检测触摸释放 + else if (touch_point.num == 0 && was_touched) { + was_touched = false; + int64_t touch_duration = (esp_timer_get_time() / 1000) - touch_start_time; + + // 只有短触才触发 + if (touch_duration < TOUCH_THRESHOLD_MS) { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && + !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + } + } + } + + void InitializeFt6336TouchPad() { + ESP_LOGI(TAG, "Init FT6336"); + ft6336_ = new Ft6336(i2c_bus_, 0x38); + + // 创建定时器,20ms 间隔 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + M5StackCoreS3Board* board = (M5StackCoreS3Board*)arg; + board->PollTouchpad(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "touchpad_timer", + .skip_unhandled_events = true, + }; + + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &touchpad_timer_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(touchpad_timer_, 20 * 1000)); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_37; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_36; + 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)); + } + + void InitializeIli9342Display() { + ESP_LOGI(TAG, "Init IlI9342"); + + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_3; + io_config.dc_gpio_num = GPIO_NUM_35; + io_config.spi_mode = 2; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + ESP_LOGD(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_ELEMENT_ORDER_BGR; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + aw9523_->ResetIli9342(); + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, true); + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 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")); + } + +public: + M5StackCoreS3Board() { + InitializePowerSaveTimer(); + InitializeI2c(); + InitializeAxp2101(); + InitializeAw9523(); + I2cDetect(); + InitializeSpi(); + InitializeIli9342Display(); + InitializeIot(); + InitializeFt6336TouchPad(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static CoreS3AudioCodec audio_codec(i2c_bus_, + 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_AW88298_ADDR, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override { + static bool last_discharging = false; + charging = pmic_->IsCharging(); + discharging = pmic_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + + level = pmic_->GetBatteryLevel(); + return true; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } + + virtual Backlight *GetBacklight() override { + static CustomBacklight backlight(pmic_); + return &backlight; + } +}; + +DECLARE_BOARD(M5StackCoreS3Board); diff --git a/main/boards/magiclick-2p4/config.h b/main/boards/magiclick-2p4/config.h new file mode 100644 index 0000000..bc37001 --- /dev/null +++ b/main/boards/magiclick-2p4/config.h @@ -0,0 +1,50 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_11 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_9 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_12 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_4 // pcb v2.4不起作用,适用于2.4A +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_5 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_6 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +//led power +#define BUILTIN_LED_POWER GPIO_NUM_39 // 低电平有效 +#define BUILTIN_LED_POWER_OUTPUT_INVERT true + +#define BUILTIN_LED_NUM 2 +#define BUILTIN_LED_GPIO GPIO_NUM_38 + +#define MAIN_BUTTON_GPIO GPIO_NUM_21 +#define LEFT_BUTTON_GPIO GPIO_NUM_0 +#define RIGHT_BUTTON_GPIO GPIO_NUM_47 + +// display +#define DISPLAY_SDA_PIN GPIO_NUM_15 +#define DISPLAY_SCL_PIN GPIO_NUM_16 +#define DISPLAY_CS_PIN GPIO_NUM_17 +#define DISPLAY_DC_PIN GPIO_NUM_18 +#define DISPLAY_RST_PIN GPIO_NUM_14 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_13 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/magiclick-2p4/config.json b/main/boards/magiclick-2p4/config.json new file mode 100644 index 0000000..f416c2a --- /dev/null +++ b/main/boards/magiclick-2p4/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "magiclick-2p4", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/magiclick-2p4/magiclick_2p4_board.cc b/main/boards/magiclick-2p4/magiclick_2p4_board.cc new file mode 100644 index 0000000..8e0251f --- /dev/null +++ b/main/boards/magiclick-2p4/magiclick_2p4_board.cc @@ -0,0 +1,293 @@ +#include "wifi_board.h" +#include "display/lcd_display.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "led/circular_strip.h" +#include "iot/thing_manager.h" +#include "config.h" +#include "font_awesome_symbols.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include + +#include "../magiclick-2p5/power_manager.h" +#include "power_save_timer.h" + +#define TAG "magiclick_2p4" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class NV3023Display : public SpiLcdDisplay { +public: + NV3023Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) + : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }) { + + DisplayLockGuard lock(this); + // 只需要覆盖颜色相关的样式 + auto screen = lv_disp_get_scr_act(lv_disp_get_default()); + lv_obj_set_style_text_color(screen, lv_color_black(), 0); + + // 设置容器背景色 + lv_obj_set_style_bg_color(container_, lv_color_black(), 0); + + // 设置状态栏背景色和文本颜色 + lv_obj_set_style_bg_color(status_bar_, lv_color_white(), 0); + lv_obj_set_style_text_color(network_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(notification_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(status_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(mute_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(battery_label_, lv_color_black(), 0); + + // 设置内容区背景色和文本颜色 + lv_obj_set_style_bg_color(content_, lv_color_black(), 0); + lv_obj_set_style_border_width(content_, 0, 0); + lv_obj_set_style_text_color(emotion_label_, lv_color_white(), 0); + lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); + } +}; + +class magiclick_2p4 : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button main_button_; + Button left_button_; + Button right_button_; + NV3023Display* display_; + + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_48); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(240, 60, -1); + 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_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeButtons() { + main_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + main_button_.OnPressDown([this]() { + power_save_timer_->WakeUp(); + Application::GetInstance().StartListening(); + }); + main_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + left_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + left_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + + right_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + right_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + } + + void InitializeLedPower() { + // 设置GPIO模式 + gpio_reset_pin(BUILTIN_LED_POWER); + gpio_set_direction(BUILTIN_LED_POWER, GPIO_MODE_OUTPUT); + gpio_set_level(BUILTIN_LED_POWER, BUILTIN_LED_POWER_OUTPUT_INVERT ? 0 : 1); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL_PIN; + 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)); + } + + void InitializeNv3023Display(){ + // esp_lcd_panel_io_handle_t panel_io = nullptr; + // esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片NV3023 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_nv3023(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); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); + display_ = new NV3023Display(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + magiclick_2p4() : + main_button_(MAIN_BUTTON_GPIO), + left_button_(LEFT_BUTTON_GPIO), + right_button_(RIGHT_BUTTON_GPIO) { + InitializeLedPower(); + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeCodecI2c(); + InitializeButtons(); + InitializeSpi(); + InitializeNv3023Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + + } + + virtual Led* GetLed() override { + static CircularStrip led(BUILTIN_LED_GPIO, BUILTIN_LED_NUM); + 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); + } +}; + +DECLARE_BOARD(magiclick_2p4); diff --git a/main/boards/magiclick-2p5/config.h b/main/boards/magiclick-2p5/config.h new file mode 100644 index 0000000..46fc3fa --- /dev/null +++ b/main/boards/magiclick-2p5/config.h @@ -0,0 +1,50 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_11 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_9 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_12 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_4 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_5 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_6 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +//led power +#define BUILTIN_LED_POWER GPIO_NUM_39 // 低电平有效 +#define BUILTIN_LED_POWER_OUTPUT_INVERT true + +#define BUILTIN_LED_NUM 2 +#define BUILTIN_LED_GPIO GPIO_NUM_38 + +#define MAIN_BUTTON_GPIO GPIO_NUM_21 +#define LEFT_BUTTON_GPIO GPIO_NUM_0 +#define RIGHT_BUTTON_GPIO GPIO_NUM_47 + +// display +#define DISPLAY_SDA_PIN GPIO_NUM_16 +#define DISPLAY_SCL_PIN GPIO_NUM_15 +#define DISPLAY_CS_PIN GPIO_NUM_14 +#define DISPLAY_DC_PIN GPIO_NUM_18 +#define DISPLAY_RST_PIN GPIO_NUM_17 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_13 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/magiclick-2p5/config.json b/main/boards/magiclick-2p5/config.json new file mode 100644 index 0000000..6220641 --- /dev/null +++ b/main/boards/magiclick-2p5/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "magiclick-2p5", + "sdkconfig_append": [ + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/magiclick-2p5/magiclick_2p5_board.cc b/main/boards/magiclick-2p5/magiclick_2p5_board.cc new file mode 100644 index 0000000..28c5110 --- /dev/null +++ b/main/boards/magiclick-2p5/magiclick_2p5_board.cc @@ -0,0 +1,312 @@ +#include "wifi_board.h" +#include "display/lcd_display.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "led/circular_strip.h" +#include "iot/thing_manager.h" +#include "config.h" +#include "assets/lang_config.h" +#include "font_awesome_symbols.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "power_manager.h" +#include "power_save_timer.h" + +#define TAG "magiclick_2p5" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class GC9107Display : public SpiLcdDisplay { +public: + GC9107Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) + : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }) { + } +}; + +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb1, (uint8_t[]){0x80}, 1, 0}, + {0xb2, (uint8_t[]){0x27}, 1, 0}, + {0xb3, (uint8_t[]){0x13}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x05}, 1, 0}, + {0xac, (uint8_t[]){0xc8}, 1, 0}, + {0xab, (uint8_t[]){0x0f}, 1, 0}, + {0x3a, (uint8_t[]){0x05}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x08}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xea, (uint8_t[]){0x02}, 1, 0}, + {0xe8, (uint8_t[]){0x2A}, 1, 0}, + {0xe9, (uint8_t[]){0x47}, 1, 0}, + {0xe7, (uint8_t[]){0x5f}, 1, 0}, + {0xc6, (uint8_t[]){0x21}, 1, 0}, + {0xc7, (uint8_t[]){0x15}, 1, 0}, + {0xf0, + (uint8_t[]){0x1D, 0x38, 0x09, 0x4D, 0x92, 0x2F, 0x35, 0x52, 0x1E, 0x0C, + 0x04, 0x12, 0x14, 0x1f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x16, 0x40, 0x1C, 0x54, 0xA9, 0x2D, 0x2E, 0x56, 0x10, 0x0D, + 0x0C, 0x1A, 0x14, 0x1E}, + 14, 0}, + {0xf4, (uint8_t[]){0x00, 0x00, 0xFF}, 3, 0}, + {0xba, (uint8_t[]){0xFF, 0xFF}, 2, 0}, +}; + +class magiclick_2p5 : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button main_button_; + Button left_button_; + Button right_button_; + GC9107Display* display_; + + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_48); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(240, 60, -1); + 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_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeButtons() { + main_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + main_button_.OnPressDown([this]() { + power_save_timer_->WakeUp(); + Application::GetInstance().StartListening(); + }); + main_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + left_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + left_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + + right_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + right_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + } + + void InitializeLedPower() { + // 设置GPIO模式 + gpio_reset_pin(BUILTIN_LED_POWER); + gpio_set_direction(BUILTIN_LED_POWER, GPIO_MODE_OUTPUT); + gpio_set_level(BUILTIN_LED_POWER, BUILTIN_LED_POWER_OUTPUT_INVERT ? 0 : 1); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL_PIN; + 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)); + } + + void InitializeGc9107Display(){ + // esp_lcd_panel_io_handle_t panel_io = nullptr; + // esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片GC9107 + ESP_LOGD(TAG, "Install LCD driver"); + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = &gc9107_vendor_config; + + esp_lcd_new_panel_gc9a01(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); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); + display_ = new GC9107Display(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + magiclick_2p5() : + main_button_(MAIN_BUTTON_GPIO), + left_button_(LEFT_BUTTON_GPIO), + right_button_(RIGHT_BUTTON_GPIO) { + InitializeLedPower(); + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeCodecI2c(); + InitializeButtons(); + InitializeSpi(); + InitializeGc9107Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual Led* GetLed() override { + static CircularStrip led(BUILTIN_LED_GPIO, BUILTIN_LED_NUM); + 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); + } +}; + +DECLARE_BOARD(magiclick_2p5); diff --git a/main/boards/magiclick-2p5/power_manager.h b/main/boards/magiclick-2p5/power_manager.h new file mode 100644 index 0000000..5517a13 --- /dev/null +++ b/main/boards/magiclick-2p5/power_manager.h @@ -0,0 +1,195 @@ +#pragma once +#include +#include + +#include +#include +#include + +#define CHARGING_PIN GPIO_NUM_48 +#define CHARGING_ACTIVE_STATE 0 + + +class PowerManager { +private: + esp_timer_handle_t timer_handle_; + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + + gpio_num_t charging_pin_ = CHARGING_PIN; + std::vector adc_values_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 60; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + + void CheckBatteryStatus() { + // Get charging status + bool new_charging_status = gpio_get_level(charging_pin_) == 0; + // ESP_LOGI("PowerManager", "new_charging_status: %s,is_charging_:%s", new_charging_status?"True":"False",is_charging_?"True":"False"); + if (new_charging_status != is_charging_) { + is_charging_ = new_charging_status; + if (on_charging_status_changed_) { + on_charging_status_changed_(is_charging_); + } + ReadBatteryAdcData(); + return; + } + + + + // 如果电池电量数据不足,则读取电池电量数据 + if (adc_values_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_value; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_6, &adc_value)); + ESP_LOGI("PowerManager", "ADC value: %d ", adc_value); + + + // 将 ADC 值添加到队列中 + adc_values_.push_back(adc_value); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + uint32_t average_adc = 0; + for (auto value : adc_values_) { + average_adc += value; + } + average_adc /= adc_values_.size(); + + // 定义电池电量区间 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = { + {1985, 0}, + {2079, 20}, + {2141, 40}, + {2296, 60}, + {2420, 80}, + {2606, 100} + }; + + // 低于最低值时 + if (average_adc < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_adc >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) { + float ratio = static_cast(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (adc_values_.size() >= kBatteryAdcDataCount) { + bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; + if (new_low_battery_status != is_low_battery_) { + is_low_battery_ = new_low_battery_status; + if (on_low_battery_status_changed_) { + on_low_battery_status_changed_(is_low_battery_); + } + } + } + + ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); + } + +public: + PowerManager(gpio_num_t pin) : charging_pin_(pin) { + // 初始化充电引脚 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << charging_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&io_conf); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + PowerManager* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_6, &chan_config)); + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + bool IsCharging() { + // 检测充电指示引脚 + if(gpio_get_level(charging_pin_) != CHARGING_ACTIVE_STATE) + { + return false; + } + return is_charging_; + } + + bool IsDischarging() { + // 没有区分充电和放电,所以直接返回相反状态 + return !is_charging_; + } + + uint8_t GetBatteryLevel() { + return battery_level_; + } + + void OnLowBatteryStatusChanged(std::function callback) { + on_low_battery_status_changed_ = callback; + } + + void OnChargingStatusChanged(std::function callback) { + on_charging_status_changed_ = callback; + } +}; diff --git a/main/boards/magiclick-c3-v2/config.h b/main/boards/magiclick-c3-v2/config.h new file mode 100644 index 0000000..5609bf6 --- /dev/null +++ b/main/boards/magiclick-c3-v2/config.h @@ -0,0 +1,47 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_8 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_10 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_11 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_3 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_4 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_NUM 1 +#define BUILTIN_LED_GPIO GPIO_NUM_0 + +#define BOOT_BUTTON_GPIO GPIO_NUM_2 + +//battery +#define BUILTIN_BATTERY_GPIO GPIO_NUM_1 + +// display +#define DISPLAY_SDA_PIN GPIO_NUM_13 +#define DISPLAY_SCL_PIN GPIO_NUM_12 +#define DISPLAY_CS_PIN GPIO_NUM_20 +#define DISPLAY_DC_PIN GPIO_NUM_21 +#define DISPLAY_RST_PIN GPIO_NUM_NC + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_9 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/magiclick-c3-v2/config.json b/main/boards/magiclick-c3-v2/config.json new file mode 100644 index 0000000..f3eeb8f --- /dev/null +++ b/main/boards/magiclick-c3-v2/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "magiclick-c3-v2", + "sdkconfig_append": [ + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc b/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc new file mode 100644 index 0000000..8620b11 --- /dev/null +++ b/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc @@ -0,0 +1,253 @@ +#include "wifi_board.h" +#include "display/lcd_display.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "config.h" +#include "power_save_timer.h" +#include "font_awesome_symbols.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "magiclick_c3_v2" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class GC9107Display : public SpiLcdDisplay { +public: + GC9107Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) + : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }) { + + DisplayLockGuard lock(this); + // 只需要覆盖颜色相关的样式 + auto screen = lv_disp_get_scr_act(lv_disp_get_default()); + lv_obj_set_style_text_color(screen, lv_color_black(), 0); + + // 设置容器背景色 + lv_obj_set_style_bg_color(container_, lv_color_black(), 0); + + // 设置状态栏背景色和文本颜色 + lv_obj_set_style_bg_color(status_bar_, lv_color_make(0x1e, 0x90, 0xff), 0); + lv_obj_set_style_text_color(network_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(notification_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(status_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(mute_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(battery_label_, lv_color_black(), 0); + + // 设置内容区背景色和文本颜色 + lv_obj_set_style_bg_color(content_, lv_color_black(), 0); + lv_obj_set_style_border_width(content_, 0, 0); + lv_obj_set_style_text_color(emotion_label_, lv_color_white(), 0); + lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); + } +}; + +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb1, (uint8_t[]){0x80}, 1, 0}, + {0xb2, (uint8_t[]){0x27}, 1, 0}, + {0xb3, (uint8_t[]){0x13}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x05}, 1, 0}, + {0xac, (uint8_t[]){0xc8}, 1, 0}, + {0xab, (uint8_t[]){0x0f}, 1, 0}, + {0x3a, (uint8_t[]){0x05}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x08}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xea, (uint8_t[]){0x02}, 1, 0}, + {0xe8, (uint8_t[]){0x2A}, 1, 0}, + {0xe9, (uint8_t[]){0x47}, 1, 0}, + {0xe7, (uint8_t[]){0x5f}, 1, 0}, + {0xc6, (uint8_t[]){0x21}, 1, 0}, + {0xc7, (uint8_t[]){0x15}, 1, 0}, + {0xf0, + (uint8_t[]){0x1D, 0x38, 0x09, 0x4D, 0x92, 0x2F, 0x35, 0x52, 0x1E, 0x0C, + 0x04, 0x12, 0x14, 0x1f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x16, 0x40, 0x1C, 0x54, 0xA9, 0x2D, 0x2E, 0x56, 0x10, 0x0D, + 0x0C, 0x1A, 0x14, 0x1E}, + 14, 0}, + {0xf4, (uint8_t[]){0x00, 0x00, 0xFF}, 3, 0}, + {0xba, (uint8_t[]){0xFF, 0xFF}, 2, 0}, +}; + +class magiclick_c3_v2 : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button boot_button_; + GC9107Display* display_; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(160); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(10); + + auto codec = GetAudioCodec(); + codec->EnableInput(false); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto codec = GetAudioCodec(); + codec->EnableInput(true); + + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() { + power_save_timer_->WakeUp(); + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL_PIN; + 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(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeGc9107Display(){ + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片GC9107 + ESP_LOGD(TAG, "Install LCD driver"); + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = &gc9107_vendor_config; + + esp_lcd_new_panel_gc9a01(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); + esp_lcd_panel_disp_on_off(panel, true); + display_ = new GC9107Display(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + magiclick_c3_v2() : boot_button_(BOOT_BUTTON_GPIO) { + // 把 ESP32C3 的 VDD SPI 引脚作为普通 GPIO 口使用 + esp_efuse_write_field_bit(ESP_EFUSE_VDD_SPI_AS_GPIO); + + InitializeCodecI2c(); + InitializeButtons(); + InitializePowerSaveTimer(); + InitializeSpi(); + InitializeGc9107Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + 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; + } +}; + +DECLARE_BOARD(magiclick_c3_v2); diff --git a/main/boards/magiclick-c3/config.h b/main/boards/magiclick-c3/config.h new file mode 100644 index 0000000..90cd2b7 --- /dev/null +++ b/main/boards/magiclick-c3/config.h @@ -0,0 +1,47 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_5 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_8 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_10 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_11 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_3 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_4 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_NUM 1 +#define BUILTIN_LED_GPIO GPIO_NUM_0 + +#define BOOT_BUTTON_GPIO GPIO_NUM_2 + +//battery +#define BUILTIN_BATTERY_GPIO GPIO_NUM_1 + +// display +#define DISPLAY_SDA_PIN GPIO_NUM_12 +#define DISPLAY_SCL_PIN GPIO_NUM_13 +#define DISPLAY_CS_PIN GPIO_NUM_20 +#define DISPLAY_DC_PIN GPIO_NUM_21 +#define DISPLAY_RST_PIN GPIO_NUM_NC + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_9 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/magiclick-c3/config.json b/main/boards/magiclick-c3/config.json new file mode 100644 index 0000000..09eb3fd --- /dev/null +++ b/main/boards/magiclick-c3/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "magiclick-c3", + "sdkconfig_append": [ + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/magiclick-c3/magiclick_c3_board.cc b/main/boards/magiclick-c3/magiclick_c3_board.cc new file mode 100644 index 0000000..71a21f0 --- /dev/null +++ b/main/boards/magiclick-c3/magiclick_c3_board.cc @@ -0,0 +1,211 @@ +#include "wifi_board.h" +#include "display/lcd_display.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "config.h" +#include "power_save_timer.h" +#include "font_awesome_symbols.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TAG "magiclick_c3" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class NV3023Display : public SpiLcdDisplay { +public: + NV3023Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) + : SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }) { + + DisplayLockGuard lock(this); + // 只需要覆盖颜色相关的样式 + auto screen = lv_disp_get_scr_act(lv_disp_get_default()); + lv_obj_set_style_text_color(screen, lv_color_black(), 0); + + // 设置容器背景色 + lv_obj_set_style_bg_color(container_, lv_color_black(), 0); + + // 设置状态栏背景色和文本颜色 + lv_obj_set_style_bg_color(status_bar_, lv_color_white(), 0); + lv_obj_set_style_text_color(network_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(notification_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(status_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(mute_label_, lv_color_black(), 0); + lv_obj_set_style_text_color(battery_label_, lv_color_black(), 0); + + // 设置内容区背景色和文本颜色 + lv_obj_set_style_bg_color(content_, lv_color_black(), 0); + lv_obj_set_style_border_width(content_, 0, 0); + lv_obj_set_style_text_color(emotion_label_, lv_color_white(), 0); + lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0); + } +}; + +class magiclick_c3 : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button boot_button_; + NV3023Display* display_; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(160); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(10); + + auto codec = GetAudioCodec(); + codec->EnableInput(false); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto codec = GetAudioCodec(); + codec->EnableInput(true); + + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() { + power_save_timer_->WakeUp(); + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL_PIN; + 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(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeNv3023Display(){ + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + // 液晶屏控制IO初始化 + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS_PIN; + io_config.dc_gpio_num = DISPLAY_DC_PIN; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; + io_config.trans_queue_depth = 10; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); + + // 初始化液晶屏驱动芯片NV3023 + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST_PIN; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_nv3023(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); + esp_lcd_panel_disp_on_off(panel, true); + display_ = new NV3023Display(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + magiclick_c3() : boot_button_(BOOT_BUTTON_GPIO) { + // 把 ESP32C3 的 VDD SPI 引脚作为普通 GPIO 口使用 + esp_efuse_write_field_bit(ESP_EFUSE_VDD_SPI_AS_GPIO); + + InitializeCodecI2c(); + InitializeButtons(); + InitializePowerSaveTimer(); + InitializeSpi(); + InitializeNv3023Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + 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; + } +}; + +DECLARE_BOARD(magiclick_c3); diff --git a/main/boards/movecall-cuican-esp32s3/README.md b/main/boards/movecall-cuican-esp32s3/README.md new file mode 100644 index 0000000..93798bb --- /dev/null +++ b/main/boards/movecall-cuican-esp32s3/README.md @@ -0,0 +1,44 @@ +# ESP32-S3 编译配置指南 + +## 基本命令 + +### 设置目标芯片 + +```bash +idf.py set-target esp32s3 +``` + +### 打开配置界面: + +```bash +idf.py menuconfig +``` +### Flash 配置: + +``` +Serial flasher config -> Flash size -> 8 MB +``` + +### 分区表配置: + +``` +Partition Table -> Custom partition CSV file -> partitions_8M.csv +``` + +### 开发板选择: + +``` +Xiaozhi Assistant -> Board Type -> Movecall CuiCan 璀璨·AI吊坠 +``` + +### 启用编译优化: + +``` +Component config → Compiler options → Optimization Level → Optimize for size (-Os) +``` + +### 编译: + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/movecall-cuican-esp32s3/config.h b/main/boards/movecall-cuican-esp32s3/config.h new file mode 100644 index 0000000..156121d --- /dev/null +++ b/main/boards/movecall-cuican-esp32s3/config.h @@ -0,0 +1,45 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// Movecall CuiCan configuration + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_45 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_41 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_39 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_40 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_42 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_17 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_6 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_7 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_21 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_16 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define DISPLAY_SPI_SCLK_PIN GPIO_NUM_12 +#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_10 +#define DISPLAY_SPI_CS_PIN GPIO_NUM_13 +#define DISPLAY_SPI_DC_PIN GPIO_NUM_14 +#define DISPLAY_SPI_RESET_PIN GPIO_NUM_11 + +#define DISPLAY_SPI_SCLK_HZ (40 * 1000 * 1000) + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/movecall-cuican-esp32s3/config.json b/main/boards/movecall-cuican-esp32s3/config.json new file mode 100644 index 0000000..91ce01c --- /dev/null +++ b/main/boards/movecall-cuican-esp32s3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "movecall-cuican-esp32s3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/movecall-cuican-esp32s3/movecall_cuican_esp32s3.cc b/main/boards/movecall-cuican-esp32s3/movecall_cuican_esp32s3.cc new file mode 100644 index 0000000..8107812 --- /dev/null +++ b/main/boards/movecall-cuican-esp32s3/movecall_cuican_esp32s3.cc @@ -0,0 +1,140 @@ +#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 "iot/thing_manager.h" +#include "led/single_led.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "driver/gpio.h" +#include "driver/spi_master.h" + +#define TAG "MovecallCuicanESP32S3" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + +class MovecallCuicanESP32S3 : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + Button boot_button_; + Display* display_; + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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_)); + } + + // SPI初始化 + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize SPI bus"); + spi_bus_config_t buscfg = GC9A01_PANEL_BUS_SPI_CONFIG(DISPLAY_SPI_SCLK_PIN, DISPLAY_SPI_MOSI_PIN, + DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t)); + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + // GC9A01初始化 + void InitializeGc9a01Display() { + ESP_LOGI(TAG, "Init GC9A01 display"); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_io_spi_config_t io_config = GC9A01_PANEL_IO_SPI_CONFIG(DISPLAY_SPI_CS_PIN, DISPLAY_SPI_DC_PIN, NULL, NULL); + io_config.pclk_hz = DISPLAY_SPI_SCLK_HZ; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &io_handle)); + + ESP_LOGI(TAG, "Install GC9A01 panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN; // Set to -1 if not use + panel_config.rgb_endian = LCD_RGB_ENDIAN_BGR; //LCD_RGB_ENDIAN_RGB; + panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18) + + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, true, false)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); + + display_ = new SpiLcdDisplay(io_handle, panel_handle, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, + { + .text_font = &font_puhui_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_64_init(), + }); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + MovecallCuicanESP32S3() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeCodecI2c(); + InitializeSpi(); + InitializeGc9a01Display(); + InitializeButtons(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual Led* GetLed() override { + static SingleLed led_strip(BUILTIN_LED_GPIO); + return &led_strip; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + + 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; + } +}; + +DECLARE_BOARD(MovecallCuicanESP32S3); diff --git a/main/boards/movecall-moji-esp32s3/1/config.h b/main/boards/movecall-moji-esp32s3/1/config.h new file mode 100644 index 0000000..2fec375 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/1/config.h @@ -0,0 +1,75 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// Movecall Moji configuration + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_6 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_13 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_9 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_5 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_4 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +// QMI8658A姿态传感器配置 - 使用共享I2C引脚 +#define IMU_SENSOR_I2C_SDA_PIN AUDIO_CODEC_I2C_SDA_PIN +#define IMU_SENSOR_I2C_SCL_PIN AUDIO_CODEC_I2C_SCL_PIN +#define QMI8658A_I2C_ADDR 0x6A + +// LED控制引脚 - 使用qiyuan-tech的配置 +#define BUILTIN_LED_GPIO GPIO_NUM_33 // LED_CTRL +#define LED_CTRL_PIN GPIO_NUM_33 + +// 按键配置 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 // BOOT按键 + +// 四路动作按键 - 从qiyuan-tech添加 +#define KEY1_GPIO GPIO_NUM_46 // KEY1 - 音量加 +#define KEY2_GPIO GPIO_NUM_45 // KEY2 - 音量减 +#define KEY3_GPIO GPIO_NUM_17 // KEY3 - 打断/唤醒 (原显示器MOSI引脚) +#define KEY4_GPIO GPIO_NUM_18 // KEY4 - 播放故事(发送文本消息) (原显示器RESET引脚) + +// 音量按键定义 - 标准宏定义 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_46 // 音量加 - 映射到 KEY1 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_45 // 音量减 - 映射到 KEY2 + +// 六路触摸按键引出 - 从qiyuan-tech添加 +#define TOUCH1_GPIO GPIO_NUM_1 // Touch1 +#define TOUCH2_GPIO GPIO_NUM_2 // Touch2 +#define TOUCH3_GPIO GPIO_NUM_3 // Touch3 (原显示器背光引脚) +#define TOUCH4_GPIO GPIO_NUM_7 // Touch4 (原显示器DC引脚) +#define TOUCH5_GPIO GPIO_NUM_8 // Touch5 +#define TOUCH6_GPIO GPIO_NUM_10 // Touch6 + +// USB接口 - 从qiyuan-tech添加 +#define USB_DP_PIN GPIO_NUM_20 // USB_P +#define USB_DN_PIN GPIO_NUM_19 // USB_N + +// 显示器功能已删除 - 设为无效值 +#define DISPLAY_WIDTH 0 +#define DISPLAY_HEIGHT 0 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +// 显示相关引脚设为无效 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_SCLK_PIN GPIO_NUM_NC +#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_NC +#define DISPLAY_SPI_CS_PIN GPIO_NUM_NC +#define DISPLAY_SPI_DC_PIN GPIO_NUM_NC +#define DISPLAY_SPI_RESET_PIN GPIO_NUM_NC +#define DISPLAY_SPI_SCLK_HZ 0 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/movecall-moji-esp32s3/1/config.json b/main/boards/movecall-moji-esp32s3/1/config.json new file mode 100644 index 0000000..ff6dcaf --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/1/config.json @@ -0,0 +1,67 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "movecall-moji-esp32s3", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/16m.csv\"", + "CONFIG_SPIRAM=y", + "CONFIG_SPIRAM_MODE_QUAD=y", + "CONFIG_SPIRAM_SPEED_80M=y", + "CONFIG_USE_AFE_WAKE_WORD=y", + "CONFIG_USE_AUDIO_PROCESSOR=y", + "CONFIG_USE_REALTIME_CHAT=y", + "CONFIG_MODEL_IN_FLASH=y", + "CONFIG_AFE_INTERFACE_V1=y", + + "# 【更多唤醒词选项】", + "# 中文唤醒词:", + "# CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=y # 你好小智 (TTS训练版)", + "# CONFIG_SR_WN_WN9_NIHAOMIAOBAN_TTS2=y # 你好喵伴", + "# CONFIG_SR_WN_WN9_XIAOAITONGXUE=y # 小爱同学", + "# CONFIG_SR_WN_WN9_NIHAOXIAOXIN_TTS=y # 你好小鑫", + "# CONFIG_SR_WN_WN9_XIAOMEITONGXUE_TTS=y # 小美同学", + "# CONFIG_SR_WN_WN9_HIXIAOXING_TTS=y # Hi,小星", + "# CONFIG_SR_WN_WN9_XIAOLONGXIAOLONG_TTS=y # 小龙小龙", + "# CONFIG_SR_WN_WN9_MIAOMIAOTONGXUE_TTS=y # 喵喵同学", + "# CONFIG_SR_WN_WN9_HIMIAOMIAO_TTS=y # Hi,喵喵", + "# CONFIG_SR_WN_WN9_XIAOYUTONGXUE_TTS2=y # 小宇同学", + "# CONFIG_SR_WN_WN9_XIAOMINGTONGXUE_TTS2=y # 小明同学", + "# CONFIG_SR_WN_WN9_XIAOKANGTONGXUE_TTS2=y # 小康同学", + "# CONFIG_SR_WN_WN9_NIHAOXIAOYI_TTS2=y # 你好小益", + "# CONFIG_SR_WN_WN9_NIHAOBAIYING_TTS2=y # 你好百应", + "# CONFIG_SR_WN_WN9_NIHAODONGDONG_TTS2=y # 你好东东", + + "# 英文唤醒词:", + "CONFIG_SR_WN_WN9_HIESP=y", "# Hi,ESP (当前启用)", + "# CONFIG_SR_WN_WN9_HILEXIN=y # Hi,乐鑫", + "# CONFIG_SR_WN_WN9_HIJASON_TTS2=y # Hi,Jason", + "# CONFIG_SR_WN_WN9_ALEXA=y # Alexa", + "# CONFIG_SR_WN_WN9_JARVIS_TTS=y # Jarvis", + "# CONFIG_SR_WN_WN9_COMPUTER_TTS=y # Computer", + "# CONFIG_SR_WN_WN9_HEYWILLOW_TTS=y # Hey,Willow", + "# CONFIG_SR_WN_WN9_SOPHIA_TTS=y # Sophia", + "# CONFIG_SR_WN_WN9_MYCROFT_TTS=y # Mycroft", + "# CONFIG_SR_WN_WN9_HIMFIVE=y # Hi,M Five", + "# CONFIG_SR_WN_WN9_HIJOY_TTS=y # Hi,Joy", + "# CONFIG_SR_WN_WN9_HIWALLE_TTS2=y # Hi,Wall E/Hi,瓦力", + "# CONFIG_SR_WN_WN9_HILILI_TTS=y # Hi,Lily/Hi,莉莉", + "# CONFIG_SR_WN_WN9_HITELLY_TTS=y # Hi,Telly/Hi,泰力", + + "CONFIG_SR_NSN_WEBRTC=y", + "CONFIG_SR_VADN_WEBRTC=y", + "CONFIG_ESP32S3_SPIRAM_SUPPORT=y", + "CONFIG_SPIRAM_BOOT_INIT=y", + "CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096", + "CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=49152", + "CONFIG_SPIRAM_USE_MALLOC=y", + "CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y", + "CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y", + "CONFIG_ESP32S3_DATA_CACHE_64KB=y", + "CONFIG_ESP32S3_DATA_CACHE_8WAYS=y", + "CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/QMI8658A/AttitudeEstimation.c b/main/boards/movecall-moji-esp32s3/QMI8658A/AttitudeEstimation.c new file mode 100644 index 0000000..c572ef5 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/QMI8658A/AttitudeEstimation.c @@ -0,0 +1,403 @@ +#include "AttitudeEstimation.h" + +// 定义日志标签 +static const char *TAG = "AttitudeEstimation.c"; + +// 四元数归一化 +void quaternion_normalize(Quaternion *q) { + float norm = sqrt(q->w * q->w + q->x * q->x + q->y * q->y + q->z * q->z); + q->w /= norm; + q->x /= norm; + q->y /= norm; + q->z /= norm; +} + + +// 初始化 Mahony AHRS 状态 +void MahonyAHRSInit(MahonyAHRSState *state) { + state->quaternion.w = 1.0f; + state->quaternion.x = 0.0f; + state->quaternion.y = 0.0f; + state->quaternion.z = 0.0f; + state->integralError.exInt = 0.0f; + state->integralError.eyInt = 0.0f; + state->integralError.ezInt = 0.0f; +} + +/** + * @brief 对积分误差进行限幅处理,防止积分饱和 + * @param value 待限幅的积分误差值 + * @return 限幅后的积分误差值 + */ +float limitIntegral(float value) { + if (value > INTTT_MAX) return INT_MAX; + if (value < INTTT_MIN) return INT_MIN; + return value; +} + +/** + * @brief 计算四元数在当前角速度下的导数 + * @param gx 陀螺仪在 x 轴上的角速度,单位为弧度/秒 + * @param gy 陀螺仪在 y 轴上的角速度,单位为弧度/秒 + * @param gz 陀螺仪在 z 轴上的角速度,单位为弧度/秒 + * @param q 输入的四元数 + * @param dq 输出的四元数导数 + */ +void computeQuaternionDerivative(float gx, float gy, float gz, Quaternion *q, Quaternion *dq) { + dq->w = 0.5f * (-q->x * gx - q->y * gy - q->z * gz); + dq->x = 0.5f * (q->w * gx + q->y * gz - q->z * gy); + dq->y = 0.5f * (q->w * gy - q->x * gz + q->z * gx); + dq->z = 0.5f * (q->w * gz + q->x * gy - q->y * gx); +} + +/** + * @brief 使用四阶龙格 - 库塔法更新四元数 + * @param gx 陀螺仪在 x 轴上的角速度,单位为弧度/秒 + * @param gy 陀螺仪在 y 轴上的角速度,单位为弧度/秒 + * @param gz 陀螺仪在 z 轴上的角速度,单位为弧度/秒 + * @param dt 时间步长,等于 1 / 采样频率 + * @param q 输入并输出的四元数 + */ +void rk4QuaternionUpdate(float gx, float gy, float gz, float dt, Quaternion *q) { + Quaternion k1, k2, k3, k4; + Quaternion temp_q; + + // 计算 k1,即四元数在当前状态下的导数 + computeQuaternionDerivative(gx, gy, gz, q, &k1); + temp_q.w = q->w + 0.5f * dt * k1.w; + temp_q.x = q->x + 0.5f * dt * k1.x; + temp_q.y = q->y + 0.5f * dt * k1.y; + temp_q.z = q->z + 0.5f * dt * k1.z; + + // 计算 k2,基于 k1 预测的中间状态下的四元数导数 + computeQuaternionDerivative(gx, gy, gz, &temp_q, &k2); + temp_q.w = q->w + 0.5f * dt * k2.w; + temp_q.x = q->x + 0.5f * dt * k2.x; + temp_q.y = q->y + 0.5f * dt * k2.y; + temp_q.z = q->z + 0.5f * dt * k2.z; + + // 计算 k3,基于 k2 预测的中间状态下的四元数导数 + computeQuaternionDerivative(gx, gy, gz, &temp_q, &k3); + temp_q.w = q->w + dt * k3.w; + temp_q.x = q->x + dt * k3.x; + temp_q.y = q->y + dt * k3.y; + temp_q.z = q->z + dt * k3.z; + + // 计算 k4,基于 k3 预测的状态下的四元数导数 + computeQuaternionDerivative(gx, gy, gz, &temp_q, &k4); + + // 使用四阶龙格 - 库塔公式更新四元数 + q->w += (dt / 6.0f) * (k1.w + 2.0f * k2.w + 2.0f * k3.w + k4.w); + q->x += (dt / 6.0f) * (k1.x + 2.0f * k2.x + 2.0f * k3.x + k4.x); + q->y += (dt / 6.0f) * (k1.y + 2.0f * k2.y + 2.0f * k3.y + k4.y); + q->z += (dt / 6.0f) * (k1.z + 2.0f * k2.z + 2.0f * k3.z + k4.z); +} + +/** + * @brief 根据加速度计数据自适应调整比例和积分参数,动态调整范围 + * @param ax 加速度计在 x 轴上的测量值,单位为 m/s² + * @param ay 加速度计在 y 轴上的测量值,单位为 m/s² + * @param az 加速度计在 z 轴上的测量值,单位为 m/s² + * @param Kp 输出的比例参数,用于姿态融合的比例控制 + * @param Ki 输出的积分参数,用于姿态融合的积分控制 + */ +// 全局变量,用于记录上一次的加速度幅值 +float prev_accel_mag = 0.0f; +// 全局标志,用于判断是否为第一次调用函数 +int is_first_call = 1; + +void adaptiveParameterAdjustment(float ax, float ay, float az, float *Kp, float *Ki) { + float accel_mag = sqrt(ax * ax + ay * ay + az * az); + float accel_rate = 0; + float dynamic_factor_kp = 1.0f; // 用于调整 Kp 的动态因子 + float dynamic_factor_ki = 1.0f; // 用于调整 Ki 的动态因子 + + if (is_first_call) { + // 第一次调用函数,不计算加速度变化率,使用默认动态因子 + dynamic_factor_kp = 1.0f; + dynamic_factor_ki = 1.0f; + is_first_call = 0; + } else { + // 计算加速度变化率,使用绝对值 + accel_rate = fabs(accel_mag - prev_accel_mag); + + // 根据加速度变化率判断运动状态 + if (accel_rate > 0.2f) { // 震动非常剧烈,加速度变化很快 + dynamic_factor_kp = 0.1f; // Kp 取初始化值的 0.1 倍 + dynamic_factor_ki = 2.0f; // Ki 在同一数量级内适当取大一点 + } else if (accel_rate > 0.1f) { // 震动剧烈,加速度有明显变化 + dynamic_factor_kp = 0.2f; // Kp 适当减小 + dynamic_factor_ki = 1.5f; // Ki 适当增大 + } else if (accel_rate < 0.03f) { // 震动非常轻微,加速度变化极慢 + dynamic_factor_kp = 1.5f; // Kp 增大 + dynamic_factor_ki = 0.5f; // Ki 减小 + } else if (accel_rate < 0.06f) { // 震动轻微,加速度变化较缓 + dynamic_factor_kp = 1.2f; // Kp 适当增大 + dynamic_factor_ki = 0.7f; // Ki 适当减小 + } else { // 正常运动 + dynamic_factor_kp = 1.0f; + dynamic_factor_ki = 1.0f; + } + } + // 更新上一次的加速度幅值 + prev_accel_mag = accel_mag; + *Kp = KP_INITIAL * dynamic_factor_kp; + *Ki = KI_INITIAL * dynamic_factor_ki; +} + +/** + * @brief Mahony 姿态更新函数,融合加速度计和陀螺仪数据更新姿态 + * @param state Mahony AHRS 状态结构体 + * @param data 传感器数据结构体 + */ +void MahonyAHRSupdate(MahonyAHRSState *state, SensorData *data) { + float recipNorm; + float halfvx, halfvy, halfvz; + float halfex, halfey, halfez; + float Kp, Ki; + float dt = 1.0f / SAMPLE_FREQ; + + // 根据加速度计数据自适应调整比例和积分参数 + adaptiveParameterAdjustment(data->ax, data->ay, data->az, &Kp, &Ki); + + // 如果加速度计数据无效,直接使用陀螺仪数据更新四元数 + if (!((data->ax == 0.0f) && (data->ay == 0.0f) && (data->az == 0.0f))) { + // 归一化加速度计数据,使其模长为 1 + recipNorm = 1.0f / sqrt(data->ax * data->ax + data->ay * data->ay + data->az * data->az); + data->ax *= recipNorm; + data->ay *= recipNorm; + data->az *= recipNorm; + + // 根据当前四元数推算出的重力方向在机体坐标系下的分量 + halfvx = state->quaternion.x * state->quaternion.z - state->quaternion.w * state->quaternion.y; + halfvy = state->quaternion.w * state->quaternion.x + state->quaternion.y * state->quaternion.z; + halfvz = state->quaternion.w * state->quaternion.w - 0.5f + state->quaternion.z * state->quaternion.z; + + // 计算加速度计测量的重力方向与四元数推算的重力方向之间的叉积误差 + halfex = (data->ay * halfvz - data->az * halfvy); + halfey = (data->az * halfvx - data->ax * halfvz); + halfez = (data->ax * halfvy - data->ay * halfvx); + + // 条件积分法抗积分饱和 + if ((state->integralError.exInt < INT_MAX && halfex > 0) || (state->integralError.exInt > INT_MIN && halfex < 0)) { + state->integralError.exInt += halfex * Ki * dt; + } + if ((state->integralError.eyInt < INT_MAX && halfey > 0) || (state->integralError.eyInt > INT_MIN && halfey < 0)) { + state->integralError.eyInt += halfey * Ki * dt; + } + if ((state->integralError.ezInt < INT_MAX && halfez > 0) || (state->integralError.ezInt > INT_MIN && halfez < 0)) { + state->integralError.ezInt += halfez * Ki * dt; + } + + // 对积分误差进行限幅处理 + state->integralError.exInt = limitIntegral(state->integralError.exInt); + state->integralError.eyInt = limitIntegral(state->integralError.eyInt); + state->integralError.ezInt = limitIntegral(state->integralError.ezInt); + + // 使用比例 - 积分控制对陀螺仪的角速度进行补偿 + data->gx += Kp * halfex + state->integralError.exInt; + data->gy += Kp * halfey + state->integralError.eyInt; + data->gz += Kp * halfez + state->integralError.ezInt; + } + + // 使用四阶龙格 - 库塔法更新四元数以提高积分精度 + rk4QuaternionUpdate(data->gx, data->gy, data->gz, dt, &state->quaternion); + + // 归一化更新后的四元数,保证其模长为 1 + recipNorm = 1.0f / sqrt(state->quaternion.w * state->quaternion.w + + state->quaternion.x * state->quaternion.x + + state->quaternion.y * state->quaternion.y + + state->quaternion.z * state->quaternion.z); + state->quaternion.w *= recipNorm; + state->quaternion.x *= recipNorm; + state->quaternion.y *= recipNorm; + state->quaternion.z *= recipNorm; +} + + + + +// 从四元数中提取欧拉角 +void quaternion_to_euler(Quaternion q, float *roll, float *pitch, float *yaw) { + *roll = atan2(2 * (q.w * q.x + q.y * q.z), 1 - 2 * (q.x * q.x + q.y * q.y)); + *pitch = asin(2 * (q.w * q.y - q.z * q.x)); + *yaw = atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z)); + + // 将弧度转换为度 + *roll *= 180.0 / PI; + *pitch *= -180.0 / PI; + *yaw *= 180.0 / PI; +} + + +// 使用加速度计初始化初始姿态 +int Initialize_Attitude_With_Accelerometer(Quaternion *q) { + + float OutData[3]; + + for(int i=0;i<3;i++) + { + OutData[i]=GyrCompensate[i+3]; + } + + // 对数据进行归一化 + float Guideline_norm = calculateAccelerationMagnitude(OutData); + OutData[0] /= Guideline_norm; + OutData[1] /= Guideline_norm; + OutData[2] /= Guideline_norm; + + // 根据平均加速度计算欧拉角 + float roll = atan2(OutData[1], OutData[2]); + float pitch = atan2(-OutData[0], sqrt(OutData[1] * OutData[1] + OutData[2] * OutData[2])); + + float yaw = 0.0f; // 加速度计无法确定偏航角,初始化为 0 + + // 将欧拉角转换为四元数 + float cr = cos(roll * 0.5f); + float sr = sin(roll * 0.5f); + float cp = cos(pitch * 0.5f); + float sp = sin(pitch * 0.5f); + float cy = cos(yaw * 0.5f); + float sy = sin(yaw * 0.5f); + + q->w = cr * cp * cy + sr * sp * sy; + q->x = sr * cp * cy - cr * sp * sy; + q->y = cr * sp * cy + sr * cp * sy; + q->z = cr * cp * sy - sr * sp * cy; + + quaternion_normalize(q); + ESP_LOGE(TAG, "四元数初始化成功!"); + // 返回 1 表示成功完成校准 + return 1; +} + + +// 消息队列句柄 +// QueueHandle_t sensor_data_queue; // 传感器数据队列句柄 +QueueHandle_t quaternion_queue; // 四元数队列句柄 +// 互斥锁句柄 +// SemaphoreHandle_t sensor_queue_mutex; // 传感器数据队列互斥锁句柄 +SemaphoreHandle_t quaternion_queue_mutex; // 四元数队列互斥锁句柄 +// 全局的 MahonyAHRSState 变量 +MahonyAHRSState state; + +// 合并后的任务:获取传感器数据并更新四元数 +void sensor_and_quaternion_task(void *pvParameters) { + float data[6]; + SensorData sensorData; + Quaternion q; + + + while (1) { + + // 获取传感器数据:三轴加速度计单位为G,三轴角速度,单位为dps + QMI8658A_Get_G_DPS(data); + sensorData.ax = data[0] * 9.8; + sensorData.ay = data[1] * 9.8; + sensorData.az = data[2] * 9.8; + sensorData.gx = data[3] * PI / 180.0; + sensorData.gy = data[4] * PI / 180.0; + sensorData.gz = data[5] * PI / 180.0; + + // 根据陀螺仪数据更新四元数 + MahonyAHRSupdate(&state, &sensorData); + + q.w = state.quaternion.w; + q.x = state.quaternion.x; + q.y = state.quaternion.y; + q.z = state.quaternion.z; + + // 获取四元数队列互斥锁 + if (xSemaphoreTake(quaternion_queue_mutex, pdMS_TO_TICKS(4)) != pdTRUE) { + ESP_LOGE(TAG, "在合并任务中获取四元数队列互斥锁失败"); + continue; + } + // 将四元数发送到消息队列,队列满时覆盖旧数据 + if (xQueueOverwrite(quaternion_queue, &q) != pdPASS) { + ESP_LOGE(TAG, "在合并任务中覆盖四元数队列失败"); + } + // 释放四元数队列互斥锁 + if (xSemaphoreGive(quaternion_queue_mutex) != pdTRUE) { + ESP_LOGE(TAG, "在合并任务中释放四元数队列互斥锁失败"); + } + + + } +} + +// 任务3:将四元数转换为欧拉角并打印 +void euler_angle_print_task(void *pvParameters) { + Quaternion q; + float roll, pitch, yaw; + + TickType_t xLastWakeTime; + vTaskDelay(pdMS_TO_TICKS(1000)); + const TickType_t xFrequency = pdMS_TO_TICKS(100); // 100ms 的时间间隔 + // 初始化上一次唤醒时间 + xLastWakeTime = xTaskGetTickCount(); + + + while (1) { + // 获取四元数队列互斥锁 + if (xSemaphoreTake(quaternion_queue_mutex, pdMS_TO_TICKS(40)) != pdTRUE) { + ESP_LOGE(TAG, "在欧拉角打印任务中获取四元数队列互斥锁失败"); + continue; + } + // 从消息队列接收四元数 + if (xQueueReceive(quaternion_queue, &q, pdMS_TO_TICKS(40)) != pdPASS) { + ESP_LOGE(TAG, "在欧拉角打印任务中从四元数队列接收数据失败"); + // 释放四元数队列互斥锁 + if (xSemaphoreGive(quaternion_queue_mutex) != pdTRUE) { + ESP_LOGE(TAG, "在欧拉角打印任务中释放四元数队列互斥锁失败"); + } + continue; + } + // 释放四元数队列互斥锁 + if (xSemaphoreGive(quaternion_queue_mutex) != pdTRUE) { + ESP_LOGE(TAG, "在欧拉角打印任务中释放四元数队列互斥锁失败"); + } + + // // 从四元数中提取欧拉角 + // quaternion_to_euler(q, &roll, &pitch, &yaw); + + // // 输出融合后的姿态角 + // printf("%.2f,%.2f,%.2f\n", roll, pitch, yaw); + printf("%.6f,%.6f,%.6f,%.6f\n", q.w,q.x,q.y,q.z); + // 等待直到下一个周期 + vTaskDelayUntil(&xLastWakeTime, xFrequency); + } +} + +void app_QMI8658A() { + LED_Init(); + + // 创建消息队列 + quaternion_queue = xQueueCreate(1, sizeof(Quaternion)); + + // 创建互斥锁 + quaternion_queue_mutex = xSemaphoreCreateMutex(); + + if (quaternion_queue == NULL || quaternion_queue_mutex == NULL) { + ESP_LOGE(TAG, "创建队列或互斥锁失败"); + return; + } + Quaternion q; + + if (!Initialize_Attitude_With_Accelerometer(&q)) { + ESP_LOGE(TAG, "使用加速度计初始化姿态失败"); + return; + } + MahonyAHRSInit(&state); + state.quaternion.w = q.w; + state.quaternion.x = q.x; + state.quaternion.y = q.y; + state.quaternion.z = q.z; + + // 创建任务并固定到 CPU0 + if (xTaskCreatePinnedToCore(sensor_and_quaternion_task, "传感器和四元数任务", 2*2048, NULL, 5, NULL, 0) != pdPASS) { + ESP_LOGE(TAG, "创建传感器和四元数任务失败"); + } + if (xTaskCreatePinnedToCore(euler_angle_print_task, "欧拉角打印任务", 2*2048, NULL, 3, NULL, 0) != pdPASS) { + ESP_LOGE(TAG, "创建欧拉角打印任务失败"); + } +} diff --git a/main/boards/movecall-moji-esp32s3/QMI8658A/AttitudeEstimation.h b/main/boards/movecall-moji-esp32s3/QMI8658A/AttitudeEstimation.h new file mode 100644 index 0000000..093fdf7 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/QMI8658A/AttitudeEstimation.h @@ -0,0 +1,117 @@ +#ifndef ATTITUDE_ESTIMATION_H +#define ATTITUDE_ESTIMATION_H +#include +#include +#include "QMI8658A.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "LED.h" + +#define PI 3.14159265358979323846 + +// 初始比例参数,用于姿态融合时的比例控制 +#define KP_INITIAL 1.5f +// 初始积分参数,用于姿态融合时的积分控制 +#define KI_INITIAL 0.005f +// 传感器数据的采样频率,单位为 Hz +#define SAMPLE_FREQ SAMPLERATE +// 积分误差的上限值,防止积分饱和 +#define INTTT_MAX 1.0f +// 积分误差的下限值,防止积分饱和 +#define INTTT_MIN -1.0f + + +// 四元数结构体 +typedef struct { + float w; + float x; + float y; + float z; +} Quaternion; + + + +// 定义积分误差结构体 +typedef struct { + float exInt; + float eyInt; + float ezInt; +} IntegralError; + +// 定义传感器数据结构体 +typedef struct { + float gx; // 陀螺仪 x 轴角速度 + float gy; // 陀螺仪 y 轴角速度 + float gz; // 陀螺仪 z 轴角速度 + float ax; // 加速度计 x 轴测量值 + float ay; // 加速度计 y 轴测量值 + float az; // 加速度计 z 轴测量值 +} SensorData; + +// 定义 Mahony AHRS 状态结构体 +typedef struct { + Quaternion quaternion; + IntegralError integralError; +} MahonyAHRSState; + + + + +// 初始化 Mahony AHRS 状态 +void MahonyAHRSInit(MahonyAHRSState *state); + +/** + * @brief 对积分误差进行限幅处理,防止积分饱和 + * @param value 待限幅的积分误差值 + * @return 限幅后的积分误差值 + */ +float limitIntegral(float value); + +/** + * @brief 计算四元数在当前角速度下的导数 + * @param gx 陀螺仪在 x 轴上的角速度,单位为弧度/秒 + * @param gy 陀螺仪在 y 轴上的角速度,单位为弧度/秒 + * @param gz 陀螺仪在 z 轴上的角速度,单位为弧度/秒 + * @param q 输入的四元数 + * @param dq 输出的四元数导数 + */ +void computeQuaternionDerivative(float gx, float gy, float gz, Quaternion *q, Quaternion *dq); + +/** + * @brief 使用四阶龙格 - 库塔法更新四元数 + * @param gx 陀螺仪在 x 轴上的角速度,单位为弧度/秒 + * @param gy 陀螺仪在 y 轴上的角速度,单位为弧度/秒 + * @param gz 陀螺仪在 z 轴上的角速度,单位为弧度/秒 + * @param dt 时间步长,等于 1 / 采样频率 + * @param q 输入并输出的四元数 + */ +void rk4QuaternionUpdate(float gx, float gy, float gz, float dt, Quaternion *q); + +/** + * @brief 根据加速度计数据自适应调整比例和积分参数,动态调整范围 + * @param ax 加速度计在 x 轴上的测量值,单位为 m/s² + * @param ay 加速度计在 y 轴上的测量值,单位为 m/s² + * @param az 加速度计在 z 轴上的测量值,单位为 m/s² + * @param Kp 输出的比例参数,用于姿态融合的比例控制 + * @param Ki 输出的积分参数,用于姿态融合的积分控制 + */ +void adaptiveParameterAdjustment(float ax, float ay, float az, float *Kp, float *Ki); + +/** + * @brief Mahony 姿态更新函数,融合加速度计和陀螺仪数据更新姿态 + * @param state Mahony AHRS 状态结构体 + * @param data 传感器数据结构体 + */ +void MahonyAHRSupdate(MahonyAHRSState *state, SensorData *data); + + +// 从四元数中提取欧拉角 +void quaternion_to_euler(Quaternion q, float *roll, float *pitch, float *yaw); + +// 使用加速度计初始化初始姿态 +int Initialize_Attitude_With_Accelerometer(Quaternion *q); + +void app_QMI8658A() ; + +#endif \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/QMI8658A/IIC.c b/main/boards/movecall-moji-esp32s3/QMI8658A/IIC.c new file mode 100644 index 0000000..c60a280 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/QMI8658A/IIC.c @@ -0,0 +1,143 @@ +#include "IIC.h" +#include "driver/i2c.h" +#include "esp_log.h" + +// 日志标签,用于输出日志信息 +#define TAG "I2C操作" + +// 辅助函数:构建 I2C 命令句柄,用于指定设备地址和寄存器地址 +static i2c_cmd_handle_t build_i2c_cmd(uint8_t dev_addr, uint8_t reg_addr) { + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(cmd, reg_addr, true); + return cmd; +} + +// 辅助函数:执行 I2C 命令句柄并释放资源 +static esp_err_t execute_i2c_cmd(i2c_cmd_handle_t cmd) { + esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS); + i2c_cmd_link_delete(cmd); + return ret; +} + +/** + * @brief 初始化 I2C 主设备 + * @return esp_err_t 初始化结果,ESP_OK 表示成功,其他值表示失败 + */ +esp_err_t i2c_master_init(void) { + int i2c_master_port = I2C_MASTER_NUM; + i2c_config_t conf = { + .mode = I2C_MODE_MASTER, // 设置为 I2C 主设备模式 + .sda_io_num = I2C_MASTER_SDA_IO, // 指定 SDA 引脚 + .scl_io_num = I2C_MASTER_SCL_IO, // 指定 SCL 引脚 + .sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SDA 引脚的上拉电阻 + .scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SCL 引脚的上拉电阻 + .master.clk_speed = I2C_MASTER_FREQ_HZ, // 设置 I2C 时钟频率 + }; + // 配置 I2C 参数 + i2c_param_config(i2c_master_port, &conf); + // 安装 I2C 驱动 + return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0); +} + +void i2c_scan(void) { + esp_err_t err; + i2c_cmd_handle_t cmd; + ESP_LOGI(TAG, "开始扫描 I2C 总线..."); + for (int addr = 0; addr < 128; addr++) { + cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true); + i2c_master_stop(cmd); + err = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS); + if (err == ESP_OK) { + ESP_LOGI(TAG, "在地址 0x%02X 发现设备", addr); + } + i2c_cmd_link_delete(cmd); + } + ESP_LOGI(TAG, "扫描完成。"); +} + +/** + * @brief 从 I2C 设备读取单个字节数据的函数 + * @param dev_addr I2C 设备的 7 位地址 + * @param reg_addr 要读取数据的寄存器地址 + * @param data 用于存储读取到的单个字节数据的指针 + * @return esp_err_t 操作结果,ESP_OK 表示成功,其他值表示失败 + */ +static esp_err_t my_i2c_master_read_byte(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data) { + i2c_cmd_handle_t cmd = build_i2c_cmd(dev_addr, reg_addr); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true); + i2c_master_read_byte(cmd, data, I2C_MASTER_NACK); + i2c_master_stop(cmd); + return execute_i2c_cmd(cmd); +} + + + +/* + * 功能:从 8 位地址的寄存器读取一个字节数据 + * 参数: + * addr:寄存器的内部地址 + * *Data:数据存储地址 + * 返回值:(1 = 成功, 0 = 失败) + */ +unsigned char i2cread(unsigned char addr, unsigned char *Data) { + esp_err_t ret = my_i2c_master_read_byte(Device_Address, addr, Data); + return (ret == ESP_OK) ? 1 : 0; +} + +/** + * @brief 从 Device_Address 的指定寄存器地址开始读取多个字节的数据 + * @param addr I2C 设备(Device_Address)内部寄存器的起始地址 + * @param length 要读取的数据字节长度 + * @param Data 用于存储读取到的数据的缓冲区地址 + * @return unsigned char 操作状态,1 表示读取成功,0 表示读取失败 + */ +unsigned char i2creads(uint8_t addr, uint8_t length, uint8_t *Data) { + i2c_cmd_handle_t cmd = build_i2c_cmd(Device_Address, addr); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (Device_Address << 1) | I2C_MASTER_READ, true); + + if (length > 1) { + i2c_master_read(cmd, Data, length - 1, I2C_MASTER_ACK); + } + i2c_master_read_byte(cmd, Data + length - 1, I2C_MASTER_NACK); + + i2c_master_stop(cmd); + esp_err_t ret = execute_i2c_cmd(cmd); + return (ret == ESP_OK) ? 1 : 0; +} + +/** + * @brief 向 8 位地址的寄存器写入一个字节数据 + * @param addr 寄存器的内部地址 + * @param Data 要写入的数据 + * @return unsigned char 操作结果,1 表示成功,0 表示失败 + */ +unsigned char i2cwrite(uint8_t addr, uint8_t Data) { + i2c_cmd_handle_t cmd = build_i2c_cmd(Device_Address, addr); + i2c_master_write_byte(cmd, Data, true); + i2c_master_stop(cmd); + esp_err_t ret = execute_i2c_cmd(cmd); + return (ret == ESP_OK) ? 1 : 0; +} + +/** + * @brief 向 Device_Address 的指定寄存器地址开始写入多个字节的数据 + * @param addr I2C 设备(Device_Address)内部寄存器的起始地址 + * @param length 要写入的数据字节长度 + * @param Data 要写入的数据缓冲区地址 + * @return unsigned char 操作状态,1 表示写入成功,0 表示写入失败 + */ +unsigned char i2cwrites(uint8_t addr, uint8_t length, const uint8_t *Data) { + i2c_cmd_handle_t cmd = build_i2c_cmd(Device_Address, addr); + for (uint8_t i = 0; i < length; i++) { + i2c_master_write_byte(cmd, Data[i], true); + } + i2c_master_stop(cmd); + esp_err_t ret = execute_i2c_cmd(cmd); + return (ret == ESP_OK) ? 1 : 0; +} \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/QMI8658A/IIC.h b/main/boards/movecall-moji-esp32s3/QMI8658A/IIC.h new file mode 100644 index 0000000..8cb539f --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/QMI8658A/IIC.h @@ -0,0 +1,92 @@ +#ifndef IIC_H +#define IIC_H + +#include +#include "driver/i2c.h" +#include "esp_log.h" + +// 设备地址 +#define Device_Address 0x6B + + +// I2C 主设备配置宏定义 +// I2C 主设备时钟线连接的 GPIO 引脚号 +#define I2C_MASTER_SCL_IO GPIO_NUM_4 +// I2C 主设备数据线连接的 GPIO 引脚号 +#define I2C_MASTER_SDA_IO GPIO_NUM_3 +// I2C 主设备使用的 I2C 端口号 +#define I2C_MASTER_NUM I2C_NUM_0 +// I2C 主设备的时钟频率 +#define I2C_MASTER_FREQ_HZ 400000 +// I2C 主设备不使用发送缓冲区 +#define I2C_MASTER_TX_BUF_DISABLE 0 +// I2C 主设备不使用接收缓冲区 +#define I2C_MASTER_RX_BUF_DISABLE 0 + +/** + * @brief 初始化 I2C 主设备 + * + * 该函数用于对 I2C 主设备进行初始化配置,包括设置 I2C 模式、引脚、上拉电阻和时钟频率等, + * 并安装 I2C 驱动。 + * + * @return esp_err_t 初始化结果,ESP_OK 表示成功,其他值表示失败 + */ +esp_err_t i2c_master_init(void); + +/** + * @brief 向 8 位地址的寄存器写入一个字节数据 + * + * 该函数通过 I2C 总线向指定地址的寄存器写入一个字节的数据。 + * + * @param addr 寄存器的内部地址 + * @param Data 要写入的数据 + * @return unsigned char 操作结果,1 表示成功,0 表示失败 + */ +unsigned char i2cwrite(unsigned char addr, unsigned char Data); + +/** + * @brief 向 8 位地址的寄存器写入多个字节数据 + * + * 该函数通过 I2C 总线向指定地址的寄存器开始写入指定长度的字节数据。 + * + * @param addr I2C 设备内部寄存器地址 + * @param length 要写入的数据长度 + * @param Data 要写入的数据的地址 + * @return unsigned char 操作状态,1 表示成功,0 表示失败 + */ +unsigned char i2cwrites(uint8_t addr, uint8_t length, const uint8_t *Data); + +/** + * @brief 从 8 位地址的寄存器读取一个字节数据 + * + * 该函数通过 I2C 总线从指定地址的寄存器读取一个字节的数据。 + * + * @param addr I2C 设备内部寄存器地址 + * @param Data 存储读取数据的地址 + * @return unsigned char 操作状态,1 表示成功,0 表示失败 + */ +unsigned char i2cread(unsigned char addr, unsigned char *Data); + +/** + * @brief 从 8 位地址的寄存器读取多个字节数据 + * + * 该函数通过 I2C 总线从指定地址的寄存器开始读取指定长度的字节数据。 + * + * @param addr I2C 设备内部寄存器地址 + * @param length 要读取的数据长度 + * @param Data 存储读取数据的地址 + * @return unsigned char 操作状态,1 表示成功,0 表示失败 + */ +unsigned char i2creads(unsigned char addr, unsigned char length, unsigned char *Data); + + +/** + * @brief 扫描 I2C 总线上的设备 + * + * 该函数会遍历 I2C 地址范围(0 - 127),尝试与每个地址的设备进行通信, + * 若通信成功则表示该地址存在设备,并输出相应的日志信息。 + * + */ +void i2c_scan(void); + +#endif // IIC_H \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/QMI8658A/QMI8658A.c b/main/boards/movecall-moji-esp32s3/QMI8658A/QMI8658A.c new file mode 100644 index 0000000..6f8fbae --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/QMI8658A/QMI8658A.c @@ -0,0 +1,1006 @@ +#include "QMI8658A.h" + +// 定义日志标签 +static const char *TAG = "QMI8658A.c"; + + +// 定义一个数组存储所有命令信息 +CommandInfo commandInfos[] = { + {"CTRL_CMD_ACK", 0x00, "Ctrl9", "确认。主机向QMI8658确认,以结束协议。"}, + {"CTRL_CMD_RST_FIFO", 0x04, "Ctrl9", "从主机重置FIFO。"}, + {"CTRL_CMD_REQ_FIFO", 0x05, "Ctrl9R", "从设备获取FIFO数据。"}, + {"CTRL_CMD_WRITE_WOM_SETTING", 0x08, "WCtrl9", "设置并启用运动唤醒(WoM)功能。"}, + {"CTRL_CMD_ACCEL_HOST_DELTA_OFFSET", 0x09, "WCtrl9", "更改加速度计偏移量。"}, + {"CTRL_CMD_GYRO_HOST_DELTA_OFFSET", 0x0A, "WCtrl9", "更改陀螺仪偏移量。"}, + {"CTRL_CMD_CONFIGURE_TAP", 0x0C, "WCtrl9", "配置敲击检测。"}, + {"CTRL_CMD_CONFIGURE_PEDOMETER", 0x0D, "WCtrl9", "配置计步器。"}, + {"CTRL_CMD_CONFIGURE_MOTION", 0x0E, "WCtrl9", "配置任意运动/静止/显著运动检测。"}, + {"CTRL_CMD_RESET_PEDOMETER", 0x0F, "WCtrl9", "重置计步器步数。"}, + {"CTRL_CMD_COPY_USID", 0x10, "Ctrl9R", "将USID和固件版本复制到UI寄存器。"}, + {"CTRL_CMD_SET_RPU", 0x11, "WCtrl9", "配置IO上拉电阻。"}, + {"CTRL_CMD_AHB_CLOCK_GATING", 0x12, "WCtrl9", "内部AHB时钟门控开关。"}, + {"CTRL_CMD_ON_DEMAND_CALIBRATION", 0xA2, "WCtrl9", "对陀螺仪进行按需校准。"}, + {"CTRL_CMD_APPLY_GYRO_GAINS", 0xAA, "WCtrl9", "恢复保存的陀螺仪增益。"} +}; + +// 实现函数,通过枚举获取对应的命令信息结构体 +CommandInfo getCommandInfo(CommandEnum cmd) { + return commandInfos[cmd]; +} + +/*使用示例: + // 通过枚举获取命令信息并打印 + CommandInfo info = getCommandInfo(CTRL_CMD_REQ_FIFO_ENUM); + printf("Command Name: %s\n", info.commandName); + printf("CTRL9 Command Value: 0x%X\n", info.ctrl9CommandValue); + printf("Protocol Type: %s\n", info.protocolType); + printf("Description: %s\n", info.description); + */ + + +/** + * @brief 对加速度计进行自检操作 + * + * 此函数通过一系列的I2C操作对加速度计进行自检,包括禁用传感器、设置输出数据速率、 + * 等待特定状态位变化、读取自检结果并判断是否正常。 + * + * @param 无 + * + * @return uint8_t + * - 1: 加速度计自检正常 + * - 0: 加速度计自检异常或在自检过程中出现I2C通信错误 + */ +uint8_t Acc_Self_Test() +{ + uint8_t data = 0; + + // 1. 禁用传感器 + // 向寄存器CTRL7写入0x00以禁用传感器 + if (!i2cwrite(CTRL7, 0x00)) + { + // 若写入失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "发送禁用传感器命令失败!"); + return 0; + } + + // 2. 设置合适的加速度计输出数据速率4G,896.8Hz + // 向寄存器CTRL2写入0x93以设置加速度计输出数据速率为4G,896.8Hz + if (!i2cwrite(CTRL2, 0x93)) + { + // 若写入失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "设置合适的加速度计输出数据速率失败!"); + return 0; + } + + // 3. 等待STATUSINT第0位为1 + // 初始化data为0x00 + data = 0x00; + // 循环读取STATUSINT寄存器,直到第0位为1 + while ((data & 0x01) == 0) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 4. 将CTRL2.aST(第7位)设为0 + // 读取CTRL2寄存器的值到data + if (!i2cread(CTRL2, &data)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取CTRL2失败!"); + return 0; + } + // 将data的第7位清零 + data &= 0x7F; + // 将修改后的值写回CTRL2寄存器 + if (!i2cwrite(CTRL2, data)) + { + // 若写入失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "设置CTRL2.aST(第7位)设为0失败!"); + return 0; + } + + // 5. 等待STATUSINT第0位为0 + // 初始化data为0xFF + data = 0xFF; + // 此处逻辑有误,应改为(data & 0x01) != 0,原逻辑(data | 0xFE) == 0不可能成立 + while ((data & 0x01) != 0) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 6. 读取加速度计自检结果 + // 定义一个长度为6的数组datas用于存储自检结果 + unsigned char datas[6] = {}; + // 从地址0x51开始连续读取6个字节的数据到datas数组 + if (!i2creads(0x51, 6, datas)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取加速度计自检结果失败!"); + return 0; + } + + // 判断结果绝对值是否高于200mg + int16_t dVxData = 0, dVyData = 0, dVzData = 0; + // 组合低字节和高字节得到完整的x轴数据 + dVxData = (datas[1] << 8) | datas[0]; + // 组合低字节和高字节得到完整的y轴数据 + dVyData = (datas[3] << 8) | datas[2]; + // 组合低字节和高字节得到完整的z轴数据 + dVzData = (datas[5] << 8) | datas[4]; + + // 判断x、y、z轴数据的绝对值是否低于200mg + if (fabs(dVxData * 0.5) < 200 || fabs(dVyData * 0.5) < 200 || fabs(dVzData * 0.5) < 200) + { + // 若有任何一个轴的数据绝对值低于200mg,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取加速度计自检结果为:异常!"); + return 0; + } + // 若所有轴的数据绝对值都高于200mg,记录日志表示自检正常并返回1 + ESP_LOGE(TAG, "读取加速度计自检结果为:正常!"); + return 1; +} + + +/** + * @brief 对陀螺仪进行自检操作 + * + * 该函数通过一系列I2C通信步骤对陀螺仪进行自检,包含禁用传感器、设置特定寄存器位、 + * 等待状态位变化、读取自检结果并判断是否符合正常范围。 + * + * @param 无 + * @return uint8_t + * - 1: 陀螺仪自检正常 + * - 0: 陀螺仪自检异常或在自检过程中出现I2C通信错误 + */ +uint8_t Gyr_Self_Test() +{ + uint8_t data = 0; + + // 1. 禁用传感器 + // 向寄存器CTRL7写入0x00,以禁用陀螺仪传感器 + if (!i2cwrite(CTRL7, 0x00)) + { + // 若写入失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "发送禁用传感器命令失败!"); + return 0; + } + + // 2. 将gST位设为1(CTRL3第7位 = 1’b1) + // 从寄存器CTRL3读取数据到变量data + if (!i2cread(CTRL3, &data)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取CTRL3失败!"); + return 0; + } + // 将data的第7位设置为1 + data |= 0x80; + // 将修改后的数据写回寄存器CTRL3 + if (!i2cwrite(CTRL3, data)) + { + // 若写入失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "设置CTRL3.gST(第7位)设为1失败!"); + return 0; + } + + // 3. 等待STATUSINT第0位为1 + // 初始化data为0x00 + data = 0x00; + // 循环读取STATUSINT寄存器,直到其第0位变为1 + while ((data & 0x01) == 0) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 4. 将gST位设为0(CTRL3第7位 = 0’b1) + // 再次从寄存器CTRL3读取数据到变量data + if (!i2cread(CTRL3, &data)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取CTRL3失败!"); + return 0; + } + // 将data的第7位清零 + data &= 0x7F; + // 将修改后的数据写回寄存器CTRL3 + if (!i2cwrite(CTRL3, data)) + { + // 若写入失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "设置CTRL3.gST(第7位)设为0失败!"); + return 0; + } + + // 5. 等待STATUSINT第0位为0 + // 初始化data为0xFF + data = 0xFF; + // 此处原逻辑(data | 0xFE) == 0有误,应改为(data & 0x01) != 0,以等待STATUSINT第0位为0 + while ((data & 0x01) != 0) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 6. 读取陀螺仪自检结果 + // 定义一个长度为6的数组datas用于存储自检结果 + unsigned char datas[6] = {}; + // 从地址0x51开始连续读取6个字节的数据到datas数组 + if (!i2creads(0x51, 6, datas)) + { + // 此处注释有误,应是读取陀螺仪自检结果,若读取失败,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取陀螺仪自检结果失败!"); + return 0; + } + + // 判断结果绝对值是否高于300dps + int16_t dVxData = 0, dVyData = 0, dVzData = 0; + // 组合低字节和高字节得到完整的x轴数据 + dVxData = (datas[1] << 8) | datas[0]; + // 组合低字节和高字节得到完整的y轴数据 + dVyData = (datas[3] << 8) | datas[2]; + // 组合低字节和高字节得到完整的z轴数据 + dVzData = (datas[5] << 8) | datas[4]; + + // 判断x、y、z轴数据经转换后的绝对值是否低于300dps + if (fabs(dVxData * 62.5 / 1000) < 300 || fabs(dVyData * 62.5 / 1000) < 300 || fabs(dVzData * 62.5 / 1000) < 300) + { + // 若有任何一个轴的数据绝对值低于300dps,记录错误日志并返回0表示自检失败 + ESP_LOGE(TAG, "读取陀螺仪自检结果为:异常!"); + return 0; + } + // 若所有轴的数据绝对值都高于300dps,记录日志表示自检正常并返回1 + ESP_LOGE(TAG, "读取陀螺仪自检结果为:正常!"); + return 1; +} + +/** + * @brief 对陀螺仪进行按需校准(COD, Calibration On Demand)操作 + * + * 此函数用于对陀螺仪进行按需校准,校准过程中建议将设备置于安静环境, + * 否则校准可能失败并报错。函数通过一系列I2C通信操作完成校准流程, + * 包括禁用传感器、发送校准指令、等待校准完成、确认校准结果、检查校准状态, + * 最后开启加速度计和陀螺仪的同步采样模式。 + * + * @param 无 + * @return uint8_t + * - 1: 陀螺仪校准成功 + * - 0: 陀螺仪校准失败或在校准过程中出现I2C通信错误 + */ +uint8_t Gyr_COD() +{ + uint8_t data = 0; + + // 1. 禁用传感器 + // 向寄存器CTRL7写入0x00,以禁用传感器,为后续校准操作做准备 + if (!i2cwrite(CTRL7, 0x00)) + { + // 若写入失败,记录错误日志并返回0表示校准失败 + ESP_LOGE(TAG, "发送禁用传感器命令失败!"); + return 0; + } + + // 2. 通过CTRL9命令发出CTRL_CMD_ON_DEMAND_CALIBRATION(0xA2)指令 + // 向寄存器CTRL9写入0xA2,启动陀螺仪的按需校准操作 + if (!i2cwrite(CTRL9, 0xA2)) + { + // 若写入失败,记录错误日志并返回0表示校准失败 + ESP_LOGE(TAG, "发送CTRL_CMD_ON_DEMAND_CALIBRATION(0xA2)指令失败!"); + return 0; + } + + // 3. 等待约1.5秒,让QMI8658A完成CTRL9命令 + // 延时1500毫秒,确保设备有足够时间执行校准命令 + vTaskDelay(pdMS_TO_TICKS(1500)); + + // 4. 等待STATUSINT第7位为1 + // 初始化data为0x00 + data = 0x00; + // 循环读取STATUSINT寄存器,直到其第7位变为1,表示校准操作开始 + while (((data >> 7) & 0x01) == 0) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示校准失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 5. 向CTRL9寄存器写入CTRL_CMD_ACK(0x00)来确认 + // 向寄存器CTRL9写入0x00,确认校准操作开始 + if (!i2cwrite(CTRL9, 0x00)) + { + // 若写入失败,记录错误日志并返回0表示校准失败 + ESP_LOGE(TAG, "发送CTRL_CMD_ACK(0x00)指令失败!"); + return 0; + } + + // 6. 等待STATUSINT第7位为0 + // 初始化data为0xFF + data = 0xFF; + // 循环读取STATUSINT寄存器,直到其第7位变为0,表示校准操作完成 + while (((data >> 7) & 0x01) == 1) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示校准失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 7. 读取COD_STATUS寄存器(0x46),检查COD实施的结果/状态 + // 从寄存器COD_STATUS读取校准结果 + if (!i2cread(COD_STATUS, &data)) + { + // 若读取失败,记录错误日志并返回0表示校准失败 + ESP_LOGE(TAG, "读取COD_STATUS失败!"); + return 0; + } + // 若data不为0,表示校准失败 + if (data) + { + // 记录错误日志,显示错误类型 + ESP_LOGE(TAG, "陀螺仪校准失败,错误类型:%X", data); + return 0; + } + + // 8. 开启加速度计和陀螺仪,同步采样模式 + // 向寄存器CTRL7写入0x83,开启加速度计和陀螺仪的同步采样模式 + if (!i2cwrite(CTRL7, 0x83)) + { + // 若写入失败,记录错误日志并返回0表示校准失败 + ESP_LOGE(TAG, "启加速度计和陀螺仪,同步采样模式失败!"); + return 0; + } + + // 9. 校准成功 + // 记录日志表示陀螺仪校准成功,并返回1 + ESP_LOGE(TAG, "陀螺仪校准成功!"); + return 1; +} + + +// 存储陀螺仪校准值 +float GyrCompensate[6]; + +/** + * @brief 初始化QMI8658A传感器 + * + * 该函数用于对QMI8658A传感器进行初始化操作,包括复位传感器、配置通讯方式和中断引脚、 + * 进行加速度计和陀螺仪的自检、配置传感器参数、使用锁定机制、开启传感器同步采样模式、 + * 进行陀螺仪自带校准和手动校准等步骤。 + * + * @param 无 + * @return int + * - 1: 传感器初始化成功 + * - 0: 传感器初始化失败,可能是某个步骤的I2C通信出错或自检、校准失败 + */ +int QMI8658A_Init(void) +{ + uint8_t data = 0; + + // 1. 复位QMI8658A传感器 + // 向复位寄存器RESET写入0xB0,触发传感器复位操作 + if (!i2cwrite(RESET, 0xB0)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "发送复位命令失败!"); + return 0; + } + // 延时100毫秒,等待复位操作完成 + vTaskDelay(pdMS_TO_TICKS(100)); + + // 读取复位状态 + // 从dQY_L寄存器读取复位状态数据到变量data + if (!i2cread(dQY_L, &data)) + { + // 若读取失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "读取复位状态失败!"); + return 0; + } + + // 检查复位状态 + // 判断读取到的复位状态数据是否为0x80,若不是则表示复位失败 + if (data != 0x80) + { + // 记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "复位失败!"); + return 0; + } + + // 2. 配置通讯方式和中断引脚 + // 向寄存器CTRL1写入0x60,配置传感器的通讯方式和中断引脚 + if (!i2cwrite(CTRL1, 0x60)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "配置通讯方式和中断引脚失败!"); + return 0; + } + + // 3. 加速度计自检 + // 调用Acc_Self_Test函数进行加速度计自检 + if (!Acc_Self_Test()) + { + // 若自检失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "加速度计自检失败!"); + return 0; + } + + // 4. 陀螺仪自检 + // 调用Gyr_Self_Test函数进行陀螺仪自检 + if (!Gyr_Self_Test()) + { + // 若自检失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "陀螺仪自检失败!"); + return 0; + } + + // 5. 配置加速度计 + // 向寄存器CTRL2写入0x33,禁用加速度自检,设置量程为16G,输出数据速率为896.8Hz + if (!i2cwrite(CTRL2, 0x33)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "配置加速度计失败!"); + return 0; + } + + // 6. 配置陀螺仪 + // 向寄存器CTRL3写入0x73,禁用陀螺仪自检,设置量程为2048dps,输出数据速率为896.8Hz + if (!i2cwrite(CTRL3, 0x73)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "配置陀螺仪计失败!"); + return 0; + } + + // 7. 配置低通滤波器 + // 向寄存器CTRL5写入0x35,配置低通滤波器为3.63% + if (!i2cwrite(CTRL5, 0x35)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "配置低通滤波器失败!"); + return 0; + } + + // 8. 使用锁定机制 + // 8.1 禁用内部AHB时钟 + // 向CAL1_L寄存器写入0x01 + if (!i2cwrite(CAL1_L, 0x01)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "CAL1_L寄存器写入失败!"); + return 0; + } + // 在CTRL9协议中写入0x12(CTRL_CMD_AHB_CLOCK_GATING) + if (!i2cwrite(CTRL9, 0x12)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "发送CTRL_CMD_ON_DEMAND_CALIBRATION(0x12)指令失败!"); + return 0; + } + // 等待10毫秒,让QMI8658A完成CTRL9命令 + vTaskDelay(pdMS_TO_TICKS(10)); + + // 8.2 等待STATUSINT第7位为1 + // 初始化data为0x00 + data = 0x00; + // 循环读取STATUSINT寄存器,直到其第7位变为1 + while (((data >> 7) & 0x01) == 0) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 8.3 向CTRL9寄存器写入CTRL_CMD_ACK(0x00)来确认 + // 向寄存器CTRL9写入0x00,确认操作 + if (!i2cwrite(CTRL9, 0x00)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "发送CTRL_CMD_ACK(0x00)指令失败!"); + return 0; + } + + // 8.4 等待STATUSINT第7位为0 + // 初始化data为0xFF + data = 0xFF; + // 循环读取STATUSINT寄存器,直到其第7位变为0 + while (((data >> 7) & 0x01) == 1) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 9. 开启加速度计和陀螺仪,同步采样模式 + // 向寄存器CTRL7写入0x83,开启加速度计和陀螺仪的同步采样模式 + if (!i2cwrite(CTRL7, 0x83)) + { + // 若写入失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "启加速度计和陀螺仪,同步采样模式失败!"); + return 0; + } + // 延时10毫秒,等待模式开启完成 + vTaskDelay(pdMS_TO_TICKS(10)); + + // 10. 陀螺仪自带校准 + // 调用Gyr_COD函数进行陀螺仪自带校准 + if (!Gyr_COD()) + { + // 若校准失败,记录错误日志并返回0表示初始化失败 + ESP_LOGE(TAG, "陀螺仪校准失败!"); + return 0; + } + // 延时100毫秒,等待校准完成 + vTaskDelay(pdMS_TO_TICKS(100)); + + // 11. 陀螺仪手动校准 + // 循环调用calibrationGYR函数进行陀螺仪手动校准,直到校准成功 + while (!calibration_ACC_GYR(GyrCompensate)) + { + } + + // 12. 初始化成功 + // 记录日志表示传感器初始化成功,并返回1 + ESP_LOGE(TAG, "初始化成功!"); + return 1; +} + + +/** + * @brief 读取QMI8658A六轴传感器数据 + * + * 该函数用于读取QMI8658A六轴传感器的数据,在读取前会先检查数据的可用性和锁定状态, + * 确保数据有效后再进行读取操作,并将读取到的原始数据存储到传入的数组中。 + * + * @param DATA 指向一个长度为6的int16_t类型数组的指针,用于存储读取到的六轴传感器原始数据, + * 数组元素依次为AX、AY、AZ、GX、GY、GZ + * @return int + * - 1: 数据读取成功 + * - 0: 读取过程中出现错误,如读取状态寄存器失败或读取传感器数据寄存器失败 + */ +int QMI8658A_ReadData(int16_t *DATA) +{ + uint8_t data = 0; + + // 1. 判断数据是否可用(Avail) + // 循环读取STATUSINT寄存器,直到其第0位为1,表示数据可用 + while ((data & 0x01) != 1) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示读取失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 2. 判断数据是否锁定(Locked) + // 重置data为0 + data = 0; + // 循环读取STATUSINT寄存器,直到其第1位为1,表示数据已锁定 + while (((data >> 1) & 0x01) != 1) + { + // 读取STATUSINT寄存器的值到data + if (!i2cread(STATUSINT, &data)) + { + // 若读取失败,记录错误日志并返回0表示读取失败 + ESP_LOGE(TAG, "读取STATUSINT状态失败!"); + return 0; + } + } + + // 3. 读取传感器数据寄存器 + // 定义一个长度为12的数组datas,用于存储从传感器数据寄存器读取的数据 + uint8_t datas[12] = {}; + // 从地址A_XYZ开始连续读取12个字节的数据到datas数组 + if (!i2creads(A_XYZ, 12, datas)) + { + // 此处注释有误,应是读取传感器数据失败,若读取失败,记录错误日志并返回0表示读取失败 + ESP_LOGE(TAG, "读取传感器数据失败!"); + return 0; + } + + // 4. 组合数据 + // 将读取到的低字节和高字节数据组合成完整的16位数据,并存储到DATA数组中 + DATA[0] = (datas[1] << 8) | datas[0]; // AX + DATA[1] = (datas[3] << 8) | datas[2]; // AY + DATA[2] = (datas[5] << 8) | datas[4]; // AZ + DATA[3] = (datas[7] << 8) | datas[6]; // GX + DATA[4] = (datas[9] << 8) | datas[8]; // GY + DATA[5] = (datas[11] << 8) | datas[10]; // GZ + + // 5. 返回读取成功标志 + return 1; +} + +/** + * @brief 将QMI8658A传感器的原始数据根据加速度计和陀螺仪的量程进行转换,并补偿陀螺仪数据 + * + * 该函数接收QMI8658A传感器的原始数据、加速度计和陀螺仪的量程作为输入, + * 根据不同的量程将原始数据转换为实际的物理量值。加速度计数据转换为以g为单位, + * 陀螺仪数据转换为以dps(度每秒)为单位,并对陀螺仪数据进行补偿。 + * + * @param InData 指向包含原始传感器数据的数组的指针,数组长度应为6, + * 前3个元素为加速度计数据(AX, AY, AZ),后3个元素为陀螺仪数据(GX, GY, GZ) + * @param OutData 指向用于存储转换后数据的数组的指针,数组长度应为6, + * 前3个元素存储转换后的加速度计数据,后3个元素存储转换后的陀螺仪数据 + * @param accelRange 加速度计的量程,合法值为2, 4, 8, 16(单位:g) + * @param gyroRange 陀螺仪的量程,合法值为16, 32, 64, 128, 256, 512, 1024, 2048(单位:dps) + * @return 无 + */ +void QMI8658A_ConvertData(int16_t *InData, float *OutData, int accelRange, int gyroRange) +{ + float accelFactor, compensate; + + // 1. 计算加速度计转换因子 + // 根据加速度计量程选择合适的转换因子 + switch (accelRange) + { + case 2: // ±2g + accelFactor = 2.0f * 2 / 65536; + break; + case 4: // ±4g + accelFactor = 4.0f * 2 / 65536; + break; + case 8: // ±8g + accelFactor = 8.0f * 2 / 65536; + break; + case 16: // ±16g + accelFactor = 16.0f * 2 / 65536; + break; + default: + accelFactor = 0; + break; + } + + // 2. 转换加速度计数据 + // 将原始加速度计数据乘以转换因子,得到以g为单位的加速度计数据 + for (int i = 0; i < 3; i++) + { + OutData[i] = InData[i] * accelFactor; + } + + float gyroFactor; + + // 3. 计算陀螺仪转换因子 + // 根据陀螺仪量程选择合适的转换因子 + switch (gyroRange) + { + case 16: // ±16dps + gyroFactor = 16.0f * 2 / 65536; + break; + case 32: // ±32dps + gyroFactor = 32.0f * 2 / 65536; + break; + case 64: // ±64dps + gyroFactor = 64.0f * 2 / 65536; + break; + case 128: // ±128dps + gyroFactor = 128.0f * 2 / 65536; + break; + case 256: // ±256dps + gyroFactor = 256.0f * 2 / 65536; + break; + case 512: // ±512dps + gyroFactor = 512.0f * 2 / 65536; + break; + case 1024: // ±1024dps + gyroFactor = 1024.0f * 2 / 65536; + break; + case 2048: // ±2048dps + gyroFactor = 2048.0f * 2 / 65536; + break; + default: + gyroFactor = 0; + break; + } + + // 4. 转换并补偿陀螺仪数据 + // 将原始陀螺仪数据乘以转换因子,并减去对应的补偿值,得到以dps为单位的陀螺仪数据 + for (int i = 3; i < 6; i++) + { + switch (i) + { + case 3: + compensate = GyrCompensate[3]; + break; + case 4: + compensate = GyrCompensate[4]; + break; + case 5: + compensate = GyrCompensate[5]; + break; + default: + // 如果 i 不是 3、4、5,可以在这里添加默认处理逻辑 + break; + } + OutData[i] = InData[i] * gyroFactor - compensate; + } +} + +/** + * @brief 获取单位为g的三轴加速度计和单位为dps的三轴陀螺仪数据 + * + * 该函数通过调用QMI8658A_ReadData函数读取传感器的原始数据, + * 再调用QMI8658A_ConvertData函数将原始数据转换为以g为单位的加速度计数据 + * 和以dps为单位的陀螺仪数据,并将转换后的数据存储到传入的数组中。 + * + * @param OutData 指向一个长度为6的float类型数组的指针,用于存储转换后的六轴传感器数据, + * 数组元素依次为AX、AY、AZ、GX、GY、GZ,单位分别为g和dps + * @return 无 + */ +void QMI8658A_Get_G_DPS(float *OutData) +{ + // 1. 定义用于存储原始数据的数组 + int16_t data[6]; + + // 2. 读取原始数据 + // 调用QMI8658A_ReadData函数读取传感器的原始数据 + QMI8658A_ReadData(data); + + // 3. 转换数据 + // 调用QMI8658A_ConvertData函数将原始数据转换为以g和dps为单位的数据 + QMI8658A_ConvertData(data, OutData, ACCRANGE, GYRRANGE); +} + +/** + * @brief 计算加速度的模长 + * + * 该函数接收一个包含三轴加速度数据的数组,通过计算三个轴加速度值的平方和的平方根, + * 得到加速度的模长。 + * + * @param OutData 指向一个长度至少为 3 的 float 类型数组的指针,数组前三个元素依次为 X、Y、Z 轴的加速度值,单位为 g + * @return float 加速度的模长,单位为 g + */ +float calculateAccelerationMagnitude(float *OutData) +{ + return (float)sqrt(OutData[0] * OutData[0] + OutData[1] * OutData[1] + OutData[2] * OutData[2]); +} + +/** + * @brief 比较函数,用于 qsort + * + * 该函数是一个用于 qsort 函数的比较函数,用于对 float 类型的数据进行排序。 + * 它会比较两个 float 类型的值,并根据它们的大小关系返回相应的结果。 + * + * @param a 指向第一个 float 类型数据的指针 + * @param b 指向第二个 float 类型数据的指针 + * @return int + * - 若 *a > *b,返回 1 + * - 若 *a <= *b,返回 -1 + */ +int compareFloat(const void *a, const void *b) +{ + return (*(float *)a - *(float *)b) > 0 ? 1 : -1; +} + +/** + * @brief 计算平均值的辅助函数 + * + * 该函数用于计算陀螺仪在一段时间内采集的多个数据的平均值。具体做法是, + * 先将采集到的陀螺仪AX、AY、AZ、GX、GY、GZ 六个轴数据分别复制到临时数组中并排序,然后选取中间的 + * USED_DATA_COUNT 条数据计算平均值,最终将结果存储到 OutData 数组中。 + * + * @param DATA 一个二维 float 类型数组,大小为 [MIN_COLLECTION_COUNT][6], + * 存储了陀螺仪在 MIN_COLLECTION_COUNT 次采集过程中AX、AY、AZ、GX、GY、GZ 六个轴的数据,单位为 g和dps + * @param OutData 指向一个长度至少为 6 的 float 类型数组的指针,用于存储计算得到的陀螺仪AX、AY、AZ、GX、GY、GZ轴数据的平均值,单位为 g和dps + * @return 无 + */ +void calculateAverages(float DATA[MIN_COLLECTION_COUNT][6], float *OutData) { + // 动态分配内存用于存储排序后的数据 + float **sortedData = (float **)malloc(6 * sizeof(float *)); + if (sortedData == NULL) { + ESP_LOGE(TAG, "内存分配失败"); + return; + } + + for (int j = 0; j < 6; j++) { + sortedData[j] = (float *)malloc(MIN_COLLECTION_COUNT * sizeof(float)); + if (sortedData[j] == NULL) { + // 释放已分配的内存 + for (int k = 0; k < j; k++) { + free(sortedData[k]); + } + free(sortedData); + ESP_LOGE(TAG, "内存分配失败"); + return; + } + } + + // 复制数据到临时数组 + for (int i = 0; i < MIN_COLLECTION_COUNT; i++) { + for (int j = 0; j < 6; j++) { + sortedData[j][i] = DATA[i][j]; + } + } + + // 对数据分别排序 + for (int j = 0; j < 6; j++) { + qsort(sortedData[j], MIN_COLLECTION_COUNT, sizeof(float), compareFloat); + } + + // 计算中间 USED_DATA_COUNT 条数据的起始索引 + int startIndex = (MIN_COLLECTION_COUNT - USED_DATA_COUNT) / 2; + + // 计算中间 USED_DATA_COUNT 条数据的总和并计算平均值 + for (int j = 0; j < 6; j++) { + float sum = 0; + for (int i = startIndex; i < startIndex + USED_DATA_COUNT; i++) { + sum += sortedData[j][i]; + } + OutData[j] = sum / USED_DATA_COUNT; + } + + // 释放动态分配的内存 + for (int j = 0; j < 6; j++) { + free(sortedData[j]); + } + free(sortedData); +} + + +/** + * @brief 校准陀螺仪和加速度计,等待收集至少 500 次静止状态下的数据,对数据排序后取中间 200 条计算平均值。 + * + * 该函数会不断读取六轴传感器的数据,计算加速度的模长,判断设备是否处于静止状态。 + * 当收集到至少 500 次静止状态下的数据后,对陀螺仪的 AX、AY、AZ、GX、GY、GZ 六个轴数据分别排序, + * 取排序后中间的 200 条数据计算平均值,并将结果存储在 OutData 数组中。 + * + * @param[out] OutData 用于存储校准后陀螺仪六个轴( AX、AY、AZ、GX、GY、GZ)的平均值,数组长度至少为 6。 + * @return uint8_t 校准结果状态码: + * - 0:表示读取六轴数据失败、传入参数无效或者还未收集满 500 次静止数据。 + * - 1:表示成功收集 500 次静止数据并完成陀螺仪校准。 + */ +uint8_t calibration_ACC_GYR(float *OutData) { + // 检查传入的 OutData 指针是否为 NULL + if (OutData == NULL) { + ESP_LOGE(TAG, "传入的 OutData 指针为 NULL,无法进行校准。"); + return 0; + } + + // 用于存储从传感器读取的原始六轴数据,数组长度为 6,分别对应 AX、AY、AZ、GX、GY、GZ + int16_t rawData[6]; + // 用于存储转换后的六轴数据,同样数组长度为 6 + float convertedData[6], AM = 0; + // 用于存储上一次计算得到的加速度模长,初始值为 0 + float OldAM = 0; + // 动态分配内存,用于存储至少 500 次静止状态下的陀螺仪数据(AX、AY、AZ、GX、GY、GZ) + float (*DATA)[6] = (float (*)[6])malloc(MIN_COLLECTION_COUNT * sizeof(float[6])); + if (DATA == NULL) { + ESP_LOGE(TAG, "内存分配失败,无法进行校准。"); + return 0; + } + // 计数器,记录当前已经收集到的静止数据的次数,初始值为 0 + int i = 0; + + // 若还未收集满 500 次静止数据,则继续收集 + while (i < MIN_COLLECTION_COUNT) { + // 调用 QMI8658A_ReadData 函数读取六轴数据 + if (!QMI8658A_ReadData(rawData)) { + // 若读取失败,打印错误信息,释放内存并返回 0 表示失败 + ESP_LOGE(TAG, "读取六轴数据失败,无法继续处理。"); + free(DATA); + return 0; + } + + // 调用 QMI8658A_ConvertData 函数将原始数据进行转换,转换结果存储在 convertedData 数组中 + QMI8658A_ConvertData(rawData, convertedData, ACCRANGE, GYRRANGE); + + // 计算加速度的模长,存储在 AM 变量中 + AM = calculateAccelerationMagnitude(convertedData); + + // 判断当前加速度模长与上一次的差值是否小于阈值,若小于则认为处于静止状态 + if (fabs(AM - OldAM) < STATIONARY_THRESHOLD) { + // 保存数据(AX、AY、AZ、GX、GY、GZ) + DATA[i][0] = convertedData[0]; + DATA[i][1] = convertedData[1]; + DATA[i][2] = convertedData[2]; + DATA[i][3] = convertedData[3]; + DATA[i][4] = convertedData[4]; + DATA[i][5] = convertedData[5]; + // 计数器加 1,表示又收集到一次静止数据 + i++; + } else { + // 若不处于静止状态,更新 OldAM 为当前的加速度模长 + OldAM = AM; + } + } + + // 已经收集到至少 500 次静止数据,计算陀螺仪平均值 + calculateAverages(DATA, OutData); + + // 释放动态分配的内存 + free(DATA); + + // 打印校准结果,输出 GX、GY、GZ 轴的平均值 + ESP_LOGI(TAG, "陀螺仪校准完成,AX 平均值: %.2f, AY 平均值: %.2f, AZ 平均值: %.2f,GX 平均值: %.2f, GY 平均值: %.2f, GZ 平均值: %.2f", OutData[0], OutData[1], OutData[2],OutData[3], OutData[4], OutData[5]); + + // 返回 1 表示成功完成陀螺仪校准 + return 1; +} + + +/** + * @brief 读取QMI8658A传感器的六轴数据,进行转换并打印转换后的数据 + * + * 该函数会调用 QMI8658A_ReadData 函数从 QMI8658A 传感器读取原始的六轴数据, + * 若读取成功,再调用 QMI8658A_ConvertData 函数根据传入的加速度计和陀螺仪量程 + * 对原始数据进行转换,最后使用 ESP_LOGE 函数将转换后的加速度计和陀螺仪数据打印输出。 + * 如果读取数据失败,会打印错误信息并提前返回。 + * + * @param accelRange 加速度计的量程,合法值为 2, 4, 8, 16(单位:g) + * @param gyroRange 陀螺仪的量程,合法值为 16, 32, 64, 128, 256, 512, 1024, 2048(单位:dps) + * + * @return 无 + */ +void QMI8658A_ReadConvertAndPrint() { + + static int i=0; + + // 用于存储从传感器读取的原始六轴数据 + int16_t rawData[6]; + // 用于存储转换后的六轴数据 + float convertedData[6],AM=0; + + // 调用 QMI8658A_ReadData 函数读取六轴数据 + if (!QMI8658A_ReadData(rawData)) { + // 若读取失败,打印错误信息并返回 + ESP_LOGE(TAG, "读取六轴数据失败,无法继续处理。"); + return; + } + + // 调用 QMI8658A_ConvertData 函数将原始数据进行转换 + QMI8658A_ConvertData(rawData, convertedData, ACCRANGE, GYRRANGE); + + + // 计算加速度的模长 + AM= calculateAccelerationMagnitude(convertedData); + + + // 打印转换后的加速度计数据,保留小数点后6位 + // ESP_LOGE(TAG, " (g): AX=%d, AY=%d, AZ=%d (dps): GX=%d, GY=%d, GZ=%d", + // rawData[0], rawData[1], rawData[2],rawData[3], rawData[4], rawData[5]); + if(i==100) + { + i=0; + ESP_LOGE(TAG, " (g): AX=%.3f, AY=%.3f, AZ=%.3f, AM=%.3f (dps): GX=%.3f, GY=%.3f, GZ=%.3f", + (float)convertedData[0], (float)convertedData[1], (float)convertedData[2],AM,(float)convertedData[3], (float)convertedData[4], (float)convertedData[5]); + } + i++; +} + diff --git a/main/boards/movecall-moji-esp32s3/QMI8658A/QMI8658A.h b/main/boards/movecall-moji-esp32s3/QMI8658A/QMI8658A.h new file mode 100644 index 0000000..48a7672 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/QMI8658A/QMI8658A.h @@ -0,0 +1,180 @@ +#ifndef QMI8658_H +#define QMI8658_H +#include "IIC.h" +#include + +#define ACCRANGE 16 //加速度计量程 +#define GYRRANGE 2048 //陀螺仪量程 +#define SAMPLERATE 800.0f //采样频率 +#define MIN_COLLECTION_COUNT 1000 // 最小采集数据次数 +#define USED_DATA_COUNT 50 // 用于计算平均值的数据数量 +#define STATIONARY_THRESHOLD 0.001 // 判断静止的加速度模长差值阈值 +// 存储陀螺仪校准值 +extern float GyrCompensate[6]; + +#define WHO_AM_I 0x00 //设备ID默认0x05(只读) +#define REVISION_ID 0x01 //设备修订ID默认0x7C(只读) + + +#define CTRL1 0x02 //配置通讯方式、中断引脚、fifo中断,暂时配置为6C +#define CTRL2 0x03 //配置加速度计 +#define CTRL3 0x04 //配置陀螺仪 +#define CTRL5 0x06 //设置滤波器 +#define CTRL7 0x08 //启用加速度计和陀螺仪 +#define CTRL8 0x09 //运动检测 +#define CTRL9 0x0A //CTRL9执行预定指令 + +#define FIFO_WTM_TH 0x13 //FIFO 水位标记,设置触发值 +#define FIFO_CTRL 0x14 //FIFO控制寄存器 +#define FIFO_SMPL_CNT 0x15 //FIFO样本计数寄存器 +#define FIFO_STATUS 0x16 //FIFO状态寄存器 +#define FIFO_DATA 0x17 //FIFO输出寄存器 + +#define STATUSINT 0x2D //传感器数据可用和锁存寄存器 +#define STATUS1 0x2F //杂项状态寄存器(运动、步数、点击) +#define TIMESTAMP 0x30 //时间戳(0x30-0x32) + +#define TEMP_L 0x33 //温度 = TEMP_H + (TEMP_L / 256) +#define TEMP_H 0x34 + +#define A_XYZ 0x35 //加速度输出寄存器(0x35–0x3A) +#define G_XYZ 0x3B //陀螺仪输出寄存器(0x3B–0x40) + +#define COD_STATUS 0x46 //按需校准状态寄存器 + +#define TAP_STATUS 0x59 //敲击状态寄存器 +#define STEP_COUNT 0x5A //步数计数寄存器(0x5A-0x5C) + +#define RESET 0x60 //软件复位寄存器,任何模式写入0xB0复位 +#define dQY_L 0x4D //如果有成功的复位(上电复位或软复位)过程,寄存器 0x4D 的值将等于 0x80 + +// 主机控制校准寄存器(见 CTRL9,可选择使用) +#define CAL1_L 0x0B +#define CAL1_H 0x0C +#define CAL2_L 0x0D +#define CAL2_H 0x0E +#define CAL3_L 0x0F +#define CAL3_H 0x10 +#define CAL4_L 0x11 +#define CAL4_H 0x12 + +// 定义枚举来表示不同的命令序号 +typedef enum { + CTRL_CMD_ACK_ENUM, // 确认命令的枚举值,用于标识CTRL_CMD_ACK命令 + CTRL_CMD_RST_FIFO_ENUM, // 重置FIFO命令的枚举值,用于标识CTRL_CMD_RST_FIFO命令 + CTRL_CMD_REQ_FIFO_ENUM, // 获取FIFO数据命令的枚举值,用于标识CTRL_CMD_REQ_FIFO命令 + CTRL_CMD_WRITE_WOM_SETTING_ENUM, // 设置并启用运动唤醒命令的枚举值,用于标识CTRL_CMD_WRITE_WOM_SETTING命令 + CTRL_CMD_ACCEL_HOST_DELTA_OFFSET_ENUM, // 更改加速度计偏移量命令的枚举值,用于标识CTRL_CMD_ACCEL_HOST_DELTA_OFFSET命令 + CTRL_CMD_GYRO_HOST_DELTA_OFFSET_ENUM, // 更改陀螺仪偏移量命令的枚举值,用于标识CTRL_CMD_GYRO_HOST_DELTA_OFFSET命令 + CTRL_CMD_CONFIGURE_TAP_ENUM, // 配置敲击检测命令的枚举值,用于标识CTRL_CMD_CONFIGURE_TAP命令 + CTRL_CMD_CONFIGURE_PEDOMETER_ENUM, // 配置计步器命令的枚举值,用于标识CTRL_CMD_CONFIGURE_PEDOMETER命令 + CTRL_CMD_CONFIGURE_MOTION_ENUM, // 配置运动检测命令的枚举值,用于标识CTRL_CMD_CONFIGURE_MOTION命令 + CTRL_CMD_RESET_PEDOMETER_ENUM, // 重置计步器步数命令的枚举值,用于标识CTRL_CMD_RESET_PEDOMETER命令 + CTRL_CMD_COPY_USID_ENUM, // 复制USID和固件版本到UI寄存器命令的枚举值,用于标识CTRL_CMD_COPY_USID命令 + CTRL_CMD_SET_RPU_ENUM, // 配置IO上拉电阻命令的枚举值,用于标识CTRL_CMD_SET_RPU命令 + CTRL_CMD_AHB_CLOCK_GATING_ENUM, // 内部AHB时钟门控开关命令的枚举值,用于标识CTRL_CMD_AHB_CLOCK_GATING命令 + CTRL_CMD_ON_DEMAND_CALIBRATION_ENUM, // 对陀螺仪进行按需校准命令的枚举值,用于标识CTRL_CMD_ON_DEMAND_CALIBRATION命令 + CTRL_CMD_APPLY_GYRO_GAINS_ENUM // 恢复保存的陀螺仪增益命令的枚举值,用于标识CTRL_CMD_APPLY_GYRO_GAINS命令 +} CommandEnum; + +// 定义结构体来存储表格中的每一行信息 +typedef struct { + char commandName[50]; // 存储命令名称,最大长度为50个字符 + int ctrl9CommandValue; // 存储命令在CTRL9寄存器中的值 + char protocolType[10]; // 存储命令使用的协议类型,最大长度为10个字符 + char description[200]; // 存储命令的描述信息,最大长度为200个字符 +} CommandInfo; + +/** + * @brief 通过枚举获取对应的命令信息结构体 + * + * 该函数根据传入的命令枚举值,返回对应的命令信息结构体。 + * + * @param cmd 命令枚举值,用于指定要获取信息的命令 + * @return CommandInfo 包含指定命令详细信息的结构体 + */ +CommandInfo getCommandInfo(CommandEnum cmd); + +/** + * @brief 初始化QMI8658A传感器 + * + * 该函数用于对QMI8658A传感器进行初始化操作,包括复位、自检、配置等步骤。 + * + * @return int + * - 1: 初始化成功 + * - 0: 初始化失败 + */ +int QMI8658A_Init(void); + +/** + * @brief 读取、转换并打印传感器数据 + * + * 该函数读取QMI8658A传感器的数据,进行转换后打印输出。 + */ +void QMI8658A_ReadConvertAndPrint(); + +/** + * @brief 对陀螺仪进行校准 + * + * 该函数用于对陀螺仪进行校准操作,并将校准结果存储到传入的数组中。 + * + * @param OutData 指向一个长度至少为 3 的 float 类型数组的指针,用于存储校准结果 + * @return uint8_t + * - 1: 校准成功 + * - 0: 校准失败 + */ +uint8_t calibration_ACC_GYR(float *OutData); + +/** + * @brief 读取QMI8658A六轴传感器数据 + * + * 该函数用于读取QMI8658A六轴传感器的原始数据,并将其存储到传入的数组中。 + * + * @param DATA 指向一个长度为 6 的 int16_t 类型数组的指针,用于存储读取到的原始数据 + * @return int + * - 1: 读取成功 + * - 0: 读取失败 + */ +int QMI8658A_ReadData(int16_t *DATA); + +/** + * @brief 将QMI8658A传感器的原始数据进行转换 + * + * 该函数根据加速度计和陀螺仪的量程,将原始数据转换为实际的物理量值。 + * + * @param InData 指向包含原始传感器数据的数组的指针,数组长度应为 6 + * @param OutData 指向用于存储转换后数据的数组的指针,数组长度应为 6 + * @param accelRange 加速度计的量程 + * @param gyroRange 陀螺仪的量程 + */ +void QMI8658A_ConvertData(int16_t *InData, float *OutData, int accelRange, int gyroRange); + +/** + * @brief 计算加速度的模长 + * + * 该函数接收一个包含三轴加速度数据的数组,计算并返回加速度的模长。 + * + * @param OutData 指向一个长度至少为 3 的 float 类型数组的指针,数组前三个元素为加速度数据 + * @return float 加速度的模长 + */ +float calculateAccelerationMagnitude(float *OutData); + +/** + * @brief 计算陀螺仪平均值的辅助函数 + * + * 该函数用于计算陀螺仪在一段时间内采集的多个数据的平均值。 + * + * @param DATA 一个二维 float 类型数组,存储了陀螺仪在多个采集点的数据 + * @param OutData 指向一个长度至少为 3 的 float 类型数组的指针,用于存储计算得到的平均值 + */ +void calculateGyroAverages(float DATA[MIN_COLLECTION_COUNT][3], float *OutData); + +/** + * @brief 获取单位为g的三轴加速度计和单位为dps的三轴陀螺仪数据 + * + * 该函数结合读取和转换操作,获取并存储以g为单位的加速度计数据和以dps为单位的陀螺仪数据。 + * + * @param OutData 指向一个长度为 6 的 float 类型数组的指针,用于存储转换后的数据 + */ +void QMI8658A_Get_G_DPS(float *OutData); +#endif \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/README.md b/main/boards/movecall-moji-esp32s3/README.md new file mode 100644 index 0000000..812d9d5 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/README.md @@ -0,0 +1,26 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> Movecall Moji 小智AI衍生版 +``` + + +**编译:** + +```bash +idf.py build +``` \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/WAKE_WORD_GUIDE.md b/main/boards/movecall-moji-esp32s3/WAKE_WORD_GUIDE.md new file mode 100644 index 0000000..6b8c7bb --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/WAKE_WORD_GUIDE.md @@ -0,0 +1,126 @@ +# 唤醒词配置指南 + +## 🎯 快速切换唤醒词 + +### 方法一:修改配置文件 +1. 打开 `main/boards/movecall-moji-esp32s3/config.json` +2. 找到唤醒词配置行 +3. 注释掉当前唤醒词,取消注释想要的唤醒词 +4. 重新编译项目 + +### 方法二:使用menuconfig +```bash +idf.py menuconfig +``` +导航到:`Component config` → `ESP Speech Recognition` → `Wake Word` + +## 📝 支持的唤醒词列表 + +### 中文唤醒词: +- **你好小智** (推荐,TTS训练版) +- **你好喵伴** +- **小爱同学** +- **你好小鑫** +- **小美同学** +- **小龙小龙** +- **喵喵同学** +- **小宇同学** +- **小明同学** +- **小康同学** +- **你好小益** +- **你好百应** +- **你好东东** + +### 英文唤醒词: +- **Hi,ESP** (默认) +- **Hi,乐鑫** +- **Hi,Jason** +- **Alexa** +- **Jarvis** +- **Computer** +- **Hey,Willow** +- **Sophia** +- **Mycroft** +- **Hi,M Five** +- **Hi,Joy** +- **Hi,Wall E / Hi,瓦力** +- **Hi,Lily / Hi,莉莉** +- **Hi,Telly / Hi,泰力** + +## ⚙️ 配置示例 + +### 使用"你好小智": +```json +"CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=y" +``` + +### 使用"Alexa": +```json +"CONFIG_SR_WN_WN9_ALEXA=y" +``` + +### 使用"Hi,ESP": +```json +"CONFIG_SR_WN_WN9_HIESP=y" +``` + +## 🔧 工作流程 + +1. **待命状态** → 设备等待唤醒词 +2. **说出唤醒词** → 设备检测到唤醒词 +3. **唤醒成功** → 设备发送"你好,小智"到服务端 +4. **进入对话** → 可以开始语音交互 + +## 📊 性能对比 + +| 模型类型 | 内存占用 | 检测精度 | 功耗 | 推荐场景 | +|----------|----------|----------|------|----------| +| TTS训练版 | 中等 | 高 | 中等 | 生产环境 | +| 标准版 | 较低 | 中等 | 较低 | 测试环境 | + +## 🛠️ 自定义唤醒词 + +如果现有唤醒词不满足需求,可以通过以下方式自定义: + +### 方法一:联系Espressif定制 +- 通过官方渠道申请定制唤醒词 +- 需要提供大量语音样本 +- 适用于商业化项目 + +### 方法二:使用TTS管道训练 +- 使用ESP-SR提供的TTS训练管道 +- 适用于快速原型开发 +- 精度可能略低于官方模型 + +## 🚨 注意事项 + +1. **同时只能启用一个唤醒词** +2. **重新编译需要清除缓存**:`idf.py clean` +3. **确保ESP32S3有足够的PSRAM** +4. **不同唤醒词的功耗可能不同** +5. **TTS训练版通常比标准版更准确** + +## 📋 故障排除 + +### 唤醒词不响应? +1. 检查麦克风连接 +2. 确认已正确配置唤醒词 +3. 检查环境噪音 +4. 尝试不同的发音方式 + +### 编译错误? +1. 确认只启用了一个唤醒词 +2. 清除构建缓存:`idf.py clean` +3. 检查ESP-SR组件版本 + +### 误触发? +1. 调整唤醒词阈值 +2. 减少环境噪音 +3. 使用更精确的TTS训练版模型 + +## 📞 技术支持 + +如有问题,请查看: +- ESP-SR官方文档 +- ESP-IDF GitHub Issues +- Espressif技术论坛 \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/Wi-Fi配网日志.txt b/main/boards/movecall-moji-esp32s3/Wi-Fi配网日志.txt new file mode 100644 index 0000000..3d77763 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/Wi-Fi配网日志.txt @@ -0,0 +1,559 @@ +--- Warning: Serial ports accessed as /dev/tty.* will hang gdb if launched. +--- Using /dev/cu.usbmodem11301 instead... +--- esp-idf-monitor 1.7.0 on /dev/cu.usbmodem11301 115200 +--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0x15 (USB_UART_CHIP_RESET),boot:0x1c (SPI_FAST_FLASH_BOOT) +Saved PC:0x40380dd6 +--- 0x40380dd6: esp_cpu_wait_for_intr at /Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf/components/esp_hw_support/cpu.c:64 +SPIWP:0xee +mode:DIO, clock div:1 +load:0x3fce2820,len:0x56c +load:0x403c8700,len:0x4 +load:0x403c8704,len:0xc30 +load:0x403cb700,len:0x2e2c +entry 0x403c890c +I (36) octal_psram: vendor id : 0x0d (AP) +I (36) octal_psram: dev id : 0x02 (generation 3) +I (36) octal_psram: density : 0x03 (64 Mbit) +I (38) octal_psram: good-die : 0x01 (Pass) +I (42) octal_psram: Latency : 0x01 (Fixed) +I (46) octal_psram: VCC : 0x01 (3V) +I (50) octal_psram: SRF : 0x01 (Fast Refresh) +I (55) octal_psram: BurstType : 0x01 (Hybrid Wrap) +I (60) octal_psram: BurstLen : 0x01 (32 Byte) +I (64) octal_psram: Readlatency : 0x02 (10 cycles@Fixed) +I (69) octal_psram: DriveStrength: 0x00 (1/1) +I (74) MSPI Timing: PSRAM timing tuning index: 5 +I (78) esp_psram: Found 8MB PSRAM device +I (81) esp_psram: Speed: 80MHz +I (84) cpu_start: Multicore app +I (99) cpu_start: Pro cpu start user code +I (99) cpu_start: cpu freq: 240000000 Hz +I (99) app_init: Application information: +I (99) app_init: Project name: xiaozhi +I (102) app_init: App version: 1.7.2 +I (106) app_init: Compile time: Aug 13 2025 14:11:06 +I (111) app_init: ELF file SHA256: 70cab7f6a... +I (116) app_init: ESP-IDF: v5.4.2-dirty +I (120) efuse_init: Min chip rev: v0.0 +I (124) efuse_init: Max chip rev: v0.99 +I (128) efuse_init: Chip rev: v0.2 +I (132) heap_init: Initializing. RAM available for dynamic allocation: +I (138) heap_init: At 3FCAD230 len 0003C4E0 (241 KiB): RAM +I (143) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM +I (148) heap_init: At 600FE01C len 00001FBC (7 KiB): RTCRAM +I (154) esp_psram: Adding pool of 8192K of PSRAM memory to heap allocator +I (161) spi_flash: detected chip: generic +I (164) spi_flash: flash io: qio +I (168) sleep_gpio: Configure to isolate all GPIO pins in sleep state +I (173) sleep_gpio: Enable automatic switching of GPIO sleep configuration +I (180) main_task: Started on CPU0 +I (190) esp_psram: Reserving pool of 64K of internal memory for DMA/internal allocations +I (190) main_task: Calling app_main() +I (200) BackgroundTask: background_task started +I (210) Board: UUID=6830e80c-5c18-40e4-a04e-1ab889e80ef1 SKU=movecall-moji-esp32s3 +I (210) button: IoT Button Version: 3.5.0 +I (210) gpio: GPIO[0]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (220) button: IoT Button Version: 3.5.0 +I (220) gpio: GPIO[46]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (230) button: IoT Button Version: 3.5.0 +I (230) gpio: GPIO[45]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (240) button: IoT Button Version: 3.5.0 +I (250) gpio: GPIO[18]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (250) MovecallMojiESP32S3: Initializing buttons... +I (260) MovecallMojiESP32S3: Boot button initialized on GPIO0 +I (260) MovecallMojiESP32S3: Volume up button initialized on GPIO46 +I (270) MovecallMojiESP32S3: Volume down button initialized on GPIO45 +I (280) MovecallMojiESP32S3: Story button initialized on GPIO18 +I (280) MovecallMojiESP32S3: All buttons initialized successfully +I (290) MovecallMojiESP32S3: Initializing battery monitor... +I (290) MovecallMojiESP32S3: Battery monitor initialized on GPIO10 +I (300) MovecallMojiESP32S3: 在构造函数完成后调用触摸初始化 +I (310) Application: STATE: starting +I (310) MovecallMojiESP32S3: Initializing audio codec... +I (310) MovecallMojiESP32S3: Initializing I2C bus for audio codec... +I (320) MovecallMojiESP32S3: Creating Es8311AudioCodec instance... +I (330) Es8311AudioCodec: Duplex channels created +I (340) ES8311: Work in Slave mode +I (340) gpio: GPIO[9]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (340) Es8311AudioCodec: Es8311AudioCodec initialized +I (350) MovecallMojiESP32S3: Audio codec initialized successfully +I (350) Application: WiFi board detected, setting opus encoder complexity to 3 +I (360) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1 +I (360) I2S_IF: STD Mode 0 bits:16/16 channel:2 sample_rate:16000 mask:1 +I (370) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1 +I (380) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1 +I (400) Adev_Codec: Open codec device OK +I (400) AudioCodec: Set input enable to true +I (400) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1 +I (400) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1 +I (410) Adev_Codec: Open codec device OK +I (420) AudioCodec: Set output enable to true +I (420) AudioCodec: Audio codec started +I (420) Application: Device startup completed, playing boot sound +I (420) Application: STATE: configuring +I (430) DnsServer: Starting DNS server +I (430) pp: pp rom version: e7ae62f +I (430) net80211: net80211 rom version: e7ae62f +I (450) wifi:wifi driver task: 3fcdcd90, prio:23, stack:6656, core=0 +I (450) wifi:wifi firmware version: bea31f3 +I (450) wifi:wifi certification version: v7.0 +I (450) wifi:config NVS flash: enabled +I (450) wifi:config nano formatting: disabled +I (460) wifi:Init data frame dynamic rx buffer num: 32 +I (460) wifi:Init dynamic rx mgmt buffer num: 5 +I (470) wifi:Init management short buffer num: 32 +I (470) wifi:Init static tx buffer num: 16 +I (480) wifi:Init tx cache buffer num: 32 +I (480) wifi:Init static tx FG buffer num: 2 +I (480) wifi:Init static rx buffer size: 1600 +I (490) wifi:Init static rx buffer num: 16 +I (490) wifi:Init dynamic rx buffer num: 32 +I (500) wifi_init: rx ba win: 16 +I (500) wifi_init: accept mbox: 6 +I (500) wifi_init: tcpip mbox: 32 +I (500) wifi_init: udp mbox: 6 +I (510) wifi_init: tcp mbox: 6 +I (510) wifi_init: tcp tx win: 5760 +I (510) wifi_init: tcp rx win: 5760 +I (520) wifi_init: tcp mss: 1440 +I (520) wifi_init: WiFi/LWIP prefer SPIRAM +I (530) wifi:Set ps type: 0, coexist: 0 + +I (530) phy_init: phy_version 701,f4f1da3a,Mar 3 2025,15:50:10 +I (570) wifi:mode : sta (98:a3:16:c1:df:80) + softAP (98:a3:16:c1:df:81) +I (570) wifi:enable tsf +I (570) wifi:Total power save buffer number: 8 +I (570) wifi:Init max length of beacon: 752/752 +I (580) wifi:Init max length of beacon: 752/752 +I (580) WifiConfigurationAp: Access Point started with SSID Airhub-DF81 +I (580) esp_netif_lwip: DHCP server started on interface WIFI_AP_DEF with IP: 192.168.4.1 +I (600) WifiConfigurationAp: Web server started +W (600) Application: Alert 配网模式: 手机连接热点 Airhub-DF81,浏览器访问 http://192.168.4.1 + + [] +I (610) WifiBoard: Free internal: 76527 minimal internal: 72443 +I (1290) MovecallMojiESP32S3: Battery ADC: 1421, Average: 1421, Level: 0% +I (1310) MovecallMojiESP32S3: 开始延迟初始化触摸板... +I (1310) MovecallMojiESP32S3: 初始化触摸板... +I (1310) MovecallMojiESP32S3: 配置触摸传感器... +I (1310) MovecallMojiESP32S3: 校准触摸阈值... +I (1310) MovecallMojiESP32S3: 触摸板 0 初始原始值: 20504 +I (1320) MovecallMojiESP32S3: 触摸板 0 设置固定阈值: 5000 +I (1330) MovecallMojiESP32S3: 触摸板 1 初始原始值: 20977 +I (1330) MovecallMojiESP32S3: 触摸板 1 设置固定阈值: 5000 +I (1340) MovecallMojiESP32S3: 触摸板 2 初始原始值: 20422 +I (1340) MovecallMojiESP32S3: 触摸板 2 设置固定阈值: 5000 +I (1350) MovecallMojiESP32S3: 触摸板 3 初始原始值: 15889 +I (1350) MovecallMojiESP32S3: 触摸板 3 设置固定阈值: 5000 +I (1360) MovecallMojiESP32S3: 启用触摸传感器滤波器 +I (1370) MovecallMojiESP32S3: 触摸阈值校准完成,使用固定阈值: 5000 +I (1370) MovecallMojiESP32S3: 创建触摸事件队列... +I (1380) MovecallMojiESP32S3: 注册触摸中断处理程序... +I (1380) MovecallMojiESP32S3: 创建触摸事件任务... +I (1390) MovecallMojiESP32S3: 触摸事件任务启动 +I (1390) MovecallMojiESP32S3: 所有触摸状态已重置 +I (1390) MovecallMojiESP32S3: 触摸事件任务开始主循环 +I (1400) MovecallMojiESP32S3: 设置触摸监控... +I (1410) MovecallMojiESP32S3: 触摸板初始化完成 +I (2290) MovecallMojiESP32S3: Battery ADC: 1789, Average: 1605, Level: 0% +I (3290) MovecallMojiESP32S3: Battery ADC: 1526, Average: 1578, Level: 0% +I (10610) WifiBoard: Free internal: 82175 minimal internal: 68343 +I (17110) wifi:new:<1,1>, old:<1,1>, ap:<1,1>, sta:<0,0>, prof:1, snd_ch_cfg:0x0 +I (17110) wifi:station: 42:11:28:b6:60:39 join, AID=1, bgn, 40U +I (17110) WifiConfigurationAp: Station 42:11:28:b6:60:39 joined, AID=1 +I (17190) wifi:idx:2 (ifx:1, 42:11:28:b6:60:39), tid:0, ssn:0, winSize:64 +I (17240) esp_netif_lwip: DHCP server assigned IP to a client, IP is: 192.168.4.2 +I (17410) DnsServer: Sending DNS response to 192.168.4.1 +W (17450) httpd_uri: httpd_uri: URI '/generate_204_894ca791-559f-49d9-9487-9124ce5ae135' not found +W (17450) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +I (17470) DnsServer: Sending DNS response to 192.168.4.1 +I (17590) wifi:station: 42:11:28:b6:60:39 leave, AID = 1, reason = 3, bss_flags is 33786979, bss:0x3c23c554 +I (17590) wifi:new:<1,0>, old:<1,1>, ap:<1,1>, sta:<0,0>, prof:1, snd_ch_cfg:0x0 +I (17600) wifi:idx:2, tid:0 +I (17600) WifiConfigurationAp: Station 42:11:28:b6:60:39 left, AID=1 +I (20610) WifiBoard: Free internal: 82235 minimal internal: 68343 +I (21940) MovecallMojiESP32S3: BOOT button clicked +I (21940) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (21940) MovecallMojiESP32S3: 当前设备状态: 2 +I (21940) MovecallMojiESP32S3: 所有触摸状态已重置 +I (21940) MovecallMojiESP32S3: 唤醒设备 +I (26020) MovecallMojiESP32S3: BOOT button clicked +I (26020) MovecallMojiESP32S3: 当前设备状态: 2 +I (26020) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (26020) MovecallMojiESP32S3: 唤醒设备 +I (26020) MovecallMojiESP32S3: 所有触摸状态已重置 +I (26800) MovecallMojiESP32S3: BOOT button clicked +I (26800) MovecallMojiESP32S3: 当前设备状态: 2 +I (26800) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (26800) MovecallMojiESP32S3: 唤醒设备 +I (26810) MovecallMojiESP32S3: 所有触摸状态已重置 +I (27380) MovecallMojiESP32S3: BOOT button clicked +I (27380) MovecallMojiESP32S3: 当前设备状态: 2 +I (27380) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (27380) MovecallMojiESP32S3: 唤醒设备 +I (27380) MovecallMojiESP32S3: 所有触摸状态已重置 +I (27700) MovecallMojiESP32S3: BOOT button clicked too frequently, ignoring this click +I (30610) WifiBoard: Free internal: 82235 minimal internal: 68343 +I (40610) WifiBoard: Free internal: 82235 minimal internal: 68343 +I (50610) WifiBoard: Free internal: 82235 minimal internal: 68343 +I (60610) WifiBoard: Free internal: 82047 minimal internal: 68343 +I (63290) MovecallMojiESP32S3: Battery ADC: 1436, Average: 1583, Level: 0% +I (70610) WifiBoard: Free internal: 82235 minimal internal: 68343 +I (76400) MovecallMojiESP32S3: BOOT button clicked +I (76400) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (76400) MovecallMojiESP32S3: 当前设备状态: 2 +I (76400) MovecallMojiESP32S3: 所有触摸状态已重置 +I (76410) MovecallMojiESP32S3: 唤醒设备 +I (77780) MovecallMojiESP32S3: BOOT button clicked +I (77780) MovecallMojiESP32S3: 当前设备状态: 2 +I (77780) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (77780) MovecallMojiESP32S3: 唤醒设备 +I (77780) MovecallMojiESP32S3: 所有触摸状态已重置 +I (80610) WifiBoard: Free internal: 82047 minimal internal: 68343 +I (88390) wifi:new:<1,1>, old:<1,0>, ap:<1,1>, sta:<0,0>, prof:1, snd_ch_cfg:0x0 +I (88390) wifi:station: 82:9e:e0:bd:8a:73 join, AID=1, bgn, 40U +I (88400) WifiConfigurationAp: Station 82:9e:e0:bd:8a:73 joined, AID=1 +I (88420) wifi:idx:2 (ifx:1, 82:9e:e0:bd:8a:73), tid:0, ssn:0, winSize:64 +I (88660) esp_netif_lwip: DHCP server assigned IP to a client, IP is: 192.168.4.3 +I (88940) DnsServer: Sending DNS response to 192.168.4.1 +I (89040) DnsServer: Sending DNS response to 192.168.4.1 +W (89050) httpd_uri: httpd_uri: URI '/generate_204_75ee3b15-1afe-4671-8783-e2597ae9a1ec' not found +W (89050) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +I (89080) DnsServer: Sending DNS response to 192.168.4.1 +I (89140) DnsServer: Sending DNS response to 192.168.4.1 +I (89200) DnsServer: Sending DNS response to 192.168.4.1 +I (90000) DnsServer: Sending DNS response to 192.168.4.1 +I (90220) DnsServer: Sending DNS response to 192.168.4.1 +I (90250) DnsServer: Sending DNS response to 192.168.4.1 +I (90270) DnsServer: Sending DNS response to 192.168.4.1 +I (90290) DnsServer: Sending DNS response to 192.168.4.1 +I (90440) DnsServer: Sending DNS response to 192.168.4.1 +I (90440) DnsServer: Sending DNS response to 192.168.4.1 +I (90440) DnsServer: Sending DNS response to 192.168.4.1 +I (90450) DnsServer: Sending DNS response to 192.168.4.1 +I (90450) DnsServer: Sending DNS response to 192.168.4.1 +I (90610) WifiBoard: Free internal: 82195 minimal internal: 68343 +I (90890) DnsServer: Sending DNS response to 192.168.4.1 +I (91040) DnsServer: Sending DNS response to 192.168.4.1 +I (91330) wifi:idx:3 (ifx:1, 82:9e:e0:bd:8a:73), tid:6, ssn:0, winSize:64 +I (91640) DnsServer: Sending DNS response to 192.168.4.1 +I (91780) DnsServer: Sending DNS response to 192.168.4.1 +I (91780) DnsServer: Sending DNS response to 192.168.4.1 +I (92630) DnsServer: Sending DNS response to 192.168.4.1 +I (92650) WifiConfigurationAp: SSID: ZCWH, RSSI: -26, Authmode: 4 +I (92650) WifiConfigurationAp: SSID: airhub, RSSI: -32, Authmode: 3 +I (92650) WifiConfigurationAp: SSID: aWiFi, RSSI: -35, Authmode: 0 +I (92650) WifiConfigurationAp: SSID: ChinaNet-A9Gs, RSSI: -37, Authmode: 4 +I (92660) WifiConfigurationAp: SSID: -C311, RSSI: -42, Authmode: 4 +I (92670) WifiConfigurationAp: SSID: liang, RSSI: -48, Authmode: 4 +I (92670) WifiConfigurationAp: SSID: welcome to miao, RSSI: -65, Authmode: 4 +I (92680) WifiConfigurationAp: SSID: 建隆, RSSI: -67, Authmode: 4 +I (92680) WifiConfigurationAp: SSID: 建隆, RSSI: -69, Authmode: 4 +I (92690) WifiConfigurationAp: SSID: On79, RSSI: -71, Authmode: 4 +I (92700) WifiConfigurationAp: SSID: 建隆, RSSI: -72, Authmode: 4 +I (92700) WifiConfigurationAp: SSID: WiFijian, RSSI: -73, Authmode: 4 +I (92710) WifiConfigurationAp: SSID: CandyTime_B35CF6, RSSI: -73, Authmode: 3 +I (92710) WifiConfigurationAp: SSID: EZVIZ_BC4318972, RSSI: -75, Authmode: 3 +I (92720) WifiConfigurationAp: SSID: Xiaomi_2946, RSSI: -76, Authmode: 4 +I (92730) WifiConfigurationAp: SSID: DIRECT-61-HP +, RSSI: -77, Authmode: 3 +I (92730) WifiConfigurationAp: SSID: 工作2.4, RSSI: -78, Authmode: 7 +I (92740) WifiConfigurationAp: SSID: 工作2.4, RSSI: -82, Authmode: 7 +W (92750) httpd_uri: httpd_uri: URI '/favicon.ico' not found +W (92750) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +I (93020) DnsServer: Sending DNS response to 192.168.4.1 +I (93020) DnsServer: Sending DNS response to 192.168.4.1 +I (93490) DnsServer: Sending DNS response to 192.168.4.1 +I (93520) DnsServer: Sending DNS response to 192.168.4.1 +I (93520) DnsServer: Sending DNS response to 192.168.4.1 +I (93520) DnsServer: Sending DNS response to 192.168.4.1 +W (93680) httpd_uri: httpd_uri: URI '/mmtls/47908f7f' not found +W (93690) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +I (93710) DnsServer: Sending DNS response to 192.168.4.1 +I (93710) DnsServer: Sending DNS response to 192.168.4.1 +I (93730) DnsServer: Sending DNS response to 192.168.4.1 +W (93750) httpd_uri: httpd_uri: URI '/mmtls/47908f7f' not found +W (93750) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +W (93750) httpd_parse: parse_block: incomplete (0/128) with parser error = 16 +W (93760) httpd_txrx: httpd_resp_send_err: 400 Bad Request - Bad request syntax +I (94330) DnsServer: Sending DNS response to 192.168.4.1 +I (94340) DnsServer: Sending DNS response to 192.168.4.1 +W (95410) httpd_uri: httpd_uri: URI '/mmtls/23375888' not found +W (95410) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +I (96050) DnsServer: Sending DNS response to 192.168.4.1 +W (96810) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2 +I (96820) WifiConfigurationAp: Connecting to WiFi airhub +I (97070) wifi:[ADDBA]RX DELBA, reason:1, delete tid:0, initiator:1(originator) +I (97070) wifi:idx:2, tid:0 +I (97080) wifi:[ADDBA]RX DELBA, reason:1, delete tid:6, initiator:1(originator) +I (97080) wifi:idx:3, tid:6 +I (97570) wifi:idx:2 (ifx:1, 82:9e:e0:bd:8a:73), tid:0, ssn:371, winSize:64 +I (99640) DnsServer: Sending DNS response to 192.168.4.1 +I (99740) wifi:new:<1,1>, old:<1,1>, ap:<1,1>, sta:<1,0>, prof:1, snd_ch_cfg:0x0 +I (99740) wifi:state: init -> auth (0xb0) +I (99760) wifi:state: auth -> assoc (0x0) +I (99770) wifi:state: assoc -> run (0x10) +I (99800) wifi:connected with airhub, aid = 4, channel 1, BW20, bssid = 70:2a:d7:85:bc:eb +I (99800) wifi:security: WPA2-PSK, phy: bgn, rssi: -33 +I (99810) wifi:pm start, type: 0 + +I (99810) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us +I (99810) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000 +I (99820) WifiConfigurationAp: Connected to WiFi airhub +I (99830) wifi:state: run -> init (0x0) +I (99840) wifi:pm stop, total sleep time: 0 us / 23918 us + +I (99840) wifi:new:<1,1>, old:<1,1>, ap:<1,1>, sta:<1,0>, prof:1, snd_ch_cfg:0x0 +I (99840) WifiConfigurationAp: Save SSID airhub 6 +I (100270) DnsServer: Sending DNS response to 192.168.4.1 +W (100290) httpd_uri: httpd_uri: URI '/mmtls/48588809' not found +W (100300) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +I (100610) WifiBoard: Free internal: 81391 minimal internal: 68343 +I (101630) DnsServer: Sending DNS response to 192.168.4.1 +I (101640) DnsServer: Sending DNS response to 192.168.4.1 +I (101790) DnsServer: Sending DNS response to 192.168.4.1 +I (101800) DnsServer: Sending DNS response to 192.168.4.1 +I (101950) DnsServer: Sending DNS response to 192.168.4.1 +W (102640) httpd_uri: httpd_uri: URI '/mmtls/63faaef8' not found +W (102640) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI +I (103030) WifiConfigurationAp: Rebooting... +I (103430) wifi:station: 82:9e:e0:bd:8a:73 leave, AID = 1, reason = 2, bss_flags is 33786979, bss:0x3c23c528 +I (103430) wifi:new:<1,0>, old:<1,1>, ap:<1,1>, sta:<1,0>, prof:1, snd_ch_cfg:0x0 +I (103430) wifi:idx:2, tid:0 +I (103440) WifiConfigurationAp: Station 82:9e:e0:bd:8a:73 left, AID=1 +I (105030) wifi:flush txq +I (105030) wifi:stop sw txq +I (105030) wifi:lmac stop hw txq +ESP-ROM:esp32s3-20210327 +Build:Mar 27 2021 +rst:0xc (RTC_SW_CPU_RST),boot:0x1c (SPI_FAST_FLASH_BOOT) +Saved PC:0x40379e9d +--- 0x40379e9d: esp_restart_noos at /Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf/components/esp_system/port/soc/esp32s3/system_internal.c:162 +SPIWP:0xee +mode:DIO, clock div:1 +load:0x3fce2820,len:0x56c +load:0x403c8700,len:0x4 +load:0x403c8704,len:0xc30 +load:0x403cb700,len:0x2e2c +entry 0x403c890c +I (35) octal_psram: vendor id : 0x0d (AP) +I (35) octal_psram: dev id : 0x02 (generation 3) +I (36) octal_psram: density : 0x03 (64 Mbit) +I (37) octal_psram: good-die : 0x01 (Pass) +I (41) octal_psram: Latency : 0x01 (Fixed) +I (46) octal_psram: VCC : 0x01 (3V) +I (50) octal_psram: SRF : 0x01 (Fast Refresh) +I (54) octal_psram: BurstType : 0x01 (Hybrid Wrap) +I (59) octal_psram: BurstLen : 0x01 (32 Byte) +I (64) octal_psram: Readlatency : 0x02 (10 cycles@Fixed) +I (69) octal_psram: DriveStrength: 0x00 (1/1) +I (74) MSPI Timing: PSRAM timing tuning index: 5 +I (77) esp_psram: Found 8MB PSRAM device +I (81) esp_psram: Speed: 80MHz +I (84) cpu_start: Multicore app +I (98) cpu_start: Pro cpu start user code +I (98) cpu_start: cpu freq: 240000000 Hz +I (98) app_init: Application information: +I (99) app_init: Project name: xiaozhi +I (102) app_init: App version: 1.7.2 +I (106) app_init: Compile time: Aug 13 2025 14:11:06 +I (111) app_init: ELF file SHA256: 70cab7f6a... +I (115) app_init: ESP-IDF: v5.4.2-dirty +I (119) efuse_init: Min chip rev: v0.0 +I (123) efuse_init: Max chip rev: v0.99 +I (127) efuse_init: Chip rev: v0.2 +I (131) heap_init: Initializing. RAM available for dynamic allocation: +I (137) heap_init: At 3FCAD230 len 0003C4E0 (241 KiB): RAM +I (142) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM +I (148) heap_init: At 600FE01C len 00001FBC (7 KiB): RTCRAM +I (153) esp_psram: Adding pool of 8192K of PSRAM memory to heap allocator +I (160) spi_flash: detected chip: generic +I (163) spi_flash: flash io: qio +I (167) sleep_gpio: Configure to isolate all GPIO pins in sleep state +I (172) sleep_gpio: Enable automatic switching of GPIO sleep configuration +I (179) main_task: Started on CPU0 +I (189) esp_psram: Reserving pool of 64K of internal memory for DMA/internal allocations +I (189) main_task: Calling app_main() +I (209) BackgroundTask: background_task started +I (209) Board: UUID=6830e80c-5c18-40e4-a04e-1ab889e80ef1 SKU=movecall-moji-esp32s3 +I (209) button: IoT Button Version: 3.5.0 +I (219) gpio: GPIO[0]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (229) button: IoT Button Version: 3.5.0 +I (229) gpio: GPIO[46]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (239) button: IoT Button Version: 3.5.0 +I (239) gpio: GPIO[45]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (249) button: IoT Button Version: 3.5.0 +I (249) gpio: GPIO[18]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (259) MovecallMojiESP32S3: Initializing buttons... +I (269) MovecallMojiESP32S3: Boot button initialized on GPIO0 +I (269) MovecallMojiESP32S3: Volume up button initialized on GPIO46 +I (279) MovecallMojiESP32S3: Volume down button initialized on GPIO45 +I (279) MovecallMojiESP32S3: Story button initialized on GPIO18 +I (289) MovecallMojiESP32S3: All buttons initialized successfully +I (299) MovecallMojiESP32S3: Initializing battery monitor... +I (299) MovecallMojiESP32S3: Battery monitor initialized on GPIO10 +I (309) MovecallMojiESP32S3: 在构造函数完成后调用触摸初始化 +I (309) Application: STATE: starting +I (319) MovecallMojiESP32S3: Initializing audio codec... +I (319) MovecallMojiESP32S3: Initializing I2C bus for audio codec... +I (329) MovecallMojiESP32S3: Creating Es8311AudioCodec instance... +I (339) Es8311AudioCodec: Duplex channels created +I (339) ES8311: Work in Slave mode +I (349) gpio: GPIO[9]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (349) Es8311AudioCodec: Es8311AudioCodec initialized +I (349) MovecallMojiESP32S3: Audio codec initialized successfully +I (359) Application: WiFi board detected, setting opus encoder complexity to 3 +I (369) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1 +I (369) I2S_IF: STD Mode 0 bits:16/16 channel:2 sample_rate:16000 mask:1 +I (379) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1 +I (379) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1 +I (409) Adev_Codec: Open codec device OK +I (409) AudioCodec: Set input enable to true +I (409) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1 +I (409) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1 +I (419) Adev_Codec: Open codec device OK +I (429) AudioCodec: Set output enable to true +I (429) AudioCodec: Audio codec started +I (429) Application: Device startup completed, playing boot sound +I (429) pp: pp rom version: e7ae62f +I (429) net80211: net80211 rom version: e7ae62f +I (449) wifi:wifi driver task: 3fcdbbf4, prio:23, stack:6656, core=0 +I (449) wifi:wifi firmware version: bea31f3 +I (449) wifi:wifi certification version: v7.0 +I (449) wifi:config NVS flash: disabled +I (459) wifi:config nano formatting: disabled +I (459) wifi:Init data frame dynamic rx buffer num: 32 +I (459) wifi:Init dynamic rx mgmt buffer num: 5 +I (469) wifi:Init management short buffer num: 32 +I (469) wifi:Init static tx buffer num: 16 +I (479) wifi:Init tx cache buffer num: 32 +I (479) wifi:Init static tx FG buffer num: 2 +I (479) wifi:Init static rx buffer size: 1600 +I (489) wifi:Init static rx buffer num: 16 +I (489) wifi:Init dynamic rx buffer num: 32 +I (499) wifi_init: rx ba win: 16 +I (499) wifi_init: accept mbox: 6 +I (499) wifi_init: tcpip mbox: 32 +I (509) wifi_init: udp mbox: 6 +I (509) wifi_init: tcp mbox: 6 +I (509) wifi_init: tcp tx win: 5760 +I (509) wifi_init: tcp rx win: 5760 +I (519) wifi_init: tcp mss: 1440 +I (519) wifi_init: WiFi/LWIP prefer SPIRAM +I (519) phy_init: phy_version 701,f4f1da3a,Mar 3 2025,15:50:10 +I (559) phy_init: Saving new calibration data due to checksum failure or outdated calibration data, mode(0) +I (569) wifi:mode : sta (98:a3:16:c1:df:80) +I (579) wifi:enable tsf +I (1299) MovecallMojiESP32S3: Battery ADC: 1321, Average: 1321, Level: 0% +I (1309) MovecallMojiESP32S3: 开始延迟初始化触摸板... +I (1309) MovecallMojiESP32S3: 初始化触摸板... +I (1309) MovecallMojiESP32S3: 配置触摸传感器... +I (1309) MovecallMojiESP32S3: 校准触摸阈值... +I (1319) MovecallMojiESP32S3: 触摸板 0 初始原始值: 20503 +I (1319) MovecallMojiESP32S3: 触摸板 0 设置固定阈值: 5000 +I (1329) MovecallMojiESP32S3: 触摸板 1 初始原始值: 20965 +I (1329) MovecallMojiESP32S3: 触摸板 1 设置固定阈值: 5000 +I (1339) MovecallMojiESP32S3: 触摸板 2 初始原始值: 111889 +I (1349) MovecallMojiESP32S3: 触摸板 2 设置固定阈值: 5000 +I (1349) MovecallMojiESP32S3: 触摸板 3 初始原始值: 15850 +I (1359) MovecallMojiESP32S3: 触摸板 3 设置固定阈值: 5000 +I (1359) MovecallMojiESP32S3: 启用触摸传感器滤波器 +I (1369) MovecallMojiESP32S3: 触摸阈值校准完成,使用固定阈值: 5000 +I (1379) MovecallMojiESP32S3: 创建触摸事件队列... +I (1379) MovecallMojiESP32S3: 注册触摸中断处理程序... +I (1389) MovecallMojiESP32S3: 创建触摸事件任务... +I (1389) MovecallMojiESP32S3: 触摸事件任务启动 +I (1389) MovecallMojiESP32S3: 所有触摸状态已重置 +I (1399) MovecallMojiESP32S3: 触摸事件任务开始主循环 +I (1409) MovecallMojiESP32S3: 设置触摸监控... +I (1409) MovecallMojiESP32S3: 触摸板初始化完成 +I (2299) MovecallMojiESP32S3: Battery ADC: 1277, Average: 1299, Level: 0% +I (2979) wifi: Found AP: airhub, BSSID: 70:2a:d7:85:bc:eb, RSSI: -30, Channel: 1, Authmode: 3 +I (2989) WifiBoard: Starting WiFi connection, playing network connection sound +W (2989) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2 +I (3079) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1, snd_ch_cfg:0x0 +I (3089) wifi:state: init -> auth (0xb0) +I (3099) wifi:state: auth -> assoc (0x0) +I (3109) wifi:state: assoc -> run (0x10) +I (3139) wifi:connected with airhub, aid = 4, channel 1, BW20, bssid = 70:2a:d7:85:bc:eb +I (3139) wifi:security: WPA2-PSK, phy: bgn, rssi: -40 +I (3139) wifi:pm start, type: 1 + +I (3139) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us +I (3149) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000 +I (3159) wifi:AP's beacon interval = 102400 us, DTIM period = 1 +I (3169) wifi:idx:0 (ifx:0, 70:2a:d7:85:bc:eb), tid:0, ssn:0, winSize:64 +I (3299) MovecallMojiESP32S3: Battery ADC: 1289, Average: 1295, Level: 0% +I (5729) wifi: Got IP: 192.168.124.32 +I (5729) esp_netif_handlers: sta ip: 192.168.124.32, mask: 255.255.255.0, gw: 192.168.124.1 +I (5729) MODEL_LOADER: The storage free size is 22400 KB +I (5729) MODEL_LOADER: The partition size is 3072 KB +I (5739) MODEL_LOADER: Successfully load srmodels +I (5739) AudioProcessor: Non-realtime mode: Standard VAD enabled +I (5749) AudioProcessor: AFE configuration: AEC=disabled, VAD=enabled, core=1, priority=5 +I (5759) AFE: AFE Version: (1MIC_V250121) +I (5759) AFE: Input PCM Config: total 1 channels(1 microphone, 0 playback), sample rate:16000 +I (5769) AFE: AFE Pipeline: [input] -> |NS(WebRTC)| -> |VAD(WebRTC)| -> [output] +I (5769) AudioProcessor: Audio communication task started, feed size: 160 fetch size: 512 +I (5779) Application: 🔧 Using simple VAD for basic voice detection - complex echo-aware VAD disabled +I (5789) AudioProcessor: Echo-aware VAD params updated: snr_threshold=0.30, min_silence=200ms, cooldown=500ms +W (5799) AFE_CONFIG: wakenet model not found. please load wakenet model... +I (5809) AFE: AFE Version: (1MIC_V250121) +I (5809) AFE: Input PCM Config: total 1 channels(1 microphone, 0 playback), sample rate:16000 +I (5819) AFE: AFE Pipeline: [input] -> |VAD(WebRTC)| -> [output] +I (5819) WakeWordDetect: Audio detection task started, feed size: 512 fetch size: 512 +I (5829) Application: STATE: idle +I (6649) Application: Entering idle state, playing standby sound +I (6659) main_task: Returned from app_main() +I (16659) Application: Free internal: 68551 minimal internal: 65459 +I (18459) MovecallMojiESP32S3: BOOT button clicked +I (18459) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (18459) MovecallMojiESP32S3: 当前设备状态: 3 +I (18469) MovecallMojiESP32S3: 从待命状态切换到聆听状态 +I (18459) MovecallMojiESP32S3: 所有触摸状态已重置 +I (18469) MovecallMojiESP32S3: 强制重新初始化音频输出 +I (18479) I2S_IF: Pending out channel for in channel running +I (18489) AudioCodec: Set output enable to false +I (18539) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1 +I (18539) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1 +I (18549) Adev_Codec: Open codec device OK +I (18559) AudioCodec: Set output enable to true +I (18559) MovecallMojiESP32S3: 播放提示音:卡卡在呢 +I (18559) MovecallMojiESP32S3: 等待音频播放完成... +I (19199) MovecallMojiESP32S3: 音频队列已清空,等待硬件输出完成... +I (19699) MovecallMojiESP32S3: 音频播放完成 +I (19699) Application: STATE: connecting +I (19739) Application: Attempting to open audio channel +I (19739) WebSocket: Connecting to wss://airlab-xiaozhi.airlabs.art:443/xiaozhi/v1/ +I (19869) wifi:idx:1 (ifx:0, 70:2a:d7:85:bc:eb), tid:5, ssn:0, winSize:64 +I (19999) esp-x509-crt-bundle: Certificate validated +I (20869) Application: 🟢 音频通道已打开 +I (20869) Application: 🔄 禁用电源管理模式 +I (20869) wifi:Set ps type: 0, coexist: 0 + +I (20879) Application: 🟢 音频通道初始化完成 +I (20879) Application: Setting listening mode to 0 +I (20879) Application: STATE: listening +I (23339) Application: Simple VAD state change: speaking=true, device_state=5 +I (23519) Application: Simple VAD state change: speaking=false, device_state=5 +I (24099) Application: Simple VAD state change: speaking=true, device_state=5 +I (24199) Application: Simple VAD state change: speaking=false, device_state=5 +I (25339) Application: Simple VAD state change: speaking=true, device_state=5 +I (25419) MovecallMojiESP32S3: BOOT button clicked +I (25419) MovecallMojiESP32S3: 触摸任务已解锁,可以接收新的触摸 +I (25419) MovecallMojiESP32S3: 当前设备状态: 5 +I (25419) MovecallMojiESP32S3: 所有触摸状态已重置 +I (25419) MovecallMojiESP32S3: 🔵 BOOT button pressed in Listening state - switching to idle +I (25439) MovecallMojiESP32S3: 从聆听状态切换到待命状态 +I (25459) WS: Websocket disconnected +I (25459) WS: Audio processor stopped immediately +I (25459) Application: 🔴 音频通道关闭,开始清理任务 +I (25469) Application: 🔴 后台任务完成 +I (25499) WS: 🔧 WebSocket已安全删除 +I (25499) Application: 🔧 设备不在idle状态,跳过电源管理设置 +I (25499) Application: 🔄 设置设备为空闲状态 +I (25499) Application: STATE: idle +I (25499) Application: Entering idle state, playing standby sound +I (34659) Application: Free internal: 70803 minimal internal: 57567 \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/config copy.h b/main/boards/movecall-moji-esp32s3/config copy.h new file mode 100644 index 0000000..a0bdf5a --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/config copy.h @@ -0,0 +1,71 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// Movecall Moji configuration + +#include // 包含GPIO驱动库 + +// 音频采样率配置(16kHz) +#define AUDIO_INPUT_SAMPLE_RATE 16000 // 输入采样率 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 // 输出采样率 + +// I2S音频接口GPIO配置 +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_6 // 主时钟 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 // 字选择线 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14 // 位时钟 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_13 // 数据输入(麦克风) +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 // 数据输出(扬声器) + +// ES8311音频编解码器配置 +#define AUDIO_CODEC_PA_PIN GPIO_NUM_9 // 功放使能引脚 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_5 // I2C数据引脚 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_4 // I2C时钟引脚 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR // ES8311音频编解码器I2C地址 + +// 系统指示灯与启动按钮 +#define BUILTIN_LED_GPIO GPIO_NUM_21 // 板载LED (GPIO 21) +#define BOOT_BUTTON_GPIO GPIO_NUM_0 // 启动按钮 (GPIO 0) + +// 按键GPIO定义 +#define KEY1_GPIO GPIO_NUM_46 // KEY1 - 音量加(GPIO46) +#define KEY2_GPIO GPIO_NUM_45 // KEY2 - 音量减(GPIO45) +#define KEY4_GPIO GPIO_NUM_18 // KEY4 - 播放故事(发送文本消息) (GPIO18) + +// ADC电量检测引脚 +#define BATTERY_ADC_GPIO GPIO_NUM_10 // 电池电压检测引脚(GPIO10) +#define BATTERY_ADC_CHANNEL ADC_CHANNEL_9 // GPIO10对应ADC1_CHANNEL_9 +#define BATTERY_ADC_UNIT ADC_UNIT_1 // 使用ADC单元1 + +// 六路触摸按键定义 +#define TOUCH1_GPIO GPIO_NUM_1 // Touch1 +#define TOUCH2_GPIO GPIO_NUM_2 // Touch2 +#define TOUCH3_GPIO GPIO_NUM_3 // Touch3 (原显示器背光引脚) +#define TOUCH4_GPIO GPIO_NUM_7 // Touch4 (原显示器DC引脚) +#define TOUCH5_GPIO GPIO_NUM_8 // Touch5 +#define TOUCH6_GPIO GPIO_NUM_10 // Touch6 + +// UART引脚定义 (原4G接口引脚) +#define UART_TX_PIN GPIO_NUM_37 // UART TX 引脚 +#define UART_RX_PIN GPIO_NUM_36 // UART RX 引脚 + + +// 音量按键定义 +#define VOLUME_UP_BUTTON_GPIO KEY1_GPIO // 音量加 +#define VOLUME_DOWN_BUTTON_GPIO KEY2_GPIO // 音量减 + +// 显示器配置 - 无显示器板载,引脚设为无效 +#define DISPLAY_SDA_PIN GPIO_NUM_NC // 未连接 +#define DISPLAY_SCL_PIN GPIO_NUM_NC // 未连接 +#define DISPLAY_WIDTH 128 // 保留参数 +#define DISPLAY_HEIGHT 128 // 保留参数 +#define DISPLAY_MIRROR_X false // X轴镜像禁用 +#define DISPLAY_MIRROR_Y false // Y轴镜像禁用 +#define DISPLAY_SWAP_XY false // 坐标轴不交换 +#define DISPLAY_OFFSET_X 0 // X轴偏移 +#define DISPLAY_OFFSET_Y 0 // Y轴偏移 + +// 显示器背光控制(未使用) +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC // 背光控制引脚 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false // 输出不反 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/movecall-moji-esp32s3/config.h b/main/boards/movecall-moji-esp32s3/config.h new file mode 100644 index 0000000..710da6d --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/config.h @@ -0,0 +1,90 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// Movecall Moji configuration + +#include // 包含GPIO驱动库 + +// 音频采样率配置(16kHz) +#define AUDIO_INPUT_SAMPLE_RATE 16000 // 输入采样率 +#define AUDIO_OUTPUT_SAMPLE_RATE 16000 // 输出采样率 + +// I2S音频接口GPIO配置 +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_16 // 主时钟 MCLK GPIO16 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_45 // 字选择线 LRCK GPIO45 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_9 // 位时钟 SCLK GPIO09 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10 // 数据输入(麦克风) DSDIN GPIO8 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_8 // 数据输出(扬声器) ASDOUT GPIO8 + +// ES8311音频编解码器配置 +#define AUDIO_CODEC_PA_PIN GPIO_NUM_48 // 功放使能引脚 PA_CTRL GPIO48 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_17 // I2C数据引脚 ES_I2C_SDA GPIO17 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_18 // I2C时钟引脚 ES_I2C_CLK GPIO18 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR // ES8311音频编解码器I2C地址 +// ES7210音频编解码器(ADC)地址与参考通道开关 +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR +#define AUDIO_INPUT_REFERENCE 0 + +// 系统指示灯与启动按钮 +#define BUILTIN_LED_GPIO GPIO_NUM_21 // 板载LED (GPIO 21) ******* +#define BOOT_BUTTON_GPIO GPIO_NUM_0 // BOOT按钮 BOOT GPIO0 + +// 按键GPIO定义 +#define KEY1_GPIO GPIO_NUM_NC // KEY1 - 本项目不启用该 按键 +#define KEY2_GPIO GPIO_NUM_NC // KEY2 - 本项目不启用该 按键 +#define KEY4_GPIO GPIO_NUM_4 // KEY4 - 播放故事(发送文本消息) Stoey GPIO04 + +// ADC电量检测引脚 +#define BATTERY_ADC_GPIO GPIO_NUM_3 // 电池电压检测引脚(GPIO3) BAT_ADC GPIO3 +#define BATTERY_ADC_CHANNEL ADC_CHANNEL_2 // GPIO3对应ADC1_CHANNEL_2 +#define BATTERY_ADC_UNIT ADC_UNIT_1 // 使用ADC单元1 + +// 六路触摸按键定义 +#define TOUCH1_GPIO GPIO_NUM_1 // Touch1 GPIO01 +#define TOUCH2_GPIO GPIO_NUM_2 // Touch2 GPIO02 +#define TOUCH3_GPIO GPIO_NUM_15 // Touch3 GPIO15 +#define TOUCH4_GPIO GPIO_NUM_7 // Touch4 GPIO07 +#define TOUCH5_GPIO GPIO_NUM_NC // Touch5 (未连接) +#define TOUCH6_GPIO GPIO_NUM_NC // Touch6 (未连接) + +// UART引脚定义 (原4G接口引脚) +#define UART_TX_PIN GPIO_NUM_37 // UART TX 引脚 U0TXD GPIO37 +#define UART_RX_PIN GPIO_NUM_36 // UART RX 引脚 U0RXD GPIO36 + +// 音量按键定义 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC // 音量加 (未连接) +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC // 音量减 (未连接) + +// 显示器配置 - 无显示器板载,引脚设为无效 +#define DISPLAY_SDA_PIN GPIO_NUM_NC // 未连接 +#define DISPLAY_SCL_PIN GPIO_NUM_NC // 未连接 +#define DISPLAY_WIDTH 128 // 保留参数 +#define DISPLAY_HEIGHT 128 // 保留参数 +#define DISPLAY_MIRROR_X false // X轴镜像禁用 +#define DISPLAY_MIRROR_Y false // Y轴镜像禁用 +#define DISPLAY_SWAP_XY false // 坐标轴不交换 +#define DISPLAY_OFFSET_X 0 // X轴偏移 +#define DISPLAY_OFFSET_Y 0 // Y轴偏移 + +// 显示器背光控制(未使用) +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC // 背光控制引脚 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false // 输出不反 + +// // ES7210功能开关与默认参数(按需启用) +// #define AUDIO_ES7210_ENABLE 0 +// #define ES7210_INPUT_SAMPLE_RATE 16000 +// #define ES7210_OUTPUT_SAMPLE_RATE 16000 +// #define ES7210_MIC_GAIN_DB 18 +// #define ES7210_LINEIN_GAIN_DB 0 +// #define ES7210_MCLK_FREQUENCY_HZ 12288000 +// #define ES7210_BCLK_FREQUENCY_HZ 1024000 +// #define ES7210_LRCK_FREQUENCY_HZ ES7210_INPUT_SAMPLE_RATE +// #define ES7210_POWER_ENABLE_GPIO GPIO_NUM_NC +// #define ES7210_POWER_ON_LEVEL 1 +// #define ES7210_I2C_SDA_PIN GPIO_NUM_17 +// #define ES7210_I2C_SCL_PIN GPIO_NUM_18 +// #define ES7210_I2C_ADDRESS 0x40 +// #define ES7210_I2C_PORT I2C_NUM_0 +// #define ES7210_I2C_SPEED_HZ 400000 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/movecall-moji-esp32s3/config.json b/main/boards/movecall-moji-esp32s3/config.json new file mode 100644 index 0000000..6de9c27 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/config.json @@ -0,0 +1,51 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "movecall-moji-esp32s3", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/16m.csv\"", + "CONFIG_SPIRAM=y", + "CONFIG_SPIRAM_MODE_QUAD=y", + "CONFIG_SPIRAM_SPEED_80M=y", + "CONFIG_USE_AFE_WAKE_WORD=y", + "CONFIG_USE_AUDIO_PROCESSOR=y", + "CONFIG_USE_REALTIME_CHAT=y", + "CONFIG_MODEL_IN_FLASH=y", + "CONFIG_AFE_INTERFACE_V1=y", + + "# 方案一:你好小智 (默认,TTS训练版)", + "CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=y", + + "# 方案二:Hi,乐鑫 (取消注释启用)", + "# CONFIG_SR_WN_WN9_HILEXIN=y", + + "# 方案三:Hi,ESP (取消注释启用)", + "# CONFIG_SR_WN_WN9_HIESP=y", + + "# 方案四:Hi,Jason (取消注释启用)", + "# CONFIG_SR_WN_WN9_HIJASON_TTS2=y", + + "# 方案五:Alexa (取消注释启用)", + "# CONFIG_SR_WN_WN9_ALEXA=y", + + "# 方案六:小爱同学 (取消注释启用)", + "# CONFIG_SR_WN_WN9_XIAOAITONGXUE=y", + + "CONFIG_SR_NSN_WEBRTC=y", + "CONFIG_SR_VADN_WEBRTC=y", + "CONFIG_ESP32S3_SPIRAM_SUPPORT=y", + "CONFIG_SPIRAM_BOOT_INIT=y", + "CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096", + "CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=49152", + "CONFIG_SPIRAM_USE_MALLOC=y", + "CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y", + "CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y", + "CONFIG_ESP32S3_DATA_CACHE_64KB=y", + "CONFIG_ESP32S3_DATA_CACHE_8WAYS=y", + "CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/imu_sensor_thing.cc b/main/boards/movecall-moji-esp32s3/imu_sensor_thing.cc new file mode 100644 index 0000000..3bc8245 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/imu_sensor_thing.cc @@ -0,0 +1,135 @@ +#include "imu_sensor_thing.h" +#include "esp_log.h" +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "ImuSensorThing" + +namespace iot { + +ImuSensorThing::ImuSensorThing(QMI8658A* sensor) + : Thing("ImuSensor", "姿态传感器"), + imu_sensor_(sensor), + motion_detected_(false), + motion_threshold_(1.5f) { + + // 初始化数据 + memset(&latest_data_, 0, sizeof(latest_data_)); + + // 定义属性:加速度计数据 + properties_.AddNumberProperty("accel_x", "X轴加速度 (mg)", [this]() -> int { + return static_cast(latest_data_.acc_x * 1000); + }); + properties_.AddNumberProperty("accel_y", "Y轴加速度 (mg)", [this]() -> int { + return static_cast(latest_data_.acc_y * 1000); + }); + properties_.AddNumberProperty("accel_z", "Z轴加速度 (mg)", [this]() -> int { + return static_cast(latest_data_.acc_z * 1000); + }); + + // 定义属性:陀螺仪数据 + properties_.AddNumberProperty("gyro_x", "X轴角速度 (mdps)", [this]() -> int { + return static_cast(latest_data_.gyro_x * 1000); + }); + + properties_.AddNumberProperty("gyro_y", "Y轴角速度 (mdps)", [this]() -> int { + return static_cast(latest_data_.gyro_y * 1000); + }); + + properties_.AddNumberProperty("gyro_z", "Z轴角速度 (mdps)", [this]() -> int { + return static_cast(latest_data_.gyro_z * 1000); + }); + + // 定义属性:运动检测状态 + properties_.AddBooleanProperty("motion_detected", "是否检测到运动", [this]() -> bool { + return motion_detected_; + }); + + // 定义属性:传感器状态 + properties_.AddBooleanProperty("sensor_available", "传感器是否可用", [this]() -> bool { + return imu_sensor_ != nullptr; + }); + + // 定义方法:校准传感器 + methods_.AddMethod("Calibrate", "校准传感器", ParameterList(), [this](const ParameterList& parameters) { + if (imu_sensor_) { + ESP_LOGI(TAG, "开始校准IMU传感器"); + imu_sensor_->StartBufferedReading(20); + imu_sensor_->StartCalibration(6000); + bool running = false; + float progress = 0.0f; + do { + imu_sensor_->GetCalibrationStatus(&running, &progress); + vTaskDelay(pdMS_TO_TICKS(200)); + } while (running); + qmi8658a_calibration_t calib; + imu_sensor_->GetCalibrationData(&calib); + imu_sensor_->ApplyCalibration(&calib); + imu_sensor_->StopBufferedReading(); + ESP_LOGI(TAG, "校准完成"); + } + }); + + // 定义方法:设置运动检测阈值 + methods_.AddMethod("SetMotionThreshold", "设置运动检测阈值", ParameterList({ + Parameter("threshold", "运动检测阈值 (g)", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + float threshold = static_cast(parameters["threshold"].number()) / 1000.0f; + if (threshold > 0.1f && threshold < 10.0f) { + motion_threshold_ = threshold; + ESP_LOGI(TAG, "设置运动检测阈值为: %.3f g", motion_threshold_); + } else { + ESP_LOGW(TAG, "运动检测阈值超出范围 (0.1-10.0 g)"); + } + }); + + // 定义方法:获取传感器信息 + methods_.AddMethod("GetSensorInfo", "获取传感器信息", ParameterList(), [this](const ParameterList& parameters) { + if (imu_sensor_) { + ESP_LOGI(TAG, "IMU传感器: QMI8658A"); + ESP_LOGI(TAG, "当前运动阈值: %.3f g", motion_threshold_); + } + }); + + methods_.AddMethod("DumpRegisters", "寄存器转储", ParameterList(), [this](const ParameterList& parameters) { + if (imu_sensor_) { + imu_sensor_->DumpRegisters(); + } + }); + + methods_.AddMethod("BaselineDiagnostics", "静止基线诊断", ParameterList(), [this](const ParameterList& parameters) { + if (imu_sensor_) { + imu_sensor_->RunBaselineDiagnostics(200, 10); + } + }); +} + +void ImuSensorThing::UpdateData(const qmi8658a_data_t& data) { + latest_data_ = data; + + // 计算加速度幅值来检测运动 + float accel_magnitude = sqrt(data.acc_x * data.acc_x + + data.acc_y * data.acc_y + + data.acc_z * data.acc_z); + + // 检测运动(排除重力影响,1g ≈ 9.8m/s²) + float motion_level = fabs(accel_magnitude - 1.0f); + bool current_motion = motion_level > motion_threshold_; + + if (current_motion != motion_detected_) { + motion_detected_ = current_motion; + ESP_LOGI(TAG, "运动状态变化: %s (幅值: %.3f g)", + motion_detected_ ? "检测到运动" : "静止", motion_level); + } +} + +void ImuSensorThing::SetMotionDetected(bool detected) { + if (motion_detected_ != detected) { + motion_detected_ = detected; + ESP_LOGI(TAG, "运动检测状态更新: %s", detected ? "运动中" : "静止"); + } +} + +} // namespace iot diff --git a/main/boards/movecall-moji-esp32s3/imu_sensor_thing.h b/main/boards/movecall-moji-esp32s3/imu_sensor_thing.h new file mode 100644 index 0000000..c0046fb --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/imu_sensor_thing.h @@ -0,0 +1,26 @@ +#ifndef IMU_SENSOR_THING_H +#define IMU_SENSOR_THING_H + +#include "iot/thing.h" +#include "boards/common/qmi8658a.h" + +namespace iot { + +class ImuSensorThing : public Thing { +private: + QMI8658A* imu_sensor_; + qmi8658a_data_t latest_data_; + bool motion_detected_; + float motion_threshold_; + +public: + ImuSensorThing(QMI8658A* sensor); + virtual ~ImuSensorThing() = default; + + void UpdateData(const qmi8658a_data_t& data); + void SetMotionDetected(bool detected); +}; + +} // namespace iot + +#endif // IMU_SENSOR_THING_H \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc new file mode 100644 index 0000000..d9ef9fd --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc @@ -0,0 +1,2364 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "audio_codecs/box_audio_codec.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "display/display.h" +#include "boards/common/power_save_timer.h" // 添加电源管理头文件 +#include "assets/lang_config.h" // 引入语音配置头文件 新增 +#include "volume_config.h" // 引入音量配置头文件 +#include "boards/common/qmi8658a.h" // 引入QMI8658A姿态传感器头文件 +#include "boards/common/veml7700.h" // 引入VEML7700环境光传感器头文件 +#include "imu_sensor_thing.h" // 引入IMU传感器IoT设备头文件 +#include "system_info.h" // 引入系统信息头文件 +#include "settings.h" +#include // 添加数学函数头文件 + +#include +#include +#include +#include +#include +#include +#include +#include // 添加PRIu32宏的定义支持 +#include +#include "driver/gpio.h" +#include +#include +#include +#include +#include +#include "freertos/queue.h" + +#define TAG "Airhub1" +#define Pro_TAG "Airhub" + +#include +#include + +// 触摸事件类型 +typedef enum { + TOUCH_EVENT_PRESS = 0, // 触摸按下事件 + TOUCH_EVENT_RELEASE // 触摸释放事件 +} touch_event_type_t; + +// 触摸状态枚举 +typedef enum { + TOUCH_STATE_IDLE, // 空闲状态 - 未触摸 + TOUCH_STATE_PRESSED, // 已按下状态 - 已经触发事件,等待释放 + TOUCH_STATE_RELEASED, // 释放过渡状态 - 确认松手 + TOUCH_STATE_DEBOUNCE // 去抖状态 - 等待信号稳定 +} touch_state_t; + +// 触摸事件数据结构 +typedef struct { + int pad_num; // 触摸板编号 + touch_event_type_t type; // 事件类型:按下或释放 +} touch_event_data_t; + +// 前向声明TouchEventTask函数 +static void TouchEventTask(void* arg); + +class MovecallMojiESP32S3 : public WifiBoard { +private: + // 触摸状态相关 + touch_state_t touch_states_[4]; // 每个触摸点的状态 + uint32_t touch_last_time_[4]; // 每个触摸点的最后操作时间 + uint32_t raw_touch_values_[4]; // 原始触摸值 + uint32_t touch_thresholds_[4]; // 触摸阈值 + + // 去抖动和最短释放时间参数 + const uint32_t DEBOUNCE_TIME_MS = 100; // 去抖时间(毫秒) + const uint32_t MIN_RELEASE_TIME_MS = 300; // 最短释放确认时间 + + // 添加触摸任务锁定相关变量 + bool touch_task_locked_ = false; // 触摸任务锁定标志 + int active_touch_pad_ = -1; // 当前活跃的触摸点编号 + uint32_t touch_task_start_time_ = 0; // 触摸任务开始时间 + const uint32_t TOUCH_TASK_TIMEOUT_MS = 10000; // 任务超时时间(10秒) + + PowerSaveTimer* power_save_timer_; + static MovecallMojiESP32S3* instance_; + static void IRAM_ATTR TouchPadISR(void* arg); + i2c_master_bus_handle_t codec_i2c_bus_; + + // QMI8658A姿态传感器相关 + QMI8658A* imu_sensor_; + esp_timer_handle_t imu_timer_handle_; + qmi8658a_data_t latest_imu_data_; + bool imu_initialized_; + const int kImuReadInterval = 160; // 160ms读取一次IMU数据,匹配125Hz采样率 + iot::ImuSensorThing* imu_thing_; // IMU传感器IoT设备实例 + + // VEML7700环境光传感器相关 + VEML7700* light_sensor_; + bool light_sensor_initialized_; + + // 石头匹配功能相关 + static constexpr int kDefaultRatioThresholdPercent = 15; // 光谱比值匹配阈值(%) + static constexpr int kDefaultLuxThresholdPercent = 50; // 亮度等级容差阈值(%) + static constexpr const char* kStoneNvsNamespace = "stone"; // NVS命名空间 + + // 亮度等级划分(用于判断是否处于相近光照环境) + // 0=极暗(<5lux) 1=暗(5-50) 2=中(50-500) 3=亮(500-5000) 4=极亮(>5000) + static int GetBrightnessLevel(float lux) { + if (lux < 5.0f) return 0; + if (lux < 50.0f) return 1; + if (lux < 500.0f) return 2; + if (lux < 5000.0f) return 3; + return 4; + } + + static const char* GetBrightnessLevelName(int level) { + static const char* names[] = {"极暗(<5lux)", "暗(5-50)", "中(50-500)", "亮(500-5000)", "极亮(>5000)"}; + if (level >= 0 && level <= 4) return names[level]; + return "未知"; + } + + // 电量检测相关 + adc_oneshot_unit_handle_t adc_handle_; + adc_cali_handle_t adc_cali_handle_; // ADC校准句柄 + esp_timer_handle_t battery_timer_handle_; + std::vector adc_values_; // ADC采样值队列(存储校准后的mV值) + uint32_t battery_level_;// 电池电量百分比 + int battery_ticks_; + int battery_alert_ticks_; + int status_report_ticks_; // 设备状态上报计数器 + bool status_report_enabled_; // 设备状态上报是否启用 + bool rtc_online_; // RTC火山引擎连接状态(开机默认false,连接成功后设为true) + const int kBatteryAdcInterval = 10; // 10秒检测一次 + const int kStatusReportInterval = 30; // 30秒上报一次 + const int kStatusReportDelay = 3; // 启动3秒后才开始上报 + const int kBatteryAdcDataCount = 20; // 保存20个ADC值用于平均(增加采样次数) + const int kBatteryAdcSampleCount = 10; // 每次读取采样10次(增加采样次数) + const char* DEVICE_STATUS_REPORT_URL = CONFIG_DEVICE_STATUS_REPORT_URL; // 设备状态上报服务器URL + Button boot_button_{BOOT_BUTTON_GPIO}; // 初始化列表 + Button volume_up_button_{VOLUME_UP_BUTTON_GPIO}; + Button volume_down_button_{VOLUME_DOWN_BUTTON_GPIO}; + Button story_button_{KEY4_GPIO, false, 2000}; // 长按2秒触发石头匹配 + + bool production_test_mode_ = false;// 是否开启生产测试模式 + static const int TOUCH_QUEUE_SIZE = 5;// 触摸事件队列大小 + + // 生产测试模式触摸检测标志位 + bool touch_detected_flag_ = false; // 触摸检测标志位 + int touched_pad_index_ = -1; // 被触摸的触摸板索引 + + void EnterProductionTestMode();// 进入生产测试模式函数 + + void ReportDeviceStatus(int battery_level);// 上报设备状态到服务器 + +public: + // 将静态队列句柄移到public以便静态函数访问 + static QueueHandle_t touch_event_queue_; + + // 触摸事件处理方法 + void HandleTouchEvent(int touch_pad_num, touch_event_type_t event_type); + // 重置所有触摸状态 + void ResetAllTouchStates(); + // 锁定触摸任务,指定当前活跃的触摸点 + void LockTouchTask(int touch_pad_num); + // 解锁触摸任务,允许处理新的触摸 + void UnlockTouchTask(); + + // 获取电池电量百分比 + bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override { + // 确保在首次查询时已采样到足够ADC数据,避免返回0导致误判 + if (adc_values_.size() < kBatteryAdcDataCount && adc_handle_ != nullptr) { + for (int i = 0; i < kBatteryAdcDataCount; ++i) { + ReadBatteryAdcData(); + } + } + + level = static_cast(battery_level_); + charging = false; // 暂时设为false,可根据需要实现充电检测 + discharging = true; // 暂时设为true,可根据需要实现放电检测 + return true; + } + +public: + // 构造函数 + MovecallMojiESP32S3() : + power_save_timer_(nullptr), + codec_i2c_bus_(nullptr), + imu_sensor_(nullptr), + imu_timer_handle_(nullptr), + imu_initialized_(false), + imu_thing_(nullptr), + light_sensor_(nullptr), + light_sensor_initialized_(false), + adc_handle_(nullptr), + battery_timer_handle_(nullptr), + battery_level_(0), + battery_ticks_(0), + battery_alert_ticks_(0), + status_report_ticks_(0), + status_report_enabled_(false), + rtc_online_(false), + production_test_mode_(false), + touch_detected_flag_(false), + touched_pad_index_(-1) + { + // 初始化触摸状态 + for (int i = 0; i < 4; ++i) { + touch_states_[i] = TOUCH_STATE_IDLE; + touch_last_time_[i] = 0; + raw_touch_values_[i] = 0; + touch_thresholds_[i] = 0; + } + + // 初始化触摸任务锁 + touch_task_locked_ = false; + active_touch_pad_ = -1; + touch_task_start_time_ = 0; + + // 使用240MHz作为CPU最大频率,10秒进入睡眠,-1表示不自动关机 + power_save_timer_ = new PowerSaveTimer(240, 10, -1); + + // 设置低功耗模式回调 + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "🔋 进入低功耗模式:CPU降频、Light Sleep启用、功放关闭"); + + // 关闭功放,进一步节省电量 + auto codec = GetAudioCodec(); + if (codec) { + codec->EnableOutput(false); + ESP_LOGI(TAG, "🔊 功放已关闭"); + } + }); + + power_save_timer_->OnExitSleepMode([this]() { + ESP_LOGI(TAG, "🔋 退出低功耗模式:CPU恢复正常、Light Sleep禁用、功放打开"); + + // 打开功放,恢复正常音频输出 + auto codec = GetAudioCodec(); + if (codec) { + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔊 功放已打开"); + } + }); + + // 初始化按钮 + InitializeButtons(); + InitializeStoryButton(); + + // 初始化I2C总线(必须在IMU传感器初始化之前) + InitializeCodecI2c(); + + // 初始化IoT功能,启用语音音量控制 + InitializeIot(); + + // 初始化电量检测 + InitializeBatteryMonitor(); + + // 初始化IMU传感器 + InitializeImuSensor(); + + // 初始化VEML7700环境光传感器 + InitializeLightSensor(); + + // 启用PowerSaveTimer,启用完整的低功耗管理 + power_save_timer_->SetEnabled(true); + ESP_LOGI(TAG, "🔋 PowerSaveTimer已启用,20秒无活动将进入低功耗模式"); + + // 延迟调用触摸板初始化,避免在构造函数中就调用 + ESP_LOGI(TAG, "在构造函数完成后调用触摸初始化"); + // 使用task来延迟初始化触摸功能 + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + // 延迟一段时间,确保其他组件初始化完成 + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_LOGI(TAG, "开始延迟初始化触摸板..."); + if (board) { + board->InitializeTouchPads(); + } + vTaskDelete(NULL); + }, "touch_init", 4096, this, 5, NULL); + } + + // 发送触摸消息 + void SendTouchMessage(int touch_pad_num) { + const char* message = nullptr; + power_save_timer_->WakeUp(); + + // 获取当前应用状态 + auto& app = Application::GetInstance(); + auto current_state = app.GetDeviceState(); + + // 仅在 Dialog 对话状态且内部 listening 开启时有效 + if (!(current_state == kDeviceStateDialog && app.IsDialogUploadEnabled())) { + ESP_LOGI(TAG, "触摸事件无效:仅在Dialog+listening内部状态下有效"); + if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + if (board) { + board->UnlockTouchTask(); + } + vTaskDelete(NULL); + }, "unlock_invalid_state", 4096, this, 5, NULL); + } + return; + } + + // 根据流程图中的情况处理触摸事件: + // 1. 如果当前是Speaking状态,触摸事件不生效 + if (current_state == kDeviceStateSpeaking) { + ESP_LOGI(TAG, "当前处于Speaking状态,触摸事件被忽略"); + // 由于任务未能执行,立即解锁触摸任务 + if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { + ESP_LOGI(TAG, "触摸任务无法执行,创建任务来解锁"); + // 创建任务来解锁,避免直接调用可能导致栈溢出的操作 + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + if (board) { + board->UnlockTouchTask(); + } + vTaskDelete(NULL); + }, "unlock_failed", 4096, this, 5, NULL); + } + return; + } + + // 2. 如果当前是Listening状态且已检测到语音输入,触摸事件不生效 + if (current_state == kDeviceStateListening && app.IsVoiceDetected()) { + ESP_LOGI(TAG, "当前处于Listening状态且已检测到语音输入,触摸事件被忽略"); + // 由于任务未能执行,立即解锁触摸任务 + if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { + ESP_LOGI(TAG, "触摸任务无法执行,创建任务来解锁"); + // 创建任务来解锁,避免直接调用可能导致栈溢出的操作 + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + if (board) { + board->UnlockTouchTask(); + } + vTaskDelete(NULL); + }, "unlock_failed", 4096, this, 5, NULL); + } + return; + } + + // 根据触摸点选择消息 + switch (touch_pad_num) { + case 0: message = "有人在摸摸你的脑袋"; break; + case 1: message = "有人在摸摸你的肚子"; break; + case 2: message = "有人在摸摸你的下巴"; break; + case 3: message = "有人在摸摸你的后背"; break; + } + + // 发送消息 + if (message != nullptr) { + ESP_LOGI(TAG, "发送触摸消息: \"%s\"", message); + + // 仅在 Dialog+内部listening 下发送;其他状态在前面已返回 + + // SendTextMessage内部会自动检查协议是否初始化 + app.SendTextMessage(message); + ESP_LOGI(TAG, "消息已发送"); + + // 消息已发送,开始监听语音回复 + // 任务将在收到回复或超时后结束 + // 通过TaskStateMonitor监听设备状态变化 + + // 创建一个任务来监控设备状态变化 + if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { + ESP_LOGI(TAG, "创建任务状态监控"); + + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + auto& app = Application::GetInstance(); + uint32_t start_time = esp_timer_get_time() / 1000; + + // 等待设备状态变为Speaking或超时 + // 如果超时或设备重新回到Idle状态,则解锁触摸任务 + while (true) { + auto state = app.GetDeviceState(); + uint32_t current_time = esp_timer_get_time() / 1000; + uint32_t elapsed = current_time - start_time; + + // 如果设备开始说话,等待它说完 + if (state == kDeviceStateSpeaking) { + ESP_LOGI(TAG, "检测到设备进入Speaking状态,等待说话完成"); + // 等待设备回到Idle状态 + while (app.GetDeviceState() == kDeviceStateSpeaking) { + vTaskDelay(100 / portTICK_PERIOD_MS); + + // 检查超时 + uint32_t now = esp_timer_get_time() / 1000; + if (now - start_time > 30000) { // 30秒超时 + ESP_LOGW(TAG, "等待说话完成超时"); + break; + } + } + ESP_LOGI(TAG, "设备说话已完成,解锁触摸任务"); + board->UnlockTouchTask(); + break; + } + // 如果设备回到Idle状态,可能是消息被忽略 + else if (state == kDeviceStateIdle && elapsed > 1000) { + ESP_LOGW(TAG, "设备回到Idle状态,消息可能被忽略"); + board->UnlockTouchTask(); + break; + } + // 如果等待太久,自动解锁 + else if (elapsed > 10000) { // 10秒超时 + ESP_LOGW(TAG, "等待回复超时,解锁触摸任务"); + board->UnlockTouchTask(); + break; + } + + vTaskDelay(200 / portTICK_PERIOD_MS); + } + vTaskDelete(NULL); + }, "task_monitor", 8192, this, 5, NULL); + } + } else { + // 无效的触摸点或消息,自动解锁 + if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { + // 创建任务来解锁 + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + if (board) { + board->UnlockTouchTask(); + } + vTaskDelete(NULL); + }, "unlock_invalid", 4096, this, 5, NULL); + } + } + } + + // 析构函数 + ~MovecallMojiESP32S3() { + delete power_save_timer_; + + // 清理IMU传感器资源 + if (imu_timer_handle_) { + esp_timer_stop(imu_timer_handle_); + esp_timer_delete(imu_timer_handle_); + } + if (imu_sensor_) { + delete imu_sensor_; + } + if (imu_thing_) { + delete imu_thing_; + } + + // 清理VEML7700环境光传感器资源 + if (light_sensor_) { + delete light_sensor_; + } + + // 清理电量检测资源 + if (battery_timer_handle_) { + esp_timer_stop(battery_timer_handle_); + esp_timer_delete(battery_timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + void InitializeCodecI2c() { + ESP_LOGI(TAG, "Initializing I2C master bus for audio codec...");// + // 初始化I2C外设 编解码器 + i2c_master_bus_config_t i2c_bus_cfg = { + // .i2c_port = I2C_NUM_0, + .i2c_port = I2C_NUM_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_)); + + ScanI2cDevices(); // 新增 扫描I2C总线上的设备 新增陀螺仪/姿态传感器 业务代码 + } + + // 新增 扫描I2C总线上的设备 函数 + // ============================================================================== + void ScanI2cDevices() { + ESP_LOGI(TAG, "Scanning I2C bus for devices..."); + + int devices_found = 0; + // 只扫描指定的三个设备地址 + uint8_t target_addresses[] = { + 0x18, // ES8311音频编解码器地址 + 0x6A, // QMI8658A姿态传感器地址 + 0x6B, // QMI8658A姿态传感器备用地址 + 0x40, // ES7210 ADC地址 + 0x10, // VEML7700环境光传感器地址 + }; + + size_t addr_count = sizeof(target_addresses) / sizeof(target_addresses[0]); + + for (size_t i = 0; i < addr_count; i++) { + uint8_t addr = target_addresses[i]; + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = 100000, // 使用较低的速度进行扫描 + }; + + i2c_master_dev_handle_t dev_handle; + esp_err_t ret = i2c_master_bus_add_device(codec_i2c_bus_, &dev_cfg, &dev_handle); + if (ret == ESP_OK) { + // 尝试读取一个字节来检测设备是否响应 + uint8_t dummy_data; + ret = i2c_master_receive(dev_handle, &dummy_data, 1, 100); + if (ret == ESP_OK || ret == ESP_ERR_TIMEOUT) { + ESP_LOGI(TAG, "I2C设备在线: 0x%02X", addr); + devices_found++; + } + i2c_master_bus_rm_device(dev_handle); + } + } + + ESP_LOGI(TAG, "I2C scan completed. Found %d devices", devices_found); + + if (devices_found == 0) { + ESP_LOGW(TAG, "No I2C devices found. Check hardware connections."); + } + } + // ============================================================================== + + // VEML7700环境光传感器初始化 + // ============================================================================== + void InitializeLightSensor() { + ESP_LOGI(TAG, "初始化VEML7700环境光传感器..."); + + light_sensor_ = new VEML7700(codec_i2c_bus_); + esp_err_t ret = light_sensor_->Init(); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "VEML7700初始化失败,传感器可能未连接"); + delete light_sensor_; + light_sensor_ = nullptr; + light_sensor_initialized_ = false; + return; + } + + light_sensor_initialized_ = true; + + // 使用自动量程读取一次数据验证传感器工作正常 + veml7700_auto_data_t als_result, white_result; + ret = light_sensor_->ReadAutoAll(&als_result, &white_result); + if (ret == ESP_OK) { + ESP_LOGI(TAG, "VEML7700 验证成功(自动量程) - ALS: %u (%.2f lux, gain=%d, it=%d), White: %u (%.2f lux)", + als_result.raw, als_result.lux, als_result.gain, als_result.it, + white_result.raw, white_result.lux); + } + } + // ============================================================================== + + // 双击KEY4:录入本命石头光源信息到NVS + // 存储光谱比值(ALS/White)和亮度等级,而非绝对Lux值 + // ============================================================================== + void RegisterMyStone() { + ESP_LOGI(TAG, "=== 开始录入本命石头 ==="); + + if (!light_sensor_initialized_ || !light_sensor_) { + ESP_LOGE(TAG, "VEML7700传感器未初始化,无法录入石头"); + return; + } + + // 等待2秒,让用户将石头放到传感器上 + ESP_LOGI(TAG, "请将石头放到传感器上方,2秒后开始检测..."); + vTaskDelay(pdMS_TO_TICKS(2000)); + + // 连续采样3次取中位数,过滤偶发异常 + ESP_LOGI(TAG, "正在测量石头光源信息(3次采样取中位数)..."); + float als_samples[3], white_samples[3]; + for (int i = 0; i < 3; i++) { + veml7700_auto_data_t als_r, white_r; + esp_err_t ret = light_sensor_->ReadAutoAll(&als_r, &white_r); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "石头光源测量失败(第%d次): %s", i + 1, esp_err_to_name(ret)); + return; + } + als_samples[i] = als_r.lux; + white_samples[i] = white_r.lux; + ESP_LOGI(TAG, " 采样%d: ALS=%.2f lux, White=%.2f lux", i + 1, als_r.lux, white_r.lux); + if (i < 2) vTaskDelay(pdMS_TO_TICKS(200)); + } + + // 简单排序取中位数 + for (int i = 0; i < 2; i++) { + for (int j = i + 1; j < 3; j++) { + if (als_samples[i] > als_samples[j]) { float t = als_samples[i]; als_samples[i] = als_samples[j]; als_samples[j] = t; } + if (white_samples[i] > white_samples[j]) { float t = white_samples[i]; white_samples[i] = white_samples[j]; white_samples[j] = t; } + } + } + float als_lux = als_samples[1]; // 中位数 + float white_lux = white_samples[1]; + + // 计算光谱比值(石头的固有光学特征) + float ratio = 0.0f; + if (white_lux > 0.01f) { + ratio = als_lux / white_lux; + } + + int brightness_level = GetBrightnessLevel(als_lux); + + // 比值乘10000转整数存储(保留4位小数精度) + int32_t ratio_x10000 = (int32_t)(ratio * 10000.0f); + int32_t als_lux_x100 = (int32_t)(als_lux * 100.0f); + int32_t white_lux_x100 = (int32_t)(white_lux * 100.0f); + + // 存入NVS + Settings stone_settings(kStoneNvsNamespace, true); + stone_settings.SetInt("ratio", ratio_x10000); + stone_settings.SetInt("als_lux", als_lux_x100); + stone_settings.SetInt("white_lux", white_lux_x100); + stone_settings.SetInt("br_level", brightness_level); + stone_settings.SetInt("valid", 1); + stone_settings.SetInt("ratio_th", kDefaultRatioThresholdPercent); + stone_settings.SetInt("lux_th", kDefaultLuxThresholdPercent); + + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " 本命石头录入成功!"); + ESP_LOGI(TAG, " ALS: %.2f lux", als_lux); + ESP_LOGI(TAG, " White: %.2f lux", white_lux); + ESP_LOGI(TAG, " 光谱比值(ALS/White): %.4f", ratio); + ESP_LOGI(TAG, " 亮度等级: %d - %s", brightness_level, GetBrightnessLevelName(brightness_level)); + ESP_LOGI(TAG, " 比值阈值: %d%%, 亮度容差阈值: %d%%", kDefaultRatioThresholdPercent, kDefaultLuxThresholdPercent); + ESP_LOGI(TAG, "========================================"); + } + // ============================================================================== + + // 长按KEY4 2秒:识别他人石头并与本命石比对 + // 双维度匹配:光谱比值(石头固有属性)+ 亮度等级(环境相近性) + // ============================================================================== + void MatchStone() { + ESP_LOGI(TAG, "=== 开始石头匹配 ==="); + + if (!light_sensor_initialized_ || !light_sensor_) { + ESP_LOGE(TAG, "VEML7700传感器未初始化,无法匹配"); + return; + } + + // 读取NVS中的本命石数据 + Settings stone_settings(kStoneNvsNamespace, false); + int32_t valid = stone_settings.GetInt("valid", 0); + if (valid != 1) { + ESP_LOGW(TAG, "尚未录入本命石头!请先双击KEY4录入本命石"); + return; + } + + int32_t my_ratio_x10000 = stone_settings.GetInt("ratio", 0); + int32_t my_als_lux_x100 = stone_settings.GetInt("als_lux", 0); + int32_t my_white_lux_x100 = stone_settings.GetInt("white_lux", 0); + int32_t my_br_level = stone_settings.GetInt("br_level", 0); + int32_t ratio_threshold = stone_settings.GetInt("ratio_th", kDefaultRatioThresholdPercent); + int32_t lux_threshold = stone_settings.GetInt("lux_th", kDefaultLuxThresholdPercent); + + float my_ratio = (float)my_ratio_x10000 / 10000.0f; + float my_als_lux = (float)my_als_lux_x100 / 100.0f; + float my_white_lux = (float)my_white_lux_x100 / 100.0f; + + ESP_LOGI(TAG, "本命石数据 - 比值: %.4f, ALS: %.2f, White: %.2f, 亮度等级: %d(%s)", + my_ratio, my_als_lux, my_white_lux, (int)my_br_level, GetBrightnessLevelName(my_br_level)); + + // 等待2秒,让用户将对方石头放到传感器上 + ESP_LOGI(TAG, "请将对方石头放到传感器上方,2秒后开始检测..."); + vTaskDelay(pdMS_TO_TICKS(2000)); + + // 连续采样3次取中位数 + ESP_LOGI(TAG, "正在测量对方石头光源信息(3次采样取中位数)..."); + float als_samples[3], white_samples[3]; + for (int i = 0; i < 3; i++) { + veml7700_auto_data_t als_r, white_r; + esp_err_t ret = light_sensor_->ReadAutoAll(&als_r, &white_r); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "对方石头测量失败(第%d次): %s", i + 1, esp_err_to_name(ret)); + return; + } + als_samples[i] = als_r.lux; + white_samples[i] = white_r.lux; + ESP_LOGI(TAG, " 采样%d: ALS=%.2f lux, White=%.2f lux", i + 1, als_r.lux, white_r.lux); + if (i < 2) vTaskDelay(pdMS_TO_TICKS(200)); + } + + // 排序取中位数 + for (int i = 0; i < 2; i++) { + for (int j = i + 1; j < 3; j++) { + if (als_samples[i] > als_samples[j]) { float t = als_samples[i]; als_samples[i] = als_samples[j]; als_samples[j] = t; } + if (white_samples[i] > white_samples[j]) { float t = white_samples[i]; white_samples[i] = white_samples[j]; white_samples[j] = t; } + } + } + float other_als = als_samples[1]; + float other_white = white_samples[1]; + + // 计算对方石头的光谱比值 + float other_ratio = 0.0f; + if (other_white > 0.01f) { + other_ratio = other_als / other_white; + } + int other_br_level = GetBrightnessLevel(other_als); + + ESP_LOGI(TAG, "对方石数据 - 比值: %.4f, ALS: %.2f, White: %.2f, 亮度等级: %d(%s)", + other_ratio, other_als, other_white, other_br_level, GetBrightnessLevelName(other_br_level)); + + // ======== 维度1:光谱比值匹配(石头固有光学特征)======== + float ratio_max = (my_ratio > other_ratio) ? my_ratio : other_ratio; + float ratio_diff_percent = 0.0f; + if (ratio_max > 0.001f) { + float ratio_diff = (my_ratio > other_ratio) ? (my_ratio - other_ratio) : (other_ratio - my_ratio); + ratio_diff_percent = (ratio_diff / ratio_max) * 100.0f; + } + bool ratio_match = (ratio_diff_percent <= (float)ratio_threshold); + + // ======== 维度2:亮度等级匹配(环境相近性)======== + // 允许相差1个等级(如"暗"和"中"可以匹配) + int level_diff = (my_br_level > other_br_level) ? (my_br_level - other_br_level) : (other_br_level - my_br_level); + bool level_match = (level_diff <= 1); + + // 如果亮度等级相同,再用Lux值做细粒度容差检查 + float lux_diff_percent = 0.0f; + float lux_max = (my_als_lux > other_als) ? my_als_lux : other_als; + if (lux_max > 0.01f) { + float lux_diff = (my_als_lux > other_als) ? (my_als_lux - other_als) : (other_als - my_als_lux); + lux_diff_percent = (lux_diff / lux_max) * 100.0f; + } + + // 综合判定:比值匹配 AND 亮度等级相近 + bool overall_match = ratio_match && level_match; + + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " 匹配分析:"); + ESP_LOGI(TAG, " [维度1] 光谱比值: 本命=%.4f 对方=%.4f 差异=%.1f%% %s (阈值 %" PRId32 "%%)", + my_ratio, other_ratio, ratio_diff_percent, ratio_match ? "PASS" : "FAIL", ratio_threshold); + ESP_LOGI(TAG, " [维度2] 亮度等级: 本命=%d(%s) 对方=%d(%s) 相差%d级 %s", + (int)my_br_level, GetBrightnessLevelName(my_br_level), + other_br_level, GetBrightnessLevelName(other_br_level), + level_diff, level_match ? "PASS" : "FAIL"); + ESP_LOGI(TAG, " [参考] ALS亮度差异: %.1f%%(仅参考,不参与判定)", lux_diff_percent); + ESP_LOGI(TAG, " ----------------------------------------"); + if (overall_match) { + ESP_LOGI(TAG, " *** 同频匹配成功!交友成功! ***"); + } else { + if (!ratio_match) { + ESP_LOGI(TAG, " 匹配失败:光谱特征不同(石头材质/颜色差异)"); + } + if (!level_match) { + ESP_LOGI(TAG, " 匹配失败:光照环境差异过大(相差%d个等级)", level_diff); + } + } + ESP_LOGI(TAG, "========================================"); + } + // ============================================================================== + + // 按钮初始化 函数 + void InitializeButtons() { + ESP_LOGI(TAG, "初始化按钮...");// 初始化按钮... + + // BOOT按键单击事件 - 用于WiFi重置和触摸解锁 + boot_button_.OnClick([this]() { + static uint32_t last_click_time = 0; + uint32_t current_time = esp_timer_get_time() / 1000; // 当前时间(毫秒) + + // 防抖动处理:如果距离上次点击时间太短(小于500毫秒),则忽略此次点击 + if (last_click_time > 0 && current_time - last_click_time < 500) { + ESP_LOGI(TAG, "BOOT 按钮点击过于频繁,忽略此次点击");// BOOT 按钮点击过于频繁,忽略此次点击 + return; + } + + last_click_time = current_time; + ESP_LOGI(TAG, "BOOT button clicked"); + + // 创建一个单独的任务来处理触摸解锁,避免在按钮回调中执行复杂操作 + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + if (board) { + board->UnlockTouchTask(); + } + vTaskDelete(NULL); + }, "boot_unlock", 4096, this, 5, NULL); + + // 获取当前应用实例和状态 + auto &app = Application::GetInstance(); + auto current_state = app.GetDeviceState(); + + // 检查是否处于BLE配网状态,如果是则屏蔽按键响应(生产测试模式下除外) + auto* wifi_board = dynamic_cast(this); + if (wifi_board && wifi_board->IsBleProvisioningActive() && !production_test_mode_) { + ESP_LOGI(Pro_TAG, "🔵 当前为蓝牙配网模式,[BOOT按键]被按下,长按BOOT按键5秒可进入生产测试模式!");// 生产测试打印 + ESP_LOGI("BluetoothMAC", "Bluetooth MAC Address: %s", SystemInfo::GetBleMacAddress().c_str());// 生产测试打印 + ESP_LOGI(Pro_TAG, "当前电量值: %" PRIu32 "%%", battery_level_);// 生产测试打印 + return; + } + + // 如果处于生产测试模式,记录按键测试并播放音频-生产测试模式 新增代码 + // ============================================================================== + if (production_test_mode_) { + ESP_LOGI(Pro_TAG, "🔧 生产测试模式:BOOT按键已被按下!");// 生产测试打印 + ESP_LOGI(Pro_TAG, "当前电量值: %" PRIu32 "%%", battery_level_);// 生产测试打印 + ESP_LOGI("BluetoothMAC", "Bluetooth MAC Address: %s", SystemInfo::GetBleMacAddress().c_str());// 生产测试打印 + // 播放BOOT按键测试音频 + auto& app = Application::GetInstance(); + + // 确保音频输出已启用 + auto* codec = GetAudioCodec();// 获取音频编解码器 + if (codec) { + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔊 测试模式:已启用音频输出"); + } + + // 播放测试音频 + // app.PlaySound(Lang::Sounds::P3_PUTDOWN_BOOT); + app.PlaySound(Lang::Sounds::P3_1); + + ESP_LOGI(TAG, "🎵 测试模式:开始播放BOOT按键测试音频"); + + // 改进的音频播放完成等待逻辑 + int wait_count = 0; + const int max_wait_cycles = 100; // 最多等待10秒 (100 * 100ms) + + // 等待音频队列开始处理(非空状态) + while (app.IsAudioQueueEmpty() && wait_count < 20) { // 最多等待2秒音频开始 + vTaskDelay(pdMS_TO_TICKS(100)); + wait_count++; + } + + if (!app.IsAudioQueueEmpty()) { + ESP_LOGI(Pro_TAG, "🎵 测试模式:音频开始播放,等待播放完成"); // 生产测试打印 + wait_count = 0; + + // 等待音频播放完成(队列变空) + while (!app.IsAudioQueueEmpty() && wait_count < max_wait_cycles) { + vTaskDelay(pdMS_TO_TICKS(100)); + wait_count++; + } + + if (app.IsAudioQueueEmpty()) { + ESP_LOGI(Pro_TAG, "✅ 测试模式:音频播放完成");// 生产测试打印 + } else { + ESP_LOGW(Pro_TAG, "⚠️ 测试模式:音频播放超时,强制清空队列"); + app.ClearAudioQueue(); + } + } else { + ESP_LOGW(Pro_TAG, "⚠️ 测试模式:音频未能开始播放");// 生产测试打印 + } + + // 额外等待100ms确保音频完全结束 + vTaskDelay(pdMS_TO_TICKS(100)); + + return; + } + // ============================================================================== + + if (current_state == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + + // 设备启动且wifi未连接时,重置wifi配置 + ESP_LOGI(TAG, "🔄 BOOT按键触发:设备状态=%d,WiFi连接状态=%s", current_state, WifiStation::GetInstance().IsConnected() ? "已连接" : "未连接"); + ESP_LOGI(TAG, "🔄 开始重置WiFi配置,清除已保存的WiFi凭据"); + + // 清除已保存的WiFi配置,阻止自动连接 + esp_wifi_restore(); + ESP_LOGI(TAG, "✅ 已清除所有WiFi凭据,设备将进入配网模式"); + + ResetWifiConfiguration();//进入Blufi配网模式 + // 唤醒设备,防止立即进入睡眠 + power_save_timer_->WakeUp(); + } + else { + // 检查是否在BLE配网模式下,如果是则屏蔽单独的BOOT按键功能 + auto* wifi_board = dynamic_cast(this); + if (wifi_board && wifi_board->IsBleProvisioningActive() && !production_test_mode_) { + ESP_LOGI(TAG, "🔵 BLE配网模式下,屏蔽单独BOOT按键功能"); + return; + } + + ESP_LOGI(TAG, "当前设备状态: %d", current_state); + + if (current_state == kDeviceStateIdle) { + // 如果当前是待命状态,切换到聆听状态 + ESP_LOGI(TAG, "从待命状态切换到聆听状态"); + + auto codec = GetAudioCodec(); // 🔧 修复:强制重新初始化音频输出,确保硬件状态正确 + ESP_LOGI(TAG, "强制重新初始化音频输出"); + codec->EnableOutput(false); // 先关闭音频输出 + vTaskDelay(pdMS_TO_TICKS(50)); // 短暂延迟让硬件复位 + codec->EnableOutput(true); // 再开启,强制硬件重新初始化 + + // 🔧 检查音频资源是否可用 + if (codec->output_enabled()) { + ESP_LOGI(TAG, "播放提示音:卡卡在呢"); + app.ResetDecoder(); // 🔧 关键修复:重置解码器状态,清除残留 + // PlaySound(Lang::Sounds::P3_KAKAZAINNE); 原有蜡笔小新 音色播报 + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + app.PlaySound(Lang::Sounds::P3_KAKA_ZAINNE); + } + else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ + app.PlaySound(Lang::Sounds::P3_LALA_ZAINNE); + } + + // 🔧 修复:使用改进的等待逻辑,确保音频真正播放完成 + ESP_LOGI(TAG, "等待音频播放完成..."); + vTaskDelay(pdMS_TO_TICKS(100)); // 给音频足够的时间开始播放 + + // 等待音频队列清空 + 额外缓冲时间确保I2S硬件完成输出 + int timeout_count = 0; + const int max_timeout = 150; // 3秒超时 + + while (timeout_count < max_timeout) { + if (app.IsAudioQueueEmpty()) { + // 队列清空后,再等待500ms确保I2S硬件完成输出 + ESP_LOGI(TAG, "音频队列已清空,等待硬件输出完成..."); + vTaskDelay(pdMS_TO_TICKS(500)); + ESP_LOGI(TAG, "音频播放完成"); + break; + } + vTaskDelay(pdMS_TO_TICKS(20)); + timeout_count++; + } + + if (timeout_count >= max_timeout) { + ESP_LOGW(TAG, "等待音频播放超时,继续状态切换"); + } + } else { + ESP_LOGW(TAG, "音频输出无法启用,跳过提示音播放"); + } + + app.ToggleChatState(); // 切换到聆听状态 + } else if (current_state == kDeviceStateListening) { + // 如果当前是聆听状态,切换到待命状态 + ESP_LOGI(TAG, "🔵 BOOT button pressed in Listening state - switching to idle"); + ESP_LOGI(TAG, "从聆听状态切换到待命状态"); + app.ToggleChatState(); // 切换到待命状态 + } else if (current_state == kDeviceStateSpeaking) { + // 如果当前是说话状态,终止说话并切换到待命状态 + ESP_LOGI(TAG, "🔴 BOOT button pressed in Speaking state - initiating abort sequence"); + ESP_LOGI(TAG, "从说话状态切换到聆听状态"); + //app.AbortSpeakingAndReturnToIdle(); // 专门处理从说话状态到idle状态的转换 + app.AbortSpeakingAndReturnToListening(); // 专门处理从说话状态到聆听状态的转换 + } + else if(current_state == kDeviceStateDialog) { + // Application::GetInstance().ToggleChatState();// 切换对话状态 + app.ToggleChatState(); // 切换对话状态 + } + else { + // 其他状态下只唤醒设备 + ESP_LOGI(TAG, "唤醒设备"); + power_save_timer_->WakeUp(); + } + } + }); + + // 配网模式下长按 BOOT 按键5秒进入 生产测试模式 新增代码 + // ============================================================================== + // 添加BOOT按键长按事件处理 - 仅在配网模式下长按5秒进入测试模式 + boot_button_.OnLongPress([this]() { + //ESP_LOGI(TAG, "🔧 BOOT button long pressed - checking if in provisioning mode"); + + // 检查是否处于BLE配网状态,只有在配网模式下才允许进入测试模式 + auto* wifi_board = dynamic_cast(this); + if (wifi_board && wifi_board->IsBleProvisioningActive()) { + // ESP_LOGI(TAG, "🔧 设备正在进行BLE配网,长按5秒进入生产测试模式"); + EnterProductionTestMode(); + } else { + ESP_LOGI(TAG, "🔵 非配网模式下,BOOT长按被屏蔽,无法进入测试模式"); + return; + } + }); + // ============================================================================== + + ESP_LOGI(TAG, "Boot button initialized on GPIO%d", BOOT_BUTTON_GPIO); + + volume_up_button_.OnClick([this]() { + ESP_LOGI(TAG, "Volume up button clicked!"); + + // 检查是否处于BLE配网状态,如果是则屏蔽按键响应(生产测试模式下除外) + auto* wifi_board = dynamic_cast(this); + if (wifi_board && wifi_board->IsBleProvisioningActive() && !production_test_mode_) { + ESP_LOGI(TAG, "🔵 设备正在进行BLE配网,音量加按键被屏蔽"); + return; + } + + // 如果处于生产测试模式,记录按键测试 + if (production_test_mode_) { + ESP_LOGI(TAG, "🔧 生产测试模式:音量加按键点击测试"); + return; + } + + auto codec = GetAudioCodec(); + // 将当前硬件音量转换为用户音量,增加10%,再转换回硬件音量 + auto current_hw_volume = codec->output_volume(); + auto current_user_volume = HARDWARE_TO_USER_VOLUME(current_hw_volume); + auto new_user_volume = current_user_volume + 10; + if (new_user_volume > 100) { + new_user_volume = 100; + } + auto new_hw_volume = USER_TO_HARDWARE_VOLUME(new_user_volume); + codec->SetOutputVolume(new_hw_volume); + ESP_LOGI(TAG, "Volume up: User %d%% -> Hardware %d%% (Range: %d%%-%d%%)", + new_user_volume, new_hw_volume, MIN_VOLUME_PERCENT, MAX_VOLUME_PERCENT); + }); + + volume_up_button_.OnLongPress([this]() { + ESP_LOGI(TAG, "Volume up button long pressed!"); + auto codec = GetAudioCodec(); + // 设置为用户音量100%,对应硬件最高音量 + auto hw_volume = USER_TO_HARDWARE_VOLUME(100); + codec->SetOutputVolume(hw_volume); + ESP_LOGI(TAG, "Volume set to maximum: User 100%% -> Hardware %d%%", hw_volume); + }); + ESP_LOGI(TAG, "Volume up button initialized on GPIO%d", VOLUME_UP_BUTTON_GPIO); + + volume_down_button_.OnClick([this]() { + ESP_LOGI(TAG, "Volume down button clicked!"); + + // 检查是否处于BLE配网状态,如果是则屏蔽按键响应(生产测试模式下除外) + auto* wifi_board = dynamic_cast(this); + if (wifi_board && wifi_board->IsBleProvisioningActive() && !production_test_mode_) { + ESP_LOGI(TAG, "🔵 设备正在进行BLE配网,音量减按键被屏蔽"); + return; + } + + // 如果处于生产测试模式,记录按键测试 + if (production_test_mode_) { + ESP_LOGI(TAG, "🔧 生产测试模式:音量减按键点击测试"); + return; + } + + auto codec = GetAudioCodec(); + // 将当前硬件音量转换为用户音量,减少10%,再转换回硬件音量 + auto current_hw_volume = codec->output_volume(); + auto current_user_volume = HARDWARE_TO_USER_VOLUME(current_hw_volume); + auto new_user_volume = current_user_volume - 10; + if (new_user_volume < 0) { + new_user_volume = 0; + } + auto new_hw_volume = USER_TO_HARDWARE_VOLUME(new_user_volume); + codec->SetOutputVolume(new_hw_volume); + ESP_LOGI(TAG, "Volume down: User %d%% -> Hardware %d%% (Range: %d%%-%d%%)", + new_user_volume, new_hw_volume, MIN_VOLUME_PERCENT, MAX_VOLUME_PERCENT); + }); + + volume_down_button_.OnLongPress([this]() { + ESP_LOGI(TAG, "Volume down button long pressed!"); + auto codec = GetAudioCodec(); + // 设置为用户音量0%,对应硬件最低音量 + auto hw_volume = USER_TO_HARDWARE_VOLUME(0); + codec->SetOutputVolume(hw_volume); + ESP_LOGI(TAG, "Volume set to minimum: User 0%% -> Hardware %d%%", hw_volume); + }); + ESP_LOGI(TAG, "Volume down button initialized on GPIO%d", VOLUME_DOWN_BUTTON_GPIO); + } + + void InitializeBatteryMonitor() { + ESP_LOGI(TAG, "Initializing battery monitor..."); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = BATTERY_ADC_UNIT, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, // 12dB衰减,测量范围0-3.3V + .bitwidth = ADC_BITWIDTH_12, // 12位精度 + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, BATTERY_ADC_CHANNEL, &chan_config)); + + // 🔧 添加ADC校准 + adc_cali_curve_fitting_config_t cali_config = { + .unit_id = BATTERY_ADC_UNIT, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle_)); + ESP_LOGI(TAG, "ADC calibration initialized"); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + MovecallMojiESP32S3* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &battery_timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(battery_timer_handle_, 1000000)); // 每秒检查一次 + + ESP_LOGI(TAG, "电池状态监控已初始化,GPIO:%d", BATTERY_ADC_GPIO);// 电池状态监控已初始化,GPIO:%d + } + + // 初始化IMU传感器(QMI8658A 陀螺仪) + void InitializeImuSensor() { + + auto& app = Application::GetInstance();// 获取当前应用状态 + auto current_state = app.GetDeviceState();// 获取当前设备状态 + + // 在生产测试模式下或在对话状态下启用姿态传感器 + if (!production_test_mode_ && current_state != kDeviceStateDialog) { + ESP_LOGI(TAG, "非生产测试模式且不在对话状态,姿态传感器业务已禁用以节约资源"); + imu_initialized_ = false;// 非生产测试模式且不在对话状态,姿态传感器业务已禁用以节约资源 + imu_sensor_ = nullptr;// 姿态传感器实例指针 + return; + } + + const char* log_tag = production_test_mode_ ? Pro_TAG : TAG; + + if (current_state == kDeviceStateDialog) { + ESP_LOGI(log_tag, "对话状态下启用姿态传感器"); + } else { + ESP_LOGI(log_tag, "生产测试模式下启用姿态传感器");// 生产测试模式下启用姿态传感器 + } + + ESP_LOGI(log_tag, "初始化IMU传感器 QMI8658A...");// 初始化IMU传感器(QMI8658A 陀螺仪) + + // 初始化状态为false,确保系统在IMU不可用时仍能正常运行 + imu_initialized_ = false;// 初始化状态为false,确保系统在IMU不可用时仍能正常运行 + imu_sensor_ = nullptr;// 姿态传感器实例指针 + + if (!codec_i2c_bus_) { + ESP_LOGI(log_tag, "I2C总线未初始化,IMU传感器将被禁用");// I2C总线未初始化,IMU传感器将被禁用 + ESP_LOGI(log_tag, "系统将继续运行,不启用运动检测功能");// 系统将继续运行,不启用运动检测功能 + return; + } + + ESP_LOGI(log_tag, "I2C总线已初始化,创建IMU传感器实例");// I2C总线已初始化,创建IMU传感器实例 + ESP_LOGI(log_tag, "使用I2C地址: 0x6A");// 使用I2C地址: 0x6A + + + vTaskDelay(pdMS_TO_TICKS(100));// 添加延迟,确保I2C总线完全稳定 + + // 创建IMU传感器实例 (使用I2C地址0x6A) + uint8_t working_address = 0x6A; + try { + imu_sensor_ = new QMI8658A(codec_i2c_bus_, 0x6A); + ESP_LOGI(log_tag, "IMU传感器实例创建成功,I2C地址: 0x6A");// IMU传感器实例创建成功,I2C地址: 0x6A + + // 测试I2C通信 - 尝试读取芯片ID + ESP_LOGI(log_tag, "测试I2C通信,读取QMI8658A芯片ID...");// 测试I2C通信,读取QMI8658A芯片ID... + uint8_t chip_id = imu_sensor_->GetChipId(); + ESP_LOGI(log_tag, "读取到的芯片ID: 0x%02X (预期: 0x05)", chip_id);// 读取到的芯片ID: 0x%02X (预期: 0x05) + + if (chip_id == 0xFF) { + ESP_LOGI(log_tag, "I2C通信失败 - 读取到的芯片ID为0xFF");// I2C通信失败 - 读取到的芯片ID为0xFF + ESP_LOGI(log_tag, "尝试备用I2C地址 0x6B...");// 尝试备用I2C地址 0x6B... + + // 尝试使用备用地址0x6B + delete imu_sensor_; + imu_sensor_ = new QMI8658A(codec_i2c_bus_, 0x6B); + working_address = 0x6B; + chip_id = imu_sensor_->GetChipId(); + ESP_LOGI(log_tag, "读取到的芯片ID (0x6B): 0x%02X", chip_id);// 读取到的芯片ID (0x6B): 0x%02X + + if (chip_id == 0xFF) { + ESP_LOGI(log_tag, "I2C通信失败 - 读取到的芯片ID (0x6B)为0xFF");// I2C通信失败 - 读取到的芯片ID (0x6B)为0xFF + ESP_LOGI(log_tag, "可能原因:1) 硬件连接问题 2) 错误的I2C引脚 3) 电源供应问题");// 可能原因:1) 硬件连接问题 2) 错误的I2C引脚 3) 电源供应问题 + delete imu_sensor_; + imu_sensor_ = nullptr; + return; + } + } + + if (chip_id != 0x05) { + ESP_LOGI(log_tag, "读取到的芯片ID (0x%02X)与预期的0x05不符", chip_id);// 读取到的芯片ID (0x6A)与预期的0x05不符 + ESP_LOGI(log_tag, "这可能不是QMI8658A传感器,或存在通信问题");// 这可能不是QMI8658A传感器,或存在通信问题 + // 继续尝试初始化,可能是兼容的传感器 + } + + ESP_LOGI(log_tag, "成功建立I2C通信,使用地址: 0x%02X", working_address);// 成功建立I2C通信,使用地址: 0x6A + + } catch (...) { + ESP_LOGI(log_tag, "创建IMU传感器实例时发生异常");// 创建IMU传感器实例时发生异常 + ESP_LOGI(log_tag, "系统将继续运行,不启用运动检测功能");// 系统将继续运行,不启用运动检测功能 + imu_sensor_ = nullptr; + return; + } + + // 配置传感器参数 + qmi8658a_config_t config = { + .acc_range = QMI8658A_ACC_RANGE_4G, // 加速度计量程±4g + .gyro_range = QMI8658A_GYRO_RANGE_512DPS, // 陀螺仪量程±512dps + .acc_odr = QMI8658A_ODR_125HZ, // 加速度计采样率125Hz + .gyro_odr = QMI8658A_ODR_125HZ, // 陀螺仪采样率125Hz + .mode = QMI8658A_MODE_DUAL, // 同时启用加速度计和陀螺仪 + .enable_interrupt = false, // 不启用中断 + .interrupt_pin = 0, // 中断引脚 + .auto_calibration = true, // 启用自动校准 + .acc_offset = {0.0f, 0.0f, 0.0f}, // 加速度计偏移校准 + .gyro_offset = {0.0f, 0.0f, 0.0f} // 陀螺仪偏移校准 + }; + + ESP_LOGI(log_tag, "开始初始化IMU传感器...");// 开始初始化IMU传感器... + + // 初始化传感器 - 修复逻辑错误:QMI8658A_OK = 0 表示成功 + qmi8658a_error_t init_result = imu_sensor_->Initialize(&config); + if (init_result == QMI8658A_OK) { + imu_initialized_ = true; + ESP_LOGI(log_tag, "QMI8658A传感器初始化成功");// QMI8658A传感器初始化成功 + + if (config.auto_calibration) { + qmi8658a_error_t calib_buf = imu_sensor_->StartBufferedReading(20); + if (calib_buf == QMI8658A_OK) { + imu_sensor_->StartCalibration(6000); + bool running = false; + float progress = 0.0f; + do { + imu_sensor_->GetCalibrationStatus(&running, &progress); + vTaskDelay(pdMS_TO_TICKS(200)); + } while (running); + qmi8658a_calibration_t calib; + imu_sensor_->GetCalibrationData(&calib); + imu_sensor_->ApplyCalibration(&calib); + imu_sensor_->StopBufferedReading(); + } + } + + // 创建IMU数据读取定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + MovecallMojiESP32S3* self = static_cast(arg); + self->ReadImuData(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "imu_read_timer", + .skip_unhandled_events = true, + }; + + // 使用错误处理而不是ESP_ERROR_CHECK,避免系统崩溃 + esp_err_t err = esp_timer_create(&timer_args, &imu_timer_handle_); + if (err != ESP_OK) { + ESP_LOGI(log_tag, "创建IMU定时器失败: %s", esp_err_to_name(err));// 创建IMU定时器失败: %s + ESP_LOGI(log_tag, "IMU传感器将被禁用,系统继续正常运行");// IMU传感器将被禁用,系统继续正常运行 + delete imu_sensor_; + imu_sensor_ = nullptr; + imu_initialized_ = false; + return; + } + + err = esp_timer_start_periodic(imu_timer_handle_, kImuReadInterval * 1000); + if (err != ESP_OK) { + ESP_LOGI(log_tag, "启动IMU定时器失败: %s", esp_err_to_name(err));// 启动IMU定时器失败: %s + ESP_LOGI(log_tag, "IMU传感器将被禁用,系统继续正常运行");// IMU传感器将被禁用,系统继续正常运行 + esp_timer_delete(imu_timer_handle_); + imu_timer_handle_ = nullptr; + delete imu_sensor_; + imu_sensor_ = nullptr; + imu_initialized_ = false; + return; + } + + ESP_LOGI(log_tag, "IMU数据读取定时器已启动,采样间隔: %dms", kImuReadInterval);// IMU数据读取定时器已启动,采样间隔: %dms + } else { + ESP_LOGI(log_tag, "QMI8658A传感器初始化失败,错误码: %d", init_result);// QMI8658A传感器初始化失败,错误码: %d + ESP_LOGI(TAG, "可能原因:1) 硬件连接问题 2) 传感器未响应 3) I2C通信错误");// 可能原因:1) 硬件连接问题 2) 传感器未响应 3) I2C通信错误 + ESP_LOGI(TAG, "系统将继续运行,不启用运动检测功能");// 系统将继续运行,不启用运动检测功能 + + // 清理资源 + if (imu_sensor_) { + delete imu_sensor_;// 清理IMU传感器实例 + imu_sensor_ = nullptr;// 重置IMU传感器指针 + } + imu_initialized_ = false;// 重置IMU初始化状态 + } + } + + // 读取IMU数据的方法 + void ReadImuData() { + // ESP_LOGI(Pro_TAG, "读取IMU数据,是否初始化 =%d, 传感器指针 =%p", imu_initialized_, imu_sensor_);// 读取IMU数据,是否初始化:%d,传感器指针:%p + + if (!imu_initialized_ || !imu_sensor_) { + ESP_LOGI(Pro_TAG, "IMU未初始化,跳过数据读取");// IMU未初始化,跳过数据读取 + return; + } + + // 实现重试机制,最多尝试3次读取 + const int kMaxRetries = 3; + const int kRetryDelayMs = 5; // 重试间隔5ms + qmi8658a_error_t result = QMI8658A_ERROR_TIMEOUT; + int retry_count = 0; + + do { + // 读取传感器数据 + // ESP_LOGI(Pro_TAG, "尝试读取IMU传感器数据(第%d次尝试)", retry_count + 1);// 尝试读取IMU传感器数据 + result = imu_sensor_->ReadSensorData(&latest_imu_data_); + + if (result == QMI8658A_OK) { + // ESP_LOGI(Pro_TAG, "成功读取IMU数据,正在处理...");// 成功读取IMU数据,正在处理... + // 可以在这里添加数据处理逻辑 + // 例如:检测姿态变化、计算角度等 + ProcessImuData(&latest_imu_data_); + return; // 读取成功,直接返回 + } else if (result == QMI8658A_ERROR_DATA_NOT_READY && retry_count < kMaxRetries - 1) { + // 数据未就绪但还有重试次数,等待后重试 + // ESP_LOGI(Pro_TAG, "IMU数据未就绪,%dms后重试(剩余%d次)", kRetryDelayMs, kMaxRetries - retry_count - 1); + esp_rom_delay_us(kRetryDelayMs * 1000); // 延时 + retry_count++; + } else { + // 其他错误或重试次数用尽 + ESP_LOGI(Pro_TAG, "读取IMU数据失败,错误码 = %d,已尝试%d次", result, retry_count + 1); + break; + } + } while (retry_count < kMaxRetries); + + // 如果执行到这里,说明所有尝试都失败了 + ESP_LOGI(Pro_TAG, "所有尝试都失败,放弃本次IMU数据读取");// 读取IMU数据失败,错误码 = %d + } + + // 处理IMU数据的方法 + void ProcessImuData(const qmi8658a_data_t* data) { + // 处理IMU数据的逻辑 + // 可以根据需要实现姿态检测、运动检测等功能 + // 示例:检测是否有显著的加速度变化(可能表示设备被移动) + float accel_magnitude = sqrt(data->acc_x * data->acc_x + + data->acc_y * data->acc_y + + data->acc_z * data->acc_z); + + // 如果加速度幅值超过阈值,可能需要唤醒设备或触发某些功能 + const float MOTION_THRESHOLD = 1.5f; // g + if (accel_magnitude > MOTION_THRESHOLD) { + // 检测到运动,可以在这里添加相应的处理逻辑 + // ESP_LOGI(TAG, "Motion detected: accel_magnitude = %.2f g", accel_magnitude);// 检测到运动:加速度幅值 = %.2f g + ESP_LOGI(Pro_TAG, "检测到运动: 加速度幅值 = %.2f g", accel_magnitude);// 检测到运动:加速度幅值 = %.2f g + } + + // 更新IMU IoT设备的数据 + if (imu_thing_) { + imu_thing_->UpdateData(*data); + } + + // // 记录详细的传感器数据(调试用) + // ESP_LOGI(TAG, "IMU Data - Accel: (%.2f, %.2f, %.2f) g, Gyro: (%.2f, %.2f, %.2f) dps, Temp: %.1f°C", + // data->acc_x, data->acc_y, data->acc_z, + // data->gyro_x, data->gyro_y, data->gyro_z, + // data->temperature); + // 记录详细的传感器数据(调试用) + ESP_LOGI(Pro_TAG, "IMU Data - Accel: (%.2f, %.2f, %.2f) g, Gyro: (%.2f, %.2f, %.2f) dps, Temp: %.1f°C", + data->acc_x, data->acc_y, data->acc_z, + data->gyro_x, data->gyro_y, data->gyro_z, + data->temperature); + } + + // 检查电池状态的方法 + void CheckBatteryStatus() { + // 如果电池电量数据不足,则读取电池电量数据 + if (adc_values_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 检查是否启用设备状态上报(启动3秒后) + if (!status_report_enabled_ && battery_ticks_ >= kStatusReportDelay) { + status_report_enabled_ = true; + ESP_LOGI(TAG, "📤 设备状态上报已启用,每%d秒上报一次", kStatusReportInterval); + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + battery_ticks_++; + if (battery_ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + Application::GetInstance().Schedule([this]() { + auto codec = GetAudioCodec(); + if (!codec) { + return; + } + // 如果电池电量低于25%,则将输出音量设置为0(静音) + if (battery_level_ <= 25) { + if (codec->output_volume() != 0) { + codec->SetOutputVolumeRuntime(0); + } + } else { + Settings s("audio", false); + int vol = s.GetInt("output_volume", AudioCodec::default_output_volume()); + if (vol <= 0) { + vol = AudioCodec::default_output_volume(); + } + if (codec->output_volume() != vol) { + codec->SetOutputVolumeRuntime(vol); + } + } + }); + } + + // 设备状态上报逻辑:每30秒上报一次(启动3秒后),配网模式下跳过 + if (status_report_enabled_) { + status_report_ticks_++; + if (status_report_ticks_ % kStatusReportInterval == 0) { + auto& wifi_station = WifiStation::GetInstance(); + if (wifi_station.IsConnected()) { + ReportDeviceStatus(battery_level_); + } + } + } + + battery_alert_ticks_++; + auto& app = Application::GetInstance(); + auto state = app.GetDeviceState(); + + // 检测RTC火山引擎连接状态(进入dialog状态说明RTC已连接、音频通道已打开) + if (!rtc_online_ && state == kDeviceStateDialog) { + rtc_online_ = true; + ESP_LOGI(TAG, "🔗 检测到RTC火山引擎已连接"); + } + + if (battery_level_ <= 30 && battery_level_ > 25) { + if (battery_alert_ticks_ % 10 == 0) { + if (state == kDeviceStateIdle) { + app.Schedule([]() { + auto& board = Board::GetInstance(); + auto codec = board.GetAudioCodec(); + if (codec) { codec->EnableOutput(true); } + if (Application::GetInstance().IsAudioQueueEmpty()) { + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + Application::GetInstance().PlaySound(Lang::Sounds::P3_KAKA_BATTERY_L); + } else if(strcmp(CONFIG_DEVICE_ROLE, "LALA") == 0){ + Application::GetInstance().PlaySound(Lang::Sounds::P3_LALA_BATTERY_L); + } + ESP_LOGI(TAG, "电量值低警告音已播放!!"); + } + }); + } else if (state == kDeviceStateSpeaking) { + app.Schedule([]() { + auto& a = Application::GetInstance(); + a.SetLowBatteryTransition(true); + a.AbortSpeakingAndReturnToIdle(); + vTaskDelay(pdMS_TO_TICKS(500)); + a.ClearAudioQueue(); + auto& board = Board::GetInstance(); + auto codec = board.GetAudioCodec(); + if (codec) { codec->EnableOutput(true); } + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + a.PlaySound(Lang::Sounds::P3_KAKA_BATTERY_L); + } else if(strcmp(CONFIG_DEVICE_ROLE, "LALA") == 0){ + a.PlaySound(Lang::Sounds::P3_LALA_BATTERY_L); + } + ESP_LOGI(TAG, "电量值低警告音已播放!!"); + a.SetLowBatteryTransition(false); + }); + } else if (state == kDeviceStateListening) { + app.Schedule([]() { + auto& a = Application::GetInstance(); + a.SetLowBatteryTransition(true); + a.SetDeviceState(kDeviceStateIdle); + vTaskDelay(pdMS_TO_TICKS(500)); + auto& board = Board::GetInstance(); + auto codec = board.GetAudioCodec(); + if (codec) { codec->EnableOutput(true); } + if (a.IsAudioQueueEmpty()) { + if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ + a.PlaySound(Lang::Sounds::P3_KAKA_BATTERY_L); + } else if(strcmp(CONFIG_DEVICE_ROLE, "LALA") == 0){ + a.PlaySound(Lang::Sounds::P3_LALA_BATTERY_L); + } + ESP_LOGI(TAG, "电量值低警告音已播放!!"); + } + a.SetLowBatteryTransition(false); + }); + } + } + } + } + + void ReadBatteryAdcData() { + std::vector adc_samples; + for (int i = 0; i < kBatteryAdcSampleCount; i++) { + int adc_value; + esp_err_t ret = adc_oneshot_read(adc_handle_, BATTERY_ADC_CHANNEL, &adc_value); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "ADC read failed: %s", esp_err_to_name(ret)); + return; + } + adc_samples.push_back(adc_value); + vTaskDelay(pdMS_TO_TICKS(10)); // 每次采样间隔10ms + } + + std::sort(adc_samples.begin(), adc_samples.end()); + int adc_value = adc_samples[adc_samples.size() / 2]; // 中位数滤波 + + int adc_voltage_mv; + esp_err_t ret = adc_cali_raw_to_voltage(adc_cali_handle_, adc_value, &adc_voltage_mv); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "ADC calibration failed: %s", esp_err_to_name(ret)); + adc_voltage_mv = adc_value * 3300 / 4095; + } + + adc_values_.push_back(adc_voltage_mv); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + + int average_voltage_mv = 0; + for (auto value : adc_values_) { + average_voltage_mv += value; + } + average_voltage_mv /= adc_values_.size(); + + // 修正分压系数:根据满电电池电压4.2V和ADC读数967mV计算得出 + float battery_voltage = average_voltage_mv / 1000.0f * 4.34f; + + // 使用锂电池典型电压阈值 + const float kVoltage100Percent = 4.2f; // 锂电池满电电压 + const float kVoltage75Percent = 3.9f; // 75%电量电压 + const float kVoltage50Percent = 3.7f; // 50%电量电压 + const float kVoltage25Percent = 3.5f; // 25%电量电压 + const float kVoltage0Percent = 3.2f; // 0%电量电压(保护电压) + + // 基于锂电池典型电压计算电量百分比 + int battery_percentage; + if (battery_voltage >= kVoltage100Percent) { + battery_percentage = 100; + } else if (battery_voltage >= kVoltage75Percent) { + battery_percentage = 75 + (battery_voltage - kVoltage75Percent) * 25 / (kVoltage100Percent - kVoltage75Percent); + } else if (battery_voltage >= kVoltage50Percent) { + battery_percentage = 50 + (battery_voltage - kVoltage50Percent) * 25 / (kVoltage75Percent - kVoltage50Percent); + } else if (battery_voltage >= kVoltage25Percent) { + battery_percentage = 25 + (battery_voltage - kVoltage25Percent) * 25 / (kVoltage50Percent - kVoltage25Percent); + } else if (battery_voltage >= kVoltage0Percent) { + battery_percentage = 0 + (battery_voltage - kVoltage0Percent) * 25 / (kVoltage25Percent - kVoltage0Percent); + } else { + battery_percentage = 0; + } + + battery_level_ = battery_percentage; + + ESP_LOGI(TAG, "ADC: %d, 原始电压: %.2fV, 计算电池电压: %.2fV, 电量: %d%%, 满电电压: %.2fV", + average_voltage_mv, average_voltage_mv / 1000.0f, battery_voltage, battery_percentage, kVoltage100Percent); + + // 打印蓝牙MAC地址 + ESP_LOGI("BluetoothMAC", "Bluetooth MAC Address: %s", SystemInfo::GetBleMacAddress().c_str());// 生产测试打印 + } + + void InitializeStoryButton() { + story_button_.OnClick([this]() { + ESP_LOGI(TAG, "Story button clicked!"); + + // 检查是否处于BLE配网状态,如果是则屏蔽按键响应(生产测试模式下除外) + auto* wifi_board = dynamic_cast(this); + if (wifi_board && wifi_board->IsBleProvisioningActive() && !production_test_mode_) { + //ESP_LOGI(Pro_TAG, "🔵 设备正在进行蓝牙配网,长按BOOT按键5秒可进入生产测试模式!"); + ESP_LOGI(Pro_TAG, "🔵 当前为蓝牙配网模式,[故事按键]被按下,长按BOOT按键5秒可进入生产测试模式!"); + ESP_LOGI("BluetoothMAC", "Bluetooth MAC Address: %s", SystemInfo::GetBleMacAddress().c_str());// 生产测试打印 + ESP_LOGI(Pro_TAG, "当前电量值: %" PRIu32 "%%", battery_level_);// 生产测试打印 + return; + } + + // 如果处于生产测试模式,记录按键测试并播放音频 + if (production_test_mode_) { + ESP_LOGI(Pro_TAG, "🔧 生产测试模式:故事按键已被按下!");// 生产测试打印 + ESP_LOGI("BluetoothMAC", "Bluetooth MAC Address: %s", SystemInfo::GetBleMacAddress().c_str());// 生产测试打印 + ESP_LOGI(Pro_TAG, "当前电量值: %" PRIu32 "%%", battery_level_);// 生产测试打印 + // 播放故事按键测试音频 + auto& app = Application::GetInstance(); + // app.PlaySound(Lang::Sounds::P3_PUTDOWN_STORY); + app.PlaySound(Lang::Sounds::P3_2); + + // 等待音频播放完成后清空队列 + vTaskDelay(pdMS_TO_TICKS(700)); // 等待3秒确保音频播放完成 + + // 清空音频播放队列,避免残留 + app.ClearAudioQueue(); + + return; + } + + auto& app = Application::GetInstance();// 获取应用实例 + auto current_state = app.GetDeviceState();// 获取当前设备状态 + + // 如果当前状态为对话状态 + if (current_state == kDeviceStateDialog) { + if (!app.IsDialogUploadEnabled()) {// 如果音频采集上传未启用 + app.ToggleChatState();// 切换聊天状态 + vTaskDelay(300 / portTICK_PERIOD_MS);// 等待300ms确保状态切换完成 + } + app.AbortSpeaking(kAbortReasonVoiceInterrupt);// 🔊 发送中止通话请求时,加锁保护RTC句柄 + vTaskDelay(pdMS_TO_TICKS(120));// 等待120ms确保中止通话请求发送完成 + // app.SendTextMessage("给我讲个小故事");// 发送“给我讲个小故事”消息 + app.SendStoryRequest();// 发送故事请求 + } + // else if (current_state == kDeviceStateSpeaking) { + // app.AbortSpeaking(kAbortReasonNone); + // vTaskDelay(300 / portTICK_PERIOD_MS); + // if (!app.IsDialogUploadEnabled()) { + // app.ToggleChatState();// 切换聊天状态 + // vTaskDelay(300 / portTICK_PERIOD_MS);// 等待300ms确保状态切换完成 + // } + // app.SendTextMessage("给我讲个小故事");// 发送“给我讲个小故事”消息 + // } else { + // app.ToggleChatState();// 切换聊天状态 + // vTaskDelay(300 / portTICK_PERIOD_MS);// 等待300ms确保状态切换完成 + // app.SendTextMessage("给我讲个小故事");// 发送“给我讲个小故事”消息 + // } + // 唤醒设备,防止立即进入睡眠 + power_save_timer_->WakeUp(); + }); + ESP_LOGI(TAG, "故事按键单击已初始化,GPIO引脚 =%d", KEY4_GPIO); + + // 双击KEY4:录入本命石头 + // 注意:iot_button回调运行在esp_timer任务中,禁止vTaskDelay + // 必须用xTaskCreate派发到独立任务执行传感器读取 + story_button_.OnDoubleClick([this]() { + ESP_LOGI(TAG, "KEY4 双击 - 录入本命石头"); + power_save_timer_->WakeUp(); + xTaskCreate([](void* arg) { + auto* board = static_cast(arg); + board->RegisterMyStone(); + vTaskDelete(NULL); + }, "stone_reg", 4096, this, 5, NULL); + }); + ESP_LOGI(TAG, "故事按键双击(录入本命石)已初始化"); + + // 长按KEY4 2秒:识别他人石头并匹配 + story_button_.OnLongPress([this]() { + ESP_LOGI(TAG, "KEY4 长按2秒 - 石头匹配"); + power_save_timer_->WakeUp(); + xTaskCreate([](void* arg) { + auto* board = static_cast(arg); + board->MatchStone(); + vTaskDelete(NULL); + }, "stone_match", 4096, this, 5, NULL); + }); + ESP_LOGI(TAG, "故事按键长按(石头匹配)已初始化"); + + ESP_LOGI(TAG, "所有按键已成功初始化!"); + } + + // 物联网初始化,添加音频设备和IMU传感器 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance();// 获取物联网管理器实例 + thing_manager.AddThing(iot::CreateThing("Speaker")); // 添加音频设备到物联网管理器 + + // 创建并添加IMU传感器IoT设备 陀螺仪/姿态传感器业务 新增代码 + // ============================================================================== + const char* log_tag = production_test_mode_ ? Pro_TAG : TAG; + if (imu_sensor_ && imu_initialized_) { + imu_thing_ = new iot::ImuSensorThing(imu_sensor_); + thing_manager.AddThing(imu_thing_); + ESP_LOGI(log_tag, "IMU传感器已添加到IoT管理器");// IMU传感器已添加到IoT管理器 + } else { + ESP_LOGI(log_tag, "IMU传感器未初始化,跳过IoT注册");// IMU传感器未初始化,跳过IoT注册 + } + // ============================================================================== + } + + // 唤醒词方案配置 + void InitializeWakeWordSchemes() { + ESP_LOGI(TAG, "Wake word schemes initialized"); + } + + + + // 设置触摸阈值 + void CalibrateTouchThresholds() { + touch_pad_t touch_pads[4] = {TOUCH_PAD_NUM1, TOUCH_PAD_NUM2, TOUCH_PAD_NUM3, TOUCH_PAD_NUM7}; + + // 使用固定阈值5000 + uint32_t default_threshold = 5000; // 设置为5000,降低灵敏度减少误触发 + + // 为每个触摸板设置阈值 + for (int i = 0; i < 4; ++i) { + // 先读取原始值作为参考 + uint32_t touch_value = 0; + esp_err_t ret = touch_pad_read_raw_data(touch_pads[i], &touch_value); + + if (ret == ESP_OK) { + ESP_LOGI(TAG, "触摸板 %d 初始原始值: %" PRIu32, i, touch_value); + // 根据实际读数动态调整阈值 + if (touch_value > 8000) { + // 未触摸状态,使用固定阈值而非动态计算 + raw_touch_values_[i] = touch_value; // 存储当前值作为参考 + touch_thresholds_[i] = default_threshold; // 使用固定的5000阈值 + + ESP_ERROR_CHECK(touch_pad_set_thresh(touch_pads[i], touch_thresholds_[i])); + ESP_LOGI(TAG, "触摸板 %d 设置固定阈值: %" PRIu32, i, (uint32_t)touch_thresholds_[i]); + } else { + // 可能已经在触摸状态,使用固定阈值 + raw_touch_values_[i] = 10000; // 假设一个高值 + touch_thresholds_[i] = default_threshold; + + ESP_ERROR_CHECK(touch_pad_set_thresh(touch_pads[i], default_threshold)); + ESP_LOGI(TAG, "触摸板 %d 设置固定阈值(触摸中): %" PRIu32, i, default_threshold); + } + } else { + // 读取失败,使用默认值 + raw_touch_values_[i] = 10000; + touch_thresholds_[i] = default_threshold; + + ESP_ERROR_CHECK(touch_pad_set_thresh(touch_pads[i], default_threshold)); + ESP_LOGI(TAG, "触摸板 %d 无法读取原始值,使用固定阈值: %" PRIu32, i, default_threshold); + } + } + + // 使用滤波器提高稳定性 + ESP_LOGI(TAG, "启用触摸传感器滤波器"); + touch_filter_config_t filter_info = { + .mode = TOUCH_PAD_FILTER_IIR_16, // IIR滤波器,16采样 + .debounce_cnt = 1, // 消抖计数 + .noise_thr = 0, // 噪声阈值(不使用) + .jitter_step = 4, // 抖动步长 + .smh_lvl = TOUCH_PAD_SMOOTH_IIR_2 // 平滑级别 + }; + touch_pad_filter_set_config(&filter_info); + touch_pad_filter_enable(); + + ESP_LOGI(TAG, "触摸阈值校准完成,使用固定阈值: %" PRIu32, default_threshold); + } + + // 重置触摸状态的方法(可用于外部强制重置) + void ResetTouchState(int touch_pad_num) { + if (touch_pad_num >= 0 && touch_pad_num < 4) { + touch_states_[touch_pad_num] = TOUCH_STATE_IDLE; + ESP_LOGI(TAG, "触摸板 %d 状态已手动重置为空闲", touch_pad_num); + } + } + +public: + // 触摸板初始化 + void InitializeTouchPads() { + ESP_LOGI(TAG, "初始化触摸板..."); + + // 初始化触摸模块 + esp_err_t ret = touch_pad_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "触摸板初始化失败,错误代码: 0x%x", ret); + return; + } + + // 设置工作模式 + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + + // 配置触摸传感器 + ESP_LOGI(TAG, "配置触摸传感器..."); + touch_pad_t touch_pads[4] = {TOUCH_PAD_NUM1, TOUCH_PAD_NUM2, TOUCH_PAD_NUM3, TOUCH_PAD_NUM7}; + + for (int i = 0; i < 4; ++i) { + touch_pad_config(touch_pads[i]); + } + + // 先读取基准值,然后设置阈值 + ESP_LOGI(TAG, "校准触摸阈值..."); + CalibrateTouchThresholds(); + + // 创建触摸事件队列 + ESP_LOGI(TAG, "创建触摸事件队列..."); + touch_event_queue_ = xQueueCreate(TOUCH_QUEUE_SIZE, sizeof(touch_event_data_t)); + if (!touch_event_queue_) { + ESP_LOGE(TAG, "创建触摸事件队列失败"); + return; + } + + // 注册触摸中断 + ESP_LOGI(TAG, "注册触摸中断处理程序..."); + // 仅启用按下中断,由触摸任务处理释放 + touch_pad_isr_register(TouchPadISR, nullptr, TOUCH_PAD_INTR_MASK_ACTIVE); + touch_pad_intr_enable(TOUCH_PAD_INTR_MASK_ACTIVE); + + // 创建处理触摸事件的任务 + ESP_LOGI(TAG, "创建触摸事件任务..."); + xTaskCreate(TouchEventTask, "touch_event_task", 4096, this, 10, NULL); + + // 确保所有触摸状态初始为空闲 + ResetAllTouchStates(); + + // 开启触摸监控定时器,用于定期检查触摸状态是否正常 + ESP_LOGI(TAG, "设置触摸监控..."); + + instance_ = this; + touch_pad_fsm_start(); + ESP_LOGI(TAG, "触摸板初始化完成"); + } + + AudioCodec* GetAudioCodec() { + // 使用延迟初始化模式,确保I2C总线和编解码器按正确顺序初始化 + static AudioCodec* audio_codec = nullptr; + static bool init_attempted = false; + + if (audio_codec == nullptr && !init_attempted) { + init_attempted = true; // 标记为已尝试初始化 + + ESP_LOGI(TAG, "Initializing audio codec..."); + // 确保I2C总线已初始化 + if (codec_i2c_bus_ == nullptr) { + ESP_LOGI(TAG, "Initializing I2C bus for audio codec..."); + InitializeCodecI2c(); + } + + if (codec_i2c_bus_ != nullptr) { + try { + ESP_LOGI(TAG, "Creating BoxAudioCodec (ES8311+ES7210, %s reference) ...", AUDIO_INPUT_REFERENCE ? "with" : "without"); + audio_codec = new BoxAudioCodec( + codec_i2c_bus_, + 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, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE + ); + ESP_LOGI(TAG, "Audio codec initialized successfully"); + } catch (const std::exception& e) { + ESP_LOGE(TAG, "Exception during audio codec initialization: %s", e.what()); + } catch (...) { + ESP_LOGE(TAG, "Unknown exception during audio codec initialization"); + } + } else { + ESP_LOGE(TAG, "Failed to initialize I2C bus for audio codec"); + } + } + + return audio_codec; + } + + virtual Led* GetLed() override { + static SingleLed led_strip(BUILTIN_LED_GPIO);// 初始化单灯条对象 + return &led_strip; + } + + virtual Display* GetDisplay() override { + static Display display; // 空显示器对象,所有方法都是空实现 + return &display; + } + + virtual Backlight* GetBacklight() override { + return nullptr; + } + + // 获取IMU传感器数据 + bool GetImuData(qmi8658a_data_t* data) { + if (!imu_initialized_ || !imu_sensor_ || !data) { + return false; + } + + // 复制最新的IMU数据 + *data = latest_imu_data_; + return true; + } + + // 检查IMU传感器是否已初始化 + bool IsImuInitialized() const { + return imu_initialized_; + } + + // 获取生产测试模式状态 + bool IsProductionTestMode() const { + return production_test_mode_; + } + + // 唤醒PowerSaveTimer,从低功耗模式恢复到正常模式 + void WakeUp() override { + if (power_save_timer_) { + power_save_timer_->WakeUp(); + ESP_LOGI(TAG, "🔋 PowerSaveTimer已唤醒,从低功耗模式恢复到正常模式"); + } + } + + // 重启前回调:上报设备离线状态 + void OnBeforeRestart() override { + ESP_LOGI(TAG, "📤 设备即将重启,上报离线状态"); + rtc_online_ = false; + auto& wifi_station = WifiStation::GetInstance(); + if (wifi_station.IsConnected()) { + ReportDeviceStatus(battery_level_); + } + } +}; + +void MovecallMojiESP32S3::ReportDeviceStatus(int battery_level) { + ESP_LOGI(TAG, "📤 准备上报设备状态,电量: %d%%", battery_level); + + // 获取当前音量(语音调整后的音量优先,否则为系统默认音量) + auto codec = GetAudioCodec(); + int volume = codec ? codec->output_volume() : AudioCodec::default_output_volume(); + + // 构造JSON数据 + char json_buffer[512]; + snprintf(json_buffer, sizeof(json_buffer), + "{\"mac_address\":\"%s\",\"is_online\":%s," + "\"battery\":%d,\"volume\":%d,\"brightness\":%d}", + SystemInfo::GetBleMacAddress().c_str(), + rtc_online_ ? "true" : "false", + battery_level, + volume, + 50); // brightness默认50,后续根据实际调整 + + ESP_LOGI(TAG, "📤 上报数据: %s", json_buffer); + + // 创建HTTP客户端 + auto http = Board::GetInstance().CreateHttp(); + if (!http) { + ESP_LOGE(TAG, "❌ 创建HTTP客户端失败"); + return; + } + + // 设置请求头 + http->SetHeader("Content-Type", "application/json"); + + // 打开连接 + if (!http->Open("POST", DEVICE_STATUS_REPORT_URL, json_buffer)) { + ESP_LOGE(TAG, "❌ 连接服务器失败: %s", DEVICE_STATUS_REPORT_URL); + delete http; + return; + } + + // 获取响应 + auto response = http->GetBody(); + ESP_LOGI(TAG, "📥 服务器响应: %s", response.c_str()); + + // 关闭连接 + http->Close(); + delete http; + + ESP_LOGI(TAG, "✅ 设备状态上报完成"); +} + +// 初始化静态成员变量 +MovecallMojiESP32S3* MovecallMojiESP32S3::instance_ = nullptr; +QueueHandle_t MovecallMojiESP32S3::touch_event_queue_ = nullptr; + +// 处理触摸事件的任务 +static void TouchEventTask(void* arg) { + MovecallMojiESP32S3* board = (MovecallMojiESP32S3*)arg; + touch_event_data_t touch_event; + + ESP_LOGI(TAG, "触摸事件任务启动"); + + // 检查board指针 + if (board == nullptr) { + ESP_LOGE(TAG, "触摸事件任务收到无效的board指针"); + vTaskDelete(NULL); + return; + } + + // 检查触摸队列是否有效 + if (MovecallMojiESP32S3::touch_event_queue_ == nullptr) { + ESP_LOGE(TAG, "触摸事件队列未初始化"); + vTaskDelete(NULL); + return; + } + + ESP_LOGI(TAG, "触摸事件任务开始主循环"); + + // 用于跟踪每个触摸点的状态 + bool is_touch_active[4] = {false, false, false, false}; + uint32_t touch_start_time[4] = {0, 0, 0, 0}; + uint32_t last_press_time[4] = {0, 0, 0, 0}; // 记录最后一次按下时间,用于防抖 + const uint32_t RELEASE_DELAY_MS = 300; // 触摸释放延迟(毫秒) + const uint32_t PRESS_IGNORE_MS = 200; // 忽略连续按压的时间窗口(毫秒) + + while (1) { + if (xQueueReceive(MovecallMojiESP32S3::touch_event_queue_, &touch_event, 20 / portTICK_PERIOD_MS)) { + // 收到实际触摸事件(应该都是按下事件) + uint32_t current_time = esp_timer_get_time() / 1000; // 当前时间(毫秒) + + if (touch_event.pad_num >= 0 && touch_event.pad_num < 4) { + int pad = touch_event.pad_num; + + // 记录详细的调试信息 + ESP_LOGI(TAG, "TouchEventTask收到触摸事件 - 触摸板: %d, 事件类型: %s", + pad, touch_event.type == TOUCH_EVENT_PRESS ? "按下" : "释放"); + + // 过滤连续的按压事件,避免抖动 + if (touch_event.type == TOUCH_EVENT_PRESS) { + if (!is_touch_active[pad] || + (current_time - last_press_time[pad] > PRESS_IGNORE_MS)) { + // 设置该触摸点为激活状态 + is_touch_active[pad] = true; + touch_start_time[pad] = current_time; + last_press_time[pad] = current_time; + + // 处理按下事件 + board->HandleTouchEvent(pad, TOUCH_EVENT_PRESS); + } else { + ESP_LOGD(TAG, "忽略过于频繁的触摸事件 - 触摸板: %d", pad); + } + } + } else { + ESP_LOGW(TAG, "收到无效的触摸板编号: %d", touch_event.pad_num); + } + } else { + // 检查是否需要生成释放事件 + uint32_t current_time = esp_timer_get_time() / 1000; // 毫秒 + + // 检查每个触摸点 + for (int i = 0; i < 4; ++i) { + if (is_touch_active[i]) { + // 如果触摸点处于激活状态并超过释放延迟 + if (current_time - touch_start_time[i] >= RELEASE_DELAY_MS) { + // 生成释放事件 + ESP_LOGI(TAG, "生成触摸释放事件 - 触摸板: %d", i); + is_touch_active[i] = false; + board->HandleTouchEvent(i, TOUCH_EVENT_RELEASE); + } + } + } + + // 检查触摸状态 (使用touch_pad_read_raw_data直接读取触摸值) + touch_pad_t touch_pads[4] = {TOUCH_PAD_NUM1, TOUCH_PAD_NUM2, TOUCH_PAD_NUM3, TOUCH_PAD_NUM7}; + for (int i = 0; i < 4; i++) { + if (is_touch_active[i]) { + // 尝试读取当前触摸值,如果大于阈值,则触摸已释放 + uint32_t touch_value = 0; + esp_err_t ret = touch_pad_read_raw_data(touch_pads[i], &touch_value); + if (ret == ESP_OK && touch_value > 8000) { // 较大的值表示未触摸,保持高于阈值检测释放 + ESP_LOGI(TAG, "检测到触摸释放(传感器读数) - 触摸板: %d, 值: %" PRIu32, i, touch_value); + is_touch_active[i] = false; + board->HandleTouchEvent(i, TOUCH_EVENT_RELEASE); + } + } + } + } + } +} + +// 修改ISR函数处理触摸事件 +void IRAM_ATTR MovecallMojiESP32S3::TouchPadISR(void* arg) { + // 获取触摸状态 + uint32_t pad_intr = touch_pad_get_status(); + touch_pad_clear_status(); + + // 处理触摸事件 + touch_pad_t touch_pads[4] = {TOUCH_PAD_NUM1, TOUCH_PAD_NUM2, TOUCH_PAD_NUM3, TOUCH_PAD_NUM7}; + + for (int i = 0; i < 4; ++i) { + // 检查按下事件 + if (pad_intr & (1 << touch_pads[i])) { + // 生产测试模式:独立处理,不影响正常业务逻辑 + // 生产测试模式下触摸按键业务处理 新增代码 + // ============================================================================== + if (instance_->production_test_mode_) { + // 获取当前时间用于防抖 + uint32_t current_time = esp_timer_get_time() / 1000; // 转换为毫秒 + + // 检查防抖时间(500ms防抖间隔,避免重复触发) + if (current_time - instance_->touch_last_time_[i] > 500) { + // 设置触摸检测标志位 + instance_->touch_detected_flag_ = true; + instance_->touched_pad_index_ = i; + ESP_EARLY_LOGI(Pro_TAG, "🔧 检测到触摸事件,设置标志位 (触摸板%d)", i); + // 生产测试触摸音效 + const char* pad_names[4] = {"脑袋", "肚子", "下巴", "后背"}; + ESP_EARLY_LOGI(Pro_TAG, "生产测试:触摸板%d(%s)被触摸", i, pad_names[i]); + ESP_EARLY_LOGI(Pro_TAG, "生产测试:触摸板%d(%s)被触摸", i, pad_names[i]); + ESP_EARLY_LOGI(Pro_TAG, "生产测试:触摸板%d(%s)被触摸", i, pad_names[i]); + // // 通过Application播放音效(非阻塞)- 已禁用 + // auto& app = Application::GetInstance(); + // app.PlaySound(Lang::Sounds::P3_PUTDOWN_TOUCH); + // 更新最后触摸时间 + instance_->touch_last_time_[i] = current_time; + + // 重置标志位,为下次触摸做准备 + instance_->touch_detected_flag_ = false; + instance_->touched_pad_index_ = -1; + } else { + // 在防抖时间内,忽略触摸事件 + ESP_EARLY_LOGD(Pro_TAG, "触摸板%d防抖中,忽略触摸事件", i); + } + + // 生产测试模式下直接返回,不执行后续的正常业务逻辑 + return; + } + // ============================================================================== + + // 正常模式:保持原有的触摸处理逻辑 + // 创建按下事件 + touch_event_data_t event = { + .pad_num = i, + .type = TOUCH_EVENT_PRESS + }; + + // 发送到队列 - 只关注状态变化,消息处理由HandleTouchEvent负责 + if (MovecallMojiESP32S3::touch_event_queue_) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + BaseType_t result = xQueueSendFromISR(MovecallMojiESP32S3::touch_event_queue_, + &event, &xHigherPriorityTaskWoken); + + if (result == pdTRUE) { + ESP_EARLY_LOGI(TAG, "触摸事件已发送到队列"); + } else { + ESP_EARLY_LOGE(TAG, "触摸事件发送到队列失败"); + } + + if (xHigherPriorityTaskWoken) { + portYIELD_FROM_ISR(); + } + } else { + ESP_EARLY_LOGE(TAG, "触摸事件队列为空");// 队列为空是严重错误,无论什么模式都要记录 + } + } + } +} + +// 添加锁定触摸任务的方法实现 +void MovecallMojiESP32S3::LockTouchTask(int touch_pad_num) { + // 在生产测试模式下,不锁定触摸任务- 新增代码 + // ================================================================ + if (production_test_mode_) { + ESP_LOGI(TAG, "🔧 生产测试模式:跳过触摸任务锁定,保持连续测试能力"); + return; + } + // ================================================================ + + touch_task_locked_ = true; + active_touch_pad_ = touch_pad_num; + touch_task_start_time_ = esp_timer_get_time() / 1000; // 记录任务开始时间(毫秒) + ESP_LOGI(TAG, "触摸任务已锁定,活跃触摸点:%d", touch_pad_num); +} + +// 添加解锁触摸任务的方法实现 +void MovecallMojiESP32S3::UnlockTouchTask() { + // 先清除锁定状态和活跃触摸点 + touch_task_locked_ = false; + active_touch_pad_ = -1; + ESP_LOGI(TAG, "触摸任务已解锁,可以接收新的触摸"); + + // 重置所有触摸状态,但不调用其他复杂操作 + uint32_t current_time = esp_timer_get_time() / 1000; + for (int i = 0; i < 4; i++) { + touch_states_[i] = TOUCH_STATE_IDLE; + touch_last_time_[i] = current_time; + } + ESP_LOGI(TAG, "所有触摸状态已重置"); +} + +void MovecallMojiESP32S3::HandleTouchEvent(int touch_pad_num, touch_event_type_t event_type) { + if (touch_pad_num < 0 || touch_pad_num >= 4) return; + + // 获取当前时间 + uint32_t current_time = esp_timer_get_time() / 1000; // 毫秒 + + // 获取触摸点状态 + touch_state_t current_state = touch_states_[touch_pad_num]; + uint32_t time_elapsed = current_time - touch_last_time_[touch_pad_num]; + + // 添加更详细的调试信息 + const char* pad_names[4] = {"脑袋", "肚子", "下巴", "后背"}; + const char* state_names[4] = {"空闲", "按下", "释放", "去抖"}; + + ESP_LOGI(TAG, "[调试] 触摸事件处理 - 触摸板: %d(%s), 事件类型: %s, 当前状态: %s, 间隔: %" PRIu32 "ms", + touch_pad_num, + pad_names[touch_pad_num], + event_type == TOUCH_EVENT_PRESS ? "按下" : "释放", + state_names[current_state], + time_elapsed); + + // 检查触摸任务是否已锁定,如果锁定且不是活跃触摸点的事件,忽略该事件 + if (touch_task_locked_) { + // 在生产测试模式下,不锁定触摸任务,允许连续测试 + if (production_test_mode_) { + ESP_LOGI(TAG, "🔧 生产测试模式:忽略触摸任务锁定,允许连续测试"); + // 直接解锁并继续处理 + UnlockTouchTask(); + } else { + // 检查是否超时 + if (current_time - touch_task_start_time_ > TOUCH_TASK_TIMEOUT_MS) { + ESP_LOGW(TAG, "触摸任务超时(%" PRIu32 "ms),自动解锁", + current_time - touch_task_start_time_); + UnlockTouchTask(); + // 继续处理当前事件 + } + // 如果不是活跃触摸点的事件,忽略 + else if (touch_pad_num != active_touch_pad_) { + ESP_LOGI(TAG, "触摸任务已锁定,忽略非活跃触摸点(%d)的事件", touch_pad_num); + return; + } + } + } + + // 根据当前状态和事件类型进行状态转换 + switch (current_state) { + case TOUCH_STATE_IDLE: + if (event_type == TOUCH_EVENT_PRESS) { + // 从空闲转为按下状态 - 这是第一次触摸 + touch_states_[touch_pad_num] = TOUCH_STATE_PRESSED; + touch_last_time_[touch_pad_num] = current_time; + + // 锁定任务,确保只有当前触摸点的任务被执行 + if (!touch_task_locked_) { + LockTouchTask(touch_pad_num); + + // 触发一次消息 + SendTouchMessage(touch_pad_num); + power_save_timer_->WakeUp(); + } + + ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: IDLE -> PRESSED (首次触摸)", + touch_pad_num, pad_names[touch_pad_num]); + } + break; + + case TOUCH_STATE_PRESSED: + if (event_type == TOUCH_EVENT_PRESS) { + // 已经处于按下状态,忽略连续的按下事件 + ESP_LOGD(TAG, "忽略持续按下事件 - 触摸板: %d(%s)", touch_pad_num, pad_names[touch_pad_num]); + + } else if (event_type == TOUCH_EVENT_RELEASE) { + // 从按下转为释放状态 + touch_states_[touch_pad_num] = TOUCH_STATE_RELEASED; + touch_last_time_[touch_pad_num] = current_time; + + // 如果是活跃触摸点释放,解锁触摸任务 + if (touch_task_locked_ && touch_pad_num == active_touch_pad_) { + // 根据实际需求决定是否立即解锁 + // 这里我们延迟解锁,等待任务完成 + // 实际应用中,可能需要在任务完成后主动调用UnlockTouchTask + + // 示例:延迟1秒解锁,等待任务完成 + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + vTaskDelay(1000 / portTICK_PERIOD_MS); + if (board) { + board->UnlockTouchTask(); + } + vTaskDelete(NULL); + }, "unlock_touch", 4096, this, 5, NULL); + } + + ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: PRESSED -> RELEASED", + touch_pad_num, pad_names[touch_pad_num]); + } + break; + + case TOUCH_STATE_RELEASED: + if (event_type == TOUCH_EVENT_PRESS) { + // 如果释放后很快又被按下,可能是抖动,进入去抖状态 + if (time_elapsed < DEBOUNCE_TIME_MS) { + touch_states_[touch_pad_num] = TOUCH_STATE_DEBOUNCE; + touch_last_time_[touch_pad_num] = current_time; + + ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: RELEASED -> DEBOUNCE (可能抖动)", + touch_pad_num, pad_names[touch_pad_num]); + } else { + // 如果释放时间足够长,则认为是新的有效按下 + touch_states_[touch_pad_num] = TOUCH_STATE_PRESSED; + touch_last_time_[touch_pad_num] = current_time; + + // 触发一次新的消息 + SendTouchMessage(touch_pad_num); + power_save_timer_->WakeUp(); + + ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: RELEASED -> PRESSED (新的按下)", + touch_pad_num, pad_names[touch_pad_num]); + } + } else if (time_elapsed > MIN_RELEASE_TIME_MS) { + // 如果释放状态持续足够长,回到空闲状态 + touch_states_[touch_pad_num] = TOUCH_STATE_IDLE; + touch_last_time_[touch_pad_num] = current_time; + + ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: RELEASED -> IDLE (完全释放)", + touch_pad_num, pad_names[touch_pad_num]); + } + break; + + case TOUCH_STATE_DEBOUNCE: + if (event_type == TOUCH_EVENT_RELEASE) { + // 去抖动完成,回到释放状态 + touch_states_[touch_pad_num] = TOUCH_STATE_RELEASED; + touch_last_time_[touch_pad_num] = current_time; + + ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: DEBOUNCE -> RELEASED", + touch_pad_num, pad_names[touch_pad_num]); + } else if (event_type == TOUCH_EVENT_PRESS && time_elapsed > DEBOUNCE_TIME_MS) { + // 如果在去抖状态接收到新的按压且时间足够长,认为是有效按下 + touch_states_[touch_pad_num] = TOUCH_STATE_PRESSED; + touch_last_time_[touch_pad_num] = current_time; + + ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: DEBOUNCE -> PRESSED (确认按下)", + touch_pad_num, pad_names[touch_pad_num]); + } + break; + } +} + +// 添加一个新方法用于重置所有触摸状态 +void MovecallMojiESP32S3::ResetAllTouchStates() { + uint32_t current_time = esp_timer_get_time() / 1000; + + ESP_LOGI(TAG, "所有触摸状态已重置"); + + // 重置所有触摸点状态,简化日志输出 + for (int i = 0; i < 4; i++) { + touch_states_[i] = TOUCH_STATE_IDLE; + touch_last_time_[i] = current_time; + } + + // 清除触摸中断状态 + touch_pad_clear_status(); +} + +// 进入生产测试模式- 新增代码 +// ============================================================================== +void MovecallMojiESP32S3::EnterProductionTestMode() { + if (production_test_mode_) { + ESP_LOGI(TAG, "已经处于生产测试模式,忽略重复进入"); + return; + } + + production_test_mode_ = true; + esp_log_level_set("*", ESP_LOG_INFO); + esp_log_level_set("MovecallMojiESP32S3", ESP_LOG_INFO); + esp_log_level_set("Airhub1", ESP_LOG_INFO); + esp_log_level_set("AFE", ESP_LOG_ERROR); + + ESP_LOGI(Pro_TAG, "🔧 已进入生产测试模式,可以开始测试!");// 生产测试打印 + + auto& app = Application::GetInstance(); + auto* codec = GetAudioCodec(); + if (codec) { + codec->EnableOutput(true); + ESP_LOGI(TAG, "🔊 测试模式:已启用音频输出"); + } + app.PlaySound(Lang::Sounds::P3_TEST_MODAL); + ESP_LOGI(TAG, "🎵 测试模式:开始播放进入测试模式音频"); + + ESP_LOGI(Pro_TAG, "🔧 生产测试模式:重新初始化IMU传感器"); + InitializeImuSensor(); + + // 检查IMU传感器初始化状态 + ESP_LOGI(Pro_TAG, "🔧 生产测试:IMU传感器初始化状态: %s", imu_initialized_ ? "成功" : "失败"); + + if (imu_initialized_ && imu_sensor_) { + ESP_LOGI(Pro_TAG, "🔧 姿态传感器已初始化成功! 可以开始测试运动检测功能"); + xTaskCreate( + [](void* arg) { + vTaskDelay(pdMS_TO_TICKS(1500)); + auto& app_local = Application::GetInstance(); + app_local.PlaySound(Lang::Sounds::P3_TUOLUOYI); + ESP_LOGI(Pro_TAG, "🎵 播放陀螺仪检测成功音频"); + vTaskDelete(NULL); + }, + "play_tuoluo_audio", + 4096, + nullptr, + 5, + nullptr + ); + } else { + ESP_LOGI(Pro_TAG, "姿态传感器初始化失败或未连接!"); + + // 尝试再次检测连接 + if (codec_i2c_bus_) { + uint8_t detected_address = 0; + bool sensor_connected = QMI8658A::CheckConnection(codec_i2c_bus_, &detected_address); + if (sensor_connected) { + ESP_LOGI(Pro_TAG, "🔧 姿态传感器物理连接存在! 设备地址:0x%02X,但初始化失败", detected_address); + } + } + } + + // 非阻塞式触摸检测 - 触摸事件将通过现有的触摸处理机制来处理 + ESP_LOGI(Pro_TAG, "🔧 生产测试模式已启用,触摸检测已激活,其他按键功能正常可用"); + ESP_LOGI(Pro_TAG, "🔧 提示:现在可以测试触摸板、BOOT按键和讲故事按键"); +} +// ============================================================================== + +DECLARE_BOARD(MovecallMojiESP32S3); diff --git a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h new file mode 100644 index 0000000..fc4edeb --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h @@ -0,0 +1,52 @@ +#pragma once + +#include "wifi_board.h" +#include "boards/common/qmi8658a.h" + +namespace iot { + class ImuSensorThing; +} + +class PowerSaveTimer; + +class MovecallMojiESP32S3 : public WifiBoard { +public: + MovecallMojiESP32S3(); + ~MovecallMojiESP32S3(); + + // IMU传感器相关方法 + bool IsImuInitialized() const; + bool GetImuData(qmi8658a_data_t* data); + void OnMotionDetected(); + + // 触摸相关方法 + void LockTouchTask(int touch_pad_num); + void UnlockTouchTask(); + void ResetAllTouchStates(); + + // 生产测试模式 + void EnterProductionTestMode(); + +private: + // 私有成员变量和方法的声明 + PowerSaveTimer* power_save_timer_; + static MovecallMojiESP32S3* instance_; + + // IMU传感器相关 + QMI8658A* imu_sensor_; + esp_timer_handle_t imu_timer_handle_; + qmi8658a_data_t latest_imu_data_; + bool imu_initialized_; + iot::ImuSensorThing* imu_thing_; + + // 其他私有成员... + // (完整的私有成员列表在.cc文件中) + + // 私有方法声明 + void InitializeImuSensor();// 初始化IMU传感器(QMI8658A 陀螺仪) + void InitializeIot();// 初始化IoT设备(包括IMU传感器) + void ProcessImuData(const qmi8658a_data_t* data);// 处理IMU数据的方法 + + // 静态回调函数 + static void ImuTimerCallback(void* arg);// IMU传感器定时器回调函数 +}; \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/qmi8658-master/README.md b/main/boards/movecall-moji-esp32s3/qmi8658-master/README.md new file mode 100644 index 0000000..bc1a56a --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/qmi8658-master/README.md @@ -0,0 +1,199 @@ +I2C接口:I2C3 +base_moduke +hd_i2c + +芯片SCL max 400kHz + +## 复位 +上电复位: +通过将 VDD 和 VDDIO 线从断电状态(VDD = 0V,VDDIO = 0V)驱动到有效工作范围来初始化上电复位。 详见 3.2。 上电复位过程从 VDDIO 开始,并且 VDD 在最终电平的 1% 以内。 +触发复位(上电复位和软件复位)后,QMI8658 将运行复位过程。 UI 寄存器、内部 RAM、FIFO 将设置为默认值,模拟和数字电路将被禁用。 + +3.2 推荐工作条件 +|符号| 参数 |MIN|TYPE|MAX|单位| +|:--:|:--:|:--:|:--:|:--:|:--:| +|VDD|供电电压|1.71|1.8|3.6|V| +|VDDIO|IO脚供电电压|1.71|1.8|3.6|V| +|V(IL)|数字低电平输入电压| | |0.3*VDDIO|V| +|V(IH)|数字高电平输入电压|0.7*VDDIO| |0.3+VDDIO|V| +|V(OL)|数字低电平输出电压| | |0.1*VDDIO|V| +|V(OH)|数字高电平输出电压|0.9*VDDIO| | |V| + +## 校准(COD) +支持陀螺仪X轴和Y轴的按需校准。 基于内部集成功能,QMI8658A可以校准陀螺仪X轴和Y轴的内部增益,从而获得更精确的灵敏度,并在QMI8658A芯片上更紧密地分布X轴和Y轴灵敏度。 +请注意, 陀螺仪的 Z 轴不受 COD 影响。 +### 校准步骤 +1. 设置 CTRL7.aEN = 0 和 CTRL7.gEN = 0,禁用加速度计和陀螺仪。 +2. 通过 CTRL9 命令发出 CTRL_CMD_ON_DEMAND_CALIBRATION (0xA2)。 +3. 等待约 1.5 秒让 QMI8658A 完成 CTRL9 命令。 +4. 读取 COD_STATUS 寄存器 (0x46) 以检查 COD 实现的结果/状态。 + +### 校准注意事项: +在此过程中,建议将设备置于安静状态,否则 COD 可能会失败并报错。 +如果成功,之后重新校准的增益参数将应用于传感器数据。 更新后的增益输出到 UI 寄存器,主机可以读取,参见 14.3。 如果执行上电复位或软复位,重新校准的增益参数将丢失,QMI8658A 将使用片上默认增益参数。 +如果失败,不影响陀螺仪的运行,QMI8658A 将继续使用之前的可用参数(最后一次成功的 COD 参数或片内默认参数)。 + +### 校准状态指示 +如果 COD 命令成功执行,COD_STATUS 寄存器将输出 0x00 进行指示 +### 校准参数更新 +成功 COD 后,经过 COD 校正的新增益将应用于陀螺仪 X 和 Y 轴的未来数据。 同时,新的参数会更新到下面的寄存器中,供主机读取和保存。 +1. Gyro-X gain (16 bits) will be in dVX_L and dVX_H registers (0x51, 0x52) +2. Gyro-Y gain (16 bits) will be in dVY_L and dVY_H registers (0x53, 0x54) +3. Gyro-Z gain (16 bits) will be in dVZ_L and dVZ_H registers (0x55, 0x56) + +如果主机保存了这些增益参数,则可以使用 CTRL9 命令 CTRL_CMD_APPLY_GYRO_GAINS (0xAA) 将它们传回 QMI8658A(无需再次调用 COD 例程),如下所示: +1. 通过设置 CTRL7.aEN = 0 和 CTRL7.gEN = 0 禁用加速度计和陀螺仪 +2. 将 Gyro-X 增益(16 位)写入寄存器 CAL1_L 和 CAL1_H 寄存器(0x0B,0x0C) +3. 将 Gyro-Y 增益(16 位)写入寄存器 CAL2_L 和 CAL2_H 寄存器(0x0D,0x0E) +4. 将 Gyro-Z 增益(16 位)写入寄存器 CAL3_L 和 CAL3_H 寄存器(0x0F,0x10) +5. 将 0xAA 写入 CTRL9 并遵循 CTRL9 协议 + +一旦 CTRL9 命令成功完成,恢复的增益将对 Gyroscope 的未来数据生效。 +请注意,始终建议不时运行 COD 以应用 Gyro-X 和 Gyro-Y 灵敏度的精确和最新校正。 设计人员应小心恢复过时的增益参数,尤其是当 PCB 应力发生显着变化时。 + +## 自检 +### 加速度计自检 +加速度计自检 (Check-Alive) 用于确定加速度计是否正常工作并在可接受的参数范围内工作。 +它是通过施加静电力来驱动加速度计的三个 X、Y 和 Z 轴中的每一个来实现的。 如果加速度计机械结构通过感应至少 200 mg 来响应此输入刺激,则可以认为加速度计是正常工作的。 +加速度计自检数据可在寄存器 dVX_L、dVX_H、dVY_L、dVY_H、dVZ_L 和 dVZ_H 中读取。 主机可以通过以下程序随时启动自检。 +加速度计自检程序: +1. 禁用传感器 (CTRL7 = 0x00)。 +2. 将适当的加速度计 ODR (CTRL2.aODR) 和位 CTRL2.aST (bit7) 设置为 1 以触发自检。 +3. 等待 QMI8658A 将 INT2 驱动为高电平,如果 INT2 已启用(CTRL1.bit4 = 1),或者 STATUSINT.bit0 设置为 1。 +4. 将 CTRL2.aST(bit7) 设置为 0,以清除 STATUSINT1.bit0 和/或 INT2。 +5. 检查 QMI8658A 是否将 INT2 驱动回低电平,并将 STATUSINT1.bit0 设置为 0。 +6. 阅读 Accel 自检结果: + * X channel: dVX_L and dVX_H (registers 0x51 and 0x52) + * Y channel: dVY_L and dVY_H (registers 0x53 and 0x54) + * Z channel: dVZ_L and dVZ_H (registers 0x55 and 0x56) + + 结果为 16 位,格式为 U5.11,分辨率为 0.5mg (1 / 2^11 g)。 + 如果所有三个轴的绝对结果都高于 200mg,则可以认为加速度计功能正常。 否则,加速度计不能被认为是有效的。 +请注意,自检功能会自动将 full-scall 设置为 16g,并使用用户设置的 aODR (CTRL2.aODR)。 在自检结束时,QMI8658A 将在启动 Check-Alive) 例程之前使用用户设置的原始值更新 CTR2。 +自检的典型时间(从将 aST 设置为 1,直到启用 INT2 的上升沿,或 STATUSINT.bit0 设置为 1)大约需要 25 个 ODR: +* 25ms @ 1KHz ODR +* 800ms @ 32Hz ODR +* 2.2s @ 11Hz ODR + +### 陀螺仪自检 +陀螺仪自检 (Check-Alive) 用于确定陀螺仪是否正常工作。 +它是通过施加静电力来驱动陀螺仪的三个 X、Y 和 Z 轴中的每一个并测量相应 X、Y 和 Z 轴上的机械响应来实现的。 如果陀螺仪输出的等效幅度对于每个轴大于 300dps,则可以认为该陀螺仪正常工作。 +陀螺仪自检数据可在输出寄存器 dVX_L、dVX_H、dVY_L、dVY_H、dVZ_L 和 dVZ_H 中读取。 主机可以通过以下程序随时启动自检。 +陀螺仪自检程序: +1. 禁用传感器 (CTRL7 = 0x00)。 +2. 将位 gST 设置为 1。(CTRL3.bit7 = 1'b1)。 +3. 等待 QMI8658A 将 INT2 驱动为高电平,如果 INT2 已启用,或者 STATUSINT.bit0 设置为 1。 +4. 将 CTRL3.aST(bit7) 设置为 0,以清除 STATUSINT1.bit0 和/或 INT2。 +5. 检查 QMI8658A 是否将 INT2 驱动回低电平,或将 STATUSINT1.bit0 设置为 0。 +6. 读取陀螺仪自检结果: +* X channel: dVX_L and dVX_H (registers 0x51 and 0x52) +* Y channel: dVY_L and dVY_H (registers 0x53 and 0x54) +* Z channel: dVZ_L and dVZ_H (registers 0x55 and 0x56) + +以 U12.4 格式读取 16 位结果,分辨率为 62.5mdps (1 / 2^4 dps)。 +如果所有三个轴的绝对结果都高于 300dps,则可以认为陀螺仪功能正常。 否则,陀螺仪不能被认为是功能性的。 +请注意,自检功能会自动设置 CTRL3 的满量程 (gFS) 和 ODR (gODR)。 在自检结束时,QMI8658A 将在开始自检程序之前将 CTR3 更新为用户设置的原始值。 +自检过程的典型时间(从将 gST 写入 1,直到 INT2 的上升沿(如果启用,或 STATUSINT.bit0 设置为 1))的成本约为 400 毫秒。 + +## Ctrl9 +### 写Ctrl9 +1. 主机需要将此命令所需的数据提供给 QMI8658A。 主机通常通过将数据放置在一组称为 CAL 寄存器的寄存器中来实现这一点。 最多使用八个 CAL 寄存器。 请参阅表 29。 +2. 使用适当的命令值写入 Ctrl9 寄存器 0x0A,请参见表 28。 +3. 一旦根据命令值执行了相应的功能,设备将设置 STATUSINT.bit7 为 1,并提高 INT1(如果 CTRL1.bit3 = 1 & CTRL8.bit7 == 0)。 +4. 主机必须通过将 CTRL_CMD_ACK (0x00) 写入 CTRL9 寄存器来确认这一点,STATUSINT.bit7 (CmdDone) 将在收到 CTRL_CMD_ACK 命令时重置为 0。 如果 CTRL1.bit3 = 1 & CTRL8.bit7 == 0,则 INT1 在寄存器读取时被拉低。 +5. 如果设备需要任何数据,此时将可用。 为每个命令单独指定数据的位置。 + +### 读Ctrl9 +1. 使用适当的命令值写入 Ctrl9 寄存器 0x0A。 +2. 一旦根据命令值执行了相应的功能,设备将设置 STATUSINT.bit7 为 1,并提高 INT1(如果 CTRL1.bit3 = 1 & CTRL8.bit7 == 0)。 +3. 主机必须通过将 CTRL_CMD_ACK (0x00) 写入 CTRL9 寄存器来确认这一点,STATUSINT.bit7 (CmdDone) 将在收到 CTRL_CMD_ACK 命令时复位为 0。 如果 CTRL1.bit3 = 1 & CTRL8.bit7 == 0,则 INT1 在寄存器读取时被拉低。 +4. 数据可从设备的 CAL 寄存器中获得。 为每个命令单独指定数据的位置。 + +### Ctrl9详细命令说明 +* CTRL_CMD_ACK(0x00) +主机在收到 CmdDone 信息时确认 QMI8658A,以结束 CTRL9 协议。 +* CTRL_CMD_RST_FIFO(0x04) +将 0x04 写入 Ctrl9 寄存器 0x​​0a 的 CTRL9 命令允许主机指示设备重置 FIFO。 FIFO 数据、样本计数和标志将被清除并重置为默认状态。 +* CTRL_CMD_REQ_FIFO(0x05) +当主机想要通过 CTRL9 进程写入 0x05 从 FIFO 中获取数据时,会发出此 CTRL9 命令。 +成功完成 CTRL9 过程后,将启用 FIFO 读取模式,设备将 FIFO 数据定向到 FIFO_DATA 寄存器(0x17),直到 FIFO 为空。读取 FIFO 数据后,主机必须通过写入 FIFO_CTRL 寄存器将 FIFO_CTRL.FIFO_rd_mode 设置为 0,这将导致 FIFO_STATUS.FIFO_WTM/FIFO_FULL 被清除和/或 INT 引脚(如果启用)被取消断言。请参阅错误!未找到参考来源。错误!未找到参考来源。 CTRL9 操作,详见 8.8。 +* CTRL_CMD_WRITE_WOM_SETTING(0x08) +当主机想要启用/修改设备的运动唤醒功能的触发阈值或消隐间隔时,会发出此 CTRL9 命令。有关设置此功能的详细信息,请参阅 12 运动唤醒 (WoM)。一旦指定的 CAL 寄存器加载了适当的数据,就会通过将 0x08 写入 CTRL9 寄存器 0x​​0A 来发出命令。 +* CTRL_CMD_ACCEL_HOST_DELTA_OFFSET(0x09) +当主机想要手动更改加速度计偏移时,会发出此 CTRL9 命令。 每个增量偏移值应包含 16 位,格式为有符号 4.12(12 小数位,单位为 1 / 2^12)。 用户必须将偏移量写入以下寄存器: + * Accel_Delta_X : {CAL1_H, CAL1_L} + * Accel_Delta_Y : {CAL2_H, CAL2_L} + * Accel_Delta_Z : {CAL3_H, CAL3_L} + 接下来,通过将 0x09 写入 CTRL9 寄存器 0x0A 来发出命令。 请注意,当传感器重新上电或系统重置时,此偏移更改会丢失。 +* CTRL_CMD_GYRO_HOST_DELTA_OFFSET(0x0A) +当主机想要手动更改陀螺仪偏移时发出此 CTRL9 命令。 每个增量偏移值应包含 16 位,格式为有符号 11.5(5 个小数位,单位为 1 / 2^5)。 用户必须将偏移量写入以下寄存器: + * Gyro_Delta_X : {CAL1_H, CAL1_L} + * Gyro_Delta_Y : {CAL2_H, CAL2_L} + * Gyro_Delta_Z : {CAL3_H, CAL3_L} +接下来,通过将 0x0A 写入 CTRL9 寄存器 0x0A 来发出命令。 请注意,当传感器重新上电或系统重置时,此偏移更改会丢失。 +* CTRL_CMD_CONFIGURE_TAP(0x0C) +此 CTRL9 命令用于配置 Tap 检测参数。 有关详细信息,请参阅 10.3 配置 Tap。 +* CTRL_CMD_CONFIGURE_PEDOMETER(0x0D) +发出此 CTRL9 命令以配置计步器检测的参数。 有关详细信息,请参阅 11.2 配置计步器。 +* CTRL_CMD_CONFIGURE_MOTION(0x0E) +发出此 CTRL9 命令以配置运动检测的参数。 请参阅 9.4 配置移动侦测。 +* CTRL_CMD_RESET_PEDOMETER(0x0F) +发出此 CTRL9 命令以清除计步器的步数。 有关详细信息,请参阅 11.6 重置步数。 +* CTRL_CMD_COPY_USID(0x10) +USID 是每个 QMI8658A 部件的唯一 ID。 +此 CTRL9 命令将以下数据复制到 UI 寄存器中。 它由主机将 0x10 写入 CTRL9 来启动。 发出命令后,主机可以从如下所示的寄存器中读取数据: +FW_Version byte 0 → dQW_L +FW_Version byte 1 → dQW_H +FW_Version byte 2 → dQX_L +USID_Byte_0 → dVX_L +USID_Byte_1 → dVX_H +USID_Byte_2 → dVY_L +USID_Byte_3 → dVY_H +USID_Byte_4 → dVZ_L +USID_Byte_5 → dVZ_H +请注意,在上电复位或软复位成功后,FW_Version 和 USID 将自动复制到相应的寄存器一次,以供主机读取。 这些寄存器可以在启用传感器后更改,因此在读取之前应执行 CTRL_CMD_COPY_USID 命令将 FW_Version 和 USID 复制到相应的寄存器中。 +* TRL_CMD_SET_RPU(0x11) +此 CTRL9 命令在主机配置 IO 上拉电阻时发出。 每个位控制一个电阻器组合,如表 30 所示: + +|Bit|名称|pin脚|说明|默认值| +|:--:|:--:|:--:|:--:|:--:| +|0|aux_rpu_dis|SDx,SCx,RESV-NC(pin10)|0:启用上拉 1:禁用上拉|0| +|1|icm_rpu_dis|SDx|0:启用上拉 1:禁用上拉|0| +|2|cs_rpu_dis|CS|0:启用上拉 1:禁用上拉|0| +|3|i2c_rpu_dis|SCL, SDA|0:启用上拉 1:禁用上拉|0| +|4:7|保留|NA||| + +主机通过发出带有 0x11 的 WCtrl9 命令写入适当的 CAL1_L 位。 +默认情况下,启用所有上拉电阻。 向该位写入 1 将相应地禁用上拉电阻,而写入 0 将启用上拉电阻。 +* CTRL_CMD_AHB_CLOCK_GATING(0x12) +当设置了锁定机制(CTRL7.bit7 == 1(syncSmpl))时,应该禁用CTRL_CMD_AHB_CLOCK_GATING,以保证数据读取的锁定机制,防止可能的错位。 有关详细信息,请参阅 14 按需校准 (COD)。 +* CTRL_CMD_ON_DEMAND_CALIBRATION(0xA2) +此 CTRL9 命令使主机能够不时重新校准陀螺仪灵敏度。 请参阅 14 按需校准 (COD)。 +* CTRL_CMD_APPLY_GYRO_GAINS(0xAA) +此 CTRL9 命令使主机能够将保存的陀螺仪增益恢复到 QMI8658A,以避免再次运行 COD。 当环境发生显着变化时不建议这样做,例如显着的 PCB 应力变化。 请参阅 14.4 保存和恢复新的增益参数。 + +## 中断 +QMI8658A 有两条中断线,INT1 和 INT2。 +通过配置 CTRL1.bit3(INT1) 或 CTRL1.bit4(INT2),可以将 INT1 和 INT2 配置为 High-Z 模式或 Push-Pull 模式。如果 CTRL1.bit3 (CTRL1.bit4) 设置为 0,则 INT1(INT2) 将相应地设置为 High-Z 模式。而如果 CTRL1.bit3 (CTRL1.bit4) 设置为 1,则 INT1(INT2) 将相应地设置为 Push-Pull 模式。默认情况下,INT1 和 INT2 处于高阻模式。 +如果 QMI8658A 配置为运动唤醒 (WoM) 模式,则不会生成传感器数据。 INT 引脚行为遵循 WoM 的配置,请参阅 12 运动唤醒 (WoM)。 +如果 QMI8658A 未处于运动唤醒模式,则中断映射有两种模式,如下所述。主机可以将多个内部信号/中断源配置到 INT 引脚(INT1 和/或 INT2)。如果驱动到一个 INT 引脚,则多个源在 LOGIC-OR 中起作用。 +### 同步采样模式 +SyncSample 模式支持在读取过程中锁定值。请参阅 13 锁定机构有关传感器数据寄存器的详细信息。 + +设置 CTRL7.bit7(SyncSample) == 1 将启用 SyncSample 模式。 +如图 12 所示。在 SyncSample 模式下,CTRL9 握手信号将被路由到 INT1。详情请查看 5.10。 +如果启用,运动事件中断(任何运动、无运动、显着运动、计步器、轻敲)将路由到 INT1。 +该模式不支持 FIFO 功能,DRDY 信号将被路由到 INT2。 +### 非同步采样模式 +该模式支持 FIFO 功能和自由中断配置,如图 13 所示。 +如果 CTRL7.bit7(SyncSample) == 0,则 STATUSINT 寄存器的第 1 位将具有与 INT1 相同的值,而 STATUSINT 寄存器的第 0 位将具有与 INT2 相同的值。 +在 Non-SyncSample 模式下,CTRL9 握手有两种方法。如果设置 CTRL8.bit7 = 0,主机可以检查 INT1 引脚是否为握手信号的高电平;如果设置 CTRL8.bit7 = 1,则轮询 STATUSINT.bit7 进行握手。 +在 Non-SyncSample 模式下,可以通过设置 CTRL8.bit6 = 1 将运动事件中断配置为 INT1,或者通过设置 CTRL8.bit6 = 0 将运动事件中断配置为 INT2。请注意,运动事件引擎可以通过 CTRL8.bit 启用[4:0],详见表 22。 +在 Non-SycnSample 模式下,传感器数据可以通过数据寄存器或 FIFO 输出。配置 FIFO_CTRL.FIFO_MODE = ‘bypass’ 模式,将启用 DRDY 功能并禁用 FIFO 功能;配置 FIFO_CTRL.FIFO_MODE = other 模式,将启用 FIFO 功能并禁用 DRDY 功能。 +如果启用 FIFO 模式,如果 CTRL1.bit2 设置为 1,则 FIFO 中断可以配置到 INT1 引脚,如果 CTRL1.bit2 设置为 0,则可以将 FIFO 中断配置到 INT2 引脚。有关 FIFO 中断行为的更多详细信息,请参阅 8 FIFO 说明。 +如果启用 DRDY 模式,如果 CTRL7.bit5(DRDY_DIS) 设置为 0,则 DRDY 信号将被路由到 INT2,如果 CTRL7.bit5(DRDY_DIS) 设置为 1,则 DRDY 信号将被路由到 INT2 引脚。 + + +|cmd|value|addr|time max(ms)|flag addr|flag value| +|:--:|:--:|:--:|:--:|:--:|:--:| +|reset| 0x0B|0x60|15|0x4D|0x80| \ No newline at end of file diff --git a/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658.c b/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658.c new file mode 100644 index 0000000..77bf1b0 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658.c @@ -0,0 +1,1034 @@ +#include "qmi8658.h" + +//#define QMI8658_UINT_MG_DPS +#define M_PI (3.14159265358979323846f) +#define ONE_G (9.807f) +#define QFABS(x) (((x)<0.0f)?(-1.0f*(x)):(x)) + + +static qmi8658_state g_imu; +#if defined(QMI8658_USE_CALI) +static qmi8658_cali g_cali; +#endif + +unsigned char qmi8658_write_reg(unsigned char reg, unsigned char value) +{ + unsigned char ret=0; + unsigned int retry = 0; + + while((!ret) && (retry++ < 5)) + { +#if defined(QMI8658_USE_SPI) + ret = qst_imu_spi_write(reg,value); +#elif defined(QST_USE_SW_I2C) + ret = qst_sw_writereg(g_imu.slave<<1, reg, value); +#else + ret = mx_i2c1_write(g_imu.slave<<1, reg, value); +#endif + } + return ret; +} + +unsigned char qmi8658_write_regs(unsigned char reg, unsigned char *value, unsigned char len) +{ + int i, ret; + + for(i=0; i=4 && layout <= 7) + { + data_a[2] = -data_a[2]; + data_g[2] = -data_g[2]; + } + + if(layout%2) + { + data_a[0] = raw[1]; + data_a[1] = raw[0]; + + data_g[0] = raw_g[1]; + data_g[1] = raw_g[0]; + } + else + { + data_a[0] = raw[0]; + data_a[1] = raw[1]; + + data_g[0] = raw_g[0]; + data_g[1] = raw_g[1]; + } + + if((layout==1)||(layout==2)||(layout==4)||(layout==7)) + { + data_a[0] = -data_a[0]; + data_g[0] = -data_g[0]; + } + if((layout==2)||(layout==3)||(layout==6)||(layout==7)) + { + data_a[1] = -data_a[1]; + data_g[1] = -data_g[1]; + } +} + +#if defined(QMI8658_USE_CALI) +void qmi8658_data_cali(unsigned char sensor, float data[3]) +{ + float data_diff[3]; + + if(sensor == 1) // accel + { + data_diff[0] = QFABS((data[0]-g_cali.acc_last[0])); + data_diff[1] = QFABS((data[1]-g_cali.acc_last[1])); + data_diff[2] = QFABS((data[2]-g_cali.acc_last[2])); + g_cali.acc_last[0] = data[0]; + g_cali.acc_last[1] = data[1]; + g_cali.acc_last[2] = data[2]; + +// qmi8658_log("acc diff : %f ", (data_diff[0]+data_diff[1]+data_diff[2])); + if((data_diff[0]+data_diff[1]+data_diff[2]) < 0.5f) + { + if(g_cali.acc_cali_num == 0) + { + g_cali.acc_sum[0] = 0.0f; + g_cali.acc_sum[1] = 0.0f; + g_cali.acc_sum[2] = 0.0f; + } + if(g_cali.acc_cali_num < QMI8658_CALI_DATA_NUM) + { + g_cali.acc_cali_num++; + g_cali.acc_sum[0] += data[0]; + g_cali.acc_sum[1] += data[1]; + g_cali.acc_sum[2] += data[2]; + if(g_cali.acc_cali_num == QMI8658_CALI_DATA_NUM) + { + if((g_cali.acc_cali_flag == 0)&&(data[2]<11.8f)&&(data[2]>7.8f)) + { + g_cali.acc_sum[0] = g_cali.acc_sum[0]/QMI8658_CALI_DATA_NUM; + g_cali.acc_sum[1] = g_cali.acc_sum[1]/QMI8658_CALI_DATA_NUM; + g_cali.acc_sum[2] = g_cali.acc_sum[2]/QMI8658_CALI_DATA_NUM; + + g_cali.acc_bias[0] = 0.0f - g_cali.acc_sum[0]; + g_cali.acc_bias[1] = 0.0f - g_cali.acc_sum[1]; + g_cali.acc_bias[2] = 9.807f - g_cali.acc_sum[2]; + g_cali.acc_cali_flag = 1; + } + g_cali.imu_static_flag = 1; + qmi8658_log("qmi8658 acc static!!!\n"); + } + } + + if(g_cali.imu_static_flag) + { + if(g_cali.acc_fix_flag == 0) + { + g_cali.acc_fix_flag = 1; + g_cali.acc_fix[0] = data[0]; + g_cali.acc_fix[1] = data[1]; + g_cali.acc_fix[2] = data[2]; + } + } + else + { + g_cali.acc_fix_flag = 0; + g_cali.gyr_fix_flag = 0; + } + } + else + { + g_cali.acc_cali_num = 0; + g_cali.acc_sum[0] = 0.0f; + g_cali.acc_sum[1] = 0.0f; + g_cali.acc_sum[2] = 0.0f; + + g_cali.imu_static_flag = 0; + g_cali.acc_fix_flag = 0; + g_cali.gyr_fix_flag = 0; + } + + if(g_cali.acc_fix_flag) + { + if(g_cali.acc_fix_index != 0) + g_cali.acc_fix_index = 0; + else + g_cali.acc_fix_index = 1; + + data[0] = g_cali.acc_fix[0] + g_cali.acc_fix_index*0.01f; + data[1] = g_cali.acc_fix[1] + g_cali.acc_fix_index*0.01f; + data[2] = g_cali.acc_fix[2] + g_cali.acc_fix_index*0.01f; + } + if(g_cali.acc_cali_flag) + { + g_cali.acc[0] = data[0] + g_cali.acc_bias[0]; + g_cali.acc[1] = data[1] + g_cali.acc_bias[1]; + g_cali.acc[2] = data[2] + g_cali.acc_bias[2]; + data[0] = g_cali.acc[0]; + data[1] = g_cali.acc[1]; + data[2] = g_cali.acc[2]; + } + else + { + g_cali.acc[0] = data[0]; + g_cali.acc[1] = data[1]; + g_cali.acc[2] = data[2]; + } + } + else if(sensor == 2) // gyroscope + { + data_diff[0] = QFABS((data[0]-g_cali.gyr_last[0])); + data_diff[1] = QFABS((data[1]-g_cali.gyr_last[1])); + data_diff[2] = QFABS((data[2]-g_cali.gyr_last[2])); + g_cali.gyr_last[0] = data[0]; + g_cali.gyr_last[1] = data[1]; + g_cali.gyr_last[2] = data[2]; + +// qmi8658_log("gyr diff : %f \n", (data_diff[0]+data_diff[1]+data_diff[2])); + if(((data_diff[0]+data_diff[1]+data_diff[2]) < 0.03f) + && ((data[0]>-1.0f)&&(data[0]<1.0f)) + && ((data[1]>-1.0f)&&(data[1]<1.0f)) + && ((data[2]>-1.0f)&&(data[2]<1.0f)) + ) + { + if(g_cali.gyr_cali_num == 0) + { + g_cali.gyr_sum[0] = 0.0f; + g_cali.gyr_sum[1] = 0.0f; + g_cali.gyr_sum[2] = 0.0f; + } + if(g_cali.gyr_cali_num < QMI8658_CALI_DATA_NUM) + { + g_cali.gyr_cali_num++; + g_cali.gyr_sum[0] += data[0]; + g_cali.gyr_sum[1] += data[1]; + g_cali.gyr_sum[2] += data[2]; + if(g_cali.gyr_cali_num == QMI8658_CALI_DATA_NUM) + { + if(g_cali.gyr_cali_flag == 0) + { + g_cali.gyr_sum[0] = g_cali.gyr_sum[0]/QMI8658_CALI_DATA_NUM; + g_cali.gyr_sum[1] = g_cali.gyr_sum[1]/QMI8658_CALI_DATA_NUM; + g_cali.gyr_sum[2] = g_cali.gyr_sum[2]/QMI8658_CALI_DATA_NUM; + + g_cali.gyr_bias[0] = 0.0f - g_cali.gyr_sum[0]; + g_cali.gyr_bias[1] = 0.0f - g_cali.gyr_sum[1]; + g_cali.gyr_bias[2] = 0.0f - g_cali.gyr_sum[2]; + g_cali.gyr_cali_flag = 1; + } + g_cali.imu_static_flag = 1; + qmi8658_log("qmi8658 gyro static!!!\n"); + } + } + + if(g_cali.imu_static_flag) + { + if(g_cali.gyr_fix_flag == 0) + { + g_cali.gyr_fix_flag = 1; + g_cali.gyr_fix[0] = data[0]; + g_cali.gyr_fix[1] = data[1]; + g_cali.gyr_fix[2] = data[2]; + } + } + else + { + g_cali.gyr_fix_flag = 0; + g_cali.acc_fix_flag = 0; + } + } + else + { + g_cali.gyr_cali_num = 0; + g_cali.gyr_sum[0] = 0.0f; + g_cali.gyr_sum[1] = 0.0f; + g_cali.gyr_sum[2] = 0.0f; + + g_cali.imu_static_flag = 0; + g_cali.gyr_fix_flag = 0; + g_cali.acc_fix_flag = 0; + } + + if(g_cali.gyr_fix_flag) + { + if(g_cali.gyr_fix_index != 0) + g_cali.gyr_fix_index = 0; + else + g_cali.gyr_fix_index = 1; + + data[0] = g_cali.gyr_fix[0] + g_cali.gyr_fix_index*0.00005f; + data[1] = g_cali.gyr_fix[1] + g_cali.gyr_fix_index*0.00005f; + data[2] = g_cali.gyr_fix[2] + g_cali.gyr_fix_index*0.00005f; + } + + if(g_cali.gyr_cali_flag) + { + g_cali.gyr[0] = data[0] + g_cali.gyr_bias[0]; + g_cali.gyr[1] = data[1] + g_cali.gyr_bias[1]; + g_cali.gyr[2] = data[2] + g_cali.gyr_bias[2]; + data[0] = g_cali.gyr[0]; + data[1] = g_cali.gyr[1]; + data[2] = g_cali.gyr[2]; + } + else + { + g_cali.gyr[0] = data[0]; + g_cali.gyr[1] = data[1]; + g_cali.gyr[2] = data[2]; + } + } +} + +#endif + +void qmi8658_config_acc(enum qmi8658_AccRange range, enum qmi8658_AccOdr odr, enum qmi8658_LpfConfig lpfEnable, enum qmi8658_StConfig stEnable) +{ + unsigned char ctl_dada; + + switch(range) + { + case Qmi8658AccRange_2g: + g_imu.ssvt_a = (1<<14); + break; + case Qmi8658AccRange_4g: + g_imu.ssvt_a = (1<<13); + break; + case Qmi8658AccRange_8g: + g_imu.ssvt_a = (1<<12); + break; + case Qmi8658AccRange_16g: + g_imu.ssvt_a = (1<<11); + break; + default: + range = Qmi8658AccRange_8g; + g_imu.ssvt_a = (1<<12); + } + if(stEnable == Qmi8658St_Enable) + ctl_dada = (unsigned char)range|(unsigned char)odr|0x80; + else + ctl_dada = (unsigned char)range|(unsigned char)odr; + + qmi8658_write_reg(Qmi8658Register_Ctrl2, ctl_dada); +// set LPF & HPF + qmi8658_read_reg(Qmi8658Register_Ctrl5, &ctl_dada, 1); + ctl_dada &= 0xf0; + if(lpfEnable == Qmi8658Lpf_Enable) + { + ctl_dada |= A_LSP_MODE_3; + ctl_dada |= 0x01; + } + else + { + ctl_dada &= ~0x01; + } + //ctl_dada = 0x00; + qmi8658_write_reg(Qmi8658Register_Ctrl5,ctl_dada); +// set LPF & HPF +} + +void qmi8658_config_gyro(enum qmi8658_GyrRange range, enum qmi8658_GyrOdr odr, enum qmi8658_LpfConfig lpfEnable, enum qmi8658_StConfig stEnable) +{ + // Set the CTRL3 register to configure dynamic range and ODR + unsigned char ctl_dada; + + // Store the scale factor for use when processing raw data + switch (range) + { + case Qmi8658GyrRange_16dps: + g_imu.ssvt_g = 2048; + break; + case Qmi8658GyrRange_32dps: + g_imu.ssvt_g = 1024; + break; + case Qmi8658GyrRange_64dps: + g_imu.ssvt_g = 512; + break; + case Qmi8658GyrRange_128dps: + g_imu.ssvt_g = 256; + break; + case Qmi8658GyrRange_256dps: + g_imu.ssvt_g = 128; + break; + case Qmi8658GyrRange_512dps: + g_imu.ssvt_g = 64; + break; + case Qmi8658GyrRange_1024dps: + g_imu.ssvt_g = 32; + break; + case Qmi8658GyrRange_2048dps: + g_imu.ssvt_g = 16; + break; +// case Qmi8658GyrRange_4096dps: +// g_imu.ssvt_g = 8; +// break; + default: + range = Qmi8658GyrRange_512dps; + g_imu.ssvt_g = 64; + break; + } + + if(stEnable == Qmi8658St_Enable) + ctl_dada = (unsigned char)range|(unsigned char)odr|0x80; + else + ctl_dada = (unsigned char)range | (unsigned char)odr; + qmi8658_write_reg(Qmi8658Register_Ctrl3, ctl_dada); + +// Conversion from degrees/s to rad/s if necessary +// set LPF & HPF + qmi8658_read_reg(Qmi8658Register_Ctrl5, &ctl_dada,1); + ctl_dada &= 0x0f; + if(lpfEnable == Qmi8658Lpf_Enable) + { + ctl_dada |= G_LSP_MODE_3; + ctl_dada |= 0x10; + } + else + { + ctl_dada &= ~0x10; + } + //ctl_dada = 0x00; + qmi8658_write_reg(Qmi8658Register_Ctrl5,ctl_dada); +// set LPF & HPF +} + + +void qmi8658_send_ctl9cmd(enum qmi8658_Ctrl9Command cmd) +{ + unsigned char status1 = 0x00; + unsigned short count=0; + + qmi8658_write_reg(Qmi8658Register_Ctrl9, (unsigned char)cmd); // write commond to ctrl9 +#if 1 //defined(QMI8658_NEW_FIRMWARE) + unsigned char status_reg = Qmi8658Register_StatusInt; + unsigned char cmd_done = 0x80; + //unsigned char status_reg = Qmi8658Register_Status1; + //unsigned char cmd_done = 0x01; + + qmi8658_read_reg(status_reg, &status1, 1); + while(((status1&cmd_done)!=cmd_done)&&(count++<100)) // read statusINT until bit7 is 1 + { + qmi8658_delay(1); + qmi8658_read_reg(status_reg, &status1, 1); + } + //qmi8658_log("ctrl9 cmd done1 count=%d\n",count); + + qmi8658_write_reg(Qmi8658Register_Ctrl9, qmi8658_Ctrl9_Cmd_NOP); // write commond 0x00 to ctrl9 + count = 0; + qmi8658_read_reg(status_reg, &status1, 1); + while(((status1&cmd_done)==cmd_done)&&(count++<100)) // read statusINT until bit7 is 0 + { + qmi8658_delay(1); // 1 ms + qmi8658_read_reg(status_reg, &status1, 1); + } + //qmi8658_log("ctrl9 cmd done2 count=%d\n",count); +#else + while(((status1&QMI8658_STATUS1_CMD_DONE)==0)&&(count++<100)) + { + qmi8658_delay(1); + qmi8658_read_reg(Qmi8658Register_Status1, &status1, sizeof(status1)); + } +#endif + +} + +unsigned char qmi8658_readStatusInt(void) +{ + unsigned char status_int; + + qmi8658_read_reg(Qmi8658Register_StatusInt, &status_int, 1); + + return status_int; +} + +unsigned char qmi8658_readStatus0(void) +{ + unsigned char status0; + + qmi8658_read_reg(Qmi8658Register_Status0, &status0, 1); + + return status0; +} + +unsigned char qmi8658_readStatus1(void) +{ + unsigned char status1; + + qmi8658_read_reg(Qmi8658Register_Status1, &status1, 1); + + return status1; +} + +float qmi8658_readTemp(void) +{ + unsigned char buf[2]; + short temp = 0; + float temp_f = 0; + + qmi8658_read_reg(Qmi8658Register_Tempearture_L, buf, 2); + temp = ((short)buf[1]<<8)|buf[0]; + temp_f = (float)temp/256.0f; + + return temp_f; +} + +void qmi8658_read_timestamp(unsigned int *tim_count) +{ + unsigned char buf[3]; + unsigned int timestamp; + + if(tim_count) + { + qmi8658_read_reg(Qmi8658Register_Timestamp_L, buf, 3); + timestamp = (unsigned int)(((unsigned int)buf[2]<<16)|((unsigned int)buf[1]<<8)|buf[0]); + if(timestamp > g_imu.timestamp) + g_imu.timestamp = timestamp; + else + g_imu.timestamp = (timestamp+0x1000000-g_imu.timestamp); + + *tim_count = g_imu.timestamp; + } +} + +void qmi8658_read_sensor_data(float acc[3], float gyro[3]) +{ + unsigned char buf_reg[12]; + short raw_acc_xyz[3]; + short raw_gyro_xyz[3]; + + qmi8658_read_reg(Qmi8658Register_Ax_L, buf_reg, 12); + raw_acc_xyz[0] = (short)((unsigned short)(buf_reg[1]<<8) |( buf_reg[0])); + raw_acc_xyz[1] = (short)((unsigned short)(buf_reg[3]<<8) |( buf_reg[2])); + raw_acc_xyz[2] = (short)((unsigned short)(buf_reg[5]<<8) |( buf_reg[4])); + + raw_gyro_xyz[0] = (short)((unsigned short)(buf_reg[7]<<8) |( buf_reg[6])); + raw_gyro_xyz[1] = (short)((unsigned short)(buf_reg[9]<<8) |( buf_reg[8])); + raw_gyro_xyz[2] = (short)((unsigned short)(buf_reg[11]<<8) |( buf_reg[10])); + +#if defined(QMI8658_UINT_MG_DPS) + // mg + acc[0] = (float)(raw_acc_xyz[0]*1000.0f)/g_imu.ssvt_a; + acc[1] = (float)(raw_acc_xyz[1]*1000.0f)/g_imu.ssvt_a; + acc[2] = (float)(raw_acc_xyz[2]*1000.0f)/g_imu.ssvt_a; +#else + // m/s2 + acc[0] = (float)(raw_acc_xyz[0]*ONE_G)/g_imu.ssvt_a; + acc[1] = (float)(raw_acc_xyz[1]*ONE_G)/g_imu.ssvt_a; + acc[2] = (float)(raw_acc_xyz[2]*ONE_G)/g_imu.ssvt_a; +#endif + +#if defined(QMI8658_UINT_MG_DPS) + // dps + gyro[0] = (float)(raw_gyro_xyz[0]*1.0f)/g_imu.ssvt_g; + gyro[1] = (float)(raw_gyro_xyz[1]*1.0f)/g_imu.ssvt_g; + gyro[2] = (float)(raw_gyro_xyz[2]*1.0f)/g_imu.ssvt_g; +#else + // rad/s + gyro[0] = (float)(raw_gyro_xyz[0]*M_PI)/(g_imu.ssvt_g*180); // *pi/180 + gyro[1] = (float)(raw_gyro_xyz[1]*M_PI)/(g_imu.ssvt_g*180); + gyro[2] = (float)(raw_gyro_xyz[2]*M_PI)/(g_imu.ssvt_g*180); +#endif +} + +void qmi8658_read_xyz(float acc[3], float gyro[3]) +{ + unsigned char status; + unsigned char data_ready = 0; + +#if defined(QMI8658_SYNC_SAMPLE_MODE) + qmi8658_read_reg(Qmi8658Register_StatusInt, &status, 1); + if(status&0x01) + { + data_ready = 1; + qmi8658_delay_us(6); // delay 6us + } +#else + qmi8658_read_reg(Qmi8658Register_Status0, &status, 1); + if(status&0x03) + { + data_ready = 1; + } +#endif + if(data_ready) + { + qmi8658_read_sensor_data(acc, gyro); + qmi8658_axis_convert(acc, gyro, 0); +#if defined(QMI8658_USE_CALI) + qmi8658_data_cali(1, acc); + qmi8658_data_cali(2, gyro); +#endif + g_imu.imu[0] = acc[0]; + g_imu.imu[1] = acc[1]; + g_imu.imu[2] = acc[2]; + g_imu.imu[3] = gyro[0]; + g_imu.imu[4] = gyro[1]; + g_imu.imu[5] = gyro[2]; + } + else + { + acc[0] = g_imu.imu[0]; + acc[1] = g_imu.imu[1]; + acc[2] = g_imu.imu[2]; + gyro[0] = g_imu.imu[3]; + gyro[1] = g_imu.imu[4]; + gyro[2] = g_imu.imu[5]; + qmi8658_log("data ready fail!\n"); + } +} + + +void qmi8658_enableSensors(unsigned char enableFlags) +{ +#if defined(QMI8658_SYNC_SAMPLE_MODE) + qmi8658_write_reg(Qmi8658Register_Ctrl7, enableFlags | 0x80); +#elif defined(QMI8658_USE_FIFO) + //qmi8658_write_reg(Qmi8658Register_Ctrl7, enableFlags|QMI8658_DRDY_DISABLE); + qmi8658_write_reg(Qmi8658Register_Ctrl7, enableFlags); +#else + qmi8658_write_reg(Qmi8658Register_Ctrl7, enableFlags); +#endif + g_imu.cfg.enSensors = enableFlags&0x03; + + qmi8658_delay(1); +} + +void qmi8658_dump_reg(void) +{ + unsigned char read_data[8]; + + qmi8658_read_reg(Qmi8658Register_Ctrl1, read_data, 8); + qmi8658_log("Ctrl1[0x%x]\nCtrl2[0x%x]\nCtrl3[0x%x]\nCtrl4[0x%x]\nCtrl5[0x%x]\nCtrl6[0x%x]\nCtrl7[0x%x]\nCtrl8[0x%x]\n", + read_data[0],read_data[1],read_data[2],read_data[3],read_data[4],read_data[5],read_data[6],read_data[7]); +} + +//void qmi8658_soft_reset(void) +//{ +// qmi8658_log("qmi8658_soft_reset \n"); +// qmi8658_write_reg(Qmi8658Register_Reset, 0xb0); +// qmi8658_delay(2000); +// qmi8658_write_reg(Qmi8658Register_Reset, 0x00); +// qmi8658_delay(5); +//} + +void qmi8658_on_demand_cali(void) +{ + qmi8658_log("qmi8658_on_demand_cali start\n"); + qmi8658_write_reg(Qmi8658Register_Reset, 0xb0); + qmi8658_delay(10); // delay + qmi8658_write_reg(Qmi8658Register_Ctrl9, (unsigned char)qmi8658_Ctrl9_Cmd_On_Demand_Cali); + qmi8658_delay(2200); // delay 2000ms above + qmi8658_write_reg(Qmi8658Register_Ctrl9, (unsigned char)qmi8658_Ctrl9_Cmd_NOP); + qmi8658_delay(100); // delay + qmi8658_log("qmi8658_on_demand_cali done\n"); +} + +void qmi8658_config_reg(unsigned char low_power) +{ + qmi8658_enableSensors(QMI8658_DISABLE_ALL); + if(low_power) + { + g_imu.cfg.enSensors = QMI8658_ACC_ENABLE; + g_imu.cfg.accRange = Qmi8658AccRange_8g; + g_imu.cfg.accOdr = Qmi8658AccOdr_LowPower_21Hz; + g_imu.cfg.gyrRange = Qmi8658GyrRange_1024dps; + g_imu.cfg.gyrOdr = Qmi8658GyrOdr_250Hz; + } + else + { + g_imu.cfg.enSensors = QMI8658_ACCGYR_ENABLE; + g_imu.cfg.accRange = Qmi8658AccRange_8g; + g_imu.cfg.accOdr = Qmi8658AccOdr_250Hz; + g_imu.cfg.gyrRange = Qmi8658GyrRange_1024dps; + g_imu.cfg.gyrOdr = Qmi8658GyrOdr_250Hz; + } + + if(g_imu.cfg.enSensors & QMI8658_ACC_ENABLE) + { + qmi8658_config_acc(g_imu.cfg.accRange, g_imu.cfg.accOdr, Qmi8658Lpf_Disable, Qmi8658St_Disable); + } + if(g_imu.cfg.enSensors & QMI8658_GYR_ENABLE) + { + qmi8658_config_gyro(g_imu.cfg.gyrRange, g_imu.cfg.gyrOdr, Qmi8658Lpf_Disable, Qmi8658St_Disable); + } +} + + +unsigned char qmi8658_get_id(void) +{ + unsigned char qmi8658_chip_id = 0x00; + unsigned char qmi8658_revision_id = 0x00; + unsigned char qmi8658_slave[2] = {QMI8658_SLAVE_ADDR_L, QMI8658_SLAVE_ADDR_H}; + int retry = 0; + unsigned char iCount = 0; + unsigned char firmware_id[3]; + unsigned char uuid[6]; + unsigned int uuid_low, uuid_high; + + while(iCount<2) + { + g_imu.slave = qmi8658_slave[iCount]; + retry = 0; + while((qmi8658_chip_id != 0x05)&&(retry++ < 5)) + { + qmi8658_read_reg(Qmi8658Register_WhoAmI, &qmi8658_chip_id, 1); + qmi8658_log("Qmi8658Register_WhoAmI = 0x%x\n", qmi8658_chip_id); + } + if(qmi8658_chip_id == 0x05) + { + qmi8658_on_demand_cali(); + + g_imu.cfg.ctrl8_value = 0xc0; + //QMI8658_INT1_ENABLE, QMI8658_INT2_ENABLE + qmi8658_write_reg(Qmi8658Register_Ctrl1, 0x60|QMI8658_INT2_ENABLE|QMI8658_INT1_ENABLE); + qmi8658_read_reg(Qmi8658Register_Revision, &qmi8658_revision_id, 1); + qmi8658_read_reg(Qmi8658Register_firmware_id, firmware_id, 3); + qmi8658_read_reg(Qmi8658Register_uuid, uuid, 6); + qmi8658_write_reg(Qmi8658Register_Ctrl7, 0x00); + qmi8658_write_reg(Qmi8658Register_Ctrl8, g_imu.cfg.ctrl8_value); + uuid_low = (unsigned int)((unsigned int)(uuid[2]<<16)|(unsigned int)(uuid[1]<<8)|(uuid[0])); + uuid_high = (unsigned int)((unsigned int)(uuid[5]<<16)|(unsigned int)(uuid[4]<<8)|(uuid[3])); + qmi8658_log("qmi8658_init slave=0x%x Revision=0x%x\n", g_imu.slave, qmi8658_revision_id); + qmi8658_log("Firmware ID[0x%x 0x%x 0x%x]\n", firmware_id[2], firmware_id[1],firmware_id[0]); + qmi8658_log("UUID[0x%x %x]\n", uuid_high ,uuid_low); + + break; + } + iCount++; + } + + return qmi8658_chip_id; +} + +#if defined(QMI8658_USE_AMD) +void qmi8658_config_amd(void) +{ + g_imu.cfg.ctrl8_value &= (~QMI8658_CTRL8_ANYMOTION_EN); + qmi8658_write_reg(Qmi8658Register_Ctrl8, g_imu.cfg.ctrl8_value); + + qmi8658_write_reg(Qmi8658Register_Cal1_L, 0x03); // any motion X threshold U 3.5 first three bit(uint 1g) last five bit (uint 1/32 g) + qmi8658_write_reg(Qmi8658Register_Cal1_H, 0x03); // any motion Y threshold U 3.5 first three bit(uint 1g) last five bit (uint 1/32 g) + qmi8658_write_reg(Qmi8658Register_Cal2_L, 0x03); // any motion Z threshold U 3.5 first three bit(uint 1g) last five bit (uint 1/32 g) + qmi8658_write_reg(Qmi8658Register_Cal2_H, 0x02); // no motion X threshold U 3.5 first three bit(uint 1g) last five bit (uint 1/32 g) + qmi8658_write_reg(Qmi8658Register_Cal3_L, 0x02); + qmi8658_write_reg(Qmi8658Register_Cal3_H, 0x02); + + qmi8658_write_reg(Qmi8658Register_Cal4_L, 0xf7); // MOTION_MODE_CTRL + qmi8658_write_reg(Qmi8658Register_Cal4_H, 0x01); // value 0x01 + + qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_Motion); + + qmi8658_write_reg(Qmi8658Register_Cal1_L, 0x03); // AnyMotionWindow. + qmi8658_write_reg(Qmi8658Register_Cal1_H, 0x01); // NoMotionWindow + qmi8658_write_reg(Qmi8658Register_Cal2_L, 0x2c); // SigMotionWaitWindow[7:0] + qmi8658_write_reg(Qmi8658Register_Cal2_H, 0x01); // SigMotionWaitWindow [15:8] + qmi8658_write_reg(Qmi8658Register_Cal3_L, 0x64); // SigMotionConfirmWindow[7:0] + qmi8658_write_reg(Qmi8658Register_Cal3_H, 0x00); // SigMotionConfirmWindow[15:8] + //qmi8658_write_reg(Qmi8658Register_Cal4_L, 0xf7); + qmi8658_write_reg(Qmi8658Register_Cal4_H, 0x02); // value 0x02 + + qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_Motion); +} + +void qmi8658_enable_amd(unsigned char enable, enum qmi8658_Interrupt int_map, unsigned char low_power) +{ + if(int_map == qmi8658_Int1) + { + g_imu.cfg.ctrl8_value &= (~QMI8658_CTRL8_ANYMOTION_EN); + g_imu.cfg.ctrl8_value |= QMI8658_CTRL8_DATAVALID_EN; + } + else if(int_map == qmi8658_Int2) + { + g_imu.cfg.ctrl8_value &= (~QMI8658_CTRL8_ANYMOTION_EN); + g_imu.cfg.ctrl8_value &= (~QMI8658_CTRL8_DATAVALID_EN); + } + qmi8658_write_reg(Qmi8658Register_Ctrl8, g_imu.cfg.ctrl8_value); + qmi8658_delay(2); + + if(enable) + { + unsigned char ctrl1; + + qmi8658_enableSensors(QMI8658_DISABLE_ALL); + qmi8658_config_reg(low_power); + + qmi8658_read_reg(Qmi8658Register_Ctrl1, &ctrl1, 1); + if(int_map == qmi8658_Int1) + { + ctrl1 |= QMI8658_INT1_ENABLE; + qmi8658_write_reg(Qmi8658Register_Ctrl1, ctrl1);// enable int for dev-E + } + else if(int_map == qmi8658_Int2) + { + ctrl1 |= QMI8658_INT2_ENABLE; + qmi8658_write_reg(Qmi8658Register_Ctrl1, ctrl1);// enable int for dev-E + } + g_imu.cfg.ctrl8_value |= QMI8658_CTRL8_ANYMOTION_EN; + qmi8658_write_reg(Qmi8658Register_Ctrl8, g_imu.cfg.ctrl8_value); + + qmi8658_delay(1); + qmi8658_enableSensors(g_imu.cfg.enSensors); + } + else + { + + } +} +#endif + +#if defined(QMI8658_USE_PEDOMETER) +void qmi8658_config_pedometer(unsigned short odr) +{ + float finalRate = (float)(200.0f/odr); //14.285 + unsigned short ped_sample_cnt = (unsigned short)(0x0032 / finalRate);//6;//(unsigned short)(0x0032 / finalRate) ; + unsigned short ped_fix_peak2peak = 0x00AC;//0x0006;//0x00CC; + unsigned short ped_fix_peak = 0x00AC;//0x0006;//0x00CC; + unsigned short ped_time_up = (unsigned short)(200 / finalRate); + unsigned char ped_time_low = (unsigned char) (20 / finalRate); + unsigned char ped_time_cnt_entry = 8; + unsigned char ped_fix_precision = 0; + unsigned char ped_sig_count = 1;//Ʋ1 + + qmi8658_write_reg(Qmi8658Register_Cal1_L, ped_sample_cnt & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal1_H, (ped_sample_cnt >> 8) & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal2_L, ped_fix_peak2peak & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal2_H, (ped_fix_peak2peak >> 8) & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal3_L, ped_fix_peak & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal3_H, (ped_fix_peak >> 8) & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal4_H, 0x01); + qmi8658_write_reg(Qmi8658Register_Cal4_L, 0x02); + qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_EnablePedometer); + + qmi8658_write_reg(Qmi8658Register_Cal1_L, ped_time_up & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal1_H, (ped_time_up >> 8) & 0xFF); + qmi8658_write_reg(Qmi8658Register_Cal2_L, ped_time_low); + qmi8658_write_reg(Qmi8658Register_Cal2_H, ped_time_cnt_entry); + qmi8658_write_reg(Qmi8658Register_Cal3_L, ped_fix_precision); + qmi8658_write_reg(Qmi8658Register_Cal3_H, ped_sig_count); + qmi8658_write_reg(Qmi8658Register_Cal4_H, 0x02); + qmi8658_write_reg(Qmi8658Register_Cal4_L, 0x02); + qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_EnablePedometer); +} + +void qmi8658_enable_pedometer(unsigned char enable) +{ + if(enable) + { + g_imu.cfg.ctrl8_value |= QMI8658_CTRL8_PEDOMETER_EN; + } + else + { + g_imu.cfg.ctrl8_value &= (~QMI8658_CTRL8_PEDOMETER_EN); + } + qmi8658_write_reg(Qmi8658Register_Ctrl8, g_imu.cfg.ctrl8_value); +} + +unsigned int qmi8658_read_pedometer(void) +{ + unsigned char buf[3]; + + qmi8658_read_reg(Qmi8658Register_Pedo_L, buf, 3); // 0x5a + g_imu.step = (unsigned int)((buf[2]<<16)|(buf[1]<<8)|(buf[0])); + + return g_imu.step; +} +#endif + +#if defined(QMI8658_USE_FIFO) +void qmi8658_config_fifo(unsigned char watermark,enum qmi8658_FifoSize size,enum qmi8658_FifoMode mode,enum qmi8658_Interrupt int_map) +{ + unsigned char ctrl1; + + qmi8658_enableSensors(QMI8658_DISABLE_ALL); + qmi8658_read_reg(Qmi8658Register_Ctrl1, &ctrl1, 1); + if(int_map == qmi8658_Int1) + { + ctrl1 |= QMI8658_FIFO_MAP_INT1; + } + else if(int_map == qmi8658_Int2) + { + ctrl1 &= QMI8658_FIFO_MAP_INT2; + } + qmi8658_write_reg(Qmi8658Register_Ctrl1, ctrl1); + + g_imu.cfg.fifo_ctrl = (unsigned char)(size | mode); + qmi8658_write_reg(Qmi8658Register_FifoCtrl, g_imu.cfg.fifo_ctrl); + qmi8658_write_reg(Qmi8658Register_FifoWmkTh, watermark); + + qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_Rst_Fifo); + qmi8658_enableSensors(QMI8658_ACCGYR_ENABLE); +} + +unsigned short qmi8658_read_fifo(unsigned char* data) +{ + unsigned char fifo_status[2] = {0,0}; + unsigned char fifo_sensors = 1; + unsigned short fifo_bytes = 0; + unsigned short fifo_level = 0; + + if((g_imu.cfg.fifo_ctrl&0x03)!=qmi8658_Fifo_Bypass) + { + //qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_Req_Fifo); + + qmi8658_read_reg(Qmi8658Register_FifoCount, fifo_status, 2); + fifo_bytes = (unsigned short)(((fifo_status[1]&0x03)<<8)|fifo_status[0]); + if((g_imu.cfg.enSensors == QMI8658_ACC_ENABLE)||(g_imu.cfg.enSensors == QMI8658_GYR_ENABLE)) + { + fifo_sensors = 1; + } + else if(g_imu.cfg.enSensors == QMI8658_ACCGYR_ENABLE) + { + fifo_sensors = 2; + } + fifo_level = fifo_bytes/(3*fifo_sensors); + fifo_bytes = fifo_level*(6*fifo_sensors); + //qmi8658_log("fifo-level : %d\n", fifo_level); + if(fifo_level > 0) + { + qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_Req_Fifo); +#if 1 + for(int i=0; i 15) + { + qmi8658_log("qmi8658_do_selftest-fail\n"); + return 0; + } + else + { + qmi8658_log("qmi8658_do_selftest-ok\n"); + return 1; + } +} +#endif + +unsigned char qmi8658_init(void) +{ + if(qmi8658_get_id() == 0x05) + { +#if defined(QMI8658_USE_AMD) + qmi8658_config_amd(); +#endif +#if defined(QMI8658_USE_PEDOMETER) + qmi8658_config_pedometer(125); + qmi8658_enable_pedometer(1); +#endif + qmi8658_config_reg(0); + qmi8658_enableSensors(g_imu.cfg.enSensors); + qmi8658_dump_reg(); +#if defined(QMI8658_USE_CALI) + memset(&g_cali, 0, sizeof(g_cali)); +#endif + return 1; + } + else + { + qmi8658_log("qmi8658_init fail\n"); + return 0; + } +} + + diff --git a/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658.h b/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658.h new file mode 100644 index 0000000..8f31a56 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658.h @@ -0,0 +1,369 @@ +#ifndef QMI8658_H +#define QMI8658_H + +#include +#include + +//#define QMI8658_USE_SPI +//#define QMI8658_SYNC_SAMPLE_MODE +//#define QMI8658_SOFT_SELFTEST +//#define QMI8658_USE_CALI + +#define QMI8658_USE_FIFO +//#define QMI8658_USE_AMD +//#define QMI8658_USE_PEDOMETER + +#define QMI8658_SLAVE_ADDR_L 0x6a +#define QMI8658_SLAVE_ADDR_H 0x6b + +#define QMI8658_DISABLE_ALL (0x0) +#define QMI8658_ACC_ENABLE (0x1) +#define QMI8658_GYR_ENABLE (0x2) +#define QMI8658_ACCGYR_ENABLE (QMI8658_ACC_ENABLE | QMI8658_GYR_ENABLE) + +#define QMI8658_STATUS1_CMD_DONE (0x01) +#define QMI8658_STATUS1_WAKEUP_EVENT (0x04) + +#define QMI8658_CTRL8_DATAVALID_EN 0x40 // bit6:1 int1, 0 int2 +#define QMI8658_CTRL8_PEDOMETER_EN 0x10 +#define QMI8658_CTRL8_SIGMOTION_EN 0x08 +#define QMI8658_CTRL8_NOMOTION_EN 0x04 +#define QMI8658_CTRL8_ANYMOTION_EN 0x02 +#define QMI8658_CTRL8_TAP_EN 0x01 + +#define QMI8658_INT1_ENABLE 0x08 +#define QMI8658_INT2_ENABLE 0x10 + +#define QMI8658_DRDY_DISABLE 0x20 // ctrl7 + +#define QMI8658_FIFO_MAP_INT1 0x04 // ctrl1 +#define QMI8658_FIFO_MAP_INT2 ~0x04 // ctrl1 + +#define qmi8658_log printf + +enum Qmi8658Register +{ + Qmi8658Register_WhoAmI = 0, + Qmi8658Register_Revision, + Qmi8658Register_Ctrl1, + Qmi8658Register_Ctrl2, + Qmi8658Register_Ctrl3, + Qmi8658Register_Ctrl4, + Qmi8658Register_Ctrl5, + Qmi8658Register_Ctrl6, + Qmi8658Register_Ctrl7, + Qmi8658Register_Ctrl8, + Qmi8658Register_Ctrl9, + Qmi8658Register_Cal1_L = 11, + Qmi8658Register_Cal1_H, + Qmi8658Register_Cal2_L, + Qmi8658Register_Cal2_H, + Qmi8658Register_Cal3_L, + Qmi8658Register_Cal3_H, + Qmi8658Register_Cal4_L, + Qmi8658Register_Cal4_H, + Qmi8658Register_FifoWmkTh = 19, + Qmi8658Register_FifoCtrl = 20, + Qmi8658Register_FifoCount = 21, + Qmi8658Register_FifoStatus = 22, + Qmi8658Register_FifoData = 23, + Qmi8658Register_StatusI2CM = 44, + Qmi8658Register_StatusInt = 45, + Qmi8658Register_Status0, + Qmi8658Register_Status1, + Qmi8658Register_Timestamp_L = 48, + Qmi8658Register_Timestamp_M, + Qmi8658Register_Timestamp_H, + Qmi8658Register_Tempearture_L = 51, + Qmi8658Register_Tempearture_H, + Qmi8658Register_Ax_L = 53, + Qmi8658Register_Ax_H, + Qmi8658Register_Ay_L, + Qmi8658Register_Ay_H, + Qmi8658Register_Az_L, + Qmi8658Register_Az_H, + Qmi8658Register_Gx_L = 59, + Qmi8658Register_Gx_H, + Qmi8658Register_Gy_L, + Qmi8658Register_Gy_H, + Qmi8658Register_Gz_L, + Qmi8658Register_Gz_H, + Qmi8658Register_Mx_L = 65, + Qmi8658Register_Mx_H, + Qmi8658Register_My_L, + Qmi8658Register_My_H, + Qmi8658Register_Mz_L, + Qmi8658Register_Mz_H, + Qmi8658Register_firmware_id = 73, + Qmi8658Register_uuid = 81, + + Qmi8658Register_Pedo_L = 90, + Qmi8658Register_Pedo_M = 91, + Qmi8658Register_Pedo_H = 92, + + Qmi8658Register_Reset = 96 +}; + +enum qmi8658_Ois_Register +{ + qmi8658_OIS_Reg_Ctrl1 = 0x02, + qmi8658_OIS_Reg_Ctrl2, + qmi8658_OIS_Reg_Ctrl3, + qmi8658_OIS_Reg_Ctrl5 = 0x06, + qmi8658_OIS_Reg_Ctrl7 = 0x08, + qmi8658_OIS_Reg_StatusInt = 0x2D, + qmi8658_OIS_Reg_Status0 = 0x2E, + qmi8658_OIS_Reg_Ax_L = 0x33, + qmi8658_OIS_Reg_Ax_H, + qmi8658_OIS_Reg_Ay_L, + qmi8658_OIS_Reg_Ay_H, + qmi8658_OIS_Reg_Az_L, + qmi8658_OIS_Reg_Az_H, + + qmi8658_OIS_Reg_Gx_L = 0x3B, + qmi8658_OIS_Reg_Gx_H, + qmi8658_OIS_Reg_Gy_L, + qmi8658_OIS_Reg_Gy_H, + qmi8658_OIS_Reg_Gz_L, + qmi8658_OIS_Reg_Gz_H, +}; + +enum qmi8658_Ctrl9Command +{ + qmi8658_Ctrl9_Cmd_NOP = 0X00, + qmi8658_Ctrl9_Cmd_GyroBias = 0X01, + qmi8658_Ctrl9_Cmd_Rqst_Sdi_Mod = 0X03, + qmi8658_Ctrl9_Cmd_Rst_Fifo = 0X04, + qmi8658_Ctrl9_Cmd_Req_Fifo = 0X05, + qmi8658_Ctrl9_Cmd_I2CM_Write = 0X06, + qmi8658_Ctrl9_Cmd_WoM_Setting = 0x08, + qmi8658_Ctrl9_Cmd_AccelHostDeltaOffset = 0x09, + qmi8658_Ctrl9_Cmd_GyroHostDeltaOffset = 0x0A, + qmi8658_Ctrl9_Cmd_EnableExtReset = 0x0B, + qmi8658_Ctrl9_Cmd_EnableTap = 0x0C, + qmi8658_Ctrl9_Cmd_EnablePedometer = 0x0D, + qmi8658_Ctrl9_Cmd_Motion = 0x0E, + qmi8658_Ctrl9_Cmd_CopyUsid = 0x10, + qmi8658_Ctrl9_Cmd_SetRpu = 0x11, + qmi8658_Ctrl9_Cmd_On_Demand_Cali = 0xA2, + qmi8658_Ctrl9_Cmd_Dbg_WoM_Data_Enable = 0xF8 +}; + + +enum qmi8658_LpfConfig +{ + Qmi8658Lpf_Disable, + Qmi8658Lpf_Enable +}; + +enum qmi8658_HpfConfig +{ + Qmi8658Hpf_Disable, + Qmi8658Hpf_Enable +}; + +enum qmi8658_StConfig +{ + Qmi8658St_Disable, + Qmi8658St_Enable +}; + +enum qmi8658_LpfMode +{ + A_LSP_MODE_0 = 0x00<<1, + A_LSP_MODE_1 = 0x01<<1, + A_LSP_MODE_2 = 0x02<<1, + A_LSP_MODE_3 = 0x03<<1, + + G_LSP_MODE_0 = 0x00<<5, + G_LSP_MODE_1 = 0x01<<5, + G_LSP_MODE_2 = 0x02<<5, + G_LSP_MODE_3 = 0x03<<5 +}; + +enum qmi8658_AccRange +{ + Qmi8658AccRange_2g = 0x00 << 4, + Qmi8658AccRange_4g = 0x01 << 4, + Qmi8658AccRange_8g = 0x02 << 4, + Qmi8658AccRange_16g = 0x03 << 4 +}; + + +enum qmi8658_AccOdr +{ + Qmi8658AccOdr_8000Hz = 0x00, + Qmi8658AccOdr_4000Hz = 0x01, + Qmi8658AccOdr_2000Hz = 0x02, + Qmi8658AccOdr_1000Hz = 0x03, + Qmi8658AccOdr_500Hz = 0x04, + Qmi8658AccOdr_250Hz = 0x05, + Qmi8658AccOdr_125Hz = 0x06, + Qmi8658AccOdr_62_5Hz = 0x07, + Qmi8658AccOdr_31_25Hz = 0x08, + Qmi8658AccOdr_LowPower_128Hz = 0x0c, + Qmi8658AccOdr_LowPower_21Hz = 0x0d, + Qmi8658AccOdr_LowPower_11Hz = 0x0e, + Qmi8658AccOdr_LowPower_3Hz = 0x0f +}; + +enum qmi8658_GyrRange +{ + Qmi8658GyrRange_16dps = 0 << 4, + Qmi8658GyrRange_32dps = 1 << 4, + Qmi8658GyrRange_64dps = 2 << 4, + Qmi8658GyrRange_128dps = 3 << 4, + Qmi8658GyrRange_256dps = 4 << 4, + Qmi8658GyrRange_512dps = 5 << 4, + Qmi8658GyrRange_1024dps = 6 << 4, + Qmi8658GyrRange_2048dps = 7 << 4 +}; + +/*! + * \brief Gyroscope output rate configuration. + */ +enum qmi8658_GyrOdr +{ + Qmi8658GyrOdr_8000Hz = 0x00, + Qmi8658GyrOdr_4000Hz = 0x01, + Qmi8658GyrOdr_2000Hz = 0x02, + Qmi8658GyrOdr_1000Hz = 0x03, + Qmi8658GyrOdr_500Hz = 0x04, + Qmi8658GyrOdr_250Hz = 0x05, + Qmi8658GyrOdr_125Hz = 0x06, + Qmi8658GyrOdr_62_5Hz = 0x07, + Qmi8658GyrOdr_31_25Hz = 0x08 +}; + +enum qmi8658_AccUnit +{ + Qmi8658AccUnit_g, + Qmi8658AccUnit_ms2 +}; + +enum qmi8658_GyrUnit +{ + Qmi8658GyrUnit_dps, + Qmi8658GyrUnit_rads +}; + +enum qmi8658_FifoMode +{ + qmi8658_Fifo_Bypass = 0, + qmi8658_Fifo_Fifo = 1, + qmi8658_Fifo_Stream = 2, + qmi8658_Fifo_StreamToFifo = 3 +}; + + +enum qmi8658_FifoWmkLevel +{ + qmi8658_Fifo_WmkEmpty = (0 << 4), + qmi8658_Fifo_WmkOneQuarter = (1 << 4), + qmi8658_Fifo_WmkHalf = (2 << 4), + qmi8658_Fifo_WmkThreeQuarters = (3 << 4) +}; + +enum qmi8658_FifoSize +{ + qmi8658_Fifo_16 = (0 << 2), + qmi8658_Fifo_32 = (1 << 2), + qmi8658_Fifo_64 = (2 << 2), + qmi8658_Fifo_128 = (3 << 2) +}; + +enum qmi8658_Interrupt +{ + qmi8658_Int_none, + qmi8658_Int1, + qmi8658_Int2, + + qmi8658_Int_total +}; + +enum qmi8658_InterruptState +{ + Qmi8658State_high = (1 << 7), + Qmi8658State_low = (0 << 7) +}; + +#define QMI8658_CALI_DATA_NUM 200 + +typedef struct qmi8658_cali +{ + float acc_last[3]; + float acc[3]; + float acc_fix[3]; + float acc_bias[3]; + float acc_sum[3]; + + float gyr_last[3]; + float gyr[3]; + float gyr_fix[3]; + float gyr_bias[3]; + float gyr_sum[3]; + + unsigned char imu_static_flag; + unsigned char acc_fix_flag; + unsigned char gyr_fix_flag; + char acc_fix_index; + unsigned char gyr_fix_index; + + unsigned char acc_cali_flag; + unsigned char gyr_cali_flag; + unsigned short acc_cali_num; + unsigned short gyr_cali_num; +// unsigned char acc_avg_num; +// unsigned char gyr_avg_num; +} qmi8658_cali; + +typedef struct +{ + unsigned char enSensors; + enum qmi8658_AccRange accRange; + enum qmi8658_AccOdr accOdr; + enum qmi8658_GyrRange gyrRange; + enum qmi8658_GyrOdr gyrOdr; + unsigned char ctrl8_value; +#if defined(QMI8658_USE_FIFO) + unsigned char fifo_ctrl; +#endif +} qmi8658_config; + +typedef struct +{ + unsigned char slave; + qmi8658_config cfg; + unsigned short ssvt_a; + unsigned short ssvt_g; + unsigned int timestamp; + unsigned int step; + float imu[6]; +} qmi8658_state; + +extern unsigned char qmi8658_write_reg(unsigned char reg, unsigned char value); +extern unsigned char qmi8658_read_reg(unsigned char reg, unsigned char* buf, unsigned short len); +extern unsigned char qmi8658_init(void); +extern void qmi8658_config_reg(unsigned char low_power); +extern void qmi8658_enableSensors(unsigned char enableFlags); +extern unsigned char qmi8658_readStatusInt(void); +extern unsigned char qmi8658_readStatus0(void); +extern unsigned char qmi8658_readStatus1(void); +extern float qmi8658_readTemp(void); +extern void qmi8658_read_timestamp(unsigned int *tim_count); +extern void qmi8658_read_xyz(float acc[3], float gyro[3]); +extern void qmi8658_read_sensor_data(float acc[3], float gyro[3]); +#if defined(QMI8658_USE_PEDOMETER) +extern unsigned int qmi8658_read_pedometer(void); +#endif +#if defined(QMI8658_USE_AMD) +void qmi8658_config_amd(void); +void qmi8658_enable_amd(unsigned char enable, enum qmi8658_Interrupt int_map, unsigned char low_power); +#endif +#if defined(QMI8658_USE_FIFO) +extern void qmi8658_config_fifo(unsigned char watermark,enum qmi8658_FifoSize size,enum qmi8658_FifoMode mode,enum qmi8658_Interrupt int_map); +extern unsigned short qmi8658_read_fifo(unsigned char* data); +#endif +extern void qmi8658_send_ctl9cmd(enum qmi8658_Ctrl9Command cmd); + +#endif diff --git a/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658A.h b/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658A.h new file mode 100644 index 0000000..2547621 --- /dev/null +++ b/main/boards/movecall-moji-esp32s3/qmi8658-master/qmi8658A.h @@ -0,0 +1,99 @@ +#ifndef QMI8658A_H +#define QMI8658A_H + + +#include +#include + +enum Qmi8658AReg +{ + Register_WhoAmI = 0, + Register_Revision, + Register_Ctrl1, + Register_Ctrl2, + Register_Ctrl3, + Register_Reserved, + Register_Ctrl5, + Register_Reserved1, + Register_Ctrl7, + Register_Ctrl8, + Register_Ctrl9, + Register_Cal1_L = 11, + Register_Cal1_H, + Register_Cal2_L, + Register_Cal2_H, + Register_Cal3_L, + Register_Cal3_H, + Register_Cal4_L, + Register_Cal4_H, + Register_FifoWmkTh = 19, + Register_FifoCtrl = 20, + Register_FifoCount = 21, + Register_FifoStatus = 22, + Register_FifoData = 23, + Register_StatusInt = 45, + Register_Status0, + Register_Status1, + Register_Timestamp_L = 48, + Register_Timestamp_M, + Register_Timestamp_H, + Register_Tempearture_L = 51, + Register_Tempearture_H, + Register_Ax_L = 53, + Register_Ax_H, + Register_Ay_L, + Register_Ay_H, + Register_Az_L, + Register_Az_H, + Register_Gx_L = 59, + Register_Gx_H, + Register_Gy_L, + Register_Gy_H, + Register_Gz_L, + Register_Gz_H, + Register_COD_Status = 70, + Register_dQW_L = 73, + Register_dQW_H, + Register_dQX_L, + Register_dQX_H, + Register_dQY_L, + Register_dQY_H, + Register_dQZ_L, + Register_dQZ_H, + Register_dVX_L, + Register_dVX_H, + Register_dVY_L, + Register_dVY_H, + Register_dVZ_L, + Register_dVZ_H, + + Register_TAP_Status = 89, + Register_Step_Cnt_L = 90, + Register_Step_Cnt_M = 91, + Register_Step_Cnt_H = 92, + + Register_Reset = 96 +}; + +//详细说明参照note.md Ctrl9详细命令说明 +enum Ctrl9Command +{ + Ctrl9_Cmd_Ack = 0X00, + Ctrl9_Cmd_RstFifo = 0X04, + Ctrl9_Cmd_ReqFifo = 0X05,//Get FIFO data from Device + Ctrl9_Cmd_WoM_Setting = 0x08,// 设置并启用运动唤醒 + Ctrl9_Cmd_AccelHostDeltaOffset = 0x09,//更改加速度计偏移 + Ctrl9_Cmd_GyroHostDeltaOffset = 0x0A,//更改陀螺仪偏移 + Ctrl9_Cmd_CfgTap = 0x0C, //配置TAP检测 + Ctrl9_Cmd_CfgPedometer = 0x0D,//配置计步器 + Ctrl9_Cmd_Motion = 0x0E,//配置任何运动/无运动/显着运动检测 + Ctrl9_Cmd_RstPedometer = 0x0F,//重置计步器计数(步数) + Ctrl9_Cmd_CopyUsid = 0x10,//将 USID 和 FW 版本复制到 UI 寄存器 + Ctrl9_Cmd_SetRpu = 0x11,//配置 IO 上拉 + Ctrl9_Cmd_AHBClockGating = 0x12,//内部 AHB 时钟门控开关 + Ctrl9_Cmd_OnDemandCalivration = 0xA2,//陀螺仪按需校准 + Ctrl9_Cmd_ApplyGyroGains = 0xAA//恢复保存的陀螺仪增益 +}; + + +#endif \ No newline at end of file diff --git a/main/boards/sensecap-watcher/README.md b/main/boards/sensecap-watcher/README.md new file mode 100644 index 0000000..a96d8c6 --- /dev/null +++ b/main/boards/sensecap-watcher/README.md @@ -0,0 +1,34 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> SenseCAP Watcher +``` + +**编译烧入:** + +```bash +idf.py build flash +``` +注意: 请特别小心处理闪存固件分区地址,以避免错误擦除 SenseCAP Watcher 的自身设备信息(EUI 等),否则设备可能无法正确连接到 SenseCraft 服务器!在刷写固件之前,请务必记录设备的相关必要信息,以确保有恢复的方法! + +您可以使用以下命令备份生产信息 + +```bash +# firstly backup the factory information partition which contains the credentials for connecting the SenseCraft server +esptool.py --chip esp32s3 --baud 2000000 --before default_reset --after hard_reset --no-stub read_flash 0x9000 204800 nvsfactory.bin + +``` \ No newline at end of file diff --git a/main/boards/sensecap-watcher/config.h b/main/boards/sensecap-watcher/config.h new file mode 100644 index 0000000..7375af0 --- /dev/null +++ b/main/boards/sensecap-watcher/config.h @@ -0,0 +1,101 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include "esp_io_expander.h" + +/* General I2C */ +#define BSP_GENERAL_I2C_NUM (I2C_NUM_0) +#define BSP_GENERAL_I2C_SDA (GPIO_NUM_47) +#define BSP_GENERAL_I2C_SCL (GPIO_NUM_48) + +/* Audio */ +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE false + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_11 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_15 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_16 + + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7243E_ADDR (0x14) + + + +#define BUILTIN_LED_GPIO GPIO_NUM_40 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +/* Expander */ +#define BSP_IO_EXPANDER_INT (GPIO_NUM_2) +#define DRV_IO_EXP_INPUT_MASK (0x20ff) // P0.0 ~ P0.7 | P1.3 +#define DRV_IO_EXP_OUTPUT_MASK (0xDf00) // P1.0 ~ P1.7 & ~P1.3 + +/* Expander IO PIN */ +#define BSP_PWR_CHRG_DET (IO_EXPANDER_PIN_NUM_0) +#define BSP_PWR_STDBY_DET (IO_EXPANDER_PIN_NUM_1) +#define BSP_PWR_VBUS_IN_DET (IO_EXPANDER_PIN_NUM_2) +#define BSP_PWR_SDCARD (IO_EXPANDER_PIN_NUM_8) +#define BSP_PWR_LCD (IO_EXPANDER_PIN_NUM_9) +#define BSP_PWR_SYSTEM (IO_EXPANDER_PIN_NUM_10) +#define BSP_PWR_AI_CHIP (IO_EXPANDER_PIN_NUM_11) +#define BSP_PWR_CODEC_PA (IO_EXPANDER_PIN_NUM_12) +#define BSP_PWR_BAT_DET (IO_EXPANDER_PIN_NUM_13) +#define BSP_PWR_GROVE (IO_EXPANDER_PIN_NUM_14) +#define BSP_PWR_BAT_ADC (IO_EXPANDER_PIN_NUM_15) + +#define BSP_PWR_START_UP (BSP_PWR_SDCARD | BSP_PWR_LCD | BSP_PWR_SYSTEM | BSP_PWR_AI_CHIP | BSP_PWR_CODEC_PA | BSP_PWR_GROVE | BSP_PWR_BAT_ADC) + +#define BSP_KNOB_BTN (IO_EXPANDER_PIN_NUM_3) +#define BSP_KNOB_A_PIN GPIO_NUM_41 +#define BSP_KNOB_B_PIN GPIO_NUM_42 + +/* QSPI */ +#define BSP_SPI3_HOST_PCLK (GPIO_NUM_7) +#define BSP_SPI3_HOST_DATA0 (GPIO_NUM_9) +#define BSP_SPI3_HOST_DATA1 (GPIO_NUM_1) +#define BSP_SPI3_HOST_DATA2 (GPIO_NUM_14) +#define BSP_SPI3_HOST_DATA3 (GPIO_NUM_13) + +/* LCD */ +#define BSP_LCD_SPI_NUM (SPI3_HOST) +#define BSP_LCD_SPI_CS (GPIO_NUM_45) +#define BSP_LCD_GPIO_RST (GPIO_NUM_NC) +#define BSP_LCD_GPIO_DC (GPIO_NUM_1) + +#define DISPLAY_WIDTH 412 +#define DISPLAY_HEIGHT 412 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_8 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +/* Touch */ +#define BSP_TOUCH_I2C_NUM (1) +#define BSP_TOUCH_GPIO_INT (IO_EXPANDER_PIN_NUM_5) +#define BSP_TOUCH_I2C_SDA (GPIO_NUM_39) +#define BSP_TOUCH_I2C_SCL (GPIO_NUM_38) +#define BSP_TOUCH_I2C_CLK (400000) + +/* Settings */ +#define DRV_LCD_PIXEL_CLK_HZ (40 * 1000 * 1000) +#define DRV_LCD_CMD_BITS (32) +#define DRV_LCD_PARAM_BITS (8) +#define DRV_LCD_RGB_ELEMENT_ORDER (LCD_RGB_ELEMENT_ORDER_RGB) +#define DRV_LCD_BITS_PER_PIXEL (16) + +#define CONFIG_BSP_LCD_SPI_DMA_SIZE_DIV 16 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/sensecap-watcher/config.json b/main/boards/sensecap-watcher/config.json new file mode 100644 index 0000000..31bc672 --- /dev/null +++ b/main/boards/sensecap-watcher/config.json @@ -0,0 +1,15 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "sensecap-watcher", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_32M_sensecap.csv\"", + "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH=y", + "CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=n", + "CONFIG_IDF_EXPERIMENTAL_FEATURES=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/sensecap-watcher/sensecap_audio_codec.cc b/main/boards/sensecap-watcher/sensecap_audio_codec.cc new file mode 100644 index 0000000..c2ade56 --- /dev/null +++ b/main/boards/sensecap-watcher/sensecap_audio_codec.cc @@ -0,0 +1,214 @@ +#include "sensecap_audio_codec.h" + +#include +#include +#include + +static const char TAG[] = "SensecapAudioCodec"; + +SensecapAudioCodec::SensecapAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7243e_addr, bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + // Do initialize of related interface: data_if, ctrl_if and gpio_if + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Output + audio_codec_i2c_cfg_t i2c_cfg = { + .port = (i2c_port_t)0, + .addr = es8311_addr, + .bus_handle = i2c_master_handle, + }; + out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(out_ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8311_codec_cfg_t es8311_cfg = {}; + es8311_cfg.ctrl_if = out_ctrl_if_; + es8311_cfg.gpio_if = gpio_if_; + es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC; + es8311_cfg.pa_pin = pa_pin; + es8311_cfg.use_mclk = true; + es8311_cfg.hw_gain.pa_voltage = 5.0; + es8311_cfg.hw_gain.codec_dac_voltage = 3.3; + out_codec_if_ = es8311_codec_new(&es8311_cfg); + assert(out_codec_if_ != NULL); + + esp_codec_dev_cfg_t dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = out_codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&dev_cfg); + assert(output_dev_ != NULL); + + // Input + i2c_cfg.addr = es7243e_addr << 1; + in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(in_ctrl_if_ != NULL); + + es7243e_codec_cfg_t es7243e_cfg = {}; + es7243e_cfg.ctrl_if = in_ctrl_if_; + in_codec_if_ = es7243e_codec_new(&es7243e_cfg); + assert(in_codec_if_ != NULL); + + dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; + dev_cfg.codec_if = in_codec_if_; + input_dev_ = esp_codec_dev_new(&dev_cfg); + assert(input_dev_ != NULL); + + esp_codec_set_disable_when_closed(output_dev_, false); + esp_codec_set_disable_when_closed(input_dev_, false); + + ESP_LOGI(TAG, "SensecapAudioDevice initialized"); +} + +SensecapAudioCodec::~SensecapAudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void SensecapAudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_MONO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + + std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void SensecapAudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void SensecapAudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 2, + .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1), + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 27.0)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void SensecapAudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + // Play 16bit 1 channel + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 1); + } + } + else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 0); + } + } + AudioCodec::EnableOutput(enable); +} + +int SensecapAudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int SensecapAudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} \ No newline at end of file diff --git a/main/boards/sensecap-watcher/sensecap_audio_codec.h b/main/boards/sensecap-watcher/sensecap_audio_codec.h new file mode 100644 index 0000000..794a4d7 --- /dev/null +++ b/main/boards/sensecap-watcher/sensecap_audio_codec.h @@ -0,0 +1,38 @@ +#ifndef _SENSECAP_AUDIO_CODEC_H +#define _SENSECAP_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include + +class SensecapAudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; + const audio_codec_if_t* out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; + const audio_codec_if_t* in_codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + gpio_num_t pa_pin_ = GPIO_NUM_NC; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +public: + SensecapAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, + gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, + gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference); + virtual ~SensecapAudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _SENSECAP_AUDIO_CODEC_H diff --git a/main/boards/sensecap-watcher/sensecap_watcher.cc b/main/boards/sensecap-watcher/sensecap_watcher.cc new file mode 100644 index 0000000..a520d8a --- /dev/null +++ b/main/boards/sensecap-watcher/sensecap_watcher.cc @@ -0,0 +1,358 @@ +#include "display/lv_display.h" +#include "misc/lv_event.h" +#include "wifi_board.h" +#include "sensecap_audio_codec.h" +#include "display/lcd_display.h" +#include "font_awesome_symbols.h" +#include "application.h" +#include "button.h" +#include "knob.h" +#include "config.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "power_save_timer.h" + +#include +#include "esp_check.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "sensecap_watcher" + + +LV_FONT_DECLARE(font_puhui_30_4); +LV_FONT_DECLARE(font_awesome_30_4); + +class SensecapWatcher : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + LcdDisplay* display_; + std::unique_ptr knob_; + esp_io_expander_handle_t io_exp_handle; + button_handle_t btns; + PowerSaveTimer* power_save_timer_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + GetBacklight()->SetBrightness(10); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + GetBacklight()->RestoreBrightness(); + }); + power_save_timer_->OnShutdownRequest([this]() { + ESP_LOGI(TAG, "Shutting down"); + bool is_charging = (IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0); + if (is_charging) { + ESP_LOGI(TAG, "charging"); + GetBacklight()->SetBrightness(0); + } else { + IoExpanderSetLevel(BSP_PWR_SYSTEM, 0); + } + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = BSP_GENERAL_I2C_SDA, + .scl_io_num = BSP_GENERAL_I2C_SCL, + .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, &i2c_bus_)); + + // pulldown for lcd i2c + const gpio_config_t io_config = { + .pin_bit_mask = (1ULL << BSP_TOUCH_I2C_SDA) | (1ULL << BSP_TOUCH_I2C_SCL) | (1ULL << BSP_SPI3_HOST_PCLK) | (1ULL << BSP_SPI3_HOST_DATA0) | (1ULL << BSP_SPI3_HOST_DATA1) + | (1ULL << BSP_SPI3_HOST_DATA2) | (1ULL << BSP_SPI3_HOST_DATA3) | (1ULL << BSP_LCD_SPI_CS) | (1UL << DISPLAY_BACKLIGHT_PIN), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&io_config); + + gpio_set_level(BSP_TOUCH_I2C_SDA, 0); + gpio_set_level(BSP_TOUCH_I2C_SCL, 0); + + gpio_set_level(BSP_LCD_SPI_CS, 0); + gpio_set_level(DISPLAY_BACKLIGHT_PIN, 0); + gpio_set_level(BSP_SPI3_HOST_PCLK, 0); + gpio_set_level(BSP_SPI3_HOST_DATA0, 0); + gpio_set_level(BSP_SPI3_HOST_DATA1, 0); + gpio_set_level(BSP_SPI3_HOST_DATA2, 0); + gpio_set_level(BSP_SPI3_HOST_DATA3, 0); + + } + + esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) { + return esp_io_expander_set_level(io_exp_handle, pin_mask, level); + } + + uint8_t IoExpanderGetLevel(uint16_t pin_mask) { + uint32_t pin_val = 0; + esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); + pin_mask &= DRV_IO_EXP_INPUT_MASK; + return (uint8_t)((pin_val & pin_mask) ? 1 : 0); + } + + void InitializeExpander() { + esp_err_t ret = ESP_OK; + esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_001, &io_exp_handle); + + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT); + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT); + ret |= esp_io_expander_set_level(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, 0); + ret |= esp_io_expander_set_level(io_exp_handle, BSP_PWR_SYSTEM, 1); + vTaskDelay(100 / portTICK_PERIOD_MS); + ret |= esp_io_expander_set_level(io_exp_handle, BSP_PWR_START_UP, 1); + vTaskDelay(50 / portTICK_PERIOD_MS); + + uint32_t pin_val = 0; + ret |= esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); + ESP_LOGI(TAG, "IO expander initialized: %x", DRV_IO_EXP_OUTPUT_MASK | (uint16_t)pin_val); + + assert(ret == ESP_OK); + } + + void OnKnobRotate(bool clockwise) { + auto codec = GetAudioCodec(); + int current_volume = codec->output_volume(); + int new_volume = current_volume + (clockwise ? 5 : -5); + + // 确保音量在有效范围内 + if (new_volume > 100) { + new_volume = 100; + ESP_LOGW(TAG, "Volume reached maximum limit: %d", new_volume); + } else if (new_volume < 0) { + new_volume = 0; + ESP_LOGW(TAG, "Volume reached minimum limit: %d", new_volume); + } + + codec->SetOutputVolume(new_volume); + ESP_LOGI(TAG, "Volume changed from %d to %d", current_volume, new_volume); + + // 显示通知前检查实际变化 + if (new_volume != codec->output_volume()) { + ESP_LOGE(TAG, "Failed to set volume! Expected:%d Actual:%d", + new_volume, codec->output_volume()); + } + GetDisplay()->ShowNotification("音量: " + std::to_string(codec->output_volume())); + power_save_timer_->WakeUp(); + } + + void InitializeKnob() { + knob_ = std::make_unique(BSP_KNOB_A_PIN, BSP_KNOB_B_PIN); + knob_->OnRotate([this](bool clockwise) { + ESP_LOGD(TAG, "Knob rotation detected. Clockwise:%s", clockwise ? "true" : "false"); + OnKnobRotate(clockwise); + }); + ESP_LOGI(TAG, "Knob initialized with pins A:%d B:%d", BSP_KNOB_A_PIN, BSP_KNOB_B_PIN); + } + + void InitializeButton() { + button_config_t btn_config = { + .type = BUTTON_TYPE_CUSTOM, + .long_press_time = 2000, + .short_press_time = 50, + .custom_button_config = { + .active_level = 0, + .button_custom_init =nullptr, + .button_custom_get_key_value = [](void *param) -> uint8_t { + auto self = static_cast(param); + return self->IoExpanderGetLevel(BSP_KNOB_BTN); + }, + .button_custom_deinit = nullptr, + .priv = this, + }, + }; + + //watcher 是通过长按滚轮进行开机的, 需要等待滚轮释放, 否则用户开机松手时可能会误触成单击 + ESP_LOGI(TAG, "waiting for knob button release"); + while(IoExpanderGetLevel(BSP_KNOB_BTN) == 0) { + vTaskDelay(50 / portTICK_PERIOD_MS); + } + + btns = iot_button_create(&btn_config); + iot_button_register_cb(btns, BUTTON_SINGLE_CLICK, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + self->ResetWifiConfiguration(); + } + self->power_save_timer_->WakeUp(); + // 使用新的打断按键功能:按一次进入listening,再按一次进入idle + app.ToggleListeningState(); + }, this); + iot_button_register_cb(btns, BUTTON_LONG_PRESS_START, [](void* button_handle, void* usr_data) { + auto self = static_cast(usr_data); + bool is_charging = (self->IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0); + if (is_charging) { + ESP_LOGI(TAG, "charging"); + } else { + self->IoExpanderSetLevel(BSP_PWR_LCD, 0); + self->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0); + } + }, this); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize QSPI bus"); + + spi_bus_config_t qspi_cfg = {0}; + qspi_cfg.sclk_io_num = BSP_SPI3_HOST_PCLK; + qspi_cfg.data0_io_num = BSP_SPI3_HOST_DATA0; + qspi_cfg.data1_io_num = BSP_SPI3_HOST_DATA1; + qspi_cfg.data2_io_num = BSP_SPI3_HOST_DATA2; + qspi_cfg.data3_io_num = BSP_SPI3_HOST_DATA3; + qspi_cfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * DRV_LCD_BITS_PER_PIXEL / 8 / CONFIG_BSP_LCD_SPI_DMA_SIZE_DIV; + + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &qspi_cfg, SPI_DMA_CH_AUTO)); + } + + void Initializespd2010Display() { + ESP_LOGI(TAG, "Install panel IO"); + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = BSP_LCD_SPI_CS, + .dc_gpio_num = -1, + .spi_mode = 3, + .pclk_hz = DRV_LCD_PIXEL_CLK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = DRV_LCD_CMD_BITS, + .lcd_param_bits = DRV_LCD_PARAM_BITS, + .flags = { + .quad_mode = true, + }, + }; + spd2010_vendor_config_t vendor_config = { + .flags = { + .use_qspi_interface = 1, + }, + }; + esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, &panel_io_); + + ESP_LOGD(TAG, "Install LCD driver"); + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = BSP_LCD_GPIO_RST, // Shared with Touch reset + .rgb_ele_order = DRV_LCD_RGB_ELEMENT_ORDER, + .bits_per_pixel = DRV_LCD_BITS_PER_PIXEL, + .vendor_config = &vendor_config, + }; + esp_lcd_new_panel_spd2010(panel_io_, &panel_config, &panel_); + + esp_lcd_panel_reset(panel_); + esp_lcd_panel_init(panel_); + esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + esp_lcd_panel_disp_on_off(panel_, true); + + 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_30_4, + .icon_font = &font_awesome_30_4, + .emoji_font = font_emoji_64_init(), + }); + + // 使每次刷新的起始列数索引是4的倍数且列数总数是4的倍数,以满足SPD2010的要求 + lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t *e) { + lv_area_t *area = (lv_area_t *)lv_event_get_param(e); + uint16_t x1 = area->x1; + uint16_t x2 = area->x2; + // round the start of area down to the nearest 4N number + area->x1 = (x1 >> 2) << 2; + // round the end of area up to the nearest 4M+3 number + area->x2 = ((x2 >> 2) << 2) + 3; + }, LV_EVENT_INVALIDATE_AREA, NULL); + + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + +public: + SensecapWatcher(){ + ESP_LOGI(TAG, "Initialize Sensecap Watcher"); + InitializePowerSaveTimer(); + InitializeI2c(); + InitializeSpi(); + InitializeExpander(); + InitializeButton(); + InitializeKnob(); + Initializespd2010Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static SensecapAudioCodec audio_codec( + i2c_bus_, + 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, + AUDIO_CODEC_ES7243E_ADDR, + AUDIO_INPUT_REFERENCE); + 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; + } + + // 根据 https://github.com/Seeed-Studio/OSHW-SenseCAP-Watcher/blob/main/Hardware/SenseCAP_Watcher_v1.0_SCH.pdf + // RGB LED型号为 ws2813 mini, 连接在GPIO 40,供电电压 3.3v, 没有连接 BIN 双信号线 + // 可以直接兼容SingleLED采用的ws2812 + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(SensecapWatcher); diff --git a/main/boards/taiji-pi-s3/README.md b/main/boards/taiji-pi-s3/README.md new file mode 100644 index 0000000..d4be2a1 --- /dev/null +++ b/main/boards/taiji-pi-s3/README.md @@ -0,0 +1,25 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> 太极小派esp32s3 +``` + +**编译:** + +```bash +idf.py build +``` diff --git a/main/boards/taiji-pi-s3/config.h b/main/boards/taiji-pi-s3/config.h new file mode 100644 index 0000000..6b9f54a --- /dev/null +++ b/main/boards/taiji-pi-s3/config.h @@ -0,0 +1,66 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// Taiji Pi S3 Board configuration + +#include +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +#define AUDIO_DEFAULT_OUTPUT_VOLUME 80 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_21 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_16 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_18 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_NC +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_17 +#define AUDIO_MUTE_PIN GPIO_NUM_48 // 低电平静音 + +#define AUDIO_MIC_WS_PIN GPIO_NUM_45 +#define AUDIO_MIC_SD_PIN GPIO_NUM_46 +#define AUDIO_MIC_SCK_PIN GPIO_NUM_42 + +#define DISPLAY_WIDTH 360 +#define DISPLAY_HEIGHT 360 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define QSPI_LCD_H_RES (360) +#define QSPI_LCD_V_RES (360) +#define QSPI_LCD_BIT_PER_PIXEL (16) + +#define QSPI_LCD_HOST SPI2_HOST +#define QSPI_PIN_NUM_LCD_PCLK GPIO_NUM_9 +#define QSPI_PIN_NUM_LCD_CS GPIO_NUM_10 +#define QSPI_PIN_NUM_LCD_DATA0 GPIO_NUM_11 +#define QSPI_PIN_NUM_LCD_DATA1 GPIO_NUM_12 +#define QSPI_PIN_NUM_LCD_DATA2 GPIO_NUM_13 +#define QSPI_PIN_NUM_LCD_DATA3 GPIO_NUM_14 +#define QSPI_PIN_NUM_LCD_RST GPIO_NUM_47 +#define QSPI_PIN_NUM_LCD_BL GPIO_NUM_15 + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define TP_PORT (I2C_NUM_1) +#define TP_PIN_NUM_TP_SDA (GPIO_NUM_7) +#define TP_PIN_NUM_TP_SCL (GPIO_NUM_8) +#define TP_PIN_NUM_TP_RST (GPIO_NUM_40) +#define TP_PIN_NUM_TP_INT (GPIO_NUM_41) + +#define DISPLAY_BACKLIGHT_PIN QSPI_PIN_NUM_LCD_BL +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(sclk, d0, d1, d2, d3, max_trans_sz) \ + { \ + .data0_io_num = d0, \ + .data1_io_num = d1, \ + .sclk_io_num = sclk, \ + .data2_io_num = d2, \ + .data3_io_num = d3, \ + .max_transfer_sz = max_trans_sz, \ + } + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/taiji-pi-s3/config.json b/main/boards/taiji-pi-s3/config.json new file mode 100644 index 0000000..d66def5 --- /dev/null +++ b/main/boards/taiji-pi-s3/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "taiji-pi-s3", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/taiji-pi-s3/taiji_pi_s3.cc b/main/boards/taiji-pi-s3/taiji_pi_s3.cc new file mode 100644 index 0000000..ca4c2d1 --- /dev/null +++ b/main/boards/taiji-pi-s3/taiji_pi_s3.cc @@ -0,0 +1,249 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "application.h" +#include "i2c_device.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "TaijiPiS3Board" + + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class Cst816s : public I2cDevice { +public: + struct TouchPoint_t { + int num = 0; + int x = -1; + int y = -1; + }; + Cst816s(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + uint8_t chip_id = ReadReg(0xA3); + ESP_LOGI(TAG, "Get chip ID: 0x%02X", chip_id); + read_buffer_ = new uint8_t[6]; + } + + ~Cst816s() { + delete[] read_buffer_; + } + + void UpdateTouchPoint() { + ReadRegs(0x02, read_buffer_, 6); + tp_.num = read_buffer_[0] & 0x0F; + tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2]; + tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4]; + } + + const TouchPoint_t& GetTouchPoint() { + return tp_; + } + +private: + uint8_t* read_buffer_ = nullptr; + TouchPoint_t tp_; +}; + +class TaijiPiS3Board : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Cst816s* cst816s_; + LcdDisplay* display_; + esp_timer_handle_t touchpad_timer_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)1, + .sda_io_num = TP_PIN_NUM_TP_SDA, + .scl_io_num = TP_PIN_NUM_TP_SCL, + .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, &i2c_bus_)); + } + + static void touchpad_timer_callback(void* arg) { + auto& board = (TaijiPiS3Board&)Board::GetInstance(); + auto touchpad = board.GetTouchpad(); + static bool was_touched = false; + static int64_t touch_start_time = 0; + const int64_t TOUCH_THRESHOLD_MS = 500; // 触摸时长阈值,超过500ms视为长按 + + touchpad->UpdateTouchPoint(); + auto touch_point = touchpad->GetTouchPoint(); + + // 检测触摸开始 + if (touch_point.num > 0 && !was_touched) { + was_touched = true; + touch_start_time = esp_timer_get_time() / 1000; // 转换为毫秒 + } + // 检测触摸释放 + else if (touch_point.num == 0 && was_touched) { + was_touched = false; + int64_t touch_duration = (esp_timer_get_time() / 1000) - touch_start_time; + + // 只有短触才触发 + if (touch_duration < TOUCH_THRESHOLD_MS) { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && + !WifiStation::GetInstance().IsConnected()) { + board.ResetWifiConfiguration(); + } + app.ToggleChatState(); + } + } + } + + void InitializeCst816sTouchPad() { + ESP_LOGI(TAG, "Init Cst816s"); + cst816s_ = new Cst816s(i2c_bus_, 0x15); + + // 创建定时器,10ms 间隔 + esp_timer_create_args_t timer_args = { + .callback = touchpad_timer_callback, + .arg = NULL, + .dispatch_method = ESP_TIMER_TASK, + .name = "touchpad_timer", + .skip_unhandled_events = true, + }; + + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &touchpad_timer_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(touchpad_timer_, 10 * 1000)); // 10ms = 10000us + } + + void BspLcdBlSet(int brightness_percent) + { + if (brightness_percent > 100) { + brightness_percent = 100; + } + if (brightness_percent < 0) { + brightness_percent = 0; + } + + ESP_LOGI(TAG, "Setting LCD backlight: %d%%", brightness_percent); + uint32_t duty_cycle = (1023 * brightness_percent) / 100; // LEDC resolution set to 10bits, thus: 100% = 1023 + ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty_cycle); + ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize QSPI bus"); + + const spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK, + QSPI_PIN_NUM_LCD_DATA0, + QSPI_PIN_NUM_LCD_DATA1, + QSPI_PIN_NUM_LCD_DATA2, + QSPI_PIN_NUM_LCD_DATA3, + QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); + ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO)); + } + + void Initializest77916Display() { + + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + ESP_LOGI(TAG, "Install panel IO"); + + const esp_lcd_panel_io_spi_config_t io_config = ST77916_PANEL_IO_QSPI_CONFIG(QSPI_PIN_NUM_LCD_CS, NULL, NULL); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)QSPI_LCD_HOST, &io_config, &panel_io)); + + ESP_LOGI(TAG, "Install ST77916 panel driver"); + + st77916_vendor_config_t vendor_config = { + .flags = { + .use_qspi_interface = 1, + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = QSPI_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // Implemented by LCD command `36h` + .bits_per_pixel = QSPI_LCD_BIT_PER_PIXEL, // Implemented by LCD command `3Ah` (16/18) + .vendor_config = &vendor_config, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st77916(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + esp_lcd_panel_disp_on_off(panel, true); + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Screen")); + } + void InitializeMute() { + gpio_reset_pin(AUDIO_MUTE_PIN); + /* Set the GPIO as a push/pull output */ + gpio_set_direction(AUDIO_MUTE_PIN, GPIO_MODE_OUTPUT); + gpio_set_level(AUDIO_MUTE_PIN, 1); + } + +public: + TaijiPiS3Board() { + InitializeI2c(); + InitializeCst816sTouchPad(); + InitializeSpi(); + Initializest77916Display(); + InitializeIot(); + InitializeMute(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec( + AUDIO_INPUT_SAMPLE_RATE, + AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, + AUDIO_I2S_GPIO_WS, + AUDIO_I2S_GPIO_DOUT, + AUDIO_MIC_SCK_PIN, + AUDIO_MIC_WS_PIN, + AUDIO_MIC_SD_PIN + ); + 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; + } + + Cst816s* GetTouchpad() { + return cst816s_; + } +}; + +DECLARE_BOARD(TaijiPiS3Board); \ No newline at end of file diff --git a/main/boards/tudouzi/config.h b/main/boards/tudouzi/config.h new file mode 100644 index 0000000..a272900 --- /dev/null +++ b/main/boards/tudouzi/config.h @@ -0,0 +1,41 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE true + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_40 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_47 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_38 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_48 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_9 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_42 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_41 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_3 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_1 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_2 + +#define DISPLAY_SDA_PIN GPIO_NUM_7 +#define DISPLAY_SCL_PIN GPIO_NUM_8 +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false + +#define ML307_RX_PIN GPIO_NUM_5 +#define ML307_TX_PIN GPIO_NUM_6 + +#define AXP2101_I2C_ADDR 0x34 + + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/tudouzi/config.json b/main/boards/tudouzi/config.json new file mode 100644 index 0000000..d9eee68 --- /dev/null +++ b/main/boards/tudouzi/config.json @@ -0,0 +1,13 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "tudouzi", + "sdkconfig_append": [ + "CONFIG_USE_WAKE_WORD_DETECT=n", + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/tudouzi/kevin_box_board.cc b/main/boards/tudouzi/kevin_box_board.cc new file mode 100644 index 0000000..e2a9287 --- /dev/null +++ b/main/boards/tudouzi/kevin_box_board.cc @@ -0,0 +1,286 @@ +#include "ml307_board.h" +#include "audio_codecs/box_audio_codec.h" +#include "display/oled_display.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "config.h" +#include "power_save_timer.h" +#include "axp2101.h" +#include "assets/lang_config.h" +#include "font_awesome_symbols.h" + +#include +#include +#include +#include +#include + +#define TAG "KevinBoxBoard" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + + +class Pmic : public Axp2101 { +public: + Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) { + // ** EFUSE defaults ** + WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable + WriteReg(0x27, 0x10); // hold 4s to power off + + WriteReg(0x93, 0x1C); // 配置 aldo2 输出为 3.3V + + uint8_t value = ReadReg(0x90); // XPOWERS_AXP2101_LDO_ONOFF_CTRL0 + value = value | 0x02; // set bit 1 (ALDO2) + WriteReg(0x90, value); // and power channels now enabled + + WriteReg(0x64, 0x03); // CV charger voltage setting to 4.2V + + WriteReg(0x61, 0x05); // set Main battery precharge current to 125mA + WriteReg(0x62, 0x0A); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA ) + WriteReg(0x63, 0x15); // set Main battery term charge current to 125mA + + WriteReg(0x14, 0x00); // set minimum system voltage to 4.1V (default 4.7V), for poor USB cables + WriteReg(0x15, 0x00); // set input voltage limit to 3.88v, for poor USB cables + WriteReg(0x16, 0x05); // set input current limit to 2000mA + + WriteReg(0x24, 0x01); // set Vsys for PWROFF threshold to 3.2V (default - 2.6V and kill battery) + WriteReg(0x50, 0x14); // set TS pin to EXTERNAL input (not temperature) + } +}; + + +class KevinBoxBoard : public Ml307Board { +private: + i2c_master_bus_handle_t display_i2c_bus_; + i2c_master_bus_handle_t codec_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Pmic* pmic_ = nullptr; + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(240, 60, -1); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + if (!modem_.Command("AT+MLPMCFG=\"sleepmode\",2,0")) { + ESP_LOGE(TAG, "Failed to enable module sleep mode"); + } + + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + + auto codec = GetAudioCodec(); + codec->EnableInput(false); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto codec = GetAudioCodec(); + codec->EnableInput(true); + + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + }); + power_save_timer_->SetEnabled(true); + } + + void Enable4GModule() { + // Make GPIO HIGH to enable the 4G module + gpio_config_t ml307_enable_config = { + .pin_bit_mask = (1ULL << 4), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&ml307_enable_config); + gpio_set_level(GPIO_NUM_4, 1); + } + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeCodecI2c() { + // 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 InitializeButtons() { + boot_button_.OnPressDown([this]() { + power_save_timer_->WakeUp(); + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + volume_up_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Battery")); + } + +public: + KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeCodecI2c(); + pmic_ = new Pmic(codec_i2c_bus_, AXP2101_I2C_ADDR); + + Enable4GModule(); + + InitializeButtons(); + InitializePowerSaveTimer(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static BoxAudioCodec audio_codec(codec_i2c_bus_, 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, AUDIO_CODEC_ES7210_ADDR, AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override { + static bool last_discharging = false; + charging = pmic_->IsCharging(); + discharging = pmic_->IsDischarging(); + if (discharging != last_discharging) { + power_save_timer_->SetEnabled(discharging); + last_discharging = discharging; + } + + level = pmic_->GetBatteryLevel(); + return true; + } +}; + +DECLARE_BOARD(KevinBoxBoard); \ No newline at end of file diff --git a/main/boards/xingzhi-cube-0.85tft-ml307/config.h b/main/boards/xingzhi-cube-0.85tft-ml307/config.h new file mode 100644 index 0000000..7388d39 --- /dev/null +++ b/main/boards/xingzhi-cube-0.85tft-ml307/config.h @@ -0,0 +1,40 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA GPIO_NUM_10 +#define DISPLAY_SCL GPIO_NUM_9 +#define DISPLAY_DC GPIO_NUM_8 +#define DISPLAY_CS GPIO_NUM_14 +#define DISPLAY_RES GPIO_NUM_18 +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define BACKLIGHT_INVERT false +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_13 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define ML307_RX_PIN GPIO_NUM_11 +#define ML307_TX_PIN GPIO_NUM_12 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xingzhi-cube-0.85tft-ml307/config.json b/main/boards/xingzhi-cube-0.85tft-ml307/config.json new file mode 100644 index 0000000..0305c46 --- /dev/null +++ b/main/boards/xingzhi-cube-0.85tft-ml307/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "xingzhi-cube-0.85tft-ml307", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc b/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc new file mode 100644 index 0000000..f3c0b3d --- /dev/null +++ b/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc @@ -0,0 +1,262 @@ +#include "ml307_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "power_save_timer.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "../xingzhi-cube-1.54tft-wifi/power_manager.h" + +#include +#include +#include + +#include +#include + +#include +#include "settings.h" + +#define TAG "XINGZHI_CUBE_0_85TFT_ML307" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + + +static const nv3023_lcd_init_cmd_t lcd_init_cmds[] = { + {0xff, (uint8_t[]){0xa5}, 1, 0}, + {0x3E, (uint8_t[]){0x09}, 1, 0}, + {0x3A, (uint8_t[]){0x65}, 1, 0}, + {0x82, (uint8_t[]){0x00}, 1, 0}, + {0x98, (uint8_t[]){0x00}, 1, 0}, + {0x63, (uint8_t[]){0x0f}, 1, 0}, + {0x64, (uint8_t[]){0x0f}, 1, 0}, + {0xB4, (uint8_t[]){0x34}, 1, 0}, + {0xB5, (uint8_t[]){0x30}, 1, 0}, + {0x83, (uint8_t[]){0x03}, 1, 0}, + {0x86, (uint8_t[]){0x04}, 1, 0}, + {0x87, (uint8_t[]){0x16}, 1, 0}, + {0x88, (uint8_t[]){0x0A}, 1, 0}, + {0x89, (uint8_t[]){0x27}, 1, 0}, + {0x93, (uint8_t[]){0x63}, 1, 0}, + {0x96, (uint8_t[]){0x81}, 1, 0}, + {0xC3, (uint8_t[]){0x10}, 1, 0}, + {0xE6, (uint8_t[]){0x00}, 1, 0}, + {0x99, (uint8_t[]){0x01}, 1, 0}, + {0x70, (uint8_t[]){0x09}, 1, 0}, + {0x71, (uint8_t[]){0x1D}, 1, 0}, + {0x72, (uint8_t[]){0x14}, 1, 0}, + {0x73, (uint8_t[]){0x0a}, 1, 0}, + {0x74, (uint8_t[]){0x11}, 1, 0}, + {0x75, (uint8_t[]){0x16}, 1, 0}, + {0x76, (uint8_t[]){0x38}, 1, 0}, + {0x77, (uint8_t[]){0x0B}, 1, 0}, + {0x78, (uint8_t[]){0x08}, 1, 0}, + {0x79, (uint8_t[]){0x3E}, 1, 0}, + {0x7a, (uint8_t[]){0x07}, 1, 0}, + {0x7b, (uint8_t[]){0x0D}, 1, 0}, + {0x7c, (uint8_t[]){0x16}, 1, 0}, + {0x7d, (uint8_t[]){0x0F}, 1, 0}, + {0x7e, (uint8_t[]){0x14}, 1, 0}, + {0x7f, (uint8_t[]){0x05}, 1, 0}, + {0xa0, (uint8_t[]){0x04}, 1, 0}, + {0xa1, (uint8_t[]){0x28}, 1, 0}, + {0xa2, (uint8_t[]){0x0c}, 1, 0}, + {0xa3, (uint8_t[]){0x11}, 1, 0}, + {0xa4, (uint8_t[]){0x0b}, 1, 0}, + {0xa5, (uint8_t[]){0x23}, 1, 0}, + {0xa6, (uint8_t[]){0x45}, 1, 0}, + {0xa7, (uint8_t[]){0x07}, 1, 0}, + {0xa8, (uint8_t[]){0x0a}, 1, 0}, + {0xa9, (uint8_t[]){0x3b}, 1, 0}, + {0xaa, (uint8_t[]){0x0d}, 1, 0}, + {0xab, (uint8_t[]){0x18}, 1, 0}, + {0xac, (uint8_t[]){0x14}, 1, 0}, + {0xad, (uint8_t[]){0x0F}, 1, 0}, + {0xae, (uint8_t[]){0x19}, 1, 0}, + {0xaf, (uint8_t[]){0x08}, 1, 0}, + {0xff, (uint8_t[]){0x00}, 1, 0}, + {0x11, (uint8_t[]){0x00}, 0, 120}, + {0x29, (uint8_t[]){0x00}, 0, 10} +}; + +class XINGZHI_CUBE_0_85TFT_ML307 : public Ml307Board { +private: + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + SpiLcdDisplay* display_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_38); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + 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"); + rtc_gpio_set_level(GPIO_NUM_21, 0); + // 启用保持功能,确保睡眠期间电平不变 + rtc_gpio_hold_en(GPIO_NUM_21); + esp_lcd_panel_disp_on_off(panel_, false); //关闭显示 + esp_deep_sleep_start(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL; + buscfg.quadwp_io_num = GPIO_NUM_NC; + buscfg.quadhd_io_num = GPIO_NUM_NC; + buscfg.max_transfer_sz = DISPLAY_HEIGHT * 80 *sizeof(uint16_t); + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + app.ToggleChatState(); + }); + } + + void InitializeNv3023Display() { + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = NV3023_PANEL_IO_SPI_CONFIG(DISPLAY_CS, DISPLAY_DC, NULL, NULL); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI3_HOST, &io_config, &panel_io_)); + + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + nv3023_vendor_config_t vendor_config = { // Uncomment these lines if use custom initialization commands + .init_cmds = lcd_init_cmds, + .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(nv3023_lcd_init_cmd_t), + }; + panel_config.reset_gpio_num = DISPLAY_RES; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = &vendor_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_nv3023(panel_io_, &panel_config, &panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, false)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + 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")); + } + + void Initializegpio21_45() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + //gpio_num_t sp_45 = GPIO_NUM_45; + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = (1ULL << GPIO_NUM_45); + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE; + gpio_config(&io_conf); + gpio_set_level(GPIO_NUM_45, 0); + } + +public: + XINGZHI_CUBE_0_85TFT_ML307(): Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + Initializegpio21_45(); // 初始时,拉高21引脚,保证4g模块正常工作 + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeSpi(); + InitializeButtons(); + InitializeNv3023Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + 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(); + } + Ml307Board::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(XINGZHI_CUBE_0_85TFT_ML307); diff --git a/main/boards/xingzhi-cube-0.85tft-wifi/config.h b/main/boards/xingzhi-cube-0.85tft-wifi/config.h new file mode 100644 index 0000000..8b2bf9d --- /dev/null +++ b/main/boards/xingzhi-cube-0.85tft-wifi/config.h @@ -0,0 +1,37 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA GPIO_NUM_10 +#define DISPLAY_SCL GPIO_NUM_9 +#define DISPLAY_DC GPIO_NUM_8 +#define DISPLAY_CS GPIO_NUM_14 +#define DISPLAY_RES GPIO_NUM_18 +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y true +#define BACKLIGHT_INVERT false +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_13 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xingzhi-cube-0.85tft-wifi/config.json b/main/boards/xingzhi-cube-0.85tft-wifi/config.json new file mode 100644 index 0000000..867160f --- /dev/null +++ b/main/boards/xingzhi-cube-0.85tft-wifi/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "xingzhi-cube-0.85tft-wifi", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/xingzhi-cube-0.85tft-wifi/xingzhi-cube-0.85tft-wifi.cc b/main/boards/xingzhi-cube-0.85tft-wifi/xingzhi-cube-0.85tft-wifi.cc new file mode 100644 index 0000000..0380e2b --- /dev/null +++ b/main/boards/xingzhi-cube-0.85tft-wifi/xingzhi-cube-0.85tft-wifi.cc @@ -0,0 +1,266 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "power_save_timer.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "../xingzhi-cube-1.54tft-wifi/power_manager.h" + +#include +#include +#include + +#include +#include + +#include +#include "settings.h" + +#define TAG "XINGZHI_CUBE_0_85TFT_WIFI" + +LV_FONT_DECLARE(font_puhui_16_4); +LV_FONT_DECLARE(font_awesome_16_4); + + +static const nv3023_lcd_init_cmd_t lcd_init_cmds[] = { + {0xff, (uint8_t[]){0xa5}, 1, 0}, + {0x3E, (uint8_t[]){0x09}, 1, 0}, + {0x3A, (uint8_t[]){0x65}, 1, 0}, + {0x82, (uint8_t[]){0x00}, 1, 0}, + {0x98, (uint8_t[]){0x00}, 1, 0}, + {0x63, (uint8_t[]){0x0f}, 1, 0}, + {0x64, (uint8_t[]){0x0f}, 1, 0}, + {0xB4, (uint8_t[]){0x34}, 1, 0}, + {0xB5, (uint8_t[]){0x30}, 1, 0}, + {0x83, (uint8_t[]){0x03}, 1, 0}, + {0x86, (uint8_t[]){0x04}, 1, 0}, + {0x87, (uint8_t[]){0x16}, 1, 0}, + {0x88, (uint8_t[]){0x0A}, 1, 0}, + {0x89, (uint8_t[]){0x27}, 1, 0}, + {0x93, (uint8_t[]){0x63}, 1, 0}, + {0x96, (uint8_t[]){0x81}, 1, 0}, + {0xC3, (uint8_t[]){0x10}, 1, 0}, + {0xE6, (uint8_t[]){0x00}, 1, 0}, + {0x99, (uint8_t[]){0x01}, 1, 0}, + {0x70, (uint8_t[]){0x09}, 1, 0}, + {0x71, (uint8_t[]){0x1D}, 1, 0}, + {0x72, (uint8_t[]){0x14}, 1, 0}, + {0x73, (uint8_t[]){0x0a}, 1, 0}, + {0x74, (uint8_t[]){0x11}, 1, 0}, + {0x75, (uint8_t[]){0x16}, 1, 0}, + {0x76, (uint8_t[]){0x38}, 1, 0}, + {0x77, (uint8_t[]){0x0B}, 1, 0}, + {0x78, (uint8_t[]){0x08}, 1, 0}, + {0x79, (uint8_t[]){0x3E}, 1, 0}, + {0x7a, (uint8_t[]){0x07}, 1, 0}, + {0x7b, (uint8_t[]){0x0D}, 1, 0}, + {0x7c, (uint8_t[]){0x16}, 1, 0}, + {0x7d, (uint8_t[]){0x0F}, 1, 0}, + {0x7e, (uint8_t[]){0x14}, 1, 0}, + {0x7f, (uint8_t[]){0x05}, 1, 0}, + {0xa0, (uint8_t[]){0x04}, 1, 0}, + {0xa1, (uint8_t[]){0x28}, 1, 0}, + {0xa2, (uint8_t[]){0x0c}, 1, 0}, + {0xa3, (uint8_t[]){0x11}, 1, 0}, + {0xa4, (uint8_t[]){0x0b}, 1, 0}, + {0xa5, (uint8_t[]){0x23}, 1, 0}, + {0xa6, (uint8_t[]){0x45}, 1, 0}, + {0xa7, (uint8_t[]){0x07}, 1, 0}, + {0xa8, (uint8_t[]){0x0a}, 1, 0}, + {0xa9, (uint8_t[]){0x3b}, 1, 0}, + {0xaa, (uint8_t[]){0x0d}, 1, 0}, + {0xab, (uint8_t[]){0x18}, 1, 0}, + {0xac, (uint8_t[]){0x14}, 1, 0}, + {0xad, (uint8_t[]){0x0F}, 1, 0}, + {0xae, (uint8_t[]){0x19}, 1, 0}, + {0xaf, (uint8_t[]){0x08}, 1, 0}, + {0xff, (uint8_t[]){0x00}, 1, 0}, + {0x11, (uint8_t[]){0x00}, 0, 120}, + {0x29, (uint8_t[]){0x00}, 0, 10} +}; + +class XINGZHI_CUBE_0_85TFT_WIFI : public WifiBoard { +private: + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + SpiLcdDisplay* display_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_38); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + 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"); + rtc_gpio_set_level(GPIO_NUM_21, 0); + // 启用保持功能,确保睡眠期间电平不变 + rtc_gpio_hold_en(GPIO_NUM_21); + esp_lcd_panel_disp_on_off(panel_, false); //关闭显示 + esp_deep_sleep_start(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL; + buscfg.quadwp_io_num = GPIO_NUM_NC; + buscfg.quadhd_io_num = GPIO_NUM_NC; + buscfg.max_transfer_sz = DISPLAY_HEIGHT * 80 *sizeof(uint16_t); + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + + void InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + + void InitializeNv3023Display() { + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = NV3023_PANEL_IO_SPI_CONFIG(DISPLAY_CS, DISPLAY_DC, NULL, NULL); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI3_HOST, &io_config, &panel_io_)); + + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + nv3023_vendor_config_t vendor_config = { // Uncomment these lines if use custom initialization commands + .init_cmds = lcd_init_cmds, + .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(nv3023_lcd_init_cmd_t), + }; + panel_config.reset_gpio_num = DISPLAY_RES; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = &vendor_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_nv3023(panel_io_, &panel_config, &panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, false)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + 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_16_4, + .icon_font = &font_awesome_16_4, + .emoji_font = font_emoji_32_init(), + }); + } + + 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")); + } + + void Initializegpio21_45() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + //gpio_num_t sp_45 = GPIO_NUM_45; + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = (1ULL << GPIO_NUM_45); + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE; + gpio_config(&io_conf); + gpio_set_level(GPIO_NUM_45, 0); + } + +public: + XINGZHI_CUBE_0_85TFT_WIFI(): + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + Initializegpio21_45(); // 初始时,拉高21引脚,保证4g模块正常工作 + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeSpi(); + InitializeButtons(); + InitializeNv3023Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + 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); + } +}; + +DECLARE_BOARD(XINGZHI_CUBE_0_85TFT_WIFI); diff --git a/main/boards/xingzhi-cube-0.96oled-ml307/config.h b/main/boards/xingzhi-cube-0.96oled-ml307/config.h new file mode 100644 index 0000000..0f3f8ce --- /dev/null +++ b/main/boards/xingzhi-cube-0.96oled-ml307/config.h @@ -0,0 +1,30 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#define DISPLAY_SDA_PIN GPIO_NUM_41 +#define DISPLAY_SCL_PIN GPIO_NUM_42 +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + +#define ML307_RX_PIN GPIO_NUM_11 +#define ML307_TX_PIN GPIO_NUM_12 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xingzhi-cube-0.96oled-ml307/config.json b/main/boards/xingzhi-cube-0.96oled-ml307/config.json new file mode 100644 index 0000000..be5919c --- /dev/null +++ b/main/boards/xingzhi-cube-0.96oled-ml307/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "xingzhi-cube-0.96oled-ml307", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc b/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc new file mode 100644 index 0000000..78ed10e --- /dev/null +++ b/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc @@ -0,0 +1,238 @@ +#include "ml307_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/oled_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "power_save_timer.h" +#include "../xingzhi-cube-1.54tft-wifi/power_manager.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "XINGZHI_CUBE_0_96OLED_ML307" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + + +class XINGZHI_CUBE_0_96OLED_ML307 : public Ml307Board { +private: + i2c_master_bus_handle_t display_i2c_bus_; + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + Display* display_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_38); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + }); + power_save_timer_->OnShutdownRequest([this]() { + ESP_LOGI(TAG, "Shutting down"); + rtc_gpio_set_level(GPIO_NUM_21, 0); + // 启用保持功能,确保睡眠期间电平不变 + rtc_gpio_hold_en(GPIO_NUM_21); + esp_lcd_panel_disp_on_off(panel_, false); //关闭显示 + esp_deep_sleep_start(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + app.ToggleChatState(); + }); + + volume_up_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Battery")); + } + +public: + XINGZHI_CUBE_0_96OLED_ML307() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + 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(); + } + Ml307Board::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(XINGZHI_CUBE_0_96OLED_ML307); diff --git a/main/boards/xingzhi-cube-0.96oled-wifi/config.h b/main/boards/xingzhi-cube-0.96oled-wifi/config.h new file mode 100644 index 0000000..b353890 --- /dev/null +++ b/main/boards/xingzhi-cube-0.96oled-wifi/config.h @@ -0,0 +1,27 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#define BUILTIN_LED_GPIO GPIO_NUM_48 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#define DISPLAY_SDA_PIN GPIO_NUM_41 +#define DISPLAY_SCL_PIN GPIO_NUM_42 +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xingzhi-cube-0.96oled-wifi/config.json b/main/boards/xingzhi-cube-0.96oled-wifi/config.json new file mode 100644 index 0000000..2cba4c6 --- /dev/null +++ b/main/boards/xingzhi-cube-0.96oled-wifi/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "xingzhi-cube-0.96oled-wifi", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/xingzhi-cube-0.96oled-wifi/xingzhi-cube-0.96oled-wifi.cc b/main/boards/xingzhi-cube-0.96oled-wifi/xingzhi-cube-0.96oled-wifi.cc new file mode 100644 index 0000000..e636acd --- /dev/null +++ b/main/boards/xingzhi-cube-0.96oled-wifi/xingzhi-cube-0.96oled-wifi.cc @@ -0,0 +1,243 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/oled_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "power_save_timer.h" +#include "../xingzhi-cube-1.54tft-wifi/power_manager.h" + +#include + +#include +#include +#include +#include +#include +#include + +#define TAG "XINGZHI_CUBE_0_96OLED_WIFI" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + + +class XINGZHI_CUBE_0_96OLED_WIFI : public WifiBoard { +private: + i2c_master_bus_handle_t display_i2c_bus_; + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + Display* display_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_38); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + }); + power_save_timer_->OnShutdownRequest([this]() { + ESP_LOGI(TAG, "Shutting down"); + rtc_gpio_set_level(GPIO_NUM_21, 0); + // 启用保持功能,确保睡眠期间电平不变 + rtc_gpio_hold_en(GPIO_NUM_21); + esp_lcd_panel_disp_on_off(panel_, false); //关闭显示 + esp_deep_sleep_start(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeDisplayI2c() { + i2c_master_bus_config_t bus_config = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = DISPLAY_SDA_PIN, + .scl_io_num = DISPLAY_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(&bus_config, &display_i2c_bus_)); + } + + void InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + + volume_up_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Battery")); + } + +public: + XINGZHI_CUBE_0_96OLED_WIFI() : + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeDisplayI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + 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); + } +}; + +DECLARE_BOARD(XINGZHI_CUBE_0_96OLED_WIFI); diff --git a/main/boards/xingzhi-cube-1.54tft-ml307/config.h b/main/boards/xingzhi-cube-1.54tft-ml307/config.h new file mode 100644 index 0000000..9167578 --- /dev/null +++ b/main/boards/xingzhi-cube-1.54tft-ml307/config.h @@ -0,0 +1,40 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#define DISPLAY_SDA GPIO_NUM_10 +#define DISPLAY_SCL GPIO_NUM_9 +#define DISPLAY_DC GPIO_NUM_8 +#define DISPLAY_CS GPIO_NUM_14 +#define DISPLAY_RES GPIO_NUM_18 +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define BACKLIGHT_INVERT false +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_13 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#define ML307_RX_PIN GPIO_NUM_11 +#define ML307_TX_PIN GPIO_NUM_12 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xingzhi-cube-1.54tft-ml307/config.json b/main/boards/xingzhi-cube-1.54tft-ml307/config.json new file mode 100644 index 0000000..9611231 --- /dev/null +++ b/main/boards/xingzhi-cube-1.54tft-ml307/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "xingzhi-cube-1.54tft-ml307", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc b/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc new file mode 100644 index 0000000..53dd362 --- /dev/null +++ b/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc @@ -0,0 +1,219 @@ +#include "ml307_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "power_save_timer.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "../xingzhi-cube-1.54tft-wifi/power_manager.h" + +#include +#include + +#include +#include + +#define TAG "XINGZHI_CUBE_1_54TFT_ML307" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + + +class XINGZHI_CUBE_1_54TFT_ML307 : public Ml307Board { +private: + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + SpiLcdDisplay* display_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_38); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + 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"); + rtc_gpio_set_level(GPIO_NUM_21, 0); + // 启用保持功能,确保睡眠期间电平不变 + rtc_gpio_hold_en(GPIO_NUM_21); + esp_lcd_panel_disp_on_off(panel_, false); //关闭显示 + esp_deep_sleep_start(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + app.ToggleChatState(); + }); + + volume_up_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + void InitializeSt7789Display() { + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS; + io_config.dc_gpio_num = DISPLAY_DC; + 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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io_)); + + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RES; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io_, &panel_config, &panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, true)); + + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + 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")); + } + +public: + XINGZHI_CUBE_1_54TFT_ML307() : + Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096), + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeSpi(); + InitializeButtons(); + InitializeSt7789Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + 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(); + } + Ml307Board::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(XINGZHI_CUBE_1_54TFT_ML307); diff --git a/main/boards/xingzhi-cube-1.54tft-wifi/config.h b/main/boards/xingzhi-cube-1.54tft-wifi/config.h new file mode 100644 index 0000000..c1a998a --- /dev/null +++ b/main/boards/xingzhi-cube-1.54tft-wifi/config.h @@ -0,0 +1,36 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 16000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4 +#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5 +#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6 +#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7 +#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15 +#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16 + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#define DISPLAY_SDA GPIO_NUM_10 +#define DISPLAY_SCL GPIO_NUM_9 +#define DISPLAY_DC GPIO_NUM_8 +#define DISPLAY_CS GPIO_NUM_14 +#define DISPLAY_RES GPIO_NUM_18 +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define BACKLIGHT_INVERT false +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_13 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xingzhi-cube-1.54tft-wifi/config.json b/main/boards/xingzhi-cube-1.54tft-wifi/config.json new file mode 100644 index 0000000..6cfa0d3 --- /dev/null +++ b/main/boards/xingzhi-cube-1.54tft-wifi/config.json @@ -0,0 +1,9 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "xingzhi-cube-1.54tft-wifi", + "sdkconfig_append": [] + } + ] +} \ No newline at end of file diff --git a/main/boards/xingzhi-cube-1.54tft-wifi/power_manager.h b/main/boards/xingzhi-cube-1.54tft-wifi/power_manager.h new file mode 100644 index 0000000..8d238f2 --- /dev/null +++ b/main/boards/xingzhi-cube-1.54tft-wifi/power_manager.h @@ -0,0 +1,186 @@ +#pragma once +#include +#include + +#include +#include +#include + + +class PowerManager { +private: + esp_timer_handle_t timer_handle_; + std::function on_charging_status_changed_; + std::function on_low_battery_status_changed_; + + gpio_num_t charging_pin_ = GPIO_NUM_NC; + std::vector adc_values_; + uint32_t battery_level_ = 0; + bool is_charging_ = false; + bool is_low_battery_ = false; + int ticks_ = 0; + const int kBatteryAdcInterval = 60; + const int kBatteryAdcDataCount = 3; + const int kLowBatteryLevel = 20; + + adc_oneshot_unit_handle_t adc_handle_; + + void CheckBatteryStatus() { + // Get charging status + bool new_charging_status = gpio_get_level(charging_pin_) == 1; + if (new_charging_status != is_charging_) { + is_charging_ = new_charging_status; + if (on_charging_status_changed_) { + on_charging_status_changed_(is_charging_); + } + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据不足,则读取电池电量数据 + if (adc_values_.size() < kBatteryAdcDataCount) { + ReadBatteryAdcData(); + return; + } + + // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 + ticks_++; + if (ticks_ % kBatteryAdcInterval == 0) { + ReadBatteryAdcData(); + } + } + + void ReadBatteryAdcData() { + int adc_value; + ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_6, &adc_value)); + + // 将 ADC 值添加到队列中 + adc_values_.push_back(adc_value); + if (adc_values_.size() > kBatteryAdcDataCount) { + adc_values_.erase(adc_values_.begin()); + } + uint32_t average_adc = 0; + for (auto value : adc_values_) { + average_adc += value; + } + average_adc /= adc_values_.size(); + + // 定义电池电量区间 + const struct { + uint16_t adc; + uint8_t level; + } levels[] = { + {1970, 0}, + {2062, 20}, + {2154, 40}, + {2246, 60}, + {2338, 80}, + {2430, 100} + }; + + // 低于最低值时 + if (average_adc < levels[0].adc) { + battery_level_ = 0; + } + // 高于最高值时 + else if (average_adc >= levels[5].adc) { + battery_level_ = 100; + } else { + // 线性插值计算中间值 + for (int i = 0; i < 5; i++) { + if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) { + float ratio = static_cast(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc); + battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); + break; + } + } + } + + // Check low battery status + if (adc_values_.size() >= kBatteryAdcDataCount) { + bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; + if (new_low_battery_status != is_low_battery_) { + is_low_battery_ = new_low_battery_status; + if (on_low_battery_status_changed_) { + on_low_battery_status_changed_(is_low_battery_); + } + } + } + + ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); + } + +public: + PowerManager(gpio_num_t pin) : charging_pin_(pin) { + // 初始化充电引脚 + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << charging_pin_); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + // 创建电池电量检查定时器 + esp_timer_create_args_t timer_args = { + .callback = [](void* arg) { + PowerManager* self = static_cast(arg); + self->CheckBatteryStatus(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "battery_check_timer", + .skip_unhandled_events = true, + }; + ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); + + // 初始化 ADC + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_2, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); + + adc_oneshot_chan_cfg_t chan_config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_12, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_6, &chan_config)); + } + + ~PowerManager() { + if (timer_handle_) { + esp_timer_stop(timer_handle_); + esp_timer_delete(timer_handle_); + } + if (adc_handle_) { + adc_oneshot_del_unit(adc_handle_); + } + } + + bool IsCharging() { + // 如果电量已经满了,则不再显示充电中 + if (battery_level_ == 100) { + return false; + } + return is_charging_; + } + + bool IsDischarging() { + // 没有区分充电和放电,所以直接返回相反状态 + return !is_charging_; + } + + uint8_t GetBatteryLevel() { + return battery_level_; + } + + void OnLowBatteryStatusChanged(std::function callback) { + on_low_battery_status_changed_ = callback; + } + + void OnChargingStatusChanged(std::function callback) { + on_charging_status_changed_ = callback; + } +}; diff --git a/main/boards/xingzhi-cube-1.54tft-wifi/xingzhi-cube-1.54tft-wifi.cc b/main/boards/xingzhi-cube-1.54tft-wifi/xingzhi-cube-1.54tft-wifi.cc new file mode 100644 index 0000000..e4d002b --- /dev/null +++ b/main/boards/xingzhi-cube-1.54tft-wifi/xingzhi-cube-1.54tft-wifi.cc @@ -0,0 +1,223 @@ +#include "wifi_board.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "power_save_timer.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" +#include "assets/lang_config.h" +#include "power_manager.h" + +#include +#include +#include + +#include +#include + +#define TAG "XINGZHI_CUBE_1_54TFT_WIFI" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + + +class XINGZHI_CUBE_1_54TFT_WIFI : public WifiBoard { +private: + Button boot_button_; + Button volume_up_button_; + Button volume_down_button_; + SpiLcdDisplay* display_; + PowerSaveTimer* power_save_timer_; + PowerManager* power_manager_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + void InitializePowerManager() { + power_manager_ = new PowerManager(GPIO_NUM_38); + power_manager_->OnChargingStatusChanged([this](bool is_charging) { + if (is_charging) { + power_save_timer_->SetEnabled(false); + } else { + power_save_timer_->SetEnabled(true); + } + }); + } + + void InitializePowerSaveTimer() { + rtc_gpio_init(GPIO_NUM_21); + rtc_gpio_set_direction(GPIO_NUM_21, RTC_GPIO_MODE_OUTPUT_ONLY); + rtc_gpio_set_level(GPIO_NUM_21, 1); + + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + 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"); + rtc_gpio_set_level(GPIO_NUM_21, 0); + // 启用保持功能,确保睡眠期间电平不变 + rtc_gpio_hold_en(GPIO_NUM_21); + esp_lcd_panel_disp_on_off(panel_, false); //关闭显示 + esp_deep_sleep_start(); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_SDA; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCL; + 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)); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + // 使用新的打断按键功能:按一次进入listening,再按一次进入idle + app.ToggleListeningState(); + }); + + volume_up_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() + 10; + if (volume > 100) { + volume = 100; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_up_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); + }); + + volume_down_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + auto volume = codec->output_volume() - 10; + if (volume < 0) { + volume = 0; + } + codec->SetOutputVolume(volume); + GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); + }); + + volume_down_button_.OnLongPress([this]() { + power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + GetDisplay()->ShowNotification(Lang::Strings::MUTED); + }); + } + + void InitializeSt7789Display() { + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS; + io_config.dc_gpio_num = DISPLAY_DC; + 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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io_)); + + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RES; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io_, &panel_config, &panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, true)); + + 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, + .emoji_font = font_emoji_64_init(), + }); + } + + 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")); + } + +public: + XINGZHI_CUBE_1_54TFT_WIFI() : + boot_button_(BOOT_BUTTON_GPIO), + volume_up_button_(VOLUME_UP_BUTTON_GPIO), + volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) { + InitializePowerManager(); + InitializePowerSaveTimer(); + InitializeSpi(); + InitializeButtons(); + InitializeSt7789Display(); + InitializeIot(); + GetBacklight()->RestoreBrightness(); + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN); + 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); + } +}; + +DECLARE_BOARD(XINGZHI_CUBE_1_54TFT_WIFI); diff --git a/main/boards/xmini-c3/config.h b/main/boards/xmini-c3/config.h new file mode 100644 index 0000000..f37a035 --- /dev/null +++ b/main/boards/xmini-c3/config.h @@ -0,0 +1,28 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_6 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_5 + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_11 +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_3 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_4 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR + +#define BUILTIN_LED_GPIO GPIO_NUM_2 +#define BOOT_BUTTON_GPIO GPIO_NUM_9 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/xmini-c3/config.json b/main/boards/xmini-c3/config.json new file mode 100644 index 0000000..d6d2796 --- /dev/null +++ b/main/boards/xmini-c3/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "xmini-c3", + "sdkconfig_append": [ + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/xmini-c3/xmini_c3_board.cc b/main/boards/xmini-c3/xmini_c3_board.cc new file mode 100644 index 0000000..9e765b5 --- /dev/null +++ b/main/boards/xmini-c3/xmini_c3_board.cc @@ -0,0 +1,223 @@ +#include "wifi_board.h" +#include "audio_codecs/es8311_audio_codec.h" +#include "display/oled_display.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include "settings.h" +#include "config.h" +#include "power_save_timer.h" +#include "font_awesome_symbols.h" + +#include +#include +#include +#include +#include +#include + +#define TAG "XminiC3Board" + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_14_1); + +class XminiC3Board : public WifiBoard { +private: + i2c_master_bus_handle_t codec_i2c_bus_; + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + bool press_to_talk_enabled_ = false; + PowerSaveTimer* power_save_timer_; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(160, 60); + power_save_timer_->OnEnterSleepMode([this]() { + ESP_LOGI(TAG, "Enabling sleep mode"); + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("sleepy"); + + auto codec = GetAudioCodec(); + codec->EnableInput(false); + }); + power_save_timer_->OnExitSleepMode([this]() { + auto codec = GetAudioCodec(); + codec->EnableInput(true); + + auto display = GetDisplay(); + display->SetChatMessage("system", ""); + display->SetEmotion("neutral"); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeCodecI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_0, + .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 InitializeSsd1306Display() { + // SSD1306 config + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = 0x3C, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .control_phase_bytes = 1, + .dc_bit_offset = 6, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .flags = { + .dc_low_on_data = 0, + .disable_control_phase = 0, + }, + .scl_speed_hz = 400 * 1000, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c_v2(codec_i2c_bus_, &io_config, &panel_io_)); + + ESP_LOGI(TAG, "Install SSD1306 driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = -1; + panel_config.bits_per_pixel = 1; + + esp_lcd_panel_ssd1306_config_t ssd1306_config = { + .height = static_cast(DISPLAY_HEIGHT), + }; + panel_config.vendor_config = &ssd1306_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_)); + ESP_LOGI(TAG, "SSD1306 driver installed"); + + // Reset the display + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_)); + if (esp_lcd_panel_init(panel_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize display"); + display_ = new NoDisplay(); + return; + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + {&font_puhui_14_1, &font_awesome_14_1}); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + if (!press_to_talk_enabled_) { + app.ToggleChatState(); + } + }); + boot_button_.OnPressDown([this]() { + power_save_timer_->WakeUp(); + if (press_to_talk_enabled_) { + Application::GetInstance().StartListening(); + } + }); + boot_button_.OnPressUp([this]() { + if (press_to_talk_enabled_) { + Application::GetInstance().StopListening(); + } + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + Settings settings("vendor"); + press_to_talk_enabled_ = settings.GetInt("press_to_talk", 0) != 0; + + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("PressToTalk")); + } + +public: + XminiC3Board() : boot_button_(BOOT_BUTTON_GPIO) { + // 把 ESP32C3 的 VDD SPI 引脚作为普通 GPIO 口使用 + esp_efuse_write_field_bit(ESP_EFUSE_VDD_SPI_AS_GPIO); + + InitializeCodecI2c(); + InitializeSsd1306Display(); + InitializeButtons(); + InitializePowerSaveTimer(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual Display* GetDisplay() override { + return display_; + } + + 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; + } + + void SetPressToTalkEnabled(bool enabled) { + press_to_talk_enabled_ = enabled; + + Settings settings("vendor", true); + settings.SetInt("press_to_talk", enabled ? 1 : 0); + ESP_LOGI(TAG, "Press to talk enabled: %d", enabled); + } + + bool IsPressToTalkEnabled() { + return press_to_talk_enabled_; + } +}; + +DECLARE_BOARD(XminiC3Board); + + +namespace iot { + +class PressToTalk : public Thing { +public: + PressToTalk() : Thing("PressToTalk", "控制对话模式,一种是长按对话,一种是单击后连续对话。") { + // 定义设备的属性 + properties_.AddBooleanProperty("enabled", "true 表示长按说话模式,false 表示单击说话模式", []() -> bool { + auto board = static_cast(&Board::GetInstance()); + return board->IsPressToTalkEnabled(); + }); + + // 定义设备可以被远程执行的指令 + methods_.AddMethod("SetEnabled", "启用或禁用长按说话模式,调用前需要经过用户确认", ParameterList({ + Parameter("enabled", "true 表示长按说话模式,false 表示单击说话模式", kValueTypeBoolean, true) + }), [](const ParameterList& parameters) { + bool enabled = parameters["enabled"].boolean(); + auto board = static_cast(&Board::GetInstance()); + board->SetPressToTalkEnabled(enabled); + }); + } +}; + +} // namespace iot + +DECLARE_THING(PressToTalk); diff --git a/main/display/display.cc b/main/display/display.cc new file mode 100644 index 0000000..bba858c --- /dev/null +++ b/main/display/display.cc @@ -0,0 +1,10 @@ +#include +#include +#include +#include +#include + +#include "display.h" + +// 空的显示器实现,用于无显示器的板子 +// 所有方法都已经在头文件中定义为空实现 diff --git a/main/display/display.h b/main/display/display.h new file mode 100644 index 0000000..a4a93bf --- /dev/null +++ b/main/display/display.h @@ -0,0 +1,46 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#include +#include +#include +#include + +// 前向声明或空类型定义 +struct lv_font_t {}; + +struct DisplayFonts { + const lv_font_t* text_font = nullptr; + const lv_font_t* icon_font = nullptr; + const lv_font_t* emoji_font = nullptr; +}; + +class Display { +public: + Display() : width_(0), height_(0) {} + virtual ~Display() {} + + virtual void SetStatus(const char* status) {} + virtual void ShowNotification(const char* notification, int duration_ms = 3000) {} + virtual void ShowNotification(const std::string ¬ification, int duration_ms = 3000) {} + virtual void SetEmotion(const char* emotion) {} + virtual void SetChatMessage(const char* role, const char* content) {} + virtual void SetIcon(const char* icon) {} + virtual void SetTheme(const std::string& theme_name) {} + virtual std::string GetTheme() { return current_theme_name_; } + + inline int width() const { return width_; } + inline int height() const { return height_; } + + virtual bool Lock(int timeout_ms = 0) { return true; } + virtual void Unlock() {} + + virtual void Update() {} + +protected: + int width_; + int height_; + std::string current_theme_name_; +}; + +#endif // DISPLAY_H diff --git a/main/display/lcd_display.cc b/main/display/lcd_display.cc new file mode 100644 index 0000000..7488dd1 --- /dev/null +++ b/main/display/lcd_display.cc @@ -0,0 +1,887 @@ +#include "lcd_display.h" + +#include +#include +#include +#include +#include +#include "assets/lang_config.h" +#include +#include "settings.h" + +#include "board.h" + +#define TAG "LcdDisplay" + +// Color definitions for dark theme +#define DARK_BACKGROUND_COLOR lv_color_hex(0x121212) // Dark background +#define DARK_TEXT_COLOR lv_color_white() // White text +#define DARK_CHAT_BACKGROUND_COLOR lv_color_hex(0x1E1E1E) // Slightly lighter than background +#define DARK_USER_BUBBLE_COLOR lv_color_hex(0x1A6C37) // Dark green +#define DARK_ASSISTANT_BUBBLE_COLOR lv_color_hex(0x333333) // Dark gray +#define DARK_SYSTEM_BUBBLE_COLOR lv_color_hex(0x2A2A2A) // Medium gray +#define DARK_SYSTEM_TEXT_COLOR lv_color_hex(0xAAAAAA) // Light gray text +#define DARK_BORDER_COLOR lv_color_hex(0x333333) // Dark gray border +#define DARK_LOW_BATTERY_COLOR lv_color_hex(0xFF0000) // Red for dark mode + +// Color definitions for light theme +#define LIGHT_BACKGROUND_COLOR lv_color_white() // White background +#define LIGHT_TEXT_COLOR lv_color_black() // Black text +#define LIGHT_CHAT_BACKGROUND_COLOR lv_color_hex(0xE0E0E0) // Light gray background +#define LIGHT_USER_BUBBLE_COLOR lv_color_hex(0x95EC69) // WeChat green +#define LIGHT_ASSISTANT_BUBBLE_COLOR lv_color_white() // White +#define LIGHT_SYSTEM_BUBBLE_COLOR lv_color_hex(0xE0E0E0) // Light gray +#define LIGHT_SYSTEM_TEXT_COLOR lv_color_hex(0x666666) // Dark gray text +#define LIGHT_BORDER_COLOR lv_color_hex(0xE0E0E0) // Light gray border +#define LIGHT_LOW_BATTERY_COLOR lv_color_black() // Black for light mode + +// Theme color structure +struct ThemeColors { + lv_color_t background; + lv_color_t text; + lv_color_t chat_background; + lv_color_t user_bubble; + lv_color_t assistant_bubble; + lv_color_t system_bubble; + lv_color_t system_text; + lv_color_t border; + lv_color_t low_battery; +}; + +// Define dark theme colors +static const ThemeColors DARK_THEME = { + .background = DARK_BACKGROUND_COLOR, + .text = DARK_TEXT_COLOR, + .chat_background = DARK_CHAT_BACKGROUND_COLOR, + .user_bubble = DARK_USER_BUBBLE_COLOR, + .assistant_bubble = DARK_ASSISTANT_BUBBLE_COLOR, + .system_bubble = DARK_SYSTEM_BUBBLE_COLOR, + .system_text = DARK_SYSTEM_TEXT_COLOR, + .border = DARK_BORDER_COLOR, + .low_battery = DARK_LOW_BATTERY_COLOR +}; + +// Define light theme colors +static const ThemeColors LIGHT_THEME = { + .background = LIGHT_BACKGROUND_COLOR, + .text = LIGHT_TEXT_COLOR, + .chat_background = LIGHT_CHAT_BACKGROUND_COLOR, + .user_bubble = LIGHT_USER_BUBBLE_COLOR, + .assistant_bubble = LIGHT_ASSISTANT_BUBBLE_COLOR, + .system_bubble = LIGHT_SYSTEM_BUBBLE_COLOR, + .system_text = LIGHT_SYSTEM_TEXT_COLOR, + .border = LIGHT_BORDER_COLOR, + .low_battery = LIGHT_LOW_BATTERY_COLOR +}; + +// Current theme - initialize based on default config +static ThemeColors current_theme = LIGHT_THEME; + + +LV_FONT_DECLARE(font_awesome_30_4); + +SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy, + DisplayFonts fonts) + : LcdDisplay(panel_io, panel, fonts) { + width_ = width; + height_ = height; + + // draw white + std::vector buffer(width_, 0xFFFF); + for (int y = 0; y < height_; y++) { + esp_lcd_panel_draw_bitmap(panel_, 0, y, width_, y + 1, buffer.data()); + } + + // Set the display to on + ESP_LOGI(TAG, "Turning display on"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_, true)); + + ESP_LOGI(TAG, "Initialize LVGL library"); + lv_init(); + + ESP_LOGI(TAG, "Initialize LVGL port"); + lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG(); + port_cfg.task_priority = 1; + port_cfg.timer_period_ms = 50; + lvgl_port_init(&port_cfg); + + ESP_LOGI(TAG, "Adding LCD screen"); + const lvgl_port_display_cfg_t display_cfg = { + .io_handle = panel_io_, + .panel_handle = panel_, + .control_handle = nullptr, + .buffer_size = static_cast(width_ * 10), + .double_buffer = false, + .trans_size = 0, + .hres = static_cast(width_), + .vres = static_cast(height_), + .monochrome = false, + .rotation = { + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + }, + .color_format = LV_COLOR_FORMAT_RGB565, + .flags = { + .buff_dma = 1, + .buff_spiram = 0, + .sw_rotate = 0, + .swap_bytes = 1, + .full_refresh = 0, + .direct_mode = 0, + }, + }; + + display_ = lvgl_port_add_disp(&display_cfg); + if (display_ == nullptr) { + ESP_LOGE(TAG, "Failed to add display"); + return; + } + + if (offset_x != 0 || offset_y != 0) { + lv_display_set_offset(display_, offset_x, offset_y); + } + + // Update the theme + if (current_theme_name_ == "dark") { + current_theme = DARK_THEME; + } else if (current_theme_name_ == "light") { + current_theme = LIGHT_THEME; + } + + SetupUI(); +} + +// RGB LCD实现 +RgbLcdDisplay::RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, + bool mirror_x, bool mirror_y, bool swap_xy, + DisplayFonts fonts) + : LcdDisplay(panel_io, panel, fonts) { + width_ = width; + height_ = height; + + // draw white + std::vector buffer(width_, 0xFFFF); + for (int y = 0; y < height_; y++) { + esp_lcd_panel_draw_bitmap(panel_, 0, y, width_, y + 1, buffer.data()); + } + + ESP_LOGI(TAG, "Initialize LVGL library"); + lv_init(); + + ESP_LOGI(TAG, "Initialize LVGL port"); + lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG(); + port_cfg.task_priority = 1; + lvgl_port_init(&port_cfg); + + ESP_LOGI(TAG, "Adding LCD screen"); + const lvgl_port_display_cfg_t display_cfg = { + .io_handle = panel_io_, + .panel_handle = panel_, + .buffer_size = static_cast(width_ * 10), + .double_buffer = true, + .hres = static_cast(width_), + .vres = static_cast(height_), + .rotation = { + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + }, + .flags = { + .buff_dma = 1, + .swap_bytes = 0, + .full_refresh = 1, + .direct_mode = 1, + }, + }; + + const lvgl_port_display_rgb_cfg_t rgb_cfg = { + .flags = { + .bb_mode = true, + .avoid_tearing = true, + } + }; + + display_ = lvgl_port_add_disp_rgb(&display_cfg, &rgb_cfg); + if (display_ == nullptr) { + ESP_LOGE(TAG, "Failed to add RGB display"); + return; + } + + if (offset_x != 0 || offset_y != 0) { + lv_display_set_offset(display_, offset_x, offset_y); + } + + // Update the theme + if (current_theme_name_ == "dark") { + current_theme = DARK_THEME; + } else if (current_theme_name_ == "light") { + current_theme = LIGHT_THEME; + } + + SetupUI(); +} + +LcdDisplay::~LcdDisplay() { + // 然后再清理 LVGL 对象 + if (content_ != nullptr) { + lv_obj_del(content_); + } + if (status_bar_ != nullptr) { + lv_obj_del(status_bar_); + } + if (side_bar_ != nullptr) { + lv_obj_del(side_bar_); + } + if (container_ != nullptr) { + lv_obj_del(container_); + } + if (display_ != nullptr) { + lv_display_delete(display_); + } + + if (panel_ != nullptr) { + esp_lcd_panel_del(panel_); + } + if (panel_io_ != nullptr) { + esp_lcd_panel_io_del(panel_io_); + } +} + +bool LcdDisplay::Lock(int timeout_ms) { + return lvgl_port_lock(timeout_ms); +} + +void LcdDisplay::Unlock() { + lvgl_port_unlock(); +} + +#if CONFIG_USE_WECHAT_MESSAGE_STYLE +void LcdDisplay::SetupUI() { + DisplayLockGuard lock(this); + + auto screen = lv_screen_active(); + lv_obj_set_style_text_font(screen, fonts_.text_font, 0); + lv_obj_set_style_text_color(screen, current_theme.text, 0); + lv_obj_set_style_bg_color(screen, current_theme.background, 0); + + /* Container */ + container_ = lv_obj_create(screen); + lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES); + lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(container_, 0, 0); + lv_obj_set_style_border_width(container_, 0, 0); + lv_obj_set_style_pad_row(container_, 0, 0); + lv_obj_set_style_bg_color(container_, current_theme.background, 0); + lv_obj_set_style_border_color(container_, current_theme.border, 0); + + /* Status bar */ + status_bar_ = lv_obj_create(container_); + lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT); + lv_obj_set_style_radius(status_bar_, 0, 0); + lv_obj_set_style_bg_color(status_bar_, current_theme.background, 0); + lv_obj_set_style_text_color(status_bar_, current_theme.text, 0); + + /* Content - Chat area */ + content_ = lv_obj_create(container_); + lv_obj_set_style_radius(content_, 0, 0); + lv_obj_set_width(content_, LV_HOR_RES); + lv_obj_set_flex_grow(content_, 1); + lv_obj_set_style_pad_all(content_, 10, 0); + lv_obj_set_style_bg_color(content_, current_theme.chat_background, 0); // Background for chat area + lv_obj_set_style_border_color(content_, current_theme.border, 0); // Border color for chat area + + // Enable scrolling for chat content + lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_scroll_dir(content_, LV_DIR_VER); + + // Create a flex container for chat messages + lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(content_, 10, 0); // Space between messages + + // We'll create chat messages dynamically in SetChatMessage + chat_message_label_ = nullptr; + + /* Status bar */ + lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(status_bar_, 0, 0); + lv_obj_set_style_border_width(status_bar_, 0, 0); + lv_obj_set_style_pad_column(status_bar_, 0, 0); + lv_obj_set_style_pad_left(status_bar_, 10, 0); + lv_obj_set_style_pad_right(status_bar_, 10, 0); + lv_obj_set_style_pad_top(status_bar_, 2, 0); + lv_obj_set_style_pad_bottom(status_bar_, 2, 0); + lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF); + // 设置状态栏的内容垂直居中 + lv_obj_set_flex_align(status_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + // 创建emotion_label_在状态栏最左侧 + emotion_label_ = lv_label_create(status_bar_); + lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_4, 0); + lv_obj_set_style_text_color(emotion_label_, current_theme.text, 0); + lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP); + lv_obj_set_style_margin_right(emotion_label_, 5, 0); // 添加右边距,与后面的元素分隔 + + notification_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(notification_label_, 1); + lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(notification_label_, current_theme.text, 0); + lv_label_set_text(notification_label_, ""); + lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); + + status_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(status_label_, 1); + lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(status_label_, current_theme.text, 0); + lv_label_set_text(status_label_, Lang::Strings::INITIALIZING); + + mute_label_ = lv_label_create(status_bar_); + lv_label_set_text(mute_label_, ""); + lv_obj_set_style_text_font(mute_label_, fonts_.icon_font, 0); + lv_obj_set_style_text_color(mute_label_, current_theme.text, 0); + + network_label_ = lv_label_create(status_bar_); + lv_label_set_text(network_label_, ""); + lv_obj_set_style_text_font(network_label_, fonts_.icon_font, 0); + lv_obj_set_style_text_color(network_label_, current_theme.text, 0); + lv_obj_set_style_margin_left(network_label_, 5, 0); // 添加左边距,与前面的元素分隔 + + battery_label_ = lv_label_create(status_bar_); + lv_label_set_text(battery_label_, ""); + lv_obj_set_style_text_font(battery_label_, fonts_.icon_font, 0); + lv_obj_set_style_text_color(battery_label_, current_theme.text, 0); + lv_obj_set_style_margin_left(battery_label_, 5, 0); // 添加左边距,与前面的元素分隔 + + low_battery_popup_ = lv_obj_create(screen); + lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, fonts_.text_font->line_height * 2); + lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_set_style_bg_color(low_battery_popup_, current_theme.low_battery, 0); + lv_obj_set_style_radius(low_battery_popup_, 10, 0); + lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_); + lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE); + lv_obj_set_style_text_color(low_battery_label, lv_color_white(), 0); + lv_obj_center(low_battery_label); + lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); +} + +#define MAX_MESSAGES 20 +void LcdDisplay::SetChatMessage(const char* role, const char* content) { + DisplayLockGuard lock(this); + if (content_ == nullptr) { + return; + } + + //避免出现空的消息框 + if(strlen(content) == 0) return; + + // 检查消息数量是否超过限制 + uint32_t child_count = lv_obj_get_child_cnt(content_); + if (child_count >= MAX_MESSAGES) { + // 删除最早的消息(第一个子对象) + lv_obj_t* first_child = lv_obj_get_child(content_, 0); + lv_obj_t* last_child = lv_obj_get_child(content_, child_count - 1); + if (first_child != nullptr) { + lv_obj_del(first_child); + } + // Scroll to the last message immediately + if (last_child != nullptr) { + lv_obj_scroll_to_view_recursive(last_child, LV_ANIM_OFF); + } + } + + // Create a message bubble + lv_obj_t* msg_bubble = lv_obj_create(content_); + lv_obj_set_style_radius(msg_bubble, 8, 0); + lv_obj_set_scrollbar_mode(msg_bubble, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_border_width(msg_bubble, 1, 0); + lv_obj_set_style_border_color(msg_bubble, current_theme.border, 0); + lv_obj_set_style_pad_all(msg_bubble, 8, 0); + + // Create the message text + lv_obj_t* msg_text = lv_label_create(msg_bubble); + lv_label_set_text(msg_text, content); + + // 计算文本实际宽度 + lv_coord_t text_width = lv_txt_get_width(content, strlen(content), fonts_.text_font, 0); + + // 计算气泡宽度 + lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 屏幕宽度的85% + lv_coord_t min_width = 20; + lv_coord_t bubble_width; + + // 确保文本宽度不小于最小宽度 + if (text_width < min_width) { + text_width = min_width; + } + + // 如果文本宽度小于最大宽度,使用文本宽度 + if (text_width < max_width) { + bubble_width = text_width; + } else { + bubble_width = max_width; + } + + // 设置消息文本的宽度 + lv_obj_set_width(msg_text, bubble_width); // 减去padding + lv_label_set_long_mode(msg_text, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_font(msg_text, fonts_.text_font, 0); + + // 设置气泡宽度 + lv_obj_set_width(msg_bubble, bubble_width); + lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); + + // Set alignment and style based on message role + if (strcmp(role, "user") == 0) { + // User messages are right-aligned with green background + lv_obj_set_style_bg_color(msg_bubble, current_theme.user_bubble, 0); + // Set text color for contrast + lv_obj_set_style_text_color(msg_text, current_theme.text, 0); + + // 设置自定义属性标记气泡类型 + lv_obj_set_user_data(msg_bubble, (void*)"user"); + + // Set appropriate width for content + lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT); + lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); + + // Don't grow + lv_obj_set_style_flex_grow(msg_bubble, 0, 0); + } else if (strcmp(role, "assistant") == 0) { + // Assistant messages are left-aligned with white background + lv_obj_set_style_bg_color(msg_bubble, current_theme.assistant_bubble, 0); + // Set text color for contrast + lv_obj_set_style_text_color(msg_text, current_theme.text, 0); + + // 设置自定义属性标记气泡类型 + lv_obj_set_user_data(msg_bubble, (void*)"assistant"); + + // Set appropriate width for content + lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT); + lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); + + // Don't grow + lv_obj_set_style_flex_grow(msg_bubble, 0, 0); + } else if (strcmp(role, "system") == 0) { + // System messages are center-aligned with light gray background + lv_obj_set_style_bg_color(msg_bubble, current_theme.system_bubble, 0); + // Set text color for contrast + lv_obj_set_style_text_color(msg_text, current_theme.system_text, 0); + + // 设置自定义属性标记气泡类型 + lv_obj_set_user_data(msg_bubble, (void*)"system"); + + // Set appropriate width for content + lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT); + lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); + + // Don't grow + lv_obj_set_style_flex_grow(msg_bubble, 0, 0); + } + + // Create a full-width container for user messages to ensure right alignment + if (strcmp(role, "user") == 0) { + // Create a full-width container + lv_obj_t* container = lv_obj_create(content_); + lv_obj_set_width(container, LV_HOR_RES); + lv_obj_set_height(container, LV_SIZE_CONTENT); + + // Make container transparent and borderless + lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(container, 0, 0); + lv_obj_set_style_pad_all(container, 0, 0); + + // Move the message bubble into this container + lv_obj_set_parent(msg_bubble, container); + + // Right align the bubble in the container + lv_obj_align(msg_bubble, LV_ALIGN_RIGHT_MID, -25, 0); + + // Auto-scroll to this container + lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON); + } else if (strcmp(role, "system") == 0) { + // 为系统消息创建全宽容器以确保居中对齐 + lv_obj_t* container = lv_obj_create(content_); + lv_obj_set_width(container, LV_HOR_RES); + lv_obj_set_height(container, LV_SIZE_CONTENT); + + // 使容器透明且无边框 + lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(container, 0, 0); + lv_obj_set_style_pad_all(container, 0, 0); + + // 将消息气泡移入此容器 + lv_obj_set_parent(msg_bubble, container); + + // 将气泡居中对齐在容器中 + lv_obj_align(msg_bubble, LV_ALIGN_CENTER, 0, 0); + + // 自动滚动底部 + lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON); + } else { + // For assistant messages + // Left align assistant messages + lv_obj_align(msg_bubble, LV_ALIGN_LEFT_MID, 0, 0); + + // Auto-scroll to the message bubble + lv_obj_scroll_to_view_recursive(msg_bubble, LV_ANIM_ON); + } + + // Store reference to the latest message label + chat_message_label_ = msg_text; +} +#else +void LcdDisplay::SetupUI() { + DisplayLockGuard lock(this); + + auto screen = lv_screen_active(); + lv_obj_set_style_text_font(screen, fonts_.text_font, 0); + lv_obj_set_style_text_color(screen, current_theme.text, 0); + lv_obj_set_style_bg_color(screen, current_theme.background, 0); + + /* Container */ + container_ = lv_obj_create(screen); + lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES); + lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(container_, 0, 0); + lv_obj_set_style_border_width(container_, 0, 0); + lv_obj_set_style_pad_row(container_, 0, 0); + lv_obj_set_style_bg_color(container_, current_theme.background, 0); + lv_obj_set_style_border_color(container_, current_theme.border, 0); + + /* Status bar */ + status_bar_ = lv_obj_create(container_); + lv_obj_set_size(status_bar_, LV_HOR_RES, fonts_.text_font->line_height); + lv_obj_set_style_radius(status_bar_, 0, 0); + lv_obj_set_style_bg_color(status_bar_, current_theme.background, 0); + lv_obj_set_style_text_color(status_bar_, current_theme.text, 0); + + /* Content */ + content_ = lv_obj_create(container_); + lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_radius(content_, 0, 0); + lv_obj_set_width(content_, LV_HOR_RES); + lv_obj_set_flex_grow(content_, 1); + lv_obj_set_style_pad_all(content_, 5, 0); + lv_obj_set_style_bg_color(content_, current_theme.chat_background, 0); + lv_obj_set_style_border_color(content_, current_theme.border, 0); // Border color for content + + lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); // 垂直布局(从上到下) + lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); // 子对象居中对齐,等距分布 + + emotion_label_ = lv_label_create(content_); + lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_4, 0); + lv_obj_set_style_text_color(emotion_label_, current_theme.text, 0); + lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP); + + chat_message_label_ = lv_label_create(content_); + lv_label_set_text(chat_message_label_, ""); + lv_obj_set_width(chat_message_label_, LV_HOR_RES * 0.9); // 限制宽度为屏幕宽度的 90% + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // 设置为自动换行模式 + lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐 + lv_obj_set_style_text_color(chat_message_label_, current_theme.text, 0); + + /* Status bar */ + lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(status_bar_, 0, 0); + lv_obj_set_style_border_width(status_bar_, 0, 0); + lv_obj_set_style_pad_column(status_bar_, 0, 0); + lv_obj_set_style_pad_left(status_bar_, 2, 0); + lv_obj_set_style_pad_right(status_bar_, 2, 0); + + network_label_ = lv_label_create(status_bar_); + lv_label_set_text(network_label_, ""); + lv_obj_set_style_text_font(network_label_, fonts_.icon_font, 0); + lv_obj_set_style_text_color(network_label_, current_theme.text, 0); + + notification_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(notification_label_, 1); + lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(notification_label_, current_theme.text, 0); + lv_label_set_text(notification_label_, ""); + lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); + + status_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(status_label_, 1); + lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(status_label_, current_theme.text, 0); + lv_label_set_text(status_label_, Lang::Strings::INITIALIZING); + mute_label_ = lv_label_create(status_bar_); + lv_label_set_text(mute_label_, ""); + lv_obj_set_style_text_font(mute_label_, fonts_.icon_font, 0); + lv_obj_set_style_text_color(mute_label_, current_theme.text, 0); + + battery_label_ = lv_label_create(status_bar_); + lv_label_set_text(battery_label_, ""); + lv_obj_set_style_text_font(battery_label_, fonts_.icon_font, 0); + lv_obj_set_style_text_color(battery_label_, current_theme.text, 0); + + low_battery_popup_ = lv_obj_create(screen); + lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, fonts_.text_font->line_height * 2); + lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_set_style_bg_color(low_battery_popup_, current_theme.low_battery, 0); + lv_obj_set_style_radius(low_battery_popup_, 10, 0); + lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_); + lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE); + lv_obj_set_style_text_color(low_battery_label, lv_color_white(), 0); + lv_obj_center(low_battery_label); + lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); +} +#endif + +void LcdDisplay::SetEmotion(const char* emotion) { + struct Emotion { + const char* icon; + const char* text; + }; + + static const std::vector emotions = { + {"😶", "neutral"}, + {"🙂", "happy"}, + {"😆", "laughing"}, + {"😂", "funny"}, + {"😔", "sad"}, + {"😠", "angry"}, + {"😭", "crying"}, + {"😍", "loving"}, + {"😳", "embarrassed"}, + {"😯", "surprised"}, + {"😱", "shocked"}, + {"🤔", "thinking"}, + {"😉", "winking"}, + {"😎", "cool"}, + {"😌", "relaxed"}, + {"🤤", "delicious"}, + {"😘", "kissy"}, + {"😏", "confident"}, + {"😴", "sleepy"}, + {"😜", "silly"}, + {"🙄", "confused"} + }; + + // 查找匹配的表情 + std::string_view emotion_view(emotion); + auto it = std::find_if(emotions.begin(), emotions.end(), + [&emotion_view](const Emotion& e) { return e.text == emotion_view; }); + + DisplayLockGuard lock(this); + if (emotion_label_ == nullptr) { + return; + } + + // 如果找到匹配的表情就显示对应图标,否则显示默认的neutral表情 + lv_obj_set_style_text_font(emotion_label_, fonts_.emoji_font, 0); + if (it != emotions.end()) { + lv_label_set_text(emotion_label_, it->icon); + } else { + lv_label_set_text(emotion_label_, "😶"); + } +} + +void LcdDisplay::SetIcon(const char* icon) { + DisplayLockGuard lock(this); + if (emotion_label_ == nullptr) { + return; + } + lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_4, 0); + lv_label_set_text(emotion_label_, icon); +} + +void LcdDisplay::SetTheme(const std::string& theme_name) { + DisplayLockGuard lock(this); + + if (theme_name == "dark" || theme_name == "DARK") { + current_theme = DARK_THEME; + } else if (theme_name == "light" || theme_name == "LIGHT") { + current_theme = LIGHT_THEME; + } else { + // Invalid theme name, return false + ESP_LOGE(TAG, "Invalid theme name: %s", theme_name.c_str()); + return; + } + + // Get the active screen + lv_obj_t* screen = lv_screen_active(); + + // Update the screen colors + lv_obj_set_style_bg_color(screen, current_theme.background, 0); + lv_obj_set_style_text_color(screen, current_theme.text, 0); + + // Update container colors + if (container_ != nullptr) { + lv_obj_set_style_bg_color(container_, current_theme.background, 0); + lv_obj_set_style_border_color(container_, current_theme.border, 0); + } + + // Update status bar colors + if (status_bar_ != nullptr) { + lv_obj_set_style_bg_color(status_bar_, current_theme.background, 0); + lv_obj_set_style_text_color(status_bar_, current_theme.text, 0); + + // Update status bar elements + if (network_label_ != nullptr) { + lv_obj_set_style_text_color(network_label_, current_theme.text, 0); + } + if (status_label_ != nullptr) { + lv_obj_set_style_text_color(status_label_, current_theme.text, 0); + } + if (notification_label_ != nullptr) { + lv_obj_set_style_text_color(notification_label_, current_theme.text, 0); + } + if (mute_label_ != nullptr) { + lv_obj_set_style_text_color(mute_label_, current_theme.text, 0); + } + if (battery_label_ != nullptr) { + lv_obj_set_style_text_color(battery_label_, current_theme.text, 0); + } + if (emotion_label_ != nullptr) { + lv_obj_set_style_text_color(emotion_label_, current_theme.text, 0); + } + } + + // Update content area colors + if (content_ != nullptr) { + lv_obj_set_style_bg_color(content_, current_theme.chat_background, 0); + lv_obj_set_style_border_color(content_, current_theme.border, 0); + + // If we have the chat message style, update all message bubbles +#if CONFIG_USE_WECHAT_MESSAGE_STYLE + // Iterate through all children of content (message containers or bubbles) + uint32_t child_count = lv_obj_get_child_cnt(content_); + for (uint32_t i = 0; i < child_count; i++) { + lv_obj_t* obj = lv_obj_get_child(content_, i); + if (obj == nullptr) continue; + + lv_obj_t* bubble = nullptr; + + // 检查这个对象是容器还是气泡 + // 如果是容器(用户或系统消息),则获取其子对象作为气泡 + // 如果是气泡(助手消息),则直接使用 + if (lv_obj_get_child_cnt(obj) > 0) { + // 可能是容器,检查它是否为用户或系统消息容器 + // 用户和系统消息容器是透明的 + lv_opa_t bg_opa = lv_obj_get_style_bg_opa(obj, 0); + if (bg_opa == LV_OPA_TRANSP) { + // 这是用户或系统消息的容器 + bubble = lv_obj_get_child(obj, 0); + } else { + // 这可能是助手消息的气泡自身 + bubble = obj; + } + } else { + // 没有子元素,可能是其他UI元素,跳过 + continue; + } + + if (bubble == nullptr) continue; + + // 使用保存的用户数据来识别气泡类型 + void* bubble_type_ptr = lv_obj_get_user_data(bubble); + if (bubble_type_ptr != nullptr) { + const char* bubble_type = static_cast(bubble_type_ptr); + + // 根据气泡类型应用正确的颜色 + if (strcmp(bubble_type, "user") == 0) { + lv_obj_set_style_bg_color(bubble, current_theme.user_bubble, 0); + } else if (strcmp(bubble_type, "assistant") == 0) { + lv_obj_set_style_bg_color(bubble, current_theme.assistant_bubble, 0); + } else if (strcmp(bubble_type, "system") == 0) { + lv_obj_set_style_bg_color(bubble, current_theme.system_bubble, 0); + } + + // Update border color + lv_obj_set_style_border_color(bubble, current_theme.border, 0); + + // Update text color for the message + if (lv_obj_get_child_cnt(bubble) > 0) { + lv_obj_t* text = lv_obj_get_child(bubble, 0); + if (text != nullptr) { + // 根据气泡类型设置文本颜色 + if (strcmp(bubble_type, "system") == 0) { + lv_obj_set_style_text_color(text, current_theme.system_text, 0); + } else { + lv_obj_set_style_text_color(text, current_theme.text, 0); + } + } + } + } else { + // 如果没有标记,回退到之前的逻辑(颜色比较) + // ...保留原有的回退逻辑... + lv_color_t bg_color = lv_obj_get_style_bg_color(bubble, 0); + + // 改进bubble类型检测逻辑,不仅使用颜色比较 + bool is_user_bubble = false; + bool is_assistant_bubble = false; + bool is_system_bubble = false; + + // 检查用户bubble + if (lv_color_eq(bg_color, DARK_USER_BUBBLE_COLOR) || + lv_color_eq(bg_color, LIGHT_USER_BUBBLE_COLOR) || + lv_color_eq(bg_color, current_theme.user_bubble)) { + is_user_bubble = true; + } + // 检查系统bubble + else if (lv_color_eq(bg_color, DARK_SYSTEM_BUBBLE_COLOR) || + lv_color_eq(bg_color, LIGHT_SYSTEM_BUBBLE_COLOR) || + lv_color_eq(bg_color, current_theme.system_bubble)) { + is_system_bubble = true; + } + // 剩余的都当作助手bubble处理 + else { + is_assistant_bubble = true; + } + + // 根据bubble类型应用正确的颜色 + if (is_user_bubble) { + lv_obj_set_style_bg_color(bubble, current_theme.user_bubble, 0); + } else if (is_assistant_bubble) { + lv_obj_set_style_bg_color(bubble, current_theme.assistant_bubble, 0); + } else if (is_system_bubble) { + lv_obj_set_style_bg_color(bubble, current_theme.system_bubble, 0); + } + + // Update border color + lv_obj_set_style_border_color(bubble, current_theme.border, 0); + + // Update text color for the message + if (lv_obj_get_child_cnt(bubble) > 0) { + lv_obj_t* text = lv_obj_get_child(bubble, 0); + if (text != nullptr) { + // 回退到颜色检测逻辑 + if (lv_color_eq(bg_color, current_theme.system_bubble) || + lv_color_eq(bg_color, DARK_SYSTEM_BUBBLE_COLOR) || + lv_color_eq(bg_color, LIGHT_SYSTEM_BUBBLE_COLOR)) { + lv_obj_set_style_text_color(text, current_theme.system_text, 0); + } else { + lv_obj_set_style_text_color(text, current_theme.text, 0); + } + } + } + } + } +#else + // Simple UI mode - just update the main chat message + if (chat_message_label_ != nullptr) { + lv_obj_set_style_text_color(chat_message_label_, current_theme.text, 0); + } + + if (emotion_label_ != nullptr) { + lv_obj_set_style_text_color(emotion_label_, current_theme.text, 0); + } +#endif + } + + // Update low battery popup + if (low_battery_popup_ != nullptr) { + lv_obj_set_style_bg_color(low_battery_popup_, current_theme.low_battery, 0); + } + + // No errors occurred. Save theme to settings + Display::SetTheme(theme_name); +} diff --git a/main/display/lcd_display.h b/main/display/lcd_display.h new file mode 100644 index 0000000..e721e7c --- /dev/null +++ b/main/display/lcd_display.h @@ -0,0 +1,90 @@ +#ifndef LCD_DISPLAY_H +#define LCD_DISPLAY_H + +#include "display.h" + +#include +#include +#include + +#include + +class LcdDisplay : public Display { +protected: + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + lv_draw_buf_t draw_buf_; + lv_obj_t* status_bar_ = nullptr; + lv_obj_t* content_ = nullptr; + lv_obj_t* container_ = nullptr; + lv_obj_t* side_bar_ = nullptr; + + DisplayFonts fonts_; + + void SetupUI(); + virtual bool Lock(int timeout_ms = 0) override; + virtual void Unlock() override; + +protected: + // 添加protected构造函数 + LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, DisplayFonts fonts) + : panel_io_(panel_io), panel_(panel), fonts_(fonts) {} + +public: + ~LcdDisplay(); + virtual void SetEmotion(const char* emotion) override; + virtual void SetIcon(const char* icon) override; +#if CONFIG_USE_WECHAT_MESSAGE_STYLE + virtual void SetChatMessage(const char* role, const char* content) override; +#endif + + // Add theme switching function + virtual void SetTheme(const std::string& theme_name) override; +}; + +// RGB LCD显示器 +class RgbLcdDisplay : public LcdDisplay { +public: + RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, + bool mirror_x, bool mirror_y, bool swap_xy, + DisplayFonts fonts); +}; + +// MIPI LCD显示器 +class MipiLcdDisplay : public LcdDisplay { +public: + MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, + bool mirror_x, bool mirror_y, bool swap_xy, + DisplayFonts fonts); +}; + +// // SPI LCD显示器 +class SpiLcdDisplay : public LcdDisplay { +public: + SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, + bool mirror_x, bool mirror_y, bool swap_xy, + DisplayFonts fonts); +}; + +// QSPI LCD显示器 +class QspiLcdDisplay : public LcdDisplay { +public: + QspiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, + bool mirror_x, bool mirror_y, bool swap_xy, + DisplayFonts fonts); +}; + +// MCU8080 LCD显示器 +class Mcu8080LcdDisplay : public LcdDisplay { +public: + Mcu8080LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, int offset_x, int offset_y, + bool mirror_x, bool mirror_y, bool swap_xy, + DisplayFonts fonts); +}; +#endif // LCD_DISPLAY_H diff --git a/main/display/oled_display.cc b/main/display/oled_display.cc new file mode 100644 index 0000000..3b1b905 --- /dev/null +++ b/main/display/oled_display.cc @@ -0,0 +1,309 @@ +#include "oled_display.h" +#include "font_awesome_symbols.h" +#include "assets/lang_config.h" + +#include +#include + +#include +#include +#include + +#define TAG "OledDisplay" + +LV_FONT_DECLARE(font_awesome_30_1); + +OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, + int width, int height, bool mirror_x, bool mirror_y, DisplayFonts fonts) + : panel_io_(panel_io), panel_(panel), fonts_(fonts) { + width_ = width; + height_ = height; + + ESP_LOGI(TAG, "Initialize LVGL"); + lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG(); + port_cfg.task_priority = 1; + port_cfg.timer_period_ms = 50; + lvgl_port_init(&port_cfg); + + ESP_LOGI(TAG, "Adding LCD screen"); + const lvgl_port_display_cfg_t display_cfg = { + .io_handle = panel_io_, + .panel_handle = panel_, + .control_handle = nullptr, + .buffer_size = static_cast(width_ * height_), + .double_buffer = false, + .trans_size = 0, + .hres = static_cast(width_), + .vres = static_cast(height_), + .monochrome = true, + .rotation = { + .swap_xy = false, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + }, + .flags = { + .buff_dma = 1, + .buff_spiram = 0, + .sw_rotate = 0, + .full_refresh = 0, + .direct_mode = 0, + }, + }; + + display_ = lvgl_port_add_disp(&display_cfg); + if (display_ == nullptr) { + ESP_LOGE(TAG, "Failed to add display"); + return; + } + + if (height_ == 64) { + SetupUI_128x64(); + } else { + SetupUI_128x32(); + } +} + +OledDisplay::~OledDisplay() { + if (content_ != nullptr) { + lv_obj_del(content_); + } + if (status_bar_ != nullptr) { + lv_obj_del(status_bar_); + } + if (side_bar_ != nullptr) { + lv_obj_del(side_bar_); + } + if (container_ != nullptr) { + lv_obj_del(container_); + } + + if (panel_ != nullptr) { + esp_lcd_panel_del(panel_); + } + if (panel_io_ != nullptr) { + esp_lcd_panel_io_del(panel_io_); + } + lvgl_port_deinit(); +} + +bool OledDisplay::Lock(int timeout_ms) { + return lvgl_port_lock(timeout_ms); +} + +void OledDisplay::Unlock() { + lvgl_port_unlock(); +} + +void OledDisplay::SetChatMessage(const char* role, const char* content) { + DisplayLockGuard lock(this); + if (chat_message_label_ == nullptr) { + return; + } + + // Replace all newlines with spaces + std::string content_str = content; + std::replace(content_str.begin(), content_str.end(), '\n', ' '); + + if (content_right_ == nullptr) { + lv_label_set_text(chat_message_label_, content_str.c_str()); + } else { + if (content == nullptr || content[0] == '\0') { + lv_obj_add_flag(content_right_, LV_OBJ_FLAG_HIDDEN); + } else { + lv_label_set_text(chat_message_label_, content_str.c_str()); + lv_obj_clear_flag(content_right_, LV_OBJ_FLAG_HIDDEN); + } + } +} + +void OledDisplay::SetupUI_128x64() { + DisplayLockGuard lock(this); + + auto screen = lv_screen_active(); + lv_obj_set_style_text_font(screen, fonts_.text_font, 0); + lv_obj_set_style_text_color(screen, lv_color_black(), 0); + + /* Container */ + container_ = lv_obj_create(screen); + lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES); + lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(container_, 0, 0); + lv_obj_set_style_border_width(container_, 0, 0); + lv_obj_set_style_pad_row(container_, 0, 0); + + /* Status bar */ + status_bar_ = lv_obj_create(container_); + lv_obj_set_size(status_bar_, LV_HOR_RES, 16); + lv_obj_set_style_border_width(status_bar_, 0, 0); + lv_obj_set_style_pad_all(status_bar_, 0, 0); + lv_obj_set_style_radius(status_bar_, 0, 0); + + /* Content */ + content_ = lv_obj_create(container_); + lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_radius(content_, 0, 0); + lv_obj_set_style_pad_all(content_, 0, 0); + lv_obj_set_width(content_, LV_HOR_RES); + lv_obj_set_flex_grow(content_, 1); + lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_ROW); + lv_obj_set_style_flex_main_place(content_, LV_FLEX_ALIGN_CENTER, 0); + + // 创建左侧固定宽度的容器 + content_left_ = lv_obj_create(content_); + lv_obj_set_size(content_left_, 32, LV_SIZE_CONTENT); // 固定宽度32像素 + lv_obj_set_style_pad_all(content_left_, 0, 0); + lv_obj_set_style_border_width(content_left_, 0, 0); + + emotion_label_ = lv_label_create(content_left_); + lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0); + lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP); + lv_obj_center(emotion_label_); + lv_obj_set_style_pad_top(emotion_label_, 8, 0); + + // 创建右侧可扩展的容器 + content_right_ = lv_obj_create(content_); + lv_obj_set_size(content_right_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(content_right_, 0, 0); + lv_obj_set_style_border_width(content_right_, 0, 0); + lv_obj_set_flex_grow(content_right_, 1); + lv_obj_add_flag(content_right_, LV_OBJ_FLAG_HIDDEN); + + chat_message_label_ = lv_label_create(content_right_); + lv_label_set_text(chat_message_label_, ""); + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_LEFT, 0); + lv_obj_set_width(chat_message_label_, width_ - 32); + lv_obj_set_style_pad_top(chat_message_label_, 14, 0); + + // 延迟一定的时间后开始滚动字幕 + static lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_delay(&a, 1000); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN); + lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN); + + /* Status bar */ + lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(status_bar_, 0, 0); + lv_obj_set_style_border_width(status_bar_, 0, 0); + lv_obj_set_style_pad_column(status_bar_, 0, 0); + + network_label_ = lv_label_create(status_bar_); + lv_label_set_text(network_label_, ""); + lv_obj_set_style_text_font(network_label_, fonts_.icon_font, 0); + + notification_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(notification_label_, 1); + lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(notification_label_, ""); + lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); + + status_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(status_label_, 1); + lv_label_set_text(status_label_, Lang::Strings::INITIALIZING); + lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0); + + mute_label_ = lv_label_create(status_bar_); + lv_label_set_text(mute_label_, ""); + lv_obj_set_style_text_font(mute_label_, fonts_.icon_font, 0); + + battery_label_ = lv_label_create(status_bar_); + lv_label_set_text(battery_label_, ""); + lv_obj_set_style_text_font(battery_label_, fonts_.icon_font, 0); + + low_battery_popup_ = lv_obj_create(screen); + lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, fonts_.text_font->line_height * 2); + lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_set_style_bg_color(low_battery_popup_, lv_color_black(), 0); + lv_obj_set_style_radius(low_battery_popup_, 10, 0); + lv_obj_t* low_battery_label = lv_label_create(low_battery_popup_); + lv_label_set_text(low_battery_label, Lang::Strings::BATTERY_NEED_CHARGE); + lv_obj_set_style_text_color(low_battery_label, lv_color_white(), 0); + lv_obj_center(low_battery_label); + lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); +} + +void OledDisplay::SetupUI_128x32() { + DisplayLockGuard lock(this); + + auto screen = lv_screen_active(); + lv_obj_set_style_text_font(screen, fonts_.text_font, 0); + + /* Container */ + container_ = lv_obj_create(screen); + lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES); + lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(container_, 0, 0); + lv_obj_set_style_border_width(container_, 0, 0); + lv_obj_set_style_pad_column(container_, 0, 0); + + /* Emotion label on the left side */ + content_ = lv_obj_create(container_); + lv_obj_set_size(content_, 32, 32); + lv_obj_set_style_pad_all(content_, 0, 0); + lv_obj_set_style_border_width(content_, 0, 0); + lv_obj_set_style_radius(content_, 0, 0); + + emotion_label_ = lv_label_create(content_); + lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0); + lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP); + lv_obj_center(emotion_label_); + + /* Right side */ + side_bar_ = lv_obj_create(container_); + lv_obj_set_size(side_bar_, width_ - 32, 32); + lv_obj_set_flex_flow(side_bar_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(side_bar_, 0, 0); + lv_obj_set_style_border_width(side_bar_, 0, 0); + lv_obj_set_style_radius(side_bar_, 0, 0); + lv_obj_set_style_pad_row(side_bar_, 0, 0); + + /* Status bar */ + status_bar_ = lv_obj_create(side_bar_); + lv_obj_set_size(status_bar_, width_ - 32, 16); + lv_obj_set_style_radius(status_bar_, 0, 0); + lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(status_bar_, 0, 0); + lv_obj_set_style_border_width(status_bar_, 0, 0); + lv_obj_set_style_pad_column(status_bar_, 0, 0); + + status_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(status_label_, 1); + lv_obj_set_style_pad_left(status_label_, 2, 0); + lv_label_set_text(status_label_, Lang::Strings::INITIALIZING); + + notification_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(notification_label_, 1); + lv_obj_set_style_pad_left(notification_label_, 2, 0); + lv_label_set_text(notification_label_, ""); + lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); + + mute_label_ = lv_label_create(status_bar_); + lv_label_set_text(mute_label_, ""); + lv_obj_set_style_text_font(mute_label_, fonts_.icon_font, 0); + + network_label_ = lv_label_create(status_bar_); + lv_label_set_text(network_label_, ""); + lv_obj_set_style_text_font(network_label_, fonts_.icon_font, 0); + + battery_label_ = lv_label_create(status_bar_); + lv_label_set_text(battery_label_, ""); + lv_obj_set_style_text_font(battery_label_, fonts_.icon_font, 0); + + chat_message_label_ = lv_label_create(side_bar_); + lv_obj_set_size(chat_message_label_, width_ - 32, LV_SIZE_CONTENT); + lv_obj_set_style_pad_left(chat_message_label_, 2, 0); + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_text(chat_message_label_, ""); + + // 延迟一定的时间后开始滚动字幕 + static lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_delay(&a, 1000); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN); + lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN); +} + diff --git a/main/display/oled_display.h b/main/display/oled_display.h new file mode 100644 index 0000000..f605372 --- /dev/null +++ b/main/display/oled_display.h @@ -0,0 +1,37 @@ +#ifndef OLED_DISPLAY_H +#define OLED_DISPLAY_H + +#include "display.h" + +#include +#include + +class OledDisplay : public Display { +private: + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + + lv_obj_t* status_bar_ = nullptr; + lv_obj_t* content_ = nullptr; + lv_obj_t* content_left_ = nullptr; + lv_obj_t* content_right_ = nullptr; + lv_obj_t* container_ = nullptr; + lv_obj_t* side_bar_ = nullptr; + + DisplayFonts fonts_; + + virtual bool Lock(int timeout_ms = 0) override; + virtual void Unlock() override; + + void SetupUI_128x64(); + void SetupUI_128x32(); + +public: + OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, bool mirror_x, bool mirror_y, + DisplayFonts fonts); + ~OledDisplay(); + + virtual void SetChatMessage(const char* role, const char* content) override; +}; + +#endif // OLED_DISPLAY_H diff --git a/main/font_awesome_symbols.h b/main/font_awesome_symbols.h new file mode 100644 index 0000000..b301bbe --- /dev/null +++ b/main/font_awesome_symbols.h @@ -0,0 +1,74 @@ +#ifndef FONT_AWESOME_SYMBOLS_H +#define FONT_AWESOME_SYMBOLS_H + +// 空的字体符号定义 - 用于无显示器的板子 +// 所有符号都定义为空字符串,避免编译错误 + +#define FONT_AWESOME_PLAY "" +#define FONT_AWESOME_PAUSE "" +#define FONT_AWESOME_STOP "" +#define FONT_AWESOME_VOLUME_UP "" +#define FONT_AWESOME_VOLUME_DOWN "" +#define FONT_AWESOME_VOLUME_MUTE "" +#define FONT_AWESOME_WIFI "" +#define FONT_AWESOME_BATTERY "" +#define FONT_AWESOME_BLUETOOTH "" +#define FONT_AWESOME_MICROPHONE "" +#define FONT_AWESOME_SPEAKER "" +#define FONT_AWESOME_SETTINGS "" +#define FONT_AWESOME_INFO "" +#define FONT_AWESOME_WARNING "" +#define FONT_AWESOME_ERROR "" +#define FONT_AWESOME_SUCCESS "" +#define FONT_AWESOME_HEART "" +#define FONT_AWESOME_STAR "" +#define FONT_AWESOME_HOME "" +#define FONT_AWESOME_MENU "" +#define FONT_AWESOME_BACK "" +#define FONT_AWESOME_FORWARD "" +#define FONT_AWESOME_UP "" +#define FONT_AWESOME_DOWN "" +#define FONT_AWESOME_LEFT "" +#define FONT_AWESOME_RIGHT "" +#define FONT_AWESOME_POWER "" +#define FONT_AWESOME_REFRESH "" +#define FONT_AWESOME_SEARCH "" +#define FONT_AWESOME_CLOSE "" +#define FONT_AWESOME_CHECK "" +#define FONT_AWESOME_PLUS "" +#define FONT_AWESOME_MINUS "" +#define FONT_AWESOME_EDIT "" +#define FONT_AWESOME_DELETE "" +#define FONT_AWESOME_SAVE "" +#define FONT_AWESOME_LOAD "" +#define FONT_AWESOME_UPLOAD "" +#define FONT_AWESOME_DOWNLOAD "" +#define FONT_AWESOME_CLOUD "" +#define FONT_AWESOME_LOCK "" +#define FONT_AWESOME_UNLOCK "" +#define FONT_AWESOME_KEY "" +#define FONT_AWESOME_USER "" +#define FONT_AWESOME_USERS "" +#define FONT_AWESOME_MAIL "" +#define FONT_AWESOME_PHONE "" +#define FONT_AWESOME_CHAT "" +#define FONT_AWESOME_MESSAGE "" +#define FONT_AWESOME_NOTIFICATION "" +#define FONT_AWESOME_ALARM "" +#define FONT_AWESOME_CALENDAR "" +#define FONT_AWESOME_CLOCK "" +#define FONT_AWESOME_TIMER "" +#define FONT_AWESOME_STOPWATCH "" + +// 添加缺失的符号定义 +#define FONT_AWESOME_WIFI_OFF "" +#define FONT_AWESOME_WIFI_FAIR "" +#define FONT_AWESOME_WIFI_WEAK "" +#define FONT_AWESOME_SIGNAL_OFF "" +#define FONT_AWESOME_SIGNAL_1 "" +#define FONT_AWESOME_SIGNAL_2 "" +#define FONT_AWESOME_SIGNAL_3 "" +#define FONT_AWESOME_SIGNAL_4 "" +#define FONT_AWESOME_AI_CHIP "" + +#endif // FONT_AWESOME_SYMBOLS_H \ No newline at end of file diff --git a/main/font_emoji.h b/main/font_emoji.h new file mode 100644 index 0000000..835207a --- /dev/null +++ b/main/font_emoji.h @@ -0,0 +1,18 @@ +#ifndef FONT_EMOJI_H +#define FONT_EMOJI_H + +// 空的表情符号定义 - 用于无显示器的板子 +// 所有符号都定义为空字符串,避免编译错误 + +#define EMOJI_SMILE "" +#define EMOJI_SAD "" +#define EMOJI_HAPPY "" +#define EMOJI_ANGRY "" +#define EMOJI_SURPRISE "" +#define EMOJI_LOVE "" +#define EMOJI_THINKING "" +#define EMOJI_SLEEPING "" +#define EMOJI_WINK "" +#define EMOJI_COOL "" + +#endif // FONT_EMOJI_H \ No newline at end of file diff --git a/main/get_weather.py b/main/get_weather.py new file mode 100644 index 0000000..2d892ab --- /dev/null +++ b/main/get_weather.py @@ -0,0 +1,425 @@ +import requests +import json +import base64 +import time +from bs4 import BeautifulSoup +from config.logger import setup_logging +from plugins_func.register import register_function, ToolType, ActionResponse, Action +from core.utils.util import get_ip_info +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519 + +TAG = __name__ +logger = setup_logging() + +GET_WEATHER_FUNCTION_DESC = { + "type": "function", + "function": { + "name": "get_weather", + "description": ( + "获取某个地点的天气,用户应提供一个位置,比如用户说杭州天气,参数为:杭州。" + "如果用户说的是省份,默认用省会城市。如果用户说的不是省份或城市而是一个地名,默认用该地所在省份的省会城市。" + "如果用户没有指明地点,说“天气怎么样”,”今天天气如何“,location参数为空" + ), + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "地点名,例如杭州。可选参数,如果不提供则不传", + }, + "lang": { + "type": "string", + "description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN", + }, + }, + "required": ["lang"], + }, + }, +} + +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36" + ) +} + +# 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons +WEATHER_CODE_MAP = { + "100": "晴", + "101": "多云", + "102": "少云", + "103": "晴间多云", + "104": "阴", + "150": "晴", + "151": "多云", + "152": "少云", + "153": "晴间多云", + "300": "阵雨", + "301": "强阵雨", + "302": "雷阵雨", + "303": "强雷阵雨", + "304": "雷阵雨伴有冰雹", + "305": "小雨", + "306": "中雨", + "307": "大雨", + "308": "极端降雨", + "309": "毛毛雨/细雨", + "310": "暴雨", + "311": "大暴雨", + "312": "特大暴雨", + "313": "冻雨", + "314": "小到中雨", + "315": "中到大雨", + "316": "大到暴雨", + "317": "暴雨到大暴雨", + "318": "大暴雨到特大暴雨", + "350": "阵雨", + "351": "强阵雨", + "399": "雨", + "400": "小雪", + "401": "中雪", + "402": "大雪", + "403": "暴雪", + "404": "雨夹雪", + "405": "雨雪天气", + "406": "阵雨夹雪", + "407": "阵雪", + "408": "小到中雪", + "409": "中到大雪", + "410": "大到暴雪", + "456": "阵雨夹雪", + "457": "阵雪", + "499": "雪", + "500": "薄雾", + "501": "雾", + "502": "霾", + "503": "扬沙", + "504": "浮尘", + "507": "沙尘暴", + "508": "强沙尘暴", + "509": "浓雾", + "510": "强浓雾", + "511": "中度霾", + "512": "重度霾", + "513": "严重霾", + "514": "大雾", + "515": "特强浓雾", + "900": "热", + "901": "冷", + "999": "未知", +} + +# Base64URL编码 +def base64url_encode(data): + """Base64URL encode without padding""" + return base64.urlsafe_b64encode(data).decode('utf-8').rstrip('=') + +# 生成JWT token +def generate_jwt_token(kid, project_id, private_key_pem): + """ + Generate JWT token for QWeather API authentication + + Args: + kid: Credential ID from QWeather console + project_id: Project ID from QWeather console + private_key_pem: Private key in PEM format + + Returns: + JWT token string + """ + # Header + header = { + "alg": "EdDSA", + "kid": kid + } + + # Payload + current_time = int(time.time()) + payload = { + "sub": project_id, + "iat": current_time - 30, # Set to 30 seconds before current time to prevent time errors + "exp": current_time + 3600 # Expire in 1 hour + } + + # Encode header and payload + header_encoded = base64url_encode(json.dumps(header, separators=(',', ':')).encode('utf-8')) + payload_encoded = base64url_encode(json.dumps(payload, separators=(',', ':')).encode('utf-8')) + + # Create signing input + signing_input = f"{header_encoded}.{payload_encoded}" + + # Load private key and sign + private_key = serialization.load_pem_private_key( + private_key_pem.encode('utf-8'), + password=None + ) + + signature = private_key.sign(signing_input.encode('utf-8')) + signature_encoded = base64url_encode(signature) + + # Return complete JWT + return f"{signing_input}.{signature_encoded}" + +# 获取城市信息GeoAPI +def fetch_city_info(location, api_key, api_host, kid=None, project_id=None, private_key=None): + # Use JWT authentication if JWT parameters are provided + if kid and project_id and private_key: + try: + logger.bind(tag=TAG).info(f"使用JWT认证,kid: {kid[:10]}..., project_id: {project_id[:10]}...") + jwt_token = generate_jwt_token(kid, project_id, private_key) + headers = HEADERS.copy() + headers['Authorization'] = f'Bearer {jwt_token}' + url = f"https://{api_host}/geo/v2/city/lookup?location={location}&lang=zh" + logger.bind(tag=TAG).info(f"JWT请求URL: {url}") + + response = requests.get(url, headers=headers) + logger.bind(tag=TAG).info(f"HTTP状态码: {response.status_code}") + + if response.status_code != 200: + logger.bind(tag=TAG).error(f"HTTP请求失败: {response.status_code} - {response.text}") + return None + + response_json = response.json() + logger.bind(tag=TAG).info(f"API响应: {response_json}") + + except Exception as e: + logger.bind(tag=TAG).error(f"JWT认证失败: {str(e)}") + # Fallback to API key authentication + logger.bind(tag=TAG).info("回退到API KEY认证") + url = f"https://{api_host}/geo/v2/city/lookup?key={api_key}&location={location}&lang=zh" + response = requests.get(url, headers=HEADERS) + if response.status_code != 200: + logger.bind(tag=TAG).error(f"API KEY认证也失败: {response.status_code} - {response.text}") + return None + response_json = response.json() + else: + # Fallback to API key authentication + logger.bind(tag=TAG).info(f"使用API KEY认证: {api_key}") + url = f"https://{api_host}/geo/v2/city/lookup?key={api_key}&location={location}&lang=zh" + response = requests.get(url, headers=HEADERS) + if response.status_code != 200: + logger.bind(tag=TAG).error(f"API KEY认证失败: {response.status_code} - {response.text}") + return None + response_json = response.json() + + if response_json.get("error") is not None: + error_detail = response_json.get('error', {}) + logger.bind(tag=TAG).error(f"API错误 - 状态: {error_detail.get('status')}, 详情: {error_detail.get('detail')}") + return None + + locations = response_json.get("location", []) + if not locations: + logger.bind(tag=TAG).warning(f"未找到位置信息: {location}") + return None + + return locations[0] + +# 获取天气页面 +def fetch_weather_page(url): + logger.bind(tag=TAG).info(f"正在获取天气页面: {url}") + response = requests.get(url, headers=HEADERS) + logger.bind(tag=TAG).info(f"页面请求状态码: {response.status_code}") + + if response.ok: + soup = BeautifulSoup(response.text, "html.parser") + logger.bind(tag=TAG).info(f"页面内容长度: {len(response.text)}") + logger.bind(tag=TAG).info(f"页面标题: {soup.title.string if soup.title else '无标题'}") + return soup + else: + logger.bind(tag=TAG).error(f"获取天气页面失败: {response.status_code}") + return None + +# 解析天气信息 +def parse_weather_info(soup): + try: + # 尝试解析城市名称 + city_element = soup.select_one("h1.c-submenu__location") + if city_element: + city_name = city_element.get_text(strip=True) + logger.bind(tag=TAG).info(f"成功解析城市名称: {city_name}") + else: + logger.bind(tag=TAG).warning("未找到城市名称元素,尝试其他选择器") + # 尝试其他可能的选择器 + alt_selectors = ["h1", ".location", ".city-name", "title"] + city_name = "未知城市" + for selector in alt_selectors: + element = soup.select_one(selector) + if element: + text = element.get_text(strip=True) + logger.bind(tag=TAG).info(f"使用选择器 {selector} 找到: {text}") + city_name = text + break + + # 尝试解析当前天气摘要 + current_abstract = soup.select_one(".c-city-weather-current .current-abstract") + if current_abstract: + current_abstract = current_abstract.get_text(strip=True) + logger.bind(tag=TAG).info(f"成功解析天气摘要: {current_abstract}") + else: + current_abstract = "未知" + logger.bind(tag=TAG).warning("未找到天气摘要元素") + + # 尝试解析详细参数 + current_basic = {} + basic_items = soup.select(".c-city-weather-current .current-basic .current-basic___item") + logger.bind(tag=TAG).info(f"找到 {len(basic_items)} 个基本信息元素") + + for item in basic_items: + parts = item.get_text(strip=True, separator=" ").split(" ") + if len(parts) == 2: + key, value = parts[1], parts[0] + current_basic[key] = value + logger.bind(tag=TAG).debug(f"解析基本信息: {key} = {value}") + + # 尝试解析7天预报 + temps_list = [] + forecast_rows = soup.select(".city-forecast-tabs__row") + logger.bind(tag=TAG).info(f"找到 {len(forecast_rows)} 个预报行") + + for i, row in enumerate(forecast_rows[:7]): + try: + date_element = row.select_one(".date-bg .date") + icon_element = row.select_one(".date-bg .icon") + temp_elements = row.select(".tmp-cont .temp") + + if date_element and icon_element and temp_elements: + date = date_element.get_text(strip=True) + weather_code = icon_element["src"].split("/")[-1].split(".")[0] + weather = WEATHER_CODE_MAP.get(weather_code, "未知") + temps = [span.get_text(strip=True) for span in temp_elements] + high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None) + temps_list.append((date, weather, high_temp, low_temp)) + logger.bind(tag=TAG).debug(f"解析第{i+1}天: {date} {weather} {high_temp}~{low_temp}") + else: + logger.bind(tag=TAG).warning(f"第{i+1}天预报元素不完整") + except Exception as e: + logger.bind(tag=TAG).warning(f"解析第{i+1}天预报失败: {e}") + + if not temps_list: + logger.bind(tag=TAG).error("未能解析任何预报数据,页面结构可能已更改") + # 打印页面结构用于调试 + logger.bind(tag=TAG).debug(f"页面主要结构: {[tag.name for tag in soup.find_all()[:20]]}") + + return city_name, current_abstract, current_basic, temps_list + + except Exception as e: + logger.bind(tag=TAG).error(f"解析天气信息时发生错误: {e}") + return "解析失败", "无法获取", {}, [] + + +@register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)# 获取天气 + +# @param location 城市名称,默认值为"北京" +# @param lang 语言,默认值为"zh_CN" +def get_weather(conn, location: str = None, lang: str = "zh_CN"): + from core.utils.cache.manager import cache_manager, CacheType + + # 硬编码配置参数 + api_host = "kq3aapg9h5.re.qweatherapi.com" + api_key = "aa5ec0859c144ac7b33966e25eef5580" + default_location = "北京" + kid = "T45F5GTR8Y" + project_id = "4N855TEVNN" + private_key = """-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIA26lz31HoaZV17EjIGcyo9YNGGQ77/gOZU8Chw8wlWq +-----END PRIVATE KEY-----""" + + # Debug JWT configuration + logger.bind(tag=TAG).info(f"使用硬编码JWT配置 - kid: {kid}, project_id: {project_id}") + logger.bind(tag=TAG).info(f"✅ JWT认证已配置,将使用JWT认证访问: {api_host}") + client_ip = conn.client_ip + + # 优先使用用户提供的location参数 + if not location: + # 通过客户端IP解析城市 + if client_ip: + # 先从缓存获取IP对应的城市信息 + cached_ip_info = cache_manager.get(CacheType.IP_INFO, client_ip) + if cached_ip_info: + location = cached_ip_info.get("city") + else: + # 缓存未命中,调用API获取 + ip_info = get_ip_info(client_ip, logger) + if ip_info: + cache_manager.set(CacheType.IP_INFO, client_ip, ip_info) + location = ip_info.get("city") + + if not location: + location = default_location + else: + # 若无IP,使用默认位置 + location = default_location + # 尝试从缓存获取完整天气报告 + weather_cache_key = f"full_weather_{location}_{lang}" + cached_weather_report = cache_manager.get(CacheType.WEATHER, weather_cache_key) + if cached_weather_report: + return ActionResponse(Action.REQLLM, cached_weather_report, None) + + # 缓存未命中,获取实时天气数据 + city_info = fetch_city_info(location, api_key, api_host, kid, project_id, private_key) + if not city_info: + return ActionResponse( + Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None + ) + soup = fetch_weather_page(city_info["fxLink"]) + if not soup: + return ActionResponse(Action.REQLLM, None, "请求失败") + city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup) + + weather_report = f"您查询的位置是:{city_name}\n\n当前天气: {current_abstract}\n" + + # 添加有效的当前天气参数 + if current_basic: + weather_report += "详细参数:\n" + for key, value in current_basic.items(): + if value != "0": # 过滤无效值 + weather_report += f" · {key}: {value}\n" + + # 添加7天预报 + weather_report += "\n未来7天预报:\n" + for date, weather, high, low in temps_list: + weather_report += f"{date}: {weather},气温 {low}~{high}\n" + + # 提示语 + weather_report += "\n(如需某一天的具体天气,请告诉我日期)"// 在adjust_audio_val处理逻辑后添加 + + +if (strcmp(name->valuestring, "get_weather") == 0) { + cJSON* location = cJSON_GetObjectItem(args_obj, "location"); + cJSON* lang = cJSON_GetObjectItem(args_obj, "lang"); + + // 设置默认值 + const char* location_str = (location && cJSON_IsString(location)) ? location->valuestring : "广州"; + const char* lang_str = (lang && cJSON_IsString(lang)) ? lang->valuestring : "zh_CN"; + + ESP_LOGI(TAG, "获取天气: location=%s, lang=%s", location_str, lang_str); + + // 创建异步任务处理天气获取 + Schedule([this, location_str, lang_str, call_id]() { + try { + // 调用天气API获取结果 + std::string weather_result = GetWeatherInfo(location_str, lang_str); + + if (!call_id || !call_id[0]) { + if (protocol_) { + protocol_->SendTextMessage(weather_result); + } + } else if (protocol_) { + protocol_->SendFunctionResult(call_id, weather_result); + } + } catch (const std::exception& e) { + ESP_LOGE(TAG, "天气获取异常: %s", e.what()); + std::string error_msg = "获取天气信息失败,请稍后重试"; + if (protocol_) { + protocol_->SendTextMessage(error_msg); + } + } + }); +} + + # 缓存完整的天气报告 + cache_manager.set(CacheType.WEATHER, weather_cache_key, weather_report) + + return ActionResponse(Action.REQLLM, weather_report, None) diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..721dfd4 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,14 @@ +## IDF Component Manager Manifest File +dependencies: + 78/esp-wifi-connect: "~2.3.1" + 78/esp-opus-encoder: "~2.3.0" + 78/esp-ml307: "~1.7.3" + espressif/led_strip: "^2.4.1" + espressif/esp_codec_dev: "~1.3.2" + espressif/esp-sr: "^2.0.3" + espressif/button: "^3.3.1" + espressif/knob: "^1.0.0" + ## Required IDF version + idf: + version: ">=5.3" + diff --git a/main/iot/README.md b/main/iot/README.md new file mode 100644 index 0000000..7bbfd52 --- /dev/null +++ b/main/iot/README.md @@ -0,0 +1,209 @@ +# 物联网控制模块 + +本模块实现了小智AI语音聊天机器人的物联网控制功能,使用户可以通过语音指令控制接入到ESP32开发板的各种物联网设备。 + +## 工作原理 + +整个物联网控制模块的工作流程如下: + +1. **设备注册**:在开发板初始化阶段(如在`compact_wifi_board.cc`中),各种物联网设备通过`ThingManager`注册到系统中 +2. **设备描述**:系统将设备描述信息(包括名称、属性、方法等)通过通信协议(如MQTT或WebSocket)发送给AI服务器 +3. **用户交互**:用户通过语音与小智AI对话,表达控制物联网设备的意图 +4. **命令执行**:AI服务器解析用户意图,生成控制命令,通过协议发送回ESP32,由`ThingManager`分发给对应的设备执行 +5. **状态更新**:设备执行命令后,状态变化会通过`ThingManager`收集并发送回AI服务器,保持状态同步 + +## 核心组件 + +### ThingManager + +`ThingManager`是物联网控制模块的核心管理类,采用单例模式实现: + +- `AddThing`:注册物联网设备 +- `GetDescriptorsJson`:获取所有设备的描述信息,用于向AI服务器报告设备能力 +- `GetStatesJson`:获取所有设备的当前状态,可以选择只返回变化的部分 +- `Invoke`:根据AI服务器下发的命令,调用对应设备的方法 + +### Thing + +`Thing`是所有物联网设备的基类,提供了以下核心功能: + +- 属性管理:通过`PropertyList`定义设备的可查询状态 +- 方法管理:通过`MethodList`定义设备可执行的操作 +- JSON序列化:将设备描述和状态转换为JSON格式,便于网络传输 +- 命令执行:解析和执行来自AI服务器的指令 + +## 设备设计示例 + +### 灯(Lamp) + +灯是一个简单的物联网设备示例,通过GPIO控制LED的开关状态: + +```cpp +class Lamp : public Thing { +private: + gpio_num_t gpio_num_ = GPIO_NUM_18; // GPIO引脚 + bool power_ = false; // 灯的开关状态 + +public: + Lamp() : Thing("Lamp", "一个测试用的灯") { + // 初始化GPIO + InitializeGpio(); + + // 定义属性:power(表示灯的开关状态) + properties_.AddBooleanProperty("power", "灯是否打开", [this]() -> bool { + return power_; + }); + + // 定义方法:TurnOn(打开灯) + methods_.AddMethod("TurnOn", "打开灯", ParameterList(), [this](const ParameterList& parameters) { + power_ = true; + gpio_set_level(gpio_num_, 1); + }); + + // 定义方法:TurnOff(关闭灯) + methods_.AddMethod("TurnOff", "关闭灯", ParameterList(), [this](const ParameterList& parameters) { + power_ = false; + gpio_set_level(gpio_num_, 0); + }); + } +}; +``` + +用户可以通过语音指令如"小智,请打开灯"来控制灯的开关。 + +### 扬声器(Speaker) + +扬声器控制实现了音量调节功能: + +```cpp +class Speaker : public Thing { +public: + Speaker() : Thing("Speaker", "扬声器") { + // 定义属性:volume(当前音量值) + properties_.AddNumberProperty("volume", "当前音量值", [this]() -> int { + auto codec = Board::GetInstance().GetAudioCodec(); + return codec->output_volume(); + }); + + // 定义方法:SetVolume(设置音量) + methods_.AddMethod("SetVolume", "设置音量", ParameterList({ + Parameter("volume", "0到100之间的整数", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + auto codec = Board::GetInstance().GetAudioCodec(); + codec->SetOutputVolume(static_cast(parameters["volume"].number())); + }); + } +}; +``` + +用户可以通过语音指令如"小智,把音量调到50"来控制扬声器的音量。 + +## 设计自定义物联网设备 + +要设计一个新的物联网设备,需要以下步骤: + +1. **创建设备类**:继承`Thing`基类 +2. **定义属性**:使用`properties_`添加设备的可查询状态 +3. **定义方法**:使用`methods_`添加设备可执行的操作 +4. **实现硬件控制**:在方法回调中实现对硬件的控制 +5. **注册设备**:注册设备有两种方式(见下文),并在板级初始化中添加设备实例 + +### 两种设备注册方式 + +1. **使用DECLARE_THING宏**:适合通用设备类型 + ```cpp + // 在设备实现文件末尾添加 + DECLARE_THING(MyDevice); + + // 然后在板级初始化中 + thing_manager.AddThing(iot::CreateThing("MyDevice")); + ``` + +2. **直接创建设备实例**:适合特定于板级的设备 + ```cpp + // 在板级初始化中 + auto my_device = new iot::MyDevice(); + thing_manager.AddThing(my_device); + ``` + +### 设备实现位置建议 + +您可以根据设备的通用性选择不同的实现位置: + +1. **通用设备**:放在`main/iot/things/`目录下,使用`DECLARE_THING`注册 +2. **板级特定设备**:直接在板级目录(如`main/boards/your_board/`)中实现,使用直接创建实例的方式注册 + +这种灵活性允许您为不同的板设计特定的设备实现,同时保持通用设备的可重用性。 + +### 属性类型 + +物联网设备支持以下属性类型: + +- **布尔值**(`kValueTypeBoolean`):开关状态等 +- **数值**(`kValueTypeNumber`):温度、音量等 +- **字符串**(`kValueTypeString`):设备名称、状态描述等 + +### 方法参数 + +设备方法可以定义参数,支持以下参数类型: + +- **布尔值**:开关等 +- **数值**:温度、音量等 +- **字符串**:命令、模式等 + +## 使用示例 + +在板级初始化代码(如`compact_wifi_board.cc`)中注册物联网设备: + +```cpp +void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Lamp")); +} +``` + +之后,用户可以通过语音指令控制这些设备,例如: + +- "小智,打开灯" +- "我要睡觉了,帮我关灯" +- "音量有点太小了" +- "把音量设置为80%" + +AI服务器会将这些语音指令解析为对应的设备控制命令,通过协议发送给ESP32执行。 + +## 注意事项 + +### 大模型控制的随机性 + +由于语音控制由大型语言模型(LLM)处理,控制过程可能存在一定的随机性和不确定性。为了增强安全性和可靠性,请考虑以下建议: + +1. **关键操作添加警示信息**:对于潜在危险或不可逆的操作,在方法描述中添加警示信息 + ```cpp + methods_.AddMethod("PowerOff", "关闭系统电源[警告:此操作将导致设备完全关闭,请慎重使用]", + ParameterList(), [this](const ParameterList& parameters) { + // 关闭电源的实现 + }); + ``` + +2. **二次确认机制**:重要操作应在描述中明确要求二次确认 + ```cpp + methods_.AddMethod("ResetToFactory", "恢复出厂设置[必须要用户二次确认]", + ParameterList(), [this](const ParameterList& parameters) { + // 恢复出厂设置的实现 + }); + ``` + +### 通信协议限制 + +当前IoT协议(1.0版本)存在以下限制: + +1. **单向控制流**:大模型只能下发指令,无法立即获取指令执行结果 +2. **状态更新延迟**:设备状态变更需要等到下一轮对话时,通过读取property属性值才能获知 +3. **异步反馈**:如果需要操作结果反馈,必须通过设备属性的方式间接实现 + +### 最佳实践 + +1. **使用有意义的属性名称**:属性名称应清晰表达其含义,便于大模型理解和使用 + +2. **不产生歧义的方法描述**:为每个方法提供明确的自然语言描述,帮助大模型更准确地理解和调用 \ No newline at end of file diff --git a/main/iot/thing.cc b/main/iot/thing.cc new file mode 100644 index 0000000..88e4fc7 --- /dev/null +++ b/main/iot/thing.cc @@ -0,0 +1,77 @@ +#include "thing.h" +#include "application.h" + +#include + +#define TAG "Thing" + + +namespace iot { + +static std::map>* thing_creators = nullptr; + +void RegisterThing(const std::string& type, std::function creator) { + if (thing_creators == nullptr) { + thing_creators = new std::map>(); + } + (*thing_creators)[type] = creator; +} + +Thing* CreateThing(const std::string& type) { + auto creator = thing_creators->find(type); + if (creator == thing_creators->end()) { + ESP_LOGE(TAG, "Thing type not found: %s", type.c_str()); + return nullptr; + } + return creator->second(); +} + +std::string Thing::GetDescriptorJson() { + std::string json_str = "{"; + json_str += "\"name\":\"" + name_ + "\","; + json_str += "\"description\":\"" + description_ + "\","; + json_str += "\"properties\":" + properties_.GetDescriptorJson() + ","; + json_str += "\"methods\":" + methods_.GetDescriptorJson(); + json_str += "}"; + return json_str; +} + +std::string Thing::GetStateJson() { + std::string json_str = "{"; + json_str += "\"name\":\"" + name_ + "\","; + json_str += "\"state\":" + properties_.GetStateJson(); + json_str += "}"; + return json_str; +} + +void Thing::Invoke(const cJSON* command) { + auto method_name = cJSON_GetObjectItem(command, "method"); + auto input_params = cJSON_GetObjectItem(command, "parameters"); + + try { + auto& method = methods_[method_name->valuestring]; + for (auto& param : method.parameters()) { + auto input_param = cJSON_GetObjectItem(input_params, param.name().c_str()); + if (param.required() && input_param == nullptr) { + throw std::runtime_error("Parameter " + param.name() + " is required"); + } + if (param.type() == kValueTypeNumber) { + param.set_number(input_param->valueint); + } else if (param.type() == kValueTypeString) { + param.set_string(input_param->valuestring); + } else if (param.type() == kValueTypeBoolean) { + param.set_boolean(input_param->valueint == 1); + } + } + + Application::GetInstance().Schedule([&method]() { + method.Invoke(); + }); + } catch (const std::runtime_error& e) { + ESP_LOGE(TAG, "Method not found: %s", method_name->valuestring); + return; + } +} + + +} // namespace iot diff --git a/main/iot/thing.h b/main/iot/thing.h new file mode 100644 index 0000000..21c8d69 --- /dev/null +++ b/main/iot/thing.h @@ -0,0 +1,300 @@ +#ifndef THING_H +#define THING_H + +#include +#include +#include +#include +#include +#include + +namespace iot { + +enum ValueType { + kValueTypeBoolean, + kValueTypeNumber, + kValueTypeString +}; + +class Property { +private: + std::string name_; + std::string description_; + ValueType type_; + std::function boolean_getter_; + std::function number_getter_; + std::function string_getter_; + +public: + Property(const std::string& name, const std::string& description, std::function getter) : + name_(name), description_(description), type_(kValueTypeBoolean), boolean_getter_(getter) {} + Property(const std::string& name, const std::string& description, std::function getter) : + name_(name), description_(description), type_(kValueTypeNumber), number_getter_(getter) {} + Property(const std::string& name, const std::string& description, std::function getter) : + name_(name), description_(description), type_(kValueTypeString), string_getter_(getter) {} + + const std::string& name() const { return name_; } + const std::string& description() const { return description_; } + ValueType type() const { return type_; } + + bool boolean() const { return boolean_getter_(); } + int number() const { return number_getter_(); } + std::string string() const { return string_getter_(); } + + std::string GetDescriptorJson() { + std::string json_str = "{"; + json_str += "\"description\":\"" + description_ + "\","; + if (type_ == kValueTypeBoolean) { + json_str += "\"type\":\"boolean\""; + } else if (type_ == kValueTypeNumber) { + json_str += "\"type\":\"number\""; + } else if (type_ == kValueTypeString) { + json_str += "\"type\":\"string\""; + } + json_str += "}"; + return json_str; + } + + std::string GetStateJson() { + if (type_ == kValueTypeBoolean) { + return boolean_getter_() ? "true" : "false"; + } else if (type_ == kValueTypeNumber) { + return std::to_string(number_getter_()); + } else if (type_ == kValueTypeString) { + return "\"" + string_getter_() + "\""; + } + return "null"; + } +}; + +class PropertyList { +private: + std::vector properties_; + +public: + PropertyList() = default; + PropertyList(const std::vector& properties) : properties_(properties) {} + + void AddBooleanProperty(const std::string& name, const std::string& description, std::function getter) { + properties_.push_back(Property(name, description, getter)); + } + void AddNumberProperty(const std::string& name, const std::string& description, std::function getter) { + properties_.push_back(Property(name, description, getter)); + } + void AddStringProperty(const std::string& name, const std::string& description, std::function getter) { + properties_.push_back(Property(name, description, getter)); + } + + const Property& operator[](const std::string& name) const { + for (auto& property : properties_) { + if (property.name() == name) { + return property; + } + } + throw std::runtime_error("Property not found: " + name); + } + + std::string GetDescriptorJson() { + std::string json_str = "{"; + for (auto& property : properties_) { + json_str += "\"" + property.name() + "\":" + property.GetDescriptorJson() + ","; + } + if (json_str.back() == ',') { + json_str.pop_back(); + } + json_str += "}"; + return json_str; + } + + std::string GetStateJson() { + std::string json_str = "{"; + for (auto& property : properties_) { + json_str += "\"" + property.name() + "\":" + property.GetStateJson() + ","; + } + if (json_str.back() == ',') { + json_str.pop_back(); + } + json_str += "}"; + return json_str; + } +}; + +class Parameter { +private: + std::string name_; + std::string description_; + ValueType type_; + bool required_; + bool boolean_; + int number_; + std::string string_; + +public: + Parameter(const std::string& name, const std::string& description, ValueType type, bool required = true) : + name_(name), description_(description), type_(type), required_(required) {} + + const std::string& name() const { return name_; } + const std::string& description() const { return description_; } + ValueType type() const { return type_; } + bool required() const { return required_; } + + bool boolean() const { return boolean_; } + int number() const { return number_; } + const std::string& string() const { return string_; } + + void set_boolean(bool value) { boolean_ = value; } + void set_number(int value) { number_ = value; } + void set_string(const std::string& value) { string_ = value; } + + std::string GetDescriptorJson() { + std::string json_str = "{"; + json_str += "\"description\":\"" + description_ + "\","; + if (type_ == kValueTypeBoolean) { + json_str += "\"type\":\"boolean\""; + } else if (type_ == kValueTypeNumber) { + json_str += "\"type\":\"number\""; + } else if (type_ == kValueTypeString) { + json_str += "\"type\":\"string\""; + } + json_str += "}"; + return json_str; + } +}; + +class ParameterList { +private: + std::vector parameters_; + +public: + ParameterList() = default; + ParameterList(const std::vector& parameters) : parameters_(parameters) {} + void AddParameter(const Parameter& parameter) { + parameters_.push_back(parameter); + } + + const Parameter& operator[](const std::string& name) const { + for (auto& parameter : parameters_) { + if (parameter.name() == name) { + return parameter; + } + } + throw std::runtime_error("Parameter not found: " + name); + } + + // iterator + auto begin() { return parameters_.begin(); } + auto end() { return parameters_.end(); } + + std::string GetDescriptorJson() { + std::string json_str = "{"; + for (auto& parameter : parameters_) { + json_str += "\"" + parameter.name() + "\":" + parameter.GetDescriptorJson() + ","; + } + if (json_str.back() == ',') { + json_str.pop_back(); + } + json_str += "}"; + return json_str; + } +}; + +class Method { +private: + std::string name_; + std::string description_; + ParameterList parameters_; + std::function callback_; + +public: + Method(const std::string& name, const std::string& description, const ParameterList& parameters, std::function callback) : + name_(name), description_(description), parameters_(parameters), callback_(callback) {} + + const std::string& name() const { return name_; } + const std::string& description() const { return description_; } + ParameterList& parameters() { return parameters_; } + + std::string GetDescriptorJson() { + std::string json_str = "{"; + json_str += "\"description\":\"" + description_ + "\","; + json_str += "\"parameters\":" + parameters_.GetDescriptorJson(); + json_str += "}"; + return json_str; + } + + void Invoke() { + callback_(parameters_); + } +}; + +class MethodList { +private: + std::vector methods_; + +public: + MethodList() = default; + MethodList(const std::vector& methods) : methods_(methods) {} + + void AddMethod(const std::string& name, const std::string& description, const ParameterList& parameters, std::function callback) { + methods_.push_back(Method(name, description, parameters, callback)); + } + + Method& operator[](const std::string& name) { + for (auto& method : methods_) { + if (method.name() == name) { + return method; + } + } + throw std::runtime_error("Method not found: " + name); + } + + std::string GetDescriptorJson() { + std::string json_str = "{"; + for (auto& method : methods_) { + json_str += "\"" + method.name() + "\":" + method.GetDescriptorJson() + ","; + } + if (json_str.back() == ',') { + json_str.pop_back(); + } + json_str += "}"; + return json_str; + } +}; + +class Thing { +public: + Thing(const std::string& name, const std::string& description) : + name_(name), description_(description) {} + virtual ~Thing() = default; + + virtual std::string GetDescriptorJson(); + virtual std::string GetStateJson(); + virtual void Invoke(const cJSON* command); + + const std::string& name() const { return name_; } + const std::string& description() const { return description_; } + +protected: + PropertyList properties_; + MethodList methods_; + +private: + std::string name_; + std::string description_; +}; + + +void RegisterThing(const std::string& type, std::function creator); +Thing* CreateThing(const std::string& type); + +#define DECLARE_THING(TypeName) \ + static iot::Thing* Create##TypeName() { \ + return new iot::TypeName(); \ + } \ + static bool Register##TypeNameHelper = []() { \ + RegisterThing(#TypeName, Create##TypeName); \ + return true; \ + }(); + +} // namespace iot + +#endif // THING_H diff --git a/main/iot/thing_manager.cc b/main/iot/thing_manager.cc new file mode 100644 index 0000000..9243869 --- /dev/null +++ b/main/iot/thing_manager.cc @@ -0,0 +1,63 @@ +#include "thing_manager.h" + +#include + +#define TAG "ThingManager" + +namespace iot { + +void ThingManager::AddThing(Thing* thing) { + things_.push_back(thing); +} + +std::string ThingManager::GetDescriptorsJson() { + std::string json_str = "["; + for (auto& thing : things_) { + json_str += thing->GetDescriptorJson() + ","; + } + if (json_str.back() == ',') { + json_str.pop_back(); + } + json_str += "]"; + return json_str; +} + +bool ThingManager::GetStatesJson(std::string& json, bool delta) { + if (!delta) { + last_states_.clear(); + } + bool changed = false; + json = "["; + // 枚举thing,获取每个thing的state,如果发生变化,则更新,保存到last_states_ + // 如果delta为true,则只返回变化的部分 + for (auto& thing : things_) { + std::string state = thing->GetStateJson(); + if (delta) { + // 如果delta为true,则只返回变化的部分 + auto it = last_states_.find(thing->name()); + if (it != last_states_.end() && it->second == state) { + continue; + } + changed = true; + last_states_[thing->name()] = state; + } + json += state + ","; + } + if (json.back() == ',') { + json.pop_back(); + } + json += "]"; + return changed; +} + +void ThingManager::Invoke(const cJSON* command) { + auto name = cJSON_GetObjectItem(command, "name"); + for (auto& thing : things_) { + if (thing->name() == name->valuestring) { + thing->Invoke(command); + return; + } + } +} + +} // namespace iot diff --git a/main/iot/thing_manager.h b/main/iot/thing_manager.h new file mode 100644 index 0000000..d51c910 --- /dev/null +++ b/main/iot/thing_manager.h @@ -0,0 +1,42 @@ +#ifndef THING_MANAGER_H +#define THING_MANAGER_H + + +#include "thing.h" + +#include + +#include +#include +#include +#include + +namespace iot { + +class ThingManager { +public: + static ThingManager& GetInstance() { + static ThingManager instance; + return instance; + } + ThingManager(const ThingManager&) = delete; + ThingManager& operator=(const ThingManager&) = delete; + + void AddThing(Thing* thing); + + std::string GetDescriptorsJson(); + bool GetStatesJson(std::string& json, bool delta = false); + void Invoke(const cJSON* command); + +private: + ThingManager() = default; + ~ThingManager() = default; + + std::vector things_; + std::map last_states_; +}; + + +} // namespace iot + +#endif // THING_MANAGER_H diff --git a/main/iot/things/battery.cc b/main/iot/things/battery.cc new file mode 100644 index 0000000..d503a4e --- /dev/null +++ b/main/iot/things/battery.cc @@ -0,0 +1,35 @@ +#include "iot/thing.h" +#include "board.h" + +#include + +#define TAG "Battery" + +namespace iot { + +// 这里仅定义 Battery 的属性和方法,不包含具体的实现 +class Battery : public Thing { +private: + int level_ = 0; + bool charging_ = false; + bool discharging_ = false; + +public: + Battery() : Thing("Battery", "电池管理") { + // 定义设备的属性 + properties_.AddNumberProperty("level", "当前电量百分比", [this]() -> int { + auto& board = Board::GetInstance(); + if (board.GetBatteryLevel(level_, charging_, discharging_)) { + return level_; + } + return 0; + }); + properties_.AddBooleanProperty("charging", "是否充电中", [this]() -> int { + return charging_; + }); + } +}; + +} // namespace iot + +DECLARE_THING(Battery); \ No newline at end of file diff --git a/main/iot/things/lamp.cc b/main/iot/things/lamp.cc new file mode 100644 index 0000000..cf016d4 --- /dev/null +++ b/main/iot/things/lamp.cc @@ -0,0 +1,58 @@ +#include "iot/thing.h" +#include "board.h" +#include "audio_codec.h" + +#include +#include + +#define TAG "Lamp" + +namespace iot { + +// 这里仅定义 Lamp 的属性和方法,不包含具体的实现 +class Lamp : public Thing { +private: +#ifdef CONFIG_IDF_TARGET_ESP32 + gpio_num_t gpio_num_ = GPIO_NUM_12; +#else + gpio_num_t gpio_num_ = GPIO_NUM_18; +#endif + bool power_ = false; + + void InitializeGpio() { + gpio_config_t config = { + .pin_bit_mask = (1ULL << gpio_num_), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ESP_ERROR_CHECK(gpio_config(&config)); + gpio_set_level(gpio_num_, 0); + } + +public: + Lamp() : Thing("Lamp", "一个测试用的灯"), power_(false) { + InitializeGpio(); + + // 定义设备的属性 + properties_.AddBooleanProperty("power", "灯是否打开", [this]() -> bool { + return power_; + }); + + // 定义设备可以被远程执行的指令 + methods_.AddMethod("TurnOn", "打开灯", ParameterList(), [this](const ParameterList& parameters) { + power_ = true; + gpio_set_level(gpio_num_, 1); + }); + + methods_.AddMethod("TurnOff", "关闭灯", ParameterList(), [this](const ParameterList& parameters) { + power_ = false; + gpio_set_level(gpio_num_, 0); + }); + } +}; + +} // namespace iot + +DECLARE_THING(Lamp); diff --git a/main/iot/things/screen.cc b/main/iot/things/screen.cc new file mode 100644 index 0000000..87761ea --- /dev/null +++ b/main/iot/things/screen.cc @@ -0,0 +1,63 @@ +#include "iot/thing.h" +#include "board.h" +// #include "display/lcd_display.h" // 移除显示器头文件引用 +#include "settings.h" + +#include +#include + +#define TAG "Screen" + +namespace iot { + +// 这里仅定义 Screen 的属性和方法,不包含具体的实现 +class Screen : public Thing { +public: + Screen() : Thing("Screen", "这是一个屏幕,可设置主题和亮度") { + // 定义设备的属性 + properties_.AddStringProperty("theme", "主题", [this]() -> std::string { + auto display = Board::GetInstance().GetDisplay(); + if (display) { + return display->GetTheme(); + } + return "default"; // 无显示器时返回默认主题 + }); + + properties_.AddNumberProperty("brightness", "当前亮度百分比", [this]() -> int { + // 这里可以添加获取当前亮度的逻辑 + auto backlight = Board::GetInstance().GetBacklight(); + return backlight ? backlight->brightness() : 100; + }); + + // 定义设备可以被远程执行的指令 + methods_.AddMethod("SetTheme", "设置屏幕主题", ParameterList({ + Parameter("theme_name", "主题模式, light 或 dark", kValueTypeString, true) + }), [this](const ParameterList& parameters) { + std::string theme_name = static_cast(parameters["theme_name"].string()); + auto display = Board::GetInstance().GetDisplay(); + if (display) { + display->SetTheme(theme_name); + ESP_LOGI(TAG, "设置主题为: %s", theme_name.c_str()); + } else { + ESP_LOGW(TAG, "无显示器,忽略设置主题命令: %s", theme_name.c_str()); + } + }); + + methods_.AddMethod("SetBrightness", "设置亮度", ParameterList({ + Parameter("brightness", "0到100之间的整数", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + uint8_t brightness = static_cast(parameters["brightness"].number()); + auto backlight = Board::GetInstance().GetBacklight(); + if (backlight) { + backlight->SetBrightness(brightness, true); + ESP_LOGI(TAG, "设置亮度为: %d", brightness); + } else { + ESP_LOGW(TAG, "无背光控制,忽略设置亮度命令: %d", brightness); + } + }); + } +}; + +} // namespace iot + +DECLARE_THING(Screen); \ No newline at end of file diff --git a/main/iot/things/speaker.cc b/main/iot/things/speaker.cc new file mode 100644 index 0000000..df70ad1 --- /dev/null +++ b/main/iot/things/speaker.cc @@ -0,0 +1,79 @@ +// #include "iot/thing.h" +// #include "board.h" +// #include "audio_codec.h" + +// #include + +// #define TAG "Speaker" + +// namespace iot { + +// // 这里仅定义 Speaker 的属性和方法,不包含具体的实现 +// class Speaker : public Thing { +// public: +// Speaker() : Thing("Speaker", "扬声器") { +// // 定义设备的属性 +// properties_.AddNumberProperty("volume", "当前音量值", [this]() -> int { +// auto codec = Board::GetInstance().GetAudioCodec(); +// return codec->output_volume(); +// }); + +// // 定义设备可以被远程执行的指令 +// methods_.AddMethod("SetVolume", "设置音量", ParameterList({ +// Parameter("volume", "0到100之间的整数", kValueTypeNumber, true) +// }), [this](const ParameterList& parameters) { +// auto codec = Board::GetInstance().GetAudioCodec(); +// codec->SetOutputVolume(static_cast(parameters["volume"].number())); +// }); +// } +// }; + +// } // namespace iot + +// DECLARE_THING(Speaker); + + + + +#include "iot/thing.h" +#include "board.h" +#include "audio_codec.h" +#include "volume_config.h" + +#include + +#define TAG "Speaker" + +namespace iot { + +// 这里仅定义 Speaker 的属性和方法,不包含具体的实现 +class Speaker : public Thing { +public: + Speaker() : Thing("Speaker", "扬声器") { + // 定义设备的属性 + properties_.AddNumberProperty("volume", "当前音量值", [this]() -> int { + auto codec = Board::GetInstance().GetAudioCodec(); + return codec->output_volume(); + }); + + // 定义设备可以被远程执行的指令 + methods_.AddMethod("SetVolume", "设置音量", ParameterList({ + Parameter("volume", "0到100之间的整数", kValueTypeNumber, true) + }), [this](const ParameterList& parameters) { + auto codec = Board::GetInstance().GetAudioCodec(); + // 用户音量范围0%-100%映射到硬件音量范围MIN_VOLUME_PERCENT%-MAX_VOLUME_PERCENT% + uint8_t user_volume = static_cast(parameters["volume"].number()); + // 使用宏定义进行动态映射计算 + uint8_t hardware_volume = USER_TO_HARDWARE_VOLUME(user_volume); + + ESP_LOGI("Speaker", "User volume: %d%% -> Hardware volume: %d%% (Min: %d%%, Range: %d%%)", + user_volume, hardware_volume, MIN_VOLUME_PERCENT, VOLUME_RANGE); + codec->SetOutputVolume(hardware_volume); + }); + } +}; + +} // namespace iot + +DECLARE_THING(Speaker); + diff --git a/main/led/circular_strip.cc b/main/led/circular_strip.cc new file mode 100644 index 0000000..f4f0fee --- /dev/null +++ b/main/led/circular_strip.cc @@ -0,0 +1,232 @@ +#include "circular_strip.h" +#include "application.h" +#include + +#define TAG "CircularStrip" + +#define BLINK_INFINITE -1 + +CircularStrip::CircularStrip(gpio_num_t gpio, uint8_t max_leds) : max_leds_(max_leds) { + // If the gpio is not connected, you should use NoLed class + assert(gpio != GPIO_NUM_NC); + + colors_.resize(max_leds_); + + led_strip_config_t strip_config = {}; + strip_config.strip_gpio_num = gpio; + strip_config.max_leds = max_leds_; + strip_config.led_pixel_format = LED_PIXEL_FORMAT_GRB; + strip_config.led_model = LED_MODEL_WS2812; + + led_strip_rmt_config_t rmt_config = {}; + rmt_config.resolution_hz = 10 * 1000 * 1000; // 10MHz + + ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip_)); + led_strip_clear(led_strip_); + + esp_timer_create_args_t strip_timer_args = { + .callback = [](void *arg) { + auto strip = static_cast(arg); + std::lock_guard lock(strip->mutex_); + if (strip->strip_callback_ != nullptr) { + strip->strip_callback_(); + } + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "strip_timer", + .skip_unhandled_events = false, + }; + ESP_ERROR_CHECK(esp_timer_create(&strip_timer_args, &strip_timer_)); +} + +CircularStrip::~CircularStrip() { + esp_timer_stop(strip_timer_); + if (led_strip_ != nullptr) { + led_strip_del(led_strip_); + } +} + + +void CircularStrip::SetAllColor(StripColor color) { + std::lock_guard lock(mutex_); + esp_timer_stop(strip_timer_); + for (int i = 0; i < max_leds_; i++) { + colors_[i] = color; + led_strip_set_pixel(led_strip_, i, color.red, color.green, color.blue); + } + led_strip_refresh(led_strip_); +} + +void CircularStrip::SetSingleColor(uint8_t index, StripColor color) { + std::lock_guard lock(mutex_); + esp_timer_stop(strip_timer_); + colors_[index] = color; + led_strip_set_pixel(led_strip_, index, color.red, color.green, color.blue); + led_strip_refresh(led_strip_); +} + +void CircularStrip::Blink(StripColor color, int interval_ms) { + for (int i = 0; i < max_leds_; i++) { + colors_[i] = color; + } + StartStripTask(interval_ms, [this]() { + static bool on = true; + if (on) { + for (int i = 0; i < max_leds_; i++) { + led_strip_set_pixel(led_strip_, i, colors_[i].red, colors_[i].green, colors_[i].blue); + } + led_strip_refresh(led_strip_); + } else { + led_strip_clear(led_strip_); + } + on = !on; + }); +} + +void CircularStrip::FadeOut(int interval_ms) { + StartStripTask(interval_ms, [this]() { + bool all_off = true; + for (int i = 0; i < max_leds_; i++) { + colors_[i].red /= 2; + colors_[i].green /= 2; + colors_[i].blue /= 2; + if (colors_[i].red != 0 || colors_[i].green != 0 || colors_[i].blue != 0) { + all_off = false; + } + led_strip_set_pixel(led_strip_, i, colors_[i].red, colors_[i].green, colors_[i].blue); + } + if (all_off) { + led_strip_clear(led_strip_); + esp_timer_stop(strip_timer_); + } else { + led_strip_refresh(led_strip_); + } + }); +} + +void CircularStrip::Breathe(StripColor low, StripColor high, int interval_ms) { + StartStripTask(interval_ms, [this, low, high]() { + static bool increase = true; + static StripColor color = low; + if (increase) { + if (color.red < high.red) { + color.red++; + } + if (color.green < high.green) { + color.green++; + } + if (color.blue < high.blue) { + color.blue++; + } + if (color.red == high.red && color.green == high.green && color.blue == high.blue) { + increase = false; + } + } else { + if (color.red > low.red) { + color.red--; + } + if (color.green > low.green) { + color.green--; + } + if (color.blue > low.blue) { + color.blue--; + } + if (color.red == low.red && color.green == low.green && color.blue == low.blue) { + increase = true; + } + } + for (int i = 0; i < max_leds_; i++) { + led_strip_set_pixel(led_strip_, i, color.red, color.green, color.blue); + } + led_strip_refresh(led_strip_); + }); +} + +void CircularStrip::Scroll(StripColor low, StripColor high, int length, int interval_ms) { + for (int i = 0; i < max_leds_; i++) { + colors_[i] = low; + } + StartStripTask(interval_ms, [this, low, high, length]() { + static int offset = 0; + for (int i = 0; i < max_leds_; i++) { + colors_[i] = low; + } + for (int j = 0; j < length; j++) { + int i = (offset + j) % max_leds_; + colors_[i] = high; + } + for (int i = 0; i < max_leds_; i++) { + led_strip_set_pixel(led_strip_, i, colors_[i].red, colors_[i].green, colors_[i].blue); + } + led_strip_refresh(led_strip_); + offset = (offset + 1) % max_leds_; + }); +} + +void CircularStrip::StartStripTask(int interval_ms, std::function cb) { + if (led_strip_ == nullptr) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(strip_timer_); + + strip_callback_ = cb; + esp_timer_start_periodic(strip_timer_, interval_ms * 1000); +} + +void CircularStrip::SetBrightness(uint8_t default_brightness, uint8_t low_brightness) { + default_brightness_ = default_brightness; + low_brightness_ = low_brightness; + OnStateChanged(); +} + +void CircularStrip::OnStateChanged() { + auto& app = Application::GetInstance(); + auto device_state = app.GetDeviceState(); + switch (device_state) { + case kDeviceStateStarting: { + StripColor low = { 0, 0, 0 }; + StripColor high = { low_brightness_, low_brightness_, default_brightness_ }; + Scroll(low, high, 3, 100); + break; + } + case kDeviceStateWifiConfiguring: { + StripColor color = { low_brightness_, low_brightness_, default_brightness_ }; + Blink(color, 500); + break; + } + case kDeviceStateIdle: + FadeOut(50); + break; + case kDeviceStateConnecting: { + StripColor color = { low_brightness_, low_brightness_, default_brightness_ }; + SetAllColor(color); + break; + } + case kDeviceStateListening: { + StripColor color = { default_brightness_, low_brightness_, low_brightness_ }; + SetAllColor(color); + break; + } + case kDeviceStateSpeaking: { + StripColor color = { low_brightness_, default_brightness_, low_brightness_ }; + SetAllColor(color); + break; + } + case kDeviceStateUpgrading: { + StripColor color = { low_brightness_, default_brightness_, low_brightness_ }; + Blink(color, 100); + break; + } + case kDeviceStateActivating: { + StripColor color = { low_brightness_, default_brightness_, low_brightness_ }; + Blink(color, 500); + break; + } + default: + // ESP_LOGW(TAG, "Unknown led strip event: %d", device_state); + return; + } +} diff --git a/main/led/circular_strip.h b/main/led/circular_strip.h new file mode 100644 index 0000000..d5d6c22 --- /dev/null +++ b/main/led/circular_strip.h @@ -0,0 +1,51 @@ +#ifndef _CIRCULAR_STRIP_H_ +#define _CIRCULAR_STRIP_H_ + +#include "led.h" +#include +#include +#include +#include +#include +#include + +#define DEFAULT_BRIGHTNESS 32 +#define LOW_BRIGHTNESS 4 + +struct StripColor { + uint8_t red = 0, green = 0, blue = 0; +}; + +class CircularStrip : public Led { +public: + CircularStrip(gpio_num_t gpio, uint8_t max_leds); + virtual ~CircularStrip(); + + void OnStateChanged() override; + void SetBrightness(uint8_t default_brightness, uint8_t low_brightness); + void SetAllColor(StripColor color); + void SetSingleColor(uint8_t index, StripColor color); + void Blink(StripColor color, int interval_ms); + void Breathe(StripColor low, StripColor high, int interval_ms); + void Scroll(StripColor low, StripColor high, int length, int interval_ms); + +private: + std::mutex mutex_; + TaskHandle_t blink_task_ = nullptr; + led_strip_handle_t led_strip_ = nullptr; + int max_leds_ = 0; + std::vector colors_; + int blink_counter_ = 0; + int blink_interval_ms_ = 0; + esp_timer_handle_t strip_timer_ = nullptr; + std::function strip_callback_ = nullptr; + + uint8_t default_brightness_ = DEFAULT_BRIGHTNESS; + uint8_t low_brightness_ = LOW_BRIGHTNESS; + + void StartStripTask(int interval_ms, std::function cb); + void Rainbow(StripColor low, StripColor high, int interval_ms); + void FadeOut(int interval_ms); +}; + +#endif // _CIRCULAR_STRIP_H_ diff --git a/main/led/gpio_led.cc b/main/led/gpio_led.cc new file mode 100644 index 0000000..fcd5866 --- /dev/null +++ b/main/led/gpio_led.cc @@ -0,0 +1,247 @@ +#include "gpio_led.h" +#include "application.h" +#include + +#define TAG "GpioLed" + +#define DEFAULT_BRIGHTNESS 50 +#define HIGH_BRIGHTNESS 100 +#define LOW_BRIGHTNESS 10 + +#define IDLE_BRIGHTNESS 5 +#define SPEAKING_BRIGHTNESS 75 +#define UPGRADING_BRIGHTNESS 25 +#define ACTIVATING_BRIGHTNESS 35 + +#define BLINK_INFINITE -1 + +// GPIO_LED +#define LEDC_LS_TIMER LEDC_TIMER_1 +#define LEDC_LS_MODE LEDC_LOW_SPEED_MODE +#define LEDC_LS_CH0_CHANNEL LEDC_CHANNEL_0 + +#define LEDC_DUTY (8191) +#define LEDC_FADE_TIME (1000) +// GPIO_LED + +GpioLed::GpioLed(gpio_num_t gpio) + : GpioLed(gpio, 0, LEDC_LS_TIMER, LEDC_LS_CH0_CHANNEL) { +} + +GpioLed::GpioLed(gpio_num_t gpio, int output_invert) + : GpioLed(gpio, output_invert, LEDC_LS_TIMER, LEDC_LS_CH0_CHANNEL) { +} + +GpioLed::GpioLed(gpio_num_t gpio, int output_invert, ledc_timer_t timer_num, ledc_channel_t channel) { + // If the gpio is not connected, you should use NoLed class + assert(gpio != GPIO_NUM_NC); + + /* + * Prepare and set configuration of timers + * that will be used by LED Controller + */ + ledc_timer_config_t ledc_timer = {}; + ledc_timer.duty_resolution = LEDC_TIMER_13_BIT; // resolution of PWM duty + ledc_timer.freq_hz = 4000; // frequency of PWM signal + ledc_timer.speed_mode = LEDC_LS_MODE; // timer mode + ledc_timer.timer_num = timer_num; // timer index + ledc_timer.clk_cfg = LEDC_AUTO_CLK; // Auto select the source clock + + ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); + + ledc_channel_.channel = channel, + ledc_channel_.duty = 0, + ledc_channel_.gpio_num = gpio, + ledc_channel_.speed_mode = LEDC_LS_MODE, + ledc_channel_.hpoint = 0, + ledc_channel_.timer_sel = timer_num, + ledc_channel_.flags.output_invert = output_invert & 0x01, + + // Set LED Controller with previously prepared configuration + ledc_channel_config(&ledc_channel_); + + // Initialize fade service. + ledc_fade_func_install(0); + + // When the callback registered by ledc_cb_degister is called, run led ->OnFadeEnd() + ledc_cbs_t ledc_callbacks = { + .fade_cb = FadeCallback + }; + ledc_cb_register(ledc_channel_.speed_mode, ledc_channel_.channel, &ledc_callbacks, this); + + esp_timer_create_args_t blink_timer_args = { + .callback = [](void *arg) { + auto led = static_cast(arg); + led->OnBlinkTimer(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "Blink Timer", + .skip_unhandled_events = false, + }; + ESP_ERROR_CHECK(esp_timer_create(&blink_timer_args, &blink_timer_)); + + ledc_initialized_ = true; +} + +GpioLed::~GpioLed() { + esp_timer_stop(blink_timer_); + if (ledc_initialized_) { + ledc_fade_stop(ledc_channel_.speed_mode, ledc_channel_.channel); + ledc_fade_func_uninstall(); + } +} + + +void GpioLed::SetBrightness(uint8_t brightness) { + if (brightness == 100) { + duty_ = LEDC_DUTY; + } else { + duty_ = brightness * LEDC_DUTY / 100; + } +} + +void GpioLed::TurnOn() { + if (!ledc_initialized_) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(blink_timer_); + ledc_fade_stop(ledc_channel_.speed_mode, ledc_channel_.channel); + ledc_set_duty(ledc_channel_.speed_mode, ledc_channel_.channel, duty_); + ledc_update_duty(ledc_channel_.speed_mode, ledc_channel_.channel); +} + +void GpioLed::TurnOff() { + if (!ledc_initialized_) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(blink_timer_); + ledc_fade_stop(ledc_channel_.speed_mode, ledc_channel_.channel); + ledc_set_duty(ledc_channel_.speed_mode, ledc_channel_.channel, 0); + ledc_update_duty(ledc_channel_.speed_mode, ledc_channel_.channel); +} + +void GpioLed::BlinkOnce() { + Blink(1, 100); +} + +void GpioLed::Blink(int times, int interval_ms) { + StartBlinkTask(times, interval_ms); +} + +void GpioLed::StartContinuousBlink(int interval_ms) { + StartBlinkTask(BLINK_INFINITE, interval_ms); +} + +void GpioLed::StartBlinkTask(int times, int interval_ms) { + if (!ledc_initialized_) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(blink_timer_); + ledc_fade_stop(ledc_channel_.speed_mode, ledc_channel_.channel); + + blink_counter_ = times * 2; + blink_interval_ms_ = interval_ms; + esp_timer_start_periodic(blink_timer_, interval_ms * 1000); +} + +void GpioLed::OnBlinkTimer() { + std::lock_guard lock(mutex_); + blink_counter_--; + if (blink_counter_ & 1) { + ledc_set_duty(ledc_channel_.speed_mode, ledc_channel_.channel, duty_); + } else { + ledc_set_duty(ledc_channel_.speed_mode, ledc_channel_.channel, 0); + + if (blink_counter_ == 0) { + esp_timer_stop(blink_timer_); + } + } + ledc_update_duty(ledc_channel_.speed_mode, ledc_channel_.channel); +} + +void GpioLed::StartFadeTask() { + if (!ledc_initialized_) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(blink_timer_); + ledc_fade_stop(ledc_channel_.speed_mode, ledc_channel_.channel); + fade_up_ = true; + ledc_set_fade_with_time(ledc_channel_.speed_mode, + ledc_channel_.channel, LEDC_DUTY, LEDC_FADE_TIME); + ledc_fade_start(ledc_channel_.speed_mode, + ledc_channel_.channel, LEDC_FADE_NO_WAIT); +} + +void GpioLed::OnFadeEnd() { + std::lock_guard lock(mutex_); + fade_up_ = !fade_up_; + ledc_set_fade_with_time(ledc_channel_.speed_mode, + ledc_channel_.channel, fade_up_ ? LEDC_DUTY : 0, LEDC_FADE_TIME); + ledc_fade_start(ledc_channel_.speed_mode, + ledc_channel_.channel, LEDC_FADE_NO_WAIT); +} + +bool GpioLed::FadeCallback(const ledc_cb_param_t *param, void *user_arg) { + if (param->event == LEDC_FADE_END_EVT) { + auto led = static_cast(user_arg); + led->OnFadeEnd(); + } + return true; +} + +void GpioLed::OnStateChanged() { + auto& app = Application::GetInstance(); + auto device_state = app.GetDeviceState(); + switch (device_state) { + case kDeviceStateStarting: + SetBrightness(DEFAULT_BRIGHTNESS); + StartContinuousBlink(100); + break; + case kDeviceStateWifiConfiguring: + SetBrightness(DEFAULT_BRIGHTNESS); + StartContinuousBlink(500); + break; + case kDeviceStateIdle: + SetBrightness(IDLE_BRIGHTNESS); + TurnOn(); + // TurnOff(); + break; + case kDeviceStateConnecting: + SetBrightness(DEFAULT_BRIGHTNESS); + TurnOn(); + break; + case kDeviceStateListening: + if (app.IsVoiceDetected()) { + SetBrightness(HIGH_BRIGHTNESS); + } else { + SetBrightness(LOW_BRIGHTNESS); + } + // TurnOn(); + StartFadeTask(); + break; + case kDeviceStateSpeaking: + SetBrightness(SPEAKING_BRIGHTNESS); + TurnOn(); + break; + case kDeviceStateUpgrading: + SetBrightness(UPGRADING_BRIGHTNESS); + StartContinuousBlink(100); + break; + case kDeviceStateActivating: + SetBrightness(ACTIVATING_BRIGHTNESS); + StartContinuousBlink(500); + break; + default: + ESP_LOGE(TAG, "Unknown gpio led event: %d", device_state); + return; + } +} diff --git a/main/led/gpio_led.h b/main/led/gpio_led.h new file mode 100644 index 0000000..6f6a2c1 --- /dev/null +++ b/main/led/gpio_led.h @@ -0,0 +1,47 @@ +#ifndef _GPIO_LED_H_ +#define _GPIO_LED_H_ + +#include +#include +#include "led.h" +#include +#include +#include +#include +#include + +class GpioLed : public Led { + public: + GpioLed(gpio_num_t gpio); + GpioLed(gpio_num_t gpio, int output_invert); + GpioLed(gpio_num_t gpio, int output_invert, ledc_timer_t timer_num, ledc_channel_t channel); + virtual ~GpioLed(); + + void OnStateChanged() override; + void TurnOn(); + void TurnOff(); + void SetBrightness(uint8_t brightness); + + private: + std::mutex mutex_; + TaskHandle_t blink_task_ = nullptr; + ledc_channel_config_t ledc_channel_ = {0}; + bool ledc_initialized_ = false; + uint32_t duty_ = 0; + int blink_counter_ = 0; + int blink_interval_ms_ = 0; + esp_timer_handle_t blink_timer_ = nullptr; + bool fade_up_ = true; + + void StartBlinkTask(int times, int interval_ms); + void OnBlinkTimer(); + + void BlinkOnce(); + void Blink(int times, int interval_ms); + void StartContinuousBlink(int interval_ms); + void StartFadeTask(); + void OnFadeEnd(); + static bool FadeCallback(const ledc_cb_param_t *param, void *user_arg); +}; + +#endif // _GPIO_LED_H_ diff --git a/main/led/led.h b/main/led/led.h new file mode 100644 index 0000000..251fd6a --- /dev/null +++ b/main/led/led.h @@ -0,0 +1,17 @@ +#ifndef _LED_H_ +#define _LED_H_ + +class Led { +public: + virtual ~Led() = default; + // Set the led state based on the device state + virtual void OnStateChanged() = 0; +}; + + +class NoLed : public Led { +public: + virtual void OnStateChanged() override {} +}; + +#endif // _LED_H_ diff --git a/main/led/single_led.cc b/main/led/single_led.cc new file mode 100644 index 0000000..8cc46e6 --- /dev/null +++ b/main/led/single_led.cc @@ -0,0 +1,162 @@ +#include "single_led.h" +#include "application.h" +#include + +#define TAG "SingleLed" + +#define DEFAULT_BRIGHTNESS 4 +#define HIGH_BRIGHTNESS 16 +#define LOW_BRIGHTNESS 2 + +#define BLINK_INFINITE -1 + + +SingleLed::SingleLed(gpio_num_t gpio) { + // If the gpio is not connected, you should use NoLed class + assert(gpio != GPIO_NUM_NC); + + led_strip_config_t strip_config = {}; + strip_config.strip_gpio_num = gpio; + strip_config.max_leds = 1; + strip_config.led_pixel_format = LED_PIXEL_FORMAT_GRB; + strip_config.led_model = LED_MODEL_WS2812; + + led_strip_rmt_config_t rmt_config = {}; + rmt_config.resolution_hz = 10 * 1000 * 1000; // 10MHz + + ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip_)); + led_strip_clear(led_strip_); + + esp_timer_create_args_t blink_timer_args = { + .callback = [](void *arg) { + auto led = static_cast(arg); + led->OnBlinkTimer(); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "blink_timer", + .skip_unhandled_events = false, + }; + ESP_ERROR_CHECK(esp_timer_create(&blink_timer_args, &blink_timer_)); +} + +SingleLed::~SingleLed() { + esp_timer_stop(blink_timer_); + if (led_strip_ != nullptr) { + led_strip_del(led_strip_); + } +} + + +void SingleLed::SetColor(uint8_t r, uint8_t g, uint8_t b) { + r_ = r; + g_ = g; + b_ = b; +} + +void SingleLed::TurnOn() { + if (led_strip_ == nullptr) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(blink_timer_); + led_strip_set_pixel(led_strip_, 0, r_, g_, b_); + led_strip_refresh(led_strip_); +} + +void SingleLed::TurnOff() { + if (led_strip_ == nullptr) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(blink_timer_); + led_strip_clear(led_strip_); +} + +void SingleLed::BlinkOnce() { + Blink(1, 100); +} + +void SingleLed::Blink(int times, int interval_ms) { + StartBlinkTask(times, interval_ms); +} + +void SingleLed::StartContinuousBlink(int interval_ms) { + StartBlinkTask(BLINK_INFINITE, interval_ms); +} + +void SingleLed::StartBlinkTask(int times, int interval_ms) { + if (led_strip_ == nullptr) { + return; + } + + std::lock_guard lock(mutex_); + esp_timer_stop(blink_timer_); + + blink_counter_ = times * 2; + blink_interval_ms_ = interval_ms; + esp_timer_start_periodic(blink_timer_, interval_ms * 1000); +} + +void SingleLed::OnBlinkTimer() { + std::lock_guard lock(mutex_); + blink_counter_--; + if (blink_counter_ & 1) { + led_strip_set_pixel(led_strip_, 0, r_, g_, b_); + led_strip_refresh(led_strip_); + } else { + led_strip_clear(led_strip_); + + if (blink_counter_ == 0) { + esp_timer_stop(blink_timer_); + } + } +} + + +void SingleLed::OnStateChanged() { + auto& app = Application::GetInstance(); + auto device_state = app.GetDeviceState(); + switch (device_state) { + case kDeviceStateStarting: + SetColor(0, 0, DEFAULT_BRIGHTNESS); + StartContinuousBlink(100); + break; + case kDeviceStateWifiConfiguring: + SetColor(0, 0, DEFAULT_BRIGHTNESS); + StartContinuousBlink(500); + break; + case kDeviceStateIdle: + TurnOff(); + break; + case kDeviceStateConnecting: + SetColor(0, 0, DEFAULT_BRIGHTNESS); + TurnOn(); + break; + case kDeviceStateListening: + if (app.IsVoiceDetected()) { + SetColor(HIGH_BRIGHTNESS, 0, 0); + } else { + SetColor(LOW_BRIGHTNESS, 0, 0); + } + TurnOn(); + break; + case kDeviceStateSpeaking: + SetColor(0, DEFAULT_BRIGHTNESS, 0); + TurnOn(); + break; + case kDeviceStateUpgrading: + SetColor(0, DEFAULT_BRIGHTNESS, 0); + StartContinuousBlink(100); + break; + case kDeviceStateActivating: + SetColor(0, DEFAULT_BRIGHTNESS, 0); + StartContinuousBlink(500); + break; + default: + // ESP_LOGW(TAG, "Unknown led strip event: %d", device_state); + return; + } +} diff --git a/main/led/single_led.h b/main/led/single_led.h new file mode 100644 index 0000000..b949f74 --- /dev/null +++ b/main/led/single_led.h @@ -0,0 +1,38 @@ +#ifndef _SINGLE_LED_H_ +#define _SINGLE_LED_H_ + +#include "led.h" +#include +#include +#include +#include +#include + +class SingleLed : public Led { +public: + SingleLed(gpio_num_t gpio); + virtual ~SingleLed(); + + void OnStateChanged() override; + +private: + std::mutex mutex_; + TaskHandle_t blink_task_ = nullptr; + led_strip_handle_t led_strip_ = nullptr; + uint8_t r_ = 0, g_ = 0, b_ = 0; + int blink_counter_ = 0; + int blink_interval_ms_ = 0; + esp_timer_handle_t blink_timer_ = nullptr; + + void StartBlinkTask(int times, int interval_ms); + void OnBlinkTimer(); + + void BlinkOnce(); + void Blink(int times, int interval_ms); + void StartContinuousBlink(int interval_ms); + void TurnOn(); + void TurnOff(); + void SetColor(uint8_t r, uint8_t g, uint8_t b); +}; + +#endif // _SINGLE_LED_H_ diff --git a/main/main.cc b/main/main.cc new file mode 100644 index 0000000..e9d8477 --- /dev/null +++ b/main/main.cc @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "application.h" +#include "system_info.h" +#include "settings.h" + +#define TAG "main" + +// // 新增禁用日志配置(生产环境) +// // 重定向printf到空函数,彻底禁用所有输出 新增禁用日志配置 +// // ================================================================ +// extern "C" { +// int printf(const char* format, ...) { return 0; } +// int puts(const char* s) { return 0; } +// int putchar(int c) { return c; } +// size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream) { return size * count; } +// } +// // ================================================================ + +extern "C" void app_main(void) +{ + // // // ==================================================================================================== + // //全局禁用所有日志输出 - 必须在最开始就设置 + // esp_log_level_set("*", ESP_LOG_NONE); // 全局禁用所有日志 + // //特别禁用可能的残留日志组件 + // esp_log_level_set("coexist", ESP_LOG_NONE); + // esp_log_level_set("main_task", ESP_LOG_NONE); + // esp_log_level_set("MC Quantized wakenet9", ESP_LOG_NONE); + // esp_log_level_set("wakenet", ESP_LOG_NONE); + // esp_log_level_set("esp_netif_lwip", ESP_LOG_NONE); + // esp_log_level_set("wifi", ESP_LOG_NONE); + // esp_log_level_set("phy_init", ESP_LOG_NONE); + // esp_log_level_set("system_api", ESP_LOG_NONE); + // esp_log_level_set("MovecallMojiESP32S3", ESP_LOG_NONE); // 生产环境:屏蔽MovecallMojiESP32S3板级日志 + // //esp_log_level_set("MovecallMojiESP32S3", ESP_LOG_INFO); // 启用MovecallMojiESP32S3板级日志以支持触摸检测 + // esp_log_level_set("BluetoothMAC", ESP_LOG_INFO); // 仅允许BluetoothMAC组件的INFO级别日志(蓝牙MAC地址) + // //esp_log_level_set("Airhub", ESP_LOG_INFO); // 仅允许Airhub组件的INFO级别日志(生产测试日志) + // // ======================================================================================================= + + // 增加姿态传感器日志打印 + esp_log_level_set("Airhub", ESP_LOG_DEBUG); // 看到运动检测日志 + esp_log_level_set("Airhub", ESP_LOG_VERBOSE); // 看到详细数据日志 + + // 屏蔽AFE模块的警告日志 + esp_log_level_set("AFE", ESP_LOG_ERROR); + + // 开启VolcRtcProtocol调试日志,便于观察上行G711A帧打印 + esp_log_level_set("VolcRtcProtocol", ESP_LOG_DEBUG); + + // Initialize the default event loop + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // Initialize network interface (必须在WiFi初始化之前) + ESP_ERROR_CHECK(esp_netif_init()); + + // Initialize NVS flash for WiFi configuration + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_LOGW(TAG, "Erasing NVS flash to fix corruption"); + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // Initialize SPIFFS filesystem - Temporarily disabled to reduce resource usage + // ESP_LOGI(TAG, "Initializing SPIFFS..."); + // esp_vfs_spiffs_conf_t conf = { + // .base_path = "/spiffs", + // .partition_label = "model", + // .max_files = 5, + // .format_if_mount_failed = true + // }; + + // // Register and mount SPIFFS filesystem + // ret = esp_vfs_spiffs_register(&conf); + // if (ret != ESP_OK) { + // if (ret == ESP_FAIL) { + // ESP_LOGE(TAG, "Failed to mount or format filesystem"); + // } else if (ret == ESP_ERR_NOT_FOUND) { + // ESP_LOGE(TAG, "Failed to find SPIFFS partition"); + // } else { + // ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); + // } + // } else { + // ESP_LOGI(TAG, "SPIFFS initialized successfully"); + // // Check SPIFFS space + // size_t total = 0, used = 0; + // ret = esp_spiffs_info(conf.partition_label, &total, &used); + // if (ret != ESP_OK) { + // ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret)); + // } else { + // ESP_LOGI(TAG, "SPIFFS: total: %d bytes, used: %d bytes", total, used); + // } + // } + + // Launch the application + Application::GetInstance().Start(); + // The main thread will exit and release the stack memory +} diff --git a/main/ota.cc b/main/ota.cc new file mode 100644 index 0000000..d308621 --- /dev/null +++ b/main/ota.cc @@ -0,0 +1,356 @@ +#include "ota.h" +#include "system_info.h" +#include "board.h" +#include "settings.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "Ota" + + +Ota::Ota() { +} + +Ota::~Ota() { +} + +void Ota::SetCheckVersionUrl(std::string check_version_url) { + check_version_url_ = check_version_url; +} + +void Ota::SetHeader(const std::string& key, const std::string& value) { + headers_[key] = value; +} + +void Ota::SetPostData(const std::string& post_data) { + post_data_ = post_data; +} + +bool Ota::CheckVersion() { + current_version_ = esp_app_get_description()->version; + // ESP_LOGI(TAG, "Current version: %s", current_version_.c_str()); + + if (check_version_url_.length() < 10) { + ESP_LOGE(TAG, "Check version URL is not properly set"); + return false; + } + + auto http = Board::GetInstance().CreateHttp(); + for (const auto& header : headers_) { + http->SetHeader(header.first, header.second); + } + + http->SetHeader("Content-Type", "application/json"); + std::string method = post_data_.length() > 0 ? "POST" : "GET"; + if (!http->Open(method, check_version_url_, post_data_)) { + ESP_LOGE(TAG, "Failed to open HTTP connection"); + delete http; + return false; + } + + int status_code = http->GetStatusCode();// 获取HTTP状态码 + ESP_LOGI(TAG, "HTTP response status code: %d", status_code);// 日志记录状态码 + + auto response = http->GetBody(); + http->Close(); + delete http; + + // Parse the JSON response and check if the version is newer + // If it is, set has_new_version_ to true and store the new version and URL + + cJSON *root = cJSON_Parse(response.c_str()); + if (root == NULL) { + ESP_LOGE(TAG, "Failed to parse JSON response"); + return false; + } + + has_activation_code_ = false; + cJSON *activation = cJSON_GetObjectItem(root, "activation"); + if (activation != NULL) { + cJSON* message = cJSON_GetObjectItem(activation, "message"); + if (message != NULL) { + activation_message_ = message->valuestring; + } + cJSON* code = cJSON_GetObjectItem(activation, "code"); + if (code != NULL) { + activation_code_ = code->valuestring; + } + has_activation_code_ = true; + } + + has_mqtt_config_ = false; + cJSON *mqtt = cJSON_GetObjectItem(root, "mqtt"); + if (mqtt != NULL) { + Settings settings("mqtt", true); + cJSON *item = NULL; + cJSON_ArrayForEach(item, mqtt) { + if (item->type == cJSON_String) { + if (settings.GetString(item->string) != item->valuestring) { + settings.SetString(item->string, item->valuestring); + } + } + } + has_mqtt_config_ = true; + } + + has_server_time_ = false; + cJSON *server_time = cJSON_GetObjectItem(root, "server_time"); + if (server_time != NULL) { + cJSON *timestamp = cJSON_GetObjectItem(server_time, "timestamp"); + cJSON *timezone_offset = cJSON_GetObjectItem(server_time, "timezone_offset"); + + if (timestamp != NULL) { + // 设置系统时间 + struct timeval tv; + double ts = timestamp->valuedouble; + + // 如果有时区偏移,计算本地时间 + if (timezone_offset != NULL) { + ts += (timezone_offset->valueint * 60 * 1000); // 转换分钟为毫秒 + } + + tv.tv_sec = (time_t)(ts / 1000); // 转换毫秒为秒 + tv.tv_usec = (suseconds_t)((long long)ts % 1000) * 1000; // 剩余的毫秒转换为微秒 + settimeofday(&tv, NULL); + has_server_time_ = true; + } + } + + cJSON *firmware = cJSON_GetObjectItem(root, "firmware"); + if (firmware == NULL) { + ESP_LOGE(TAG, "Failed to get firmware object"); + cJSON_Delete(root); + return false; + } + cJSON *version = cJSON_GetObjectItem(firmware, "version"); + if (version == NULL) { + ESP_LOGE(TAG, "Failed to get version object"); + cJSON_Delete(root); + return false; + } + cJSON *url = cJSON_GetObjectItem(firmware, "url"); + if (url == NULL) { + ESP_LOGE(TAG, "Failed to get url object"); + cJSON_Delete(root); + return false; + } + + firmware_version_ = version->valuestring; + firmware_url_ = url->valuestring; + + // 解析设备角色字段 - 严格校验模式 + bool role_matched = false;// 角色匹配标志 + std::string server_role = "";// 服务端角色 + + cJSON *role = cJSON_GetObjectItem(firmware, "role");// 获取 服务端角色字段 + if (role != NULL && cJSON_IsString(role)) {// 服务端角色字段存在且为字符串类型 + server_role = role->valuestring;// 服务端角色赋值 + ESP_LOGI(TAG, "Server role: %s, Device role: %s", server_role.c_str(), CONFIG_DEVICE_ROLE);// 日志记录服务端角色和设备角色 + + if (server_role == CONFIG_DEVICE_ROLE) {// 服务端角色与设备角色匹配 + role_matched = true; // 角色匹配标志设为true + ESP_LOGI(TAG, "Role verification passed: %s", CONFIG_DEVICE_ROLE);//角色验证通过! + } else { + ESP_LOGW(TAG, "Role mismatch (Device:%s vs Server:%s), upgrade denied", CONFIG_DEVICE_ROLE, server_role.c_str());//角色不匹配,OTA升级被拒绝 + } + } else { + ESP_LOGW(TAG, "服务端响应中没有角色字段,OTA升级被拒绝");//服务端响应中没有角色字段,OTA升级被拒绝 + } + + // 双重校验:角色匹配 + 版本检查 + has_new_version_ = false; // 默认无可用更新 + + if (role_matched) {// 角色匹配标志位 为真时才进行版本检查 + bool version_available = IsNewVersionAvailable(current_version_, firmware_version_);//检查当前版本是否比服务端版本新 + if (version_available) { + has_new_version_ = true; + ESP_LOGI(TAG, "✓ Role matched & New version available: %s -> %s", current_version_.c_str(), firmware_version_.c_str());//角色匹配且有新的版本可用 + } else { + ESP_LOGI(TAG, "✓ Role matched but current version is latest: %s", current_version_.c_str());//角色匹配但当前版本已是最新 + } + } else { + ESP_LOGW(TAG, "✗ Upgrade conditions not met - Role: %s, Version check: skipped", + role_matched ? "✓" : "✗");//升级条件未满足 - 角色:%s,版本检查:跳过 + } + + cJSON_Delete(root); + return true; +} + +void Ota::MarkCurrentVersionValid() { + auto partition = esp_ota_get_running_partition(); + if (strcmp(partition->label, "factory") == 0) { + ESP_LOGI(TAG, "Running from factory partition, skipping"); + return; + } + + ESP_LOGI(TAG, "Running partition: %s", partition->label); + esp_ota_img_states_t state; + if (esp_ota_get_state_partition(partition, &state) != ESP_OK) { + ESP_LOGE(TAG, "Failed to get state of partition"); + return; + } + + if (state == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGI(TAG, "Marking firmware as valid"); + esp_ota_mark_app_valid_cancel_rollback(); + } +} + +void Ota::Upgrade(const std::string& firmware_url) { + ESP_LOGI(TAG, "Upgrading firmware from %s", firmware_url.c_str()); + esp_ota_handle_t update_handle = 0; + auto update_partition = esp_ota_get_next_update_partition(NULL); + if (update_partition == NULL) { + ESP_LOGE(TAG, "Failed to get update partition"); + return; + } + + ESP_LOGI(TAG, "Writing to partition %s at offset 0x%lx", update_partition->label, update_partition->address); + bool image_header_checked = false; + std::string image_header; + + auto http = Board::GetInstance().CreateHttp(); + if (!http->Open("GET", firmware_url)) { + ESP_LOGE(TAG, "Failed to open HTTP connection"); + delete http; + return; + } + + size_t content_length = http->GetBodyLength(); + if (content_length == 0) { + ESP_LOGE(TAG, "Failed to get content length"); + delete http; + return; + } + + char buffer[512]; + size_t total_read = 0, recent_read = 0; + auto last_calc_time = esp_timer_get_time(); + while (true) { + int ret = http->Read(buffer, sizeof(buffer)); + if (ret < 0) { + ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret)); + delete http; + return; + } + + // Calculate speed and progress every second + recent_read += ret; + total_read += ret; + if (esp_timer_get_time() - last_calc_time >= 1000000 || ret == 0) { + size_t progress = total_read * 100 / content_length; + ESP_LOGI(TAG, "📊 Progress: %zu%% (%zu/%zu), Speed: %zuB/s", progress, total_read, content_length, recent_read); + if (upgrade_callback_) { + upgrade_callback_(progress, recent_read); + } + last_calc_time = esp_timer_get_time(); + recent_read = 0; + } + + if (ret == 0) { + break; + } + + if (!image_header_checked) { + image_header.append(buffer, ret); + if (image_header.size() >= sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) { + esp_app_desc_t new_app_info; + memcpy(&new_app_info, image_header.data() + sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t), sizeof(esp_app_desc_t)); + ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version); + + auto current_version = esp_app_get_description()->version; + if (memcmp(new_app_info.version, current_version, sizeof(new_app_info.version)) == 0) { + ESP_LOGE(TAG, "Firmware version is the same, skipping upgrade"); + delete http; + return; + } + + if (esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle)) { + esp_ota_abort(update_handle); + delete http; + ESP_LOGE(TAG, "Failed to begin OTA"); + return; + } + + image_header_checked = true; + std::string().swap(image_header); + } + } + auto err = esp_ota_write(update_handle, buffer, ret); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); + esp_ota_abort(update_handle); + delete http; + return; + } + } + delete http; + + esp_err_t err = esp_ota_end(update_handle); + if (err != ESP_OK) { + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); + } else { + ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); + } + return; + } + + err = esp_ota_set_boot_partition(update_partition); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to set boot partition: %s", esp_err_to_name(err)); + return; + } + + ESP_LOGI(TAG, "Firmware upgrade successful, rebooting in 3 seconds..."); + vTaskDelay(pdMS_TO_TICKS(3000)); + esp_restart(); +} + +void Ota::StartUpgrade(std::function callback) { + upgrade_callback_ = callback; + Upgrade(firmware_url_); +} + +std::vector Ota::ParseVersion(const std::string& version) { + std::vector versionNumbers; + std::stringstream ss(version); + std::string segment; + + while (std::getline(ss, segment, '.')) { + try { + int num = std::stoi(segment);// 转换为整数 + versionNumbers.push_back(num);// 存储整数 + } catch (const std::exception& e) { + ESP_LOGE(TAG, "Failed to parse version segment '%s': %s", segment.c_str(), e.what());// 日志记录错误 + versionNumbers.push_back(0); // 出错时使用默认值 + } + } + + return versionNumbers; +} + +bool Ota::IsNewVersionAvailable(const std::string& currentVersion, const std::string& newVersion) { + std::vector current = ParseVersion(currentVersion); + std::vector newer = ParseVersion(newVersion); + + for (size_t i = 0; i < std::min(current.size(), newer.size()); ++i) { + if (newer[i] > current[i]) { + return true; + } else if (newer[i] < current[i]) { + return false; + } + } + + return newer.size() > current.size(); +} diff --git a/main/ota.h b/main/ota.h new file mode 100644 index 0000000..7f7507a --- /dev/null +++ b/main/ota.h @@ -0,0 +1,49 @@ +#ifndef _OTA_H +#define _OTA_H + +#include +#include +#include + +class Ota { +public: + Ota(); + ~Ota(); + + void SetCheckVersionUrl(std::string check_version_url); + void SetHeader(const std::string& key, const std::string& value); + void SetPostData(const std::string& post_data); + bool CheckVersion(); + bool HasNewVersion() { return has_new_version_; } + bool HasMqttConfig() { return has_mqtt_config_; } + bool HasActivationCode() { return has_activation_code_; } + bool HasServerTime() { return has_server_time_; } + void StartUpgrade(std::function callback); + void MarkCurrentVersionValid(); + + const std::string& GetFirmwareVersion() const { return firmware_version_; } + const std::string& GetCurrentVersion() const { return current_version_; } + const std::string& GetActivationMessage() const { return activation_message_; } + const std::string& GetActivationCode() const { return activation_code_; } + +private: + std::string check_version_url_; + std::string activation_message_; + std::string activation_code_; + bool has_new_version_ = false; + bool has_mqtt_config_ = false; + bool has_server_time_ = false; + bool has_activation_code_ = false; + std::string current_version_; + std::string firmware_version_; + std::string firmware_url_; + std::string post_data_; + std::map headers_; + + void Upgrade(const std::string& firmware_url); + std::function upgrade_callback_; + std::vector ParseVersion(const std::string& version); + bool IsNewVersionAvailable(const std::string& currentVersion, const std::string& newVersion); +}; + +#endif // _OTA_H diff --git a/main/protocols/mqtt_protocol.cc b/main/protocols/mqtt_protocol.cc new file mode 100644 index 0000000..81f8432 --- /dev/null +++ b/main/protocols/mqtt_protocol.cc @@ -0,0 +1,304 @@ +#include "mqtt_protocol.h" +#include "board.h" +#include "application.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include "assets/lang_config.h" + +#define TAG "MQTT" + +MqttProtocol::MqttProtocol() { + event_group_handle_ = xEventGroupCreate(); +} + +MqttProtocol::~MqttProtocol() { + ESP_LOGI(TAG, "MqttProtocol deinit"); + if (udp_ != nullptr) { + delete udp_; + } + if (mqtt_ != nullptr) { + delete mqtt_; + } + vEventGroupDelete(event_group_handle_); +} + +void MqttProtocol::Start() { + StartMqttClient(false); +} + +bool MqttProtocol::StartMqttClient(bool report_error) { + if (mqtt_ != nullptr) { + ESP_LOGW(TAG, "Mqtt client already started"); + delete mqtt_; + } + + Settings settings("mqtt", false); + endpoint_ = settings.GetString("endpoint"); + client_id_ = settings.GetString("client_id"); + username_ = settings.GetString("username"); + password_ = settings.GetString("password"); + publish_topic_ = settings.GetString("publish_topic"); + + if (endpoint_.empty()) { + ESP_LOGW(TAG, "MQTT endpoint is not specified"); + if (report_error) { + SetError(Lang::Strings::SERVER_NOT_FOUND); + } + return false; + } + + mqtt_ = Board::GetInstance().CreateMqtt(); + mqtt_->SetKeepAlive(90); + + mqtt_->OnDisconnected([this]() { + ESP_LOGI(TAG, "Disconnected from endpoint"); + }); + + mqtt_->OnMessage([this](const std::string& topic, const std::string& payload) { + cJSON* root = cJSON_Parse(payload.c_str()); + if (root == nullptr) { + ESP_LOGE(TAG, "Failed to parse json message %s", payload.c_str()); + return; + } + cJSON* type = cJSON_GetObjectItem(root, "type"); + if (type == nullptr) { + ESP_LOGE(TAG, "Message type is not specified"); + cJSON_Delete(root); + return; + } + + if (strcmp(type->valuestring, "hello") == 0) { + ParseServerHello(root); + } else if (strcmp(type->valuestring, "goodbye") == 0) { + auto session_id = cJSON_GetObjectItem(root, "session_id"); + ESP_LOGI(TAG, "Received goodbye message, session_id: %s", session_id ? session_id->valuestring : "null"); + if (session_id == nullptr || session_id_ == session_id->valuestring) { + Application::GetInstance().Schedule([this]() { + CloseAudioChannel(); + }); + } + } else if (on_incoming_json_ != nullptr) { + on_incoming_json_(root); + } + cJSON_Delete(root); + last_incoming_time_ = std::chrono::steady_clock::now(); + }); + + ESP_LOGI(TAG, "Connecting to endpoint %s", endpoint_.c_str()); + if (!mqtt_->Connect(endpoint_, 8883, client_id_, username_, password_)) { + ESP_LOGE(TAG, "Failed to connect to endpoint"); + SetError(Lang::Strings::SERVER_NOT_CONNECTED); + return false; + } + + ESP_LOGI(TAG, "Connected to endpoint"); + return true; +} + +void MqttProtocol::SendText(const std::string& text) { + if (publish_topic_.empty()) { + return; + } + if (!mqtt_->Publish(publish_topic_, text)) { + ESP_LOGE(TAG, "Failed to publish message: %s", text.c_str()); + SetError(Lang::Strings::SERVER_ERROR); + } +} + +void MqttProtocol::SendAudio(const std::vector& data) { + std::lock_guard lock(channel_mutex_); + if (udp_ == nullptr) { + return; + } + + std::string nonce(aes_nonce_); + *(uint16_t*)&nonce[2] = htons(data.size()); + *(uint32_t*)&nonce[12] = htonl(++local_sequence_); + + std::string encrypted; + encrypted.resize(aes_nonce_.size() + data.size()); + memcpy(encrypted.data(), nonce.data(), nonce.size()); + + size_t nc_off = 0; + uint8_t stream_block[16] = {0}; + if (mbedtls_aes_crypt_ctr(&aes_ctx_, data.size(), &nc_off, (uint8_t*)nonce.c_str(), stream_block, + (uint8_t*)data.data(), (uint8_t*)&encrypted[nonce.size()]) != 0) { + ESP_LOGE(TAG, "Failed to encrypt audio data"); + return; + } + udp_->Send(encrypted); +} + +void MqttProtocol::CloseAudioChannel() { + { + std::lock_guard lock(channel_mutex_); + if (udp_ != nullptr) { + delete udp_; + udp_ = nullptr; + } + } + + std::string message = "{"; + message += "\"session_id\":\"" + session_id_ + "\","; + message += "\"type\":\"goodbye\""; + message += "}"; + SendText(message); + + if (on_audio_channel_closed_ != nullptr) { + on_audio_channel_closed_(); + } +} + +bool MqttProtocol::OpenAudioChannel() { + if (mqtt_ == nullptr || !mqtt_->IsConnected()) { + ESP_LOGI(TAG, "MQTT is not connected, try to connect now"); + if (!StartMqttClient(true)) { + return false; + } + } + + error_occurred_ = false; + session_id_ = ""; + xEventGroupClearBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT); + + // 发送 hello 消息申请 UDP 通道 + std::string message = "{"; + message += "\"type\":\"hello\","; + message += "\"version\": 3,"; + message += "\"transport\":\"udp\","; + message += "\"audio_params\":{"; + message += "\"format\":\"opus\", \"sample_rate\":16000, \"channels\":1, \"frame_duration\":" + std::to_string(OPUS_FRAME_DURATION_MS); + message += "}}"; + SendText(message); + + // 等待服务器响应 + EventBits_t bits = xEventGroupWaitBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000)); + if (!(bits & MQTT_PROTOCOL_SERVER_HELLO_EVENT)) { + ESP_LOGE(TAG, "Failed to receive server hello"); + SetError(Lang::Strings::SERVER_TIMEOUT); + return false; + } + + std::lock_guard lock(channel_mutex_); + if (udp_ != nullptr) { + delete udp_; + } + udp_ = Board::GetInstance().CreateUdp(); + udp_->OnMessage([this](const std::string& data) { + if (data.size() < sizeof(aes_nonce_)) { + ESP_LOGE(TAG, "Invalid audio packet size: %zu", data.size()); + return; + } + if (data[0] != 0x01) { + ESP_LOGE(TAG, "Invalid audio packet type: %x", data[0]); + return; + } + uint32_t sequence = ntohl(*(uint32_t*)&data[12]); + if (sequence < remote_sequence_) { + ESP_LOGW(TAG, "Received audio packet with old sequence: %lu, expected: %lu", sequence, remote_sequence_); + return; + } + if (sequence != remote_sequence_ + 1) { + ESP_LOGW(TAG, "Received audio packet with wrong sequence: %lu, expected: %lu", sequence, remote_sequence_ + 1); + } + + std::vector decrypted; + size_t decrypted_size = data.size() - aes_nonce_.size(); + size_t nc_off = 0; + uint8_t stream_block[16] = {0}; + decrypted.resize(decrypted_size); + auto nonce = (uint8_t*)data.data(); + auto encrypted = (uint8_t*)data.data() + aes_nonce_.size(); + int ret = mbedtls_aes_crypt_ctr(&aes_ctx_, decrypted_size, &nc_off, nonce, stream_block, encrypted, (uint8_t*)decrypted.data()); + if (ret != 0) { + ESP_LOGE(TAG, "Failed to decrypt audio data, ret: %d", ret); + return; + } + if (on_incoming_audio_ != nullptr) { + on_incoming_audio_(std::move(decrypted)); + } + remote_sequence_ = sequence; + last_incoming_time_ = std::chrono::steady_clock::now(); + }); + + udp_->Connect(udp_server_, udp_port_); + + if (on_audio_channel_opened_ != nullptr) { + on_audio_channel_opened_(); + } + return true; +} + +void MqttProtocol::ParseServerHello(const cJSON* root) { + auto transport = cJSON_GetObjectItem(root, "transport"); + if (transport == nullptr || strcmp(transport->valuestring, "udp") != 0) { + ESP_LOGE(TAG, "Unsupported transport: %s", transport->valuestring); + return; + } + + auto session_id = cJSON_GetObjectItem(root, "session_id"); + if (session_id != nullptr) { + session_id_ = session_id->valuestring; + ESP_LOGI(TAG, "Session ID: %s", session_id_.c_str()); + } + + // Get sample rate from hello message + auto audio_params = cJSON_GetObjectItem(root, "audio_params"); + if (audio_params != NULL) { + auto sample_rate = cJSON_GetObjectItem(audio_params, "sample_rate"); + if (sample_rate != NULL) { + server_sample_rate_ = sample_rate->valueint; + } + auto frame_duration = cJSON_GetObjectItem(audio_params, "frame_duration"); + if (frame_duration != NULL) { + server_frame_duration_ = frame_duration->valueint; + } + } + + auto udp = cJSON_GetObjectItem(root, "udp"); + if (udp == nullptr) { + ESP_LOGE(TAG, "UDP is not specified"); + return; + } + udp_server_ = cJSON_GetObjectItem(udp, "server")->valuestring; + udp_port_ = cJSON_GetObjectItem(udp, "port")->valueint; + auto key = cJSON_GetObjectItem(udp, "key")->valuestring; + auto nonce = cJSON_GetObjectItem(udp, "nonce")->valuestring; + + // auto encryption = cJSON_GetObjectItem(udp, "encryption")->valuestring; + // ESP_LOGI(TAG, "UDP server: %s, port: %d, encryption: %s", udp_server_.c_str(), udp_port_, encryption); + aes_nonce_ = DecodeHexString(nonce); + mbedtls_aes_init(&aes_ctx_); + mbedtls_aes_setkey_enc(&aes_ctx_, (const unsigned char*)DecodeHexString(key).c_str(), 128); + local_sequence_ = 0; + remote_sequence_ = 0; + xEventGroupSetBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT); +} + +static const char hex_chars[] = "0123456789ABCDEF"; +// 辅助函数,将单个十六进制字符转换为对应的数值 +static inline uint8_t CharToHex(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return 0; // 对于无效输入,返回0 +} + +std::string MqttProtocol::DecodeHexString(const std::string& hex_string) { + std::string decoded; + decoded.reserve(hex_string.size() / 2); + for (size_t i = 0; i < hex_string.size(); i += 2) { + char byte = (CharToHex(hex_string[i]) << 4) | CharToHex(hex_string[i + 1]); + decoded.push_back(byte); + } + return decoded; +} + +bool MqttProtocol::IsAudioChannelOpened() const { + return udp_ != nullptr && !error_occurred_ && !IsTimeout();// 检查音频通道是否已打开 +} diff --git a/main/protocols/mqtt_protocol.h b/main/protocols/mqtt_protocol.h new file mode 100644 index 0000000..d7253fe --- /dev/null +++ b/main/protocols/mqtt_protocol.h @@ -0,0 +1,61 @@ +#ifndef MQTT_PROTOCOL_H +#define MQTT_PROTOCOL_H + + +#include "protocol.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define MQTT_PING_INTERVAL_SECONDS 90 +#define MQTT_RECONNECT_INTERVAL_MS 10000 + +#define MQTT_PROTOCOL_SERVER_HELLO_EVENT (1 << 0) + +class MqttProtocol : public Protocol { +public: + MqttProtocol(); + ~MqttProtocol(); + + void Start() override; + void SendAudio(const std::vector& data) override; + bool OpenAudioChannel() override; + void CloseAudioChannel() override; + bool IsAudioChannelOpened() const override; + +private: + EventGroupHandle_t event_group_handle_; + + std::string endpoint_; + std::string client_id_; + std::string username_; + std::string password_; + std::string publish_topic_; + + std::mutex channel_mutex_; + Mqtt* mqtt_ = nullptr; + Udp* udp_ = nullptr; + mbedtls_aes_context aes_ctx_; + std::string aes_nonce_; + std::string udp_server_; + int udp_port_; + uint32_t local_sequence_; + uint32_t remote_sequence_; + + bool StartMqttClient(bool report_error=false); + void ParseServerHello(const cJSON* root); + std::string DecodeHexString(const std::string& hex_string); + + void SendText(const std::string& text) override; +}; + + +#endif // MQTT_PROTOCOL_H diff --git a/main/protocols/protocol.cc b/main/protocols/protocol.cc new file mode 100644 index 0000000..6a3a419 --- /dev/null +++ b/main/protocols/protocol.cc @@ -0,0 +1,151 @@ +#include "protocol.h" + +#include + +#define TAG "Protocol" + +void Protocol::OnIncomingJson(std::function callback) { + on_incoming_json_ = callback; +} + +void Protocol::OnIncomingAudio(std::function&& data)> callback) { + on_incoming_audio_ = callback; +} + +void Protocol::OnAudioChannelOpened(std::function callback) { + on_audio_channel_opened_ = callback; +} + +void Protocol::OnAudioChannelClosed(std::function callback) { + on_audio_channel_closed_ = callback;// 设置音频通道关闭回调 +} + +void Protocol::OnNetworkError(std::function callback) { + on_network_error_ = callback; +} + +void Protocol::OnBotMessage(std::function callback) { + on_bot_message_ = callback; +} + +void Protocol::SetError(const std::string& message) { + error_occurred_ = true; + if (on_network_error_ != nullptr) { + on_network_error_(message); + } +} + +void Protocol::SendAbortSpeaking(AbortReason reason) { + std::string message = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"abort\""; + if (reason == kAbortReasonWakeWordDetected) { + message += ",\"reason\":\"wake_word_detected\""; + } else if (reason == kAbortReasonVoiceInterrupt) { + message += ",\"reason\":\"voice_interrupt\""; + } + message += "}"; + SendText(message); +} + +// 发送故事请求 【新增】 +void Protocol::SendStoryRequest() { + std::string json = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"story\"}";// 构建故事请求 json 消息 + ESP_LOGI(TAG, "Sending story request JSON: %s", json.c_str()); // 打印测试 + SendText(json);// 向服务器发送 json消息 +} + +void Protocol::SendWakeWordDetected(const std::string& wake_word) { + std::string json = "{\"session_id\":\"" + session_id_ + + "\",\"type\":\"listen\",\"state\":\"detect\",\"text\":\"" + wake_word + "\"}"; + SendText(json); +} + +void Protocol::SendStartListening(ListeningMode mode) { + std::string message = "{"; + if (!session_id_.empty()) { + message += "\"session_id\":\"" + session_id_ + "\","; + } + message += "\"type\":\"listen\",\"state\":\"start\""; + if (mode == kListeningModeRealtime) { + message += ",\"mode\":\"realtime\""; + } else if (mode == kListeningModeAutoStop) { + message += ",\"mode\":\"auto\""; + } else { + message += ",\"mode\":\"manual\""; + } + message += "}"; + ESP_LOGI(TAG, "SendStartListening: mode=%d session_id=%s", (int)mode, session_id_.c_str()); + SendText(message); +} + +void Protocol::SendStopListening() { + std::string message = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"listen\",\"state\":\"stop\"}"; + SendText(message);// 向服务器发送 json消息 +} + +void Protocol::SendTextMessage(const std::string& text) { + std::string json = "{\"session_id\":\"" + session_id_ + + "\",\"type\":\"listen\",\"state\":\"detect\",\"text\":\"" + text + "\"}"; + SendText(json); +} + + +void Protocol::SendIotDescriptors(const std::string& descriptors) { + cJSON* root = cJSON_Parse(descriptors.c_str()); + if (root == nullptr) { + ESP_LOGE(TAG, "Failed to parse IoT descriptors: %s", descriptors.c_str()); + return; + } + + if (!cJSON_IsArray(root)) { + ESP_LOGE(TAG, "IoT descriptors should be an array"); + cJSON_Delete(root); + return; + } + + int arraySize = cJSON_GetArraySize(root); + for (int i = 0; i < arraySize; ++i) { + cJSON* descriptor = cJSON_GetArrayItem(root, i); + if (descriptor == nullptr) { + ESP_LOGE(TAG, "Failed to get IoT descriptor at index %d", i); + continue; + } + + cJSON* messageRoot = cJSON_CreateObject(); + cJSON_AddStringToObject(messageRoot, "session_id", session_id_.c_str()); + cJSON_AddStringToObject(messageRoot, "type", "iot"); + cJSON_AddBoolToObject(messageRoot, "update", true); + + cJSON* descriptorArray = cJSON_CreateArray(); + cJSON_AddItemToArray(descriptorArray, cJSON_Duplicate(descriptor, 1)); + cJSON_AddItemToObject(messageRoot, "descriptors", descriptorArray); + + char* message = cJSON_PrintUnformatted(messageRoot); + if (message == nullptr) { + ESP_LOGE(TAG, "Failed to print JSON message for IoT descriptor at index %d", i); + cJSON_Delete(messageRoot); + continue; + } + + SendText(std::string(message)); + cJSON_free(message); + cJSON_Delete(messageRoot); + } + + cJSON_Delete(root); +} + +void Protocol::SendIotStates(const std::string& states) { + std::string message = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"iot\",\"update\":true,\"states\":" + states + "}"; + SendText(message); +} + +bool Protocol::IsTimeout() const { + const int kTimeoutSeconds = 120; + auto now = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast(now - last_incoming_time_); + bool timeout = duration.count() > kTimeoutSeconds; + if (timeout) { + ESP_LOGE(TAG, "Channel timeout %lld seconds", duration.count()); + } + return timeout; +} diff --git a/main/protocols/protocol.h b/main/protocols/protocol.h new file mode 100644 index 0000000..7e9c635 --- /dev/null +++ b/main/protocols/protocol.h @@ -0,0 +1,97 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#include +#include +#include +#include + +struct BinaryProtocol3 { + uint8_t type; + uint8_t reserved; + uint16_t payload_size; + uint8_t payload[]; +} __attribute__((packed)); + +enum AbortReason { + kAbortReasonNone, + kAbortReasonWakeWordDetected, + kAbortReasonVoiceInterrupt + //kAbortReasonNewStory // websocket推送新故事时中断当前播放 +}; + +enum ListeningMode { + kListeningModeAutoStop, + kListeningModeManualStop, + kListeningModeRealtime // 需要 AEC 支持 +}; + +class Protocol { +public: + virtual ~Protocol() = default; + + inline int server_sample_rate() const { + return server_sample_rate_; + } + inline int server_frame_duration() const { + return server_frame_duration_; + } + inline bool downlink_is_pcm() const { + return downlink_is_pcm_; + } + inline const std::string& session_id() const { + return session_id_; + } + inline void SetSuppressIncomingMessageLog(bool v) { suppress_incoming_message_log_ = v; } + + void OnIncomingAudio(std::function&& data)> callback); + void OnIncomingJson(std::function callback); + void OnAudioChannelOpened(std::function callback); + void OnAudioChannelClosed(std::function callback); + void OnNetworkError(std::function callback); + void OnBotMessage(std::function callback); + + virtual void Start() = 0; + virtual bool OpenAudioChannel() = 0; + virtual void CloseAudioChannel() = 0; + virtual bool IsAudioChannelOpened() const = 0; + virtual void SendAudio(const std::vector& data) = 0; + virtual void SendPcm(const std::vector& data) {} + virtual void SendG711A(const std::vector& data) {} + virtual void SendWakeWordDetected(const std::string& wake_word); + virtual void SendStartListening(ListeningMode mode); + virtual void SendStopListening(); + virtual void SendAbortSpeaking(AbortReason reason); + virtual void SendTextMessage(const std::string& text); + virtual void SendStoryRequest(); // 声明 发送讲故事请求 【新增】 + virtual void SendIotDescriptors(const std::string& descriptors); + virtual void SendIotStates(const std::string& states); + virtual void SendFunctionResult(const std::string& tool_call_id, const std::string& content) { + (void)tool_call_id; + SendTextMessage(content); + } + +protected: + std::function on_incoming_json_; + std::function&& data)> on_incoming_audio_; + std::function on_audio_channel_opened_; + std::function on_audio_channel_closed_; + std::function on_network_error_; + std::function on_bot_message_; + + int server_sample_rate_ = 24000; + int server_frame_duration_ = 60; + bool downlink_is_pcm_ = false;// 是否是PCM格式 + bool error_occurred_ = false; + std::string session_id_; + bool start_listening_pending_ = false;// 是否有待处理的监听请求 + ListeningMode pending_listening_mode_ = kListeningModeRealtime;// 待处理的监听模式 + std::chrono::time_point last_incoming_time_; + bool suppress_incoming_message_log_ = false; + + virtual void SendText(const std::string& text) = 0; + virtual void SetError(const std::string& message); + virtual bool IsTimeout() const; +}; + +#endif // PROTOCOL_H diff --git a/main/protocols/volc_rtc_protocol.cc b/main/protocols/volc_rtc_protocol.cc new file mode 100644 index 0000000..6a66609 --- /dev/null +++ b/main/protocols/volc_rtc_protocol.cc @@ -0,0 +1,853 @@ +#include "volc_rtc_protocol.h" +#include +#include "esp_log.h" +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_heap_caps.h" +#include "esp_system.h" +#include +#include +#include +#include +#include +#include +// 新增包含 system_info.h 头文件以使用 SystemInfo 类 +#include "system_info.h" +#include "application.h" +// SNTP is initialized in WiFi board after network is up; no duplicate init here +#include "base/volc_device_manager.h" +#include "settings.h" + +static const char* TAG = "VolcRtcProtocol"; + +VolcRtcProtocol::VolcRtcProtocol() { + event_group_handle_ = xEventGroupCreate(); +} + +VolcRtcProtocol::~VolcRtcProtocol() { + if (event_group_handle_) { + vEventGroupDelete(event_group_handle_); + } + if (rtc_handle_) { + volc_rtc_stop(rtc_handle_); + volc_rtc_destroy(rtc_handle_); + } + // 释放动态分配的设备名称内存 + if (iot_info_.device_name && iot_info_.device_name != (char*)CONFIG_VOLC_DEVICE_NAME) { + free(iot_info_.device_name); + iot_info_.device_name = nullptr; + } +} + +void VolcRtcProtocol::Start() { + ESP_LOGI(TAG, "VolcRtcProtocol 开始启动...");// VolcRtcProtocol 开始启动... + esp_log_level_set(TAG, ESP_LOG_DEBUG); + + // 注释掉所有文件系统相关操作,避免设备重启 + // 这些操作需要文件系统支持,但当前设备可能没有正确挂载文件系统 + // ESP_LOGI(TAG, "跳过文件系统操作以防止设备重启");// 跳过文件系统操作以防止设备重启 + // TODO: Implement proper file system initialization if file logging is needed + + // 禁用获取当前工作目录的操作,避免文件系统访问 + // TODO: Re-enable if filesystem is properly initialized + // ESP_LOGI(TAG, "当前工作目录检查已禁用,以防止文件系统访问");// 当前工作目录检查已禁用,以防止文件系统访问 + + // 如果已有RTC实例,先停止并销毁 + if (rtc_handle_) { + volc_rtc_stop(rtc_handle_); + volc_rtc_destroy(rtc_handle_); + rtc_handle_ = nullptr; + } + + // 创建火山RTC配置 + cJSON* config = cJSON_CreateObject(); + if (!config) { + ESP_LOGE(TAG, "RTC配置创建失败");// RTC配置创建失败 + SetError("Failed to create RTC config"); + return; + } + + // 添加必要的RTC配置项 + cJSON* audio_config = cJSON_CreateObject(); + if (audio_config) { + cJSON_AddBoolToObject(audio_config, "publish", true); + cJSON_AddBoolToObject(audio_config, "subscribe", true); + cJSON_AddNumberToObject(audio_config, "codec", 4); // 设置音频编解码器为4(根据设计文档) + cJSON_AddItemToObject(config, "audio", audio_config);// 添加音频配置到RTC配置 + } + + cJSON* video_config = cJSON_CreateObject(); + if (video_config) { + cJSON_AddBoolToObject(video_config, "publish", false); + cJSON_AddBoolToObject(video_config, "subscribe", false); + cJSON_AddNumberToObject(video_config, "codec", 1); // 设置视频编解码器为1(根据设计文档) + cJSON_AddItemToObject(config, "video", video_config); + } + + cJSON_AddNumberToObject(config, "log_level", 1); // 设置日志级别 + + // 添加参数数组,与 Airhub_Rtc_h 项目保持一致 + cJSON* params = cJSON_CreateArray(); + if (params) { + // 只输出日志到控制台,不输出到文件 + cJSON_AddItemToArray(params, cJSON_CreateString("{\"debug\":{\"log_to_console\":1}}"));// 添加日志到控制台配置 + cJSON_AddItemToArray(params, cJSON_CreateString("{\"audio\":{\"codec\":{\"internal\":{\"enable\":1}}}}"));// 添加音频编解码器内部配置,启用 SDK 内部编解码 + cJSON_AddItemToArray(params, cJSON_CreateString("{\"rtc\":{\"access\":{\"concurrent_requests\":1}}}"));// 添加RTC并发请求配置 + cJSON_AddItemToArray(params, cJSON_CreateString("{\"rtc\":{\"ice\":{\"concurrent_agents\":1}}}"));// 添加RTC并发ICE代理配置 + cJSON_AddItemToObject(config, "params", params); + } + + // 创建IoT信息并优先从NVS加载 + memset(&iot_info_, 0, sizeof(iot_info_)); + iot_info_.instance_id = (char*)CONFIG_VOLC_INSTANCE_ID; + iot_info_.product_key = (char*)CONFIG_VOLC_PRODUCT_KEY; + iot_info_.product_secret = (char*)CONFIG_VOLC_PRODUCT_SECRET; + iot_info_.bot_id = (char*)CONFIG_VOLC_BOT_ID; + + // 优先使用配置文件中的设备名称,如果为空则使用MAC地址 + if (CONFIG_VOLC_DEVICE_NAME && strlen(CONFIG_VOLC_DEVICE_NAME) > 0) { + // 使用配置文件中的设备名称 + iot_info_.device_name = (char*)CONFIG_VOLC_DEVICE_NAME; + ESP_LOGI(TAG, "使用配置文件中的设备名称: %s", iot_info_.device_name); + } else { + // 配置文件中的设备名称为空,使用蓝牙MAC地址作为设备名称 + std::string mac_address = SystemInfo::GetBleMacAddress(); + // MAC地址中替换冒号为下划线,避免文件名中包含冒号 + std::replace(mac_address.begin(), mac_address.end(), ':', '_'); + char* mac_buffer = (char*)malloc(mac_address.length() + 1); + strcpy(mac_buffer, mac_address.c_str()); + iot_info_.device_name = mac_buffer; + ESP_LOGI(TAG, "使用蓝牙MAC地址作为设备名称(已替换冒号为下划线): %s", iot_info_.device_name); + } + + Settings s("volc"); + auto saved_name = s.GetString("device_name", ""); + bool name_mismatch = (!saved_name.empty() && strcmp(saved_name.c_str(), iot_info_.device_name) != 0); + std::string saved_secret; + std::string saved_appid; + if (name_mismatch) { + ESP_LOGW(TAG, "检测到设备名称变更:%s -> %s,清除旧凭证", saved_name.c_str(), iot_info_.device_name); + Settings sw("volc", true); + sw.EraseKey("device_secret"); + sw.EraseKey("rtc_app_id"); + sw.SetString("device_name", iot_info_.device_name); + } else { + saved_secret = s.GetString("device_secret", ""); + saved_appid = s.GetString("rtc_app_id", ""); + if (saved_name.empty()) { + Settings sw("volc", true); + sw.SetString("device_name", iot_info_.device_name); + } + } + if (!saved_secret.empty()) { + iot_info_.device_secret = strdup(saved_secret.c_str()); + } + if (!saved_appid.empty()) { + iot_info_.rtc_app_id = strdup(saved_appid.c_str()); + } + ESP_LOGI(TAG, "NVS凭证已加载:secret=%d appid=%d device_name=%s, free_heap=%u", + !saved_secret.empty(), !saved_appid.empty(), iot_info_.device_name, + (unsigned)heap_caps_get_free_size(MALLOC_CAP_DEFAULT)); + + // 创建一个结构体来传递参数给任务 + struct InitParams { + VolcRtcProtocol* protocol; + cJSON* config; + }; + + InitParams* init_params = new InitParams(); + init_params->protocol = this; + init_params->config = config; + + // 将设备注册和RTC创建操作移到单独的任务中执行,避免main任务栈溢出 + xTaskCreate([](void* arg) { + InitParams* init_params = static_cast(arg); + VolcRtcProtocol* protocol = init_params->protocol; + cJSON* config = init_params->config; + + // 如果没有设备密钥或RTC应用ID,进行设备注册 + if (!protocol->iot_info_.device_secret || !protocol->iot_info_.rtc_app_id) { + char* device_secret_ptr = nullptr; + if (volc_device_register(&protocol->iot_info_, &device_secret_ptr) != 0 || device_secret_ptr == nullptr) { + ESP_LOGE(TAG, "设备注册失败");// 设备注册失败 + protocol->SetError("Failed to register device"); + cJSON_Delete(config); + delete init_params; + vTaskDelete(NULL); + return; + } + protocol->iot_info_.device_secret = device_secret_ptr; + Settings sw("volc", true); + sw.SetString("device_secret", protocol->iot_info_.device_secret); + if (protocol->iot_info_.rtc_app_id) { + sw.SetString("rtc_app_id", protocol->iot_info_.rtc_app_id); + } + sw.SetString("device_name", protocol->iot_info_.device_name); + } + + // 创建RTC实例 + protocol->rtc_handle_ = volc_rtc_create( + protocol->iot_info_.rtc_app_id ? protocol->iot_info_.rtc_app_id : CONFIG_VOLC_INSTANCE_ID, + protocol, + config, + &MessageCallback, + &DataCallback + ); + cJSON_Delete(config); + delete init_params; + + if (!protocol->rtc_handle_) { + ESP_LOGE(TAG, "RTC实例创建失败");// RTC实例创建失败 + protocol->SetError("Failed to create RTC instance"); + } else { + protocol->iot_ready_ = true; + ESP_LOGI(TAG, "RTC实例已准备就绪;房间加入将在监听状态后执行");// RTC实例已准备就绪;房间加入将在监听状态后执行 + Application::GetInstance().InitializeWebsocketProtocol();// RTC初始化成功后,初始化Websocket协议 + } + + vTaskDelete(NULL); + }, "volc_rtc_init", 16384, init_params, 5, NULL); + + // 注意:此处不再立即创建RTC实例,而是将其推迟到任务中执行 + ESP_LOGI(TAG, "VolcRtcProtocol初始化任务已创建");// VolcRtcProtocol初始化任务已创建 +} + +// 新增:设置AgentConfig配置参数,包含body中的config参数和agent_config参数 +void VolcRtcProtocol::SetAgentConfig(const std::string& params) { + extra_params_ = params; + ESP_LOGI(TAG, "设置Agent配置参数: %s", extra_params_.c_str()); +} + +// 🔊 发送音频数据到RTC +void VolcRtcProtocol::SendAudio(const std::vector& data) { + if (!rtc_handle_ || !is_connected_ || !is_audio_channel_opened_) { + ESP_LOGW(TAG, "无法发送音频:RTC未准备就绪");// 无法发送音频:RTC未准备就绪 + return; + } + + std::lock_guard lock(rtc_mutex_); + + volc_data_info_t data_info; + memset(&data_info, 0, sizeof(data_info)); + data_info.type = VOLC_DATA_TYPE_AUDIO; // 音频数据类型 + data_info.info.audio.data_type = VOLC_AUDIO_DATA_TYPE_OPUS; // 格式:OPUS + + // 音频参数应该在RTC初始化时已经设置好,这里只需要发送数据 + int ret = volc_rtc_send(rtc_handle_, data.data(), data.size(), &data_info); + if (ret != 0) { + ESP_LOGE(TAG, "发送音频失败:%d", ret);// 发送音频失败 + } else { + opus_bytes_accum_ += data.size(); + opus_frames_accum_ += 1; + LogUplinkStatsMaybe(); + } +} + +// 🔊 发送PCM音频数据到RTC +void VolcRtcProtocol::SendPcm(const std::vector& data) { + if (!rtc_handle_ || !is_connected_ || !is_audio_channel_opened_) { + ESP_LOGW(TAG, "无法发送音频:RTC未准备就绪"); + return; + } + std::lock_guard lock(rtc_mutex_); + pcm_pending_.insert(pcm_pending_.end(), data.begin(), data.end()); + // 以 20ms 固定帧打包 PCM(8k/16bit/mono),即 320 字节;静音段也持续发送以满足 AEC/RTC 的恒定节拍 + const size_t frame_bytes = (size_t)(8000 * 20 / 1000) * sizeof(int16_t); + size_t offset = 0; + while (offset + frame_bytes <= pcm_pending_.size()) { + volc_data_info_t data_info; + memset(&data_info, 0, sizeof(data_info)); + data_info.type = VOLC_DATA_TYPE_AUDIO; + data_info.info.audio.data_type = VOLC_AUDIO_DATA_TYPE_PCM; + data_info.info.audio.commit = false; + int ret = volc_rtc_send(rtc_handle_, pcm_pending_.data() + offset, frame_bytes, &data_info); + if (ret != 0) { + ESP_LOGE(TAG, "发送音频失败:%d", ret); + break; + } else { + pcm_bytes_accum_ += frame_bytes; + pcm_frames_accum_ += 1; + } + offset += frame_bytes; + } + if (offset > 0) { + pcm_pending_.erase(pcm_pending_.begin(), pcm_pending_.begin() + offset); + } + LogUplinkStatsMaybe(); +} + +// 🔊 发送G711A音频数据到RTC +void VolcRtcProtocol::SendG711A(const std::vector& data) { + if (!rtc_handle_ || !is_connected_ || !is_audio_channel_opened_) { + ESP_LOGW(TAG, "无法发送音频:RTC未准备就绪"); + return; + } + std::lock_guard lock(rtc_mutex_); + g711a_pending_.insert(g711a_pending_.end(), data.begin(), data.end()); + const size_t frame_bytes = 160; + size_t offset = 0; + while (offset + frame_bytes <= g711a_pending_.size()) { + volc_data_info_t data_info; + memset(&data_info, 0, sizeof(data_info)); + data_info.type = VOLC_DATA_TYPE_AUDIO; + data_info.info.audio.data_type = VOLC_AUDIO_DATA_TYPE_G711A; + data_info.info.audio.commit = true; + int ret = volc_rtc_send(rtc_handle_, g711a_pending_.data() + offset, frame_bytes, &data_info); + if (ret != 0) { + ESP_LOGE(TAG, "发送音频失败:%d", ret); + break; + } else { + ESP_LOGI(TAG, "发送上行G711A帧: 大小=%zu", (size_t)frame_bytes); + g711a_bytes_accum_ += frame_bytes; + g711a_frames_accum_ += 1; + } + offset += frame_bytes; + } + if (offset > 0) { + g711a_pending_.erase(g711a_pending_.begin(), g711a_pending_.begin() + offset); + } + LogUplinkStatsMaybe(); +} + +// 🔊 日志上行音频统计 +void VolcRtcProtocol::LogUplinkStatsMaybe() { + uint64_t now_us = esp_timer_get_time(); + if (uplink_last_log_us_ == 0) uplink_last_log_us_ = now_us; + uint64_t diff_us = now_us - uplink_last_log_us_; + if (diff_us >= 2000000) { + uint64_t bps = ((uint64_t)(opus_bytes_accum_ + pcm_bytes_accum_ + g711a_bytes_accum_) * 8 * 1000000ULL) / (diff_us ? diff_us : 1); + ESP_LOGI(TAG, "上行音频统计: PCM帧=%d 字节=%zu, G711A帧=%d 字节=%zu, 速率=%llu bps", + pcm_frames_accum_, (size_t)pcm_bytes_accum_, g711a_frames_accum_, (size_t)g711a_bytes_accum_, (unsigned long long)bps); + ESP_LOGI(TAG, "下行音频统计: PCM字节=%zu, OPUS字节=%zu", + (size_t)down_pcm_bytes_accum_, (size_t)down_opus_bytes_accum_); + opus_bytes_accum_ = 0; + pcm_bytes_accum_ = 0; + g711a_bytes_accum_ = 0; + down_pcm_bytes_accum_ = 0; + down_opus_bytes_accum_ = 0; + opus_frames_accum_ = 0; + pcm_frames_accum_ = 0; + g711a_frames_accum_ = 0; + uplink_last_log_us_ = now_us; + } +} +// 🔊 打开音频通道 +bool VolcRtcProtocol::OpenAudioChannel() { + if (!rtc_handle_) { + ESP_LOGW(TAG, "无法打开音频通道:RTC句柄未准备就绪");// 无法打开音频通道:RTC句柄未准备就绪 + return false; + } + if (!is_connected_) { + if (!iot_ready_) { + ESP_LOGE(TAG, "IoT信息未准备就绪,无法加入房间");// IoT信息未准备就绪,无法加入房间 + ESP_LOGW(TAG, "Diag: app_id=%s device_name=%s bot_id=%s secret=%s", iot_info_.rtc_app_id ? iot_info_.rtc_app_id : "(null)", iot_info_.device_name ? iot_info_.device_name : "(null)", CONFIG_VOLC_BOT_ID, iot_info_.device_secret ? "yes" : "no"); + return false; + } + xEventGroupClearBits(event_group_handle_, 0x1 | 0x2); + // 新增:extra_params 用于传递额外的AgentConfig配置参数 + ESP_LOGI(TAG, "Join RTC: handle=%p bot=%s iot_ready=%d free_heap=%u", rtc_handle_, CONFIG_VOLC_BOT_ID, (int)iot_ready_, (unsigned)heap_caps_get_free_size(MALLOC_CAP_DEFAULT)); + int ret = volc_rtc_start(rtc_handle_, CONFIG_VOLC_BOT_ID, &iot_info_, extra_params_.empty() ? NULL : extra_params_.c_str()); + if (ret != 0) { + ESP_LOGE(TAG, "RTC启动失败:%d", ret);// RTC启动失败:%d + ESP_LOGW(TAG, "Diag: start failed. Possible causes: invalid IoT creds, TLS/HTTP error, network unreachable, time not synced");// 诊断:启动失败可能原因:无效的IoT凭证、TLS/HTTP错误、网络不可达、时间未同步 + return false; + } + EventBits_t bits = xEventGroupWaitBits(event_group_handle_, 0x1, pdFALSE, pdFALSE, pdMS_TO_TICKS(5000)); + ESP_LOGI(TAG, "Wait connect bits=0x%x free_heap=%u", (unsigned)bits, (unsigned)heap_caps_get_free_size(MALLOC_CAP_DEFAULT)); + if ((bits & 0x1) == 0) { + ESP_LOGE(TAG, "RTC连接超时");// RTC连接超时 + ESP_LOGW(TAG, "Diag: check Wi-Fi, SNTP time sync, IoT creds, RTC server availability");// 诊断:检查Wi-Fi、SNTP时间同步、IoT凭证、RTC服务器可用性 + return false; + } + // Do not block audio readiness on remote user join; enable subscribe immediately + bits = xEventGroupWaitBits(event_group_handle_, 0x2, pdFALSE, pdFALSE, pdMS_TO_TICKS(3000)); + if ((bits & 0x2) == 0) { + ESP_LOGW(TAG, "RTC远程用户未加入 yet - 主动开启音频通道");// RTC远程用户未加入 yet - 主动开启音频通道 + // 远程用户未加入时,需要手动设置状态 + server_sample_rate_ = 16000; + server_frame_duration_ = 60; + is_audio_channel_opened_ = true; + first_downlink_logged_ = false; + ESP_LOGI(TAG, "音频通道已打开");// 音频通道已打开 + if (on_audio_channel_opened_) { + on_audio_channel_opened_(); + } + } else { + // 远程用户已加入时,不要重复打印日志,因为MessageCallback中已经处理 + // 但需要确保状态正确设置 + if (!is_audio_channel_opened_) { + server_sample_rate_ = 16000; + server_frame_duration_ = 60; + is_audio_channel_opened_ = true; + first_downlink_logged_ = false; + ESP_LOGI(TAG, "音频通道已打开");// 音频通道已打开 + if (on_audio_channel_opened_) { + on_audio_channel_opened_(); + } + } + } + } + return true; +} +// 🔊 关闭音频通道 +void VolcRtcProtocol::CloseAudioChannel() { + if (!rtc_handle_) { + return; + } + if (is_connected_) { + volc_rtc_stop(rtc_handle_);// 关闭RTC音频通道 + is_connected_ = false;// 标记音频通道已关闭 + } + ESP_LOGI(TAG, "音频通道已关闭");// 音频通道已关闭 + is_audio_channel_opened_ = false;// 标记音频通道已关闭 + if (on_audio_channel_closed_) { + on_audio_channel_closed_();// 调用音频通道关闭回调 + } +} + +// 🔊 检查音频通道是否已打开 +bool VolcRtcProtocol::IsAudioChannelOpened() const { + return is_audio_channel_opened_; +} + +void VolcRtcProtocol::MessageCallback(void* context, volc_msg_t* message) { + VolcRtcProtocol* protocol = static_cast(context); + // 目前只处理简单的连接状态消息 + switch (message->code) { + case VOLC_MSG_CONNECTED: + protocol->is_connected_ = true; + xEventGroupSetBits(protocol->event_group_handle_, 0x1); + protocol->server_sample_rate_ = 16000; + protocol->server_frame_duration_ = 60; + ESP_LOGI(TAG, "RTC连接成功");// RTC连接成功 + //Application::GetInstance().InitializeWebsocketProtocol();// RTC连接成功后初始化Websocket协议 + break; + case VOLC_MSG_DISCONNECTED: + protocol->is_connected_ = false; + protocol->is_audio_channel_opened_ = false; + xEventGroupClearBits(protocol->event_group_handle_, 0x1 | 0x2); + ESP_LOGI(TAG, "RTC断开连接");// RTC断开连接 + break; + case VOLC_MSG_USER_JOINED: + // 只有在音频通道尚未打开的情况下才设置状态和调用回调 + if (!protocol->is_audio_channel_opened_) { + protocol->is_audio_channel_opened_ = true; + xEventGroupSetBits(protocol->event_group_handle_, 0x2); + ESP_LOGI(TAG, "RTC远程用户加入");// RTC远程用户加入 + // Set default decoder parameters before audio starts + protocol->server_sample_rate_ = 16000; + protocol->server_frame_duration_ = 60; + // 调用音频通道打开回调 + if (protocol->on_audio_channel_opened_) { + protocol->on_audio_channel_opened_(); + } + } else { + // 音频通道已经打开,只更新事件标志 + xEventGroupSetBits(protocol->event_group_handle_, 0x2); + ESP_LOGD(TAG, "RTC远程用户加入,音频通道已打开");// 调试信息,不重复打印 + } + break; + case VOLC_MSG_KEY_FRAME_REQ: + // 关键帧请求消息,不需要处理msg字段 + ESP_LOGI(TAG, "接收RTC关键帧请求");// 接收RTC关键帧请求 + break; + case VOLC_MSG_TARGET_BITRATE_CHANGED: + // 目标码率变化消息,使用target_bitrate字段 + // ESP_LOGI(TAG, "RTC target bitrate changed: %lu bps", message->data.target_bitrate); + break; + case VOLC_MSG_CONV_STATUS: + // 会话状态消息,使用conv_status字段 + ESP_LOGI(TAG, "RTC会话状态:%lu", message->data.conv_status); + if (message && message->data.msg && message->data.msg[0] != '\0') { + std::string text(message->data.msg); + ESP_LOGI(TAG, "RTC会话状态消息内容: %s", text.c_str()); + cJSON* root = cJSON_Parse(text.c_str()); + if (root) { + const char* sid_keys[] = {"sessionId", "session_id", "sid"}; + cJSON* sid = nullptr; + for (size_t i = 0; i < sizeof(sid_keys) / sizeof(sid_keys[0]); ++i) { + sid = cJSON_GetObjectItem(root, sid_keys[i]); + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') { + break; + } + sid = nullptr; + } + if (!sid) { + const char* containers[] = {"data", "payload", "context", "session"}; + for (size_t i = 0; i < sizeof(containers) / sizeof(containers[0]); ++i) { + cJSON* obj = cJSON_GetObjectItem(root, containers[i]); + if (obj && cJSON_IsObject(obj)) { + for (size_t j = 0; j < sizeof(sid_keys) / sizeof(sid_keys[0]); ++j) { + sid = cJSON_GetObjectItem(obj, sid_keys[j]); + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') { + break; + } + } + } + if (sid) break; + } + } + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') { + protocol->session_id_ = sid->valuestring; + ESP_LOGI(TAG, "Session ID set: %s", protocol->session_id_.c_str()); + if (protocol->is_audio_channel_opened_ && protocol->start_listening_pending_) { + ListeningMode m = protocol->pending_listening_mode_; + protocol->start_listening_pending_ = false; + protocol->SendStartListening(m); + } + } + if (protocol->on_incoming_json_) { + protocol->on_incoming_json_(root); + } + cJSON_Delete(root); + } + } + break; + default: + ESP_LOGI(TAG, "接收RTC消息:%d", message->code);// 接收RTC消息:%d + if (message && message->data.msg && message->data.msg[0] != '\0') { + std::string text(message->data.msg); + ESP_LOGI(TAG, "RTC消息内容: %s", text.c_str()); + cJSON* root = cJSON_Parse(text.c_str()); + if (root) { + const char* sid_keys[] = {"sessionId", "session_id", "sid"}; + cJSON* sid = nullptr; + for (size_t i = 0; i < sizeof(sid_keys) / sizeof(sid_keys[0]); ++i) { + sid = cJSON_GetObjectItem(root, sid_keys[i]); + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') { + break; + } + sid = nullptr; + } + if (!sid) { + const char* containers[] = {"data", "payload", "context", "session"}; + for (size_t i = 0; i < sizeof(containers) / sizeof(containers[0]); ++i) { + cJSON* obj = cJSON_GetObjectItem(root, containers[i]); + if (obj && cJSON_IsObject(obj)) { + for (size_t j = 0; j < sizeof(sid_keys) / sizeof(sid_keys[0]); ++j) { + sid = cJSON_GetObjectItem(obj, sid_keys[j]); + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') { + break; + } + } + } + if (sid) break; + } + } + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') { + protocol->session_id_ = sid->valuestring; + ESP_LOGI(TAG, "Session ID set: %s", protocol->session_id_.c_str()); + if (protocol->is_audio_channel_opened_ && protocol->start_listening_pending_) { + ListeningMode m = protocol->pending_listening_mode_; + protocol->start_listening_pending_ = false; + protocol->SendStartListening(m); + } + } + if (protocol->on_incoming_json_) { + protocol->on_incoming_json_(root);// 调用回调函数处理JSON消息 + } + cJSON_Delete(root);// 删除JSON根对象,释放内存 + } + } + break; + } +} +// 处理RTC音频数据 +void VolcRtcProtocol::DataCallback(void* context, const void* data, size_t len, volc_data_info_t* info) { + VolcRtcProtocol* protocol = static_cast(context); + // ESP_LOGI(TAG, "RTC data: type=%d len=%u free_heap=%u", info->type, (unsigned)len, (unsigned)heap_caps_get_free_size(MALLOC_CAP_DEFAULT)); + if (info->type == VOLC_DATA_TYPE_AUDIO) { + if (info) { + protocol->downlink_is_pcm_ = (info->info.audio.data_type == VOLC_AUDIO_DATA_TYPE_PCM); + if (protocol->downlink_is_pcm_) { + protocol->down_pcm_bytes_accum_ += len; + protocol->server_sample_rate_ = 8000; + protocol->server_frame_duration_ = 20; + } else { + protocol->down_opus_bytes_accum_ += len; + protocol->server_sample_rate_ = 16000; + protocol->server_frame_duration_ = 60; + } + if (!protocol->first_downlink_logged_) { + ESP_LOGI(TAG, "接收下行音频首包: 类型=%s 大小=%d", protocol->downlink_is_pcm_ ? "PCM" : "OPUS", (int)len);// 接收下行音频首包: 类型=%s 大小=%d + protocol->first_downlink_logged_ = true;// 标记已记录首包 + } + } + protocol->ProcessAudioData(data, len);// 处理音频数据 + } else if (info->type == VOLC_DATA_TYPE_MESSAGE) { + if (data && len > 0) { + const uint8_t* buf = static_cast(data); + std::string json_text; + // 检测二进制前缀格式: [prefix(4字节)] + [json_len(4字节大端)] + [JSON] + // 注意: SDK DataCallback中 is_binary 始终为false,不能依赖此字段 + bool is_subv = false; + if (len >= 8) { + bool is_ctrl = (memcmp(buf, "ctrl", 4) == 0); + bool is_conv = (memcmp(buf, "conv", 4) == 0); + bool is_tool = (memcmp(buf, "tool", 4) == 0); + is_subv = (memcmp(buf, "subv", 4) == 0); + bool is_info = (memcmp(buf, "info", 4) == 0); + if (is_ctrl || is_conv || is_tool || is_subv || is_info) { + uint32_t json_len = (uint32_t)((buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | (buf[7])); + if (json_len > 0 && (size_t)(8 + json_len) <= len) { + json_text.assign(reinterpret_cast(buf + 8), json_len); + // 字幕消息不打印内容(频率高) + if (!is_subv && !protocol->suppress_incoming_message_log_) { + const char* prefix = is_ctrl ? "ctrl" : (is_conv ? "conv" : (is_tool ? "tool" : "info")); + ESP_LOGI(TAG, "接收下行消息(%s): %.*s", prefix, (int)json_text.size(), json_text.c_str()); + } + } + } + } + if (json_text.empty()) { + json_text.assign(reinterpret_cast(data), len); + if (!protocol->suppress_incoming_message_log_) { + ESP_LOGI(TAG, "接收下行消息: %.*s", (int)json_text.size(), json_text.c_str()); + } + } + + // 非subv消息立即通知应用层中止HTTPS播放(尽早触发,不等JSON解析) + // subv字幕消息由应用层subtitle handler处理(可区分USER/AI) + if (!is_subv && protocol->on_bot_message_) { + protocol->on_bot_message_(); + } + + cJSON* root = cJSON_Parse(json_text.c_str()); + if (root) { + // 提取 Session ID(支持多种字段名和嵌套位置) + const char* sid_keys[] = {"sessionId", "session_id", "sid"}; + cJSON* sid = nullptr; + for (size_t i = 0; i < sizeof(sid_keys) / sizeof(sid_keys[0]); ++i) { + sid = cJSON_GetObjectItem(root, sid_keys[i]); + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') break; + sid = nullptr; + } + if (!sid) { + const char* containers[] = {"data", "payload", "context", "session"}; + for (size_t i = 0; i < sizeof(containers) / sizeof(containers[0]); ++i) { + cJSON* obj = cJSON_GetObjectItem(root, containers[i]); + if (obj && cJSON_IsObject(obj)) { + for (size_t j = 0; j < sizeof(sid_keys) / sizeof(sid_keys[0]); ++j) { + sid = cJSON_GetObjectItem(obj, sid_keys[j]); + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') break; + } + } + if (sid) break; + } + } + if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') { + protocol->session_id_ = sid->valuestring; + ESP_LOGI(TAG, "Session ID set: %s", protocol->session_id_.c_str()); + if (protocol->is_audio_channel_opened_ && protocol->start_listening_pending_) { + ListeningMode m = protocol->pending_listening_mode_; + protocol->start_listening_pending_ = false; + protocol->SendStartListening(m); + } + } + if (protocol->on_incoming_json_) { + protocol->on_incoming_json_(root); + } + cJSON_Delete(root); + } + } + } + +} + +// 解析服务器发送的JSON消息 +void VolcRtcProtocol::ParseServerMessage(const char* message) { + ESP_LOGI(TAG, "接收服务器消息:%s", message);// 接收服务器消息:%s + + cJSON* root = cJSON_Parse(message); + if (!root) { + ESP_LOGE(TAG, "解析服务器消息失败");// 解析服务器消息失败 + return; + } + + if (on_incoming_json_) { + on_incoming_json_(root); + } + + cJSON_Delete(root); +} + +void VolcRtcProtocol::ProcessAudioData(const void* data, int size) { + if (!on_incoming_audio_) { + return; + } + + ESP_LOGD(TAG, "接收音频数据,大小:%d 字节", size);// 接收音频数据,大小:%d 字节 + + // 直接使用原始数据指针,避免内存分配 + // 如果on_incoming_audio_需要持久化数据,它应该自己负责复制 + on_incoming_audio_(std::vector(static_cast(data), static_cast(data) + size)); +} + +void VolcRtcProtocol::SendText(const std::string& text) { + if (!rtc_handle_ || !is_connected_) { + ESP_LOGW(TAG, "不能发送文本消息:RTC未准备好");// 不能发送文本消息,RTC未准备好 + return; + } + + std::lock_guard lock(rtc_mutex_); + + volc_data_info_t data_info; + memset(&data_info, 0, sizeof(data_info)); + data_info.type = VOLC_DATA_TYPE_MESSAGE; // 文本数据类型 + + int ret = volc_rtc_send(rtc_handle_, text.data(), text.size(), &data_info); + if (ret != 0) { + ESP_LOGE(TAG, "发送文本消息失败:%d", ret);// 发送文本消息失败:%d + } else { + ESP_LOGD(TAG, "发送文本消息: %s", text.c_str());// 发送文本消息:%s + } +} + +// 🔊 发送开始监听消息到RTC +void VolcRtcProtocol::SendStartListening(ListeningMode mode) { + // 若尚未建立会话ID或远端未加入,则排队,待会话就绪后发送 + if (session_id_.empty() || !is_connected_) { + start_listening_pending_ = true; + pending_listening_mode_ = mode; + ESP_LOGI(TAG, "延迟发送StartListening,等待会话就绪"); + return; + } + + Protocol::SendStartListening(mode);// 调用基类方法发送开始监听消息 +} + +// 🔊 发送控制指令到RTC +void VolcRtcProtocol::SendCtrl(const std::string& json) { + if (!rtc_handle_ || !is_connected_) { + ESP_LOGW(TAG, "不能发送ctrl二进制消息:RTC未准备好");// 不能发送ctrl二进制消息,RTC未准备好 + return; + } + + std::lock_guard lock(rtc_mutex_);// 🔊 发送控制指令到RTC时,加锁保护RTC句柄 + + // 构建二进制消息:"ctrl" + 4字节大端长度 + JSON负载 + const char magic[4] = {'c','t','r','l'}; + const uint32_t len = (uint32_t)json.size(); + std::vector payload; + payload.reserve(4 + 4 + len); + payload.insert(payload.end(), magic, magic + 4); + payload.push_back((uint8_t)((len >> 24) & 0xFF)); + payload.push_back((uint8_t)((len >> 16) & 0xFF)); + payload.push_back((uint8_t)((len >> 8) & 0xFF)); + payload.push_back((uint8_t)(len & 0xFF)); + payload.insert(payload.end(), json.begin(), json.end()); + + volc_data_info_t data_info; + memset(&data_info, 0, sizeof(data_info)); + data_info.type = VOLC_DATA_TYPE_MESSAGE; + data_info.info.message.is_binary = true; + + int ret = volc_rtc_send(rtc_handle_, payload.data(), (int)payload.size(), &data_info); + if (ret != 0) { + ESP_LOGE(TAG, "发送ctrl二进制消息失败:%d", ret); + } else { + ESP_LOGD(TAG, "发送ctrl二进制消息: %.*s", (int)json.size(), json.c_str()); + } +} + +// 🔊 发送函数调用指令到RTC +void VolcRtcProtocol::SendFunc(const std::string& json) { + if (!rtc_handle_ || !is_connected_) { + ESP_LOGW(TAG, "不能发送func二进制消息:RTC未准备好");// 不能发送func二进制消息,RTC未准备好 + return; + } + + std::lock_guard lock(rtc_mutex_);// 🔊 发送函数调用指令到RTC时,加锁保护RTC句柄 + + const char magic[4] = {'f','u','n','c'}; + const uint32_t len = (uint32_t)json.size(); + std::vector payload; + payload.reserve(4 + 4 + len); + payload.insert(payload.end(), magic, magic + 4); + payload.push_back((uint8_t)((len >> 24) & 0xFF)); + payload.push_back((uint8_t)((len >> 16) & 0xFF)); + payload.push_back((uint8_t)((len >> 8) & 0xFF)); + payload.push_back((uint8_t)(len & 0xFF)); + payload.insert(payload.end(), json.begin(), json.end()); + + volc_data_info_t data_info; + memset(&data_info, 0, sizeof(data_info)); + data_info.type = VOLC_DATA_TYPE_MESSAGE; + data_info.info.message.is_binary = true; + + int ret = volc_rtc_send(rtc_handle_, payload.data(), (int)payload.size(), &data_info); + if (ret != 0) { + ESP_LOGE(TAG, "发送func二进制消息失败:%d", ret); + } else { + ESP_LOGD(TAG, "发送func二进制消息: %.*s", (int)json.size(), json.c_str()); + } +} + +// 🔊 发送函数调用结果到RTC +void VolcRtcProtocol::SendFunctionResult(const std::string& tool_call_id, const std::string& content) { + cJSON* obj = cJSON_CreateObject(); + if (!obj) { + ESP_LOGE(TAG, "创建函数结果JSON失败,回退为文本");// 创建函数结果JSON失败,回退为文本 + Protocol::SendFunctionResult(tool_call_id, content); + return; + } + cJSON_AddStringToObject(obj, "ToolCallID", tool_call_id.c_str());// 添加函数调用ID到JSON + cJSON_AddStringToObject(obj, "Content", content.c_str());// 添加函数调用结果到JSON + char* printed = cJSON_PrintUnformatted(obj); + std::string json = printed ? printed : std::string(); + if (printed) cJSON_free(printed); + cJSON_Delete(obj); + if (json.empty()) { + ESP_LOGW(TAG, "函数结果JSON为空,回退为文本"); + Protocol::SendFunctionResult(tool_call_id, content); + return; + } + SendFunc(json); +} + +// 🔊 发送文本消息到RTC (传入大模型上下文信息) +void VolcRtcProtocol::SendTextMessage(const std::string& text) { + // 按官方方案封装:ExternalTextToLLM,确保进入LLM并触发TTS + cJSON* root = cJSON_CreateObject(); + if (!root) { + ESP_LOGE(TAG, "创建JSON失败,回退为文本消息"); + Protocol::SendTextMessage(text); + return; + } + cJSON_AddStringToObject(root, "Command", "ExternalTextToLLM"); + cJSON_AddStringToObject(root, "Message", text.c_str()); + cJSON_AddNumberToObject(root, "InterruptMode", 1); + char* printed = cJSON_PrintUnformatted(root); + std::string json = printed ? printed : std::string(); + if (printed) cJSON_free(printed); + cJSON_Delete(root); + + if (json.empty()) { + ESP_LOGW(TAG, "生成的JSON为空,回退为文本消息"); + Protocol::SendTextMessage(text); + return; + } + + SendCtrl(json); +} + +// 🔊 发送中止通话请求 +void VolcRtcProtocol::SendAbortSpeaking(AbortReason reason) { + if (!rtc_handle_ || !is_connected_ || !is_audio_channel_opened_) { + ESP_LOGW(TAG, "不能发送中止通话请求:RTC未准备好");// 不能发送打断请求,RTC未准备好 + return; + } + + std::lock_guard lock(rtc_mutex_);// 🔊 发送中止通话请求时,加锁保护RTC句柄 + + ESP_LOGI(TAG, "通过Volc RTC中断发送中止通话请求!");// 发送打断请求,通过火山RTC中断 + + // 调用火山RTC的打断API + int ret = volc_rtc_interrupt(rtc_handle_); + if (ret != 0) { + ESP_LOGE(TAG, "通过Volc RTC中断发送打断请求失败:%d", ret);// 发送打断请求,通过火山RTC中断失败:%d + } else { + ESP_LOGI(TAG, "通过Volc RTC中断发送打断请求成功!");// 发送打断请求,通过火山RTC中断成功 + } +} diff --git a/main/protocols/volc_rtc_protocol.h b/main/protocols/volc_rtc_protocol.h new file mode 100644 index 0000000..070f195 --- /dev/null +++ b/main/protocols/volc_rtc_protocol.h @@ -0,0 +1,69 @@ +#ifndef _VOLC_RTC_PROTOCOL_H_ +#define _VOLC_RTC_PROTOCOL_H_ + +#include "protocol.h" +#include "volc_rtc.h" +#include "base/volc_device_manager.h" +#include +#include +#include +#include + +class VolcRtcProtocol : public Protocol { +public: + VolcRtcProtocol(); + ~VolcRtcProtocol(); + + void Start() override; + void SendAudio(const std::vector& data) override;// 🔊 发送音频数据到RTC + void SendPcm(const std::vector& data) override;// 🔊 发送PCM音频数据到RTC + void SendG711A(const std::vector& data) override;// 🔊 发送G711A音频数据到RTC + bool OpenAudioChannel() override;// 🔊 打开音频通道 + void CloseAudioChannel() override;// 🔊 关闭音频通道 + bool IsAudioChannelOpened() const override;// 🔊 检查音频通道是否已打开 + void SendAbortSpeaking(AbortReason reason) override;// 🔊 发送中止通话请求 + void SendStartListening(ListeningMode mode) override;// 🔊 发送开始监听请求 + void SendTextMessage(const std::string& text) override;// 🔊 发送文本消息到RTC + void SendFunctionResult(const std::string& tool_call_id, const std::string& content) override;// 🔊 发送函数调用结果到RTC + + /** + * @brief 设置Agent配置参数(如音色、提示词等) + * @param params JSON格式的配置参数字符串 + */ + void SetAgentConfig(const std::string& params); + +private: + EventGroupHandle_t event_group_handle_; + volc_rtc_t rtc_handle_ = nullptr; + std::mutex rtc_mutex_; + std::string extra_params_; // 存储额外的Agent配置参数 + + bool is_connected_ = false; + bool is_audio_channel_opened_ = false; + bool iot_ready_ = false; + volc_iot_info_t iot_info_ = {}; + size_t opus_bytes_accum_ = 0; + size_t pcm_bytes_accum_ = 0; + size_t g711a_bytes_accum_ = 0; + size_t down_pcm_bytes_accum_ = 0; + size_t down_opus_bytes_accum_ = 0; + int opus_frames_accum_ = 0; + int pcm_frames_accum_ = 0; + int g711a_frames_accum_ = 0; + uint64_t uplink_last_log_us_ = 0; + std::vector pcm_pending_; + std::vector g711a_pending_; + bool first_downlink_logged_ = false; + + static void MessageCallback(void* context, volc_msg_t* message); + static void DataCallback(void* context, const void* data, size_t len, volc_data_info_t* info); + + void ParseServerMessage(const char* message); + void ProcessAudioData(const void* data, int size); + void SendText(const std::string& text) override; + void LogUplinkStatsMaybe();// 打印上传统计信息 + void SendCtrl(const std::string& json);// 🔊 发送控制指令到RTC + void SendFunc(const std::string& json);// 🔊 发送函数调用指令到RTC +}; + +#endif diff --git a/main/protocols/websocket_protocol.cc b/main/protocols/websocket_protocol.cc new file mode 100644 index 0000000..aa37daa --- /dev/null +++ b/main/protocols/websocket_protocol.cc @@ -0,0 +1,322 @@ +#include "websocket_protocol.h" +#include "board.h" +#include "system_info.h" +#include "application.h" +#include "background_task.h" + +#include +#include +#include +#include +#include +#include "assets/lang_config.h" + +#define TAG "WS" + +// 初始化静态成员 +std::atomic WebsocketProtocol::pending_delete_tasks_{0}; + +WebsocketProtocol::WebsocketProtocol() { + event_group_handle_ = xEventGroupCreate(); +} + +// 设置是否为主要连接协议 +void WebsocketProtocol::SetPrimary(bool primary) { + is_primary_ = primary; +} +WebsocketProtocol::~WebsocketProtocol() { + if (websocket_ != nullptr) { + delete websocket_; + } + vEventGroupDelete(event_group_handle_); +} + +void WebsocketProtocol::Start() { +} + +void WebsocketProtocol::SendAudio(const std::vector& data) { + ESP_LOGD(TAG, "WebSocket auxiliary mode: drop uplink audio, bytes=%zu", data.size()); +} + +void WebsocketProtocol::SendText(const std::string& text) { + // 🔧 修复:增强连接状态检查,防止访问无效连接 + if (websocket_ == nullptr) { + ESP_LOGD(TAG, "WebSocket is null, dropping text message: %s", text.c_str()); + return; + } + + // 🔧 双重检查连接状态,防止偶发性连接异常 + if (!websocket_->IsConnected()) { + ESP_LOGD(TAG, "WebSocket not connected, dropping text message: %s", text.c_str()); + return; + } + + // 🔧 再次验证连接有效性(防止偶发性TLS状态异常) + if (!IsAudioChannelOpened()) { + ESP_LOGW(TAG, "Audio channel not properly opened, dropping message: %s", text.c_str()); + return; + } + + // 🔧 添加异常处理,防止TLS层崩溃 + try { + // 验证消息内容有效性 + if (text.empty()) { + ESP_LOGW(TAG, "Attempted to send empty message"); + return; + } + + if (!websocket_->Send(text)) { + ESP_LOGE(TAG, "Failed to send text: %s", text.c_str()); + SetError(Lang::Strings::SERVER_ERROR); + } else { + ESP_LOGD(TAG, "Successfully sent WebSocket message: %s", text.c_str()); + } + } catch (const std::exception& e) { + ESP_LOGE(TAG, "Exception sending text: %s, message: %s", e.what(), text.c_str()); + SetError(Lang::Strings::SERVER_ERROR); + } catch (...) { + ESP_LOGE(TAG, "Unknown exception sending text: %s", text.c_str()); + SetError(Lang::Strings::SERVER_ERROR); + } +} + +bool WebsocketProtocol::IsAudioChannelOpened() const { + if (websocket_ == nullptr) { + return false; + } + + // 🔧 增强连接状态验证:不仅检查IsConnected,还验证实际可用性 + bool basic_check = websocket_->IsConnected() && !error_occurred_ && !IsTimeout(); + + if (!basic_check) { + return false; + } + + // 🔧 额外验证:确保WebSocket真正可用(偶发性保护) + try { + // 这里可以添加轻量级的连接测试,但要避免频繁调用 + return true; + } catch (...) { + ESP_LOGW(TAG, "WebSocket connection validation failed"); + return false; + } +} + +void WebsocketProtocol::CloseAudioChannel() { + std::lock_guard lock(websocket_mutex_); + if (websocket_ != nullptr && !is_being_deleted_) { + // ESP_LOGI(TAG, "🔧 关闭WebSocket连接"); + is_being_deleted_ = true; + + try { + websocket_->Close(); + } catch (const std::exception& e) { + ESP_LOGE(TAG, "WebSocket close failed: %s", e.what()); + } + + auto websocket_to_delete = websocket_; + websocket_ = nullptr; // 立即置空,防止重复访问 + + // 使用更安全的延迟进行异步删除,确保其他线程完成访问 + Application::GetInstance().Schedule([this, websocket_to_delete]() { + vTaskDelay(pdMS_TO_TICKS(50)); + try { + delete websocket_to_delete; + ESP_LOGI(TAG, "🔧 WebSocket已安全删除"); + } catch (const std::exception& e) { + ESP_LOGE(TAG, "WebSocket deletion failed: %s", e.what()); + } + is_being_deleted_ = false; + }); + } +} + +bool WebsocketProtocol::OpenAudioChannel() { + std::lock_guard lock(websocket_mutex_); + if (websocket_ != nullptr && !is_being_deleted_) { + // ESP_LOGI(TAG, "🔧 关闭现有WebSocket连接"); + is_being_deleted_ = true; + + try { + // 🔧 关键修复:清除OnDisconnected回调,防止触发OnAudioChannelClosed + websocket_->OnDisconnected(nullptr); + websocket_->Close(); + } catch (const std::exception& e) { + ESP_LOGE(TAG, "WebSocket close failed during reopen: %s", e.what()); + } + + auto websocket_to_delete = websocket_; + websocket_ = nullptr; // 立即置空,防止重复访问 + + // 增加待删除任务计数 + pending_delete_tasks_++; + + // 使用更安全的异步删除机制 + Application::GetInstance().Schedule([this, websocket_to_delete]() { + vTaskDelay(pdMS_TO_TICKS(200)); // 增加延迟到200ms + try { + delete websocket_to_delete; + ESP_LOGI(TAG, "🔧 旧WebSocket已安全删除,剩余待删除任务: %d", pending_delete_tasks_.load() - 1); + } catch (const std::exception& e) { + ESP_LOGE(TAG, "WebSocket deletion failed: %s", e.what()); + } + pending_delete_tasks_--; // 减少计数 + is_being_deleted_ = false; + }); + + // 短暂延迟让删除任务启动 + vTaskDelay(pdMS_TO_TICKS(150)); + } + + // 如果有太多待删除任务,等待一下 + if (pending_delete_tasks_.load() > 2) { + ESP_LOGW(TAG, "⚠️ 检测到多个待删除任务 (%d),等待清理完成", pending_delete_tasks_.load()); + int wait_count = 0; + while (pending_delete_tasks_.load() > 1 && wait_count < 10) { + vTaskDelay(pdMS_TO_TICKS(100)); + wait_count++; + } + } + + error_occurred_ = false; + std::string url = CONFIG_WEBSOCKET_URL; + std::string token = "Bearer " + std::string(CONFIG_WEBSOCKET_ACCESS_TOKEN); + + // 🔧 添加内存检查和错误处理 + try { + websocket_ = Board::GetInstance().CreateWebSocket(); + if (websocket_ == nullptr) { + ESP_LOGE("WebsocketProtocol", "Failed to create WebSocket - out of memory"); + return false; + } + } catch (const std::exception& e) { + ESP_LOGE("WebsocketProtocol", "Exception creating WebSocket: %s", e.what()); + return false; + } catch (...) { + ESP_LOGE("WebsocketProtocol", "Unknown exception creating WebSocket"); + return false; + } + websocket_->SetHeader("Authorization", token.c_str()); + websocket_->SetHeader("Protocol-Version", "1"); + websocket_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str()); + websocket_->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str()); + + websocket_->OnData([this](const char* data, size_t len, bool binary) { + if (binary) { + if (on_incoming_audio_ != nullptr) { + + + on_incoming_audio_(std::vector((uint8_t*)data, (uint8_t*)data + len));// 接收音频数据 + } + } else { + // Parse JSON data + auto root = cJSON_Parse(data); + + // 添加调试日志:打印接收到的原始JSON数据 + ESP_LOGI(TAG, "🔍 接收到JSON数据: %s", data); + + // 添加调试日志:解析JSON结构 + if (root == NULL) { + ESP_LOGE(TAG, "❌ JSON解析失败,数据格式错误"); + return; + } + + // 打印JSON对象的所有字段 + char* json_string = cJSON_Print(root); + if (json_string != NULL) { + ESP_LOGI(TAG, "📋 解析后的JSON结构: %s", json_string); + free(json_string); + } + + auto type = cJSON_GetObjectItem(root, "type");// + + if (type != NULL) { + ESP_LOGI(TAG, "📝 消息类型: %s", type->valuestring); + if (strcmp(type->valuestring, "hello") == 0) {// 接收服务器hello消息 + ParseServerHello(root);// 解析服务器hello消息 + } else { + if (on_incoming_json_ != nullptr) {// 接收服务器其他消息 + on_incoming_json_(root);// 调用回调函数处理其他消息 + } + } + } else { + ESP_LOGE(TAG, "缺少消息类型, data: %s", data);// 缺少消息类型 + } + cJSON_Delete(root); + } + last_incoming_time_ = std::chrono::steady_clock::now(); + }); + + websocket_->OnDisconnected([this]() { + ESP_LOGI(TAG, "Websocket disconnected"); + auto& app = Application::GetInstance(); +#if CONFIG_USE_AUDIO_PROCESSOR + if (is_primary_) { + app.StopAudioProcessor(); + ESP_LOGI(TAG, "Audio processor stopped immediately"); + } +#endif + + if (on_audio_channel_closed_ != nullptr) { + on_audio_channel_closed_(); + } + }); + + if (!websocket_->Connect(url.c_str())) { + ESP_LOGE(TAG, "Failed to connect to websocket server"); + SetError(Lang::Strings::SERVER_NOT_FOUND); + return false; + } + + // Send hello message to describe the client + // keys: message type, version, audio_params (format, sample_rate, channels) + std::string message = "{"; + message += "\"type\":\"hello\","; + message += "\"version\": 1,"; + message += "\"transport\":\"websocket\","; + message += "\"audio_params\":{"; + message += "\"format\":\"opus\", \"sample_rate\":16000, \"channels\":1, \"frame_duration\":" + std::to_string(OPUS_FRAME_DURATION_MS); + message += "}}"; + websocket_->Send(message); + + // Wait for server hello + EventBits_t bits = xEventGroupWaitBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000)); + if (!(bits & WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT)) { + ESP_LOGE(TAG, "Failed to receive server hello"); + SetError(Lang::Strings::SERVER_TIMEOUT); + return false; + } + + if (on_audio_channel_opened_ != nullptr) { + on_audio_channel_opened_(); + } + + return true; +} + +void WebsocketProtocol::ParseServerHello(const cJSON* root) { + auto transport = cJSON_GetObjectItem(root, "transport"); + if (transport == nullptr || strcmp(transport->valuestring, "websocket") != 0) { + ESP_LOGE(TAG, "Unsupported transport: %s", transport->valuestring); + return; + } + + auto audio_params = cJSON_GetObjectItem(root, "audio_params"); + if (audio_params != NULL) { + auto sample_rate = cJSON_GetObjectItem(audio_params, "sample_rate"); + if (sample_rate != NULL) { + server_sample_rate_ = sample_rate->valueint; + } + auto frame_duration = cJSON_GetObjectItem(audio_params, "frame_duration"); + if (frame_duration != NULL) { + server_frame_duration_ = frame_duration->valueint; + } + } + // 解析session_id + auto sid = cJSON_GetObjectItem(root, "session_id"); + if (sid && cJSON_IsString(sid) && sid->valuestring) { + session_id_ = sid->valuestring;// 保存session_id + } + + xEventGroupSetBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT); +} diff --git a/main/protocols/websocket_protocol.h b/main/protocols/websocket_protocol.h new file mode 100644 index 0000000..778b578 --- /dev/null +++ b/main/protocols/websocket_protocol.h @@ -0,0 +1,38 @@ +#ifndef _WEBSOCKET_PROTOCOL_H_ +#define _WEBSOCKET_PROTOCOL_H_ + + +#include "protocol.h" + +#include +#include +#include +#include + +#define WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT (1 << 0) + +class WebsocketProtocol : public Protocol { +public: + WebsocketProtocol(); + ~WebsocketProtocol(); + + void SetPrimary(bool primary);// 设置是否为主要连接协议 + void Start() override; + void SendAudio(const std::vector& data) override; + bool OpenAudioChannel() override; + void CloseAudioChannel() override; + bool IsAudioChannelOpened() const override; + +private: + EventGroupHandle_t event_group_handle_; + WebSocket* websocket_ = nullptr; + static std::atomic pending_delete_tasks_; // 待删除任务计数 + std::mutex websocket_mutex_; // WebSocket操作互斥锁 + bool is_being_deleted_ = false; // 删除状态标志 + bool is_primary_ = true;// 是否为主要连接协议 + + void ParseServerHello(const cJSON* root); + void SendText(const std::string& text) override; +}; + +#endif diff --git a/main/settings.cc b/main/settings.cc new file mode 100644 index 0000000..bf73b06 --- /dev/null +++ b/main/settings.cc @@ -0,0 +1,99 @@ +#include "settings.h" + +#include +#include + +#define TAG "Settings" + +Settings::Settings(const std::string& ns, bool read_write) : ns_(ns), read_write_(read_write) { + nvs_open(ns.c_str(), read_write_ ? NVS_READWRITE : NVS_READONLY, &nvs_handle_); +} + +Settings::~Settings() { + if (nvs_handle_ != 0) { + if (read_write_ && dirty_) { + ESP_ERROR_CHECK(nvs_commit(nvs_handle_)); + } + nvs_close(nvs_handle_); + } +} + +std::string Settings::GetString(const std::string& key, const std::string& default_value) { + if (nvs_handle_ == 0) { + return default_value; + } + + size_t length = 0; + if (nvs_get_str(nvs_handle_, key.c_str(), nullptr, &length) != ESP_OK) { + return default_value; + } + + std::string value; + value.resize(length); + ESP_ERROR_CHECK(nvs_get_str(nvs_handle_, key.c_str(), value.data(), &length)); + while (!value.empty() && value.back() == '\0') { + value.pop_back(); + } + return value; +} + +void Settings::SetString(const std::string& key, const std::string& value) { + if (read_write_) { + ESP_ERROR_CHECK(nvs_set_str(nvs_handle_, key.c_str(), value.c_str())); + dirty_ = true; + } else { + ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str()); + } +} + +int32_t Settings::GetInt(const std::string& key, int32_t default_value) { + if (nvs_handle_ == 0) { + return default_value; + } + + int32_t value; + if (nvs_get_i32(nvs_handle_, key.c_str(), &value) != ESP_OK) { + return default_value; + } + return value; +} + +void Settings::SetInt(const std::string& key, int32_t value) { + if (read_write_) { + ESP_ERROR_CHECK(nvs_set_i32(nvs_handle_, key.c_str(), value)); + dirty_ = true; + } else { + ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str()); + } +} + +void Settings::EraseKey(const std::string& key) { + if (read_write_) { + auto ret = nvs_erase_key(nvs_handle_, key.c_str()); + if (ret != ESP_ERR_NVS_NOT_FOUND) { + ESP_ERROR_CHECK(ret); + } + } else { + ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str()); + } +} + +void Settings::EraseAll() { + if (read_write_) { + ESP_ERROR_CHECK(nvs_erase_all(nvs_handle_)); + } else { + ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str()); + } +} + +void Settings::Commit() { + if (read_write_ && nvs_handle_ != 0) { + esp_err_t ret = nvs_commit(nvs_handle_); + if (ret == ESP_OK) { + dirty_ = false; + ESP_LOGI(TAG, "Committed NVS namespace %s", ns_.c_str()); + } else { + ESP_LOGE(TAG, "Commit failed for namespace %s (%s)", ns_.c_str(), esp_err_to_name(ret)); + } + } +} diff --git a/main/settings.h b/main/settings.h new file mode 100644 index 0000000..534ada4 --- /dev/null +++ b/main/settings.h @@ -0,0 +1,27 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include + +class Settings { +public: + Settings(const std::string& ns, bool read_write = false); + ~Settings(); + + std::string GetString(const std::string& key, const std::string& default_value = ""); + void SetString(const std::string& key, const std::string& value); + int32_t GetInt(const std::string& key, int32_t default_value = 0); + void SetInt(const std::string& key, int32_t value); + void Commit();// 提交更改 + void EraseKey(const std::string& key);// 删除指定键值对 + void EraseAll();// 删除所有键值对 + +private: + std::string ns_; + nvs_handle_t nvs_handle_ = 0; + bool read_write_ = false; + bool dirty_ = false; +}; + +#endif diff --git a/main/system_info.cc b/main/system_info.cc new file mode 100644 index 0000000..a1ce792 --- /dev/null +++ b/main/system_info.cc @@ -0,0 +1,136 @@ +#include "system_info.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +#define TAG "SystemInfo" + +size_t SystemInfo::GetFlashSize() { + uint32_t flash_size; + if (esp_flash_get_size(NULL, &flash_size) != ESP_OK) { + ESP_LOGE(TAG, "Failed to get flash size"); + return 0; + } + return (size_t)flash_size; +} + +size_t SystemInfo::GetMinimumFreeHeapSize() { + return esp_get_minimum_free_heap_size(); +} + +size_t SystemInfo::GetFreeHeapSize() { + return esp_get_free_heap_size(); +} + +std::string SystemInfo::GetMacAddress() { + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + char mac_str[18]; + snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return std::string(mac_str); +} + +std::string SystemInfo::GetBleMacAddress() { + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_BT); + char mac_str[18]; + snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return std::string(mac_str); +} + +std::string SystemInfo::GetChipModelName() { + return std::string(CONFIG_IDF_TARGET); +} + +esp_err_t SystemInfo::PrintRealTimeStats(TickType_t xTicksToWait) { + #define ARRAY_SIZE_OFFSET 5 + TaskStatus_t *start_array = NULL, *end_array = NULL; + UBaseType_t start_array_size, end_array_size; + configRUN_TIME_COUNTER_TYPE start_run_time, end_run_time; + esp_err_t ret; + uint32_t total_elapsed_time; + + //Allocate array to store current task states + start_array_size = uxTaskGetNumberOfTasks() + ARRAY_SIZE_OFFSET; + start_array = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * start_array_size); + if (start_array == NULL) { + ret = ESP_ERR_NO_MEM; + goto exit; + } + //Get current task states + start_array_size = uxTaskGetSystemState(start_array, start_array_size, &start_run_time); + if (start_array_size == 0) { + ret = ESP_ERR_INVALID_SIZE; + goto exit; + } + + vTaskDelay(xTicksToWait); + + //Allocate array to store tasks states post delay + end_array_size = uxTaskGetNumberOfTasks() + ARRAY_SIZE_OFFSET; + end_array = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * end_array_size); + if (end_array == NULL) { + ret = ESP_ERR_NO_MEM; + goto exit; + } + //Get post delay task states + end_array_size = uxTaskGetSystemState(end_array, end_array_size, &end_run_time); + if (end_array_size == 0) { + ret = ESP_ERR_INVALID_SIZE; + goto exit; + } + + //Calculate total_elapsed_time in units of run time stats clock period. + total_elapsed_time = (end_run_time - start_run_time); + if (total_elapsed_time == 0) { + ret = ESP_ERR_INVALID_STATE; + goto exit; + } + + printf("| Task | Run Time | Percentage\n"); + //Match each task in start_array to those in the end_array + for (int i = 0; i < start_array_size; i++) { + int k = -1; + for (int j = 0; j < end_array_size; j++) { + if (start_array[i].xHandle == end_array[j].xHandle) { + k = j; + //Mark that task have been matched by overwriting their handles + start_array[i].xHandle = NULL; + end_array[j].xHandle = NULL; + break; + } + } + //Check if matching task found + if (k >= 0) { + uint32_t task_elapsed_time = end_array[k].ulRunTimeCounter - start_array[i].ulRunTimeCounter; + uint32_t percentage_time = (task_elapsed_time * 100UL) / (total_elapsed_time * CONFIG_FREERTOS_NUMBER_OF_CORES); + printf("| %-16s | %8lu | %4lu%%\n", start_array[i].pcTaskName, task_elapsed_time, percentage_time); + } + } + + //Print unmatched tasks + for (int i = 0; i < start_array_size; i++) { + if (start_array[i].xHandle != NULL) { + printf("| %s | Deleted\n", start_array[i].pcTaskName); + } + } + for (int i = 0; i < end_array_size; i++) { + if (end_array[i].xHandle != NULL) { + printf("| %s | Created\n", end_array[i].pcTaskName); + } + } + ret = ESP_OK; + +exit: //Common return path + free(start_array); + free(end_array); + return ret; +} + diff --git a/main/system_info.h b/main/system_info.h new file mode 100644 index 0000000..9ef7541 --- /dev/null +++ b/main/system_info.h @@ -0,0 +1,20 @@ +#ifndef _SYSTEM_INFO_H_ +#define _SYSTEM_INFO_H_ + +#include + +#include +#include + +class SystemInfo { +public: + static size_t GetFlashSize(); + static size_t GetMinimumFreeHeapSize(); + static size_t GetFreeHeapSize(); + static std::string GetMacAddress(); + static std::string GetBleMacAddress(); + static std::string GetChipModelName(); + static esp_err_t PrintRealTimeStats(TickType_t xTicksToWait); +}; + +#endif // _SYSTEM_INFO_H_ diff --git a/main/volume_config.h b/main/volume_config.h new file mode 100644 index 0000000..2671887 --- /dev/null +++ b/main/volume_config.h @@ -0,0 +1,17 @@ +#ifndef VOLUME_CONFIG_H +#define VOLUME_CONFIG_H + +// 音量控制配置 宏定义最低音量 +#define MIN_VOLUME_PERCENT 50 // 最低音量百分比,可根据需要修改 +#define MAX_VOLUME_PERCENT 100 // 最高音量百分比 + +// 计算音量范围 +#define VOLUME_RANGE (MAX_VOLUME_PERCENT - MIN_VOLUME_PERCENT) + +// 用户音量(0-100%)映射到硬件音量的宏函数 +#define USER_TO_HARDWARE_VOLUME(user_vol) (MIN_VOLUME_PERCENT + ((user_vol) * VOLUME_RANGE / 100)) + +// 硬件音量映射到用户音量的宏函数 +#define HARDWARE_TO_USER_VOLUME(hw_vol) (((hw_vol) - MIN_VOLUME_PERCENT) * 100 / VOLUME_RANGE) + +#endif // VOLUME_CONFIG_H \ No newline at end of file diff --git a/main/weather_api.cc b/main/weather_api.cc new file mode 100644 index 0000000..a83ffbf --- /dev/null +++ b/main/weather_api.cc @@ -0,0 +1,769 @@ +#include "weather_api.h" +#include +#include +#include +#include +#include + +// ESP32 ESP-IDF headers +#ifdef __cplusplus +extern "C" { +#endif +#include "esp_http_client.h" +#include "esp_log.h" +#include "cJSON.h" +#include "esp_crt_bundle.h" +#include "esp_wifi.h" +#include "nvs.h" +#include "nvs_flash.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#ifdef __cplusplus +} +#endif +#include "zlib.h" + +static const char* TAG = "WeatherApi"; + +// 天气代码映射表 - 将和风天气API的天气代码转换为中文描述 +std::unordered_map WeatherApi::WEATHER_CODE_MAP = { + {"100", "晴"}, {"101", "多云"}, {"102", "少云"}, {"103", "晴间多云"}, + {"104", "阴"}, {"150", "晴"}, {"151", "多云"}, {"152", "少云"}, + {"153", "晴间多云"}, {"300", "阵雨"}, {"301", "强阵雨"}, {"302", "雷阵雨"}, + {"303", "强雷阵雨"}, {"304", "雷阵雨伴有冰雹"}, {"305", "小雨"}, {"306", "中雨"}, + {"307", "大雨"}, {"308", "极端降雨"}, {"309", "毛毛雨/细雨"}, {"310", "暴雨"}, + {"311", "大暴雨"}, {"312", "特大暴雨"}, {"313", "冻雨"}, {"314", "小到中雨"}, + {"315", "中到大雨"}, {"316", "大到暴雨"}, {"317", "暴雨到大暴雨"}, {"318", "大暴雨到特大暴雨"}, + {"350", "阵雨"}, {"351", "强阵雨"}, {"399", "雨"}, {"400", "小雪"}, + {"401", "中雪"}, {"402", "大雪"}, {"403", "暴雪"}, {"404", "雨夹雪"}, + {"405", "雨雪天气"}, {"406", "阵雨夹雪"}, {"407", "阵雪"}, {"408", "小到中雪"}, + {"409", "中到大雪"}, {"410", "大到暴雪"}, {"456", "阵雨夹雪"}, {"457", "阵雪"}, + {"499", "雪"}, {"500", "薄雾"}, {"501", "雾"}, {"502", "霾"}, + {"503", "扬沙"}, {"504", "浮尘"}, {"507", "沙尘暴"}, {"508", "强沙尘暴"}, + {"509", "浓雾"}, {"510", "强浓雾"}, {"511", "中度霾"}, {"512", "重度霾"}, + {"513", "严重霾"}, {"514", "大雾"}, {"515", "特强浓雾"}, {"900", "热"}, + {"901", "冷"}, {"999", "未知"} +}; + +static std::string MapLangCode(const std::string& lang) { + if (lang.empty()) return "zh"; + if (lang == "zh_CN" || lang == "zh" || lang == "zh-Hans") return "zh"; + if (lang == "zh_HK" || lang == "zh-TW" || lang == "zh-Hant") return "zh-Hant"; + if (lang == "en_US" || lang == "en") return "en"; + if (lang == "ja_JP" || lang == "ja") return "ja"; + if (lang == "ko_KR" || lang == "ko") return "ko"; + if (lang == "fr_FR" || lang == "fr") return "fr"; + if (lang == "de_DE" || lang == "de") return "de"; + if (lang == "es_ES" || lang == "es") return "es"; + return "zh"; +} + +static bool IsGzipData(const std::string& data) { + if (data.size() < 2) return false; + const unsigned char* p = reinterpret_cast(data.data()); + return p[0] == 0x1F && p[1] == 0x8B; +} + +static bool GzipDecompress(const std::string& in, std::string& out) { + if (in.empty()) return false; + z_stream strm; + memset(&strm, 0, sizeof(strm)); + strm.next_in = (Bytef*)in.data(); + strm.avail_in = (uInt)in.size(); + int ret = inflateInit2(&strm, 16 + MAX_WBITS); + if (ret != Z_OK) { + ESP_LOGE(TAG, "inflateInit2失败: %d", ret); + return false; + } + std::vector buf(1024); + while (true) { + strm.next_out = (Bytef*)buf.data(); + strm.avail_out = (uInt)buf.size(); + ret = inflate(&strm, Z_NO_FLUSH); + if (ret == Z_STREAM_END) { + size_t produced = buf.size() - strm.avail_out; + if (produced) out.append(buf.data(), produced); + break; + } else if (ret == Z_OK || ret == Z_BUF_ERROR) { + size_t produced = buf.size() - strm.avail_out; + if (produced) out.append(buf.data(), produced); + continue; + } else { + ESP_LOGE(TAG, "inflate失败: %d", ret); + inflateEnd(&strm); + return false; + } + } + inflateEnd(&strm); + return true; +} + +// 缓存条目结构体,包含时间戳信息 +typedef struct { + time_t timestamp;// 缓存条目时间戳 +} CacheEntry; + +// 缓存上限常量 +#define MAX_WIFI_CITY_CACHE 5 + +// WeatherApi构造函数,初始化配置参数 +WeatherApi::WeatherApi() { + // 硬编码配置参数 - 从get_weather.py文件中提取的配置,与小智server保持一致 + api_host_ = "kq3aapg9h5.re.qweatherapi.com"; + api_key_ = "aa5ec0859c144ac7b33966e25eef5580"; + default_location_ = "北京"; // 默认城市设置为北京 + kid_ = "T45F5GTR8Y"; + project_id_ = "4N855TEVNN"; + private_key_ = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIA26lz31HoaZV17EjIGcyo9YNGGQ77/gOZU8Chw8wlWq\n-----END PRIVATE KEY-----"; + + ESP_LOGI(TAG, "初始化天气API配置 - 默认城市: %s", default_location_.c_str()); + ESP_LOGI(TAG, "WiFi位置缓存限制已设置为: %d 条", MAX_WIFI_CITY_CACHE); +} + +// 生成JWT令牌(预留接口,当前版本暂不实现完整JWT认证) +std::string WeatherApi::GenerateJwtToken() { + ESP_LOGI(TAG, "JWT令牌生成预留接口"); + return ""; +} + +// 封装HTTP GET请求 +bool WeatherApi::HttpGet(const std::string& url, const std::string& headers, std::string& response) { + ESP_LOGI(TAG, "HTTP请求: %s", url.c_str()); + + esp_http_client_config_t config = {}; + config.url = url.c_str(); + config.method = HTTP_METHOD_GET; + config.transport_type = HTTP_TRANSPORT_OVER_SSL; +#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + config.crt_bundle_attach = esp_crt_bundle_attach; +#else + ESP_LOGE(TAG, "证书包未启用,无法进行服务器证书验证。请在 menuconfig 启用 ESP-TLS Certificate Bundle"); +#endif + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + ESP_LOGE(TAG, "HTTP客户端初始化失败"); + return false; + } + + if (!headers.empty()) { + esp_http_client_set_header(client, "Authorization", headers.c_str()); + } + + esp_http_client_set_header(client, "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"); + esp_http_client_set_header(client, "Accept", "application/json"); + // esp_http_client_set_header(client, "Accept-Encoding", "identity");// 禁用压缩,防止解压失败 + + esp_err_t err = esp_http_client_open(client, 0); + bool success = false; + + if (err == ESP_OK) { + int64_t headers_len = esp_http_client_fetch_headers(client); + int status_code = esp_http_client_get_status_code(client); + ESP_LOGI(TAG, "HTTP状态码: %d, 头长度: %lld", status_code, (long long)headers_len); + if (status_code == 200) { + char buf[512]; + int read_len; + int total = 0; + while ((read_len = esp_http_client_read(client, buf, sizeof(buf))) > 0) { + response.append(buf, read_len); + total += read_len; + } + if (IsGzipData(response)) { + std::string decompressed; + if (GzipDecompress(response, decompressed)) { + response.swap(decompressed); + ESP_LOGI(TAG, "GZIP解压成功,解压后长度: %d", (int)response.size()); + } else { + ESP_LOGE(TAG, "GZIP解压失败,保留原始响应"); + } + } + success = (total > 0); + ESP_LOGI(TAG, "读取完成,累计长度: %d", total); + if (!success) { + ESP_LOGE(TAG, "读取响应内容失败"); + } + } else { + ESP_LOGE(TAG, "HTTP请求失败,状态码: %d", status_code); + } + esp_http_client_close(client); + } else { + ESP_LOGE(TAG, "HTTP请求执行失败: %s", esp_err_to_name(err)); + } + + esp_http_client_cleanup(client); + + vTaskDelay(100 / portTICK_PERIOD_MS); + + return success; +} + +// URL编码函数 +std::string UrlEncode(const std::string& str) { + std::string encoded; + for (char c : str) { + if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + encoded += c; + } else { + encoded += '%'; + encoded += "0123456789ABCDEF"[static_cast(c) / 16]; + encoded += "0123456789ABCDEF"[static_cast(c) % 16]; + } + } + return encoded; +} + +// 获取城市信息 +std::string WeatherApi::FetchCityInfo(const std::string& location, const std::string& lang) { + ESP_LOGI(TAG, "[FetchCityInfo] 开始获取城市信息,location参数: '%s', 长度: %zu", location.c_str(), location.length()); + + // 对location参数进行URL编码 + std::string encoded_location = UrlEncode(location); + ESP_LOGI(TAG, "[FetchCityInfo] URL编码后: '%s'", encoded_location.c_str()); + + // 构建城市信息查询URL + std::string q_lang = MapLangCode(lang); + std::string url = "https://" + api_host_ + "/geo/v2/city/lookup?key=" + api_key_ + + "\u0026location=" + encoded_location + "\u0026lang=" + q_lang; + std::string response; + + ESP_LOGI(TAG, "[FetchCityInfo] API主机: '%s', API密钥长度: %zu", api_host_.c_str(), api_key_.length()); + ESP_LOGI(TAG, "[FetchCityInfo] 查询城市信息URL: '%s'", url.c_str()); + + // 发送HTTP请求 + ESP_LOGI(TAG, "[FetchCityInfo] 开始发送HTTP GET请求"); + if (HttpGet(url, "", response)) { + ESP_LOGI(TAG, "[FetchCityInfo] HTTP请求成功,响应长度: %zu 字节", response.length()); + ESP_LOGD(TAG, "[FetchCityInfo] 响应内容前100字节: '%s'", response.substr(0, std::min(size_t(100), response.length())).c_str()); + + // 解析JSON响应 + cJSON* root = cJSON_Parse(response.c_str()); + if (root) { + ESP_LOGI(TAG, "[FetchCityInfo] JSON解析成功"); + + // 检查响应状态 + cJSON* code = cJSON_GetObjectItem(root, "code"); + if (code) { + ESP_LOGI(TAG, "[FetchCityInfo] 响应状态码存在: '%s'", code->valuestring); + if (cJSON_IsString(code) && strcmp(code->valuestring, "200") == 0) { + ESP_LOGI(TAG, "[FetchCityInfo] 响应状态码为200,成功"); + + // 获取location数组 + cJSON* location_array = cJSON_GetObjectItem(root, "location"); + if (location_array) { + ESP_LOGI(TAG, "[FetchCityInfo] location数组存在"); + if (cJSON_IsArray(location_array)) { + int array_size = cJSON_GetArraySize(location_array); + ESP_LOGI(TAG, "[FetchCityInfo] location数组大小: %d", array_size); + + if (array_size > 0) { + // 获取第一个城市信息 + cJSON* first_location = cJSON_GetArrayItem(location_array, 0); + if (first_location) { + ESP_LOGI(TAG, "[FetchCityInfo] 获取到第一个城市信息对象"); + + // 获取城市ID + cJSON* id = cJSON_GetObjectItem(first_location, "id"); + if (id) { + ESP_LOGI(TAG, "[FetchCityInfo] 城市ID字段存在"); + if (cJSON_IsString(id)) { + std::string city_id = id->valuestring; + ESP_LOGI(TAG, "[FetchCityInfo] 成功获取城市ID: '%s'", city_id.c_str()); + cJSON_Delete(root); + return city_id; + } else { + ESP_LOGE(TAG, "[FetchCityInfo] 城市ID不是字符串类型"); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] 未能获取城市ID字段"); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] 未能获取第一个城市信息对象"); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] location数组为空"); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] location不是数组类型"); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] 响应中没有location数组"); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] 城市信息API返回错误码: '%s'", code->valuestring); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] 响应中没有code字段"); + } + cJSON_Delete(root); + } else { + ESP_LOGE(TAG, "[FetchCityInfo] 解析城市信息JSON失败"); + ESP_LOGD(TAG, "[FetchCityInfo] 失败的JSON响应: '%s'", response.c_str()); + } + } else { + ESP_LOGE(TAG, "[FetchCityInfo] HTTP请求失败"); + } + + ESP_LOGI(TAG, "[FetchCityInfo] 函数结束,返回空字符串"); + return ""; +} + +// 解析并打印IP位置信息- IP查询 +static bool ParseAndPrintIpInfo(const std::string& response) { + ESP_LOGI(TAG, "开始解析IP位置信息JSON"); + + // 解析JSON响应 + cJSON* root = cJSON_Parse(response.c_str()); + if (!root) { + const char* error_ptr = cJSON_GetErrorPtr(); + ESP_LOGE(TAG, "解析IP信息JSON失败: %s", error_ptr ? error_ptr : "未知错误"); + return false; + } + + // 提取并打印各个字段(ip-api.com 格式) + cJSON* status = cJSON_GetObjectItem(root, "status"); + cJSON* ip = cJSON_GetObjectItem(root, "query"); + cJSON* country = cJSON_GetObjectItem(root, "country"); + cJSON* region = cJSON_GetObjectItem(root, "region"); + cJSON* regionName = cJSON_GetObjectItem(root, "regionName"); + cJSON* city = cJSON_GetObjectItem(root, "city"); + cJSON* zip = cJSON_GetObjectItem(root, "zip"); + cJSON* lat = cJSON_GetObjectItem(root, "lat"); + cJSON* lon = cJSON_GetObjectItem(root, "lon"); + cJSON* timezone = cJSON_GetObjectItem(root, "timezone"); + cJSON* isp = cJSON_GetObjectItem(root, "isp"); + cJSON* org = cJSON_GetObjectItem(root, "org"); + cJSON* asn = cJSON_GetObjectItem(root, "as"); + + // 打印解析结果 + ESP_LOGI(TAG, "=============== IP位置信息 ==============="); + if (status && cJSON_IsString(status)) { + ESP_LOGI(TAG, "状态: %s", status->valuestring); + } + if (ip && cJSON_IsString(ip)) { + ESP_LOGI(TAG, "IP: %s", ip->valuestring); + } + if (country && cJSON_IsString(country)) { + ESP_LOGI(TAG, "国家: %s", country->valuestring); + } + if (region && cJSON_IsString(region)) { + ESP_LOGI(TAG, "区域代码: %s", region->valuestring); + } + if (regionName && cJSON_IsString(regionName)) { + ESP_LOGI(TAG, "省份: %s", regionName->valuestring); + } + if (city && cJSON_IsString(city)) { + ESP_LOGI(TAG, "城市: %s", city->valuestring); + } + if (zip && cJSON_IsString(zip)) { + ESP_LOGI(TAG, "邮编: %s", zip->valuestring); + } + if (lat && cJSON_IsNumber(lat)) { + ESP_LOGI(TAG, "纬度: %.4f", lat->valuedouble); + } + if (lon && cJSON_IsNumber(lon)) { + ESP_LOGI(TAG, "经度: %.4f", lon->valuedouble); + } + if (timezone && cJSON_IsString(timezone)) { + ESP_LOGI(TAG, "时区: %s", timezone->valuestring); + } + if (isp && cJSON_IsString(isp)) { + ESP_LOGI(TAG, "运营商: %s", isp->valuestring); + } + if (org && cJSON_IsString(org)) { + ESP_LOGI(TAG, "组织: %s", org->valuestring); + } + if (asn && cJSON_IsString(asn)) { + ESP_LOGI(TAG, "AS号: %s", asn->valuestring); + } + ESP_LOGI(TAG, "======================================"); + + // 释放JSON对象 + cJSON_Delete(root); + return true; +} + +// 获取IP位置信息(ip-api.com) +// 自动获取设备当前IP的地理位置信息 +std::string WeatherApi::GetIpInfo() { + ESP_LOGI(TAG, "[GetIpInfo] 开始获取IP位置信息"); + + // 构建IP查询API的URL + std::string url = "http://ip-api.com/json/?lang=zh-CN"; + ESP_LOGI(TAG, "[GetIpInfo] 查询URL: %s", url.c_str()); + std::string response; + std::string city_info = default_location_; // 默认返回默认位置 + + // 使用HTTP + esp_http_client_config_t config = {}; + config.url = url.c_str(); + config.method = HTTP_METHOD_GET; + config.transport_type = HTTP_TRANSPORT_OVER_TCP; // 使用HTTP而非HTTPS + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + ESP_LOGE(TAG, "[GetIpInfo] HTTP客户端初始化失败"); + return city_info; + } + + // 设置请求头 + esp_http_client_set_header(client, "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"); + + esp_http_client_set_header(client, "Accept-Encoding", "identity"); + esp_err_t err = esp_http_client_open(client, 0); + if (err == ESP_OK) { + int64_t headers_len = esp_http_client_fetch_headers(client); + int status_code = esp_http_client_get_status_code(client); + ESP_LOGI(TAG, "[GetIpInfo] HTTP状态码: %d, 头长度: %lld", status_code, (long long)headers_len); + if (status_code == 200) { + char buf[512]; + int read_len; + while ((read_len = esp_http_client_read(client, buf, sizeof(buf))) > 0) { + response.append(buf, read_len); + } + + ESP_LOGI(TAG, "[GetIpInfo] 获取到响应,长度: %zu 字节", response.length()); + + if (ParseAndPrintIpInfo(response)) { + cJSON* root = cJSON_Parse(response.c_str()); + if (root) { + cJSON* city = cJSON_GetObjectItem(root, "city"); + cJSON* regionName = cJSON_GetObjectItem(root, "regionName"); + if (city && cJSON_IsString(city) && city->valuestring[0] != '\0') { + city_info = city->valuestring; + } else if (regionName && cJSON_IsString(regionName)) { + city_info = regionName->valuestring; + } + cJSON_Delete(root); + } + } + } else { + ESP_LOGE(TAG, "[GetIpInfo] HTTP请求失败,状态码: %d", status_code); + } + + esp_http_client_close(client);// 关闭HTTP客户端连接,释放资源 + } else { + ESP_LOGE(TAG, "[GetIpInfo] HTTP请求执行失败: %s", esp_err_to_name(err)); + } + + esp_http_client_cleanup(client);// 清理HTTP客户端资源,释放内存 + vTaskDelay(100 / portTICK_PERIOD_MS);// 延时100ms,确保资源释放完成 + + ESP_LOGI(TAG, "[GetIpInfo] 返回城市信息: %s", city_info.c_str()); + return city_info;// 返回提取的城市信息 +} + +// 获取天气信息主函数 +std::string WeatherApi::GetWeather(const std::string& location, const std::string& lang) { + ESP_LOGI(TAG, "[GetWeather] ===== 开始获取天气信息 ====="); + ESP_LOGI(TAG, "[GetWeather] 输入参数: location='%s' (长度: %zu), lang='%s'", + location.c_str(), location.length(), lang.c_str()); + + // 确定查询位置 - 当location为空或为"None"字符串时,使用默认城市 + std::string query_location = (location.empty() || location == "None") ? default_location_ : location; + ESP_LOGI(TAG, "[GetWeather] 查询位置: '%s'", query_location.c_str()); + + // 检查缓存 - 简单的内存缓存 + std::string cache_key = "weather_" + query_location; + ESP_LOGI(TAG, "[GetWeather] 缓存键: '%s'", cache_key.c_str()); + + auto it = weather_cache_.find(cache_key); + if (it != weather_cache_.end()) { + ESP_LOGI(TAG, "[GetWeather] 使用缓存的天气信息: '%s'", query_location.c_str()); + ESP_LOGI(TAG, "[GetWeather] ===== 天气信息获取完成 (缓存命中) ====="); + return it->second; // 返回缓存的结果 + } + + ESP_LOGI(TAG, "[GetWeather] 缓存未命中,开始获取天气信息: '%s'", query_location.c_str()); + + // 获取城市ID + ESP_LOGI(TAG, "[GetWeather] 开始获取城市ID"); + std::string city_id = FetchCityInfo(query_location, lang); + ESP_LOGI(TAG, "[GetWeather] FetchCityInfo返回结果: city_id='%s' (长度: %zu)", + city_id.c_str(), city_id.length()); + + if (city_id.empty()) { + ESP_LOGI(TAG, "[GetWeather] 未找到相关城市信息,请确认城市名称是否正确"); + ESP_LOGI(TAG, "[GetWeather] ===== 天气信息获取失败 (城市ID为空) ====="); + return "未找到相关城市信息,请确认城市名称是否正确"; + } + + std::string q_lang = MapLangCode(lang);// 映射语言代码为API需要的格式 + std::string weather_url = "https://" + api_host_ + "/v7/weather/now?key=" + api_key_ + "\u0026location=" + city_id + "\u0026lang=" + q_lang; + std::string weather_response;// 存储天气API响应的字符串 + + ESP_LOGI(TAG, "[GetWeather] 构建天气查询URL: '%s'", weather_url.c_str()); + std::string result = "";// 存储最终格式化的天气信息 + + // 发送天气查询请求 + ESP_LOGI(TAG, "[GetWeather] 开始发送天气查询HTTP请求"); + if (HttpGet(weather_url, "", weather_response)) { + ESP_LOGI(TAG, "[GetWeather] HTTP请求成功,响应长度: %zu 字节", weather_response.length()); + + // 解析天气JSON响应 + ESP_LOGI(TAG, "[GetWeather] 开始解析天气JSON响应"); + cJSON* root = cJSON_Parse(weather_response.c_str()); + if (root) { + ESP_LOGI(TAG, "[GetWeather] JSON解析成功"); + + // 检查响应状态 + cJSON* code = cJSON_GetObjectItem(root, "code"); + if (code && cJSON_IsString(code) && strcmp(code->valuestring, "200") == 0) { + ESP_LOGI(TAG, "[GetWeather] 响应状态码: 200 (成功)"); + + // 获取天气数据 + cJSON* now = cJSON_GetObjectItem(root, "now"); + if (now) { + ESP_LOGI(TAG, "[GetWeather] 获取到now字段,开始构建天气报告"); + + // 构建天气报告 + result = "前天气情况 (" + query_location + ")\n\n"; + + // 获取天气描述 + cJSON* text = cJSON_GetObjectItem(now, "text"); + if (text && cJSON_IsString(text)) { + result += "天气状况: " + std::string(text->valuestring) + "\n"; + ESP_LOGI(TAG, "[GetWeather] 天气状况: '%s'", text->valuestring); + } else { + ESP_LOGW(TAG, "[GetWeather] 未能获取天气状况"); + } + + // 获取温度 + cJSON* temp = cJSON_GetObjectItem(now, "temp"); + if (temp && cJSON_IsString(temp)) { + result += "当前温度: " + std::string(temp->valuestring) + "摄氏度\n"; + ESP_LOGI(TAG, "[GetWeather] 当前温度: '%s'摄氏度", temp->valuestring); + } else { + ESP_LOGW(TAG, "[GetWeather] 未能获取当前温度"); + } + + // 获取体感温度 + cJSON* feelsLike = cJSON_GetObjectItem(now, "feelsLike"); + if (feelsLike && cJSON_IsString(feelsLike)) { + result += "体感温度: " + std::string(feelsLike->valuestring) + "摄氏度\n"; + ESP_LOGI(TAG, "[GetWeather] 体感温度: '%s'摄氏度", feelsLike->valuestring); + } else { + ESP_LOGW(TAG, "[GetWeather] 未能获取体感温度"); + } + + // 获取风向风速 + cJSON* windDir = cJSON_GetObjectItem(now, "windDir"); + cJSON* windScale = cJSON_GetObjectItem(now, "windScale"); + if (windDir && cJSON_IsString(windDir) && windScale && cJSON_IsString(windScale)) { + result += "风向风速: " + std::string(windDir->valuestring) + " " + + std::string(windScale->valuestring) + "级\n"; + ESP_LOGI(TAG, "[GetWeather] 风向风速: '%s' %s级", windDir->valuestring, windScale->valuestring); + } else { + ESP_LOGW(TAG, "[GetWeather] 未能获取风向风速信息"); + } + + // 获取湿度 + cJSON* humidity = cJSON_GetObjectItem(now, "humidity"); + if (humidity && cJSON_IsString(humidity)) { + result += "相对湿度: " + std::string(humidity->valuestring) + "%\n"; + ESP_LOGI(TAG, "[GetWeather] 相对湿度: '%s'%%", humidity->valuestring); + } else { + ESP_LOGW(TAG, "[GetWeather] 未能获取相对湿度"); + } + + // 获取气压 + cJSON* pressure = cJSON_GetObjectItem(now, "pressure"); + if (pressure && cJSON_IsString(pressure)) { + result += "大气压强: " + std::string(pressure->valuestring) + "hPa\n"; + ESP_LOGI(TAG, "[GetWeather] 大气压强: '%s'hPa", pressure->valuestring); + } else { + ESP_LOGW(TAG, "[GetWeather] 未能获取大气压强"); + } + + // 添加更新时间 + result += std::string("\n数据更新时间: ") + __DATE__ + " " + __TIME__; + ESP_LOGI(TAG, "[GetWeather] 数据更新时间: %s %s", __DATE__, __TIME__); + + } else { + ESP_LOGE(TAG, "[GetWeather] 无法获取当前天气数据(now字段为空)"); + result = "无法获取当前天气数据"; + } + } else { + const char* error_code = code && cJSON_IsString(code) ? code->valuestring : "未知"; + ESP_LOGE(TAG, "[GetWeather] 天气API请求失败,错误码: '%s'", error_code); + result = "天气API请求失败,错误码: " + std::string(error_code); + } + cJSON_Delete(root); + } else { + ESP_LOGE(TAG, "[GetWeather] 解析天气数据失败,请稍后重试"); + result = "解析天气数据失败,请稍后重试"; + } + } else { + ESP_LOGE(TAG, "[GetWeather] 网络请求失败,无法获取天气信息"); + result = "网络请求失败,无法获取天气信息"; + } + + // 缓存结果 - 简单实现,实际项目中应考虑缓存过期时间 + if (!result.empty()) { + ESP_LOGI(TAG, "[GetWeather] 开始缓存天气结果,当前缓存大小: %zu", weather_cache_.size()); + weather_cache_[cache_key] = result; + + // 限制缓存大小 + if (weather_cache_.size() > 10) { + ESP_LOGI(TAG, "[GetWeather] 缓存大小超过10,移除最旧的缓存项"); + weather_cache_.erase(weather_cache_.begin()); + } + ESP_LOGI(TAG, "[GetWeather] 缓存完成,当前缓存大小: %zu", weather_cache_.size()); + } + + ESP_LOGI(TAG, "[GetWeather] 天气信息获取完成"); + ESP_LOGI(TAG, "[GetWeather] ===== 天气信息获取流程结束 ====="); + return result; +} + +// 创建全局WeatherApi实例 +WeatherApi g_weather_api; + +// 实现全局GetWeatherInfo函数,用于获取天气信息// 全局函数,供外部调用 +std::string GetWeatherInfo(const std::string& location, const std::string& lang) { + ESP_LOGI(TAG, "[GetWeatherInfo] ===== 全局函数调用开始 ====="); + ESP_LOGI(TAG, "[GetWeatherInfo] 收到调用请求,location='%s' (长度: %zu), lang='%s'",location.c_str(), location.length(), lang.c_str()); + + // 调用g_weather_api单例的GetWeather方法 + ESP_LOGI(TAG, "[GetWeatherInfo] 准备调用g_weather_api.GetWeather"); + std::string result = g_weather_api.GetWeather(location, lang);// 调用单例实例的GetWeather方法获取天气信息 + + ESP_LOGI(TAG, "[GetWeatherInfo] g_weather_api.GetWeather调用完成,结果长度: %zu 字节", result.length()); + ESP_LOGD(TAG, "[GetWeatherInfo] 返回结果前100字节: '%s'", result.substr(0, std::min(size_t(100), result.length())).c_str()); + ESP_LOGI(TAG, "[GetWeatherInfo] ===== 全局函数调用结束 ====="); + return result; +} + +std::string WeatherApi::GetDefaultLocation() const { + return default_location_;// 返回默认位置 +} + +// 新增方法:自动检测NVS中是否存在当前位置的城市信息,并设置当前位置 +void WeatherApi::AutoDetectLocation() { + ESP_LOGI(TAG, "[AutoDetectLocation] ===== 开始自动检测位置 ====="); + wifi_config_t wc{};// 定义WiFi配置结构体 + esp_wifi_get_config(WIFI_IF_STA, &wc);// 获取当前WiFi配置 + std::string ssid = std::string(reinterpret_cast(wc.sta.ssid));// 转换SSID为字符串 + wifi_ap_record_t ap{};// 定义AP记录结构体 + std::string bssid;// 定义BSSID字符串 + if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) { + char buf[18];// 定义BSSID缓冲区 + snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", + ap.bssid[0], ap.bssid[1], ap.bssid[2], + ap.bssid[3], ap.bssid[4], ap.bssid[5]);// 格式化BSSID为字符串 + bssid.assign(buf);// 赋值给BSSID字符串 + } + nvs_handle_t h;// 定义NVS句柄 + // 打开NVS命名空间"wifi_city_map",读写模式 + if (nvs_open("wifi_city_map", NVS_READWRITE, &h) == ESP_OK) { + auto try_get = [&](const std::string& key)->std::string{ + size_t len = 0; + // 尝试获取NVS中存储的城市字符串 + if (nvs_get_str(h, key.c_str(), NULL, &len) == ESP_OK && len > 0) { + std::vector buf(len);// 定义缓冲区 + // 尝试从NVS获取城市字符串 + if (nvs_get_str(h, key.c_str(), buf.data(), &len) == ESP_OK) { + return std::string(buf.data());// 返回城市字符串 + } + } + return std::string();// 未找到对应城市,返回空字符串 + }; + std::string city;// 定义城市字符串 + if (!ssid.empty()) { + if (!bssid.empty()) { + city = try_get(ssid + "|" + bssid);// 先尝试SSID+BSSID + } + if (city.empty()) { + city = try_get(ssid);// 如果未找到,再尝试SSID + } + } + // 如果仍未从NVS中找到城市信息,将调用位置API获取城市信息 + if (city.empty()) { + ESP_LOGI(TAG, "[AutoDetectLocation] 未从NVS命中城市信息,将调用位置API获取城市信息"); + std::string detected = GetIpInfo();// 获取IP位置信息 调用位置查询API获取当前位置的城市信息 + if (!detected.empty()) { + default_location_ = detected;// 更新默认城市为检测到的位置 + if (!ssid.empty()) { + size_t cache_count = 0;// 初始化缓存条目数量 + std::vector cache_entries; // 定义缓存条目向量,存储所有键 + std::vector deletable_entries; // 存储可删除的条目(排除当前使用的SSID和SSID+BSSID) + + // 遍历NVS中的所有键 + nvs_iterator_t it = NULL; + esp_err_t res = nvs_entry_find(NULL, "wifi_city_map", NVS_TYPE_STR, &it);// 查找所有字符串类型的条目 + while (res == ESP_OK) { + nvs_entry_info_t info; // 定义NVS条目信息结构体 + nvs_entry_info(it, &info);// 获取当前迭代器指向的条目信息 + + std::string key = info.key; // 获取当前条目的键名 + cache_entries.push_back(key);// 将键添加到缓存向量中 + cache_count++;// 增加缓存条目数量 + + // 检查是否为当前使用的键,如果不是则添加到可删除列表 + std::string current_full_key = ssid; + if (!bssid.empty()) {current_full_key = ssid + "|" + bssid;}// 组合SSID和BSSID作为完整键 + + // 排除当前正在使用的SSID和SSID+BSSID键 + if (key != ssid && key != current_full_key) {deletable_entries.push_back(key);}// 将键添加到可删除列表 + res = nvs_entry_next(&it);// 移动到下一个条目 + } + nvs_release_iterator(it);// 释放NVS迭代器 + + // 如果缓存数量超过限制且有可删除的条目,随机删除一个 + if (cache_count >= MAX_WIFI_CITY_CACHE && !deletable_entries.empty()) { + ESP_LOGI(TAG, "[AutoDetectLocation] WiFi位置缓存数量(%zu)达到限制(%d),开始随机删除策略",cache_count, MAX_WIFI_CITY_CACHE); + + // 随机选择一个条目删除(使用更简单的方式选择索引),ESP32平台可以使用esp_random()获得更好的随机性,但这里保持简单实现 + int random_index = (int)(esp_timer_get_time() % deletable_entries.size());// 生成随机索引 + const std::string& key_to_delete = deletable_entries[random_index];// 获取随机选择的键名 + ESP_LOGI(TAG, "[AutoDetectLocation] 随机删除缓存条目: %s", key_to_delete.c_str()); + nvs_erase_key(h, key_to_delete.c_str());// 删除城市字符串键 + } + + // 保存新的位置信息 + if (!bssid.empty()) { + const std::string full_key = ssid + "|" + bssid;// 组合SSID和BSSID作为完整键 + nvs_set_str(h, full_key.c_str(), detected.c_str());// 缓存SSID+BSSID到城市 + } + nvs_set_str(h, ssid.c_str(), detected.c_str());// 缓存SSID到城市 + + if (nvs_commit(h) == ESP_OK) { + ESP_LOGI(TAG, "[AutoDetectLocation] 城市信息保存到NVS成功!"); + } else { + ESP_LOGW(TAG, "[AutoDetectLocation] 城市信息保存到NVS失败!"); + } + } + ESP_LOGI(TAG, "[AutoDetectLocation] 自动检测到位置: '%s',已更新默认城市", detected.c_str()); + } else { + ESP_LOGI(TAG, "[AutoDetectLocation] 位置检测失败或未变化,保持默认城市: '%s'", default_location_.c_str()); + } + } + // 如果从NVS命中城市,找到了城市信息 + else { + default_location_ = city;// 更新默认城市为NVS中命中的城市 + ESP_LOGI(TAG, "[AutoDetectLocation] 从NVS命中位置: '%s',已更新默认城市", city.c_str()); + } + nvs_close(h);// 关闭NVS句柄 + } + // 如果打开NVS检索城市信息失败 + else { + std::string detected = GetIpInfo();// 调用位置API获取城市信息 + if (!detected.empty()) { + default_location_ = detected;// 更新默认城市为检测到的位置 + ESP_LOGI(TAG, "[AutoDetectLocation] 自动检测到位置: '%s',已更新默认城市", detected.c_str()); + } else { + ESP_LOGI(TAG, "[AutoDetectLocation] 位置检测失败或未变化,保持默认城市: '%s'", default_location_.c_str()); + } + } + ESP_LOGI(TAG, "[AutoDetectLocation] ===== 位置检测完成 ====="); +} + +// 全局函数:供应用程序网络连接后调用,用于自动检测位置 +void AutoDetectAndSetLocation() { + ESP_LOGI(TAG, "[AutoDetectAndSetLocation] 调用全局函数自动检测位置"); + g_weather_api.AutoDetectLocation();// 自动检测NVS中是否存在当前位置的城市信息,并设置当前位置 +} diff --git a/main/weather_api.h b/main/weather_api.h new file mode 100644 index 0000000..daafecc --- /dev/null +++ b/main/weather_api.h @@ -0,0 +1,102 @@ +#pragma once +#include +#include + +/** + * @brief WeatherApi类用于封装和风天气API调用 + */ +class WeatherApi { +private: + // 和风天气API配置参数 + std::string api_host_; // API主机地址 + std::string api_key_; // API密钥 + std::string default_location_; // 默认城市位置 + std::string kid_; // JWT认证ID + std::string project_id_; // 项目ID + std::string private_key_; // 私钥 + + // 简单缓存管理 + std::unordered_map weather_cache_; // 天气数据缓存 + std::unordered_map ip_info_cache_; // IP信息缓存 + + // 天气代码映射表,将API返回的天气代码转换为中文描述 + static std::unordered_map WEATHER_CODE_MAP; + + /** + * @brief 生成JWT令牌(预留接口) + * @return JWT令牌字符串 + */ + std::string GenerateJwtToken(); + + /** + * @brief 封装HTTP GET请求 + * @param url 请求URL + * @param headers 请求头 + * @param response 响应内容 + * @return 是否请求成功 + */ + bool HttpGet(const std::string& url, const std::string& headers, std::string& response); + + /** + * @brief 获取城市信息 + * @param location 城市名称 + * @return 城市详细信息 + */ + std::string FetchCityInfo(const std::string& location, const std::string& lang); + + /** + * @brief 获取IP位置信息 + * @details 自动获取设备当前IP的地理位置信息,无需指定IP参数 + * @return IP对应的地理位置信息 + */ + std::string GetIpInfo(); + +public: + /** + * @brief 构造函数,初始化配置参数 + */ + WeatherApi(); + + /** + * @brief 获取当前默认城市 + */ + std::string GetDefaultLocation() const; + + /** + * @brief 析构函数 + */ + ~WeatherApi() = default; + + /** + * @brief 获取天气信息主函数 + * @param location 城市名称 + * @param lang 语言设置 + * @return 格式化的天气信息 + */ + std::string GetWeather(const std::string& location, const std::string& lang); + + /** + * @brief 自动检测并设置当前位置 + * @details 调用太平洋IP查询API自动检测当前地理位置,并更新默认城市设置 + */ + void AutoDetectLocation();// 自动检测NVS中是否存在当前位置的城市信息,并设置当前位置 +}; + +/** + * @brief 全局函数,用于获取天气信息 + * @param location 城市名称 + * @param lang 语言设置 + * @return 格式化的天气信息 + */ +extern std::string GetWeatherInfo(const std::string& location, const std::string& lang); + +/** + * @brief 全局函数,用于自动检测并设置当前位置 + * @details 供应用程序在网络连接后调用,自动检测地理位置并更新默认城市 + */ +extern void AutoDetectAndSetLocation(); + +/** + * @brief 全局WeatherApi实例 + */ +extern WeatherApi g_weather_api; diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..560fe12 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,8 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +model, data, spiffs, 0x10000, 0x300000, +ota_0, app, ota_0, 0x310000, 5M, +ota_1, app, ota_1, 0x820000, 5M, diff --git a/partitions_32M_sensecap.csv b/partitions_32M_sensecap.csv new file mode 100644 index 0000000..e95eb22 --- /dev/null +++ b/partitions_32M_sensecap.csv @@ -0,0 +1,10 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvsfactory, data, nvs, , 200K, +nvs, data, nvs, , 840K, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +model, data, spiffs, , 0xF0000, +# According to scripts/versions.py, app partition must be aligned to 1MB +ota_0, app, ota_0, 0x200000, 12M, +ota_1, app, ota_1, , 12M, diff --git a/partitions_4M.csv b/partitions_4M.csv new file mode 100644 index 0000000..101349f --- /dev/null +++ b/partitions_4M.csv @@ -0,0 +1,7 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +model, data, spiffs, 0x10000, 0xF0000, +factory, app, factory, 0x100000, 3M, diff --git a/partitions_8M.csv b/partitions_8M.csv new file mode 100644 index 0000000..1e0e943 --- /dev/null +++ b/partitions_8M.csv @@ -0,0 +1,8 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +model, data, spiffs, 0x10000, 0xF0000, +ota_0, app, ota_0, 0x100000, 0x380000, +ota_1, app, ota_1, 0x480000, 0x380000, diff --git a/scripts/Image_Converter/LVGLImage.py b/scripts/Image_Converter/LVGLImage.py new file mode 100644 index 0000000..b2ffbb3 --- /dev/null +++ b/scripts/Image_Converter/LVGLImage.py @@ -0,0 +1,1426 @@ +#!/usr/bin/env python3 +import os +import logging +import argparse +import subprocess +from os import path +from enum import Enum +from typing import List +from pathlib import Path + +try: + import png +except ImportError: + raise ImportError("Need pypng package, do `pip3 install pypng`") + +try: + import lz4.block +except ImportError: + raise ImportError("Need lz4 package, do `pip3 install lz4`") + + +def uint8_t(val) -> bytes: + return val.to_bytes(1, byteorder='little') + + +def uint16_t(val) -> bytes: + return val.to_bytes(2, byteorder='little') + + +def uint24_t(val) -> bytes: + return val.to_bytes(3, byteorder='little') + + +def uint32_t(val) -> bytes: + try: + return val.to_bytes(4, byteorder='little') + except OverflowError: + raise ParameterError(f"overflow: {hex(val)}") + + +def color_pre_multiply(r, g, b, a, background): + bb = background & 0xff + bg = (background >> 8) & 0xff + br = (background >> 16) & 0xff + + return ((r * a + (255 - a) * br) >> 8, (g * a + (255 - a) * bg) >> 8, + (b * a + (255 - a) * bb) >> 8, a) + + +class Error(Exception): + + def __str__(self): + return self.__class__.__name__ + ': ' + ' '.join(self.args) + + +class FormatError(Error): + """ + Problem with input filename format. + BIN filename does not conform to standard lvgl bin image format + """ + + +class ParameterError(Error): + """ + Parameter for LVGL image not correct + """ + + +class PngQuant: + """ + Compress PNG file to 8bit mode using `pngquant` + """ + + def __init__(self, ncolors=256, dither=True, exec_path="") -> None: + executable = path.join(exec_path, "pngquant") + self.cmd = (f"{executable} {'--nofs' if not dither else ''} " + f"{ncolors} --force - < ") + + def convert(self, filename) -> bytes: + if not os.path.isfile(filename): + raise BaseException(f"file not found: {filename}") + + try: + compressed = subprocess.check_output( + f'{self.cmd} "{str(filename)}"', + stderr=subprocess.STDOUT, + shell=True) + except subprocess.CalledProcessError: + raise BaseException( + "cannot find pngquant tool, install it via " + "`sudo apt install pngquant` for debian " + "or `brew install pngquant` for macintosh " + "For windows, you may need to download pngquant.exe from " + "https://pngquant.org/, and put it in your PATH.") + + return compressed + + +class CompressMethod(Enum): + NONE = 0x00 + RLE = 0x01 + LZ4 = 0x02 + + +class ColorFormat(Enum): + UNKNOWN = 0x00 + RAW = 0x01, + RAW_ALPHA = 0x02, + L8 = 0x06 + I1 = 0x07 + I2 = 0x08 + I4 = 0x09 + I8 = 0x0A + A1 = 0x0B + A2 = 0x0C + A4 = 0x0D + A8 = 0x0E + ARGB8888 = 0x10 + XRGB8888 = 0x11 + RGB565 = 0x12 + ARGB8565 = 0x13 + RGB565A8 = 0x14 + RGB888 = 0x0F + + @property + def bpp(self) -> int: + """ + Return bit per pixel for this cf + """ + cf_map = { + ColorFormat.L8: 8, + ColorFormat.I1: 1, + ColorFormat.I2: 2, + ColorFormat.I4: 4, + ColorFormat.I8: 8, + ColorFormat.A1: 1, + ColorFormat.A2: 2, + ColorFormat.A4: 4, + ColorFormat.A8: 8, + ColorFormat.ARGB8888: 32, + ColorFormat.XRGB8888: 32, + ColorFormat.RGB565: 16, + ColorFormat.RGB565A8: 16, # 16bpp + a8 map + ColorFormat.ARGB8565: 24, + ColorFormat.RGB888: 24, + } + + return cf_map[self] if self in cf_map else 0 + + @property + def ncolors(self) -> int: + """ + Return number of colors in palette if cf is indexed1/2/4/8. + Return zero if cf is not indexed format + """ + + cf_map = { + ColorFormat.I1: 2, + ColorFormat.I2: 4, + ColorFormat.I4: 16, + ColorFormat.I8: 256, + } + return cf_map.get(self, 0) + + @property + def is_indexed(self) -> bool: + """ + Return if cf is indexed color format + """ + return self.ncolors != 0 + + @property + def is_alpha_only(self) -> bool: + return ColorFormat.A1.value <= self.value <= ColorFormat.A8.value + + @property + def has_alpha(self) -> bool: + return self.is_alpha_only or self.is_indexed or self in ( + ColorFormat.ARGB8888, + ColorFormat.XRGB8888, # const alpha: 0xff + ColorFormat.ARGB8565, + ColorFormat.RGB565A8) + + @property + def is_colormap(self) -> bool: + return self in (ColorFormat.ARGB8888, ColorFormat.RGB888, + ColorFormat.XRGB8888, ColorFormat.RGB565A8, + ColorFormat.ARGB8565, ColorFormat.RGB565) + + @property + def is_luma_only(self) -> bool: + return self in (ColorFormat.L8, ) + + +def bit_extend(value, bpp): + """ + Extend value from bpp to 8 bit with interpolation to reduce rounding error. + """ + + if value == 0: + return 0 + + res = value + bpp_now = bpp + while bpp_now < 8: + res |= value << (8 - bpp_now) + bpp_now += bpp + + return res + + +def unpack_colors(data: bytes, cf: ColorFormat, w) -> List: + """ + Unpack lvgl 1/2/4/8/16/32 bpp color to png color: alpha map, grey scale, + or R,G,B,(A) map + """ + ret = [] + bpp = cf.bpp + if bpp == 8: + ret = data + elif bpp == 4: + if cf == ColorFormat.A4: + values = [x * 17 for x in range(16)] + else: + values = [x for x in range(16)] + + for p in data: + for i in range(2): + ret.append(values[(p >> (4 - i * 4)) & 0x0f]) + if len(ret) % w == 0: + break + + elif bpp == 2: + if cf == ColorFormat.A2: + values = [x * 85 for x in range(4)] + else: # must be ColorFormat.I2 + values = [x for x in range(4)] + for p in data: + for i in range(4): + ret.append(values[(p >> (6 - i * 2)) & 0x03]) + if len(ret) % w == 0: + break + elif bpp == 1: + if cf == ColorFormat.A1: + values = [0, 255] + else: + values = [0, 1] + for p in data: + for i in range(8): + ret.append(values[(p >> (7 - i)) & 0x01]) + if len(ret) % w == 0: + break + elif bpp == 16: + # This is RGB565 + pixels = [(data[2 * i + 1] << 8) | data[2 * i] + for i in range(len(data) // 2)] + + for p in pixels: + ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R + ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G + ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B + elif bpp == 24: + if cf == ColorFormat.RGB888: + B = data[0::3] + G = data[1::3] + R = data[2::3] + for r, g, b in zip(R, G, B): + ret += [r, g, b] + elif cf == ColorFormat.RGB565A8: + alpha_size = len(data) // 3 + pixel_alpha = data[-alpha_size:] + pixel_data = data[:-alpha_size] + pixels = [(pixel_data[2 * i + 1] << 8) | pixel_data[2 * i] + for i in range(len(pixel_data) // 2)] + + for a, p in zip(pixel_alpha, pixels): + ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R + ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G + ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B + ret.append(a) + elif cf == ColorFormat.ARGB8565: + L = data[0::3] + H = data[1::3] + A = data[2::3] + + for h, l, a in zip(H, L, A): + p = (h << 8) | (l) + ret.append(bit_extend((p >> 11) & 0x1f, 5)) # R + ret.append(bit_extend((p >> 5) & 0x3f, 6)) # G + ret.append(bit_extend((p >> 0) & 0x1f, 5)) # B + ret.append(a) # A + + elif bpp == 32: + B = data[0::4] + G = data[1::4] + R = data[2::4] + A = data[3::4] + for r, g, b, a in zip(R, G, B, A): + ret += [r, g, b, a] + else: + assert 0 + + return ret + + +def write_c_array_file( + w: int, h: int, + stride: int, + cf: ColorFormat, + filename: str, + premultiplied: bool, + compress: CompressMethod, + data: bytes): + varname = path.basename(filename).split('.')[0] + varname = varname.replace("-", "_") + varname = varname.replace(".", "_") + + flags = "0" + if compress is not CompressMethod.NONE: + flags += " | LV_IMAGE_FLAGS_COMPRESSED" + if premultiplied: + flags += " | LV_IMAGE_FLAGS_PREMULTIPLIED" + + macro = "LV_ATTRIBUTE_" + varname.upper() + header = f''' +#if defined(LV_LVGL_H_INCLUDE_SIMPLE) +#include "lvgl.h" +#elif defined(LV_BUILD_TEST) +#include "../lvgl.h" +#else +#include "lvgl/lvgl.h" +#endif + + +#ifndef LV_ATTRIBUTE_MEM_ALIGN +#define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef {macro} +#define {macro} +#endif + +static const +LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST {macro} +uint8_t {varname}_map[] = {{ +''' + + ending = f''' +}}; + +const lv_image_dsc_t {varname} = {{ + .header.magic = LV_IMAGE_HEADER_MAGIC, + .header.cf = LV_COLOR_FORMAT_{cf.name}, + .header.flags = {flags}, + .header.w = {w}, + .header.h = {h}, + .header.stride = {stride}, + .data_size = sizeof({varname}_map), + .data = {varname}_map, +}}; + +''' + + def write_binary(f, data, stride): + stride = 16 if stride == 0 else stride + for i, v in enumerate(data): + if i % stride == 0: + f.write("\n ") + f.write(f"0x{v:02x},") + f.write("\n") + + with open(filename, "w+") as f: + f.write(header) + + if compress != CompressMethod.NONE: + write_binary(f, data, 16) + else: + # write palette separately + ncolors = cf.ncolors + if ncolors: + write_binary(f, data[:ncolors * 4], 16) + + write_binary(f, data[ncolors * 4:], stride) + + f.write(ending) + + +class LVGLImageHeader: + + def __init__(self, + cf: ColorFormat = ColorFormat.UNKNOWN, + w: int = 0, + h: int = 0, + stride: int = 0, + align: int = 1, + flags: int = 0): + self.cf = cf + self.flags = flags + self.w = w & 0xffff + self.h = h & 0xffff + if w > 0xffff or h > 0xffff: + raise ParameterError(f"w, h overflow: {w}x{h}") + if align < 1: + # stride align in bytes must be larger than 1 + raise ParameterError(f"Invalid stride align: {align}") + + self.stride = self.stride_align(align) if stride == 0 else stride + + def stride_align(self, align: int) -> int: + stride = self.stride_default + if align == 1: + pass + elif align > 1: + stride = (stride + align - 1) // align + stride *= align + else: + raise ParameterError(f"Invalid stride align: {align}") + + self.stride = stride + return stride + + @property + def stride_default(self) -> int: + return (self.w * self.cf.bpp + 7) // 8 + + @property + def binary(self) -> bytearray: + binary = bytearray() + binary += uint8_t(0x19) # magic number for lvgl version 9 + binary += uint8_t(self.cf.value) + binary += uint16_t(self.flags) # 16bits flags + + binary += uint16_t(self.w) # 16bits width + binary += uint16_t(self.h) # 16bits height + binary += uint16_t(self.stride) # 16bits stride + + binary += uint16_t(0) # 16bits reserved + return binary + + def from_binary(self, data: bytes): + if len(data) < 12: + raise FormatError("invalid header length") + + try: + self.cf = ColorFormat(data[1] & 0x1f) # color format + except ValueError as exc: + raise FormatError(f"invalid color format: {hex(data[0])}") from exc + self.w = int.from_bytes(data[4:6], 'little') + self.h = int.from_bytes(data[6:8], 'little') + self.stride = int.from_bytes(data[8:10], 'little') + return self + + +class LVGLCompressData: + + def __init__(self, + cf: ColorFormat, + method: CompressMethod, + raw_data: bytes = b''): + self.blk_size = (cf.bpp + 7) // 8 + self.compress = method + self.raw_data = raw_data + self.raw_data_len = len(raw_data) + self.compressed = self._compress(raw_data) + + def _compress(self, raw_data: bytes) -> bytearray: + if self.compress == CompressMethod.NONE: + return raw_data + + if self.compress == CompressMethod.RLE: + # RLE compression performs on pixel unit, pad data to pixel unit + pad = b'\x00' * 0 + if self.raw_data_len % self.blk_size: + pad = b'\x00' * (self.blk_size - self.raw_data_len % self.blk_size) + compressed = RLEImage().rle_compress(raw_data + pad, self.blk_size) + elif self.compress == CompressMethod.LZ4: + compressed = lz4.block.compress(raw_data, store_size=False) + else: + raise ParameterError(f"Invalid compress method: {self.compress}") + + self.compressed_len = len(compressed) + + bin = bytearray() + bin += uint32_t(self.compress.value) + bin += uint32_t(self.compressed_len) + bin += uint32_t(self.raw_data_len) + bin += compressed + return bin + + +class LVGLImage: + + def __init__(self, + cf: ColorFormat = ColorFormat.UNKNOWN, + w: int = 0, + h: int = 0, + data: bytes = b'') -> None: + self.stride = 0 # default no valid stride value + self.premultiplied = False + self.rgb565_dither = False + self.set_data(cf, w, h, data) + + def __repr__(self) -> str: + return (f"'LVGL image {self.w}x{self.h}, {self.cf.name}, " + f"{'Pre-multiplied, ' if self.premultiplied else ''}" + f"stride: {self.stride} " + f"(12+{self.data_len})Byte'") + + def adjust_stride(self, stride: int = 0, align: int = 1): + """ + Stride can be set directly, or by stride alignment in bytes + """ + if self.stride == 0: + # stride can only be 0, when LVGLImage is created with empty data + logging.warning("Cannot adjust stride for empty image") + return + + if align >= 1 and stride == 0: + # The header with specified stride alignment + header = LVGLImageHeader(self.cf, self.w, self.h, align=align) + stride = header.stride + elif stride > 0: + pass + else: + raise ParameterError(f"Invalid parameter, align:{align}," + f" stride:{stride}") + + if self.stride == stride: + return # no stride adjustment + + # if current image is empty, no need to do anything + if self.data_len == 0: + self.stride = 0 + return + + current = LVGLImageHeader(self.cf, self.w, self.h, stride=self.stride) + + if stride < current.stride_default: + raise ParameterError(f"Stride is too small:{stride}, " + f"minimal:{current.stride_default}") + + def change_stride(data: bytearray, h, current_stride, new_stride): + data_in = data + data_out = [] # stride adjusted new data + if new_stride < current_stride: # remove padding byte + for i in range(h): + start = i * current_stride + end = start + new_stride + data_out.append(data_in[start:end]) + else: # adding more padding bytes + padding = b'\x00' * (new_stride - current_stride) + for i in range(h): + data_out.append(data_in[i * current_stride:(i + 1) * + current_stride]) + data_out.append(padding) + return b''.join(data_out) + + palette_size = self.cf.ncolors * 4 + data_out = [self.data[:palette_size]] + data_out.append( + change_stride(self.data[palette_size:], self.h, current.stride, + stride)) + + # deal with alpha map for RGB565A8 + if self.cf == ColorFormat.RGB565A8: + logging.warning("handle RGB565A8 alpha map") + a8_stride = self.stride // 2 + a8_map = self.data[-a8_stride * self.h:] + data_out.append( + change_stride(a8_map, self.h, current.stride // 2, + stride // 2)) + + self.stride = stride + self.data = bytearray(b''.join(data_out)) + + def premultiply(self): + """ + Pre-multiply image RGB data with alpha, set corresponding image header flags + """ + if self.premultiplied: + raise ParameterError("Image already pre-multiplied") + + if not self.cf.has_alpha: + raise ParameterError(f"Image has no alpha channel: {self.cf.name}") + + if self.cf.is_indexed: + + def multiply(r, g, b, a): + r, g, b = (r * a) >> 8, (g * a) >> 8, (b * a) >> 8 + return uint8_t(b) + uint8_t(g) + uint8_t(r) + uint8_t(a) + + # process the palette only. + palette_size = self.cf.ncolors * 4 + palette = self.data[:palette_size] + palette = [ + multiply(palette[i], palette[i + 1], palette[i + 2], + palette[i + 3]) for i in range(0, len(palette), 4) + ] + palette = b''.join(palette) + self.data = palette + self.data[palette_size:] + elif self.cf is ColorFormat.ARGB8888: + + def multiply(b, g, r, a): + r, g, b = (r * a) >> 8, (g * a) >> 8, (b * a) >> 8 + return uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0)) + + line_width = self.w * 4 + for h in range(self.h): + offset = h * self.stride + map = self.data[offset:offset + self.stride] + + processed = b''.join([ + multiply(map[i], map[i + 1], map[i + 2], map[i + 3]) + for i in range(0, line_width, 4) + ]) + self.data[offset:offset + line_width] = processed + elif self.cf is ColorFormat.RGB565A8: + + def multiply(data, a): + r = (data >> 11) & 0x1f + g = (data >> 5) & 0x3f + b = (data >> 0) & 0x1f + + r, g, b = (r * a) // 255, (g * a) // 255, (b * a) // 255 + return uint16_t((r << 11) | (g << 5) | (b << 0)) + + line_width = self.w * 2 + for h in range(self.h): + # alpha map offset for this line + offset = self.h * self.stride + h * (self.stride // 2) + a = self.data[offset:offset + self.stride // 2] + + # RGB map offset + offset = h * self.stride + rgb = self.data[offset:offset + self.stride] + + processed = b''.join([ + multiply((rgb[i + 1] << 8) | rgb[i], a[i // 2]) + for i in range(0, line_width, 2) + ]) + self.data[offset:offset + line_width] = processed + elif self.cf is ColorFormat.ARGB8565: + + def multiply(data, a): + r = (data >> 11) & 0x1f + g = (data >> 5) & 0x3f + b = (data >> 0) & 0x1f + + r, g, b = (r * a) // 255, (g * a) // 255, (b * a) // 255 + return uint24_t((a << 16) | (r << 11) | (g << 5) | (b << 0)) + + line_width = self.w * 3 + for h in range(self.h): + offset = h * self.stride + map = self.data[offset:offset + self.stride] + + processed = b''.join([ + multiply((map[i + 1] << 8) | map[i], map[i + 2]) + for i in range(0, line_width, 3) + ]) + self.data[offset:offset + line_width] = processed + else: + raise ParameterError(f"Not supported yet: {self.cf.name}") + + self.premultiplied = True + + @property + def data_len(self) -> int: + """ + Return data_len in byte of this image, excluding image header + """ + + # palette is always in ARGB format, 4Byte per color + p = self.cf.ncolors * 4 if self.is_indexed and self.w * self.h else 0 + p += self.stride * self.h + if self.cf is ColorFormat.RGB565A8: + a8_stride = self.stride // 2 + p += a8_stride * self.h + return p + + @property + def header(self) -> bytearray: + return LVGLImageHeader(self.cf, self.w, self.h) + + @property + def is_indexed(self): + return self.cf.is_indexed + + def set_data(self, + cf: ColorFormat, + w: int, + h: int, + data: bytes, + stride: int = 0): + """ + Directly set LVGL image parameters + """ + + if w > 0xffff or h > 0xffff: + raise ParameterError(f"w, h overflow: {w}x{h}") + + self.cf = cf + self.w = w + self.h = h + + # if stride is 0, then it's aligned to 1byte by default, + # let image header handle it + self.stride = LVGLImageHeader(cf, w, h, stride, align=1).stride + + if self.data_len != len(data): + raise ParameterError(f"{self} data length error got: {len(data)}, " + f"expect: {self.data_len}, {self}") + + self.data = data + + return self + + def from_data(self, data: bytes): + header = LVGLImageHeader().from_binary(data) + return self.set_data(header.cf, header.w, header.h, + data[len(header.binary):], header.stride) + + def from_bin(self, filename: str): + """ + Read from existing bin file and update image parameters + """ + + if not filename.endswith(".bin"): + raise FormatError("filename not ended with '.bin'") + + with open(filename, "rb") as f: + data = f.read() + return self.from_data(data) + + def _check_ext(self, filename: str, ext): + if not filename.lower().endswith(ext): + raise FormatError(f"filename not ended with {ext}") + + def _check_dir(self, filename: str): + dir = path.dirname(filename) + if dir and not path.exists(dir): + logging.info(f"mkdir of {dir} for {filename}") + os.makedirs(dir) + + def to_bin(self, + filename: str, + compress: CompressMethod = CompressMethod.NONE): + """ + Write this image to file, filename should be ended with '.bin' + """ + self._check_ext(filename, ".bin") + self._check_dir(filename) + + with open(filename, "wb+") as f: + bin = bytearray() + flags = 0 + flags |= 0x08 if compress != CompressMethod.NONE else 0 + flags |= 0x01 if self.premultiplied else 0 + + header = LVGLImageHeader(self.cf, + self.w, + self.h, + self.stride, + flags=flags) + bin += header.binary + compressed = LVGLCompressData(self.cf, compress, self.data) + bin += compressed.compressed + + f.write(bin) + + return self + + def to_c_array(self, + filename: str, + compress: CompressMethod = CompressMethod.NONE): + self._check_ext(filename, ".c") + self._check_dir(filename) + + if compress != CompressMethod.NONE: + data = LVGLCompressData(self.cf, compress, self.data).compressed + else: + data = self.data + write_c_array_file(self.w, self.h, self.stride, self.cf, filename, + self.premultiplied, + compress, data) + + def to_png(self, filename: str): + self._check_ext(filename, ".png") + self._check_dir(filename) + + old_stride = self.stride + self.adjust_stride(align=1) + if self.cf.is_indexed: + data = self.data + # Separate lvgl bin image data to palette and bitmap + # The palette is in format of [(RGBA), (RGBA)...]. + # LVGL palette is in format of B,G,R,A,... + palette = [(data[i * 4 + 2], data[i * 4 + 1], data[i * 4 + 0], + data[i * 4 + 3]) for i in range(self.cf.ncolors)] + + data = data[self.cf.ncolors * 4:] + + encoder = png.Writer(self.w, + self.h, + palette=palette, + bitdepth=self.cf.bpp) + # separate packed data to plain data + data = unpack_colors(data, self.cf, self.w) + elif self.cf.is_alpha_only: + # separate packed data to plain data + transparency = unpack_colors(self.data, self.cf, self.w) + data = [] + for a in transparency: + data += [0, 0, 0, a] + encoder = png.Writer(self.w, self.h, greyscale=False, alpha=True) + elif self.cf == ColorFormat.L8: + # to grayscale + encoder = png.Writer(self.w, + self.h, + bitdepth=self.cf.bpp, + greyscale=True, + alpha=False) + data = self.data + elif self.cf.is_colormap: + encoder = png.Writer(self.w, + self.h, + alpha=self.cf.has_alpha, + greyscale=False) + data = unpack_colors(self.data, self.cf, self.w) + else: + logging.warning(f"missing logic: {self.cf.name}") + return + + with open(filename, "wb") as f: + encoder.write_array(f, data) + + self.adjust_stride(stride=old_stride) + + def from_png(self, + filename: str, + cf: ColorFormat = None, + background: int = 0x00_00_00, + rgb565_dither=False): + """ + Create lvgl image from png file. + If cf is none, used I1/2/4/8 based on palette size + """ + + self.background = background + self.rgb565_dither = rgb565_dither + + if cf is None: # guess cf from filename + # split filename string and match with ColorFormat to check + # which cf to use + names = str(path.basename(filename)).split(".") + for c in names[1:-1]: + if c in ColorFormat.__members__: + cf = ColorFormat[c] + break + + if cf is None or cf.is_indexed: # palette mode + self._png_to_indexed(cf, filename) + elif cf.is_alpha_only: + self._png_to_alpha_only(cf, filename) + elif cf.is_luma_only: + self._png_to_luma_only(cf, filename) + elif cf.is_colormap: + self._png_to_colormap(cf, filename) + else: + logging.warning(f"missing logic: {cf.name}") + + logging.info(f"from png: {filename}, cf: {self.cf.name}") + return self + + def _png_to_indexed(self, cf: ColorFormat, filename: str): + # convert to palette mode + auto_cf = cf is None + + # read the image data to get the metadata + reader = png.Reader(filename=filename) + w, h, rows, metadata = reader.read() + + # to preserve original palette data only convert the image if needed. For this + # check if image has a palette and the requested palette size equals the existing one + if not 'palette' in metadata or not auto_cf and len(metadata['palette']) != 2 ** cf.bpp: + # reread and convert file + reader = png.Reader( + bytes=PngQuant(256 if auto_cf else cf.ncolors).convert(filename)) + w, h, rows, _ = reader.read() + + palette = reader.palette(alpha="force") # always return alpha + + palette_len = len(palette) + if auto_cf: + if palette_len <= 2: + cf = ColorFormat.I1 + elif palette_len <= 4: + cf = ColorFormat.I2 + elif palette_len <= 16: + cf = ColorFormat.I4 + else: + cf = ColorFormat.I8 + + if palette_len != cf.ncolors: + if not auto_cf: + logging.warning( + f"{path.basename(filename)} palette: {palette_len}, " + f"extended to: {cf.ncolors}") + palette += [(255, 255, 255, 0)] * (cf.ncolors - palette_len) + + # Assemble lvgl image palette from PNG palette. + # PNG palette is a list of tuple(R,G,B,A) + + rawdata = bytearray() + for (r, g, b, a) in palette: + rawdata += uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0)) + + # pack data if not in I8 format + if cf == ColorFormat.I8: + for e in rows: + rawdata += e + else: + for e in png.pack_rows(rows, cf.bpp): + rawdata += e + + self.set_data(cf, w, h, rawdata) + + def _png_to_alpha_only(self, cf: ColorFormat, filename: str): + reader = png.Reader(str(filename)) + w, h, rows, info = reader.asRGBA8() + if not info['alpha']: + raise FormatError(f"{filename} has no alpha channel") + + rawdata = bytearray() + if cf == ColorFormat.A8: + for row in rows: + A = row[3::4] + for e in A: + rawdata += uint8_t(e) + else: + shift = 8 - cf.bpp + mask = 2**cf.bpp - 1 + rows = [[(a >> shift) & mask for a in row[3::4]] for row in rows] + for row in png.pack_rows(rows, cf.bpp): + rawdata += row + + self.set_data(cf, w, h, rawdata) + + def sRGB_to_linear(self, x): + if x < 0.04045: + return x / 12.92 + return pow((x + 0.055) / 1.055, 2.4) + + def linear_to_sRGB(self, y): + if y <= 0.0031308: + return 12.92 * y + return 1.055 * pow(y, 1 / 2.4) - 0.055 + + def _png_to_luma_only(self, cf: ColorFormat, filename: str): + reader = png.Reader(str(filename)) + w, h, rows, info = reader.asRGBA8() + rawdata = bytearray() + for row in rows: + R = row[0::4] + G = row[1::4] + B = row[2::4] + A = row[3::4] + for r, g, b, a in zip(R, G, B, A): + r, g, b, a = color_pre_multiply(r, g, b, a, self.background) + r = self.sRGB_to_linear(r / 255.0) + g = self.sRGB_to_linear(g / 255.0) + b = self.sRGB_to_linear(b / 255.0) + luma = 0.2126 * r + 0.7152 * g + 0.0722 * b + rawdata += uint8_t(int(self.linear_to_sRGB(luma) * 255)) + + self.set_data(ColorFormat.L8, w, h, rawdata) + + def _png_to_colormap(self, cf, filename: str): + + if cf == ColorFormat.ARGB8888: + + def pack(r, g, b, a): + return uint32_t((a << 24) | (r << 16) | (g << 8) | (b << 0)) + elif cf == ColorFormat.XRGB8888: + + def pack(r, g, b, a): + r, g, b, a = color_pre_multiply(r, g, b, a, self.background) + return uint32_t((0xff << 24) | (r << 16) | (g << 8) | (b << 0)) + elif cf == ColorFormat.RGB888: + + def pack(r, g, b, a): + r, g, b, a = color_pre_multiply(r, g, b, a, self.background) + return uint24_t((r << 16) | (g << 8) | (b << 0)) + elif cf == ColorFormat.RGB565: + + def pack(r, g, b, a): + r, g, b, a = color_pre_multiply(r, g, b, a, self.background) + color = (r >> 3) << 11 + color |= (g >> 2) << 5 + color |= (b >> 3) << 0 + return uint16_t(color) + + elif cf == ColorFormat.RGB565A8: + + def pack(r, g, b, a): + color = (r >> 3) << 11 + color |= (g >> 2) << 5 + color |= (b >> 3) << 0 + return uint16_t(color) + elif cf == ColorFormat.ARGB8565: + + def pack(r, g, b, a): + color = (r >> 3) << 11 + color |= (g >> 2) << 5 + color |= (b >> 3) << 0 + return uint24_t((a << 16) | color) + else: + raise FormatError(f"Invalid color format: {cf.name}") + + reader = png.Reader(str(filename)) + w, h, rows, _ = reader.asRGBA8() + rawdata = bytearray() + alpha = bytearray() + for y, row in enumerate(rows): + R = row[0::4] + G = row[1::4] + B = row[2::4] + A = row[3::4] + for x, (r, g, b, a) in enumerate(zip(R, G, B, A)): + if cf == ColorFormat.RGB565A8: + alpha += uint8_t(a) + + if ( + self.rgb565_dither and + cf in (ColorFormat.RGB565, ColorFormat.RGB565A8, ColorFormat.ARGB8565) + ): + treshold_id = ((y & 7) << 3) + (x & 7) + + r = min(r + red_thresh[treshold_id], 0xFF) & 0xF8 + g = min(g + green_thresh[treshold_id], 0xFF) & 0xFC + b = min(b + blue_thresh[treshold_id], 0xFF) & 0xF8 + + rawdata += pack(r, g, b, a) + + if cf == ColorFormat.RGB565A8: + rawdata += alpha + + self.set_data(cf, w, h, rawdata) + + +red_thresh = [ + 1, 7, 3, 5, 0, 8, 2, 6, + 7, 1, 5, 3, 8, 0, 6, 2, + 3, 5, 0, 8, 2, 6, 1, 7, + 5, 3, 8, 0, 6, 2, 7, 1, + 0, 8, 2, 6, 1, 7, 3, 5, + 8, 0, 6, 2, 7, 1, 5, 3, + 2, 6, 1, 7, 3, 5, 0, 8, + 6, 2, 7, 1, 5, 3, 8, 0 +] + +green_thresh = [ + 1, 3, 2, 2, 3, 1, 2, 2, + 2, 2, 0, 4, 2, 2, 4, 0, + 3, 1, 2, 2, 1, 3, 2, 2, + 2, 2, 4, 0, 2, 2, 0, 4, + 1, 3, 2, 2, 3, 1, 2, 2, + 2, 2, 0, 4, 2, 2, 4, 0, + 3, 1, 2, 2, 1, 3, 2, 2, + 2, 2, 4, 0, 2, 2, 0, 4 +] + +blue_thresh = [ + 5, 3, 8, 0, 6, 2, 7, 1, + 3, 5, 0, 8, 2, 6, 1, 7, + 8, 0, 6, 2, 7, 1, 5, 3, + 0, 8, 2, 6, 1, 7, 3, 5, + 6, 2, 7, 1, 5, 3, 8, 0, + 2, 6, 1, 7, 3, 5, 0, 8, + 7, 1, 5, 3, 8, 0, 6, 2, + 1, 7, 3, 5, 0, 8, 2, 6 +] + + +class RLEHeader: + + def __init__(self, blksize: int, len: int): + self.blksize = blksize + self.len = len + + @property + def binary(self): + magic = 0x5aa521e0 + + rle_header = self.blksize + rle_header |= (self.len & 0xffffff) << 4 + + binary = bytearray() + binary.extend(uint32_t(magic)) + binary.extend(uint32_t(rle_header)) + return binary + + +class RLEImage(LVGLImage): + + def __init__(self, + cf: ColorFormat = ColorFormat.UNKNOWN, + w: int = 0, + h: int = 0, + data: bytes = b'') -> None: + super().__init__(cf, w, h, data) + + def to_rle(self, filename: str): + """ + Compress this image to file, filename should be ended with '.rle' + """ + self._check_ext(filename, ".rle") + self._check_dir(filename) + + # compress image data excluding lvgl image header + blksize = (self.cf.bpp + 7) // 8 + compressed = self.rle_compress(self.data, blksize) + with open(filename, "wb+") as f: + header = RLEHeader(blksize, len(self.data)).binary + header.extend(self.header.binary) + f.write(header) + f.write(compressed) + + def rle_compress(self, data: bytearray, blksize: int, threshold=16): + index = 0 + data_len = len(data) + compressed_data = [] + memview = memoryview(data) + while index < data_len: + repeat_cnt = self.get_repeat_count(memview[index:], blksize) + if repeat_cnt == 0: + # done + break + elif repeat_cnt < threshold: + nonrepeat_cnt = self.get_nonrepeat_count( + memview[index:], blksize, threshold) + ctrl_byte = uint8_t(nonrepeat_cnt | 0x80) + compressed_data.append(ctrl_byte) + compressed_data.append(memview[index:index + + nonrepeat_cnt * blksize]) + index += nonrepeat_cnt * blksize + else: + ctrl_byte = uint8_t(repeat_cnt) + compressed_data.append(ctrl_byte) + compressed_data.append(memview[index:index + blksize]) + index += repeat_cnt * blksize + + return b"".join(compressed_data) + + def get_repeat_count(self, data: bytearray, blksize: int): + if len(data) < blksize: + return 0 + + start = data[:blksize] + index = 0 + repeat_cnt = 0 + value = 0 + + while index < len(data): + value = data[index:index + blksize] + + if value == start: + repeat_cnt += 1 + if repeat_cnt == 127: # limit max repeat count to max value of signed char. + break + else: + break + index += blksize + + return repeat_cnt + + def get_nonrepeat_count(self, data: bytearray, blksize: int, threshold): + if len(data) < blksize: + return 0 + + pre_value = data[:blksize] + + index = 0 + nonrepeat_count = 0 + + repeat_cnt = 0 + while True: + value = data[index:index + blksize] + if value == pre_value: + repeat_cnt += 1 + if repeat_cnt > threshold: + # repeat found. + break + else: + pre_value = value + nonrepeat_count += 1 + repeat_cnt + repeat_cnt = 0 + if nonrepeat_count >= 127: # limit max repeat count to max value of signed char. + nonrepeat_count = 127 + break + + index += blksize # move to next position + if index >= len(data): # data end + nonrepeat_count += repeat_cnt + break + + return nonrepeat_count + + +class RAWImage(): + ''' + RAW image is an exception to LVGL image, it has color format of RAW or RAW_ALPHA. + It has same image header as LVGL image, but the data is pure raw data from file. + It does not support stride adjustment etc. features for LVGL image. + It only supports convert an image to C array with RAW or RAW_ALPHA format. + ''' + CF_SUPPORTED = (ColorFormat.RAW, ColorFormat.RAW_ALPHA) + + class NotSupported(NotImplementedError): + pass + + def __init__(self, + cf: ColorFormat = ColorFormat.UNKNOWN, + data: bytes = b'') -> None: + self.cf = cf + self.data = data + + def to_c_array(self, + filename: str): + # Image size is set to zero, to let PNG or JPEG decoder to handle it + # Stride is meaningless for RAW image + write_c_array_file(0, 0, 0, self.cf, filename, + False, CompressMethod.NONE, self.data) + + def from_file(self, + filename: str, + cf: ColorFormat = None): + if cf not in RAWImage.CF_SUPPORTED: + raise RAWImage.NotSupported(f"Invalid color format: {cf.name}") + + with open(filename, "rb") as f: + self.data = f.read() + self.cf = cf + return self + + +class OutputFormat(Enum): + C_ARRAY = "C" + BIN_FILE = "BIN" + PNG_FILE = "PNG" # convert to lvgl image and then to png + + +class PNGConverter: + + def __init__(self, + files: List, + cf: ColorFormat, + ofmt: OutputFormat, + odir: str, + background: int = 0x00, + align: int = 1, + premultiply: bool = False, + compress: CompressMethod = CompressMethod.NONE, + keep_folder=True, + rgb565_dither=False) -> None: + self.files = files + self.cf = cf + self.ofmt = ofmt + self.output = odir + self.pngquant = None + self.keep_folder = keep_folder + self.align = align + self.premultiply = premultiply + self.compress = compress + self.background = background + self.rgb565_dither = rgb565_dither + + def _replace_ext(self, input, ext): + if self.keep_folder: + name, _ = path.splitext(input) + else: + name, _ = path.splitext(path.basename(input)) + output = name + ext + output = path.join(self.output, output) + return output + + def convert(self): + output = [] + for f in self.files: + if self.cf in (ColorFormat.RAW, ColorFormat.RAW_ALPHA): + # Process RAW image explicitly + img = RAWImage().from_file(f, self.cf) + img.to_c_array(self._replace_ext(f, ".c")) + else: + img = LVGLImage().from_png(f, self.cf, background=self.background, rgb565_dither=self.rgb565_dither) + img.adjust_stride(align=self.align) + + if self.premultiply: + img.premultiply() + output.append((f, img)) + if self.ofmt == OutputFormat.BIN_FILE: + img.to_bin(self._replace_ext(f, ".bin"), + compress=self.compress) + elif self.ofmt == OutputFormat.C_ARRAY: + img.to_c_array(self._replace_ext(f, ".c"), + compress=self.compress) + elif self.ofmt == OutputFormat.PNG_FILE: + img.to_png(self._replace_ext(f, ".png")) + + return output + + +def main(): + parser = argparse.ArgumentParser(description='LVGL PNG to bin image tool.') + parser.add_argument('--ofmt', + help="output filename format, C or BIN", + default="BIN", + choices=["C", "BIN", "PNG"]) + parser.add_argument( + '--cf', + help=("bin image color format, use AUTO for automatically " + "choose from I1/2/4/8"), + default="I8", + choices=[ + "L8", "I1", "I2", "I4", "I8", "A1", "A2", "A4", "A8", "ARGB8888", + "XRGB8888", "RGB565", "RGB565A8", "ARGB8565", "RGB888", "AUTO", + "RAW", "RAW_ALPHA" + ]) + + parser.add_argument('--rgb565dither', action='store_true', + help="use dithering to correct banding in gradients", default=False) + + parser.add_argument('--premultiply', action='store_true', + help="pre-multiply color with alpha", default=False) + + parser.add_argument('--compress', + help=("Binary data compress method, default to NONE"), + default="NONE", + choices=["NONE", "RLE", "LZ4"]) + + parser.add_argument('--align', + help="stride alignment in bytes for bin image", + default=1, + type=int, + metavar='byte', + nargs='?') + parser.add_argument('--background', + help="Background color for formats without alpha", + default=0x00_00_00, + type=lambda x: int(x, 0), + metavar='color', + nargs='?') + parser.add_argument('-o', + '--output', + default="./output", + help="Select the output folder, default to ./output") + parser.add_argument('-v', '--verbose', action='store_true') + parser.add_argument( + 'input', help="the filename or folder to be recursively converted") + + args = parser.parse_args() + + if path.isfile(args.input): + files = [args.input] + elif path.isdir(args.input): + files = list(Path(args.input).rglob("*.[pP][nN][gG]")) + else: + raise BaseException(f"invalid input: {args.input}") + + if args.verbose: + logging.basicConfig(level=logging.INFO) + + logging.info(f"options: {args.__dict__}, files:{[str(f) for f in files]}") + + if args.cf == "AUTO": + cf = None + else: + cf = ColorFormat[args.cf] + + ofmt = OutputFormat(args.ofmt) if cf not in ( + ColorFormat.RAW, ColorFormat.RAW_ALPHA) else OutputFormat.C_ARRAY + compress = CompressMethod[args.compress] + + converter = PNGConverter(files, + cf, + ofmt, + args.output, + background=args.background, + align=args.align, + premultiply=args.premultiply, + compress=compress, + keep_folder=False, + rgb565_dither=args.rgb565dither) + output = converter.convert() + for f, img in output: + logging.info(f"len: {img.data_len} for {path.basename(f)} ") + + print(f"done {len(files)} files") + + +def test(): + logging.basicConfig(level=logging.INFO) + f = "pngs/cogwheel.RGB565A8.png" + img = LVGLImage().from_png(f, + cf=ColorFormat.ARGB8565, + background=0xFF_FF_00, + rgb565_dither=True) + img.adjust_stride(align=16) + img.premultiply() + img.to_bin("output/cogwheel.ARGB8565.bin") + img.to_c_array("output/cogwheel-abc.c") # file name is used as c var name + img.to_png("output/cogwheel.ARGB8565.png.png") # convert back to png + + +def test_raw(): + logging.basicConfig(level=logging.INFO) + f = "pngs/cogwheel.RGB565A8.png" + img = RAWImage().from_file(f, + cf=ColorFormat.RAW_ALPHA) + img.to_c_array("output/cogwheel-raw.c") + + +if __name__ == "__main__": + # test() + # test_raw() + main() diff --git a/scripts/Image_Converter/README.md b/scripts/Image_Converter/README.md new file mode 100644 index 0000000..91b4ed0 --- /dev/null +++ b/scripts/Image_Converter/README.md @@ -0,0 +1,33 @@ +# LVGL图片转换工具 + +这个目录包含两个用于处理和转换图片为LVGL格式的Python脚本: + +## 1. LVGLImage (LVGLImage.py) + +引用自LVGL[官方repo](https://github.com/lvgl/lvgl)的转换脚本[LVGLImage.py](https://github.com/lvgl/lvgl/blob/master/scripts/LVGLImage.py) + +## 2. LVGL图片转换工具 (lvgl_tools_gui.py) + +调用`LVGLImage.py`,将图片批量转换为LVGL图片格式 +可用于修改小智的默认表情,具体修改教程[在这里](https://www.bilibili.com/video/BV12FQkYeEJ3/) + +### 特性 + +- 图形化操作,界面更友好 +- 支持批量转换图片 +- 自动识别图片格式并选择最佳的颜色格式转换 +- 多分辨率支持 + +### 使用方法 + +安装Pillow + +```bash +pip install Pillow # 处理图像需要 +``` + +运行转换工具 + +```bash +python lvgl_tools_gui.py +``` diff --git a/scripts/Image_Converter/lvgl_tools_gui.py b/scripts/Image_Converter/lvgl_tools_gui.py new file mode 100644 index 0000000..821a295 --- /dev/null +++ b/scripts/Image_Converter/lvgl_tools_gui.py @@ -0,0 +1,253 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +from PIL import Image +import os +import tempfile +import sys +from LVGLImage import LVGLImage, ColorFormat, CompressMethod + +HELP_TEXT = """LVGL图片转换工具使用说明: + +1. 添加文件:点击“添加文件”按钮选择需要转换的图片,支持批量导入 + +2. 移除文件:在列表中选中文件前的复选框“[ ]”(选中后会变成“[√]”),点击“移除选中”可删除选定文件 + +3. 设置分辨率:选择需要的分辨率,如128x128 + 建议根据自己的设备的屏幕分辨率来选择。过大和过小都会影响显示效果。 + +4. 颜色格式:选择“自动识别”会根据图片是否透明自动选择,或手动指定 + 除非你了解这个选项,否则建议使用自动识别,不然可能会出现一些意想不到的问题…… + +5. 压缩方式:选择NONE或RLE压缩 + 除非你了解这个选项,否则建议保持默认NONE不压缩 + +6. 输出目录:设置转换后文件的保存路径 + 默认为程序所在目录下的output文件夹 + +7. 转换:点击“转换全部”或“转换选中”开始转换 +""" + +class ImageConverterApp: + def __init__(self, root): + self.root = root + self.root.title("LVGL图片转换工具") + self.root.geometry("750x650") + + # 初始化变量 + self.output_dir = tk.StringVar(value=os.path.abspath("output")) + self.resolution = tk.StringVar(value="128x128") + self.color_format = tk.StringVar(value="自动识别") + self.compress_method = tk.StringVar(value="NONE") + + # 创建UI组件 + self.create_widgets() + self.redirect_output() + + def create_widgets(self): + # 参数设置框架 + settings_frame = ttk.LabelFrame(self.root, text="转换设置") + settings_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew") + + # 分辨率设置 + ttk.Label(settings_frame, text="分辨率:").grid(row=0, column=0, padx=2) + ttk.Combobox(settings_frame, textvariable=self.resolution, + values=["128x128", "64x64", "32x32"], width=8).grid(row=0, column=1, padx=2) + + # 颜色格式 + ttk.Label(settings_frame, text="颜色格式:").grid(row=0, column=2, padx=2) + ttk.Combobox(settings_frame, textvariable=self.color_format, + values=["自动识别", "RGB565", "RGB565A8"], width=10).grid(row=0, column=3, padx=2) + + # 压缩方式 + ttk.Label(settings_frame, text="压缩方式:").grid(row=0, column=4, padx=2) + ttk.Combobox(settings_frame, textvariable=self.compress_method, + values=["NONE", "RLE"], width=8).grid(row=0, column=5, padx=2) + + # 文件操作框架 + file_frame = ttk.LabelFrame(self.root, text="输入文件") + file_frame.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") + + # 文件操作按钮 + btn_frame = ttk.Frame(file_frame) + btn_frame.pack(fill=tk.X, pady=2) + ttk.Button(btn_frame, text="添加文件", command=self.select_files).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_frame, text="移除选中", command=self.remove_selected).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_frame, text="清空列表", command=self.clear_files).pack(side=tk.LEFT, padx=2) + + # 文件列表(Treeview) + self.tree = ttk.Treeview(file_frame, columns=("selected", "filename"), + show="headings", height=10) + self.tree.heading("selected", text="选中", anchor=tk.W) + self.tree.heading("filename", text="文件名", anchor=tk.W) + self.tree.column("selected", width=60, anchor=tk.W) + self.tree.column("filename", width=600, anchor=tk.W) + self.tree.pack(fill=tk.BOTH, expand=True) + self.tree.bind("", self.on_tree_click) + + # 输出目录 + output_frame = ttk.LabelFrame(self.root, text="输出目录") + output_frame.grid(row=2, column=0, padx=10, pady=5, sticky="ew") + ttk.Entry(output_frame, textvariable=self.output_dir, width=60).pack(side=tk.LEFT, padx=5) + ttk.Button(output_frame, text="浏览", command=self.select_output_dir).pack(side=tk.RIGHT, padx=5) + + # 转换按钮和帮助按钮 + convert_frame = ttk.Frame(self.root) + convert_frame.grid(row=3, column=0, padx=10, pady=10) + ttk.Button(convert_frame, text="转换全部文件", command=lambda: self.start_conversion(True)).pack(side=tk.LEFT, padx=5) + ttk.Button(convert_frame, text="转换选中文件", command=lambda: self.start_conversion(False)).pack(side=tk.LEFT, padx=5) + ttk.Button(convert_frame, text="帮助", command=self.show_help).pack(side=tk.RIGHT, padx=5) + + # 日志区域(新增清空按钮部分) + log_frame = ttk.LabelFrame(self.root, text="日志") + log_frame.grid(row=4, column=0, padx=10, pady=5, sticky="nsew") + + # 添加按钮框架 + log_btn_frame = ttk.Frame(log_frame) + log_btn_frame.pack(fill=tk.X, side=tk.BOTTOM) + + # 清空日志按钮 + ttk.Button(log_btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.RIGHT, padx=5, pady=2) + + self.log_text = tk.Text(log_frame, height=15) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 布局配置 + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(1, weight=1) + self.root.rowconfigure(4, weight=1) + + def clear_log(self): + """清空日志内容""" + self.log_text.delete(1.0, tk.END) + + def show_help(self): + messagebox.showinfo("帮助", HELP_TEXT) + + def redirect_output(self): + class StdoutRedirector: + def __init__(self, text_widget): + self.text_widget = text_widget + self.original_stdout = sys.stdout + + def write(self, message): + self.text_widget.insert(tk.END, message) + self.text_widget.see(tk.END) + self.original_stdout.write(message) + + def flush(self): + self.original_stdout.flush() + + sys.stdout = StdoutRedirector(self.log_text) + + def on_tree_click(self, event): + region = self.tree.identify("region", event.x, event.y) + if region == "cell": + col = self.tree.identify_column(event.x) + item = self.tree.identify_row(event.y) + if col == "#1": # 点击的是选中列 + current_val = self.tree.item(item, "values")[0] + new_val = "[√]" if current_val == "[ ]" else "[ ]" + self.tree.item(item, values=(new_val, self.tree.item(item, "values")[1])) + + def select_output_dir(self): + path = filedialog.askdirectory() + if path: + self.output_dir.set(path) + + def select_files(self): + files = filedialog.askopenfilenames(filetypes=[("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif")]) + for f in files: + self.tree.insert("", tk.END, values=("[ ]", os.path.basename(f)), tags=(f,)) + + def remove_selected(self): + to_remove = [] + for item in self.tree.get_children(): + if self.tree.item(item, "values")[0] == "[√]": + to_remove.append(item) + for item in reversed(to_remove): + self.tree.delete(item) + + def clear_files(self): + for item in self.tree.get_children(): + self.tree.delete(item) + + def start_conversion(self, convert_all): + input_files = [ + self.tree.item(item, "tags")[0] + for item in self.tree.get_children() + if convert_all or self.tree.item(item, "values")[0] == "[√]" + ] + + if not input_files: + msg = "没有找到可转换的文件" if convert_all else "没有选中任何文件" + messagebox.showwarning("警告", msg) + return + + os.makedirs(self.output_dir.get(), exist_ok=True) + + # 解析转换参数 + width, height = map(int, self.resolution.get().split('x')) + compress = CompressMethod.RLE if self.compress_method.get() == "RLE" else CompressMethod.NONE + + # 执行转换 + self.convert_images(input_files, width, height, compress) + + def convert_images(self, input_files, width, height, compress): + success_count = 0 + total_files = len(input_files) + + for idx, file_path in enumerate(input_files): + try: + print(f"正在处理: {os.path.basename(file_path)}") + + with Image.open(file_path) as img: + # 调整图片大小 + img = img.resize((width, height), Image.Resampling.LANCZOS) + + # 处理颜色格式 + color_format_str = self.color_format.get() + if color_format_str == "自动识别": + # 检测透明通道 + has_alpha = img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info) + if has_alpha: + img = img.convert('RGBA') + cf = ColorFormat.RGB565A8 + else: + img = img.convert('RGB') + cf = ColorFormat.RGB565 + else: + if color_format_str == "RGB565A8": + img = img.convert('RGBA') + cf = ColorFormat.RGB565A8 + else: + img = img.convert('RGB') + cf = ColorFormat.RGB565 + + # 保存调整后的图片 + base_name = os.path.splitext(os.path.basename(file_path))[0] + output_image_path = os.path.join(self.output_dir.get(), f"{base_name}_{width}x{height}.png") + img.save(output_image_path, 'PNG') + + # 创建临时文件 + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile: + temp_path = tmpfile.name + img.save(temp_path, 'PNG') + + # 转换为LVGL C数组 + lvgl_img = LVGLImage().from_png(temp_path, cf=cf) + output_c_path = os.path.join(self.output_dir.get(), f"{base_name}.c") + lvgl_img.to_c_array(output_c_path, compress=compress) + + success_count += 1 + os.unlink(temp_path) + print(f"成功转换: {base_name}.c\n") + + except Exception as e: + print(f"转换失败: {str(e)}\n") + + print(f"转换完成! 成功 {success_count}/{total_files} 个文件\n") + +if __name__ == "__main__": + root = tk.Tk() + app = ImageConverterApp(root) + root.mainloop() \ No newline at end of file diff --git a/scripts/flash.sh b/scripts/flash.sh new file mode 100644 index 0000000..444ed47 --- /dev/null +++ b/scripts/flash.sh @@ -0,0 +1,2 @@ +#!/bin/sh +esptool.py -p /dev/ttyACM0 -b 2000000 write_flash 0 ../releases/v0.9.9_bread-compact-wifi/merged-binary.bin diff --git a/scripts/gen_lang.py b/scripts/gen_lang.py new file mode 100644 index 0000000..ed1abc6 --- /dev/null +++ b/scripts/gen_lang.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import argparse +import json +import os + +HEADER_TEMPLATE = """// Auto-generated language config +#pragma once + +#include + +#ifndef {lang_code_for_font} + #define {lang_code_for_font} // 預設語言 +#endif + +namespace Lang {{ + // 语言元数据 + constexpr const char* CODE = "{lang_code}"; + + // 字符串资源 + namespace Strings {{ +{strings} + }} + + // 音效资源 + namespace Sounds {{ +{sounds} + }} +}} +""" + +def generate_header(input_path, output_path): + with open(input_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 验证数据结构 + if 'language' not in data or 'strings' not in data: + raise ValueError("Invalid JSON structure") + + lang_code = data['language']['type'] + + # 生成字符串常量 + strings = [] + sounds = [] + for key, value in data['strings'].items(): + value = value.replace('"', '\\"') + strings.append(f' constexpr const char* {key.upper()} = "{value}";') + + # 生成音效常量 + for file in os.listdir(os.path.dirname(input_path)): + if file.endswith('.p3'): + base_name = os.path.splitext(file)[0] + sounds.append(f''' + extern const char p3_{base_name}_start[] asm("_binary_{base_name}_p3_start"); + extern const char p3_{base_name}_end[] asm("_binary_{base_name}_p3_end"); + static const std::string_view P3_{base_name.upper()} {{ + static_cast(p3_{base_name}_start), + static_cast(p3_{base_name}_end - p3_{base_name}_start) + }};''') + + # 生成公共音效 + for file in os.listdir(os.path.join(os.path.dirname(output_path), 'common')): + if file.endswith('.p3'): + base_name = os.path.splitext(file)[0] + sounds.append(f''' + extern const char p3_{base_name}_start[] asm("_binary_{base_name}_p3_start"); + extern const char p3_{base_name}_end[] asm("_binary_{base_name}_p3_end"); + static const std::string_view P3_{base_name.upper()} {{ + static_cast(p3_{base_name}_start), + static_cast(p3_{base_name}_end - p3_{base_name}_start) + }};''') + + # 填充模板 + content = HEADER_TEMPLATE.format( + lang_code=lang_code, + lang_code_for_font=lang_code.replace('-', '_').lower(), + strings="\n".join(sorted(strings)), + sounds="\n".join(sorted(sounds)) + ) + + # 写入文件 + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as f: + f.write(content) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--input", required=True, help="输入JSON文件路径") + parser.add_argument("--output", required=True, help="输出头文件路径") + args = parser.parse_args() + + generate_header(args.input, args.output) \ No newline at end of file diff --git a/scripts/p3_tools/README.md b/scripts/p3_tools/README.md new file mode 100644 index 0000000..0ee279c --- /dev/null +++ b/scripts/p3_tools/README.md @@ -0,0 +1,95 @@ +# P3音频格式转换与播放工具 + +这个目录包含两个用于处理P3格式音频文件的Python脚本: + +## 1. 音频转换工具 (convert_audio_to_p3.py) + +将普通音频文件转换为P3格式(4字节header + Opus数据包的流式结构)并进行响度标准化。 + +### 使用方法 + +```bash +python convert_audio_to_p3.py <输入音频文件> <输出P3文件> [-l LUFS] [-d] +``` + +其中,可选选项 `-l` 用于指定响度标准化的目标响度,默认为 -16 LUFS;可选选项 `-d` 可以禁用响度标准化。 + +如果输入的音频文件符合下面的任一条件,建议使用 `-d` 禁用响度标准化: +- 音频过短 +- 音频已经调整过响度 +- 音频来自默认 TTS (小智当前使用的 TTS 的默认响度已是 -16 LUFS) + +例如: +```bash +python convert_audio_to_p3.py input.mp3 output.p3 +``` + +## 2. P3音频播放工具 (play_p3.py) + +播放P3格式的音频文件。 + +### 特性 + +- 解码并播放P3格式的音频文件 +- 在播放结束或用户中断时应用淡出效果,避免破音 +- 支持通过命令行参数指定要播放的文件 + +### 使用方法 + +```bash +python play_p3.py +``` + +例如: +```bash +python play_p3.py output.p3 +``` + +## 3. 音频转回工具 (convert_p3_to_audio.py) + +将P3格式转换回普通音频文件。 + +### 使用方法 + +```bash +python convert_p3_to_audio.py <输入P3文件> <输出音频文件> +``` + +输出音频文件需要有扩展名。 + +例如: +```bash +python convert_p3_to_audio.py input.p3 output.wav +``` +## 4. 音频/P3批量转换工具 + +一个图形化的工具,支持批量转换音频到P3,P3到音频 + +![](./img/img.png) + +### 使用方法: +```bash +python batch_convert_gui.py +``` + +## 依赖安装 + +在使用这些脚本前,请确保安装了所需的Python库: + +```bash +pip install librosa opuslib numpy tqdm sounddevice pyloudnorm soundfile +``` + +或者使用提供的requirements.txt文件: + +```bash +pip install -r requirements.txt +``` + +## P3格式说明 + +P3格式是一种简单的流式音频格式,结构如下: +- 每个音频帧由一个4字节的头部和一个Opus编码的数据包组成 +- 头部格式:[1字节类型, 1字节保留, 2字节长度] +- 采样率固定为16000Hz,单声道 +- 每帧时长为60ms \ No newline at end of file diff --git a/scripts/p3_tools/batch_convert_gui.py b/scripts/p3_tools/batch_convert_gui.py new file mode 100644 index 0000000..8555e55 --- /dev/null +++ b/scripts/p3_tools/batch_convert_gui.py @@ -0,0 +1,221 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import os +import threading +import sys +from convert_audio_to_p3 import encode_audio_to_opus +from convert_p3_to_audio import decode_p3_to_audio + +class AudioConverterApp: + def __init__(self, master): + self.master = master + master.title("音频/P3 批量转换工具") + master.geometry("680x600") # 调整窗口高度 + + # 初始化变量 + self.mode = tk.StringVar(value="audio_to_p3") + self.output_dir = tk.StringVar() + self.output_dir.set(os.path.abspath("output")) + self.enable_loudnorm = tk.BooleanVar(value=True) + self.target_lufs = tk.DoubleVar(value=-16.0) + + # 创建UI组件 + self.create_widgets() + self.redirect_output() + + def create_widgets(self): + # 模式选择 + mode_frame = ttk.LabelFrame(self.master, text="转换模式") + mode_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew") + + ttk.Radiobutton(mode_frame, text="音频转P3", variable=self.mode, + value="audio_to_p3", command=self.toggle_settings, + width=12).grid(row=0, column=0, padx=5) + ttk.Radiobutton(mode_frame, text="P3转音频", variable=self.mode, + value="p3_to_audio", command=self.toggle_settings, + width=12).grid(row=0, column=1, padx=5) + + # 响度设置 + self.loudnorm_frame = ttk.Frame(self.master) + self.loudnorm_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew") + + ttk.Checkbutton(self.loudnorm_frame, text="启用响度调整", + variable=self.enable_loudnorm, width=15 + ).grid(row=0, column=0, padx=2) + ttk.Entry(self.loudnorm_frame, textvariable=self.target_lufs, + width=6).grid(row=0, column=1, padx=2) + ttk.Label(self.loudnorm_frame, text="LUFS").grid(row=0, column=2, padx=2) + + # 文件选择 + file_frame = ttk.LabelFrame(self.master, text="输入文件") + file_frame.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") + + # 文件操作按钮 + ttk.Button(file_frame, text="选择文件", command=self.select_files, + width=12).grid(row=0, column=0, padx=5, pady=2) + ttk.Button(file_frame, text="移除选中", command=self.remove_selected, + width=12).grid(row=0, column=1, padx=5, pady=2) + ttk.Button(file_frame, text="清空列表", command=self.clear_files, + width=12).grid(row=0, column=2, padx=5, pady=2) + + # 文件列表(使用Treeview) + self.tree = ttk.Treeview(file_frame, columns=("selected", "filename"), + show="headings", height=8) + self.tree.heading("selected", text="选中", anchor=tk.W) + self.tree.heading("filename", text="文件名", anchor=tk.W) + self.tree.column("selected", width=60, anchor=tk.W) + self.tree.column("filename", width=600, anchor=tk.W) + self.tree.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=5, pady=2) + self.tree.bind("", self.on_tree_click) + + # 输出目录 + output_frame = ttk.LabelFrame(self.master, text="输出目录") + output_frame.grid(row=3, column=0, padx=10, pady=5, sticky="ew") + + ttk.Entry(output_frame, textvariable=self.output_dir, width=60 + ).grid(row=0, column=0, padx=5, sticky="ew") + ttk.Button(output_frame, text="浏览", command=self.select_output_dir, + width=8).grid(row=0, column=1, padx=5) + + # 转换按钮区域 + button_frame = ttk.Frame(self.master) + button_frame.grid(row=4, column=0, padx=10, pady=10, sticky="ew") + + ttk.Button(button_frame, text="转换全部文件", command=lambda: self.start_conversion(True), + width=15).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="转换选中文件", command=lambda: self.start_conversion(False), + width=15).pack(side=tk.LEFT, padx=5) + + # 日志区域 + log_frame = ttk.LabelFrame(self.master, text="日志") + log_frame.grid(row=5, column=0, padx=10, pady=5, sticky="nsew") + + self.log_text = tk.Text(log_frame, height=14, width=80) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置布局权重 + self.master.columnconfigure(0, weight=1) + self.master.rowconfigure(2, weight=1) + self.master.rowconfigure(5, weight=3) + file_frame.columnconfigure(0, weight=1) + file_frame.rowconfigure(1, weight=1) + + def toggle_settings(self): + if self.mode.get() == "audio_to_p3": + self.loudnorm_frame.grid() + else: + self.loudnorm_frame.grid_remove() + + def select_files(self): + file_types = [ + ("音频文件", "*.wav *.mp3 *.ogg *.flac") if self.mode.get() == "audio_to_p3" + else ("P3文件", "*.p3") + ] + + files = filedialog.askopenfilenames(filetypes=file_types) + for f in files: + self.tree.insert("", tk.END, values=("[ ]", os.path.basename(f)), tags=(f,)) + + def on_tree_click(self, event): + """处理复选框点击事件""" + region = self.tree.identify("region", event.x, event.y) + if region == "cell": + col = self.tree.identify_column(event.x) + item = self.tree.identify_row(event.y) + if col == "#1": # 点击的是选中列 + current_val = self.tree.item(item, "values")[0] + new_val = "[√]" if current_val == "[ ]" else "[ ]" + self.tree.item(item, values=(new_val, self.tree.item(item, "values")[1])) + + def remove_selected(self): + """移除选中的文件""" + to_remove = [] + for item in self.tree.get_children(): + if self.tree.item(item, "values")[0] == "[√]": + to_remove.append(item) + for item in reversed(to_remove): + self.tree.delete(item) + + def clear_files(self): + """清空所有文件""" + for item in self.tree.get_children(): + self.tree.delete(item) + + def select_output_dir(self): + path = filedialog.askdirectory() + if path: + self.output_dir.set(path) + + def redirect_output(self): + class StdoutRedirector: + def __init__(self, text_widget): + self.text_widget = text_widget + self.original_stdout = sys.stdout + + def write(self, message): + self.text_widget.insert(tk.END, message) + self.text_widget.see(tk.END) + self.original_stdout.write(message) + + def flush(self): + self.original_stdout.flush() + + sys.stdout = StdoutRedirector(self.log_text) + + def start_conversion(self, convert_all): + """开始转换""" + input_files = [] + for item in self.tree.get_children(): + if convert_all or self.tree.item(item, "values")[0] == "[√]": + input_files.append(self.tree.item(item, "tags")[0]) + + if not input_files: + msg = "没有找到可转换的文件" if convert_all else "没有选中任何文件" + messagebox.showwarning("警告", msg) + return + + os.makedirs(self.output_dir.get(), exist_ok=True) + + try: + if self.mode.get() == "audio_to_p3": + target_lufs = self.target_lufs.get() if self.enable_loudnorm.get() else None + thread = threading.Thread(target=self.convert_audio_to_p3, args=(target_lufs, input_files)) + else: + thread = threading.Thread(target=self.convert_p3_to_audio, args=(input_files,)) + + thread.start() + except Exception as e: + print(f"转换初始化失败: {str(e)}") + + def convert_audio_to_p3(self, target_lufs, input_files): + """音频转P3转换逻辑""" + for input_path in input_files: + try: + filename = os.path.basename(input_path) + base_name = os.path.splitext(filename)[0] + output_path = os.path.join(self.output_dir.get(), f"{base_name}.p3") + + print(f"正在转换: {filename}") + encode_audio_to_opus(input_path, output_path, target_lufs) + print(f"转换成功: {filename}\n") + except Exception as e: + print(f"转换失败: {str(e)}\n") + + def convert_p3_to_audio(self, input_files): + """P3转音频转换逻辑""" + for input_path in input_files: + try: + filename = os.path.basename(input_path) + base_name = os.path.splitext(filename)[0] + output_path = os.path.join(self.output_dir.get(), f"{base_name}.wav") + + print(f"正在转换: {filename}") + decode_p3_to_audio(input_path, output_path) + print(f"转换成功: {filename}\n") + except Exception as e: + print(f"转换失败: {str(e)}\n") + +if __name__ == "__main__": + root = tk.Tk() + app = AudioConverterApp(root) + root.mainloop() \ No newline at end of file diff --git a/scripts/p3_tools/convert_audio_to_p3 copy.py b/scripts/p3_tools/convert_audio_to_p3 copy.py new file mode 100644 index 0000000..20e3d3a --- /dev/null +++ b/scripts/p3_tools/convert_audio_to_p3 copy.py @@ -0,0 +1,71 @@ +# convert audio files to protocol v3 stream +import librosa +import opuslib +import struct +import sys +import tqdm +import numpy as np +import argparse +import pyloudnorm as pyln + +def encode_audio_to_opus(input_file, output_file, target_lufs=None): + # Load audio file using librosa + audio, sample_rate = librosa.load(input_file, sr=None, mono=False, dtype=np.float32) + + # Convert mono to stereo if necessary + if audio.ndim == 1: # 检查是否为单声道 + audio = np.stack([audio, audio], axis=0) # 复制单声道数据到两个声道,创建立体声 + + if target_lufs is not None: + print("Note: Automatic loudness adjustment is enabled, which may cause", file=sys.stderr) + print(" audio distortion. If the input audio has already been ", file=sys.stderr) + print(" loudness-adjusted or if the input audio is TTS audio, ", file=sys.stderr) + print(" please use the `-d` parameter to disable loudness adjustment.", file=sys.stderr) + meter = pyln.Meter(sample_rate) + current_loudness = meter.integrated_loudness(audio) + audio = pyln.normalize.loudness(audio, current_loudness, target_lufs) + print(f"Adjusted loudness: {current_loudness:.1f} LUFS -> {target_lufs} LUFS") + + # Convert sample rate to 16000Hz if necessary + target_sample_rate = 16000 + if sample_rate != target_sample_rate: + audio = librosa.resample(audio, orig_sr=sample_rate, target_sr=target_sample_rate) + sample_rate = target_sample_rate + + # Convert audio data back to int16 after processing + # 对于立体声,需要先调整形状,然后转换为int16 + audio = (audio * 32767).astype(np.int16) + + # Initialize Opus encoder + encoder = opuslib.Encoder(sample_rate, 2, opuslib.APPLICATION_AUDIO) + + # Encode and save + with open(output_file, 'wb') as f: + duration = 60 # 60ms per frame + frame_size = int(sample_rate * duration / 1000) + # 对于立体声,audio.shape[0]是2(两个声道),audio.shape[1]是采样点数 + for i in tqdm.tqdm(range(0, audio.shape[1] - frame_size, frame_size)): + # 提取当前帧的两个声道数据 + frame_left = audio[0, i:i + frame_size] + frame_right = audio[1, i:i + frame_size] + # 交错两个声道的数据(L1, R1, L2, R2, ...) + interleaved = np.empty((frame_size * 2,), dtype=np.int16) + interleaved[0::2] = frame_left + interleaved[1::2] = frame_right + # 编码交错的数据 + opus_data = encoder.encode(interleaved.tobytes(), frame_size=frame_size) + packet = struct.pack('>BBH', 0, 0, len(opus_data)) + opus_data + f.write(packet) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Convert audio to Opus with loudness normalization') + parser.add_argument('input_file', help='Input audio file') + parser.add_argument('output_file', help='Output .opus file') + parser.add_argument('-l', '--lufs', type=float, default=-16.0, + help='Target loudness in LUFS (default: -16)') + parser.add_argument('-d', '--disable-loudnorm', action='store_true', + help='Disable loudness normalization') + args = parser.parse_args() + + target_lufs = None if args.disable_loudnorm else args.lufs + encode_audio_to_opus(args.input_file, args.output_file, target_lufs) \ No newline at end of file diff --git a/scripts/p3_tools/convert_audio_to_p3.py b/scripts/p3_tools/convert_audio_to_p3.py new file mode 100644 index 0000000..519d662 --- /dev/null +++ b/scripts/p3_tools/convert_audio_to_p3.py @@ -0,0 +1,62 @@ +# convert audio files to protocol v3 stream +import librosa +import opuslib +import struct +import sys +import tqdm +import numpy as np +import argparse +import pyloudnorm as pyln + +def encode_audio_to_opus(input_file, output_file, target_lufs=None): + # Load audio file using librosa + audio, sample_rate = librosa.load(input_file, sr=None, mono=False, dtype=np.float32) + + # Convert to mono if stereo + if audio.ndim == 2: + audio = librosa.to_mono(audio) + + if target_lufs is not None: + print("Note: Automatic loudness adjustment is enabled, which may cause", file=sys.stderr) + print(" audio distortion. If the input audio has already been ", file=sys.stderr) + print(" loudness-adjusted or if the input audio is TTS audio, ", file=sys.stderr) + print(" please use the `-d` parameter to disable loudness adjustment.", file=sys.stderr) + meter = pyln.Meter(sample_rate) + current_loudness = meter.integrated_loudness(audio) + audio = pyln.normalize.loudness(audio, current_loudness, target_lufs) + print(f"Adjusted loudness: {current_loudness:.1f} LUFS -> {target_lufs} LUFS") + + # Convert sample rate to 16000Hz if necessary + target_sample_rate = 16000 + if sample_rate != target_sample_rate: + audio = librosa.resample(audio, orig_sr=sample_rate, target_sr=target_sample_rate) + sample_rate = target_sample_rate + + # Convert audio data back to int16 after processing + audio = (audio * 32767).astype(np.int16) + + # Initialize Opus encoder + encoder = opuslib.Encoder(sample_rate, 1, opuslib.APPLICATION_AUDIO) + + # Encode and save + with open(output_file, 'wb') as f: + duration = 60 # 60ms per frame + frame_size = int(sample_rate * duration / 1000) + for i in tqdm.tqdm(range(0, len(audio) - frame_size, frame_size)): + frame = audio[i:i + frame_size] + opus_data = encoder.encode(frame.tobytes(), frame_size=frame_size) + packet = struct.pack('>BBH', 0, 0, len(opus_data)) + opus_data + f.write(packet) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Convert audio to Opus with loudness normalization') + parser.add_argument('input_file', help='Input audio file') + parser.add_argument('output_file', help='Output .opus file') + parser.add_argument('-l', '--lufs', type=float, default=-16.0, + help='Target loudness in LUFS (default: -16)') + parser.add_argument('-d', '--disable-loudnorm', action='store_true', + help='Disable loudness normalization') + args = parser.parse_args() + + target_lufs = None if args.disable_loudnorm else args.lufs + encode_audio_to_opus(args.input_file, args.output_file, target_lufs) \ No newline at end of file diff --git a/scripts/p3_tools/convert_p3_to_audio.py b/scripts/p3_tools/convert_p3_to_audio.py new file mode 100644 index 0000000..f870b01 --- /dev/null +++ b/scripts/p3_tools/convert_p3_to_audio.py @@ -0,0 +1,51 @@ +import struct +import sys +import opuslib +import numpy as np +from tqdm import tqdm +import soundfile as sf + + +def decode_p3_to_audio(input_file, output_file): + sample_rate = 16000 + channels = 1 + decoder = opuslib.Decoder(sample_rate, channels) + + pcm_frames = [] + frame_size = int(sample_rate * 60 / 1000) + + with open(input_file, "rb") as f: + f.seek(0, 2) + total_size = f.tell() + f.seek(0) + + with tqdm(total=total_size, unit="B", unit_scale=True) as pbar: + while True: + header = f.read(4) + if not header or len(header) < 4: + break + + pkt_type, reserved, opus_len = struct.unpack(">BBH", header) + opus_data = f.read(opus_len) + if len(opus_data) != opus_len: + break + + pcm = decoder.decode(opus_data, frame_size) + pcm_frames.append(np.frombuffer(pcm, dtype=np.int16)) + + pbar.update(4 + opus_len) + + if not pcm_frames: + raise ValueError("No valid audio data found") + + pcm_data = np.concatenate(pcm_frames) + + sf.write(output_file, pcm_data, sample_rate, subtype="PCM_16") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python convert_p3_to_audio.py ") + sys.exit(1) + + decode_p3_to_audio(sys.argv[1], sys.argv[2]) diff --git a/scripts/p3_tools/img/img.png b/scripts/p3_tools/img/img.png new file mode 100644 index 0000000..7ee34ee Binary files /dev/null and b/scripts/p3_tools/img/img.png differ diff --git a/scripts/p3_tools/p3_gui_player.py b/scripts/p3_tools/p3_gui_player.py new file mode 100644 index 0000000..3bbc8a3 --- /dev/null +++ b/scripts/p3_tools/p3_gui_player.py @@ -0,0 +1,241 @@ +import tkinter as tk +from tkinter import filedialog, messagebox +import threading +import time +import opuslib +import struct +import numpy as np +import sounddevice as sd +import os + + +def play_p3_file(input_file, stop_event=None, pause_event=None): + """ + 播放p3格式的音频文件 + p3格式: [1字节类型, 1字节保留, 2字节长度, Opus数据] + """ + # 初始化Opus解码器 + sample_rate = 16000 # 采样率固定为16000Hz + channels = 1 # 单声道 + decoder = opuslib.Decoder(sample_rate, channels) + + # 帧大小 (60ms) + frame_size = int(sample_rate * 60 / 1000) + + # 打开音频流 + stream = sd.OutputStream( + samplerate=sample_rate, + channels=channels, + dtype='int16' + ) + stream.start() + + try: + with open(input_file, 'rb') as f: + print(f"正在播放: {input_file}") + + while True: + if stop_event and stop_event.is_set(): + break + + if pause_event and pause_event.is_set(): + time.sleep(0.1) + continue + + # 读取头部 (4字节) + header = f.read(4) + if not header or len(header) < 4: + break + + # 解析头部 + packet_type, reserved, data_len = struct.unpack('>BBH', header) + + # 读取Opus数据 + opus_data = f.read(data_len) + if not opus_data or len(opus_data) < data_len: + break + + # 解码Opus数据 + pcm_data = decoder.decode(opus_data, frame_size) + + # 将字节转换为numpy数组 + audio_array = np.frombuffer(pcm_data, dtype=np.int16) + + # 播放音频 + stream.write(audio_array) + + except KeyboardInterrupt: + print("\n播放已停止") + finally: + stream.stop() + stream.close() + print("播放完成") + + +class P3PlayerApp: + def __init__(self, root): + self.root = root + self.root.title("P3 文件简易播放器") + self.root.geometry("500x400") + + self.playlist = [] + self.current_index = 0 + self.is_playing = False + self.is_paused = False + self.stop_event = threading.Event() + self.pause_event = threading.Event() + self.loop_playback = tk.BooleanVar(value=False) # 循环播放复选框的状态 + + # 创建界面组件 + self.create_widgets() + + def create_widgets(self): + # 播放列表 + self.playlist_label = tk.Label(self.root, text="播放列表:") + self.playlist_label.pack(pady=5) + + self.playlist_frame = tk.Frame(self.root) + self.playlist_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + self.playlist_listbox = tk.Listbox(self.playlist_frame, selectmode=tk.SINGLE) + self.playlist_listbox.pack(fill=tk.BOTH, expand=True) + + # 复选框和移除按钮 + self.checkbox_frame = tk.Frame(self.root) + self.checkbox_frame.pack(pady=5) + + self.remove_button = tk.Button(self.checkbox_frame, text="移除文件", command=self.remove_files) + self.remove_button.pack(side=tk.LEFT, padx=5) + + # 循环播放复选框 + self.loop_checkbox = tk.Checkbutton(self.checkbox_frame, text="循环播放", variable=self.loop_playback) + self.loop_checkbox.pack(side=tk.LEFT, padx=5) + + # 控制按钮 + self.control_frame = tk.Frame(self.root) + self.control_frame.pack(pady=10) + + self.add_button = tk.Button(self.control_frame, text="添加文件", command=self.add_file) + self.add_button.grid(row=0, column=0, padx=5) + + self.play_button = tk.Button(self.control_frame, text="播放", command=self.play) + self.play_button.grid(row=0, column=1, padx=5) + + self.pause_button = tk.Button(self.control_frame, text="暂停", command=self.pause) + self.pause_button.grid(row=0, column=2, padx=5) + + self.stop_button = tk.Button(self.control_frame, text="停止", command=self.stop) + self.stop_button.grid(row=0, column=3, padx=5) + + # 状态标签 + self.status_label = tk.Label(self.root, text="未在播放", fg="blue") + self.status_label.pack(pady=10) + + def add_file(self): + files = filedialog.askopenfilenames(filetypes=[("P3 文件", "*.p3")]) + if files: + self.playlist.extend(files) + self.update_playlist() + + def update_playlist(self): + self.playlist_listbox.delete(0, tk.END) + for file in self.playlist: + self.playlist_listbox.insert(tk.END, os.path.basename(file)) # 仅显示文件名 + + def update_status(self, status_text, color="blue"): + """更新状态标签的内容""" + self.status_label.config(text=status_text, fg=color) + + def play(self): + if not self.playlist: + messagebox.showwarning("警告", "播放列表为空!") + return + + if self.is_paused: + self.is_paused = False + self.pause_event.clear() + self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green") + return + + if self.is_playing: + return + + self.is_playing = True + self.stop_event.clear() + self.pause_event.clear() + self.current_index = self.playlist_listbox.curselection()[0] if self.playlist_listbox.curselection() else 0 + self.play_thread = threading.Thread(target=self.play_audio, daemon=True) + self.play_thread.start() + self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green") + + def play_audio(self): + while True: + if self.stop_event.is_set(): + break + + if self.pause_event.is_set(): + time.sleep(0.1) + continue + + # 检查当前索引是否有效 + if self.current_index >= len(self.playlist): + if self.loop_playback.get(): # 如果勾选了循环播放 + self.current_index = 0 # 回到第一首 + else: + break # 否则停止播放 + + file = self.playlist[self.current_index] + self.playlist_listbox.selection_clear(0, tk.END) + self.playlist_listbox.selection_set(self.current_index) + self.playlist_listbox.activate(self.current_index) + self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green") + play_p3_file(file, self.stop_event, self.pause_event) + + if self.stop_event.is_set(): + break + + if not self.loop_playback.get(): # 如果没有勾选循环播放 + break # 播放完当前文件后停止 + + self.current_index += 1 + if self.current_index >= len(self.playlist): + if self.loop_playback.get(): # 如果勾选了循环播放 + self.current_index = 0 # 回到第一首 + + self.is_playing = False + self.is_paused = False + self.update_status("播放已停止", "red") + + def pause(self): + if self.is_playing: + self.is_paused = not self.is_paused + if self.is_paused: + self.pause_event.set() + self.update_status("播放已暂停", "orange") + else: + self.pause_event.clear() + self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green") + + def stop(self): + if self.is_playing or self.is_paused: + self.is_playing = False + self.is_paused = False + self.stop_event.set() + self.pause_event.clear() + self.update_status("播放已停止", "red") + + def remove_files(self): + selected_indices = self.playlist_listbox.curselection() + if not selected_indices: + messagebox.showwarning("警告", "请先选择要移除的文件!") + return + + for index in reversed(selected_indices): + self.playlist.pop(index) + self.update_playlist() + + +if __name__ == "__main__": + root = tk.Tk() + app = P3PlayerApp(root) + root.mainloop() diff --git a/scripts/p3_tools/play_p3.py b/scripts/p3_tools/play_p3.py new file mode 100644 index 0000000..3c9ec81 --- /dev/null +++ b/scripts/p3_tools/play_p3.py @@ -0,0 +1,71 @@ +# 播放p3格式的音频文件 +import opuslib +import struct +import numpy as np +import sounddevice as sd +import argparse + +def play_p3_file(input_file): + """ + 播放p3格式的音频文件 + p3格式: [1字节类型, 1字节保留, 2字节长度, Opus数据] + """ + # 初始化Opus解码器 + sample_rate = 16000 # 采样率固定为16000Hz + channels = 1 # 单声道 + decoder = opuslib.Decoder(sample_rate, channels) + + # 帧大小 (60ms) + frame_size = int(sample_rate * 60 / 1000) + + # 打开音频流 + stream = sd.OutputStream( + samplerate=sample_rate, + channels=channels, + dtype='int16' + ) + stream.start() + + try: + with open(input_file, 'rb') as f: + print(f"正在播放: {input_file}") + + while True: + # 读取头部 (4字节) + header = f.read(4) + if not header or len(header) < 4: + break + + # 解析头部 + packet_type, reserved, data_len = struct.unpack('>BBH', header) + + # 读取Opus数据 + opus_data = f.read(data_len) + if not opus_data or len(opus_data) < data_len: + break + + # 解码Opus数据 + pcm_data = decoder.decode(opus_data, frame_size) + + # 将字节转换为numpy数组 + audio_array = np.frombuffer(pcm_data, dtype=np.int16) + + # 播放音频 + stream.write(audio_array) + + except KeyboardInterrupt: + print("\n播放已停止") + finally: + stream.stop() + stream.close() + print("播放完成") + +def main(): + parser = argparse.ArgumentParser(description='播放p3格式的音频文件') + parser.add_argument('input_file', help='输入的p3文件路径') + args = parser.parse_args() + + play_p3_file(args.input_file) + +if __name__ == "__main__": + main() diff --git a/scripts/p3_tools/requirements.txt b/scripts/p3_tools/requirements.txt new file mode 100644 index 0000000..d76d4cd --- /dev/null +++ b/scripts/p3_tools/requirements.txt @@ -0,0 +1,7 @@ +librosa>=0.9.2 +opuslib>=3.0.1 +numpy>=1.20.0 +tqdm>=4.62.0 +sounddevice>=0.4.4 +pyloudnorm>=0.1.1 +soundfile>=0.13.1 diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 0000000..314063b --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,135 @@ +import sys +import os +import json +import zipfile + +# 切换到项目根目录 +os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +def get_board_type(): + with open("build/compile_commands.json") as f: + data = json.load(f) + for item in data: + if not item["file"].endswith("main.cc"): + continue + command = item["command"] + # extract -DBOARD_TYPE=xxx + board_type = command.split("-DBOARD_TYPE=\\\"")[1].split("\\\"")[0].strip() + return board_type + return None + +def get_project_version(): + with open("CMakeLists.txt") as f: + for line in f: + if line.startswith("set(PROJECT_VER"): + return line.split("\"")[1].split("\"")[0].strip() + return None + +def merge_bin(): + if os.system("idf.py merge-bin") != 0: + print("merge bin failed") + sys.exit(1) + +def zip_bin(board_type, project_version): + if not os.path.exists("releases"): + os.makedirs("releases") + output_path = f"releases/v{project_version}_{board_type}.zip" + if os.path.exists(output_path): + os.remove(output_path) + with zipfile.ZipFile(output_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: + zipf.write("build/merged-binary.bin", arcname="merged-binary.bin") + print(f"zip bin to {output_path} done") + + +def release_current(): + merge_bin() + board_type = get_board_type() + print("board type:", board_type) + project_version = get_project_version() + print("project version:", project_version) + zip_bin(board_type, project_version) + +def get_all_board_types(): + board_configs = {} + with open("main/CMakeLists.txt") as f: + lines = f.readlines() + for i, line in enumerate(lines): + # 查找 if(CONFIG_BOARD_TYPE_*) 行 + if "if(CONFIG_BOARD_TYPE_" in line: + config_name = line.strip().split("if(")[1].split(")")[0] + # 查找下一行的 set(BOARD_TYPE "xxx") + next_line = lines[i + 1].strip() + if next_line.startswith("set(BOARD_TYPE"): + board_type = next_line.split('"')[1] + board_configs[config_name] = board_type + return board_configs + +def release(board_type, board_config): + config_path = f"main/boards/{board_type}/config.json" + if not os.path.exists(config_path): + print(f"跳过 {board_type} 因为 config.json 不存在") + return + + # Print Project Version + project_version = get_project_version() + print(f"Project Version: {project_version}", config_path) + release_path = f"releases/v{project_version}_{board_type}.zip" + if os.path.exists(release_path): + print(f"跳过 {board_type} 因为 {release_path} 已存在") + return + + with open(config_path, "r") as f: + config = json.load(f) + target = config["target"] + builds = config["builds"] + + for build in builds: + name = build["name"] + if not name.startswith(board_type): + raise ValueError(f"name {name} 必须 {board_type} 开头") + + sdkconfig_append = [f"{board_config}=y"] + for append in build.get("sdkconfig_append", []): + sdkconfig_append.append(append) + print(f"name: {name}") + print(f"target: {target}") + for append in sdkconfig_append: + print(f"sdkconfig_append: {append}") + # unset IDF_TARGET + os.environ.pop("IDF_TARGET", None) + # Call set-target + if os.system(f"idf.py set-target {target}") != 0: + print("set-target failed") + sys.exit(1) + # Append sdkconfig + with open("sdkconfig", "a") as f: + f.write("\n") + for append in sdkconfig_append: + f.write(f"{append}\n") + # Build with macro BOARD_NAME defined to name + if os.system(f"idf.py -DBOARD_NAME={name} build") != 0: + print("build failed") + sys.exit(1) + # Call merge-bin + if os.system("idf.py merge-bin") != 0: + print("merge-bin failed") + sys.exit(1) + # Zip bin + zip_bin(name, project_version) + print("-" * 80) + +if __name__ == "__main__": + if len(sys.argv) > 1: + board_configs = get_all_board_types() + found = False + for board_config, board_type in board_configs.items(): + if sys.argv[1] == 'all' or board_type == sys.argv[1]: + release(board_type, board_config) + found = True + if not found: + print(f"未找到板子类型: {sys.argv[1]}") + print("可用的板子类型:") + for board_type in board_configs.values(): + print(f" {board_type}") + else: + release_current() diff --git a/scripts/set_custom_wake_word.py b/scripts/set_custom_wake_word.py new file mode 100644 index 0000000..5140613 --- /dev/null +++ b/scripts/set_custom_wake_word.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +自定义唤醒词配置脚本 +解决menuconfig中文乱码问题 +""" + +import os +import sys +import re + +def set_custom_wake_word(pinyin, display_text): + """ + 设置自定义唤醒词配置 + + Args: + pinyin (str): 拼音,如 "ni hao qi yuan" + display_text (str): 显示文本,如 "你好气元" + """ + sdkconfig_path = "sdkconfig" + + if not os.path.exists(sdkconfig_path): + print(f"错误:找不到 {sdkconfig_path} 文件") + return False + + # 读取sdkconfig文件 + with open(sdkconfig_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 定义要修改的配置项 + configs = { + 'CONFIG_USE_WAKE_WORD_DETECT': 'n', + 'CONFIG_USE_CUSTOM_WAKE_WORD': 'y', + 'CONFIG_CUSTOM_WAKE_WORD': f'"{pinyin}"', + 'CONFIG_CUSTOM_WAKE_WORD_DISPLAY': f'"{display_text}"' + } + + # 应用配置 + for key, value in configs.items(): + pattern = rf'^{key}=.*$' + replacement = f'{key}={value}' + + if re.search(pattern, content, re.MULTILINE): + # 如果配置项存在,替换它 + content = re.sub(pattern, replacement, content, flags=re.MULTILINE) + else: + # 如果配置项不存在,添加到文件末尾 + content += f'\n{replacement}' + + # 写回文件 + with open(sdkconfig_path, 'w', encoding='utf-8') as f: + f.write(content) + + print(f"✅ 成功设置自定义唤醒词配置:") + print(f" 拼音: {pinyin}") + print(f" 显示: {display_text}") + print(f" 文件: {sdkconfig_path}") + + return True + +def main(): + """主函数""" + if len(sys.argv) != 3: + print("使用方法:") + print(" python scripts/set_custom_wake_word.py <拼音> <显示文本>") + print("") + print("示例:") + print(" python scripts/set_custom_wake_word.py 'ni hao qi yuan' '你好气元'") + print(" python scripts/set_custom_wake_word.py 'xiao ai tong xue' '小爱同学'") + print(" python scripts/set_custom_wake_word.py 'tian mao jing ling' '天猫精灵'") + return + + pinyin = sys.argv[1] + display_text = sys.argv[2] + + if set_custom_wake_word(pinyin, display_text): + print("\n🎉 配置完成!现在可以编译项目:") + print(" idf.py build") + else: + print("\n❌ 配置失败!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/versions.py b/scripts/versions.py new file mode 100644 index 0000000..cc084c2 --- /dev/null +++ b/scripts/versions.py @@ -0,0 +1,205 @@ +#! /usr/bin/env python3 +from dotenv import load_dotenv +load_dotenv() + +import os +import struct +import zipfile +import oss2 +import json +import requests +from requests.exceptions import RequestException + +# 切换到项目根目录 +os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +def get_chip_id_string(chip_id): + return { + 0x0000: "esp32", + 0x0002: "esp32s2", + 0x0005: "esp32c3", + 0x0009: "esp32s3", + 0x000C: "esp32c2", + 0x000D: "esp32c6", + 0x0010: "esp32h2", + 0x0011: "esp32c5", + 0x0012: "esp32p4", + 0x0017: "esp32c5", + }[chip_id] + +def get_flash_size(flash_size): + MB = 1024 * 1024 + return { + 0x00: 1 * MB, + 0x01: 2 * MB, + 0x02: 4 * MB, + 0x03: 8 * MB, + 0x04: 16 * MB, + 0x05: 32 * MB, + 0x06: 64 * MB, + 0x07: 128 * MB, + }[flash_size] + +def get_app_desc(data): + magic = struct.unpack("> 4) + chip_id = get_chip_id_string(data[0xC]) + # get segments + segment_count = data[0x1] + segments = [] + offset = 0x18 + for i in range(segment_count): + segment_size = struct.unpack(" int: + self._msg_id_counter += 1 + return self._msg_id_counter + + def _on_notify(self, sender: BleakGATTCharacteristic, data: bytearray): + """NOTIFY 回调: 接收设备返回的 JSON 数据""" + try: + text = data.decode("utf-8") + parsed = json.loads(text) + self.responses.append(parsed) + self._response_event.set() + except (UnicodeDecodeError, json.JSONDecodeError) as e: + self.responses.append({"_raw": data.hex(), "_error": str(e)}) + self._response_event.set() + + async def _send_cmd(self, cmd: str, data: Optional[dict] = None, + msg_id: Optional[int] = None, raw: Optional[str] = None) -> Optional[dict]: + """ + 发送 JSON 命令并等待响应。 + + Args: + cmd: 命令名 + data: 命令数据 (可选) + msg_id: 消息 ID (可选,自动生成) + raw: 直接发送原始字符串 (跳过 JSON 构建) + + Returns: + 解析后的 JSON 响应,超时返回 None + """ + if msg_id is None: + msg_id = self._next_msg_id() + + # 清空之前的响应 + self.responses.clear() + self._response_event.clear() + + # 构建并发送 + if raw is not None: + payload = raw.encode("utf-8") + else: + request = {"cmd": cmd, "id": msg_id} + if data is not None: + request["data"] = data + payload = json.dumps(request, separators=(",", ":")).encode("utf-8") + + await self.client.write_gatt_char(CHAR_WRITE_UUID, payload, response=True) + + # 等待响应 + try: + await asyncio.wait_for(self._response_event.wait(), timeout=self.timeout) + except asyncio.TimeoutError: + return None + + return self.responses[-1] if self.responses else None + + def _record(self, name: str, passed: bool, detail: str = "", duration_ms: float = 0.0): + self.results.append(TestResult(name, passed, detail, duration_ms)) + status = "PASS" if passed else "FAIL" + symbol = "+" if passed else "x" + print(f" [{symbol}] {name}: {status} {detail}") + + # ============================================================ + # 测试用例 + # ============================================================ + + async def test_scan_device(self): + """测试 1: 扫描 BLE 设备""" + t0 = time.monotonic() + print(f"\n正在扫描设备 '{self.device_name}'...") + device = await BleakScanner.find_device_by_name( + self.device_name, timeout=10.0 + ) + dt = (time.monotonic() - t0) * 1000 + + if device: + self._record("test_scan_device", True, + f"找到设备 addr={device.address}", dt) + return device + else: + self._record("test_scan_device", False, + "未找到设备,请确认设备已开机且蓝牙广播中", dt) + return None + + async def test_discover_service(self): + """测试 2: 发现 GATT Service 和 Characteristic""" + t0 = time.monotonic() + services = self.client.services + + svc = services.get_service(SERVICE_UUID) + if not svc: + self._record("test_discover_service", False, + f"未发现 Service {SERVICE_UUID}") + return False + + chars_found = [] + for uuid in [CHAR_WRITE_UUID, CHAR_NOTIFY_UUID, CHAR_STATUS_UUID]: + char = svc.get_characteristic(uuid) + if char: + chars_found.append(uuid.split("-")[0][-4:].upper()) + else: + self._record("test_discover_service", False, + f"缺少 Characteristic {uuid}") + return False + + dt = (time.monotonic() - t0) * 1000 + self._record("test_discover_service", True, + f"Service 0xAB00, Chars: {chars_found}", dt) + return True + + async def test_enable_notify(self): + """测试 3: 启用 NOTIFY""" + t0 = time.monotonic() + try: + await self.client.start_notify(CHAR_NOTIFY_UUID, self._on_notify) + dt = (time.monotonic() - t0) * 1000 + self._record("test_enable_notify", True, + "NOTIFY 已启用 (CCCD 写入成功)", dt) + return True + except Exception as e: + dt = (time.monotonic() - t0) * 1000 + self._record("test_enable_notify", False, str(e), dt) + return False + + async def test_ping(self): + """测试 4: ping/pong 连通性测试""" + t0 = time.monotonic() + msg_id = self._next_msg_id() + resp = await self._send_cmd("ping", msg_id=msg_id) + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_ping", False, "超时未收到响应", dt) + return + + ok = (resp.get("cmd") == "ping" + and resp.get("code") == 0 + and resp.get("msg") == "pong" + and resp.get("id") == msg_id) + + self._record("test_ping", ok, + f"响应: {json.dumps(resp, ensure_ascii=False)}", dt) + + async def test_status(self): + """测试 5: 查询设备状态""" + t0 = time.monotonic() + resp = await self._send_cmd("status") + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_status", False, "超时未收到响应", dt) + return + + data = resp.get("data", {}) + has_state = "s" in data + code_ok = resp.get("code") == 0 + valid_states = ["unknown", "starting", "configuring", "idle", + "connecting", "listening", "speaking", "dialog", + "upgrading", "activating", "fatal_error"] + state_ok = data.get("s") in valid_states + + ok = code_ok and has_state and state_ok + detail = f"state={data.get('s')}, bat={data.get('bat')}, vol={data.get('vol')}" + self._record("test_status", ok, detail, dt) + + async def test_dev_info(self): + """测试 6: 查询设备信息""" + t0 = time.monotonic() + resp = await self._send_cmd("dev_info") + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_dev_info", False, "超时未收到响应", dt) + return + + data = resp.get("data", {}) + required_fields = ["mac", "board", "fw", "chip"] + missing = [f for f in required_fields if f not in data] + + ok = resp.get("code") == 0 and len(missing) == 0 + if ok: + detail = f"mac={data['mac']}, board={data['board']}, fw={data['fw']}, chip={data['chip']}" + else: + detail = f"缺少字段: {missing}, 响应: {json.dumps(resp, ensure_ascii=False)}" + self._record("test_dev_info", ok, detail, dt) + + async def test_wifi_list(self): + """测试 7: WiFi 扫描列表""" + t0 = time.monotonic() + # WiFi 扫描是阻塞的,给更长超时 + old_timeout = self.timeout + self.timeout = 15.0 + resp = await self._send_cmd("wifi_list") + self.timeout = old_timeout + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_wifi_list", False, "超时未收到响应 (WiFi扫描可能耗时较长)", dt) + return + + data = resp.get("data", {}) + wifi_list = data.get("list", []) + code_ok = resp.get("code") == 0 + is_list = isinstance(wifi_list, list) + + ok = code_ok and is_list + if ok and len(wifi_list) > 0: + first = wifi_list[0] + has_fields = "ssid" in first and "rssi" in first + ok = ok and has_fields + detail = f"扫描到 {len(wifi_list)} 个网络" + if len(wifi_list) > 0: + detail += f", 第1个: ssid={first.get('ssid')}, rssi={first.get('rssi')}" + else: + detail = f"扫描到 {len(wifi_list)} 个网络" + + self._record("test_wifi_list", ok, detail, dt) + + async def test_set_vol(self): + """测试 8: 设置音量并验证""" + # 先获取当前音量 + resp0 = await self._send_cmd("status") + original_vol = resp0.get("data", {}).get("vol", 50) if resp0 else 50 + + # 设置新音量 + target_vol = 35 if original_vol != 35 else 65 + t0 = time.monotonic() + resp = await self._send_cmd("set_vol", {"vol": target_vol}) + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_set_vol", False, "超时未收到响应", dt) + return + + set_ok = resp.get("code") == 0 + + # 验证: 再查一次 status 确认音量变化 + await asyncio.sleep(0.3) + resp2 = await self._send_cmd("status") + new_vol = resp2.get("data", {}).get("vol") if resp2 else None + verify_ok = new_vol == target_vol + + ok = set_ok and verify_ok + detail = f"设置 vol={target_vol}, set_ok={set_ok}, 验证 vol={new_vol}" + self._record("test_set_vol", ok, detail, dt) + + # 恢复原音量 + await self._send_cmd("set_vol", {"vol": original_vol}) + + async def test_set_wifi_missing_ssid(self): + """测试 9: set_wifi 缺少 ssid 参数""" + t0 = time.monotonic() + resp = await self._send_cmd("set_wifi", {"pwd": "12345678"}) + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_set_wifi_missing_ssid", False, "超时未收到响应", dt) + return + + ok = resp.get("code") == -1 and "ssid" in resp.get("msg", "").lower() + self._record("test_set_wifi_missing_ssid", ok, + f"code={resp.get('code')}, msg={resp.get('msg')}", dt) + + async def test_unknown_cmd(self): + """测试 10: 未知命令""" + t0 = time.monotonic() + resp = await self._send_cmd("this_cmd_does_not_exist") + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_unknown_cmd", False, "超时未收到响应", dt) + return + + ok = resp.get("code") == -99 + self._record("test_unknown_cmd", ok, + f"code={resp.get('code')}, msg={resp.get('msg')}", dt) + + async def test_invalid_json(self): + """测试 11: 发送非法 JSON 数据""" + t0 = time.monotonic() + resp = await self._send_cmd("", raw="{this is not valid json!}") + dt = (time.monotonic() - t0) * 1000 + + if resp is None: + self._record("test_invalid_json", False, "超时未收到响应 (可能设备未发送错误通知)", dt) + return + + # 设备应返回 error 响应 + ok = resp.get("code", 0) != 0 or resp.get("cmd") == "error" + self._record("test_invalid_json", ok, + f"响应: {json.dumps(resp, ensure_ascii=False)}", dt) + + async def test_msg_id_correlation(self): + """测试 12: msg_id 关联性验证 — 连续发送多个命令,验证每个响应的 id 正确""" + t0 = time.monotonic() + ids = [100, 200, 300] + results = [] + + for mid in ids: + self.responses.clear() + self._response_event.clear() + + request = {"cmd": "ping", "id": mid} + payload = json.dumps(request, separators=(",", ":")).encode("utf-8") + await self.client.write_gatt_char(CHAR_WRITE_UUID, payload, response=True) + + try: + await asyncio.wait_for(self._response_event.wait(), timeout=self.timeout) + resp = self.responses[-1] if self.responses else None + except asyncio.TimeoutError: + resp = None + + if resp: + results.append((mid, resp.get("id"))) + else: + results.append((mid, None)) + + await asyncio.sleep(0.2) + + dt = (time.monotonic() - t0) * 1000 + all_match = all(sent == recv for sent, recv in results) + detail = ", ".join(f"sent={s}->recv={r}" for s, r in results) + self._record("test_msg_id_correlation", all_match, detail, dt) + + # ============================================================ + # 主运行流程 + # ============================================================ + + async def run(self): + """执行全部测试""" + print("=" * 60) + print(" BLE JSON 通讯模块 - 自动化测试") + print("=" * 60) + + # Step 1: 扫描 + device = await self.test_scan_device() + if not device: + self._print_summary() + return + + # Step 2: 连接 + print(f"\n正在连接 {device.address}...") + try: + self.client = BleakClient(device, timeout=10.0) + await self.client.connect() + print(f" 已连接, MTU={self.client.mtu_size}") + except Exception as e: + print(f" 连接失败: {e}") + self._print_summary() + return + + try: + # Step 3-12: 运行测试用例 + print("\n--- 服务发现 ---") + if not await self.test_discover_service(): + return + + print("\n--- NOTIFY 启用 ---") + if not await self.test_enable_notify(): + return + + # 等待 CCCD 写入生效 + await asyncio.sleep(0.5) + + print("\n--- 功能测试 ---") + await self.test_ping() + await self.test_status() + await self.test_dev_info() + await self.test_wifi_list() + await self.test_set_vol() + + print("\n--- 错误处理测试 ---") + await self.test_set_wifi_missing_ssid() + await self.test_unknown_cmd() + await self.test_invalid_json() + + print("\n--- 关联性测试 ---") + await self.test_msg_id_correlation() + + finally: + print("\n断开连接...") + try: + await self.client.stop_notify(CHAR_NOTIFY_UUID) + except Exception: + pass + await self.client.disconnect() + + self._print_summary() + + def _print_summary(self): + """打印测试总结""" + print("\n" + "=" * 60) + total = len(self.results) + passed = sum(1 for r in self.results if r.passed) + failed = total - passed + + print(f" 测试结果: {passed}/{total} 通过, {failed} 失败") + print("=" * 60) + + if failed > 0: + print("\n失败用例:") + for r in self.results: + if not r.passed: + print(f" [x] {r.name}: {r.detail}") + print() + + # 返回退出码 + return 0 if failed == 0 else 1 + + +def main(): + parser = argparse.ArgumentParser(description="BLE JSON 通讯模块测试") + parser.add_argument("--device", default=DEFAULT_DEVICE_NAME, + help=f"BLE 设备名称 (默认: {DEFAULT_DEVICE_NAME})") + parser.add_argument("--timeout", type=float, default=5.0, + help="命令响应超时秒数 (默认: 5.0)") + args = parser.parse_args() + + tester = BleJsonTester(device_name=args.device, timeout=args.timeout) + exit_code = asyncio.run(tester.run()) + sys.exit(exit_code or 0) + + +if __name__ == "__main__": + main() diff --git a/tests/ble_provision_test.py b/tests/ble_provision_test.py new file mode 100644 index 0000000..6191c1e --- /dev/null +++ b/tests/ble_provision_test.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +""" +BLE 配网协议测试脚本 + +用途: 模拟手机端,通过 BLE 与 ESP32 设备通信,测试自定义 GATT 配网协议。 +依赖: pip install bleak +运行: + # 交互式配网(输入 SSID 和密码) + python tests/ble_provision_test.py --ssid "MyWiFi" --pwd "12345678" + + # 扫描 WiFi 列表 + python tests/ble_provision_test.py --scan-wifi + + # 查询 WiFi 状态 + python tests/ble_provision_test.py --get-status + + # 指定设备名 + python tests/ble_provision_test.py --device "Airhub_Ble" --ssid "MyWiFi" --pwd "12345678" + +协议说明: + Service UUID: 0xABF0 + Write Char: 0xABF1 (手机→设备, 二进制命令) + Notify Char: 0xABF2 (设备→手机, 二进制响应) + + 命令格式: [cmd(1字节)] + [payload...] + 响应格式: [resp(1字节)] + [data...] +""" + +import argparse +import asyncio +import sys +import time +import struct + +try: + from bleak import BleakClient, BleakScanner + from bleak.backends.characteristic import BleakGATTCharacteristic +except ImportError: + print("错误: 缺少 bleak 库,请执行: pip install bleak") + sys.exit(1) + +# ============================================================ +# BLE 参数定义 (与 bluetooth_provisioning.h 一致) +# ============================================================ + +SERVICE_UUID = "0000abf0-0000-1000-8000-00805f9b34fb" +CHAR_WRITE_UUID = "0000abf1-0000-1000-8000-00805f9b34fb" +CHAR_NOTIFY_UUID = "0000abf2-0000-1000-8000-00805f9b34fb" +DEFAULT_DEVICE = "Airhub_" # 前缀匹配,设备名格式: Airhub_xx:xx:xx:xx:xx:xx + +# 命令码 (手机→设备) +CMD_SET_SSID = 0x01 +CMD_SET_PASSWORD = 0x02 +CMD_SET_BSSID = 0x03 +CMD_CONNECT_AP = 0x04 +CMD_DISCONNECT_AP = 0x05 +CMD_GET_WIFI_LIST = 0x06 +CMD_DISCONNECT_BLE = 0x07 +CMD_SET_WIFI_MODE = 0x08 +CMD_GET_WIFI_STATUS = 0x09 +CMD_CUSTOM_DATA = 0x10 + +# 响应码 (设备→手机) +RESP_WIFI_STATUS = 0x81 +RESP_WIFI_LIST = 0x82 +RESP_WIFI_LIST_END = 0x83 +RESP_CUSTOM_DATA = 0x84 + + +class BleProvisionTester: + """BLE 配网协议测试器""" + + def __init__(self, device_name: str, timeout: float = 10.0): + self.device_name = device_name + self.timeout = timeout + self.client = None + self.notifications = [] + self._notify_event = asyncio.Event() + + def _on_notify(self, sender: BleakGATTCharacteristic, data: bytearray): + """NOTIFY 回调""" + self.notifications.append(bytes(data)) + self._notify_event.set() + self._print_notification(data) + + @staticmethod + def _print_notification(data: bytearray): + """解析并打印 NOTIFY 数据""" + if len(data) < 1: + print(f" <- [空数据]") + return + + resp_type = data[0] + hex_str = data.hex(" ") + + if resp_type == RESP_WIFI_STATUS: + success = data[1] if len(data) > 1 else 0 + reason = data[2] if len(data) > 2 else 0 + status = "成功" if success == 1 else f"失败 (原因码: {reason})" + print(f" <- [WiFi状态] {status} (raw: {hex_str})") + + elif resp_type == RESP_WIFI_LIST: + if len(data) >= 3: + rssi = struct.unpack("b", bytes([data[1]]))[0] # 有符号 + ssid_len = data[2] + ssid = data[3:3 + ssid_len].decode("utf-8", errors="replace") + print(f" <- [WiFi] RSSI={rssi}dBm SSID=\"{ssid}\"") + else: + print(f" <- [WiFi列表] 数据不完整 (raw: {hex_str})") + + elif resp_type == RESP_WIFI_LIST_END: + print(f" <- [WiFi列表结束]") + + elif resp_type == RESP_CUSTOM_DATA: + payload = data[1:] + print(f" <- [自定义数据] {payload.hex(' ')} text=\"{payload.decode('utf-8', errors='replace')}\"") + + else: + print(f" <- [未知响应 0x{resp_type:02x}] {hex_str}") + + async def _wait_notifications(self, timeout: float = None, count: int = 1): + """等待指定数量的通知""" + timeout = timeout or self.timeout + start = len(self.notifications) + deadline = time.monotonic() + timeout + while len(self.notifications) - start < count: + remaining = deadline - time.monotonic() + if remaining <= 0: + break + self._notify_event.clear() + try: + await asyncio.wait_for(self._notify_event.wait(), timeout=remaining) + except asyncio.TimeoutError: + break + return self.notifications[start:] + + async def scan_and_connect(self): + """扫描并连接设备(支持前缀匹配)""" + print(f"正在扫描设备 '{self.device_name}*'...") + devices = await BleakScanner.discover(timeout=10.0) + device = None + for d in devices: + if d.name and d.name.startswith(self.device_name): + device = d + break + if not device: + print(f"未找到以 '{self.device_name}' 开头的设备,请确认设备已开机且处于配网模式") + return False + + print(f"找到设备: {device.name} ({device.address})") + print(f"正在连接...") + + self.client = BleakClient(device, timeout=15.0) + await self.client.connect() + print(f"已连接, MTU={self.client.mtu_size}") + + # 验证服务 + svc = self.client.services.get_service(SERVICE_UUID) + if not svc: + print(f"错误: 未发现配网服务 (UUID: 0xABF0)") + return False + print(f"发现配网服务 0xABF0") + + # 启用 NOTIFY + await self.client.start_notify(CHAR_NOTIFY_UUID, self._on_notify) + print(f"NOTIFY 已启用 (0xABF2)") + await asyncio.sleep(0.3) + return True + + async def send_cmd(self, cmd: int, payload: bytes = b""): + """发送二进制命令""" + data = bytes([cmd]) + payload + hex_str = data.hex(" ") + print(f" -> [0x{cmd:02x}] {hex_str}") + await self.client.write_gatt_char(CHAR_WRITE_UUID, data, response=True) + + async def provision_wifi(self, ssid: str, password: str): + """执行 WiFi 配网流程""" + print(f"\n{'='*50}") + print(f" 开始配网: SSID=\"{ssid}\"") + print(f"{'='*50}\n") + + # 第1步: 设置 SSID + print("[1/3] 发送 SSID...") + await self.send_cmd(CMD_SET_SSID, ssid.encode("utf-8")) + await asyncio.sleep(0.3) + + # 第2步: 设置密码(设置密码后设备会自动发起连接) + print("[2/3] 发送密码...") + await self.send_cmd(CMD_SET_PASSWORD, password.encode("utf-8")) + + # 第3步: 等待连接结果 + print("[3/3] 等待WiFi连接结果 (最长30秒)...") + result = await self._wait_wifi_result(timeout=30.0) + + if result is None: + print("\n超时: 未收到WiFi连接结果") + # 可尝试显式发送连接命令 + print("尝试发送显式连接命令...") + await self.send_cmd(CMD_CONNECT_AP) + result = await self._wait_wifi_result(timeout=30.0) + + if result is None: + print("\n配网结果: 超时,未收到设备响应") + elif result: + print("\n配网结果: WiFi 连接成功!") + else: + print("\n配网结果: WiFi 连接失败") + + return result + + async def _wait_wifi_result(self, timeout: float = 30.0): + """等待 WiFi 状态通知""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + remaining = deadline - time.monotonic() + if remaining <= 0: + break + self._notify_event.clear() + try: + await asyncio.wait_for(self._notify_event.wait(), timeout=remaining) + except asyncio.TimeoutError: + break + # 检查最新通知 + if self.notifications: + last = self.notifications[-1] + if len(last) >= 2 and last[0] == RESP_WIFI_STATUS: + return last[1] == 1 # 1=成功, 0=失败 + return None + + async def scan_wifi_list(self): + """请求 WiFi 扫描列表""" + print(f"\n{'='*50}") + print(f" 扫描 WiFi 列表") + print(f"{'='*50}\n") + + self.notifications.clear() + await self.send_cmd(CMD_GET_WIFI_LIST) + + # WiFi 扫描需要时间,等待列表结束标记 + print("等待扫描结果 (最长15秒)...") + deadline = time.monotonic() + 15.0 + wifi_list = [] + got_end = False + + while time.monotonic() < deadline and not got_end: + remaining = deadline - time.monotonic() + if remaining <= 0: + break + self._notify_event.clear() + try: + await asyncio.wait_for(self._notify_event.wait(), timeout=remaining) + except asyncio.TimeoutError: + break + + # 只处理最新一条通知 + if self.notifications: + n = self.notifications[-1] + if len(n) >= 3 and n[0] == RESP_WIFI_LIST: + rssi = struct.unpack("b", bytes([n[1]]))[0] + ssid_len = n[2] + ssid = n[3:3 + ssid_len].decode("utf-8", errors="replace") + wifi_list.append({"ssid": ssid, "rssi": rssi}) + elif len(n) >= 1 and n[0] == RESP_WIFI_LIST_END: + got_end = True + + if wifi_list: + print(f"\n扫描到 {len(wifi_list)} 个WiFi网络:") + print(f" {'序号':>4} {'RSSI':>6} {'SSID'}") + print(f" {'─'*4} {'─'*6} {'─'*30}") + for i, w in enumerate(wifi_list, 1): + print(f" {i:>4} {w['rssi']:>4}dBm {w['ssid']}") + else: + print("未扫描到WiFi网络") + + return wifi_list + + async def get_wifi_status(self): + """查询 WiFi 状态""" + print(f"\n{'='*50}") + print(f" 查询 WiFi 状态") + print(f"{'='*50}\n") + + self.notifications.clear() + await self.send_cmd(CMD_GET_WIFI_STATUS) + await self._wait_notifications(timeout=5.0, count=1) + + async def disconnect(self): + """断开连接""" + if self.client and self.client.is_connected: + try: + await self.client.stop_notify(CHAR_NOTIFY_UUID) + except Exception: + pass + await self.client.disconnect() + print("已断开BLE连接") + + +async def main(): + parser = argparse.ArgumentParser( + description="BLE 配网协议测试脚本", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +使用示例: + # WiFi 配网 + python tests/ble_provision_test.py --ssid "MyWiFi" --pwd "12345678" + + # 扫描 WiFi 列表 + python tests/ble_provision_test.py --scan-wifi + + # 查询 WiFi 状态 + python tests/ble_provision_test.py --get-status + """ + ) + parser.add_argument("--device", default=DEFAULT_DEVICE, + help=f"BLE 设备名称 (默认: {DEFAULT_DEVICE})") + parser.add_argument("--ssid", help="要连接的 WiFi SSID") + parser.add_argument("--pwd", default="", help="WiFi 密码") + parser.add_argument("--scan-wifi", action="store_true", + help="扫描 WiFi 列表") + parser.add_argument("--get-status", action="store_true", + help="查询 WiFi 连接状态") + parser.add_argument("--timeout", type=float, default=10.0, + help="命令超时秒数 (默认: 10.0)") + args = parser.parse_args() + + # 至少指定一个操作 + if not args.ssid and not args.scan_wifi and not args.get_status: + parser.print_help() + print("\n请指定操作: --ssid/--scan-wifi/--get-status") + return 1 + + tester = BleProvisionTester(device_name=args.device, timeout=args.timeout) + + # 扫描连接 + if not await tester.scan_and_connect(): + return 1 + + try: + # 执行操作 + if args.scan_wifi: + await tester.scan_wifi_list() + + if args.get_status: + await tester.get_wifi_status() + + if args.ssid: + result = await tester.provision_wifi(args.ssid, args.pwd) + return 0 if result else 1 + + finally: + await tester.disconnect() + + return 0 + + +if __name__ == "__main__": + exit_code = asyncio.run(main()) + sys.exit(exit_code or 0)