feat: refine vehicle console and self-repair workflow

This commit is contained in:
vee
2026-08-13 17:38:04 +08:00
parent fd3c219c39
commit 0ec43052f3
20 changed files with 3925 additions and 552 deletions
+23 -115
View File
@@ -4,8 +4,8 @@
const API_ROOT = "/api/calibration";
const TARGET_KEY = "cognitive-agent.calibration-target";
const pageTitles = {
live: ["SEAT · ZED JOINT CALIBRATION", "座椅—双目联合标定", "座椅姿态、四轴 Hall、ZED 画面与双目位姿同步更新"],
system: ["CONNECTION SETTINGS", "连接与运行设置", "CAN、MQTT 与 RTSP 接入参数"],
live: ["SELF-CALIBRATION", "座椅与双目联合标定", "同步标定座椅姿态、四轴 Hall 与 ZED 实时画面"],
system: ["SELF-CALIBRATION · SETTINGS", "连接与运行设置", "配置 CAN、MQTT 与 RTSP 接入参数"],
};
const phaseLabels = {
0: "待命",
@@ -25,7 +25,6 @@
let reconnectTimer = 0;
let toastTimer = 0;
let settingsHydrated = false;
let modelRequested = false;
let activeView = "live";
function buildShell() {
@@ -83,10 +82,10 @@
root.setAttribute("aria-label", "N7 自标定工作台");
root.innerHTML = `
<header class="v2-calibration-heading">
<div>
<span class="section-kicker" id="calibrationPageKicker">SEAT · ZED JOINT CALIBRATION</span>
<h1 id="calibrationPageTitle">座椅—双目联合标定</h1>
<p id="calibrationPageDescription">座椅姿态、四轴 Hall、ZED 画面与双目位姿同步更新</p>
<div class="v2-module-heading">
<span class="section-kicker" id="calibrationPageKicker">SELF-CALIBRATION</span>
<h1 id="calibrationPageTitle">座椅与双目联合标定</h1>
<p id="calibrationPageDescription">同步标定座椅姿态、四轴 Hall 与 ZED 实时画面</p>
</div>
<div class="v2-calibration-statuses">
<div><i id="calibrationCanDot"></i><span><small>CAN</small><strong id="calibrationCanState">未连接</strong></span></div>
@@ -101,12 +100,12 @@
<header><div><span class="section-kicker">DIGITAL TWIN</span><h2>实时座椅姿态</h2></div><span class="v2-calibration-badge wait" id="calibrationPhaseBadge">待命</span></header>
<div class="v2-calibration-seat-scene">
<div class="v2-calibration-floor-grid"></div>
<div id="seatModelHost" class="seat-model-host">
<canvas id="seatCanvas" aria-label="N7 原始三维座椅模型,可拖动旋转并滚轮缩放"></canvas>
<div id="calibrationSeatViewer" class="seat-model-host calibration-cartoon-viewer">
<canvas id="calibrationSeatCanvas" aria-label="自标定卡通风格三维座椅,可拖动旋转并滚轮缩放"></canvas>
<div id="seatModelLoading" class="model-loading">
<span class="model-spinner"></span>
<strong>正在加载原始 N7 座椅</strong>
<small>ANDROID GLB · LOCAL</small>
<strong>正在构建座椅数字孪生</strong>
<small>CALIBRATION · CARTOON TWIN</small>
</div>
<div class="orbit-hint">拖动旋转 · 滚轮缩放 · 双击复位</div>
</div>
@@ -127,17 +126,6 @@
<div class="v2-calibration-actions"><button id="calibrationVideoStart" type="button">开始接收</button><button id="calibrationVideoStop" type="button">停止</button></div>
</section>
<section class="v2-calibration-panel v2-calibration-mqtt-panel">
<header><div><span class="section-kicker">ZED / SEATPOSE</span><h2>双目位姿数据</h2></div><span class="v2-calibration-badge wait" id="calibrationMqttBadge">等待数据</span></header>
<div class="v2-calibration-metrics"><div><small>消息总数</small><strong id="calibrationMqttCount">0</strong></div><div><small>最新帧</small><strong id="calibrationMqttFrame">—</strong></div><div><small>解析错误</small><strong id="calibrationMqttErrors">0</strong></div></div>
<p class="v2-calibration-detail" id="calibrationMqttDetail">MQTT 尚未连接</p>
<div class="v2-calibration-pose-areas" id="calibrationPoseAreas"></div>
<div class="v2-calibration-live-mapping">
<div class="v2-calibration-subheading"><span>HALL ↔ 3D</span><strong>实时映射</strong></div>
<div class="v2-calibration-mapping-rows" id="calibrationMappingRows"></div>
</div>
<div class="v2-calibration-actions"><button id="calibrationMqttConnect" type="button">连接 MQTT</button><button id="calibrationMqttDisconnect" type="button">断开</button><button id="calibrationMqttTest" type="button">回环测试</button></div>
</section>
</div>
<section class="v2-calibration-panel v2-calibration-axis-section v2-calibration-live-axes">
@@ -312,9 +300,7 @@
renderWorkflow(state.operation.phase);
renderAxes(state.axes, controller.job_active, transport.state === "online");
renderSeat(state.axes);
renderMqtt(mqtt);
renderVideo(state.video);
renderMappings(state.axes, mqtt.areas);
$("#calibrationStartAll").disabled =
transport.state !== "online" || controller.job_active;
if (!settingsHydrated) hydrateSettings(state.settings, state.video.ffmpeg_available);
@@ -408,55 +394,25 @@
};
window.n7SeatPose = pose;
window.dispatchEvent(new CustomEvent("n7-seat-pose", { detail: pose }));
window.dispatchEvent(new CustomEvent("calibration-seat-state", {
detail: {
axes: [
{ motor: 0, position_pct: longitudinal.permille / 10 },
{ motor: 1, position_pct: height.permille / 10 },
{ motor: 2, position_pct: backrest.permille / 10 },
{ motor: 3, position_pct: legrest.permille / 10 },
],
state: axes.some((axis) => axis.fault)
? "FAILED"
: axes.some((axis) => axis.moving || axis.calibrating) ? "WORKING" : "IDLE",
},
}));
$("#calibrationTagLong").textContent = `${longitudinal.physical_value.toFixed(2)} cm`;
$("#calibrationTagLeg").textContent = `${legrest.physical_value.toFixed(1)}°`;
$("#calibrationTagBack").textContent = `${backrest.physical_value.toFixed(1)}°`;
$("#calibrationTagHeight").textContent = `${height.physical_value.toFixed(2)} cm`;
}
function renderMqtt(mqtt) {
const friendly = {
simulated: "仿真数据",
subscribed: "已订阅",
connecting: "连接中",
disconnected: "未连接",
error: "异常",
};
$("#calibrationMqttBadge").textContent = friendly[mqtt.state] || mqtt.state;
$("#calibrationMqttBadge").className = `v2-calibration-badge ${
mqtt.state === "error"
? "error"
: ["disconnected", "connecting"].includes(mqtt.state) ? "wait" : ""
}`;
$("#calibrationMqttCount").textContent = mqtt.count;
$("#calibrationMqttFrame").textContent = mqtt.last_frame >= 0 ? mqtt.last_frame : "—";
$("#calibrationMqttErrors").textContent = mqtt.parse_errors;
$("#calibrationMqttDetail").textContent = mqtt.detail;
const container = $("#calibrationPoseAreas");
container.replaceChildren();
const entries = Object.entries(mqtt.areas || {});
if (!entries.length) {
const empty = document.createElement("p");
empty.className = "v2-calibration-empty";
empty.textContent = "等待 Seat_cushion / Seat_backrest / Seat_leg_rest 位姿…";
container.append(empty);
}
entries.forEach(([name, pose]) => {
const row = document.createElement("div");
row.className = "v2-calibration-pose-row";
const title = document.createElement("strong");
title.textContent = name;
const angle = document.createElement("span");
angle.textContent = pose.angle == null ? "ANGLE —" : `${Number(pose.angle).toFixed(2)}°`;
const translation = document.createElement("small");
translation.textContent = pose.translation
? `T [${pose.translation.slice(0, 3).map((value) => Number(value).toFixed(3)).join(", ")}]`
: "T —";
row.append(title, angle, translation);
container.append(row);
});
}
function renderVideo(video) {
const friendly = { idle: "未启动", connecting: "连接中", playing: "播放中", error: "异常" };
$("#calibrationVideoBadge").textContent = friendly[video.state] || video.state;
@@ -475,28 +431,6 @@
}
}
function renderMappings(axes, areas = {}) {
const container = $("#calibrationMappingRows");
container.replaceChildren();
const areaFor = ["Seat_cushion", "Seat_leg_rest", "Seat_backrest", "Seat_cushion"];
axes.forEach((axis) => {
const pose = areas[areaFor[axis.id]] || {};
const observed = axis.id === 0 || axis.id === 3
? pose.translation?.[axis.id === 0 ? 0 : 1]
: pose.angle;
const row = document.createElement("div");
row.className = "v2-calibration-mapping-row";
row.innerHTML = "<strong></strong><span></span><div><i></i></div><b></b>";
$("strong", row).textContent = axis.name;
$("span", row).textContent = `${axis.hall} Hall`;
$("i", row).style.width = `${axis.permille / 10}%`;
$("b", row).textContent = observed == null
? "3D —"
: `${Number(observed).toFixed(3)}${axis.id === 1 || axis.id === 2 ? "°" : " T"}`;
container.append(row);
});
}
function hydrateSettings(settings, ffmpegAvailable) {
settingsHydrated = true;
$("#calibrationCanInterface").value = settings.can.interface;
@@ -580,19 +514,6 @@
}, settings.can.mode === "simulation" ? "标定仿真台架已连接" : "真实台架连接完成");
}
function loadSeatModel() {
if (modelRequested || !$("#seatModelHost")) return;
modelRequested = true;
const script = document.createElement("script");
script.src = "/assets/self-calibration-model.bundle.js";
script.async = true;
script.onerror = () => {
modelRequested = false;
$("#seatModelLoading")?.classList.add("error");
};
document.head.append(script);
}
function closeEvents() {
window.clearTimeout(reconnectTimer);
reconnectTimer = 0;
@@ -621,7 +542,6 @@
closeEvents();
return;
}
loadSeatModel();
fetchState().catch((error) => toast(`自标定服务连接失败:${error.message}`, true));
connectEvents();
}
@@ -641,18 +561,6 @@
() => api("/start", { axis: "all" }),
"四轴自动标定已启动",
));
$("#calibrationMqttConnect").addEventListener("click", () => run(
() => api("/mqtt/connect"),
"正在连接 MQTT",
));
$("#calibrationMqttDisconnect").addEventListener("click", () => run(
() => api("/mqtt/disconnect"),
"MQTT 已断开",
));
$("#calibrationMqttTest").addEventListener("click", () => run(
() => api("/mqtt/test"),
"MQTT 测试帧已发送",
));
$("#calibrationVideoStart").addEventListener("click", () => run(
() => api("/video/start"),
"正在启动 RTSP 接收",