Files
code-test-drone/utils/tests/pod_bridge/protocol_test.py
T
keita 477ad5f5de 新增pod_bridge吊舱桥接中间件(树莓派4B部署)
- pod_bridge: 协议G版编解码/地理位置引导律/PX4网关/串口链路/闭环主循环
- 含三项安全修复: position_time_ms拆分、表2-2锁定门控、run()异常护栏
- 测试92条全绿; 附树莓派4B(Ubuntu 24.04)部署注意事项与最小依赖清单
2026-09-23 14:38:23 +08:00

223 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""protocol_test.py —— 光电吊舱协议编解码单元测试(规格:docs/光电吊舱控制协议G版-PB.docx)。
覆盖:文档样例校验和、四帧 encode/decode 回环、量纲换算、遥测位域属性、
分帧器(粘包/半帧/噪声前缀/坏校验恢复)、异常帧拒绝。
"""
import pytest
from pod_bridge.protocol import (
CarrierAttitude,
ControlCode,
ControlCommand,
FrameParser,
Telemetry,
TargetGeo,
checksum,
decode_attitude,
decode_control,
decode_target,
decode_telemetry,
encode_attitude,
encode_control,
encode_target,
encode_telemetry,
)
# --- 校验和:文档第 589 行样例 ---------------------------------------------
def test_checksum_matches_doc_example():
# 文档样例 eb 90 2b 00..00(15 字节 body)累加取低 8 位应为 0xa6
body = bytes.fromhex("eb902b000000000000000000000000")
assert len(body) == 15
assert checksum(body) == 0xA6
def test_encode_control_center_matches_doc_bytes():
frame = encode_control(ControlCommand(code=ControlCode.CENTER))
assert frame == bytes.fromhex("eb902b000000000000000000000000a6")
assert len(frame) == 16
# --- 表2-1 控制指令回环 -----------------------------------------------------
def test_control_roundtrip_box_track():
cmd = ControlCommand(code=ControlCode.BOX_TRACK, param_x=-120, param_y=340,
param3=2, file_save_mode=1, param_x2=500, param_y2=-600)
decoded = decode_control(encode_control(cmd))
assert decoded == cmd
def test_control_signed_params_preserved():
cmd = ControlCommand(code=ControlCode.POINT_TRACK, param_x=-32768, param_y=32767)
decoded = decode_control(encode_control(cmd))
assert decoded.param_x == -32768
assert decoded.param_y == 32767
# --- 表3 载机姿态回环(含量纲换算)----------------------------------------
def test_attitude_roundtrip():
att = CarrierAttitude(
roll_deg=1.23, pitch_deg=-4.56, yaw_deg=179.99,
latitude_deg=31.2345678, longitude_deg=121.4737012,
abs_alt_m=123.4, rel_alt_m=56.8,
year=2026, month=9, day=22, hour=14, minute=30, second=15, centisecond=42,
airspeed_mps=15.5, ground_speed_mps=12.0,
)
d = decode_attitude(encode_attitude(att))
assert d.roll_deg == pytest.approx(1.23, abs=0.01)
assert d.pitch_deg == pytest.approx(-4.56, abs=0.01)
assert d.yaw_deg == pytest.approx(179.99, abs=0.01)
assert d.latitude_deg == pytest.approx(31.2345678, abs=1e-7)
assert d.longitude_deg == pytest.approx(121.4737012, abs=1e-7)
assert d.abs_alt_m == pytest.approx(123.4, abs=0.2)
assert d.rel_alt_m == pytest.approx(56.8, abs=0.2)
assert (d.year, d.month, d.day) == (2026, 9, 22)
assert (d.hour, d.minute, d.second, d.centisecond) == (14, 30, 15, 42)
assert d.airspeed_mps == pytest.approx(15.5, abs=0.5)
assert d.ground_speed_mps == pytest.approx(12.0, abs=0.5)
def test_attitude_sync_and_length():
frame = encode_attitude(CarrierAttitude())
assert len(frame) == 32
assert frame[0] == 0xEB and frame[1] == 0x91
# --- 表4 目标经纬度回环 -----------------------------------------------------
def test_target_roundtrip():
tgt = TargetGeo(latitude_deg=31.2345678, longitude_deg=121.4737012,
abs_alt_m=100.2, rel_alt_m=33.4,
year=2026, month=9, day=22, hour=8, minute=5, second=30,
centisecond=99)
d = decode_target(encode_target(tgt))
assert d.latitude_deg == pytest.approx(31.2345678, abs=1e-7)
assert d.longitude_deg == pytest.approx(121.4737012, abs=1e-7)
assert d.abs_alt_m == pytest.approx(100.2, abs=0.2)
assert d.rel_alt_m == pytest.approx(33.4, abs=0.2)
assert d.year == 2026 and d.centisecond == 99
def test_target_negative_longitude():
tgt = TargetGeo(latitude_deg=-33.8688, longitude_deg=-70.6693)
d = decode_target(encode_target(tgt))
assert d.longitude_deg == pytest.approx(-70.6693, abs=1e-7)
assert d.latitude_deg == pytest.approx(-33.8688, abs=1e-7)
# --- 表2-2 遥测:物理量 + 位域 ---------------------------------------------
def test_telemetry_roundtrip_physical_values():
t = Telemetry(status1=0b10000100, status2=0b00101010, status3=0b01000000,
off_target_x_deg=1.23, off_target_y_deg=-4.56,
frame_roll_deg=10.0, frame_pitch_deg=-20.0, frame_yaw_deg=90.5,
att_roll_deg=2.0, att_pitch_deg=-3.0,
roll_rate_dps=1.5, pitch_rate_dps=-2.5, yaw_rate_dps=30.0,
laser_range_m=123.4, self_check=0)
d = decode_telemetry(encode_telemetry(t))
assert d.off_target_x_deg == pytest.approx(1.23, abs=0.01)
assert d.off_target_y_deg == pytest.approx(-4.56, abs=0.01)
assert d.frame_yaw_deg == pytest.approx(90.5, abs=0.01)
assert d.yaw_rate_dps == pytest.approx(30.0, abs=0.01)
assert d.laser_range_m == pytest.approx(123.4, abs=0.1)
def test_telemetry_laser_zero_is_none():
d = decode_telemetry(encode_telemetry(Telemetry(laser_range_m=None)))
assert d.laser_range_m is None
def test_telemetry_bitfield_properties():
t = Telemetry(status1=0b11000100, status2=0b00101110, status3=0b01100000)
d = decode_telemetry(encode_telemetry(t))
assert d.track_source == 0b11 # 红外2
assert d.is_target_locked is True # status1 Bit2 = 1
assert d.motor_on is True # status2 Bit3 = 1
assert d.follow_on is True # status2 Bit2 = 1
assert d.big_picture == 0b01
assert d.small_picture == 0b10
# --- 异常帧拒绝 ------------------------------------------------------------
def test_decode_rejects_bad_sync():
frame = bytearray(encode_control(ControlCommand(code=ControlCode.CENTER)))
frame[1] = 0x91 # 破坏同步字
with pytest.raises(ValueError, match="同步字"):
decode_control(bytes(frame))
def test_decode_rejects_bad_checksum():
frame = bytearray(encode_target(TargetGeo(latitude_deg=10.0)))
frame[31] ^= 0xFF
with pytest.raises(ValueError, match="校验和"):
decode_target(bytes(frame))
def test_decode_rejects_wrong_length():
with pytest.raises(ValueError, match="帧长"):
decode_telemetry(b"\xee\x16" + b"\x00" * 10)
# --- 分帧器 ----------------------------------------------------------------
def test_parser_single_frame():
parser = FrameParser()
frames = parser.feed(encode_target(TargetGeo(latitude_deg=31.0, longitude_deg=121.0)))
assert len(frames) == 1
assert isinstance(frames[0], TargetGeo)
assert frames[0].latitude_deg == pytest.approx(31.0, abs=1e-7)
def test_parser_byte_by_byte_half_frame():
parser = FrameParser()
raw = encode_telemetry(Telemetry(laser_range_m=50.0))
collected = []
for i in range(len(raw)):
collected.extend(parser.feed(raw[i:i + 1]))
assert len(collected) == 1
assert collected[0].laser_range_m == pytest.approx(50.0, abs=0.1)
def test_parser_concatenated_and_mixed_frames():
parser = FrameParser()
blob = (encode_telemetry(Telemetry(laser_range_m=10.0))
+ encode_target(TargetGeo(latitude_deg=1.0))
+ encode_telemetry(Telemetry(laser_range_m=20.0)))
frames = parser.feed(blob)
assert [type(f) for f in frames] == [Telemetry, TargetGeo, Telemetry]
assert frames[0].laser_range_m == pytest.approx(10.0, abs=0.1)
assert frames[2].laser_range_m == pytest.approx(20.0, abs=0.1)
def test_parser_skips_noise_prefix():
parser = FrameParser()
blob = b"\x00\xff\x12\x34" + encode_target(TargetGeo(latitude_deg=5.0))
frames = parser.feed(blob)
assert len(frames) == 1
assert frames[0].latitude_deg == pytest.approx(5.0, abs=1e-7)
def test_parser_recovers_from_bad_checksum():
parser = FrameParser()
bad = bytearray(encode_telemetry(Telemetry(laser_range_m=1.0)))
bad[31] ^= 0xFF # 坏校验
good = encode_target(TargetGeo(latitude_deg=7.0))
frames = parser.feed(bytes(bad) + good)
# 坏帧被丢弃,紧随的好帧仍能解析
assert len(frames) == 1
assert isinstance(frames[0], TargetGeo)
def test_parser_split_across_feeds():
parser = FrameParser()
raw = encode_target(TargetGeo(latitude_deg=42.0))
assert parser.feed(raw[:5]) == [] # 半帧,未输出
frames = parser.feed(raw[5:])
assert len(frames) == 1
assert frames[0].latitude_deg == pytest.approx(42.0, abs=1e-7)
def test_parser_accepts_configured_kinds():
parser = FrameParser(kinds=((0xEB, 0x90),)) # 只识别控制帧(模拟器侧)
frames = parser.feed(encode_control(ControlCommand(code=ControlCode.CENTER)))
assert len(frames) == 1
assert isinstance(frames[0], ControlCommand)
assert frames[0].code == ControlCode.CENTER