(function () { "use strict"; const API_ROOT = "/api/calibration"; const TARGET_KEY = "cognitive-agent.calibration-target"; const pageTitles = { hall: ["N7 SEAT CALIBRATION", "座椅 Hall 标定", "四轴位置、端点与自动标定"], stereo: ["STEREO CALIBRATION", "双目映射标定", "MQTT 位姿、RTSP 画面与 Hall 映射"], system: ["CONNECTION 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 modelRequested = false; let activeView = "hall"; 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 = `
N7 SEAT CALIBRATION

座椅 Hall 标定

四轴位置、端点与自动标定

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

实时座椅姿态

待命
正在加载原始 N7 座椅 ANDROID GLB · LOCAL
拖动旋转 · 滚轮缩放 · 双击复位
靠背 0.0°
前后 0.00 cm
腿托 0.0°
高度 0.00 cm
当前任务等待设备检测
JOB —
CALIBRATION FLOW

自动标定

依次寻找近端、远端并返回近端,完成后保存真实 Hall 量程。

01
寻找最近点建立机械零位
02
寻找最远点测量完整量程
03
返回最近点验证端点一致性

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

LIVE AXES

四轴实时控制

松开滑块发送目标位置;端点按钮为一次性命令。

`; 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); } 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); 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); } 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 })); $("#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; $("#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 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", 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; $("#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 : "hall"; $$(".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 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; 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; } loadSeatModel(); fetchState().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" }), "四轴自动标定已启动", )); $("#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 接收", )); $("#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("beforeunload", closeEvents); } function init() { if (!buildShell()) return; bindEvents(); updateTargetPresentation(); showView(activeView); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); activate(); } document.addEventListener("DOMContentLoaded", init); }());