From bffd31645ef8b19bf3e830eb71e6b2d805049b89 Mon Sep 17 00:00:00 2001 From: Rdzleo Date: Thu, 21 May 2026 14:42:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(provisioning):=20BLE=20=E9=85=8D=E7=BD=91?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E4=BF=AE=E5=A4=8D=20(=E8=B7=B3=E8=BF=87=20EA?= =?UTF-8?q?F=20=E8=B5=84=E6=BA=90=20+=20EAF=20=E6=9C=80=E5=B0=8F=E5=8C=96?= =?UTF-8?q?=20+=20=E9=9F=B3=E6=95=88=E6=92=AD=E6=94=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 4 个配网模式核心问题, 让 Rtc_AIavatar 分支 (含火山 RTC SDK + 软件 AEC + 完整业务) 能像 adaptation_dzbjImg_shar 一样正常配网, 同时显示居中提示文字. ============ 问题与修复 ============ ### 问题 1: 配网模式 BLE 广播 ADV_DATA malloc 失败 (手机搜不到设备) 日志: E (3731) BLE_INIT: Malloc failed E (3731) BT_HCI: CC evt: op=0x2008 (HCI_BLE_WRITE_ADV_DATA), status=0x7 I (3731) BluetoothProvisioning: ✅ 广播启动成功 (假成功, 广播数据空) 根因: Rtc_AIavatar 比 adaptation_dzbjImg_shar 多 ~50-80 KB DRAM 业务 .bss (软件 AEC + HTTPS 完整状态机 + dialog watchdog + 完整 RTC 状态), + 火山 RTC SDK 静态库 .bss ~30-50 KB (g_cnxMgr 14.6KB, ack$14 12.6KB 等), 配网模式时 BLE Bluedroid stack 抢不到广播数据 malloc 所需的 ~10KB DRAM. 修复 (前次 commit 已做): sdkconfig 关闭 BLE 5.0 6 个特性 (项目实际只用 4.2 legacy), 省 ~15 KB controller DRAM, 广播数据 malloc 成功. ### 问题 2: 配网模式下 LCD 绘制跟 WiFi/BLE 初始化抢 DRAM 导致 reboot 日志: E (1200) wifi:Expected to init 10 rx buffer, actual is 1 E (1220) BluetoothProvisioning: WiFi初始化失败: ESP_ERR_NO_MEM assert failed: vQueueDelete queue.c:2355 (pxQueue) ← BLE GATT fixed_queue_new 失败 → 反向清理 NULL 队列 排查路径 (失败方案记录): - esp_lcd_panel_draw_bitmap 一次画 360x360 (253KB): SPI queue 满, 下半屏未画 + DRAM 抢 WiFi - 分块画 (60 行/块) + vTaskDelay 块间: SPI driver 内部 queue 持续保留 DRAM, 仍然抢 - 强制 codec output_only=false 完整 duplex: 多 15KB DRAM, BLE BTU_StartUp malloc 失败 reboot - CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY=y: 引入 BTU bt_workqueue 分配失败 → vQueueDelete NULL → assert 修复 (本次 commit): EAF 最小化初始化 movecall_moji_esp32s3.cc 配网模式调用新增的 ai_chat_screen_init_provisioning(), 跳过 8 张 EAF 资源加载 (省 4.32 MB PSRAM + ~10KB DRAM), 跳过数字人 anim, 只启用 gfx_emote renderer + 单个 gfx_label, flush buffer 启动时预分配 (~30KB DRAM 一次到位), 跟 BLE 初始化不再有动态分配冲突. 跟 adaptation_dzbjImg_shar 用 LVGL 显示 GIF 同思路, 用 EAF 替代 LVGL 避免引入 50-80KB LVGL .bss. ### 问题 3: 配网模式音效不播放 根因: ResetWifiConfiguration 由 BOOT 按键 OnClick 调用, 跑在 esp_timer task 上下文, vTaskDelay(4000ms) 实测只等了 1.1 秒就被唤醒, 音效没播完就 esp_restart. 修复 (前次 commit 已做): 派发到独立 task 跑 PlaySound + vTaskDelay + esp_restart, 独立 task 中 vTaskDelay 正常工作, 等 4 秒确保 解码 + DMA + 功放尾音完整. ### 问题 4: 配网时屏幕黑屏 (UX 不友好) 实施: ai_chat_screen_init_provisioning("请使用APP\n蓝牙配网~") LCD 黑底白字居中显示提示文字, 用户感知"配网中". label height=64 (恰好包 2 行 + 余白), GFX_ALIGN_CENTER 上下左右居中. ============ 文件改动 ============ main/application.cc: Application 构造时显式注释: 不能在配网模式置 background_task_=nullptr (OnAudioOutput 无判空, 会 std::mutex::lock 异常 abort). main/boards/common/wifi_board.cc: ResetWifiConfiguration 派发独立 task 跑 PlaySound + 4s delay + esp_restart, EnterWifiConfigMode BLE 启动后早 return (StartBleProvisioning 内部已 Alert 音效). main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc: AI 对话模式分支: 配网时调 ai_chat_screen_init_provisioning() 显示文字, 正常模式调 ai_chat_screen_init() 显示数字人. main/dzbj/ai_chat_ui.h: 新增 ai_chat_screen_init_provisioning(const char* hint_text) 声明. main/dzbj/ai_chat_ui_eaf.c: 新增 ai_chat_screen_init_provisioning() 实现, EAF 最小化路径: gfx_emote_init + gfx_disp_add + 单 label 显示文字, 跳过 EAF 资源/anim/背景图. ============ 测试结果 (设备实测) ============ - 按 BOOT 触发配网: 听到完整配网音效 (P3_LALA_WIFICONFIG 约 1 秒) - 设备重启 → 配网模式启动 → LCD 显示"请使用APP\n蓝牙配网~" 居中 - 手机能搜到 Airhub_d0:cf:13:03:bb:f2 → 能连接 → 配网完成 - 配网完成重启 → 正常模式数字人 + RTC 对话功能正常 Co-Authored-By: Claude Opus 4.7 (1M context) --- main/application.cc | 2 + main/boards/common/wifi_board.cc | 8 +- .../movecall_moji_esp32s3.cc | 26 +++++- main/dzbj/ai_chat_ui.h | 7 ++ main/dzbj/ai_chat_ui_eaf.c | 89 +++++++++++++++++++ 5 files changed, 125 insertions(+), 7 deletions(-) diff --git a/main/application.cc b/main/application.cc index 90c481a..5b77a42 100644 --- a/main/application.cc +++ b/main/application.cc @@ -83,6 +83,8 @@ static const char* const STATE_STRINGS[] = { Application::Application() { event_group_ = xEventGroupCreate(); // 吧唧模式不需要后台任务(节省32KB栈内存) + // ⚠️ 不能在配网模式下置 nullptr: OnAudioOutput 无判空, 直接调 + // background_task_->Schedule() 会 std::mutex::lock 异常 → abort reboot loop #ifdef CONFIG_BAJI_BADGE_MODE if (!device_mode_is_badge()) { background_task_ = new BackgroundTask(4096 * 8); diff --git a/main/boards/common/wifi_board.cc b/main/boards/common/wifi_board.cc index 36b2422..85abb32 100644 --- a/main/boards/common/wifi_board.cc +++ b/main/boards/common/wifi_board.cc @@ -424,10 +424,10 @@ bool WifiBoard::StartBleProvisioning() { // 播放配网提示音 auto& application = Application::GetInstance(); if(strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0){ - application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "happy", Lang::Sounds::P3_KAKA_WIFICONFIG); + application.Alert("BLE配网模式", "请使用手机APP搜索设备连接WI-FI", "happy", Lang::Sounds::P3_KAKA_WIFICONFIG); } else if(strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0){ - application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "happy", Lang::Sounds::P3_LALA_WIFICONFIG); + application.Alert("BLE配网模式", "请使用手机APP搜索设备连接WI-FI", "happy", Lang::Sounds::P3_LALA_WIFICONFIG); } @@ -468,9 +468,9 @@ bool WifiBoard::StartBleProvisioning() { // } // auto& application = Application::GetInstance(); // if (strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0) { -// application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "", Lang::Sounds::P3_KAKA_WIFICONFIG); +// application.Alert("BLE配网模式", "请使用手机APP搜索设备连接WI-FI", "", Lang::Sounds::P3_KAKA_WIFICONFIG); // } else if (strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0) { -// application.Alert("BLE配网模式", "请使用手机APP搜索Airhub_开头的蓝牙设备", "", Lang::Sounds::P3_LALA_WIFICONFIG); +// application.Alert("BLE配网模式", "请使用手机APP搜索设备连接WI-FI", "", Lang::Sounds::P3_LALA_WIFICONFIG); // } // while (true) { // int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); diff --git a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc index bad7a1b..0196a7f 100644 --- a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc +++ b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc @@ -16,9 +16,14 @@ #include "settings.h" #include "dzbj/dzbj_init.h" // dzbj 显示模块初始化(公共: dzbj_hw_display_init;吧唧专用 dzbj_display_init 在头内 #ifdef 包裹) #include "display/ai_chat_display.h" // AI 对话模式专用显示 +#include "dzbj/fatfs.h" // SPIFFS DecodeImg (配网模式背景图也用) +#include "esp_spiffs.h" // 配网模式 SPIFFS 挂载 +#include "esp_lcd_panel_ops.h" // esp_lcd_panel_draw_bitmap (配网模式直接画 bitmap) +// ⚠️ 不 include "dzbj/lcd.h" 因为它会 include esp_lvgl_port.h → 触发 lv_font_t 跟 display.h 冲突 +// panel_handle 是 lcd.c 暴露的全局 C 变量, 直接 extern 声明: +extern "C" esp_lcd_panel_handle_t panel_handle; #ifdef CONFIG_BAJI_BADGE_MODE #include "dzbj/device_mode.h" // 设备模式管理(AI/吧唧) -#include "dzbj/fatfs.h" // SPIFFS 文件系统 #include "dzbj/dzbj_ble.h" // BLE 图传服务 #include "dzbj/dzbj_battery.h" // 电池监测 #include "dzbj/dzbj_button.h" // 按键驱动 @@ -54,6 +59,8 @@ extern "C" void init_spiffs_image_list(void); // AI 对话屏幕初始化(纯 C,避免 lv_font_t 冲突) extern "C" void ai_chat_screen_init(void); extern "C" void ai_chat_resume_animation(void); +// 配网模式专用最小化初始化 (只显示文字, 不加载 EAF 资源 / 不创建 anim) +extern "C" void ai_chat_screen_init_provisioning(const char* hint_text); // 背光初始化(pages_pwm.h 包含 LVGL 头文件,不能直接 include) extern "C" void pwm_init(void); @@ -248,8 +255,21 @@ public: // ===== AI 对话模式 ===== // 仅硬件+LVGL 初始化(不加载 SquareLine UI) dzbj_hw_display_init(codec_i2c_bus_); - // 加载 AI 对话专用屏幕 - ai_chat_screen_init(); + // 配网模式跳过 EAF 数字人初始化, 节省 ~30 KB DRAM + 4.32 MB PSRAM, + // 让 BLE Bluedroid stack 有足够 DRAM 完成 advertising data malloc. + // 配网期间用户操作只需要听音效 + 看简单提示, 不需要数字人界面. + if (WifiBoard::NeedsProvisioning()) { + // 配网模式: EAF 最小化初始化 (仅显示文字, 不加载 8 张 EAF 资源 + 不创建 anim) + // 省 4.32 MB PSRAM (EAF 资源) + 数字人 anim 的 DRAM 占用 + // 保留 ~30 KB DRAM 给 gfx flush buffer (启动时预分配, 不抢 BLE 初始化) + // 显示"请使用APP\n蓝牙配网~"提示 + // 跟 adaptation_dzbjImg_shar (用 LVGL 显示 GIF + 配网) 同思路, 用 EAF 替代 LVGL + ESP_LOGI(TAG, "🔵 配网模式: EAF 最小化初始化, 显示配网提示文字"); + ai_chat_screen_init_provisioning("请使用APP\n蓝牙配网~"); + } else { + // 加载 AI 对话专用屏幕 + ai_chat_screen_init(); + } vTaskDelay(pdMS_TO_TICKS(100)); // 等待首帧渲染 pwm_init(); // 点亮背光 ESP_LOGI(TAG, "🤖 AI对话模式启动"); diff --git a/main/dzbj/ai_chat_ui.h b/main/dzbj/ai_chat_ui.h index 6a1eba8..0fdd30a 100644 --- a/main/dzbj/ai_chat_ui.h +++ b/main/dzbj/ai_chat_ui.h @@ -8,6 +8,13 @@ extern "C" { // 创建并加载 AI 对话屏幕 void ai_chat_screen_init(void); +// 配网模式专用 - 最小化 EAF 初始化, 只显示文字提示 +// 不加载 8 张 EAF 资源 (省 4.32 MB PSRAM) +// 不创建数字人 anim (省 DRAM) +// 不加载背景图 (省 DRAM + SPI buffer) +// 只启用 gfx renderer + label, 显示静态文字 +void ai_chat_screen_init_provisioning(const char* hint_text); + // 更新状态文本(如 "Listening...", "Speaking...") void ai_chat_set_status(const char* status); diff --git a/main/dzbj/ai_chat_ui_eaf.c b/main/dzbj/ai_chat_ui_eaf.c index 9ce5fb4..542a609 100644 --- a/main/dzbj/ai_chat_ui_eaf.c +++ b/main/dzbj/ai_chat_ui_eaf.c @@ -541,3 +541,92 @@ void ai_chat_resume_animation(void) { // EAF 动画由 gfx_anim_start 持续播放,无需手动 resume ESP_LOGD(TAG, "resume_animation(EAF 模式下自动循环,无需操作)"); } + +// ========================================================== +// 配网模式专用 - 最小化 EAF 初始化, 只显示文字提示 +// 设计目的: 配网模式下 DRAM 紧张 (RTC SDK + 应用 .bss 占 30-50KB), +// 不能完整初始化 EAF (加载 4.32 MB EAF 资源 + 数字人 anim). +// 只启用 gfx renderer + 单个 label, 在启动早期预分配 flush buffer, +// 避免跟后续 BLE Bluedroid 初始化抢动态分配的 DRAM. +// 跟 adaptation_dzbjImg_shar (用 LVGL 显示 GIF) 同思路, 用 EAF 替代 LVGL 省 .bss +// ========================================================== +void ai_chat_screen_init_provisioning(const char* hint_text) { + if (s_initialized) { + ESP_LOGW(TAG, "[配网] EAF 已初始化, 跳过"); + return; + } + ESP_LOGI(TAG, "============================"); + ESP_LOGI(TAG, "=== EAF 最小化 (配网模式) ==="); + ESP_LOGI(TAG, "============================"); + + // 1. 初始化 gfx 核心 (Core 0, 跟原版一致, 不抢音频 Core 1) + gfx_core_config_t gfx_cfg = { + .fps = 25, + .task = GFX_EMOTE_INIT_CONFIG(), + }; + gfx_cfg.task.task_priority = 4; + gfx_cfg.task.task_affinity = 0; + gfx_cfg.task.task_stack = 8 * 1024; + s_emote_handle = gfx_emote_init(&gfx_cfg); + if (!s_emote_handle) { + ESP_LOGE(TAG, "[配网] gfx_emote_init 失败"); + return; + } + + // 2. 添加 display (接管 panel_handle, 启动时预分配 flush buffer ~30KB DRAM) + gfx_disp_config_t disp_cfg = { + .h_res = LCD_W, + .v_res = LCD_H, + .flush_cb = eaf_disp_flush_cb, + .update_cb = NULL, + .user_data = (void *)panel_handle, + .flags = { + .swap = true, + .buff_dma = true, + .buff_spiram = false, + .double_buffer = true, + }, + .buffers = { .buf1 = NULL, .buf2 = NULL, .buf_pixels = LCD_W * 20 }, + }; + s_disp = gfx_disp_add(s_emote_handle, &disp_cfg); + if (!s_disp) { + ESP_LOGE(TAG, "[配网] gfx_disp_add 失败"); + gfx_emote_deinit(s_emote_handle); + s_emote_handle = NULL; + return; + } + + const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = eaf_flush_io_ready }; + esp_lcd_panel_io_register_event_callbacks(lcd_io_handle, &cbs, s_disp); + + // 3. 设置背景色 = 黑色 + gfx_disp_set_bg_color(s_disp, GFX_COLOR_HEX(0x000000)); + + s_initialized = true; + + // 4. 创建文字 label - 不加载 EAF 资源 + 不创建 anim + // 上下/左右居中: label height 恰好包文字 (避免 gfx_label 内部顶对齐导致视觉偏上), + // 配合 GFX_ALIGN_CENTER 整体居中到屏幕中央 + gfx_emote_lock(s_emote_handle); + s_chat_label = gfx_label_create(s_disp); + if (s_chat_label) { + gfx_label_set_font(s_chat_label, (gfx_font_t)&font_puhui_20_4); + // 字体 20px + line_spacing 8 = 28px/行, 2 行文字 = 56px, 留 8px 余白 = 64px + gfx_obj_set_size(s_chat_label, 300, 64); + gfx_label_set_long_mode(s_chat_label, GFX_LABEL_LONG_WRAP); + gfx_label_set_text_align(s_chat_label, GFX_TEXT_ALIGN_CENTER); // 文字水平居中 + gfx_label_set_color(s_chat_label, GFX_COLOR_HEX(0xFFFFFF)); // 白字 (黑底背景) + gfx_label_set_bg_enable(s_chat_label, false); + gfx_label_set_line_spacing(s_chat_label, 8); + // label 整体居中到屏幕正中 → 文字视觉上上下左右居中 + gfx_obj_align(s_chat_label, GFX_ALIGN_CENTER, 0, 0); + gfx_label_set_text(s_chat_label, hint_text ? hint_text : "请使用APP\n蓝牙配网~"); + gfx_obj_set_visible(s_chat_label, true); + ESP_LOGI(TAG, "[配网] 文字 label 创建成功 (居中显示)"); + } else { + ESP_LOGE(TAG, "[配网] gfx_label_create 失败"); + } + gfx_emote_unlock(s_emote_handle); + + ESP_LOGI(TAG, "=== EAF 最小化初始化完成 ==="); +}