构建:修复 GTest 链接失败——显式指定 Linux GTest 配置,避免从 PATH 探测到 Windows Anaconda 的 .lib
This commit is contained in:
@@ -0,0 +1,720 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pPowerManger 上位机模拟</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--panel: #1e293b;
|
||||
--border: #334155;
|
||||
--fg: #e2e8f0;
|
||||
--acc: #38bdf8;
|
||||
--ok: #22c55e;
|
||||
--warn: #f59e0b;
|
||||
--err: #ef4444;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
background: var(--panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
header h1 { font-size: 16px; margin: 0; }
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--err);
|
||||
}
|
||||
.dot.on { background: var(--ok); }
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
main { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
section {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
section.wide {
|
||||
margin: 0 16px 16px;
|
||||
max-width: 1468px;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-size: 14px;
|
||||
margin: 0 0 10px;
|
||||
color: var(--acc);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
select, input, textarea, button {
|
||||
background: #0b1220;
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
width: 100%;
|
||||
min-height: 90px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
background: #0ea5e9;
|
||||
border-color: #0ea5e9;
|
||||
color: #fff;
|
||||
}
|
||||
button:hover { filter: brightness(1.1); }
|
||||
button.sec {
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
margin: 2px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.chip:hover { border-color: var(--acc); }
|
||||
|
||||
.var-item {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.var-item summary {
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
background: #16202f;
|
||||
user-select: none;
|
||||
}
|
||||
.var-item pre {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
background: #0b1220;
|
||||
color: #a5f3fc;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#log {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#log .send { color: var(--acc); }
|
||||
#log .err { color: var(--err); }
|
||||
|
||||
#chart {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
display: block;
|
||||
background: #0b1220;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.serie {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 8px;
|
||||
margin: 2px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
background: #0b1220;
|
||||
}
|
||||
.serie .swatch {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
.serie .rm {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-weight: bold;
|
||||
}
|
||||
.serie .rm:hover { color: var(--err); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<span class="dot" id="wsDot"></span>
|
||||
<h1>pPowerManger 上位机模拟 (Web UI)</h1>
|
||||
<span class="muted" id="wsState">连接中…</span>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2>指令下发</h2>
|
||||
<div class="row">
|
||||
<label>命令类型</label>
|
||||
<select id="cmdType"></select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>MOOS 变量</label>
|
||||
<input id="cmdKey" style="flex:1" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>JSON 载荷</label>
|
||||
</div>
|
||||
<textarea id="cmdValue"></textarea>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<button id="btnSend">发送</button>
|
||||
<button id="btnReset" class="sec">重置模板</button>
|
||||
</div>
|
||||
|
||||
<h2>快捷指令</h2>
|
||||
<div id="quick"></div>
|
||||
|
||||
<h2>通用 MOOS 通知</h2>
|
||||
<div class="row">
|
||||
<input id="rawKey" placeholder="变量名" style="flex:1" />
|
||||
<input id="rawValue" placeholder="JSON 字符串" style="flex:2" />
|
||||
<button id="btnRawSend">Notify</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>反馈监控(pPowerManger 发往上位机的全部消息)</h2>
|
||||
<div class="muted" id="monitorMeta"></div>
|
||||
<div id="monitor"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section class="wide">
|
||||
<h2>连续数据曲线</h2>
|
||||
<div class="row">
|
||||
<label>选择字段</label>
|
||||
<select id="fieldSel" style="flex:1"></select>
|
||||
<button id="btnAddSeries">添加曲线</button>
|
||||
<button id="btnClearSeries" class="sec">清空全部</button>
|
||||
</div>
|
||||
<div id="seriesBox" style="margin-bottom:8px"></div>
|
||||
<canvas id="chart"></canvas>
|
||||
</section>
|
||||
|
||||
<section class="wide">
|
||||
<h2>操作日志</h2>
|
||||
<div id="log"></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// ---- 数据定义 ----
|
||||
const WORK_CONDITIONS = [
|
||||
[0, "切换中"],
|
||||
[1, "待机"],
|
||||
[2, "岸基备航"],
|
||||
[3, "水中备航"],
|
||||
[4, "遥控"],
|
||||
[5, "巡航"],
|
||||
[6, "高速"],
|
||||
[7, "上浮下潜"],
|
||||
[8, "浮调"],
|
||||
[9, "水下侦查"],
|
||||
[10, "水面侦查"],
|
||||
[11, "DJ"],
|
||||
[12, "调试"]
|
||||
];
|
||||
|
||||
const MODES = [
|
||||
[0, "正常"],
|
||||
[1, "测试"],
|
||||
[2, "调试"]
|
||||
];
|
||||
|
||||
const POWER_CMDS = [
|
||||
[0, "断电"],
|
||||
[1, "上电"],
|
||||
[2, "保持"]
|
||||
];
|
||||
|
||||
const TASKS = [
|
||||
["throwLoad", "抛载"],
|
||||
["floatUp", "运动上浮"],
|
||||
["recycle", "回收"],
|
||||
["hover", "悬停"],
|
||||
["satComm", "上浮通信"],
|
||||
["satCalibrate", "上浮校准"],
|
||||
["tour", "游曳"],
|
||||
["sail", "航行"],
|
||||
["emerSatComm", "紧急上浮通信"],
|
||||
["emerSail", "紧急航行"]
|
||||
];
|
||||
|
||||
const DEVICES = [
|
||||
[0, "singleBeamSonar1"],
|
||||
[1, "singleBeamSonar2"],
|
||||
[2, "singleBeamSonar3"],
|
||||
[3, "singleBeamSonar4"],
|
||||
[4, "singleBeamSonar5"],
|
||||
[5, "singleBeamSonar6"],
|
||||
[6, "singleBeamSonar7"],
|
||||
[7, "singleBeamSonar8"],
|
||||
[8, "forwardSonar"],
|
||||
[9, "bowCover1"],
|
||||
[10, "bowCover2"],
|
||||
[11, "bowCover3"],
|
||||
[12, "bowCover4"],
|
||||
[13, "bowCover5"],
|
||||
[14, "bowCover6"],
|
||||
[15, "bowRudderDeploy"],
|
||||
[16, "bowRudderServo1"],
|
||||
[17, "bowRudderServo2"],
|
||||
[18, "depth1"],
|
||||
[19, "depth2"],
|
||||
[20, "depth3"],
|
||||
[21, "overDeep1"],
|
||||
[22, "overDeep2"],
|
||||
[23, "ctd1"],
|
||||
[24, "ctd2"],
|
||||
[25, "soundVeloc"],
|
||||
[26, "waterInject"],
|
||||
[27, "bowWaterInet1"],
|
||||
[28, "bowWaterInet2"],
|
||||
[29, "bowWaterInet3"],
|
||||
[30, "bowWaterInet4"],
|
||||
[31, "bowWaterInet5"],
|
||||
[32, "bowWaterInet6"],
|
||||
[33, "bowWaterInet7"],
|
||||
[34, "bowWaterInet8"],
|
||||
[35, "sideSonar"],
|
||||
[36, "lowFreqSonar"],
|
||||
[37, "launchControler"],
|
||||
[38, "photoSensor"],
|
||||
[39, "electSensor"],
|
||||
[40, "commMast"],
|
||||
[41, "mastLiftingServo"],
|
||||
[42, "USBL"],
|
||||
[43, "DVL"],
|
||||
[44, "buoyancyAdjust1"],
|
||||
[45, "buoyancyAdjust2"],
|
||||
[46, "buoyancyAdjust3"],
|
||||
[47, "buoyancyAdjust4"],
|
||||
[48, "buoyancyAdjust5"],
|
||||
[49, "sternRudderServo1"],
|
||||
[50, "sternRudderServo2"],
|
||||
[51, "sternRudderServo3"],
|
||||
[52, "sternRudderServo4"],
|
||||
[53, "thruster"],
|
||||
[54, "bowThrow"],
|
||||
[55, "sternThrow"],
|
||||
[56, "buoyage"],
|
||||
[57, "ins"],
|
||||
[58, "ntp"],
|
||||
[59, "inputWaterSensor1"],
|
||||
[60, "inputWaterSensor2"],
|
||||
[61, "inputWaterSensor3"]
|
||||
];
|
||||
|
||||
const DEV_CMDS = [
|
||||
[0, "断电(OFF)"],
|
||||
[1, "上电(ON)"],
|
||||
[2, "仪表上电"],
|
||||
[3, "动力上电"],
|
||||
[4, "仪表断电"],
|
||||
[5, "动力断电"]
|
||||
];
|
||||
|
||||
const COMMANDS = {
|
||||
setOpCondition: {
|
||||
key: "uExternComm_setOpCondition_cmd",
|
||||
tpl: { state_uint8: 1 },
|
||||
label: "设置工况"
|
||||
},
|
||||
setOpMode: {
|
||||
key: "uExternComm_setOpMode_cmd",
|
||||
tpl: { state_uint8: 0 },
|
||||
label: "设置模式"
|
||||
},
|
||||
setPowerSystem: {
|
||||
key: "uExternComm_setPowerSystem_cmd",
|
||||
tpl: { powerCmd_uint8: 1 },
|
||||
label: "电源系统"
|
||||
},
|
||||
masterControl: {
|
||||
key: "uPower_sysState_cmd",
|
||||
tpl: { powerCMD: 0, modCMD: 0, workCMD: 0 },
|
||||
label: "主控指令"
|
||||
},
|
||||
setDeviceSwitch: {
|
||||
key: "uExternComm_setDeviceSwitch_cmd",
|
||||
tpl: { id_uint8: 0, cmd_uint8: 1 },
|
||||
label: "设备开关"
|
||||
},
|
||||
taskStart: {
|
||||
key: "uPlan_taskStart_st",
|
||||
tpl: { taskType_str: "sail", balanceEnabled_bool: true, bowRudderUnfold_bool: true },
|
||||
label: "任务启动"
|
||||
},
|
||||
planStop: {
|
||||
key: "uPlan_planStop_st",
|
||||
tpl: { op_int32: 1 },
|
||||
label: "任务停止"
|
||||
},
|
||||
depth: {
|
||||
key: "uDevice_depth_st",
|
||||
tpl: { value_array_int32: [0, 0, 0] },
|
||||
label: "深度"
|
||||
},
|
||||
equState: {
|
||||
key: "uPower_equState_cmd",
|
||||
tpl: {},
|
||||
label: "设备指令(DriverTable)"
|
||||
},
|
||||
secdistState: {
|
||||
key: "uDevice_secdistState_st",
|
||||
tpl: {},
|
||||
label: "次级配电状态"
|
||||
}
|
||||
};
|
||||
|
||||
// ---- 曲线相关 ----
|
||||
const COLORS = [
|
||||
"#38bdf8", "#22c55e", "#f59e0b", "#ef4444", "#a78bfa",
|
||||
"#f472b6", "#34d399", "#fbbf24", "#fb923c", "#e879f9"
|
||||
];
|
||||
const MAX_POINTS = 300;
|
||||
|
||||
let history = {}; // path -> { color, points: [[t, v], ...] }
|
||||
let allFields = new Set();
|
||||
let nextColor = 0;
|
||||
let ws = null;
|
||||
let currentCmd = "setOpCondition";
|
||||
|
||||
// ---- 工具函数 ----
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function log(msg, cls) {
|
||||
const d = document.createElement("div");
|
||||
if (cls) d.className = cls;
|
||||
d.textContent = msg;
|
||||
el("log").appendChild(d);
|
||||
el("log").scrollTop = el("log").scrollHeight;
|
||||
}
|
||||
|
||||
// ---- 通信 ----
|
||||
function connect() {
|
||||
const proto = location.protocol === "https:" ? "wss://" : "ws://";
|
||||
ws = new WebSocket(proto + location.host + "/ws");
|
||||
ws.onopen = () => {
|
||||
el("wsDot").classList.add("on");
|
||||
el("wsState").textContent = "已连接";
|
||||
};
|
||||
ws.onclose = () => {
|
||||
el("wsDot").classList.remove("on");
|
||||
el("wsState").textContent = "已断开,重连中…";
|
||||
setTimeout(connect, 1000);
|
||||
};
|
||||
ws.onerror = () => ws.close();
|
||||
ws.onmessage = (e) => {
|
||||
try {
|
||||
const snap = JSON.parse(e.data);
|
||||
onSnapshot(snap);
|
||||
} catch (err) {
|
||||
log("解析反馈失败: " + err, "err");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function sendMsg(obj) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
log("未连接", "err");
|
||||
return;
|
||||
}
|
||||
ws.send(JSON.stringify(obj));
|
||||
log("发送 -> " + obj.key + " : " + JSON.stringify(obj.value), "send");
|
||||
}
|
||||
|
||||
// ---- 指令下发 ----
|
||||
function applyTemplate(cmdType) {
|
||||
const c = COMMANDS[cmdType];
|
||||
el("cmdKey").value = c.key;
|
||||
el("cmdValue").value = JSON.stringify(c.tpl, null, 2);
|
||||
}
|
||||
|
||||
function sendCurrent() {
|
||||
let v;
|
||||
try {
|
||||
v = JSON.parse(el("cmdValue").value);
|
||||
} catch (e) {
|
||||
log("JSON 非法: " + e.message, "err");
|
||||
return;
|
||||
}
|
||||
sendMsg({ key: el("cmdKey").value, value: v });
|
||||
}
|
||||
|
||||
function buildQuick(){
|
||||
const box = el("quick");
|
||||
let html = "";
|
||||
|
||||
html += '<div class="muted">工况 (setOpCondition)</div><div>';
|
||||
WORK_CONDITIONS.forEach(([v,n]) => { html += `<span class="chip" data-act="cond" data-v="${v}">${n}</span>`; });
|
||||
html += "</div>";
|
||||
|
||||
html += '<div class="muted" style="margin-top:8px">模式 (setOpMode)</div><div>';
|
||||
MODES.forEach(([v,n]) => { html += `<span class="chip" data-act="mode" data-v="${v}">${n}</span>`; });
|
||||
html += "</div>";
|
||||
|
||||
html += '<div class="muted" style="margin-top:8px">电源 (setPowerSystem)</div><div>';
|
||||
POWER_CMDS.forEach(([v,n]) => { html += `<span class="chip" data-act="power" data-v="${v}">${n}</span>`; });
|
||||
html += "</div>";
|
||||
|
||||
html += '<div class="muted" style="margin-top:8px">任务启动 (taskStart)</div><div>';
|
||||
TASKS.forEach(([v,n]) => { html += `<span class="chip" data-act="task" data-v="${v}">${n}</span>`; });
|
||||
html += "</div>";
|
||||
|
||||
html += '<div class="muted" style="margin-top:8px">设备开关 (setDeviceSwitch)</div>';
|
||||
html += '<div class="row"><select id="devSel">' + DEVICES.map(([v,n])=>`<option value="${v}">${n}</option>`).join("") + "</select>" +
|
||||
'<select id="devCmd">' + DEV_CMDS.map(([v,n])=>`<option value="${v}">${n}</option>`).join("") + "</select>" +
|
||||
'<button id="btnDev">执行</button></div>';
|
||||
|
||||
box.innerHTML = html;
|
||||
|
||||
box.querySelectorAll(".chip").forEach(ch => {
|
||||
ch.onclick = () => {
|
||||
const act = ch.dataset.act, v = ch.dataset.v;
|
||||
if(act === "cond") sendMsg({ key: COMMANDS.setOpCondition.key, value: {state_uint8: Number(v)} });
|
||||
else if(act === "mode") sendMsg({ key: COMMANDS.setOpMode.key, value: {state_uint8: Number(v)} });
|
||||
else if(act === "power") sendMsg({ key: COMMANDS.setPowerSystem.key, value: {powerCmd_uint8: Number(v)} });
|
||||
else if(act === "task") sendMsg({ key: COMMANDS.taskStart.key, value: {taskType_str: v, balanceEnabled_bool: true, bowRudderUnfold_bool: true} });
|
||||
};
|
||||
});
|
||||
|
||||
el("btnDev").onclick = () => {
|
||||
sendMsg({ key: COMMANDS.setDeviceSwitch.key, value: { id_uint8: Number(el("devSel").value), cmd_uint8: Number(el("devCmd").value) } });
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(s){ return s.replace(/[&<>]/g, c => ({"&":"&","<":"<",">":">"}[c])); }
|
||||
|
||||
function renderMonitor(snap){
|
||||
const keys = Object.keys(snap).sort();
|
||||
el("monitorMeta").textContent = "收到 " + keys.length + " 个反馈变量 · " + new Date().toLocaleTimeString();
|
||||
const box = el("monitor");
|
||||
box.innerHTML = "";
|
||||
keys.forEach(k => {
|
||||
const raw = snap[k];
|
||||
let pretty = raw;
|
||||
try { pretty = JSON.stringify(JSON.parse(raw), null, 2); } catch(e){}
|
||||
const d = document.createElement("details");
|
||||
d.className = "var-item";
|
||||
d.innerHTML = `<summary>${k}</summary><pre>${escapeHtml(pretty)}</pre>`;
|
||||
box.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 连续数据曲线 ----
|
||||
function collectNumbers(node, prefix, out){
|
||||
if (Array.isArray(node)) return;
|
||||
if (typeof node === "number") { out[prefix] = node; return; }
|
||||
if (node && typeof node === "object") {
|
||||
for (const k in node) collectNumbers(node[k], prefix + "." + k, out);
|
||||
}
|
||||
}
|
||||
|
||||
function onSnapshot(snap){
|
||||
renderMonitor(snap);
|
||||
|
||||
const now = Date.now();
|
||||
const nums = {};
|
||||
for (const key in snap) {
|
||||
let obj; try { obj = JSON.parse(snap[key]); } catch(e){ continue; }
|
||||
collectNumbers(obj, key, nums);
|
||||
}
|
||||
for (const p in nums) allFields.add(p);
|
||||
|
||||
for (const p in history) {
|
||||
if (p in nums) {
|
||||
history[p].points.push([now, nums[p]]);
|
||||
if (history[p].points.length > MAX_POINTS) history[p].points.shift();
|
||||
}
|
||||
}
|
||||
|
||||
refreshFieldOptions();
|
||||
drawChart();
|
||||
}
|
||||
|
||||
function refreshFieldOptions(){
|
||||
const sel = el("fieldSel");
|
||||
const prev = sel.value;
|
||||
const arr = Array.from(allFields).sort();
|
||||
sel.innerHTML = arr.map(p => `<option value="${p}">${p}</option>`).join("");
|
||||
if (prev && arr.indexOf(prev) >= 0) sel.value = prev;
|
||||
}
|
||||
|
||||
function addSeries(path){
|
||||
if (!path || history[path]) return;
|
||||
history[path] = { color: COLORS[nextColor % COLORS.length], points: [] };
|
||||
nextColor++;
|
||||
renderSeriesBox();
|
||||
drawChart();
|
||||
}
|
||||
|
||||
function removeSeries(path){
|
||||
delete history[path];
|
||||
renderSeriesBox();
|
||||
drawChart();
|
||||
}
|
||||
|
||||
function renderSeriesBox(){
|
||||
const box = el("seriesBox");
|
||||
const paths = Object.keys(history);
|
||||
if (!paths.length) { box.innerHTML = '<span class="muted">未添加曲线</span>'; return; }
|
||||
box.innerHTML = paths.map(p =>
|
||||
`<span class="serie"><span class="swatch" style="background:${history[p].color}"></span>${p}` +
|
||||
`<span class="rm" data-path="${p}">×</span></span>`
|
||||
).join("");
|
||||
box.querySelectorAll(".rm").forEach(x => { x.onclick = () => removeSeries(x.dataset.path); });
|
||||
}
|
||||
|
||||
function fmt(v){ return Math.abs(v) >= 1000 ? v.toFixed(0) : (Math.abs(v) >= 10 ? v.toFixed(1) : v.toFixed(2)); }
|
||||
function fmtTime(t){ const d = new Date(t); return d.toLocaleTimeString(); }
|
||||
|
||||
function drawChart(){
|
||||
const canvas = el("chart");
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.clientWidth || 1000;
|
||||
const h = canvas.clientHeight || 280;
|
||||
canvas.width = Math.floor(w * dpr);
|
||||
canvas.height = Math.floor(h * dpr);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
ctx.fillStyle = "#0b1220"; ctx.fillRect(0,0,w,h);
|
||||
|
||||
const series = Object.keys(history).filter(k => history[k].points.length >= 1);
|
||||
if (!series.length){
|
||||
ctx.fillStyle = "#64748b"; ctx.font = "13px sans-serif"; ctx.textAlign = "center";
|
||||
ctx.fillText("添加曲线后显示连续数据趋势", w/2, h/2);
|
||||
return;
|
||||
}
|
||||
|
||||
let minT=Infinity, maxT=-Infinity, minV=Infinity, maxV=-Infinity;
|
||||
series.forEach(k => history[k].points.forEach(([t,v])=>{
|
||||
if(t<minT)minT=t; if(t>maxT)maxT=t; if(v<minV)minV=v; if(v>maxV)maxV=v;
|
||||
}));
|
||||
if(minV === maxV){ minV -= 1; maxV += 1; }
|
||||
if(minT === maxT){ minT -= 500; maxT += 500; }
|
||||
|
||||
const pad = {l:52,r:12,t:12,b:26};
|
||||
const pw = w - pad.l - pad.r, ph = h - pad.t - pad.b;
|
||||
const X = t => pad.l + (t-minT)/(maxT-minT)*pw;
|
||||
const Y = v => pad.t + (1-(v-minV)/(maxV-minV))*ph;
|
||||
|
||||
ctx.strokeStyle = "#1e293b"; ctx.fillStyle = "#94a3b8"; ctx.font = "10px sans-serif";
|
||||
for(let i=0;i<=4;i++){
|
||||
const val = minV + (maxV-minV)*i/4, yy = Y(val);
|
||||
ctx.textAlign = "right"; ctx.fillText(fmt(val), pad.l-6, yy+3);
|
||||
ctx.beginPath(); ctx.moveTo(pad.l, yy); ctx.lineTo(w-pad.r, yy); ctx.stroke();
|
||||
}
|
||||
for(let i=0;i<=4;i++){
|
||||
const tt = minT + (maxT-minT)*i/4, xx = X(tt);
|
||||
ctx.textAlign = "center"; ctx.fillText(fmtTime(tt), xx, h-pad.b+14);
|
||||
ctx.beginPath(); ctx.moveTo(xx, pad.t); ctx.lineTo(xx, h-pad.b); ctx.stroke();
|
||||
}
|
||||
|
||||
series.forEach(k => {
|
||||
const {color, points} = history[k];
|
||||
ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.beginPath();
|
||||
points.forEach(([t,v],i)=>{ const xx=X(t), yy=Y(v); if(i)ctx.lineTo(xx,yy); else ctx.moveTo(xx,yy); });
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
function init(){
|
||||
const sel = el("cmdType");
|
||||
Object.keys(COMMANDS).forEach(k => {
|
||||
const o = document.createElement("option");
|
||||
o.value = k; o.textContent = COMMANDS[k].label;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
sel.value = currentCmd;
|
||||
applyTemplate(currentCmd);
|
||||
sel.onchange = () => { currentCmd = sel.value; applyTemplate(currentCmd); };
|
||||
|
||||
el("btnSend").onclick = sendCurrent;
|
||||
el("btnReset").onclick = () => applyTemplate(currentCmd);
|
||||
el("btnRawSend").onclick = () => {
|
||||
const k = el("rawKey").value.trim();
|
||||
const v = el("rawValue").value.trim();
|
||||
if(!k){ log("变量名为空", "err"); return; }
|
||||
sendMsg({ key: k, value: v });
|
||||
};
|
||||
|
||||
el("btnAddSeries").onclick = () => addSeries(el("fieldSel").value);
|
||||
el("btnClearSeries").onclick = () => { history = {}; nextColor = 0; renderSeriesBox(); drawChart(); };
|
||||
|
||||
window.addEventListener("resize", drawChart);
|
||||
|
||||
buildQuick();
|
||||
renderSeriesBox();
|
||||
drawChart();
|
||||
connect();
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user