Rdzleo c24a9bc162 feat: 集成 dzbj LVGL 显示模块 + 配网模式内存优化
阶段1: 将 dzbj 项目的 LVGL 8.3.11 LCD 显示集成到 AI小智 主项目,
开机显示 ScreenHome 界面,同时优化配网模式下的内存使用,
确保 WiFi+BLE+LVGL 三者共存运行。

## 新增功能

### dzbj 显示模块集成
- 新增 main/dzbj/ 目录,移植 LCD 驱动(ST77916 QSPI)、触摸驱动(CST816S)、
  LVGL 初始化和 SquareLine Studio UI 界面
- I2C 总线共享:dzbj 触摸控制器复用主项目的 I2C_NUM_1 总线
- GPIO 冲突解决:LED(GPIO21)、Touch1(GPIO1)、Touch4(GPIO7) 改为 NC,
  电池 ADC 从 GPIO6 改为 GPIO3
- 添加 LVGL、esp_lcd_st77916、esp_lcd_touch_cst816s 等组件依赖
- managed_components 纳入版本管理

### 配网模式轻量化启动
- BoxAudioCodec: 新增 output_only 模式,仅创建 I2S TX 通道(省 ~13KB DMA)
  跳过 ES7210 ADC 初始化(省 ~2-4KB)
- AudioCodec: 新增 StartOutputOnly() 方法,仅启用扬声器输出
- Application: 配网模式跳过 Opus 编码器、输入重采样器、协议初始化、
  天气位置检测等网络业务
- 板级构造函数: 配网模式跳过电池检测、IMU传感器、PowerSaveTimer

### WifiBoard 配网流程修复
- NeedsProvisioning() 静态方法: 读取 NVS force_ap 和 SSID 列表,
  用于提前判断配网模式
- force_ap 竞态修复: 构造函数不再清零 force_ap,改在 StartNetwork() 清零,
  确保 NeedsProvisioning() 能正确读到 force_ap=1
- Application 缓存 provisioning_mode_ 成员变量,避免重复读 NVS

### BLE 配网重启修复
- 配网成功后用 esp_timer 延迟重启替代 xTaskCreate,
  避免内存紧张时任务创建失败导致设备不重启
- 注释掉 WiFi 连接成功后的 MAC 地址发送步骤

### sdkconfig 内存优化
- BT_ALLOCATION_FROM_SPIRAM_FIRST=y (BLE 动态分配优先 PSRAM)
- SPIRAM_MALLOC_RESERVE_INTERNAL=32768
- NVS_ALLOCATE_CACHE_IN_SPIRAM=y
- WiFi 静态缓冲区数量优化 (RX=10, TX=8)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:07:51 +08:00

209 lines
6.8 KiB
Python

#!/usr/bin/env python3
import argparse
import errno
import glob
import shutil
import subprocess
import sys
import os
lvgl_test_dir = os.path.dirname(os.path.realpath(__file__))
# Key values must match variable names in CMakeLists.txt.
build_only_options = {
'OPTIONS_MINIMAL_MONOCHROME': 'Minimal config monochrome',
'OPTIONS_NORMAL_8BIT': 'Normal config, 8 bit color depth',
'OPTIONS_16BIT': 'Minimal config, 16 bit color depth',
'OPTIONS_16BIT_SWAP': 'Normal config, 16 bit color depth swapped',
'OPTIONS_FULL_32BIT': 'Full config, 32 bit color depth',
}
test_options = {
'OPTIONS_TEST_SYSHEAP': 'Test config, system heap, 32 bit color depth',
'OPTIONS_TEST_DEFHEAP': 'Test config, LVGL heap, 32 bit color depth',
}
def is_valid_option_name(option_name):
return option_name in build_only_options or option_name in test_options
def get_option_description(option_name):
if option_name in build_only_options:
return build_only_options[option_name]
return test_options[option_name]
def delete_dir_ignore_missing(dir_path):
'''Recursively delete a directory and ignore if missing.'''
try:
shutil.rmtree(dir_path)
except FileNotFoundError:
pass
def generate_test_runners():
'''Generate the test runner source code.'''
global lvgl_test_dir
os.chdir(lvgl_test_dir)
delete_dir_ignore_missing('src/test_runners')
os.mkdir('src/test_runners')
# TODO: Intermediate files should be in the build folders, not alongside
# the other repo source.
for f in glob.glob("./src/test_cases/test_*.c"):
r = f[:-2] + "_Runner.c"
r = r.replace("/test_cases/", "/test_runners/")
subprocess.check_call(['ruby', 'unity/generate_test_runner.rb',
f, r, 'config.yml'])
def options_abbrev(options_name):
'''Return an abbreviated version of the option name.'''
prefix = 'OPTIONS_'
assert options_name.startswith(prefix)
return options_name[len(prefix):].lower()
def get_base_buid_dir(options_name):
'''Given the build options name, return the build directory name.
Does not return the full path to the directory - just the base name.'''
return 'build_%s' % options_abbrev(options_name)
def get_build_dir(options_name):
'''Given the build options name, return the build directory name.
Returns absolute path to the build directory.'''
global lvgl_test_dir
return os.path.join(lvgl_test_dir, get_base_buid_dir(options_name))
def build_tests(options_name, build_type, clean):
'''Build all tests for the specified options name.'''
global lvgl_test_dir
print()
print()
label = 'Building: %s: %s' % (options_abbrev(
options_name), get_option_description(options_name))
print('=' * len(label))
print(label)
print('=' * len(label))
print(flush=True)
build_dir = get_build_dir(options_name)
if clean:
delete_dir_ignore_missing(build_dir)
os.chdir(lvgl_test_dir)
created_build_dir = False
if not os.path.isdir(build_dir):
os.mkdir(build_dir)
created_build_dir = True
os.chdir(build_dir)
if created_build_dir:
subprocess.check_call(['cmake', '-DCMAKE_BUILD_TYPE=%s' % build_type,
'-D%s=1' % options_name, '..'])
subprocess.check_call(['cmake', '--build', build_dir,
'--parallel', str(os.cpu_count())])
def run_tests(options_name):
'''Run the tests for the given options name.'''
print()
print()
label = 'Running tests for %s' % options_abbrev(options_name)
print('=' * len(label))
print(label)
print('=' * len(label), flush=True)
os.chdir(get_build_dir(options_name))
subprocess.check_call(
['ctest', '--timeout', '30', '--parallel', str(os.cpu_count()), '--output-on-failure'])
def generate_code_coverage_report():
'''Produce code coverage test reports for the test execution.'''
global lvgl_test_dir
print()
print()
label = 'Generating code coverage reports'
print('=' * len(label))
print(label)
print('=' * len(label))
print(flush=True)
os.chdir(lvgl_test_dir)
delete_dir_ignore_missing('report')
os.mkdir('report')
root_dir = os.pardir
html_report_file = 'report/index.html'
cmd = ['gcovr', '--root', root_dir, '--html-details', '--output',
html_report_file, '--xml', 'report/coverage.xml',
'-j', str(os.cpu_count()), '--print-summary',
'--html-title', 'LVGL Test Coverage']
for d in ('.*\\bexamples/.*', '\\bsrc/test_.*', '\\bsrc/lv_test.*', '\\bunity\\b'):
cmd.extend(['--exclude', d])
subprocess.check_call(cmd)
print("Done: See %s" % html_report_file, flush=True)
if __name__ == "__main__":
epilog = '''This program builds and optionally runs the LVGL test programs.
There are two types of LVGL tests: "build", and "test". The build-only
tests, as their name suggests, only verify that the program successfully
compiles and links (with various build options). There are also a set of
tests that execute to verify correct LVGL library behavior.
'''
parser = argparse.ArgumentParser(
description='Build and/or run LVGL tests.', epilog=epilog)
parser.add_argument('--build-options', nargs=1,
help='''the build option name to build or run. When
omitted all build configurations are used.
''')
parser.add_argument('--clean', action='store_true', default=False,
help='clean existing build artifacts before operation.')
parser.add_argument('--report', action='store_true',
help='generate code coverage report for tests.')
parser.add_argument('actions', nargs='*', choices=['build', 'test'],
help='build: compile build tests, test: compile/run executable tests.')
args = parser.parse_args()
if args.build_options:
options_to_build = args.build_options
else:
if 'build' in args.actions:
if 'test' in args.actions:
options_to_build = {**build_only_options, **test_options}
else:
options_to_build = build_only_options
else:
options_to_build = test_options
for opt in options_to_build:
if not is_valid_option_name(opt):
print('Invalid build option "%s"' % opt, file=sys.stderr)
sys.exit(errno.EINVAL)
generate_test_runners()
for options_name in options_to_build:
is_test = options_name in test_options
build_type = 'Debug'
build_tests(options_name, build_type, args.clean)
if is_test:
try:
run_tests(options_name)
except subprocess.CalledProcessError as e:
sys.exit(e.returncode)
if args.report:
generate_code_coverage_report()