- utils/task_runner: 6态状态机(WAIT→ARM_TAKEOFF→GOTO(L2)→TRACK(L1)→APPROACH→RTL) + 编排器(快照采集/状态动作) + 双层日志(事件JSONL/遥测CSV) + 吊舱控制指令(0x07/0x0F/0x0E/0x08) - pod_bridge: ground_link 地面坐标UDP注入(超龄判无效,无激光替代表4); set_heading_servo_enabled() 运行时L1/L2切换(切换即reset清积分防输出跳变); pod_locked 提为公共门控; config 新增 ground_inject 段 - virtual_pod 模拟吊舱(无电机语义) + verify_px4 增量④仅飞控验证 + pod_sitl_test L1 SITL全流程(飞行编排不塞进 run_bridge, 保持纯offboard闭环入口) - 入口: task_runner_sitl(VirtualPod+虚拟目标, SITL端到端跑通约1m24s) / task_runner_real(真串口+UDP注入+吊舱控制) - 地面站 ground_station: 坐标注入 + 状态可视化(Flask) - S1标定回填(2026-10-09): beta_sign=+1 / alpha_sign=-1, 加标定回归测试守护 - 新增测试: task_runner x4 + ground_link/virtual_pod*/bridge/px4_gateway 增量; 全量 483 passed - 文档: 预定流程(需求) + 预定流程-操作指南(操作/待办) + L1方案补S1结论与实现状态
138 lines
6.0 KiB
HTML
138 lines
6.0 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; }
|
|
.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; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>地面站 · 预定流程</h1>
|
|
<div class="sub">地面坐标注入 + 状态可视化(临时测试用)</div>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
<div class="col">
|
|
<h2>坐标注入</h2>
|
|
<label>纬度 lat</label>
|
|
<input id="lat" type="text" value="47.398241" placeholder="如 47.398241">
|
|
<label>经度 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 onclick="sendInject()">注入坐标</button>
|
|
<div class="hint" id="inject-msg"></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">
|
|
<h2>模拟视野(脱靶量)</h2>
|
|
<canvas id="view" width="300" height="200"></canvas>
|
|
<div class="hint">十字线 = 视野中心,红点 = 目标(按吊舱脱靶量映射)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const FOV_H = 60, FOV_V = 35;
|
|
|
|
function $(id){ return document.getElementById(id); }
|
|
|
|
async function sendInject(){
|
|
const lat = $("lat").value, lon = $("lon").value, alt = $("alt").value || "0";
|
|
const msg = $("inject-msg");
|
|
try {
|
|
const r = await fetch("/api/inject", {
|
|
method:"POST", headers:{"Content-Type":"application/json"},
|
|
body: JSON.stringify({lat:Number(lat), lon:Number(lon), alt:Number(alt)})
|
|
});
|
|
const j = await r.json();
|
|
msg.textContent = j.ok ? `已注入 (${j.sent.lat}, ${j.sent.lon})` : `失败: ${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 || "-");
|
|
drawView(parseFloat(t.off_target_x_deg), parseFloat(t.off_target_y_deg));
|
|
} catch(e){ /* 后端未起时静默 */ }
|
|
}
|
|
|
|
poll();
|
|
setInterval(poll, 1000);
|
|
</script>
|
|
</body>
|
|
</html>
|