实现预定流程全链路: 任务状态机 + 编排器 + 地面站 + 真机/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:
@@ -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)激光测距有效,才算目标有效。
|
||||
|
||||
Reference in New Issue
Block a user