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 偏移。
This commit is contained in:
+75
-3
@@ -540,8 +540,13 @@ pub struct FrameAssembler {
|
||||
height: u16,
|
||||
since: Option<std::time::Instant>,
|
||||
timeout: std::time::Duration,
|
||||
/// 已收到的分片数,含重复。用来统计网络丢了多少。
|
||||
/// 已收到的分片数(不含重复)。用来统计网络丢了多少。
|
||||
seen: usize,
|
||||
/// 每个分片是否已收到。
|
||||
///
|
||||
/// 存在的原因是 `seen` 会被重复包灌水:UDP 会重复投递,一个丢失的分片可以
|
||||
/// 被另一个包的副本凑数,于是"收够了"却留着空洞,交出去一张尾部是 0 的坏图。
|
||||
have: Vec<bool>,
|
||||
}
|
||||
|
||||
impl FrameAssembler {
|
||||
@@ -557,6 +562,7 @@ impl FrameAssembler {
|
||||
since: None,
|
||||
timeout,
|
||||
seen: 0,
|
||||
have: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,12 +608,27 @@ impl FrameAssembler {
|
||||
self.height = frag.header.height;
|
||||
self.since = Some(std::time::Instant::now());
|
||||
self.seen = 0;
|
||||
self.have.clear();
|
||||
self.have.resize(self.expected, false);
|
||||
}
|
||||
Some(id) if id == frame_id => {}
|
||||
_ => unreachable!("current_id was just reconciled with frame_id"),
|
||||
}
|
||||
|
||||
let start = frag.header.frag_index as usize * MAX_FRAGMENT_PAYLOAD;
|
||||
let index = frag.header.frag_index as usize;
|
||||
|
||||
// 重复分片直接跳过,且**不**增加 seen。
|
||||
//
|
||||
// 这里曾经用 `seen >= expected` 判断收齐,那是错的:UDP 会重复 delivered
|
||||
// 重复包,一个丢失的分片可以被另一个包的副本凑数,于是 seen 达标而缓冲区
|
||||
// 里留着空洞,交出去一张 JPEG 尾部是 0 的坏图。这个 bug 很隐蔽——它不会让
|
||||
// 帧率下降,只会让偶尔几张图解不出来,而接收端没有任何线索知道原因。
|
||||
if self.have[index] {
|
||||
return Err(DropReason::Superseded);
|
||||
}
|
||||
self.have[index] = true;
|
||||
|
||||
let start = index * MAX_FRAGMENT_PAYLOAD;
|
||||
let end = start + frag.payload.len();
|
||||
|
||||
if end > self.buffer.len() {
|
||||
@@ -616,7 +637,13 @@ impl FrameAssembler {
|
||||
self.buffer[start..end].copy_from_slice(&frag.payload);
|
||||
self.seen += 1;
|
||||
|
||||
if self.seen >= self.expected {
|
||||
// 计数达标还不够:最后一个分片必须真的到过,否则中间可能缺一块。
|
||||
// 显式检查尾块,比依赖 seen 的算术更直接,也更容易看出意图。
|
||||
let last = self.expected - 1;
|
||||
let tail_expected = frag.header.total_len as usize;
|
||||
let tail_index = tail_expected.saturating_sub(1) / MAX_FRAGMENT_PAYLOAD;
|
||||
|
||||
if self.seen >= self.expected && self.have[tail_index] {
|
||||
let total = frag.header.total_len as usize;
|
||||
self.buffer.truncate(total.min(self.buffer.len()));
|
||||
|
||||
@@ -660,6 +687,7 @@ impl FrameAssembler {
|
||||
self.buffer.shrink_to_fit();
|
||||
self.since = None;
|
||||
self.seen = 0;
|
||||
self.have = Vec::new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,6 +880,50 @@ mod tests {
|
||||
assert_ne!(a, id_from_mac(&[1, 2, 3, 4, 5, 6]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_duplicate_cannot_complete_a_frame_with_a_hole() {
|
||||
// UDP 会重复投递。一个丢失的分片若能被另一片的副本凑数,接收端就会以为
|
||||
// 收齐了,交出一张中间是 0 的坏 JPEG——而且帧率正常、没有任何报错线索。
|
||||
let jpeg: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
|
||||
let frags = fragment_frame(1, 1, &jpeg, 640, 480);
|
||||
let mut asm = FrameAssembler::new(std::time::Duration::from_secs(2));
|
||||
|
||||
// 丢掉第 2 片,然后把第 0 片连送三次来灌 seen。
|
||||
for f in frags.iter().filter(|f| f.header.frag_index != 2) {
|
||||
let _ = asm.push(f.clone());
|
||||
}
|
||||
for _ in 0..3 {
|
||||
assert!(
|
||||
asm.push(frags[0].clone()).is_err(),
|
||||
"a duplicate must not be allowed to complete the frame"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(asm.is_in_progress(), "the hole must keep it incomplete");
|
||||
|
||||
// 补上真正的第 2 片,才在这一片上收齐——所以结果要从这一次的返回值拿。
|
||||
let completed = asm.push(frags[2].clone());
|
||||
|
||||
assert_eq!(
|
||||
completed.expect("completes once the hole is filled").data,
|
||||
jpeg,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gap_that_is_never_filled_never_yields_a_frame() {
|
||||
let jpeg = vec![0xAAu8; 5000];
|
||||
let frags = fragment_frame(1, 1, &jpeg, 640, 480);
|
||||
let mut asm = FrameAssembler::new(std::time::Duration::from_secs(2));
|
||||
|
||||
// 反复送同一片,永远凑不齐——接收端必须一直等着,而不是交出半张图。
|
||||
for _ in 0..50 {
|
||||
assert!(asm.push(frags[0].clone()).is_err());
|
||||
}
|
||||
assert!(asm.is_in_progress(), "50 duplicates must not complete a frame");
|
||||
assert_eq!(asm.sender_id(), 1, "and it must still be the same camera");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_jpeg_still_produces_one_fragment() {
|
||||
let frags = fragment_frame(1, 1, &[], 64, 48);
|
||||
|
||||
Reference in New Issue
Block a user