通信验收、闭环测试与故障定位

[English]

验收证据的四个层级

图 24:下一级成功不能替代上一级的应用证据。 打开串口、接管成功、上传 ACK 与摄像头真的拍到一张图,分别是四种结论。

只证明控制面能响应,不足以报告文件、脚本、preview 或完整硬件矩阵通过。真实验收记录应包括板卡/物理身份、安装固件提交、所用端点、当前 session/epoch、请求阶段、时间预算和应用级证据。

建议的验收矩阵

按功能与边界验证

类别

操作

成功依据

framing

分片 metadata/payload,连续粘包,CRC 错误,payload 含 SOF/EOF,UTF-8 跨帧。

仅完整合法帧交付,错误有界重同步,正文没有被分隔字节截断。

会话

两连接争用、同 nonce acquire 重试、release 重接管、租约到期、TCP 槽位复用。

唯一 owner,正确错误码,旧 link/session 输入输出不进入新连接。

VM

当前脚本占用、Ctrl-C、软复位、新 epoch ready、旧输出延迟到达。

真实 REPL 新标记,旧 epoch 不能完成新请求。

文件

空文件、中文/二进制、分块、64 KiB、取消、发布结果未知。

bytes/文件大小/内容 hash 校验,原文件及保护文件不被破坏。

preview

持续 JPEG、慢读者、串口切换/重连、与 RPC 交错。

完整图像、正确尺寸和当前范围;允许 preview 丢帧但不接受旧 owner 数据。

网络

配置 accepted 与最终 IP、关闭/恢复、FIN/RST、静默客户端。

最终事务状态、owner/slot 分别回收,新连接可恢复;未知业务不重放。

下载/复位

Python 请求、USB control 请求、ROM 枚举、写入/读回、运行态重接管。

分别证明模式切换、镜像校验与新运行态身份,而非仅观察端口消失。

使用真实安装固件的版本记录,不把“从源码推断”“CI 编译通过”“在另一块板运行过”写成当前板卡已经通过真机验收。浏览器字节桥测试也应说明是否覆盖原生串口 chooser/DTR。

统一 Core 真机检查

设备协议与业务真机测试统一运行 host/device-core 中的 npm run test:hardware,直接调用 createDevice() 的公开 API。摄像头、文件和 LCD 均走同一个 Core,不再维护独立的主机协议客户端或 Agent smoke 入口。串口适配器只传字节,TCP 使用 Node socket;无需安装 Agent。

使用 Node.js 22。串口模式需要 Python 与 pyserial;--python 可指定解释器。所有输出放在项目目录中;以下从仓库根目录执行:

npm --prefix host/device-core test
npm --prefix host/device-core run check:schema
npm --prefix host/device-core run check:package
npm --prefix host/device-core run test:hardware -- --serial <USJ-port> --role usj --checks all --timeout 600000
npm --prefix host/device-core run test:hardware -- --serial <USB-OTG-port> --role usb --checks identity,camera,display
npm --prefix host/device-core run test:hardware -- --tcp <device-ipv4>:3232 --checks identity,memory,camera

--checks 接受 all 或逗号分隔的检查名。摄像头/显示检查包括 identity、memory、camera、framesizes、pixformats、orientation、ipa_stats、snapshot_rate、preview、image_ops、display、display_stream、camera_display_lifecycle、image_copy_to_fb、image_transform_argument、sensor_snapshot_buffer、memory_policy、espdl_inference、isp_ccm_frames 和 isp_lsc_display。all 选择完整检查集,不表示所有板卡都具备对应硬件;报告应明确区分通过、失败与缺少能力的跳过,不能将跳过算作已通过。初次实验只测试当前硬件,最终提交才由 CI 运行完整固件矩阵。

ISP 图像与内存检查

调查 ISP 时使用静止且有足够照明的目标,并指定准确板型和实际安装的固件版本:

npm --prefix host/device-core run test:hardware -- --serial <USB-OTG-port> --role usb --checks isp_ccm_frames,isp_lsc_display --expect-board ESP32_P4X_EYE --expect-firmware-version <installed-version> --frames 400 --require-all

isp_ccm_frames 对受控的红色/灰度 CCM 矩阵各单写一次,记录连续八张实际图像的帧序号、耗时、RGB 总和及平均饱和度。其中一次灰度采集故意在写入后休眠两秒:第一张仍可能来自旧缓冲,但持续消费后应在有限序列内看到新效果。检查还验证 IPA 占用时拒绝手动写入,以及重写单位 CCM 后显式 ISP 白平衡增益的读回值和实际 RGB 比例保持一致。比例对照要求目标静止,不测量或冻结传感器内部自动白平衡。亮度足够的采样点少于十六个时,检查会以效果不可观测失败,不将暗图当作 CCM 生效证据。

isp_lsc_display 使用 QVGA RGB565、驱动报告的 LSC 表长度和原报告从 1.0 到 0.5 的二次增益曲线,将摄像头图像居中绘制到全屏 canvas 后调用 lcd.write()。预热两个 LSC 表存储区和显示暂存区后至少执行八十帧(--frames 可增加帧数),每帧双写 CCM,每二十帧切换 LSC。报告包含 DMA/内部/PSRAM 的空闲量与最大连续块、非法表被拒绝后恢复显示、资源释放及新一轮摄像头/LCD 初始化。内存历史最多保留十个采样点,避免观测记录持续增长。小幅内存容差允许无关分配;长时间测试应结合完整序列检查趋势。这属于软件图像与资源证据;判断物理面板是否仍花屏,需要在 LSC 切换时观察真实 LCD,检查通过或软件截图均不能单独证明。当前固件通过也不等于已经查明旧固件报告的根因。

CI target test 契约

CI 硬件门禁使用 ESP-IDF v5.5.6,在 P4 Function EV Board 上执行显式选定的 18 项运行时检查,包括 espdl_inference。两项 ISP 检查保留为照亮的静止目标上的单独人工验收;新增注册检查不会静默扩大无人值守 CI 套件。CI 同时保存摄像头 JPEG 与 LCD 测试图案 JPEG,并将截图尺寸与活动显示对象核对。所有选中项必须通过(--require-all),不支持能力的 SKIP 也会使该板卡门禁失败。摄像头模式循环仅容忍明确的不支持错误;内存分配失败、超时及非预期的模式切换错误都会使检查失败。这只验证所选开发板与 IDF 组合,不代表编译矩阵中的其它板卡。驱动会将 --expect-board 和下载构建的 project_description.json 中的精确 project_version 同时传给启动探测与完整套件,在执行 Python、摄像头和显示检查前拒绝错误板型或旧固件。

运行时辅助脚本通过有界的 python.execute() 调用装载到独立 RAM 命名空间。每条编码后命令都满足固件声明的 replInputMax (最多 511 字节);更长的请求会触发 Device Core 的临时文件执行路径。成功、失败或取消后都会释放辅助命名空间和 LCD 资源,因此内存、摄像头和显示检查不依赖可写文件系统。

espdl_inference 通过有界的公共 python.execute() 调用,将仓库自带的 495,840 字节 ESPDet Pico Face 模型装入只读 RAM VFS,在主机和设备两端验证 SHA-256,再通过真实 espdl.ESPDet 对 RGB565 摄像头图像推理。检查同一复制帧的结果可重复性、检测元组结构、检测框与图像重叠、置信度/类别范围及正的推理耗时;报告包含模型 SHA-256、传输分块数/耗时、加载耗时、帧数/检测数及平均/最慢推理延迟。镜头内没有人脸时允许空检测结果;这项合理性检查不代表模型准确率或性能指标已达标。无论成功、失败或取消,都会释放模型、RAM 挂载及摄像头,不读写 Flash/SD 文件,也不取得 MSC writer 租约。

模型上传通过命名空间 nonce 和准确的已提交字节偏移确认完成。只有超时分块允许重试,每块最多调用三次公共执行接口;重放只接受最近一个字节完全相同的分块。数据冲突或乱序、确认缺失或错误以及取消均立即失败。报告包含 modelRetries 和 modelRetryOffsets。挂载、模型清理及最终命名空间不存在均要求显式确认或读回,不能将打印 Python 异常后执行包装器返回成功当作操作成功。各命令与整体的现有超时保持不变。

默认模型 SHA-256 为 04eec2220adbf642a843c953817fb219a06c094aedb76c181ba2a958c65201dc。--model MODEL.espdl 可以选择兼容的 ESPDet 模型(至多 2 MiB),并通过 --model-sha256 HEX 固定预期字节。已安装的 npm 包不携带模型资源,需要显式提供 --model。模型缺失只会跳过可选验收,在 --require-all 下仍失败。严格推理验收要求摄像头和 ESP-DL 能力,与持久化存储是否可用无关。

存储单独取证。身份检查会报告各卷的存在、挂载及写入状态;运行时通过不代表文件上传可用。在已准备好文件系统的板卡上使用 --write,显式要求 Flash 已挂载且可写,执行唯一临时文件的二进制写入与读回,并仅清理测试自己的文件。未挂载或不可用的存储会使该项以 STORAGE_UNAVAILABLE 失败。CI 和硬件 CLI 都不会格式化存储、通过擦除数据掩盖挂载失败,或用 SD 卡代替 Flash。

摄像头闭环要求当前会话中新的完整 stdout marker 或经校验的 JPEG。Core 管理上传、执行和关联,测试继续检查像素格式、尺寸、方向、IPA 状态、快照速率、preview、图像处理及显示。握手、上传 ACK、执行排队或 REPL 提示符本身都不能替代业务完成证据。长期运行时持续接收数据并处理 VM_BUSY / VM_TIMEOUT。

LCD 抓屏与图案核验

--screen-output 显示已知测试图案,通过 device.capture({ source: 'screen', format: 'jpeg' }) 读取 LCD 最后一帧 RGB565 的 JPEG,再保存到指定路径。预先创建项目内输出目录:

mkdir -p /home/libo/project/.esp-vision/hardware-validation
npm --prefix host/device-core run test:hardware -- --serial <USJ-port> --role usj --checks display --screen-output /home/libo/project/.esp-vision/hardware-validation/lcd.jpg

抓屏调用固件 debug.capture_screen,在 VM 任务中读取显示缓冲区,不调用摄像头。应核验 JPEG 可解码、尺寸与图案符合预期,并对照设备真实屏幕观察。LCD 未初始化返回 DISPLAY_NOT_READY;无 LCD 或该固件不支持时返回 NOT_SUPPORTED,不得改用摄像头掩盖失败。抓屏反映软件提交的最后显示帧,不证明物理面板、电缆或背光正常。

这些检查会停止当前 Python 程序,并可能改变显示内容。安排独占设备时段,不与 IDE/其他测试同时持有设备。设备有有效 owner 时应释放或等待租约,不通过换接口抢占。测试使用独立临时文件并清理自己的文件;保存报告、JPEG 和日志时记录实际固件、板卡与接口。复位、掉线和取消后保留未知业务状态,不自动重放写入。

测试入口与职责

一套协议实现,分层验证

入口

覆盖范围

host/device-core: npm test

Core 公开 API、协议、会话、发现及烧写契约的确定性主机测试。

host/device-core: npm run test:hardware

串口/TCP 实际连接、摄像头及显示闭环,同一个公开 Core。

tools/ci/flash_and_test.py

CI 板卡/IDF 产物选择与烧写,随后调度 Core 真机检查;不另实现 EV-MUX。

tools/tests/

固件主机原生状态机、JSON、队列、存储及复位契约。

故障定位顺序

现象与下一步

现象

先检查

串口有字但 hello 超时

是否为原始启动/panic 文本;选错 USB 接口;startup 半帧;EV-MUX 是否被运行期关闭;帧 CRC/字节长度与 reader 是否持续运行。

USB 已配置却不能接管

capabilities 与有效 owner;不要拿 DTR=false 当成 v4 不可用条件。

SESSION_BUSY

哪个 IDE/Agent/终端仍保活;先显式释放或等待租约,不修改 route 来抢占。

vmReady=true 但文件/抓帧超时

boot/main.py 是否仍运行;VM 队列是否忙;中断后是否打印了真实的新 REPL marker。

收到 seq 相同的数据却 RPC 未完成

type/channel/method/session/epoch 是否符合请求;是否拿 event 代替 rsp。

preview 偶尔跳帧

JPEG 是否完整、是否属于当前范围;慢读者和 latest-frame 策略是否符合预期。

TCP 断线后两槽耗尽

handshake/lease/30 秒 idle 各自状态,旧 generation 是否被回收,而不是仅看 owner 已释放。

下载请求后端口消失

ROM 是否重新枚举,USJ 是否有 ROM 日志,下载位是否按掩码设置/清除,原句柄是否已失效。

全局 drop 增长但 TCP 标记通过

未读默认/回退口、队列类别和采样范围;不要把不同端点的计数等同于这个业务失败。

只保存必要的状态与标量计数。SSID/密码、完整文件内容和 Flash/NVS 备份可能含敏感信息,不应混入公开诊断报告;占位示例不代表设备实际配置。

文档与实现的同步边界

若新增 channel、后端、控制 action 或修改 stderr/所有权/限额,应同时更新本章相关页、capabilities、参考客户端与契约测试。仅修改图中的名称不构成协议变更;新方法还须有实际固件分发实现。

固件单元测试与 Core 验收的边界

tools/tests/test_ev_*.py 在主机上借助 ESP-IDF/FreeRTOS 替身编译运行生产 C 逻辑,不连接设备,也不实现另一套主机协议客户端。保留部分写入、队列背压、分配失败、旧连接 generation、会话过期和存储交接等确定性覆盖。配套 C 测试和 stub 头文件属于测试基础设施,不是另一套设备操作工具。

用户可见功能和真机验收统一走 Device Core;新增连接、Python、文件、preview 和抓图场景使用公开 API。固件故障注入仍与固件同处维护,因为普通 SDK 请求无法稳定强制任务调度、内存分配或驱动失败。源码字符串断言仅作为补充,不能替代行为执行和真实硬件验收。