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 读回帧数与分辨率(只断言"文件存在"会漏掉空容器)。
This commit is contained in:
+78
-8
@@ -453,14 +453,14 @@ impl Hello {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 装甲板(预留)
|
||||
// 装甲板
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 一块装甲板的上报。
|
||||
///
|
||||
/// 现在没有实现也没有数据源,但格式在这里定下来,并且 [`MsgType::ArmorHealth`]
|
||||
/// 与 [`ARMOR_PORT`] 已经就位:板子接上以后向 `ARMOR_PORT` 发这个结构,裁判终端
|
||||
/// 监听即可,界面与其它代码都不用改。
|
||||
/// 由 `armor/armor-firmware` 里的主板(ESP32-C3)广播到 [`ARMOR_PORT`],裁判终端监听
|
||||
/// 该端口并把它显示成四格血量。整车一条血量,所以四条上报的 `hp` 相同、`index`
|
||||
/// 分别是 0..3。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ArmorHealth {
|
||||
/// 哪一边:0 = 队伍一,1 = 队伍二。
|
||||
@@ -472,10 +472,20 @@ pub struct ArmorHealth {
|
||||
}
|
||||
|
||||
impl ArmorHealth {
|
||||
/// 编码成线上字节,帧头的 `sender_id` 留 0。
|
||||
///
|
||||
/// 0 是 [`REFEREE_ID`],所以板子发的时候应该用 [`encode_from`](Self::encode_from)
|
||||
/// 填上自己的 id(由 MAC 派生),抓包时才能分辨是哪块板子。
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
self.encode_from(0)
|
||||
}
|
||||
|
||||
/// 与 [`encode`](Self::encode) 相同,但帧头带上发送方自己的 id。
|
||||
pub fn encode_from(&self, sender_id: u32) -> Vec<u8> {
|
||||
let header = Header {
|
||||
msg_type: MsgType::ArmorHealth,
|
||||
version: PROTOCOL_VERSION,
|
||||
sender_id,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -488,17 +498,32 @@ impl ArmorHealth {
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> Option<Self> {
|
||||
Self::decode_from(bytes).map(|(_, health)| health)
|
||||
}
|
||||
|
||||
/// 与 [`decode`](Self::decode) 相同,但把帧头里的 `sender_id` 一起返回。
|
||||
///
|
||||
/// 板子用 [`encode_from`](Self::encode_from) 把自己的 MAC 派生 id 填进去,所以
|
||||
/// 这一个 u32 就是**哪块装甲板主板在说话**。裁判端与队伍端要列出"场上有哪几块
|
||||
/// 装甲板"并让用户选中其中一块,靠的就是它——没有它就只剩 `team` 字段,
|
||||
/// 同队的两块板子无从区分。
|
||||
///
|
||||
/// 保持 [`decode`](Self::decode) 不变是有意的:`team`/`index`/`hp` 是上报的语义,
|
||||
/// 而 `sender_id` 是链路层的信封,不需要它的人不该被迫处理它。
|
||||
pub fn decode_from(bytes: &[u8]) -> Option<(u32, Self)> {
|
||||
let header = decode_header(bytes)?;
|
||||
|
||||
if header.msg_type != MsgType::ArmorHealth || bytes.len() < HEADER_LEN + 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ArmorHealth {
|
||||
let health = ArmorHealth {
|
||||
team: bytes[HEADER_LEN],
|
||||
index: bytes[HEADER_LEN + 1],
|
||||
hp: u16::from_le_bytes(bytes[HEADER_LEN + 2..HEADER_LEN + 4].try_into().ok()?),
|
||||
})
|
||||
};
|
||||
|
||||
Some((header.sender_id, health))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,8 +884,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn armor_health_roundtrips() {
|
||||
// 现在没有实现,但格式必须先定下来并且可往返,否则接上板子时才发现布局
|
||||
// 不兼容就晚了。
|
||||
// 现在有实现了(armor/ 里的 ESP32-C3 固件在发它),布局必须保持可往返。
|
||||
let hp = ArmorHealth {
|
||||
team: 1,
|
||||
index: 2,
|
||||
@@ -871,6 +895,52 @@ mod tests {
|
||||
assert_eq!(ArmorHealth::decode(&encoded), Some(hp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn armor_health_can_carry_the_sending_board_id() {
|
||||
let hp = ArmorHealth {
|
||||
team: 0,
|
||||
index: 3,
|
||||
hp: 97,
|
||||
};
|
||||
|
||||
let encoded = hp.encode_from(0x1234_5678);
|
||||
assert_eq!(
|
||||
decode_header(&encoded).map(|h| h.sender_id),
|
||||
Some(0x1234_5678),
|
||||
"抓包时要能分辨是哪块装甲板发的"
|
||||
);
|
||||
assert_eq!(ArmorHealth::decode(&encoded), Some(hp));
|
||||
|
||||
// 帧头 id 不影响帧体,也不该影响旧调用方的行为。
|
||||
assert_eq!(ArmorHealth::decode(&hp.encode()), Some(hp));
|
||||
assert_eq!(decode_header(&hp.encode()).map(|h| h.sender_id), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn armor_health_decode_from_returns_the_board_id() {
|
||||
// 客户端要列出"场上有哪几块装甲板",只能靠帧头里这个 id:team 只有 0/1,
|
||||
// 同队的两块板子在 team 上无法区分。
|
||||
let hp = ArmorHealth {
|
||||
team: 1,
|
||||
index: 1,
|
||||
hp: 100,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ArmorHealth::decode_from(&hp.encode_from(0xABCD_1234)),
|
||||
Some((0xABCD_1234, hp))
|
||||
);
|
||||
|
||||
// decode 是 decode_from 丢掉信封的那一半,两者必须一致。
|
||||
assert_eq!(
|
||||
ArmorHealth::decode(&hp.encode_from(0xABCD_1234)),
|
||||
ArmorHealth::decode_from(&hp.encode_from(0xABCD_1234)).map(|(_, h)| h)
|
||||
);
|
||||
|
||||
// 不是装甲板上报就不该解出来。
|
||||
assert!(ArmorHealth::decode_from(&[0u8; 8]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ids_are_stable_and_never_zero() {
|
||||
let mac = [0x90, 0x70, 0x69, 0xF8, 0xF2, 0x14];
|
||||
|
||||
Reference in New Issue
Block a user