(function () { "use strict"; const API_ROOT = "/api/calibration"; const TARGET_KEY = "cognitive-agent.calibration-target"; const pageTitles = { live: ["SELF-CALIBRATION", "座椅与双目联合标定", "同步标定座椅姿态、四轴 Hall 与 ZED 实时画面"], system: ["SELF-CALIBRATION · SETTINGS", "连接与运行设置", "配置 CAN、MQTT 与 RTSP 接入参数"], }; const phaseLabels = { 0: "待命", 1: "寻找最近点", 2: "寻找最远点", 3: "返回最近点", 4: "完成", 5: "失败", 6: "已停止", }; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; let currentState = null; let eventSource = null; let reconnectTimer = 0; let toastTimer = 0; let settingsHydrated = false; let activeView = "live"; let bootstrapResetComplete = false; function buildShell() { const sidebar = $(".v2-sidebar"); const mainPanel = $(".main-panel"); const moduleSwitcher = $("#v2ModuleSwitcher"); if (!sidebar || !mainPanel || !moduleSwitcher || $("#v2SelfCalibration")) return false; const calibrationSidebar = document.createElement("section"); calibrationSidebar.className = "v2-sidebar-section v2-calibration-sidebar"; calibrationSidebar.innerHTML = `
CALIBRATION DESK

控制台

标定工作台
配置信息
当前车型腾势 N7
N7
推理模型GPT-5.6-sol
READY
执行目标
IDLE

选择执行目标后连接台架

`; sidebar.insertBefore(calibrationSidebar, $(".v2-sidebar-footer")); const calibrationFooter = document.createElement("footer"); calibrationFooter.className = "v2-sidebar-footer v2-calibration-footer"; calibrationFooter.innerHTML = `
自标定安全链路尚未连接台架
`; sidebar.append(calibrationFooter); const root = document.createElement("section"); root.className = "v2-self-calibration"; root.id = "v2SelfCalibration"; root.setAttribute("aria-label", "N7 自标定工作台"); root.innerHTML = `
SELF-CALIBRATION

座椅与双目联合标定

同步标定座椅姿态、四轴 Hall 与 ZED 实时画面

CAN未连接
MOTOR等待状态
MQTT等待数据
DIGITAL TWIN

实时座椅姿态

待命
正在构建座椅数字孪生 CALIBRATION · CARTOON TWIN
拖动空白处旋转 · 拖动轴标签或座椅部件调轴 · 滚轮缩放 · 双击复位
靠背 0.0°
前后 0.00 cm
腿托 0.0°
高度 0.00 cm
当前任务等待设备检测
JOB —
ZED LIVE · RTSP

ZED 实时画面

未启动
ZED 双目实时画面
ZED 双目画面RTSP → MJPEG 实时转发

视频接收尚未启动

LIVE AXES

四轴实时控制

滑块设定位置 · 端点按钮单次执行
01
最近点建立机械零位
02
最远点测量完整量程
03
返回近点验证端点一致

安全租约已启用运动期间每 500 ms 心跳;超时由固件自动 STOP。

`; moduleSwitcher.insertAdjacentElement("afterend", root); const target = $("#calibrationExecutionSelect"); target.value = sessionStorage.getItem(TARGET_KEY) === "simulation" ? "simulation" : "hardware"; window.CognitiveAgentConsoleV2?.enhanceSelect(target); return true; } function isAdmin() { return window.CognitiveAgentConsoleV2?.getRole() === "admin" || document.body.dataset.demoRole === "admin"; } function isActive() { return isAdmin() && document.body.dataset.module === "self-calibration"; } function toast(message, error = false) { const node = $("#toast"); if (!node) return; node.textContent = message; node.className = error ? "toast show error" : "toast show"; window.clearTimeout(toastTimer); toastTimer = window.setTimeout(() => { node.className = "toast"; }, 3600); } async function api(path, body = {}) { const response = await fetch(`${API_ROOT}${path}`, { method: "POST", credentials: "same-origin", headers: { Accept: "application/json", "Content-Type": "application/json", "X-Cognitive-Agent-Role": "admin", }, body: JSON.stringify(body), }); const data = await response.json().catch(() => ({ message: `HTTP ${response.status}`, })); if (!response.ok) throw new Error(data.message || data.error || `HTTP ${response.status}`); if (data.state) render(data.state); return data; } async function fetchState() { const response = await fetch(`${API_ROOT}/state`, { credentials: "same-origin", headers: { Accept: "application/json", "X-Cognitive-Agent-Role": "admin", }, }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.message || data.error || `HTTP ${response.status}`); render(data); return data; } async function run(action, success = "") { try { await action(); if (success) toast(success); } catch (error) { toast(error.message || String(error), true); } } function statusClass(state) { if (["online", "subscribed", "simulated", "playing"].includes(state)) return "online"; if (["connecting", "idle"].includes(state)) return "wait"; if (state === "error") return "error"; return ""; } function setStatusDot(selector, state) { const node = $(selector); if (node) node.className = statusClass(state); } function render(state) { if (!state?.transport || !state?.controller) return; currentState = state; const { transport, controller, mqtt } = state; setStatusDot("#calibrationCanDot", transport.state); setStatusDot( "#calibrationMotorDot", controller.motor_ready ? "online" : transport.state === "online" ? "connecting" : transport.state, ); setStatusDot("#calibrationMqttDot", mqtt.state); $("#calibrationCanState").textContent = transport.state === "online" ? transport.mode === "simulation" ? "仿真在线" : "实机在线" : transport.state === "connecting" ? "连接中" : transport.state === "error" ? "连接异常" : "未连接"; $("#calibrationMotorState").textContent = controller.motor_ready ? controller.armed ? "已解锁" : "已就绪" : "等待状态"; $("#calibrationMqttState").textContent = mqtt.state === "simulated" ? "仿真数据" : mqtt.state === "subscribed" ? "已订阅" : mqtt.state === "connecting" ? "连接中" : "未连接"; $("#calibrationPhaseBadge").textContent = state.operation.phase_name || phaseLabels[state.operation.phase] || "待命"; $("#calibrationPhaseBadge").className = `v2-calibration-badge ${ [5, 6].includes(state.operation.phase) ? "error" : state.operation.phase === 0 ? "wait" : "" }`; $("#calibrationOperationLabel").textContent = state.operation.label; $("#calibrationJobId").textContent = controller.job_id || "—"; $("#calibrationTransportDetail").textContent = transport.detail; $("#calibrationConnectionText").textContent = transport.detail; const targetState = $("#calibrationTargetState"); targetState.textContent = transport.state === "online" ? "READY" : transport.state === "connecting" ? "LINK" : transport.state === "error" ? "ERROR" : "IDLE"; $("#calibrationTargetCard").dataset.state = transport.state; $("#calibrationTargetNote").textContent = transport.detail; renderWorkflow(state.operation.phase); renderAxes(state.axes, controller.job_active, transport.state === "online"); renderSeat(state.axes); const seatViewer = $("#calibrationSeatViewer"); const axisControlEnabled = transport.state === "online" && !controller.job_active; if (seatViewer) { seatViewer.dataset.axisControlEnabled = String(axisControlEnabled); } $$('[data-seat-axis-label]').forEach((label) => { label.dataset.controlEnabled = String(axisControlEnabled); label.setAttribute("aria-disabled", String(!axisControlEnabled)); }); renderVideo(state.video); $("#calibrationStartAll").disabled = transport.state !== "online" || controller.job_active; if (!settingsHydrated) hydrateSettings(state.settings, state.video.ffmpeg_available); } function renderWorkflow(phase) { $$(".v2-calibration-flow > div").forEach((step) => { const value = Number(step.dataset.phase); step.classList.toggle("active", phase === value); step.classList.toggle("done", phase === 4 || (phase > value && phase < 5)); }); } function createAxisCards(axes) { const container = $("#calibrationAxisCards"); if (!container) return; axes.forEach((axis) => { const card = document.createElement("article"); card.className = "v2-calibration-axis-card"; card.dataset.axis = axis.id; card.innerHTML = `
AXIS 0${axis.id + 1}就绪
0HALL 0 / 0
近端 0%远端 100%
`; $("header strong", card).textContent = axis.title; const slider = $('input[type="range"]', card); slider.addEventListener("input", () => { $(".v2-calibration-axis-reading strong", card).textContent = formatPhysical(axis, Number(slider.value)); }); slider.addEventListener("change", () => run( () => api("/move", { axis: axis.id, permille: Number(slider.value) }), )); $('[data-seek="near"]', card).addEventListener("click", () => run( () => api("/seek", { axis: axis.id, far: false }), )); $('[data-seek="far"]', card).addEventListener("click", () => run( () => api("/seek", { axis: axis.id, far: true }), )); $("[data-axis-calibrate]", card).addEventListener("click", () => run( () => api("/start", { axis: axis.id }), `${axis.name}自动标定已启动`, )); container.append(card); }); } function renderAxes(axes, busy, online) { if (!$("#calibrationAxisCards")?.children.length) createAxisCards(axes); axes.forEach((axis) => { const card = $(`.v2-calibration-axis-card[data-axis="${axis.id}"]`); if (!card) return; const slider = $('input[type="range"]', card); if (document.activeElement !== slider) slider.value = axis.permille; $(".v2-calibration-axis-reading strong", card).textContent = formatPhysical(axis, Number(slider.value)); $(".v2-calibration-axis-reading span", card).textContent = `HALL ${axis.hall} / ${axis.range || "—"}`; const state = $("header b", card); state.textContent = axis.fault ? "故障" : axis.calibrating ? "标定中" : axis.moving ? axis.direction : axis.position_valid ? "就绪" : "位置未知"; state.className = axis.moving || axis.calibrating ? "moving" : ""; slider.disabled = busy || !online; $$(".v2-calibration-axis-actions button", card).forEach((button) => { button.disabled = busy || !online; }); }); } function formatPhysical(axis, permille) { const value = axis.physical_range * permille / 1000; return axis.unit === "cm" ? `${value.toFixed(2)} ${axis.unit}` : `${value.toFixed(1)}${axis.unit}`; } function renderSeat(axes) { if (!axes?.length) return; const longitudinal = axes.find((axis) => axis.id === 0) || axes[0]; const legrest = axes.find((axis) => axis.id === 1) || axes[1]; const backrest = axes.find((axis) => axis.id === 2) || axes[2]; const height = axes.find((axis) => axis.id === 3) || axes[3]; const pose = { longitudinalM: (longitudinal.permille / 1000 - 0.5) * longitudinal.physical_range * 0.01, verticalM: (0.5 - height.permille / 1000) * height.physical_range * 0.01, backrestDeg: backrest.physical_value, legrestDeg: legrest.physical_value, }; 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`; [ ["longitudinal", longitudinal], ["legrest", legrest], ["backrest", backrest], ["height", height], ].forEach(([name, axis]) => { const label = $(`[data-seat-axis-label="${name}"]`); if (!label) return; label.setAttribute("aria-valuenow", String(Math.round(axis.permille))); label.setAttribute("aria-valuetext", formatPhysical(axis, axis.permille)); }); } function previewSeatAxis(event) { if (!currentState?.axes?.length) return; const viewer = $("#calibrationSeatViewer"); if (viewer?.dataset.axisControlEnabled !== "true") return; const axisId = Number(event.detail?.axis); const permille = Math.max(0, Math.min(1000, Number(event.detail?.permille))); if (!Number.isInteger(axisId) || !Number.isFinite(permille)) return; const axes = currentState.axes.map((axis) => ({ ...axis })); const axis = axes.find((item) => item.id === axisId); if (!axis) return; axis.permille = permille; axis.physical_value = axis.physical_range * permille / 1000; const card = $(`.v2-calibration-axis-card[data-axis="${axisId}"]`); if (card) { const slider = $('input[type="range"]', card); slider.value = String(permille); $(".v2-calibration-axis-reading strong", card).textContent = formatPhysical(axis, permille); } renderSeat(axes); } function commitSeatAxis(event) { const viewer = $("#calibrationSeatViewer"); if (viewer?.dataset.axisControlEnabled !== "true") return; const axis = Number(event.detail?.axis); const permille = Math.max(0, Math.min(1000, Math.round(Number(event.detail?.permille)))); if (!Number.isInteger(axis) || !Number.isFinite(permille)) return; run(() => api("/move", { axis, permille })); } function renderVideo(video) { const friendly = { idle: "未启动", connecting: "连接中", playing: "播放中", error: "异常" }; $("#calibrationVideoBadge").textContent = friendly[video.state] || video.state; $("#calibrationVideoBadge").className = `v2-calibration-badge ${ video.state === "error" ? "error" : video.state === "idle" ? "wait" : "" }`; $("#calibrationVideoDetail").textContent = video.detail; const image = $("#calibrationVideoImage"); if (video.state === "playing" && !image.classList.contains("playing")) { image.src = `${API_ROOT}/video/stream?role=admin&t=${Date.now()}`; image.classList.add("playing"); } if (!["playing", "connecting"].includes(video.state)) { image.classList.remove("playing"); image.removeAttribute("src"); } } function hydrateSettings(settings, ffmpegAvailable) { settingsHydrated = true; $("#calibrationCanInterface").value = settings.can.interface; $("#calibrationCanChannel").value = settings.can.channel; $("#calibrationCanBitrate").value = settings.can.bitrate; $("#calibrationCanPython").value = settings.can.python_executable || ""; $("#calibrationCanDll").value = settings.can.dll_path || ""; $("#calibrationCanDeviceType").value = settings.can.device_type ?? 41; $("#calibrationCanDeviceIndex").value = settings.can.device_index ?? 0; $("#calibrationMqttHost").value = settings.mqtt.host; $("#calibrationMqttPort").value = settings.mqtt.port; $("#calibrationMqttTopic").value = settings.mqtt.topic; $("#calibrationVideoHost").value = settings.video.host; $("#calibrationVideoPort").value = settings.video.port; $("#calibrationVideoPath").value = settings.video.path; $("#calibrationFfmpegHint").textContent = ffmpegAvailable ? "已检测到 FFmpeg,可转发 RTSP。" : "未检测到 FFmpeg;其余功能不受影响。"; } function collectSettings() { return { can: { mode: $("#calibrationExecutionSelect").value, interface: $("#calibrationCanInterface").value.trim(), channel: $("#calibrationCanChannel").value.trim(), bitrate: Number($("#calibrationCanBitrate").value), python_executable: $("#calibrationCanPython").value.trim(), dll_path: $("#calibrationCanDll").value.trim(), device_type: Number($("#calibrationCanDeviceType").value), device_index: Number($("#calibrationCanDeviceIndex").value), resistance: 1, }, mqtt: { host: $("#calibrationMqttHost").value.trim(), port: Number($("#calibrationMqttPort").value), topic: $("#calibrationMqttTopic").value.trim(), }, video: { host: $("#calibrationVideoHost").value.trim(), port: Number($("#calibrationVideoPort").value), path: $("#calibrationVideoPath").value.trim(), }, }; } function showView(view) { activeView = pageTitles[view] ? view : "live"; $$(".v2-calibration-view").forEach((page) => { const active = page.dataset.calibrationPage === activeView; page.hidden = !active; page.classList.toggle("active", active); }); $$("[data-calibration-view]").forEach((button) => { button.classList.toggle("active", button.dataset.calibrationView === activeView); }); $("#calibrationSettingsLink").classList.toggle("active", activeView === "system"); const [kicker, title, description] = pageTitles[activeView]; $("#calibrationPageKicker").textContent = kicker; $("#calibrationPageTitle").textContent = title; $("#calibrationPageDescription").textContent = description; window.setTimeout(() => window.dispatchEvent(new Event("resize")), 80); } function updateTargetPresentation() { const simulation = $("#calibrationExecutionSelect").value === "simulation"; sessionStorage.setItem(TARGET_KEY, simulation ? "simulation" : "hardware"); $("#calibrationConnectTarget span").textContent = simulation ? "连接标定仿真台架" : "连接真实台架"; if (!currentState || currentState.transport.state === "disconnected") { $("#calibrationTargetNote").textContent = simulation ? "完整 CAN 协议仿真,不驱动实体设备" : "本机 admin 可连接真实 CAN 台架"; } } function connectTarget() { const settings = collectSettings(); return run(async () => { await api("/settings", settings); await api("/can/connect", { can: settings.can }); }, settings.can.mode === "simulation" ? "标定仿真台架已连接" : "真实台架连接完成"); } function closeEvents() { window.clearTimeout(reconnectTimer); reconnectTimer = 0; eventSource?.close(); eventSource = null; } function connectEvents() { if (!isActive() || eventSource) return; eventSource = new EventSource(`${API_ROOT}/events?role=admin`); eventSource.onmessage = (event) => { try { render(JSON.parse(event.data)); } catch (error) { console.error("自标定状态解析失败", error); } }; eventSource.onerror = () => { closeEvents(); if (isActive()) reconnectTimer = window.setTimeout(connectEvents, 1600); }; } function activate() { if (!isActive()) { closeEvents(); return; } fetchState().then(async (state) => { if (bootstrapResetComplete) return; bootstrapResetComplete = true; if (state.transport?.mode === "simulation") { await api("/session/reset"); } }).catch((error) => toast(`自标定服务连接失败:${error.message}`, true)); connectEvents(); } function bindEvents() { $$("[data-calibration-view]").forEach((button) => { button.addEventListener("click", () => showView(button.dataset.calibrationView)); }); $("#calibrationSettingsLink").addEventListener("click", () => showView("system")); $("#calibrationExecutionSelect").addEventListener("change", updateTargetPresentation); $("#calibrationConnectTarget").addEventListener("click", connectTarget); $("#calibrationGlobalStop").addEventListener("click", () => run( () => api("/stop"), "全轴 STOP 已确认", )); $("#calibrationStartAll").addEventListener("click", () => run( () => api("/start", { axis: "all" }), "四轴自动标定已启动", )); $("#calibrationVideoStart").addEventListener("click", () => run( () => api("/video/start"), "正在启动 RTSP 接收", )); $("#calibrationVideoStop").addEventListener("click", () => run( () => api("/video/stop"), "视频接收已停止", )); $("#calibrationCanForm").addEventListener("submit", (event) => { event.preventDefault(); connectTarget(); }); $("#calibrationMqttForm").addEventListener("submit", (event) => { event.preventDefault(); run(() => api("/settings", collectSettings()), "MQTT 设置已保存"); }); $("#calibrationVideoForm").addEventListener("submit", (event) => { event.preventDefault(); run(() => api("/settings", collectSettings()), "RTSP 设置已保存"); }); window.addEventListener("cognitive-agent:module-change", activate); window.addEventListener("cognitive-agent:demo-role-change", activate); window.addEventListener("calibration-seat-axis-preview", previewSeatAxis); window.addEventListener("calibration-seat-axis-commit", commitSeatAxis); window.addEventListener("beforeunload", closeEvents); } function init() { if (!buildShell()) return; bindEvents(); updateTargetPresentation(); showView(activeView); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); activate(); } document.addEventListener("DOMContentLoaded", init); }());