核心结论: L1 不用新写控制律 —— TrackingControlLaw 第7步已内建无距离降级 (hold / slow_approach / exit), 复用其限幅/超龄/Coast/无距离/NED旋转(81测试保障)。 三个关键设计: 1. 航向信号用「框架角+脱靶量」而非只用脱靶量 —— 吊舱伺服会把目标保持居中, 脱靶量稳态≈0 没信号; 框架角才是目标相对机头的方位。且角度输入与 FOV 无关, 吊舱变焦不会失真(像素路径会)。 2. 接入路线 A1 推荐: ControlSnapshot 加可选 los_beta_deg/los_alpha_deg。 3. tracked 判据不能只信 bit2(实测不复位), 需第二判据。 含改动清单/配置外置/台架 S0-S5 步骤/风险表/L2 接口预留, 以及待拍板三项。
9.7 KiB
L1 航向伺服落地方案(无距离,树莓派 / Python)
目标:先把"只修正飞行方向、不需要距离"的部分落地,速度-距离闭环(L2)滞后。 基于结论:本吊舱无激光 → 表4 永不出,且不允许二次开发(部署文档 §10.0 / §10)。 日期:2026-10-09 状态:方案待确认,未动代码
0. 结论先行:L1 不用新写控制律
visual_track_control.TrackingControlLaw 已经内建"无距离"路径,第 ⑦ 步:
distance_available = snapshot.distance_valid and distance>0 and 未超龄
├─ True → 距离 PID → 前向速度(这是 L2,滞后做)
└─ False → 清积分,按 cfg.distance.no_distance_action 三选一:
HOLD → vx = 0 (只转向,不前近)← 首次上机用这个
SLOW_APPROACH → vx = 固定保守限速 ← 航向验证通过后用这个
EXIT → 持续无距离达 grace 才降级
所以 L1 = TrackingControlLaw + distance_valid=False + 无距离动作配置,
航向/垂直通道、限幅、超龄、Coast 清积分、NED 旋转全部复用既有实现
(该包有 81 个测试保障)。要新写的只有"胶水层":把吊舱表2-2 的角度量喂进去。
1. 现有构件盘点(能复用什么)
| 构件 | 位置 | L1 怎么用 |
|---|---|---|
TrackingControlLaw |
visual_track_control/tracking_control_law.py |
主控制律,直接用 |
ControlSnapshot / TrackState |
visual_track_control/types.py |
输入结构,distance_valid=False |
PixelToAngle |
line_of_sight.py |
❌ 绕过(见 §2.1,吊舱给的已是角度) |
PidController / SlewRateLimiter |
同包 | 被控制律内部使用 |
PodBridge 主循环 |
pod_bridge/bridge.py |
改:把 GuidanceLaw 换成 TrackingControlLaw |
_pod_locked() |
bridge.py:173 |
目标门控(⚠ 有坑,见 §2.3) |
| 表2-2 角度量 | protocol.py:150-160 |
off_target_x/y_deg、frame_yaw/pitch_deg |
2. 三个关键设计决策
2.1 🔴 航向信号要用「框架角 + 脱靶量」,不能只用脱靶量
这是本方案最容易踩空的地方:
吊舱锁定后内部伺服会自己把目标保持在画面中心,所以脱靶量长期 ≈ 0。 若只用脱靶量驱动航向,稳态时根本没有信号,飞机会"不知道该往哪转"。
正确的信号是框架角(frame_yaw_deg,云台相对机体的方位角):目标居中时它
依然非零,它就是"目标相对机头的方位"。合成:
β = frame_yaw_deg + off_target_x_deg (水平,右正)
α = frame_pitch_deg + off_target_y_deg (垂直,下正)
- 目标居中时:
off_target ≈ 0,β 由框架角提供(主信号) - 云台滞后 / 大偏差时:脱靶量提供动态补充
控制律让 β → 0,等价于把机体航向转向目标、让云台回中(标准 gimbal-slaved 航向控制)。这正是"用吊舱修正飞行方向"的正确做法。
⚠ 必须在台架标定:① 框架角是否确为"相对机体"(而非绝对方位); ② β/α 的正方向是否与控制律约定(右正 / 下正)一致。符号错了会反向转。
额外好处:走角度输入就与视场 FOV 无关——吊舱会变焦,若走像素换算
(PixelToAngle 反推),变焦后 FOV 变化会直接失真。这是不用像素路径的硬理由。
2.2 如何接入 TrackingControlLaw(三选一,需拍板)
控制律第 ④ 步现在是硬编码从像素算角度,没有"直接喂角度"的入口:
x_offset = snapshot.track.center[0] - image_width * 0.5 # tracking_control_law.py:135
beta_deg = self._pixel_to_beta.convert(x_offset) * RAD_TO_DEG
| 路线 | 做法 | 评价 |
|---|---|---|
| A1(推荐) | ControlSnapshot 加可选字段 los_beta_deg / los_alpha_deg(默认 None);第 ④ 步:提供了就直接用,否则走原像素路径 |
改动约 10 行,向后兼容、现有 81 测试不受影响,⑤~⑧ 步(限幅/超龄/Coast/无距离/NED 旋转)全部复用 |
| A2 | 不动上游,在 pod_bridge 里写 PodHeadingServo 薄封装,复用 PidController/SlewRateLimiter/is_stale |
不碰稳定模块,但要重寫 ⑤⑥⑦⑧ 的逻辑,与既有实现双份维护,易走偏 |
| B | 把角度反推成"等价像素偏移"喂进现有字段 | ❌ 不推荐:需要 FOV,吊舱变焦即失真,且多绕一层徒增误差 |
建议 A1。代价是动一下 visual_track_control(需补 3~5 条测试守住"默认 None
仍走像素路径"的兼容性)。
2.3 ⚠ tracked 判据不能只信 bit2
已知事实(部署文档 §10):锁定位置位 bit2 在目标丢失后不复位,且脱靶量 没有无效标志位、丢失时表现为 0。后果:
- 只信 bit2 → 目标早已丢失,仍认为"锁定中"继续转;
- 只看脱靶量 0 → 误判为"跟得很好"。
L1 的 tracked 判据按阶段递进:
| 阶段 | 判据 |
|---|---|
| 台架(S1~S2) | bit2 锁定 且 遥测未超龄(现有 _pod_locked())+ 人工在旁观察,随时按键 u 解锁 |
| 接入视觉后(S3+) | bit2 且 我方 stereo_yolo/target_tracker 有目标(第二判据,必做) |
3. 改动清单(文件级)
| # | 文件 | 改动 |
|---|---|---|
| 1 | utils/visual_track_control/types.py |
ControlSnapshot 加 los_beta_deg / los_alpha_deg: Optional[float] = None(路线 A1) |
| 2 | utils/visual_track_control/tracking_control_law.py |
第 ④ 步:角度直入优先,否则回退像素路径 |
| 3 | utils/pod_bridge/bridge.py |
新增 _build_snapshot():表2-2 → ControlSnapshot(β/α 按 §2.1,符号由配置);step() 改走 TrackingControlLaw;tracked 用 §2.3 判据 |
| 4 | utils/pod_bridge/config.py |
新增 L1 配置段(信号源、符号、门限、是否禁用垂直) |
| 5 | utils/config/pod_bridge.json |
落配置值(铁律:数值不写死在代码里) |
| 6 | utils/tests/... |
新增测试:角度直入路径、默认 None 兼容性、bridge L1 组装(FakePod) |
暂不动:guidance.py(L2 地理引导)保留,按配置在 L1 / L2 间切换,
等 L2 有标定过的距离源再启用。
4. 配置外置(要点,非最终字段名)
// utils/config/pod_bridge.json —— L1 段
"heading_servo": {
"enabled": true,
"beta_sign": 1, // 台架标定后填 ±1(框架角 + 脱靶量合成的符号)
"alpha_sign": 1,
"use_off_target": true, // β 是否叠加脱靶量(false = 只用框架角)
"vertical_enabled": false, // L1 先禁垂直,避免撞地;vz=0
"require_vision": false // true = 需我方视觉作为第二判据(S3 起开)
}
// visual_track_control 侧(沿用既有结构,只改值)
"heading": { "mode": "rate", "gain": ..., "yaw_rate_limit_dps": 20.0, "yaw_slew_limit_dps2": 40.0 },
"distance": { "no_distance_action": "hold", // 首次上机 = hold
"no_distance_approach_limit_mps": 0.3 },
"vertical": { "vz_gain_mps_per_deg": 0.0 } // L1 禁用
首次上机一律
no_distance_action = "hold":只转向、不前近。 航向验证通过后再切slow_approach并把限速压到 0.3 m/s 起步。
5. 台架落地步骤(增量,每步过了再下一步)
| 步骤 | 做什么 | 通过判据 |
|---|---|---|
| S0 纯软单测 | 新增测试 + 全量回归 | 全绿(当前 372 → 增加) |
| S1 标定 | pod_console.py 观察云台转动 / 移动目标时 frame_yaw/frame_pitch、off_target_x/y 的变化 |
确认框架角是"相对机体";定出 beta_sign / alpha_sign |
| S2 干跑 | bridge 以 dry-run 跑 L1,不下发 offboard,只打印 yaw_rate |
目标偏左 → yaw_rate 为负(左转),符号正确;目标居中 → 趋于 0 |
| S3 不装桨接飞控 | 真实 offboard | 机体转向目标、云台逐步回中;人工可随时接管 |
| S4 切 slow_approach | vx = 0.3 m/s 起步 | 缓慢接近不振荡;异常立即 RC 切手动 |
| S5 装桨试飞 | — | 最后一步,先在空旷场地 |
⚠ S3 起必须:不装桨 / 空旷场地 / RC 随时可切手动 / 有人值守。
6. 风险与判据
| 风险 | 判据 / 对策 |
|---|---|
| 符号反了 → 反向转 | S1 标定 + S2 干跑必验;配置化而非硬编码 |
| bit2 粘滞 → 丢失后仍转 | 第二判据(我方视觉);台架期人工值守 |
| 框架角定义不符(绝对而非相对机体) | S1 实测确认,不符则改用脱靶量积分等替代 |
| 云台归中/搜索态框架角乱转 | 仅锁定态启用;非锁定 → tracked=False → BRAKE_HOVER |
| 垂直通道撞地 | L1 强制 vz_gain=0 |
| 测距噪声(L2 才有) | L2 滞后,且不用连续 PID,改分段限速 |
7. 与 L2 的接口预留
L1 落地时就留好口子,将来 L2 只是"换个输入源",不动结构:
ControlSnapshot.distance_valid/target_distance_m/distance_time_ms字段 现在就按False填全,L2 时换成stereo_ranger的forward_distance_m;bridge.step()按配置在TrackingControlLaw(L1)与GuidanceLaw(L2)间切换;guidance.py的输入接口保持不变,等有标定过的距离源再启用。
8. 待你拍板
- 接入路线:A1(改
ControlSnapshot加可选角度字段,推荐)/ A2(pod_bridge 侧薄封装)/ B(像素反推,不推荐); - 目标类型:人车(地面)/ 无人机(空中)—— 影响 L2 的几何反推法能否用,L1 不受影响;
- 首次上机无距离动作:
hold(只转不前,我建议)/slow_approach(0.3 m/s)。