(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
控制台
标定工作台
配置信息
选择执行目标后连接台架
`;
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 = `
正在构建座椅数字孪生
CALIBRATION · CARTOON TWIN
拖动空白处旋转 · 拖动轴标签或座椅部件调轴 · 滚轮缩放 · 双击复位
靠背 0.0°
前后 0.00 cm
腿托 0.0°
高度 0.00 cm
ZED LIVE · RTSP
ZED 实时画面
未启动
![ZED 双目实时画面]()
ZED 双目画面RTSP → MJPEG 实时转发
视频接收尚未启动
安全租约已启用运动期间每 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 = `
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);
}());