归档 legacy/:stereo_yolo/stereo_ranger/target_tracker 三包 + 175 条测试 + demo/配置/权重/三份规格;运行链路不再依赖,默认 pytest 446 条,归档测试按需运行。 目标有效性第二判据改为 LOS 运动学一致性(pod_bridge/los_consistency.py),删除 require_vision;pod_locked() 叠加该判据,状态机随之后退重搜。 自定义返航(task_runner/return_home.py):保持转场高度回起飞点 → 恢复初始航向 → 垂直降落,不再爬 RTL_RETURN_ALT;未起飞/无起飞点/超时/异常仍退回 PX4 原生 RTL。 任务链安全兜底:主循环异常→紧急返航、stop 幂等、landing_stop 保留遥测、in_air 流复用+超时、解锁/起飞与吊舱识别仅执行一次、rtl_timeout 上限。 入口共享件拆到 utils/sitl_support.py;pod_sitl_test.py→l1_sitl_test.py;删除旧架构 test.py;相机脚本移入 tools/。 新增 .gitignore 并剔除历史误跟踪的 59 个 .pyc;requirements 按实际依赖重写;文档全面对齐,新增 legacy/README.md 与 tools/README.md。
425 lines
19 KiB
HTML
425 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>地面站 · 预定流程</title>
|
||
<style>
|
||
:root { --bg:#f7f4ec; --card:#fffdf7; --ink:#2f3b2f; --mut:#6b756b;
|
||
--green:#2f5d46; --green-ink:#eaf3de; --line:#e2ddcf; --red:#b04a3a; }
|
||
* { box-sizing: border-box; }
|
||
body { margin:0; font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
|
||
background:var(--bg); color:var(--ink); }
|
||
header { padding:16px 24px; border-bottom:1px solid var(--line); background:var(--card); }
|
||
header h1 { margin:0; font-size:18px; font-weight:600; }
|
||
header .sub { color:var(--mut); font-size:12px; margin-top:2px; }
|
||
.wrap { display:flex; gap:16px; padding:16px 24px; flex-wrap:wrap; align-items:flex-start; }
|
||
.col { background:var(--card); border:1px solid var(--line); border-radius:10px;
|
||
padding:16px; min-width:280px; }
|
||
.col h2 { margin:0 0 12px; font-size:14px; font-weight:600; color:var(--green); }
|
||
label { display:block; font-size:12px; color:var(--mut); margin:8px 0 4px; }
|
||
input { width:100%; padding:8px; border:1px solid var(--line); border-radius:6px;
|
||
font-size:14px; background:#fff; color:var(--ink); }
|
||
button { margin-top:12px; padding:9px 16px; border:none; border-radius:6px;
|
||
background:var(--green); color:var(--green-ink); font-size:14px; cursor:pointer; }
|
||
button:active { opacity:.85; }
|
||
button.sec { background:#5b6a5e; }
|
||
button.danger { background:var(--red); color:#fff; }
|
||
.btn-row { display:flex; flex-wrap:wrap; gap:8px; }
|
||
.btn-row button { margin-top:0; flex:1 1 auto; }
|
||
input[type=checkbox] { width:auto; }
|
||
.kv { display:flex; justify-content:space-between; padding:5px 0;
|
||
border-bottom:1px dashed var(--line); font-size:13px; }
|
||
.kv:last-child { border-bottom:none; }
|
||
.kv .k { color:var(--mut); }
|
||
.kv .v { font-variant-numeric:tabular-nums; }
|
||
.badge { display:inline-block; padding:2px 10px; border-radius:12px; font-size:12px;
|
||
background:var(--green); color:var(--green-ink); }
|
||
.badge.warn { background:var(--red); color:#fff; }
|
||
canvas { background:#0e1410; border-radius:8px; display:block; }
|
||
.hint { color:var(--mut); font-size:12px; margin-top:8px; }
|
||
.nav { margin-top:8px; font-size:13px; }
|
||
.nav a { color:var(--green); text-decoration:none; margin-right:14px; }
|
||
.nav a.active { color:var(--ink); font-weight:600; text-decoration:underline; }
|
||
select { width:100%; padding:8px; border:1px solid var(--line); border-radius:6px;
|
||
font-size:14px; background:#fff; color:var(--ink); }
|
||
.badge.mut { background:#c9c3b2; color:#4a514a; }
|
||
.log-list { max-height:300px; overflow:auto; font-size:12.5px;
|
||
font-family:Consolas,"Courier New",monospace; border:1px solid var(--line);
|
||
border-radius:8px; padding:8px 10px; }
|
||
.log-line { padding:2px 0; border-bottom:1px dashed var(--line); word-break:break-all; }
|
||
.log-line:last-child { border-bottom:none; }
|
||
.log-t { color:var(--mut); margin-right:6px; }
|
||
.lv-state { color:var(--green); }
|
||
.lv-info { color:#3a6ea5; }
|
||
.lv-warn { color:#b06a2a; }
|
||
.lv-err { color:var(--red); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>地面站 · 预定流程</h1>
|
||
<div class="sub">坐标注入 + 任务/吊舱控制 + 状态可视化(临时测试用)</div>
|
||
<nav class="nav"><a class="active" href="/">主页面</a><a href="/px4">PX4 信息</a></nav>
|
||
</header>
|
||
|
||
<div class="wrap">
|
||
<div class="col">
|
||
<h2>链路目标 <span class="badge" id="link-badge">-</span></h2>
|
||
<label>坐标与命令经 UDP 发往此地址</label>
|
||
<select id="link-sel"></select>
|
||
<input id="link-custom" type="text" placeholder="自定义 IP,如 192.168.144.10" style="display:none;margin-top:8px">
|
||
<button onclick="applyLink()">切换目标</button>
|
||
<div class="hint" id="link-msg">默认「本地回环」= 本机跑仿真/runner;真机可切到机载或伴侣机 IP。</div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<h2>坐标注入</h2>
|
||
<label>坐标形式</label>
|
||
<select id="coord-mode" onchange="applyCoordMode()">
|
||
<option value="geo">经纬度(绝对)</option>
|
||
<option value="local">局部坐标(相对原点,北/东 米)</option>
|
||
</select>
|
||
<div id="origin-row" style="display:none">
|
||
<label>局部原点</label>
|
||
<select id="origin-mode">
|
||
<option value="current">以当前坐标为原点(注入时刻无人机位置)</option>
|
||
<option value="home">以起飞点(home)为原点(本次运行起始位置)</option>
|
||
</select>
|
||
</div>
|
||
<label id="lbl-a">纬度 lat</label>
|
||
<input id="lat" type="text" value="47.398241" placeholder="如 47.398241">
|
||
<label id="lbl-b">经度 lon</label>
|
||
<input id="lon" type="text" value="8.546363" placeholder="如 8.546363">
|
||
<label>高度 alt (m)</label>
|
||
<input id="alt" type="text" value="0">
|
||
<button id="inject-btn" onclick="sendInject()">注入坐标</button>
|
||
<div class="hint">局部坐标按选定原点换算(起飞前注入即为起飞点;「当前坐标」模式随注入时刻无人机位置变化)。</div>
|
||
<label style="display:flex;align-items:center;gap:6px;margin-top:10px">
|
||
<input type="checkbox" id="cont" onchange="toggleContinuous()">
|
||
持续注入(服务端每 0.5s 自动重发)
|
||
</label>
|
||
<div class="hint">坐标须持续注入:断流超过 5s 判失效(任务将返航)。持续注入由服务端执行,
|
||
切窗口/最小化不受影响;取消勾选或关闭地面站程序才停止。</div>
|
||
<div class="hint" id="inject-msg"></div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<h2>任务控制</h2>
|
||
<div class="btn-row">
|
||
<button onclick="sendCmd('rtl', '确认请求返航(RTL)?')">请求返航 RTL</button>
|
||
<button class="danger" onclick="sendCmd('emergency', '确认紧急终止(立即返航)?')">紧急终止</button>
|
||
</div>
|
||
<div class="hint">RTL = 人工返航请求;紧急 = 最高优先级立即返航(均经 UDP 下发到 runner)。</div>
|
||
<div class="hint" id="cmd-msg-t"></div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<h2>吊舱控制</h2>
|
||
<div class="btn-row">
|
||
<button class="sec" onclick="sendCmd('pod_detect_on')">开识别</button>
|
||
<button class="sec" onclick="sendCmd('pod_auto_lock')">自动锁定</button>
|
||
<button class="sec" onclick="sendCmd('pod_stop_track')">停跟踪</button>
|
||
<button class="sec" onclick="sendCmd('pod_detect_off')">关识别</button>
|
||
<button class="sec" onclick="sendCmd('pod_center')">归中</button>
|
||
</div>
|
||
<div class="hint">对齐 pod_console 按键:开识别 d(0x07+0x0F) / 自动锁定 a(0x0F+0x10) /
|
||
停跟踪 u(0x0E,仅解当前锁,0x0F 自动锁定仍常驻) / 关识别 s(0x0E+0x08,彻底退出) / 归中 c(0x2B)</div>
|
||
<div class="hint">真机经 runner → 吊舱串口下发;SITL 无吊舱控制口,命令将被忽略。</div>
|
||
<div class="hint" id="cmd-msg-p"></div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<h2>任务状态 <span class="badge" id="state">-</span></h2>
|
||
<div class="kv"><span class="k">位置</span><span class="v" id="pos">-</span></div>
|
||
<div class="kv"><span class="k">距目标</span><span class="v" id="dist">-</span></div>
|
||
<div class="kv"><span class="k">锁定</span><span class="v" id="locked">-</span></div>
|
||
<div class="kv"><span class="k">脱靶量 (x/y)</span><span class="v" id="off">-</span></div>
|
||
<div class="kv"><span class="k">控制</span><span class="v" id="mode">-</span></div>
|
||
<div class="kv"><span class="k">yaw_rate / vx</span><span class="v" id="cmd">-</span></div>
|
||
<div class="hint" id="log-dir"></div>
|
||
</div>
|
||
|
||
<div class="col" style="flex:1 1 400px">
|
||
<h2>任务日志 <span class="badge mut" id="log-count">0</span></h2>
|
||
<div class="log-list" id="log-list"><div class="log-line">加载中…</div></div>
|
||
<div class="hint">阶段切换 / 地面站命令 / 飞控状态(STATUSTEXT 警告)实时滚动</div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<h2>模拟视野(脱靶量)</h2>
|
||
<canvas id="view" width="300" height="200"></canvas>
|
||
<div class="hint">十字线 = 视野中心,红点 = 目标(按吊舱脱靶量映射)</div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<h2>吊舱视频 <span class="badge" id="vid-badge">-</span></h2>
|
||
<img id="vid" alt="吊舱视频流" style="display:none; width:320px; border-radius:8px; background:#0e1410">
|
||
<div class="hint" id="vid-hint">检测中…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const FOV_H = 60, FOV_V = 35;
|
||
let vidReloadAt = 0;
|
||
|
||
function $(id){ return document.getElementById(id); }
|
||
|
||
function applyCoordMode(){
|
||
const local = $("coord-mode").value === "local";
|
||
$("origin-row").style.display = local ? "block" : "none";
|
||
$("lbl-a").textContent = local ? "北向偏移 north (m)" : "纬度 lat";
|
||
$("lbl-b").textContent = local ? "东向偏移 east (m)" : "经度 lon";
|
||
$("lat").value = local ? "30" : "47.398241";
|
||
$("lon").value = local ? "15" : "8.546363";
|
||
$("lat").placeholder = local ? "如 30(北为正)" : "如 47.398241";
|
||
$("lon").placeholder = local ? "如 15(东为正)" : "如 8.546363";
|
||
$("inject-btn").textContent = local ? "注入局部坐标" : "注入坐标";
|
||
}
|
||
|
||
async function sendInject(){
|
||
const local = $("coord-mode").value === "local";
|
||
const a = $("lat").value, b = $("lon").value, alt = $("alt").value || "0";
|
||
const msg = $("inject-msg");
|
||
const body = local
|
||
? {north_m: Number(a), east_m: Number(b), alt: Number(alt),
|
||
origin: $("origin-mode").value}
|
||
: {lat: Number(a), lon: Number(b), alt: Number(alt)};
|
||
try {
|
||
const r = await fetch("/api/inject", {
|
||
method:"POST", headers:{"Content-Type":"application/json"},
|
||
body: JSON.stringify(body)
|
||
});
|
||
const j = await r.json();
|
||
if (j.ok) {
|
||
if (local && j.local){
|
||
const orgLabel = j.local.origin === "home" ? "起飞点" : "当前坐标";
|
||
msg.textContent = `已注入(局部 北${j.local.north_m}m/东${j.local.east_m}m → `
|
||
+ `${j.sent.lat.toFixed(6)}, ${j.sent.lon.toFixed(6)};`
|
||
+ `原点(${orgLabel}) ${j.local.origin_lat.toFixed(6)}, ${j.local.origin_lon.toFixed(6)})`;
|
||
} else {
|
||
msg.textContent = `已注入 (${j.sent.lat}, ${j.sent.lon})`;
|
||
}
|
||
msg.style.color = "var(--green)";
|
||
} else {
|
||
msg.textContent = `失败: ${j.error}`;
|
||
msg.style.color = "var(--red)";
|
||
}
|
||
} catch(e){ msg.textContent = "请求失败: "+e; msg.style.color = "var(--red)"; }
|
||
}
|
||
|
||
async function toggleContinuous(){
|
||
const on = $("cont").checked;
|
||
const msg = $("inject-msg");
|
||
try {
|
||
const r = await fetch("/api/continuous", {
|
||
method:"POST", headers:{"Content-Type":"application/json"},
|
||
body: JSON.stringify({on})
|
||
});
|
||
const j = await r.json();
|
||
if (!j.ok) {
|
||
$("cont").checked = !on; // 失败回滚勾选
|
||
msg.textContent = `失败: ${j.error}`;
|
||
msg.style.color = "var(--red)";
|
||
return;
|
||
}
|
||
msg.textContent = j.continuous ? "持续注入中(服务端发送,每 0.5s)" : "已停止持续注入";
|
||
msg.style.color = j.continuous ? "var(--green)" : "var(--mut)";
|
||
} catch(e){
|
||
$("cont").checked = !on;
|
||
msg.textContent = "请求失败: "+e;
|
||
msg.style.color = "var(--red)";
|
||
}
|
||
}
|
||
|
||
async function sendCmd(cmd, ask){
|
||
if (ask && !confirm(ask)) return;
|
||
const msg = (cmd==="rtl" || cmd==="emergency") ? $("cmd-msg-t") : $("cmd-msg-p");
|
||
try {
|
||
const r = await fetch("/api/command", {
|
||
method:"POST", headers:{"Content-Type":"application/json"},
|
||
body: JSON.stringify({cmd})
|
||
});
|
||
const j = await r.json();
|
||
msg.textContent = j.ok ? `已发送: ${j.sent.cmd}` : `失败: ${j.error}`;
|
||
msg.style.color = j.ok ? "var(--green)" : "var(--red)";
|
||
} catch(e){ msg.textContent = "请求失败: "+e; msg.style.color = "var(--red)"; }
|
||
}
|
||
|
||
function fmt(v, d){ return (v===null||v===undefined||v==="") ? "-" : Number(v).toFixed(d); }
|
||
|
||
function drawView(offX, offY){
|
||
const cv = $("view"), ctx = cv.getContext("2d");
|
||
const w = cv.width, h = cv.height, cx = w/2, cy = h/2;
|
||
ctx.clearRect(0,0,w,h);
|
||
ctx.strokeStyle = "#3d5343"; ctx.lineWidth = 1;
|
||
ctx.strokeRect(0,0,w,h);
|
||
ctx.strokeStyle = "#5f7a68"; ctx.setLineDash([4,4]);
|
||
ctx.beginPath(); ctx.moveTo(cx,0); ctx.lineTo(cx,h);
|
||
ctx.moveTo(0,cy); ctx.lineTo(w,cy); ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
if(offX===null||offX===undefined){ return; }
|
||
const x = cx + (offX / FOV_H) * w;
|
||
const y = cy - (offY / FOV_V) * h; // 上正 → 屏幕减
|
||
ctx.fillStyle = "#e0574a";
|
||
ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2); ctx.fill();
|
||
}
|
||
|
||
async function poll(){
|
||
try {
|
||
const r = await fetch("/api/state");
|
||
const j = await r.json();
|
||
const t = j.telemetry || {};
|
||
const state = j.state || "unknown";
|
||
const s = $("state");
|
||
s.textContent = state;
|
||
s.className = "badge" + (state==="rtl"||state==="wait" ? " warn" : "");
|
||
$("pos").textContent = t.latitude_deg ? `${fmt(t.latitude_deg,6)}, ${fmt(t.longitude_deg,6)}` : "-";
|
||
$("dist").textContent = t.distance_to_target_m ? fmt(t.distance_to_target_m,1)+" m" : "-";
|
||
$("locked").textContent = t.target_locked==="True" ? "锁定" : "未锁定";
|
||
$("off").textContent = t.off_target_x_deg ? `${fmt(t.off_target_x_deg,2)}° / ${fmt(t.off_target_y_deg,2)}°` : "-";
|
||
$("mode").textContent = t.mode || "-";
|
||
$("cmd").textContent = t.yaw_rate_dps ? `${fmt(t.yaw_rate_dps,1)}°/s / ${fmt(t.vx_body_mps,2)} m/s` : "-";
|
||
$("log-dir").textContent = "日志: " + (j.log_dir || "-");
|
||
// 持续注入开关与服务端同步(刷新/多标签时不失真)
|
||
if (typeof j.continuous === "boolean" && $("cont").checked !== j.continuous) {
|
||
$("cont").checked = j.continuous;
|
||
const m = $("inject-msg");
|
||
m.textContent = j.continuous ? "持续注入中(服务端发送,每 0.5s)" : "";
|
||
m.style.color = j.continuous ? "var(--green)" : "var(--mut)";
|
||
}
|
||
// 吊舱视频面板:状态徽标 + 断流自动重挂(限频 5s,避免打断重连)
|
||
const v = j.video || {};
|
||
const img = $("vid"), badge = $("vid-badge");
|
||
if (!v.enabled) {
|
||
img.style.display = "none";
|
||
badge.textContent = "未配置"; badge.className = "badge warn";
|
||
$("vid-hint").textContent = "服务端未配置视频源(--rtsp-url)";
|
||
} else {
|
||
img.style.display = "block";
|
||
badge.textContent = v.online ? "在线" : "断流";
|
||
badge.className = "badge" + (v.online ? "" : " warn");
|
||
$("vid-hint").textContent = v.online
|
||
? "服务端 RTSP → MJPEG 实时画面"
|
||
: "视频流中断,自动重连中…";
|
||
const now = Date.now(), src = img.getAttribute("src");
|
||
if (!v.online) {
|
||
if (!src || now - vidReloadAt > 5000) { img.src = "/video_feed?t=" + now; vidReloadAt = now; }
|
||
} else if (!src) {
|
||
img.src = "/video_feed";
|
||
}
|
||
}
|
||
// 任务日志面板(阶段切换 / 地面站命令 / 飞控状态)
|
||
renderTaskLog(j.events || []);
|
||
drawView(parseFloat(t.off_target_x_deg), parseFloat(t.off_target_y_deg));
|
||
} catch(e){ /* 后端未起时静默 */ }
|
||
}
|
||
|
||
// ============================================================ 链路目标
|
||
let linkPopulated = false;
|
||
|
||
async function loadLink(){
|
||
try {
|
||
const r = await fetch("/api/link");
|
||
const j = await r.json();
|
||
$("link-badge").textContent = `${j.host}:${j.port}`;
|
||
if (linkPopulated) return;
|
||
const sel = $("link-sel");
|
||
sel.innerHTML = "";
|
||
for (const o of j.options){
|
||
const opt = document.createElement("option");
|
||
opt.value = o.host;
|
||
opt.textContent = `${o.label}(${o.host})`;
|
||
sel.appendChild(opt);
|
||
}
|
||
const c = document.createElement("option");
|
||
c.value = "__custom__"; c.textContent = "自定义…";
|
||
sel.appendChild(c);
|
||
sel.value = j.host;
|
||
if (sel.value !== j.host){ // 当前目标不在列表 → 自定义项承载
|
||
sel.value = "__custom__";
|
||
$("link-custom").value = j.host;
|
||
$("link-custom").style.display = "block";
|
||
}
|
||
sel.onchange = () => {
|
||
$("link-custom").style.display = sel.value === "__custom__" ? "block" : "none";
|
||
};
|
||
linkPopulated = true;
|
||
} catch(e){ /* 后端未起时静默 */ }
|
||
}
|
||
|
||
async function applyLink(){
|
||
const sel = $("link-sel");
|
||
const host = sel.value === "__custom__" ? $("link-custom").value.trim() : sel.value;
|
||
const msg = $("link-msg");
|
||
try {
|
||
const r = await fetch("/api/link", {
|
||
method:"POST", headers:{"Content-Type":"application/json"},
|
||
body: JSON.stringify({host})
|
||
});
|
||
const j = await r.json();
|
||
msg.textContent = j.ok ? `已切换到 ${j.host}:${j.port}` : `失败: ${j.error}`;
|
||
msg.style.color = j.ok ? "var(--green)" : "var(--red)";
|
||
if (j.ok) $("link-badge").textContent = `${j.host}:${j.port}`;
|
||
} catch(e){ msg.textContent = "请求失败: " + e; msg.style.color = "var(--red)"; }
|
||
}
|
||
|
||
// ============================================================ 任务日志
|
||
const STATE_CN = {
|
||
wait: "待命", arm_takeoff: "起飞", goto: "坐标引导", track: "追踪",
|
||
approach: "接近悬停", rtl: "返航"
|
||
};
|
||
let lastLogKey = "";
|
||
|
||
function fmtT(t){
|
||
if (t === null || t === undefined) return "--:--.-";
|
||
const m = Math.floor(t / 60), s = t % 60;
|
||
return String(m).padStart(2, "0") + ":" + s.toFixed(1).padStart(4, "0");
|
||
}
|
||
|
||
function evLine(e){
|
||
if (e.type === "state"){
|
||
const a = STATE_CN[e.from_state] || e.from_state || "?";
|
||
const b = STATE_CN[e.to_state] || e.to_state || "?";
|
||
return { cls: "lv-state", txt: `阶段切换: ${a} → ${b}(${e.reason || ""})` };
|
||
}
|
||
if (e.type === "command") return { cls: "lv-info", txt: `地面站命令: ${e.name}` };
|
||
if (e.type === "fc_status"){
|
||
const sev = String(e.severity || "").toUpperCase();
|
||
const cls = sev.includes("ERR") ? "lv-err" : (sev.includes("WARN") ? "lv-warn" : "lv-info");
|
||
return { cls, txt: `飞控 ${sev || "STATUS"}: ${e.text || ""}` };
|
||
}
|
||
if (e.type === "takeoff") return { cls: "lv-state", txt: `起飞至 ${e.altitude_m} m` };
|
||
if (e.type === "takeoff_failed") return { cls: "lv-err", txt: `起飞失败: ${e.error || ""}` };
|
||
if (e.type === "rtl") return { cls: "lv-state", txt: "开始返航" };
|
||
const parts = [];
|
||
for (const [k, v] of Object.entries(e)){
|
||
if (["type", "ts_ms", "t_s"].includes(k)) continue;
|
||
parts.push(`${k}=${v}`);
|
||
}
|
||
return { cls: "", txt: `${e.type || "事件"}: ${parts.join(", ")}` };
|
||
}
|
||
|
||
function renderTaskLog(evs){
|
||
const tail = evs.length ? evs[evs.length - 1] : null;
|
||
const key = evs.length + "|" + (tail ? `${tail.ts_ms}|${tail.type}` : "");
|
||
if (key === lastLogKey) return; // 无变化不重建,避免打断滚动
|
||
lastLogKey = key;
|
||
const box = $("log-list");
|
||
box.innerHTML = evs.length
|
||
? evs.map(e => {
|
||
const l = evLine(e);
|
||
return `<div class="log-line ${l.cls}"><span class="log-t">${fmtT(e.t_s)}</span>${l.txt}</div>`;
|
||
}).join("")
|
||
: '<div class="log-line">暂无事件(等待任务运行)</div>';
|
||
box.scrollTop = box.scrollHeight;
|
||
$("log-count").textContent = evs.length;
|
||
}
|
||
|
||
loadLink();
|
||
poll();
|
||
setInterval(poll, 1000);
|
||
</script>
|
||
</body>
|
||
</html>
|