精简仓库结构并增强任务链路:归档双目可选后端、新增LOS判据与自定义返航
归档 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。
This commit is contained in:
+115
-13
@@ -33,6 +33,8 @@ from visual_track_control.types import (ControlMode, ControlSnapshot,
|
||||
from .config import BridgeConfig
|
||||
from .ground_link import GroundTarget
|
||||
from .guidance import GuidanceInput, GuidanceLaw
|
||||
from .los_consistency import (LosConsistencyConfig, LosConsistencyMonitor,
|
||||
LosStatus)
|
||||
from .pod_serial import PodLink, PodSerial
|
||||
from .protocol import (CarrierAttitude, ControlCommand, TargetGeo, Telemetry,
|
||||
encode_attitude, encode_control)
|
||||
@@ -49,23 +51,39 @@ 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,
|
||||
now_ms_fn=_monotonic_ms,
|
||||
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)
|
||||
# 目标有效性第二判据:LOS 运动学一致性(取代已废弃的 require_vision)
|
||||
hs = cfg.heading_servo
|
||||
self._los_monitor = LosConsistencyMonitor(LosConsistencyConfig(
|
||||
enabled=hs.los_consistency_enabled,
|
||||
window_ms=hs.los_window_ms,
|
||||
min_yaw_change_deg=hs.los_min_yaw_change_deg,
|
||||
max_residual_deg=hs.los_max_residual_deg))
|
||||
self._los_consistent = True # 最近一次一致性判定(公共门控用)
|
||||
self._los_logged_ok = True # 日志:只在判定翻转时打印
|
||||
self._attitude_period_ms = max(
|
||||
1, int(round(1000.0 / cfg.link.attitude_downlink_hz)))
|
||||
self._last_attitude_ms: Optional[int] = None
|
||||
# 生效律周期:step() 被外部以更高频率调用时(任务编排器统一 20Hz,而
|
||||
# L2 引导律配置为 10Hz),按此节流,避免 dt 相关项/限幅被以双倍速率推进。
|
||||
self._law_period_ms = max(
|
||||
1, int(round(1000.0 / max(1e-6, self._active_frequency_hz()))))
|
||||
self._last_law_ms: Optional[int] = None
|
||||
self._last_law_output = None # 节流期内重发的上一拍输出
|
||||
self._latest_target = None # (TargetGeo, time_ms)
|
||||
self._latest_telemetry = None # (Telemetry, time_ms)
|
||||
self._step_failures = 0 # run() 连续异常计数(告警节流用)
|
||||
self._stopped = False # stop() 幂等标志(真机实测:RTL 与收尾会各调一次)
|
||||
self._landing_stopped = False # 是否已为降落退出 offboard(遥测仍保留)
|
||||
|
||||
# --- 生命周期 -----------------------------------------------------------
|
||||
async def start(self) -> bool:
|
||||
@@ -74,13 +92,22 @@ class PodBridge:
|
||||
if not await self._px4.start_offboard():
|
||||
logger.error("[Bridge] offboard 启动失败,桥接未运行")
|
||||
return False
|
||||
self._stopped = False
|
||||
logger.info("[Bridge] 启动:引导 %.1fHz,姿态下发 %.1fHz",
|
||||
self._cfg.guidance.frequency_hz,
|
||||
self._cfg.link.attitude_downlink_hz)
|
||||
return True
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""安全停机:先零速刹车,再停 offboard / 遥测 / 串口。"""
|
||||
"""安全停机:先零速刹车,再停 offboard / 遥测 / 串口。
|
||||
|
||||
幂等:任务编排器在进入 RTL 时停一次,收尾 `_teardown` 还会再停一次
|
||||
(真机在环实测确认)。重复执行会二次关闭串口、二次停 offboard,
|
||||
在 MAVSDK/串口驱动上产生无意义告警,故第二次直接返回。
|
||||
"""
|
||||
if self._stopped:
|
||||
return
|
||||
self._stopped = True
|
||||
try:
|
||||
await self._px4.send_brake_hover(self._now_ms())
|
||||
finally:
|
||||
@@ -91,6 +118,20 @@ class PodBridge:
|
||||
self._tracking.reset()
|
||||
logger.info("[Bridge] 已停止")
|
||||
|
||||
async def landing_stop(self) -> None:
|
||||
"""降落前停机:零速刹车 + 退出 offboard,但**保留遥测与串口**。
|
||||
|
||||
与 `stop()` 的差别:交给飞控 `land()` 之后仍需遥测来判定落地、记录高度
|
||||
曲线。若此时就 `stop_telemetry()`,落地段的遥测会冻结在最后高度(SITL
|
||||
实测:日志停在 5m,看不到下降)。完整清理留给收尾 `stop()`。
|
||||
"""
|
||||
try:
|
||||
await self._px4.send_brake_hover(self._now_ms())
|
||||
finally:
|
||||
await self._px4.stop_offboard()
|
||||
self._landing_stopped = True
|
||||
logger.info("[Bridge] 已退出 offboard(保留遥测用于落地判定)")
|
||||
|
||||
# --- 主循环 -------------------------------------------------------------
|
||||
async def run(self, stop_event: asyncio.Event) -> None:
|
||||
"""按引导频率循环 step(),直到 stop_event 置位。
|
||||
@@ -133,8 +174,16 @@ class PodBridge:
|
||||
self._use_heading_servo = enabled
|
||||
if enabled:
|
||||
self._tracking.reset()
|
||||
# 进入 L1:LOS 一致性历史从零开始(避免上一段跟踪的样本污染本次判定)
|
||||
self._los_monitor.reset()
|
||||
self._los_consistent = True
|
||||
self._los_logged_ok = True
|
||||
else:
|
||||
self._guidance.reset()
|
||||
# 切律后按新律频率重算节流周期,并允许立即出第一拍
|
||||
self._law_period_ms = max(
|
||||
1, int(round(1000.0 / max(1e-6, self._active_frequency_hz()))))
|
||||
self._last_law_ms = None
|
||||
|
||||
async def step(self, now_ms: int):
|
||||
"""单控制周期(显式传入时钟,便于确定性单测)。返回 ControlOutput。
|
||||
@@ -142,19 +191,43 @@ class PodBridge:
|
||||
按运行时选择的控制律在 L1(TrackingControlLaw,吊舱角度直入)与
|
||||
L2(GuidanceLaw,表4/注入坐标地理引导)间切换(方案 §7)。
|
||||
"""
|
||||
# 链路维持先做:poll 吊舱 + 按 link.attitude_downlink_hz 下发表3 姿态。
|
||||
# 这两件事与生效律频率无关(吊舱解算依赖 表3,目标门控依赖遥测新鲜度)。
|
||||
self._ingest_pod_frames(now_ms)
|
||||
|
||||
own = self._px4.own_state()
|
||||
self._downlink_attitude(own, now_ms)
|
||||
|
||||
# 生效律节流:外部(任务编排器)统一 20Hz 调用,而 L2 引导律配置为 10Hz。
|
||||
# 未到下一拍时**重发上一拍输出**(而非刹车),否则会与本拍的 GOTO 指令打架。
|
||||
if (self._last_law_ms is not None
|
||||
and now_ms - self._last_law_ms < self._law_period_ms
|
||||
and self._last_law_output is not None):
|
||||
await self._dispatch(self._last_law_output, now_ms)
|
||||
return self._last_law_output
|
||||
|
||||
self._last_law_ms = now_ms
|
||||
if self._use_heading_servo:
|
||||
out = self._tracking.update(self._build_snapshot(own, now_ms), now_ms)
|
||||
else:
|
||||
out = self._guidance.update(
|
||||
self._build_guidance_input(own, now_ms), now_ms)
|
||||
await self._dispatch(out, now_ms)
|
||||
self._last_law_output = out
|
||||
return out
|
||||
|
||||
async def search_step(self, now_ms: int, yaw_rate_dps: float) -> None:
|
||||
"""搜索周期(SEARCH 态):只维持链路与心跳 + 原地偏航扫视,不出控制律。
|
||||
|
||||
之所以不让 SEARCH 走 step():该态既不用 L2 地理引导(已在参考点,再发
|
||||
速度会漂离),也不用 L1(还没锁定目标,控制律会判丢输出刹车)。此处
|
||||
**仍要 poll 吊舱并下发表3 姿态**——否则搜索期间吊舱遥测超龄、锁定位永远
|
||||
不新鲜,搜到目标也切不进 TRACK。
|
||||
"""
|
||||
self._ingest_pod_frames(now_ms)
|
||||
own = self._px4.own_state()
|
||||
self._downlink_attitude(own, now_ms)
|
||||
await self._px4.send_yaw_rate(yaw_rate_dps)
|
||||
|
||||
# --- 吊舱侧 -------------------------------------------------------------
|
||||
def _ingest_pod_frames(self, now_ms: int) -> None:
|
||||
for frame in self._pod.poll():
|
||||
@@ -239,16 +312,24 @@ class PodBridge:
|
||||
return target
|
||||
|
||||
def pod_locked(self, now_ms: int) -> bool:
|
||||
"""吊舱目标门控(公共接口,供编排层/状态机复用,A3)。"""
|
||||
return self._pod_locked(now_ms)
|
||||
"""目标门控(公共接口,供编排层/状态机复用,A3)。
|
||||
|
||||
= 吊舱 bit2 门控 **AND** LOS 运动学一致性判定。
|
||||
这样"bit2 粘滞但角度冻结"的幽灵目标会让状态机退出 TRACK 重新搜索,
|
||||
而不只是让控制律单方面刹车(否则状态机会一直以为还锁着)。
|
||||
"""
|
||||
return self._pod_locked(now_ms) and self._los_consistent
|
||||
|
||||
def _pod_locked(self, now_ms: int) -> bool:
|
||||
"""吊舱目标门控:表2-2 遥测在 link.telemetry_timeout_ms 内、状态字节1
|
||||
bit2=锁定、且(若 link.require_laser_valid)激光测距有效,才算目标有效。
|
||||
"""吊舱 bit2 门控(**不含** LOS 一致性,供内部合成本拍快照用)。
|
||||
|
||||
表2-2 遥测在 link.telemetry_timeout_ms 内、状态字节1 bit2=锁定、
|
||||
且(若 link.require_laser_valid)激光测距有效,才算"吊舱自报锁定"。
|
||||
|
||||
实测依据:部分吊舱目标丢失后**锁定位置位不复位**(脱靶量也没有无效标志位,
|
||||
恒为 0),仅凭 bit2 会朝过期坐标继续飞;激光无效时吊舱不解算目标经纬度,
|
||||
故此兜底。无激光或激光恒无效的吊舱把 require_laser_valid 置 False。
|
||||
更进一步的"角度是否活体"由 LOS 一致性判据在 _build_snapshot 里叠加。
|
||||
|
||||
从未收到遥测 / 遥测超龄 / 搜索状态 / 激光无效 一律视为目标无效(宁可刹车)。"""
|
||||
tel = self._latest_telemetry
|
||||
@@ -293,17 +374,38 @@ class PodBridge:
|
||||
alpha_deg = 0.0
|
||||
|
||||
tracked = self._pod_locked(now_ms)
|
||||
# §2.3:require_vision 时需我方视觉作第二判据(bit2 粘滞兜底)。未接入视觉源
|
||||
# 时 fail-safe 为 False(无法确认 → 宁可刹车)。
|
||||
if tracked and hs.require_vision:
|
||||
tracked = self._vision_target_fn() if self._vision_target_fn else False
|
||||
# 第二判据:LOS 运动学一致性(独立于 bit2)。
|
||||
# 本吊舱无电机、相机刚性固连机体 → 机头转 Δψ 时真实目标的脱靶量必变 ≈−Δψ;
|
||||
# 机头已明显转动而 β 不动 → 角度冻结/幽灵目标 → 当丢目标处理(fail-safe)。
|
||||
# 仅在"吊舱自报锁定"期间喂样本;解锁即清历史,避免跨段污染。
|
||||
if tracked:
|
||||
status = self._los_monitor.update(
|
||||
now_ms, math.degrees(own.yaw_rad), beta_deg)
|
||||
self._los_consistent = status.consistent
|
||||
if status.frozen:
|
||||
tracked = False
|
||||
if self._los_logged_ok:
|
||||
self._los_logged_ok = False
|
||||
logger.warning(
|
||||
"[Bridge] 吊舱角度与机头转动不一致(Δψ=%.1f° Δβ=%.1f° "
|
||||
"残差=%.1f°,超 %.1f°)→ 判为丢目标",
|
||||
status.delta_yaw_deg, status.delta_beta_deg,
|
||||
status.residual_deg, hs.los_max_residual_deg)
|
||||
elif self._los_consistent and not self._los_logged_ok:
|
||||
self._los_logged_ok = True
|
||||
logger.info("[Bridge] 吊舱角度一致性恢复(残差 %.1f°)",
|
||||
status.residual_deg)
|
||||
else:
|
||||
self._los_monitor.reset()
|
||||
self._los_consistent = True
|
||||
self._los_logged_ok = True
|
||||
|
||||
return ControlSnapshot(
|
||||
track=TrackState(tracked=tracked),
|
||||
track_time_ms=t_ms, # 无遥测(t_ms=0) → 控制律判视觉超龄 → 降级
|
||||
los_beta_deg=beta_deg,
|
||||
los_alpha_deg=alpha_deg,
|
||||
distance_valid=False, # L1 无距离(L2 换 stereo_ranger.forward_distance_m)
|
||||
distance_valid=False, # L1 无距离(距离源接入后改为可用,见 prompt 文档)
|
||||
attitude_present=own.has_attitude,
|
||||
attitude_time_ms=own.time_ms,
|
||||
yaw_rad=own.yaw_rad,
|
||||
|
||||
Reference in New Issue
Block a user