构建:修复 GTest 链接失败——显式指定 Linux GTest 配置,避免从 PATH 探测到 Windows Anaconda 的 .lib

This commit is contained in:
zjk
2026-08-18 19:45:50 +08:00
parent a6da5e3ae9
commit f3dd9e2c5a
20 changed files with 4629 additions and 6 deletions
+720
View File
@@ -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 => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[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>