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 帧、状态栏却显示离线(橙色)"的矛盾状态。正在推 流本身就是最可靠的存活证据,所以收到视频帧也刷新时间戳。
This commit is contained in:
+73
-1
@@ -185,6 +185,17 @@ impl Discovery {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 只刷新存活时间戳,不改地址或固件版本。
|
||||
///
|
||||
/// 收到视频帧时调用:正在推流本身就是最可靠的存活证据。
|
||||
fn touch(&self, id: u32) {
|
||||
let mut cameras = self.cameras.lock().expect("cameras mutex poisoned");
|
||||
|
||||
if let Some(info) = cameras.get_mut(&id) {
|
||||
info.last_seen = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
fn note_hello(&self, id: u32, addr: SocketAddrV4, firmware: u16) {
|
||||
let mut cameras = self.cameras.lock().expect("cameras mutex poisoned");
|
||||
|
||||
@@ -239,6 +250,18 @@ pub struct Receiver {
|
||||
frames: Arc<LatestFrames>,
|
||||
role: SubscribeRole,
|
||||
camera: Ipv4Addr,
|
||||
/// 只处理这一块板子的数据,`None` 表示全都要(裁判端)。
|
||||
///
|
||||
/// 队伍端在 release 下设成 `Some(bound)`,这是一道**接收端**的闸门而不只是界面
|
||||
/// 的:对手的板子即使把流推到了本机端口,也不会被重组、不会进解码队列、不会
|
||||
/// 出现在任何统计里。
|
||||
///
|
||||
/// 为什么必须在这一层而不是靠界面隐藏:`.slint` 只控制显示,而数据已经收下并
|
||||
/// 解码了。隐藏一行文字不阻止任何事。
|
||||
///
|
||||
/// 用 `Mutex` 而不是 `Cell`/`AtomicU32`:接收线程要读它,而绑定回调在主线程上
|
||||
/// 写它。订阅循环本身已经是串行的,所以锁的持有时间极短。
|
||||
only_sender: Mutex<Option<u32>>,
|
||||
stop: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
@@ -247,7 +270,13 @@ impl Receiver {
|
||||
///
|
||||
/// 绑定固定端口(而非临时端口)是必须的:板子是从订阅的**源端口**学到推流目标的,
|
||||
/// 临时端口也能工作,但固定端口让三台客户端行为一致,也方便抓包排查。
|
||||
pub fn start(camera: Ipv4Addr, role: SubscribeRole) -> std::io::Result<Self> {
|
||||
///
|
||||
/// `only_sender` 为 `Some` 时只接受那一块板子的数据,见 [`Receiver`] 的字段说明。
|
||||
pub fn start(
|
||||
camera: Ipv4Addr,
|
||||
role: SubscribeRole,
|
||||
only_sender: Option<u32>,
|
||||
) -> std::io::Result<Self> {
|
||||
let socket =
|
||||
UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, DEFAULT_PORT))?;
|
||||
socket.set_read_timeout(Some(Duration::from_millis(100)))?;
|
||||
@@ -262,10 +291,24 @@ impl Receiver {
|
||||
frames: Arc::new(LatestFrames::default()),
|
||||
role,
|
||||
camera,
|
||||
only_sender: Mutex::new(only_sender),
|
||||
stop: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// 绑定之后切换限制目标,或解除限制。
|
||||
///
|
||||
/// 队伍端在选择界面里点定一块板子时调用:此前要能看到所有候选(否则无从选
|
||||
/// 起),此后只看这一块。
|
||||
pub fn restrict_to(&self, only_sender: Option<u32>) {
|
||||
*self.only_sender.lock().expect("only_sender mutex poisoned") = only_sender;
|
||||
}
|
||||
|
||||
/// 当前限制到哪一块板子。
|
||||
fn sender_limit(&self) -> Option<u32> {
|
||||
*self.only_sender.lock().expect("only_sender mutex poisoned")
|
||||
}
|
||||
|
||||
pub fn discovery(&self) -> Arc<Discovery> {
|
||||
Arc::clone(&self.discovery)
|
||||
}
|
||||
@@ -348,9 +391,22 @@ impl Receiver {
|
||||
return;
|
||||
}
|
||||
|
||||
let limit = self.sender_limit();
|
||||
|
||||
// 板子的在线通告。注意这里不看来源端口:广播来的源端口没有意义,身份由
|
||||
// 头里的 sender_id 决定。
|
||||
if let Some(hello) = protocol::Hello::decode(bytes) {
|
||||
// 限制生效时不记录其他板子。理由不只是"不显示":如果把它们记进
|
||||
// discovery,`--list` 式的输出、槽位排序、界面上任何一处遍历 cameras()
|
||||
// 的地方都会间接泄露场上还有谁。
|
||||
if let (Some(only), SocketAddr::V4(v4)) = (limit, from) {
|
||||
if hello.sender_id != only {
|
||||
return;
|
||||
}
|
||||
self.discovery.note_hello(hello.sender_id, v4, hello.firmware);
|
||||
return;
|
||||
}
|
||||
|
||||
if let SocketAddr::V4(v4) = from {
|
||||
let is_new = !self.discovery.cameras().iter().any(|c| c.id == hello.sender_id);
|
||||
self.discovery.note_hello(hello.sender_id, v4, hello.firmware);
|
||||
@@ -372,6 +428,22 @@ impl Receiver {
|
||||
|
||||
let id = frag.header.sender_id;
|
||||
|
||||
// 视频同样要过闸门。对手的板子一旦把流推到这个端口(IP 已被广播泄露,
|
||||
// 端口是公开常量),它的数据不会进任何重组器。
|
||||
if let Some(only) = limit {
|
||||
if id != only {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 视频帧也算存活证据。
|
||||
//
|
||||
// `is_online` 看的是 `last_seen`,而它此前只由 Hello 更新。于是出现一个
|
||||
// 荒唐的状态:画面在每秒刷 22 帧,状态栏却显示"离线"(橙色)。真板子每 2
|
||||
// 秒广播一次所以不易撞上,但它本身就是设计错误——正在推流的板子比广播更
|
||||
// 可靠地证明自己活着。
|
||||
self.discovery.touch(id);
|
||||
|
||||
match assemblers
|
||||
.entry(id)
|
||||
.or_insert_with(|| FrameAssembler::new(FRAME_TIMEOUT))
|
||||
|
||||
Reference in New Issue
Block a user