feat(client): RMIC::2026 界面重做、可计算的版本机制与固件版本联动
- feat(client): 界面换成墨黑蓝底 + 单支红的矩形语言,切角全部去掉,仅装甲条保留平行四边形 - feat(client): 自托管 HarmonyOS Sans SC 与 JetBrains Mono,中文与拉丁数字逐字回退 - feat(client): 新增启动序列、首启门、用户端 HUD、裁判端控制台、设置抽屉与固件维护页 - feat(client): 裁判端中缝与下方控制台可拖,鼠标落在哪一路下方就看哪一路 - feat(client): 设置抽屉常驻实时预览,与画面共用同一份准星绘制代码 - feat(client): 动效改用 Material 3 的缓动曲线与时长,逐条替换掉自造的贝塞尔 - feat(version): 版本号与 CHANGELOG 由 scripts/version.mjs 从 git 历史算出,不再手写 - feat(firmware): Hello 报文携带语义化版本、构建号与自报协议号,负载向后兼容 - feat(client): 固件版本显示在绑定列表与裁判端每一格;协议不匹配时给出具体原因 - feat(firmware): 固件包版本与客户端同源,都从 version.json 读 - fix(client): getCurrentWindow() 在没有 Tauri 注入时同步抛异常,浏览器预览整棵树被卸载 - fix(client): 启动序列的淡出 effect 清掉了自己刚设的 setTimeout,界面永远停在启动页 - fix(client): 设置抽屉左侧导航的锚点没挂到 section 上,且用错了坐标系 - fix(protocol): Hello 改用宽松帧头解析,让协议不匹配的板子能被解释而不是静默消失 - fix(build): Windows 上 bindgen 加载不了 libclang 的同目录依赖,构建脚本补上搜索路径 - fix(build): esp-idf-sys 在长路径下拒绝构建,固件构建改用短的目标目录 - build(client): 队标图标,含光学尺寸分级与手工拼的多尺寸 ICO - build(ci): 守门 job 增加版本一致性检查,产物名改为 rmic-2026-* - docs: 补充界面语言、版本机制与协议通告布局的说明
This commit is contained in:
@@ -67,9 +67,16 @@ fn spawn_camera(state: Arc<AppState>, camera: u32, team: u8) {
|
||||
41000 + team as u16,
|
||||
));
|
||||
|
||||
// 演示模式合成的通告带上**本客户端自己的版本**(同一份 version.json),
|
||||
// 所以界面上的"固件 v0.2.0 · 34"与这一份二进制是一致的。演示模式靠顶栏
|
||||
// 那条琥珀标签区分,不靠版本号露馅——版本号是真实信息,不该为了演示造假。
|
||||
let build = crate::version::build_info();
|
||||
let hello = protocol::Hello {
|
||||
sender_id: camera,
|
||||
firmware: 0x0DE0,
|
||||
firmware: 1,
|
||||
version: build.packed(),
|
||||
build: build.build,
|
||||
protocol: build.protocol,
|
||||
};
|
||||
|
||||
let frame_interval = Duration::from_micros(1_000_000 / FPS);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! cam-desktop —— ESP32 图传与装甲板系统的桌面端。
|
||||
//! RMIC::2026 —— ESP32 图传与装甲板系统的桌面端。
|
||||
//!
|
||||
//! 一个进程、两套页面:
|
||||
//!
|
||||
@@ -23,7 +23,9 @@
|
||||
|
||||
mod commands;
|
||||
mod demo;
|
||||
mod version;
|
||||
mod latency;
|
||||
mod maintenance;
|
||||
mod recording;
|
||||
mod server;
|
||||
mod state;
|
||||
@@ -39,11 +41,15 @@ use tauri::{Manager, RunEvent, WindowEvent};
|
||||
use state::AppState;
|
||||
|
||||
const USAGE: &str = "\
|
||||
用法: cam-desktop [选项]
|
||||
用法: RMIC::2026 [选项]
|
||||
|
||||
--role <team|referee> 身份。默认看配置文件;没有配置文件时是 referee
|
||||
--config <路径> 配置文件位置
|
||||
--latency-report <路径> 打开裁判端屏幕标记校准,写入新的本地 NDJSON 报告
|
||||
--maintenance-probe <串口> 连接设备、打印芯片/分区表/App 信息后退出(会复位一次)
|
||||
--maintenance-bundle <固件包> 只校验 .zip/目录包并打印清单,不连设备
|
||||
--maintenance-plan <串口> <固件包> 打印烧录计划后退出(只读,不写 Flash)
|
||||
--maintenance-write-test <串口> <固件包目录> <偏移> 写 App 并回读,不切分区表
|
||||
--demo 演示模式:画面与血量都是本地合成的
|
||||
--list 打印发现的摄像头与装甲板后退出
|
||||
-h, --help 显示这段帮助
|
||||
@@ -63,7 +69,7 @@ fn main() {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
if let Err(err) = run() {
|
||||
log::error!("{err}");
|
||||
log::error!("{err:#}");
|
||||
|
||||
// 走到这里说明连窗口都没起来(配置坏了、回环端口绑不上)。GUI 程序从访达启动时
|
||||
// 没有终端可看,所以把原因也写进一个旁边的文件——现场排查时这是唯一线索。
|
||||
@@ -81,6 +87,10 @@ struct Args {
|
||||
role: Option<Role>,
|
||||
config: Option<PathBuf>,
|
||||
latency_report: Option<PathBuf>,
|
||||
maintenance_probe: Option<String>,
|
||||
maintenance_bundle: Option<PathBuf>,
|
||||
maintenance_plan: Option<(String, PathBuf)>,
|
||||
maintenance_write_test: Option<(String, PathBuf, u32)>,
|
||||
demo: bool,
|
||||
list: bool,
|
||||
help: bool,
|
||||
@@ -110,6 +120,27 @@ impl Args {
|
||||
"--latency-report" => {
|
||||
args.latency_report = it.next().map(PathBuf::from);
|
||||
}
|
||||
"--maintenance-probe" => {
|
||||
args.maintenance_probe = it.next();
|
||||
}
|
||||
"--maintenance-bundle" => {
|
||||
args.maintenance_bundle = it.next().map(PathBuf::from);
|
||||
}
|
||||
"--maintenance-plan" => {
|
||||
let port = it.next();
|
||||
let bundle = it.next();
|
||||
if let (Some(port), Some(bundle)) = (port, bundle) {
|
||||
args.maintenance_plan = Some((port, PathBuf::from(bundle)));
|
||||
}
|
||||
}
|
||||
"--maintenance-write-test" => {
|
||||
let port = it.next();
|
||||
let bundle = it.next();
|
||||
let offset = it.next().and_then(|value| parse_offset(&value));
|
||||
if let (Some(port), Some(bundle), Some(offset)) = (port, bundle, offset) {
|
||||
args.maintenance_write_test = Some((port, PathBuf::from(bundle), offset));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
// 见模块文档:macOS 的 .app 启动参数与 shell 里的笔误都可能出现在这里,
|
||||
// 但报错的代价(窗口起不来)远大于收益。
|
||||
@@ -130,6 +161,39 @@ fn run() -> Result<(), Box<dyn Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 维护探测:不加载配置、不绑 UDP 端口,也不起窗口。现场可以用它确认串口和分区表。
|
||||
if let Some(port) = &args.maintenance_probe {
|
||||
let temp = std::env::current_dir()?.join("target").join("flash-tmp");
|
||||
maintenance::set_temp_dir(temp);
|
||||
let snapshot = maintenance::probe_snapshot(port)?;
|
||||
println!("{}", serde_json::to_string_pretty(&snapshot)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(bundle) = &args.maintenance_bundle {
|
||||
let temp = std::env::current_dir()?.join("target").join("flash-tmp");
|
||||
maintenance::set_temp_dir(temp);
|
||||
let info = maintenance::inspect_bundle(bundle)?;
|
||||
println!("{}", serde_json::to_string_pretty(&info)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some((port, bundle)) = &args.maintenance_plan {
|
||||
let temp = std::env::current_dir()?.join("target").join("flash-tmp");
|
||||
maintenance::set_temp_dir(temp);
|
||||
let plan = maintenance::plan_snapshot(port, bundle)?;
|
||||
println!("{}", serde_json::to_string_pretty(&plan)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some((port, bundle, offset)) = &args.maintenance_write_test {
|
||||
let temp = std::env::current_dir()?.join("target").join("flash-tmp");
|
||||
maintenance::set_temp_dir(temp);
|
||||
let attempt = maintenance::write_test(port, bundle, *offset)?;
|
||||
println!("{}", serde_json::to_string_pretty(&attempt)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config_path = args
|
||||
.config
|
||||
.clone()
|
||||
@@ -228,11 +292,32 @@ fn run() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let listener_state = Arc::clone(&state);
|
||||
|
||||
// 日志第一行就报版本:现场拿到一台机器,最先要回答的两个问题就是"跑的是哪一版"
|
||||
// 与"它和板子的协议对不对得上"。
|
||||
{
|
||||
let build = version::build_info();
|
||||
log::info!(
|
||||
"RMIC::2026 client {} (build {}, wire protocol {})",
|
||||
build.version_text(),
|
||||
build.build,
|
||||
build.protocol
|
||||
);
|
||||
}
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.manage(Arc::clone(&state))
|
||||
.manage(latency_recorder)
|
||||
.manage(Arc::new(maintenance::MaintenanceState::default()))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
maintenance::maintenance_list_ports,
|
||||
maintenance::maintenance_load_bundle,
|
||||
maintenance::maintenance_probe,
|
||||
maintenance::maintenance_plan,
|
||||
maintenance::maintenance_flash,
|
||||
maintenance::maintenance_restore_table,
|
||||
maintenance::maintenance_cancel,
|
||||
latency::latency_enabled,
|
||||
latency::latency_sample,
|
||||
commands::stream_info,
|
||||
@@ -262,7 +347,13 @@ fn run() -> Result<(), Box<dyn Error>> {
|
||||
commands::quit_app,
|
||||
commands::refresh,
|
||||
])
|
||||
.setup(move |_app| {
|
||||
.setup(move |app| {
|
||||
let temp = app
|
||||
.path()
|
||||
.app_cache_dir()
|
||||
.unwrap_or_else(|_| std::env::temp_dir())
|
||||
.join("flash-tmp");
|
||||
maintenance::set_temp_dir(temp);
|
||||
listener_state.spawn_background();
|
||||
Ok(())
|
||||
})
|
||||
@@ -313,6 +404,14 @@ fn run() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_offset(text: &str) -> Option<u32> {
|
||||
if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
|
||||
u32::from_str_radix(hex, 16).ok()
|
||||
} else {
|
||||
text.parse::<u32>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// 让 [`Config`] 在日志里可读。
|
||||
///
|
||||
/// 实现成自由函数而不是给 `Config` 加 `Display`:那是 cam-core 的公共类型,而"日志里长
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
//! 编译期嵌入的构建信息。
|
||||
//!
|
||||
//! # 为什么读仓库根的那个文件
|
||||
//!
|
||||
//! `version.json` 由 `scripts/version.mjs` 从 git 历史算出来,是版本号的唯一来源。
|
||||
//! 这里用 `include_str!` 把仓库根的那一份**嵌进二进制**:
|
||||
//!
|
||||
//! - 不依赖运行时的文件(装到别人机器上也能读到);
|
||||
//! - `include_str!` 会让 cargo 把它登记成依赖,所以它一变,这个 crate 就会重编——
|
||||
//! 不需要再手写 `cargo:rerun-if-changed`。
|
||||
//!
|
||||
//! 二进制里的版本号因此一定与它编译时的那棵源码树一致,不会出现"装包写着 0.1.0、
|
||||
//! 其实是两周后的代码"。
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// 仓库根 `version.json` 的原文。
|
||||
const RAW: &str = include_str!("../../../version.json");
|
||||
|
||||
/// 一份构建的全部身份信息。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BuildInfo {
|
||||
pub major: u16,
|
||||
pub minor: u16,
|
||||
pub patch: u16,
|
||||
/// git 提交数,单调递增。
|
||||
pub build: u16,
|
||||
/// 线路协议版本。
|
||||
pub protocol: u16,
|
||||
}
|
||||
|
||||
impl BuildInfo {
|
||||
/// 打包成 `major << 16 | minor << 8 | patch`,与协议里的 `Hello.version` 同一套编码。
|
||||
pub fn packed(&self) -> u32 {
|
||||
((self.major as u32) << 16) | (((self.minor as u32) & 0xff) << 8) | ((self.patch as u32) & 0xff)
|
||||
}
|
||||
|
||||
pub fn version_text(&self) -> String {
|
||||
format!("{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析失败时退回全 0——那会让界面显示"版本未知",比编不过或者编出一个假版本好。
|
||||
pub fn build_info() -> BuildInfo {
|
||||
let Ok(json) = serde_json::from_str::<Value>(RAW) else {
|
||||
return BuildInfo {
|
||||
major: 0,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
build: 0,
|
||||
protocol: 0,
|
||||
};
|
||||
};
|
||||
|
||||
let field = |name: &str| -> u16 {
|
||||
json.get(name)
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0)
|
||||
.min(u16::MAX as u64) as u16
|
||||
};
|
||||
|
||||
BuildInfo {
|
||||
major: field("major"),
|
||||
minor: field("minor"),
|
||||
patch: field("patch"),
|
||||
build: field("build"),
|
||||
protocol: field("protocol"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user