toy-hardware/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc

2003 lines
87 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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按键触发设备状态=%dWiFi连接状态=%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);