sloray
|
98dc87ec6a
|
ci: 构建客户端 workflow(Windows / Linux / macOS)
构建客户端 / 检查(类型 + 单元测试) (push) Successful in 7m59s
构建客户端 / Linux x64(deb + AppImage) (push) Failing after 10m45s
构建客户端 / macOS 通用(.app + .dmg) (push) Skipped
构建客户端 / Windows x64(NSIS 安装包) (push) Skipped
触发:push 到 main(只认 desktop/ 与 protocol/ 的改动)、v* tag、PR、手动。
产物:Linux deb + AppImage、Windows NSIS 安装包、macOS 通用 dmg,各自传 artifact。
几个不是随手写的点:
- 三个平台都显式 --target / CARGO_BUILD_TARGET。desktop/.cargo/config.toml 里钉着
aarch64-apple-darwin,不覆盖的话 Linux/Windows 的 runner 会去编 macOS 的目标,
然后在依赖深处以 libc 找不到 MAP_FAILED 这类形式失败。
- protocol 的测试写 cargo +stable:protocol/ 没有自己的 rust-toolchain.toml,会被
根目录那个 `esp` 接住,而 CI 上没有那个工具链(espup 要下几百 MB)。
- Windows 与 macOS 两个 job 默认不跑(本仓库目前只有 Linux runner),要用仓库变量
ENABLE_WINDOWS / ENABLE_MACOS 打开;没有 runner 时它们只会一直排队。
- cargo 只缓存 registry、不缓存 target/:容器化的 runner 每次都是干净的,拉几 GB
的 target 缓存常常比重新编译还慢。
- 守门 job 先跑 tsc + vite build + protocol/core 的单元测试,三个构建都 needs 它。
本地(Windows)实测:同一条 pnpm tauri build --target x86_64-pc-windows-msvc
--bundles nsis 真的编出了 src-tauri/target/x86_64-pc-windows-msvc/release/bundle/
nsis/cam-desktop_0.1.0_x64-setup.exe(2.97 MiB),也验证了 --target 确实盖住了
.cargo/config.toml;protocol 15 个测试、cam-core 80 个测试、tsc、vite build 都过;
三个 Cargo.lock 与 --locked 一致;workflow 本身过 actionlint(带 shellcheck)。
Linux 与 macOS 的打包只能在你们的 runner 上验,第一次跑完把日志贴回来。
|
2026-10-15 18:41:31 +08:00 |
|
Horldsence
|
a93ae065cf
|
desktop: 双方血条(角上平行四边形)、归零即击败、去掉受击抖动、裁判端可改队名
界面改动都在浏览器预览里逐条核对过(DOM 尺寸 + 视觉),截图留在 desktop/screenshots/
(该目录不进仓库)。
1. 客户端显示对手的血量
快照里两端都给两支队伍(顺序固定为队伍号 0/1),界面按 `team` 取那一格,不再靠
数组下标。顺带修掉一个真 bug:用户端"对手那一格"以前会走 `armor_id`——那是我自己
绑定的主板,于是右上角显示的是自己的血,比不显示更糟。现在只有"自己那一块"才用
`armor_id`;同理 `assigned_armor` 里的裁判端槽位指定对用户端不再生效。
2. 血条改到画面两个上角,平行四边形,一条分 5 格
左上角自己、右上角对手(裁判端两格各贴自己的角,读法一致)。整条 skewX(-15deg),
右条再 scaleX(-1) 镜像,两条斜边朝外对称、**都从靠中心的一侧开始灭**。一格 20 点血。
条挂在绝对定位的一层上并 pointer-events:none(那一层盖着画面,而画面要能滚轮缩放、
拖动),倍率与复位徽标因此从上沿挪到下沿。未接入显示"装甲板未接入"而不是 0。
注意:不要在 JS 里再反转一次数组——那会和 CSS 的 scaleX(-1) 互相抵消,表现是右条
从外侧开始灭(代码里写了注释钉住这件事)。
3. 受击画面不抖动
删掉整幅图(含瞄准器)的位移与 shakeOffset。`HitFeedbackConfig::shake` 字段保留但
**不再生效**:配置文件是 deny_unknown_fields 的,删字段会让所有写过 shake = true 的
配置解析失败,而配置解析失败等于程序起不来。默认值改 false,设置面板去掉开关,
并有单测钉住"旧配置读得进来、默认不抖"。
4. 血量归零算击败,不显示"损毁"
血条上不再出现"已击毁/损毁",归零的结论放到图传中央:胜利 / 击败 / 平局。结论在
Rust 侧算(snapshot::resolve_outcomes,4 条单测),规则有意不对称——**输只看自己**
(自己归零就是输,哪怕对手主板掉线),**赢要看两边**(对手归零且自己还有血),
两边同时归零是平局。它一直显示到血量被重置,不做几秒淡出,否则截图与回放里就再也
没有"这局结束了"的痕迹。
5. 裁判端可以改队伍名
两格画面各自下面一个输入框:回车/失焦生效、Esc 取消、超 12 字截断、留空回落到
「队伍一 / 队伍二」。命令是 `set_team_name(team, name)`,**一次只改一边**——传整个
两条的数组就成了"读两条、改一条、写两条",而读来的那两条来自最多 200ms 前的快照,
紧接着改另一边会把上一次的改动覆盖回去。规范化统一在 `config::sanitize_team_name`
(合并空白、按字符截断、留空回落),并同样过密码闸门(裁判端本来就没有密码)。
名字只有一个来源(快照的 `teamNames` / DTO 的 `name`):装甲板表、槽位标签、设置
面板与"等待图传"的占位文字都跟着改,不再有第二处自己拼"队伍一/队伍二"。
测试:core 80(69→80)、src-tauri 30、protocol 15;`pnpm build`(tsc + vite)通过。
|
2026-10-14 20:48:12 +08:00 |
|
Horldsence
|
fd71b83e3a
|
reference: 早先那版 C 写的装甲板固件(ESP-IDF 工程)
`armor/` 是它的 Rust 改写版,这一份留着当参考:ADC 阈值的 2000 就是从它标定出来的
(`.env.example` 里那句 "same as the reference firmware" 指的就是它),而且它的 README
记着实测设备与 MAC,换硬件时是有用的现场记录。
只收了 20 个源文件。它自带的 `.gitignore` 已经排掉 `build/`(211 MB 的 ESP-IDF 构建产物)
与生成的 `sdkconfig`,所以仓库里不会多出这些东西。
两处顺带处理:
- `reference/armor/.git` 是一个**空的**仓库壳(`git init` 过,零提交、零 remote、零 ref),
而外层 git 正因为它的存在而拒绝索引 `reference/` 下的任何文件
(`error: 'reference/armor/' does not have a commit checked out`)。它被挪到
`desktop/.dev/backup/reference-armor-git`(可逆,且该目录已被忽略),不是删除。
- 它的 README 指着 `../armor-platformio-backup/`,那份备份并不存在。改成明说"没有进仓库",
免得以后有人照着找一个不存在的目录。
另外把 `/docs/` 加进 .gitignore:那是工作区里另起的一个 Astro 文档站,与本仓库无关。
|
2026-10-14 19:45:15 +08:00 |
|
Horldsence
|
a30eb3200a
|
armor: ESP32-C3 装甲板固件(一主三从)+ 烧录脚本,之前一直没提交
这一块代码本来就写好了、根 README 里也按"已实现"描述,但一直没有进仓库,
只存在于工作区里。同一个提交里带上 `.env.example` 的两个新变量(ARMOR_TEAM、
ARMOR_THRESHOLD)——它们的说明文字就是为这套固件写的。
内容:
- armor-protocol:规则层(血量、每板独立 50ms 受击冷却、受击后只有那一路灯带全黑、
按键 FSM、上报编码)。纯计算,主机上 22 个测试,已跑通。
- armor-firmware:四路压电片 ADC 连续采样、四路 WS2812(C3 只有一个 RMT 通道可用,
靠 rmt_tx_switch_gpio 轮流驱动)、NVS、BOOT 键、WiFi + UDP 广播。
- scripts:build_armor.sh / flash_armor.sh / armor_probe.py(不开界面单看广播)。
armor/ 是独立 workspace(目标 RISC-V 的 ESP32-C3),与外层 Xtensa 的图传固件互不相干,
各自 .cargo/config.toml 就是这条边界。构建产物由根 .gitignore 的 `/armor/target/` 覆盖,
本次没有把任何 target/ 或 build/ 下的东西加进来。
顺带把 `.DS_Store` 加进 .gitignore。
|
2026-10-14 19:43:14 +08:00 |
|
Horldsence
|
7fdebe3ec7
|
desktop: 修两处小问题(预览模式的命中节奏、设置面板里漏出来的 **)
- 浏览器预览的掉血脚本写成"打空就重置,然后按 to >= from 判断"——两块板子都打空之后
每一次都是重置,命中再也不出现,盯着界面等闪红会等到超时。现在打空那一拍只重置、
其余每一拍都出命中。
- 设置面板里"偏移存的是**画面比例**"直接写进了 JSX,`**` 原样显示出来了。JSX 不渲染
Markdown,改成 <span class="em">。
顺带把界面截图落到 desktop/screenshots/(不进仓库):这个环境里 Page.captureScreenshot
不可用(空白页也超时),改用 Page.printToPDF + sips 转 PNG;打印路径不渲染 box-shadow
的模糊半径(会变成实心块),所以 .dev/shoot.js 在拍之前把它去掉。
|
2026-10-14 19:37:59 +08:00 |
|
Horldsence
|
4f7a142a39
|
desktop: 用户端与裁判端两套页面(Tauri v2 + React),删掉 Slint 客户端
界面部分。后端在前一个提交里,这一提交把它接上,并删掉 client/。
用户端:
- 底部胶囊血条(宽度/数字/阈值/脉动可配),未接入时显示占位说明而不是 0;
- 瞄准器画在 canvas 上并与画面共用同一次变换计算——位置跟着缩放走、大小不跟,
像真瞄具;偏移校准存的是画面比例而不是像素,换分辨率不会失效;
- 命中反馈:整屏闪红 + 抖动 + 准星旁的伤害数字。大号数字刻意不放画面正中:
第一版放了,正好压住瞄准点。掉血事件由相邻两次读数之差派生(主板只报"现在多少血");
- 摄像头/装甲板下拉绑定、队伍号、镜像;改动要密码,退出/关闭也要。
裁判端:
- 两路等宽(flex: 1 1 0 + min-width: 0)各自独立缩放;
- 装甲板发现列表:id、地址、自报队伍、血量、四条齐不齐、静默多久、分到哪个槽位;
- 截图 PNG + ffmpeg 录 mp4,界面内回放(资源协议白名单外的目录给"用系统播放器打开"的退路)。
密码:Argon2id PHC,首启强制设置,会话内解锁一次。执行点在 Rust 侧命令里,界面上的
解锁对话框只是入口。故意不做 --camera-id 这类命令行覆盖:那是一条不用密码就能改绑的路。
其它:
- 配置换成 serde + toml,字段名统一 camelCase(磁盘=IPC=TS),仍然拒绝未知键名,
原子写;旧客户端配置首次启动导入并备份成 .bak(只在默认路径下做);
- 演示模式(CAM_DEMO=1)合成协议数据报喂进真实接收路径,没有板子也能把整条链路跑起来;
- 仅 dev 的浏览器预览(?preview=referee),两重门保证不进正式产物(已验证);
- CSP 收紧成白名单,验证方式是"前端能不能连上后端":clients 从 0 变成 1 同时证明
JS 加载、IPC、WebSocket、React effect 四件事。
删掉 client/:它的功能全部被 desktop/ 覆盖,且其中有几处真 bug 在新实现里改掉了
(槽位分配会让两格显示同一块板子、帧率窗口基线、密码哈希截断被当成密码错、录制文件名
同秒互相覆盖)。注意 client/ 里那几处**未提交**的改动随之丢弃,历史里的版本还在。
测试:protocol 15、cam-core 69、src-tauri 30;前端 tsc + vite build 通过。
|
2026-10-14 19:28:49 +08:00 |
|
Horldsence
|
a89d7ae5fb
|
desktop: Tauri v2 后端 + 共享网络层(用户端/裁判端双身份)
把桌面端从 Slint 迁到 Tauri v2,后端先落地,界面待接。拆成三层:
- desktop/core(cam-core,67 测试):从旧 client/ 抽出的非界面部分。UDP 接收按
发送方重组、装甲板发现与血量、帧率窗口、槽位分配、配置与密码。
- desktop/src-tauri(27 测试):Tauri 后端。回环 WebSocket 推二进制 JPEG、
每 200ms 一份状态快照、密码闸门、截图、ffmpeg 录制 mp4、演示数据源。
- protocol:新增 ArmorHealth::decode_from,把帧头里的 sender_id 交出来——
装甲板一直带着 MAC 派生 id 在广播,旧解码路径把它丢了,所以"装甲板下拉可选"
不需要改固件。
几个改对了的旧问题:
- 槽位分配:旧实现只排除"槽位自己原来那个 id",于是队伍一的板子一离线,它的槽位
会去拿队伍二那块,两格显示同一块板子。现在候选要排除另一个槽位占着的 id。
- 帧率窗口的基线:掉线又回来的板子带着一个很大的累计帧数回来,从 0 起算会报出
几百 fps。改成 seeded + 用"已结算窗口数"判断有没有读数。
- 密码哈希:password-hash 能解析 `$argon2id$broken` 这种截断串,于是"配置坏了"
会被读成"密码错了"。现在要求盐与摘要都在。
- 录制文件名:同一秒停一次再开一次会用同一个名字,ffmpeg 的 -y 静默覆盖上一段。
现在先收尾上一段再开新的,并且重名往后排号。
- 两道闸门:帧的限制与发现列表的限制分开。设置界面需要列候选摄像头,但**画面**
不需要——旧版把两件事绑在一道闸门上,只能在"能列候选"和"挡住对手"之间二选一。
测试:protocol 15、cam-core 67、desktop 27,其中录制那条真的起 ffmpeg 并用
ffprobe 读回帧数与分辨率(只断言"文件存在"会漏掉空容器)。
|
2026-10-14 19:14:16 +08:00 |
|
Horldsence
|
eafa112959
|
文档:写防作弊、缩放布局、槽位语义
client/README.md 新增三节:
防作弊 说明为什么必须在**接收端**挡而不是界面里隐藏——Hello 广播已经带着
MAC 派生 id 和 IP,对手 tcpdump 一下就有,而数据本来也已收下解码。同时
明确写出这一版挡不住什么:对手知道 IP 后可以自己发 Subscribe,固件会向
任何订阅者推流。要真正封住得回固件加配对 token。
缩放 image-fit 必须跟着 zoom 在 contain/fill 之间切,否则放大不会改变画面;
缩回 1.0 要归位,否则下次放大从偏移开始像跳了一下。
槽位 记下"按 id 顺序对号入座"这个做法错在哪,以及为什么会出现画面左右互换。
根 README 排查表加两条:两格宽窄不一的检查点,以及放大后画面没变清晰的原因。
|
2026-10-05 21:45:24 +08:00 |
|
Horldsence
|
3631a037d8
|
client: 槽位对应队伍,不再按 id 顺序对号入座
裁判端两个槽位的语义应该是"队伍一 / 队伍二",而不是"第 N 块发现的板子"。之前
两者混为一谈,出现两个都会被现场误读成故障的现象:
1. **两块槽位显示同一块板。** 每轮都从 `online_cameras()` 取前两个 id 填满两
个槽位,于是只有一块板在场时两边一模一样。裁判会以为两队的图传串了。
2. **板子掉线时画面左右互换。** 低 id 的板子一离线,候选列表缩短,后面那块就
跳到前面的槽位——同样像串线,而实际上只是排序。
规则改成:每轮最多消费一个候选 id;已填且仍在线的槽位不动;掉线的槽位保留它的
id 并显示"离线",等它回来时还在原位。
空槽的提示文案也改准:左槽"等待队伍一的图传"、右槽"等待队伍二的图传",而不是笼
统的"未收到画面"。
顺带修两个假数字:`frames_shown == 0` 时 `stats.width`/`height` 还是默认值 0,界
面上显示成 `0x0`——一个看起来像分辨率的假分辨率;fps 显示 `0.0` 读起来像"板子
卡死了",而真相是"还没收到"。状态行已经写了"已连接,等待画面…",不该再补一个矛
盾的数字。
关于"别把刚掉线的板子又立刻选回同一个槽位"那条过滤:它只在槽位掉线时起作用,
且会跳过当前 id 去找下一个候选,槽位仍然有机会回来。实测唯一那块在推流的板子正
常占用一个槽位,另一槽保留着在线但不推流的真板子——符合预期。
|
2026-10-05 21:45:24 +08:00 |
|
Horldsence
|
cd8344c698
|
client: 修裁判端两格不等宽,加滚轮缩放与拖动平移,启动即最大化
**塌缩问题**(用户报的现象):裁判端窗口放大时,左边那格吃掉约 55% 宽度,右边
只有 45%;空的那一侧更极端时会被压成一条缝。
原因不是缺 stretch,而是 `HorizontalLayout` 先按内容的**最大**需求分配、剩下
的才按 stretch 分。左格写着"已连接,等待画面…"(8 个字),右格"未收到画面"
(5 个字),左格就多要十几个像素。而没有画面的一格最小宽度接近 0,可以被压没。
三处一起改:
1. 占位文字改成绝对定位(给出 x/y),不再参与父级尺寸计算。文字长短本不该
决定画面大小。
2. 两格统一 `preferred-width: 480px` + `horizontal-stretch: 1`,需求变成相同,
于是精确平分。`min-width: 320px` 兜底,保证任何一侧至少有一个能看画的宽度。
3. 缩放指示与复位按钮也绝对定位。否则它们一出现就改变这一格的尺寸需求,把
不等宽重新引进来——而且是在用户放大之后才出现,最难排查。
**缩放**:Slint 的 `Image` 没有缩放因子属性,所以把它的 `width`/`height` 按 zoom
放大后放进 `clip: true` 的容器,靠 offset 平移。滚轮改 zoom、拖动改 offset、双击
复位、缩回 1.0 时自动归位(否则下一次放大会从偏移位置开始,看起来像"跳了一下")。
`image-fit` 要跟着 zoom 切:`contain` 在 zoom=1 时保证完整可见且不变形,`fill`
才让画面跟着放大——只用 contain 的话放大不会有任何视觉变化。
**启动即最大化**:Slint 没有"最大化"这个状态,但有 `set_maximized`,它铺满工作区
且保留菜单栏和 Dock(真全屏 `set_fullscreen` 会盖掉它们,裁判终端要切去别的窗口
核对数据)。必须在 `show()` 之前调用,之后调用会被后端当成"用户点了最大化按钮"。
抽出 `VideoPane` 与 `StatusBar` 两个组件给两种身份复用。两者都不加 `export`——
它们只在 .slint 内部使用,加了会触发"doesn't inherit Window. No code will be
generated for it"的警告。
Slint 的两处写法记在这里,都是编译器试出来的:
- `TouchArea` 的 `pressed` 是**属性**不是回调,写 `pressed => {}` 编译不过。
拖动要用 `pointer-event(e)` 配 `PointerEventKind.down` / `.move`。
- `EventResult.accept` 要写全限定;`accept` 单独写无法解析。
|
2026-10-05 21:28:32 +08:00 |
|
Horldsence
|
a16ad8595e
|
client: 队伍端 release 只接收绑定的那一块板子
对抗赛里两队都用这套系统看自己小车的图传。需求是防止对手开第二个窗口连到另一
队的板子。
**关键是这必须做在接收端,而不是界面里。** 我先按"只在 release 下不显示 id 和
IP"理解,但那样等于没做:广播的 Hello 里就有 MAC 派生的 id 和 IP,对手
tcpdump 一下全都知道,而数据本来也已经收下并解码了。隐藏一行文字不阻止任何事。
所以改为 `Receiver::restrict_to`:队伍端 release 且已绑定时,只重组、只解码、
只统计绑定的那一块板子。对手的板子即使把流推到了本机端口(IP 已泄露、端口是
公开常量),数据也不会进入任何管线。
四种情况不限制:裁判端(中立第三方,本来就要两块)、debug 构建(开发时需要
看到全场)、队伍端未绑定(选择界面需要列出候选)、命令行临时覆盖 camera_id。
`--list` 在 release 队伍端直接报错:它会打印场上所有板子的 id 和 IP,正是要防
的那件事。与其列出来再叮嘱"别看",不如不给。
`TEAM_DIAGNOSTICS = cfg!(debug_assertions)` 门控界面上的 fps / 丢帧 / 数据报数
等遥测和 stdout 的诊断日志。裁判端不受影响——它是中立方,双方遥测对判罚有用。
选择界面保留:现场换机器时还要用它重新绑定。数据侧的闸门已经足够。
顺带修一个设计上就错的判断:`is_online()` 看的是 `last_seen`,而它此前只由 Hello
更新,于是出现"画面每秒刷 22 帧、状态栏却显示离线(橙色)"的矛盾状态。正在推
流本身就是最可靠的存活证据,所以收到视频帧也刷新时间戳。
|
2026-10-05 21:27:36 +08:00 |
|
Horldsence
|
7b6593bb75
|
文档:按 UDP 多端架构重写根 README 与 client README
三份文档此前还是 UDP 之前的内容(旧 HTTP 架构、单路、无身份概念),与实现不符。
根 README 是总入口:拓扑(两块板不区分队伍,"哪一边"由客户端决定)、实测性
能对比、发现协议、身份派生理由、构建烧录、硬件坑、排查表。
client README 覆盖:两种身份对应的两个组件、命令行、绑定流程与配置格式、三个
线程的分工、Slint 的硬约束(Image/Weak 不是 Send、ModelRc 没有 From<Vec<T>>、
基础元素不能挂 callback)。
写的时候逐条核对了代码,发现并修正了几处"我以为是这样":
- WIFI_AUTH 一直能用但从未进文档,.env.example 里也没有
- 两端各有超时淘汰(原以为没有),值不同:固件 6 秒丢订阅者、客户端 60 秒
才从界面列表移除。固件还有 MAX_PEERS=4 的上限,也补上了
- 两处注释写死 app 分区在 0x40000,实际由 flash.sh 自动挑选
- 实测数字重测:22 fps / 0 丢帧 / 平均帧 22 kB / 350 包每秒
也把两条踩过的坑写进去了,因为它们都属于"看起来是 A,实际是 B":帧率必须按
计数除以墙钟(累计值除窗口值会报出单调上升的假数字,这个错误出现过两次),
以及探到链路异常先怀疑测量工具。
|
2026-10-05 19:13:52 +08:00 |
|
Horldsence
|
c7f42a1979
|
scripts: 修 udp_probe.py 的重组与 fps 两处测量错误
探测脚本是诊断工具,测错比不测更糟——它给出的数字会被当成结论。
**重组状态必须按 (sender_id, frame_id) 存。** 原先只按 sender_id 存,于是丢片
之后下一帧的分片会接着凑上一帧的计数,凑够了交出去一张混着两帧的 JPEG,表现为
"JPEG 不完整"。修完:14 秒 132 帧,0 张坏帧。
换帧时被放弃的半成品单独计为"被新帧取代",不并入"JPEG 不完整"——它们本来就
不完整,混在一起会把两种情形都算成链路有问题。
**分窗 fps 的分子分母必须同量纲。** 原先用累计帧数除以窗口时长,于是报出 39、
61、79 这种随时间单调上升的假数字,看起来像板子越跑越快。改成窗口增量除以窗
口时长后稳定在 22,也就是传感器上限。
顺带修正两处与代码不符的注释:
partitions.csv / sdkconfig.defaults
两处都写死 app 分区在 0x40000,实际是 scripts/flash.sh 自动挑选并写回,
当前为 0x060000。补上 flash.sh 会重编译并复验的原因(重编译会让镜像位移,
所以偏移无法是常量)。
.env.example
补 CAM_VSYNC_INVERT 和 WIFI_AUTH。WIFI_AUTH 一直能用但没进文档:通常不用
设,扫描会报告 AP 的真实认证方式;只有扫不到时(隐藏 SSID)才需要指定,
指定后只试这一种,不成功直接报错而不是挨个试 10 秒——挨个试等于在猜一个
本该已知的值。
|
2026-10-05 19:13:37 +08:00 |
|
Horldsence
|
7be8d7d2ba
|
protocol: 更正上一个提交的归因——坏帧不在这里
上一个提交(1c65eaa)说"34 张 JPEG 不完整是重复分片凑数导致的",并说修完从
34 降到 1。**这个结论是错的**,是我归因到了错误的地方。
实测复查:一到两个订阅者、20 秒 2000+ 个分片,重复投递数为 0(固定端口和
临时端口两种订阅方式都试过)。这条链路上根本不产生重复。
34 张坏帧的真实来源是探测脚本自己:scripts/udp_probe.py 的重组状态只按
sender_id 存,不按 frame_id 存。于是某帧丢了分片之后,下一帧的分片会接着往
同一个计数器里凑,凑够了就交出去一张混着两帧的 JPEG。
FrameAssembler 一直按 frame_id 隔离(换个 frame_id 就重置),所以它没这个
问题——脚本原先跟它不一致,而我又把脚本的输出当成了 crate 的行为。
本次改动:
protocol/src/lib.rs
位图去重保留,理由改为正确性而非正在发生的故障,并在注释里写明实测重复
为 0、以及真正的坏帧来源。删掉一个死变量 `last`。行为不变。
protocol/README.md
"重复分片必须去重" 一节改写:说清防护的定位是"哪天真的重复了"。
新增"按 frame_id 隔离,换帧即放弃"一节,讲清不隔离的具体后果。
记下探到坏帧时先怀疑测量工具。
值得记的不是位图本身,是那个错误归因:34 张坏帧配上"帧率正常、无报错线索"
这个症状,太像一个真的丢包/重复 bug 了,于是一路写到了注释、文档和提交说明
里。以后探到链路异常,先确认测量工具和被测对象说的是同一件事。
|
2026-10-05 19:13:24 +08:00 |
|
Horldsence
|
1c65eaabc6
|
protocol: 修复重复分片能凑出一张坏帧
`seen >= expected` 判断分片是否收齐是错的:UDP 会重复投递,一个丢失的
分片可以被另一个包的副本凑数,于是计数达标而缓冲区里留着空洞,交出去
一张尾部是 0 的 JPEG。
这个 bug 很隐蔽——帧率完全正常,没有任何报错线索,只是偶尔几张图解不
出来。实测 10 秒内出现 34 张这样的坏帧。
改为按分片下标记录是否收到,重复的直接跳过且不计数;再显式确认最后一个
分片确实到过。实测坏帧降到 1(板子启动瞬间的残帧),完整帧率不变。
补两个测试覆盖它——原有测试没有一个会重复发分片,所以这个 bug 一直没被
发现:
- a_duplicate_cannot_complete_a_frame_with_a_hole
- a_gap_that_is_never_filled_never_yields_a_frame
同时更新 scripts/udp_probe.py:它有同一个 bug,所以测不出修复效果。它还
把 Subscribe 的 role 字节多填了 12 个零字节,固件按偏移严格读取因而拒收,
表现为"板子在线但 0 clients subscribed"。脚本的意义正是独立核对线路格式,
所以这些偏移是手写的,改协议时必须同步。
另删 scripts/pick-flash-pad.sh:链接期 padding 已被证明移不动
.flash.text 内部,flash.sh 现在会自动挑选避开坏字节的 app 偏移。
|
2026-10-05 18:54:35 +08:00 |
|
Horldsence
|
4f893ce140
|
ESP32-S3 摄像头图传:固件 + Slint 客户端 + 共享协议
两块 ESP32-S3-WROOM-1-N16R8 图传板(DVP 接口接 OV2640/OV3660/OV5640),
向队伍客户端与裁判终端推 UDP 图传。固件是跑在 ESP-IDF 上的 Rust,用官方
esp32-camera 组件采集;客户端是 Rust + Slint 桌面程序。
三个 crate 各自独立构建:
. firmware,交叉编译到 Xtensa
protocol/ 线路格式与帧重组,两端共用,无 ESP-IDF 依赖,有单元测试
client/ Slint 桌面客户端
视频走 UDP 而非 TCP:实测传感器产出 22 fps,TCP MJPEG 只送出 1.9 fps,
且只占用链路能力的 1%——链路是空的,帧堵在队头阻塞后面等一个实时画面
等不起的重传计时器。改为独立数据报、不重传、缺片丢整帧后达到 20.8 fps。
发现是双向的:板子每 2 秒广播 Hello,客户端广播 Subscribe,板子据此定向
推流。板子身份由 MAC 派生,重启不变,所以客户端可以把绑定存进配置。
裁判端同时收两块板子(每个发送方一个重组器),队伍端绑定其中一块并只显示
自己那一边的血量。装甲板接口已预留(ArmorSource trait + ARMOR_PORT),
当前无数据源,界面显示占位符。
vendor 了 esp32-camera 2.1.7:组件注册表的版本求解器找不到它,且需要打一处
补丁(见 components/esp32-camera/target/esp32s3/ll_cam.c)。
.gitignore 三条 target/ 都锚定在根目录,因为 protocol/ 与 client/ 各自也是
独立 crate;而 components/esp32-camera/target/ 是上游的按芯片分目录,里面是
源码,必须保留。
|
2026-10-05 18:22:19 +08:00 |
|