Files
ESP32S3_vt/DEVELOPMENT.md
T
sloray cc6b82d7ec
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s
feat: 链路控制、延迟校准与长测工具,补全文档
- 固件:新增 link_control / stream_control,覆盖运行中 Wi-Fi 断线恢复、取帧节拍、
  温控与质量/帧率减载;UDP 发送增加有界重试预算与帧期限,PSRAM JPEG 直发减少拷贝。
- 协议:重组器严格校验分片布局,统计未收齐、超时与完全缺失的帧,处理帧号回绕与设备重启。
- 桌面端:视频改 latest-value 通道并前置帧时间标记,拒绝积压和慢解码的过期画面;新增
  延迟标记页与 --latency-report 校准,完善录制、服务端与退出流程。
- 文档与脚本:新增硬件、开发、延迟、功耗、实时性、稳定性文档和改动记录,重写根 README;
  新增构建、性能、延迟与串口长测脚本,以及长测客户端脚本。
- 构建:固定 esp-1.97.0.0 工具链,分区表随构建复制,更新 sdkconfig 与忽略规则。
2026-10-19 00:19:07 +08:00

8.9 KiB
Raw Blame History

开发与验证

编译入口见 README.md,硬件限制见 HARDWARE.md,历史变化见 CHANGELOG.md。

镜像导出

以下使用 espflash 4.4.0 与固定的 esp-1.97.0.0 工具链。完成构建后,使用 esp-idf-sys 复制到 release/ 的 Bootloader。Windows PowerShell:

$targetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { 'target' }
New-Item -ItemType Directory -Force target/firmware | Out-Null
espflash save-image --chip esp32s3 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$targetDir/xtensa-esp32s3-espidf/release/bootloader.bin" --merge --skip-padding "$targetDir/xtensa-esp32s3-espidf/release/esp32" target/firmware/esp32s3-merged.bin

Linux / macOS(Bash):

target_dir="${CARGO_TARGET_DIR:-target}"
mkdir -p target/firmware
espflash save-image --chip esp32s3 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$target_dir/xtensa-esp32s3-espidf/release/bootloader.bin" --merge --skip-padding "$target_dir/xtensa-esp32s3-espidf/release/esp32" target/firmware/esp32s3-merged.bin

合并镜像包含 Bootloader、分区表、App 和必要地址填充,--skip-padding 避免将文件填满 16 MB。镜像从 0x0 写入:

espflash write-bin --chip esp32s3 --port COM5 0x0 target/firmware/esp32s3-merged.bin

去掉导出命令的 --merge --skip-padding 可生成独立 App,它需写入 partitions.csv 声明的 Factory 偏移,并与板上 Bootloader 和分区表匹配。坏板重编可能改变该偏移;新板优先使用 ELF 或合并镜像。

主机测试

协议层不依赖 ESP-IDF。进入 protocol/,以 Windows MSVC 为例:

cargo +stable test --locked --target x86_64-pc-windows-msvc --config 'unstable.build-std=[]'

Linux 常见 host 为 x86_64-unknown-linux-gnu,Apple Silicon 为 aarch64-apple-darwin,可用 rustc +stable -vV 查看。--target 覆盖仓库里的其他平台配置,空 build-std 避免主机测试继承嵌入式标准库重编译。

装甲规则从 armor/ 执行 cargo +stable test -p armor-protocol,桌面核心从 desktop/core/ 执行 cargo +stable test,同样按需指定宿主 --target 和空 build-std。

限帧、温度回差和 JPEG 压缩回压策略可以独立在主机运行,无需连接板卡。Windows 在根目录执行:

New-Item -ItemType Directory -Force target | Out-Null
rustc +stable --edition 2021 --test src/stream_control.rs -o target/stream-control-tests.exe
.\target\stream-control-tests.exe

实际持续运行的串口与客户端通道记录方法见 STABILITY.md。主机策略测试不等于已经触发板上过温减载。

清理构建缓存

改动 C 组件或 sdkconfig.defaults 后,增量构建若没有反映变化,可从根目录执行:

cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys

随后用构建脚本重编,确认 CMake / Ninja 重新编译相应组件。仍有缓存问题时使用 cargo clean 完整清理。不要删除 components/esp32-camera/target/。

构建脚本设置 CARGO_WORKSPACE_DIR,并通过 ESP_IDF_GLOB_BASE / ESP_IDF_GLOB_PARTITIONS 将分区表复制到生成的 CMake 工程,因此支持外部 CARGO_TARGET_DIR 和不同 Cargo 输出层级。Windows 使用短目录,如 C:\s3vt;设置 ESP_IDF_PATH_ISSUES=ignore 或 subst 不能消除底层路径限制。

发现与协议

  • 图传板每 2 秒广播 Hello;客户端每 800 ms 广播 Subscribe,并向已发现的在线板卡 IP 单播续订,固定绑定 UDP 5600。
  • 全局广播可能被 VPN / 虚拟网卡路由到其它接口。收到 Hello 后按设备 IP 定向续订,可以使用通往实际局域网的路由;队伍端仅向绑定的板卡增加单播续订。
  • 固件使用订阅包的源地址作为发送目标,6 秒不续订即移除,最多保留 4 个对端。
  • 新客户端为每个接收器生成随机会话 ID;同一源 IP 下,同一会话换端口会更新推流目标。旧版公共固定 ID 0x0C110001 保持按地址区分,避免合并同机旧客户端。
  • MAC 派生的 sender_id 不随 DHCP 地址变化,适合客户端持久绑定。
  • Hello 和视频都是设备在线证据;客户端列表清理与固件订阅使用不同超时。
  • 数据报为 27 字节头 + 最多 1400 字节载荷,采用小端编码;字段及拒收条件见 protocol/README.md。

缺失分片不重传,不完整帧整体丢弃。JPEG 由传感器编码,桌面后端转交原始 JPEG,由 WebView 解码,避免接收线程进行耗时编解码。

性能测量

本次在保留 TUN 环境中的对照测试、最终参数与验证边界见 PERFORMANCE.md。

下表为原 README 保存的历史单次测试数据,不是本次构建实测或性能保证。配置为 OV5640、VGA;原始日志、时长和完整网络条件未随这些数字保存。

指标 早期 HTTP MJPEG / TCP 后续 UDP
传感器产出 约 22 fps 约 22 fps
客户端收到 约 1.9 fps 约 22 fps
吞吐 约 0.28 Mbit/s 约 1.6 Mbit/s
丢帧 未记录 当次报告 0%

这些数据不足以单独证明性能差异的根因或无线链路容量。复测时记录板型、固件版本、分辨率、画质、信号、订阅者数量、时长和丢帧数。

FPS 应按同一时间窗口的帧数增量 / 窗口秒数计算。累计帧数与窗口时长混用会产生假数值,UI 轮询间隔也不代表相机出帧间隔。探测脚本的重组状态需按设备及帧隔离,避免测量错误。

可重复测量:python scripts/perf_probe.py <板卡IP> --seconds 30 --json perf.json。默认使用独立的临时端口和 1 MiB 接收缓冲,每 800 ms 定向续订。脚本只依赖 Python 标准库;--save-frame frame.jpg 可保存首张完整 JPEG。公平比较应保持订阅者数量、分辨率、画质与网络条件一致。

frame_loss_percent 包含不完整、超时及帧号缺口,不包含开始订阅前或测量结束后的帧。assembly_p95_ms 是首末分片到达的时间差,inter_frame_p95_ms 是完整帧到达间隔,均使用高精度计时;没有发送时间戳及两端时钟同步,不能称为端到端延迟。

固件日志将采集尝试与完整提交分开计数。submitted 只表示全部分片交给了网络,不代表无线确认或客户端显示;send failures 可能包含随后重试成功的调用,应结合 peer frames dropped 和接收端指标判断。

画面链路测量使用裁判端 --latency-report <新文件>,让相机拍摄校准页左侧标记,固定相机和窗口位置。标记经正常采集、网络与绘制路径回到客户端,源与回显共用主机单调时钟;不需要 ESP 时钟同步。测试期间只保留一个原生客户端,额外的本地 WebSocket 观察器不增加 UDP 订阅。方法、刷新估计边界及最新结果见 LATENCY.md。

按记录文件的半开行号区间复算延迟:

python scripts/analyze_latency.py --report latency-run.ndjson --range fb3-range.json --range fb2-range.json --out latency-result.json

区间文件至少包含 start_row、end_row 和 fb_count,行号从 0 开始;报告关闭后再分析,避免读取写入中的半行。分析器拒绝重复源标记、无效时间边界与空样本,输出 P50/P95/P99、rAF 区间宽度和解码完成到绘制的耗时。JPEG 接收间隔仍是独立指标。

恢复策略是独立纯 Rust 模块,可在主机运行:

rustc +stable --test src/link_control.rs -o link-control-tests
./link-control-tests

Windows 为输出文件加 .exe。实机故障注入仅在诊断构建设置 WIFI_TEST_DROP_AFTER_S:主循环运行相应秒数后断开一次并保持 5 秒,正式构建保持 0。重连后需观察重新取址、socket 重建和订阅恢复,不能仅凭 connect 调用成功判定恢复视频。

板上验证

阻塞与缓冲交接测试:python scripts/test-latest-frame.py 使用主机 clang/gcc 编译真实 cam_take_latest 函数及队列替身,验证持续生产下有限取帧、旧缓冲仅归还一次、无新帧保留有效旧帧,以及首次超时不泄漏。rustc +stable --test src/stream_control.rs 验证发送期限、共享重试预算和零重试模式;desktop/ 的 pnpm test:stream 验证 WebView 积压、解码阻塞及过期帧清理。实机验证范围另见 REALTIME.md。

  1. 相机识别、PSRAM 初始化及稳定采集。
  2. Wi-Fi 完成关联和 DHCP,得到有效 IP。
  3. 客户端启动后订阅者增加并持续显示画面。
  4. python scripts/udp_probe.py 10 独立核对协议及帧统计。
  5. 分别验证多端订阅、断网重连、不同分辨率与持续运行。

编译、镜像生成与主机测试通过均不等于烧录、板上运行或硬件稳定性已经验证。