#include "wifi_board.h" #include "audio_codecs/es8311_audio_codec.h" #include "audio_codecs/box_audio_codec.h" #include "application.h" #include "button.h" #include "config.h" #include "iot/thing_manager.h" #include "led/single_led.h" #include "display/display.h" #include "boards/common/power_save_timer.h" // 添加电源管理头文件 #include "assets/lang_config.h" // 引入语音配置头文件 新增 #include "volume_config.h" // 引入音量配置头文件 #include "boards/common/qmi8658a.h" // 引入QMI8658A姿态传感器头文件 #include "imu_sensor_thing.h" // 引入IMU传感器IoT设备头文件 #include "system_info.h" // 引入系统信息头文件 #include "settings.h" #include // 添加数学函数头文件 #include #include #include #include #include #include #include #include // 添加PRIu32宏的定义支持 #include #include "driver/gpio.h" #include #include #include #include #include #include "freertos/queue.h" #define TAG "Airhub1" #define Pro_TAG "Airhub" #include #include // 触摸事件类型 typedef enum { TOUCH_EVENT_PRESS = 0, // 触摸按下事件 TOUCH_EVENT_RELEASE // 触摸释放事件 } touch_event_type_t; // 触摸状态枚举 typedef enum { TOUCH_STATE_IDLE, // 空闲状态 - 未触摸 TOUCH_STATE_PRESSED, // 已按下状态 - 已经触发事件,等待释放 TOUCH_STATE_RELEASED, // 释放过渡状态 - 确认松手 TOUCH_STATE_DEBOUNCE // 去抖状态 - 等待信号稳定 } touch_state_t; // 触摸事件数据结构 typedef struct { int pad_num; // 触摸板编号 touch_event_type_t type; // 事件类型:按下或释放 } touch_event_data_t; // 前向声明TouchEventTask函数 static void TouchEventTask(void* arg); class MovecallMojiESP32S3 : public WifiBoard { private: // 触摸状态相关 touch_state_t touch_states_[4]; // 每个触摸点的状态 uint32_t touch_last_time_[4]; // 每个触摸点的最后操作时间 uint32_t raw_touch_values_[4]; // 原始触摸值 uint32_t touch_thresholds_[4]; // 触摸阈值 // 去抖动和最短释放时间参数 const uint32_t DEBOUNCE_TIME_MS = 100; // 去抖时间(毫秒) const uint32_t MIN_RELEASE_TIME_MS = 300; // 最短释放确认时间 // 添加触摸任务锁定相关变量 bool touch_task_locked_ = false; // 触摸任务锁定标志 int active_touch_pad_ = -1; // 当前活跃的触摸点编号 uint32_t touch_task_start_time_ = 0; // 触摸任务开始时间 const uint32_t TOUCH_TASK_TIMEOUT_MS = 10000; // 任务超时时间(10秒) PowerSaveTimer* power_save_timer_; static MovecallMojiESP32S3* instance_; static void IRAM_ATTR TouchPadISR(void* arg); i2c_master_bus_handle_t codec_i2c_bus_; // QMI8658A姿态传感器相关 QMI8658A* imu_sensor_; esp_timer_handle_t imu_timer_handle_; qmi8658a_data_t latest_imu_data_; bool imu_initialized_; const int kImuReadInterval = 160; // 160ms读取一次IMU数据,匹配125Hz采样率 iot::ImuSensorThing* imu_thing_; // IMU传感器IoT设备实例 // 电量检测相关 adc_oneshot_unit_handle_t adc_handle_; adc_cali_handle_t adc_cali_handle_; // ADC校准句柄 esp_timer_handle_t battery_timer_handle_; std::vector adc_values_; // ADC采样值队列(存储校准后的mV值) uint32_t battery_level_;// 电池电量百分比 int battery_ticks_; int battery_alert_ticks_; int 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(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已启用,20秒无活动将进入低功耗模式"); // 延迟调用触摸板初始化,避免在构造函数中就调用 ESP_LOGI(TAG, "在构造函数完成后调用触摸初始化"); // 使用task来延迟初始化触摸功能 xTaskCreate([](void* arg) { MovecallMojiESP32S3* board = static_cast(arg); // 延迟一段时间,确保其他组件初始化完成 vTaskDelay(1000 / portTICK_PERIOD_MS); ESP_LOGI(TAG, "开始延迟初始化触摸板..."); if (board) { board->InitializeTouchPads(); } vTaskDelete(NULL); }, "touch_init", 4096, this, 5, NULL); } // 发送触摸消息 void SendTouchMessage(int touch_pad_num) { const char* message = nullptr; power_save_timer_->WakeUp(); // 获取当前应用状态 auto& app = Application::GetInstance(); auto current_state = app.GetDeviceState(); // 仅在 Dialog 对话状态且内部 listening 开启时有效 if (!(current_state == kDeviceStateDialog && app.IsDialogUploadEnabled())) { ESP_LOGI(TAG, "触摸事件无效:仅在Dialog+listening内部状态下有效"); if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { xTaskCreate([](void* arg) { MovecallMojiESP32S3* board = static_cast(arg); if (board) { board->UnlockTouchTask(); } vTaskDelete(NULL); }, "unlock_invalid_state", 4096, this, 5, NULL); } return; } // 根据流程图中的情况处理触摸事件: // 1. 如果当前是Speaking状态,触摸事件不生效 if (current_state == kDeviceStateSpeaking) { ESP_LOGI(TAG, "当前处于Speaking状态,触摸事件被忽略"); // 由于任务未能执行,立即解锁触摸任务 if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { ESP_LOGI(TAG, "触摸任务无法执行,创建任务来解锁"); // 创建任务来解锁,避免直接调用可能导致栈溢出的操作 xTaskCreate([](void* arg) { MovecallMojiESP32S3* board = static_cast(arg); if (board) { board->UnlockTouchTask(); } vTaskDelete(NULL); }, "unlock_failed", 4096, this, 5, NULL); } return; } // 2. 如果当前是Listening状态且已检测到语音输入,触摸事件不生效 if (current_state == kDeviceStateListening && app.IsVoiceDetected()) { ESP_LOGI(TAG, "当前处于Listening状态且已检测到语音输入,触摸事件被忽略"); // 由于任务未能执行,立即解锁触摸任务 if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { ESP_LOGI(TAG, "触摸任务无法执行,创建任务来解锁"); // 创建任务来解锁,避免直接调用可能导致栈溢出的操作 xTaskCreate([](void* arg) { MovecallMojiESP32S3* board = static_cast(arg); if (board) { board->UnlockTouchTask(); } vTaskDelete(NULL); }, "unlock_failed", 4096, this, 5, NULL); } return; } // 根据触摸点选择消息 switch (touch_pad_num) { case 0: message = "有人在摸摸你的脑袋"; break; case 1: message = "有人在摸摸你的肚子"; break; case 2: message = "有人在摸摸你的下巴"; break; case 3: message = "有人在摸摸你的后背"; break; } // 发送消息 if (message != nullptr) { ESP_LOGI(TAG, "发送触摸消息: \"%s\"", message); // 仅在 Dialog+内部listening 下发送;其他状态在前面已返回 // SendTextMessage内部会自动检查协议是否初始化 app.SendTextMessage(message); ESP_LOGI(TAG, "消息已发送"); // 消息已发送,开始监听语音回复 // 任务将在收到回复或超时后结束 // 通过TaskStateMonitor监听设备状态变化 // 创建一个任务来监控设备状态变化 if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { ESP_LOGI(TAG, "创建任务状态监控"); xTaskCreate([](void* arg) { MovecallMojiESP32S3* board = static_cast(arg); auto& app = Application::GetInstance(); uint32_t start_time = esp_timer_get_time() / 1000; // 等待设备状态变为Speaking或超时 // 如果超时或设备重新回到Idle状态,则解锁触摸任务 while (true) { auto state = app.GetDeviceState(); uint32_t current_time = esp_timer_get_time() / 1000; uint32_t elapsed = current_time - start_time; // 如果设备开始说话,等待它说完 if (state == kDeviceStateSpeaking) { ESP_LOGI(TAG, "检测到设备进入Speaking状态,等待说话完成"); // 等待设备回到Idle状态 while (app.GetDeviceState() == kDeviceStateSpeaking) { vTaskDelay(100 / portTICK_PERIOD_MS); // 检查超时 uint32_t now = esp_timer_get_time() / 1000; if (now - start_time > 30000) { // 30秒超时 ESP_LOGW(TAG, "等待说话完成超时"); break; } } ESP_LOGI(TAG, "设备说话已完成,解锁触摸任务"); board->UnlockTouchTask(); break; } // 如果设备回到Idle状态,可能是消息被忽略 else if (state == kDeviceStateIdle && elapsed > 1000) { ESP_LOGW(TAG, "设备回到Idle状态,消息可能被忽略"); board->UnlockTouchTask(); break; } // 如果等待太久,自动解锁 else if (elapsed > 10000) { // 10秒超时 ESP_LOGW(TAG, "等待回复超时,解锁触摸任务"); board->UnlockTouchTask(); break; } vTaskDelay(200 / portTICK_PERIOD_MS); } vTaskDelete(NULL); }, "task_monitor", 8192, this, 5, NULL); } } else { // 无效的触摸点或消息,自动解锁 if (touch_task_locked_ && active_touch_pad_ == touch_pad_num) { // 创建任务来解锁 xTaskCreate([](void* arg) { MovecallMojiESP32S3* board = static_cast(arg); if (board) { board->UnlockTouchTask(); } vTaskDelete(NULL); }, "unlock_invalid", 4096, this, 5, NULL); } } } // 析构函数 ~MovecallMojiESP32S3() { delete power_save_timer_; // 清理IMU传感器资源 if (imu_timer_handle_) { esp_timer_stop(imu_timer_handle_); esp_timer_delete(imu_timer_handle_); } if (imu_sensor_) { delete imu_sensor_; } if (imu_thing_) { delete imu_thing_; } // 清理电量检测资源 if (battery_timer_handle_) { esp_timer_stop(battery_timer_handle_); esp_timer_delete(battery_timer_handle_); } if (adc_handle_) { adc_oneshot_del_unit(adc_handle_); } } void InitializeCodecI2c() { ESP_LOGI(TAG, "Initializing I2C master bus for audio codec...");// // 初始化I2C外设 编解码器 i2c_master_bus_config_t i2c_bus_cfg = { // .i2c_port = I2C_NUM_0, .i2c_port = I2C_NUM_1, .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, .clk_source = I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt = 7, .intr_priority = 0, .trans_queue_depth = 0, .flags = { .enable_internal_pullup = 1, }, }; ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_)); ScanI2cDevices(); // 新增 扫描I2C总线上的设备 新增陀螺仪/姿态传感器 业务代码 } // 新增 扫描I2C总线上的设备 函数 // ============================================================================== void ScanI2cDevices() { ESP_LOGI(TAG, "Scanning I2C bus for devices..."); int devices_found = 0; // 只扫描指定的三个设备地址 uint8_t target_addresses[] = { 0x18, // ES8311音频编解码器地址 0x6A, // QMI8658A姿态传感器地址 0x6B, // QMI8658A姿态传感器备用地址 0x40 }; size_t addr_count = sizeof(target_addresses) / sizeof(target_addresses[0]); for (size_t i = 0; i < addr_count; i++) { uint8_t addr = target_addresses[i]; i2c_device_config_t dev_cfg = { .dev_addr_length = I2C_ADDR_BIT_LEN_7, .device_address = addr, .scl_speed_hz = 100000, // 使用较低的速度进行扫描 }; i2c_master_dev_handle_t dev_handle; esp_err_t ret = i2c_master_bus_add_device(codec_i2c_bus_, &dev_cfg, &dev_handle); if (ret == ESP_OK) { // 尝试读取一个字节来检测设备是否响应 uint8_t dummy_data; ret = i2c_master_receive(dev_handle, &dummy_data, 1, 100); if (ret == ESP_OK || ret == ESP_ERR_TIMEOUT) { ESP_LOGI(TAG, "I2C设备在线: 0x%02X", addr); devices_found++; } i2c_master_bus_rm_device(dev_handle); } } ESP_LOGI(TAG, "I2C scan completed. Found %d devices", devices_found); if (devices_found == 0) { ESP_LOGW(TAG, "No I2C devices found. Check hardware connections."); } } // ============================================================================== // 按钮初始化 函数 void InitializeButtons() { ESP_LOGI(TAG, "初始化按钮...");// 初始化按钮... // BOOT按键单击事件 - 用于WiFi重置和触摸解锁 boot_button_.OnClick([this]() { static uint32_t last_click_time = 0; uint32_t current_time = esp_timer_get_time() / 1000; // 当前时间(毫秒) // 防抖动处理:如果距离上次点击时间太短(小于500毫秒),则忽略此次点击 if (last_click_time > 0 && current_time - last_click_time < 500) { ESP_LOGI(TAG, "BOOT 按钮点击过于频繁,忽略此次点击");// BOOT 按钮点击过于频繁,忽略此次点击 return; } last_click_time = current_time; ESP_LOGI(TAG, "BOOT button clicked"); // 创建一个单独的任务来处理触摸解锁,避免在按钮回调中执行复杂操作 xTaskCreate([](void* arg) { MovecallMojiESP32S3* board = static_cast(arg); if (board) { board->UnlockTouchTask(); } vTaskDelete(NULL); }, "boot_unlock", 4096, this, 5, NULL); // 获取当前应用实例和状态 auto &app = Application::GetInstance(); auto current_state = app.GetDeviceState(); // 检查是否处于BluFi配网状态,如果是则屏蔽按键响应(生产测试模式下除外) auto* wifi_board = dynamic_cast(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(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, "RTC_Test") == 0){ app.PlaySound(Lang::Sounds::P3_LALA_ZAINNE); } // 🔧 修复:使用改进的等待逻辑,确保音频真正播放完成 ESP_LOGI(TAG, "等待音频播放完成..."); vTaskDelay(pdMS_TO_TICKS(100)); // 给音频足够的时间开始播放 // 等待音频队列清空 + 额外缓冲时间确保I2S硬件完成输出 int timeout_count = 0; const int max_timeout = 150; // 3秒超时 while (timeout_count < max_timeout) { if (app.IsAudioQueueEmpty()) { // 队列清空后,再等待500ms确保I2S硬件完成输出 ESP_LOGI(TAG, "音频队列已清空,等待硬件输出完成..."); vTaskDelay(pdMS_TO_TICKS(500)); ESP_LOGI(TAG, "音频播放完成"); break; } vTaskDelay(pdMS_TO_TICKS(20)); timeout_count++; } if (timeout_count >= max_timeout) { ESP_LOGW(TAG, "等待音频播放超时,继续状态切换"); } } else { ESP_LOGW(TAG, "音频输出无法启用,跳过提示音播放"); } app.ToggleChatState(); // 切换到聆听状态 } else if (current_state == kDeviceStateListening) { // 如果当前是聆听状态,切换到待命状态 ESP_LOGI(TAG, "🔵 BOOT button pressed in Listening state - switching to idle"); ESP_LOGI(TAG, "从聆听状态切换到待命状态"); app.ToggleChatState(); // 切换到待命状态 } else if (current_state == kDeviceStateSpeaking) { // 如果当前是说话状态,终止说话并切换到待命状态 ESP_LOGI(TAG, "🔴 BOOT button pressed in Speaking state - initiating abort sequence"); ESP_LOGI(TAG, "从说话状态切换到聆听状态"); //app.AbortSpeakingAndReturnToIdle(); // 专门处理从说话状态到idle状态的转换 app.AbortSpeakingAndReturnToListening(); // 专门处理从说话状态到聆听状态的转换 } else if(current_state == kDeviceStateDialog) { // Application::GetInstance().ToggleChatState();// 切换对话状态 app.ToggleChatState(); // 切换对话状态 } else { // 其他状态下只唤醒设备 ESP_LOGI(TAG, "唤醒设备"); power_save_timer_->WakeUp(); } } }); // 配网模式下长按 BOOT 按键5秒进入 生产测试模式 新增代码 // ============================================================================== // 添加BOOT按键长按事件处理 - 仅在配网模式下长按5秒进入测试模式 boot_button_.OnLongPress([this]() { //ESP_LOGI(TAG, "🔧 BOOT button long pressed - checking if in provisioning mode"); // 检查是否处于BluFi配网状态,只有在配网模式下才允许进入测试模式 auto* wifi_board = dynamic_cast(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(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(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(arg); self->CheckBatteryStatus(); }, .arg = this, .dispatch_method = ESP_TIMER_TASK, .name = "battery_check_timer", .skip_unhandled_events = true, }; ESP_ERROR_CHECK(esp_timer_create(&timer_args, &battery_timer_handle_)); ESP_ERROR_CHECK(esp_timer_start_periodic(battery_timer_handle_, 1000000)); // 每秒检查一次 ESP_LOGI(TAG, "电池状态监控已初始化,GPIO:%d", BATTERY_ADC_GPIO);// 电池状态监控已初始化,GPIO:%d } // 初始化IMU传感器(QMI8658A 陀螺仪) void InitializeImuSensor() { auto& app = Application::GetInstance();// 获取当前应用状态 auto current_state = app.GetDeviceState();// 获取当前设备状态 // 在生产测试模式下或在对话状态下启用姿态传感器 if (!production_test_mode_ && current_state != kDeviceStateDialog) { ESP_LOGI(TAG, "非生产测试模式且不在对话状态,姿态传感器业务已禁用以节约资源"); imu_initialized_ = false;// 非生产测试模式且不在对话状态,姿态传感器业务已禁用以节约资源 imu_sensor_ = nullptr;// 姿态传感器实例指针 return; } const char* log_tag = production_test_mode_ ? Pro_TAG : TAG; if (current_state == kDeviceStateDialog) { ESP_LOGI(log_tag, "对话状态下启用姿态传感器"); } else { ESP_LOGI(log_tag, "生产测试模式下启用姿态传感器");// 生产测试模式下启用姿态传感器 } ESP_LOGI(log_tag, "初始化IMU传感器 QMI8658A...");// 初始化IMU传感器(QMI8658A 陀螺仪) // 初始化状态为false,确保系统在IMU不可用时仍能正常运行 imu_initialized_ = false;// 初始化状态为false,确保系统在IMU不可用时仍能正常运行 imu_sensor_ = nullptr;// 姿态传感器实例指针 if (!codec_i2c_bus_) { ESP_LOGI(log_tag, "I2C总线未初始化,IMU传感器将被禁用");// I2C总线未初始化,IMU传感器将被禁用 ESP_LOGI(log_tag, "系统将继续运行,不启用运动检测功能");// 系统将继续运行,不启用运动检测功能 return; } ESP_LOGI(log_tag, "I2C总线已初始化,创建IMU传感器实例");// I2C总线已初始化,创建IMU传感器实例 ESP_LOGI(log_tag, "使用I2C地址: 0x6A");// 使用I2C地址: 0x6A vTaskDelay(pdMS_TO_TICKS(100));// 添加延迟,确保I2C总线完全稳定 // 创建IMU传感器实例 (使用I2C地址0x6A) uint8_t working_address = 0x6A; try { imu_sensor_ = new QMI8658A(codec_i2c_bus_, 0x6A); ESP_LOGI(log_tag, "IMU传感器实例创建成功,I2C地址: 0x6A");// IMU传感器实例创建成功,I2C地址: 0x6A // 测试I2C通信 - 尝试读取芯片ID ESP_LOGI(log_tag, "测试I2C通信,读取QMI8658A芯片ID...");// 测试I2C通信,读取QMI8658A芯片ID... uint8_t chip_id = imu_sensor_->GetChipId(); ESP_LOGI(log_tag, "读取到的芯片ID: 0x%02X (预期: 0x05)", chip_id);// 读取到的芯片ID: 0x%02X (预期: 0x05) if (chip_id == 0xFF) { ESP_LOGI(log_tag, "I2C通信失败 - 读取到的芯片ID为0xFF");// I2C通信失败 - 读取到的芯片ID为0xFF ESP_LOGI(log_tag, "尝试备用I2C地址 0x6B...");// 尝试备用I2C地址 0x6B... // 尝试使用备用地址0x6B delete imu_sensor_; imu_sensor_ = new QMI8658A(codec_i2c_bus_, 0x6B); working_address = 0x6B; chip_id = imu_sensor_->GetChipId(); ESP_LOGI(log_tag, "读取到的芯片ID (0x6B): 0x%02X", chip_id);// 读取到的芯片ID (0x6B): 0x%02X if (chip_id == 0xFF) { ESP_LOGI(log_tag, "I2C通信失败 - 读取到的芯片ID (0x6B)为0xFF");// I2C通信失败 - 读取到的芯片ID (0x6B)为0xFF ESP_LOGI(log_tag, "可能原因:1) 硬件连接问题 2) 错误的I2C引脚 3) 电源供应问题");// 可能原因:1) 硬件连接问题 2) 错误的I2C引脚 3) 电源供应问题 delete imu_sensor_; imu_sensor_ = nullptr; return; } } if (chip_id != 0x05) { ESP_LOGI(log_tag, "读取到的芯片ID (0x%02X)与预期的0x05不符", chip_id);// 读取到的芯片ID (0x6A)与预期的0x05不符 ESP_LOGI(log_tag, "这可能不是QMI8658A传感器,或存在通信问题");// 这可能不是QMI8658A传感器,或存在通信问题 // 继续尝试初始化,可能是兼容的传感器 } ESP_LOGI(log_tag, "成功建立I2C通信,使用地址: 0x%02X", working_address);// 成功建立I2C通信,使用地址: 0x6A } catch (...) { ESP_LOGI(log_tag, "创建IMU传感器实例时发生异常");// 创建IMU传感器实例时发生异常 ESP_LOGI(log_tag, "系统将继续运行,不启用运动检测功能");// 系统将继续运行,不启用运动检测功能 imu_sensor_ = nullptr; return; } // 配置传感器参数 qmi8658a_config_t config = { .acc_range = QMI8658A_ACC_RANGE_4G, // 加速度计量程±4g .gyro_range = QMI8658A_GYRO_RANGE_512DPS, // 陀螺仪量程±512dps .acc_odr = QMI8658A_ODR_125HZ, // 加速度计采样率125Hz .gyro_odr = QMI8658A_ODR_125HZ, // 陀螺仪采样率125Hz .mode = QMI8658A_MODE_DUAL, // 同时启用加速度计和陀螺仪 .enable_interrupt = false, // 不启用中断 .interrupt_pin = 0, // 中断引脚 .auto_calibration = true, // 启用自动校准 .acc_offset = {0.0f, 0.0f, 0.0f}, // 加速度计偏移校准 .gyro_offset = {0.0f, 0.0f, 0.0f} // 陀螺仪偏移校准 }; ESP_LOGI(log_tag, "开始初始化IMU传感器...");// 开始初始化IMU传感器... // 初始化传感器 - 修复逻辑错误:QMI8658A_OK = 0 表示成功 qmi8658a_error_t init_result = imu_sensor_->Initialize(&config); if (init_result == QMI8658A_OK) { imu_initialized_ = true; ESP_LOGI(log_tag, "QMI8658A传感器初始化成功");// QMI8658A传感器初始化成功 if (config.auto_calibration) { qmi8658a_error_t calib_buf = imu_sensor_->StartBufferedReading(20); if (calib_buf == QMI8658A_OK) { imu_sensor_->StartCalibration(6000); bool running = false; float progress = 0.0f; do { imu_sensor_->GetCalibrationStatus(&running, &progress); vTaskDelay(pdMS_TO_TICKS(200)); } while (running); qmi8658a_calibration_t calib; imu_sensor_->GetCalibrationData(&calib); imu_sensor_->ApplyCalibration(&calib); imu_sensor_->StopBufferedReading(); } } // 创建IMU数据读取定时器 esp_timer_create_args_t timer_args = { .callback = [](void* arg) { MovecallMojiESP32S3* self = static_cast(arg); self->ReadImuData(); }, .arg = this, .dispatch_method = ESP_TIMER_TASK, .name = "imu_read_timer", .skip_unhandled_events = true, }; // 使用错误处理而不是ESP_ERROR_CHECK,避免系统崩溃 esp_err_t err = esp_timer_create(&timer_args, &imu_timer_handle_); if (err != ESP_OK) { ESP_LOGI(log_tag, "创建IMU定时器失败: %s", esp_err_to_name(err));// 创建IMU定时器失败: %s ESP_LOGI(log_tag, "IMU传感器将被禁用,系统继续正常运行");// IMU传感器将被禁用,系统继续正常运行 delete imu_sensor_; imu_sensor_ = nullptr; imu_initialized_ = false; return; } err = esp_timer_start_periodic(imu_timer_handle_, kImuReadInterval * 1000); if (err != ESP_OK) { ESP_LOGI(log_tag, "启动IMU定时器失败: %s", esp_err_to_name(err));// 启动IMU定时器失败: %s ESP_LOGI(log_tag, "IMU传感器将被禁用,系统继续正常运行");// IMU传感器将被禁用,系统继续正常运行 esp_timer_delete(imu_timer_handle_); imu_timer_handle_ = nullptr; delete imu_sensor_; imu_sensor_ = nullptr; imu_initialized_ = false; return; } ESP_LOGI(log_tag, "IMU数据读取定时器已启动,采样间隔: %dms", kImuReadInterval);// IMU数据读取定时器已启动,采样间隔: %dms } else { ESP_LOGI(log_tag, "QMI8658A传感器初始化失败,错误码: %d", init_result);// QMI8658A传感器初始化失败,错误码: %d ESP_LOGI(TAG, "可能原因:1) 硬件连接问题 2) 传感器未响应 3) I2C通信错误");// 可能原因:1) 硬件连接问题 2) 传感器未响应 3) I2C通信错误 ESP_LOGI(TAG, "系统将继续运行,不启用运动检测功能");// 系统将继续运行,不启用运动检测功能 // 清理资源 if (imu_sensor_) { delete imu_sensor_;// 清理IMU传感器实例 imu_sensor_ = nullptr;// 重置IMU传感器指针 } imu_initialized_ = false;// 重置IMU初始化状态 } } // 读取IMU数据的方法 void ReadImuData() { // ESP_LOGI(Pro_TAG, "读取IMU数据,是否初始化 =%d, 传感器指针 =%p", imu_initialized_, imu_sensor_);// 读取IMU数据,是否初始化:%d,传感器指针:%p if (!imu_initialized_ || !imu_sensor_) { ESP_LOGI(Pro_TAG, "IMU未初始化,跳过数据读取");// IMU未初始化,跳过数据读取 return; } // 实现重试机制,最多尝试3次读取 const int kMaxRetries = 3; const int kRetryDelayMs = 5; // 重试间隔5ms qmi8658a_error_t result = QMI8658A_ERROR_TIMEOUT; int retry_count = 0; do { // 读取传感器数据 // ESP_LOGI(Pro_TAG, "尝试读取IMU传感器数据(第%d次尝试)", retry_count + 1);// 尝试读取IMU传感器数据 result = imu_sensor_->ReadSensorData(&latest_imu_data_); if (result == QMI8658A_OK) { // ESP_LOGI(Pro_TAG, "成功读取IMU数据,正在处理...");// 成功读取IMU数据,正在处理... // 可以在这里添加数据处理逻辑 // 例如:检测姿态变化、计算角度等 ProcessImuData(&latest_imu_data_); return; // 读取成功,直接返回 } else if (result == QMI8658A_ERROR_DATA_NOT_READY && retry_count < kMaxRetries - 1) { // 数据未就绪但还有重试次数,等待后重试 // ESP_LOGI(Pro_TAG, "IMU数据未就绪,%dms后重试(剩余%d次)", kRetryDelayMs, kMaxRetries - retry_count - 1); esp_rom_delay_us(kRetryDelayMs * 1000); // 延时 retry_count++; } else { // 其他错误或重试次数用尽 ESP_LOGI(Pro_TAG, "读取IMU数据失败,错误码 = %d,已尝试%d次", result, retry_count + 1); break; } } while (retry_count < kMaxRetries); // 如果执行到这里,说明所有尝试都失败了 ESP_LOGI(Pro_TAG, "所有尝试都失败,放弃本次IMU数据读取");// 读取IMU数据失败,错误码 = %d } // 处理IMU数据的方法 void ProcessImuData(const qmi8658a_data_t* data) { // 处理IMU数据的逻辑 // 可以根据需要实现姿态检测、运动检测等功能 // 示例:检测是否有显著的加速度变化(可能表示设备被移动) float accel_magnitude = sqrt(data->acc_x * data->acc_x + data->acc_y * data->acc_y + data->acc_z * data->acc_z); // 如果加速度幅值超过阈值,可能需要唤醒设备或触发某些功能 const float MOTION_THRESHOLD = 1.5f; // g if (accel_magnitude > MOTION_THRESHOLD) { // 检测到运动,可以在这里添加相应的处理逻辑 // ESP_LOGI(TAG, "Motion detected: accel_magnitude = %.2f g", accel_magnitude);// 检测到运动:加速度幅值 = %.2f g ESP_LOGI(Pro_TAG, "检测到运动: 加速度幅值 = %.2f g", accel_magnitude);// 检测到运动:加速度幅值 = %.2f g } // 更新IMU IoT设备的数据 if (imu_thing_) { imu_thing_->UpdateData(*data); } // // 记录详细的传感器数据(调试用) // ESP_LOGI(TAG, "IMU Data - Accel: (%.2f, %.2f, %.2f) g, Gyro: (%.2f, %.2f, %.2f) dps, Temp: %.1f°C", // data->acc_x, data->acc_y, data->acc_z, // data->gyro_x, data->gyro_y, data->gyro_z, // data->temperature); // 记录详细的传感器数据(调试用) ESP_LOGI(Pro_TAG, "IMU Data - Accel: (%.2f, %.2f, %.2f) g, Gyro: (%.2f, %.2f, %.2f) dps, Temp: %.1f°C", data->acc_x, data->acc_y, data->acc_z, data->gyro_x, data->gyro_y, data->gyro_z, data->temperature); } // 检查电池状态的方法 void CheckBatteryStatus() { // 如果电池电量数据不足,则读取电池电量数据 if (adc_values_.size() < kBatteryAdcDataCount) { ReadBatteryAdcData(); return; } // 检查是否启用电量上报(启动3秒后) if (!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 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(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 == kDeviceStateDialog) { if (!app.IsDialogUploadEnabled()) {// 如果音频采集上传未启用 app.ToggleChatState();// 切换聊天状态 vTaskDelay(300 / portTICK_PERIOD_MS);// 等待300ms确保状态切换完成 } app.AbortSpeaking(kAbortReasonVoiceInterrupt);// 🔊 发送中止通话请求时,加锁保护RTC句柄 vTaskDelay(pdMS_TO_TICKS(120));// 等待120ms确保中止通话请求发送完成 // app.SendTextMessage("给我讲个小故事");// 发送“给我讲个小故事”消息 app.SendStoryRequest();// 发送故事请求 } // else if (current_state == kDeviceStateSpeaking) { // app.AbortSpeaking(kAbortReasonNone); // vTaskDelay(300 / portTICK_PERIOD_MS); // if (!app.IsDialogUploadEnabled()) { // app.ToggleChatState();// 切换聊天状态 // vTaskDelay(300 / portTICK_PERIOD_MS);// 等待300ms确保状态切换完成 // } // app.SendTextMessage("给我讲个小故事");// 发送“给我讲个小故事”消息 // } else { // app.ToggleChatState();// 切换聊天状态 // vTaskDelay(300 / portTICK_PERIOD_MS);// 等待300ms确保状态切换完成 // app.SendTextMessage("给我讲个小故事");// 发送“给我讲个小故事”消息 // } // 唤醒设备,防止立即进入睡眠 power_save_timer_->WakeUp(); }); ESP_LOGI(TAG, "故事按键已初始化,GPIO引脚 =%d", KEY4_GPIO);// 故事按键已初始化,GPIO引脚 = %d ESP_LOGI(TAG, "所有按键已成功初始化!");// 所有按键已成功初始化 } // 物联网初始化,添加音频设备和IMU传感器 void InitializeIot() { auto& thing_manager = iot::ThingManager::GetInstance();// 获取物联网管理器实例 thing_manager.AddThing(iot::CreateThing("Speaker")); // 添加音频设备到物联网管理器 // 创建并添加IMU传感器IoT设备 陀螺仪/姿态传感器业务 新增代码 // ============================================================================== const char* log_tag = production_test_mode_ ? Pro_TAG : TAG; if (imu_sensor_ && imu_initialized_) { imu_thing_ = new iot::ImuSensorThing(imu_sensor_); thing_manager.AddThing(imu_thing_); ESP_LOGI(log_tag, "IMU传感器已添加到IoT管理器");// IMU传感器已添加到IoT管理器 } else { ESP_LOGI(log_tag, "IMU传感器未初始化,跳过IoT注册");// IMU传感器未初始化,跳过IoT注册 } // ============================================================================== } // 唤醒词方案配置 void InitializeWakeWordSchemes() { ESP_LOGI(TAG, "Wake word schemes initialized"); } // 设置触摸阈值 void CalibrateTouchThresholds() { touch_pad_t touch_pads[4] = {TOUCH_PAD_NUM1, TOUCH_PAD_NUM2, TOUCH_PAD_NUM3, TOUCH_PAD_NUM7}; // 使用固定阈值5000 uint32_t default_threshold = 5000; // 设置为5000,降低灵敏度减少误触发 // 为每个触摸板设置阈值 for (int i = 0; i < 4; ++i) { // 先读取原始值作为参考 uint32_t touch_value = 0; esp_err_t ret = touch_pad_read_raw_data(touch_pads[i], &touch_value); if (ret == ESP_OK) { ESP_LOGI(TAG, "触摸板 %d 初始原始值: %" PRIu32, i, touch_value); // 根据实际读数动态调整阈值 if (touch_value > 8000) { // 未触摸状态,使用固定阈值而非动态计算 raw_touch_values_[i] = touch_value; // 存储当前值作为参考 touch_thresholds_[i] = default_threshold; // 使用固定的5000阈值 ESP_ERROR_CHECK(touch_pad_set_thresh(touch_pads[i], touch_thresholds_[i])); ESP_LOGI(TAG, "触摸板 %d 设置固定阈值: %" PRIu32, i, (uint32_t)touch_thresholds_[i]); } else { // 可能已经在触摸状态,使用固定阈值 raw_touch_values_[i] = 10000; // 假设一个高值 touch_thresholds_[i] = default_threshold; ESP_ERROR_CHECK(touch_pad_set_thresh(touch_pads[i], default_threshold)); ESP_LOGI(TAG, "触摸板 %d 设置固定阈值(触摸中): %" PRIu32, i, default_threshold); } } else { // 读取失败,使用默认值 raw_touch_values_[i] = 10000; touch_thresholds_[i] = default_threshold; ESP_ERROR_CHECK(touch_pad_set_thresh(touch_pads[i], default_threshold)); ESP_LOGI(TAG, "触摸板 %d 无法读取原始值,使用固定阈值: %" PRIu32, i, default_threshold); } } // 使用滤波器提高稳定性 ESP_LOGI(TAG, "启用触摸传感器滤波器"); touch_filter_config_t filter_info = { .mode = TOUCH_PAD_FILTER_IIR_16, // IIR滤波器,16采样 .debounce_cnt = 1, // 消抖计数 .noise_thr = 0, // 噪声阈值(不使用) .jitter_step = 4, // 抖动步长 .smh_lvl = TOUCH_PAD_SMOOTH_IIR_2 // 平滑级别 }; touch_pad_filter_set_config(&filter_info); touch_pad_filter_enable(); ESP_LOGI(TAG, "触摸阈值校准完成,使用固定阈值: %" PRIu32, default_threshold); } // 重置触摸状态的方法(可用于外部强制重置) void ResetTouchState(int touch_pad_num) { if (touch_pad_num >= 0 && touch_pad_num < 4) { touch_states_[touch_pad_num] = TOUCH_STATE_IDLE; ESP_LOGI(TAG, "触摸板 %d 状态已手动重置为空闲", touch_pad_num); } } public: // 触摸板初始化 void InitializeTouchPads() { ESP_LOGI(TAG, "初始化触摸板..."); // 初始化触摸模块 esp_err_t ret = touch_pad_init(); if (ret != ESP_OK) { ESP_LOGE(TAG, "触摸板初始化失败,错误代码: 0x%x", ret); return; } // 设置工作模式 touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); // 配置触摸传感器 ESP_LOGI(TAG, "配置触摸传感器..."); touch_pad_t touch_pads[4] = {TOUCH_PAD_NUM1, TOUCH_PAD_NUM2, TOUCH_PAD_NUM3, TOUCH_PAD_NUM7}; for (int i = 0; i < 4; ++i) { touch_pad_config(touch_pads[i]); } // 先读取基准值,然后设置阈值 ESP_LOGI(TAG, "校准触摸阈值..."); CalibrateTouchThresholds(); // 创建触摸事件队列 ESP_LOGI(TAG, "创建触摸事件队列..."); touch_event_queue_ = xQueueCreate(TOUCH_QUEUE_SIZE, sizeof(touch_event_data_t)); if (!touch_event_queue_) { ESP_LOGE(TAG, "创建触摸事件队列失败"); return; } // 注册触摸中断 ESP_LOGI(TAG, "注册触摸中断处理程序..."); // 仅启用按下中断,由触摸任务处理释放 touch_pad_isr_register(TouchPadISR, nullptr, TOUCH_PAD_INTR_MASK_ACTIVE); touch_pad_intr_enable(TOUCH_PAD_INTR_MASK_ACTIVE); // 创建处理触摸事件的任务 ESP_LOGI(TAG, "创建触摸事件任务..."); xTaskCreate(TouchEventTask, "touch_event_task", 4096, this, 10, NULL); // 确保所有触摸状态初始为空闲 ResetAllTouchStates(); // 开启触摸监控定时器,用于定期检查触摸状态是否正常 ESP_LOGI(TAG, "设置触摸监控..."); instance_ = this; touch_pad_fsm_start(); ESP_LOGI(TAG, "触摸板初始化完成"); } AudioCodec* GetAudioCodec() { // 使用延迟初始化模式,确保I2C总线和编解码器按正确顺序初始化 static AudioCodec* audio_codec = nullptr; static bool init_attempted = false; if (audio_codec == nullptr && !init_attempted) { init_attempted = true; // 标记为已尝试初始化 ESP_LOGI(TAG, "Initializing audio codec..."); // 确保I2C总线已初始化 if (codec_i2c_bus_ == nullptr) { ESP_LOGI(TAG, "Initializing I2C bus for audio codec..."); InitializeCodecI2c(); } if (codec_i2c_bus_ != nullptr) { try { ESP_LOGI(TAG, "Creating BoxAudioCodec (ES8311+ES7210, %s reference) ...", AUDIO_INPUT_REFERENCE ? "with" : "without"); audio_codec = new BoxAudioCodec( codec_i2c_bus_, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN, AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR, AUDIO_CODEC_ES7210_ADDR, AUDIO_INPUT_REFERENCE ); ESP_LOGI(TAG, "Audio codec initialized successfully"); } catch (const std::exception& e) { ESP_LOGE(TAG, "Exception during audio codec initialization: %s", e.what()); } catch (...) { ESP_LOGE(TAG, "Unknown exception during audio codec initialization"); } } else { ESP_LOGE(TAG, "Failed to initialize I2C bus for audio codec"); } } return audio_codec; } virtual Led* GetLed() override { static SingleLed led_strip(BUILTIN_LED_GPIO);// 初始化单灯条对象 return &led_strip; } virtual Display* GetDisplay() override { static Display display; // 空显示器对象,所有方法都是空实现 return &display; } virtual Backlight* GetBacklight() override { return nullptr; } // 获取IMU传感器数据 bool GetImuData(qmi8658a_data_t* data) { if (!imu_initialized_ || !imu_sensor_ || !data) { return false; } // 复制最新的IMU数据 *data = latest_imu_data_; return true; } // 检查IMU传感器是否已初始化 bool IsImuInitialized() const { return imu_initialized_; } // 获取生产测试模式状态 bool IsProductionTestMode() const { return production_test_mode_; } // 唤醒PowerSaveTimer,从低功耗模式恢复到正常模式 void WakeUp() override { if (power_save_timer_) { power_save_timer_->WakeUp(); ESP_LOGI(TAG, "🔋 PowerSaveTimer已唤醒,从低功耗模式恢复到正常模式"); } } }; void MovecallMojiESP32S3::ReportBatteryToServer(int battery_level) { ESP_LOGI(TAG, "📤 准备上报电量: %d%%", battery_level); // 获取当前已连接的Wi-Fi信号强度 int8_t rssi = -100; // 默认值,表示未连接或获取失败 auto* wifi_board = dynamic_cast(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(arg); vTaskDelay(1000 / portTICK_PERIOD_MS); if (board) { board->UnlockTouchTask(); } vTaskDelete(NULL); }, "unlock_touch", 4096, this, 5, NULL); } ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: PRESSED -> RELEASED", touch_pad_num, pad_names[touch_pad_num]); } break; case TOUCH_STATE_RELEASED: if (event_type == TOUCH_EVENT_PRESS) { // 如果释放后很快又被按下,可能是抖动,进入去抖状态 if (time_elapsed < DEBOUNCE_TIME_MS) { touch_states_[touch_pad_num] = TOUCH_STATE_DEBOUNCE; touch_last_time_[touch_pad_num] = current_time; ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: RELEASED -> DEBOUNCE (可能抖动)", touch_pad_num, pad_names[touch_pad_num]); } else { // 如果释放时间足够长,则认为是新的有效按下 touch_states_[touch_pad_num] = TOUCH_STATE_PRESSED; touch_last_time_[touch_pad_num] = current_time; // 触发一次新的消息 SendTouchMessage(touch_pad_num); power_save_timer_->WakeUp(); ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: RELEASED -> PRESSED (新的按下)", touch_pad_num, pad_names[touch_pad_num]); } } else if (time_elapsed > MIN_RELEASE_TIME_MS) { // 如果释放状态持续足够长,回到空闲状态 touch_states_[touch_pad_num] = TOUCH_STATE_IDLE; touch_last_time_[touch_pad_num] = current_time; ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: RELEASED -> IDLE (完全释放)", touch_pad_num, pad_names[touch_pad_num]); } break; case TOUCH_STATE_DEBOUNCE: if (event_type == TOUCH_EVENT_RELEASE) { // 去抖动完成,回到释放状态 touch_states_[touch_pad_num] = TOUCH_STATE_RELEASED; touch_last_time_[touch_pad_num] = current_time; ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: DEBOUNCE -> RELEASED", touch_pad_num, pad_names[touch_pad_num]); } else if (event_type == TOUCH_EVENT_PRESS && time_elapsed > DEBOUNCE_TIME_MS) { // 如果在去抖状态接收到新的按压且时间足够长,认为是有效按下 touch_states_[touch_pad_num] = TOUCH_STATE_PRESSED; touch_last_time_[touch_pad_num] = current_time; ESP_LOGI(TAG, "[状态] 触摸板 %d(%s) 状态: DEBOUNCE -> PRESSED (确认按下)", touch_pad_num, pad_names[touch_pad_num]); } break; } } // 添加一个新方法用于重置所有触摸状态 void MovecallMojiESP32S3::ResetAllTouchStates() { uint32_t current_time = esp_timer_get_time() / 1000; ESP_LOGI(TAG, "所有触摸状态已重置"); // 重置所有触摸点状态,简化日志输出 for (int i = 0; i < 4; i++) { touch_states_[i] = TOUCH_STATE_IDLE; touch_last_time_[i] = current_time; } // 清除触摸中断状态 touch_pad_clear_status(); } // 进入生产测试模式- 新增代码 // ============================================================================== void MovecallMojiESP32S3::EnterProductionTestMode() { if (production_test_mode_) { ESP_LOGI(TAG, "已经处于生产测试模式,忽略重复进入"); return; } production_test_mode_ = true; esp_log_level_set("*", ESP_LOG_INFO); esp_log_level_set("MovecallMojiESP32S3", ESP_LOG_INFO); esp_log_level_set("Airhub1", ESP_LOG_INFO); esp_log_level_set("AFE", ESP_LOG_ERROR); ESP_LOGI(Pro_TAG, "🔧 已进入生产测试模式,可以开始测试!");// 生产测试打印 auto& app = Application::GetInstance(); auto* codec = GetAudioCodec(); if (codec) { codec->EnableOutput(true); ESP_LOGI(TAG, "🔊 测试模式:已启用音频输出"); } app.PlaySound(Lang::Sounds::P3_TEST_MODAL); ESP_LOGI(TAG, "🎵 测试模式:开始播放进入测试模式音频"); ESP_LOGI(Pro_TAG, "🔧 生产测试模式:重新初始化IMU传感器"); InitializeImuSensor(); // 检查IMU传感器初始化状态 ESP_LOGI(Pro_TAG, "🔧 生产测试:IMU传感器初始化状态: %s", imu_initialized_ ? "成功" : "失败"); if (imu_initialized_ && imu_sensor_) { ESP_LOGI(Pro_TAG, "🔧 姿态传感器已初始化成功! 可以开始测试运动检测功能"); xTaskCreate( [](void* arg) { vTaskDelay(pdMS_TO_TICKS(1500)); auto& app_local = Application::GetInstance(); app_local.PlaySound(Lang::Sounds::P3_TUOLUOYI); ESP_LOGI(Pro_TAG, "🎵 播放陀螺仪检测成功音频"); vTaskDelete(NULL); }, "play_tuoluo_audio", 4096, nullptr, 5, nullptr ); } else { ESP_LOGI(Pro_TAG, "姿态传感器初始化失败或未连接!"); // 尝试再次检测连接 if (codec_i2c_bus_) { uint8_t detected_address = 0; bool sensor_connected = QMI8658A::CheckConnection(codec_i2c_bus_, &detected_address); if (sensor_connected) { ESP_LOGI(Pro_TAG, "🔧 姿态传感器物理连接存在! 设备地址:0x%02X,但初始化失败", detected_address); } } } // 非阻塞式触摸检测 - 触摸事件将通过现有的触摸处理机制来处理 ESP_LOGI(Pro_TAG, "🔧 生产测试模式已启用,触摸检测已激活,其他按键功能正常可用"); ESP_LOGI(Pro_TAG, "🔧 提示:现在可以测试触摸板、BOOT按键和讲故事按键"); } // ============================================================================== DECLARE_BOARD(MovecallMojiESP32S3);