ESP32-S3 网页截图 LVGL 界面,作为 AI 视觉闭环的实机入口

界面跑在立创实战派 ESP32-S3 上,由 LVGL 9.5 渲染。要进文档只能对着屏幕拍照;模拟器又覆盖不了实机行为,AI 要验证界面必须碰真机。我给设备加了一个网页截图接口:浏览器点一下,LVGL 当前画面变成 BMP 图片回传。这个接口同时服务文档整理和 AI 视觉闭环。

实机截图效果

实机界面是 AI 视觉闭环的入口

LVGL 有 PC 模拟器,能跑布局和事件逻辑,但触摸采样、手势轮询、局部刷新时序都依赖真实硬件。模拟器跑不全的界面状态,只能在实机上出现,AI 也就只能在实机上确认。截图接口把实机画面变成机器可读的图片,AI 用视觉模型识别界面状态,再决定下一步构建或修复,形成闭环。文档截图只是这个接口的副产物。

总体链路

网页截图链路

请求从浏览器出发:点「截图」→ GET /screenshot → HTTP 任务把参数写进截图请求结构 → 显示任务用 LVGL 快照渲染当前屏到 PSRAM 缓冲 → BMP 编码逐行流式回传 → 浏览器 blob 预览和下载。

链路的硬约束是 LVGL 线程模型:渲染只允许在显示任务内发生,HTTP 任务不能碰渲染 API。两端用「请求结构 + 二进制信号量」握手。

显示任务内的 LVGL 快照渲染

HTTP 服务器任务收到请求后,把宽高、目标缓冲、完成信号量写进全局请求槽。显示任务每帧轮询这个槽,消费后渲染,完成后发信号量,调用方阻塞等待。

渲染调用 lv_snapshot_take_to_draw_buf,把一个 lv_draw_buf_t 指向调用方提供的 PSRAM 缓冲,目标是当前激活屏。前置条件是 CONFIG_LV_USE_SNAPSHOT=y,这个开关默认关闭。缓冲按 320×320×3 预留,RGB888 每像素 3 字节。

RGB888 捕获消除 RGB565 偏色

第一版用 RGB565 捕获,网页里看界面背景偏绿。逐像素算过:设计色 (237,237,237) 存进 RGB565,G 通道 6 位向上取整到 59,展开回 8 位变成 253;R、B 各 5 位向下取整到 29,展开 239。结果 (239,253,239),绿色高了一截。

屏幕本身就是 RGB565,面板上实际显示的也是这个偏色,截图忠实还原了面板。文档要的是设计原色,所以捕获格式改成 RGB888,渲染器直接输出 24 位,背景精确回到 (237,237,237)。

字节序顺带省事:LVGL 的 RGB888 内存序是 B,G,R(LV_COLOR_MAKE 宏展开为 {b8,g8,r8}),正好是 BMP 的字节序。逐行 memcpy 直接发送,省掉逐像素转换。

BMP 流式回传

编码结果流式发送,不整张缓存。先发 54 字节 BMP 头,再按行从底到顶 httpd_resp_send_chunk。320×240 的 24 位 BMP 约 230KB,设备侧只需 307KB 的 RGB888 快照缓冲,输出侧零大缓冲。

网页端截图按钮

页面顶部工具栏加「📷 截图」按钮,fetch('/screenshot') 取回 blob,URL.createObjectURL 生成预览,弹窗内可下载 BMP。更新网页文件走文件管理器的 /api/fs 上传接口,不重烧 littlefs 镜像,避免覆盖设备上已保存的配置。

两个落地时的坑。一是 httpd 的 max_uri_handlers 默认 8,平台各端点注册完是 17 个,撞满当时 16 的上限,静态回退注册直接触发 ESP_ERROR_CHECK 重启,上限改成 24 解决。二是第一版按钮点了没反应,检查发现页面里 takeScreenshot 函数没有写进去,此后每次改网页都用 curl 校验函数存在。

适用边界

接口提供一次性截图,实时投屏需要定时轮询和更高压缩率的编码,本实现没有做。BMP 无压缩,单帧约 230KB,适合调试和文档。AI 闭环目前按「截图 → 视觉识别 → 决定下一步」的节奏工作,瓶颈在识别刷新率,截图链路本身开销很小。