feat: 链路控制、延迟校准与长测工具,补全文档
- 固件:新增 link_control / stream_control,覆盖运行中 Wi-Fi 断线恢复、取帧节拍、 温控与质量/帧率减载;UDP 发送增加有界重试预算与帧期限,PSRAM JPEG 直发减少拷贝。 - 协议:重组器严格校验分片布局,统计未收齐、超时与完全缺失的帧,处理帧号回绕与设备重启。 - 桌面端:视频改 latest-value 通道并前置帧时间标记,拒绝积压和慢解码的过期画面;新增 延迟标记页与 --latency-report 校准,完善录制、服务端与退出流程。 - 文档与脚本:新增硬件、开发、延迟、功耗、实时性、稳定性文档和改动记录,重写根 README; 新增构建、性能、延迟与串口长测脚本,以及长测客户端脚本。 - 构建:固定 esp-1.97.0.0 工具链,分区表随构建复制,更新 sdkconfig 与忽略规则。
This commit is contained in:
@@ -1,395 +1,294 @@
|
||||
# ESP32-S3 摄像头图传(Rust / ESP-IDF / UDP)
|
||||
# ESP32-S3 摄像头图传系统
|
||||
|
||||
两块 ESP32-S3-WROOM-1-N16R8 图传板(DVP 接口接 OV5640),向队伍客户端与裁判终端推
|
||||
UDP 图传。
|
||||
基于 **Rust + ESP-IDF** 的局域网实时图传系统。ESP32-S3 采集摄像头输出的 JPEG,通过 UDP 向桌面客户端推流;桌面端提供队伍端与裁判端界面,并接收 ESP32-C3 装甲板的血量广播。
|
||||
|
||||
固件是跑在 ESP-IDF 上的 Rust,用官方 esp32-camera 组件采集;桌面端是 **Tauri v2 +
|
||||
React**,用户端与裁判端两套页面(见 [`desktop/README.md`](desktop/README.md))。同一套
|
||||
系统里还有一队一块的**装甲板**(ESP32-C3,一主三从),负责四路击打检测、灯带与整车血量,
|
||||
并把血量广播给局域网里的两端。
|
||||
## 功能与架构
|
||||
|
||||
- 摄像头自动探测、JPEG 采集、UDP 分片发送与完整帧重组。
|
||||
- 局域网广播发现与订阅,单块图传板最多向 4 个客户端发送画面。
|
||||
- 运行中 Wi-Fi 断开后自动重连,重新获得 IP 后重建 UDP socket 并恢复订阅。
|
||||
- 使用 MAC 派生的稳定设备 ID;两块图传板可烧录同一固件,在客户端绑定队伍。
|
||||
- Tauri v2 + React 桌面端:视频显示、双方血量、设备绑定与录制。
|
||||
- 独立 ESP32-C3 装甲板固件:四路受击检测、WS2812 灯带及血量上报。
|
||||
|
||||
```text
|
||||
protocol/ 线路格式 + 帧重组 三端共用,无 ESP-IDF 依赖,14 个单元测试
|
||||
. 图传固件 -> ESP32-S3 交叉编译,Xtensa
|
||||
armor/ 装甲板固件 -> ESP32-C3 独立 workspace,RISC-V;规则层 22 个单元测试
|
||||
desktop/ 桌面端 Tauri v2 + React;用户端(双方血条 + 瞄准器)/ 裁判端(可改队名)
|
||||
core/ 不含界面与 Tauri 的网络层、配置、密码(可单测)
|
||||
src-tauri/ Tauri 后端:推流、命令、录制
|
||||
src/ React 前端:两套页面 + HUD
|
||||
ESP32-S3 图传板 A ─┐ ┌─ 队伍客户端
|
||||
├─ 同一局域网 / UDP ─┼─ 裁判客户端
|
||||
ESP32-S3 图传板 B ─┤ └─ 队伍客户端
|
||||
ESP32-C3 装甲主板 ─┘
|
||||
```
|
||||
|
||||
五份产物各自独立构建,各有自己的 `.cargo/config.toml`。
|
||||
图传使用 UDP **5600**,装甲板血量使用 UDP **5601**。板卡与客户端需在可互相通信、允许广播的网络中;关闭 AP 客户端隔离,并允许客户端程序通过防火墙。
|
||||
|
||||
## 拓扑
|
||||
## 项目结构
|
||||
|
||||
| 路径 | 用途 |
|
||||
| --- | --- |
|
||||
| `src/` | ESP32-S3 图传固件,根目录 Cargo 项目 |
|
||||
| `components/` | 本地相机驱动及 C/Rust 接口封装 |
|
||||
| `protocol/` | 共享 UDP 协议与帧重组,支持主机测试 |
|
||||
| `armor/` | 独立 ESP32-C3 装甲板 workspace |
|
||||
| `desktop/` | Tauri + React 客户端及独立网络核心 `core/` |
|
||||
| `scripts/` | 构建、烧录与探测脚本 |
|
||||
| `reference/armor/` | 早期 C 版装甲板工程 |
|
||||
|
||||
各子项目的构建目标不同。以下编译、烧录流程针对**根目录的 ESP32-S3 图传固件**。
|
||||
|
||||
## 硬件要求
|
||||
|
||||
| 项目 | 当前配置 |
|
||||
| --- | --- |
|
||||
| 开发板 | Freenove ESP32-S3-WROOM CAM |
|
||||
| 模组 | ESP32-S3-WROOM-1-N16R8,16 MB Flash + 8 MB Octal PSRAM |
|
||||
| 摄像头 | OV5640;驱动包含 OV2640 / OV3660 支持,接线需核对 |
|
||||
| 网络 | 2.4 GHz Wi-Fi |
|
||||
| 连接 | 支持数据传输的 USB 线、可识别的串口及稳定供电 |
|
||||
|
||||
默认采集 VGA(640 × 480)、彩色 JPEG quality 12,按传感器节拍发送。相机引脚、Flash 分区和 PSRAM 配置见 [HARDWARE.md](HARDWARE.md)。换板型前必须核对接线,避免占用 Flash / PSRAM 引脚。
|
||||
|
||||
## ESP Rust 工具链入门
|
||||
|
||||
本项目使用 Rust `std` 和 ESP-IDF。ESP32-S3 属于 **Xtensa** 架构,需要通过 `espup` 安装 Espressif Rust 工具链;普通 `rustup target add` 不能替代这一步。根目录固定选择名为 `esp-1.97.0.0` 的工具链,避免浮动版本影响构建。
|
||||
|
||||
### 1. 安装基础依赖与 Rust 工具
|
||||
|
||||
先安装 [Rust / rustup](https://rustup.rs/)、[Git](https://git-scm.com/downloads)、[Python 3](https://www.python.org/downloads/)(建议 3.11)、[CMake](https://cmake.org/download/) 与 [Ninja](https://ninja-build.org/),确保命令在 `PATH` 中。
|
||||
|
||||
- **Windows**:使用 PowerShell 7.5 或更新版本,安装 Visual Studio Build Tools 的“使用 C++ 的桌面开发”和 Windows SDK,使用 MSVC Rust 主机工具链。仓库和工具链尽量使用较短、无空格和中文的路径。
|
||||
- **Linux(Debian / Ubuntu)**:安装 `build-essential pkg-config libudev-dev libssl-dev python3 python3-venv cmake ninja-build git curl`。
|
||||
- **macOS**:安装 Xcode Command Line Tools(`xcode-select --install`),以及 Python、CMake 和 Ninja。
|
||||
|
||||
以下命令显式使用 `+stable`,即使在仓库内执行,也不会要求事先已有 `esp`:
|
||||
|
||||
```text
|
||||
┌──────────────────────────────┐
|
||||
│ 裁判终端(PC,仅显示) │
|
||||
│ 队伍一 + 队伍二 + 双方血量 │
|
||||
└───────────────┬──────────────┘
|
||||
│ 同一个局域网,广播可用
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
|
||||
│ 图传板 A │ │ 图传板 B │ │ 装甲板主板 │
|
||||
│ (队伍一) │ │ (队伍二) │ │ ×2(每队一块)│
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
rustup toolchain install stable
|
||||
cargo +stable install espup --locked
|
||||
cargo +stable install ldproxy --locked
|
||||
cargo +stable install espflash --version 4.4.0 --locked
|
||||
espup install --name esp-1.97.0.0 --toolchain-version 1.97.0.0 --targets esp32s3 --std
|
||||
```
|
||||
|
||||
**图传板不区分队伍。** 它们只知道自己是谁(MAC 派生的 `sender_id`),"哪一边"由
|
||||
客户端决定。烧同一份固件即可,配置全在客户端一侧。
|
||||
也可以从各工具的官方 releases 下载预编译程序。`espup --std` 安装本项目所需的 Rust / LLVM 工具,ESP-IDF 的 C 工具链由后续构建准备。
|
||||
|
||||
**装甲板反过来:队伍是编译期定的**(`ARMOR_TEAM=0/1`,同时决定灯色红/蓝),因为血量
|
||||
上报里必须带 `team` 字段,而且灯色现场不该靠按键切错。见 `armor/README.md`。
|
||||
Linux / macOS 每次新开终端后执行:
|
||||
|
||||
裁判终端同时收两块图传板,也收两块装甲板主板在 5601 上广播的血量。队伍客户端也收
|
||||
全部板子的视频,然后在**界面层**过滤到绑定的那一块;**血量相反,两边都显示**——用户端的
|
||||
画面左上角是自己、右上角是对手。血量本来就是明文广播的(裁判端与用户端收的是同一份),而
|
||||
"自己的血与对手的血一起看"是这项运动里最基本的对照,藏起来只会让操作手看不见对手的血。
|
||||
|
||||
## 实测性能
|
||||
|
||||
VGA 640x480,传感器产出上限 22 fps(OV5640 在此配置下的实测值)。
|
||||
|
||||
| | TCP(HTTP MJPEG) | UDP(现在) |
|
||||
|---|---|---|
|
||||
| 传感器实际产出 | 22 fps | 22 fps |
|
||||
| 客户端实际收到 | **1.9 fps** | **22 fps** |
|
||||
| 实际吞吐 | 0.28 Mbit/s | 1.6 Mbit/s |
|
||||
| 丢帧 | — | 0% |
|
||||
|
||||
TCP 那一栏只用了链路能力的 1% 左右(ESP32-S3 在良好 2.4GHz 链路上 TCP 实测上限约
|
||||
20–30 Mbit/s)。**链路是空的**,帧却堵在队头阻塞后面等一个实时画面等不起的重传计时
|
||||
器。
|
||||
|
||||
UDP 没有重传与确认:缺了分片的帧整个丢弃,下一帧顶上。丢帧率实测 0%,因为带宽从来
|
||||
不是瓶颈——即便如此也只用掉 6–9%。
|
||||
|
||||
其余数字:平均帧 22 kB,约 350 包/秒,每帧约 17 个分片,客户端观测的帧间隔 7 ms。
|
||||
|
||||
## 为什么视频走 UDP 而不是 TCP
|
||||
|
||||
**延迟优先。** 丢失 5% 的画面远好于等待 500 ms 的重传。
|
||||
|
||||
**没有队头阻塞。** 一帧的任一分片卡住,后面所有帧都跟着卡——对实时画面这正好是错
|
||||
的方向。
|
||||
|
||||
**带宽不是约束。** 见上表,TCP 那栏证明了链路是空的,所以 TCP 的优势在这里换不来
|
||||
任何东西。
|
||||
|
||||
代价是丢包丢整帧。这被接受了:**撕裂的画面比过期的画面更糟**,所以接收端不做补洞,
|
||||
只等下一张完整帧。
|
||||
|
||||
## 帧率必须按计数除以墙钟
|
||||
|
||||
"22 fps" 这个数字不是对帧间隔取平均得来的,是每个窗口的**帧数增量除以窗口时长**。
|
||||
|
||||
一次 UI 轮询(33ms)之间可能完成多帧,平均间隔量的是**显示**的刷新快慢而不是**相机**
|
||||
的出帧快慢——那会把上限 22fps 的传感器报成 36fps。**一个笃定的错误数字比没有数字更
|
||||
糟**,现场你会以为还有性能余量,然后花时间去找一个不存在的问题。
|
||||
|
||||
这个错误在本项目里出现过两次(客户端的 `View::update_fps`、探测脚本的分窗打印),两
|
||||
次都是同一个形状:分子用累计值,分母用窗口值,于是报出随时间单调上升的假数字。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 固件
|
||||
|
||||
```sh
|
||||
cp .env.example .env # 填 WIFI_SSID / WIFI_PASS
|
||||
./scripts/flash.sh --monitor 20
|
||||
```bash
|
||||
. "$HOME/export-esp.sh"
|
||||
```
|
||||
|
||||
`flash.sh` 会编译、挑选避开坏字节的 app 偏移、烧录三段(bootloader / 分区表 /
|
||||
app)、打印串口日志。ESP-IDF 位置从 `.cargo/config.toml` 读,不用先 `activate`。
|
||||
Windows 安装后重新打开 PowerShell,让环境变量生效;若当前终端仍找不到 LLVM,可执行 `. "$HOME/export-esp.ps1"`。检查结果:
|
||||
|
||||
`.env`:
|
||||
|
||||
```sh
|
||||
WIFI_SSID=你的2.4G网络
|
||||
WIFI_PASS=密码
|
||||
|
||||
# 装甲板(armor/)用这两个里的第一个;队伍号决定灯色与上报里的 team 字段:
|
||||
#ARMOR_TEAM=0 # 0 = 队伍一(红),1 = 队伍二(蓝)
|
||||
|
||||
# 以下都有默认值:
|
||||
#CAM_RES=vga # qqvga | qvga | cif | hvga | vga | svga | xga | sxga | uxga
|
||||
#CAM_QUALITY=12 # 0-63,数字越小画质越好、字节越少
|
||||
#CAM_VSYNC_INVERT=1 # 若板子在硬件上反相了 VSYNC,填 0
|
||||
#WIFI_AUTH=wpa2 # 固定认证方式,仅当扫描看不到 AP 时需要(如隐藏 SSID)
|
||||
```text
|
||||
rustup toolchain list
|
||||
rustc +esp-1.97.0.0 --version
|
||||
cargo +esp-1.97.0.0 --version
|
||||
espflash --version
|
||||
```
|
||||
|
||||
凭据由 `env!` 在**编译期**读取,所以没有运行时解析路径,也不会因为忘了读 `.env` 而
|
||||
在板子上带着别人的密码。`ARMOR_TEAM` 同理:换队伍重刷装甲板固件即可,不用擦 NVS。
|
||||
官方入门资料:[The Rust on ESP Book](https://docs.espressif.com/projects/rust/book/getting-started/toolchain.html)、[espup](https://github.com/esp-rs/espup)、[espflash](https://github.com/esp-rs/espflash/tree/v4.4.0/espflash)。
|
||||
|
||||
`WIFI_AUTH` 一般不用设:固件先扫描 AP,用它报告的真实认证方式去连,只有扫不到时才
|
||||
需要人工指定。指定之后**只**试这一种方式——不成功就直接报错,而不是挨个试 10 秒。
|
||||
挨个试等于在猜一个本该已知的值。
|
||||
### 2. 准备 ESP-IDF
|
||||
|
||||
### 2. 装甲板固件(ESP32-C3)
|
||||
默认使用 **ESP-IDF v5.5.5**。首次构建时,`esp-idf-sys` 下载 SDK、对应 C 工具链及 Python 依赖,默认缓存到用户目录的 `.espressif/`;需要网络、磁盘空间及缓存目录写入权限。
|
||||
|
||||
```sh
|
||||
scripts/flash_armor.sh # 编译 + 烧录 + 20 秒串口日志
|
||||
python3 scripts/armor_probe.py 15 # 不开界面,单看板子在广播什么血量
|
||||
已有匹配版本 SDK 时可在终端设置 `IDF_PATH`,不要提交个人绝对路径:
|
||||
|
||||
```powershell
|
||||
# Windows:替换为实际 SDK 源码目录。
|
||||
$env:IDF_PATH = 'C:\Espressif\frameworks\esp-idf-v5.5.5'
|
||||
```
|
||||
|
||||
接线表、血量规则、C3 的 RMT/ADC 踩坑都在 [`armor/README.md`](armor/README.md)。
|
||||
```bash
|
||||
# Linux / macOS:替换为实际 SDK 源码目录。
|
||||
export IDF_PATH="$HOME/esp/esp-idf-v5.5.5"
|
||||
```
|
||||
|
||||
### 3. 桌面端
|
||||
使用自动下载时,清除指向其他 SDK 的旧 `IDF_PATH`。缓存位置等高级设置见 [esp-idf-sys 构建选项](https://github.com/esp-rs/esp-idf-sys/blob/master/BUILD-OPTIONS.md)。
|
||||
|
||||
```sh
|
||||
## 配置 Wi-Fi 与摄像头
|
||||
|
||||
在根目录复制 `.env.example` 为 `.env`,再用编辑器填写;已有 `.env` 时不要覆盖。
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
Copy-Item .env.example .env
|
||||
```
|
||||
|
||||
```bash
|
||||
# Linux / macOS
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
```dotenv
|
||||
WIFI_SSID='你的2.4G网络名称'
|
||||
WIFI_PASS='你的Wi-Fi密码'
|
||||
|
||||
# 可选,省略时使用固件默认值。
|
||||
#CAM_RES=vga
|
||||
#CAM_QUALITY=12
|
||||
#CAM_QUALITY_MAX=0
|
||||
#CAM_MAX_FPS=0
|
||||
#CAM_CLOCK_PROFILE=default
|
||||
#CAM_FB_COUNT=3
|
||||
#CAM_GRAYSCALE=0
|
||||
#CAM_EXPOSURE_EV=0
|
||||
#CAM_AE_LEVEL=-1
|
||||
#CAM_SHUTTER_LINES=0
|
||||
#CAM_EXPOSURE_MAX_LINES=0
|
||||
#CAM_GAIN_CEILING=0
|
||||
#CAM_HTS=0
|
||||
#UDP_PACKET_GAP_US=0
|
||||
#UDP_RETRY_BUDGET_US=8000
|
||||
#CAM_THERMAL_LIMIT_C=75
|
||||
#CAM_VSYNC_INVERT=1
|
||||
#WIFI_AUTH=wpa2
|
||||
```
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `WIFI_SSID` | 无,必填 | 2.4 GHz 网络名称,最多 32 字节 |
|
||||
| `WIFI_PASS` | 空字符串 | Wi-Fi 密码;开放网络可留空 |
|
||||
| `CAM_RES` | `vga` | `qqvga` / `qvga` / `cif` / `hvga` / `vga` / `svga` / `xga` / `sxga` / `uxga` |
|
||||
| `CAM_QUALITY` | `12` | 0–63,越小通常画质越好、JPEG 越大 |
|
||||
| `CAM_QUALITY_MAX` | `0` | `0` 固定画质;设为 `CAM_QUALITY`–63 时按发送回压在两者之间调节压缩,保留彩色和分辨率。拥塞时每 5 秒增大最多 3 档,连续稳定 15 秒才恢复 1 档 |
|
||||
| `CAM_MAX_FPS` | `0` | 取帧与发送的最高节拍,1–60;`0` 不限帧。仅限制消费节拍,不修改传感器时钟 |
|
||||
| `CAM_CLOCK_PROFILE` | `default` | `default` 保留驱动时钟;`vga30` 使用 OV5640 VGA 约 30.09 fps PLL 配置,保持 HTS/VTS 和读出窗口,SYSCLK 61 MHz / PCLK 15.25 MHz。切换后需重新核对曝光行数 |
|
||||
| `CAM_FB_COUNT` | `3` | PSRAM 帧缓冲数,允许 2 或 3。当前交付选三缓冲,保留发送突发时的采集余量;双缓冲曾在短测降低部分回显延迟,但用户长程运行反馈峰值丢帧明显,保留为试验选项 |
|
||||
| `CAM_GRAYSCALE` | `0` | `1` 使用传感器灰度效果,保留硬件 JPEG;`0` 彩色 |
|
||||
| `CAM_EXPOSURE_EV` | `0` | OV5640 自动曝光目标偏置:`-1` / `-2` 约对应 −1.1 / −1.9 EV;不是标定后的绝对 EV,也不保证快门缩短 |
|
||||
| `CAM_AE_LEVEL` | 未设置 | 可选 OV5640 驱动 AE 档位,−5–5;仅与 `CAM_EXPOSURE_EV=0` 同用。`-1` 将目标从 55 小幅降至 45,目标比值约 −0.29 EV,保留自动曝光/增益;未设置时按 EV 参数处理。实际快门和亮度变化取决于场景 |
|
||||
| `CAM_SHUTTER_LINES` | `0` | OV5640 固定快门,单位为传感器曝光行;`0` 自动曝光,非零保持自动增益。默认 VGA / HTS 2060 下 220 行估算约 10 ms;数值不能超过当前 VTS |
|
||||
| `CAM_EXPOSURE_MAX_LINES` | `0` | OV5640 自动曝光上限,`0` 保留驱动默认。非零保持 AEC/AGC 联动并禁用夜景延帧,不能与固定快门同时使用。默认 VGA 下 440 行约 20 ms;`vga30` 下 592 行约 20 ms。实际曝光由场景和抗频闪步长决定 |
|
||||
| `CAM_GAIN_CEILING` | `0` | OV5640 自动增益上限,`0` 保留驱动默认;1–63,单位为倍,实际上限略低于整数档。短曝光遇到室内弱光时可提高,但暗部噪点和 JPEG 负载可能增加 |
|
||||
| `CAM_HTS` | `0` | OV5640 水平总长度试验参数,`0` 使用驱动默认。缩短消隐会影响彩色稳定性,本机 1500 / 1800 出现绿紫闪烁,已撤回;几何检查通过不代表可用 |
|
||||
| `UDP_PACKET_GAP_US` | `0` | 成功提交视频数据报之间的最小间隔;0–5000 μs,须为调度 tick 的整数倍(当前为 1000 μs)。配合有界帧等待,避免集中突发写入队列 |
|
||||
| `UDP_RETRY_BUDGET_US` | `8000` | 每帧、所有分片及订阅者共用的让出调度重试预算,0–25000 μs,须为 tick 整数倍;`0` 为非阻塞失败即弃。总发送检查期限为 25 ms,采集帧龄期限为 100 ms;分片间隔也计入期限,不会额外延长 |
|
||||
| `CAM_THERMAL_LIMIT_C` | `75` | 芯片内部温度达到该值时减半帧率(向上取整),下降 5°C 后恢复;40–95,`0` 禁用。原先不限帧时降至 8 fps |
|
||||
| `CAM_VSYNC_INVERT` | `1` | 驱动反相 VSYNC;硬件已反相时填 `0` |
|
||||
| `WIFI_AUTH` | 自动选择 | `none` / `wep` / `wpa` / `wpa2` / `wpawpa2` / `wpa3` / `wpa2wpa3` / `wapi` |
|
||||
| `WIFI_TX_POWER_DBM` | SDK 默认上限 | 可选整数 2–20 dBm;当前交付请求 15 dBm,本板驱动回读 15.00 dBm;启动及重连时应用,拥塞和温控逻辑不降低此上限 |
|
||||
|
||||
`.env` 使用简单的 `KEY=value`;值含空格、`#` 或 `$` 时用单引号包围,不使用变量展开或命令替换。脚本会加载 `.env`,直接 `cargo build` 不会自动读取它。上述参数在编译时写入固件,修改后需要重新编译和烧录。`.env` 已被 Git 忽略;固件也包含凭据,分享前应重新配置。
|
||||
|
||||
运行时断线恢复不重置摄像头、曝光或增益。首次尝试立即发起,失败后按 1、2、4、8、16、30 秒退避;关联与 DHCP 分别有 15 秒期限。恢复 IP 后丢弃旧采集帧、重建 socket 并重新广播,由客户端自动续订。启动时首次联网仍使用扫描与认证模式探测流程。实现依据 [ESP-IDF 断线事件处理说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected)。
|
||||
|
||||
## 编译固件
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
$env:CARGO_TARGET_DIR = 'C:\s3vt'
|
||||
.\scripts\build.ps1
|
||||
```
|
||||
|
||||
```bash
|
||||
# Linux / macOS
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
脚本执行 `cargo build --release --locked`,目标为 `xtensa-esp32s3-espidf`。首次编译包含 Rust 标准库、ESP-IDF 和相机组件,耗时较长。
|
||||
|
||||
Windows 的 ESP-IDF 构建输出路径需不超过 88 字符,因此示例使用短目录 `C:\s3vt`(需可写,且不与其他项目共用)。`subst` 不能绕过该检查。下表的 `<TARGET_DIR>` 在 Windows 示例中为 `C:/s3vt`,Linux / macOS 默认是仓库的 `target`。每次新开 Windows 终端都要设置相同的 `CARGO_TARGET_DIR`。
|
||||
|
||||
| 产物 | 位置 |
|
||||
| --- | --- |
|
||||
| 固件 ELF | `<TARGET_DIR>/xtensa-esp32s3-espidf/release/esp32`(没有 `.exe` 后缀) |
|
||||
| Bootloader | `<TARGET_DIR>/xtensa-esp32s3-espidf/release/bootloader.bin` |
|
||||
| 分区表 BIN | `<TARGET_DIR>/xtensa-esp32s3-espidf/release/partition-table.bin` |
|
||||
| 分区表源文件 | 根目录 `partitions.csv` |
|
||||
|
||||
ELF 供 `espflash` 烧录与符号解析,不能当作 BIN 直接写入 Flash。Bootloader、分区表与 App 应来自同一次构建。
|
||||
|
||||
## 烧录与串口验证
|
||||
|
||||
### 1. 查找串口
|
||||
|
||||
```text
|
||||
espflash list-ports
|
||||
```
|
||||
|
||||
Windows 常见 `COM5`;Linux 为 `/dev/ttyACM0` 或 `/dev/ttyUSB0`;macOS 为 `/dev/cu.usbmodem...`。关闭其他串口监视器,Linux 需具备串口访问权限。
|
||||
|
||||
### 2. 烧录 ELF
|
||||
|
||||
本项目使用自定义分区表,显式传入 `partitions.csv` 和本次构建的 Bootloader。将示例端口替换为实际端口:
|
||||
|
||||
```powershell
|
||||
$targetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { 'target' }
|
||||
espflash flash --chip esp32s3 --port COM5 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$targetDir/xtensa-esp32s3-espidf/release/bootloader.bin" --monitor "$targetDir/xtensa-esp32s3-espidf/release/esp32"
|
||||
```
|
||||
|
||||
```bash
|
||||
target_dir="${CARGO_TARGET_DIR:-target}"
|
||||
espflash flash --chip esp32s3 --port /dev/ttyACM0 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$target_dir/xtensa-esp32s3-espidf/release/bootloader.bin" --monitor "$target_dir/xtensa-esp32s3-espidf/release/esp32"
|
||||
```
|
||||
|
||||
连接失败时按住 **BOOT**,点按 **RESET / EN**,松开 BOOT 后重试。正常启动应看到相机识别、Wi-Fi 获取 IP 和 `ESP32 camera is live` 日志。没有客户端时,`0 clients subscribed` 属于正常状态。
|
||||
|
||||
仅查看日志:
|
||||
|
||||
```text
|
||||
espflash monitor --port COM5
|
||||
```
|
||||
|
||||
### 3. 导出 BIN
|
||||
|
||||
`espflash save-image --merge` 可导出从 `0x0` 写入的合并镜像。完整命令、独立镜像偏移与测试方法见 [DEVELOPMENT.md](DEVELOPMENT.md)。
|
||||
|
||||
保留的 `scripts/flash.sh` 针对**特定坏 Flash 模组**,依赖 macOS 环境,会修改分区并重编译。普通板卡使用上述标准流程;坏板限制见 [HARDWARE.md](HARDWARE.md#损坏-flash-模组的兼容处理)。
|
||||
|
||||
## 客户端与装甲板
|
||||
|
||||
桌面端入口如下。安装依赖、主机 target 设置与功能说明见 [desktop/README.md](desktop/README.md):
|
||||
|
||||
```text
|
||||
cd desktop
|
||||
pnpm install
|
||||
pnpm app # = tauri dev
|
||||
pnpm app
|
||||
```
|
||||
|
||||
没有板子也能把整条链路跑起来:`CAM_DEMO=1 pnpm app` 会合成**协议数据报**喂进真实的接收
|
||||
路径(分片、重组、发现、掉血派生、两道闸门全在跑真代码),界面上标着"演示模式"。
|
||||
装甲板是独立的 **ESP32-C3 / RISC-V** 工程,不能使用 S3 的 ELF。`ARMOR_TEAM=0/1` 在编译期决定红 / 蓝队伍。接线、规则与构建烧录见 [armor/README.md](armor/README.md)。
|
||||
|
||||
首次启动会问这台机器是哪一端:**用户端**要绑一块图传板与一块装甲板(改动需密码),
|
||||
**裁判端**不需要任何配置就能看两路。
|
||||
## 常见问题
|
||||
|
||||
**`desktop/.cargo/config.toml` 里的 `target = "aarch64-apple-darwin"` 换机器必须改。**
|
||||
desktop/ 嵌在固件的目录树里,会继承父目录那个 `xtensa-esp32s3-espidf` 的交叉编译配置,
|
||||
然后在依赖深处以 `libc` 找不到 `MAP_FAILED` 的形式失败。
|
||||
| 现象 | 检查方法 |
|
||||
| --- | --- |
|
||||
| 提示缺少 `esp-1.97.0.0` | 按上述命令安装指定名称的工具链,重新打开终端后检查版本 |
|
||||
| `espup` 查询 GitHub 版本返回 403 | 对上述固定版本安装命令追加 `--skip-version-parse`,或稍后重试 |
|
||||
| 找不到 `ldproxy` / `libclang` | 安装链接工具;Unix 加载 `export-esp.sh`,Windows 重开终端 |
|
||||
| ESP-IDF 路径或版本错误 | 清除旧 `IDF_PATH` 或指向 v5.5.5 SDK |
|
||||
| Windows 路径过长 | 缩短仓库和工具链路径,见开发说明 |
|
||||
| Wi-Fi 连不上 | 核对 2.4 GHz、凭据和认证方式,再重新编译烧录 |
|
||||
| 相机无输出 / 初始化失败 | 核对板型接线、供电与 Octal PSRAM |
|
||||
| `NO-EOI`、几帧后停止 | 确认 `CONFIG_CAMERA_PSRAM_DMA=n` |
|
||||
| 在线但没有画面 | 检查客户端订阅、UDP 5600、防火墙及 AP 隔离 |
|
||||
| 开启 VPN / 虚拟网卡后设备在线但无画面 | 使用支持按已发现设备 IP 定向续订的客户端;仅发送 `255.255.255.255` 广播可能走错网卡 |
|
||||
| 改 C 组件后无变化 | 清理 `esp-idf-sys` 构建缓存后重编,见开发说明 |
|
||||
| 主机项目被编为 Xtensa | 显式指定宿主 target,见各子项目说明 |
|
||||
|
||||
界面、视频通道、密码模型、录制与排查都在 [`desktop/README.md`](desktop/README.md)。
|
||||
用 `python scripts/udp_probe.py 10` 可独立检查视频协议;装甲板用 `python scripts/armor_probe.py 15`。这些脚本需要板卡在线,构建通过不代表已完成硬件验证。
|
||||
|
||||
## 发现是双向的
|
||||
测量帧率和丢帧可运行以下命令,将 IP 换成板卡实际地址。对照测试时先退出桌面客户端,避免额外订阅者增加无线负载:
|
||||
|
||||
单靠一个方向建立不了"谁要看我":
|
||||
|
||||
- 板子每 **2 秒**广播一次 Hello —— "我在线,我是谁"
|
||||
- 客户端每 **800 ms**广播一次 Subscribe —— "我是这个角色,请给我画面"
|
||||
|
||||
板子从订阅的**源地址**学到推流目标(所以客户端的 socket 必须固定绑 5600 端口,不能
|
||||
用临时端口)。
|
||||
|
||||
对端靠**源地址**记忆,不发探测包。续订本来就每 800 ms 一次,所以"还活着吗"这个问题
|
||||
由续订本身回答了——超时即代表它离开了,多一份心跳状态就多一个能出错的地方。
|
||||
|
||||
两端各有自己的超时淘汰,阈值不同:
|
||||
|
||||
| | 超时 | 为什么是这个量级 |
|
||||
|---|---|---|
|
||||
| 固件丢弃订阅者 | 6 s | 续订间隔 800 ms,7 倍余量,偶发丢包不会把人踢掉 |
|
||||
| 客户端移除板子 | 60 s | 只影响界面列表,不影响推流 |
|
||||
|
||||
客户端的 60 s 刻意很长:界面列表里留着一块刚重启还没广播的板子,比它短暂消失更可接
|
||||
受。而它每 5 秒才扫一次,不值得为这个上一份状态。
|
||||
|
||||
固件同时最多向 **4 个**订阅者推流(`MAX_PEERS`)。每个对端每帧要一次 `sendto`,这是帧
|
||||
CPU 成本的倍数;三台客户端(两队 + 裁判)够用,且不让一个陈旧的地址条目拖慢每一帧。
|
||||
|
||||
## 身份:为什么用 MAC 派生
|
||||
|
||||
`protocol::id_from_mac()` 用 FNV-1a 从 MAC 派生 `sender_id`,避开 0(裁判终端的保留
|
||||
值)。
|
||||
|
||||
**用 MAC 而不是 IP**,因为客户端要把绑定存进配置文件并在重启后继续有效——DHCP 换地
|
||||
址是常态,MAC 不会变。用 MAC 也意味着烧同一份固件即可,不需要任何"我是谁"的配置。
|
||||
|
||||
## 线路格式
|
||||
|
||||
27 字节头 + 至多 1400 字节载荷,全部小端。细节在 `protocol/README.md`,那里有完整
|
||||
的字节图和每个拒收条件。
|
||||
|
||||
三处关键决定:
|
||||
|
||||
- **27 字节不凑整。** 头只占每个 1427 字节数据报的 1.9%,而为对齐在两端各写一段
|
||||
手写偏移常量,是引入静默错位的更划算的代价。
|
||||
- **每个数据报 1427 字节,低于 1500 MTU。** 超 MTU 的数据报要么被 AP 丢、要么被 IP
|
||||
分片,两种情况的丢包都记在应用账上而不是网络账上——后者本来更容易排查。
|
||||
- **`version` 不匹配就拒收**,而不是按错误布局继续解析。后者表现为随机崩溃或画面错
|
||||
乱,比拒绝连接难查得多。
|
||||
|
||||
## 硬件
|
||||
|
||||
Freenove ESP32-S3-WROOM CAM,ESP32-S3-WROOM-1-N16R8(16 MB flash + 8 MB octal
|
||||
PSRAM),OV5640。
|
||||
|
||||
### 引脚
|
||||
|
||||
`src/camera.rs` 的 `pins` 模块:
|
||||
|
||||
| | GPIO |
|
||||
|---|---|
|
||||
| RESET | 0 |
|
||||
| XCLK | 15 |
|
||||
| PCLK | 13 |
|
||||
| VSYNC | 6 |
|
||||
| HREF | 7 |
|
||||
| SIOD (SDA) | 4 |
|
||||
| SIOC (SCL) | 5 |
|
||||
| DATA[0..8] | 11, 9, 8, 10, 12, 18, 17, 16 |
|
||||
|
||||
### 千万不要套用别的板子的引脚表
|
||||
|
||||
这块板子的 GPIO 分配是死的:
|
||||
|
||||
| 范围 | 占用 |
|
||||
|---|---|
|
||||
| 26–32 | SPI flash |
|
||||
| 33–37 | octal PSRAM |
|
||||
| 19 / 20 | USB |
|
||||
| 43 / 44 | console |
|
||||
| 45 / 46 | 模组内部 |
|
||||
|
||||
直接套用 AI-Thinker ESP32-CAM 的引脚表会有 **7 根冲突**。冲突的表现不是"某根线不
|
||||
通",而是 `PIN_FUNC_SELECT` 把某个外设接到了 flash 或 PSRAM 上,**CPU 在传输中途挂
|
||||
死**——没有报错、没有回溯、串口直接静默。
|
||||
|
||||
改引脚前先对照上面这张表。
|
||||
|
||||
### 那块坏掉的 flash 字节
|
||||
|
||||
测试用的这块模组在 `0xFADE7` 有个字节的 bit 5–7 **写不进去**。镜像放在哪里会决定哪
|
||||
个镜像字节落到这个地址,落上去就校验失败、bootloader 拒绝启动。
|
||||
|
||||
镜像本身不因位置而变,所以移动 app 分区就够了。`scripts/flash.sh` 会挑最低的 64 KB
|
||||
对齐、且该地址处字节 flash 真的能存的偏移,写回 `partitions.csv`,然后重新编译
|
||||
(重编译会让镜像位移,所以编译后再验证一次)。
|
||||
|
||||
**换到健康硬件**:把 `factory` 分区放回 `0x10000`,删掉 `flash.sh` 里的
|
||||
`BAD_FLASH_ADDR`,别的什么都不用改。
|
||||
|
||||
Flash 跑 DIO @ 40 MHz 而不是这颗片子的标称 QIO/80 MHz:测试模组的坏字节在 quad 模式
|
||||
下会被推到 boot loop。QIO 对**代码执行**速度的提升是明显的(程序从 flash 跑,cache
|
||||
miss 直接打在这条总线上),健康硬件上值得换回去试。
|
||||
|
||||
### `PSRAM DMA` 是故意关掉的
|
||||
|
||||
开着的话,esp32-camera 让 GDMA 直接把帧缓冲写进 PSRAM,然后**只在最后一个 DMA 节点
|
||||
(1 KB)里**找 JPEG 结束标记。标记落在窗口外就丢帧并报 `NO-EOI`——在这块板子上是大部
|
||||
分的帧,几帧之后驱动干脆不再交出任何帧,而且不恢复。
|
||||
|
||||
关掉之后驱动 DMA 到内部 RAM 的中转缓冲,再 memcpy 进 PSRAM。每帧多一次约 23 KB 的
|
||||
memcpy,但搜索整个缓冲找标记。对一条能用的图传链路,这是对的取舍。
|
||||
|
||||
### CPU 频率
|
||||
|
||||
**`CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y`** 是 `sdkconfig.defaults` 里最值钱的一行,
|
||||
而且免费。
|
||||
|
||||
ESP-IDF v5.5 的 Kconfig 把 ESP32-S3 默认成 **160** MHz(见
|
||||
`components/esp_system/port/soc/esp32s3/Kconfig.cpu`),所以默认编译悄悄浪费了一半
|
||||
CPU。相机采集、esp32-camera 里的 JPEG 搜索、lwIP、发送路径全是 CPU 密集的,这一行一
|
||||
次性把它们全部搬上去。
|
||||
|
||||
### WiFi 缓冲留在内部 RAM
|
||||
|
||||
`CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=n`。
|
||||
|
||||
这个值曾经按"PSRAM 很空闲"的直觉写成 `y`,方向恰好相反:WiFi 描述符和 TCP 段**每个
|
||||
包都要碰**,内部 SRAM 比隔着内存总线的 PSRAM 快一个数量级。把它们放进 PSRAM 的表现是
|
||||
高延迟和差吞吐,无论 PSRAM 还剩多少。
|
||||
|
||||
帧缓冲相反——它大、相对少碰,留在 PSRAM。
|
||||
|
||||
### 相机任务钉在 CPU1
|
||||
|
||||
`CONFIG_CAMERA_CORE0=n`(即跑在 CPU1)。WiFi 任务钉在 CPU0。esp32-camera 的采集任务
|
||||
时间是短促且延迟敏感的(DMA 完成、JPEG 标记搜索、拷进帧缓冲),在共享核上会被抢占,
|
||||
每一帧都被拉长。双核芯片上没有理由共享。
|
||||
|
||||
## vendored esp32-camera
|
||||
|
||||
`components/esp32-camera/` 是上游 2.1.7 的副本,因为组件注册表的版本求解器找不到它。
|
||||
|
||||
它打了一处补丁:`target/esp32s3/ll_cam.c` 里的 `CAM_IO_MUX_REG()`。这个宏负责把引脚
|
||||
接到 I2C(OV5640 的 SCCB 走 I2C),不接的话传感器根本不会被发现。
|
||||
|
||||
**注意这个目录叫 `target/`,但它不是构建目录。** 它是上游组件的按芯片分目录
|
||||
(`esp32/`、`esp32s2/`、`esp32s3/`),里面是 `ll_cam.c` 等源码,包含上述补丁。所以
|
||||
`.gitignore` 里三条 `target/` 规则全部锚定在根目录——这是有意的。
|
||||
|
||||
## 改过 C 文件之后
|
||||
|
||||
```sh
|
||||
rm -rf target/xtensa-esp32s3-espidf/release/build/esp-idf-sys-*
|
||||
```text
|
||||
python scripts/perf_probe.py 192.168.1.139 --seconds 30 --json perf.json
|
||||
```
|
||||
|
||||
不删不会重新编译,你的补丁看起来像完全没生效。
|
||||
脚本定向续订,支持保留 VPN / TUN 的环境。输出包含完整帧率、帧损失率、接收间隔和组装耗时;这些时间不等于摄像头到屏幕的端到端延迟。
|
||||
|
||||
## 摄像头端不编码,客户端不编码
|
||||
运动图传保持 `CAM_GRAYSCALE=0`,优先使用自动曝光上限和自动增益协同控制拖影。室内强制 −1 EV / 固定短快门在本机过暗,已撤回;提高增益上限也会增加噪点。需要限流时可设置 `CAM_MAX_FPS=15`,追求帧率时使用 `0`;可启用有界 JPEG 压缩调节应对发送回压,分片间隔按网络实测选择。灰度效果仍保留硬件 JPEG,不是改为 RGB565 或降低 JPEG 采样位数。75°C 是主动减载阈值,温度来自 ESP 芯片内部,不能代表摄像头或环境温度,也不是芯片损坏温度规格。五分钟彩色对照与此前中止的灰度长测见 [STABILITY.md](STABILITY.md)。
|
||||
|
||||
OV5640 的 JPEG 编码器**在传感器内部**。DVP 接口出来的一帧要么是 JPEG,要么是 YUV 裸
|
||||
数据。
|
||||
## 文档索引
|
||||
|
||||
不编码的话,单帧从约 21 KB 涨到 **450 KB**(21 倍),而 ESP32-S3 **没有硬件 JPEG
|
||||
编码器**(整个 ESP32 系列只有 P4 有,`esp_driver_jpeg` 里
|
||||
`CONFIG_IDF_TARGET_ESP32P4` 是写死的)。
|
||||
|
||||
同理,客户端也不编码:板子出的就是传感器里的 JPEG,客户端只解码。ESP32-S3 也没有硬
|
||||
件 JPEG 解码器。
|
||||
|
||||
## 为什么收包和解码要分开线程
|
||||
|
||||
一张 VGA 的 JPEG 解码要几毫秒。放在收包线程里会挤进数据报之间,造成**链路上本不存在
|
||||
的丢帧**——不是真的丢了网络包,是应用来不及取走 socket 缓冲。
|
||||
|
||||
现在桌面端把 JPEG **原样**交给 WebView,由浏览器的原生解码器解——后端连解都不解,
|
||||
接收线程只做一次 16 字节头的拼接与一次广播。这一条比上面那条更彻底:省掉的不是几毫秒,
|
||||
而是一整个线程与一次内存拷贝。细节在 [`desktop/README.md`](desktop/README.md)。
|
||||
|
||||
## 排查
|
||||
|
||||
| 现象 | 原因 |
|
||||
|---|---|
|
||||
| 板子无输出、串口静默 | GPIO 冲突。CPU 在传输中途挂了,见上面那张占用表 |
|
||||
| 几帧后停止出图,报 `NO-EOI` | `PSRAM DMA` 被打开了 |
|
||||
| 板子在线但 `0 clients subscribed` | 订阅包格式不对。`Subscribe` 的 `role` 字节紧跟 27 字节头,在偏移 27 |
|
||||
| 一直接到但画面偶尔解不出来 | 帧重组的问题。**先怀疑你的测量工具**:本项目的探测脚本就因为重组状态没按 `frame_id` 隔离,误报过 34 张坏帧 |
|
||||
| `libc` 找不到 `MAP_FAILED` | `desktop/.cargo/config.toml` 的 target 没改成宿主三元组 |
|
||||
| 串口看不到任何输出 | 非 TTY 下 `espflash monitor` 不可用,用 `/tmp/rd2.py` 裸读 |
|
||||
| WiFi 连不上 | SSID 必须在 2.4GHz。5G 频段看不见 |
|
||||
| 裁判端两块画面宽窄不一 | 两格必须是 `flex: 1 1 0` 且 `min-width: 0`:按内容分宽度时,占位文字长的那一格会多占十几个像素 |
|
||||
| 裁判端两格显示同一块板子 | 旧版槽位分配的真 bug。现在候选里要排除另一个槽位占着的 id |
|
||||
| 有画面但状态栏说"离线" | 视频帧也必须算存活证据(`Discovery::touch`),不能只看 Hello |
|
||||
| 改了绑定却"没生效" | 改动需要先解锁;后端每个改动命令都会校验,界面上的入口只是入口 |
|
||||
|
||||
独立核对线路格式:
|
||||
|
||||
```sh
|
||||
python3 scripts/udp_probe.py 10
|
||||
```
|
||||
|
||||
它手写了每一个协议常量,**不依赖 `protocol` crate**——正因为如此,如果 crate 的编码
|
||||
有错,脚本会立刻报出不一致。
|
||||
|
||||
## 四份文档
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 根 `README.md` | 你在这里。拓扑、性能、构建烧录、硬件坑 |
|
||||
| `protocol/README.md` | 线路格式、帧重组的取舍、每个拒收条件 |
|
||||
| `armor/README.md` | 装甲板接线表、血量规则、上报、C3 的 RMT/ADC 踩坑 |
|
||||
| `desktop/README.md` | 两套页面、视频通道路径、两道闸门、密码模型、录制与排查 |
|
||||
|
||||
## 装甲板(已实现)
|
||||
|
||||
`armor/` 是一套独立的 Rust workspace(目标 **ESP32-C3**,RISC-V):
|
||||
|
||||
- `armor-protocol` —— 规则层:血量(100 起、每击 −3)、每板独立的 50 ms 受击冷却、
|
||||
受击后**只有那一路**灯带全黑 50 ms、按键 FSM、上报编码。纯计算,主机上 22 个测试。
|
||||
- `armor-firmware` —— 固件:四路压电片 40 kHz ADC 连续采样、四路 WS2812(C3 只有一个
|
||||
RMT 通道可用,靠 `rmt_tx_switch_gpio` 轮流驱动)、NVS、BOOT 键、WiFi + UDP。
|
||||
|
||||
硬件是"一主三从":三块从板里没有 MCU,各自只有一路灯带和一路压电片,直接接线到主板。
|
||||
接线表与实现细节见 [`armor/README.md`](armor/README.md)。
|
||||
|
||||
```sh
|
||||
scripts/build_armor.sh # 编译(WIFI_SSID/WIFI_PASS/ARMOR_TEAM 取自 .env)
|
||||
scripts/flash_armor.sh # 编译 + 烧录 + 串口日志
|
||||
python3 scripts/armor_probe.py 15 # 不开界面,单看板子在广播什么血量
|
||||
```
|
||||
|
||||
桌面端侧:`desktop/core/src/armor.rs` 的 `ArmorHealthStore` 监听 5601,按 `(team, index)`
|
||||
存最后一次读数,四条都在 1.5 s 内收到才显示——**没有数据时显示占位符而不是 0**,
|
||||
0 看起来像真值,现场看到会以为对方已经没血了。界面上的血量画在画面角上(一条分 5 格的
|
||||
平行四边形,20 点血一格),归零不写"已击毁/损毁",而是由图传中央的「胜利 / 击败」横幅表达
|
||||
(见 [`desktop/README.md`](desktop/README.md));数字变化也会打进客户端日志。
|
||||
|
||||
同一块板子还会被单独记下来(帧头里的 MAC 派生 id),所以两端都能列出"场上有哪几块装甲板"
|
||||
并选一块绑定——命中反馈也是从这个 id 上归位的。**这不需要改固件**:主板一直在用
|
||||
`encode_from(sender_id)` 把自己的 id 打进帧头。
|
||||
- [CHANGELOG.md](CHANGELOG.md):改动记录与历史修复。
|
||||
- [HARDWARE.md](HARDWARE.md):接线、内存、Flash 与驱动限制。
|
||||
- [DEVELOPMENT.md](DEVELOPMENT.md):镜像导出、测试、协议与性能测量。
|
||||
- [PERFORMANCE.md](PERFORMANCE.md):保留 TUN 的图传排查、参数取舍与实际测试结果。
|
||||
- [STABILITY.md](STABILITY.md):限帧、灰度与温度减载,长时间运行测试。
|
||||
- [REALTIME.md](REALTIME.md):阻塞时取最新帧、超期丢弃和客户端积压处理。
|
||||
- [POWER.md](POWER.md):15 dBm 回退、功率与距离的关系,以及发送拥塞证据。
|
||||
- [LATENCY.md](LATENCY.md):当前约 30 fps 配置、Wi-Fi 重连及实际画面链路延迟对照。
|
||||
- [protocol/README.md](protocol/README.md):UDP 线路格式及帧重组。
|
||||
- [armor/README.md](armor/README.md):装甲板固件与规则。
|
||||
- [desktop/README.md](desktop/README.md):桌面端配置、录制与排查。
|
||||
|
||||
Reference in New Issue
Block a user