Files
code-test-drone/ground_station/app.py
T
keita ded17d7995 实现预定流程全链路: 任务状态机 + 编排器 + 地面站 + 真机/SITL入口
- 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结论与实现状态
2026-10-13 15:50:32 +08:00

140 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ground_station/app.py —— 地面站页面(Flask,临时测试用)。
预定流程 C 阶段:地面坐标输入 + 状态可视化配套页面。
功能:
* GET / 地面站页面(坐标输入 + 状态面板 + 模拟视野)
* POST /api/inject 接收 {lat, lon, alt},UDP 发到吊舱桥接的 ground_inject 端口
(默认 14555,GroundLink.run() 接收)
* GET /api/state 返回最新任务状态(读任务日志:事件最后状态 + 遥测最后一行)
与无人机 runner 解耦:本页面只做「发坐标(UDP)+ 读日志(只读)」,不侵入 runner。
用法:
python ground_station/app.py --udp-port 14555 --log-dir logs
"""
import argparse
import csv
import json
import os
import socket
from pathlib import Path
from flask import Flask, jsonify, render_template, request
_GROUND_UDP_PORT = 14555
_LOG_DIR = "logs"
def _read_last_line(path: Path):
"""读文件最后一行(tail,避免读全文)。"""
try:
with open(path, "rb") as f:
f.seek(0, 2)
size = f.tell()
if size == 0:
return None
f.seek(max(0, size - 8192))
lines = f.read().decode("utf-8", errors="replace").splitlines()
return lines[-1] if lines else None
except OSError:
return None
def _latest_state(log_dir: str) -> dict:
"""从任务日志读出当前状态 + 最新遥测(只读,供 /api/state)。"""
log_path = Path(log_dir)
state = "unknown"
events = []
# 事件日志:找最后一个 state 事件(当前任务状态)
events_path = log_path / "task_events.jsonl"
if events_path.exists():
try:
with open(events_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
events.append(ev)
if ev.get("type") == "state":
state = ev.get("to_state", state)
except OSError:
pass
# 遥测最后一行
telemetry = None
tel_path = log_path / "task_telemetry.csv"
last = _read_last_line(tel_path)
if last:
try:
with open(tel_path, encoding="utf-8") as f:
header = f.readline().strip().split(",")
values = last.split(",")
telemetry = dict(zip(header, values))
except OSError:
pass
return {"state": state, "telemetry": telemetry,
"events": events[-20:], "log_dir": str(log_path)}
def create_app(udp_port: int, log_dir: str) -> Flask:
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html", udp_port=udp_port)
@app.route("/api/inject", methods=["POST"])
def inject():
data = request.get_json(silent=True) or {}
lat = data.get("lat")
lon = data.get("lon")
if lat is None or lon is None:
return jsonify({"ok": False, "error": "需要 lat 和 lon"}), 400
try:
lat = float(lat)
lon = float(lon)
except (TypeError, ValueError):
return jsonify({"ok": False, "error": "lat/lon 必须是数值"}), 400
alt = float(data.get("alt", 0.0) or 0.0)
payload = json.dumps({"lat": lat, "lon": lon, "alt": alt})
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto(payload.encode("utf-8"), ("127.0.0.1", udp_port))
except OSError as exc:
return jsonify({"ok": False, "error": f"UDP 发送失败: {exc}"}), 500
return jsonify({"ok": True, "sent": {"lat": lat, "lon": lon, "alt": alt}})
@app.route("/api/state")
def state():
return jsonify(_latest_state(log_dir))
return app
def main() -> int:
ap = argparse.ArgumentParser(description="地面站页面(Flask)")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=5000, help="Flask 监听端口")
ap.add_argument("--udp-port", type=int, default=_GROUND_UDP_PORT,
help="坐标注入 UDP 端口(GroundLink 监听)")
ap.add_argument("--log-dir", default=_LOG_DIR, help="任务日志目录")
args = ap.parse_args()
app = create_app(args.udp_port, args.log_dir)
print(f"[地面站] http://{args.host}:{args.port} | UDP 注入 → 127.0.0.1:{args.udp_port}"
f" | 日志 → {args.log_dir}")
app.run(host=args.host, port=args.port, debug=False)
return 0
if __name__ == "__main__":
main()