diff --git a/05-最新日志.txt b/05-最新日志.txt index 4706fd6..2cebcc4 100644 --- a/05-最新日志.txt +++ b/05-最新日志.txt @@ -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 -ools/idf_monitor.py' -p /dev/tty.usbmodem834401 -b 115200 --toolchain-prefix xtensa-esp32s3-elf- --make ''/Users/rdzleo/.espressif/python_env/idf5.4_py3 -.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' ---- Warning: Serial ports accessed as /dev/tty.* will hang gdb if launched. ---- Using /dev/cu.usbmodem834401 instead... ---- esp-idf-monitor 1.8.0 on /dev/cu.usbmodem834401 115200 ---- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H +I (49) WeatherApi: 初始化天气API配置 - 默认城市: 北京 +I (49) WeatherApi: WiFi位置缓存限制已设置为: 5 条 +I (50) coexist: coex firmware version: 831ec70 +I (50) coexist: coexist rom version e7ae62f +I (51) main_task: Started on CPU0 +I (61) main_task: Calling app_main() +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::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::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 Build:Mar 27 2021 -rst:0x15 (USB_UART_CHIP_RESET),boot:0xb (SPI_FAST_FLASH_BOOT) -Saved PC:0x42143b2f ---- 0x42143b2f: timer_process_alarm at /Users/rdzleo/esp/esp-idf/components/esp_timer/src/esp_timer.c:413 -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按键触发:设备状态=1,WiFi连接状态=未连接 -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 +rst:0xc (RTC_SW_CPU_RST),boot:0x2b (SPI_FAST_FLASH_BOOT) +Saved PC:0x40379e89 +--- 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 @@ -210,307 +47,38 @@ load:0x403c8704,len:0xb88 load:0x403cb700,len:0x2df4 entry 0x403c88f4 I (49) WeatherApi: 初始化天气API配置 - 默认城市: 北京 -I (50) WeatherApi: WiFi位置缓存限制已设置为: 5 条 +I (49) WeatherApi: WiFi位置缓存限制已设置为: 5 条 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 (61) main_task: Calling app_main() -I (81) BackgroundTask: background_task started -I (81) BluetoothProvisioning: 蓝牙配网对象创建完成 -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 (81) Application: 🎴 吧唧模式:跳过 WiFi/协议/音频初始化 +I (81) Application: 打印设置设备状态日志: idle -I (24861) BluetoothProvisioning: 🔍 WiFi扫描已启动 -I (33621) BluetoothProvisioning: 📡 WiFi扫描完成,准备发送WiFi列表 -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 +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 -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: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::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::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 - -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 \ No newline at end of file +Rebooting... \ No newline at end of file diff --git a/06-AI对话和电子吧唧双模式适配说明.md b/06-AI对话和电子吧唧双模式适配说明.md index 655191c..9706511 100644 --- a/06-AI对话和电子吧唧双模式适配说明.md +++ b/06-AI对话和电子吧唧双模式适配说明.md @@ -1,9 +1,9 @@ -# AI对话 + 电子吧唧 双模式适配可行性分析 +# AI对话 + 电子吧唧 双模式适配说明 -> 分析日期:2026-02-27 +> 更新日期:2026-02-27 > 硬件平台:movecall-moji-esp32s3 (ESP32-S3-N16R8) > ESP-IDF版本:5.4.2 -> LVGL版本:8.3.11 (dzbj项目) +> LVGL版本:8.3.11 --- @@ -11,55 +11,31 @@ ### 1.1 主项目 (Baji_Rtc_Toy) -基于 AI小智 开源项目改造,当前功能: +基于 AI小智 开源项目改造,当前已集成功能: - 火山引擎 RTC 语音对话(WiFi 连接) - BLE 配网(Bluedroid,Service 0xABF0) - 音频编解码(ES8311 + Opus) - 唤醒词检测(esp-sr AFE) -- **无 LCD 显示**(`lcd_display.cc` 已注释,managed_components 中无 LVGL) - -关键文件: -- `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 接口,未编译) +- **LVGL 8.3.11 LCD 显示**(Phase 1 已完成,开机显示 ScreenHome) +- ST77916 QSPI 360×360 LCD + CST816S 触摸(已初始化) ### 1.2 dzbj 子项目 (电子吧唧) -独立的 ESP32-S3 LVGL 项目,位于 `/dzbj/` 目录,当前功能: +独立的 ESP32-S3 LVGL 项目,位于 `/dzbj/` 目录,功能: - 360×360 ST77916 QSPI LCD + CST816S 触摸 - LVGL 8.3.11 三屏界面(ScreenHome/ScreenImg/ScreenSet) - BLE GATT 图片传输服务(Service 0x0B00) - GIF 播放、JPEG 解码、SPIFFS 图片管理 -- 低功耗休眠/唤醒管理 -- PWM 背光控制、手电筒功能 +- 低功耗休眠/唤醒管理(10s 超时熄屏) +- PWM 背光控制 -关键文件: -- `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 实施进度 -### 1.3 AI小智原生 LVGL 版本 - -**AI小智原生项目不使用 LVGL 9.2.2**。实际情况: - -| 项目 | LVGL 版本 | 状态 | -|------|-----------|------| -| 主项目 (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种)、聊天气泡(微信风格)、主题切换(深色/浅色) 功能,但当前未编译链接。 +| 阶段 | 状态 | 说明 | +|------|------|------| +| Phase 1: 点亮屏幕 | **已完成** | LCD + LVGL + ScreenHome 显示 | +| Phase 2+4: 完整模式 + 切换 | **实施中** | 移植 dzbj 全模块 + 双模式切换 | +| Phase 3: AI 聊天 UI | 待定 | 基于 LVGL 的 emoji + 聊天气泡 | --- @@ -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 │ -│ emoji 表情显示 │ ScreenHome/Img/Set │ -│ 聊天气泡文本 │ 图片浏览 + GIF │ -│ 唤醒词检测 │ BLE 图片传输 │ -│ 音频编解码 │ 手电筒/低功耗 │ +│ ScreenHome (仅显示) │ ScreenHome/Img/Set │ +│ 音频编解码 + 唤醒词 │ BLE 图片传输 (0x0B00) │ +│ PowerSaveTimer │ sleep_mgr (10s熄屏) │ +│ IMU 传感器 │ battery 电池监测 │ +│ 电量检测 (板级) │ SPIFFS 图片管理 │ ├────────────────────┴────────────────────────────┤ -│ 长按 BOOT 5秒 切换 │ -│ AI→吧唧: 关WiFi+RTC → 启BLE → 切换UI │ -│ 吧唧→AI: 关BLE → 启WiFi+RTC → 切换UI │ +│ BOOT 双击 切换(写NVS + 重启) │ └─────────────────────────────────────────────────┘ ``` ### 2.2 模式定义 -**AI 聊天模式**: +**AI 对话模式** (device_mode=0, 默认): - 网络:WiFi 连接 - 协议:火山引擎 RTC 实时对话 - 音频:唤醒词检测 + Opus 编解码 + I2S 输出 -- 显示:emoji 表情 + 聊天气泡文本 + 状态栏 -- BLE:**关闭** +- 显示:ScreenHome(仅显示,无触摸交互) +- BLE:**关闭**(仅配网时启动) -**电子吧唧模式**: +**电子吧唧模式** (device_mode=1): - 网络:**WiFi 关闭** -- BLE:GATT Server(图片传输 + 配网服务) -- 显示:ScreenHome(主界面)→ ScreenImg(图片浏览)→ ScreenSet(设置) -- 功能:GIF 播放、JPEG 解码、SPIFFS 图片管理、手电筒、低功耗 +- BLE:GATT Server(图片传输服务 0x0B00) +- 显示:ScreenHome → ScreenImg(图片浏览)→ ScreenSet(设置) +- 功能: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 硬件规格 @@ -111,228 +103,149 @@ AI小智框架的 `LcdDisplay` 类已预留 emoji 表情(21种)、聊天气泡( - **PSRAM**:8MB OCT-SPI 80MHz - **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 | **可行**(偏紧) | -| 电子吧唧模式(单独) | ~190KB | ~144KB | **可行**(充裕) | +| AI 对话模式(WiFi+RTC+音频+LVGL) | ~212-237KB | ~97-122KB | **可行**(偏紧) | +| 电子吧唧模式(BLE+LVGL+SPIFFS) | ~190KB | ~144KB | **可行**(充裕) | | 两模式同时运行 | ~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 音频通道 - │ ├─ volc_rtc_stop() + volc_rtc_destroy() // 销毁 RTC 实例 - │ ├─ StopAudioProcessor() // 停止音频处理器 - │ ├─ 停止唤醒词检测 - │ └─ esp_wifi_stop() + esp_wifi_deinit() // 完全释放 WiFi - │ → 释放 ~83-108KB 内部 SRAM - │ - ├─ 2. 启动吧唧资源 - │ ├─ esp_bt_controller_init() // 初始化 BLE 控制器 - │ ├─ esp_bluedroid_init() + enable() // 启动 Bluedroid - │ ├─ 注册 GATT Server(图片传输服务) - │ ├─ 启动 BLE 广播 - │ └─ 启动 dzbj 业务任务(按键/电池/睡眠管理) - │ - └─ 3. 切换界面 - └─ lv_scr_load_anim(ui_ScreenHome, ...) +开机 + │ + ├── 板级构造函数(通用) + │ ├── PowerSaveTimer 初始化 + │ ├── InitializeButtons()(主项目 Button 类,双击注册在此) + │ ├── InitializeCodecI2c() + │ ├── dzbj_display_init() ← LCD + LVGL 始终初始化 + │ │ + │ ├── if device_mode == BADGE (吧唧模式) + │ │ └── InitializeBadgeMode() + │ │ ├── fatfs_init() // SPIFFS 文件系统 + │ │ ├── init_spiffs_image_list() // 扫描图片 + │ │ ├── dzbj_button_init() // ISR按键 + │ │ ├── battery_init() // 电池检测 + │ │ ├── dzbj_ble_init() // BLE 图传 + │ │ └── sleep_mgr_init() // 低功耗管理 + │ │ + │ └── else (AI模式, 默认) + │ ├── 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 按键行为 -``` -用户长按 BOOT 5秒 - │ - ├─ 1. 关闭吧唧资源 - │ ├─ esp_ble_gap_stop_advertising() // 停止广播 - │ ├─ 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 ↔ 吧唧) -``` +| 事件 | AI模式 | 吧唧模式 | 配网模式 | +|------|--------|---------|---------| +| 单击 | Idle↔Listening 切换 | 待定(返回 ScreenHome?) | 显示 MAC 地址 | +| 双击 | **切换到吧唧模式** | **切换到AI模式** | 无响应 | +| 长按5s | 无响应 | 无响应 | 进入生产测试 | --- -## 五、实施方案 +## 五、模块移植清单 -### 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 更新 | -| 文件 | 改动内容 | 改动程度 | -|------|----------|----------| -| `movecall-moji-esp32s3/config.h` | 添加 LCD/Touch GPIO 定义 | 小 | -| `movecall_moji_esp32s3.cc` | 初始化 LCD 驱动(参考 dzbj lcd.c) | 中 | +### 5.2 新建模块 -**GPIO 冲突注意**: -- movecall-moji `BUILTIN_LED_GPIO = GPIO_NUM_21` 与 dzbj LCD D3 (GPIO 21) 冲突 -- 需要重新映射 LED 引脚或调整 LCD 引脚 -- dzbj 触摸用独立 I2C 引脚(GPIO 5/6),与音频 ES8311 (GPIO 17/18) 不冲突 +| 模块 | 文件 | 功能 | +|------|------|------| +| device_mode | `main/dzbj/device_mode.c/h` | NVS 模式读写 + 重启切换 | -#### 第二步:LVGL 集成(改动中等) +### 5.3 修改的现有文件 -| 文件 | 改动内容 | 改动程度 | -|------|----------|----------| -| `main/idf_component.yml` | 添加 lvgl 8.3.11 + esp_lvgl_port 2.5.0 + esp_lcd_st77916 | 小 | -| `main/CMakeLists.txt` | 取消注释 lcd_display.cc,添加 dzbj 模块源文件 | 中 | -| `main/ui/` | 从 dzbj 复制 SquareLine 生成的 UI 代码 | 复制 | -| `main/pages/` | 从 dzbj 复制页面管理模块 | 复制+小改 | -| `main/sleep_mgr/` | 从 dzbj 复制低功耗管理模块 | 复制+小改 | +| 文件 | 修改内容 | +|------|---------| +| `movecall_moji_esp32s3.cc` | 模式分支 + InitializeBadgeMode() + BOOT 双击回调 | +| `application.cc` | Start() 模式分支(吧唧模式早返回) | +| `main/CMakeLists.txt` | 添加新源文件 | +| `main/idf_component.yml` | 添加 esp_jpeg 依赖 | +| `main/sleep_mgr/include/sleep_mgr.h` | stub 改为真实函数声明 | -#### 第三步:AI 聊天 UI(新开发) +### 5.4 删除的文件 -| 内容 | 说明 | +| 文件 | 原因 | |------|------| -| 创建 AI 聊天屏幕 | 基于 LVGL 8.3.11,包含 emoji 显示区 + 聊天气泡容器 + 状态栏 | -| 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模式下原样使用 | +| `main/pages/pages_stub.c` | 被 `main/dzbj/pages.c` 真实实现替代 | --- -## 六、风险评估 +## 六、GPIO 引脚分配(已解决) -### 6.1 高风险 +Phase 1 已完成的 GPIO 冲突解决: -| 风险 | 影响 | 缓解方案 | -|------|------|----------| -| **WiFi deinit 内存泄漏** | 每次切换泄漏几KB,多次切换后崩溃 | 实测 `esp_wifi_deinit()` 后用 `heap_caps_get_free_size()` 验证回收量 | -| **BLE deinit 内存泄漏** | Bluedroid 完全释放困难 | 考虑使用 NimBLE 替代 Bluedroid(更轻量,deinit 更可靠) | -| **内部 SRAM 碎片化** | 反复 init/deinit 导致碎片,大块分配失败 | 用 `heap_caps_get_largest_free_block()` 监控最大连续块 | - -### 6.2 中等风险 - -| 风险 | 影响 | 缓解方案 | -|------|------|----------| -| **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 | +| GPIO | 主项目原用途 | dzbj用途 | 解决方案 | +|------|------------|---------|---------| +| 21 | BUILTIN_LED | LCD D3 | LED 改为 GPIO_NUM_NC | +| 1 | Touch1 (电容触摸) | LCD 背光 EN | Touch1 改为 GPIO_NUM_NC | +| 7 | Touch4 (电容触摸) | LCD RST | Touch4 改为 GPIO_NUM_NC | +| 6 | Battery ADC | Touch RST | Battery ADC 改为 GPIO 3 | +| 17/18 | I2C_NUM_1 (音频) | I2C_NUM_0 (触摸) | 统一为 I2C_NUM_1 共享 | --- -## 七、NimBLE vs Bluedroid 选型建议 +## 七、风险评估 -当前 dzbj 和配网都使用 Bluedroid,但在双模式切换场景下 NimBLE 更优: +### 7.1 重启切换方案(已选定) -| 对比项 | Bluedroid | NimBLE | -|--------|-----------|--------| -| Flash 占用 | ~277KB (libbt+libbtdm) | ~120KB | -| 内部 SRAM | ~35-45KB 动态 | ~15-20KB 动态 | -| deinit 可靠性 | 一般(可能有泄漏) | 较好 | -| Classic BT | 支持 | 不支持(仅 BLE) | -| GATT Server | 支持 | 支持 | -| 迁移工作量 | — | 中等(API 不同,逻辑相同) | +| 风险 | 等级 | 说明 | +|------|------|------| +| 内存泄漏 | **无** | 每次重启全新初始化,无残留 | +| 内存碎片化 | **无** | 重启清除所有堆分配 | +| WiFi/BLE deinit 不可靠 | **无** | 无需 deinit,重启自然释放 | +| NVS 擦写寿命 | **极低** | 10-100万次,日常使用完全足够 | +| 切换体验 | **低** | ~3-4秒重启时间,可加转场动画优化 | -**建议**:如果不需要经典蓝牙,优先考虑迁移到 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 # Name, Type, SubType, Offset, Size, Flags @@ -344,51 +257,35 @@ ota_0, app, ota_0, 0x310000, 5M, ota_1, app, ota_1, 0x820000, 5M, ``` -**建议**:如果需要 SPIFFS 存储图片(dzbj 的图片浏览功能),需要重新添加 storage 分区,或复用 model 分区的一部分空间。 +dzbj 图片浏览功能需要 SPIFFS 存储。`model` 分区(3MB, spiffs 类型)可复用,或需新增 storage 分区。 --- -## 九、推荐实施路线 +## 九、验证计划 -``` -阶段 1: 点亮屏幕(基础验证) 预计改动量: 小 - ├─ 确认 LCD GPIO 映射(解决 GPIO 21 冲突) - ├─ 在 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.1 编译验证 +```bash +idf.py build ``` ---- +### 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 -| 问题 | 结论 | -|------|------| -| AI小智用 LVGL 9.2.2? | **不是**,当前项目无 LVGL,框架预留了 LCD 接口 | -| 显示 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(点亮屏幕)开始逐步推进。 +### 9.4 稳定性验证 +- [ ] 来回切换 10+ 次,功能正常 +- [ ] 各模式下长时间运行(>1小时)无崩溃 diff --git a/CMakeLists.txt b/CMakeLists.txt index 04a9e19..b906ae9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,3 +16,6 @@ add_compile_options(-Wno-missing-field-initializers) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(kapi) + +# 自动生成并烧录 SPIFFS 镜像(将 spiffs_image/ 目录中的图片打包到 storage 分区) +spiffs_create_partition_image(storage spiffs_image FLASH_IN_PROJECT) diff --git a/dependencies.lock b/dependencies.lock index 4728324..1980945 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -107,6 +107,16 @@ dependencies: registry_url: https://components.espressif.com/ type: service 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: component_hash: 5fa0f8b1274576d4484e2b8d9358e2a5d09c721511bef0dce6a55b4206b5f0e9 dependencies: @@ -202,6 +212,7 @@ direct_dependencies: - espressif/button - espressif/esp-sr - espressif/esp_codec_dev +- espressif/esp_jpeg - espressif/esp_lcd_st77916 - espressif/esp_lcd_touch - espressif/esp_lcd_touch_cst816s @@ -210,6 +221,6 @@ direct_dependencies: - espressif/led_strip - idf - lvgl/lvgl -manifest_hash: f912ad61bf8c653f10e6eb6988299d0c0083fc812504487ba14571084326b35a +manifest_hash: 567fb06fed7b7df9c9bbd2a0615df5b600cd13d08df4b38a71d28971feaec792 target: esp32s3 version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 1a66b3b..08b716b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -26,6 +26,13 @@ set(SOURCES "audio_codecs/audio_codec.cc" "dzbj/lcd.c" "dzbj/pages_pwm.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 文件 "ui/ui.c" "ui/ui_helpers.c" @@ -33,8 +40,6 @@ set(SOURCES "audio_codecs/audio_codec.cc" "ui/screens/ui_ScreenImg.c" "ui/screens/ui_ScreenSet.c" "ui/components/ui_comp_hook.c" - # dzbj stub 实现(Phase 1 空实现) - "pages/pages_stub.c" # UI 图片资源 "ui/images/ui_img_s1_png.c" "ui/images/ui_img_s6_png.c" diff --git a/main/application.cc b/main/application.cc index 28e2990..0222a05 100644 --- a/main/application.cc +++ b/main/application.cc @@ -17,6 +17,7 @@ #include "boards/common/qmi8658a.h" // 添加qmi8658a_data_t类型的头文件 #include "boards/movecall-moji-esp32s3/movecall_moji_esp32s3.h" // 添加MovecallMojiESP32S3类的头文件 #include "weather_api.h" +#include "dzbj/device_mode.h" // 设备模式管理(AI/吧唧) #include #include @@ -53,7 +54,12 @@ static const char* const STATE_STRINGS[] = { Application::Application() { 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(); // 初始化最后一次有声音输出的时间点 skip_dialog_idle_session_ = false; // 初始化跳过对话待机会话标志为false dialog_watchdog_running_ = false; // 初始化对话看门狗运行标志 @@ -521,6 +527,13 @@ void Application::SendTextMessage(const std::string& text) { } void Application::Start() { + // 电子吧唧模式:不启动 WiFi、协议、音频,所有交互由 LVGL + BLE 处理 + if (device_mode_is_badge()) { + ESP_LOGI(TAG, "🎴 吧唧模式:跳过 WiFi/协议/音频初始化"); + SetDeviceState(kDeviceStateIdle); + return; + } + auto& board = Board::GetInstance(); SetDeviceState(kDeviceStateStarting); @@ -2246,7 +2259,7 @@ void Application::SetDeviceState(DeviceState state) { } ESP_LOGI(TAG, "打印设置设备状态日志: %s", STATE_STRINGS[device_state_]);// 打印设置设备状态日志 // 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 display = board.GetDisplay(); diff --git a/main/boards/common/button.cc b/main/boards/common/button.cc index 0b24bed..3ee0a9c 100644 --- a/main/boards/common/button.cc +++ b/main/boards/common/button.cc @@ -7,8 +7,7 @@ static const char* TAG = "Button"; Button::Button(const button_adc_config_t& adc_cfg) { button_config_t button_config = { .type = BUTTON_TYPE_ADC, - // .long_press_time = 1000, // 原有长按3秒时的时间 - .long_press_time = 5000, // 长按5秒时间 + .long_press_time = 3000, // 长按3秒触发(模式切换) .short_press_time = 50, .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 = { .type = BUTTON_TYPE_GPIO, - // .long_press_time = 1000, // 原有长按3秒时的时间 - .long_press_time = 5000, // 长按5秒时间 + .long_press_time = 3000, // 长按3秒触发(模式切换) .short_press_time = 50, .gpio_button_config = { .gpio_num = gpio_num, diff --git a/main/boards/common/button.h b/main/boards/common/button.h index d2e44fd..9eb24cc 100644 --- a/main/boards/common/button.h +++ b/main/boards/common/button.h @@ -18,6 +18,10 @@ public: void OnLongPress(std::function callback); void OnClick(std::function callback); void OnDoubleClick(std::function callback); + + // 获取底层 iot_button 句柄(用于 iot_button_register_event_cb 等高级 API) + button_handle_t GetHandle() const { return button_handle_; } + private: gpio_num_t gpio_num_; button_handle_t button_handle_ = nullptr; diff --git a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc index b36c3db..bcf3bf5 100644 --- a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc +++ b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc @@ -15,6 +15,12 @@ #include "system_info.h" // 引入系统信息头文件 #include "settings.h" #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 // 添加数学函数头文件 #include @@ -37,6 +43,12 @@ #define TAG "Airhub1" #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 #include #include @@ -201,84 +213,90 @@ public: touched_pad_index_ = -1; #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传感器初始化之前) InitializeCodecI2c(); // 初始化 dzbj 显示模块(LCD + Touch + LVGL + UI) dzbj_display_init(codec_i2c_bus_); - // 初始化IoT功能,启用语音音量控制 - InitializeIot(); - - // 配网模式下跳过非必要外设,节省内部 DRAM 给 WiFi+BLE 使用 - bool provisioning_mode = WifiBoard::NeedsProvisioning(); - if (provisioning_mode) { - ESP_LOGI(TAG, "配网模式:跳过电池检测、IMU传感器、低功耗管理"); - battery_level_ = 100; // 设置默认电量 + // === 根据设备模式分支初始化(完全隔离,互不干扰) === + if (device_mode_is_badge()) { + // ===== 电子吧唧模式 ===== + // 不创建 PowerSaveTimer(吧唧模式使用 sleep_mgr) + // 不初始化 AI 音频/协议/WiFi 相关资源 + ESP_LOGI(TAG, "🎴 电子吧唧模式启动"); + battery_level_ = 100; // 默认电量,后续由 dzbj_battery 接管 + InitializeBadgeModeButtons(); // 仅注册吧唧专用回调 + InitializeBadgeMode(); } else { - // 初始化电量检测 - InitializeBatteryMonitor(); + // ===== AI 对话模式 ===== + ESP_LOGI(TAG, "🤖 AI对话模式启动"); - // 初始化IMU传感器 - InitializeImuSensor(); + // 创建 PowerSaveTimer(仅 AI 模式需要) + 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,启用完整的低功耗管理 - power_save_timer_->SetEnabled(true); - ESP_LOGI(TAG, "🔋 PowerSaveTimer已启用,20秒无活动将进入低功耗模式"); - } + InitializeAiModeButtons(); // 完整 AI 回调(含模式切换、音量、生产测试) + InitializeStoryButton(); + + // 初始化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 - // 延迟调用触摸板初始化,避免在构造函数中就调用 - ESP_LOGI(TAG, "在构造函数完成后调用触摸初始化"); - xTaskCreate([](void* arg) { - MovecallMojiESP32S3* board = static_cast(arg); - vTaskDelay(1000 / portTICK_PERIOD_MS); - ESP_LOGI(TAG, "开始延迟初始化触摸板..."); - if (board) { - board->InitializeTouchPads(); - } - vTaskDelete(NULL); - }, "touch_init", 4096, this, 5, NULL); + // 延迟调用触摸板初始化,避免在构造函数中就调用 + ESP_LOGI(TAG, "在构造函数完成后调用触摸初始化"); + xTaskCreate([](void* arg) { + MovecallMojiESP32S3* board = static_cast(arg); + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_LOGI(TAG, "开始延迟初始化触摸板..."); + if (board) { + board->InitializeTouchPads(); + } + vTaskDelete(NULL); + }, "touch_init", 4096, this, 5, NULL); #else - ESP_LOGI(TAG, "电容触摸板按钮已禁用 (ENABLE_TOUCH_PAD_BUTTONS=0)"); + ESP_LOGI(TAG, "电容触摸板按钮已禁用 (ENABLE_TOUCH_PAD_BUTTONS=0)"); #endif + } } #if ENABLE_TOUCH_PAD_BUTTONS // 发送触摸消息 void SendTouchMessage(int touch_pad_num) { const char* message = nullptr; - power_save_timer_->WakeUp(); + if (power_save_timer_) power_save_timer_->WakeUp(); auto& app = Application::GetInstance(); auto current_state = app.GetDeviceState(); @@ -458,8 +476,52 @@ public: - // 按钮初始化 函数 - void InitializeButtons() { + void InitializeBadgeMode() { + 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, "初始化按钮...");// 初始化按钮... // BOOT按键单击事件 - 用于WiFi重置和触摸解锁 @@ -657,23 +719,42 @@ public: } }); - // 配网模式下长按 BOOT 按键5秒进入 生产测试模式 新增代码 - // ============================================================================== - // 添加BOOT按键长按事件处理 - 仅在配网模式下长按5秒进入测试模式 + // BOOT 长按 3 秒 → 切换设备模式(AI ↔ 吧唧) + // 配网模式下跳过,因为配网模式有自己的 5s 长按处理 boot_button_.OnLongPress([this]() { - //ESP_LOGI(TAG, "🔧 BOOT button long pressed - checking if in provisioning mode"); - - // 检查是否处于BLE配网状态,只有在配网模式下才允许进入测试模式 + // 配网模式下不切换模式(留给 5s 长按进入生产测试) auto* wifi_board = dynamic_cast(this); if (wifi_board && wifi_board->IsBleProvisioningActive()) { - // ESP_LOGI(TAG, "🔧 设备正在进行BLE配网,长按5秒进入生产测试模式"); - EnterProductionTestMode(); - } else { - ESP_LOGI(TAG, "🔵 非配网模式下,BOOT长按被屏蔽,无法进入测试模式"); + ESP_LOGI(TAG, "配网模式下长按3秒,等待5秒进入生产测试..."); 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(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); diff --git a/main/dzbj/device_mode.c b/main/dzbj/device_mode.c new file mode 100644 index 0000000..dd82b07 --- /dev/null +++ b/main/dzbj/device_mode.c @@ -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; +} diff --git a/main/dzbj/device_mode.h b/main/dzbj/device_mode.h new file mode 100644 index 0000000..8e11c20 --- /dev/null +++ b/main/dzbj/device_mode.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#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 diff --git a/main/dzbj/dzbj_battery.c b/main/dzbj/dzbj_battery.c new file mode 100644 index 0000000..e77ab6c --- /dev/null +++ b/main/dzbj/dzbj_battery.c @@ -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 + +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); +} diff --git a/main/dzbj/dzbj_battery.h b/main/dzbj/dzbj_battery.h new file mode 100644 index 0000000..e7a07cf --- /dev/null +++ b/main/dzbj/dzbj_battery.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_err.h" +#include + +// 电池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 diff --git a/main/dzbj/dzbj_ble.c b/main/dzbj/dzbj_ble.c new file mode 100644 index 0000000..7086e41 --- /dev/null +++ b/main/dzbj/dzbj_ble.c @@ -0,0 +1,410 @@ +#include +#include +#include +#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; + } +} diff --git a/main/dzbj/dzbj_ble.h b/main/dzbj/dzbj_ble.h new file mode 100644 index 0000000..d84859a --- /dev/null +++ b/main/dzbj/dzbj_ble.h @@ -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 diff --git a/main/dzbj/dzbj_button.c b/main/dzbj/dzbj_button.c new file mode 100644 index 0000000..9e6cd53 --- /dev/null +++ b/main/dzbj/dzbj_button.c @@ -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%d,BOOT由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); + } + } +} diff --git a/main/dzbj/dzbj_button.h b/main/dzbj/dzbj_button.h new file mode 100644 index 0000000..76e9c86 --- /dev/null +++ b/main/dzbj/dzbj_button.h @@ -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 diff --git a/main/dzbj/fatfs.c b/main/dzbj/fatfs.c new file mode 100644 index 0000000..fb8099c --- /dev/null +++ b/main/dzbj/fatfs.c @@ -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 +#include +#include +#include + +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); +} diff --git a/main/dzbj/fatfs.h b/main/dzbj/fatfs.h new file mode 100644 index 0000000..f657e62 --- /dev/null +++ b/main/dzbj/fatfs.h @@ -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 diff --git a/main/dzbj/pages.c b/main/dzbj/pages.c new file mode 100644 index 0000000..92d239e --- /dev/null +++ b/main/dzbj/pages.c @@ -0,0 +1,1042 @@ +#include "lvgl.h" +#if LV_USE_GIF +#include "extra/libs/gif/lv_gif.h" +#include "extra/libs/gif/gifdec.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#endif +#include "esp_heap_caps.h" +#include "fatfs.h" +#include "pages_pwm.h" +#include "esp_lvgl_port.h" +#include "nvs_flash.h" +#include "esp_log.h" +#include "jpeg_decoder.h" +#include "screens/ui_ScreenImg.h" +#include + +// 前向声明界面切换函数 +extern void _ui_screen_change(lv_obj_t **target, lv_scr_load_anim_t fademode, int spd, int delay, void (*target_init)(void)); +#include +#include +#include +#include +#include +#include +char img_path[40]; +char *img_filename; +lv_obj_t *app_img; +lv_obj_t *act_mainscreen; +uint8_t *app_img_data = 0; +esp_jpeg_image_output_t outdata; +lv_img_dsc_t image; + +#define MAX_IMAGE_FILES 10 +#define MAX_FILENAME_LEN 32 +static char spiffs_image_files[MAX_IMAGE_FILES][MAX_FILENAME_LEN]; +static int spiffs_image_count = 0; +static int current_image_index = 0; +static bool image_list_initialized = false; + +#if LV_USE_GIF +// === 自定义 GIF 播放器(替代 lv_gif,性能优化) === +// 优化: 1.Palette LUT查表 2.TRUE_COLOR无Alpha 3.后台线程解码流水线 +static gd_GIF *gif_decoder = NULL; // gifdec 解码句柄 +static lv_obj_t *gif_img_obj = NULL; // 普通 lv_img 控件(替代 lv_gif) +static uint16_t *gif_rgb565_buf[2] = {NULL, NULL}; // 双缓冲 RGB565 帧 (PSRAM) +static lv_img_dsc_t gif_frame_dsc; // LVGL 图片描述符(TRUE_COLOR,无Alpha) +static uint16_t gif_palette_lut[256]; // 调色板 RGB565 查找表 +static volatile uint8_t gif_front_idx = 0; // 当前显示的缓冲区索引 +static lv_timer_t *gif_play_timer = NULL; // LVGL 播放定时器 +static TaskHandle_t gif_decode_task_handle = NULL; // 后台解码任务句柄 +static volatile bool gif_playing = false; // 播放状态标志 +static volatile bool gif_new_frame_ready = false; // 新帧就绪标志 +static uint32_t gif_last_frame_ms = 0; // 上一帧显示时间戳 +static uint8_t *gif_psram_buf = NULL; // GIF 文件数据(PSRAM) +static bool current_is_gif = false; // 当前是否为 GIF 模式 + +// 前向声明 +static bool is_gif_file(const char *filename); +static void gif_player_start(void); +static void gif_player_stop(void); +#endif // LV_USE_GIF + +// 从NVS中读取图片路径 +esp_err_t nvs_read_img(void) { + nvs_handle_t nvs_handle; // NVS 句柄 + esp_err_t err; // NVS 错误码 + err = nvs_open("config", NVS_READONLY, &nvs_handle);// 打开 NVS 句柄 + if (err != ESP_OK) return err; // 如果打开失败,返回错误码 + size_t imgname_len; + err = nvs_get_str(nvs_handle, "img_filename", NULL, &imgname_len);// 获取图片路径长度 + if (err == ESP_OK) { + img_filename = malloc(imgname_len);// 分配内存 + err = nvs_get_str(nvs_handle, "img_filename", img_filename, &imgname_len);// 获取图片路径 + if (err != ESP_OK) { + nvs_close(nvs_handle);// 关闭 NVS 句柄 + return err; // 如果获取失败,返回错误码 + } + ESP_LOGI("NVS", "img_filename: %s", img_filename);// 打印图片路径 + } + nvs_close(nvs_handle);// 关闭 NVS 句柄 + return err; +} + +// 测试改变NVS中的图片路径 +esp_err_t nvs_change_img(char *imgname) { + nvs_handle_t nvs_handle;// NVS 句柄 + esp_err_t err; + err = nvs_open("config", NVS_READWRITE, &nvs_handle);// 打开 NVS 句柄 + if (err != ESP_OK) goto close_handle; + err = nvs_set_str(nvs_handle, "img_filename", imgname);// 设置图片路径 + if (err != ESP_OK) goto close_handle; + err = nvs_commit(nvs_handle);// 提交更改 + if (err != ESP_OK) goto close_handle;// 如果提交失败,关闭句柄并返回错误码 + close_handle: + nvs_close(nvs_handle); // 关闭 NVS 句柄 + return err; +} + +// 仅更新现有图片,显示其他图片 +// img_name: 图片文件名,为NULL时从NVS读取 +void app_img_change(const char *img_name){ + // 释放之前的图片数据 + if(app_img_data){ + free(app_img_data); + app_img_data = NULL; + ESP_LOGI("IMG", "释放之前显示的图片数据缓存"); + } + + const char *current_img_name = img_name; + + // 如果没有指定图片名,从NVS读取 + if(!current_img_name) { + esp_err_t ret_nvs = nvs_read_img();// 从NVS中读取图片路径 + if(ret_nvs != ESP_OK){ + ESP_LOGE("NVS","图片路径获取失败2"); + return; + } + current_img_name = img_filename; + } + + // 构建图片路径 + snprintf(img_path, sizeof(img_path), "/spiflash/%s", current_img_name);// 格式化图片路径 + ESP_LOGI("IMG", "准备显示图片: %s, 路径: %s", current_img_name, img_path); + + // 检查文件是否存在 + struct stat file_stat; + if(stat(img_path, &file_stat) != 0) { + ESP_LOGE("IMG", "文件不存在: %s", img_path); + return; + } + ESP_LOGI("IMG", "文件大小: %ld 字节", file_stat.st_size); + + // 解码图片 + esp_err_t ret = DecodeImg(img_path,&app_img_data,&outdata);// 解码图片 + if(ret == ESP_OK){ + ESP_LOGI("IMG", "图片解码成功,数据地址: %p, 宽度: %d, 高度: %d", + app_img_data, outdata.width, outdata.height); + + // 检查解码后的数据 + if(app_img_data == NULL) { + ESP_LOGE("IMG", "解码数据为空"); + return; + } + + // 配置图片数据 + image.header.cf = LV_IMG_CF_TRUE_COLOR; + image.header.always_zero = 0; + image.header.reserved = 0; + image.header.w = outdata.width; + image.header.h = outdata.height; + image.data_size = outdata.output_len; + image.data = app_img_data; + + // 获取屏幕对象 + act_mainscreen = lv_scr_act(); + if(act_mainscreen == NULL) { + ESP_LOGE("IMG", "获取屏幕对象失败"); + return; + } + + // 如果图片对象不存在,创建它 + if(app_img == NULL) { + app_img = lv_img_create(act_mainscreen); + if(app_img == NULL) { + ESP_LOGE("IMG", "创建图片对象失败"); + return; + } + lv_obj_center(app_img); + ESP_LOGI("IMG", "创建图片对象成功"); + } + + // 更新图片显示 + lvgl_port_lock(0);// 锁定LVGL端口 + lv_img_set_src(app_img, &image);// 设置图片源 + lv_scr_load(act_mainscreen);// 加载主屏幕 + lvgl_port_unlock();// 解锁LVGL端口 + + ESP_LOGI("IMG", "图片显示成功: %s", current_img_name); + } else { + ESP_LOGE("IMG", "图片解码失败,错误码: %d", ret); + } +} + +// 完整的图片显示初始化 +void app_img_display(){ + ESP_LOGI("IMG", "开始显示图片"); + esp_err_t ret_nvs = nvs_read_img();// 从NVS中读取图片路径 + if(ret_nvs != ESP_OK){ + ESP_LOGE("NVS","图片路径获取失败1"); + return; + } + ESP_LOGI("IMG", "图片路径: %s", img_filename); + snprintf(img_path, sizeof(img_path), "/spiflash/%s",img_filename);// 格式化图片路径 + ESP_LOGI("IMG", "完整路径: %s", img_path); + + // 检查文件是否存在 + struct stat file_stat; + if(stat(img_path, &file_stat) != 0){ + ESP_LOGE("IMG", "文件不存在"); + return; + } + ESP_LOGI("IMG", "文件大小: %ld 字节", file_stat.st_size); + + esp_err_t ret = DecodeImg(img_path,&app_img_data,&outdata);// 解码图片 + if(ret == ESP_OK){ + ESP_LOGI("IMG", "图片解码成功,数据地址: %p", app_img_data); + + // 检查解码后的数据 + if(app_img_data == NULL){ + ESP_LOGE("IMG", "解码数据为空"); + return; + } + + image.header.cf = LV_IMG_CF_TRUE_COLOR; + image.header.always_zero = 0; + image.header.reserved = 0; + image.header.w = outdata.width; + image.header.h = outdata.height; + image.data_size = outdata.output_len; + image.data = app_img_data; + + ESP_LOGI("IMG", "LV_IMG_CF_RGB565 值: %d", LV_IMG_CF_RGB565); + ESP_LOGI("IMG", "设置图片数据: 宽度=%lu, 高度=%lu, 数据大小=%lu", (unsigned long)image.header.w, (unsigned long)image.header.h, (unsigned long)image.data_size); + + act_mainscreen = lv_scr_act();// 获取当前主屏幕对象 + if(act_mainscreen == NULL){ + ESP_LOGE("IMG", "获取屏幕对象失败"); + return; + } + ESP_LOGI("IMG", "获取屏幕对象成功"); + + app_img = lv_img_create(act_mainscreen);// 创建图片对象 + if(app_img == NULL){ + ESP_LOGE("IMG", "创建图片对象失败"); + return; + } + ESP_LOGI("IMG", "创建图片对象成功"); + + lvgl_port_lock(0);// 锁定LVGL端口 + ESP_LOGI("IMG", "设置图片源前"); + lv_img_set_src(app_img, &image);// 设置图片源 + ESP_LOGI("IMG", "设置图片源后"); + lv_obj_center(app_img);// 居中显示图片 + ESP_LOGI("IMG", "居中显示图片后"); + lv_scr_load(act_mainscreen);// 加载主屏幕 + ESP_LOGI("IMG", "加载主屏幕后"); + lvgl_port_unlock();// 解锁LVGL端口 + vTaskDelay(50);// 延时50ms + pwm_init();// 初始化PWM + ESP_LOGI("IMG", "图片显示完成"); + } else { + ESP_LOGE("IMG", "图片解码失败,错误码: %d", ret); + } +} + +// 新的显示测试屏幕函数 +void app_test_display(){ + lvgl_port_lock(0);// 锁定LVGL端口 + + // 获取或创建屏幕对象 + lv_obj_t *screen = lv_scr_act(); + if(screen == NULL) { + screen = lv_obj_create(NULL);// 创建屏幕对象 + if(screen == NULL) { + ESP_LOGE("TEST", "Failed to create screen object");// 创建屏幕对象失败 + lvgl_port_unlock();// 解锁LVGL端口 + return; + } + } + + // 清空屏幕 + lv_obj_clean(screen); + + // 创建标签 + lv_obj_t *label = lv_label_create(screen); + if(label) { + lv_label_set_text(label, "Test Screen\nLCD is working!");// 设置标签文本 + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + + // 创建基本样式 + static lv_style_t style;// 基本样式 + lv_style_init(&style);// 初始化基本样式 + lv_style_set_text_font(&style, &lv_font_montserrat_14);// 设置字体 + lv_style_set_text_color(&style, lv_color_hex(0xf9076a));// 设置文本颜色 + lv_style_set_bg_color(&style, lv_color_hex(0x000000));// 设置背景颜色 + + lv_obj_add_style(label, &style, 0);// 添加样式到标签 + } + lv_scr_load(screen);// 加载屏幕 + lvgl_port_unlock();// 解锁LVGL端口 +} + + +// 图片循环显示任务 - 显示spiffs中的所有图片 +void img_loop_task(void *pvParameters) { + // 存储SPIFFS中的图片文件名 + char *image_files[10]; // 最多支持10张图片 + int file_count = 0; + int current_index = 0; + static bool backlight_initialized = false; + + // 初始化背光(只执行一次) + if(!backlight_initialized) { + pwm_init(); + backlight_initialized = true;// 初始化背光 + ESP_LOGI("IMG_LOOP", "背光初始化完成"); + } + + while(1) { + // 重新扫描SPIFFS中的图片文件 + ESP_LOGI("IMG_LOOP", "开始扫描SPIFFS中的图片文件"); + + // 打开SPIFFS目录 + DIR *dir = opendir("/spiflash"); + if(!dir) { + ESP_LOGE("IMG_LOOP", "无法打开SPIFFS目录"); + vTaskDelay(pdMS_TO_TICKS(3000)); + continue; + } + + // 重置文件计数 + file_count = 0; + + // 遍历目录 + struct dirent *entry; + while((entry = readdir(dir)) != NULL && file_count < 10) { + // 检查是否是图片文件(.jpg, .jpeg, .png等) + const char *name = entry->d_name; + int len = strlen(name); + if(len > 4) { + const char *ext = name + len - 4; + if(strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 || + strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0) { + // 存储图片文件名 + image_files[file_count] = strdup(name); + ESP_LOGI("IMG_LOOP", "发现图片文件: %s", name); + file_count++; + } + } + } + + closedir(dir); + + // 检查是否找到图片 + if(file_count == 0) { + ESP_LOGE("IMG_LOOP", "未找到图片文件"); + vTaskDelay(pdMS_TO_TICKS(3000)); + continue; + } + + ESP_LOGI("IMG_LOOP", "共发现 %d 张图片,开始循环显示", file_count); + + // 循环显示所有图片 + for(current_index = 0; current_index < file_count; current_index++) { + const char *current_image = image_files[current_index]; + ESP_LOGI("IMG_LOOP", "显示图片 %d/%d: %s", current_index + 1, file_count, current_image); + + // 使用修改后的app_img_change函数显示图片 + app_img_change(current_image); + + // 等待3秒 + vTaskDelay(pdMS_TO_TICKS(3000)); + } + + // 释放文件名内存 + for(int i = 0; i < file_count; i++) { + free(image_files[i]); + } + + // 再次扫描前短暂延时 + vTaskDelay(pdMS_TO_TICKS(500)); + } +} + +// 图片切换任务 - 显示指定的两张图片 +void img_switch_task(void *pvParameters) { + char *image_files[] = {"default.jpg", "02.jpg"}; + int file_count = 2; + int current_index = 0; + + while(1) { + // 使用修改后的app_img_change函数显示图片 + const char *current_image = image_files[current_index]; + ESP_LOGI("IMG_SWITCH", "切换到图片: %s", current_image); + app_img_change(current_image); + + // 切换到下一张图片 + current_index = (current_index + 1) % file_count; + + // 等待2秒 + vTaskDelay(pdMS_TO_TICKS(2000)); + } +} + +// 初始化SPIFFS图片列表 +void init_spiffs_image_list(void) { + if(image_list_initialized) { + ESP_LOGI("IMG_LIST", "图片列表已初始化,跳过"); + return; + } + + ESP_LOGI("IMG_LIST", "开始扫描SPIFFS中的图片文件"); + + // 打开SPIFFS目录 + DIR *dir = opendir("/spiflash"); + if(!dir) { + ESP_LOGE("IMG_LIST", "无法打开SPIFFS目录"); + return; + } + + // 重置文件计数 + spiffs_image_count = 0; + + // 遍历目录 + struct dirent *entry; + while((entry = readdir(dir)) != NULL && spiffs_image_count < MAX_IMAGE_FILES) { + // 检查是否是图片文件(.jpg, .jpeg, .png等) + const char *name = entry->d_name; + int len = strlen(name); + if(len > 4 && len < MAX_FILENAME_LEN) { + const char *ext = name + len - 4; + if(strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 || + strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0 +#if LV_USE_GIF + || strcasecmp(ext, ".gif") == 0 +#endif + ) { + // 存储图片文件名到静态缓冲区 + strncpy(spiffs_image_files[spiffs_image_count], name, MAX_FILENAME_LEN - 1); + spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0'; + ESP_LOGI("IMG_LIST", "发现图片文件: %s", name); + spiffs_image_count++; + } + } + } + + closedir(dir); + + // 检查是否找到图片 + if(spiffs_image_count == 0) { + ESP_LOGE("IMG_LIST", "未找到图片文件"); + return; + } + + image_list_initialized = true; + ESP_LOGI("IMG_LIST", "图片列表初始化完成,共发现 %d 张图片", spiffs_image_count); + + // 查找default.jpg并设置为当前索引 + for(int i = 0; i < spiffs_image_count; i++) { + if(strcmp(spiffs_image_files[i], "default.jpg") == 0) { + current_image_index = i; + ESP_LOGI("IMG_LIST", "设置默认图片索引: %d", current_image_index); + break; + } + } +} + +// 获取下一张图片 +const char* get_next_image(void) { + if(!image_list_initialized || spiffs_image_count == 0) { + ESP_LOGE("IMG_LIST", "图片列表未初始化或为空"); + return NULL; + } + + current_image_index = (current_image_index + 1) % spiffs_image_count; + ESP_LOGI("IMG_LIST", "切换到下一张图片,索引: %d/%d", current_image_index + 1, spiffs_image_count); + return spiffs_image_files[current_image_index]; +} + +// 获取上一张图片 +const char* get_prev_image(void) { + if(!image_list_initialized || spiffs_image_count == 0) { + ESP_LOGE("IMG_LIST", "图片列表未初始化或为空"); + return NULL; + } + + current_image_index = (current_image_index - 1 + spiffs_image_count) % spiffs_image_count; + ESP_LOGI("IMG_LIST", "切换到上一张图片,索引: %d/%d", current_image_index + 1, spiffs_image_count); + return spiffs_image_files[current_image_index]; +} + +// 重置图片列表 +void free_spiffs_image_list(void) { + if(!image_list_initialized) { + return; + } + + spiffs_image_count = 0; + current_image_index = 0; + image_list_initialized = false; + + ESP_LOGI("IMG_LIST", "图片列表已重置"); +} + +// 根据文件名设置当前图片索引 +bool set_image_index_by_name(const char *name) { + if(!image_list_initialized || spiffs_image_count == 0 || !name) { + return false; + } + for(int i = 0; i < spiffs_image_count; i++) { + if(strcmp(spiffs_image_files[i], name) == 0) { + current_image_index = i; + ESP_LOGI("IMG_LIST", "设置图片索引为 %d: %s", i, name); + return true; + } + } + ESP_LOGW("IMG_LIST", "未找到图片: %s", name); + return false; +} + +// BLE接收图片后导航到ScreenImg显示 +void ble_image_navigate(const char *filename) { + // 将新文件直接追加到列表(避免重扫 SPIFFS 目录,节省 ~200ms) + if (!image_list_initialized) { + init_spiffs_image_list(); + } + // 检查文件是否已在列表中(避免重复) + bool found = false; + for (int i = 0; i < spiffs_image_count; i++) { + if (strcmp(spiffs_image_files[i], filename) == 0) { + current_image_index = i; + found = true; + break; + } + } + if (!found && spiffs_image_count < MAX_IMAGE_FILES) { + strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1); + spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0'; + current_image_index = spiffs_image_count; + spiffs_image_count++; + } + + // 检查是否已在ScreenImg界面 + lvgl_port_lock(0); + bool already_on_screen = (lv_scr_act() == ui_ScreenImg); + if (!already_on_screen) { + // 不在ScreenImg,导航过去(SCREEN_LOADED事件会触发update_ui_ImgBle) + _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); + } + lvgl_port_unlock(); + + // 已在ScreenImg时,_ui_screen_change不会触发SCREEN_LOADED,需手动更新图片 + if (already_on_screen) { + update_ui_ImgBle(filename); + } + + ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename); +} + +// BLE接收图片后导航显示(携带预加载数据,跳过 SPIFFS 重读) +void ble_image_navigate_with_data(const char *filename, uint8_t *data, size_t data_size) { + // 将新文件追加到列表 + if (!image_list_initialized) { + init_spiffs_image_list(); + } + bool found = false; + for (int i = 0; i < spiffs_image_count; i++) { + if (strcmp(spiffs_image_files[i], filename) == 0) { + current_image_index = i; + found = true; + break; + } + } + if (!found && spiffs_image_count < MAX_IMAGE_FILES) { + strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1); + spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0'; + current_image_index = spiffs_image_count; + spiffs_image_count++; + } + +#if LV_USE_GIF + // 如果有预加载数据且是 GIF,直接用内存数据显示(跳过 SPIFFS 重读) + if (data && data_size > 0 && is_gif_file(filename)) { + // 停止旧 GIF 播放器 + gif_player_stop(); + if (gif_psram_buf) { + free(gif_psram_buf); + } + // BLE 数据直接作为 GIF 源(所有权转移) + gif_psram_buf = data; + + // 打开 gifdec 解码器(从 PSRAM 内存源) + gif_decoder = gd_open_gif_data(gif_psram_buf); + if (!gif_decoder) { + ESP_LOGE("GIF", "gifdec 打开失败"); + free(gif_psram_buf); + gif_psram_buf = NULL; + return; + } + + // 确保在 ScreenImg 界面 + lvgl_port_lock(0); + bool already_on_screen = (lv_scr_act() == ui_ScreenImg); + if (!already_on_screen) { + _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); + } + lvgl_port_unlock(); + + // 启动自定义 GIF 播放器 + gif_player_start(); + + ESP_LOGI("IMG_LIST", "BLE GIF直通显示(优化): %s", filename); + return; + } +#endif // LV_USE_GIF + + // 非 GIF 或无预加载数据,释放 BLE 数据,走常规 SPIFFS 路径 + if (data) { + free(data); + } + + lvgl_port_lock(0); + bool already_on_screen = (lv_scr_act() == ui_ScreenImg); + if (!already_on_screen) { + _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); + } + lvgl_port_unlock(); + + if (already_on_screen) { + update_ui_ImgBle(filename); + } + + ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename); +} + +// 获取当前图片文件名 +const char* get_current_image(void) { + if(!image_list_initialized || spiffs_image_count == 0) { + ESP_LOGE("IMG_LIST", "图片列表未初始化或为空"); + return NULL; + } + + return spiffs_image_files[current_image_index]; +} + +// 删除当前图片并从列表中移除 +bool delete_current_image(void) { + if(!image_list_initialized || spiffs_image_count == 0) { + ESP_LOGE("IMG_DEL", "图片列表未初始化或为空"); + return false; + } + + const char *current_img = spiffs_image_files[current_image_index]; + + // 构建完整路径 + char full_path[64]; + snprintf(full_path, sizeof(full_path), "/spiflash/%s", current_img); + + ESP_LOGI("IMG_DEL", "准备删除图片: %s", full_path); + + // 从SPIFFS文件系统中删除文件 + if(unlink(full_path) != 0) { + ESP_LOGE("IMG_DEL", "删除文件失败: %s", full_path); + return false; + } + + ESP_LOGI("IMG_DEL", "文件删除成功: %s", current_img); + + // 从列表中移除该图片 + for(int i = current_image_index; i < spiffs_image_count - 1; i++) { + strncpy(spiffs_image_files[i], spiffs_image_files[i + 1], MAX_FILENAME_LEN); + } + + // 更新计数 + spiffs_image_count--; + + // 如果列表为空,重置状态 + if(spiffs_image_count == 0) { + ESP_LOGI("IMG_DEL", "所有图片已删除"); + image_list_initialized = false; + current_image_index = 0; + return true; + } + + // 调整当前索引(如果删除的是最后一张,回到第一张) + if(current_image_index >= spiffs_image_count) { + current_image_index = 0; + } + + ESP_LOGI("IMG_DEL", "图片列表已更新,剩余 %d 张图片,当前索引: %d", + spiffs_image_count, current_image_index); + + return true; +} + +#if LV_USE_GIF +// 判断文件是否为 GIF 格式 +static bool is_gif_file(const char *filename) { + int len = strlen(filename); + if (len < 4) return false; + return strcasecmp(filename + len - 4, ".gif") == 0; +} + +// === GIF 播放器内部函数 === + +// 构建调色板 RGB565 查找表(每次帧解码前调用,处理局部调色板) +static void gif_build_palette_lut(gd_Palette *palette) { + for (int i = 0; i < palette->size; i++) { + uint8_t r = palette->colors[i * 3 + 0]; + uint8_t g = palette->colors[i * 3 + 1]; + uint8_t b = palette->colors[i * 3 + 2]; + lv_color_t c = lv_color_make(r, g, b); // 自动处理 LV_COLOR_16_SWAP + gif_palette_lut[i] = c.full; + } +} + +// 快速渲染帧到 canvas(palette LUT 替代逐像素 lv_color_make) +static void gif_render_frame_fast(gd_GIF *gif) { + int i = gif->fy * gif->width + gif->fx; + for (int j = 0; j < gif->fh; j++) { + for (int k = 0; k < gif->fw; k++) { + uint8_t index = gif->frame[(gif->fy + j) * gif->width + gif->fx + k]; + if (!gif->gce.transparency || index != gif->gce.tindex) { + uint16_t c = gif_palette_lut[index]; + gif->canvas[(i + k) * 3 + 0] = c & 0xff; + gif->canvas[(i + k) * 3 + 1] = (c >> 8) & 0xff; + gif->canvas[(i + k) * 3 + 2] = 0xff; + } + } + i += gif->width; + } +} + +// canvas (RGB565+Alpha 3字节/像素) -> RGB565 (2字节/像素) 快速拷贝 +static void gif_canvas_to_rgb565(gd_GIF *gif, uint16_t *out) { + uint8_t *canvas = gif->canvas; + int total = gif->width * gif->height; + for (int i = 0; i < total; i++) { + out[i] = (uint16_t)canvas[i * 3] | ((uint16_t)canvas[i * 3 + 1] << 8); + } +} + +// 后台解码任务(FreeRTOS,与 LVGL 显示并行) +static void gif_decode_task(void *pvParameters) { + while (1) { + // 等待解码请求 + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + if (!gif_playing || !gif_decoder) break; + + // LZW 解码(含 dispose 处理) + int ret = gd_get_frame(gif_decoder); + if (ret == 0) { + // GIF 循环播放 + gd_rewind(gif_decoder); + ret = gd_get_frame(gif_decoder); + } + if (ret < 0 || !gif_playing) break; + + // 更新调色板 LUT(可能有局部调色板变化) + gif_build_palette_lut(gif_decoder->palette); + + // 快速渲染到 canvas + 拷贝到后台 RGB565 缓冲 + gif_render_frame_fast(gif_decoder); + uint8_t back = 1 - gif_front_idx; + gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[back]); + + // 标记新帧就绪 + gif_new_frame_ready = true; + } + + gif_decode_task_handle = NULL; + vTaskDelete(NULL); +} + +// LVGL 定时器回调(检查新帧并切换显示) +static void gif_play_timer_cb(lv_timer_t *t) { + if (!gif_playing || !gif_decoder || !gif_img_obj) return; + + // 检查 GIF 帧延时 + uint32_t delay_ms = gif_decoder->gce.delay * 10; + if (delay_ms < 20) delay_ms = 20; + + uint32_t elapsed = lv_tick_elaps(gif_last_frame_ms); + if (elapsed < delay_ms) return; + + if (!gif_new_frame_ready) return; + + gif_last_frame_ms = lv_tick_get(); + gif_new_frame_ready = false; + + // 切换前后缓冲 + uint8_t back = 1 - gif_front_idx; + gif_front_idx = back; + + // 更新 LVGL 图片源(TRUE_COLOR,无 Alpha 混合) + gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[gif_front_idx]; + lv_img_cache_invalidate_src(&gif_frame_dsc); + lv_obj_invalidate(gif_img_obj); + + // 通知后台线程解码下一帧 + if (gif_decode_task_handle) { + xTaskNotifyGive(gif_decode_task_handle); + } +} + +// 启动自定义 GIF 播放器 +static void gif_player_start(void) { + if (!gif_decoder || !gif_psram_buf) return; + + uint16_t w = gif_decoder->width; + uint16_t h = gif_decoder->height; + size_t buf_size = w * h * sizeof(uint16_t); + + // 分配双缓冲 RGB565 帧 (PSRAM) + for (int i = 0; i < 2; i++) { + if (!gif_rgb565_buf[i]) { + gif_rgb565_buf[i] = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM); + if (!gif_rgb565_buf[i]) { + ESP_LOGE("GIF", "RGB565 缓冲分配失败: %d", i); + return; + } + } + memset(gif_rgb565_buf[i], 0, buf_size); + } + + // 初始化调色板 LUT + gif_build_palette_lut(gif_decoder->palette); + + // 同步解码第一帧(确保立即显示) + int ret = gd_get_frame(gif_decoder); + if (ret <= 0) { + ESP_LOGE("GIF", "首帧解码失败"); + return; + } + gif_render_frame_fast(gif_decoder); + gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[0]); + gif_front_idx = 0; + gif_new_frame_ready = false; + gif_last_frame_ms = lv_tick_get(); + + // 配置 LVGL 图片描述符(TRUE_COLOR,无 Alpha) + gif_frame_dsc.header.cf = LV_IMG_CF_TRUE_COLOR; + gif_frame_dsc.header.always_zero = 0; + gif_frame_dsc.header.reserved = 0; + gif_frame_dsc.header.w = w; + gif_frame_dsc.header.h = h; + gif_frame_dsc.data_size = buf_size; + gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[0]; + + // 创建 lv_img 控件 + LVGL 播放定时器 + lvgl_port_lock(0); + lv_obj_add_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN); + gif_img_obj = lv_img_create(ui_ScreenImg); + lv_obj_set_size(gif_img_obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_align(gif_img_obj, LV_ALIGN_CENTER); + lv_obj_clear_flag(gif_img_obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_move_background(gif_img_obj); + lv_img_set_src(gif_img_obj, &gif_frame_dsc); + gif_play_timer = lv_timer_create(gif_play_timer_cb, 10, NULL); + lvgl_port_unlock(); + + // 启动后台解码任务 + gif_playing = true; + current_is_gif = true; + xTaskCreatePinnedToCore(gif_decode_task, "gif_dec", 4096, NULL, 5, &gif_decode_task_handle, 1); + + // 触发后台解码第2帧 + xTaskNotifyGive(gif_decode_task_handle); + + ESP_LOGI("GIF", "播放器启动: %dx%d, 双缓冲 %dKB×2", w, h, buf_size / 1024); +} + +// 停止自定义 GIF 播放器 +static void gif_player_stop(void) { + // 停止后台解码任务 + gif_playing = false; + if (gif_decode_task_handle) { + xTaskNotifyGive(gif_decode_task_handle); + // 等待任务退出(最多 500ms) + for (int i = 0; i < 50 && gif_decode_task_handle; i++) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + } + + // 删除 LVGL 定时器和控件 + lvgl_port_lock(0); + if (gif_play_timer) { + lv_timer_del(gif_play_timer); + gif_play_timer = NULL; + } + if (gif_img_obj) { + lv_obj_del(gif_img_obj); + gif_img_obj = NULL; + } + lvgl_port_unlock(); + + // 关闭 gifdec 解码器 + if (gif_decoder) { + gd_close_gif(gif_decoder); + gif_decoder = NULL; + } + + // 释放 RGB565 双缓冲 + for (int i = 0; i < 2; i++) { + if (gif_rgb565_buf[i]) { + free(gif_rgb565_buf[i]); + gif_rgb565_buf[i] = NULL; + } + } + + current_is_gif = false; + gif_new_frame_ready = false; +} + +// 清理 GIF 资源(公开接口,供界面切换时调用) +void pages_cleanup_gif(void) { + gif_player_stop(); + // 释放 GIF 源文件缓冲区 + if (gif_psram_buf) { + free(gif_psram_buf); + gif_psram_buf = NULL; + } +} +#endif // LV_USE_GIF + +// 更新ui_ImgBle控件的图片(支持 JPEG) +void update_ui_ImgBle(const char *img_name) { + if(!img_name) { + ESP_LOGE("IMG_UI", "图片名为空"); + return; + } + + if(!ui_ImgBle) { + ESP_LOGE("IMG_UI", "ui_ImgBle控件不存在"); + return; + } + + static uint8_t *ui_img_data = NULL; + static lv_img_dsc_t ui_image; + + // 构建图片路径 + snprintf(img_path, sizeof(img_path), "/spiflash/%s", img_name); + ESP_LOGI("IMG_UI", "准备显示图片: %s, 路径: %s", img_name, img_path); + + // 检查文件是否存在 + struct stat file_stat; + if(stat(img_path, &file_stat) != 0) { + ESP_LOGE("IMG_UI", "文件不存在: %s", img_path); + return; + } + ESP_LOGI("IMG_UI", "文件大小: %ld 字节", file_stat.st_size); + +#if LV_USE_GIF + if (is_gif_file(img_name)) { + // === GIF 显示路径(自定义播放器:Palette LUT + 无Alpha + 后台解码) === + // 释放之前的 JPEG 数据 + if(ui_img_data) { + free(ui_img_data); + ui_img_data = NULL; + } + + // 停止旧 GIF 播放器 + 释放旧 GIF 数据 + gif_player_stop(); + if (gif_psram_buf) { + free(gif_psram_buf); + gif_psram_buf = NULL; + } + + // 将 GIF 文件整体读入 PSRAM + FILE *gif_file = fopen(img_path, "rb"); + if (!gif_file) { + ESP_LOGE("IMG_UI", "GIF文件打开失败: %s", img_path); + return; + } + gif_psram_buf = heap_caps_malloc(file_stat.st_size, MALLOC_CAP_SPIRAM); + if (!gif_psram_buf) { + ESP_LOGE("IMG_UI", "PSRAM分配失败: %ld 字节", file_stat.st_size); + fclose(gif_file); + return; + } + fread(gif_psram_buf, 1, file_stat.st_size, gif_file); + fclose(gif_file); + + // 打开 gifdec 解码器(从 PSRAM 内存源) + gif_decoder = gd_open_gif_data(gif_psram_buf); + if (!gif_decoder) { + ESP_LOGE("IMG_UI", "gifdec 打开失败: %s", img_name); + free(gif_psram_buf); + gif_psram_buf = NULL; + return; + } + + // 启动自定义 GIF 播放器(Palette LUT + 双缓冲流水线) + gif_player_start(); + + ESP_LOGI("IMG_UI", "GIF显示启动(优化): %s", img_name); + } else +#endif // LV_USE_GIF + { + // === JPEG 显示路径 === +#if LV_USE_GIF + // 停止 GIF 播放器并释放资源 + gif_player_stop(); + if (gif_psram_buf) { + free(gif_psram_buf); + gif_psram_buf = NULL; + } + // 恢复 JPEG 控件显示 + lvgl_port_lock(0); + lv_obj_clear_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN); + lvgl_port_unlock(); +#endif + + // 释放之前的图片数据 + if(ui_img_data) { + free(ui_img_data); + ui_img_data = NULL; + ESP_LOGI("IMG_UI", "释放之前的图片数据"); + } + + // 解码图片 + esp_jpeg_image_output_t ui_outdata; + esp_err_t ret = DecodeImg(img_path, &ui_img_data, &ui_outdata); + if(ret == ESP_OK) { + ESP_LOGI("IMG_UI", "图片解码成功,宽度: %d, 高度: %d", ui_outdata.width, ui_outdata.height); + + if(ui_img_data == NULL) { + ESP_LOGE("IMG_UI", "解码数据为空"); + return; + } + + // 配置图片数据 + ui_image.header.cf = LV_IMG_CF_TRUE_COLOR; + ui_image.header.always_zero = 0; + ui_image.header.reserved = 0; + ui_image.header.w = ui_outdata.width; + ui_image.header.h = ui_outdata.height; + ui_image.data_size = ui_outdata.output_len; + ui_image.data = ui_img_data; + + lvgl_port_lock(0); + lv_img_set_src(ui_ImgBle, &ui_image); + lvgl_port_unlock(); + + ESP_LOGI("IMG_UI", "JPEG图片更新成功: %s", img_name); + } else { + ESP_LOGE("IMG_UI", "图片解码失败,错误码: %d", ret); + ui_img_data = NULL; + } + } +} diff --git a/main/dzbj/pages.h b/main/dzbj/pages.h new file mode 100644 index 0000000..2979664 --- /dev/null +++ b/main/dzbj/pages.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esp_err.h" +#include +#include +#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 diff --git a/main/dzbj/sleep_mgr.c b/main/dzbj/sleep_mgr.c new file mode 100644 index 0000000..ae47a10 --- /dev/null +++ b/main/dzbj/sleep_mgr.c @@ -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 + +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; +} diff --git a/main/idf_component.yml b/main/idf_component.yml index 6c53ee0..a2fc2ba 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -15,6 +15,8 @@ dependencies: esp_lcd_st77916: "1.0.1" esp_lcd_touch: "1.1.2" esp_lcd_touch_cst816s: "1.1.0" + ## JPEG 解码(dzbj 图片显示) + esp_jpeg: "*" ## Required IDF version idf: version: ">=5.3" diff --git a/main/sleep_mgr/include/sleep_mgr.h b/main/sleep_mgr/include/sleep_mgr.h index 1093fbc..68f23c9 100644 --- a/main/sleep_mgr/include/sleep_mgr.h +++ b/main/sleep_mgr/include/sleep_mgr.h @@ -1,13 +1,29 @@ -#ifndef _SLEEP_MGR_STUB_H_ -#define _SLEEP_MGR_STUB_H_ - -// Stub 头文件:dzbj ui_ScreenSet.c 引用,Phase 1 仅提供声明 -// 实际实现将在后续阶段添加 +#pragma once #include -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 diff --git a/managed_components/espressif__esp_lvgl_port/src/lvgl8/esp_lvgl_port_touch.c b/managed_components/espressif__esp_lvgl_port/src/lvgl8/esp_lvgl_port_touch.c index aea1221..aaf5a4d 100644 --- a/managed_components/espressif__esp_lvgl_port/src/lvgl8/esp_lvgl_port_touch.c +++ b/managed_components/espressif__esp_lvgl_port/src/lvgl8/esp_lvgl_port_touch.c @@ -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.y = touchpad_y[0]; 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 { data->state = LV_INDEV_STATE_RELEASED; } diff --git a/partitions.csv b/partitions.csv index 560fe12..faf08db 100644 --- a/partitions.csv +++ b/partitions.csv @@ -6,3 +6,4 @@ phy_init, data, phy, 0xf000, 0x1000, model, data, spiffs, 0x10000, 0x300000, ota_0, app, ota_0, 0x310000, 5M, ota_1, app, ota_1, 0x820000, 5M, +storage, data, spiffs, 0xD20000, 0x2E0000, diff --git a/sdkconfig b/sdkconfig index 81ca308..e38e31a 100644 --- a/sdkconfig +++ b/sdkconfig @@ -2761,6 +2761,12 @@ CONFIG_CODEC_TAS5805M_SUPPORT=y # CONFIG_CODEC_CJC8910_SUPPORT is not set # end of Audio Codec Device Configuration +# +# JPEG Decoder +# +CONFIG_JD_USE_ROM=y +# end of JPEG Decoder + # # ESP LCD TOUCH #