Files
simba-rmic-2026/desktop/src/lib/maintenance.ts
T
sloray 896b21fe44
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s
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: 补充界面语言、版本机制与协议通告布局的说明
2026-10-19 02:40:08 +08:00

214 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 固件维护页的 IPC 与 DTO。
*
* 字段名与 `desktop/src-tauri/src/maintenance.rs` 的 `#[serde(rename_all = "camelCase")]`
* 一一对应;改一边必须改另一边。
*/
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
export interface PortInfo {
portName: string;
label: string;
vid: number | null;
pid: number | null;
likelyEsp: boolean;
}
export interface PartitionEntry {
name: string;
kind: number;
subtype: number;
offset: number;
size: number;
flags: number;
}
export interface FreeRange {
start: number;
size: number;
}
export interface AppInfo {
offset: number;
version: string;
projectName: string;
buildTime: string;
idfVersion: string;
}
export interface DeviceSnapshot {
port: string;
chip: string;
revision: string | null;
flashSize: number;
flashSizeText: string;
mac: string | null;
features: string[];
secureBoot: boolean;
secureDownload: boolean;
flashEncryption: boolean;
securityFlags: number;
flashCryptCnt: number;
partitionTable: PartitionEntry[];
app: AppInfo | null;
freeRanges: FreeRange[];
tableSha256: string;
}
export interface BundleInfo {
path: string;
sourceKind: string;
sourcePath: string;
sourceSha256: string | null;
entryCount: number | null;
name: string;
version: string;
chip: string;
appFile: string;
appSha256: string;
appSize: number;
partitionTableFile: string;
partitionTableSha256: string;
bootloaderFile: string | null;
bootloaderSha256: string | null;
appPartitionSize: number;
flashSize: string;
flashSizeBytes: number;
flashMode: string;
flashFrequency: string;
notes: string | null;
}
export interface SlotCandidate {
offset: number;
size: number;
}
export interface FlashPlan {
port: string;
device: DeviceSnapshot;
bundle: BundleInfo;
candidates: SlotCandidate[];
targetOffset: number;
targetSize: number;
appSize: number;
appPartitionSize: number;
currentFactoryOffset: number;
currentFactorySize: number;
currentTable: PartitionEntry[];
patchedTable: PartitionEntry[];
tableShaBefore: string;
tableShaAfter: string;
oldAppPreserved: boolean;
warnings: string[];
canFlash: boolean;
}
export interface Mismatch {
offset: number;
expected: number;
actual: number;
}
export interface SlotAttempt {
offset: number;
size: number;
writeOk: boolean;
verifyOk: boolean;
mismatches: Mismatch[];
badSectors: number[];
error: string | null;
}
export interface FlashProgress {
phase: string;
message: string;
current: number;
total: number;
}
export interface FlashReport {
ok: boolean;
port: string;
chip: string;
mac: string | null;
bundleName: string;
bundleVersion: string;
targetOffset: number;
targetSize: number;
appSize: number;
appSha256: string;
tableShaBefore: string;
tableShaAfter: string;
attempts: SlotAttempt[];
appVerified: boolean;
tableVerified: boolean;
tableRestored: boolean;
needsRollback: boolean;
bootOk: boolean | null;
bootLog: string;
backupDir: string;
note: string | null;
error: string | null;
startedAt: string;
finishedAt: string;
}
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 maintenanceApi = {
listPorts: () => call<PortInfo[]>("maintenance_list_ports"),
loadBundle: (path: string) => call<BundleInfo>("maintenance_load_bundle", { path }),
probe: (port: string) => call<DeviceSnapshot>("maintenance_probe", { port }),
plan: (port: string, bundlePath: string) =>
call<FlashPlan>("maintenance_plan", { port, bundlePath }),
flash: (port: string, bundlePath: string, backupApp = true) =>
call<FlashReport>("maintenance_flash", { port, bundlePath, backupApp }),
restoreTable: (port: string, backupDir: string) =>
call<string>("maintenance_restore_table", { port, backupDir }),
cancel: () => call<void>("maintenance_cancel"),
};
/** 原生 .zip 选择。返回 null 表示用户取消。 */
export async function selectBundleZip(): Promise<string | null> {
const selected = await open({
directory: false,
multiple: false,
title: "选择官方固件包(.zip)",
filters: [{ name: "固件包", extensions: ["zip"] }],
});
return typeof selected === "string" ? selected : null;
}
/** 兼容旧包:选择目录。返回 null 表示用户取消。 */
export async function selectBundleDirectory(): Promise<string | null> {
const selected = await open({
directory: true,
multiple: false,
title: "选择官方固件包目录(含 bundle.json)",
});
return typeof selected === "string" ? selected : null;
}
export function formatBytes(value: number): string {
if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MiB`;
if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
return `${value} B`;
}
export function formatOffset(value: number): string {
return `0x${value.toString(16).toUpperCase().padStart(6, "0")}`;
}
export function shortSha(value: string): string {
return value.length > 16 ? `${value.slice(0, 12)}…${value.slice(-4)}` : value;
}