Files
Cognitive_Agent/apps/web-console/self-calibration.js
T

600 lines
28 KiB
JavaScript

(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";
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 = `
<div class="v2-section-heading">
<span>CALIBRATION DESK</span>
<h2>控制台</h2>
</div>
<span class="v2-field-caption">标定工作台</span>
<div class="v2-calibration-mode-switch" role="group" aria-label="标定工作台">
<button type="button" class="active" data-calibration-view="live"><i data-lucide="scan-eye"></i><span>联合实时标定</span></button>
</div>
<div class="v2-divider"></div>
<span class="v2-field-caption">配置信息</span>
<div class="v2-calibration-config-stack">
<div class="v2-calibration-config-card"><i data-lucide="car-front"></i><div><small>当前车型</small><strong>腾势 N7</strong></div><span>N7</span></div>
<div class="v2-calibration-config-card"><i data-lucide="sparkles"></i><div><small>推理模型</small><strong>GPT-5.6-sol</strong></div><span>READY</span></div>
<div class="v2-calibration-config-card v2-config-select-card" id="calibrationTargetCard">
<i data-lucide="workflow"></i>
<div class="v2-config-card-copy">
<small>执行目标</small>
<div class="v2-config-card-select">
<select id="calibrationExecutionSelect" aria-label="选择自标定执行目标">
<option value="hardware">真实台架</option>
<option value="simulation">标定仿真台架</option>
</select>
</div>
</div>
<span id="calibrationTargetState">IDLE</span>
</div>
</div>
<p class="v2-calibration-target-note" id="calibrationTargetNote">选择执行目标后连接台架</p>
<button class="v2-calibration-connect" id="calibrationConnectTarget" type="button"><i data-lucide="plug-zap"></i><span>连接真实台架</span></button>
<button class="v2-calibration-settings-link" id="calibrationSettingsLink" type="button"><i data-lucide="settings-2"></i><span>连接设置</span><i data-lucide="chevron-right"></i></button>`;
sidebar.insertBefore(calibrationSidebar, $(".v2-sidebar-footer"));
const calibrationFooter = document.createElement("footer");
calibrationFooter.className = "v2-sidebar-footer v2-calibration-footer";
calibrationFooter.innerHTML = `
<span class="v2-live-dot"></span>
<div><strong>自标定安全链路</strong><small id="calibrationConnectionText">尚未连接台架</small></div>
<button type="button" id="calibrationGlobalStop" title="全轴安全停止" aria-label="全轴安全停止"><i data-lucide="octagon"></i></button>`;
sidebar.append(calibrationFooter);
const root = document.createElement("section");
root.className = "v2-self-calibration";
root.id = "v2SelfCalibration";
root.setAttribute("aria-label", "N7 自标定工作台");
root.innerHTML = `
<header class="v2-calibration-heading">
<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>
<div><i id="calibrationMotorDot"></i><span><small>MOTOR</small><strong id="calibrationMotorState">等待状态</strong></span></div>
<div><i id="calibrationMqttDot"></i><span><small>MQTT</small><strong id="calibrationMqttState">等待数据</strong></span></div>
</div>
</header>
<section class="v2-calibration-view active v2-calibration-live-view" data-calibration-page="live">
<div class="v2-calibration-live-grid">
<section class="v2-calibration-panel v2-calibration-stage">
<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="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>正在构建座椅数字孪生</strong>
<small>CALIBRATION · CARTOON TWIN</small>
</div>
<div class="orbit-hint">拖动旋转 · 滚轮缩放 · 双击复位</div>
</div>
<div class="v2-axis-tag tag-back">靠背 <b id="calibrationTagBack">0.0°</b></div>
<div class="v2-axis-tag tag-base">前后 <b id="calibrationTagLong">0.00 cm</b></div>
<div class="v2-axis-tag tag-leg">腿托 <b id="calibrationTagLeg">0.0°</b></div>
<div class="v2-axis-tag tag-height">高度 <b id="calibrationTagHeight">0.00 cm</b></div>
</div>
<div class="v2-calibration-operation">
<i></i><div><small>当前任务</small><strong id="calibrationOperationLabel">等待设备检测</strong></div><span>JOB <b id="calibrationJobId">—</b></span>
</div>
</section>
<section class="v2-calibration-panel v2-calibration-video-panel">
<header><div><span class="section-kicker">ZED LIVE · RTSP</span><h2>ZED 实时画面</h2></div><span class="v2-calibration-badge wait" id="calibrationVideoBadge">未启动</span></header>
<div class="v2-calibration-video-frame"><img id="calibrationVideoImage" alt="ZED 双目实时画面"><div id="calibrationVideoPlaceholder"><i data-lucide="scan-line"></i><strong>ZED 双目画面</strong><small>RTSP → MJPEG 实时转发</small></div></div>
<p class="v2-calibration-detail" id="calibrationVideoDetail">视频接收尚未启动</p>
<div class="v2-calibration-actions"><button id="calibrationVideoStart" type="button">开始接收</button><button id="calibrationVideoStop" type="button">停止</button></div>
</section>
</div>
<section class="v2-calibration-panel v2-calibration-axis-section v2-calibration-live-axes">
<header class="v2-calibration-live-toolbar">
<div class="v2-calibration-axis-title"><span class="section-kicker">LIVE AXES</span><h2>四轴实时控制</h2><small>滑块设定位置 · 端点按钮单次执行</small></div>
<div class="v2-calibration-flow" aria-label="自动标定流程">
<div data-phase="1"><span>01</span><div><strong>最近点</strong><small>建立机械零位</small></div></div>
<i></i>
<div data-phase="2"><span>02</span><div><strong>最远点</strong><small>测量完整量程</small></div></div>
<i></i>
<div data-phase="3"><span>03</span><div><strong>返回近点</strong><small>验证端点一致</small></div></div>
</div>
<button class="v2-calibration-primary" id="calibrationStartAll" type="button" disabled><span>开始四轴自动标定</span><i data-lucide="arrow-right"></i></button>
</header>
<div class="v2-calibration-axis-grid" id="calibrationAxisCards"></div>
<div class="v2-calibration-safety v2-calibration-live-safety"><i data-lucide="shield-check"></i><p><strong>安全租约已启用</strong><span>运动期间每 500 ms 心跳;超时由固件自动 STOP。</span></p></div>
</section>
</section>
<section class="v2-calibration-view" data-calibration-page="system" hidden>
<div class="v2-calibration-settings-grid">
<form class="v2-calibration-panel v2-calibration-settings-card" id="calibrationCanForm">
<header><span>CAN</span><div><small>TRANSPORT</small><h2>CAN 适配器</h2></div></header>
<p class="v2-calibration-form-note">执行目标由左侧下拉菜单选择。</p>
<div class="v2-calibration-two-col"><label>接口<input id="calibrationCanInterface" value="zlgcan"></label><label>通道<input id="calibrationCanChannel" value="0"></label></div>
<label>位速率<input id="calibrationCanBitrate" value="500000" readonly></label>
<label>ZCANPRO Python<input id="calibrationCanPython" value="C:\\Program Files (x86)\\ZCANPRO\\python38\\python.exe"></label>
<label>ZLG DLL<input id="calibrationCanDll" value="C:\\Program Files (x86)\\ZCANPRO\\zlgcan.dll"></label>
<div class="v2-calibration-two-col"><label>设备类型<input id="calibrationCanDeviceType" type="number" value="41"></label><label>设备序号<input id="calibrationCanDeviceIndex" type="number" value="0"></label></div>
<button class="v2-calibration-primary" type="submit">保存并连接台架</button>
<p class="v2-calibration-detail" id="calibrationTransportDetail"></p>
</form>
<form class="v2-calibration-panel v2-calibration-settings-card" id="calibrationMqttForm">
<header><span>MQ</span><div><small>STEREO DATA</small><h2>MQTT Broker</h2></div></header>
<div class="v2-calibration-two-col"><label>主机<input id="calibrationMqttHost"></label><label>端口<input id="calibrationMqttPort" type="number"></label></div>
<label>主题<input id="calibrationMqttTopic"></label>
<button type="submit">保存 MQTT 设置</button>
<p class="v2-calibration-form-note">正式数据主题默认:zed/seatpose</p>
</form>
<form class="v2-calibration-panel v2-calibration-settings-card" id="calibrationVideoForm">
<header><span>RT</span><div><small>VIDEO</small><h2>RTSP 视频</h2></div></header>
<div class="v2-calibration-two-col"><label>主机<input id="calibrationVideoHost"></label><label>端口<input id="calibrationVideoPort" type="number"></label></div>
<label>流路径<input id="calibrationVideoPath"></label>
<button type="submit">保存 RTSP 设置</button>
<p class="v2-calibration-form-note" id="calibrationFfmpegHint"></p>
</form>
</div>
</section>`;
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);
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 = `
<header><span>AXIS 0${axis.id + 1}</span><strong></strong><b>就绪</b></header>
<div class="v2-calibration-axis-reading"><strong>0</strong><span>HALL 0 / 0</span></div>
<input type="range" min="0" max="1000" value="0" aria-label="${axis.name}位置">
<div class="v2-calibration-range-labels"><span>近端 0%</span><span>远端 100%</span></div>
<div class="v2-calibration-axis-actions"><button data-seek="near" type="button">近端</button><button data-axis-calibrate type="button">标定</button><button data-seek="far" type="button">远端</button></div>`;
$("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`;
}
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().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("beforeunload", closeEvents);
}
function init() {
if (!buildShell()) return;
bindEvents();
updateTargetPresentation();
showView(activeView);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
activate();
}
document.addEventListener("DOMContentLoaded", init);
}());