- pod_bridge: 协议G版编解码/地理位置引导律/PX4网关/串口链路/闭环主循环 - 含三项安全修复: position_time_ms拆分、表2-2锁定门控、run()异常护栏 - 测试92条全绿; 附树莓派4B(Ubuntu 24.04)部署注意事项与最小依赖清单
153 lines
6.1 KiB
Python
153 lines
6.1 KiB
Python
"""guidance_test.py —— 地理位置引导律单元测试。
|
||
|
||
覆盖:大地测量(方位/距离)、对准/未对准/背向目标的速率式输出、到达悬停、
|
||
目标/位置超龄与不可用刹车、速度上限与加速度限幅渐变。
|
||
"""
|
||
|
||
import math
|
||
|
||
import pytest
|
||
|
||
from pod_bridge.guidance import (GuidanceConfig, GuidanceInput, GuidanceLaw,
|
||
haversine_distance_m, initial_bearing_deg)
|
||
from visual_track_control.types import ControlMode
|
||
|
||
_DEG_LAT_M = 111194.93 # 1° 纬度 ≈ 此米数(R=6371008.8)
|
||
|
||
|
||
def _inp(own_lat=0.0, own_lon=0.0, own_yaw=0.0, tgt_lat=0.0, tgt_lon=0.0,
|
||
pos_valid=True, tgt_valid=True, pos_ms=1000, tgt_ms=1000):
|
||
return GuidanceInput(
|
||
own_lat_deg=own_lat, own_lon_deg=own_lon, own_yaw_deg=own_yaw,
|
||
own_position_valid=pos_valid, own_position_time_ms=pos_ms,
|
||
target_lat_deg=tgt_lat, target_lon_deg=tgt_lon,
|
||
target_valid=tgt_valid, target_time_ms=tgt_ms,
|
||
)
|
||
|
||
|
||
# --- 大地测量 ---------------------------------------------------------------
|
||
@pytest.mark.parametrize("tgt,expected", [
|
||
((0.001, 0.0), 0.0), # 正北
|
||
((0.0, 0.001), 90.0), # 正东(赤道)
|
||
((-0.001, 0.0), 180.0), # 正南
|
||
((0.0, -0.001), 270.0), # 正西
|
||
])
|
||
def test_initial_bearing_cardinals(tgt, expected):
|
||
b = initial_bearing_deg(0.0, 0.0, tgt[0], tgt[1])
|
||
# 0 与 360 等价,用回绕差判定
|
||
diff = abs((b - expected + 180.0) % 360.0 - 180.0)
|
||
assert diff < 0.5
|
||
|
||
|
||
def test_haversine_distance_one_millidegree():
|
||
d = haversine_distance_m(0.0, 0.0, 0.001, 0.0)
|
||
assert d == pytest.approx(_DEG_LAT_M / 1000.0, abs=1.0)
|
||
|
||
|
||
def test_haversine_zero_distance():
|
||
assert haversine_distance_m(31.2, 121.4, 31.2, 121.4) == pytest.approx(0.0, abs=1e-6)
|
||
|
||
|
||
# --- 对准目标:前进、不偏航 -------------------------------------------------
|
||
def test_aligned_target_moves_forward_no_yaw():
|
||
law = GuidanceLaw(GuidanceConfig())
|
||
out = law.update(_inp(tgt_lat=0.001, own_yaw=0.0), now_ms=1000) # 正北 111m,机头朝北
|
||
assert out.mode is ControlMode.VELOCITY_HEADING
|
||
assert out.valid is True
|
||
assert out.vx_body_mps > 0.0
|
||
assert out.yaw_rate_dps == pytest.approx(0.0, abs=1e-6)
|
||
assert out.vz_mps == 0.0
|
||
assert law.last_bearing_deg == pytest.approx(0.0, abs=0.5)
|
||
|
||
|
||
def test_target_to_east_yaws_right_no_forward():
|
||
law = GuidanceLaw(GuidanceConfig())
|
||
# 目标正东(方位 90),机头朝北(0)→ 误差 +90:右转为正、未对准时前向门控为 0
|
||
out = law.update(_inp(tgt_lon=0.001, own_yaw=0.0), now_ms=1000)
|
||
assert out.yaw_rate_dps > 0.0
|
||
assert out.vx_body_mps == pytest.approx(0.0, abs=1e-6)
|
||
|
||
|
||
def test_facing_target_moves_forward():
|
||
law = GuidanceLaw(GuidanceConfig())
|
||
# 目标正东、机头也朝东(yaw=90)→ 已对准,前进不偏航
|
||
out = law.update(_inp(tgt_lon=0.001, own_yaw=90.0), now_ms=1000)
|
||
assert out.vx_body_mps > 0.0
|
||
assert out.yaw_rate_dps == pytest.approx(0.0, abs=1e-6)
|
||
|
||
|
||
def test_target_behind_no_forward_and_turns():
|
||
law = GuidanceLaw(GuidanceConfig())
|
||
out = law.update(_inp(tgt_lat=-0.001, own_yaw=0.0), now_ms=1000) # 正南,机头朝北
|
||
assert out.vx_body_mps == pytest.approx(0.0, abs=1e-6) # cos(180°)<0 门控为 0
|
||
assert abs(out.yaw_rate_dps) > 0.0
|
||
|
||
|
||
# --- 到达 / 不可用 / 超龄 → 刹车 -------------------------------------------
|
||
def test_arrived_within_stop_radius_brakes():
|
||
law = GuidanceLaw(GuidanceConfig(stop_radius_m=5.0))
|
||
near = 2.0 / _DEG_LAT_M # ≈2m 纬度偏移
|
||
out = law.update(_inp(tgt_lat=near), now_ms=1000)
|
||
assert out.mode is ControlMode.BRAKE_HOVER
|
||
assert out.vx_body_mps == 0.0
|
||
|
||
|
||
def test_stale_target_brakes():
|
||
law = GuidanceLaw(GuidanceConfig(target_stale_ms=1000))
|
||
out = law.update(_inp(tgt_lat=0.001, tgt_ms=1000), now_ms=5000)
|
||
assert out.mode is ControlMode.BRAKE_HOVER
|
||
|
||
|
||
def test_invalid_position_brakes():
|
||
law = GuidanceLaw(GuidanceConfig())
|
||
out = law.update(_inp(tgt_lat=0.001, pos_valid=False), now_ms=1000)
|
||
assert out.mode is ControlMode.BRAKE_HOVER
|
||
|
||
|
||
def test_invalid_target_brakes():
|
||
law = GuidanceLaw(GuidanceConfig())
|
||
out = law.update(_inp(tgt_lat=0.001, tgt_valid=False), now_ms=1000)
|
||
assert out.mode is ControlMode.BRAKE_HOVER
|
||
|
||
|
||
# --- 速度上限与加速度限幅渐变 ----------------------------------------------
|
||
def test_speed_ramps_up_and_clamps_to_max():
|
||
cfg = GuidanceConfig(frequency_hz=10.0, max_speed_mps=5.0,
|
||
speed_slew_mps2=2.0, stop_radius_m=5.0)
|
||
law = GuidanceLaw(cfg)
|
||
forward = 0.0
|
||
now = 1000
|
||
for _ in range(60): # 6s,足够加速度限幅爬到上限
|
||
out = law.update(_inp(tgt_lat=0.05, own_yaw=0.0, pos_ms=now, tgt_ms=now),
|
||
now_ms=now)
|
||
forward = out.vx_body_mps
|
||
assert forward <= cfg.max_speed_mps + 1e-9
|
||
now += 100
|
||
assert forward == pytest.approx(cfg.max_speed_mps, abs=0.1)
|
||
|
||
|
||
def test_first_command_is_slew_limited_not_full_speed():
|
||
law = GuidanceLaw(GuidanceConfig(frequency_hz=10.0, max_speed_mps=5.0,
|
||
speed_slew_mps2=2.0))
|
||
out = law.update(_inp(tgt_lat=0.05, own_yaw=0.0), now_ms=1000)
|
||
# 首拍从零渐变:dt=0.1s,加速度上限 2 → 前向 ≤ 0.2,远小于 5
|
||
assert 0.0 < out.vx_body_mps <= 0.2 + 1e-9
|
||
|
||
|
||
def test_brake_then_recover_ramps_from_zero():
|
||
law = GuidanceLaw(GuidanceConfig(frequency_hz=10.0, speed_slew_mps2=2.0))
|
||
law.update(_inp(tgt_lat=0.05, own_yaw=0.0), now_ms=1000) # 前进
|
||
law.update(_inp(tgt_lat=0.05, tgt_valid=False), now_ms=1100) # 刹车,清状态
|
||
out = law.update(_inp(tgt_lat=0.05, own_yaw=0.0), now_ms=1200) # 恢复
|
||
assert out.mode is ControlMode.VELOCITY_HEADING
|
||
assert out.vx_body_mps <= 0.2 + 1e-9 # 从零重新渐变
|
||
|
||
|
||
def test_reset_clears_state():
|
||
law = GuidanceLaw(GuidanceConfig())
|
||
law.update(_inp(tgt_lat=0.05, own_yaw=0.0), now_ms=1000)
|
||
law.reset()
|
||
out = law.update(_inp(tgt_lat=0.05, own_yaw=0.0, pos_ms=2000, tgt_ms=2000),
|
||
now_ms=2000)
|
||
assert out.vx_body_mps <= 0.2 + 1e-9 # 复位后首条仍从零渐变
|