/** * 固件维护页的 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(command: string, args?: Record): Promise { try { return await invoke(command, args); } catch (raw) { throw new Error(typeof raw === "string" ? raw : String(raw)); } } export const maintenanceApi = { listPorts: () => call("maintenance_list_ports"), loadBundle: (path: string) => call("maintenance_load_bundle", { path }), probe: (port: string) => call("maintenance_probe", { port }), plan: (port: string, bundlePath: string) => call("maintenance_plan", { port, bundlePath }), flash: (port: string, bundlePath: string, backupApp = true) => call("maintenance_flash", { port, bundlePath, backupApp }), restoreTable: (port: string, backupDir: string) => call("maintenance_restore_table", { port, backupDir }), cancel: () => call("maintenance_cancel"), }; /** 原生 .zip 选择。返回 null 表示用户取消。 */ export async function selectBundleZip(): Promise { 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 { 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; }