实现预定流程全链路: 任务状态机 + 编排器 + 地面站 + 真机/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结论与实现状态
This commit is contained in:
keita
2026-10-13 15:50:32 +08:00
parent 5a944af4f5
commit ded17d7995
30 changed files with 3552 additions and 27 deletions
+58 -5
View File
@@ -31,6 +31,7 @@ from visual_track_control.types import (ControlMode, ControlSnapshot,
TrackState)
from .config import BridgeConfig
from .ground_link import GroundTarget
from .guidance import GuidanceInput, GuidanceLaw
from .pod_serial import PodLink, PodSerial
from .protocol import (CarrierAttitude, ControlCommand, TargetGeo, Telemetry,
@@ -48,12 +49,15 @@ class PodBridge:
"""px4:Px4Gateway(或等价 mock);pod:PodLink(PodSerial 或内存 FakePod)。"""
def __init__(self, cfg: BridgeConfig, px4: Px4Gateway, pod: PodLink,
now_ms_fn=_monotonic_ms, vision_target_fn=None) -> None:
now_ms_fn=_monotonic_ms, vision_target_fn=None,
ground_target_fn=None) -> None:
self._cfg = cfg
self._px4 = px4
self._pod = pod
self._now_ms = now_ms_fn
self._vision_target_fn = vision_target_fn # §2.3 第二判据(可选,S3+ 接入我方视觉)
self._ground_target_fn = ground_target_fn # 地面注入坐标访问器(A2,无激光替代表4)
self._use_heading_servo = cfg.heading_servo.enabled # 运行时控制律选择(A3 状态机切换)
self._guidance = GuidanceLaw(cfg.guidance)
self._tracking = TrackingControlLaw(cfg.tracking) # L1 航向伺服(方案 §0)
self._attitude_period_ms = max(
@@ -114,22 +118,36 @@ class PodBridge:
def _active_frequency_hz(self) -> float:
"""当前生效控制律的循环频率:L1 用 tracking.control,L2 用 guidance。"""
if self._cfg.heading_servo.enabled:
if self._use_heading_servo:
return self._cfg.tracking.control.frequency_hz
return self._cfg.guidance.frequency_hz
def set_heading_servo_enabled(self, enabled: bool) -> None:
"""运行时切换控制律(任务状态机 GOTO(L2) ↔ TRACK(L1) 切换,A3)。
切换时 reset 即将启用的律(清积分/限幅器历史),使指令从零渐变,
避免切换瞬间输出跳变。L1 用吊舱角度直入,L2 用地理引导(注入坐标/表4)。
"""
if enabled == self._use_heading_servo:
return
self._use_heading_servo = enabled
if enabled:
self._tracking.reset()
else:
self._guidance.reset()
async def step(self, now_ms: int):
"""单控制周期(显式传入时钟,便于确定性单测)。返回 ControlOutput。
按 heading_servo.enabled 在 L1(TrackingControlLaw,吊舱角度直入)与
L2(GuidanceLaw,表4 地理引导)间切换(方案 §7)。
按运行时选择的控制律在 L1(TrackingControlLaw,吊舱角度直入)与
L2(GuidanceLaw,表4/注入坐标地理引导)间切换(方案 §7)。
"""
self._ingest_pod_frames(now_ms)
own = self._px4.own_state()
self._downlink_attitude(own, now_ms)
if self._cfg.heading_servo.enabled:
if self._use_heading_servo:
out = self._tracking.update(self._build_snapshot(own, now_ms), now_ms)
else:
out = self._guidance.update(
@@ -175,6 +193,21 @@ class PodBridge:
# --- 飞控侧 -------------------------------------------------------------
def _build_guidance_input(self, own, now_ms: int) -> GuidanceInput:
injected = self._injected_target(now_ms)
if injected is not None:
# 地面注入坐标优先(本吊舱无激光 → 表4 永不出,目标坐标改走地面站)
return GuidanceInput(
own_lat_deg=own.latitude_deg,
own_lon_deg=own.longitude_deg,
own_yaw_deg=math.degrees(own.yaw_rad),
own_position_valid=own.position_valid,
own_position_time_ms=own.position_time_ms, # position 流专用时间戳
target_lat_deg=injected.latitude_deg,
target_lon_deg=injected.longitude_deg,
target_valid=True, # 注入坐标已验证新鲜
target_time_ms=injected.time_ms,
)
# 回退表4(有激光吊舱 / 未接入地面站)
tgt = self._latest_target
locked = self._pod_locked(now_ms)
return GuidanceInput(
@@ -189,6 +222,26 @@ class PodBridge:
target_time_ms=tgt[1] if tgt else 0,
)
def _injected_target(self, now_ms: int) -> Optional[GroundTarget]:
"""地面注入坐标(新鲜有效则返回,否则 None → 回退表4 / 无目标)。
本吊舱无激光 → 表4 永不出,目标坐标改由地面站注入(预定流程「地面给坐标」)。
未启用 / 未注入 / 超龄 → None(下游按无目标刹车)。"""
if not self._cfg.ground_inject.enabled:
return None
if self._ground_target_fn is None:
return None
target = self._ground_target_fn()
if target is None or target.time_ms <= 0:
return None
if is_stale(target.time_ms, now_ms, self._cfg.ground_inject.timeout_ms):
return None
return target
def pod_locked(self, now_ms: int) -> bool:
"""吊舱目标门控(公共接口,供编排层/状态机复用,A3)。"""
return self._pod_locked(now_ms)
def _pod_locked(self, now_ms: int) -> bool:
"""吊舱目标门控:表2-2 遥测在 link.telemetry_timeout_ms 内、状态字节1
bit2=锁定、且(若 link.require_laser_valid)激光测距有效,才算目标有效。