- 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: 补充界面语言、版本机制与协议通告布局的说明
214 lines
5.2 KiB
TypeScript
214 lines
5.2 KiB
TypeScript
/**
|
||
* 固件维护页的 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;
|
||
}
|