Compare commits

...

3 Commits

Author SHA1 Message Date
38b71ce221 chore: 添加 esp_jpeg 托管组件到仓库
将 ESP Component Registry 自动下载的 espressif__esp_jpeg 组件纳入版本管理,
避免其他开发者拉取后需要重新下载。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:32:38 +08:00
c4de88d7ee chore: 移除 pages_stub.c(已被 dzbj/pages.c 替代)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:23:28 +08:00
14776acb0a feat: 完成 AI/吧唧双模式完全隔离重构 + 触摸坐标日志 + SPIFFS 预烧录
## 核心变更

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:23:04 +08:00
73 changed files with 44735 additions and 879 deletions

View File

@ -1,207 +1,44 @@
rdzleo@RdzleodeMac-Studio Baji_Rtc_Toy % '/Users/rdzleo/.espressif/python_env/idf5.4_py3.13_env/bin/python3' '/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf/t
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<std::mutex>::lock() at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:142
--- 0x4201bf6f: std::unique_lock<std::mutex>::unique_lock(std::mutex&) at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:73
--- (inlined by) BackgroundTask::WaitForCompletion() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/background_task.cc:46
--- 0x420174b6: Application::SetDeviceState(DeviceState) at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:2262
--- 0x42018c69: Application::Start() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:533
--- 0x4202069d: app_main at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/main.cc:105
--- 0x42229453: main_task at /Users/rdzleo/esp/esp-idf/components/freertos/app_startup.c:208
--- 0x40387e11: vPortTaskWrapper at /Users/rdzleo/esp/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/port.c:139
ELF file SHA256: 0061c1350
Rebooting...
ESP-ROM:esp32s3-20210327
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按键触发设备状态=1WiFi连接状态=未连接
I (6992) Airhub1: 🔄 开始重置WiFi配置清除已保存的WiFi凭据
I (6992) wifi:state: run -> init (0x0)
I (6992) wifi:pm stop, total sleep time: 1072363 us / 1413544 us
I (6992) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1, snd_ch_cfg:0x0
I (6992) wifi: Reconnecting airhub (attempt 1 / 5)
I (7102) wifi:flush txq
I (7102) wifi:stop sw txq
I (7102) wifi:lmac stop hw txq
I (7102) Airhub1: ✅ 已清除所有WiFi凭据设备将进入配网模式
I (7102) WifiBoard: 🔄 重置WiFi配置设备将重启进入配网模式
I (7612) WifiBoard: 🔄 正在重启设备...
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0xc (RTC_SW_CPU_RST),boot:0xb (SPI_FAST_FLASH_BOOT)
Saved PC:0x40379e85
--- 0x40379e85: esp_restart_noos at /Users/rdzleo/esp/esp-idf/components/esp_system/port/soc/esp32s3/system_internal.c:162
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:<ba-add>idx:0 (ifx:0, 70:2a:d7:85:bc:eb), tid:0, ssn:0, winSize:64
I (49371) esp_netif_handlers: sta ip: 192.168.124.22, mask: 255.255.255.0, gw: 192.168.124.1
I (49371) BluetoothProvisioning: ✅ WiFi获取IP地址成功: 192.168.124.22
I (49371) BluetoothProvisioning: 💾 启用WiFi配置自动保存到NVS存储...
I (49371) BluetoothProvisioning: ✅ WiFi配置将自动保存到NVS存储
I (49371) BluetoothProvisioning: 📋 获取当前WiFi配置成功SSID: airhub
I (49371) SsidManager: compare [airhub:6] [airhub:6]
W (49371) SsidManager: SSID airhub already exists, overwrite it
I (49371) BluetoothProvisioning: ✅ WiFi凭据已保存到NVS列表
I (49371) BluetoothProvisioning: BluetoothProvisioning WIFI_CONNECTED skip_session=0
I (49371) BluetoothProvisioning: 🔍 准备设置状态为SUCCESS并触发回调
I (49371) BluetoothProvisioning: 🔄 配网状态变化: CONNECTED -> SUCCESS
I (49371) WifiBoard: 设备配网成功,已连接到WiFi网络!
I (49371) BluetoothProvisioning: 🔍 [DEBUG] ReportWiFiStatus调用: success=true, client_connected_=true
I (49371) BluetoothProvisioning: 向客户端报告设备连接WiFi成功!
I (49371) BluetoothProvisioning: 📋 配网流程完成,状态: SUCCESS, client_connected_: true
I (49371) BluetoothProvisioning: ⏰ 延迟2000ms后重启设备以确保配置生效...
W (50081) wifi:m f null
W (50121) wifi:m f null
Backtrace: 0x40379f49:0x3fcbcbe0 0x4038731d:0x3fcbcc00 0x4038f9d9:0x3fcbcc20 0x421cd333:0x3fcbcc90 0x421cd368:0x3fcbccb0 0x421cd443:0x3fcbccd0 0x421ddb09:0x3fcbccf0 0x42013a05:0x3fcbcd30 0x4201bf6f:0x3fcbcd50 0x420174b6:0x3fcbcd80 0x42018c69:0x3fcbcde0 0x4202069d:0x3fcbd370 0x42229453:0x3fcbd390 0x40387e11:0x3fcbd3c0
.--- 0x40379f49: panic_abort at /Users/rdzleo/esp/esp-idf/components/esp_system/panic.c:469
--- 0x4038731d: esp_system_abort at /Users/rdzleo/esp/esp-idf/components/esp_system/port/esp_system_chip.c:87
--- 0x4038f9d9: abort at /Users/rdzleo/esp/esp-idf/components/newlib/abort.c:38
--- 0x421cd333: __cxxabiv1::__terminate(void (*)()) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:48
--- 0x421cd368: std::terminate() at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_terminate.cc:58
--- 0x421cd443: __cxa_throw at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/libsupc++/eh_throw.cc:98
--- 0x421ddb09: std::__throw_system_error(int) at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/gcc/libstdc++-v3/src/c++11/system_error.cc:595
--- 0x42013a05: std::unique_lock<std::mutex>::lock() at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:142
--- 0x4201bf6f: std::unique_lock<std::mutex>::unique_lock(std::mutex&) at /Users/rdzleo/.espressif/tools/xtensa-esp-elf/esp-14.2.0_20241119/xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/unique_lock.h:73
--- (inlined by) BackgroundTask::WaitForCompletion() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/background_task.cc:46
--- 0x420174b6: Application::SetDeviceState(DeviceState) at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:2262
--- 0x42018c69: Application::Start() at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/application.cc:533
--- 0x4202069d: app_main at /Users/rdzleo/Desktop/Baji_Rtc_Toy/main/main.cc:105
--- 0x42229453: main_task at /Users/rdzleo/esp/esp-idf/components/freertos/app_startup.c:208
--- 0x40387e11: vPortTaskWrapper at /Users/rdzleo/esp/esp-idf/components/freertos/FreeRTOS-Kernel/portable/xtensa/port.c:139
W (50181) wifi:m f null
W (51101) wifi:m f null
W (51151) wifi:m f null
W (51201) wifi:m f null
ELF file SHA256: 0061c1350
W (52071) wifi:m f null
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
Rebooting...

View File

@ -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 配网BluedroidService 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 关闭**
- BLEGATT Server图片传输 + 配网服务)
- 显示ScreenHome主界面→ ScreenImg图片浏览→ ScreenSet设置
- 功能GIF 播放、JPEG 解码、SPIFFS 图片管理、手电筒、低功耗
- BLEGATT 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小时无崩溃

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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 <cstring>
#include <esp_log.h>
@ -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();

View File

@ -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,

View File

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

View File

@ -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 <cmath> // 添加数学函数头文件
#include <wifi_station.h>
@ -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 <driver/touch_pad.h>
#include <driver/touch_sensor.h>
@ -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<MovecallMojiESP32S3*>(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<MovecallMojiESP32S3*>(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<WifiBoard*>(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<WifiBoard*>(self);
if (wifi_board && wifi_board->IsBleProvisioningActive()) {
ESP_LOGI(TAG, "BOOT长按5秒进入生产测试模式");
self->EnterProductionTestMode();
}
}, nullptr);
}
ESP_LOGI(TAG, "Boot button initialized on GPIO%d", BOOT_BUTTON_GPIO);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1042
main/dzbj/pages.c Normal file

File diff suppressed because it is too large Load Diff

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

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

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

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

View File

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

View File

@ -1,15 +0,0 @@
// Stub 实现dzbj UI 文件引用的函数Phase 1 仅提供空实现
// 实际实现将在后续阶段添加
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
const char* get_current_image(void) { return NULL; }
bool delete_current_image(void) { return false; }
void init_spiffs_image_list(void) {}
void free_spiffs_image_list(void) {}
bool set_image_index_by_name(const char *name) { (void)name; return false; }
const char* get_next_image(void) { return NULL; }
const char* get_prev_image(void) { return NULL; }
void update_ui_ImgBle(const char *img_name) { (void)img_name; }

View File

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

View File

@ -0,0 +1 @@
defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7

View File

@ -0,0 +1,40 @@
## 1.3.1
- Fixed the format of Kconfig file
## 1.3.0
- Added option to get image size without decoding it
## 1.2.1
- Fixed decoding of non-conforming 0xFFFF marker
## 1.2.0
- Added option to for passing user defined working buffer
## 1.1.0
- Added support for decoding images without Huffman tables
- Fixed undefined configuration options from Kconfig
## 1.0.5~3
- Added option to swap output color bytes regardless of JD_FORMAT
## 1.0.4
- Added ROM implementation support for ESP32-C6
## 1.0.2
- Fixed compiler warnings
## 1.0.1
- Fixed: exclude ESP32-C2 from list of ROM implementations
## 1.0.0
- Initial version

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
set(sources "jpeg_decoder.c")
set(includes "include")
# Compile only when cannot use ROM code
if(NOT CONFIG_JD_USE_ROM)
list(APPEND sources "tjpgd/tjpgd.c")
list(APPEND includes "tjpgd")
endif()
if(CONFIG_JD_DEFAULT_HUFFMAN)
list(APPEND sources "jpeg_default_huffman_table.c")
endif()
idf_component_register(SRCS ${sources} INCLUDE_DIRS ${includes})

View File

@ -0,0 +1,80 @@
menu "JPEG Decoder"
config JD_USE_ROM
bool "Use TinyJPG Decoder from ROM"
depends on ESP_ROM_HAS_JPEG_DECODE
default y
help
By default, Espressif SoCs use TJpg decoder implemented in ROM code.
If this feature is disabled, new configuration of TJpg decoder can be used.
Refer to REAME.md for more details.
config JD_SZBUF
int "Size of stream input buffer"
depends on !JD_USE_ROM
default 512
config JD_FORMAT
int
depends on !JD_USE_ROM
default 0 if JD_FORMAT_RGB888
default 1 if JD_FORMAT_RGB565
choice
prompt "Output pixel format"
depends on !JD_USE_ROM
default JD_FORMAT_RGB888
help
Output format is selected at runtime.
config JD_FORMAT_RGB888
bool "Support RGB565 and RGB888 output (16-bit/pix and 24-bit/pix)"
config JD_FORMAT_RGB565
bool "Support RGB565 output (16-bit/pix)"
endchoice
config JD_USE_SCALE
bool "Enable descaling"
depends on !JD_USE_ROM
default y
help
If scaling is enabled, size of output image can be lowered during decoding.
config JD_TBLCLIP
bool "Use table conversion for saturation arithmetic"
depends on !JD_USE_ROM
default y
help
Use table conversion for saturation arithmetic. A bit faster, but increases 1 KB of code size.
config JD_FASTDECODE
int
depends on !JD_USE_ROM
default 0 if JD_FASTDECODE_BASIC
default 1 if JD_FASTDECODE_32BIT
default 2 if JD_FASTDECODE_TABLE
choice
prompt "Optimization level"
depends on !JD_USE_ROM
default JD_FASTDECODE_32BIT
config JD_FASTDECODE_BASIC
bool "Basic optimization. Suitable for 8/16-bit MCUs"
config JD_FASTDECODE_32BIT
bool "+ 32-bit barrel shifter. Suitable for 32-bit MCUs"
config JD_FASTDECODE_TABLE
bool "+ Table conversion for huffman decoding (wants 6 << HUFF_BIT bytes of RAM)"
endchoice
config JD_DEFAULT_HUFFMAN
bool "Support images without Huffman table"
depends on !JD_USE_ROM
default n
help
Enable this option to support decoding JPEG images that lack an embedded Huffman table.
When enabled, a default Huffman table is used during decoding, allowing the JPEG decoder to handle
images without explicitly provided Huffman tables.
Note: Enabling this option increases ROM usage due to the inclusion of default Huffman tables.
endmenu

View File

@ -0,0 +1,112 @@
# JPEG Decoder: TJpgDec - Tiny JPEG Decompressor
[![Component Registry](https://components.espressif.com/components/espressif/esp_jpeg/badge.svg)](https://components.espressif.com/components/espressif/esp_jpeg)
![maintenance-status](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg)
TJpgDec is a lightweight JPEG image decompressor optimized for embedded systems with minimal memory consumption.
On some microcontrollers, TJpgDec is available in ROM and will be used by default, though this can be disabled in menuconfig if desired[^1].
[^1]: **_NOTE:_** When the ROM decoder is used, the configuration can't be changed. The configuration is fixed.
## Features
**Compilation configuration:**
- Stream input buffer size (default: 512 bytes)
- Output pixel format (default: RGB888; options: RGB888/RGB565)
- Enable/disable output descaling (default: enabled)
- Use table-based saturation for arithmetic operations (default: enabled)
- Use default Huffman tables: Useful from decoding frames from cameras, that do not provide Huffman tables (default: disabled to save ROM)
- Three optimization levels (default: 32-bit MCUs) for different CPU types:
- 8/16-bit MCUs
- 32-bit MCUs
- Table-based Huffman decoding
**Runtime configuration:**
- Pixel format options: RGB888, RGB565
- Selectable scaling ratios: 1/1, 1/2, 1/4, or 1/8 (chosen at decompression)
- Option to swap the first and last bytes of color values
## TJpgDec in ROM
On certain microcontrollers, TJpgDec is available in ROM and used by default. This can be disabled in menuconfig if you prefer to use the library code provided in this component.
### List of MCUs, which have TJpgDec in ROM
- ESP32
- ESP32-S3
- ESP32-C3
- ESP32-C6
- ESP32-C5
- ESP32-C61
### Fixed compilation configuration of the ROM code
The ROM version uses the following fixed settings:
- Stream input buffer: 512 bytes
- Output pixel format: RGB888
- Output descaling: enabled
- Saturation table: enabled
- Optimization level: Basic (JD_FASTDECODE = 0)
### Pros and cons using ROM code
**Advantages:**
- Saves approximately 5 KB of flash memory with the same configuration
**Disadvantages:**
- Compilation configuration cannot be changed
- Certain configurations may provide faster performance
## Speed comparison
The table below shows example decoding times for a JPEG image using various configurations:
* Image size: 320 x 180 px
* Output format: RGB565
* CPU: ESP32-S3
* CPU frequency: 240 MHz
* SPI mode: DIO
* Internal RAM used
* Measured in 1000 retries
| ROM used | JD_SZBUF | JD_FORMAT | JD_USE_SCALE | JD_TBLCLIP | JD_FASTDECODE | RAM buffer | Flash size | Approx. time |
| :------: | :------: | :-------: | :----------: | :--------: | :-----------: | :--------: | :--------: | :----------: |
| YES | 512 | RGB888 | 1 | 1 | 0 | 3.1 kB | 0 kB | 52 ms |
| NO | 512 | RGB888 | 1 | 1 | 0 | 3.1 kB | 5 kB | 50 ms |
| NO | 512 | RGB888 | 1 | 0 | 0 | 3.1 kB | 4 kB | 68 ms |
| NO | 512 | RGB888 | 1 | 1 | 1 | 3.1 kB | 5 kB | 50 ms |
| NO | 512 | RGB888 | 1 | 0 | 1 | 3.1 kB | 4 kB | 62 ms |
| NO | 512 | RGB888 | 1 | 1 | 2 | 65.5 kB | 5.5 kB | 46 ms |
| NO | 512 | RGB888 | 1 | 0 | 2 | 65.5 kB | 4.5 kB | 59 ms |
| NO | 512 | RGB565 | 1 | 1 | 0 | 5 kB | 5 kB | 60 ms |
| NO | 512 | RGB565 | 1 | 1 | 1 | 5 kB | 5 kB | 59 ms |
| NO | 512 | RGB565 | 1 | 1 | 2 | 65.5 kB | 5.5 kB | 56 ms |
## Add to project
Packages from this repository are uploaded to [Espressif's component service](https://components.espressif.com/).
You can add them to your project via `idf.py add-dependancy`, e.g.
```
idf.py add-dependency esp_jpeg==1.0.0
```
Alternatively, you can create `idf_component.yml`. More is in [Espressif's documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-component-manager.html).
## Example use
Here is example of usage. This calling is **blocking**.
```
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = (uint8_t *)jpeg_img_buf,
.indata_size = jpeg_img_buf_size,
.outbuf = out_img_buf,
.outbuf_size = out_img_buf_size,
.out_format = JPEG_IMAGE_OUT_FORMAT_RGB565,
.out_scale = JPEG_IMAGE_SCALE_0,
.flags = {
.swap_color_bytes = 1,
}
};
esp_jpeg_image_output_t outimg;
esp_jpeg_decode(&jpeg_cfg, &outimg);
```

View File

@ -0,0 +1,7 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS main)
project(lcd_tjpgd)

View File

@ -0,0 +1,54 @@
# LCD tjpgd example
This example shows how to decode a jpeg image and display it on an SPI-interfaced LCD, and rotates the image periodically.
Example using initialization of the LCD from [ESP-BSP](https://github.com/espressif/esp-bsp) project. For change the Espressif's board, go to [idf_component.yml](main/idf_component.yml) and change `esp-box` to another board from BSP.
## How to Use Example
### Hardware Required
* An ESP development board
* An SPI-interfaced LCD
* An USB cable for power supply and programming
### Hardware Connection
The connection between ESP Board and the LCD is as follows:
```text
ESP Board LCD Screen
+---------+ +---------------------------------+
| | | |
| 3V3 +--------------+ VCC +----------------------+ |
| | | | | |
| GND +--------------+ GND | | |
| | | | | |
| DATA0 +--------------+ MOSI | | |
| | | | | |
| PCLK +--------------+ SCK | | |
| | | | | |
| CS +--------------+ CS | | |
| | | | | |
| D/C +--------------+ D/C | | |
| | | | | |
| RST +--------------+ RST | | |
| | | | | |
|BK_LIGHT +--------------+ BCKL +----------------------+ |
| | | |
+---------+ +---------------------------------+
```
The GPIO numbers used by this example is taken from BSP.
### Build and Flash
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. A flowing picture will be shown on the LCD screen.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
## Troubleshooting
For any technical queries, please open an [issue] (https://github.com/espressif/idf-extra-components/issues) on GitHub. We will get back to you soon.

View File

@ -0,0 +1,9 @@
set(srcs "pretty_effect.c"
"lcd_tjpgd_example_main.c"
"decode_image.c"
)
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
EMBED_FILES image.jpg
PRIV_REQUIRES esp_lcd)

View File

@ -0,0 +1,9 @@
menu "Example Configuration"
config EXAMPLE_LCD_FLUSH_PARALLEL_LINES
int "LCD flush parallel lines"
default 12 if IDF_TARGET_ESP32C2
default 16
help
To speed up transfers, every SPI transfer sends a bunch of lines.
endmenu

View File

@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
/*
The image used for the effect on the LCD in the SPI master example is stored in flash
as a jpeg file. This file contains the decode_image routine, which uses the tiny JPEG
decoder library to decode this JPEG into a format that can be sent to the display.
Keep in mind that the decoder library cannot handle progressive files (will give
``Image decoder: jd_prepare failed (8)`` as an error) so make sure to save in the correct
format if you want to use a different image file.
*/
#include <string.h>
#include "decode_image.h"
#include "jpeg_decoder.h"
#include "esp_log.h"
#include "esp_check.h"
#include "freertos/FreeRTOS.h"
//Reference the binary-included jpeg file
extern const uint8_t image_jpg_start[] asm("_binary_image_jpg_start");
extern const uint8_t image_jpg_end[] asm("_binary_image_jpg_end");
//Define the height and width of the jpeg file. Make sure this matches the actual jpeg
//dimensions.
const char *TAG = "ImageDec";
//Decode the embedded image into pixel lines that can be used with the rest of the logic.
esp_err_t decode_image(uint16_t **pixels)
{
*pixels = NULL;
esp_err_t ret = ESP_OK;
//Alocate pixel memory. Each line is an array of IMAGE_W 16-bit pixels; the `*pixels` array itself contains pointers to these lines.
*pixels = calloc(IMAGE_H * IMAGE_W, sizeof(uint16_t));
ESP_GOTO_ON_FALSE((*pixels), ESP_ERR_NO_MEM, err, TAG, "Error allocating memory for lines");
//JPEG decode config
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = (uint8_t *)image_jpg_start,
.indata_size = image_jpg_end - image_jpg_start,
.outbuf = (uint8_t *)(*pixels),
.outbuf_size = IMAGE_W * IMAGE_H * sizeof(uint16_t),
.out_format = JPEG_IMAGE_FORMAT_RGB565,
.out_scale = JPEG_IMAGE_SCALE_0,
.flags = {
.swap_color_bytes = 1,
}
};
//JPEG decode
esp_jpeg_image_output_t outimg;
esp_jpeg_decode(&jpeg_cfg, &outimg);
ESP_LOGI(TAG, "JPEG image decoded! Size of the decoded image is: %dpx x %dpx", outimg.width, outimg.height);
return ret;
err:
//Something went wrong! Exit cleanly, de-allocating everything we allocated.
if (*pixels != NULL) {
free(*pixels);
}
return ret;
}

View File

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#define IMAGE_W 320
#define IMAGE_H 240
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Decode the jpeg ``image.jpg`` embedded into the program file into pixel data.
*
* @param pixels A pointer to a pointer for an array of rows, which themselves are an array of pixels.
* Effectively, you can get the pixel data by doing ``decode_image(&myPixels); pixelval=myPixels[ypos][xpos];``
* @return - ESP_ERR_NOT_SUPPORTED if image is malformed or a progressive jpeg file
* - ESP_ERR_NO_MEM if out of memory
* - ESP_OK on succesful decode
*/
esp_err_t decode_image(uint16_t **pixels);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,16 @@
dependencies:
esp-box:
rules:
- if: target == esp32s3
version: ^2.4
esp32_s2_kaluga_kit:
rules:
- if: target == esp32s2
version: ^3.0
esp_jpeg:
version: '>=1.0.2'
esp_wrover_kit:
rules:
- if: target == esp32
version: ^1.5
idf: '>=5.0'

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_panel_ops.h"
#include "esp_heap_caps.h"
#include "pretty_effect.h"
#include "bsp/esp-bsp.h"
#include "bsp/display.h"
// Using SPI2 in the example, as it also supports octal modes on some targets
#define LCD_HOST SPI2_HOST
// To speed up transfers, every SPI transfer sends a bunch of lines. This define specifies how many.
// More means more memory use, but less overhead for setting up / finishing transfers. Make sure 240
// is dividable by this.
#define PARALLEL_LINES CONFIG_EXAMPLE_LCD_FLUSH_PARALLEL_LINES
// The number of frames to show before rotate the graph
#define ROTATE_FRAME 30
#if BSP_LCD_H_RES > BSP_LCD_V_RES
#define EXAMPLE_LCD_SWAP 0
#define EXAMPLE_LCD_H_RES BSP_LCD_H_RES
#define EXAMPLE_LCD_V_RES BSP_LCD_V_RES
#else
#define EXAMPLE_LCD_SWAP 1
#define EXAMPLE_LCD_H_RES BSP_LCD_V_RES
#define EXAMPLE_LCD_V_RES BSP_LCD_H_RES
#endif
// Simple routine to generate some patterns and send them to the LCD. Because the
// SPI driver handles transactions in the background, we can calculate the next line
// while the previous one is being sent.
static uint16_t *s_lines[2];
static void display_pretty_colors(esp_lcd_panel_handle_t panel_handle)
{
int frame = 0;
// Indexes of the line currently being sent to the LCD and the line we're calculating
int sending_line = 0;
int calc_line = 0;
// After ROTATE_FRAME frames, the image will be rotated
while (frame <= ROTATE_FRAME) {
frame++;
for (int y = 0; y < EXAMPLE_LCD_V_RES; y += PARALLEL_LINES) {
// Calculate a line
pretty_effect_calc_lines(s_lines[calc_line], y, frame, PARALLEL_LINES);
sending_line = calc_line;
calc_line = !calc_line;
// Send the calculated data
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, 0 + EXAMPLE_LCD_H_RES, y + PARALLEL_LINES, s_lines[sending_line]);
}
}
}
void app_main(void)
{
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_handle_t panel_handle = NULL;
bsp_display_config_t disp_cfg = {
.max_transfer_sz = EXAMPLE_LCD_H_RES * PARALLEL_LINES * sizeof(uint16_t),
};
// Display initialize from BSP
bsp_display_new(&disp_cfg, &panel_handle, &io_handle);
esp_lcd_panel_disp_on_off(panel_handle, true);
bsp_display_backlight_on();
// Initialize the effect displayed
ESP_ERROR_CHECK(pretty_effect_init());
// "Rotate or not" flag
bool is_rotated = false;
// Allocate memory for the pixel buffers
for (int i = 0; i < 2; i++) {
s_lines[i] = heap_caps_malloc(EXAMPLE_LCD_H_RES * PARALLEL_LINES * sizeof(uint16_t), MALLOC_CAP_DMA);
assert(s_lines[i] != NULL);
}
#if EXAMPLE_LCD_SWAP
esp_lcd_panel_swap_xy(panel_handle, true);
#endif
// Start and rotate
while (1) {
// Set driver configuration to rotate 180 degrees each time
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, is_rotated, is_rotated));
// Display
display_pretty_colors(panel_handle);
is_rotated = !is_rotated;
}
}

View File

@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <math.h>
#include "pretty_effect.h"
#include "decode_image.h"
uint16_t *pixels;
//Grab a rgb16 pixel from the esp32_tiles image
static inline uint16_t get_bgnd_pixel(int x, int y)
{
//Get color of the pixel on x,y coords
return (uint16_t) * (pixels + (y * IMAGE_W) + x);
}
//This variable is used to detect the next frame.
static int prev_frame = -1;
//Instead of calculating the offsets for each pixel we grab, we pre-calculate the valueswhenever a frame changes, then re-use
//these as we go through all the pixels in the frame. This is much, much faster.
static int8_t xofs[320], yofs[240];
static int8_t xcomp[320], ycomp[240];
//Calculate the pixel data for a set of lines (with implied line size of 320). Pixels go in dest, line is the Y-coordinate of the
//first line to be calculated, linect is the amount of lines to calculate. Frame increases by one every time the entire image
//is displayed; this is used to go to the next frame of animation.
void pretty_effect_calc_lines(uint16_t *dest, int line, int frame, int linect)
{
if (frame != prev_frame) {
//We need to calculate a new set of offset coefficients. Take some random sines as offsets to make everything
//look pretty and fluid-y.
for (int x = 0; x < 320; x++) {
xofs[x] = sin(frame * 0.15 + x * 0.06) * 4;
}
for (int y = 0; y < 240; y++) {
yofs[y] = sin(frame * 0.1 + y * 0.05) * 4;
}
for (int x = 0; x < 320; x++) {
xcomp[x] = sin(frame * 0.11 + x * 0.12) * 4;
}
for (int y = 0; y < 240; y++) {
ycomp[y] = sin(frame * 0.07 + y * 0.15) * 4;
}
prev_frame = frame;
}
for (int y = line; y < line + linect; y++) {
for (int x = 0; x < 320; x++) {
*dest++ = get_bgnd_pixel(x + yofs[y] + xcomp[x], y + xofs[x] + ycomp[y]);
}
}
}
esp_err_t pretty_effect_init(void)
{
return decode_image(&pixels);
}

View File

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Calculate the effect for a bunch of lines.
*
* @param dest Destination for the pixels. Assumed to be LINECT * 320 16-bit pixel values.
* @param line Starting line of the chunk of lines.
* @param frame Current frame, used for animation
* @param linect Amount of lines to calculate
*/
void pretty_effect_calc_lines(uint16_t *dest, int line, int frame, int linect);
/**
* @brief Initialize the effect
*
* @return ESP_OK on success, an error from the jpeg decoder otherwise.
*/
esp_err_t pretty_effect_init(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,4 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.5.0 Project Minimal Configuration
#
CONFIG_TOUCH_SUPPRESS_DEPRECATE_WARN=y

View File

@ -0,0 +1,9 @@
dependencies:
idf: '>=5.0'
description: 'JPEG Decoder: TJpgDec'
repository: git://github.com/espressif/idf-extra-components.git
repository_info:
commit_sha: 746e83ddbea0db9c3d24993a87c4c737a60337ae
path: esp_jpeg
url: https://github.com/espressif/idf-extra-components/tree/master/esp_jpeg/
version: 1.3.1

View File

@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Scale of output image
*
*/
typedef enum {
JPEG_IMAGE_SCALE_0 = 0, /*!< No scale */
JPEG_IMAGE_SCALE_1_2, /*!< Scale 1:2 */
JPEG_IMAGE_SCALE_1_4, /*!< Scale 1:4 */
JPEG_IMAGE_SCALE_1_8, /*!< Scale 1:8 */
} esp_jpeg_image_scale_t;
/**
* @brief Format of output image
*
*/
typedef enum {
JPEG_IMAGE_FORMAT_RGB888 = 0, /*!< Format RGB888 */
JPEG_IMAGE_FORMAT_RGB565, /*!< Format RGB565 */
} esp_jpeg_image_format_t;
/**
* @brief JPEG Configuration Type
*
*/
typedef struct esp_jpeg_image_cfg_s {
uint8_t *indata; /*!< Input JPEG image */
uint32_t indata_size; /*!< Size of input image */
uint8_t *outbuf; /*!< Output buffer */
uint32_t outbuf_size; /*!< Output buffer size */
esp_jpeg_image_format_t out_format; /*!< Output image format */
esp_jpeg_image_scale_t out_scale; /*!< Output scale */
struct {
uint8_t swap_color_bytes: 1; /*!< Swap first and last color bytes */
} flags;
struct {
void *working_buffer; /*!< If set to NULL, a working buffer will be allocated in esp_jpeg_decode().
Tjpgd does not use dynamic allocation, se we pass this buffer to Tjpgd that uses it as scratchpad */
size_t working_buffer_size; /*!< Size of the working buffer. Must be set it working_buffer != NULL.
Default size is 3.1kB or 65kB if JD_FASTDECODE == 2 */
} advanced;
struct {
uint32_t read; /*!< Internal count of read bytes */
} priv;
} esp_jpeg_image_cfg_t;
/**
* @brief JPEG output info
*/
typedef struct esp_jpeg_image_output_s {
uint16_t width; /*!< Width of the output image */
uint16_t height; /*!< Height of the output image */
size_t output_len; /*!< Length of the output image in bytes */
} esp_jpeg_image_output_t;
/**
* @brief Decode JPEG image
*
* @note This function is blocking.
*
* @param[in] cfg: Configuration structure
* @param[out] img: Output image info
*
* @return
* - ESP_OK on success
* - ESP_ERR_NO_MEM if there is no memory for allocating main structure
* - ESP_FAIL if there is an error in decoding JPEG
*/
esp_err_t esp_jpeg_decode(esp_jpeg_image_cfg_t *cfg, esp_jpeg_image_output_t *img);
/**
* @brief Get information about the JPEG image
*
* Use this function to get the size of the JPEG image without decoding it.
* Allocate a buffer of size img->output_len to store the decoded image.
*
* @note cfg->outbuf and cfg->outbuf_size are not used in this function.
* @param[in] cfg: Configuration structure
* @param[out] img: Output image info
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if cfg or img is NULL
* - ESP_FAIL if there is an error in decoding JPEG
*/
esp_err_t esp_jpeg_get_image_info(esp_jpeg_image_cfg_t *cfg, esp_jpeg_image_output_t *img);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,287 @@
/*
* SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "esp_system.h"
#include "esp_rom_caps.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_check.h"
#include "jpeg_decoder.h"
#if CONFIG_JD_USE_ROM
/* When supported in ROM, use ROM functions */
#if defined(ESP_ROM_HAS_JPEG_DECODE)
#include "rom/tjpgd.h"
#else
#error Using JPEG decoder from ROM is not supported for selected target. Please select external code in menuconfig.
#endif
/* The ROM code of TJPGD is older and has different return type in decode callback */
typedef unsigned int jpeg_decode_out_t;
#else
/* When Tiny JPG Decoder is not in ROM or selected external code */
#include "tjpgd.h"
/* The TJPGD outside the ROM code is newer and has different return type in decode callback */
typedef int jpeg_decode_out_t;
#endif
static const char *TAG = "JPEG";
#define LOBYTE(u16) ((uint8_t)(((uint16_t)(u16)) & 0xff))
#define HIBYTE(u16) ((uint8_t)((((uint16_t)(u16))>>8) & 0xff))
#if defined(JD_FASTDECODE) && (JD_FASTDECODE == 2)
#define JPEG_WORK_BUF_SIZE 65472
#else
#define JPEG_WORK_BUF_SIZE 3100 /* Recommended buffer size; Independent on the size of the image */
#endif
/* If not set JD_FORMAT, it is set in ROM to RGB888, otherwise, it can be set in config */
#ifndef JD_FORMAT
#define JD_FORMAT 0
#endif
/* Output color bytes from tjpgd (depends on JD_FORMAT) */
#if (JD_FORMAT==0)
#define ESP_JPEG_COLOR_BYTES 3
#elif (JD_FORMAT==1)
#define ESP_JPEG_COLOR_BYTES 2
#elif (JD_FORMAT==2)
#error Grayscale image output format is not supported
#define ESP_JPEG_COLOR_BYTES 1
#endif
/*******************************************************************************
* Function definitions
*******************************************************************************/
static uint8_t jpeg_get_div_by_scale(esp_jpeg_image_scale_t scale);
static uint8_t jpeg_get_color_bytes(esp_jpeg_image_format_t format);
static unsigned int jpeg_decode_in_cb(JDEC *jd, uint8_t *buff, unsigned int nbyte);
static jpeg_decode_out_t jpeg_decode_out_cb(JDEC *jd, void *bitmap, JRECT *rect);
static inline uint16_t ldb_word(const void *ptr);
/*******************************************************************************
* Public API functions
*******************************************************************************/
esp_err_t esp_jpeg_decode(esp_jpeg_image_cfg_t *cfg, esp_jpeg_image_output_t *img)
{
esp_err_t ret = ESP_OK;
uint8_t *workbuf = NULL;
JRESULT res;
JDEC JDEC;
assert(cfg != NULL);
assert(img != NULL);
const bool allocate_buffer = (cfg->advanced.working_buffer == NULL);
const size_t workbuf_size = allocate_buffer ? JPEG_WORK_BUF_SIZE : cfg->advanced.working_buffer_size;
if (allocate_buffer) {
workbuf = heap_caps_malloc(JPEG_WORK_BUF_SIZE, MALLOC_CAP_DEFAULT);
ESP_GOTO_ON_FALSE(workbuf, ESP_ERR_NO_MEM, err, TAG, "no mem for JPEG work buffer");
} else {
workbuf = cfg->advanced.working_buffer;
ESP_RETURN_ON_FALSE(workbuf_size != 0, ESP_ERR_INVALID_ARG, TAG, "Working buffer size not defined!");
}
cfg->priv.read = 0;
/* Prepare image */
res = jd_prepare(&JDEC, jpeg_decode_in_cb, workbuf, workbuf_size, cfg);
ESP_GOTO_ON_FALSE((res == JDR_OK), ESP_FAIL, err, TAG, "Error in preparing JPEG image! %d", res);
const uint8_t scale_div = jpeg_get_div_by_scale(cfg->out_scale);
const uint8_t out_color_bytes = jpeg_get_color_bytes(cfg->out_format);
/* Size of output image */
const uint32_t outsize = (JDEC.height / scale_div) * (JDEC.width / scale_div) * out_color_bytes;
ESP_GOTO_ON_FALSE((outsize <= cfg->outbuf_size), ESP_ERR_NO_MEM, err, TAG, "Not enough size in output buffer!");
/* Size of output image */
img->height = JDEC.height / scale_div;
img->width = JDEC.width / scale_div;
img->output_len = outsize;
/* Decode JPEG */
res = jd_decomp(&JDEC, jpeg_decode_out_cb, cfg->out_scale);
ESP_GOTO_ON_FALSE((res == JDR_OK), ESP_FAIL, err, TAG, "Error in decoding JPEG image! %d", res);
err:
if (workbuf && allocate_buffer) {
free(workbuf);
}
return ret;
}
esp_err_t esp_jpeg_get_image_info(esp_jpeg_image_cfg_t *cfg, esp_jpeg_image_output_t *img)
{
if (cfg == NULL || img == NULL) {
return ESP_ERR_INVALID_ARG;
} else if (cfg->indata == NULL || cfg->indata_size < 5) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = ESP_FAIL;
if (ldb_word(cfg->indata) != 0xFFD8) {
return ESP_FAIL; /* Err: SOI is not detected */
}
unsigned ofs = 2; // Start after SOI marker
while (true) {
/* Get a JPEG marker */
uint8_t *seg = cfg->indata + ofs; /* Segment pointer */
unsigned short marker = ldb_word(seg); /* Marker */
unsigned int len = ldb_word(seg + 2); /* Length field */
if (len <= 2 || (marker >> 8) != 0xFF) {
return ESP_FAIL;
}
ofs += 2 + len; /* Number of bytes loaded */
if (ofs > cfg->indata_size) {
return ESP_FAIL; // No more data
}
if ((marker & 0xFF) == 0xC0) { /* SOF0 (baseline JPEG) */
seg += 4; /* Skip marker and length field */
/* Size of output image */
img->height = ldb_word(seg + 1);
img->width = ldb_word(seg + 3);
const uint8_t scale_div = jpeg_get_div_by_scale(cfg->out_scale);
const uint8_t out_color_bytes = jpeg_get_color_bytes(cfg->out_format);
img->output_len = (img->height / scale_div) * (img->width / scale_div) * out_color_bytes;
ret = ESP_OK;
break;
}
}
return ret;
}
/*******************************************************************************
* Private API functions
*******************************************************************************/
static unsigned int jpeg_decode_in_cb(JDEC *dec, uint8_t *buff, unsigned int nbyte)
{
assert(dec != NULL);
uint32_t to_read = nbyte;
esp_jpeg_image_cfg_t *cfg = (esp_jpeg_image_cfg_t *)dec->device;
assert(cfg != NULL);
if (buff) {
if (cfg->priv.read + to_read > cfg->indata_size) {
to_read = cfg->indata_size - cfg->priv.read;
}
/* Copy data from JPEG image */
memcpy(buff, &cfg->indata[cfg->priv.read], to_read);
cfg->priv.read += to_read;
} else if (buff == NULL) {
/* Skip data */
cfg->priv.read += to_read;
}
return to_read;
}
static jpeg_decode_out_t jpeg_decode_out_cb(JDEC *dec, void *bitmap, JRECT *rect)
{
uint16_t color = 0;
assert(dec != NULL);
esp_jpeg_image_cfg_t *cfg = (esp_jpeg_image_cfg_t *)dec->device;
assert(cfg != NULL);
assert(bitmap != NULL);
assert(rect != NULL);
uint8_t scale_div = jpeg_get_div_by_scale(cfg->out_scale);
uint8_t out_color_bytes = jpeg_get_color_bytes(cfg->out_format);
/* Copy decoded image data to output buffer */
uint8_t *in = (uint8_t *)bitmap;
uint32_t line = dec->width / scale_div;
uint8_t *dst = (uint8_t *)cfg->outbuf;
for (int y = rect->top; y <= rect->bottom; y++) {
for (int x = rect->left; x <= rect->right; x++) {
if ( (JD_FORMAT == 0 && cfg->out_format == JPEG_IMAGE_FORMAT_RGB888) ||
(JD_FORMAT == 1 && cfg->out_format == JPEG_IMAGE_FORMAT_RGB565) ) {
/* Output image format is same as set in TJPGD */
for (int b = 0; b < ESP_JPEG_COLOR_BYTES; b++) {
if (cfg->flags.swap_color_bytes) {
dst[(y * line * out_color_bytes) + x * out_color_bytes + b] = in[out_color_bytes - b - 1];
} else {
dst[(y * line * out_color_bytes) + x * out_color_bytes + b] = in[b];
}
}
} else if (JD_FORMAT == 0 && cfg->out_format == JPEG_IMAGE_FORMAT_RGB565) {
/* Output image format is not same as set in TJPGD */
/* We need to convert the 3 bytes in `in` to a rgb565 value */
color = ((in[0] & 0xF8) << 8);
color |= ((in[1] & 0xFC) << 3);
color |= (in[2] >> 3);
if (cfg->flags.swap_color_bytes) {
dst[(y * line * out_color_bytes) + (x * out_color_bytes)] = HIBYTE(color);
dst[(y * line * out_color_bytes) + (x * out_color_bytes) + 1] = LOBYTE(color);
} else {
dst[(y * line * out_color_bytes) + (x * out_color_bytes) + 1] = HIBYTE(color);
dst[(y * line * out_color_bytes) + (x * out_color_bytes)] = LOBYTE(color);
}
} else {
ESP_LOGE(TAG, "Selected output format is not supported!");
assert(0);
}
in += ESP_JPEG_COLOR_BYTES;
}
}
return 1;
}
static uint8_t jpeg_get_div_by_scale(esp_jpeg_image_scale_t scale)
{
switch (scale) {
/* Not scaled */
case JPEG_IMAGE_SCALE_0:
return 1;
/* Scaled 1:2 */
case JPEG_IMAGE_SCALE_1_2:
return 2;
/* Scaled 1:4 */
case JPEG_IMAGE_SCALE_1_4:
return 4;
/* Scaled 1:8 */
case JPEG_IMAGE_SCALE_1_8:
return 8;
}
return 1;
}
static uint8_t jpeg_get_color_bytes(esp_jpeg_image_format_t format)
{
switch (format) {
/* RGB888 (24-bit/pix) */
case JPEG_IMAGE_FORMAT_RGB888:
return 3;
/* RGB565 (16-bit/pix) */
case JPEG_IMAGE_FORMAT_RGB565:
return 2;
}
return 1;
}
static inline uint16_t ldb_word(const void *ptr)
{
const uint8_t *p = (const uint8_t *)ptr;
return ((uint16_t)p[0] << 8) | p[1];
}

View File

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
// Default Huffman tables for baseline JPEG
// These values are taken directly from CCITT Rec. T.81 (1992 E) Appendix K.3.3
// The *_num_bits array always contains exactly 16 elements.
// Each element represents the number of Huffman codes of a specific length:
// - The first element corresponds to codes of length 1 bit,
// - The second element to codes of length 2 bits, and so forth up to 16 bits.
//
// The *_values array has a length equal to the sum of all elements in the *_num_bits array,
// representing the actual values associated with each Huffman code in order.
// Luminance DC Table
const unsigned char esp_jpeg_lum_dc_num_bits[16] = {0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0};
const unsigned esp_jpeg_lum_dc_codes_total = 12;
const unsigned char esp_jpeg_lum_dc_values[12] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// Chrominance DC Table
const unsigned char esp_jpeg_chrom_dc_num_bits[16] = {0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0};
const unsigned esp_jpeg_chrom_dc_codes_total = 12;
const unsigned char esp_jpeg_chrom_dc_values[12] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// Luminance AC Table
const unsigned char esp_jpeg_lum_ac_num_bits[16] = {0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125};
const unsigned esp_jpeg_lum_ac_codes_total = 162;
const unsigned char esp_jpeg_lum_ac_values[162] = {
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
0xF9, 0xFA
};
// Chrominance AC Table
const unsigned char esp_jpeg_chrom_ac_num_bits[16] = {0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119};
const unsigned esp_jpeg_chrom_ac_codes_total = 162;
const unsigned char esp_jpeg_chrom_ac_values[162] = {
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0,
0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26,
0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5,
0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3,
0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA,
0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
0xF9, 0xFA
};

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS main)
project(esp_jpeg_test)

View File

@ -0,0 +1,5 @@
idf_component_register(SRCS "tjpgd_test.c" "test_tjpgd_main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES "unity"
WHOLE_ARCHIVE
EMBED_FILES "logo.jpg" "usb_camera.jpg" "usb_camera_2.jpg")

View File

@ -0,0 +1,4 @@
dependencies:
espressif/esp_jpeg:
version: "*"
override_path: "../../"

View File

@ -0,0 +1,64 @@
from PIL import Image
def jpg_to_rgb888_hex_c_array(input_filename: str, output_filename: str) -> str:
"""
Convert a .jpg file to RGB888 hex data and format it as a C-style array.
Parameters:
input_filename (str): The path to the JPEG file.
Returns:
str: A string representing the RGB888 hex data formatted as a C array.
"""
# Open the image file
with Image.open(input_filename) as img:
# Ensure the image is in RGB mode
rgb_img = img.convert("RGB")
# Get image dimensions
width, height = rgb_img.size
# List to store hex values as C-style entries
hex_data = []
# Iterate over each pixel to get RGB values
for y in range(height):
for x in range(width):
r, g, b = rgb_img.getpixel((x, y))
# Format each RGB value as C-style hex (e.g., 0xRRGGBB)
hex_data.append(f"0x{r:02X}{g:02X}{b:02X}")
# Format as a C-style array with line breaks for readability
hex_array = ",\n ".join(hex_data)
c_array = f"unsigned int image_data[{width * height}] = {{\n {hex_array}\n}};"
# Write the C array to the output file
with open(output_filename, "w") as file:
file.write(c_array)
print(f"C-style RGB888 hex array saved to {output_filename}")
return c_array
def main():
"""
Main function to convert a JPEG file to an RGB888 C-style hex array.
Instructions:
1. Replace 'input.jpg' with the path to your JPEG file.
2. Run the script to get the C-style array output.
"""
# Input JPEG file path
input_filename = "usb_camera.jpg" # Replace with your JPEG file path
# Output file path for the C array
output_filename = "output_array.c" # Specify your desired output filename
# Convert JPEG to C-style RGB888 hex array
jpg_to_rgb888_hex_c_array(input_filename, output_filename)
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,7 @@
// JPEG encoded image 46x46, 7561 bytes
extern const unsigned char logo_jpg[] asm("_binary_logo_jpg_start");
extern char _binary_logo_jpg_start;
extern char _binary_logo_jpg_end;
// Must be defined as macro because extern variables are not known at compile time (but at link time)
#define logo_jpg_len (&_binary_logo_jpg_end - &_binary_logo_jpg_start)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "unity.h"
#include "unity_test_runner.h"
#include "esp_heap_caps.h"
#include "esp_newlib.h"
#include "unity_test_utils_memory.h"
void setUp(void)
{
unity_utils_record_free_mem();
}
void tearDown(void)
{
esp_reent_cleanup(); //clean up some of the newlib's lazy allocations
unity_utils_evaluate_leaks_direct(0);
}
void app_main(void)
{
printf("Running esp_jpeg component tests\n");
unity_run_menu();
}

View File

@ -0,0 +1,12 @@
/*
Raw data from Logitech C170 USB camera was reconstructed to usb_camera_2.jpg
It was converted to RGB888 array with jpg_to_rgb888_hex.py
*/
// JPEG encoded frame 160x120, 1384 bytes, has broken 0xFFFF marker
extern const unsigned char camera_2_jpg[] asm("_binary_usb_camera_2_jpg_start");
extern char _binary_usb_camera_2_jpg_start;
extern char _binary_usb_camera_2_jpg_end;
// Must be defined as macro because extern variables are not known at compile time (but at link time)
#define camera_2_jpg_len (&_binary_usb_camera_2_jpg_end - &_binary_usb_camera_2_jpg_start)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
/*
Raw data from Logitech C270 USB camera was reconstructed to usb_camera.jpg
It was converted to RGB888 array with jpg_to_rgb888_hex.py
*/
// JPEG encoded frame 160x120, 2632 bytes, no huffman tables, double block size (16x8 pixels)
extern const unsigned char jpeg_no_huffman[] asm("_binary_usb_camera_jpg_start");
extern char _binary_usb_camera_jpg_start;
extern char _binary_usb_camera_jpg_end;
// Must be defined as macro because extern variables are not known at compile time (but at link time)
#define jpeg_no_huffman_len (&_binary_usb_camera_jpg_end - &_binary_usb_camera_jpg_start)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,328 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "sdkconfig.h"
#include "unity.h"
#include "jpeg_decoder.h"
#include "test_logo_jpg.h"
#include "test_logo_rgb888.h"
#include "test_usb_camera_2_jpg.h"
#include "test_usb_camera_2_rgb888.h"
#define TESTW 46
#define TESTH 46
void esp_jpeg_print_ascii(unsigned char *rgb888, esp_jpeg_image_output_t *outimg)
{
char aapix[] = " .:;+=xX$$";
unsigned char *p = rgb888 + 2;
for (int y = 0; y < outimg->width; y++) {
for (int x = 0; x < outimg->height; x++) {
int v = ((*p) * (sizeof(aapix) - 2) * 2) / 256;
printf("%c%c", aapix[v / 2], aapix[(v + 1) / 2]);
p += 3;
}
printf("%c%c", ' ', '\n');
}
}
TEST_CASE("Test JPEG decompression library", "[esp_jpeg]")
{
unsigned char *decoded, *p;
const unsigned char *o;
int decoded_outsize = TESTW * TESTH * 3;
decoded = malloc(decoded_outsize);
for (int x = 0; x < decoded_outsize; x += 2) {
decoded[x] = 0;
decoded[x + 1] = 0xff;
}
/* JPEG decode */
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = (uint8_t *)logo_jpg,
.indata_size = logo_jpg_len,
.outbuf = decoded,
.outbuf_size = decoded_outsize,
.out_format = JPEG_IMAGE_FORMAT_RGB888,
.out_scale = JPEG_IMAGE_SCALE_0,
.flags = {
.swap_color_bytes = 0,
}
};
esp_jpeg_image_output_t outimg;
esp_err_t err = esp_jpeg_decode(&jpeg_cfg, &outimg);
TEST_ASSERT_EQUAL(err, ESP_OK);
/* Decoded image size */
TEST_ASSERT_EQUAL(outimg.width, TESTW);
TEST_ASSERT_EQUAL(outimg.height, TESTH);
p = decoded;
o = logo_rgb888;
for (int x = 0; x < outimg.width * outimg.height; x++) {
/* The color can be +- 2 */
TEST_ASSERT_UINT8_WITHIN(2, o[0], p[0]);
TEST_ASSERT_UINT8_WITHIN(2, o[1], p[1]);
TEST_ASSERT_UINT8_WITHIN(2, o[2], p[2]);
p += 3;
o += 3;
}
esp_jpeg_print_ascii(decoded, &outimg);
free(decoded);
}
/**
* @brief JPEG unknown size test
*
* This test case verifies the functionality of the JPEG decompression library
* when decoding an image with unknown size. The image is decoded from a
* JPEG file, and the output size is determined dynamically. The test checks
* that the decoded image dimensions match the expected values and that the
* pixel data is within an acceptable tolerance range.
*/
TEST_CASE("Test JPEG unknown size", "[esp_jpeg]")
{
unsigned char *decoded, *p;
const unsigned char *o;
/* JPEG decode */
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = (uint8_t *)logo_jpg,
.indata_size = logo_jpg_len,
.out_format = JPEG_IMAGE_FORMAT_RGB888,
};
// 1. Get required output size
esp_jpeg_image_output_t outimg;
esp_err_t err = esp_jpeg_get_image_info(&jpeg_cfg, &outimg);
TEST_ASSERT_EQUAL(err, ESP_OK);
TEST_ASSERT_EQUAL(TESTW * TESTH * 3, outimg.output_len);
TEST_ASSERT_EQUAL(outimg.width, TESTW);
TEST_ASSERT_EQUAL(outimg.height, TESTH);
// 2. Allocate output buffer and assign it to the config
decoded = malloc(outimg.output_len);
TEST_ASSERT_NOT_NULL(decoded);
jpeg_cfg.outbuf = decoded;
jpeg_cfg.outbuf_size = outimg.output_len;
// 3. Decode the image
err = esp_jpeg_decode(&jpeg_cfg, &outimg);
TEST_ASSERT_EQUAL(err, ESP_OK);
/* Decoded image size */
TEST_ASSERT_EQUAL(TESTW * TESTH * 3, outimg.output_len);
TEST_ASSERT_EQUAL(outimg.width, TESTW);
TEST_ASSERT_EQUAL(outimg.height, TESTH);
p = decoded;
o = logo_rgb888;
for (int x = 0; x < outimg.width * outimg.height; x++) {
/* The color can be +- 2 */
TEST_ASSERT_UINT8_WITHIN(2, o[0], p[0]);
TEST_ASSERT_UINT8_WITHIN(2, o[1], p[1]);
TEST_ASSERT_UINT8_WITHIN(2, o[2], p[2]);
p += 3;
o += 3;
}
free(decoded);
}
#define WORKING_BUFFER_SIZE 4096
TEST_CASE("Test JPEG decompression library: User defined working buffer", "[esp_jpeg]")
{
unsigned char *decoded, *p;
const unsigned char *o;
int decoded_outsize = TESTW * TESTH * 3;
decoded = malloc(decoded_outsize);
uint8_t *working_buf = malloc(WORKING_BUFFER_SIZE);
assert(decoded);
assert(working_buf);
for (int x = 0; x < decoded_outsize; x += 2) {
decoded[x] = 0;
decoded[x + 1] = 0xff;
}
/* JPEG decode */
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = (uint8_t *)logo_jpg,
.indata_size = logo_jpg_len,
.outbuf = decoded,
.outbuf_size = decoded_outsize,
.out_format = JPEG_IMAGE_FORMAT_RGB888,
.out_scale = JPEG_IMAGE_SCALE_0,
.flags = {
.swap_color_bytes = 0,
},
.advanced = {
.working_buffer = working_buf,
.working_buffer_size = WORKING_BUFFER_SIZE,
},
};
esp_jpeg_image_output_t outimg;
esp_err_t err = esp_jpeg_decode(&jpeg_cfg, &outimg);
TEST_ASSERT_EQUAL(err, ESP_OK);
/* Decoded image size */
TEST_ASSERT_EQUAL(outimg.width, TESTW);
TEST_ASSERT_EQUAL(outimg.height, TESTH);
p = decoded;
o = logo_rgb888;
for (int x = 0; x < outimg.width * outimg.height; x++) {
/* The color can be +- 2 */
TEST_ASSERT_UINT8_WITHIN(2, o[0], p[0]);
TEST_ASSERT_UINT8_WITHIN(2, o[1], p[1]);
TEST_ASSERT_UINT8_WITHIN(2, o[2], p[2]);
p += 3;
o += 3;
}
free(working_buf);
free(decoded);
}
#if CONFIG_JD_DEFAULT_HUFFMAN
#include "test_usb_camera_jpg.h"
#include "test_usb_camera_rgb888.h"
/**
* @brief Test for JPEG decompression without Huffman tables
*
* This test case verifies the functionality of the JPEG decompression library
* when decoding an image that lacks Huffman tables, such as a USB frame
* from a Logitech C270 USB camera. The image was reconstructed from raw USB data
* (using `hex_to_jpg.py`) and then converted into an RGB888 C-style array
* (using `jpg_to_rgb888_hex.py`).
*
* Due to the unique structure of the JPEG data (double block size, 16x8 pixels)
* and absence of Huffman tables, this test assesses whether the decompression
* library correctly decodes the image and outputs RGB888 pixel data within
* an acceptable tolerance range.
*
* The test performs the following steps:
* - Allocates a buffer for the decoded image.
* - Configures and runs the JPEG decoder with the RGB888 output format.
* - Checks that the decoded image dimensions match expected values.
* - Compares the decompressed image data against the reference RGB888 data,
* allowing a tolerance of ±16 in each color component due to potential
* differences in Huffman tables or decompression accuracy.
*
* @note This test allows a margin of error in pixel values due to potential
* differences in how color data is interpreted across different decoders.
*
* @param None
*
* @return None
*
* @test Requirements:
* - JPEG decompression library support for images without Huffman tables.
* - JPEG decompression accuracy within acceptable error margins.
*/
TEST_CASE("Test JPEG decompression library: No Huffman tables", "[esp_jpeg]")
{
unsigned char *decoded, *p;
const unsigned int *o;
int decoded_outsize = 160 * 120 * 3;
decoded = malloc(decoded_outsize);
/* JPEG decode */
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = (uint8_t *)jpeg_no_huffman,
.indata_size = jpeg_no_huffman_len,
.outbuf = decoded,
.outbuf_size = decoded_outsize,
.out_format = JPEG_IMAGE_FORMAT_RGB888,
.out_scale = JPEG_IMAGE_SCALE_0,
.flags = {
.swap_color_bytes = 0,
}
};
esp_jpeg_image_output_t outimg;
esp_err_t err = esp_jpeg_decode(&jpeg_cfg, &outimg);
TEST_ASSERT_EQUAL(err, ESP_OK);
/* Decoded image size */
TEST_ASSERT_EQUAL(outimg.width, 160);
TEST_ASSERT_EQUAL(outimg.height, 120);
p = decoded;
o = jpeg_no_huffman_rgb888;
for (int x = 0; x < outimg.width * outimg.height; x++) {
/* The color can be +- 16 */
// Here we allow bigger decoding error
// It might be that the Windows decoder used slightly different Huffman tables
TEST_ASSERT_UINT8_WITHIN(16, (*o) & 0xff, p[0]);
TEST_ASSERT_UINT8_WITHIN(16, (*o >> 8) & 0xff, p[1]);
TEST_ASSERT_UINT8_WITHIN(16, (*o >> 16) & 0xff, p[2]);
p += 3; // this is uint8_t
o ++; // this is unt32_t
}
free(decoded);
}
#endif
/**
* @brief Invalid JPEG marker test
*
* This test case verifies the behavior of the JPEG decompression library
* when encountering an invalid marker (0xFFFF) in the JPEG data stream.
* The test uses a known JPEG image (camera_2_jpg) that contains this invalid
* marker. The test checks whether the library can handle the invalid marker
* gracefully and still decode the image correctly.
*/
TEST_CASE("Test JPEG invalid marker 0xFFFF", "[esp_jpeg]")
{
unsigned char *decoded;
int decoded_outsize = 160 * 120 * 3;
decoded = malloc(decoded_outsize);
assert(decoded);
for (int x = 0; x < decoded_outsize; x += 2) {
decoded[x] = 0;
decoded[x + 1] = 0xff;
}
/* JPEG decode */
esp_jpeg_image_cfg_t jpeg_cfg = {
.indata = (uint8_t *)camera_2_jpg,
.indata_size = camera_2_jpg_len,
.outbuf = decoded,
.outbuf_size = decoded_outsize,
.out_format = JPEG_IMAGE_FORMAT_RGB888,
.out_scale = JPEG_IMAGE_SCALE_0,
.flags = {
.swap_color_bytes = 0,
}
};
esp_jpeg_image_output_t outimg;
esp_err_t err = esp_jpeg_decode(&jpeg_cfg, &outimg);
TEST_ASSERT_EQUAL(ESP_OK, err);
/* Decoded image size */
TEST_ASSERT_EQUAL(160, outimg.width);
TEST_ASSERT_EQUAL(120, outimg.height);
free(decoded);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,6 @@
import pytest
@pytest.mark.generic
def test_esp_jpeg(dut) -> None:
dut.run_all_single_board_cases()

View File

@ -0,0 +1,6 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.4.0 Project Minimal Configuration
#
CONFIG_ESP_TASK_WDT_INIT=n
CONFIG_JD_USE_ROM=n
CONFIG_JD_DEFAULT_HUFFMAN=y

View File

@ -0,0 +1,4 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.4.0 Project Minimal Configuration
#
CONFIG_ESP_TASK_WDT_INIT=n

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,102 @@
/*----------------------------------------------------------------------------/
/ TJpgDec - Tiny JPEG Decompressor R0.03 include file (C)ChaN, 2021
/----------------------------------------------------------------------------*/
#ifndef DEF_TJPGDEC
#define DEF_TJPGDEC
#ifdef __cplusplus
extern "C" {
#endif
#include "tjpgdcnf.h"
#include <string.h>
#if defined(_WIN32) /* VC++ or some compiler without stdint.h */
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef short int16_t;
typedef unsigned long uint32_t;
typedef long int32_t;
#else /* Embedded platform */
#include <stdint.h>
#endif
#if JD_FASTDECODE >= 1
typedef int16_t jd_yuv_t;
#else
typedef uint8_t jd_yuv_t;
#endif
/* Error code */
typedef enum {
JDR_OK = 0, /* 0: Succeeded */
JDR_INTR, /* 1: Interrupted by output function */
JDR_INP, /* 2: Device error or wrong termination of input stream */
JDR_MEM1, /* 3: Insufficient memory pool for the image */
JDR_MEM2, /* 4: Insufficient stream input buffer */
JDR_PAR, /* 5: Parameter error */
JDR_FMT1, /* 6: Data format error (may be broken data) */
JDR_FMT2, /* 7: Right format but not supported */
JDR_FMT3 /* 8: Not supported JPEG standard */
} JRESULT;
/* Rectangular region in the output image */
typedef struct {
uint16_t left; /* Left end */
uint16_t right; /* Right end */
uint16_t top; /* Top end */
uint16_t bottom; /* Bottom end */
} JRECT;
/* Decompressor object structure */
typedef struct JDEC JDEC;
struct JDEC {
size_t dctr; /* Number of bytes available in the input buffer */
uint8_t *dptr; /* Current data read ptr */
uint8_t *inbuf; /* Bit stream input buffer */
uint8_t dbit; /* Number of bits availavble in wreg or reading bit mask */
uint8_t scale; /* Output scaling ratio */
uint8_t msx, msy; /* MCU size in unit of block (width, height) */
uint8_t qtid[3]; /* Quantization table ID of each component, Y, Cb, Cr */
uint8_t ncomp; /* Number of color components 1:grayscale, 3:color */
int16_t dcv[3]; /* Previous DC element of each component */
uint16_t nrst; /* Restart inverval */
uint16_t width, height; /* Size of the input image (pixel) */
uint8_t *huffbits[2][2]; /* Huffman bit distribution tables [id][dcac] */
uint16_t *huffcode[2][2]; /* Huffman code word tables [id][dcac] */
uint8_t *huffdata[2][2]; /* Huffman decoded data tables [id][dcac] */
int32_t *qttbl[4]; /* Dequantizer tables [id] */
#if JD_FASTDECODE >= 1
uint32_t wreg; /* Working shift register */
uint8_t marker; /* Detected marker (0:None) */
#if JD_FASTDECODE == 2
uint8_t longofs[2][2]; /* Table offset of long code [id][dcac] */
uint16_t *hufflut_ac[2]; /* Fast huffman decode tables for AC short code [id] */
uint8_t *hufflut_dc[2]; /* Fast huffman decode tables for DC short code [id] */
#endif
#endif
void *workbuf; /* Working buffer for IDCT and RGB output */
jd_yuv_t *mcubuf; /* Working buffer for the MCU */
void *pool; /* Pointer to available memory pool */
size_t sz_pool; /* Size of momory pool (bytes available) */
size_t (*infunc)(JDEC *, uint8_t *, size_t); /* Pointer to jpeg stream input function */
void *device; /* Pointer to I/O device identifiler for the session */
};
/* TJpgDec API functions */
JRESULT jd_prepare (JDEC *jd, size_t (*infunc)(JDEC *, uint8_t *, size_t), void *pool, size_t sz_pool, void *dev);
JRESULT jd_decomp (JDEC *jd, int (*outfunc)(JDEC *, void *, JRECT *), uint8_t scale);
#ifdef __cplusplus
}
#endif
#endif /* _TJPGDEC */

View File

@ -0,0 +1,48 @@
/*----------------------------------------------*/
/* TJpgDec System Configurations R0.03 */
/*----------------------------------------------*/
#include "sdkconfig.h"
#define JD_SZBUF CONFIG_JD_SZBUF
/* Specifies size of stream input buffer */
#define JD_FORMAT CONFIG_JD_FORMAT
/* Specifies output pixel format.
/ 0: RGB888 (24-bit/pix)
/ 1: RGB565 (16-bit/pix)
/ 2: Grayscale (8-bit/pix)
*/
#if defined(CONFIG_JD_USE_SCALE)
#define JD_USE_SCALE CONFIG_JD_USE_SCALE
#else
#define JD_USE_SCALE 0
#endif
/* Switches output descaling feature.
/ 0: Disable
/ 1: Enable
*/
#if defined(CONFIG_JD_TBLCLIP)
#define JD_TBLCLIP CONFIG_JD_TBLCLIP
#else
#define JD_TBLCLIP 0
#endif
/* Use table conversion for saturation arithmetic. A bit faster, but increases 1 KB of code size.
/ 0: Disable
/ 1: Enable
*/
#define JD_FASTDECODE CONFIG_JD_FASTDECODE
/* Optimization level
/ 0: Basic optimization. Suitable for 8/16-bit MCUs.
/ 1: + 32-bit barrel shifter. Suitable for 32-bit MCUs.
/ 2: + Table conversion for huffman decoding (wants 6 << HUFF_BIT bytes of RAM)
*/
#if defined(CONFIG_JD_DEFAULT_HUFFMAN)
#define JD_DEFAULT_HUFFMAN CONFIG_JD_DEFAULT_HUFFMAN
#else
#define JD_DEFAULT_HUFFMAN 0
#endif

View File

@ -95,6 +95,7 @@ static void lvgl_port_touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *
data->point.x = touchpad_x[0];
data->point.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;
}

View File

@ -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,

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

View File

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