2003 lines
87 KiB
C++
2003 lines
87 KiB
C++
#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/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 "imu_sensor_thing.h" // 引入IMU传感器IoT设备头文件
|
||
#include "system_info.h" // 引入系统信息头文件
|
||
#include "settings.h"
|
||
#include <cmath> // 添加数学函数头文件
|
||
|
||
#include <wifi_station.h>
|
||
#include <ssid_manager.h>
|
||
#include <esp_wifi.h>
|
||
#include <nvs_flash.h>
|
||
#include <esp_log.h>
|
||
#include <esp_efuse_table.h>
|
||
#include <driver/i2c_master.h>
|
||
#include <inttypes.h> // 添加PRIu32宏的定义支持
|
||
#include <cstring>
|
||
#include "driver/gpio.h"
|
||
#include <esp_adc/adc_oneshot.h>
|
||
#include <esp_adc/adc_cali_scheme.h>
|
||
#include <esp_log.h>
|
||
#include <esp_sleep.h>
|
||
#include <vector>
|
||
#include <algorithm>
|
||
#include "freertos/queue.h"
|
||
|
||
#define TAG "Airhub1"
|
||
#define Pro_TAG "Airhub"
|
||
|
||
#include <driver/touch_pad.h>
|
||
#include <driver/touch_sensor.h>
|
||
|
||
// 触摸事件类型
|
||
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 = 100; // 100ms读取一次IMU数据
|
||
iot::ImuSensorThing* imu_thing_; // IMU传感器IoT设备实例
|
||
|
||
// 电量检测相关
|
||
adc_oneshot_unit_handle_t adc_handle_;
|
||
adc_cali_handle_t adc_cali_handle_; // ADC校准句柄
|
||
esp_timer_handle_t battery_timer_handle_;
|
||
std::vector<int> adc_values_; // ADC采样值队列(存储校准后的mV值)
|
||
uint32_t battery_level_;// 电池电量百分比
|
||
int battery_ticks_;
|
||
int battery_alert_ticks_;
|
||
int battery_report_ticks_; // 电量上报计数器
|
||
bool battery_report_enabled_; // 电量上报是否启用
|
||
const int kBatteryAdcInterval = 10; // 10秒检测一次
|
||
const int kBatteryReportInterval = 30; // 30秒上报一次
|
||
const int kBatteryReportDelay = 3; // 启动3秒后才开始上报
|
||
const int kBatteryAdcDataCount = 20; // 保存20个ADC值用于平均(增加采样次数)
|
||
const int kBatteryAdcSampleCount = 10; // 每次读取采样10次(增加采样次数)
|
||
const char* BATTERY_REPORT_URL = CONFIG_BATTERY_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};
|
||
|
||
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 ReportBatteryToServer(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<int>(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),
|
||
adc_handle_(nullptr),
|
||
battery_timer_handle_(nullptr),
|
||
battery_level_(0),
|
||
battery_ticks_(0),
|
||
battery_alert_ticks_(0),
|
||
battery_report_ticks_(0),
|
||
battery_report_enabled_(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();
|
||
|
||
// 启用PowerSaveTimer,启用完整的低功耗管理
|
||
power_save_timer_->SetEnabled(true);
|
||
ESP_LOGI(TAG, "🔋 PowerSaveTimer已启用,10秒无活动将进入低功耗模式");
|
||
|
||
// 延迟调用触摸板初始化,避免在构造函数中就调用
|
||
ESP_LOGI(TAG, "在构造函数完成后调用触摸初始化");
|
||
// 使用task来延迟初始化触摸功能
|
||
xTaskCreate([](void* arg) {
|
||
MovecallMojiESP32S3* board = static_cast<MovecallMojiESP32S3*>(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();
|
||
|
||
// 根据流程图中的情况处理触摸事件:
|
||
// 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<MovecallMojiESP32S3*>(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<MovecallMojiESP32S3*>(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);
|
||
|
||
// 3. 如果当前是待命状态,触摸会唤醒并发送消息
|
||
if (current_state == kDeviceStateIdle) {
|
||
ESP_LOGI(TAG, "从待命状态唤醒并发送触摸消息");
|
||
// 先尝试切换到监听状态,确保设备被唤醒
|
||
app.StartListening();
|
||
// 短暂延时,确保状态已经切换
|
||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||
}
|
||
// 4. 如果当前是Listening状态但未检测到语音输入,触摸会发送消息
|
||
else if (current_state == kDeviceStateListening) {
|
||
ESP_LOGI(TAG, "监听状态下未检测到语音输入,发送触摸消息");
|
||
}
|
||
|
||
// 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<MovecallMojiESP32S3*>(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<MovecallMojiESP32S3*>(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_;
|
||
}
|
||
|
||
// 清理电量检测资源
|
||
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,
|
||
.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姿态传感器备用地址
|
||
};
|
||
|
||
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, "Found I2C device at address 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.");
|
||
}
|
||
}
|
||
// ==============================================================================
|
||
|
||
|
||
|
||
// 按钮初始化 函数
|
||
void InitializeButtons() {
|
||
ESP_LOGI(TAG, "Initializing buttons...");
|
||
|
||
// 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 button clicked too frequently, ignoring this click");
|
||
return;
|
||
}
|
||
|
||
last_click_time = current_time;
|
||
ESP_LOGI(TAG, "BOOT button clicked");
|
||
|
||
// 创建一个单独的任务来处理触摸解锁,避免在按钮回调中执行复杂操作
|
||
xTaskCreate([](void* arg) {
|
||
MovecallMojiESP32S3* board = static_cast<MovecallMojiESP32S3*>(arg);
|
||
if (board) {
|
||
board->UnlockTouchTask();
|
||
}
|
||
vTaskDelete(NULL);
|
||
}, "boot_unlock", 4096, this, 5, NULL);
|
||
|
||
// 获取当前应用实例和状态
|
||
auto &app = Application::GetInstance();
|
||
auto current_state = app.GetDeviceState();
|
||
|
||
// 检查是否处于BluFi配网状态,如果是则屏蔽按键响应(生产测试模式下除外)
|
||
auto* wifi_board = dynamic_cast<WifiBoard*>(this);
|
||
if (wifi_board && wifi_board->IsBluFiProvisioningActive() && !production_test_mode_) {
|
||
ESP_LOGI(Pro_TAG, "🔵 当前为蓝牙配网模式,[BOOT按键]被按下,长按BOOT按键5秒可进入生产测试模式!");// 生产测试打印
|
||
ESP_LOGI("WiFiMAC", "Wi-Fi MAC Address: %s", SystemInfo::GetMacAddress().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("WiFiMAC", "Wi-Fi MAC Address: %s", SystemInfo::GetMacAddress().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 {
|
||
// 检查是否在BluFi配网模式下,如果是则屏蔽单独的BOOT按键功能
|
||
auto* wifi_board = dynamic_cast<WifiBoard*>(this);
|
||
if (wifi_board && wifi_board->IsBluFiProvisioningActive() && !production_test_mode_) {
|
||
ESP_LOGI(TAG, "🔵 BluFi配网模式下,屏蔽单独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, "LALA") == 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 {
|
||
// 其他状态下只唤醒设备
|
||
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");
|
||
|
||
// 检查是否处于BluFi配网状态,只有在配网模式下才允许进入测试模式
|
||
auto* wifi_board = dynamic_cast<WifiBoard*>(this);
|
||
if (wifi_board && wifi_board->IsBluFiProvisioningActive()) {
|
||
// ESP_LOGI(TAG, "🔧 设备正在进行BluFi配网,长按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!");
|
||
|
||
// 检查是否处于BluFi配网状态,如果是则屏蔽按键响应(生产测试模式下除外)
|
||
auto* wifi_board = dynamic_cast<WifiBoard*>(this);
|
||
if (wifi_board && wifi_board->IsBluFiProvisioningActive() && !production_test_mode_) {
|
||
ESP_LOGI(TAG, "🔵 设备正在进行BluFi配网,音量加按键被屏蔽");
|
||
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!");
|
||
|
||
// 检查是否处于BluFi配网状态,如果是则屏蔽按键响应(生产测试模式下除外)
|
||
auto* wifi_board = dynamic_cast<WifiBoard*>(this);
|
||
if (wifi_board && wifi_board->IsBluFiProvisioningActive() && !production_test_mode_) {
|
||
ESP_LOGI(TAG, "🔵 设备正在进行BluFi配网,音量减按键被屏蔽");
|
||
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<MovecallMojiESP32S3*>(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, "Battery monitor initialized on GPIO%d", BATTERY_ADC_GPIO);
|
||
}
|
||
|
||
void InitializeImuSensor() {
|
||
// 在非生产测试模式下,不启动姿态传感器业务以节约资源
|
||
if (!production_test_mode_) {
|
||
ESP_LOGI(TAG, "非生产测试模式,姿态传感器业务已禁用以节约资源");
|
||
imu_initialized_ = false;
|
||
imu_sensor_ = nullptr;
|
||
return;
|
||
}
|
||
|
||
ESP_LOGI(TAG, "Initializing IMU sensor QMI8658A...");
|
||
|
||
// 初始化状态为false,确保系统在IMU不可用时仍能正常运行
|
||
imu_initialized_ = false;
|
||
imu_sensor_ = nullptr;
|
||
|
||
if (!codec_i2c_bus_) {
|
||
ESP_LOGW(TAG, "I2C bus not initialized, IMU sensor will be disabled");
|
||
ESP_LOGW(TAG, "System will continue to operate without motion detection features");
|
||
return;
|
||
}
|
||
|
||
ESP_LOGI(TAG, "I2C bus is initialized, creating IMU sensor instance");
|
||
ESP_LOGI(TAG, "Using I2C address: 0x6A");
|
||
|
||
// 添加延迟,确保I2C总线完全稳定
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
|
||
// 创建IMU传感器实例 (使用I2C地址0x6A)
|
||
uint8_t working_address = 0x6A;
|
||
try {
|
||
imu_sensor_ = new QMI8658A(codec_i2c_bus_, 0x6A);
|
||
ESP_LOGI(TAG, "IMU sensor instance created successfully with I2C address 0x6A");
|
||
|
||
// 测试I2C通信 - 尝试读取芯片ID
|
||
ESP_LOGI(TAG, "Testing I2C communication with QMI8658A...");
|
||
uint8_t chip_id = imu_sensor_->GetChipId();
|
||
ESP_LOGI(TAG, "Read chip ID: 0x%02X (expected: 0x05)", chip_id);
|
||
|
||
if (chip_id == 0xFF) {
|
||
ESP_LOGE(TAG, "I2C communication failed - chip ID read returned 0xFF");
|
||
ESP_LOGI(TAG, "Trying alternative I2C address 0x6B...");
|
||
|
||
// 尝试使用备用地址0x6B
|
||
delete imu_sensor_;
|
||
imu_sensor_ = new QMI8658A(codec_i2c_bus_, 0x6B);
|
||
working_address = 0x6B;
|
||
chip_id = imu_sensor_->GetChipId();
|
||
ESP_LOGI(TAG, "Read chip ID with address 0x6B: 0x%02X", chip_id);
|
||
|
||
if (chip_id == 0xFF) {
|
||
ESP_LOGE(TAG, "I2C communication failed with both addresses (0x6A and 0x6B)");
|
||
ESP_LOGE(TAG, "Possible causes: 1) Hardware connection issue 2) Wrong I2C pins 3) Power supply issue");
|
||
delete imu_sensor_;
|
||
imu_sensor_ = nullptr;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (chip_id != 0x05) {
|
||
ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X, expected: 0x05", chip_id);
|
||
ESP_LOGW(TAG, "This may not be a QMI8658A sensor or there's a communication issue");
|
||
// 继续尝试初始化,可能是兼容的传感器
|
||
}
|
||
|
||
ESP_LOGI(TAG, "Successfully established I2C communication with address 0x%02X", working_address);
|
||
|
||
} catch (...) {
|
||
ESP_LOGW(TAG, "Exception occurred while creating IMU sensor instance");
|
||
ESP_LOGW(TAG, "System will continue to operate without motion detection features");
|
||
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(TAG, "Starting IMU sensor initialization...");
|
||
|
||
// 初始化传感器 - 修复逻辑错误:QMI8658A_OK = 0 表示成功
|
||
qmi8658a_error_t init_result = imu_sensor_->Initialize(&config);
|
||
if (init_result == QMI8658A_OK) {
|
||
imu_initialized_ = true;
|
||
ESP_LOGI(TAG, "QMI8658A sensor initialized successfully");
|
||
|
||
// 创建IMU数据读取定时器
|
||
esp_timer_create_args_t timer_args = {
|
||
.callback = [](void* arg) {
|
||
MovecallMojiESP32S3* self = static_cast<MovecallMojiESP32S3*>(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_LOGW(TAG, "Failed to create IMU timer: %s", esp_err_to_name(err));
|
||
ESP_LOGW(TAG, "IMU sensor will be disabled, system continues normally");
|
||
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_LOGW(TAG, "Failed to start IMU timer: %s", esp_err_to_name(err));
|
||
ESP_LOGW(TAG, "IMU sensor will be disabled, system continues normally");
|
||
esp_timer_delete(imu_timer_handle_);
|
||
imu_timer_handle_ = nullptr;
|
||
delete imu_sensor_;
|
||
imu_sensor_ = nullptr;
|
||
imu_initialized_ = false;
|
||
return;
|
||
}
|
||
|
||
ESP_LOGI(TAG, "IMU data reading timer started with %dms interval", kImuReadInterval);
|
||
} else {
|
||
ESP_LOGW(TAG, "Failed to initialize QMI8658A sensor, error code: %d", init_result);
|
||
ESP_LOGW(TAG, "Possible causes: 1) Hardware connection issue 2) Sensor not responding 3) I2C communication error");
|
||
ESP_LOGW(TAG, "System will continue to operate without motion detection features");
|
||
|
||
// 清理资源
|
||
if (imu_sensor_) {
|
||
delete imu_sensor_;
|
||
imu_sensor_ = nullptr;
|
||
}
|
||
imu_initialized_ = false;
|
||
}
|
||
}
|
||
|
||
void ReadImuData() {
|
||
if (!imu_initialized_ || !imu_sensor_) {
|
||
return;
|
||
}
|
||
|
||
// 读取传感器数据
|
||
if (imu_sensor_->ReadSensorData(&latest_imu_data_)) {
|
||
// 可以在这里添加数据处理逻辑
|
||
// 例如:检测姿态变化、计算角度等
|
||
ProcessImuData(&latest_imu_data_);
|
||
} else {
|
||
ESP_LOGW(TAG, "Failed to read IMU data");
|
||
}
|
||
}
|
||
|
||
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_LOGD(TAG, "Motion detected: accel_magnitude = %.2f g", accel_magnitude);
|
||
}
|
||
|
||
// 更新IMU IoT设备的数据
|
||
if (imu_thing_) {
|
||
imu_thing_->UpdateData(*data);
|
||
}
|
||
|
||
// 记录详细的传感器数据(调试用)
|
||
ESP_LOGV(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 (!battery_report_enabled_ && battery_ticks_ >= kBatteryReportDelay) {
|
||
battery_report_enabled_ = true;
|
||
ESP_LOGI(TAG, "📤 电量上报功能已启用,每%d秒上报一次", kBatteryReportInterval);
|
||
}
|
||
|
||
// 如果电池电量数据充足,则每 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 (battery_report_enabled_) {
|
||
battery_report_ticks_++;
|
||
if (battery_report_ticks_ % kBatteryReportInterval == 0) {
|
||
ReportBatteryToServer(battery_level_);
|
||
}
|
||
}
|
||
|
||
battery_alert_ticks_++;
|
||
auto& app = Application::GetInstance();
|
||
auto state = app.GetDeviceState();
|
||
|
||
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<int> 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();
|
||
|
||
float battery_voltage = average_voltage_mv / 1000.0f * 2.0f;
|
||
|
||
// 使用固定电压阈值
|
||
const float kVoltage100Percent = 4.0f; // 满电电压
|
||
const float kVoltage75Percent = 3.6f; // 75%电量电压
|
||
const float kVoltage50Percent = 3.3f; // 50%电量电压(功放驱动电压)
|
||
const float kVoltage25Percent = 3.0f; // 25%电量电压
|
||
const float kVoltage0Percent = 0.0f; // 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, 电量: %d%%, 满电电压: %.2fV",
|
||
average_voltage_mv, battery_voltage, battery_percentage, kVoltage100Percent);
|
||
|
||
// 打印Wi-Fi的Mac地址
|
||
ESP_LOGI("WiFiMAC", "Wi-Fi MAC Address: %s", SystemInfo::GetMacAddress().c_str());// 生产测试打印
|
||
}
|
||
|
||
void InitializeStoryButton() {
|
||
story_button_.OnClick([this]() {
|
||
ESP_LOGI(TAG, "Story button clicked!");
|
||
|
||
// 检查是否处于BluFi配网状态,如果是则屏蔽按键响应(生产测试模式下除外)
|
||
auto* wifi_board = dynamic_cast<WifiBoard*>(this);
|
||
if (wifi_board && wifi_board->IsBluFiProvisioningActive() && !production_test_mode_) {
|
||
//ESP_LOGI(Pro_TAG, "🔵 设备正在进行蓝牙配网,长按BOOT按键5秒可进入生产测试模式!");
|
||
ESP_LOGI(Pro_TAG, "🔵 当前为蓝牙配网模式,[故事按键]被按下,长按BOOT按键5秒可进入生产测试模式!");
|
||
ESP_LOGI("WiFiMAC", "Wi-Fi MAC Address: %s", SystemInfo::GetMacAddress().c_str());// 生产测试打印
|
||
ESP_LOGI(Pro_TAG, "当前电量值: %" PRIu32 "%%", battery_level_);// 生产测试打印
|
||
return;
|
||
}
|
||
|
||
// 如果处于生产测试模式,记录按键测试并播放音频
|
||
if (production_test_mode_) {
|
||
ESP_LOGI(Pro_TAG, "🔧 生产测试模式:故事按键已被按下!");// 生产测试打印
|
||
ESP_LOGI("WiFiMAC", "Wi-Fi MAC Address: %s", SystemInfo::GetMacAddress().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 == kDeviceStateSpeaking) {
|
||
ESP_LOGI(TAG, "正在说话,先终止当前语音");
|
||
app.AbortSpeaking(kAbortReasonNone);
|
||
// 等待一小段时间再发送故事请求
|
||
vTaskDelay(300 / portTICK_PERIOD_MS);
|
||
//app.SendTextMessage("给我讲个小故事");
|
||
app.SendStoryRequest(); // 发送json格式讲故事请求
|
||
} else if (current_state == kDeviceStateIdle || current_state == kDeviceStateListening) {
|
||
// 在待机或监听状态下可以安全地发送消息
|
||
ESP_LOGI(TAG, "发送故事请求");
|
||
//app.SendTextMessage("给我讲个小故事");
|
||
app.SendStoryRequest(); // 发送json格式讲故事请求
|
||
} else {
|
||
ESP_LOGI(TAG, "当前状态(%d)不适合发送故事请求", current_state);
|
||
}
|
||
|
||
// 唤醒设备,防止立即进入睡眠
|
||
power_save_timer_->WakeUp();
|
||
});
|
||
ESP_LOGI(TAG, "Story button initialized on GPIO%d", KEY4_GPIO);
|
||
|
||
ESP_LOGI(TAG, "All buttons initialized successfully");
|
||
}
|
||
|
||
// 物联网初始化,添加音频设备和IMU传感器
|
||
void InitializeIot() {
|
||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||
|
||
// 创建并添加IMU传感器IoT设备 陀螺仪/姿态传感器业务 新增代码
|
||
// ==============================================================================
|
||
if (imu_sensor_ && imu_initialized_) {
|
||
imu_thing_ = new iot::ImuSensorThing(imu_sensor_);
|
||
thing_manager.AddThing(imu_thing_);
|
||
ESP_LOGI(TAG, "IMU sensor added to IoT manager");
|
||
} else {
|
||
ESP_LOGW(TAG, "IMU sensor not initialized, skipping IoT registration");
|
||
}
|
||
// ==============================================================================
|
||
}
|
||
|
||
// 唤醒词方案配置
|
||
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 Es8311AudioCodec* 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 {
|
||
// 在确认I2C总线已初始化后创建编解码器
|
||
ESP_LOGI(TAG, "Creating Es8311AudioCodec instance...");
|
||
audio_codec = new Es8311AudioCodec(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);
|
||
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 MovecallMojiESP32S3::ReportBatteryToServer(int battery_level) {
|
||
ESP_LOGI(TAG, "📤 准备上报电量: %d%%", battery_level);
|
||
|
||
// 获取当前已连接的Wi-Fi信号强度
|
||
int8_t rssi = -100; // 默认值,表示未连接或获取失败
|
||
auto* wifi_board = dynamic_cast<WifiBoard*>(this);
|
||
if (wifi_board) {
|
||
auto& wifi_station = WifiStation::GetInstance();
|
||
if (wifi_station.IsConnected()) {
|
||
rssi = wifi_station.GetRssi();
|
||
ESP_LOGI(TAG, "当前WiFi信号强度: %d dBm", rssi);
|
||
} else {
|
||
ESP_LOGW(TAG, "WiFi未连接,无法获取信号强度");
|
||
}
|
||
} else {
|
||
ESP_LOGW(TAG, "无法获取WifiBoard实例");
|
||
}
|
||
|
||
// 构造JSON数据
|
||
char json_buffer[512];
|
||
snprintf(json_buffer, sizeof(json_buffer),
|
||
"{\"mac_address\":\"%s\",\"battery_level\":%d,\"wifi_rssi\":%d}",
|
||
SystemInfo::GetMacAddress().c_str(),
|
||
battery_level,
|
||
rssi);
|
||
|
||
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", BATTERY_REPORT_URL, json_buffer)) {
|
||
ESP_LOGE(TAG, "❌ 连接服务器失败: %s", BATTERY_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<MovecallMojiESP32S3*>(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, "🎵 测试模式:开始播放进入测试模式音频");
|
||
|
||
// 改进的音频播放完成等待逻辑
|
||
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));
|
||
|
||
// 生产测试模式下检测姿态传感器连接
|
||
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);// 生产测试打印
|
||
|
||
// 延时1秒后播放陀螺仪检测成功音频
|
||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||
auto& app = Application::GetInstance();
|
||
app.PlaySound(Lang::Sounds::P3_TUOLUOYI);
|
||
ESP_LOGI(Pro_TAG, "🎵 播放陀螺仪检测成功音频");// 生产测试打印
|
||
|
||
// 等待音频播放完成后清空队列
|
||
vTaskDelay(pdMS_TO_TICKS(3000)); // 等待3秒确保音频播放完成
|
||
|
||
// 清空音频播放队列,避免残留
|
||
app.ClearAudioQueue();
|
||
} else {
|
||
ESP_LOGI(Pro_TAG, "姿态传感器未连接或通信异常!");// 生产测试打印
|
||
}
|
||
} else {
|
||
ESP_LOGI(Pro_TAG, "姿态传感器未连接或通信异常!");// 生产测试打印
|
||
// ESP_LOGI(TAG, "I2C总线未初始化,无法检测姿态传感器");
|
||
}
|
||
|
||
// 非阻塞式触摸检测 - 触摸事件将通过现有的触摸处理机制来处理
|
||
ESP_LOGI(Pro_TAG, "生产测试模式已启用,触摸检测已激活,其他按键功能正常可用");
|
||
ESP_LOGI(Pro_TAG, "🔧 提示:现在可以测试触摸板、BOOT按键和讲故事按键!!");
|
||
}
|
||
// ==============================================================================
|
||
|
||
DECLARE_BOARD(MovecallMojiESP32S3);
|