feat: 完成 AI/吧唧双模式完全隔离重构 + 触摸坐标日志 + SPIFFS 预烧录

## 核心变更

### 1. 双模式完全隔离 (Phase 2+4)
- 拆分 InitializeButtons() 为 InitializeBadgeModeButtons() + InitializeAiModeButtons()
- 构造函数按 device_mode 分支:吧唧模式不创建 PowerSaveTimer/BackgroundTask
- 吧唧模式不注册音量/故事按键回调,避免调用 GetAudioCodec() 崩溃
- GPIO0 由 iot_button 统一处理,dzbj_button 仅注册 KEY2(GPIO4)
- SetDeviceState() 中 background_task_ 空指针保护

### 2. 吧唧模式 BOOT 按键崩溃修复
- 新增 dzbj_boot_click_handler()(C 函数,避免 lvgl.h 与 display.h 冲突)
- 移植 dzbj 的唤醒屏幕/退出手电筒/返回Home 完整逻辑

### 3. esp_timer 阻塞 LVGL 渲染修复
- iot_button 回调在 esp_timer 任务中执行,vTaskDelay 会阻塞 lv_tick_inc
- 改为 xTaskCreate 派发到独立 FreeRTOS 任务,避免冻结 LVGL 渲染

### 4. 触摸坐标日志 + SPIFFS 预烧录
- esp_lvgl_port_touch.c 添加触摸坐标打印
- CMakeLists.txt 添加 spiffs_create_partition_image 自动打包 spiffs_image/

### 5. dzbj 模块文件新增
- device_mode: NVS 设备模式管理 (AI=0/吧唧=1)
- dzbj_button: GPIO4 KEY2 中断 + BOOT 点击处理
- dzbj_ble: BLE GATT 图传服务 (0x0B00)
- dzbj_battery: ADC 电池电压监测
- sleep_mgr: 10s 超时熄屏低功耗管理
- pages: 图片浏览/GIF播放/PWM亮度
- fatfs: SPIFFS 文件管理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rdzleo 2026-02-28 10:23:04 +08:00
parent bcfd35b9b8
commit 14776acb0a
27 changed files with 2999 additions and 864 deletions

View File

@ -1,207 +1,44 @@
rdzleo@RdzleodeMac-Studio Baji_Rtc_Toy % '/Users/rdzleo/.espressif/python_env/idf5.4_py3.13_env/bin/python3' '/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf/t I (49) WeatherApi: 初始化天气API配置 - 默认城市: 北京
ools/idf_monitor.py' -p /dev/tty.usbmodem834401 -b 115200 --toolchain-prefix xtensa-esp32s3-elf- --make ''/Users/rdzleo/.espressif/python_env/idf5.4_py3 I (49) WeatherApi: WiFi位置缓存限制已设置为: 5 条
.13_env/bin/python3' '/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf/tools/idf.py'' --target esp32s3 '/Users/rdzleo/Desktop/Baji_Rtc_Toy/build/kapi.elf' I (50) coexist: coex firmware version: 831ec70
--- Warning: Serial ports accessed as /dev/tty.* will hang gdb if launched. I (50) coexist: coexist rom version e7ae62f
--- Using /dev/cu.usbmodem834401 instead... I (51) main_task: Started on CPU0
--- esp-idf-monitor 1.8.0 on /dev/cu.usbmodem834401 115200 I (61) main_task: Calling app_main()
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H I (81) Application: 🎴 吧唧模式:跳过 WiFi/协议/音频初始化
I (81) Application: 打印设置设备状态日志: idle
abort() was called at PC 0x421cd333 on core 0
.--- 0x421cd333: __cxxabiv1::__terminate(void (*)()) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:48
Backtrace: 0x40379f49:0x3fcbcbe0 0x4038731d:0x3fcbcc00 0x4038f9d9:0x3fcbcc20 0x421cd333:0x3fcbcc90 0x421cd368:0x3fcbccb0 0x421cd443:0x3fcbccd0 0x421ddb09:0x3fcbccf0 0x42013a05:0x3fcbcd30 0x4201bf6f:0x3fcbcd50 0x420174b6:0x3fcbcd80 0x42018c69:0x3fcbcde0 0x4202069d:0x3fcbd370 0x42229453:0x3fcbd390 0x40387e11:0x3fcbd3c0
--- 0x40379f49: panic_abort at /Users/rdzleo/esp/esp-idf/components/esp_system/panic.c:469
--- 0x4038731d: esp_system_abort at /Users/rdzleo/esp/esp-idf/components/esp_system/port/esp_system_chip.c:87
--- 0x4038f9d9: abort at /Users/rdzleo/esp/esp-idf/components/newlib/abort.c:38
--- 0x421cd333: __cxxabiv1::__terminate(void (*)()) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:48
--- 0x421cd368: std::terminate() at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:58
--- 0x421cd443: __cxa_throw at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_throw.cc:98
--- 0x421ddb09: std::__throw_system_error(int) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/src/c++11/system_error.cc:595
--- 0x42013a05: std::unique_lock<std::mutex>::lock() at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:142
--- 0x4201bf6f: std::unique_lock<std::mutex>::unique_lock(std::mutex&) at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:73
--- (inlined by) BackgroundTask::WaitForCompletion() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/background_task.cc:46
--- 0x420174b6: Application::SetDeviceState(DeviceState) at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:2262
--- 0x42018c69: Application::Start() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:533
--- 0x4202069d: app_main at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/main.cc:105
--- 0x42229453: main_task at /Users/rdzleo/esp/esp-idf/components/freertos/app_startup.c:208
--- 0x40387e11: vPortTaskWrapper at /Users/rdzleo/esp/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/port.c:139
ELF file SHA256: 0061c1350
Rebooting...
ESP-ROM:esp32s3-20210327 ESP-ROM:esp32s3-20210327
Build:Mar 27 2021 Build:Mar 27 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0xb (SPI_FAST_FLASH_BOOT) rst:0xc (RTC_SW_CPU_RST),boot:0x2b (SPI_FAST_FLASH_BOOT)
Saved PC:0x42143b2f Saved PC:0x40379e89
--- 0x42143b2f: timer_process_alarm at /Users/rdzleo/esp/esp-idf/components/esp_timer/src/esp_timer.c:413 --- 0x40379e89: esp_restart_noos at /Users/rdzleo/esp/esp-idf/components/esp_system/port/soc/esp32s3/system_internal.c:162
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce2820,len:0x56c
load:0x403c8700,len:0x4
load:0x403c8704,len:0xb88
load:0x403cb700,len:0x2df4
entry 0x403c88f4
I (50) WeatherApi: 初始化天气API配置 - 默认城市: 北京
I (50) WeatherApi: WiFi位置缓存限制已设置为: 5 条
I (51) coexist: coex firmware version: 831ec70
I (51) coexist: coexist rom version e7ae62f
I (52) main_task: Started on CPU0
I (62) main_task: Calling app_main()
I (82) BackgroundTask: background_task started
I (82) BluetoothProvisioning: 蓝牙配网对象创建完成
I (82) button: IoT Button Version: 3.5.0
I (82) gpio: GPIO[0]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (82) button: IoT Button Version: 3.5.0
I (82) gpio: GPIO[4]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (82) Airhub1: 初始化按钮...
I (82) Airhub1: Boot button initialized on GPIO0
I (82) Airhub1: Volume up button initialized on GPIO-1
I (82) Airhub1: Volume down button initialized on GPIO-1
I (82) Airhub1: 故事按键已初始化GPIO引脚 =4
I (82) Airhub1: 所有按键已成功初始化!
I (82) Airhub1: Initializing I2C master bus for audio codec...
I (82) Airhub1: Scanning I2C bus for devices...
I (82) Airhub1: I2C设备在线: 0x18
I (82) Airhub1: I2C设备在线: 0x40
I (82) Airhub1: I2C scan completed. Found 2 devices
I (82) DZBJ: 开始初始化 dzbj 显示模块...
I (82) gpio: GPIO[7]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (82) st77916: LCD panel create success, version: 1.0.1
W (212) st77916: The 3Ah command has been used and will be overwritten by external initialization sequence
I (332) LCD: LCD GRAM cleared (black filled)
I (332) DZBJ: LCD 硬件初始化完成
I (332) gpio: GPIO[5]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:2
I (332) gpio: GPIO[6]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (732) CST816S: IC id: 182
I (732) LCD: Touch controller initialized successfully
I (732) LVGL: Starting LVGL task
I (732) LCD: LVGL buffer: 14400 bytes (W:360, Lines:20, DMA, single)
I (732) LCD: Touch controller added to LVGL
I (732) DZBJ: LVGL 初始化完成
I (742) DZBJ: UI 初始化完成
I (842) DZBJ: 背光已点亮dzbj 显示模块初始化完成
I (842) Airhub1: IMU传感器未初始化跳过IoT注册
I (842) Airhub1: Initializing battery monitor...
I (842) Airhub1: ADC calibration initialized
I (842) Airhub1: 电池状态监控已初始化GPIO:3
I (842) Airhub1: 非生产测试模式且不在对话状态,姿态传感器业务已禁用以节约资源
I (842) PowerSaveTimer: Power save timer enabled
I (842) Airhub1: 🔋 PowerSaveTimer已启用20秒无活动将进入低功耗模式
I (842) Airhub1: 电容触摸板按钮已禁用 (ENABLE_TOUCH_PAD_BUTTONS=0)
I (842) Application: 打印设置设备状态日志: starting
I (842) Application: 正常启动流程,将执行开机播报和网络连接播报
I (842) Airhub1: Initializing audio codec (duplex)...
I (842) Airhub1: Creating BoxAudioCodec (ES8311+ES7210, without reference) ...
I (842) BoxAudioCodec: Duplex channels created
I (852) ES8311: Work in Slave mode
I (852) gpio: GPIO[48]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (852) ES7210: Work in Slave mode
I (862) ES7210: Enable ES7210_INPUT_MIC1
I (862) ES7210: Enable ES7210_INPUT_MIC2
I (872) BoxAudioCodec: BoxAudioDevice initialized (duplex)
I (872) Airhub1: Audio codec initialized successfully
I (872) Application: 检测到WiFi板卡将opus编码器复杂度设置为3
I (872) OpusResampler: Resampler configured with input sample rate 16000, output sample rate 8000, and channels 1
I (872) I2S_IF: channel mode 2 bits:16/16 channel:2 mask:1
I (872) I2S_IF: TDM Mode 0 bits:16/16 channel:2 sample_rate:16000 mask:1
I (872) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1
I (872) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1
I (872) ES7210: Bits 16
I (882) ES7210: Enable ES7210_INPUT_MIC1
I (882) ES7210: Enable ES7210_INPUT_MIC2
I (892) ES7210: Unmuted
I (892) Adev_Codec: Open codec device OK
I (892) BoxAudioCodec: Input opened: sr=16000 ch=1 mask=0x1 ref=0
I (892) AudioCodec: Set input enable to true
I (892) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1
I (892) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1
I (912) Adev_Codec: Open codec device OK
I (912) AudioCodec: Set output enable to true
I (912) AudioCodec: Audio codec started
I (1012) Airhub1: ADC: 2370, 原始电压: 2.37V, 计算电池电压: 10.29V, 电量: 100%, 满电电压: 4.20V
I (1012) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1112) Airhub1: ADC: 2368, 原始电压: 2.37V, 计算电池电压: 10.28V, 电量: 100%, 满电电压: 4.20V
I (1112) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1212) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1212) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1322) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1322) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1422) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1422) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1522) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1522) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1622) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1622) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1722) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1722) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1822) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1822) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1922) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1922) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (1942) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (1942) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2022) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2022) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2122) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2122) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2222) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2222) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2322) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2322) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2422) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2422) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2522) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2522) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2622) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2622) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2722) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2722) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2822) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2822) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2922) Airhub1: ADC: 2367, 原始电压: 2.37V, 计算电池电压: 10.27V, 电量: 100%, 满电电压: 4.20V
I (2922) BluetoothMAC: Bluetooth MAC Address: d0:cf:13:03:bb:f2
I (2922) AudioCodec: 将运行时输出音量设置为80
I (2922) Application: 设备启动完成,播放开机播报语音
I (2922) pp: pp rom version: e7ae62f
I (2922) net80211: net80211 rom version: e7ae62f
I (2932) wifi:wifi driver task: 3fce5690, prio:23, stack:6656, core=0
I (2932) wifi:wifi firmware version: 3263cda
I (2932) wifi:wifi certification version: v7.0
I (2932) wifi:config NVS flash: disabled
I (2932) wifi:config nano formatting: disabled
I (2932) wifi:Init data frame dynamic rx buffer num: 32
I (2932) wifi:Init dynamic rx mgmt buffer num: 5
I (2932) wifi:Init management short buffer num: 32
I (2932) wifi:Init static tx buffer num: 8
I (2932) wifi:Init tx cache buffer num: 32
I (2932) wifi:Init static tx FG buffer num: 2
I (2932) wifi:Init static rx buffer size: 1600
I (2932) wifi:Init static rx buffer num: 10
I (2932) wifi:Init dynamic rx buffer num: 32
I (2932) wifi_init: rx ba win: 16
I (2932) wifi_init: accept mbox: 6
I (2932) wifi_init: tcpip mbox: 32
I (2932) wifi_init: udp mbox: 6
I (2932) wifi_init: tcp mbox: 6
I (2932) wifi_init: tcp tx win: 5760
I (2932) wifi_init: tcp rx win: 5760
I (2932) wifi_init: tcp mss: 1440
I (2932) wifi_init: WiFi/LWIP prefer SPIRAM
I (2932) phy_init: phy_version 701,f4f1da3a,Mar 3 2025,15:50:10
I (2972) phy_init: Saving new calibration data due to checksum failure or outdated calibration data, mode(0)
I (2972) Application: 开始播放下行音频: 样本=960 采样率=16000
I (3022) wifi:mode : sta (d0:cf:13:03:bb:f0)
I (3022) wifi:enable tsf
I (5432) wifi: 发现可连接 AP: airhub, BSSID: 70:2a:d7:85:bc:eb, RSSI: -36, Channel: 1, Authmode: 3
I (5432) WifiBoard: Starting WiFi connection, playing network connection sound
W (5432) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2
I (5522) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1, snd_ch_cfg:0x0
I (5522) wifi:state: init -> auth (0xb0)
I (5532) wifi:state: auth -> assoc (0x0)
I (5542) wifi:state: assoc -> run (0x10)
I (5582) wifi:connected with airhub, aid = 3, channel 1, BW20, bssid = 70:2a:d7:85:bc:eb
I (5582) wifi:security: WPA2-PSK, phy: bgn, rssi: -38
I (5582) wifi:pm start, type: 1
I (5582) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us
I (5582) wifi:set rx beacon pti, rx_bcn_pti: 14, bcn_timeout: 25000, mt_pti: 14, mt_time: 10000
I (5682) wifi:AP's beacon interval = 102400 us, DTIM period = 1
I (5842) Airhub1: 📤 设备状态上报已启用每30秒上报一次
I (6992) Airhub1: BOOT button clicked
I (6992) Airhub1: 🔄 BOOT按键触发设备状态=1WiFi连接状态=未连接
I (6992) Airhub1: 🔄 开始重置WiFi配置清除已保存的WiFi凭据
I (6992) wifi:state: run -> init (0x0)
I (6992) wifi:pm stop, total sleep time: 1072363 us / 1413544 us
I (6992) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1, snd_ch_cfg:0x0
I (6992) wifi: Reconnecting airhub (attempt 1 / 5)
I (7102) wifi:flush txq
I (7102) wifi:stop sw txq
I (7102) wifi:lmac stop hw txq
I (7102) Airhub1: ✅ 已清除所有WiFi凭据设备将进入配网模式
I (7102) WifiBoard: 🔄 重置WiFi配置设备将重启进入配网模式
I (7612) WifiBoard: 🔄 正在重启设备...
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0xc (RTC_SW_CPU_RST),boot:0xb (SPI_FAST_FLASH_BOOT)
Saved PC:0x40379e85
--- 0x40379e85: esp_restart_noos at /Users/rdzleo/esp/esp-idf/components/esp_system/port/soc/esp32s3/system_internal.c:162
SPIWP:0xee SPIWP:0xee
mode:DIO, clock div:1 mode:DIO, clock div:1
load:0x3fce2820,len:0x56c load:0x3fce2820,len:0x56c
@ -210,307 +47,38 @@ load:0x403c8704,len:0xb88
load:0x403cb700,len:0x2df4 load:0x403cb700,len:0x2df4
entry 0x403c88f4 entry 0x403c88f4
I (49) WeatherApi: 初始化天气API配置 - 默认城市: 北京 I (49) WeatherApi: 初始化天气API配置 - 默认城市: 北京
I (50) WeatherApi: WiFi位置缓存限制已设置为: 5 条 I (49) WeatherApi: WiFi位置缓存限制已设置为: 5 条
I (50) coexist: coex firmware version: 831ec70 I (50) coexist: coex firmware version: 831ec70
I (51) coexist: coexist rom version e7ae62f I (50) coexist: coexist rom version e7ae62f
I (51) main_task: Started on CPU0 I (51) main_task: Started on CPU0
I (61) main_task: Calling app_main() I (61) main_task: Calling app_main()
I (81) BackgroundTask: background_task started I (81) Application: 🎴 吧唧模式:跳过 WiFi/协议/音频初始化
I (81) BluetoothProvisioning: 蓝牙配网对象创建完成 I (81) Application: 打印设置设备状态日志: idle
I (81) WifiBoard: force_ap is set to 1, will clear in StartNetwork()
I (81) button: IoT Button Version: 3.5.0
I (81) gpio: GPIO[0]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (81) button: IoT Button Version: 3.5.0
I (81) gpio: GPIO[4]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (81) Airhub1: 初始化按钮...
I (81) Airhub1: Boot button initialized on GPIO0
I (81) Airhub1: Volume up button initialized on GPIO-1
I (81) Airhub1: Volume down button initialized on GPIO-1
I (81) Airhub1: 故事按键已初始化GPIO引脚 =4
I (81) Airhub1: 所有按键已成功初始化!
I (81) Airhub1: Initializing I2C master bus for audio codec...
I (81) Airhub1: Scanning I2C bus for devices...
I (81) Airhub1: I2C设备在线: 0x18
I (81) Airhub1: I2C设备在线: 0x40
I (81) Airhub1: I2C scan completed. Found 2 devices
I (81) DZBJ: 开始初始化 dzbj 显示模块...
I (81) gpio: GPIO[7]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (81) st77916: LCD panel create success, version: 1.0.1
W (211) st77916: The 3Ah command has been used and will be overwritten by external initialization sequence
I (331) LCD: LCD GRAM cleared (black filled)
I (331) DZBJ: LCD 硬件初始化完成
I (331) gpio: GPIO[5]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:2
I (331) gpio: GPIO[6]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (731) CST816S: IC id: 182
I (731) LCD: Touch controller initialized successfully
I (731) LVGL: Starting LVGL task
I (731) LCD: LVGL buffer: 14400 bytes (W:360, Lines:20, DMA, single)
I (731) LCD: Touch controller added to LVGL
I (731) DZBJ: LVGL 初始化完成
I (741) DZBJ: UI 初始化完成
I (841) DZBJ: 背光已点亮dzbj 显示模块初始化完成
I (841) Airhub1: IMU传感器未初始化跳过IoT注册
I (841) Airhub1: 配网模式跳过电池检测、IMU传感器、低功耗管理
I (841) Airhub1: 电容触摸板按钮已禁用 (ENABLE_TOUCH_PAD_BUTTONS=0)
I (841) Application: 打印设置设备状态日志: starting
I (841) Application: 正常启动流程,将执行开机播报和网络连接播报
I (841) Airhub1: Initializing audio codec (output only)...
I (841) Airhub1: Creating BoxAudioCodec (ES8311, without reference) ...
I (841) BoxAudioCodec: TX-only channel created (provisioning mode)
I (841) ES8311: Work in Slave mode
I (851) gpio: GPIO[48]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (851) BoxAudioCodec: BoxAudioDevice initialized (output only)
I (851) Airhub1: Audio codec initialized successfully
I (851) Application: 配网模式:跳过 Opus 编码器、输入重采样器、麦克风输入
I (851) I2S_IF: channel mode 0 bits:16/16 channel:2 mask:1
I (851) I2S_IF: STD Mode 1 bits:16/16 channel:2 sample_rate:16000 mask:1
I (861) Adev_Codec: Open codec device OK
I (861) AudioCodec: Set output enable to true
I (871) AudioCodec: Audio codec started (output only, provisioning mode)
I (871) AudioCodec: 将运行时输出音量设置为80
I (871) Application: 设备启动完成,播放开机播报语音
I (881) WifiBoard: force_ap cleared to 0
I (881) WifiBoard: 🔵 进入配网模式 - BLE蓝牙配网
I (881) WifiBoard: 🔵 进入配网模式 - 使用BLE蓝牙配网
I (881) WifiBoard: 🔵 正在启动BLE蓝牙配网服务...
I (881) Application: 🎵 测试模式:音频开始播放,等待播放完成
I (881) Application: ✅ 测试模式:音频播放完成
I (881) BluetoothProvisioning: 🔄 配网状态变化: IDLE -> INITIALIZING
I (881) BluetoothProvisioning: 初始化WiFi...
I (881) pp: pp rom version: e7ae62f
I (881) net80211: net80211 rom version: e7ae62f
I (891) wifi:wifi driver task: 3fce2524, prio:23, stack:6656, core=0
I (891) wifi:wifi firmware version: 3263cda
I (891) wifi:wifi certification version: v7.0
I (891) wifi:config NVS flash: enabled
I (891) wifi:config nano formatting: disabled
I (891) wifi:Init data frame dynamic rx buffer num: 32
I (891) wifi:Init dynamic rx mgmt buffer num: 5
I (891) wifi:Init management short buffer num: 32
I (891) wifi:Init static tx buffer num: 8
I (891) wifi:Init tx cache buffer num: 32
I (891) wifi:Init static tx FG buffer num: 2
I (891) wifi:Init static rx buffer size: 1600
I (901) wifi:Init static rx buffer num: 10
I (901) wifi:Init dynamic rx buffer num: 32
I (901) wifi_init: rx ba win: 16
I (901) wifi_init: accept mbox: 6
I (901) wifi_init: tcpip mbox: 32
I (901) wifi_init: udp mbox: 6
I (901) wifi_init: tcp mbox: 6
I (901) wifi_init: tcp tx win: 5760
I (901) wifi_init: tcp rx win: 5760
I (901) wifi_init: tcp mss: 1440
I (901) wifi_init: WiFi/LWIP prefer SPIRAM
I (901) phy_init: phy_version 701,f4f1da3a,Mar 3 2025,15:50:10
I (941) wifi:mode : sta (d0:cf:13:03:bb:f0)
I (941) wifi:enable tsf
I (941) BluetoothProvisioning: WiFi初始化完成
I (941) BluetoothProvisioning: 初始化蓝牙控制器...
I (941) BLE_INIT: BT controller compile version [2edb0b0]
I (941) BLE_INIT: Using main XTAL as clock source
I (941) BLE_INIT: Feature Config, ADV:1, BLE_50:0, DTM:1, SCAN:1, CCA:0, SMP:1, CONNECT:1
I (941) BLE_INIT: Bluetooth MAC: d0:cf:13:03:bb:f2
I (951) BluetoothProvisioning: 初始化Bluedroid协议栈...
I (961) BluetoothProvisioning: 注册 BLE GAP/GATTS 回调...
I (961) BluetoothProvisioning: ✅ GATTS App 注册成功, gatts_if=3
I (961) BluetoothProvisioning: Service 创建成功, handle=40
I (961) BluetoothProvisioning: WRITE 特征添加成功, handle=42
I (961) BluetoothProvisioning: NOTIFY 特征添加成功, handle=44
I (961) BluetoothProvisioning: CCCD 添加成功, handle=45
I (961) BluetoothProvisioning: ✅ GATT Service 启动成功
I (961) BluetoothProvisioning: 注册WiFi事件处理器...
I (961) BluetoothProvisioning: 🔄 配网状态变化: INITIALIZING -> IDLE
I (961) BluetoothProvisioning: 蓝牙配网初始化完成 (GATT Server 模式)
I (961) BluetoothProvisioning: 蓝牙MAC地址: d0:cf:13:03:bb:f2
I (961) WifiBoard: 🔍 BLE Initialize返回结果: true
I (961) BluetoothProvisioning: 🔵 开始启动蓝牙配网服务 (GATT Server)...
I (961) BluetoothProvisioning: 🔍 检查初始化状态: initialized_ = true
I (961) BluetoothProvisioning: MAC地址发送状态已重置
I (961) BluetoothProvisioning: 🔄 MAC地址发送状态已重置
I (961) BluetoothProvisioning: 🧹 清除之前的WiFi凭据...
I (961) BluetoothProvisioning: ✅ WiFi凭据清除完成准备接收新的配网信息
I (961) BluetoothProvisioning: 📡 蓝牙设备名称: Airhub_d0:cf:13:03:bb:f2
I (961) BluetoothProvisioning: 📡 广播数据构建完成,长度: 29 字节
I (961) BluetoothProvisioning: 📡 扫描响应数据构建完成,长度: 7 字节
I (971) BluetoothProvisioning: 📡 广播数据设置完成,配置扫描响应数据
E (971) BLE_INIT: Malloc failed
E (971) BT_HCI: CC evt: op=0x2009, status=0x7
I (971) BluetoothProvisioning: 📡 扫描响应数据设置完成,启动广播
I (971) BluetoothProvisioning: ✅ 广播启动成功
I (971) BluetoothProvisioning: 🔄 配网状态变化: IDLE -> ADVERTISING
I (971) BluetoothProvisioning: 蓝牙配网广播已启动,等待客户端连接...
I (971) WifiBoard: ✅ BLE蓝牙配网启动成功
I (971) WifiBoard: 📱 请使用支持BLE的手机APP连接设备进行配网
W (971) Application: Alert BLE配网模式: 请使用手机APP搜索Airhub_开头的蓝牙设备 []
I (971) WifiBoard: 🔍 BLE配网启动结果: 成功
I (971) WifiBoard: ✅ BLE配网启动成功等待手机连接
I (971) Application: 配网模式:跳过协议初始化、位置检测等网络业务
I (971) Application: 打印设置设备状态日志: idle
I (971) WeatherApi: [AutoDetectAndSetLocation] 调用全局函数自动检测位置
I (971) WeatherApi: [AutoDetectLocation] ===== 开始自动检测位置 =====
W (971) wifi:Haven't to connect to a suitable AP now!
I (971) WeatherApi: [AutoDetectLocation] 从NVS命中位置: '广州市',已更新默认城市
I (971) WeatherApi: [AutoDetectLocation] ===== 位置检测完成 =====
I (971) main_task: Returned from app_main()
I (1001) Application: 开始播放下行音频: 样本=960 采样率=16000
I (13481) AudioCodec: Set output enable to false
I (23591) BluetoothProvisioning: 📱 客户端已连接, conn_id=0, addr=6b:a1:99:6d:51:25
I (23591) BluetoothProvisioning: 🔍 [DEBUG] 设置client_connected_为true
I (23591) BluetoothProvisioning: MAC地址发送状态已重置
I (23591) BluetoothProvisioning: 🔄 MAC地址发送状态已重置
I (23591) BluetoothProvisioning: 🔄 配网状态变化: ADVERTISING -> CONNECTED
I (23591) WifiBoard: BLE client connected
I (23591) BluetoothProvisioning: 🔍 [DEBUG] BLE连接处理完成client_connected_=true
I (23591) BluetoothProvisioning: 广播已停止
I (23951) BluetoothProvisioning: 连接参数更新: status=0, conn_int=24, latency=0, timeout=400
I (24281) BluetoothProvisioning: 连接参数更新: status=0, conn_int=6, latency=0, timeout=500
I (24351) BluetoothProvisioning: MTU 更新: 512
I (24431) BluetoothProvisioning: 连接参数更新: status=0, conn_int=24, latency=0, timeout=400
I (24791) BluetoothProvisioning: NOTIFY 已启用
I (24851) BluetoothProvisioning: 📱 手机请求获取WiFi列表开始扫描
W (24851) wifi:Error! Should use default active scan time parameter for WiFi scan when Bluetooth is enabled!!!!!!
I (24861) BluetoothProvisioning: 🔍 WiFi扫描已启动 abort() was called at PC 0x421cd333 on core 0
I (33621) BluetoothProvisioning: 📡 WiFi扫描完成准备发送WiFi列表 --- 0x421cd333: __cxxabiv1::__terminate(void (*)()) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:48
I (33621) BluetoothProvisioning: 📊 扫描到 57 个WiFi热点
I (33631) BluetoothProvisioning: ✅ 成功获取WiFi扫描结果
I (33631) BluetoothProvisioning: 📊 过滤后剩余 33 个2.4GHz热点 (原始: 57)
I (33631) BluetoothProvisioning: 向客户端发送WiFi列表共33个AP
I (34291) BluetoothProvisioning: 📤 WiFi列表已发送给客户端包含 33 个热点
I (34291) BluetoothProvisioning: 📤 WiFi列表已发送包含 33 个热点
I (45611) BluetoothProvisioning: 📶 收到WiFi SSID: airhub
I (45761) BluetoothProvisioning: 🔐 收到WiFi密码 (长度: 9)
W (45761) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2
I (45791) BluetoothProvisioning: 📡 已发起WiFi连接请求启动超时监控
I (45791) WifiBoard: WiFi credentials received via BLE
I (45951) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1, snd_ch_cfg:0x0
I (45951) wifi:state: init -> auth (0xb0)
I (46381) wifi:state: auth -> assoc (0x0)
I (46411) wifi:state: assoc -> run (0x10)
I (46491) wifi:connected with airhub, aid = 3, channel 1, BW20, bssid = 70:2a:d7:85:bc:eb
I (46491) wifi:security: WPA2-PSK, phy: bgn, rssi: -27
I (46501) wifi:pm start, type: 1
I (46501) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us
I (46501) wifi:set rx beacon pti, rx_bcn_pti: 14, bcn_timeout: 25000, mt_pti: 14, mt_time: 10000
I (46501) BluetoothProvisioning: ✅ WiFi连接成功SSID: airhub等待获取IP地址
I (46501) wifi:AP's beacon interval = 102400 us, DTIM period = 1
I (48151) wifi:<ba-add>idx:0 (ifx:0, 70:2a:d7:85:bc:eb), tid:0, ssn:0, winSize:64
I (49371) esp_netif_handlers: sta ip: 192.168.124.22, mask: 255.255.255.0, gw: 192.168.124.1
I (49371) BluetoothProvisioning: ✅ WiFi获取IP地址成功: 192.168.124.22
I (49371) BluetoothProvisioning: 💾 启用WiFi配置自动保存到NVS存储...
I (49371) BluetoothProvisioning: ✅ WiFi配置将自动保存到NVS存储
I (49371) BluetoothProvisioning: 📋 获取当前WiFi配置成功SSID: airhub
I (49371) SsidManager: compare [airhub:6] [airhub:6]
W (49371) SsidManager: SSID airhub already exists, overwrite it
I (49371) BluetoothProvisioning: ✅ WiFi凭据已保存到NVS列表
I (49371) BluetoothProvisioning: BluetoothProvisioning WIFI_CONNECTED skip_session=0
I (49371) BluetoothProvisioning: 🔍 准备设置状态为SUCCESS并触发回调
I (49371) BluetoothProvisioning: 🔄 配网状态变化: CONNECTED -> SUCCESS
I (49371) WifiBoard: 设备配网成功,已连接到WiFi网络!
I (49371) BluetoothProvisioning: 🔍 [DEBUG] ReportWiFiStatus调用: success=true, client_connected_=true
I (49371) BluetoothProvisioning: 向客户端报告设备连接WiFi成功!
I (49371) BluetoothProvisioning: 📋 配网流程完成,状态: SUCCESS, client_connected_: true
I (49371) BluetoothProvisioning: ⏰ 延迟2000ms后重启设备以确保配置生效...
W (50081) wifi:m f null
W (50121) wifi:m f null Backtrace: 0x40379f49:0x3fcbcbe0 0x4038731d:0x3fcbcc00 0x4038f9d9:0x3fcbcc20 0x421cd333:0x3fcbcc90 0x421cd368:0x3fcbccb0 0x421cd443:0x3fcbccd0 0x421ddb09:0x3fcbccf0 0x42013a05:0x3fcbcd30 0x4201bf6f:0x3fcbcd50 0x420174b6:0x3fcbcd80 0x42018c69:0x3fcbcde0 0x4202069d:0x3fcbd370 0x42229453:0x3fcbd390 0x40387e11:0x3fcbd3c0
.--- 0x40379f49: panic_abort at /Users/rdzleo/esp/esp-idf/components/esp_system/panic.c:469
--- 0x4038731d: esp_system_abort at /Users/rdzleo/esp/esp-idf/components/esp_system/port/esp_system_chip.c:87
--- 0x4038f9d9: abort at /Users/rdzleo/esp/esp-idf/components/newlib/abort.c:38
--- 0x421cd333: __cxxabiv1::__terminate(void (*)()) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:48
--- 0x421cd368: std::terminate() at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:58
--- 0x421cd443: __cxa_throw at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_throw.cc:98
--- 0x421ddb09: std::__throw_system_error(int) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/src/c++11/system_error.cc:595
--- 0x42013a05: std::unique_lock<std::mutex>::lock() at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:142
--- 0x4201bf6f: std::unique_lock<std::mutex>::unique_lock(std::mutex&) at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:73
--- (inlined by) BackgroundTask::WaitForCompletion() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/background_task.cc:46
--- 0x420174b6: Application::SetDeviceState(DeviceState) at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:2262
--- 0x42018c69: Application::Start() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:533
--- 0x4202069d: app_main at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/main.cc:105
--- 0x42229453: main_task at /Users/rdzleo/esp/esp-idf/components/freertos/app_startup.c:208
--- 0x40387e11: vPortTaskWrapper at /Users/rdzleo/esp/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/port.c:139
W (50181) wifi:m f null
W (51101) wifi:m f null
W (51151) wifi:m f null
W (51201) wifi:m f null ELF file SHA256: 0061c1350
W (52071) wifi:m f null Rebooting...
W (52131) wifi:m f null
W (60981) wifi:m f null
W (61041) wifi:m f null
W (70981) wifi:m f null
W (71091) wifi:m f null
W (71181) wifi:m f null
W (71281) wifi:m f null
W (71391) wifi:m f null
W (71491) wifi:m f null
W (71591) wifi:m f null
W (71691) wifi:m f null
W (71801) wifi:m f null
W (71901) wifi:m f null
W (72001) wifi:m f null
W (72101) wifi:m f null
W (72211) wifi:m f null
W (72311) wifi:m f null
W (72411) wifi:m f null
W (72511) wifi:m f null
W (72621) wifi:m f null
W (72721) wifi:m f null
W (72831) wifi:m f null
W (72921) wifi:m f null
W (73031) wifi:m f null
W (73131) wifi:m f null
W (73231) wifi:m f null
W (73331) wifi:m f null
W (73441) wifi:m f null
W (73541) wifi:m f null
W (73641) wifi:m f null
W (73741) wifi:m f null
W (73841) wifi:m f null
W (73951) wifi:m f null
W (74051) wifi:m f null
W (74251) wifi:m f null
W (74361) wifi:m f null
W (74661) wifi:m f null
W (74781) wifi:m f null
W (74871) wifi:m f null
W (74981) wifi:m f null
W (75071) wifi:m f null
W (75181) wifi:m f null
W (75281) wifi:m f null
W (75381) wifi:m f null

View File

@ -1,9 +1,9 @@
# AI对话 + 电子吧唧 双模式适配可行性分析 # AI对话 + 电子吧唧 双模式适配说明
> 分析日期2026-02-27 > 更新日期2026-02-27
> 硬件平台movecall-moji-esp32s3 (ESP32-S3-N16R8) > 硬件平台movecall-moji-esp32s3 (ESP32-S3-N16R8)
> ESP-IDF版本5.4.2 > ESP-IDF版本5.4.2
> LVGL版本8.3.11 (dzbj项目) > LVGL版本8.3.11
--- ---
@ -11,55 +11,31 @@
### 1.1 主项目 (Baji_Rtc_Toy) ### 1.1 主项目 (Baji_Rtc_Toy)
基于 AI小智 开源项目改造,当前功能: 基于 AI小智 开源项目改造,当前已集成功能:
- 火山引擎 RTC 语音对话WiFi 连接) - 火山引擎 RTC 语音对话WiFi 连接)
- BLE 配网BluedroidService 0xABF0 - BLE 配网BluedroidService 0xABF0
- 音频编解码ES8311 + Opus - 音频编解码ES8311 + Opus
- 唤醒词检测esp-sr AFE - 唤醒词检测esp-sr AFE
- **无 LCD 显示**`lcd_display.cc` 已注释managed_components 中无 LVGL - **LVGL 8.3.11 LCD 显示**Phase 1 已完成,开机显示 ScreenHome
- ST77916 QSPI 360×360 LCD + CST816S 触摸(已初始化)
关键文件:
- `main/application.cc` — 应用主类,状态管理
- `main/boards/movecall-moji-esp32s3/` — 板级实现
- `main/boards/common/wifi_board.cc` — WiFi/BLE 网络管理
- `main/protocols/volc_rtc_protocol.cc` — 火山 RTC 协议
- `main/bluetooth_provisioning.cc` — BLE 配网
- `main/display/` — Display 抽象层(已预留 LCD 接口,未编译)
### 1.2 dzbj 子项目 (电子吧唧) ### 1.2 dzbj 子项目 (电子吧唧)
独立的 ESP32-S3 LVGL 项目,位于 `/dzbj/` 目录,当前功能: 独立的 ESP32-S3 LVGL 项目,位于 `/dzbj/` 目录,功能:
- 360×360 ST77916 QSPI LCD + CST816S 触摸 - 360×360 ST77916 QSPI LCD + CST816S 触摸
- LVGL 8.3.11 三屏界面ScreenHome/ScreenImg/ScreenSet - LVGL 8.3.11 三屏界面ScreenHome/ScreenImg/ScreenSet
- BLE GATT 图片传输服务Service 0x0B00 - BLE GATT 图片传输服务Service 0x0B00
- GIF 播放、JPEG 解码、SPIFFS 图片管理 - GIF 播放、JPEG 解码、SPIFFS 图片管理
- 低功耗休眠/唤醒管理 - 低功耗休眠/唤醒管理10s 超时熄屏)
- PWM 背光控制、手电筒功能 - PWM 背光控制
关键文件: ### 1.3 实施进度
- `dzbj/main/lcd/lcd.c` — ST77916 QSPI + CST816S 驱动
- `dzbj/main/ui/` — SquareLine Studio 生成的 UI 代码
- `dzbj/main/pages/pages.c` — 图片处理 + PWM 亮度
- `dzbj/main/ble/ble.c` — BLE 图片传输 GATT Server
- `dzbj/main/sleep_mgr/sleep_mgr.c` — 低功耗管理
### 1.3 AI小智原生 LVGL 版本 | 阶段 | 状态 | 说明 |
|------|------|------|
**AI小智原生项目不使用 LVGL 9.2.2**。实际情况: | Phase 1: 点亮屏幕 | **已完成** | LCD + LVGL + ScreenHome 显示 |
| Phase 2+4: 完整模式 + 切换 | **实施中** | 移植 dzbj 全模块 + 双模式切换 |
| 项目 | LVGL 版本 | 状态 | | Phase 3: AI 聊天 UI | 待定 | 基于 LVGL 的 emoji + 聊天气泡 |
|------|-----------|------|
| 主项目 (Baji_Rtc_Toy) | **无 LVGL** | `lcd_display.cc` 已注释managed_components 中无 lvgl |
| dzbj 子项目 | **8.3.11** | 完整集成 LVGL + esp_lvgl_port 2.5.0 |
| AI小智 Display 框架 | **预留接口** | `Display` 基类已编译,方法为空操作(no-op) |
`main/CMakeLists.txt` 第11-12行明确注释了 LCD 支持:
```cmake
#"display/lcd_display.cc" # 移除LCD显示器支持
#"display/oled_display.cc" # 移除OLED显示器支持
```
AI小智框架的 `LcdDisplay` 类已预留 emoji 表情(21种)、聊天气泡(微信风格)、主题切换(深色/浅色) 功能,但当前未编译链接。
--- ---
@ -69,41 +45,57 @@ AI小智框架的 `LcdDisplay` 类已预留 emoji 表情(21种)、聊天气泡(
``` ```
┌─────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────┐
LVGL 8.3.11 │ │ LVGL 8.3.11 + LCD
(常驻,两个模式共享) │ (常驻,两个模式共享显示硬件) │
├────────────────────┬────────────────────────────┤ ├────────────────────┬────────────────────────────┤
│ AI 聊天模式 │ 电子吧唧模式 │ │ AI 对话模式 │ 电子吧唧模式 │
│ (mode=0, 默认) │ (mode=1) │
│ │ │ │ │ │
│ WiFi + RTC 协议 │ BLE GATT Server │ │ WiFi + RTC 协议 │ BLE GATT Server │
│ emoji 表情显示 │ ScreenHome/Img/Set │ │ ScreenHome (仅显示) │ ScreenHome/Img/Set │
│ 聊天气泡文本 │ 图片浏览 + GIF │ │ 音频编解码 + 唤醒词 │ BLE 图片传输 (0x0B00) │
│ 唤醒词检测 │ BLE 图片传输 │ │ PowerSaveTimer │ sleep_mgr (10s熄屏) │
│ 音频编解码 │ 手电筒/低功耗 │ │ IMU 传感器 │ battery 电池监测 │
│ 电量检测 (板级) │ SPIFFS 图片管理 │
├────────────────────┴────────────────────────────┤ ├────────────────────┴────────────────────────────┤
│ 长按 BOOT 5秒 切换 │ │ BOOT 双击 切换写NVS + 重启) │
│ AI→吧唧: 关WiFi+RTC → 启BLE → 切换UI │
│ 吧唧→AI: 关BLE → 启WiFi+RTC → 切换UI │
└─────────────────────────────────────────────────┘ └─────────────────────────────────────────────────┘
``` ```
### 2.2 模式定义 ### 2.2 模式定义
**AI 聊天模式** **AI 对话模式** (device_mode=0, 默认)
- 网络WiFi 连接 - 网络WiFi 连接
- 协议:火山引擎 RTC 实时对话 - 协议:火山引擎 RTC 实时对话
- 音频:唤醒词检测 + Opus 编解码 + I2S 输出 - 音频:唤醒词检测 + Opus 编解码 + I2S 输出
- 显示:emoji 表情 + 聊天气泡文本 + 状态栏 - 显示:ScreenHome仅显示无触摸交互
- BLE**关闭** - BLE**关闭**(仅配网时启动)
**电子吧唧模式** **电子吧唧模式** (device_mode=1)
- 网络:**WiFi 关闭** - 网络:**WiFi 关闭**
- BLEGATT Server图片传输 + 配网服务) - BLEGATT Server图片传输服务 0x0B00
- 显示ScreenHome主界面→ ScreenImg图片浏览→ ScreenSet设置 - 显示ScreenHome → ScreenImg图片浏览→ ScreenSet设置
- 功能GIF 播放、JPEG 解码、SPIFFS 图片管理、手电筒、低功耗 - 功能JPEG 解码、GIF 播放、SPIFFS 图片管理、低功耗、电池监测
### 2.3 模式切换方案
**方案重启切换NVS 标志)**
```
BOOT 双击 → 读取 NVS device_mode → 切换 0↔1 → 写入 NVS → esp_restart()
```
切换时间约 3-4 秒重启时间NVS 擦写寿命 10-100万次无限次切换。
**选择重启而非热切换的原因**
1. WiFi + BLE Bluedroid 同时运行内部 SRAM 不足(约需 280KB可用 ~334KB
2. 热切换需处理大量资源释放/重建协议、音频管道、FreeRTOS 任务),复杂度极高
3. Application 单例内部状态event_group, opus 编解码器, background_task难以干净重置
4. 重启方式简单可靠,避免内存泄漏和碎片化风险
--- ---
## 三、内存预算分析(核心瓶颈) ## 三、内存预算分析
### 3.1 硬件规格 ### 3.1 硬件规格
@ -111,228 +103,149 @@ AI小智框架的 `LcdDisplay` 类已预留 emoji 表情(21种)、聊天气泡(
- **PSRAM**8MB OCT-SPI 80MHz - **PSRAM**8MB OCT-SPI 80MHz
- **Flash**16MB - **Flash**16MB
### 3.2 常驻组件(两模式共享) ### 3.2 各模式内存占用
| 组件 | DIRAM 占用 | 说明 | | 场景 | 估算占用 | 剩余 | 可行性 |
|------|-----------|------|
| LVGL 库 (.bss/.data) | ~34KB | 图形引擎核心 |
| FreeRTOS | ~22KB | 内核 + idle/timer 任务 |
| HAL + SPI Flash | ~30KB | 硬件抽象 + Flash 驱动 |
| Heap 管理器 | ~8KB | 内存分配器 |
| esp_system / esp_hw | ~18KB | 系统支持 |
| lwip 协议栈 | ~4KB | TCP/IP即使 WiFi 关闭也常驻) |
| 主应用 (main) | ~5KB | Application 框架 |
| LVGL 任务栈 | 8KB | LVGL 刷新任务 |
| **常驻小计** | **~129KB** | |
### 3.3 AI 聊天模式额外占用
| 组件 | DIRAM 占用 | 说明 |
|------|-----------|------|
| WiFi 驱动 (net80211+pp) | ~13KB 静态 + ~40-50KB 动态 | TX/RX 缓冲区 |
| RTC 协议 | ~5-10KB | 火山引擎连接 |
| 音频处理 | ~10-15KB | Opus编解码 + 管道 |
| 唤醒词 (esp-sr) | ~15-20KB | AFE + 模型加载 |
| **AI模式小计** | **~83-108KB** | |
| **AI模式总计** | **~212-237KB** | 常驻 + AI |
| **AI模式剩余** | **~97-122KB** | 用于堆分配 |
### 3.4 电子吧唧模式额外占用
| 组件 | DIRAM 占用 | 说明 |
|------|-----------|------|
| BLE Bluedroid 静态 | ~13KB | libbt.a 静态数据 |
| BLE 控制器 | ~15KB | 动态分配PSRAM优先后减少 |
| BLE 任务栈 | ~15KB | BTC(3KB) + BTU(4KB) + 控制器 |
| dzbj 业务任务 | ~18KB | GIF(4KB) + 按键(3KB) + 电池(4KB) + 睡眠(3KB) + BLE处理(8KB) |
| **吧唧模式小计** | **~61KB** | |
| **吧唧模式总计** | **~190KB** | 常驻 + 吧唧 |
| **吧唧模式剩余** | **~144KB** | 充裕 |
### 3.5 关键结论
| 场景 | 内存占用 | 剩余 | 可行性 |
|------|----------|------|--------| |------|----------|------|--------|
| AI 聊天模式(单独 | ~212-237KB | ~97-122KB | **可行**(偏紧) | | AI 对话模式WiFi+RTC+音频+LVGL | ~212-237KB | ~97-122KB | **可行**(偏紧) |
| 电子吧唧模式(单独 | ~190KB | ~144KB | **可行**(充裕) | | 电子吧唧模式BLE+LVGL+SPIFFS | ~190KB | ~144KB | **可行**(充裕) |
| 两模式同时运行 | ~274-345KB | 不足 | **不可行** | | 两模式同时运行 | ~274-345KB | 不足 | **不可行** |
| 模式切换(互斥) | 单次一个模式 | 够用 | **可行,需验证** |
**验证数据**:之前测试中 WiFi + BLE 同时运行导致 `assert failed: vQueueDelete queue.c:2355`FreeRTOS 信号量分配失败),确认内部 SRAM 不足以支撑两者同时运行。 ### 3.3 关键验证数据
- Phase 1 测试WiFi + BLE 同时运行导致 `assert failed: vQueueDelete queue.c:2355`FreeRTOS 信号量分配失败)
- BLE 配网成功后 `xTaskCreate` 分配 2048 栈失败(已改用 `esp_timer` 解决)
- 确认两模式必须互斥运行
--- ---
## 四、模式切换技术方案 ## 四、启动流程
### 4.1 AI → 电子吧唧 切换流程 ### 4.1 双模式启动序列
``` ```
用户长按 BOOT 5秒 开机
├─ 1. 关闭 AI 资源 ├── 板级构造函数(通用)
│ ├─ protocol_->CloseAudioChannel() // 关闭 RTC 音频通道 │ ├── PowerSaveTimer 初始化
│ ├─ volc_rtc_stop() + volc_rtc_destroy() // 销毁 RTC 实例 │ ├── InitializeButtons()(主项目 Button 类,双击注册在此)
│ ├─ StopAudioProcessor() // 停止音频处理器 │ ├── InitializeCodecI2c()
│ ├─ 停止唤醒词检测 │ ├── dzbj_display_init() ← LCD + LVGL 始终初始化
│ └─ esp_wifi_stop() + esp_wifi_deinit() // 完全释放 WiFi │ │
│ → 释放 ~83-108KB 内部 SRAM │ ├── if device_mode == BADGE (吧唧模式)
│ │ └── InitializeBadgeMode()
├─ 2. 启动吧唧资源 │ │ ├── fatfs_init() // SPIFFS 文件系统
│ ├─ esp_bt_controller_init() // 初始化 BLE 控制器 │ │ ├── init_spiffs_image_list() // 扫描图片
│ ├─ esp_bluedroid_init() + enable() // 启动 Bluedroid │ │ ├── dzbj_button_init() // ISR按键
│ ├─ 注册 GATT Server图片传输服务 │ │ ├── battery_init() // 电池检测
│ ├─ 启动 BLE 广播 │ │ ├── dzbj_ble_init() // BLE 图传
│ └─ 启动 dzbj 业务任务(按键/电池/睡眠管理) │ │ └── sleep_mgr_init() // 低功耗管理
│ │
└─ 3. 切换界面 │ └── else (AI模式, 默认)
└─ lv_scr_load_anim(ui_ScreenHome, ...) │ ├── InitializeIot()
│ ├── InitializeBatteryMonitor()
│ ├── InitializeImuSensor()
│ └── PowerSaveTimer 启用
├── Application::Start()
│ ├── if device_mode == BADGE
│ │ └── SetDeviceState(Idle); return; // 不启动WiFi/协议/音频
│ │
│ └── else (AI模式)
│ ├── Opus 编解码器初始化
│ ├── 音频管道启动
│ ├── board.StartNetwork() // WiFi 连接
│ ├── RTC 协议初始化
│ └── MainLoop + AudioLoop 启动
``` ```
### 4.2 电子吧唧 → AI 切换流程 ### 4.2 BOOT 按键行为
``` | 事件 | AI模式 | 吧唧模式 | 配网模式 |
用户长按 BOOT 5秒 |------|--------|---------|---------|
| 单击 | Idle↔Listening 切换 | 待定(返回 ScreenHome | 显示 MAC 地址 |
├─ 1. 关闭吧唧资源 | 双击 | **切换到吧唧模式** | **切换到AI模式** | 无响应 |
│ ├─ esp_ble_gap_stop_advertising() // 停止广播 | 长按5s | 无响应 | 无响应 | 进入生产测试 |
│ ├─ esp_ble_gatts_app_unregister() // 注销 GATT
│ ├─ esp_bluedroid_disable() + deinit() // 关闭 Bluedroid
│ ├─ esp_bt_controller_disable() + deinit() // 关闭控制器
│ └─ 停止 dzbj 业务任务
│ → 释放 ~43-61KB 内部 SRAM
├─ 2. 启动 AI 资源
│ ├─ esp_wifi_init() + esp_wifi_start() // 初始化 WiFi
│ ├─ WifiStation::Start() // 连接 WiFi
│ ├─ WaitForConnected() // 等待连接
│ ├─ 创建 VolcRtcProtocol 实例
│ ├─ protocol_->Start() // 启动 RTC 连接
│ └─ 启动音频处理器 + 唤醒词检测
└─ 3. 切换界面
└─ lv_scr_load_anim(ai_chat_screen, ...)
```
### 4.3 BOOT 按键检测逻辑
```
当前逻辑movecall_moji_esp32s3.cc:
- 单击: Idle↔Listening 切换
- 长按 5s仅配网模式: 生产测试
需要改为:
- 单击: 保持原逻辑AI模式下 Idle↔Listening吧唧模式下返回 ScreenHome
- 长按 5s: 模式切换AI ↔ 吧唧)
```
--- ---
## 五、实施方案 ## 五、模块移植清单
### 5.1 改动清单 ### 5.1 从 dzbj 移植的模块
#### 第一步:硬件层(改动小) | 模块 | 源文件 | 目标文件 | 适配要点 |
|------|--------|---------|---------|
| fatfs | `dzbj/main/fatfs/` | `main/dzbj/fatfs.c/h` | `gpio.h``dzbj_gpio.h` |
| pages | `dzbj/main/pages/pages.c` | `main/dzbj/pages.c` | 移除 `wifi.h`PWM 去重 |
| BLE图传 | `dzbj/main/ble/ble.c` | `main/dzbj/dzbj_ble.c/h` | **新增 deinit 函数** |
| sleep_mgr | `dzbj/main/sleep_mgr/` | `main/dzbj/sleep_mgr.c` | 按键回调适配 |
| button | `dzbj/main/button/` | `main/dzbj/dzbj_button.c/h` | ISR+队列+去抖 |
| battery | `dzbj/main/battery/` | `main/dzbj/battery.c/h` | ADC 校准 + UI 更新 |
| 文件 | 改动内容 | 改动程度 | ### 5.2 新建模块
|------|----------|----------|
| `movecall-moji-esp32s3/config.h` | 添加 LCD/Touch GPIO 定义 | 小 |
| `movecall_moji_esp32s3.cc` | 初始化 LCD 驱动(参考 dzbj lcd.c | 中 |
**GPIO 冲突注意** | 模块 | 文件 | 功能 |
- movecall-moji `BUILTIN_LED_GPIO = GPIO_NUM_21` 与 dzbj LCD D3 (GPIO 21) 冲突 |------|------|------|
- 需要重新映射 LED 引脚或调整 LCD 引脚 | device_mode | `main/dzbj/device_mode.c/h` | NVS 模式读写 + 重启切换 |
- dzbj 触摸用独立 I2C 引脚(GPIO 5/6),与音频 ES8311 (GPIO 17/18) 不冲突
#### 第二步LVGL 集成(改动中等) ### 5.3 修改的现有文件
| 文件 | 改内容 | 改动程度 | | 文件 | 修改内容 |
|------|----------|----------| |------|---------|
| `main/idf_component.yml` | 添加 lvgl 8.3.11 + esp_lvgl_port 2.5.0 + esp_lcd_st77916 | 小 | | `movecall_moji_esp32s3.cc` | 模式分支 + InitializeBadgeMode() + BOOT 双击回调 |
| `main/CMakeLists.txt` | 取消注释 lcd_display.cc添加 dzbj 模块源文件 | 中 | | `application.cc` | Start() 模式分支(吧唧模式早返回) |
| `main/ui/` | 从 dzbj 复制 SquareLine 生成的 UI 代码 | 复制 | | `main/CMakeLists.txt` | 添加新源文件 |
| `main/pages/` | 从 dzbj 复制页面管理模块 | 复制+小改 | | `main/idf_component.yml` | 添加 esp_jpeg 依赖 |
| `main/sleep_mgr/` | 从 dzbj 复制低功耗管理模块 | 复制+小改 | | `main/sleep_mgr/include/sleep_mgr.h` | stub 改为真实函数声明 |
#### 第三步AI 聊天 UI新开发 ### 5.4 删除的文件
| 内容 | 说明 | | 文件 | 原因 |
|------|------| |------|------|
| 创建 AI 聊天屏幕 | 基于 LVGL 8.3.11,包含 emoji 显示区 + 聊天气泡容器 + 状态栏 | | `main/pages/pages_stub.c` | 被 `main/dzbj/pages.c` 真实实现替代 |
| emoji 表情渲染 | 参考 `LcdDisplay::SetEmotion()` 实现21种表情映射 |
| 聊天气泡 | 参考 `LcdDisplay::SetChatMessage()` 实现微信风格气泡 |
| 对接 Application | SetEmotion/SetChatMessage 调用新 UI |
**注意**`main/display/lcd_display.h` 中使用了 `lv_draw_buf_t`LVGL 9.x 类型),需要适配为 8.3.11 的 `lv_disp_draw_buf_t`
#### 第四步:模式切换管理(核心改动)
| 文件 | 改动内容 | 改动程度 |
|------|----------|----------|
| 新增 `mode_manager.h/cc` | 双模式状态机 + WiFi/BLE init/deinit | 新建 |
| `movecall_moji_esp32s3.cc` | BOOT 长按 5s 检测 | 中 |
| `application.cc` | 添加模式切换回调 | 中 |
### 5.2 不需要大改动的模块
| 模块 | 改动程度 | 说明 |
|------|----------|------|
| dzbj UI 代码 (ui_ScreenHome/Img/Set) | **几乎不改** | 直接复制使用 |
| dzbj pages.c (GIF/JPEG/PWM) | **小改** | 适配新的 GPIO 定义 |
| dzbj sleep_mgr | **小改** | 与 AI小智的 PowerSaveTimer 整合 |
| dzbj ble.c (图片传输) | **不改** | 电子吧唧模式下直接使用 |
| bluetooth_provisioning.cc | **不改** | 配网逻辑保持不变 |
| VolcRtcProtocol | **不改** | AI模式下原样使用 |
--- ---
## 六、风险评估 ## 六、GPIO 引脚分配(已解决)
### 6.1 高风险 Phase 1 已完成的 GPIO 冲突解决:
| 风险 | 影响 | 缓解方案 | | GPIO | 主项目原用途 | dzbj用途 | 解决方案 |
|------|------|----------| |------|------------|---------|---------|
| **WiFi deinit 内存泄漏** | 每次切换泄漏几KB多次切换后崩溃 | 实测 `esp_wifi_deinit()` 后用 `heap_caps_get_free_size()` 验证回收量 | | 21 | BUILTIN_LED | LCD D3 | LED 改为 GPIO_NUM_NC |
| **BLE deinit 内存泄漏** | Bluedroid 完全释放困难 | 考虑使用 NimBLE 替代 Bluedroid更轻量deinit 更可靠) | | 1 | Touch1 (电容触摸) | LCD 背光 EN | Touch1 改为 GPIO_NUM_NC |
| **内部 SRAM 碎片化** | 反复 init/deinit 导致碎片,大块分配失败 | 用 `heap_caps_get_largest_free_block()` 监控最大连续块 | | 7 | Touch4 (电容触摸) | LCD RST | Touch4 改为 GPIO_NUM_NC |
| 6 | Battery ADC | Touch RST | Battery ADC 改为 GPIO 3 |
### 6.2 中等风险 | 17/18 | I2C_NUM_1 (音频) | I2C_NUM_0 (触摸) | 统一为 I2C_NUM_1 共享 |
| 风险 | 影响 | 缓解方案 |
|------|------|----------|
| **GPIO 引脚冲突** | LCD 引脚与现有外设冲突 (GPIO 21) | 仔细对照两个项目的 GPIO 分配表,重新映射 |
| **LVGL API 版本差异** | `lcd_display.h` 用了 `lv_draw_buf_t` (9.x) | 适配为 8.3.11 的 `lv_disp_draw_buf_t` |
| **Flash 空间** | 新增 LVGL(323KB) + emoji字体 + UI资源 | 当前 app 分区 5MB固件 ~3.5MB,剩余 ~1.5MB 充足 |
### 6.3 低风险
| 风险 | 影响 | 缓解方案 |
|------|------|----------|
| I2C 总线共享 | 音频 ES8311 (GPIO 17/18) vs 触摸 CST816S | dzbj 触摸用独立 I2C 引脚(GPIO 5/6),不冲突 |
| PSRAM 带宽 | LVGL DMA + 音频 + WiFi 并行 | AI模式无 GIF 播放PSRAM 带宽充足 |
| LVGL 界面切换 | 两套 UI 共存 | LVGL 对象可存放 PSRAM界面切换无需重启 LVGL |
--- ---
## 七、NimBLE vs Bluedroid 选型建议 ## 七、风险评估
当前 dzbj 和配网都使用 Bluedroid但在双模式切换场景下 NimBLE 更优: ### 7.1 重启切换方案(已选定)
| 对比项 | Bluedroid | NimBLE | | 风险 | 等级 | 说明 |
|--------|-----------|--------| |------|------|------|
| Flash 占用 | ~277KB (libbt+libbtdm) | ~120KB | | 内存泄漏 | **无** | 每次重启全新初始化,无残留 |
| 内部 SRAM | ~35-45KB 动态 | ~15-20KB 动态 | | 内存碎片化 | **无** | 重启清除所有堆分配 |
| deinit 可靠性 | 一般(可能有泄漏) | 较好 | | WiFi/BLE deinit 不可靠 | **无** | 无需 deinit重启自然释放 |
| Classic BT | 支持 | 不支持(仅 BLE | | NVS 擦写寿命 | **极低** | 10-100万次日常使用完全足够 |
| GATT Server | 支持 | 支持 | | 切换体验 | **低** | ~3-4秒重启时间可加转场动画优化 |
| 迁移工作量 | — | 中等API 不同,逻辑相同) |
**建议**:如果不需要经典蓝牙,优先考虑迁移到 NimBLE。节省 ~150KB Flash + ~20KB 内部 SRAM并且 deinit 更可靠。 ### 7.2 其他风险
| 风险 | 等级 | 缓解方案 |
|------|------|---------|
| 符号冲突pages_stub vs pages.c | 中 | 删除 stub真实实现始终编译 |
| button 模块冲突C++ Button vs C ISR | 中 | 条件初始化,两模式用不同实现 |
| SPIFFS 分区未配置 | 中 | 检查分区表是否有 spiffs 分区 |
| Flash 空间 | 低 | 当前 app 分区 5MB固件 ~3.5MB,剩余充足 |
--- ---
## 八、分区表设计 ## 八、分区表
当前分区表(已移除 storage 当前分区表:
```csv ```csv
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
@ -344,51 +257,35 @@ ota_0, app, ota_0, 0x310000, 5M,
ota_1, app, ota_1, 0x820000, 5M, ota_1, app, ota_1, 0x820000, 5M,
``` ```
**建议**:如果需要 SPIFFS 存储图片dzbj 的图片浏览功能),需要重新添加 storage 分区,或复用 model 分区的一部分空间 dzbj 图片浏览功能需要 SPIFFS 存储。`model` 分区3MB, spiffs 类型)可复用,或需新增 storage 分区
--- ---
## 九、推荐实施路线 ## 九、验证计划
``` ### 9.1 编译验证
阶段 1: 点亮屏幕(基础验证) 预计改动量: 小 ```bash
├─ 确认 LCD GPIO 映射(解决 GPIO 21 冲突) idf.py build
├─ 在 main/idf_component.yml 添加 LVGL 8.3.11 依赖
├─ 在 movecall-moji 板上初始化 LCD + LVGL
├─ 显示 dzbj ScreenHome 界面
└─ 验证 LVGL + WiFi 内存占用(确认不冲突)
阶段 2: 电子吧唧模式完整复制 预计改动量: 中
├─ 复制 dzbj 的 UI/pages/ble/sleep_mgr 模块到主项目
├─ 关闭 WiFi 后启动 BLE + ScreenHome
├─ 验证 BLE 图片传输功能
└─ 验证低功耗管理
阶段 3: AI 聊天 UI 开发 预计改动量: 中
├─ 基于 LVGL 8.3.11 创建聊天屏幕emoji + 气泡)
├─ 对接 Application 的 SetEmotion/SetChatMessage
├─ 关闭 BLE 后启动 WiFi + RTC
└─ 验证语音对话 + 屏幕显示联动
阶段 4: 模式切换集成 预计改动量: 中
├─ 实现 BOOT 长按 5s 检测
├─ 实现 WiFi ↔ BLE 完整 init/deinit 切换
├─ 界面切换 + 资源释放验证
└─ 长时间稳定性测试(反复切换 100+ 次)
``` ```
--- ### 9.2 AI 模式验证(默认 mode=0
- [ ] 开机正常进入 WiFi 连接 + RTC 对话
- [ ] LVGL 显示 ScreenHome
- [ ] BOOT 单击切换对话状态
- [ ] BOOT 双击 → 切换到吧唧模式,设备重启
- [ ] 内存剩余 > 80KB
## 十、结论 ### 9.3 吧唧模式验证mode=1
- [ ] 开机日志显示"电子吧唧模式启动"
- [ ] 不连接 WiFi不播放开机语音
- [ ] BLE 广播可见(手机搜索 "Airhub_XX:XX:XX"
- [ ] 手机 APP 可传输图片到设备
- [ ] 屏幕显示传输的图片
- [ ] 10s 无操作后屏幕熄灭
- [ ] 按键或触摸唤醒屏幕
- [ ] BOOT 双击 → 切换回 AI 模式,设备重启
- [ ] 内存剩余 > 150KB
| 问题 | 结论 | ### 9.4 稳定性验证
|------|------| - [ ] 来回切换 10+ 次,功能正常
| AI小智用 LVGL 9.2.2 | **不是**,当前项目无 LVGL框架预留了 LCD 接口 | - [ ] 各模式下长时间运行(>1小时无崩溃
| 显示 emoji 需要 LVGL | **是**emoji 字体渲染和聊天气泡都依赖 LVGL |
| 保持 LVGL 8.3.11 | **可行**dzbj 代码直接复用 |
| 双模式切换可行? | **可行**,但需验证 WiFi/BLE deinit 的内存回收 |
| 内存够用? | **单模式够用**AI剩余~100KB吧唧剩余~144KB同时运行不够 |
| dzbj 代码大改? | **不需要**UI/pages/ble 模块几乎原样复制 |
| 最大技术风险? | **WiFi/BLE 反复 init/deinit 的内存泄漏和碎片化** |
**总体评估**:双模式互斥切换方案在技术上可行,资源预算满足单模式运行。最大不确定性在于 WiFi/BLE 的完整 deinit 是否能可靠回收内存需要实际编码验证。建议从阶段1点亮屏幕开始逐步推进。

View File

@ -16,3 +16,6 @@ add_compile_options(-Wno-missing-field-initializers)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(kapi) project(kapi)
# SPIFFS spiffs_image/ storage
spiffs_create_partition_image(storage spiffs_image FLASH_IN_PROJECT)

View File

@ -107,6 +107,16 @@ dependencies:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com/
type: service type: service
version: 1.3.6 version: 1.3.6
espressif/esp_jpeg:
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.3.1
espressif/esp_lcd_st77916: espressif/esp_lcd_st77916:
component_hash: 5fa0f8b1274576d4484e2b8d9358e2a5d09c721511bef0dce6a55b4206b5f0e9 component_hash: 5fa0f8b1274576d4484e2b8d9358e2a5d09c721511bef0dce6a55b4206b5f0e9
dependencies: dependencies:
@ -202,6 +212,7 @@ direct_dependencies:
- espressif/button - espressif/button
- espressif/esp-sr - espressif/esp-sr
- espressif/esp_codec_dev - espressif/esp_codec_dev
- espressif/esp_jpeg
- espressif/esp_lcd_st77916 - espressif/esp_lcd_st77916
- espressif/esp_lcd_touch - espressif/esp_lcd_touch
- espressif/esp_lcd_touch_cst816s - espressif/esp_lcd_touch_cst816s
@ -210,6 +221,6 @@ direct_dependencies:
- espressif/led_strip - espressif/led_strip
- idf - idf
- lvgl/lvgl - lvgl/lvgl
manifest_hash: f912ad61bf8c653f10e6eb6988299d0c0083fc812504487ba14571084326b35a manifest_hash: 567fb06fed7b7df9c9bbd2a0615df5b600cd13d08df4b38a71d28971feaec792
target: esp32s3 target: esp32s3
version: 2.0.0 version: 2.0.0

View File

@ -26,6 +26,13 @@ set(SOURCES "audio_codecs/audio_codec.cc"
"dzbj/lcd.c" "dzbj/lcd.c"
"dzbj/pages_pwm.c" "dzbj/pages_pwm.c"
"dzbj/dzbj_init.c" "dzbj/dzbj_init.c"
"dzbj/device_mode.c"
"dzbj/fatfs.c"
"dzbj/pages.c"
"dzbj/dzbj_ble.c"
"dzbj/sleep_mgr.c"
"dzbj/dzbj_button.c"
"dzbj/dzbj_battery.c"
# SquareLine Studio UI # SquareLine Studio UI
"ui/ui.c" "ui/ui.c"
"ui/ui_helpers.c" "ui/ui_helpers.c"
@ -33,8 +40,6 @@ set(SOURCES "audio_codecs/audio_codec.cc"
"ui/screens/ui_ScreenImg.c" "ui/screens/ui_ScreenImg.c"
"ui/screens/ui_ScreenSet.c" "ui/screens/ui_ScreenSet.c"
"ui/components/ui_comp_hook.c" "ui/components/ui_comp_hook.c"
# dzbj stub Phase 1
"pages/pages_stub.c"
# UI # UI
"ui/images/ui_img_s1_png.c" "ui/images/ui_img_s1_png.c"
"ui/images/ui_img_s6_png.c" "ui/images/ui_img_s6_png.c"

View File

@ -17,6 +17,7 @@
#include "boards/common/qmi8658a.h" // 添加qmi8658a_data_t类型的头文件 #include "boards/common/qmi8658a.h" // 添加qmi8658a_data_t类型的头文件
#include "boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h" // 添加MovecallMojiESP32S3类的头文件 #include "boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h" // 添加MovecallMojiESP32S3类的头文件
#include "weather_api.h" #include "weather_api.h"
#include "dzbj/device_mode.h" // 设备模式管理AI/吧唧)
#include <cstring> #include <cstring>
#include <esp_log.h> #include <esp_log.h>
@ -53,7 +54,12 @@ static const char* const STATE_STRINGS[] = {
Application::Application() { Application::Application() {
event_group_ = xEventGroupCreate(); event_group_ = xEventGroupCreate();
background_task_ = new BackgroundTask(4096 * 8); // 吧唧模式不需要后台任务节省32KB栈内存
if (!device_mode_is_badge()) {
background_task_ = new BackgroundTask(4096 * 8);
} else {
background_task_ = nullptr;
}
last_audible_output_time_ = std::chrono::steady_clock::now(); // 初始化最后一次有声音输出的时间点 last_audible_output_time_ = std::chrono::steady_clock::now(); // 初始化最后一次有声音输出的时间点
skip_dialog_idle_session_ = false; // 初始化跳过对话待机会话标志为false skip_dialog_idle_session_ = false; // 初始化跳过对话待机会话标志为false
dialog_watchdog_running_ = false; // 初始化对话看门狗运行标志 dialog_watchdog_running_ = false; // 初始化对话看门狗运行标志
@ -521,6 +527,13 @@ void Application::SendTextMessage(const std::string& text) {
} }
void Application::Start() { void Application::Start() {
// 电子吧唧模式:不启动 WiFi、协议、音频所有交互由 LVGL + BLE 处理
if (device_mode_is_badge()) {
ESP_LOGI(TAG, "🎴 吧唧模式:跳过 WiFi/协议/音频初始化");
SetDeviceState(kDeviceStateIdle);
return;
}
auto& board = Board::GetInstance(); auto& board = Board::GetInstance();
SetDeviceState(kDeviceStateStarting); SetDeviceState(kDeviceStateStarting);
@ -2246,7 +2259,7 @@ void Application::SetDeviceState(DeviceState state) {
} }
ESP_LOGI(TAG, "打印设置设备状态日志: %s", STATE_STRINGS[device_state_]);// 打印设置设备状态日志 ESP_LOGI(TAG, "打印设置设备状态日志: %s", STATE_STRINGS[device_state_]);// 打印设置设备状态日志
// The state is changed, wait for all background tasks to finish // The state is changed, wait for all background tasks to finish
background_task_->WaitForCompletion(); if (background_task_) background_task_->WaitForCompletion();
auto& board = Board::GetInstance(); auto& board = Board::GetInstance();
auto display = board.GetDisplay(); auto display = board.GetDisplay();

View File

@ -7,8 +7,7 @@ static const char* TAG = "Button";
Button::Button(const button_adc_config_t& adc_cfg) { Button::Button(const button_adc_config_t& adc_cfg) {
button_config_t button_config = { button_config_t button_config = {
.type = BUTTON_TYPE_ADC, .type = BUTTON_TYPE_ADC,
// .long_press_time = 1000, // 原有长按3秒时的时间 .long_press_time = 3000, // 长按3秒触发模式切换
.long_press_time = 5000, // 长按5秒时间
.short_press_time = 50, .short_press_time = 50,
.adc_button_config = adc_cfg .adc_button_config = adc_cfg
}; };
@ -26,8 +25,7 @@ Button::Button(gpio_num_t gpio_num, bool active_high) : gpio_num_(gpio_num) {
} }
button_config_t button_config = { button_config_t button_config = {
.type = BUTTON_TYPE_GPIO, .type = BUTTON_TYPE_GPIO,
// .long_press_time = 1000, // 原有长按3秒时的时间 .long_press_time = 3000, // 长按3秒触发模式切换
.long_press_time = 5000, // 长按5秒时间
.short_press_time = 50, .short_press_time = 50,
.gpio_button_config = { .gpio_button_config = {
.gpio_num = gpio_num, .gpio_num = gpio_num,

View File

@ -18,6 +18,10 @@ public:
void OnLongPress(std::function<void()> callback); void OnLongPress(std::function<void()> callback);
void OnClick(std::function<void()> callback); void OnClick(std::function<void()> callback);
void OnDoubleClick(std::function<void()> callback); void OnDoubleClick(std::function<void()> callback);
// 获取底层 iot_button 句柄(用于 iot_button_register_event_cb 等高级 API
button_handle_t GetHandle() const { return button_handle_; }
private: private:
gpio_num_t gpio_num_; gpio_num_t gpio_num_;
button_handle_t button_handle_ = nullptr; button_handle_t button_handle_ = nullptr;

View File

@ -15,6 +15,12 @@
#include "system_info.h" // 引入系统信息头文件 #include "system_info.h" // 引入系统信息头文件
#include "settings.h" #include "settings.h"
#include "dzbj/dzbj_init.h" // dzbj 显示模块初始化 #include "dzbj/dzbj_init.h" // dzbj 显示模块初始化
#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" // 按键驱动
#include "sleep_mgr/include/sleep_mgr.h" // 休眠管理
#include <cmath> // 添加数学函数头文件 #include <cmath> // 添加数学函数头文件
#include <wifi_station.h> #include <wifi_station.h>
@ -37,6 +43,12 @@
#define TAG "Airhub1" #define TAG "Airhub1"
#define Pro_TAG "Airhub" #define Pro_TAG "Airhub"
// 前向声明pages.h 与 display.h 的 lv_font_t 冲突,改用前向声明)
extern "C" void init_spiffs_image_list(void);
// 吧唧模式 BOOT 单击处理(实现在 dzbj_button.c避免 lvgl.h 与 display.h 冲突)
extern "C" void dzbj_boot_click_handler(void);
#if ENABLE_TOUCH_PAD_BUTTONS #if ENABLE_TOUCH_PAD_BUTTONS
#include <driver/touch_pad.h> #include <driver/touch_pad.h>
#include <driver/touch_sensor.h> #include <driver/touch_sensor.h>
@ -201,84 +213,90 @@ public:
touched_pad_index_ = -1; touched_pad_index_ = -1;
#endif #endif
// 使用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传感器初始化之前 // 初始化I2C总线必须在IMU传感器初始化之前
InitializeCodecI2c(); InitializeCodecI2c();
// 初始化 dzbj 显示模块LCD + Touch + LVGL + UI // 初始化 dzbj 显示模块LCD + Touch + LVGL + UI
dzbj_display_init(codec_i2c_bus_); dzbj_display_init(codec_i2c_bus_);
// 初始化IoT功能启用语音音量控制 // === 根据设备模式分支初始化(完全隔离,互不干扰) ===
InitializeIot(); if (device_mode_is_badge()) {
// ===== 电子吧唧模式 =====
// 配网模式下跳过非必要外设,节省内部 DRAM 给 WiFi+BLE 使用 // 不创建 PowerSaveTimer吧唧模式使用 sleep_mgr
bool provisioning_mode = WifiBoard::NeedsProvisioning(); // 不初始化 AI 音频/协议/WiFi 相关资源
if (provisioning_mode) { ESP_LOGI(TAG, "🎴 电子吧唧模式启动");
ESP_LOGI(TAG, "配网模式跳过电池检测、IMU传感器、低功耗管理"); battery_level_ = 100; // 默认电量,后续由 dzbj_battery 接管
battery_level_ = 100; // 设置默认电量 InitializeBadgeModeButtons(); // 仅注册吧唧专用回调
InitializeBadgeMode();
} else { } else {
// 初始化电量检测 // ===== AI 对话模式 =====
InitializeBatteryMonitor(); ESP_LOGI(TAG, "🤖 AI对话模式启动");
// 初始化IMU传感器 // 创建 PowerSaveTimer仅 AI 模式需要)
InitializeImuSensor(); 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, "🔊 功放已打开");
}
});
// 启用PowerSaveTimer启用完整的低功耗管理 InitializeAiModeButtons(); // 完整 AI 回调(含模式切换、音量、生产测试)
power_save_timer_->SetEnabled(true); InitializeStoryButton();
ESP_LOGI(TAG, "🔋 PowerSaveTimer已启用20秒无活动将进入低功耗模式");
} // 初始化IoT功能启用语音音量控制
InitializeIot();
// 配网模式下跳过非必要外设,节省内部 DRAM 给 WiFi+BLE 使用
bool provisioning_mode = WifiBoard::NeedsProvisioning();
if (provisioning_mode) {
ESP_LOGI(TAG, "配网模式跳过电池检测、IMU传感器、低功耗管理");
battery_level_ = 100;
} else {
// 初始化电量检测
InitializeBatteryMonitor();
// 初始化IMU传感器
InitializeImuSensor();
// 启用PowerSaveTimer启用完整的低功耗管理
power_save_timer_->SetEnabled(true);
ESP_LOGI(TAG, "🔋 PowerSaveTimer已启用20秒无活动将进入低功耗模式");
}
#if ENABLE_TOUCH_PAD_BUTTONS #if ENABLE_TOUCH_PAD_BUTTONS
// 延迟调用触摸板初始化,避免在构造函数中就调用 // 延迟调用触摸板初始化,避免在构造函数中就调用
ESP_LOGI(TAG, "在构造函数完成后调用触摸初始化"); ESP_LOGI(TAG, "在构造函数完成后调用触摸初始化");
xTaskCreate([](void* arg) { xTaskCreate([](void* arg) {
MovecallMojiESP32S3* board = static_cast<MovecallMojiESP32S3*>(arg); MovecallMojiESP32S3* board = static_cast<MovecallMojiESP32S3*>(arg);
vTaskDelay(1000 / portTICK_PERIOD_MS); vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "开始延迟初始化触摸板..."); ESP_LOGI(TAG, "开始延迟初始化触摸板...");
if (board) { if (board) {
board->InitializeTouchPads(); board->InitializeTouchPads();
} }
vTaskDelete(NULL); vTaskDelete(NULL);
}, "touch_init", 4096, this, 5, NULL); }, "touch_init", 4096, this, 5, NULL);
#else #else
ESP_LOGI(TAG, "电容触摸板按钮已禁用 (ENABLE_TOUCH_PAD_BUTTONS=0)"); ESP_LOGI(TAG, "电容触摸板按钮已禁用 (ENABLE_TOUCH_PAD_BUTTONS=0)");
#endif #endif
}
} }
#if ENABLE_TOUCH_PAD_BUTTONS #if ENABLE_TOUCH_PAD_BUTTONS
// 发送触摸消息 // 发送触摸消息
void SendTouchMessage(int touch_pad_num) { void SendTouchMessage(int touch_pad_num) {
const char* message = nullptr; const char* message = nullptr;
power_save_timer_->WakeUp(); if (power_save_timer_) power_save_timer_->WakeUp();
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
auto current_state = app.GetDeviceState(); auto current_state = app.GetDeviceState();
@ -458,8 +476,52 @@ public:
// 按钮初始化 函数 void InitializeBadgeMode() {
void InitializeButtons() { ESP_LOGI(TAG, "初始化电子吧唧模式外设...");
fatfs_init(); // SPIFFS 文件系统
fatfs_remove_nullData("/spiflash"); // 清理空文件
init_spiffs_image_list(); // 扫描图片列表
dzbj_button_init(); // ISR 按键驱动
dzbj_battery_init(); // 电池检测
dzbj_battery_monitor_start(); // 电池监控任务
dzbj_ble_init(); // BLE 图传服务
sleep_mgr_init(); // 低功耗管理(最后启动)
ESP_LOGI(TAG, "电子吧唧模式初始化完成");
}
// === 吧唧模式按钮初始化(仅注册吧唧专用回调,不涉及 AI 音频/协议资源) ===
void InitializeBadgeModeButtons() {
ESP_LOGI(TAG, "初始化吧唧模式按钮...");
// BOOT 单击 → 唤醒屏幕 / 退出手电筒 / 返回Home
// 注意iot_button 回调在 esp_timer 任务中执行,不能调用 vTaskDelay
// (会阻塞 lv_tick_inc 导致 LVGL 渲染停滞),必须转发到独立任务
boot_button_.OnClick([this]() {
static uint32_t last_click_time = 0;
uint32_t current_time = esp_timer_get_time() / 1000;
if (last_click_time > 0 && current_time - last_click_time < 500) {
return;
}
last_click_time = current_time;
ESP_LOGI(TAG, "吧唧模式 BOOT 单击");
xTaskCreate([](void *arg) {
dzbj_boot_click_handler();
vTaskDelete(NULL);
}, "boot_click", 4096, NULL, 5, NULL);
});
// BOOT 长按 3 秒 → 切换到 AI 模式
boot_button_.OnLongPress([this]() {
ESP_LOGI(TAG, "BOOT长按3秒吧唧→AI模式");
device_mode_set(DEVICE_MODE_AI);
});
ESP_LOGI(TAG, "Boot button initialized on GPIO%d", BOOT_BUTTON_GPIO);
ESP_LOGI(TAG, "吧唧模式按钮初始化完成");
}
// === AI 模式按钮初始化(完整的 AI 对话相关回调) ===
void InitializeAiModeButtons() {
ESP_LOGI(TAG, "初始化按钮...");// 初始化按钮... ESP_LOGI(TAG, "初始化按钮...");// 初始化按钮...
// BOOT按键单击事件 - 用于WiFi重置和触摸解锁 // BOOT按键单击事件 - 用于WiFi重置和触摸解锁
@ -657,23 +719,42 @@ public:
} }
}); });
// 配网模式下长按 BOOT 按键5秒进入 生产测试模式 新增代码 // BOOT 长按 3 秒 → 切换设备模式AI ↔ 吧唧)
// ============================================================================== // 配网模式下跳过,因为配网模式有自己的 5s 长按处理
// 添加BOOT按键长按事件处理 - 仅在配网模式下长按5秒进入测试模式
boot_button_.OnLongPress([this]() { boot_button_.OnLongPress([this]() {
//ESP_LOGI(TAG, "🔧 BOOT button long pressed - checking if in provisioning mode"); // 配网模式下不切换模式(留给 5s 长按进入生产测试)
// 检查是否处于BLE配网状态只有在配网模式下才允许进入测试模式
auto* wifi_board = dynamic_cast<WifiBoard*>(this); auto* wifi_board = dynamic_cast<WifiBoard*>(this);
if (wifi_board && wifi_board->IsBleProvisioningActive()) { if (wifi_board && wifi_board->IsBleProvisioningActive()) {
// ESP_LOGI(TAG, "🔧 设备正在进行BLE配网长按5秒进入生产测试模式"); ESP_LOGI(TAG, "配网模式下长按3秒等待5秒进入生产测试...");
EnterProductionTestMode();
} else {
ESP_LOGI(TAG, "🔵 非配网模式下BOOT长按被屏蔽无法进入测试模式");
return; return;
} }
ESP_LOGI(TAG, "BOOT长按3秒切换设备模式");
if (device_mode_is_badge()) {
ESP_LOGI(TAG, "吧唧模式 → AI模式");
device_mode_set(DEVICE_MODE_AI); // 写NVS + 重启
} else {
ESP_LOGI(TAG, "AI模式 → 吧唧模式");
device_mode_set(DEVICE_MODE_BADGE); // 写NVS + 重启
}
}); });
// ==============================================================================
// BOOT 长按 5 秒 → 仅在配网模式下进入生产测试模式
// 使用 iot_button_register_event_cb 注册 5s 阈值(独立于 3s 的 OnLongPress
{
static MovecallMojiESP32S3* self = this;
button_event_config_t evt_cfg = {};
evt_cfg.event = BUTTON_LONG_PRESS_START;
evt_cfg.event_data.long_press.press_time = 5000;
iot_button_register_event_cb(boot_button_.GetHandle(), evt_cfg,
[](void* handle, void* usr_data) {
auto* wifi_board = dynamic_cast<WifiBoard*>(self);
if (wifi_board && wifi_board->IsBleProvisioningActive()) {
ESP_LOGI(TAG, "BOOT长按5秒进入生产测试模式");
self->EnterProductionTestMode();
}
}, nullptr);
}
ESP_LOGI(TAG, "Boot button initialized on GPIO%d", BOOT_BUTTON_GPIO); ESP_LOGI(TAG, "Boot button initialized on GPIO%d", BOOT_BUTTON_GPIO);

37
main/dzbj/device_mode.c Normal file
View File

@ -0,0 +1,37 @@
#include "device_mode.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define TAG "DeviceMode"
#define NVS_NAMESPACE "device"
#define NVS_KEY "mode"
device_mode_t device_mode_get(void) {
nvs_handle_t h;
int32_t mode = DEVICE_MODE_AI;
if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &h) == ESP_OK) {
nvs_get_i32(h, NVS_KEY, &mode);
nvs_close(h);
}
return (device_mode_t)mode;
}
void device_mode_set(device_mode_t mode) {
nvs_handle_t h;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h) == ESP_OK) {
nvs_set_i32(h, NVS_KEY, (int32_t)mode);
nvs_commit(h);
nvs_close(h);
}
ESP_LOGI(TAG, "模式切换为 %s即将重启...",
mode == DEVICE_MODE_BADGE ? "吧唧" : "AI");
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
}
bool device_mode_is_badge(void) {
return device_mode_get() == DEVICE_MODE_BADGE;
}

38
main/dzbj/device_mode.h Normal file
View File

@ -0,0 +1,38 @@
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief
*/
typedef enum {
DEVICE_MODE_AI = 0, // AI对话模式WiFi + RTC
DEVICE_MODE_BADGE = 1, // 电子吧唧模式BLE + 图片)
} device_mode_t;
/**
* @brief NVS
* @return DEVICE_MODE_AI
*/
device_mode_t device_mode_get(void);
/**
* @brief
* @param mode
* NVS 500ms esp_restart()
*/
void device_mode_set(device_mode_t mode);
/**
* @brief
* @return true , false AI模式
*/
bool device_mode_is_badge(void);
#ifdef __cplusplus
}
#endif

228
main/dzbj/dzbj_battery.c Normal file
View File

@ -0,0 +1,228 @@
#include "dzbj_battery.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_log.h"
#include "esp_check.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lvgl_port.h"
#include "../ui/screens/ui_ScreenSet.h"
// ScreenHome界面不关联电池电量显示
#include <stdio.h>
static const char *TAG = "DZBJ_BAT";
// ADC句柄
static adc_oneshot_unit_handle_t adc_handle = NULL;
static adc_cali_handle_t cali_handle = NULL;
static bool cali_enabled = false;
// 当前电池数据
static uint32_t bat_voltage_mv = 0;
static uint8_t bat_level = 0;
// 锂电池放电曲线查找表基于典型3.7V单节锂电池放电特性)
// 电压单位:毫伏,电量单位:百分比
typedef struct {
uint16_t voltage_mv;
uint8_t level;
} bat_curve_point_t;
static const bat_curve_point_t bat_curve[] = {
{4200, 100},
{4150, 95},
{4110, 90},
{4080, 85},
{4020, 80},
{3980, 75},
{3950, 70},
{3910, 65},
{3870, 60},
{3840, 55},
{3800, 50},
{3760, 45},
{3730, 40},
{3700, 35},
{3680, 30},
{3650, 25},
{3630, 20},
{3600, 15},
{3570, 10},
{3530, 5},
{3400, 2},
{3000, 0},
};
#define BAT_CURVE_SIZE (sizeof(bat_curve) / sizeof(bat_curve[0]))
// 电压转电量(线性插值,提高精度)
static uint8_t voltage_to_level(uint32_t voltage_mv)
{
// 超出上限
if (voltage_mv >= bat_curve[0].voltage_mv) {
return 100;
}
// 低于下限
if (voltage_mv <= bat_curve[BAT_CURVE_SIZE - 1].voltage_mv) {
return 0;
}
// 在查找表中线性插值
for (int i = 0; i < BAT_CURVE_SIZE - 1; i++) {
if (voltage_mv >= bat_curve[i + 1].voltage_mv) {
uint32_t v_range = bat_curve[i].voltage_mv - bat_curve[i + 1].voltage_mv;
uint32_t l_range = bat_curve[i].level - bat_curve[i + 1].level;
uint32_t v_offset = voltage_mv - bat_curve[i + 1].voltage_mv;
return bat_curve[i + 1].level + (uint8_t)((v_offset * l_range) / v_range);
}
}
return 0;
}
// 初始化ADC校准
static void battery_cali_init(void)
{
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
// ESP32-S3 使用曲线拟合校准
adc_cali_curve_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT_1,
.chan = BAT_ADC_CHANNEL,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali_handle);
#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
// 备用:线性拟合校准
adc_cali_line_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t ret = adc_cali_create_scheme_line_fitting(&cali_cfg, &cali_handle);
#else
esp_err_t ret = ESP_ERR_NOT_SUPPORTED;
#endif
if (ret == ESP_OK) {
cali_enabled = true;
ESP_LOGI(TAG, "ADC校准初始化成功");
} else {
ESP_LOGW(TAG, "ADC校准不可用将使用原始值换算");
}
}
esp_err_t dzbj_battery_init(void)
{
// 初始化ADC单元
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = ADC_UNIT_1,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
ESP_RETURN_ON_ERROR(adc_oneshot_new_unit(&unit_cfg, &adc_handle),
TAG, "ADC单元初始化失败");
// 配置ADC通道11dB衰减量程约0~2500mV
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
ESP_RETURN_ON_ERROR(adc_oneshot_config_channel(adc_handle, BAT_ADC_CHANNEL, &chan_cfg),
TAG, "ADC通道配置失败");
// 初始化校准
battery_cali_init();
ESP_LOGI(TAG, "电池ADC初始化完成 (GPIO%d, ADC1_CH%d, 分压比=%d)",
PIN_BAT_ADC, BAT_ADC_CHANNEL, BAT_VOLTAGE_DIVIDER);
return ESP_OK;
}
uint32_t dzbj_battery_get_voltage_mv(void)
{
return bat_voltage_mv;
}
uint8_t dzbj_battery_get_level(void)
{
return bat_level;
}
// 读取ADC并计算电池电压和电量
static void battery_read(void)
{
int adc_sum = 0;
int valid_count = 0;
// 多次采样取平均,滤除噪声
for (int i = 0; i < BAT_SAMPLE_COUNT; i++) {
int raw;
if (adc_oneshot_read(adc_handle, BAT_ADC_CHANNEL, &raw) == ESP_OK) {
adc_sum += raw;
valid_count++;
}
vTaskDelay(pdMS_TO_TICKS(2));
}
if (valid_count == 0) {
ESP_LOGE(TAG, "ADC采样全部失败");
return;
}
int adc_avg = adc_sum / valid_count;
// 使用校准值或原始换算得到ADC引脚电压
int adc_voltage_mv = 0;
if (cali_enabled) {
adc_cali_raw_to_voltage(cali_handle, adc_avg, &adc_voltage_mv);
} else {
// 无校准时按3300mV参考电压线性换算
adc_voltage_mv = (adc_avg * 3300) / 4095;
}
// 乘以分压系数得到实际电池电压
bat_voltage_mv = (uint32_t)adc_voltage_mv * BAT_VOLTAGE_DIVIDER;
// 查找表+插值计算电量百分比
bat_level = voltage_to_level(bat_voltage_mv);
ESP_LOGI(TAG, "ADC原始值=%d, ADC电压=%dmV, 电池电压=%lumV, 电量=%d%%",
adc_avg, adc_voltage_mv, (unsigned long)bat_voltage_mv, bat_level);
}
// 更新UI电量显示线程安全
static void battery_update_ui(void)
{
if (!lvgl_port_lock(100)) {
return;
}
char buf[8];
snprintf(buf, sizeof(buf), "%d%%", bat_level);
// 只更新ScreenSet界面的电量圆弧和标签
if (ui_ArcPowerLevel) {
lv_arc_set_value(ui_ArcPowerLevel, bat_level);
}
if (ui_LabelPowerLevel) {
lv_label_set_text(ui_LabelPowerLevel, buf);
}
// ScreenHome界面的Arc1和Label1保持默认值不关联电池电量
lvgl_port_unlock();
}
// 电池监控任务
static void battery_monitor_task(void *pvParameters)
{
while (1) {
battery_read();
battery_update_ui();
vTaskDelay(pdMS_TO_TICKS(BAT_MONITOR_INTERVAL_MS));
}
}
void dzbj_battery_monitor_start(void)
{
xTaskCreate(battery_monitor_task, "bat_mon", 4096, NULL, 3, NULL);
ESP_LOGI(TAG, "电池监控任务已启动,更新间隔%dms", BAT_MONITOR_INTERVAL_MS);
}

38
main/dzbj/dzbj_battery.h Normal file
View File

@ -0,0 +1,38 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_err.h"
#include <stdint.h>
// 电池ADC引脚配置
#define PIN_BAT_ADC 3 // GPIO3
#define BAT_ADC_CHANNEL ADC_CHANNEL_2 // ADC1_CH2
// 分压比(实际电池电压 = ADC测量电压 * 此系数)
// 根据硬件电路中的分压电阻调整1:1分压器设为2
#define BAT_VOLTAGE_DIVIDER 2
// 采样次数(取平均值,提高精度)
#define BAT_SAMPLE_COUNT 32
// 监控间隔(毫秒)
#define BAT_MONITOR_INTERVAL_MS 5000
// 初始化电池ADC检测
esp_err_t dzbj_battery_init(void);
// 获取电池电压(毫伏)
uint32_t dzbj_battery_get_voltage_mv(void);
// 获取电池电量百分比0-100
uint8_t dzbj_battery_get_level(void);
// 启动电池监控任务周期性读取ADC并更新UI
void dzbj_battery_monitor_start(void);
#ifdef __cplusplus
}
#endif

410
main/dzbj/dzbj_ble.c Normal file
View File

@ -0,0 +1,410 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gatt_common_api.h"
#include "esp_mac.h"
#include "fatfs.h"
#include "pages.h"
#define APP_ID_PLACEHOLDER 0
#define IMAGE_SERVICE_INSTID 0x0B
#define IMAGE_SERVICE_UUID 0x0B00
#define IMAGE_WRITE_UUID 0x0B01
#define IMAGE_EDIT_UUID 0x0B02
static uint16_t image_service_handle = 0;
static uint16_t image_write_handle = 0;
static uint16_t image_edit_handle = 0;
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
static const char *CONN_TAG = "DZBJ_BLE";
static char ble_device_name[32];
static uint8_t adv_raw_len = 0;
static uint16_t conn_id;
static char *filepath;
typedef struct
{
uint8_t type;
char filename[23];
uint32_t len;
} Megtype;
typedef struct{
bool isSend;
uint32_t port;
} MegStatus;
Megtype firstMeg;
MegStatus SendStatus = {false,0};
uint8_t *img_data = 0;
FILE *file_img;
// BLE 图片处理任务NVS 写入 + 导航显示在独立任务中执行,避免 BTC_TASK 栈溢出)
static TaskHandle_t ble_process_task_handle = NULL;
static char ble_pending_filename[24];
static uint8_t *ble_pending_data = NULL; // 传输完成的图片数据(直通显示,跳过 SPIFFS 重读)
static size_t ble_pending_data_size = 0;
static void ble_process_task(void *arg) {
while (1) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
nvs_change_img(ble_pending_filename);
ble_image_navigate_with_data(ble_pending_filename, ble_pending_data, ble_pending_data_size);
ble_pending_data = NULL; // 所有权已转移,不再释放
ble_pending_data_size = 0;
}
}
static uint8_t attr_value_write[512] = {0};
static uint8_t attr_value_edit[20] = {0};
static esp_attr_value_t char_val_image_write = {
.attr_max_len = 512,
.attr_len = 512,
.attr_value = attr_value_write
} ;
static esp_attr_value_t char_val_image_edit = {
.attr_max_len = 20,
.attr_len = 20,
.attr_value = attr_value_edit
} ;
static esp_attr_control_t control_image_write = {
.auto_rsp = ESP_GATT_AUTO_RSP
};
static esp_attr_control_t control_image_edit = {
.auto_rsp = ESP_GATT_AUTO_RSP
};
// 图片传输服务
static esp_gatt_srvc_id_t server_id_image = {
.id.uuid.len = ESP_UUID_LEN_16,
.id.uuid.uuid.uuid16 = IMAGE_SERVICE_UUID,
.id.inst_id = IMAGE_SERVICE_INSTID,
.is_primary = true,
};
static esp_bt_uuid_t image_write_uuid = {
.len = ESP_UUID_LEN_16,
.uuid.uuid16 = IMAGE_WRITE_UUID,
};
static esp_bt_uuid_t image_edit_uuid = {
.len = ESP_UUID_LEN_16,
.uuid.uuid16 = IMAGE_EDIT_UUID,
};
static esp_ble_adv_params_t adv_params = {
.adv_int_min = 0x20,
.adv_int_max = 0x20,
.adv_type = ADV_TYPE_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
static uint8_t adv_raw_data[31];
// Scan Response 数据:厂商标识 + 服务UUID
static uint8_t scan_rsp_data[] = {
0x07, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x4C, 0x44, 0x64, 0x7A, 0x62, 0x6A, // "LDdzbj"
0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0x00, 0x0B, // 服务UUID 0x0B00
};
void dzbj_ble_init(void)
{
esp_err_t ret;
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(CONN_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(CONN_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(CONN_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(CONN_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_ble_gap_register_callback(esp_gap_cb);
if (ret) {
ESP_LOGE(CONN_TAG, "%s gap register failed, error code = %x", __func__, ret);
return;
}
ret = esp_ble_gatts_register_callback(gatts_event_handler);
if (ret) {
ESP_LOGE(CONN_TAG, "%s gatts register failed, error code = %x", __func__, ret);
return;
}
ret = esp_ble_gatts_app_register(APP_ID_PLACEHOLDER);
if (ret) {
ESP_LOGE(CONN_TAG, "%s gatts app register failed, error code = %x", __func__, ret);
return;
}
ret = esp_ble_gatt_set_local_mtu(512);
if (ret) {
ESP_LOGE(CONN_TAG, "set local MTU failed, error code = %x", ret);
return;
}
// 获取 BLE MAC 地址并构建设备名称: Airhub_xx:xx:xx:xx:xx:xx
const uint8_t *ble_addr = esp_bt_dev_get_address();
if (ble_addr) {
snprintf(ble_device_name, sizeof(ble_device_name),
"Airhub_%02x:%02x:%02x:%02x:%02x:%02x",
ble_addr[0], ble_addr[1], ble_addr[2],
ble_addr[3], ble_addr[4], ble_addr[5]);
ESP_LOGI(CONN_TAG, "BLE MAC: %02x:%02x:%02x:%02x:%02x:%02x",
ble_addr[0], ble_addr[1], ble_addr[2],
ble_addr[3], ble_addr[4], ble_addr[5]);
} else {
strcpy(ble_device_name, "Airhub_BLE");
ESP_LOGW(CONN_TAG, "获取BLE MAC失败使用默认名称: %s", ble_device_name);
}
ret = esp_ble_gap_set_device_name(ble_device_name);
if (ret) {
ESP_LOGE(CONN_TAG, "set device name failed, error code = %x", ret);
return;
}
ESP_LOGI(CONN_TAG, "蓝牙设备名称: %s", ble_device_name);
// 构建广播数据: Flags + Complete Local Name
uint8_t name_len = strlen(ble_device_name);
int offset = 0;
adv_raw_data[offset++] = 0x02;
adv_raw_data[offset++] = ESP_BLE_AD_TYPE_FLAG;
adv_raw_data[offset++] = 0x06;
adv_raw_data[offset++] = name_len + 1;
adv_raw_data[offset++] = ESP_BLE_AD_TYPE_NAME_CMPL;
memcpy(&adv_raw_data[offset], ble_device_name, name_len);
offset += name_len;
adv_raw_len = offset;
ret = esp_ble_gap_config_adv_data_raw(adv_raw_data, adv_raw_len);
if (ret) {
ESP_LOGE(CONN_TAG, "config adv data failed, error code = %x", ret);
}
// 配置 Scan Response 数据(厂商标识 "dzbj" + 服务UUID
ret = esp_ble_gap_config_scan_rsp_data_raw(scan_rsp_data, sizeof(scan_rsp_data));
if (ret) {
ESP_LOGE(CONN_TAG, "config scan response data failed, error code = %x", ret);
}
// 创建图片处理任务8KB 栈,足够 SPIFFS 扫描 + LVGL + GIF 解码)
xTaskCreate(ble_process_task, "ble_img", 8192, NULL, 5, &ble_process_task_handle);
}
void dzbj_ble_deinit(void)
{
esp_ble_gap_stop_advertising();
esp_ble_gatts_app_unregister(0);
esp_bluedroid_disable();
esp_bluedroid_deinit();
esp_bt_controller_disable();
esp_bt_controller_deinit();
}
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
ESP_LOGI(CONN_TAG, "Advertising data set, status %d", param->adv_data_raw_cmpl.status);
// ADV 数据设置完成,等待 Scan Response 也设置完成后再开始广播
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
ESP_LOGI(CONN_TAG, "Scan response data set, status %d", param->scan_rsp_data_raw_cmpl.status);
esp_ble_gap_start_advertising(&adv_params);
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(CONN_TAG, "Advertising start failed, status %d", param->adv_start_cmpl.status);
break;
}
ESP_LOGI(CONN_TAG, "Advertising start successfully");
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(CONN_TAG, "Advertising stop failed, status %d", param->adv_stop_cmpl.status);
}
ESP_LOGI(CONN_TAG, "Advertising stop successfully");
break;
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
ESP_LOGI(CONN_TAG, "Connection params update, status %d, conn_int %d, latency %d, timeout %d",
param->update_conn_params.status,
param->update_conn_params.conn_int,
param->update_conn_params.latency,
param->update_conn_params.timeout);
break;
default:
break;
}
}
// GATT服务器事件处理函数
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
switch (event) {
case ESP_GATTS_REG_EVT:
ESP_LOGI(CONN_TAG, "GATT server register, status %d, app_id %d",param->reg.status, param->reg.app_id);
// 创建图片传输服务
esp_ble_gatts_create_service(gatts_if,&server_id_image,10);
break;
case ESP_GATTS_CREATE_EVT:
if (param->create.status == ESP_GATT_OK) {
image_service_handle = param->create.service_handle;
esp_ble_gatts_add_char(
image_service_handle,
&image_write_uuid,
ESP_GATT_PERM_WRITE,
ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_WRITE_NR,
&char_val_image_write,
&control_image_write
);
esp_ble_gatts_add_char(
image_service_handle,
&image_edit_uuid,
ESP_GATT_PERM_WRITE,
ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_WRITE_NR,
&char_val_image_edit,
&control_image_edit
);
ESP_LOGI(CONN_TAG, "图片传输服务创建成功,句柄: %x", image_service_handle);
} else {
ESP_LOGE(CONN_TAG, "服务创建失败,状态: %d", param->create.status);
}
break;
case ESP_GATTS_ADD_CHAR_EVT:
if (param->add_char.status == ESP_GATT_OK) {
if (param->add_char.char_uuid.uuid.uuid16 == (uint16_t)IMAGE_WRITE_UUID) {
image_write_handle = param->add_char.attr_handle;
ESP_LOGI(CONN_TAG, "图片写入特征创建成功,句柄: %d", image_write_handle);
} else if (param->add_char.char_uuid.uuid.uuid16 == (uint16_t)IMAGE_EDIT_UUID) {
image_edit_handle = param->add_char.attr_handle;
ESP_LOGI(CONN_TAG, "图片编辑特征创建成功,句柄: %d", image_edit_handle);
esp_ble_gatts_start_service(image_service_handle);
}
} else {
ESP_LOGE(CONN_TAG, "特征创建失败,状态: %d", param->add_char.status);
}
break;
case ESP_GATTS_WRITE_EVT:
if(param->write.handle == image_write_handle){
uint8_t *value = param->write.value;
if(!SendStatus.isSend){
ESP_LOGI(CONN_TAG, "处理前序数据");
firstMeg.type = value[0];
memcpy(firstMeg.filename, value + 1, 22);
firstMeg.filename[22] = '\0';
firstMeg.len = (value[23] << 16) | (value[24] << 8) | value[25];
ESP_LOGI(CONN_TAG, "图片数据长度:%d",(int)firstMeg.len);
if(firstMeg.type == 0xfd){
SendStatus.isSend = true;
img_data = malloc((int)firstMeg.len);
filepath = malloc(sizeof(char) * 33);
sprintf(filepath,"/spiflash/%s",firstMeg.filename);
file_img = fopen(filepath,"wb");
ESP_LOGI(CONN_TAG,"传输通道建立成功,数据指针:%p,文件名称:%s,文件大小:%d",img_data,firstMeg.filename,(int)firstMeg.len);
}
}else if(SendStatus.isSend){
uint8_t pkt_no = *value;
uint8_t isEnd = *(value + 1);
// 每 100 包或最后一包打印日志(减少串口输出提升传输速度)
if (pkt_no % 100 == 0 || isEnd) {
ESP_LOGI(CONN_TAG, "获取到数据:第:%d包,长度:%d,是否结束:%d", pkt_no+1, (int)param->write.len, isEnd);
}
uint8_t *data = value + 2;
memcpy(img_data + SendStatus.port,data,(int)param->write.len-2);
SendStatus.port += param->write.len-2;
if(isEnd){
ESP_LOGI(CONN_TAG,"数据接收完毕,累计:%d字节预期:%d字节首字节:%02X %02X",
(int)SendStatus.port,(int)firstMeg.len,img_data[0],img_data[1]);
fwrite(img_data,sizeof(uint8_t),firstMeg.len,file_img);
fclose(file_img);
SendStatus.isSend = false;
SendStatus.port = 0;
// img_data 不释放,传给显示任务直通显示(跳过 SPIFFS 重读)
ble_pending_data = img_data;
ble_pending_data_size = firstMeg.len;
img_data = NULL; // 转移所有权
free(filepath);
ESP_LOGI(CONN_TAG,"图片接收成功,数据直通显示(%d字节)", (int)ble_pending_data_size);
strncpy(ble_pending_filename, firstMeg.filename, sizeof(ble_pending_filename) - 1);
ble_pending_filename[sizeof(ble_pending_filename) - 1] = '\0';
xTaskNotifyGive(ble_process_task_handle);
}
}
}// 图片编辑特征写入事件
else if(param->write.handle == image_edit_handle){
uint8_t *value = param->write.value;
char imgName[23];
uint8_t type = *(value + param->write.len - 1);
memcpy(imgName, value, 23);
if(type == 0xff){
// 耗时操作转移到独立任务执行
strncpy(ble_pending_filename, imgName, sizeof(ble_pending_filename) - 1);
ble_pending_filename[sizeof(ble_pending_filename) - 1] = '\0';
xTaskNotifyGive(ble_process_task_handle);
}else if(type == 0xF1){
remove(filepath);
SendStatus.isSend = false;
SendStatus.port = 0;
free(img_data);
free(filepath);
}
}
break;
case ESP_GATTS_CONNECT_EVT: {
esp_ble_conn_update_params_t conn_params = {0};
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
conn_params.latency = 0;
conn_params.max_int = 16; // 16 × 1.25ms = 20ms缩短连接间隔提升传输吞吐量
conn_params.min_int = 6; // 6 × 1.25ms = 7.5ms
conn_params.timeout = 400;
conn_id = param->connect.conn_id;
ESP_LOGI(CONN_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR"",
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda));
esp_ble_gap_update_conn_params(&conn_params);
break;
}
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(CONN_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
esp_ble_gap_start_advertising(&adv_params);
break;
default:
break;
}
}

15
main/dzbj/dzbj_ble.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
// 初始化 BLE 图传服务
void dzbj_ble_init(void);
// 反初始化 BLE 图传服务(释放蓝牙协议栈资源)
void dzbj_ble_deinit(void);
#ifdef __cplusplus
}
#endif

155
main/dzbj/dzbj_button.c Normal file
View File

@ -0,0 +1,155 @@
#include "dzbj_button.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "DZBJ_BTN";
// 去抖间隔(微秒)
#define DEBOUNCE_US 200000
// 按键事件队列
static QueueHandle_t btn_evt_queue = NULL;
// 回调存储
typedef struct {
btn_event_cb_t cb;
void *usr_data;
} btn_cb_t;
static btn_cb_t boot_cb = {0};
static btn_cb_t key2_cb = {0};
// 去抖时间戳
static int64_t last_boot_us = 0;
static int64_t last_key2_us = 0;
// GPIO中断服务函数ISR中不做耗时操作仅发送事件到队列
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
int gpio_num = (int)arg;
xQueueSendFromISR(btn_evt_queue, &gpio_num, NULL);
}
// 按键事件处理任务
static void btn_task(void *pvParameters)
{
int gpio_num;
while (1) {
if (xQueueReceive(btn_evt_queue, &gpio_num, portMAX_DELAY)) {
int64_t now = esp_timer_get_time();
// BOOT(GPIO0) 由 iot_button 处理,这里仅处理 KEY2
if (gpio_num == PIN_BTN_KEY2) {
if (now - last_key2_us > DEBOUNCE_US) {
last_key2_us = now;
ESP_LOGI(TAG, "KEY2按键按下 (GPIO%d)", gpio_num);
if (key2_cb.cb) {
key2_cb.cb(gpio_num, key2_cb.usr_data);
}
}
}
}
}
}
esp_err_t dzbj_button_init(void)
{
btn_evt_queue = xQueueCreate(10, sizeof(int));
// 仅配置 KEY2(GPIO4)BOOT(GPIO0) 由 iot_button 统一处理
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << PIN_BTN_KEY2),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&io_conf);
// 安装GPIO中断服务如果已安装则跳过
esp_err_t ret = gpio_install_isr_service(0);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "GPIO ISR服务安装失败");
return ret;
}
gpio_isr_handler_add(PIN_BTN_KEY2, gpio_isr_handler, (void *)PIN_BTN_KEY2);
// 按键处理任务
xTaskCreate(btn_task, "btn_task", 3072, NULL, 5, NULL);
ESP_LOGI(TAG, "按键初始化完成 (KEY2=GPIO%dBOOT由iot_button处理)", PIN_BTN_KEY2);
return ESP_OK;
}
void dzbj_button_on_boot_press(btn_event_cb_t cb, void *usr_data)
{
boot_cb.cb = cb;
boot_cb.usr_data = usr_data;
}
void dzbj_button_on_key2_press(btn_event_cb_t cb, void *usr_data)
{
key2_cb.cb = cb;
key2_cb.usr_data = usr_data;
}
// === 吧唧模式 BOOT 单击处理(从 dzbj main.c boot_btn_handler 移植) ===
#include "lvgl.h"
#include "sleep_mgr/include/sleep_mgr.h"
// UI 函数前向声明
extern lv_obj_t *ui_ScreenHome;
extern lv_obj_t *ui_ScreenImg;
extern void ui_ScreenHome_screen_init(void);
extern void ui_ScreenImg_hide_delete_container(void);
extern void _ui_screen_change(lv_obj_t **target, lv_scr_load_anim_t fademode, int spd, int delay, void (*target_init)(void));
extern bool flashlight_is_active(void);
extern uint8_t flashlight_get_saved_brightness(void);
extern void flashlight_exit(void);
extern void pwm_set_brightness(uint8_t percent);
void dzbj_boot_click_handler(void)
{
bool screen_was_off = sleep_mgr_is_screen_off();
if (screen_was_off) {
// 低功耗模式:只唤醒屏幕
ESP_LOGI(TAG, "吧唧模式 BOOT低功耗模式仅唤醒屏幕");
sleep_mgr_notify_activity();
} else {
// 正常模式:退出手电筒 + 返回ScreenHome
ESP_LOGI(TAG, "吧唧模式 BOOT返回ScreenHome");
// 如果在ScreenImg界面先隐藏删除容器
lv_obj_t *current_screen = lv_scr_act();
if (current_screen == ui_ScreenImg) {
ui_ScreenImg_hide_delete_container();
}
sleep_mgr_notify_activity();
// 退出手电筒
bool was_flashlight = flashlight_is_active();
uint8_t saved_brightness = 0;
if (was_flashlight) {
saved_brightness = flashlight_get_saved_brightness();
flashlight_exit();
vTaskDelay(pdMS_TO_TICKS(80));
}
// 切换到Home界面
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
// 手电筒退出后恢复亮度
if (was_flashlight) {
vTaskDelay(pdMS_TO_TICKS(150));
pwm_set_brightness(saved_brightness);
ESP_LOGI(TAG, "亮度已恢复到%d%%", saved_brightness);
}
}
}

30
main/dzbj/dzbj_button.h Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_err.h"
// 按键引脚定义
#define PIN_BTN_BOOT 0 // GPIO0 BOOT按键低电平有效
#define PIN_BTN_KEY2 4 // GPIO4 KEY2按键低电平有效
// 按键事件回调函数类型
typedef void (*btn_event_cb_t)(int gpio_num, void *usr_data);
// 初始化按键驱动GPIO中断 + 软件去抖)
esp_err_t dzbj_button_init(void);
// 注册BOOT按键按下回调
void dzbj_button_on_boot_press(btn_event_cb_t cb, void *usr_data);
// 注册KEY2按键按下回调
void dzbj_button_on_key2_press(btn_event_cb_t cb, void *usr_data);
// 吧唧模式 BOOT 单击处理(唤醒屏幕 / 退出手电筒 / 返回Home
void dzbj_boot_click_handler(void);
#ifdef __cplusplus
}
#endif

248
main/dzbj/fatfs.c Normal file
View File

@ -0,0 +1,248 @@
/**
* @file fatfs.c
* @brief SPIFFS dzbj
*
* SPIFFS JPEG
*/
#include "esp_err.h"
#include "esp_log.h"
#include "esp_spiffs.h"
#include "fatfs.h"
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
static const char *TAG = "FATFS";
// 初始化SPIFFS文件系统
void fatfs_init(void) {
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiflash",
.partition_label = "storage",
.max_files = 5,
.format_if_mount_failed = true,
};
esp_err_t err = esp_vfs_spiffs_register(&conf);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to mount SPIFFS (%s)", esp_err_to_name(err));
return;
}
size_t total = 0, used = 0;
err = esp_spiffs_info("storage", &total, &used);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS info (%s)", esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "SPIFFS: Total size: %d, Used: %d", total, used);
}
}
// 读取图片数据到内存
void read_img(uint8_t *img_p) {
FILE *f = fopen("/spiflash/img.bin", "r");
if (f == NULL) {
ESP_LOGE(TAG, "OPEN ERROR");
return;
}
size_t size = fread(img_p, sizeof(uint8_t), 129600 * 2, f);
fclose(f);
if (size != 0) {
ESP_LOGI(TAG, "read success!");
}
}
// 测试FATFS文件系统
void fs_test(void) {
FILE *f = fopen("/spiflash/img.bin", "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for reading");
return;
}
uint8_t line[2];
fread(line, sizeof(uint8_t), 2, f);
fclose(f);
ESP_LOGI(TAG, "Read from file: %x %x", line[0], line[1]);
}
// 列出目录下所有文件名
void fatfs_list_all_filenames(const char *dir_path, bool recursive) {
DIR *dir = opendir(dir_path);
if (dir == NULL) {
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
char full_path[512];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
struct stat file_stat;
if (stat(full_path, &file_stat) == 0 && S_ISDIR(file_stat.st_mode)) {
if (recursive) {
fatfs_list_all_filenames(full_path, recursive);
}
} else if (stat(full_path, &file_stat) == 0 && S_ISREG(file_stat.st_mode)) {
ESP_LOGI(TAG, "文件名: %s, 大小:%d", full_path, (int)file_stat.st_size);
}
}
closedir(dir);
}
// 删除目录下所有空文件
void fatfs_remove_nullData(const char *dir_path) {
DIR *dir = opendir(dir_path);
if (dir == NULL) {
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
char full_path[512];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, (char*)entry->d_name);
struct stat file_stat;
stat(full_path, &file_stat);
if ((int)file_stat.st_size == 0) {
remove(full_path);
ESP_LOGE(TAG, "删除空文件: %s", full_path);
}
}
closedir(dir);
}
// 删除目录下所有文件
void fatfs_remove_allData(const char *dir_path) {
DIR *dir = opendir(dir_path);
if (dir == NULL) {
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
char full_path[512];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, (char*)entry->d_name);
remove(full_path);
ESP_LOGE(TAG, "删除文件: %s", full_path);
}
closedir(dir);
}
// 检查图片是否有效文件大小不为0
bool fats_img_isOK(char* img_path) {
struct stat file_stat;
stat(img_path, &file_stat);
return file_stat.st_size > 0;
}
// JPEG 解码:从 SPIFFS 读取 JPEG 并解码为 RGB565
esp_err_t DecodeImg(char *imgpath, uint8_t** imgData, esp_jpeg_image_output_t *outimage) {
FILE *f = fopen(imgpath, "rb");
if (f == NULL) {
ESP_LOGE(TAG, "OPEN ERROR: %s", imgpath);
return ESP_FAIL;
}
struct stat file_stat;
stat(imgpath, &file_stat);
// 分配输出缓冲区360×360 RGB565
*imgData = malloc(360 * 360 * 2);
if (*imgData == NULL) {
ESP_LOGE(TAG, "输出缓冲区分配失败");
fclose(f);
return ESP_FAIL;
}
// 分配输入缓冲区JPEG 原始数据)
uint8_t *imgEncoderData = malloc(file_stat.st_size);
if (imgEncoderData == NULL) {
ESP_LOGE(TAG, "输入缓冲区分配失败(需%d字节", (int)file_stat.st_size);
free(*imgData);
*imgData = NULL;
fclose(f);
return ESP_FAIL;
}
size_t read_len = fread(imgEncoderData, sizeof(uint8_t), file_stat.st_size, f);
fclose(f);
if (read_len != (size_t)file_stat.st_size) {
ESP_LOGE(TAG, "文件读取不完整(预期:%d实际%zu",
(int)file_stat.st_size, read_len);
free(imgEncoderData);
free(*imgData);
*imgData = NULL;
return ESP_FAIL;
}
// 验证 JPEG 头
if (file_stat.st_size < 2 || imgEncoderData[0] != 0xFF || imgEncoderData[1] != 0xD8) {
ESP_LOGE(TAG, "不是有效JPEG文件: %s", imgpath);
free(imgEncoderData);
free(*imgData);
*imgData = NULL;
return ESP_FAIL;
}
uint32_t outbuf_size = 360 * 360 * sizeof(uint8_t) * 2;
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = imgEncoderData,
.indata_size = file_stat.st_size,
.outbuf = *imgData,
.outbuf_size = outbuf_size,
.out_format = JPEG_IMAGE_FORMAT_RGB565,
.flags = {
.swap_color_bytes = true,
},
};
esp_err_t ret = esp_jpeg_decode(&jpeg_cfg, outimage);
free(imgEncoderData);
return ret;
}
// 测试读取图片数据
void test_readimg(char *imgpath, uint16_t size) {
FILE *f = fopen(imgpath, "r");
if (f == NULL) {
ESP_LOGE(TAG, "OPEN ERROR");
return;
}
uint8_t *head = malloc(size);
if (head == NULL) {
fclose(f);
return;
}
fread(head, sizeof(uint8_t), size, f);
fclose(f);
for (int i = 0; i < size; i++) {
printf("%x ", *(head + i));
}
printf("\n");
free(head);
}
// 获取目录下所有图片文件名
void fat_getAllimgList(const char *dir_path, char** list, uint8_t* num) {
*num = 0;
DIR *dir = opendir(dir_path);
if (dir == NULL) {
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
list[*num] = strdup(entry->d_name);
(*num)++;
}
closedir(dir);
}

22
main/dzbj/fatfs.h Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "jpeg_decoder.h"
void fatfs_init(void);
void fs_test(void);
void read_img(uint8_t *img);
void fatfs_list_all_filenames(const char *dir_path, bool recursive);
void fatfs_remove_nullData(const char *dir_path);
esp_err_t DecodeImg(char *imgpath, uint8_t** imgData, esp_jpeg_image_output_t *outimage);
void test_readimg(char* imgpath, uint16_t size);
void fatfs_remove_allData(const char *dir_path);
bool fats_img_isOK(char* img_path);
void fat_getAllimgList(const char *dir_path, char** list, uint8_t* num);
#ifdef __cplusplus
}
#endif

1042
main/dzbj/pages.c Normal file

File diff suppressed because it is too large Load Diff

37
main/dzbj/pages.h Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
void app_test_display(); // 测试显示
void app_img_display(); // 显示图片
esp_err_t nvs_change_img(char *imgname); // 改变NVS中的图片路径
void app_img_change(const char *img_name); // 改变图片
void img_switch_task(void *pvParameters); // 图片切换任务
void img_loop_task(void *pvParameters); // 图片循环任务
// 图片管理函数
const char* get_current_image(void); // 获取当前图片文件名
bool delete_current_image(void); // 删除当前图片
void init_spiffs_image_list(void); // 初始化/扫描SPIFFS图片列表
void free_spiffs_image_list(void); // 重置图片列表
bool set_image_index_by_name(const char *name); // 根据文件名设置当前图片索引
const char* get_next_image(void); // 获取下一张图片
const char* get_prev_image(void); // 获取上一张图片
void update_ui_ImgBle(const char *img_name); // 更新ui_ImgBle控件的图片
void ble_image_navigate(const char *filename); // BLE接收后导航到ScreenImg显示
void ble_image_navigate_with_data(const char *filename, uint8_t *data, size_t data_size); // BLE接收后直通显示跳过SPIFFS重读
#if LV_USE_GIF
void pages_cleanup_gif(void); // 清理 GIF 控件资源
#endif
#ifdef __cplusplus
}
#endif

229
main/dzbj/sleep_mgr.c Normal file
View File

@ -0,0 +1,229 @@
#include "../sleep_mgr/include/sleep_mgr.h"
#include "dzbj_button.h"
#include "pages.h"
#include "pages_pwm.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_lvgl_port.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lvgl.h"
#include "../ui/screens/ui_ScreenSet.h"
#include "lcd.h"
#include <stdio.h>
static const char *TAG = "SLEEP";
static bool sleep_enabled = false;
static bool screen_off = false;
static int64_t last_activity_us = 0;
static uint8_t saved_brightness = 50;
static const uint8_t DEFAULT_BRIGHTNESS = 50; // 默认亮度
static const uint8_t SLEEP_MODE_BRIGHTNESS = 10; // 休眠模式亮度
// 通知有用户活动
void sleep_mgr_notify_activity(void)
{
last_activity_us = esp_timer_get_time();
// 如果屏幕已关闭,立即唤醒
if (screen_off) {
screen_off = false;
// 恢复 LVGL 和触摸输入
if (lvgl_port_lock(100)) {
// 1. 启用所有输入设备(恢复触摸事件处理)
lv_indev_t *indev = lv_indev_get_next(NULL);
while (indev) {
lv_indev_enable(indev, true);
ESP_LOGI(TAG, "输入设备已启用");
indev = lv_indev_get_next(indev);
}
// 2. 恢复刷新定时器(恢复屏幕重绘)
lv_timer_t *refr_timer = _lv_disp_get_refr_timer(NULL);
if (refr_timer) {
lv_timer_resume(refr_timer);
ESP_LOGI(TAG, "LVGL 刷新定时器已恢复");
}
// 3. 强制刷新当前屏幕因为GRAM被清空为黑色需要重绘
lv_obj_invalidate(lv_scr_act());
ESP_LOGI(TAG, "已标记屏幕需要重绘");
lvgl_port_unlock();
}
// 延迟50ms等待LVGL完成至少一次重绘避免看到黑屏
vTaskDelay(pdMS_TO_TICKS(50));
// 恢复背光
pwm_set_brightness(saved_brightness);
ESP_LOGI(TAG, "屏幕唤醒,恢复亮度%d%%", saved_brightness);
}
}
// 按键活动回调BOOT和KEY2共用
static void btn_activity_cb(int gpio_num, void *usr_data)
{
sleep_mgr_notify_activity();
}
// 关闭屏幕(熄屏进入低功耗)
static void screen_turn_off(void)
{
if (screen_off) return;
// 保存当前亮度
saved_brightness = pwm_get_brightness();
if (saved_brightness == 0) {
saved_brightness = 50; // 防止保存到0值
}
// 暂停 LVGL 并禁用触摸输入
if (lvgl_port_lock(100)) {
// 1. 暂停刷新定时器(停止屏幕重绘)
lv_timer_t *refr_timer = _lv_disp_get_refr_timer(NULL);
if (refr_timer) {
lv_timer_pause(refr_timer);
ESP_LOGI(TAG, "LVGL 刷新定时器已暂停");
}
// 2. 禁用所有输入设备(停止触摸事件处理)
lv_indev_t *indev = lv_indev_get_next(NULL);
while (indev) {
lv_indev_enable(indev, false);
ESP_LOGI(TAG, "输入设备已禁用");
indev = lv_indev_get_next(indev);
}
lvgl_port_unlock();
}
// 清空LCD GRAM为黑色避免关闭背光后看到残影
lcd_clear_screen_black();
// 关闭背光
screen_off = true;
pwm_set_brightness(0);
ESP_LOGI(TAG, "屏幕已关闭(亮度=%d%%系统进入真正低功耗模式Light Sleep + LVGL暂停 + LCD GRAM清空", saved_brightness);
}
// 休眠管理任务
static void sleep_mgr_task(void *pvParameters)
{
while (1) {
uint32_t delay_ms = 500; // 默认轮询间隔 500ms
if (sleep_enabled) {
// 检查LVGL触摸活动屏幕开启时
if (!screen_off) {
if (lvgl_port_lock(50)) {
uint32_t inactive_ms = lv_disp_get_inactive_time(NULL);
lvgl_port_unlock();
// 屏幕开启状态:检测到新触摸(< 500ms立即更新活动时间
if (inactive_ms < 500) {
sleep_mgr_notify_activity();
}
}
// 检查超时熄屏
int64_t now = esp_timer_get_time();
int64_t elapsed_ms = (now - last_activity_us) / 1000;
if (elapsed_ms >= SLEEP_TIMEOUT_MS) {
screen_turn_off();
}
}
// 屏幕关闭状态:禁用触摸唤醒,只允许按键唤醒
// 如需启用触摸唤醒,取消注释以下代码:
/*
else {
if (lvgl_port_lock(50)) {
uint32_t inactive_ms = lv_disp_get_inactive_time(NULL);
lvgl_port_unlock();
// 检测到触摸(< 2000ms立即唤醒
if (inactive_ms < 2000) {
sleep_mgr_notify_activity();
ESP_LOGI(TAG, "触摸唤醒屏幕inactive=%lums", inactive_ms);
}
}
}
*/
}
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
}
void sleep_mgr_init(void)
{
last_activity_us = esp_timer_get_time();
// 注意BOOT按键由main.c的boot_btn_handler统一处理唤醒+退出手电筒+返回Home
// 这里只注册KEY2按键唤醒功能
dzbj_button_on_key2_press(btn_activity_cb, NULL);
xTaskCreate(sleep_mgr_task, "sleep_mgr", 3072, NULL, 3, NULL);
ESP_LOGI(TAG, "休眠管理器初始化完成(超时=%ds", SLEEP_TIMEOUT_MS / 1000);
}
// 更新ScreenSet界面的亮度UI控件
static void update_brightness_ui(uint8_t brightness)
{
if (!lvgl_port_lock(100)) {
return;
}
// 更新滑块位置
if (ui_SliderBrightness) {
lv_slider_set_value(ui_SliderBrightness, brightness, LV_ANIM_OFF);
}
// 更新亮度文本标签
if (ui_LabelBrightness) {
char buf[8];
snprintf(buf, sizeof(buf), "%d%%", brightness);
lv_label_set_text(ui_LabelBrightness, buf);
}
lvgl_port_unlock();
}
void sleep_mgr_set_enabled(bool enabled)
{
sleep_enabled = enabled;
if (enabled) {
last_activity_us = esp_timer_get_time();
// 进入休眠模式时将亮度调节到10%
pwm_set_brightness(SLEEP_MODE_BRIGHTNESS);
update_brightness_ui(SLEEP_MODE_BRIGHTNESS);
ESP_LOGI(TAG, "休眠模式已启用,亮度已调节至%d%%%ds无操作将熄屏",
SLEEP_MODE_BRIGHTNESS, SLEEP_TIMEOUT_MS / 1000);
} else {
// 禁用休眠模式时恢复到默认亮度50%
if (screen_off) {
screen_off = false;
pwm_set_brightness(DEFAULT_BRIGHTNESS);
update_brightness_ui(DEFAULT_BRIGHTNESS);
ESP_LOGI(TAG, "休眠模式已禁用,屏幕已恢复,亮度恢复到%d%%", DEFAULT_BRIGHTNESS);
} else {
pwm_set_brightness(DEFAULT_BRIGHTNESS);
update_brightness_ui(DEFAULT_BRIGHTNESS);
ESP_LOGI(TAG, "休眠模式已禁用,亮度恢复到%d%%", DEFAULT_BRIGHTNESS);
}
}
}
bool sleep_mgr_is_enabled(void)
{
return sleep_enabled;
}
bool sleep_mgr_is_screen_off(void)
{
return screen_off;
}

View File

@ -15,6 +15,8 @@ dependencies:
esp_lcd_st77916: "1.0.1" esp_lcd_st77916: "1.0.1"
esp_lcd_touch: "1.1.2" esp_lcd_touch: "1.1.2"
esp_lcd_touch_cst816s: "1.1.0" esp_lcd_touch_cst816s: "1.1.0"
## JPEG 解码dzbj 图片显示)
esp_jpeg: "*"
## Required IDF version ## Required IDF version
idf: idf:
version: ">=5.3" version: ">=5.3"

View File

@ -1,13 +1,29 @@
#ifndef _SLEEP_MGR_STUB_H_ #pragma once
#define _SLEEP_MGR_STUB_H_
// Stub 头文件dzbj ui_ScreenSet.c 引用Phase 1 仅提供声明
// 实际实现将在后续阶段添加
#include <stdbool.h> #include <stdbool.h>
static inline void sleep_mgr_set_enabled(bool enabled) { // 休眠超时时间(毫秒)
(void)enabled; #define SLEEP_TIMEOUT_MS 10000
}
#endif // _SLEEP_MGR_STUB_H_ #ifdef __cplusplus
extern "C" {
#endif
// 初始化休眠管理器需在UI、按键初始化之后调用
void sleep_mgr_init(void);
// 启用/禁用休眠模式
void sleep_mgr_set_enabled(bool enabled);
// 获取休眠模式是否启用
bool sleep_mgr_is_enabled(void);
// 通知有用户活动(按键按下、触摸屏幕时调用)
void sleep_mgr_notify_activity(void);
// 查询屏幕是否已关闭
bool sleep_mgr_is_screen_off(void);
#ifdef __cplusplus
}
#endif

View File

@ -95,6 +95,7 @@ static void lvgl_port_touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *
data->point.x = touchpad_x[0]; data->point.x = touchpad_x[0];
data->point.y = touchpad_y[0]; data->point.y = touchpad_y[0];
data->state = LV_INDEV_STATE_PRESSED; data->state = LV_INDEV_STATE_PRESSED;
ESP_LOGI(TAG, "Touch detected: x=%d, y=%d, count=%d", touchpad_x[0], touchpad_y[0], touchpad_cnt);
} else { } else {
data->state = LV_INDEV_STATE_RELEASED; data->state = LV_INDEV_STATE_RELEASED;
} }

View File

@ -6,3 +6,4 @@ phy_init, data, phy, 0xf000, 0x1000,
model, data, spiffs, 0x10000, 0x300000, model, data, spiffs, 0x10000, 0x300000,
ota_0, app, ota_0, 0x310000, 5M, ota_0, app, ota_0, 0x310000, 5M,
ota_1, app, ota_1, 0x820000, 5M, ota_1, app, ota_1, 0x820000, 5M,
storage, data, spiffs, 0xD20000, 0x2E0000,

1 # ESP-IDF Partition Table
6 model, data, spiffs, 0x10000, 0x300000,
7 ota_0, app, ota_0, 0x310000, 5M,
8 ota_1, app, ota_1, 0x820000, 5M,
9 storage, data, spiffs, 0xD20000, 0x2E0000,

View File

@ -2761,6 +2761,12 @@ CONFIG_CODEC_TAS5805M_SUPPORT=y
# CONFIG_CODEC_CJC8910_SUPPORT is not set # CONFIG_CODEC_CJC8910_SUPPORT is not set
# end of Audio Codec Device Configuration # end of Audio Codec Device Configuration
#
# JPEG Decoder
#
CONFIG_JD_USE_ROM=y
# end of JPEG Decoder
# #
# ESP LCD TOUCH # ESP LCD TOUCH
# #