把桌面端从 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 读回帧数与分辨率(只断言"文件存在"会漏掉空容器)。
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
/**
|
|
* 后端命令的封装。
|
|
*
|
|
* 每个函数对应 `desktop/src-tauri/src/commands.rs` 里的一个 `#[tauri::command]`。
|
|
* Tauri 会把 JS 侧的 camelCase 参数名转成 Rust 侧的 snake_case,所以这里的参数名写成
|
|
* 前端习惯的样子即可。
|
|
*
|
|
* # 错误
|
|
*
|
|
* 后端的 `Err(String)` 会以 rejected promise 的形式回来,字符串就是给现场的人看的那句
|
|
* 话。`call()` 把 `unknown` 归一成 `Error`,调用方只管 `catch (err) { err.message }`。
|
|
*/
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
import type {
|
|
ConfigView,
|
|
HudView,
|
|
MediaFileDto,
|
|
RecordingDto,
|
|
Role,
|
|
Snapshot,
|
|
StreamInfo,
|
|
} from "./types";
|
|
|
|
/** 把后端的字符串错误变成真正的 `Error`。 */
|
|
async function call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
|
try {
|
|
return await invoke<T>(command, args);
|
|
} catch (raw) {
|
|
throw new Error(typeof raw === "string" ? raw : String(raw));
|
|
}
|
|
}
|
|
|
|
export const api = {
|
|
// ---- 只读 ----
|
|
streamInfo: () => call<StreamInfo>("stream_info"),
|
|
snapshot: () => call<Snapshot>("get_snapshot"),
|
|
config: () => call<ConfigView>("get_config"),
|
|
mediaFiles: () => call<MediaFileDto[]>("list_media_files"),
|
|
configPath: () => call<string>("config_path"),
|
|
|
|
// ---- 密码 ----
|
|
unlock: (password: string) => call<boolean>("unlock", { password }),
|
|
lock: () => call<void>("lock"),
|
|
setPassword: (fresh: string, current?: string) =>
|
|
call<ConfigView>("set_password", { current: current ?? null, fresh }),
|
|
setDiscoveryOpen: (open: boolean) => call<void>("set_discovery_open", { open }),
|
|
|
|
// ---- 绑定与身份 ----
|
|
bindCamera: (cameraId: number | null, password?: string) =>
|
|
call<ConfigView>("bind_camera", { cameraId, password: password ?? null }),
|
|
bindArmor: (armorId: number | null, password?: string) =>
|
|
call<ConfigView>("bind_armor", { armorId, password: password ?? null }),
|
|
setTeam: (team: number, password?: string) =>
|
|
call<ConfigView>("set_team", { team, password: password ?? null }),
|
|
setRole: (role: Role, password?: string) =>
|
|
call<ConfigView>("set_role", { role, password: password ?? null }),
|
|
setMirror: (mirror: boolean) => call<ConfigView>("set_mirror", { mirror }),
|
|
saveHud: (hud: HudView) => call<ConfigView>("save_hud", { hud }),
|
|
setRefereeSlots: (slots: {
|
|
cameraA: number | null;
|
|
cameraB: number | null;
|
|
armorA: number | null;
|
|
armorB: number | null;
|
|
manual: boolean;
|
|
}) => call<ConfigView>("set_referee_slots", slots),
|
|
setRecordingOptions: (options: {
|
|
dir: string | null;
|
|
ffmpeg: string | null;
|
|
crf: number | null;
|
|
preset: string | null;
|
|
}) => call<ConfigView>("set_recording_options", options),
|
|
|
|
// ---- 截图与录制 ----
|
|
screenshot: (camera: number) => call<string>("screenshot", { camera }),
|
|
startRecording: (cameras: number[]) => call<RecordingDto[]>("start_recording", { cameras }),
|
|
stopRecording: (cameras: number[]) => call<RecordingDto[]>("stop_recording", { cameras }),
|
|
reveal: (path: string) => call<void>("reveal", { path }),
|
|
|
|
// ---- 退出与重置 ----
|
|
clearConfig: (password?: string) =>
|
|
call<string>("clear_config", { password: password ?? null }),
|
|
quit: (password?: string) => call<void>("quit_app", { password: password ?? null }),
|
|
|
|
refresh: () => call<void>("refresh"),
|
|
};
|