From df91b1110efac19daad89e692777ae5a5a7b5362 Mon Sep 17 00:00:00 2001 From: keita <16924591+keita_sakura@user.noreply.gitee.com> Date: Wed, 21 Oct 2026 14:48:41 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B2=BE=E7=AE=80=E4=BB=93=E5=BA=93=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=B9=B6=E5=A2=9E=E5=BC=BA=E4=BB=BB=E5=8A=A1=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=EF=BC=9A=E5=BD=92=E6=A1=A3=E5=8F=8C=E7=9B=AE=E5=8F=AF?= =?UTF-8?q?=E9=80=89=E5=90=8E=E7=AB=AF=E3=80=81=E6=96=B0=E5=A2=9ELOS?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E4=B8=8E=E8=87=AA=E5=AE=9A=E4=B9=89=E8=BF=94?= =?UTF-8?q?=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 归档 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。 --- .gitignore | 24 + README.md | 290 +++--- __pycache__/test.cpython-312.pyc | Bin 37688 -> 0 bytes docs/L1航向伺服实现方案.md | 50 +- docs/光电吊舱控制协议G版-PB.docx | Bin 0 -> 38972 bytes docs/树莓派4B部署注意事项.md | 30 +- ground_station/app.py | 385 +++++++- ground_station/templates/index.html | 301 ++++++- ground_station/templates/px4.html | 564 ++++++++++++ ground_station/video_source.py | 152 ++++ pod_sitl_test.py => l1_sitl_test.py | 108 +-- legacy/README.md | 54 ++ .../docs}/双目YOLO目标识别需求说明.md | 0 {docs => legacy/docs}/双目测距技术路线.md | 0 legacy/docs/双目相机模组到位作业指导.md | 301 +++++++ {models => legacy/models}/yolo26n-drone.pt | Bin .../utils}/assets/stereo_yolo/drone_sky.png | Bin .../utils}/config/stereo_ranger.json | 0 .../utils}/config/stereo_yolo.json | 0 legacy/utils/conftest.py | 19 + {utils => legacy/utils}/demo_stereo_ranger.py | 0 {utils => legacy/utils}/demo_stereo_yolo.py | 0 {utils => legacy/utils}/demo_track_control.py | 0 legacy/utils/pytest.ini | 6 + .../utils}/stereo_ranger/README.md | 0 .../utils}/stereo_ranger/__init__.py | 0 .../utils}/stereo_ranger/config.py | 0 .../utils}/stereo_ranger/consistency.py | 0 .../utils}/stereo_ranger/distance_filter.py | 0 .../utils}/stereo_ranger/geometry.py | 0 .../utils}/stereo_ranger/matching.py | 0 .../utils}/stereo_ranger/sampling.py | 0 .../utils}/stereo_ranger/stereo_ranger.py | 0 .../utils}/stereo_ranger/target_store.py | 0 .../utils}/stereo_ranger/types.py | 0 .../utils}/stereo_ranger/warn_throttle.py | 0 {utils => legacy/utils}/stereo_yolo/README.md | 0 .../utils}/stereo_yolo/__init__.py | 0 {utils => legacy/utils}/stereo_yolo/config.py | 0 .../utils}/stereo_yolo/detector.py | 0 .../utils}/stereo_yolo/frontend.py | 0 {utils => legacy/utils}/stereo_yolo/sbs.py | 0 .../utils}/stereo_yolo/warn_throttle.py | 0 .../utils}/target_tracker/README.md | 0 .../utils}/target_tracker/__init__.py | 0 .../target_tracker/kalman_box_filter.py | 0 .../utils}/target_tracker/target_tracker.py | 0 .../utils}/target_tracker/tracklet.py | 0 .../utils}/tests/stereo_ranger/config_test.py | 0 .../tests/stereo_ranger/consistency_test.py | 0 .../stereo_ranger/distance_filter_test.py | 0 .../tests/stereo_ranger/geometry_test.py | 0 .../tests/stereo_ranger/matching_test.py | 0 .../tests/stereo_ranger/sampling_test.py | 0 .../tests/stereo_ranger/stereo_ranger_test.py | 0 .../tests/stereo_ranger/target_store_test.py | 0 .../tests/stereo_ranger/warn_throttle_test.py | 0 .../utils}/tests/stereo_yolo/config_test.py | 0 .../utils}/tests/stereo_yolo/frontend_test.py | 0 .../utils}/tests/stereo_yolo/sbs_test.py | 0 .../target_tracker/kalman_box_filter_test.py | 0 .../target_tracker/target_tracker_test.py | 0 .../tests/target_tracker/tracklet_test.py | 0 requirements-pod.txt | 6 +- requirements.txt | 31 +- task_runner_real.py | 137 ++- task_runner_sitl.py | 57 +- test.py | 836 ------------------ tools/README.md | 23 + tools/camera.py | 182 ++++ tools/camera_test.py | 59 ++ tools/video_test.py | 42 + trajectory_plot.png | Bin 111909 -> 0 bytes utils/README.md | 141 +-- .../conftest.cpython-312-pytest-9.1.1.pyc | Bin 739 -> 0 bytes .../demo_stereo_yolo.cpython-312.pyc | Bin 17509 -> 0 bytes .../demo_track_control.cpython-312.pyc | Bin 12380 -> 0 bytes utils/config/pod_bridge.json | 5 +- utils/pod_bridge/bridge.py | 128 ++- utils/pod_bridge/config.py | 25 +- utils/pod_bridge/ground_link.py | 88 +- utils/pod_bridge/los_consistency.py | 115 +++ utils/pod_bridge/px4_gateway.py | 25 +- utils/sitl_support.py | 131 +++ .../__pycache__/__init__.cpython-312.pyc | Bin 1870 -> 0 bytes .../__pycache__/config.cpython-312.pyc | Bin 7148 -> 0 bytes .../__pycache__/consistency.cpython-312.pyc | Bin 4037 -> 0 bytes .../distance_filter.cpython-312.pyc | Bin 4474 -> 0 bytes .../__pycache__/geometry.cpython-312.pyc | Bin 5052 -> 0 bytes .../__pycache__/matching.cpython-312.pyc | Bin 4111 -> 0 bytes .../__pycache__/sampling.cpython-312.pyc | Bin 4239 -> 0 bytes .../__pycache__/stereo_ranger.cpython-312.pyc | Bin 11696 -> 0 bytes .../__pycache__/target_store.cpython-312.pyc | Bin 6325 -> 0 bytes .../__pycache__/types.cpython-312.pyc | Bin 5438 -> 0 bytes .../__pycache__/warn_throttle.cpython-312.pyc | Bin 2032 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 913 -> 0 bytes .../__pycache__/config.cpython-312.pyc | Bin 6219 -> 0 bytes .../__pycache__/detector.cpython-312.pyc | Bin 4534 -> 0 bytes .../__pycache__/frontend.cpython-312.pyc | Bin 7037 -> 0 bytes .../__pycache__/sbs.cpython-312.pyc | Bin 1232 -> 0 bytes .../__pycache__/warn_throttle.cpython-312.pyc | Bin 1767 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 973 -> 0 bytes .../kalman_box_filter.cpython-312.pyc | Bin 6297 -> 0 bytes .../target_tracker.cpython-312.pyc | Bin 13603 -> 0 bytes .../__pycache__/tracklet.cpython-312.pyc | Bin 6268 -> 0 bytes utils/task_runner/__init__.py | 15 +- utils/task_runner/log.py | 131 ++- utils/task_runner/pod_control.py | 18 +- utils/task_runner/return_home.py | 130 +++ utils/task_runner/runner.py | 463 +++++++++- utils/task_runner/state_machine.py | 107 ++- utils/tests/ground_station/app_test.py | 299 +++++++ .../tests/ground_station/video_source_test.py | 138 +++ utils/tests/pod_bridge/bridge_test.py | 199 ++++- utils/tests/pod_bridge/ground_link_test.py | 113 ++- .../tests/pod_bridge/los_consistency_test.py | 141 +++ utils/tests/pod_bridge/px4_gateway_test.py | 36 +- .../config_test.cpython-312-pytest-9.1.1.pyc | Bin 13539 -> 0 bytes ...sistency_test.cpython-312-pytest-9.1.1.pyc | Bin 14578 -> 0 bytes ...e_filter_test.cpython-312-pytest-9.1.1.pyc | Bin 22412 -> 0 bytes ...geometry_test.cpython-312-pytest-9.1.1.pyc | Bin 33605 -> 0 bytes ...matching_test.cpython-312-pytest-9.1.1.pyc | Bin 21391 -> 0 bytes ...sampling_test.cpython-312-pytest-9.1.1.pyc | Bin 25374 -> 0 bytes ...o_ranger_test.cpython-312-pytest-9.1.1.pyc | Bin 80459 -> 0 bytes ...et_store_test.cpython-312-pytest-9.1.1.pyc | Bin 15298 -> 0 bytes ...throttle_test.cpython-312-pytest-9.1.1.pyc | Bin 11571 -> 0 bytes .../config_test.cpython-312-pytest-9.1.1.pyc | Bin 7232 -> 0 bytes ...frontend_test.cpython-312-pytest-9.1.1.pyc | Bin 27869 -> 0 bytes .../sbs_test.cpython-312-pytest-9.1.1.pyc | Bin 5139 -> 0 bytes ...x_filter_test.cpython-312-pytest-9.1.1.pyc | Bin 29617 -> 0 bytes ..._tracker_test.cpython-312-pytest-9.1.1.pyc | Bin 73797 -> 0 bytes ...tracklet_test.cpython-312-pytest-9.1.1.pyc | Bin 47576 -> 0 bytes utils/tests/task_runner/log_test.py | 83 +- utils/tests/task_runner/pod_control_test.py | 31 +- utils/tests/task_runner/return_home_test.py | 142 +++ utils/tests/task_runner/runner_test.py | 683 +++++++++++++- utils/tests/task_runner/state_machine_test.py | 224 +++-- ...ransform_test.cpython-312-pytest-9.1.1.pyc | Bin 11989 -> 0 bytes ...of_sight_test.cpython-312-pytest-9.1.1.pyc | Bin 14627 -> 0 bytes ..._gateway_test.cpython-312-pytest-9.1.1.pyc | Bin 37585 -> 0 bytes ...ntroller_test.cpython-312-pytest-9.1.1.pyc | Bin 20347 -> 0 bytes ..._limiter_test.cpython-312-pytest-9.1.1.pyc | Bin 17673 -> 0 bytes ..._checker_test.cpython-312-pytest-9.1.1.pyc | Bin 6043 -> 0 bytes ...trol_law_test.cpython-312-pytest-9.1.1.pyc | Bin 63754 -> 0 bytes ...c_config_test.cpython-312-pytest-9.1.1.pyc | Bin 29276 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 1865 -> 0 bytes .../command_sender.cpython-312.pyc | Bin 2049 -> 0 bytes .../__pycache__/config.cpython-312.pyc | Bin 14696 -> 0 bytes .../frame_transform.cpython-312.pyc | Bin 1562 -> 0 bytes .../__pycache__/line_of_sight.cpython-312.pyc | Bin 1901 -> 0 bytes .../mavsdk_gateway.cpython-312.pyc | Bin 6610 -> 0 bytes .../pid_controller.cpython-312.pyc | Bin 4658 -> 0 bytes .../__pycache__/rate_limiter.cpython-312.pyc | Bin 2549 -> 0 bytes .../staleness_checker.cpython-312.pyc | Bin 1033 -> 0 bytes .../tracking_control_law.cpython-312.pyc | Bin 13663 -> 0 bytes .../__pycache__/types.cpython-312.pyc | Bin 4643 -> 0 bytes utils/visual_track_control/mavsdk_gateway.py | 25 +- 工作进度.md | 228 +++-- 预定流程-操作指南.md | 217 +++-- 159 files changed, 6121 insertions(+), 1617 deletions(-) create mode 100644 .gitignore delete mode 100644 __pycache__/test.cpython-312.pyc create mode 100644 docs/光电吊舱控制协议G版-PB.docx create mode 100644 ground_station/templates/px4.html create mode 100644 ground_station/video_source.py rename pod_sitl_test.py => l1_sitl_test.py (76%) create mode 100644 legacy/README.md rename {docs => legacy/docs}/双目YOLO目标识别需求说明.md (100%) rename {docs => legacy/docs}/双目测距技术路线.md (100%) create mode 100644 legacy/docs/双目相机模组到位作业指导.md rename {models => legacy/models}/yolo26n-drone.pt (100%) rename {utils => legacy/utils}/assets/stereo_yolo/drone_sky.png (100%) rename {utils => legacy/utils}/config/stereo_ranger.json (100%) rename {utils => legacy/utils}/config/stereo_yolo.json (100%) create mode 100644 legacy/utils/conftest.py rename {utils => legacy/utils}/demo_stereo_ranger.py (100%) rename {utils => legacy/utils}/demo_stereo_yolo.py (100%) rename {utils => legacy/utils}/demo_track_control.py (100%) create mode 100644 legacy/utils/pytest.ini rename {utils => legacy/utils}/stereo_ranger/README.md (100%) rename {utils => legacy/utils}/stereo_ranger/__init__.py (100%) rename {utils => legacy/utils}/stereo_ranger/config.py (100%) rename {utils => legacy/utils}/stereo_ranger/consistency.py (100%) rename {utils => legacy/utils}/stereo_ranger/distance_filter.py (100%) rename {utils => legacy/utils}/stereo_ranger/geometry.py (100%) rename {utils => legacy/utils}/stereo_ranger/matching.py (100%) rename {utils => legacy/utils}/stereo_ranger/sampling.py (100%) rename {utils => legacy/utils}/stereo_ranger/stereo_ranger.py (100%) rename {utils => legacy/utils}/stereo_ranger/target_store.py (100%) rename {utils => legacy/utils}/stereo_ranger/types.py (100%) rename {utils => legacy/utils}/stereo_ranger/warn_throttle.py (100%) rename {utils => legacy/utils}/stereo_yolo/README.md (100%) rename {utils => legacy/utils}/stereo_yolo/__init__.py (100%) rename {utils => legacy/utils}/stereo_yolo/config.py (100%) rename {utils => legacy/utils}/stereo_yolo/detector.py (100%) rename {utils => legacy/utils}/stereo_yolo/frontend.py (100%) rename {utils => legacy/utils}/stereo_yolo/sbs.py (100%) rename {utils => legacy/utils}/stereo_yolo/warn_throttle.py (100%) rename {utils => legacy/utils}/target_tracker/README.md (100%) rename {utils => legacy/utils}/target_tracker/__init__.py (100%) rename {utils => legacy/utils}/target_tracker/kalman_box_filter.py (100%) rename {utils => legacy/utils}/target_tracker/target_tracker.py (100%) rename {utils => legacy/utils}/target_tracker/tracklet.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/config_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/consistency_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/distance_filter_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/geometry_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/matching_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/sampling_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/stereo_ranger_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/target_store_test.py (100%) rename {utils => legacy/utils}/tests/stereo_ranger/warn_throttle_test.py (100%) rename {utils => legacy/utils}/tests/stereo_yolo/config_test.py (100%) rename {utils => legacy/utils}/tests/stereo_yolo/frontend_test.py (100%) rename {utils => legacy/utils}/tests/stereo_yolo/sbs_test.py (100%) rename {utils => legacy/utils}/tests/target_tracker/kalman_box_filter_test.py (100%) rename {utils => legacy/utils}/tests/target_tracker/target_tracker_test.py (100%) rename {utils => legacy/utils}/tests/target_tracker/tracklet_test.py (100%) delete mode 100644 test.py create mode 100644 tools/README.md create mode 100644 tools/camera.py create mode 100644 tools/camera_test.py create mode 100644 tools/video_test.py delete mode 100644 trajectory_plot.png delete mode 100644 utils/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/__pycache__/demo_stereo_yolo.cpython-312.pyc delete mode 100644 utils/__pycache__/demo_track_control.cpython-312.pyc create mode 100644 utils/pod_bridge/los_consistency.py create mode 100644 utils/sitl_support.py delete mode 100644 utils/stereo_ranger/__pycache__/__init__.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/config.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/consistency.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/distance_filter.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/geometry.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/matching.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/sampling.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/stereo_ranger.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/target_store.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/types.cpython-312.pyc delete mode 100644 utils/stereo_ranger/__pycache__/warn_throttle.cpython-312.pyc delete mode 100644 utils/stereo_yolo/__pycache__/__init__.cpython-312.pyc delete mode 100644 utils/stereo_yolo/__pycache__/config.cpython-312.pyc delete mode 100644 utils/stereo_yolo/__pycache__/detector.cpython-312.pyc delete mode 100644 utils/stereo_yolo/__pycache__/frontend.cpython-312.pyc delete mode 100644 utils/stereo_yolo/__pycache__/sbs.cpython-312.pyc delete mode 100644 utils/stereo_yolo/__pycache__/warn_throttle.cpython-312.pyc delete mode 100644 utils/target_tracker/__pycache__/__init__.cpython-312.pyc delete mode 100644 utils/target_tracker/__pycache__/kalman_box_filter.cpython-312.pyc delete mode 100644 utils/target_tracker/__pycache__/target_tracker.cpython-312.pyc delete mode 100644 utils/target_tracker/__pycache__/tracklet.cpython-312.pyc create mode 100644 utils/task_runner/return_home.py create mode 100644 utils/tests/ground_station/app_test.py create mode 100644 utils/tests/ground_station/video_source_test.py create mode 100644 utils/tests/pod_bridge/los_consistency_test.py delete mode 100644 utils/tests/stereo_ranger/__pycache__/config_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/consistency_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/distance_filter_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/geometry_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/matching_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/sampling_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/stereo_ranger_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/target_store_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_ranger/__pycache__/warn_throttle_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_yolo/__pycache__/config_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_yolo/__pycache__/frontend_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/stereo_yolo/__pycache__/sbs_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/target_tracker/__pycache__/kalman_box_filter_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/target_tracker/__pycache__/target_tracker_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/target_tracker/__pycache__/tracklet_test.cpython-312-pytest-9.1.1.pyc create mode 100644 utils/tests/task_runner/return_home_test.py delete mode 100644 utils/tests/visual_track_control/__pycache__/frame_transform_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/visual_track_control/__pycache__/line_of_sight_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/visual_track_control/__pycache__/mavsdk_gateway_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/visual_track_control/__pycache__/pid_controller_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/visual_track_control/__pycache__/rate_limiter_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/visual_track_control/__pycache__/staleness_checker_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/visual_track_control/__pycache__/tracking_control_law_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/tests/visual_track_control/__pycache__/vtc_config_test.cpython-312-pytest-9.1.1.pyc delete mode 100644 utils/visual_track_control/__pycache__/__init__.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/command_sender.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/config.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/frame_transform.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/line_of_sight.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/mavsdk_gateway.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/pid_controller.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/rate_limiter.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/staleness_checker.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/tracking_control_law.cpython-312.pyc delete mode 100644 utils/visual_track_control/__pycache__/types.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0b8f2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Qoder local settings (personal MCP config) +.qoder/settings.local.json + +# ---- Python 生成物 ---- +__pycache__/ +*.py[cod] +.pytest_cache/ +*.egg-info/ + +# ---- 运行产物(可重跑生成)---- +logs/ +trajectory_plot.png +l1_sitl_trajectory.csv +pod_sitl_trajectory.csv + +# ---- 大文件 / 外部数据 ---- +legacy/models/*.pt +stereo_calib_params.npz +stereo_calib_images/ + +# ---- 本地工具与索引 ---- +.codegraph/ +.idea/ +.workbuddy/ diff --git a/README.md b/README.md index ca3b414..514e597 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,182 @@ +# code_drone-test —— 无人机视觉目标追踪与控制系统 -# code-test-drone +> 最后对齐:**2026-10-19**。本文描述的是**当前运行架构**;历史设计与规格见 `docs/`。 -本项目包含一个视觉目标追踪与控制系统的 C++ 参考实现及其 Python 移植版本,用于无人机对移动目标的追踪与控制。 +--- -## 项目结构 +## 0. 先看这一段(避免按旧设计干活) + +本项目历史上存在**两条感知路线**,现状如下: + +| 路线 | 组成 | 现状 | +|---|---|---| +| **运行链路(现行)** | 光电吊舱(表2-2 角度 + bit2) → `pod_bridge` → `task_runner` → MAVSDK offboard → PX4 | ✅ 已落地,SITL + 真吊舱在环均验证过 | +| 可选感知后端(备用) | `stereo_yolo` → `target_tracker` → `stereo_ranger` → `visual_track_control` | 📦 **已归档到 `legacy/`**(175 条测试随包归档,默认不跑) | + +**关键约定(2026-10-19 定稿)** + +- **吊舱取代双目 + YOLO,是唯一目标源**;方位/LOS 来自吊舱脱靶量(角度直入)。 +- 目标有效性 = 吊舱 bit2 + 遥测新鲜 + **LOS 运动学一致性**(`pod_bridge/los_consistency.py`)。 + 旧的 `require_vision`(我方视觉第二判据)**已删除** —— 它指向的传感器已不在链路里。 +- **距离通道尚未接入**:`distance_valid` 恒 False → 控制律走 `slow_approach`(0.2 m/s 盲飞接近)。 + 规划中由激光雷达提供(低算力);双目因 Pi 4 上 YOLO 开销大,暂不接入。 +- 全部数值参数为**占位值**,标定回填前不得实飞。 + +权威文档:`.workbuddy/memory/ARCHITECTURE.md`(架构全貌与铁律)、 +`预定流程-操作指南.md`(操作与待办)、`工作进度.md`(进度)、`docs/`(规格)。 + +--- + +## 1. 目录结构 ``` -code-test-drone/ -├── reference-c++/ # C++ 参考实现 -│ ├── target_tracker/ # 目标追踪库(独立子工程) -│ │ ├── include/ # 头文件目录 -│ │ ├── src/ # 源文件目录 -│ │ └── tests/ # 单元测试 -│ └── visual_track_control/# 视觉追踪控制库 -│ ├── include/ # 头文件目录 -│ ├── src/ # 源文件目录 -│ └── tests/ # 单元测试 -└── utils/ # Python 移植 - ├── target_tracker/ # 目标追踪(Python 版) - ├── visual_track_control/# 追踪控制(Python 版) - └── tests/ # 单元测试 +code_drone-test/ +├── utils/ # Python 实现(主体) +│ ├── pod_bridge/ # 吊舱 ↔ PX4 桥接(现行运行链路核心) +│ │ ├── bridge.py # PodBridge:L1/L2 运行时切换、目标门控、链路维持 +│ │ ├── protocol.py # 表2-1/2-2/3/4 帧编解码 +│ │ ├── pod_serial.py # 串口链路(115200 8N1) +│ │ ├── px4_gateway.py # MAVSDK 遥测订阅 + offboard 指令 +│ │ ├── guidance.py # L2 地理引导律(飞到坐标) +│ │ ├── ground_link.py # 地面站 UDP 坐标注入 + 命令队列 +│ │ ├── los_consistency.py # 目标有效性第二判据(LOS 运动学一致性) +│ │ ├── virtual_pod.py # 虚拟吊舱(SITL/台架) +│ │ └── {run_bridge,listen_pod,pod_console,sim_fc,verify_px4}.py # 台架工具 +│ ├── task_runner/ # 任务级编排(现行) +│ │ ├── state_machine.py # WAIT→ARM_TAKEOFF→GOTO→SEARCH→TRACK→RTL +│ │ ├── runner.py # 编排器(采集快照/动作/异常兜底) +│ │ ├── return_home.py # 自定义返航(回起飞点→恢复航向→降落,不爬高) +│ │ ├── pod_control.py # 吊舱识别/锁定指令(0x07/0x0F/0x0E/0x08) +│ │ └── log.py # 事件 JSONL + 遥测 CSV + 简报 +│ ├── visual_track_control/ # 控制律 + MAVSDK 网关(被运行链路复用) +│ ├── sitl_support.py # 入口共用:时钟/遥测订阅/连接准备/速率常量 +│ ├── config/ # 配置 JSON(pod_bridge.json 为主线配置) +│ ├── tests/ # pytest(446 条,仅运行链路) +│ └── README.md # utils 索引 +├── legacy/ # 已归档:双目 + YOLO 可选感知后端(175 条测试) +│ ├── utils/ # stereo_yolo / stereo_ranger / target_tracker + demo + tests +│ ├── docs/ # 三份双目规格 +│ ├── models/ # YOLO 权重 +│ └── README.md # 归档说明与运行方式 +├── tools/ # 临时/台架脚本(相机标定、视频预览;不参与运行链路) +├── ground_station/ # 地面站页面(Flask:坐标注入/命令/状态/视频) +├── task_runner_sitl.py # 任务全流程 SITL 入口(虚拟吊舱) +├── task_runner_real.py # 任务全流程真机入口(真吊舱串口 + UDP 注入) +├── l1_sitl_test.py # L1 航向伺服 SITL 集成测试(只覆盖控制律段) +├── reference-c++/ # C++ 参考实现(target_tracker / visual_track_control) +├── docs/ # 规格与方案(权威来源) +└── .workbuddy/memory/ # 项目记忆:ARCHITECTURE.md / MEMORY.md ``` -## 主要模块 +--- -### target_tracker - 目标追踪库 +## 2. 运行链路(现行) -提供基于卡尔曼滤波的目标追踪功能: +``` +地面站 ──UDP 坐标/命令──▶ ground_link ──▶ TaskRunner(状态机) + │ + ┌─────────────────────────────────────┼──────────────────────────────┐ + ▼ ▼ ▼ + GOTO(L2 飞参考点) ──到点──▶ SEARCH(扫视) ──锁定──▶ TRACK(L1 缓慢靠近) ──超时──▶ RTL + │ │ + 吊舱表2-2 角度/bit2 自定义返航→垂直降落 + ▲ │ + PodBridge ──表3 姿态──▶ 吊舱 PX4 offboard + │ + PX4 遥测 ◀── MAVSDK +``` -- **KalmanBoxFilter**: 8维卡尔曼滤波器(位置、速度) -- **Tracklet**: 目标轨迹管理与状态更新 -- **TargetTracker**: 主追踪器,包含关联、选举逻辑 +- **状态链**:`WAIT → ARM_TAKEOFF → GOTO → SEARCH → TRACK → RTL` + (`APPROACH` 保留但已非主流程;`TRACK` 不再因"目标居中"退出) +- **参考点语义**:地面坐标是**搜索起点**,不是终点;到点后转原地扫视。 +- **缓慢靠近**:复用 `TrackingControlLaw` 的无距离降级(`no_distance_action=slow_approach`,0.2 m/s)。 +- **返航**:不调 PX4 原生 `return_to_launch()`(会爬到 `RTL_RETURN_ALT`,SITL 默认 30 m), + 改为保持转场高度回起飞点 → 原地恢复初始航向 → 垂直降落。 -核心特性: -- 贪婪关联算法进行检测匹配 -- 主目标选举三级回退机制 -- 支持 Coast(纯预测)与 Lost(丢失)状态管理 +--- -### visual_track_control - 视觉追踪控制库 +## 3. 环境与快速开始 -基于追踪结果生成无人机控制指令: +**Python 环境(唯一口径)**:conda env `pytorch` +(`D:\program file\conda\envs\pytorch\python.exe`,Python 3.12;含 numpy/mavsdk/pytest/flask/pyserial)。 -- **PIDController**: 距离 PID 控制 -- **TrackingControlLaw**: 追踪控制律 -- **MavlinkCommandGateway**: MAVLink 指令网关 -- **PixelToAngle**: 像素到角度转换 -- **StalenessChecker**: 数据时效性检查 -- **SlewRateLimiter**: 速率限制器 - -支持多种控制模式: -- **Rate 模式**: 角速率控制 -- **Position 模式**: 绝对航向控制 -- **距离控制**: 接近/后退策略 - -## 环境要求 - -### C++ 版本 -- CMake 3.10+ -- C++17 编译器 -- MAVLink 库(MAVSDK) - -### Python 版本 -- Python 3.12+ -- 依赖:`numpy`, `pymavlink`, `mavsdk` - -## 安装 - -### Python 环境 ```bash -pip install -r requirements.txt +# 单元测试(621 条) +cd "D:/ProgramData/code_drone-test/utils" +"D:/program file/conda/envs/pytorch/python.exe" -m pytest -q + +# 吊舱串口自检(只读;--detect 会下发 0x07) +"D:/program file/conda/envs/pytorch/python.exe" utils/pod_bridge/listen_pod.py COM5 --seconds 5 + +# S2 台架干跑:只接吊舱、不下发 offboard +"D:/program file/conda/envs/pytorch/python.exe" utils/pod_bridge/run_bridge.py --port COM5 --dry-run --duration 10 + +# 任务全流程 SITL(需 WSL 里 PX4 SITL) +"D:/program file/conda/envs/pytorch/python.exe" task_runner_sitl.py --conn udpin://0.0.0.0:14540 + +# 地面站页面 +"D:/program file/conda/envs/pytorch/python.exe" ground_station/app.py --udp-port 14555 --log-dir logs + +# 真机入口(真吊舱串口 + 地面站 UDP 注入 + PX4) +"D:/program file/conda/envs/pytorch/python.exe" task_runner_real.py --port COM5 \ + --conn udpin://0.0.0.0:14540 --target-type 1 # --land 改为就地降落 ``` -### C++ 构建 +> MAVSDK 连接串:本机 SITL 用 `udpin://0.0.0.0:14540`(WSL 镜像网络下可直接连通)。 + +--- + +## 4. 配置 + +主线配置:`utils/config/pod_bridge.json`(分组:`pod_serial` / `px4` / `link` / `guidance` / +`heading_servo` / `tracking` / `ground_inject`)。 + +- `heading_servo`:L1 开关、β/α 符号(S1 已标定 +1/−1)、 + **LOS 一致性判据**(`los_consistency_enabled` / `los_window_ms` / + `los_min_yaw_change_deg` / `los_max_residual_deg`)。 +- `tracking`:内嵌整份控制律配置(各分组须齐全);无距离时看 + `distance.no_distance_action` 与 `no_distance_approach_limit_mps`。 +- `ground_inject`:地面站 UDP 端口与坐标超龄门限;**真机入口会用 `timeout_ms` + 统一 `guidance.target_stale_ms`**(避免"走走停停")。 + +任务级参数在 `task_runner_{sitl,real}.py` 的 `build_sm_config()` / `build_return_config()`: +到达半径、搜索/追踪超时、扫视速率、返航速度与航向容差。**均为占位值,须台架标定。** + +--- + +## 5. 测试 + ```bash -cd reference-c++/target_tracker -mkdir build && cd build -cmake .. -make +# 运行链路(446 条) +cd utils && "D:/program file/conda/envs/pytorch/python.exe" -m pytest -q -cd ../../visual_track_control -mkdir build && cd build -cmake .. -make +# 归档的可选后端(175 条,默认不跑) +cd legacy/utils && PYTHONPATH="../../utils" "D:/program file/conda/envs/pytorch/python.exe" -m pytest -q ``` -## 测试 +| 组 | 条数 | 说明 | +|---|---|---| +| `visual_track_control` | 86 | 控制律(运行链路复用) | +| `pod_bridge` | 208 | 协议/桥接/引导/注入/**LOS 一致性** | +| `task_runner` | 122 | 状态机/编排器/**自定义返航**/日志/吊舱控制 | +| `ground_station` | 30 | 页面与日志解读 | +| **运行链路合计** | **446** | | +| (归档)`stereo_ranger` / `stereo_yolo` / `target_tracker` | 175 | 在 `legacy/`,见 `legacy/README.md` | + +--- + +## 6. C++ 参考实现 + +`reference-c++/` 是 `target_tracker` 与 `visual_track_control` 的参考实现(各自独立 CMake +子工程,含 gtest)。Python 侧为逐构件移植 + 行为修复,二者语义对齐。当前**无 build 目录**, +未复跑 `cmake && ctest`。构建方式: -### Python 测试 ```bash -cd utils -pytest +cd reference-c++/ && mkdir -p build && cd build && cmake .. && make && ctest ``` -### C++ 测试 -```bash -cd reference-c++/target_tracker/build -ctest +--- -cd ../../visual_track_control/build -ctest -``` - -## 配置 - -C++ 与 Python 版本均使用 JSON 格式配置文件: - -```json -{ - "control": { - "frequency_hz": 20.0, - "visual_stale_ms": 200, - "radar_stale_ms": 500, - "attitude_stale_ms": 300 - }, - "camera": { - "image_width": 1920, - "image_height": 1080, - "fov_h_deg": 60.0, - "fov_v_deg": 35.0 - }, - "heading": { - "mode": "rate", - "gain": 1.0, - "yaw_rate_limit_dps": 30.0, - "yaw_slew_limit_dps2": 60.0 - }, - "distance": { - "d_exp_m": 10.0, - "kp": 0.5, - "ki": 0.05, - "kd": 0.1, - "integral_limit": 2.0 - } -} -``` - -## 使用示例 - -### Python 追踪演示 -```bash -cd utils -python demo_track_control.py -``` - -### C++ 追踪演示 -```bash -cd reference-c++/visual_track_control/build -./track_control_demo -``` - -## 移植说明 - -Python 版本与 C++ 版的接口差异请参考各子目录的 README: -- [target_tracker Python 版](./utils/target_tracker/README.md) -- [visual_track_control Python 版](./utils/visual_track_control/README.md) - -## 许可证 +## 7. 许可 本项目遵循开源许可证协议。 - -## 贡献指南 - -欢迎提交 Issue 和 Pull Request 改进本项目。 \ No newline at end of file diff --git a/__pycache__/test.cpython-312.pyc b/__pycache__/test.cpython-312.pyc deleted file mode 100644 index d96a7127eb67f690921f36e298a50d8f8ad808c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37688 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 diff --git a/docs/L1航向伺服实现方案.md b/docs/L1航向伺服实现方案.md index bd7b05a..eafdad8 100644 --- a/docs/L1航向伺服实现方案.md +++ b/docs/L1航向伺服实现方案.md @@ -1,8 +1,11 @@ -# L1 航向伺服落地方案(无距离,树莓派 / Python) +# L1 航向伺服落地方案(无距离,树莓派 / Python) > 目标:**先把"只修正飞行方向、不需要距离"的部分落地**,速度-距离闭环(L2)滞后。 > 基于结论:本吊舱无激光 → 表4 永不出,且不允许二次开发(部署文档 §10.0 / §10)。 -> 日期:2026-10-09 状态:**S0 已实现(路线 A1 + L1 胶水层),全量 396 passed;符号待台架 S1 标定** +> 日期:2026-10-09 起,**2026-10-19 更新** 状态: +> **S0/S1/S2 + SITL + 真吊舱在环已验证**(全量 621 passed)。 +> 后续变更:第二判据 `require_vision` → **LOS 运动学一致性**(§2.3/§6); +> 距离源由「双目」改为规划中的「激光雷达」(§7)。 --- @@ -114,7 +117,20 @@ L1 的 `tracked` 判据按阶段递进: | 阶段 | 判据 | |---|---| | 台架(S1~S2) | bit2 锁定 **且** 遥测未超龄(现有 `_pod_locked()`)+ **人工在旁观察**,随时按键 `u` 解锁 | -| 接入视觉后(S3+) | bit2 **且** 我方 `stereo_yolo`/`target_tracker` 有目标(**第二判据**,必做) | +| ~~接入视觉后(S3+)~~ | ~~bit2 **且** 我方 `stereo_yolo`/`target_tracker` 有目标~~ | +| **现行(2026-10-19 起)** | bit2 **且** 遥测未超龄 **且 LOS 运动学一致性**(`pod_bridge/los_consistency.py`) | + +> ⚠ **2026-10-19 架构变更**:后续决定**吊舱取代双目 + YOLO 作为唯一目标源**, +> 因此原「我方视觉第二判据」(`require_vision` / `vision_target_fn`) 已**删除** —— +> 它要么恒 False(链路永远进不了 TRACK),要么指向吊舱自身(自证,零信息量)。 +> +> 替代判据:本吊舱无电机、相机刚性固连机体,机头转 Δψ 时真实目标的脱靶量必变 +> ≈−Δψ。故用 `residual = Δβ + Δψ`(3s 窗口,激励门槛 20°、残差上限 8°)判断 +> 「角度是活的还是冻住的」。超限 → `tracked=False` 且 `pod_locked()` 也返回 False +> (状态机会退出 TRACK 重新搜索,而不是只让控制律单方面刹车)。 +> +> **局限**:它只能证明「角度是活的」,不能证明「是那个目标」。若吊舱锁错了物体 +> 但角度正常变化,本判据无法识别 —— 那属于识别问题,需另接独立传感器。 --- @@ -149,7 +165,12 @@ L1 的 `tracked` 判据按阶段递进: "alpha_from_frame": false, "alpha_from_off_target": true, "vertical_enabled": true, // 决策3:无人机 L1 开垂直(vz 由 tracking.vertical 压低) - "require_vision": false // true = 需我方视觉第二判据(S3 起开,§2.3) + + // 第二判据:LOS 运动学一致性(2026-10-19 取代 require_vision,见 §2.3) + "los_consistency_enabled": true, // false → 退回"只信 bit2" + "los_window_ms": 3000, // 比较窗口 + "los_min_yaw_change_deg": 20.0, // 窗口内机头至少转这么多才判定(激励门槛) + "los_max_residual_deg": 8.0 // |Δβ+Δψ| 上限,超过判不一致 } ``` @@ -187,10 +208,10 @@ L1 的 `tracked` 判据按阶段递进: | 风险 | 判据 / 对策 | |---|---| | **符号反了** → 反向转 | S1 标定 + S2 干跑必验;配置化而非硬编码 | -| bit2 粘滞 → 丢失后仍转 | 第二判据(我方视觉);台架期人工值守 | +| bit2 粘滞 → 丢失后仍转 | **LOS 运动学一致性**(`pod_bridge/los_consistency.py`,2026-10-19 取代旧「我方视觉」方案):机头转 Δψ 而脱靶量不动 → 判丢;台架期仍人工值守 | | 框架角定义不符(绝对而非相对机体) | S1 实测确认,不符则改用脱靶量积分等替代 | | 云台归中/搜索态框架角乱转 | 仅锁定态启用;非锁定 → `tracked=False` → BRAKE_HOVER | -| 垂直通道撞地 | L1 强制 `vz_gain=0` | +| 垂直通道撞地 | L1 强制 `vz_gain=0`(本机实际按决策3 开垂直但压低限速) | | 测距噪声(L2 才有) | L2 滞后,且**不用连续 PID**,改分段限速 | --- @@ -200,7 +221,8 @@ L1 的 `tracked` 判据按阶段递进: L1 落地时就留好口子,将来 L2 只是"换个输入源",不动结构: - `ControlSnapshot.distance_valid` / `target_distance_m` / `distance_time_ms` 字段 - **现在就按 `False` 填全**,L2 时换成 `stereo_ranger` 的 `forward_distance_m`; + **现在就按 `False` 填全**;将来接**激光雷达**距离源后填入(原计划的 + `stereo_ranger.forward_distance_m` 已随"吊舱取代双目"一并搁置); - `bridge.step()` 按配置在 `TrackingControlLaw`(L1)与 `GuidanceLaw`(L2)间切换; - `guidance.py` 的输入接口保持不变,等有标定过的距离源再启用。 @@ -224,18 +246,22 @@ L1 落地时就留好口子,将来 L2 只是"换个输入源",不动结构 | 2 | `visual_track_control/tracking_control_law.py` | ✅ 第④步角度直入优先,否则回退像素 | | 3 | `visual_track_control/config.py` | ✅ 拆出 `load_from_dict()` 供 bridge 内嵌复用 | | 4 | `pod_bridge/config.py` | ✅ 加 `HeadingServoConfig` + 内嵌 `TrackingConfig`,严格校验符号(±1) | -| 5 | `pod_bridge/bridge.py` | ✅ `_build_snapshot()`;`step()` 按 enabled 切 L1/L2;`_dispatch` 对 DEGRADED_HOLD 补发刹车保心跳;`vision_target_fn` 注入点(§2.3 第二判据) | +| 5 | `pod_bridge/bridge.py` | ✅ `_build_snapshot()`;`step()` 按 enabled 切 L1/L2;`_dispatch` 对 DEGRADED_HOLD 补发刹车保心跳;~~`vision_target_fn` 注入点~~ → **已删除,改为 LOS 一致性判据**(`los_consistency.py`,§2.3) | | 6 | `config/pod_bridge.json` | ✅ 落 L1 首飞值(slow_approach/0.2、垂直压低、无电机 → 仅脱靶量) | | 7 | `tests/` | ✅ 新增 24 条(vtc 5 + config 9 + bridge L1 10);全量 **396 passed** | | 8 | `pod_bridge/run_bridge.py` | ✅ 闭环 CLI 入口:`--dry-run`(S2 只接吊舱/不发 offboard/打印 β→yaw_rate) 与实飞(S3+,`PodBridge.connect`+信号优雅停机);配套 `run_bridge_test.py` 5 条,全量 **401 passed** | | 9 | `pod_bridge/bridge.py:run()` | ✅ 循环周期改按生效律取频率(L1=tracking.control,L2=guidance),修 L1 live 频率错配 | | 10 | `pod_bridge/virtual_pod.py` | ✅ 模拟吊舱(SITL/台架工具):按无电机语义合成表2-2(框架角恒0、无激光、不可见→bit2=0+脱靶量归0),符号与 S1 标定一致 | -| 11 | `pod_sitl_test.py`(根目录) | ✅ L1 SITL 全流程(test.py 吊舱版):解锁→起飞→锚点→offboard→L1 闭环(VirtualPod→PodBridge→控制律)→丢目标→刹车→RTL→落地→上锁,轨迹落 CSV;飞行编排不塞进 run_bridge(保持其纯 offboard 闭环入口) | +| 11 | `l1_sitl_test.py`(根目录,原 `pod_sitl_test.py`) | ✅ L1 SITL 全流程:解锁→起飞→锚点→offboard→L1 闭环(VirtualPod→PodBridge→控制律)→丢目标→刹车→RTL→落地→上锁,轨迹落 CSV;飞行编排不塞进 run_bridge(保持其纯 offboard 闭环入口) | | 12 | `tests/pod_bridge/virtual_pod*_test.py` | ✅ 15 条:S1 符号回归 + VirtualPod×PodBridge×控制律集成(右转/下降/慢速前近/丢目标刹车);全量 **417 passed** | +| 13 | `pod_bridge/los_consistency.py`(2026-10-19) | ✅ 目标有效性第二判据:LOS 运动学一致性(Δβ+Δψ,3s 窗口/20° 激励/8° 残差);`pod_locked()` 叠加该判据;配套 `los_consistency_test.py` 12 条 + 桥接层 3 条 | **待办(非代码,台架)**:~~S1 标定~~ **已完成(2026-10-09)**:目标在左下时 `off_target_x/y` 均为负 → 吊舱 X 轴右正(与 β 同向)`beta_sign=+1`;Y 轴上正(与 α 下正相反) `alpha_sign=-1`(已回填 `pod_bridge.json`,并加标定回归测试守住)。厂商确认本版 -**电机无用** → `beta_from_frame/alpha_from_frame` 恒 false。剩 **S2 干跑** -`run_bridge.py --dry-run` 复核方向与量级(目标偏左→yaw_rate 负、居中→趋 0),并标定 `heading.gain`; -之后 **SITL 全流程** `python pod_sitl_test.py`(需 PX4 SITL,本机尚未实跑)→ S3 不装桨接飞控。 +**电机无用** → `beta_from_frame/alpha_from_frame` 恒 false。 + +**已完成的后续验证(2026-10-19)**:S2 干跑(`run_bridge.py --dry-run`,真吊舱 COM5,20Hz +闭环无异常)、SITL 全流程(`l1_sitl_test.py` 与 `task_runner_sitl.py` 均实跑)、 +真吊舱在环全流程(`task_runner_real.py --port COM5`,无目标时走"搜索超时→返航"安全路径)。 +剩:**`heading.gain` 标定**、**吊舱真实目标识别(bit2 0→1)**、S3 不装桨接飞控。 diff --git a/docs/光电吊舱控制协议G版-PB.docx b/docs/光电吊舱控制协议G版-PB.docx new file mode 100644 index 0000000000000000000000000000000000000000..2062dbf007dcfce15dfd4c3401ae6ffe491a0b93 GIT binary patch literal 38972 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 I 目标平台:树莓派 4B + **Raspberry Pi OS 官方镜像(64 位,Bookworm / Trixie)**。 > (2026-09-23 由 Ubuntu 24.04 换回官方镜像:Ubuntu 镜像上串口配置与 @@ -48,7 +48,7 @@ pytest # 仅上机自测需要,部署运行可省 - **mavsdk 4.x 是全新原生 API 重写,与经典 gRPC API 完全不兼容**(2026-09-24 树莓派实测):没有 `mavsdk.offboard` / `System` / `telemetry`,本仓库全部 - 飞控代码(Px4Gateway / MavsdkCommandGateway / test.py)都是经典 API, + 飞控代码(Px4Gateway / MavsdkCommandGateway / 各入口脚本)都是经典 API, 装 4.x 直接 `LegacyModuleError`。**必须锁 `mavsdk<4`**(最新 3.17.4, aarch64 官方 wheel `manylinux2014_aarch64`,PyPI 已核对); 32 位 armv7l 支持不稳定——这是必须 64 位镜像的主因。 @@ -66,7 +66,7 @@ pytest # 仅上机自测需要,部署运行可省 - 连接串写法(`pod_bridge.json` 的 `px4.connection_string`): - Pi 串口直连飞控 TELEM:`serial:///dev/ttyUSB0:57600`(波特率按飞控侧设置); - - 经数传/仿真:`udp://:14540`。 + - 经数传/仿真:`udpin://0.0.0.0:14540`(本机 SITL 实测可用;`udp://:14540` 亦可,语义为监听该端口)。 - PX4 侧需确认 TELEM 口已配 MAVLink 且 offboard 心跳未被占用。 ## 4. 串口硬件(吊舱侧,最容易翻车) @@ -251,26 +251,28 @@ python -m pod_bridge.sim_fc /dev/ttyAMA0 --detect person --auto-lock \ | 受影响项 | 后果 | |---|---| -| `guidance.py` 地理位置引导律 | **永久刹车**。桥接层 `bridge.py:169` 的 `target_valid` 依赖表4;表4 恒缺 → `_latest_target` 恒 None → `GuidanceLaw.update()` 每周期走 `_brake("载机位置或目标不可用/超龄")`。安全方向正确(降级刹车、不会乱飞),但**吊舱表4 这条引导路径在本设备上等于不存在** | -| 目标距离/坐标来源 | 必须改由**我方自有链路**提供:`stereo_yolo` 检测 → `stereo_ranger` 双目视距。吊舱只当作"稳定指向 + 表2-2 脱靶量"的伺服/传感器用 | +| `guidance.py` 地理位置引导律 | **表4 路径在本设备上恒不可用**(`target_valid` 恒假)。**现行做法:坐标走地面站 UDP 注入**(`ground_link` → `ground_target_fn`),`guidance` 吃注入坐标;表4 仅作回退 | +| 目标距离/坐标来源 | **坐标 = 地面站注入**(已落地)。**距离尚未接入**:规划由激光雷达提供(低算力);~~`stereo_yolo` 检测 → `stereo_ranger` 双目视距~~ 因 Pi 4 跑不动实时 YOLO,**暂不接入** | | `--auto-unlock`(§10.1) | **必然误触发,已实测禁用**:`last_tgt_s` 恒 None 被当成超时。见 §10.1 | | 后续选型 | 若要保留吊舱侧目标定位,只能换**带激光测距的吊舱型号**(且需确认激光能接入吊舱解算链路) | -#### 架构该怎么办(分叉已选边) +#### 架构该怎么办(2026-10-19 已定稿) -既然距离无法回灌、表4 不会有,目标地理坐标就**必须由我方自己解算**: +既然距离无法回灌、表4 不会有,目标地理坐标就**必须来自吊舱之外**。当时的设想是 +"双目自算坐标";实际实现改成了更简单的分工: ``` -stereo_yolo 检测 → stereo_ranger 双目视距(斜距/前向距离) - → 载机 GPS + 姿态(表3 我方已有) + 吊舱框架角(表2-2) - → **我方自己算出目标经纬度**,再喂给 guidance.py +目标**坐标** ← 地面站 UDP 注入(ground_link) ← 已落地 +目标**方位** ← 吊舱表2-2 脱靶量(角度直入 L1) ← 已落地 +目标**距离** ← 待接激光雷达(串口,低算力) ← 未接 ``` 吊舱在本设备上退化为:**伺服稳定指向 + 表2-2 脱靶量/框架角传感器**。 它仍然很有用(稳像、跟得住、给得出视场角),只是不再提供坐标。 -> 这意味着 `guidance.py` 的输入要从"吊舱表4"换成"我方自算的目标经纬度", -> `bridge.py:169` 的 `target_valid` 判据也要跟着改。**动手前先对齐方案再实现。** +> ~~下文"stereo_yolo → stereo_ranger 自算经纬度再喂 guidance"的方案~~ 已被上述 +> 分工取代:`guidance.py` 的输入改为**注入坐标**(已实现),不需要我方自算经纬度。 +> 距离接入后,只影响控制律的 `distance_valid` 分支,不影响坐标来源。 > 复测命令(`--json` 后是**输出产物路径**,非预置输入文件,建议给绝对路径): > ``` @@ -298,7 +300,9 @@ SEC 秒重复一次(与"假锁定"形成持续震荡:解锁 → 自动锁定 → 2. 表4 超时(需先确认表4 存在); 3. 锁定持续超过 T 秒且**从未**收到表4 → 判为假锁定,解锁一次并告警(不循环); 4. 脱靶量恒为 0 持续 T 秒 —— 居中跟踪时也会为 0,**误杀风险高**,不推荐; -5. 由我方视觉链路(stereo_yolo / target_tracker)交叉验证 —— 最可靠,待集成。 +5. ~~由我方视觉链路(stereo_yolo / target_tracker)交叉验证~~ → **已实行为 LOS 运动学一致性** + (`pod_bridge/los_consistency.py`,2026-10-19):无电机、刚性固连 → 机头转 Δψ 时脱靶量 + 必变 ≈−Δψ,不动即判丢。无需额外传感器;局限是只证"角度是活的",不证"是那个目标"。 飞行期的正确行为是**刹车保护**(现有 `target_stale_ms` 已覆盖),与"解锁以便重新 搜索"是两件事,不要混为一谈。 diff --git a/ground_station/app.py b/ground_station/app.py index c6572e8..421612c 100644 --- a/ground_station/app.py +++ b/ground_station/app.py @@ -1,30 +1,193 @@ """ground_station/app.py —— 地面站页面(Flask,临时测试用)。 -预定流程 C 阶段:地面坐标输入 + 状态可视化配套页面。 +预定流程 C 阶段:地面坐标输入 + 任务/吊舱控制 + 状态可视化配套页面。 功能: - * GET / 地面站页面(坐标输入 + 状态面板 + 模拟视野) - * POST /api/inject 接收 {lat, lon, alt},UDP 发到吊舱桥接的 ground_inject 端口 - (默认 14555,GroundLink.run() 接收) - * GET /api/state 返回最新任务状态(读任务日志:事件最后状态 + 遥测最后一行) + * GET / 地面站页面(坐标注入 + 任务控制 + 吊舱控制 + 状态面板 + 模拟视野) + * GET /px4 PX4 信息子页面(实时遥测 + 任务日志解读分页) + * POST /api/inject 接收 {lat, lon, alt} 或局部 {north_m, east_m, alt} + (局部按注入时刻无人机位置换算为经纬度),UDP 发到 ground_inject + 端口(默认 14555,GroundLink.run() 接收);同时刷新持续注入内容 + * POST /api/continuous 接收 {on: bool},启停**服务端**持续注入(后台线程每 0.5s 重发) + * POST /api/command 接收 {cmd}(白名单:rtl/emergency/pod_*),同端口 UDP 下发, + 由 TaskRunner 主循环 drain_commands() 分派 + * GET /api/link 返回链路目标(坐标/命令发往的地址)+ 本机可选地址列表 + * POST /api/link {"host":.., "port":..} 切换链路目标(默认 127.0.0.1:14555) + * GET /video_feed MJPEG 实时视频(服务端 RTSP 拉流转推;未配置 --rtsp-url 时 503) + * GET /api/state 返回最新任务状态(读任务日志:事件最后状态 + 遥测最后一行, + 附 continuous 标志与 video 状态) + * GET /api/logs/telemetry 整段遥测 CSV → JSON(子页面图表用;只读) + * GET /api/logs/events 整段事件 JSONL → JSON(子页面解读用;只读) -与无人机 runner 解耦:本页面只做「发坐标(UDP)+ 读日志(只读)」,不侵入 runner。 +与无人机 runner 解耦:本页面只做「发 UDP(坐标/命令)+ 读日志(只读)」,不侵入 runner。 +持续注入放服务端的原因:浏览器会节流不可见标签页的 JS 定时器(实测 0.5s 被降到约 +1 次/分钟,注入超龄致任务误返航),服务端线程不受页面可见性影响。 用法: python ground_station/app.py --udp-port 14555 --log-dir logs + python ground_station/app.py --rtsp-url rtsp://192.168.144.119:8554/video """ import argparse import csv import json +import math import os import socket +import sys +import threading from pathlib import Path -from flask import Flask, jsonify, render_template, request +from flask import (Flask, Response, jsonify, render_template, request, + stream_with_context) + +_REPO_ROOT = Path(__file__).resolve().parent.parent # 兼容「脚本直跑 / 包导入」双模式 +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from ground_station.video_source import RtspVideoSource # noqa: E402 _GROUND_UDP_PORT = 14555 _LOG_DIR = "logs" +# 日志解读接口的返回上限(超限返回最近 N 条,防内存/流量失控) +_LOG_MAX_ROWS = 60000 +_LOG_MAX_EVENTS = 5000 + +# 允许页面下发的控制命令(与 task_runner/runner.py 的 _POD_COMMAND_METHODS 对齐) +_COMMAND_WHITELIST = frozenset({ + "rtl", "emergency", + "pod_detect_on", "pod_auto_lock", "pod_stop_track", + "pod_detect_off", "pod_center", +}) + + +class _LinkTarget: + """链路目标:坐标/命令 UDP 发往的地址(页面可运行时切换)。""" + + def __init__(self, host: str, port: int) -> None: + self.host = host + self.port = int(port) + + +def _local_ipv4_list() -> list: + """本机 IPv4 地址列表(供页面下拉选择;解析失败返回空列表)。""" + ips = [] + try: + for info in socket.getaddrinfo(socket.gethostname(), None, + socket.AF_INET): + ip = info[4][0] + if ip not in ips: + ips.append(ip) + except OSError: + pass + return ips + + +def _is_number(value) -> bool: + """数值(排除 bool)。""" + return isinstance(value, (int, float)) and not isinstance(value, bool) + + +_DEG_LAT_M = 111320.0 # 局部平面近似的纬度米/度(与 task_runner_sitl 同口径) + + +def _offset_to_latlon(lat0: float, lon0: float, north_m: float, + east_m: float) -> tuple: + """北东偏移(米)→ 经纬度(局部平面近似,小范围够用)。""" + lat = lat0 + north_m / _DEG_LAT_M + lon = lon0 + east_m / (_DEG_LAT_M * math.cos(math.radians(lat0))) + return lat, lon + + +def _session_start_position(log_dir: str): + """本次任务运行段(遥测最后一段)的首个有效定位 ≈ 起飞点/home。 + + 运行段按 t_s 回退切分;取最后一段里第一条 pos_valid=True 的经纬度。 + 无日志 / 无有效定位 → (None, None)。 + """ + path = Path(log_dir) / "task_telemetry.csv" + try: + with open(path, encoding="utf-8", newline="") as f: + rows = list(csv.DictReader(f)) + except OSError: + return None, None + start = 0 + for i in range(1, len(rows)): + try: + if float(rows[i].get("t_s") or 0) < float(rows[i - 1].get("t_s") or 0): + start = i + except ValueError: + continue + for row in rows[start:]: + if (row.get("pos_valid") == "True" and row.get("latitude_deg") + and row.get("longitude_deg")): + try: + return float(row["latitude_deg"]), float(row["longitude_deg"]) + except ValueError: + continue + return None, None + + +def _send_udp(target: _LinkTarget, payload: dict) -> None: + """把 JSON 发到链路目标(坐标注入 / 持续注入 / 控制命令共用)。""" + data = json.dumps(payload, ensure_ascii=False) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.sendto(data.encode("utf-8"), (target.host, target.port)) + + +class _ContinuousInjector: + """服务端持续注入:后台线程按固定周期重发最近一次坐标。 + + 页面 JS 定时器在标签页不可见时被浏览器节流(实测 0.5s → 约 1 次/分钟, + 注入超龄致任务误返航),故持续注入放服务端;手动「注入坐标」同步刷新内容。 + """ + + def __init__(self, target: _LinkTarget, interval_s: float = 0.5) -> None: + self._target = target + self._interval_s = max(0.05, float(interval_s)) + self._lock = threading.Lock() + self._payload = None + self._stop = threading.Event() + self._thread = None + + @property + def running(self) -> bool: + return self._thread is not None and self._thread.is_alive() + + def update(self, payload: dict) -> None: + with self._lock: + self._payload = payload + + def start(self) -> bool: + """开始持续注入;尚无坐标(未手动注入过)→ False。""" + with self._lock: + if self._payload is None: + return False + if self.running: + return True + self._stop.clear() + self._thread = threading.Thread(target=self._loop, + name="continuous-inject", daemon=True) + self._thread.start() + return True + + def stop(self) -> None: + self._stop.set() + thread, self._thread = self._thread, None + if thread is not None: + thread.join(timeout=2.0) + + def _loop(self) -> None: + while not self._stop.is_set(): + with self._lock: + payload = self._payload + if payload is not None: + try: + _send_udp(self._target, payload) + except OSError: + pass # 端口未监听等:静默重试 + self._stop.wait(self._interval_s) + def _read_last_line(path: Path): """读文件最后一行(tail,避免读全文)。""" @@ -80,41 +243,197 @@ def _latest_state(log_dir: str) -> dict: pass return {"state": state, "telemetry": telemetry, - "events": events[-20:], "log_dir": str(log_path)} + "events": events[-40:], "log_dir": str(log_path)} -def create_app(udp_port: int, log_dir: str) -> Flask: +def create_app(udp_port: int, log_dir: str, + udp_host: str = "127.0.0.1", + inject_interval_s: float = 0.5, + rtsp_url: str = "", + video_width: int = 960, + video_fps: float = 15.0) -> Flask: app = Flask(__name__) + target = _LinkTarget(udp_host, udp_port) + injector = _ContinuousInjector(target, inject_interval_s) + video = None + if rtsp_url: + video = RtspVideoSource(rtsp_url, width=video_width, fps=video_fps) + video.start() @app.route("/") def index(): return render_template("index.html", udp_port=udp_port) + @app.route("/px4") + def px4_page(): + return render_template("px4.html") + + @app.route("/api/logs/telemetry") + def logs_telemetry(): + """整段遥测 CSV → JSON(fields + 行数组;前端负责分段/绘图)。只读。""" + path = Path(log_dir) / "task_telemetry.csv" + fields, rows = [], [] + try: + with open(path, encoding="utf-8", newline="") as f: + for i, row in enumerate(csv.reader(f)): + if i == 0: + fields = row + elif row: + rows.append(row) + except OSError: + pass + total = len(rows) + if total > _LOG_MAX_ROWS: + rows = rows[-_LOG_MAX_ROWS:] + return jsonify({"fields": fields, "rows": rows, + "total": total, "returned": len(rows)}) + + @app.route("/api/logs/events") + def logs_events(): + """整段事件 JSONL → JSON 列表(前端负责分段/解读)。只读。""" + path = Path(log_dir) / "task_events.jsonl" + events = [] + try: + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + continue + except OSError: + pass + total = len(events) + if total > _LOG_MAX_EVENTS: + events = events[-_LOG_MAX_EVENTS:] + return jsonify({"events": events, "total": total}) + @app.route("/api/inject", methods=["POST"]) def inject(): data = request.get_json(silent=True) or {} - lat = data.get("lat") - lon = data.get("lon") - if lat is None or lon is None: - return jsonify({"ok": False, "error": "需要 lat 和 lon"}), 400 try: - lat = float(lat) - lon = float(lon) + alt = float(data.get("alt", 0.0) or 0.0) except (TypeError, ValueError): - return jsonify({"ok": False, "error": "lat/lon 必须是数值"}), 400 - alt = float(data.get("alt", 0.0) or 0.0) + return jsonify({"ok": False, "error": "alt 必须是数值"}), 400 + + local = None + if "north_m" in data or "east_m" in data: + # 局部坐标:按选定原点换算为经纬度 + # current(默认)= 注入时刻无人机位置;home = 本次运行段起始位置(≈起飞点) + north, east = data.get("north_m"), data.get("east_m") + if not (_is_number(north) and _is_number(east)): + return jsonify({"ok": False, + "error": "north_m/east_m 必须是数值"}), 400 + origin_mode = data.get("origin", "current") + if origin_mode not in ("current", "home"): + return jsonify({"ok": False, + "error": "origin 必须是 current 或 home"}), 400 + if origin_mode == "home": + lat0, lon0 = _session_start_position(log_dir) + if lat0 is None: + return jsonify({"ok": False, + "error": "无本次运行的有效起始位置(遥测缺失/" + "定位无效),无法按起飞点换算"}), 400 + else: + tel = (_latest_state(log_dir).get("telemetry") or {}) + lat0, lon0 = tel.get("latitude_deg"), tel.get("longitude_deg") + if tel.get("pos_valid") != "True" or not lat0 or not lon0: + return jsonify({"ok": False, + "error": "无人机位置无效(无遥测/无 GPS)," + "无法换算局部坐标"}), 400 + lat0, lon0 = float(lat0), float(lon0) + lat, lon = _offset_to_latlon(lat0, lon0, float(north), float(east)) + local = {"north_m": float(north), "east_m": float(east), + "origin": origin_mode, + "origin_lat": lat0, "origin_lon": lon0} + else: + lat, lon = data.get("lat"), data.get("lon") + if lat is None or lon is None: + return jsonify({"ok": False, + "error": "需要 lat/lon 或 north_m/east_m"}), 400 + try: + lat = float(lat) + lon = float(lon) + except (TypeError, ValueError): + return jsonify({"ok": False, + "error": "lat/lon 必须是数值"}), 400 - payload = json.dumps({"lat": lat, "lon": lon, "alt": alt}) try: - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: - s.sendto(payload.encode("utf-8"), ("127.0.0.1", udp_port)) + _send_udp(target, {"lat": lat, "lon": lon, "alt": alt}) except OSError as exc: return jsonify({"ok": False, "error": f"UDP 发送失败: {exc}"}), 500 - return jsonify({"ok": True, "sent": {"lat": lat, "lon": lon, "alt": alt}}) + injector.update({"lat": lat, "lon": lon, "alt": alt}) # 刷新持续注入内容 + resp = {"ok": True, "sent": {"lat": lat, "lon": lon, "alt": alt}} + if local is not None: + resp["local"] = local + return jsonify(resp) + + @app.route("/api/continuous", methods=["POST"]) + def continuous(): + data = request.get_json(silent=True) or {} + on = data.get("on") + if not isinstance(on, bool): + return jsonify({"ok": False, "error": "on 必须是布尔值"}), 400 + if on and not injector.start(): + return jsonify({"ok": False, + "error": "尚未注入过坐标,请先点「注入坐标」"}), 400 + if not on: + injector.stop() + return jsonify({"ok": True, "continuous": injector.running}) + + @app.route("/api/command", methods=["POST"]) + def command(): + data = request.get_json(silent=True) or {} + cmd = data.get("cmd") + if not isinstance(cmd, str) or cmd not in _COMMAND_WHITELIST: + return jsonify({"ok": False, "error": f"未知命令: {cmd!r}"}), 400 + try: + _send_udp(target, {"cmd": cmd}) + except OSError as exc: + return jsonify({"ok": False, "error": f"UDP 发送失败: {exc}"}), 500 + return jsonify({"ok": True, "sent": {"cmd": cmd}}) + + @app.route("/api/link") + def link_get(): + options = [{"label": "本地回环(仿真)", "host": "127.0.0.1"}] + for ip in _local_ipv4_list(): + if not ip.startswith("127."): + options.append({"label": f"本机网卡 {ip}", "host": ip}) + return jsonify({"host": target.host, "port": target.port, + "options": options}) + + @app.route("/api/link", methods=["POST"]) + def link_set(): + data = request.get_json(silent=True) or {} + host = str(data.get("host") or "").strip() + port = data.get("port", target.port) + if not host or len(host) > 64 or any(c.isspace() for c in host): + return jsonify({"ok": False, "error": "host 不合法"}), 400 + try: + port = int(port) + except (TypeError, ValueError): + return jsonify({"ok": False, "error": "port 必须是整数"}), 400 + if not (1 <= port <= 65535): + return jsonify({"ok": False, "error": "port 超出范围"}), 400 + target.host, target.port = host, port + return jsonify({"ok": True, "host": target.host, "port": target.port}) @app.route("/api/state") def state(): - return jsonify(_latest_state(log_dir)) + j = _latest_state(log_dir) + j["continuous"] = injector.running + j["video"] = {"enabled": video is not None, + "online": bool(video and video.online)} + return jsonify(j) + + @app.route("/video_feed") + def video_feed(): + if video is None: + return "未配置视频源(启动参数 --rtsp-url)", 503 + return Response(stream_with_context(video.mjpeg_stream()), + mimetype="multipart/x-mixed-replace; boundary=frame") return app @@ -124,13 +443,27 @@ def main() -> int: ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--port", type=int, default=5000, help="Flask 监听端口") ap.add_argument("--udp-port", type=int, default=_GROUND_UDP_PORT, - help="坐标注入 UDP 端口(GroundLink 监听)") + help="坐标/命令注入 UDP 端口(GroundLink 监听)") + ap.add_argument("--udp-host", default="127.0.0.1", + help="坐标/命令注入目标地址(默认本机;真机可填机载/伴侣机 IP," + "页面也可运行时切换)") ap.add_argument("--log-dir", default=_LOG_DIR, help="任务日志目录") + ap.add_argument("--rtsp-url", default="", + help="吊舱 RTSP 视频地址(如 rtsp://192.168.144.119:8554/video);" + "缺省则视频面板关闭") + ap.add_argument("--video-width", type=int, default=960, + help="MJPEG 输出最大宽度(默认 960)") + ap.add_argument("--video-fps", type=float, default=15.0, + help="MJPEG 编码帧率上限(默认 15)") args = ap.parse_args() - app = create_app(args.udp_port, args.log_dir) - print(f"[地面站] http://{args.host}:{args.port} | UDP 注入 → 127.0.0.1:{args.udp_port}" - f" | 日志 → {args.log_dir}") + app = create_app(args.udp_port, args.log_dir, udp_host=args.udp_host, + rtsp_url=args.rtsp_url, + video_width=args.video_width, video_fps=args.video_fps) + video_note = f" | 视频 ← {args.rtsp_url}" if args.rtsp_url else " | 视频未配置" + print(f"[地面站] http://{args.host}:{args.port} | UDP 注入 → " + f"{args.udp_host}:{args.udp_port}" + f" | 日志 → {args.log_dir}{video_note}") app.run(host=args.host, port=args.port, debug=False) return 0 diff --git a/ground_station/templates/index.html b/ground_station/templates/index.html index b5071a9..9c0f8be 100644 --- a/ground_station/templates/index.html +++ b/ground_station/templates/index.html @@ -23,6 +23,11 @@ button { margin-top:12px; padding:9px 16px; border:none; border-radius:6px; background:var(--green); color:var(--green-ink); font-size:14px; cursor:pointer; } button:active { opacity:.85; } + button.sec { background:#5b6a5e; } + button.danger { background:var(--red); color:#fff; } + .btn-row { display:flex; flex-wrap:wrap; gap:8px; } + .btn-row button { margin-top:0; flex:1 1 auto; } + input[type=checkbox] { width:auto; } .kv { display:flex; justify-content:space-between; padding:5px 0; border-bottom:1px dashed var(--line); font-size:13px; } .kv:last-child { border-bottom:none; } @@ -33,27 +38,97 @@ .badge.warn { background:var(--red); color:#fff; } canvas { background:#0e1410; border-radius:8px; display:block; } .hint { color:var(--mut); font-size:12px; margin-top:8px; } + .nav { margin-top:8px; font-size:13px; } + .nav a { color:var(--green); text-decoration:none; margin-right:14px; } + .nav a.active { color:var(--ink); font-weight:600; text-decoration:underline; } + select { width:100%; padding:8px; border:1px solid var(--line); border-radius:6px; + font-size:14px; background:#fff; color:var(--ink); } + .badge.mut { background:#c9c3b2; color:#4a514a; } + .log-list { max-height:300px; overflow:auto; font-size:12.5px; + font-family:Consolas,"Courier New",monospace; border:1px solid var(--line); + border-radius:8px; padding:8px 10px; } + .log-line { padding:2px 0; border-bottom:1px dashed var(--line); word-break:break-all; } + .log-line:last-child { border-bottom:none; } + .log-t { color:var(--mut); margin-right:6px; } + .lv-state { color:var(--green); } + .lv-info { color:#3a6ea5; } + .lv-warn { color:#b06a2a; } + .lv-err { color:var(--red); }

地面站 · 预定流程

-
地面坐标注入 + 状态可视化(临时测试用)
+
坐标注入 + 任务/吊舱控制 + 状态可视化(临时测试用)
+
+
+

链路目标 -

+ + + + + +
+

坐标注入

- + + + + - + - + +
局部坐标按选定原点换算(起飞前注入即为起飞点;「当前坐标」模式随注入时刻无人机位置变化)。
+ +
坐标须持续注入:断流超过 5s 判失效(任务将返航)。持续注入由服务端执行, + 切窗口/最小化不受影响;取消勾选或关闭地面站程序才停止。
+
+

任务控制

+
+ + +
+
RTL = 人工返航请求;紧急 = 最高优先级立即返航(均经 UDP 下发到 runner)。
+
+
+ +
+

吊舱控制

+
+ + + + + +
+
对齐 pod_console 按键:开识别 d(0x07+0x0F) / 自动锁定 a(0x0F+0x10) / + 停跟踪 u(0x0E,仅解当前锁,0x0F 自动锁定仍常驻) / 关识别 s(0x0E+0x08,彻底退出) / 归中 c(0x2B)
+
真机经 runner → 吊舱串口下发;SITL 无吊舱控制口,命令将被忽略。
+
+
+

任务状态 -

位置-
@@ -65,28 +140,108 @@
+
+

任务日志 0

+
加载中…
+
阶段切换 / 地面站命令 / 飞控状态(STATUSTEXT 警告)实时滚动
+
+

模拟视野(脱靶量)

十字线 = 视野中心,红点 = 目标(按吊舱脱靶量映射)
+ +
+

吊舱视频 -

+ +
检测中…
+
diff --git a/ground_station/templates/px4.html b/ground_station/templates/px4.html new file mode 100644 index 0000000..7a160a6 --- /dev/null +++ b/ground_station/templates/px4.html @@ -0,0 +1,564 @@ + + + + + +地面站 · PX4 信息 + + + +
+

地面站 · PX4 信息

+
PX4 输出实时视图 + 任务日志解读
+ +
+ +
+
+ + +
+ + +
+
+

PX4 遥测 - + 注入 - + 视频 - +

+ + +
加载中…
+
+
+
+ + + +
+ + + + diff --git a/ground_station/video_source.py b/ground_station/video_source.py new file mode 100644 index 0000000..ec9241c --- /dev/null +++ b/ground_station/video_source.py @@ -0,0 +1,152 @@ +"""video_source.py —— 吊舱 RTSP 视频源(服务端拉流 → MJPEG 推流)。 + +浏览器不能直连 RTSP,由本模块在服务端用 OpenCV 拉流、编码 JPEG 帧缓存, +页面经 `/video_feed`(multipart/x-mixed-replace)显示实时画面。 + +设计要点: + * cv2 延迟导入:模块 import 无副作用,无 OpenCV 环境也能 import; + capture_factory 可注入假采集器 → 单测不依赖真实摄像头/网络; + * 断线自动重连(读帧失败 → 释放 → 退避重试); + * 抽帧节流 + 降分辨率:1080p60 全量转 JPEG 带宽/CPU 吃不消(默认 960 宽、≤15fps)。 +""" + +import logging +import threading +import time + +logger = logging.getLogger(__name__) + +_IDLE_RESEND_S = 2.0 # 无新帧时重发当前帧作保活(浏览器挂住连接) + + +class RtspVideoSource: + """后台线程拉 RTSP,维护最新 JPEG 帧供 MJPEG 消费。""" + + def __init__(self, url: str, width: int = 960, fps: float = 15.0, + quality: int = 75, capture_factory=None) -> None: + self._url = url + self._width = int(width) + self._min_interval = 1.0 / max(1.0, float(fps)) + self._quality = int(quality) + self._capture_factory = capture_factory + self._lock = threading.Lock() + self._frame = None # 最新 JPEG bytes + self._frame_seq = 0 + self._frame_at = 0.0 # 最新帧时间(monotonic) + self._stop = threading.Event() + self._thread = None + + # --- 生命周期 ----------------------------------------------------------- + def start(self) -> None: + if self._thread is not None and self._thread.is_alive(): + return + self._stop.clear() + self._thread = threading.Thread(target=self._run, name="rtsp-video", + daemon=True) + self._thread.start() + + def stop(self) -> None: + self._stop.set() + thread, self._thread = self._thread, None + if thread is not None: + thread.join(timeout=3.0) + + @property + def online(self) -> bool: + """最近 2s 内有新帧 → 在线(用于页面状态显示)。""" + with self._lock: + return (self._frame is not None + and (time.monotonic() - self._frame_at) < 2.0) + + def latest(self) -> tuple[int, bytes | None]: + """返回 (seq, jpeg);尚无帧时 (0, None)。""" + with self._lock: + return self._frame_seq, self._frame + + def mjpeg_stream(self): + """MJPEG 生成器(multipart 分块);客户端断开时随 GeneratorExit 终止。""" + last_seq = -1 + last_send = 0.0 + while not self._stop.is_set(): + seq, jpeg = self.latest() + now = time.monotonic() + if jpeg is not None and (seq != last_seq + or now - last_send > _IDLE_RESEND_S): + last_seq = seq + last_send = now + yield (b"--frame\r\nContent-Type: image/jpeg\r\n" + + f"Content-Length: {len(jpeg)}\r\n\r\n".encode("ascii") + + jpeg + b"\r\n") + time.sleep(1.0 / 30.0) + + # --- 拉流线程 ----------------------------------------------------------- + def _open(self): + if self._capture_factory is not None: + return self._capture_factory() + import cv2 # 延迟导入:仅真正拉流时依赖 OpenCV + return cv2.VideoCapture(self._url, cv2.CAP_FFMPEG) + + def _run(self) -> None: + import os + # stimeout:RTSP socket 超时(µs)。不设的话断网时单次 open 会阻塞 ~30s, + # 影响重连节奏(实测 2026-10-14)。 + os.environ.setdefault("OPENCV_FFMPEG_CAPTURE_OPTIONS", + "rtsp_transport;tcp|stimeout;5000000") + import cv2 + + cap = None + last_encode = 0.0 + try: + while not self._stop.is_set(): + if cap is None or not cap.isOpened(): + if cap is not None: + try: + cap.release() + except Exception: + pass + cap = None + cap = self._open() + if not cap.isOpened(): + logger.warning("[Video] RTSP 打开失败,1s 后重试: %s", + self._url) + self._stop.wait(1.0) + continue + logger.info("[Video] RTSP 已连接: %s", self._url) + last_encode = 0.0 + + ok, frame = cap.read() + if not ok or frame is None: + logger.warning("[Video] 读帧失败,重连") + try: + cap.release() + except Exception: + pass + cap = None + self._stop.wait(0.5) + continue + + now = time.monotonic() + if now - last_encode < self._min_interval: + continue # 抽帧节流:60fps 源 → ≤fps 编码 + last_encode = now + + h, w = frame.shape[:2] + if w > self._width: + scale = self._width / w + frame = cv2.resize( + frame, (self._width, max(1, int(h * scale))), + interpolation=cv2.INTER_AREA) + ok, buf = cv2.imencode( + ".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, self._quality]) + if not ok: + continue + with self._lock: + self._frame = buf.tobytes() + self._frame_seq += 1 + self._frame_at = time.monotonic() + finally: + if cap is not None: + try: + cap.release() + except Exception: + pass diff --git a/pod_sitl_test.py b/l1_sitl_test.py similarity index 76% rename from pod_sitl_test.py rename to l1_sitl_test.py index fd919e3..c8089ba 100644 --- a/pod_sitl_test.py +++ b/l1_sitl_test.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python3 -"""pod_sitl_test.py —— L1 航向伺服 SITL 全流程集成测试(test.py 的吊舱版)。 +#!/usr/bin/env python3 +"""l1_sitl_test.py —— L1 航向伺服 SITL 全流程集成测试。 连接 PX4 SITL,流程: 解锁 → 起飞 → 记录悬停锚点/航向 → 进 offboard @@ -7,7 +7,9 @@ → PodBridge → TrackingControlLaw 角度直入 → offboard 速度设定点) → 目标消失 → 判丢零速刹车 → 退出 offboard → RTL → 落地 → 上锁 -与 test.py 的差异: +只覆盖 L1 控制律这一段;任务级全流程见根目录 `task_runner_sitl.py`。 + +要点: * 走 L1 角度直入(吊舱脱靶量 off_target),无像素/检测框/追踪器; * 无距离(distance_valid=False)→ 距离通道按 no_distance_action 降级 (slow_approach,0.2 m/s 极低限速); @@ -15,9 +17,11 @@ * 符号使用部署配置(S1 标定:beta_sign=+1 / alpha_sign=-1), 本脚本即对该标定的端到端复核。 +共享件(时钟/遥测订阅/连接准备)在 `utils/sitl_support.py`。 + 运行前:启动 PX4 SITL(默认本机 14540)。用法: - python pod_sitl_test.py - python pod_sitl_test.py --conn udp://:14540 --config utils/config/pod_bridge.json + python l1_sitl_test.py + python l1_sitl_test.py --conn udpin://0.0.0.0:14540 --config utils/config/pod_bridge.json """ import argparse @@ -42,13 +46,14 @@ from pod_bridge.px4_gateway import Px4Gateway from pod_bridge.virtual_pod import (CarrierView, VirtualPod, VirtualTarget, VirtualTargetConfig) -logger = logging.getLogger("pod_sitl") +logger = logging.getLogger("l1_sitl") # ==================== 配置 ==================== CONNECTION = "udpin://0.0.0.0:14540" CONFIG_PATH = _UTILS_DIR / "config" / "pod_bridge.json" TAKEOFF_ALTITUDE = 5.0 # 起飞高度 (m) + # 虚拟目标:相对悬停锚点、按锚点机头朝向放置(前/右/下),随后水平圆弧运动。 TARGET_AHEAD_M = 30.0 TARGET_RIGHT_M = 15.0 @@ -61,96 +66,13 @@ TRACK_NORMAL_SECS = 20.0 # 目标保持可见时长(之后模拟丢 TRACK_BRAKE_SECS = 5.0 # 判丢后零速刹车时长 TRACK_TIMEOUT_SECS = 120.0 # 跟踪段整体安全超时 STATUS_EVERY_TICKS = 10 # 20 Hz 下每 0.5 s 打印一行状态 -CSV_PATH = Path(__file__).resolve().parent / "pod_sitl_trajectory.csv" +CSV_PATH = Path(__file__).resolve().parent / "l1_sitl_trajectory.csv" -def now_ms() -> int: - return int(time.monotonic() * 1000.0) - -class TelemetryHub: - """后台订阅 NED 位置/姿态,供 VirtualPod 几何合成与锚点记录(同 test.py)。""" - - def __init__(self) -> None: - self.yaw_rad = 0.0 - self.north_m = 0.0 - self.east_m = 0.0 - self.down_m = 0.0 - self.attitude_time_ms = 0 - self._tasks: list = [] - - def start(self, drone) -> None: - self._tasks.append(asyncio.create_task(self._attitude_loop(drone))) - self._tasks.append(asyncio.create_task(self._position_loop(drone))) - - async def _attitude_loop(self, drone) -> None: - async for euler in drone.telemetry.attitude_euler(): - self.yaw_rad = math.radians(euler.yaw_deg) - self.attitude_time_ms = now_ms() - - async def _position_loop(self, drone) -> None: - async for pos_vel in drone.telemetry.position_velocity_ned(): - pos = pos_vel.position - self.north_m = pos.north_m - self.east_m = pos.east_m - self.down_m = pos.down_m - - async def stop(self) -> None: - for task in self._tasks: - task.cancel() - for task in self._tasks: - try: - await task - except asyncio.CancelledError: - pass - self._tasks.clear() - - def view(self) -> CarrierView: - return CarrierView(north_m=self.north_m, east_m=self.east_m, - down_m=self.down_m, yaw_rad=self.yaw_rad) - - -async def print_flight_mode(drone): - async for flight_mode in drone.telemetry.flight_mode(): - print(f"[模式] 当前飞行模式: {flight_mode}") - return flight_mode - - -async def wait_until_ready(drone): - """等待连接 + 位置估计就绪(各阶段限时 30 s,同 test.py)。""" - print("等待无人机连接...") - try: - async with asyncio.timeout(30.0): - async for state in drone.core.connection_state(): - if state.is_connected: - print("-- 已连接") - break - except TimeoutError: - raise RuntimeError("30 s 内未建立 MAVSDK 连接(检查 SITL 是否在运行)") from None - - print("等待全球位置估计就绪...") - try: - async with asyncio.timeout(30.0): - async for health in drone.telemetry.health(): - if health.is_global_position_ok and health.is_home_position_ok: - print("-- 位置估计 OK") - break - except TimeoutError: - raise RuntimeError("30 s 内位置估计未就绪") from None - - -async def set_telemetry_rates(drone): - """显式设定遥测流速率(失败沿用默认并告警,同 test.py)。""" - for stream, rate in (("position", 50.0), - ("position_velocity_ned", 30.0), - ("attitude_euler", 50.0), - ("gps_info", 10.0), - ("velocity_ned", 10.0), - ("in_air", 10.0)): - try: - await getattr(drone.telemetry, f"set_rate_{stream}")(rate) - except Exception as e: - print(f"! 设置 {stream} 速率失败(沿用默认值): {e}") +# 共享件(时钟/遥测订阅/连接准备):utils/sitl_support.py +from sitl_support import (TelemetryHub, now_ms, print_flight_mode, + set_telemetry_rates, wait_until_ready) async def takeoff(drone) -> bool: diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 0000000..6b1c53f --- /dev/null +++ b/legacy/README.md @@ -0,0 +1,54 @@ +# legacy/ —— 已归档的「双目 + YOLO」可选感知后端 + +> 归档日期:**2026-10-20** +> +> 自 2026-10-19 起,**光电吊舱取代双目 + YOLO,成为唯一目标源**。运行链路是 +> `pod_bridge` → `task_runner` → PX4(见仓库根 `README.md` 与 +> `.workbuddy/memory/ARCHITECTURE.md` §11)。 +> +> 这里的内容**不参与运行链路,也不在默认 pytest 收集范围内**。保留原因:日后若要 +> 重新引入双目作为「独立身份旁证」或「距离源」,可从本目录恢复;对应的设计文档 +> 也一并归档。 + +## 归档内容 + +| 路径 | 原位置 | 说明 | +|---|---|---| +| `utils/stereo_yolo/` | `utils/stereo_yolo/` | 双目 YOLO 检测前端 | +| `utils/stereo_ranger/` | `utils/stereo_ranger/` | 双目稀疏测距 | +| `utils/target_tracker/` | `utils/target_tracker/` | 多目标追踪 + 主目标选举 | +| `utils/tests/{stereo_yolo,stereo_ranger,target_tracker}/` | 同名 | 共 175 条单测 | +| `utils/demo_*.py` | `utils/demo_*.py` | 三个演示(track_control / stereo_ranger / stereo_yolo) | +| `utils/config/stereo_*.json` | `utils/config/` | 对应配置 | +| `utils/assets/` | `utils/assets/` | demo 占位素材 | +| `models/yolo26n-drone.pt` | `models/` | YOLO 权重(5.2MB) | +| `docs/双目*.md` | `docs/` | 三份双目规格/作业指导 | +| `stereo_calib_params.npz` | 仓库根 | 双目相机标定结果 | + +## 怎么跑归档测试(默认不跑) + +归档代码依赖 `utils/` 里的 `visual_track_control`(控制律仍在运行链路中), +所以需要同时把两处加进 `PYTHONPATH`: + +```bash +cd legacy/utils +PYTHONPATH="../../utils" "D:/program file/conda/envs/pytorch/python.exe" -m pytest -q +``` + +预期 **175 passed**(111 stereo_ranger + 36 stereo_yolo + 28 target_tracker)。 + +## 怎么跑归档 demo + +```bash +cd "D:/ProgramData/code_drone-test" +PYTHONPATH="utils;legacy/utils" "D:/program file/conda/envs/pytorch/python.exe" legacy/utils/demo_stereo_ranger.py +``` + +> `demo_stereo_yolo.py` 需要 `ultralytics` 与 `legacy/models/yolo26n-drone.pt` +> (demo 内按 `legacy/` 为仓库根解析相对模型路径)。 + +## 若要正式接回 + +不要直接 `git mv` 回来就完事:先读 `ARCHITECTURE.md` §11.1 的约定 —— +需要给 `PodBridge` 增加「距离源」与「独立旁证源」两个**显式注入点**, +并重新评估 Pi 4 的算力预算(YOLO 双路推理在 Pi 4 上跑不动实时)。 diff --git a/docs/双目YOLO目标识别需求说明.md b/legacy/docs/双目YOLO目标识别需求说明.md similarity index 100% rename from docs/双目YOLO目标识别需求说明.md rename to legacy/docs/双目YOLO目标识别需求说明.md diff --git a/docs/双目测距技术路线.md b/legacy/docs/双目测距技术路线.md similarity index 100% rename from docs/双目测距技术路线.md rename to legacy/docs/双目测距技术路线.md diff --git a/legacy/docs/双目相机模组到位作业指导.md b/legacy/docs/双目相机模组到位作业指导.md new file mode 100644 index 0000000..252896b --- /dev/null +++ b/legacy/docs/双目相机模组到位作业指导.md @@ -0,0 +1,301 @@ +# 双目相机模组到位作业指导 + +> **定位**:`docs/` 下三份规格(《双目测距技术路线》《双目YOLO目标识别需求说明》 +> 《视觉跟踪控制律技术路线》)回答"应该怎样、判据是什么";本文回答"相机模组到货 +> 之后按什么顺序做什么、每步怎么判通过、结果回填到哪里"。**本文不新立判据**—— +> 所有通过标准一律引用规格原文并注明章节。 +> +> **适用范围**:双目相机模组(含厂商标定数据)到货起,至《测距路线》§12.3 四条 +> 红线解除为止。飞控/机载平台到位后的工作属《视觉跟踪控制律技术路线》§8 阶段 4~6, +> 本文只在阶段 6 交接。 +> +> **编写状态(2026-09-14)**:仓库内几何参数全部为占位值 +> (`utils/config/stereo_ranger.json`)、demo 素材为 AI 合成图,本文判据**尚未在 +> 任何真实设备上执行过**。首次执行后请把实测偏差与踩坑回填本文,并去掉本行。 + +--- + +## 0. 一页速览 + +| 阶段 | 内容 | 出口判据(出处) | 解除的阻塞 | +|------|------|-----------------|-----------| +| P0 | 开箱登记与数据清点 | 登记表(附录 A.1)填满;缺失项有替代方案 | 厂商数据可用性未知 | +| P1 | 几何参数回填 | fail-fast 校验通过 + 新有效域覆盖任务包线(§9.1/§11.2) | 几何参数为占位 | +| P2 | 校正质量核验 | 静态场景 \|Δy\| P95 ≤ 1 px(§12.1) | 极线对齐不可信 | +| P3 | 真实素材采集入库 | 覆盖 5/10/15/20/25 m 档、元数据齐(§2.3) | 素材只有合成图 | +| P4 | 测距真值验证 | 有效域内 δZ ≤ 2 m、趋势 ∝ Z²(§12.1) | 数值从未对过真值 | +| P5 | 阈值整定 | §9 各表 ⚠ 全部替换为实测值(§12.2) | 阈值全部占位 | +| P6 | 台架/SITL 联动 → 解禁评审 | 控制联动场景全覆盖(§12.1);§12.3 四条逐条勾选 | 红线未解除 | + +依赖顺序:P0 → P1 → P2 → P3 → P4 → P5 → P6。P2 必须在 P1 之后(校正质量要在 +正确内参下判读),P4 必须在 P3 之后(没有真实素材就没有真值比对)。 + +--- + +## 1. 阶段 0:开箱登记与数据清点 + +### 1.1 向厂商索取的清单 + +| 数据 | 用途 | 缺失时的替代方案 | 落位 | +|------|------|-----------------|------| +| 校正后内参 `fx_px`/`fy_px`/`cx_px`/`cy_px` | §9.1 几何组,`Z = F·B/D` 的分母来源 | 自标定(张正友);**P2 判据不达标时本就要提前做** | `utils/config/stereo_ranger.json` | +| 基线 `baseline_m`(标称 + 实测公差) | §9.1;`F·B` 常量 | 卡尺/塞规实测,记录不确定度 | 同上 | +| 畸变系数与校正映射(remap maps)或"已做校正"的书面确认 | §2.2 第 4 条:前端输出的是校正后图上的框 | 自标定 + 自建 remap 表 | 采集预处理(不在本仓) | +| 出厂标定报告与不确定度 | P1/P2 的判据依据,**要存档** | 无报告 = 按未标定处理,走自标定 | 本文附录 A.1 | +| 分辨率 / 帧率 / 像素格式 / 位深 | 原生坐标系与算力预算(《需求说明》§5) | — | 附录 A.1 + `stereo_yolo.json` 复核 | +| 同步方式与触发接口(sbs / 双流 / 硬触发) | §2.3 同帧契约的形态选择 | sbs 优先(天然同曝光,§2.3) | 采集端设计 | +| 机械图纸(安装接口、两目光轴平行度要求) | 安装外参、换机型(P6) | — | 附录 A.1 | + +### 1.2 到货当天建议实测 + +1. **基线复核**:卡尺/图纸比对标称值,偏差 >1% 时以实测为准并记录(`F·B` 直接决定距离标定); +2. **同帧性验证**:拍快速横向移动的手/笔,sbs 形态应为同一时刻;双流形态量化两路时间窗抖动并记录(§2.3 要求"残余时序抖动应记录供误差评估"); +3. **分辨率与帧率实测**:跑满声称帧率,记录实际值与丢帧情况(供 P5/P6 的算力预算); +4. **图像质量目测**:左右目曝光/增益是否一致(不一致会让同一目标两目置信度与框边缘不对称,污染视差取点)。 + +--- + +## 2. 阶段 1:几何参数回填 + +### 2.1 改哪些字段 + +只动 `utils/config/stereo_ranger.json` 的 **5 个几何项**: + +``` +fx_px, fy_px, cx_px, cy_px ← 厂商校正后内参(注意:fx=fy=F 是校正后口径) +baseline_m ← 实测基线 +``` + +`utils/config/stereo_yolo.json` **不含几何参数**(输出框天然位于输入图的原生 +坐标系,§4 配置一致性要求)——前端无需回填,只需复核 `imgsz` 与真实分辨率匹配。 + +**视差域 `disparity_min_px` / `disparity_max_px` 不是标定数据**,是任务决策项: +按任务近界 `Z_near` 反推 `disparity_max_px = F·B / Z_near`,按可测远界定 +`disparity_min_px`。回填内参后 `F·B` 变化,两者的距离语义会平移,**必须重算**。 + +### 2.2 回填后必算的派生量(写进记录表) + +| 派生量 | 公式(代码属性) | 用途 | +|--------|-----------------|------| +| `F·B` | `fx_px × baseline_m`(`focal_baseline_px_m`) | 距离解算常量 | +| `z_min_m` | `F·B / disparity_max_px` | 近界(只读派生) | +| `z_max_m` | `F·B / disparity_min_px` | 远界(只读派生) | +| `z_error_budget_max_m` | `sqrt(max_delta_z_m × F·B)` | 误差预算门对应的最远距离(§7.2,δD 基准固定 1 px) | + +以上四项**不是配置项**,由 `StereoConfig` 只读派生(v1.0 冻结:视差域权威, +距离域派生,避免两处冗余互推舍入不一致,§6.4)。回填后手算一遍与程序输出比对, +作为回填正确性的一次交叉验证。 + +### 2.3 判据 + +1. **fail-fast 校验通过**:JSON 必须含全部 17 个键、无未知键、类型正确 + (`consistency_N_frames` / `consistency_M_frames` / `filter_reset_frames` + 必须是整数,`3.5` 或 `true` 会被拒);取值约束:`0 < disparity_min_px < disparity_max_px`、 + `consistency_M_frames ≥ consistency_N_frames`、`0 < smooth_alpha ≤ 1`、 + `cx_px`/`cy_px` 非负、其余数值项一律为正(以 `stereo_ranger/config.py` 为准); +2. **有效域覆盖任务包线**:新的 `z_min_m` / `z_max_m` / `z_error_budget_max_m` + 与任务的距离区间逐项比对,**不达标时先改视差域/基线/机型(P6),不要迁就旧数值**; +3. 记录回填前后的 diff 与批准人(附录 A.2)。 + +### 2.4 连带修改清单(容易漏,会导致测试变红) + +| 受影响处 | 现象 | 处置 | +|---------|------|------| +| `utils/tests/stereo_ranger/config_test.py::test_load_from_json_matches_defaults` | **必失败**:该用例断言 JSON 内容与 `StereoConfig()` 占位默认值逐字段相等 | 改为"语义一致性"断言(域关系、派生恒等式、取值约束),或维护一份"当前标定值"副本单独比对;**不要**为了过测试而改 dataclass 默认值——默认值是单测的占位基准 | +| `utils/tests/stereo_yolo/config_test.py::test_load_from_json_matches_defaults` | 同上(仅当改动 `stereo_yolo.json` 时) | 同上 | +| `docs/双目测距技术路线.md` §9 各表的 ⚠ 标记 | 表格仍写占位值 | 回填为实测值并标注日期与来源(保持"以规格为准") | +| `utils/stereo_ranger/README.md`、`utils/stereo_yolo/README.md` 的占位值表 | 同上 | 同步,并撤掉"占位红线"字样(或改为"已标定") | +| `utils/demo_stereo_ranger.py` 的注释(`F·B=57.6 px·m`) | 注释过时 | 同步数值 | + +--- + +## 3. 阶段 2:校正质量核验 + +**判据(§12.1 校正质量)**:静态场景下,左右匹配框的 \|Δy\| 分布 **P95 ≤ 1 px**(⚠占位)。 +不达标 → **触发自标定提前**(§7 风险表第 7 项、《需求说明》§6)。 + +### 3.1 建议方法 + +1. 静态标靶(棋盘格或随机纹理板)摆在近/中/远 3 档(如 3 / 8 / 15 m),每档在画面 + 中心与四角共 5 个区域成像; +2. 每处采 ≥50 帧,用**检测链路本身**(前端 → 两目框)统计 `|y1_L − y1_R|` 与 + `|cy_L − cy_R|` 双口径的 P95; +3. 交叉验证:若统计值接近 1 px,改用角点/纹理匹配口径(不依赖检测框边缘)复核, + 避免把"检测边缘抖动"误判成"校正误差"。参考量级:本仓合成对(已知零校正误差) + 在 1080p 下实测 \|Δy\| ≈ 0.2 px,即**检测链路自身就贡献约 0.2 px 噪声**。 + +### 3.2 不达标时的排查顺序 + +1. 内参/remap 是否张冠李戴(最常见:用原始图内参解释校正图); +2. 校正映射是否按原生分辨率生成(缩放过的 map 会引入亚像素偏置); +3. 两目光轴平行度/安装(机械问题,需厂商介入); +4. 仍不达标 → 自标定(P2 不达标即 v1.1 自标定提前,§7 风险表)。 + +⚠ **工具缺口**:仓库目前**没有**该统计脚本,需新增(建议 `utils/tools/calib_check.py`, +输入左右目图像目录,输出 \|Δy\| 直方图与 P95)。 + +--- + +## 4. 阶段 3:真实素材采集与入库 + +### 4.1 采集清单 + +| 档位 | 内容 | 用途 | +|------|------|------| +| 5 / 10 / 15 / 20 m | 静态标靶(或无人机目标静置),每档 ≥50 帧,手持激光测距仪同步记录真值 | P4 测距精度(§12.1) | +| ≥25 m(域外档) | 同上 | 验证"域外不撒谎":`valid=false` 且方位照常发布(§12.3 第 3 条) | +| 横向快速移动 | 目标在画面内快速平移 | 帧同步核验(§12.1:无系统性虚假视差) | +| 多目标 | 同画面 2 个同类目标 + 1 个非目标干扰 | P5 的 `isolation_gate_px` 整定 | +| 丢失-恢复 | 目标被遮挡 ≥ `filter_reset_frames` 帧后重现 | P5 的 `filter_reset_frames` / `smooth_alpha` 整定 | + +### 4.2 命名与落位(建议) + +``` +utils/assets/stereo_yolo//z05m_01_L.png + z05m_01_R.png # 双流形态 + z05m_01_sbs.png # sbs 形态(推荐,§2.3) + z05m_01.json # 元数据 +``` + +元数据必填字段:拍摄时间、距离真值 + 测距仪型号、目标类型、光照/天气、曝光与增益、 +同步形态、分辨率、帧号范围、操作人。 + +### 4.3 注意 + +- **素材必须是校正后的图**(或同时保存原始图 + 本次使用的 remap 表)——否则 P4 的 + 误差里混进校正误差,无法归因(§2.2 第 4 条); +- 替换 `utils/assets/stereo_yolo/drone_sky.png` 只能换"单图冒烟"的输入;上面这套 + 目录结构需要配回放脚本(见 P4 工具缺口)。 + +--- + +## 5. 阶段 4:测距真值验证 + +**判据(§12.1 测距精度)**:以**地面手持激光测距仪**为真值(D8:测距方向与 `Z` +一致,不是斜距 `R`);有效域内 **δZ ≤ 2 m**(⚠占位);误差趋势符合 `δZ ∝ Z²`(§7.1)。 + +### 5.1 方法与产物 + +1. 每档 ≥50 组"前端 → `StereoRanger.update()`"输出,与真值比对,输出**误差-距离曲线**; +2. 由曲线拟合实测 δD(视差噪声基准,当前固定 1 px 是占位,§12.2), + 并用 `Z_budget = sqrt(max_delta_z_m × F·B / δD_meas)` 反推"预算门"应落在哪; +3. 单独跑域外档,确认 `valid=false` 且 `center_mid_px` 仍发布(§7.3 域外不撒谎, + 也不丢方位); +4. 结果记入附录 A.4。 + +### 5.2 不通过时的归因顺序 + +| 现象 | 首先怀疑 | 处置 | +|------|---------|------| +| 全档系统性偏置 | 内参/基线错、取点规则 | 复核 P1 回填值;复核 `x1` 取点(§6.1 同侧边缘) | +| 误差随 Z² 放大且系数偏大 | 实测 δD 大于既定基准 | 收紧 `max_delta_z_m` 或缩小有效域;把实测 δD 写回规格(§12.2) | +| 远场随机跳变 | 检测边缘抖动 + 小视差放大 | 属预期(§7.2),确认降级/门限按 §6.4 生效即可 | +| 横向移动时出现虚假视差 | 帧同步 | 回到 P3 复核同步形态(§2.3) | + +⚠ **工具缺口**:仓库目前**没有**"真实素材回放"脚本(现 demo 的合成对是解析构造的), +需新增(建议 `utils/tools/replay_stereo.py`,输入 4.2 的目录结构,输出逐帧记录 +与误差表)。 + +--- + +## 6. 阶段 5:阈值整定清单 + +§9 表中每个 ⚠ 都要在本阶段被实测值替换。整定依据与建议方法: + +| 参数 | 占位值 | 整定输入 | 建议方法 | +|------|-------|---------|---------| +| `tau_y_px` | 2.0 | P2 的 \|Δy\| 分布 | 取 P95 的 1.5~2 倍留余量 | +| `tau_h_ratio` | 0.25 | 真实框高相对差分布 | 同上口径 | +| `disparity_min_px` | 3.0 | 可测远界需求 | 由任务远界反推(P1 已重算) | +| `disparity_max_px` | 57.6 | 任务近界 | 由 `Z_near` 反推(P1 已重算) | +| `max_delta_z_m` | 2.0 | P4 的误差曲线 | 由可接受的最大距离误差定;与 δD 实测联动(P4 第 2 步) | +| `consistency_jump_px` | 3.0 | 静态场景视差噪声 P95 | 取噪声峰值的数倍:太小误判、太大漏检(§6.7) | +| `consistency_N_frames` | 3 | 安全需求 + 误报率 | N/M 决定"异常后多久降级/判无效"(3/5 ≈ 0.17 s @30 fps),与消费侧的降级时延一起定 | +| `consistency_M_frames` | 5 | 同上 | 必须 ≥ N | +| `smooth_alpha` | 0.3 | P4 数据离线拟合 | 在"噪声抑制"与"群延迟"间取平衡(0.3 ≈ 3.3 帧群延迟) | +| `jump_reset_m` | 5.0 | 真实跳变幅度观测 | 用目标切换/大机动场景标定 | +| `filter_reset_frames` | 5 | 丢失-恢复场景 | 与消费侧 coast 策略配合(§6.5) | +| `isolation_gate_px` | 50.0 | 多目标场景 | 由"同帧不同目标的最小像素间距"定,取小于该值的门限 | + +前端侧同步复核:`conf_threshold` / `iou_threshold` 与真实素材上的漏检/误检权衡; +`imgsz` 与平台算力(P7)——**这两项不随标定变化,但要与检测质量一起整定**。 + +--- + +## 7. 阶段 6:台架/SITL 联动与解禁评审 + +### 7.1 台架联动(判据见 §12.1 控制联动) + +回放/SITL 下跑通:距离闭环 ↔ 定速接近切换、`invalid` 持续降级路径、 +远场"方位闭环 + 定速接近"(D4)——场景覆盖要求见《视觉跟踪控制律技术路线》第 7 章。 + +### 7.2 解禁检查表(§12.3 四条逐条勾选) + +- [ ] **红线 1**:§7/§9 中所有 ⚠ 数值已由实测/厂商数据确认,替换记录已归档(附录 A.2/A.3/A.4); +- [ ] **红线 2**:相机参数与阈值全部在配置文件中,代码内无写死(含 demo/工具脚本复核); +- [ ] **红线 3**:域外/失配确实输出 `valid = false`——合成回归(`pytest -q`)+ 真实素材(P4 域外档)两轮都有证据; +- [ ] **红线 4**:地面真值验证完成并归档(P4 全档通过)。 + +### 7.3 解禁之后仍未解禁的 + +本文解除的是**测距/检测链路**的红线。实飞还要求控制律侧完成 +《视觉跟踪控制律技术路线》§8 阶段 6 的增益与限幅整定——两者都满足才可实飞。 + +--- + +## 附录 A:记录表模板 + +### A.1 开箱登记 + +| 项 | 值 | +|----|----| +| 型号 / 序列号 | | +| 分辨率 / 帧率 / 像素格式 | | +| 标称基线 / 实测基线(工具、公差) | | +| 校正后内参(fx, fy, cx, cy) | | +| 畸变系数 / remap 表版本 | | +| 出厂标定报告编号与日期 | | +| 同步形态(sbs / 双流 / 硬触发) | | +| 到货日期 / 登记人 | | + +### A.2 参数回填记录 + +| 字段 | 回填前 | 回填后 | 来源 | 日期 / 批准 | +|------|-------|-------|------|------------| + +派生量复核(手算 vs 程序):`F·B` = ___,`z_min_m` = ___,`z_max_m` = ___, +`z_error_budget_max_m` = ___。 + +### A.3 校正质量核验 + +| 档位 | 区域 | 样本数 | \|Δy\| P95 | 判据 ≤1 px | 备注 | +|------|------|-------|-----------|-----------|------| + +### A.4 测距真值验证 + +| 距离真值 | 样本数 | Z 均值 | δZ | 判据 ≤2 m | valid 率 | 备注 | +|---------|-------|-------|----|----------|---------|------| + +--- + +## 附录 B:判据来源索引 + +| 本文阶段 | 规格出处 | +|---------|---------| +| P1 回填与派生量 | 《测距路线》§9.1、§6.4、§11.2 | +| P1 连带修改 | `utils/stereo_ranger/config.py`(fail-fast 规则)、两份 `config_test.py` | +| P2 校正质量 | 《测距路线》§12.1、§7 风险表第 7 项;《需求说明》§6 | +| P3 采集与同步 | 《需求说明》§2.3;《测距路线》§12.1 帧同步 | +| P4 真值验证 | 《测距路线》§12.1、§7.1、§7.2、§7.3(D8 真值方向) | +| P5 阈值整定 | 《测距路线》§9 全表、§12.2;《需求说明》§4 | +| P6 联动与解禁 | 《测距路线》§12.1、§12.3;《视觉跟踪控制律技术路线》§7/§8 | + +## 附录 C:已知缺口(执行前需补齐) + +1. **工具脚本缺三件**(本文标注 ⚠):校正质量统计(P2)、真实素材回放(P4)、 + 真值-误差曲线拟合(P4)。在它们就位前,P2/P4 只能手工抽样,判据不可复现; +2. **外部参考文档缺档**:《测距路线》§12.2 把开放问题(P1 焦距规格、P2 出厂标定、 + P3 帧同步形态、P6 换机型、P7 性能实测、P8 避障激光去留)的**权威清单**指向 + 《双目深度估计思路》第 15 章,该文档当前不在仓库中——需要它才能核对问题编号与 + 结论,请补回或改引他处; +3. **P7(性能实测)不在本文范围**:它依赖机载/边缘平台,属平台工程文档。 diff --git a/models/yolo26n-drone.pt b/legacy/models/yolo26n-drone.pt similarity index 100% rename from models/yolo26n-drone.pt rename to legacy/models/yolo26n-drone.pt diff --git a/utils/assets/stereo_yolo/drone_sky.png b/legacy/utils/assets/stereo_yolo/drone_sky.png similarity index 100% rename from utils/assets/stereo_yolo/drone_sky.png rename to legacy/utils/assets/stereo_yolo/drone_sky.png diff --git a/utils/config/stereo_ranger.json b/legacy/utils/config/stereo_ranger.json similarity index 100% rename from utils/config/stereo_ranger.json rename to legacy/utils/config/stereo_ranger.json diff --git a/utils/config/stereo_yolo.json b/legacy/utils/config/stereo_yolo.json similarity index 100% rename from utils/config/stereo_yolo.json rename to legacy/utils/config/stereo_yolo.json diff --git a/legacy/utils/conftest.py b/legacy/utils/conftest.py new file mode 100644 index 0000000..96490d9 --- /dev/null +++ b/legacy/utils/conftest.py @@ -0,0 +1,19 @@ +"""pytest 根配置(归档目录专用)。 + +归档的 175 条测试仍 import `stereo_yolo` / `stereo_ranger` / `target_tracker` +(三者都在本目录的 utils/ 下),部分还复用运行链路的 `visual_track_control` +(留在仓库根的 utils/)。因此这里把本目录的 utils/ 与仓库根的 utils/ 都加入 +sys.path,使 `cd legacy/utils && pytest` 可直接运行。 + +默认(从仓库根 utils/ 跑)不会收集本目录,主链路测试不受影响。 +""" + +import sys +from pathlib import Path + +_LEGACY_UTILS = Path(__file__).resolve().parent # legacy/utils +_REPO_UTILS = _LEGACY_UTILS.parents[1] / "utils" # 仓库根/utils + +for _p in (_LEGACY_UTILS, _REPO_UTILS): + if str(_p) not in sys.path: + sys.path.insert(0, str(_p)) diff --git a/utils/demo_stereo_ranger.py b/legacy/utils/demo_stereo_ranger.py similarity index 100% rename from utils/demo_stereo_ranger.py rename to legacy/utils/demo_stereo_ranger.py diff --git a/utils/demo_stereo_yolo.py b/legacy/utils/demo_stereo_yolo.py similarity index 100% rename from utils/demo_stereo_yolo.py rename to legacy/utils/demo_stereo_yolo.py diff --git a/utils/demo_track_control.py b/legacy/utils/demo_track_control.py similarity index 100% rename from utils/demo_track_control.py rename to legacy/utils/demo_track_control.py diff --git a/legacy/utils/pytest.ini b/legacy/utils/pytest.ini new file mode 100644 index 0000000..b7f7a7b --- /dev/null +++ b/legacy/utils/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +python_files = *_test.py test_*.py +; 与运行链路 utils/pytest.ini 同口径:各包存在同名测试文件(如 config_test.py), +; importlib 模式避免按裸模块名导入时的 basename 冲突。 +addopts = --import-mode=importlib diff --git a/utils/stereo_ranger/README.md b/legacy/utils/stereo_ranger/README.md similarity index 100% rename from utils/stereo_ranger/README.md rename to legacy/utils/stereo_ranger/README.md diff --git a/utils/stereo_ranger/__init__.py b/legacy/utils/stereo_ranger/__init__.py similarity index 100% rename from utils/stereo_ranger/__init__.py rename to legacy/utils/stereo_ranger/__init__.py diff --git a/utils/stereo_ranger/config.py b/legacy/utils/stereo_ranger/config.py similarity index 100% rename from utils/stereo_ranger/config.py rename to legacy/utils/stereo_ranger/config.py diff --git a/utils/stereo_ranger/consistency.py b/legacy/utils/stereo_ranger/consistency.py similarity index 100% rename from utils/stereo_ranger/consistency.py rename to legacy/utils/stereo_ranger/consistency.py diff --git a/utils/stereo_ranger/distance_filter.py b/legacy/utils/stereo_ranger/distance_filter.py similarity index 100% rename from utils/stereo_ranger/distance_filter.py rename to legacy/utils/stereo_ranger/distance_filter.py diff --git a/utils/stereo_ranger/geometry.py b/legacy/utils/stereo_ranger/geometry.py similarity index 100% rename from utils/stereo_ranger/geometry.py rename to legacy/utils/stereo_ranger/geometry.py diff --git a/utils/stereo_ranger/matching.py b/legacy/utils/stereo_ranger/matching.py similarity index 100% rename from utils/stereo_ranger/matching.py rename to legacy/utils/stereo_ranger/matching.py diff --git a/utils/stereo_ranger/sampling.py b/legacy/utils/stereo_ranger/sampling.py similarity index 100% rename from utils/stereo_ranger/sampling.py rename to legacy/utils/stereo_ranger/sampling.py diff --git a/utils/stereo_ranger/stereo_ranger.py b/legacy/utils/stereo_ranger/stereo_ranger.py similarity index 100% rename from utils/stereo_ranger/stereo_ranger.py rename to legacy/utils/stereo_ranger/stereo_ranger.py diff --git a/utils/stereo_ranger/target_store.py b/legacy/utils/stereo_ranger/target_store.py similarity index 100% rename from utils/stereo_ranger/target_store.py rename to legacy/utils/stereo_ranger/target_store.py diff --git a/utils/stereo_ranger/types.py b/legacy/utils/stereo_ranger/types.py similarity index 100% rename from utils/stereo_ranger/types.py rename to legacy/utils/stereo_ranger/types.py diff --git a/utils/stereo_ranger/warn_throttle.py b/legacy/utils/stereo_ranger/warn_throttle.py similarity index 100% rename from utils/stereo_ranger/warn_throttle.py rename to legacy/utils/stereo_ranger/warn_throttle.py diff --git a/utils/stereo_yolo/README.md b/legacy/utils/stereo_yolo/README.md similarity index 100% rename from utils/stereo_yolo/README.md rename to legacy/utils/stereo_yolo/README.md diff --git a/utils/stereo_yolo/__init__.py b/legacy/utils/stereo_yolo/__init__.py similarity index 100% rename from utils/stereo_yolo/__init__.py rename to legacy/utils/stereo_yolo/__init__.py diff --git a/utils/stereo_yolo/config.py b/legacy/utils/stereo_yolo/config.py similarity index 100% rename from utils/stereo_yolo/config.py rename to legacy/utils/stereo_yolo/config.py diff --git a/utils/stereo_yolo/detector.py b/legacy/utils/stereo_yolo/detector.py similarity index 100% rename from utils/stereo_yolo/detector.py rename to legacy/utils/stereo_yolo/detector.py diff --git a/utils/stereo_yolo/frontend.py b/legacy/utils/stereo_yolo/frontend.py similarity index 100% rename from utils/stereo_yolo/frontend.py rename to legacy/utils/stereo_yolo/frontend.py diff --git a/utils/stereo_yolo/sbs.py b/legacy/utils/stereo_yolo/sbs.py similarity index 100% rename from utils/stereo_yolo/sbs.py rename to legacy/utils/stereo_yolo/sbs.py diff --git a/utils/stereo_yolo/warn_throttle.py b/legacy/utils/stereo_yolo/warn_throttle.py similarity index 100% rename from utils/stereo_yolo/warn_throttle.py rename to legacy/utils/stereo_yolo/warn_throttle.py diff --git a/utils/target_tracker/README.md b/legacy/utils/target_tracker/README.md similarity index 100% rename from utils/target_tracker/README.md rename to legacy/utils/target_tracker/README.md diff --git a/utils/target_tracker/__init__.py b/legacy/utils/target_tracker/__init__.py similarity index 100% rename from utils/target_tracker/__init__.py rename to legacy/utils/target_tracker/__init__.py diff --git a/utils/target_tracker/kalman_box_filter.py b/legacy/utils/target_tracker/kalman_box_filter.py similarity index 100% rename from utils/target_tracker/kalman_box_filter.py rename to legacy/utils/target_tracker/kalman_box_filter.py diff --git a/utils/target_tracker/target_tracker.py b/legacy/utils/target_tracker/target_tracker.py similarity index 100% rename from utils/target_tracker/target_tracker.py rename to legacy/utils/target_tracker/target_tracker.py diff --git a/utils/target_tracker/tracklet.py b/legacy/utils/target_tracker/tracklet.py similarity index 100% rename from utils/target_tracker/tracklet.py rename to legacy/utils/target_tracker/tracklet.py diff --git a/utils/tests/stereo_ranger/config_test.py b/legacy/utils/tests/stereo_ranger/config_test.py similarity index 100% rename from utils/tests/stereo_ranger/config_test.py rename to legacy/utils/tests/stereo_ranger/config_test.py diff --git a/utils/tests/stereo_ranger/consistency_test.py b/legacy/utils/tests/stereo_ranger/consistency_test.py similarity index 100% rename from utils/tests/stereo_ranger/consistency_test.py rename to legacy/utils/tests/stereo_ranger/consistency_test.py diff --git a/utils/tests/stereo_ranger/distance_filter_test.py b/legacy/utils/tests/stereo_ranger/distance_filter_test.py similarity index 100% rename from utils/tests/stereo_ranger/distance_filter_test.py rename to legacy/utils/tests/stereo_ranger/distance_filter_test.py diff --git a/utils/tests/stereo_ranger/geometry_test.py b/legacy/utils/tests/stereo_ranger/geometry_test.py similarity index 100% rename from utils/tests/stereo_ranger/geometry_test.py rename to legacy/utils/tests/stereo_ranger/geometry_test.py diff --git a/utils/tests/stereo_ranger/matching_test.py b/legacy/utils/tests/stereo_ranger/matching_test.py similarity index 100% rename from utils/tests/stereo_ranger/matching_test.py rename to legacy/utils/tests/stereo_ranger/matching_test.py diff --git a/utils/tests/stereo_ranger/sampling_test.py b/legacy/utils/tests/stereo_ranger/sampling_test.py similarity index 100% rename from utils/tests/stereo_ranger/sampling_test.py rename to legacy/utils/tests/stereo_ranger/sampling_test.py diff --git a/utils/tests/stereo_ranger/stereo_ranger_test.py b/legacy/utils/tests/stereo_ranger/stereo_ranger_test.py similarity index 100% rename from utils/tests/stereo_ranger/stereo_ranger_test.py rename to legacy/utils/tests/stereo_ranger/stereo_ranger_test.py diff --git a/utils/tests/stereo_ranger/target_store_test.py b/legacy/utils/tests/stereo_ranger/target_store_test.py similarity index 100% rename from utils/tests/stereo_ranger/target_store_test.py rename to legacy/utils/tests/stereo_ranger/target_store_test.py diff --git a/utils/tests/stereo_ranger/warn_throttle_test.py b/legacy/utils/tests/stereo_ranger/warn_throttle_test.py similarity index 100% rename from utils/tests/stereo_ranger/warn_throttle_test.py rename to legacy/utils/tests/stereo_ranger/warn_throttle_test.py diff --git a/utils/tests/stereo_yolo/config_test.py b/legacy/utils/tests/stereo_yolo/config_test.py similarity index 100% rename from utils/tests/stereo_yolo/config_test.py rename to legacy/utils/tests/stereo_yolo/config_test.py diff --git a/utils/tests/stereo_yolo/frontend_test.py b/legacy/utils/tests/stereo_yolo/frontend_test.py similarity index 100% rename from utils/tests/stereo_yolo/frontend_test.py rename to legacy/utils/tests/stereo_yolo/frontend_test.py diff --git a/utils/tests/stereo_yolo/sbs_test.py b/legacy/utils/tests/stereo_yolo/sbs_test.py similarity index 100% rename from utils/tests/stereo_yolo/sbs_test.py rename to legacy/utils/tests/stereo_yolo/sbs_test.py diff --git a/utils/tests/target_tracker/kalman_box_filter_test.py b/legacy/utils/tests/target_tracker/kalman_box_filter_test.py similarity index 100% rename from utils/tests/target_tracker/kalman_box_filter_test.py rename to legacy/utils/tests/target_tracker/kalman_box_filter_test.py diff --git a/utils/tests/target_tracker/target_tracker_test.py b/legacy/utils/tests/target_tracker/target_tracker_test.py similarity index 100% rename from utils/tests/target_tracker/target_tracker_test.py rename to legacy/utils/tests/target_tracker/target_tracker_test.py diff --git a/utils/tests/target_tracker/tracklet_test.py b/legacy/utils/tests/target_tracker/tracklet_test.py similarity index 100% rename from utils/tests/target_tracker/tracklet_test.py rename to legacy/utils/tests/target_tracker/tracklet_test.py diff --git a/requirements-pod.txt b/requirements-pod.txt index 392710d..40cc834 100644 --- a/requirements-pod.txt +++ b/requirements-pod.txt @@ -1,6 +1,6 @@ # requirements-pod.txt —— 树莓派 4B(Ubuntu 24.04)吊舱桥接最小依赖 -# 只装这几个,切勿在 Pi 上装全量 requirements.txt(torch/cv2 无意义且数百 MB) +# 只装这几个,切勿在 Pi 上装全量 requirements.txt(cv2/torch 无意义且数百 MB) # 详见 docs/树莓派4B部署注意事项.md §2 -mavsdk<4 # PX4 遥测订阅 + offboard;⚠ 必须锁 <4:v4 是全新原生 API 重写,与本仓库经典 gRPC API 不兼容(最新 3.17.4 有 aarch64 wheel) +mavsdk<4 # PX4 遥测订阅 + offboard;⚠ 必须锁 <4:v4 是全新原生 API 重写,与本仓库经典 gRPC API 不兼容(最新 3.17.4 有 aarch64 wheel) pyserial # 吊舱串口(115200 8N1) -pytest # 仅上机自测(tests/pod_bridge 92 条)需要,部署运行可省 +pytest # 仅上机自测(tests/pod_bridge 208 条)需要,部署运行可省 diff --git a/requirements.txt b/requirements.txt index 712b412..8283354 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,18 @@ -# code_drone-test Python 依赖 -# 扫描自全仓库 .py 文件的实际 import(2026-09-22) +# code_drone-test Python 依赖(运行链路) +# 按 utils/ + ground_station/ + tools/ 的实际 import 核对(2026-10-20 精简后) -# ---- 运行时核心 ---- -numpy # target_tracker 卡尔曼滤波 / stereo_yolo sbs 拆分 -opencv-python # camera.py / camera_test.py / video_test.py / demo_stereo_yolo.py (cv2) +# ---- 运行链路核心 ---- +mavsdk<4 # ⚠ 锁 <4:v4 为全新原生 API,经典 gRPC API(System/offboard/telemetry)已移除 +pyserial # 吊舱串口(pod_bridge.pod_serial,惰性导入) -# ---- 检测推理(stereo_yolo,真实推理才加载,torch 为其依赖) ---- -ultralytics - -# ---- 飞控通信(MAVSDK offboard -> PX4) ---- -mavsdk<4 # ⚠ 锁 <4:v4 为全新原生 API,经典 gRPC API(System/offboard/telemetry)已移除 - -# ---- 吊舱串口(pod_bridge.pod_serial,惰性导入) ---- -pyserial - -# ---- 可视化(test.py 轨迹绘图) ---- -matplotlib +# ---- 地面站页面 / 视频(ground_station.app + video_source)---- +flask # 页面与 API +opencv-python # RTSP 拉流 / MJPEG 推流;tools/ 下相机脚本也用 +numpy # video_source / tools 图像处理 # ---- 测试 ---- -pytest # utils/tests 全量单测(256 条) +pytest # utils/tests 运行链路单测(446 条) + +# ---- 可选:已归档的感知后端(仅跑 legacy/ 时需要)---- +# ultralytics # legacy/utils/stereo_yolo 真模型推理路径 +# torch # ultralytics 依赖 diff --git a/task_runner_real.py b/task_runner_real.py index ea1a84d..41587c8 100644 --- a/task_runner_real.py +++ b/task_runner_real.py @@ -1,27 +1,33 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3 """task_runner_real.py —— 真机任务编排入口(真实吊舱串口 + 地面站 UDP 注入 + PX4)。 与 task_runner_sitl.py(仿真)的差异: * 吊舱 = PodSerial(真实串口表2-2 遥测),非 VirtualPod; * 地面坐标 = GroundLink.run()(UDP 接收地面站 JSON),非 inject() 直接注入; - * 吊舱目标识别/锁定 = PodController(GOTO 进入发 0x07 识别 + 0x0F 自动锁定, - RTL 前发 0x0E 停止 + 0x08 识别关); - * PX4 连接 = cfg.px4.connection_string(真机串口/遥测),非 SITL 14540。 + * 吊舱目标识别/锁定 = PodController(首次进入引导阶段发 0x07 识别 + 0x0F 自动锁定, + RTL 前发 0x0E 停止 + 0x08 识别关;**只发一次**,重入不会重发打断跟踪); + * PX4 连接 = cfg.px4.connection_string(真机串口/遥测),非 SITL 14540; + * 收尾 = RTL 返航(默认)或 `--land` 就地降落; + * SIGINT/SIGTERM 优雅停机:先请状态机转 RTL,再退出(不把飞机留在 offboard 上)。 + +任务链(与 SITL 一致):WAIT→ARM_TAKEOFF→GOTO(飞向参考点)→SEARCH(原地扫视)→TRACK(缓慢靠近)→RTL 前置(真机联调增量:纯软单测 → 串口回环 → 仅吊舱 → 仅飞控 → 地面联调): 1. 填 utils/config/pod_bridge.json 的 pod_serial.port / px4.connection_string; 2. 吊舱上电、串口连通(run_bridge.py --dry-run 已验证遥测); - 3. 地面站往 ground_inject.port(默认 14555)发 JSON 坐标 - {"lat":..,"lon":..,"alt":..}(每 0.5~1s 一次)。 + 3. 地面站往 ground_inject.port(默认 14555)**持续**发 JSON 坐标 + {"lat":..,"lon":..,"alt":..}(每 0.5~1s 一次);停发会导致坐标超龄 → 返航。 用法: python task_runner_real.py python task_runner_real.py --config utils/config/pod_bridge.json --target-type 1 + python task_runner_real.py --port /dev/ttyAMA0 --land """ import argparse import asyncio import logging +import signal import sys import time from datetime import datetime @@ -42,9 +48,10 @@ from pod_bridge.pod_serial import PodSerial from pod_bridge.px4_gateway import Px4Gateway from task_runner.log import TaskLogConfig, TaskLogger from task_runner.pod_control import PodController +from task_runner.return_home import ReturnConfig from task_runner.runner import TaskRunner from task_runner.state_machine import TaskStateConfig, TaskStateMachine -from pod_sitl_test import now_ms, print_flight_mode, set_telemetry_rates, wait_until_ready +from sitl_support import now_ms, print_flight_mode, set_telemetry_rates, wait_until_ready logger = logging.getLogger("task_real") @@ -57,25 +64,92 @@ def build_sm_config() -> TaskStateConfig: """真机状态机参数。⚠ 全部为占位值,须台架标定后回填,标定前不得实飞。""" return TaskStateConfig( takeoff_altitude_m=5.0, - switch_radius_m=30.0, # GOTO→TRACK 切换半径(待标定) + arrival_radius_m=30.0, # 到参考点半径 → 转搜索(待标定) goto_timeout_s=180.0, - track_timeout_s=180.0, - center_hold_s=3.0, - center_threshold_deg=3.0, - approach_hover_s=5.0, + search_timeout_s=20.0, # 搜索 20s 未发现目标 → 返航 + track_timeout_s=20.0, # 追踪/靠近段上限 → 返航(无距离,判不出"已到目标") + center_threshold_deg=3.0, # 仅日志/诊断 + sweep_rate_dps=15.0, # 搜索扫视角速度(度/秒,待标定) + sweep_reverse_s=4.0, # 每 4s 换向 loop_frequency_hz=20.0, ) +def build_return_config() -> ReturnConfig: + """自定义返航参数:回起飞点 → 恢复初始航向 → 垂直降落(不额外爬高)。 + + ⚠ 速度为占位值,须按机体/场地标定。 + """ + return ReturnConfig( + enabled=True, + speed_mps=2.0, # 转场水平速度 + arrival_radius_m=3.0, # 距起飞点该半径内视为到位 + timeout_s=120.0, # 转场超时 → PX4 RTL 兜底 + yaw_kp=1.0, + yaw_rate_limit_dps=20.0, + align_kp=1.0, + align_rate_limit_dps=15.0, + yaw_align_tolerance_deg=5.0, + yaw_align_timeout_s=15.0, + alt_kp=0.5, + alt_limit_mps=0.5, + ) + + +def _sync_staleness_with_injection(cfg) -> None: + """把「坐标时效」口径统一到 `ground_inject.timeout_ms`。 + + ⚠ 这是 SITL 实测暴露的一个真机必踩的坑:`guidance.target_stale_ms` 默认 1000ms, + 而地面站按 0.5~1s 上报 → 报文间隔稍有抖动(无线链路/页面节流)就会让引导律判 + 「目标超龄 → 零速刹车」,表现为飞机在路上走走停停;而任务层要等 + `ground_inject.timeout_ms`(默认 5000ms)才判坐标失效。 + SITL 入口是硬编码 5000ms 绕过的,真机这里改为**派生**,两个门限只有一个来源。 + """ + inject_ms = int(cfg.ground_inject.timeout_ms) + if cfg.guidance.target_stale_ms < inject_ms: + logger.info("[Task] guidance.target_stale_ms %d → %d(与 ground_inject 对齐)", + cfg.guidance.target_stale_ms, inject_ms) + cfg.guidance.target_stale_ms = inject_ms + + +def _install_signal_handlers(stop: asyncio.Event, runner: TaskRunner) -> None: + """SIGINT/SIGTERM → 请求返航并退出主循环(真机在树莓派上被 kill 也要安全收尾)。 + + 只置位不抛异常:让 `runner.run()` 走正常的 RTL 分支(停机+返航+落地+上锁), + 而不是让进程带着 offboard 速度指令消失。 + """ + loop = asyncio.get_running_loop() + + def _handler(sig_name: str) -> None: + logger.warning("[Task] 收到 %s:请求返航并退出", sig_name) + runner.request_rtl() + stop.set() + + for name in ("SIGINT", "SIGTERM"): + sig = getattr(signal, name, None) + if sig is None: + continue + try: + loop.add_signal_handler(sig, _handler, name) + except (NotImplementedError, RuntimeError, ValueError): + # Windows / 非主线程不支持:退化为 signal.signal(可能不生效) + try: + signal.signal(sig, lambda *_a, _n=name: _handler(_n)) + except (ValueError, OSError): + pass + + async def run(args) -> int: cfg = load_from_json(args.config) + if args.port: + cfg.pod_serial.port = args.port # 先应用覆盖,再校验(否则 --port 救不了空配置) if not cfg.pod_serial.port: print("! 未配置吊舱串口:在 pod_bridge.json 填 pod_serial.port,或用 --port 覆盖") return 2 - if args.port: - cfg.pod_serial.port = args.port cfg.ground_inject.enabled = True # 地面注入必须开(真机无激光,表4 永不出) + _sync_staleness_with_injection(cfg) # 坐标时效口径统一(SITL 实测踩过的坑) sm_cfg = build_sm_config() + return_cfg = build_return_config() drone = System() await drone.connect(system_address=args.conn or cfg.px4.connection_string) @@ -97,15 +171,24 @@ async def run(args) -> int: log_dir = args.log_dir or f"logs/real_{datetime.now().strftime('%Y%m%d_%H%M%S')}" task_log = TaskLogger(TaskLogConfig(dir=log_dir), now_ms_fn=now_ms) runner = TaskRunner(cfg, sm_cfg, px4, bridge, sm, ground, drone, - now_ms_fn=now_ms, logger=task_log, pod_control=pod_control) + now_ms_fn=now_ms, logger=task_log, pod_control=pod_control, + land_instead_of_rtl=args.land, return_cfg=return_cfg) stop = asyncio.Event() + _install_signal_handlers(stop, runner) ground_task = asyncio.ensure_future(ground.run(stop)) # UDP 接收地面站坐标 runner_task = asyncio.ensure_future(runner.run(stop)) print(f"-- 启动真机任务全流程(吊舱 {cfg.pod_serial.port}," - f"地面站 UDP {cfg.ground_inject.port},目标类型 {args.target_type})") - print(f" 等待地面站注入坐标(每 0.5~1s 一次 JSON)… 日志 → {log_dir}") + f"地面站 UDP {cfg.ground_inject.port},目标类型 {args.target_type}," + f"收尾 {'就地降落' if args.land else '返航'})", flush=True) + print(f" 状态机: takeoff={sm_cfg.takeoff_altitude_m}m " + f"arrival_radius={sm_cfg.arrival_radius_m}m " + f"search={sm_cfg.search_timeout_s}s@{sm_cfg.sweep_rate_dps}°/s " + f"track_max={sm_cfg.track_timeout_s}s | " + f"坐标时效 ground_inject={cfg.ground_inject.timeout_ms}ms " + f"guidance={cfg.guidance.target_stale_ms}ms", flush=True) + print(f" 等待地面站注入坐标(每 0.5~1s 一次 JSON)… 日志 → {log_dir}", flush=True) started = time.monotonic() last_state = None @@ -114,17 +197,20 @@ async def run(args) -> int: await asyncio.sleep(1.0) if runner.state is not last_state: last_state = runner.state - print(f"[任务] 进入状态: {runner.state.value}") + print(f"[任务] 进入状态: {runner.state.value}", flush=True) if runner_task.done(): exc = runner_task.exception() if exc is not None: - print(f"! runner 异常退出: {type(exc).__name__}: {exc}") + print(f"! runner 异常退出: {type(exc).__name__}: {exc}", + flush=True) break + if stop.is_set(): + break # 信号处理已请求退出,交给 finally 收尾 if time.monotonic() - started > TASK_TIMEOUT_S: - print("! 全流程超时,强制返航") + print("! 全流程超时,强制返航", flush=True) runner.request_rtl() if runner.rtl_done: - print("-- 任务完成(RTL 落地)") + print("-- 任务完成(落地)", flush=True) except asyncio.CancelledError: raise finally: @@ -141,7 +227,7 @@ async def run(args) -> int: pass await print_flight_mode(drone) - print("\n===== 真机任务全流程完成 =====") + print("\n===== 真机任务全流程完成 =====", flush=True) return 0 @@ -153,11 +239,18 @@ def main() -> int: ap.add_argument("--target-type", type=int, default=1, help="吊舱目标类型:0=人/车,1=无人机") ap.add_argument("--log-dir", default=None, help="日志输出目录(默认按时间戳)") + ap.add_argument("--land", action="store_true", + help="收尾改为就地降落(默认返航 RTL)") args = ap.parse_args() logging.basicConfig(level=logging.INFO, format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s", datefmt="%H:%M:%S") + # 真机常在树莓派上经 SSH 运行:stdout 非 tty 时块缓冲会让进度看不到,强制行缓冲 + try: + sys.stdout.reconfigure(line_buffering=True) + except (AttributeError, ValueError): + pass return asyncio.run(run(args)) diff --git a/task_runner_sitl.py b/task_runner_sitl.py index 0d1310d..c52c6f6 100644 --- a/task_runner_sitl.py +++ b/task_runner_sitl.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 """task_runner_sitl.py —— 任务级状态机 SITL 全流程集成测试(预定流程.md 天空侧)。 -用状态机驱动完整流程:WAIT → ARM_TAKEOFF → GOTO(L2 坐标引导) → TRACK(L1 航向伺服) -→ APPROACH(悬停) → RTL(返航落地)。相比 pod_sitl_test.py(固定场景、人工编排), -本脚本把「飞行编排 + 控制律切换」交给 TaskRunner 状态机统一驱动。 +用状态机驱动完整流程: + WAIT → ARM_TAKEOFF → GOTO(L2 飞向参考点) → SEARCH(原地扫视) → TRACK(L1 缓慢靠近) + → RTL(自定义返航:回起飞点 → 恢复初始航向 → 垂直降落) + +相比 `l1_sitl_test.py`(只跑 L1 控制律闭环、人工编排),本脚本把「飞行编排 + 控制律切换」 +交给 TaskRunner 状态机统一驱动;返航也不再走 PX4 原生 RTL(那会先爬到 +RTL_RETURN_ALT 再降),而是自定义的低空返航(见 task_runner/return_home.py)。 场景: * 起飞前注入虚拟目标坐标(模拟地面站给坐标)——目标 = home 北 30m / 东 15m; * VirtualPod 按无电机语义合成表2-2(框架角恒0、无激光、锁定=目标在视场内); * 后台任务持续刷新注入坐标(模拟地面站持续上报),坐标始终新鲜; - * GOTO→TRACK 由状态机按「距目标 < switch_radius 且吊舱锁定」守卫切换。 + * GOTO→SEARCH 按「距参考点 ≤ arrival_radius」切换;SEARCH→TRACK 按「目标锁定」 + 切换(含 LOS 运动学一致性判据,见 pod_bridge/los_consistency.py)。 运行前:启动 PX4 SITL(默认本机 14540)。用法: python task_runner_sitl.py - python task_runner_sitl.py --conn udp://:14540 --config utils/config/pod_bridge.json + python task_runner_sitl.py --conn udpin://0.0.0.0:14540 \ + --config utils/config/pod_bridge.json """ import argparse @@ -40,9 +46,10 @@ from pod_bridge.px4_gateway import Px4Gateway from pod_bridge.virtual_pod import (CarrierView, VirtualPod, VirtualTarget, VirtualTargetConfig) from task_runner.log import TaskLogConfig, TaskLogger +from task_runner.return_home import ReturnConfig from task_runner.runner import TaskRunner from task_runner.state_machine import TaskStateConfig, TaskStateMachine -from pod_sitl_test import (TelemetryHub, now_ms, print_flight_mode, +from sitl_support import (TelemetryHub, now_ms, print_flight_mode, set_telemetry_rates, wait_until_ready) logger = logging.getLogger("task_sitl") @@ -83,16 +90,35 @@ def build_sm_config() -> TaskStateConfig: """SITL 状态机参数(占位值,仅用于仿真验证链路)。""" return TaskStateConfig( takeoff_altitude_m=5.0, - switch_radius_m=25.0, # 目标在 30m,飞进 25m 内切 TRACK + arrival_radius_m=25.0, # 目标在 30m,飞进 25m 内转搜索 goto_timeout_s=60.0, - track_timeout_s=60.0, - center_hold_s=3.0, - center_threshold_deg=3.0, # 脱靶量 < 3° 判居中 - approach_hover_s=5.0, # 悬停 5s 后 RTL + search_timeout_s=20.0, # 搜索 20s 未发现目标 → 返航 + track_timeout_s=20.0, # 追踪/靠近段上限 → 返航 + center_threshold_deg=3.0, # 脱靶量 < 3° 判居中(仅日志/诊断) + sweep_rate_dps=15.0, # 搜索扫视角速度(度/秒) + sweep_reverse_s=4.0, # 每 4s 换向 loop_frequency_hz=20.0, ) +def build_return_config() -> ReturnConfig: + """SITL 自定义返航参数(与真机同口径,仅用于仿真验证)。""" + return ReturnConfig( + enabled=True, + speed_mps=2.0, + arrival_radius_m=3.0, + timeout_s=120.0, + yaw_kp=1.0, + yaw_rate_limit_dps=20.0, + align_kp=1.0, + align_rate_limit_dps=15.0, + yaw_align_tolerance_deg=5.0, + yaw_align_timeout_s=15.0, + alt_kp=0.5, + alt_limit_mps=0.5, + ) + + async def inject_loop(ground: GroundLink, lat: float, lon: float, stop: asyncio.Event) -> None: """后台持续刷新注入坐标(模拟地面站持续上报,保坐标始终新鲜)。""" @@ -111,6 +137,7 @@ async def run(args) -> int: cfg.ground_inject.enabled = True # 启用地面注入(骨架期 JSON 默认关) cfg.guidance.target_stale_ms = 5000 # 注入坐标超时放宽(表4 默认 1000 对低频注入太紧) sm_cfg = build_sm_config() + return_cfg = build_return_config() drone = System() await drone.connect(system_address=args.conn) @@ -146,7 +173,7 @@ async def run(args) -> int: sm = TaskStateMachine(sm_cfg) task_log = TaskLogger(TaskLogConfig(), now_ms_fn=now_ms) runner = TaskRunner(cfg, sm_cfg, px4, bridge, sm, ground, drone, - now_ms_fn=now_ms, logger=task_log) + now_ms_fn=now_ms, logger=task_log, return_cfg=return_cfg) stop = asyncio.Event() if args.ground_station: @@ -160,9 +187,9 @@ async def run(args) -> int: if args.ground_station: print(f" [地面站模式] 等待 UDP {cfg.ground_inject.port} 注入坐标…") print(f" 状态机: takeoff={sm_cfg.takeoff_altitude_m}m " - f"switch_radius={sm_cfg.switch_radius_m}m " - f"center={sm_cfg.center_threshold_deg}°/{sm_cfg.center_hold_s}s " - f"hover={sm_cfg.approach_hover_s}s") + f"arrival_radius={sm_cfg.arrival_radius_m}m " + f"search={sm_cfg.search_timeout_s}s@{sm_cfg.sweep_rate_dps}°/s " + f"track_max={sm_cfg.track_timeout_s}s") # 状态打印 + 总超时保护 started = time.monotonic() diff --git a/test.py b/test.py deleted file mode 100644 index a59e18b..0000000 --- a/test.py +++ /dev/null @@ -1,836 +0,0 @@ -#!/usr/bin/env python3 -""" -PX4 SITL Offboard 视觉跟踪集成测试 (MAVSDK-Python) -连接: udpin://0.0.0.0:14540 -流程: - 解锁 → 起飞 → 切换 Offboard(速度设定点) - → 视觉跟踪闭环 20 Hz(追踪器 → 控制律 → offboard 网关) - → 目标消失 → Coast 预测 → 判丢零速刹车 → 退出 Offboard → RTL → 落地 → 上锁 -适用于 PX4 1.17 - -YOLO 检测未接入:VirtualTarget 用无人机真实位置/航向做针孔反投影生成 -检测框与双目目标距离(几何一致闭环),无人机会实际飞向该虚拟目标。 -""" - -import asyncio -import logging -import math -import sys -import time -from pathlib import Path - -import matplotlib -import matplotlib.pyplot as plt -from matplotlib import font_manager -from matplotlib.ticker import MaxNLocator - - -def _setup_chinese_font() -> str | None: - """为 matplotlib 配置可用的中文字体,避免中文标题/图例乱码。 - - 优先从字体文件注册,再按 family 名称匹配(Linux Noto/WQY、 - Windows 雅黑/黑体、macOS 苹方等)。返回实际字体名;失败返回 None。 - """ - # 1) 直接按路径注册(TTC/TTF) - path_candidates = [ - "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", - "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc", - "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", - "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", - "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", - "C:/Windows/Fonts/msyh.ttc", - "C:/Windows/Fonts/msyhbd.ttc", - "C:/Windows/Fonts/simhei.ttf", - "/System/Library/Fonts/PingFang.ttc", - "/System/Library/Fonts/STHeiti Light.ttc", - ] - chosen = None - for path in path_candidates: - if not Path(path).is_file(): - continue - try: - font_manager.fontManager.addfont(path) - prop = font_manager.FontProperties(fname=path) - chosen = prop.get_name() - break - except Exception: - continue - - # 2) 按已注册 family 名称匹配 - if chosen is None: - name_candidates = [ - "Noto Sans CJK SC", - "Noto Sans CJK JP", - "Noto Serif CJK SC", - "Noto Serif CJK JP", - "Droid Sans Fallback", - "WenQuanYi Micro Hei", - "WenQuanYi Zen Hei", - "Source Han Sans SC", - "Source Han Sans CN", - "Microsoft YaHei", - "SimHei", - "PingFang SC", - "Heiti SC", - "Arial Unicode MS", - ] - available = {f.name for f in font_manager.fontManager.ttflist} - for name in name_candidates: - if name in available: - chosen = name - break - - if chosen: - matplotlib.rcParams["font.sans-serif"] = [chosen, "DejaVu Sans"] - matplotlib.rcParams["font.family"] = "sans-serif" - # 解决坐标轴负号显示为方块的问题 - matplotlib.rcParams["axes.unicode_minus"] = False - return chosen - - -_CHINESE_FONT = _setup_chinese_font() - -_UTILS_DIR = Path(__file__).resolve().parent / "utils" -if str(_UTILS_DIR) not in sys.path: - sys.path.insert(0, str(_UTILS_DIR)) - -from mavsdk import System -from mavsdk.action import ActionError - -from utils.target_tracker import Detection, FrameShape, TargetTracker, TrackerConfig -from utils.demo_track_control import track_result_to_state -from utils.visual_track_control.config import load_from_json -from utils.visual_track_control.mavsdk_gateway import MavsdkCommandGateway -from utils.visual_track_control.tracking_control_law import TrackingControlLaw -from utils.visual_track_control.types import ControlMode, ControlOutput, ControlSnapshot - -logger = logging.getLogger("test") - -# ==================== 配置 ==================== -CONNECTION = "udpin://0.0.0.0:14540" # 推荐写法,也可用 "udp://127.0.0.1:14540" -TAKEOFF_ALTITUDE = 5.0 # 起飞高度 (m) - -TRACK_CONFIG_PATH = _UTILS_DIR / "config" / "track_control.json" -TRACK_MAX_LOST_FRAMES = 60 # 追踪器 Coast 窗口(20 Hz 下 = 3 s) -TRACK_NORMAL_SECS = 20.0 # 虚拟目标保持可见时长(之后进入丢失场景) -TRACK_BRAKE_SECS = 5.0 # 判丢后零速刹车时长 -TRACK_TIMEOUT_SECS = 120.0 # 跟踪段整体安全超时 -STATUS_EVERY_TICKS = 10 # 每 0.5 s 打印一行状态 -PLOT_TRAJECTORY = True # 跟踪结束后绘制无人机与虚拟目标轨迹 -TRAJECTORY_SAVE_PATH = Path(__file__).resolve().parent / "trajectory_plot.png" - -# 虚拟目标:相对悬停锚点、按锚点机头朝向放置的机体系偏移 -# (前方 AHEAD / 右方 RIGHT,横偏用于激发航向通道)。 -# 默认与悬停点同高:地面目标场景下垂直通道会持续下降趋近目标高度, -# SITL 中有触发 PX4 着陆检测的风险;TARGET_DOWN_M 取正值(目标更低, -# 如 +5.0 = 地面)即可切换为地面目标场景。 -TARGET_AHEAD_M = 30.0 -TARGET_RIGHT_M = 15.0 -TARGET_DOWN_M = 0.0 -MOCK_BOX_W = 120.0 # 模拟检测框尺寸 (px) -MOCK_BOX_H = 160.0 -MOCK_CONFIDENCE = 0.9 -MOCK_TARGET_SPEED_MPS = 1.0 # 圆弧切向速度 (m/s) -TARGET_ARC_RADIUS_M = 12.0 # 圆弧半径 (m) -# 旋转方向:+1 = NED 水平面逆时针(从北向东转),-1 = 顺时针 -TARGET_ARC_CCW = 1 - - -def now_ms() -> int: - return int(time.monotonic() * 1000.0) - - -def plot_trajectories( - drone_traj: list, - target_traj: list, - save_path=None, -) -> None: - """绘制无人机与虚拟目标的 NED 轨迹。 - - 参数: - drone_traj / target_traj: 列表元素为 (t_s, north_m, east_m, down_m) - save_path: 若给定则保存图片,否则仅弹窗显示 - """ - if not drone_traj and not target_traj: - print("! 无轨迹数据,跳过绘图") - return - - if _CHINESE_FONT is None: - print("! 未找到中文字体,图中中文可能显示为方框。" - "可安装 fonts-noto-cjk 或文泉驿,或改用英文标签。") - else: - # 再次确保本进程内 rc 生效(部分后端会重置) - matplotlib.rcParams["font.sans-serif"] = [_CHINESE_FONT, "DejaVu Sans"] - matplotlib.rcParams["axes.unicode_minus"] = False - - fig, axes = plt.subplots(1, 2, figsize=(12, 5)) - - # ---- 左图:水平面 N-E 轨迹(俯视图)---- - ax = axes[0] - if drone_traj: - dn = [p[1] for p in drone_traj] - de = [p[2] for p in drone_traj] - ax.plot(de, dn, "b-", linewidth=1.5, label="无人机") - ax.scatter(de[0], dn[0], c="blue", s=60, marker="o", zorder=5, label="无人机起点") - ax.scatter(de[-1], dn[-1], c="blue", s=80, marker="x", zorder=5, label="无人机终点") - if target_traj: - tn = [p[1] for p in target_traj] - te = [p[2] for p in target_traj] - ax.plot(te, tn, "r--", linewidth=1.5, label="虚拟目标") - ax.scatter(te[0], tn[0], c="red", s=60, marker="o", zorder=5, label="目标起点") - ax.scatter(te[-1], tn[-1], c="red", s=80, marker="x", zorder=5, label="目标终点") - ax.set_xlabel("East (m)") - ax.set_ylabel("North (m)") - ax.set_title("水平轨迹 (NED 俯视)") - ax.set_aspect("equal", adjustable="datalim") - ax.grid(True, linestyle="--", alpha=0.6) - ax.legend(loc="best", fontsize=8) - ax.xaxis.set_major_locator(MaxNLocator(nbins=6)) - ax.yaxis.set_major_locator(MaxNLocator(nbins=6)) - - # ---- 右图:高度随时间(Down 取负号显示为高度)---- - ax2 = axes[1] - if drone_traj: - dt = [p[0] for p in drone_traj] - d_alt = [-p[3] for p in drone_traj] # NED down → 高度 - ax2.plot(dt, d_alt, "b-", linewidth=1.5, label="无人机高度") - if target_traj: - tt = [p[0] for p in target_traj] - t_alt = [-p[3] for p in target_traj] - ax2.plot(tt, t_alt, "r--", linewidth=1.5, label="目标高度") - ax2.set_xlabel("时间 (s)") - ax2.set_ylabel("高度 (m)") - ax2.set_title("高度-时间曲线") - ax2.grid(True, linestyle="--", alpha=0.6) - ax2.legend(loc="best", fontsize=8) - - fig.suptitle("视觉跟踪段轨迹", fontsize=12) - fig.tight_layout() - - if save_path is not None: - fig.savefig(str(save_path), dpi=150, bbox_inches="tight") - print(f"-- 轨迹图已保存: {save_path}") - try: - plt.show() - except Exception as e: - print(f"! 无法弹窗显示图像(无图形界面?): {e}") - finally: - plt.close(fig) - - -class VirtualTarget: - """几何一致虚拟目标 —— YOLO 检测与双目测距的替身。 - - 目标初始位置: - 以起飞后的悬停锚点为参考, - 按起飞时机头方向放置: - 前方 TARGET_AHEAD_M - 右方 TARGET_RIGHT_M - 下方 TARGET_DOWN_M - - 目标运动(水平圆弧): - 跟踪开始时刻 t=0,目标以切向速度 MOCK_TARGET_SPEED_MPS、 - 半径 TARGET_ARC_RADIUS_M 在 NED 水平面内做匀速圆周运动。 - - 圆心:初始位置沿“起飞时右方”偏移半径 R(目标从圆心左侧/右侧 - 起转,初始切向近似沿机头前方),高度保持不变。 - - 角速度 ω = v / R,方向由 TARGET_ARC_CCW 控制(+1 逆时针)。 - - 每次 observe() 调用时,根据经过的时间实时更新目标的 - NED 世界坐标,然后再进行针孔投影。 - - 相机假定与机体固连、无视差、忽略 roll/pitch; - 仅使用 yaw 将世界 NED 坐标转换到机体系。 - """ - - def __init__( - self, - cfg, - anchor_ned: tuple[float, float, float], - anchor_yaw_rad: float, - ) -> None: - - # ============================================================ - # 1. 根据起飞后的锚点 + 起飞时航向,计算目标初始位置 - # ============================================================ - - cos_y = math.cos(anchor_yaw_rad) - sin_y = math.sin(anchor_yaw_rad) - - # 机体系: - # 前方 = (cos(yaw), sin(yaw)) - # 右方 = (-sin(yaw), cos(yaw)) - - self._start_n = ( - anchor_ned[0] - + TARGET_AHEAD_M * cos_y - - TARGET_RIGHT_M * sin_y - ) - - self._start_e = ( - anchor_ned[1] - + TARGET_AHEAD_M * sin_y - + TARGET_RIGHT_M * cos_y - ) - - self._start_d = ( - anchor_ned[2] - + TARGET_DOWN_M - ) - - # ============================================================ - # 2. 当前目标位置 - # ============================================================ - - self._pos_n = self._start_n - self._pos_e = self._start_e - self._pos_d = self._start_d - - # ============================================================ - # 3. 圆弧参数 - # - # 圆心取在初始点沿“起飞时右方”偏移 R 处,使 t=0 时目标 - # 位于圆心左侧(相对机头),初始切向大致沿机头前方: - # - # 右方单位向量: (-sin(yaw), cos(yaw)) - # 圆心 = start + R * 右方 - # 初始相位:从圆心指向 start 的向量 = -R * 右方 - # ============================================================ - - self._radius_m = max(0.1, float(TARGET_ARC_RADIUS_M)) - self._omega = ( - float(MOCK_TARGET_SPEED_MPS) / self._radius_m - ) * (1.0 if TARGET_ARC_CCW >= 0 else -1.0) - - right_n = -sin_y - right_e = cos_y - - self._center_n = self._start_n + self._radius_m * right_n - self._center_e = self._start_e + self._radius_m * right_e - - # 初始角度:圆心 → 起点 在 NE 平面的方位角 - # atan2(E, N) 与常见航向定义一致 - self._theta0 = math.atan2( - self._start_e - self._center_e, - self._start_n - self._center_n, - ) - - self._move_yaw_rad = anchor_yaw_rad # 仅作参考/日志 - - # ============================================================ - # 4. 记录目标运动开始时间 - # ============================================================ - - self._start_time = time.monotonic() - - # ============================================================ - # 5. 相机参数 - # ============================================================ - - cam = cfg.camera - - self._img_w = float(cam.image_width) - self._img_h = float(cam.image_height) - - # 针孔模型焦距(像素): - # - # f = W / (2 * tan(FOV_h / 2)) - # - self._f_px = self._img_w / ( - 2.0 * math.tan( - math.radians(cam.fov_h_deg) / 2.0 - ) - ) - - # ============================================================ - # 6. 目标可见状态 - # ============================================================ - - self._visible = True - - @property - def visible(self) -> bool: - """返回目标当前是否可见。""" - return self._visible - - def set_visible(self, visible: bool) -> None: - """设置目标是否可见。""" - self._visible = visible - - def update_position(self) -> None: - """根据经过的时间更新目标世界坐标(水平匀速圆弧)。 - - θ(t) = θ0 + ω * t - N = center_N + R * cos(θ) - E = center_E + R * sin(θ) - D 保持初始高度不变。 - - 切向速度 v = |ω| * R = MOCK_TARGET_SPEED_MPS。 - """ - - elapsed_s = time.monotonic() - self._start_time - elapsed_s = max(0.0, elapsed_s) - - theta = self._theta0 + self._omega * elapsed_s - - self._pos_n = self._center_n + self._radius_m * math.cos(theta) - self._pos_e = self._center_e + self._radius_m * math.sin(theta) - self._pos_d = self._start_d - - def observe( - self, - ned_n: float, - ned_e: float, - ned_d: float, - yaw_rad: float, - ): - """返回: - - (detections, target_distance_m, distance_valid) - - 不可见时: - - ([], 0.0, False) - - 每次调用都会先更新虚拟目标位置, - 然后根据无人机当前位置和当前 yaw - 计算目标在相机画面中的投影。 - """ - - # ============================================================ - # 1. 始终更新目标真实世界坐标(即使“不可见”也继续运动, - # 便于轨迹记录;仅检测输出被屏蔽) - # ============================================================ - - self.update_position() - - # ============================================================ - # 2. 目标不可见 → 不返回检测框 - # ============================================================ - - if not self._visible: - return [], 0.0, False - - # ============================================================ - # 3. 目标相对于无人机的 NED 坐标 - # ============================================================ - - rel_n = self._pos_n - ned_n - rel_e = self._pos_e - ned_e - rel_d = self._pos_d - ned_d - - # ============================================================ - # 4. 世界坐标 NED → 当前无人机机体系 - # - # 这里只考虑 yaw,不考虑 roll / pitch。 - # - # fwd :目标在机头前方的距离 - # right:目标在机体右侧的距离 - # ============================================================ - - cos_y = math.cos(yaw_rad) - sin_y = math.sin(yaw_rad) - - fwd = ( - rel_n * cos_y - + rel_e * sin_y - ) - - right = ( - -rel_n * sin_y - + rel_e * cos_y - ) - - # ============================================================ - # 5. 目标在侧面或后方 - # ============================================================ - - if fwd <= 0.5: - return [], 0.0, False - - # ============================================================ - # 6. 针孔相机投影 - # ============================================================ - - u = ( - self._img_w * 0.5 - + self._f_px * (right / fwd) - ) - - v = ( - self._img_h * 0.5 - + self._f_px * (rel_d / fwd) - ) - - # ============================================================ - # 7. 目标投影超出画面 - # ============================================================ - - if not ( - 0.0 <= u < self._img_w - and - 0.0 <= v < self._img_h - ): - return [], 0.0, False - - # ============================================================ - # 8. 构造模拟 YOLO 检测框 - # ============================================================ - - box = Detection( - u - MOCK_BOX_W / 2.0, - v - MOCK_BOX_H / 2.0, - u + MOCK_BOX_W / 2.0, - v + MOCK_BOX_H / 2.0, - MOCK_CONFIDENCE, - ) - - # ============================================================ - # 9. 模拟双目目标距离(前向分量 Z,与检测框几何一致) - # ============================================================ - - return [box], fwd, True - - -class TelemetryHub: - """后台订阅姿态/位置遥测,控制回路每拍读取最新快照(§7.1 同拍取样)。""" - - def __init__(self) -> None: - self.yaw_rad = 0.0 - self.roll_rad = 0.0 - self.pitch_rad = 0.0 - self.attitude_time_ms = 0 - self.north_m = 0.0 - self.east_m = 0.0 - self.down_m = 0.0 - self._tasks: list[asyncio.Task] = [] - - def start(self, drone) -> None: - self._tasks.append(asyncio.create_task(self._attitude_loop(drone))) - self._tasks.append(asyncio.create_task(self._position_loop(drone))) - - async def _attitude_loop(self, drone) -> None: - async for euler in drone.telemetry.attitude_euler(): - self.yaw_rad = math.radians(euler.yaw_deg) - self.roll_rad = math.radians(euler.roll_deg) - self.pitch_rad = math.radians(euler.pitch_deg) - self.attitude_time_ms = now_ms() - - async def _position_loop(self, drone) -> None: - async for pos_vel in drone.telemetry.position_velocity_ned(): - pos = pos_vel.position - self.north_m = pos.north_m - self.east_m = pos.east_m - self.down_m = pos.down_m - - async def wait_attitude(self, timeout_s: float = 10.0) -> bool: - deadline = time.monotonic() + timeout_s - while self.attitude_time_ms == 0 and time.monotonic() < deadline: - await asyncio.sleep(0.1) - return self.attitude_time_ms > 0 - - async def stop(self) -> None: - for task in self._tasks: - task.cancel() - for task in self._tasks: - try: - await task - except asyncio.CancelledError: - pass - self._tasks.clear() - - -async def dispatch(sender, out: ControlOutput, tick_ms: int) -> None: - """按模式分发指令。 - - DEGRADED_HOLD 在胶水层 demo 中不发送,但真实 offboard 会话中断流 - (约 0.5~1 s)会触发 PX4 offboard failsafe,故此处改发零速设定点 - (等效刹停悬停)保住设定点流。 - """ - if out.mode is ControlMode.VELOCITY_HEADING and out.valid: - await sender.send_velocity_heading(out, tick_ms) - else: - await sender.send_brake_hover(tick_ms) - - -async def run_tracking(drone, anchor_ned: tuple[float, float, float], - anchor_yaw_deg: float) -> bool: - """视觉跟踪闭环段:20 Hz 控制回路,返回是否正常走完场景。""" - cfg = load_from_json(str(TRACK_CONFIG_PATH)) - period_s = 1.0 / cfg.control.frequency_hz - - hub = TelemetryHub() - hub.start(drone) - if not await hub.wait_attitude(): - logger.error("姿态遥测未就绪,放弃跟踪段") - await hub.stop() - return False - - tracker = TargetTracker(TrackerConfig(max_lost_frames=TRACK_MAX_LOST_FRAMES)) - law = TrackingControlLaw(cfg) - gateway = MavsdkCommandGateway(drone) - target = VirtualTarget(cfg, anchor_ned, math.radians(anchor_yaw_deg)) - frame_shape = FrameShape(height=cfg.camera.image_height, - width=cfg.camera.image_width) - - if not await gateway.start(): - logger.error("offboard 启动失败,放弃跟踪段") - await hub.stop() - return False - - print(f"-- 进入视觉跟踪:锚点航向 {anchor_yaw_deg:.1f}°,虚拟目标按机头朝向放置于 " - f"前 {TARGET_AHEAD_M:.0f} m / 右 {TARGET_RIGHT_M:.0f} m / 下 {TARGET_DOWN_M:.0f} m " - f"(世界 NED: N={target._pos_n:.1f}, E={target._pos_e:.1f}, D={target._pos_d:.1f})") - print(f" 目标运动: 圆弧 R={TARGET_ARC_RADIUS_M:.1f} m, v={MOCK_TARGET_SPEED_MPS:.1f} m/s, " - f"{'逆时针' if TARGET_ARC_CCW >= 0 else '顺时针'} " - f"(圆心 N={target._center_n:.1f}, E={target._center_e:.1f})") - print(f" 场景: 正常跟踪 {TRACK_NORMAL_SECS:.0f}s → 目标消失 → " - f"Coast {TRACK_MAX_LOST_FRAMES / cfg.control.frequency_hz:.0f}s → " - f"判丢刹车 {TRACK_BRAKE_SECS:.0f}s") - - started_ms = now_ms() - target_gone_ms = started_ms + TRACK_NORMAL_SECS * 1000.0 - lost_at_ms = None - next_deadline = time.monotonic() + period_s - tick = 0 - - # 轨迹记录:(t_s, north_m, east_m, down_m) - drone_traj = [] - target_traj = [] - - try: - while True: - now = now_ms() - if now - started_ms >= TRACK_TIMEOUT_SECS * 1000.0: - logger.warning("跟踪段超时(%.0f s),提前收尾", TRACK_TIMEOUT_SECS) - break - - if target.visible and now >= target_gone_ms: - target.set_visible(False) - print("-- 模拟目标消失,追踪器进入 Coast 预测") - - detections, distance_m, distance_valid = target.observe( - hub.north_m, hub.east_m, hub.down_m, hub.yaw_rad) - - # 记录本拍位置(目标位置在 observe 内已 update) - t_s = (now - started_ms) / 1000.0 - drone_traj.append((t_s, hub.north_m, hub.east_m, hub.down_m)) - target_traj.append((t_s, target._pos_n, target._pos_e, target._pos_d)) - - result = tracker.update(detections, frame_shape) - snap = ControlSnapshot( - track=track_result_to_state(result), - track_time_ms=now, # 每拍取样即打戳(视觉始终新鲜) - target_distance_m=distance_m, - distance_valid=distance_valid, - distance_time_ms=now, - attitude_present=True, - attitude_time_ms=hub.attitude_time_ms, - yaw_rad=hub.yaw_rad, - roll_rad=hub.roll_rad, - pitch_rad=hub.pitch_rad, - ) - out = law.update(snap, now) - await dispatch(gateway, out, now) - - if tick % STATUS_EVERY_TICKS == 0: - print(f"[跟踪 t={t_s:5.1f}s] " - f"检测={len(detections)} " - f"追踪[trk={result.tracked} pred={result.is_predicted} " - f"lost={result.lost_frames}] 距离={distance_m:5.1f}m | " - f"[{out.mode.name}] vx_body={out.vx_body_mps:+.2f} " - f"vz={out.vz_mps:+.2f} yaw_rate={out.yaw_rate_dps:+.2f}°/s " - f"coast={out.coast_active}") - tick += 1 - - if not result.tracked: - if lost_at_ms is None: - lost_at_ms = now - print("-- 判丢,执行零速刹车") - elif now - lost_at_ms >= TRACK_BRAKE_SECS * 1000.0: - print("-- 判丢刹车完成,退出跟踪段") - break - - next_deadline += period_s - sleep_s = next_deadline - time.monotonic() - if sleep_s > 0: - await asyncio.sleep(sleep_s) - else: - next_deadline = time.monotonic() + period_s # 节拍落后,重锚 - finally: - await gateway.stop() - await hub.stop() - - # 跟踪段结束后绘制轨迹 - if PLOT_TRAJECTORY: - print(f"-- 绘制轨迹:无人机 {len(drone_traj)} 点,目标 {len(target_traj)} 点") - plot_trajectories(drone_traj, target_traj, TRAJECTORY_SAVE_PATH) - - return True - - -async def print_flight_mode(drone): - """打印当前飞行模式""" - async for flight_mode in drone.telemetry.flight_mode(): - print(f"[模式] 当前飞行模式: {flight_mode}") - return flight_mode - -async def wait_until_ready(drone): - """等待连接 + 位置估计就绪(各阶段限时 30 s)。""" - print("等待无人机连接...") - try: - async with asyncio.timeout(30.0): - async for state in drone.core.connection_state(): - if state.is_connected: - print("-- 已连接") - break - except TimeoutError: - raise RuntimeError("30 s 内未建立 MAVSDK 连接(检查 SITL 是否在运行、" - "14540 端口是否被占用)") from None - - print("等待全球位置估计就绪...") - try: - async with asyncio.timeout(30.0): - async for health in drone.telemetry.health(): - if health.is_global_position_ok and health.is_home_position_ok: - print("-- 位置估计 OK") - break - except TimeoutError: - raise RuntimeError("30 s 内位置估计未就绪") from None - -async def run(): - drone = System() - await drone.connect(system_address=CONNECTION) - - # 1. 等待就绪 - try: - await wait_until_ready(drone) - except RuntimeError as e: - print(f"! {e}") - return - - # 1.5 显式设定遥测流速率(不依赖默认配置;失败则沿用默认并告警) - for stream, rate in (("position", 50.0), - ("position_velocity_ned", 30.0), - ("attitude_euler", 50.0), - ("in_air", 10.0)): - try: - await getattr(drone.telemetry, f"set_rate_{stream}")(rate) - except Exception as e: - print(f"! 设置 {stream} 速率失败(沿用默认值): {e}") - print("-- 遥测速率已设定") - - # 记录初始模式(可选,用于最后恢复) - initial_mode = await print_flight_mode(drone) - - # 2. 解锁 - print("-- 解锁") - try: - await drone.action.arm() - except ActionError as e: - print(f"解锁失败: {e}") - return - - # 3. 起飞 - print(f"-- 起飞到 {TAKEOFF_ALTITUDE} m") - await drone.action.set_takeoff_altitude(TAKEOFF_ALTITUDE) - await drone.action.takeoff() - - # 等待起飞完成。注意:MAVSDK 遥测是带缓冲队列的推送流,循环体内 - # 不能 sleep —— 否则消费速率(如 2 Hz)远低于推送速率(50 Hz), - # 队列积压导致读到的是几十秒前的旧高度(问题一的根因)。全速排水 - # 使每条消息都是最新值,另加整体超时兜底。 - print("-- 等待起飞完成...") - wait_start = time.monotonic() - next_log = wait_start + 2.0 - reached = False - try: - async with asyncio.timeout(30.0): - async for position in drone.telemetry.position(): - if position.relative_altitude_m >= TAKEOFF_ALTITUDE * 0.90: - wait_s = time.monotonic() - wait_start - print(f"-- 已到达起飞高度: {position.relative_altitude_m:.2f} m " - f"(等待 {wait_s:.1f} s)") - reached = True - break - now_t = time.monotonic() - if now_t >= next_log: - print(f" 起飞爬升中: {position.relative_altitude_m:.2f} m") - next_log = now_t + 2.0 - except TimeoutError: - pass - if not reached: - print("! 30 s 内未达到起飞高度,放弃本次测试(执行降落兜底)") - try: - await drone.action.land() - except ActionError as e: - print(f"! 降落指令失败: {e}") - return - - await asyncio.sleep(1) - - # 4. 记录悬停锚点与锚点航向(虚拟目标按机头朝向放置) - try: - async with asyncio.timeout(10.0): - async for pos_vel in drone.telemetry.position_velocity_ned(): - anchor_ned = (pos_vel.position.north_m, - pos_vel.position.east_m, - pos_vel.position.down_m) - break - anchor_yaw_deg = 0.0 - async with asyncio.timeout(10.0): - async for euler in drone.telemetry.attitude_euler(): - anchor_yaw_deg = euler.yaw_deg - break - except TimeoutError: - print("! 10 s 内未取得悬停锚点/航向,放弃本次测试(执行降落兜底)") - try: - await drone.action.land() - except ActionError as e: - print(f"! 降落指令失败: {e}") - return - print(f" 悬停锚点 NED: N={anchor_ned[0]:.2f}, " - f"E={anchor_ned[1]:.2f}, D={anchor_ned[2]:.2f}," - f"机头 {anchor_yaw_deg:.1f}°") - - # 5. 视觉跟踪闭环(offboard 速度设定点,20 Hz) - await run_tracking(drone, anchor_ned, anchor_yaw_deg) - - # 6. 退出 offboard 后回到 Hold,稍候确认模式 - await asyncio.sleep(1) - await print_flight_mode(drone) - - # 7. 返航 (RTL) - print("-- 执行 RTL") - await drone.action.return_to_launch() - - # 8. 等待落地(同样全速排水,避免 in_air 队列积压读到旧状态) - print("-- 等待落地...") - try: - async with asyncio.timeout(240.0): - async for is_in_air in drone.telemetry.in_air(): - if not is_in_air: - print("-- 已落地") - break - except TimeoutError: - print("! 240 s 内未检测到落地") - - await asyncio.sleep(2) - - # 9. 上锁 - print("-- 上锁") - try: - await drone.action.disarm() - print("-- 上锁成功") - except ActionError as e: - print(f"上锁失败(可能已自动上锁): {e}") - - print(f"-- 初始模式是: {initial_mode}") - print("\n===== 测试流程全部完成 =====") - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO, - format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s", - datefmt="%H:%M:%S") - asyncio.run(run()) diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..bff1ffc --- /dev/null +++ b/tools/README.md @@ -0,0 +1,23 @@ +# tools/ —— 临时/台架小工具(不参与运行链路) + +这里放的是**一次性或台架用**的脚本,不被 `pod_bridge` / `task_runner` 引用, +也不在 pytest 收集范围内。移动进来是为了让仓库根目录只保留现行入口。 + +| 文件 | 用途 | 依赖 | 输出 | +|---|---|---|---| +| `camera.py` | 双目相机棋盘格采集 + 标定 | `opencv-python`、`numpy` | `stereo_calib_images/`(图像)、`stereo_calib_params.npz`(内参/外参) | +| `camera_test.py` | 单相机取流冒烟测试(索引/分辨率/帧率) | `opencv-python` | 无(窗口预览) | +| `video_test.py` | HDMI 全屏预览(第二屏) | `opencv-python`、`numpy` | 无(窗口预览) | + +## 注意 + +- 这些脚本会在**当前工作目录**下产出文件(`stereo_calib_images/`、`stereo_calib_params.npz`)。 + 建议在 `tools/` 内运行,避免污染仓库根: + ```bash + cd tools + "D:/program file/conda/envs/pytorch/python.exe" camera.py + ``` +- 标定结果现归档在 `legacy/stereo_calib_params.npz`(对应已归档的双目链路)。 + 当前运行链路**不需要**这些参数(吊舱不提供距离、双目未接入)。 +- 仓库根原有的 `camera.py` / `camera_test.py` / `video_test.py` 是同一批脚本, + 2026-10-20 移入本目录,内容未改。 diff --git a/tools/camera.py b/tools/camera.py new file mode 100644 index 0000000..95e16c8 --- /dev/null +++ b/tools/camera.py @@ -0,0 +1,182 @@ +import cv2 +import numpy as np +import glob +import os +from pathlib import Path + +# ==================== 配置参数 ==================== +CHESSBOARD_SIZE = (8, 11) # 内角点数量 (列, 行) +SQUARE_SIZE = 20.0 # 每个方格实际边长(毫米),请改成你的真实尺寸 + +CAMERA_INDEX = 1 +SAVE_DIR = "stereo_calib_images" # 保存标定图片的文件夹 +os.makedirs(SAVE_DIR, exist_ok=True) + +# 标定用分辨率(每个单目) +CALIB_WIDTH = 1280 +CALIB_HEIGHT = 720 + + +# ================================================= + +def capture_images(): + """采集标定图片(左右目同步保存)""" + cap = cv2.VideoCapture(CAMERA_INDEX, cv2.CAP_DSHOW) + if not cap.isOpened(): + cap = cv2.VideoCapture(CAMERA_INDEX, cv2.CAP_MSMF) + + cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, 2560) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) + + print("按空格键保存当前帧,按 q 退出采集") + count = 0 + + while True: + ret, frame = cap.read() + if not ret: + break + + left = frame[:, :1280].copy() + right = frame[:, 1280:].copy() + + gray_l = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) + gray_r = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) + + ret_l, corners_l = cv2.findChessboardCorners(gray_l, CHESSBOARD_SIZE, None) + ret_r, corners_r = cv2.findChessboardCorners(gray_r, CHESSBOARD_SIZE, None) + + # 用于显示的图像(带角点) + display_left = left.copy() + display_right = right.copy() + + if ret_l: + cv2.drawChessboardCorners(display_left, CHESSBOARD_SIZE, corners_l, ret_l) + if ret_r: + cv2.drawChessboardCorners(display_right, CHESSBOARD_SIZE, corners_r, ret_r) + + display = np.hstack([display_left, display_right]) + cv2.putText(display, f"Saved: {count} | Space: Save Q: Quit", (20, 40), + cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + cv2.imshow("Stereo Capture (Left | Right)", display) + + key = cv2.waitKey(1) & 0xFF + if key == ord(' '): # 空格保存 + if ret_l and ret_r: + # 1. 保存干净的原图(用于正式标定) + cv2.imwrite(f"{SAVE_DIR}/left_{count:02d}.png", left) + cv2.imwrite(f"{SAVE_DIR}/right_{count:02d}.png", right) + + # 2. 同时保存带检测结果的可视化图(方便检查) + cv2.imwrite(f"{SAVE_DIR}/left_{count:02d}_detect.png", display_left) + cv2.imwrite(f"{SAVE_DIR}/right_{count:02d}_detect.png", display_right) + + print(f"已保存第 {count} 组(含检测结果图)") + count += 1 + else: + print("未同时检测到棋盘格,未保存") + elif key == ord('q'): + break + + cap.release() + cv2.destroyAllWindows() + print(f"共采集 {count} 组图片") + + +def calibrate_stereo(): + """执行双目标定""" + # 准备物体点(世界坐标) + objp = np.zeros((CHESSBOARD_SIZE[0] * CHESSBOARD_SIZE[1], 3), np.float32) + objp[:, :2] = np.mgrid[0:CHESSBOARD_SIZE[0], 0:CHESSBOARD_SIZE[1]].T.reshape(-1, 2) + objp *= SQUARE_SIZE + + objpoints = [] # 3D点 + imgpoints_l = [] # 左目2D点 + imgpoints_r = [] # 右目2D点 + + left_images = sorted(glob.glob(f"{SAVE_DIR}/left_*.png")) + right_images = sorted(glob.glob(f"{SAVE_DIR}/right_*.png")) + + if len(left_images) < 10: + print("标定图片太少(建议 ≥15 组),请重新采集") + return + + print(f"开始处理 {len(left_images)} 组图片...") + + for left_path, right_path in zip(left_images, right_images): + img_l = cv2.imread(left_path) + img_r = cv2.imread(right_path) + + gray_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) + gray_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) + + ret_l, corners_l = cv2.findChessboardCorners(gray_l, CHESSBOARD_SIZE, None) + ret_r, corners_r = cv2.findChessboardCorners(gray_r, CHESSBOARD_SIZE, None) + + if ret_l and ret_r: + # 亚像素精化 + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) + corners_l = cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) + corners_r = cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) + + objpoints.append(objp) + imgpoints_l.append(corners_l) + imgpoints_r.append(corners_r) + print(f"✓ {Path(left_path).name}") + else: + print(f"✗ 跳过 {Path(left_path).name}") + + if len(objpoints) < 10: + print("有效图片不足,标定失败") + return + + # 单目标定(可选,用来给 stereoCalibrate 提供初始值) + ret_l, mtx_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera( + objpoints, imgpoints_l, (CALIB_WIDTH, CALIB_HEIGHT), None, None) + ret_r, mtx_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera( + objpoints, imgpoints_r, (CALIB_WIDTH, CALIB_HEIGHT), None, None) + + print("\n左目重投影误差:", ret_l) + print("右目重投影误差:", ret_r) + + # 双目标定 + flags = cv2.CALIB_FIX_INTRINSIC # 使用上面得到的内参 + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) + + ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( + objpoints, imgpoints_l, imgpoints_r, + mtx_l, dist_l, mtx_r, dist_r, + (CALIB_WIDTH, CALIB_HEIGHT), + criteria=criteria, + flags=flags + ) + + print("\n双目标定完成!") + print("重投影误差:", ret) + print("旋转矩阵 R:\n", R) + print("平移向量 T:\n", T) + + # 计算立体校正参数 + R1, R2, P1, P2, Q, roi_l, roi_r = cv2.stereoRectify( + mtx_l, dist_l, mtx_r, dist_r, + (CALIB_WIDTH, CALIB_HEIGHT), R, T, + alpha=0 # 0=只保留有效区域,1=保留所有像素 + ) + + # 保存所有结果 + np.savez("stereo_calib_params.npz", + mtx_l=mtx_l, dist_l=dist_l, + mtx_r=mtx_r, dist_r=dist_r, + R=R, T=T, E=E, F=F, + R1=R1, R2=R2, P1=P1, P2=P2, Q=Q) + + print("\n标定参数已保存到 stereo_calib_params.npz") + return mtx_l, dist_l, mtx_r, dist_r, R, T, Q + + +if __name__ == "__main__": + print("1. 先采集标定图片") + capture_images() + + print("\n2. 开始标定") + calibrate_stereo() \ No newline at end of file diff --git a/tools/camera_test.py b/tools/camera_test.py new file mode 100644 index 0000000..ffe1dda --- /dev/null +++ b/tools/camera_test.py @@ -0,0 +1,59 @@ +import cv2 + +# ==================== 配置区 ==================== +CAMERA_INDEX = 1 # 相机索引,如果不对请用前面的探测代码找 +FRAME_WIDTH = 2560 +FRAME_HEIGHT = 720 +TARGET_FPS = 30 +# ================================================= + +def decode_fourcc(val): + """把 get(CAP_PROP_FOURCC) 返回的浮点数转成可读的四字符代码""" + val = int(val) + return "".join([chr((val >> 8 * i) & 0xFF) for i in range(4)]) + +# 1. 打开相机,Windows 上指定 DirectShow 后端 +cap = cv2.VideoCapture(CAMERA_INDEX, cv2.CAP_DSHOW) + +if not cap.isOpened(): + print(f"无法打开相机,索引 {CAMERA_INDEX}") + exit() + +# 2. 设置分辨率(在设置 MJPG 之前或之后都可以,但建议先设格式) +cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) +cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH) +cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT) +cap.set(cv2.CAP_PROP_FPS, TARGET_FPS) + +# 3. 读取一次,确认参数是否真的生效 +ret, frame = cap.read() +if not ret: + print("相机已打开,但无法读取帧(可能分辨率不支持)") + cap.release() + exit() + +# 4. 打印实际生效的参数和帧格式 +print("===== 相机实际参数 =====") +print(f"分辨率 : {int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))} x {int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))}") +print(f"帧率 : {cap.get(cv2.CAP_PROP_FPS):.1f}") +print(f"FOURCC : {decode_fourcc(cap.get(cv2.CAP_PROP_FOURCC))}") +print(f"帧 dtype : {frame.dtype}") +print(f"帧 shape : {frame.shape} # 应为 (H, W, 3)") +print("========================") + +# 5. 实时显示循环 +while True: + ret, frame = cap.read() + if not ret: + print("读取帧失败,退出") + break + + cv2.imshow("USB Camera", frame) + + # 按 'q' 退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + +# 6. 释放资源 +cap.release() +cv2.destroyAllWindows() \ No newline at end of file diff --git a/tools/video_test.py b/tools/video_test.py new file mode 100644 index 0000000..7efd9bc --- /dev/null +++ b/tools/video_test.py @@ -0,0 +1,42 @@ +import cv2 +import numpy as np + +# 摄像头 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("无法打开摄像头") + exit() + +# 目标输出分辨率(匹配你的小屏+转换器) +OUT_WIDTH = 720 +OUT_HEIGHT = 480 + +# 创建窗口并设为可调整 +cv2.namedWindow("HDMI_Output", cv2.WINDOW_NORMAL) + +# 关键:把窗口移到第二块屏幕(HDMI)的位置 +# 假设主屏幕是 1920 宽,第二块屏幕从 x=1920 开始 +# 请根据你实际的主屏幕宽度修改这个值! +SECOND_SCREEN_X = 1920 # ← 改成你主屏幕的宽度 +SECOND_SCREEN_Y = 0 + +cv2.moveWindow("HDMI_Output", SECOND_SCREEN_X, SECOND_SCREEN_Y) +cv2.setWindowProperty("HDMI_Output", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) + +while True: + ret, frame = cap.read() + if not ret: + break + + # 缩放到目标分辨率 + frame = cv2.resize(frame, (OUT_WIDTH, OUT_HEIGHT)) + + # 显示 + cv2.imshow("HDMI_Output", frame) + + # 按 q 退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + +cap.release() +cv2.destroyAllWindows() \ No newline at end of file diff --git a/trajectory_plot.png b/trajectory_plot.png deleted file mode 100644 index d26d6506595d91063a104997de9a2eb070a2b709..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111909 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⁣9YgiN!Qiz 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 diff --git a/utils/README.md b/utils/README.md index c2c6764..6d1a185 100644 --- a/utils/README.md +++ b/utils/README.md @@ -1,51 +1,57 @@ -# utils —— reference-c++ 的 Python 移植 + 双目链路新实现 +# utils —— Python 实现(运行链路) -将 `reference-c++/` 下的 `target_tracker`(多目标追踪过滤器)与 -`visual_track_control`(视觉跟踪控制律)移植为 Python,**两包互不依赖** -(唯一的数据转换点在胶水层 demo)。 +> **架构现状(2026-10-20)**:运行链路是「光电吊舱 → `pod_bridge` → `task_runner` → PX4」。 +> 双目 + YOLO 那套可选感知后端(`stereo_yolo` / `target_tracker` / `stereo_ranger`) +> **已整体归档到仓库根的 `legacy/`**,默认不参与 pytest 收集;接回方式见 `legacy/README.md`。 -另有双目链路新实现(非 C++ 移植、按规格实现): -- `stereo_ranger`(双目稀疏目标测距),规格 `docs/双目测距技术路线.md`; -- `stereo_yolo`(双目 YOLO 检测前端),规格 `docs/双目YOLO目标识别需求说明.md`, - 输出直接作为 stereo_ranger 的输入(仅复用其 `types.py` 纯值类型)。 +本目录包含: -Python 侧定位为 **C++ 实现的先验验证**:构件划分与语义按规格冻结,便于逐构件 -移植对照;`stereo_yolo` 的 C++ 移植注记见其包 README。 +- **运行链路**:`pod_bridge`(吊舱桥接)+ `task_runner`(任务编排)+ `visual_track_control` + (控制律原语,被前两者复用) +- **入口共享件**:`sitl_support.py`(时钟/遥测订阅/连接准备/`RATE_*`) +- **测试**:`tests/`(446 条)、`conftest.py`、`pytest.ini` + +`visual_track_control` 是 `reference-c++/visual_track_control` 的逐构件 Python 移植 +(语义对齐、含行为修复);`target_tracker` 的移植版已随归档移入 `legacy/utils/`。 ## 目录结构 ``` utils/ -├── target_tracker/ # 包 1:卡尔曼滤波多目标追踪(无第三方硬依赖,仅 numpy) +├── pod_bridge/ # 吊舱 ↔ PX4 桥接(**运行链路核心**) +│ ├── bridge.py # PodBridge:L1/L2 切换、目标门控、链路维持 +│ ├── los_consistency.py # 目标有效性第二判据(LOS 运动学一致性) +│ ├── ground_link.py # 地面站 UDP 坐标注入 + 命令队列 +│ ├── guidance.py # L2 地理引导律 +│ ├── protocol.py # 表2-1/2-2/3/4 帧编解码 +│ ├── pod_serial.py # 串口链路(115200 8N1) +│ ├── px4_gateway.py # MAVSDK 遥测 + offboard 网关 +│ ├── virtual_pod.py # 虚拟吊舱(SITL/台架) +│ └── {run_bridge,listen_pod,pod_console,sim_fc,verify_px4}.py # 台架工具 +├── task_runner/ # 任务级编排 +│ ├── state_machine.py # WAIT→ARM_TAKEOFF→GOTO→SEARCH→TRACK→RTL +│ ├── runner.py # 编排器(快照采集/动作/异常兜底) +│ ├── return_home.py # 自定义返航(回起飞点→恢复航向→垂直降落) +│ ├── pod_control.py # 吊舱识别/锁定指令 +│ └── log.py # 事件 JSONL + 遥测 CSV + 简报 +├── visual_track_control/ # 控制律 + MAVSDK 网关(运行链路复用) │ └── README.md # 接口清单 + C++ 差异 -├── visual_track_control/ # 包 2:三通道控制律 + MAVSDK offboard 网关 -│ └── README.md # 接口清单 + C++ 差异 -├── stereo_ranger/ # 包 3:双目稀疏目标测距(纯标准库,无第三方依赖) -│ └── README.md # 接口清单 + 与规格的偏差 -├── stereo_yolo/ # 包 4:双目 YOLO 检测前端(真模型需 ultralytics) -│ └── README.md # 接口清单 + C++ 移植注记 -├── demo_track_control.py # 胶水层集成演示(模拟检测 → 追踪 → 控制律 → 发送) -├── demo_stereo_ranger.py # 双目测距演示(合成框对,六场景,无硬件) -├── demo_stereo_yolo.py # 检测前端演示(真模型 + 合成素材 → 测距联调) -├── assets/stereo_yolo/ # demo 占位素材(AI 合成图,可整体替换为真实素材) -├── config/track_control.json # 控制律配置(拷贝自 reference-c++) -├── config/stereo_ranger.json # 双目测距配置(占位值,待标定回填) -├── config/stereo_yolo.json # 检测前端配置(不存内参,几何参数唯一来源见上) -├── tests/ # pytest 测试 -│ ├── target_tracker/ # 28 条(对应 C++ 测试文件) -│ ├── visual_track_control/ # 81 条(对应 C++ 测试文件) -│ ├── stereo_ranger/ # 111 条(规格 §11.4 构件级 + 全流程) -│ └── stereo_yolo/ # 36 条(假检测器驱动,无模型/素材/硬件) -├── conftest.py # 保证可从任意工作目录导入各包 -└── pytest.ini # 收集 *_test.py 命名(与 C++ 测试文件名对齐); - # importlib 导入模式避免跨包同名测试文件冲突 +├── config/ # 配置 JSON(pod_bridge.json 为主线) +├── tests/ # pytest(446 条) +│ ├── pod_bridge/ # 208 条(协议/桥接/引导/注入/节流/LOS) +│ ├── task_runner/ # 122 条(状态机/编排器/返航/日志/吊舱控制) +│ ├── visual_track_control/ # 86 条(对应 C++ gtest + L1 增量) +│ └── ground_station/ # 30 条(页面与日志解读) +├── conftest.py # 把 utils/ 插进 sys.path + fake_mavsdk fixture +├── pytest.ini # 收集 *_test.py + importlib 导入模式 +└── sitl_support.py # 入口共用:时钟/遥测订阅/连接准备/速率常量 ``` ## 环境 conda env `pytorch`(`D:\program file\conda\envs\pytorch`),Python 3.12, -依赖:numpy、mavsdk、pytest;stereo_ranger 仅标准库;stereo_yolo 只有真模型 -推理路径需要 ultralytics(在检测器内部延迟导入,测试与假检测器路径不受影响)。 +依赖:numpy、mavsdk(<4)、pytest、flask、pyserial +(`opencv-python` 仅供 `tools/` 下的相机脚本;`ultralytics` 仅供已归档的双目链路)。 ## 运行测试 @@ -54,51 +60,46 @@ cd utils "D:/program file/conda/envs/pytorch/python.exe" -m pytest -q ``` -256 条测试全部通过:109 条 C++ 移植用例(含 §9.4 六条硬验收、A1/A2/A4 -回归、PID anti-windup C1、超龄 C4、首拍限幅 S6)+ 111 条 stereo_ranger -(合成框对驱动)+ 36 条 stereo_yolo(假检测器驱动)。 -stereo_ranger / stereo_yolo 测试均不需要真实图像、模型权重或硬件。 +**运行链路 446 条全部通过(2026-10-20)**: -## 运行演示 +| 组 | 条数 | +|---|---| +| `pod_bridge` | 208 | +| `task_runner` | 122 | +| `visual_track_control`(含 §9.4 硬验收、A1/A2/A4、C1、C4、S6、L1 角度直入) | 86 | +| `ground_station` | 30 | + +归档的可选后端 175 条(stereo_ranger 111 / stereo_yolo 36 / target_tracker 28) +在 `legacy/` 下,按需运行: ```bash -# mock 传输(本地记录指令,无实机) -python utils/demo_track_control.py --speedup 10 - -# MAVSDK 实发(需飞控/SITL 在 udp://:14540) -python utils/demo_track_control.py --transport mavsdk - -# 双目测距(合成框对,离线,无相机/飞控) -python utils/demo_stereo_ranger.py - -# 双目检测前端(真模型推理;需 ultralytics + models/yolo26n-drone.pt) -python utils/demo_stereo_yolo.py +cd legacy/utils && PYTHONPATH="../../utils" "D:/program file/conda/envs/pytorch/python.exe" -m pytest -q ``` -控制链路时间线 13 s(20 Hz):正常跟踪 6 s(vx 从零渐变 → 逼近限速 3 m/s)→ -目标消失 4 s(Coast 预测,60 帧后轨道判亡)→ 判零速刹车 3 s。 +运行链路测试不需要硬件(FakePod / VirtualPod / FakePx4 注入)。 -双目测距演示六场景:逼近(三道有效性门 + 滤波)→ 远场域外(方位仍发布)→ -单目退化 → 非同帧拒收 → 一致性降级/判无效 → 多目标贪心配对。 +## 运行入口 -检测前端演示五场景:单图冒烟 → 合成双目对联调(真检测框喂 stereo_ranger, -与解析式 D=F·B/Z 对照)→ 空场景 → 左右形状不一致拒收 → 帧周期计时。 -合成对用 AI 占位素材 `assets/stereo_yolo/drone_sky.png`(可整体替换为真实 -素材),本机实测 |ΔD|≈0.3 px、|Δy|≈0.2 px;帧周期 25~34 ms(640 输入, -双实例并发,随机器负载波动),与 30 fps 预算 33.3 ms 同量级,结论不可外推 -到机载平台(§5 的算力预算另见平台工程文档)。 +入口脚本在仓库根(不在 utils 内): + +```bash +PY="D:/program file/conda/envs/pytorch/python.exe" +"$PY" task_runner_sitl.py --conn udpin://0.0.0.0:14540 # 任务全流程 SITL +"$PY" task_runner_real.py --port COM5 --conn udpin://0.0.0.0:14540 --target-type 1 +"$PY" l1_sitl_test.py --conn udpin://0.0.0.0:14540 # 仅 L1 控制律闭环 +"$PY" utils/pod_bridge/run_bridge.py --port COM5 --dry-run # S2 台架干跑 +``` ## 移植约定 -- MAVLink 部分改为 **MAVSDK 语义映射**(offboard API,非字节透传),三分支 - 映射保留 C++ A2 回归语义,详见包 2 README。 +- MAVLink 部分改为 **MAVSDK 语义映射**(offboard API,非字节透传),三分支映射保留 + C++ A2 回归语义,详见 `visual_track_control/README.md`。 - 测试逐条移植 C++ gtest 断言(`EXPECT_DOUBLE_EQ` → 精确 `==`, - `EXPECT_NEAR` → `pytest.approx`);MAVSDK 部分因无 SITL/实机,仅 mock 验证。 + `EXPECT_NEAR` → `pytest.approx`)。 - 行为修复项(C1/C2/C4、A1/A2/A4、S6)在测试文件头注释中标注对应章节。 -- `stereo_ranger` 不是移植:按规格五步流程新实现,与规格的偏差(越界=有效性门、 - 视差域权威、补充 consistency_jump_px/isolation_gate_px 等)见其包 README; - ⚠ 全部数值为占位值,标定完成前不得作为飞行控制决策依据。 -- `stereo_yolo` 不是移植:推理口径为 ultralytics 直跑 `.pt`,letterbox、解码、 - NMS、坐标回映均在库内完成;C++ 侧复现要点与接口契约硬约束(原生分辨率坐标、 - 左右预处理位级一致、`x1` 即框左边缘、无跨帧状态)见其包 README 的移植注记表。 - ⚠ 占位素材 `assets/stereo_yolo/` 为 AI 合成图,真实双目素材到位后需重跑联调。 +- ⚠ 全部数值为占位值,标定完成前不得作为飞行控制决策依据。 + +## 已归档内容 + +`legacy/utils/` 下保留了原 `stereo_yolo` / `stereo_ranger` / `target_tracker` 三包、 +175 条测试、三个 demo、`stereo_*.json`、素材与 YOLO 权重;说明见 `legacy/README.md`。 diff --git a/utils/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc b/utils/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index a8a06ab6515fcae4d9eac9d214f42966876f9f79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 739 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 diff --git a/utils/__pycache__/demo_track_control.cpython-312.pyc b/utils/__pycache__/demo_track_control.cpython-312.pyc deleted file mode 100644 index 7103de541ee4bba9de393152531e2ef4ec92e4ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12380 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 diff --git a/utils/config/pod_bridge.json b/utils/config/pod_bridge.json index 98956c1..8e01b44 100644 --- a/utils/config/pod_bridge.json +++ b/utils/config/pod_bridge.json @@ -39,7 +39,10 @@ "alpha_from_frame": false, "alpha_from_off_target": true, "vertical_enabled": true, - "require_vision": false + "los_consistency_enabled": true, + "los_window_ms": 3000, + "los_min_yaw_change_deg": 20.0, + "los_max_residual_deg": 8.0 }, "tracking": { diff --git a/utils/pod_bridge/bridge.py b/utils/pod_bridge/bridge.py index e604514..38aaaec 100644 --- a/utils/pod_bridge/bridge.py +++ b/utils/pod_bridge/bridge.py @@ -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, diff --git a/utils/pod_bridge/config.py b/utils/pod_bridge/config.py index 9c8b84a..b4921c2 100644 --- a/utils/pod_bridge/config.py +++ b/utils/pod_bridge/config.py @@ -67,7 +67,14 @@ class HeadingServoConfig: alpha_from_frame: bool = False # α 是否叠加框架角 frame_pitch_deg alpha_from_off_target: bool = True # α 是否叠加脱靶量 off_target_y_deg vertical_enabled: bool = True # 无人机 L1 开垂直(决策3);false → α 强制 0 - require_vision: bool = False # true = 需我方视觉作第二判据(S3 起,§2.3) + # --- 目标有效性第二判据:LOS 运动学一致性(2026-10-19 取代 require_vision)--- + # 背景:吊舱已取代双目+YOLO 成为唯一目标源,"我方视觉"不再存在;而 bit2 + # 丢失后不复位、脱靶量丢失表现为 0。故改用"机头转动 vs 脱靶量变化"的一致性 + # 作为独立于 bit2 的活体判据(详见 los_consistency.py)。 + los_consistency_enabled: bool = True + los_window_ms: int = 3000 # 比较窗口(毫秒) + los_min_yaw_change_deg: float = 20.0 # 窗口内机头至少转这么多才判定(激励门槛) + los_max_residual_deg: float = 8.0 # |Δβ+Δψ| 上限,超过判不一致 @dataclass @@ -283,8 +290,20 @@ def _load_heading_servo(root: dict) -> HeadingServoConfig: s, "heading_servo", "alpha_from_off_target", c.alpha_from_off_target) c.vertical_enabled = _get_bool( s, "heading_servo", "vertical_enabled", c.vertical_enabled) - c.require_vision = _get_bool( - s, "heading_servo", "require_vision", c.require_vision) + c.los_consistency_enabled = _get_bool( + s, "heading_servo", "los_consistency_enabled", + c.los_consistency_enabled) + c.los_window_ms = int(_positive( + _get_integer(s, "heading_servo", "los_window_ms", c.los_window_ms), + "heading_servo.los_window_ms")) + c.los_min_yaw_change_deg = _in_range( + _get_number(s, "heading_servo", "los_min_yaw_change_deg", + c.los_min_yaw_change_deg), + 0.0, 180.0, "heading_servo.los_min_yaw_change_deg") + c.los_max_residual_deg = _in_range( + _get_number(s, "heading_servo", "los_max_residual_deg", + c.los_max_residual_deg), + 0.0, 180.0, "heading_servo.los_max_residual_deg") return c diff --git a/utils/pod_bridge/ground_link.py b/utils/pod_bridge/ground_link.py index 3681590..081c079 100644 --- a/utils/pod_bridge/ground_link.py +++ b/utils/pod_bridge/ground_link.py @@ -1,20 +1,25 @@ -"""ground_link.py —— 地面坐标注入通道(预定流程.md「地面给坐标」)。 +"""ground_link.py —— 地面站链路(坐标注入 + 控制命令,预定流程.md「地面给坐标」)。 本吊舱无激光 → 表4 目标经纬度永不出(部署文档 §10.0),目标坐标改由地面站注入。 -本模块负责接收地面站下发的目标坐标(UDP JSON),维护一份带本地打戳的注入目标 -缓存,供 PodBridge 的 L2 地理引导**优先消费**(回退表4)。 +本模块负责接收地面站下发的两类 UDP JSON 消息: + * 目标坐标 {"lat":..,"lon":..,"alt":..} → 维护一份带本地打戳的注入目标缓存, + 供 PodBridge 的 L2 地理引导**优先消费**(回退表4); + * 控制命令 {"cmd":""}(rtl / emergency / pod_*)→ 入命令队列, + 由 TaskRunner 主循环 drain_commands() 取走并分派。 设计要点: - * UDP 无连接监听,每包 JSON {"lat":..,"lon":..,"alt":..}(可选 "ts" 仅诊断,不用)。 + * UDP 无连接监听;坐标包可选 "ts" 仅诊断,不用。 * 新鲜度用「本地接收时刻」打戳(now_ms_fn 单调时钟),**不信任地面站时钟** (两端时钟不同步);超龄由下游按 GroundInjectConfig.timeout_ms 判无效。 - * parse_payload 为纯函数(可单测);socket / 事件循环拆到 run(),且 socket - 延迟导入,模块 import 无副作用(bridge 仅复用 GroundTarget 类型)。 + * 命令是事件(非状态):队列缓存、满则丢最旧;未知命令名由上层忽略。 + * parse_payload / parse_message 为纯函数(可单测);socket / 事件循环拆到 + run(),且 socket 延迟导入,模块 import 无副作用(bridge 仅复用 GroundTarget 类型)。 """ import asyncio import json import logging +from collections import deque from dataclasses import dataclass from typing import Callable, Optional @@ -42,6 +47,18 @@ class GroundTarget: time_ms: int = 0 # 本地接收时刻(单调毫秒),0 = 从未注入 +@dataclass +class GroundCommand: + """地面站控制命令(事件语义)。 + + name 为小写命令名(rtl / emergency / pod_detect_on / …);time_ms 为本地 + 接收时刻(单调毫秒,仅日志/诊断用)。合法性(白名单)由上层分派时判。 + """ + + name: str = "" + time_ms: int = 0 + + @dataclass class GroundInjectConfig: """地面坐标注入配置(全外置;数值占位,待落地时回填)。""" @@ -53,12 +70,13 @@ class GroundInjectConfig: class GroundLink: - """地面坐标注入接收器。 + """地面站链路接收器(坐标注入 + 控制命令队列)。 用法(在 runner 外部以 asyncio task 运行): link = GroundLink(cfg, now_ms_fn) task = asyncio.create_task(link.run(stop_event)) - PodBridge 通过 `ground_target_fn=lambda: link.latest()` 读取注入坐标。 + PodBridge 通过 `ground_target_fn=lambda: link.latest()` 读取注入坐标; + TaskRunner 主循环通过 `drain_commands()` 取控制命令。 """ def __init__(self, cfg: GroundInjectConfig, @@ -66,6 +84,7 @@ class GroundLink: self._cfg = cfg self._now_ms = now_ms_fn self._target = GroundTarget() + self._commands: deque[str] = deque(maxlen=16) def latest(self) -> GroundTarget: """返回当前注入目标(可能 time_ms=0 表示从未注入)。""" @@ -76,6 +95,12 @@ class GroundLink: target.time_ms = self._now_ms() self._target = target + def drain_commands(self) -> list[str]: + """取走全部待处理命令(FIFO,取后清空)。命令为事件,无新鲜度概念。""" + cmds = list(self._commands) + self._commands.clear() + return cmds + def is_valid(self, now_ms: int) -> bool: """注入坐标是否新鲜有效(未注入 / 超龄 → False)。""" t = self._target @@ -84,15 +109,19 @@ class GroundLink: return not is_stale(t.time_ms, now_ms, self._cfg.timeout_ms) @staticmethod - def parse_payload(payload: bytes) -> Optional[GroundTarget]: - """解析地面站 JSON 包(纯函数,可单测)。返回不含 time_ms 的目标; - 非法 JSON / 非对象 / 缺 lat 或 lon → None(静默丢弃)。""" + def _load_object(payload: bytes) -> Optional[dict]: + """解 JSON 且要求顶层为对象(纯函数)。非法 JSON / 非对象 → None。""" try: obj = json.loads(payload.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): return None if not isinstance(obj, dict): return None + return obj + + @staticmethod + def _target_from_object(obj: dict) -> Optional[GroundTarget]: + """从 dict 提取坐标(缺 lat/lon 或类型错 → None)。""" lat = obj.get("lat") lon = obj.get("lon") if not _is_number(lat) or not _is_number(lon): @@ -103,6 +132,27 @@ class GroundLink: return GroundTarget(latitude_deg=float(lat), longitude_deg=float(lon), abs_alt_m=float(alt)) + @staticmethod + def parse_payload(payload: bytes) -> Optional[GroundTarget]: + """解析坐标包(纯函数,可单测)。返回不含 time_ms 的目标; + 非法 JSON / 非对象 / 缺 lat 或 lon → None(静默丢弃)。""" + obj = GroundLink._load_object(payload) + if obj is None: + return None + return GroundLink._target_from_object(obj) + + @staticmethod + def parse_message(payload: bytes) -> Optional[GroundTarget | GroundCommand]: + """解析任意地面站包(纯函数):{"cmd":".."} → GroundCommand(优先), + 否则按坐标包解析;非法包 → None(静默丢弃)。""" + obj = GroundLink._load_object(payload) + if obj is None: + return None + cmd = obj.get("cmd") + if isinstance(cmd, str) and cmd.strip(): + return GroundCommand(name=cmd.strip().lower()) + return GroundLink._target_from_object(obj) + async def run(self, stop_event: asyncio.Event) -> None: """UDP 接收循环:每收一包合法 JSON 更新注入目标并本地打戳。""" import socket # 延迟导入:仅真正跑 socket 时才需要 @@ -123,13 +173,17 @@ class GroundLink: logger.warning("[GroundLink] 接收异常: %s", exc) await asyncio.sleep(0.05) continue - target = self.parse_payload(data) - if target is None: + msg = self.parse_message(data) + if msg is None: continue # 非法包静默丢弃(热路径不打日志) - target.time_ms = self._now_ms() - self._target = target + msg.time_ms = self._now_ms() + if isinstance(msg, GroundCommand): + self._commands.append(msg.name) + logger.debug("[GroundLink] 收到命令: %s", msg.name) + continue + self._target = msg logger.debug("[GroundLink] 更新注入目标 (%f, %f, %fm)", - target.latitude_deg, target.longitude_deg, - target.abs_alt_m) + msg.latitude_deg, msg.longitude_deg, + msg.abs_alt_m) finally: sock.close() diff --git a/utils/pod_bridge/los_consistency.py b/utils/pod_bridge/los_consistency.py new file mode 100644 index 0000000..68365f5 --- /dev/null +++ b/utils/pod_bridge/los_consistency.py @@ -0,0 +1,115 @@ +"""los_consistency.py —— 吊舱角度「运动学一致性」校验(替代旧的 require_vision)。 + +为什么需要它 +------------ +吊舱协议有两处已知缺陷(部署文档 §10): + * bit2 锁定位在目标丢失后**不复位**; + * 脱靶量没有无效标志位,**丢失时表现为 0**。 +只信 bit2 会让飞机朝"过期坐标/幽灵目标"继续飞。 + +原设计用「我方双目视觉」作第二判据(`require_vision`)。但后续决定**吊舱取代 +双目 + YOLO 成为唯一目标源**,于是"我方视觉"不再存在:该判据要么恒 False +(链路永远进不了 TRACK),要么指向吊舱自身(自证,零信息量)。 + +本模块给出**不依赖任何额外传感器**的替代判据: + 本吊舱无电机、框架刚性固连机体 → 相机光轴随刚体转动。 + 因此目标在世界系静止时,机头转 Δψ,脱靶量必须变化约 −Δψ。 + 若机头已经明显转动(|Δψ| ≥ 阈值)而脱靶量几乎不动 → 角度冻结/幽灵, + 判为不一致(fail-safe:当丢目标处理)。 + +判别量:`residual = Δβ + Δψ`(β 为右正视线角,ψ 为 NED 航向,均取度)。 + * 一致:|residual| ≤ max_residual_deg(目标在世界系缓慢移动也会落在余量内) + * 不一致:|residual| 超限且 |Δψ| 足够大(否则是"没有激励",不做判断) + +局限(必须在文档里写清):它只能证明「角度是活的」,**不能证明「是那个目标」**。 +若吊舱锁错了物体但角度正常变化,本判据无法识别 —— 那属于识别问题,需另接 +独立传感器(双目/其他),不是本模块能覆盖的。 +""" + +from collections import deque +from dataclasses import dataclass +import math + + +def wrap_deg_180(deg: float) -> float: + """角度回绕到 [-180, 180)。""" + d = math.fmod(deg + 180.0, 360.0) + if d < 0.0: + d += 360.0 + return d - 180.0 + + +@dataclass +class LosConsistencyConfig: + """一致性判据参数(全外置)。""" + + enabled: bool = True # False → 关掉判据(退回"只信 bit2") + window_ms: int = 3000 # 比较窗口:用"当前"与"约 window 前"的样本比对 + min_yaw_change_deg: float = 20.0 # 窗口内机头至少转这么多才做判断(激励门槛) + max_residual_deg: float = 8.0 # |Δβ+Δψ| 超过即判不一致 + history_factor: float = 2.0 # 历史缓冲 = window × factor + + +@dataclass +class LosStatus: + """单次判定结果(供门控与日志)。""" + + consistent: bool = True # 是否一致(默认一致 = 不干扰正常跟踪) + excited: bool = False # 本窗口是否达到"激励门槛"(转过足够角度) + residual_deg: float = 0.0 # Δβ + Δψ + delta_yaw_deg: float = 0.0 + delta_beta_deg: float = 0.0 + + @property + def frozen(self) -> bool: + """判为"角度冻结/幽灵":有激励但不一致。""" + return self.excited and not self.consistent + + +class LosConsistencyMonitor: + """按窗口比较 Δβ 与 Δψ,判断吊舱角度是否与机体转动一致。 + + 用法:每个控制周期喂一次(仅在吊舱报告锁定期间喂), + `update(now_ms, yaw_deg, beta_deg)` 返回 `LosStatus`。 + 吊舱解锁时应调用 `reset()`,避免上一次跟踪的历史污染下一次。 + """ + + def __init__(self, cfg: LosConsistencyConfig | None = None) -> None: + self._cfg = cfg or LosConsistencyConfig() + self._samples: deque = deque() + + def reset(self) -> None: + self._samples.clear() + + def update(self, now_ms: int, yaw_deg: float, beta_deg: float) -> LosStatus: + cfg = self._cfg + if not cfg.enabled: + return LosStatus(consistent=True, excited=False) + + self._samples.append((now_ms, float(yaw_deg), float(beta_deg))) + keep_ms = cfg.window_ms * max(1.0, cfg.history_factor) + while self._samples and now_ms - self._samples[0][0] > keep_ms: + self._samples.popleft() + + # 参考样本:窗口内**最老**的那个 + ref = None + for sample in self._samples: + if now_ms - sample[0] <= cfg.window_ms: + ref = sample + break + if ref is None: + return LosStatus(consistent=True, excited=False) + + span_ms = now_ms - ref[0] + # 历史不足一个窗口的 3/4 时,Δψ 天然偏小,不做判断(避免误判丢目标) + if span_ms < cfg.window_ms * 0.75: + return LosStatus(consistent=True, excited=False) + + delta_yaw = wrap_deg_180(yaw_deg - ref[1]) + delta_beta = beta_deg - ref[2] + residual = delta_beta + delta_yaw + excited = abs(delta_yaw) >= cfg.min_yaw_change_deg + consistent = (not excited) or (abs(residual) <= cfg.max_residual_deg) + return LosStatus(consistent=consistent, excited=excited, + residual_deg=residual, delta_yaw_deg=delta_yaw, + delta_beta_deg=delta_beta) diff --git a/utils/pod_bridge/px4_gateway.py b/utils/pod_bridge/px4_gateway.py index 96f3c56..a876dd6 100644 --- a/utils/pod_bridge/px4_gateway.py +++ b/utils/pod_bridge/px4_gateway.py @@ -3,6 +3,8 @@ 职责(桥接的飞控侧): * 订阅 PX4 遥测(position / attitude_euler / gps_info / velocity_ned),缓存为 OwnState 快照,供桥接层构造 表3(载机姿态下发) 与 GuidanceInput(引导输入); + * 订阅飞控状态文本 status_text()(STATUSTEXT),缓存供任务日志/地面站显示 + (飞控警告/异常信息); * 复用 visual_track_control.MavsdkCommandGateway 发送 offboard 速度设定点 (不重造三分支映射,直接喂 GuidanceLaw 的 ControlOutput)。 @@ -15,6 +17,7 @@ import asyncio import logging import math import time +from collections import deque from dataclasses import dataclass, replace from typing import Any, Callable, Optional @@ -23,6 +26,7 @@ from visual_track_control.mavsdk_gateway import MavsdkCommandGateway logger = logging.getLogger(__name__) _GPS_FIX_3D = 3 # mavsdk GpsFixType.THREE_D_FIX 起视为定位有效 +_FC_MSG_BUFFER = 64 # 飞控状态文本缓存上限(低频流,仅供日志消费) def _enum_int(value: Any) -> int: @@ -69,6 +73,7 @@ class Px4Gateway: self._now_ms = now_ms_fn or (lambda: int(time.monotonic() * 1000)) self._state = OwnState() self._tasks: list = [] + self._fc_msgs: deque = deque(maxlen=_FC_MSG_BUFFER) # --- 连接(实机)------------------------------------------------------- @classmethod @@ -123,13 +128,25 @@ class Px4Gateway: """返回当前缓存的快照副本(调用方修改不影响内部状态)。""" return replace(self._state) + def update_fc_status(self, st: Any) -> None: + """缓存一条飞控状态文本(STATUSTEXT);severity 取枚举名(如 WARNING)。""" + sev = getattr(st, "type", None) + sev_name = getattr(sev, "name", None) or str(sev or "UNKNOWN") + self._fc_msgs.append((sev_name, str(getattr(st, "text", "")))) + + def take_fc_messages(self) -> list: + """取走并清空飞控状态文本(供任务日志落盘;无则返回 [])。""" + msgs = list(self._fc_msgs) + self._fc_msgs.clear() + return msgs + def _recompute_valid(self) -> None: self._state.position_valid = (self._state.has_position and self._state.fix_type >= _GPS_FIX_3D) # --- 遥测订阅(异步胶水)---------------------------------------------- async def start_telemetry(self) -> None: - """订阅四路遥测流,后台任务持续刷新缓存。幂等:已订阅则直接返回。""" + """订阅遥测流 + 飞控状态文本,后台任务持续刷新缓存。幂等:已订阅则直接返回。""" if self._tasks: return # 已订阅,避免重复起任务(runner 会先订阅,bridge.start 再触发时跳过) t = self._drone.telemetry @@ -140,6 +157,8 @@ class Px4Gateway: asyncio.ensure_future(self._consume(t.gps_info(), self.update_gps)), asyncio.ensure_future( self._consume(t.velocity_ned(), self.update_velocity_ned)), + asyncio.ensure_future( + self._consume(t.status_text(), self.update_fc_status)), ] logger.info("[Px4Gateway] 遥测订阅已启动") @@ -171,5 +190,9 @@ class Px4Gateway: async def send_brake_hover(self, time_boot_ms: Optional[int] = None) -> bool: return await self._cmd.send_brake_hover(time_boot_ms) + async def send_yaw_rate(self, yaw_rate_dps: float) -> bool: + """原地偏航扫视(SEARCH 态):零速 + 指定角速度,保持 offboard 心跳。""" + return await self._cmd.send_yaw_rate(yaw_rate_dps) + async def stop_offboard(self) -> None: await self._cmd.stop() diff --git a/utils/sitl_support.py b/utils/sitl_support.py new file mode 100644 index 0000000..16698b7 --- /dev/null +++ b/utils/sitl_support.py @@ -0,0 +1,131 @@ +"""sitl_support.py —— SITL / 真机入口共用的 MAVSDK 小工具。 + +从根目录 `pod_sitl_test.py` 拆出:原先两个现行入口 +(`task_runner_sitl.py` / `task_runner_real.py`)都从这个"测试脚本"里 import +`now_ms` / `wait_until_ready` 等,语义上很别扭。现归位为独立支持模块, +入口只依赖它,`l1_sitl_test.py` 则作为纯粹的 L1 集成测试脚本。 + +内容(刻意保持无业务逻辑): + * `RATE_*`:遥测流速率**唯一来源**(各订阅方共用;PX4 速率由最后一次设置决定) + * `now_ms`:单调毫秒时钟(与 PodBridge / TaskRunner 同源) + * `TelemetryHub`:后台订阅 NED 位置 + 欧拉角,供 VirtualPod 几何合成 + * `wait_until_ready` / `set_telemetry_rates` / `print_flight_mode`:连接准备与诊断 +""" + +import asyncio +import math +import time + +from pod_bridge.virtual_pod import CarrierView + +# 遥测流速率(Hz)——**唯一来源**,供各订阅方(Px4Gateway / TelemetryHub)共用。 +# PX4 的消息速率由最后一次 SET_MESSAGE_INTERVAL 决定,多个订阅方用不同值会互相 +# 覆盖;速率越高链路负荷越大(实测压出 MAVSDK 回调队列积压告警)。 +# 取值依据见 set_telemetry_rates()。 +RATE_ATTITUDE_HZ = 20.0 # 控制律 attitude_stale_ms=300 要用姿态 +RATE_POSITION_HZ = 10.0 # 起飞到位 / 到参考点距离 +RATE_POSITION_VELOCITY_NED_HZ = 10.0 # 虚拟吊舱几何(TelemetryHub) +RATE_GPS_HZ = 2.0 +RATE_VELOCITY_HZ = 2.0 +RATE_IN_AIR_HZ = 2.0 + + +def now_ms() -> int: + """单调毫秒(与 Px4Gateway / PodBridge / TaskRunner 共用同一时钟源)。""" + return int(time.monotonic() * 1000.0) + + +class TelemetryHub: + """后台订阅 NED 位置/姿态,供 VirtualPod 几何合成与锚点记录。""" + + def __init__(self) -> None: + self.yaw_rad = 0.0 + self.north_m = 0.0 + self.east_m = 0.0 + self.down_m = 0.0 + self.attitude_time_ms = 0 + self._tasks: list = [] + + def start(self, drone) -> None: + self._tasks.append(asyncio.create_task(self._attitude_loop(drone))) + self._tasks.append(asyncio.create_task(self._position_loop(drone))) + + async def _attitude_loop(self, drone) -> None: + async for euler in drone.telemetry.attitude_euler(): + self.yaw_rad = math.radians(euler.yaw_deg) + self.attitude_time_ms = now_ms() + + async def _position_loop(self, drone) -> None: + async for pos_vel in drone.telemetry.position_velocity_ned(): + pos = pos_vel.position + self.north_m = pos.north_m + self.east_m = pos.east_m + self.down_m = pos.down_m + + async def stop(self) -> None: + for task in self._tasks: + task.cancel() + for task in self._tasks: + try: + await task + except asyncio.CancelledError: + pass + self._tasks.clear() + + def view(self) -> CarrierView: + return CarrierView(north_m=self.north_m, east_m=self.east_m, + down_m=self.down_m, yaw_rad=self.yaw_rad) + + +async def print_flight_mode(drone): + async for flight_mode in drone.telemetry.flight_mode(): + print(f"[模式] 当前飞行模式: {flight_mode}") + return flight_mode + + +async def wait_until_ready(drone): + """等待连接 + 位置估计就绪(各阶段限时 30 s)。""" + print("等待无人机连接...") + try: + async with asyncio.timeout(30.0): + async for state in drone.core.connection_state(): + if state.is_connected: + print("-- 已连接") + break + except TimeoutError: + raise RuntimeError("30 s 内未建立 MAVSDK 连接(检查 SITL 是否在运行)") from None + + print("等待全球位置估计就绪...") + try: + async with asyncio.timeout(30.0): + async for health in drone.telemetry.health(): + if health.is_global_position_ok and health.is_home_position_ok: + print("-- 位置估计 OK") + break + except TimeoutError: + raise RuntimeError("30 s 内位置估计未就绪") from None + + +async def set_telemetry_rates(drone): + """按**实际消费需求**设定遥测流速率(不是越高越好)。 + + 取值依据(2026-10-15 复核): + * `attitude_euler` 20Hz —— 控制律 `attitude_stale_ms=300` 要用姿态;20Hz 足够 + * `position` 10Hz —— 任务层只需「起飞到位」与「到参考点距离」,10Hz 足够 + * `position_velocity_ned` 10Hz —— 仅虚拟吊舱几何用(真机链路不订阅) + * `gps_info` / `velocity_ned` / `in_air` 2Hz —— 状态量,变化极慢 + + 原值(position/attitude 各 50Hz + pvn 30Hz ≈ 130 msg/s)在 CPU 紧张的宿主机上 + 会压出 MAVSDK「User callback queue slow」告警;下调后链路负荷降一个量级。 + 失败沿用默认值并告警。 + """ + for stream, rate in (("position", RATE_POSITION_HZ), + ("attitude_euler", RATE_ATTITUDE_HZ), + ("position_velocity_ned", RATE_POSITION_VELOCITY_NED_HZ), + ("gps_info", RATE_GPS_HZ), + ("velocity_ned", RATE_VELOCITY_HZ), + ("in_air", RATE_IN_AIR_HZ)): + try: + await getattr(drone.telemetry, f"set_rate_{stream}")(rate) + except Exception as e: + print(f"! 设置 {stream} 速率失败(沿用默认值): {e}") diff --git a/utils/stereo_ranger/__pycache__/__init__.cpython-312.pyc b/utils/stereo_ranger/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d35fad23914da046acd8656350d26c9386c7a5f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1870 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 diff --git a/utils/stereo_ranger/__pycache__/distance_filter.cpython-312.pyc b/utils/stereo_ranger/__pycache__/distance_filter.cpython-312.pyc deleted file mode 100644 index 0f2c03b72628fc6496868db629262a5aa8e876a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4474 zcmbVPYitwQ6`rxj&Ja&x5(D88b_bFaOdt>OM(I}FhPG-~=~mfRt0>*6$xOhB6K7{^ z)I{DYPy!AKiD?&N2yG1ujafDj7Rm+)LF%tcl~yqU*&S`A)ox~N|H5rrt<+yVXU2A% z5N%6G^11h(d(VA*_dDlY|D~WHpMdXQ+?BT1Ed=o;HZmWTuFTg!WrW~~CW0q1~9B}eY653j0st|%A6&!_x< z8rxH~`}tJ=^Qnt-lfB8{y}3t2$=-`>C99k{r%a9|KDna|2a~7H#2?*!J~gP`8jg?N zR70oY10Th248%|0Q+tnn*?T-ce@EIty)~-%AH?6koolIFJrNI_MEk*G@rUDT@0c=m zT$!ANVX$iA)LqcTN6)H5W9cpJg)L=`<|eNudixT`1`~Hb$Y(Oyh4s_N8vanZ6yCN?z0|Ls^TP?YZJWD40H;no9!*>x#666Jpr<-M^+1^%hXFX# z@bS6HA?5xZ_2F5V?9ZaPfsIkI*EIyfU3E7lDK(UJvX7zsJ6DMmQ^12B`-e& z-p$?b14!c+Ph{{%&}5Ut)pf)nGi~0EuJnC%$V~3V$!1aTc*V|!mW&4?a>n#;{!^%c zH;;tCWPR3$tw3*WyF z)*onoQ*<8&x2$(~9L+6mUf_7q-6`zw2$HAS>v6iIW(gcvaC4%g^ROT`^Jz+Qx@T8) zb;~pd3p+g=*Hf~XuvM9kmAGsVh+ZOQEe`zTsdZDZ<%xCEXT_y68+XqZ@0nrtq>RMM z&AE!0r6joXiRBedw{Nz1{|vJqb(@}8HZ0Pu+2E%JEwPPuKlPMepDn9uK+VKg6;VOa780Pi8g2~{n~OZ(w#msL%R2XR&7}#E!ei@ zEz~nLAIX!Mltcm{dy9~w%dP1n>%hooXe2y%ecA_OjXiLWnU%p3Z*|GnUJtkZKg^5n z(8eq=hGu4!9&iV|xc4X@^|gDX8VS3>`*RqY4dd!i7*Y+yDfQBa*+~Ay$zR7u@2W%B zAdM=$Q;o>w6=>mve;&WTcNZr}GsI4qwD+_&R*0$A8$7m)>mqzH)D9uJ_He?I&PwC2- z#WrKv5M#^y^}}!UzY(-Wn2j;p8vhSIvlRNJz}x-5oVC0XS{2>2GqMTzzH^z8G24b9 z|NHg}?cv6Web?WKyiymj)xmnD<$h|I>1P6s5qe$BTH?2gC9u1eY_x+$mfVFpxgj<# zN42A*NOogit}U43*OrZmdK)E>*pMMHZ`3Ga&}eL47?Y{BH^Uw5Uw+-TEU`1AwYhU0 zWcfY)S35Wjy45o0(cP+>v{T-9VAZA1AYkmGJUTtCGgAo+>F<`rt!w(*Pfxv>G^#%# z*~HC>%%cbsYlDb85DP#6Cy}@sXT=RzD?(9_ZofHZ zDGIz5UA-l;ddm~bmKd`VnHptEBTQ+qF;xDPL0yz#BMcj5$|Fp9s4-mrjH&*7Yn8uV z*N1FRn2HzahiA}Y>prObf3X!J3+a6g%yrONh~cdA=r>A#`Z=nHk=!6tt$gyaepQfq z*#<~xa1nNcjy?i^-LL?+g!hw)046P$_N-b0u%_Ju<0SMiz}9LT_VxRs)>k9eS3|~G zYxxXa{^H@YzIOnuUhePkgve^xM6G2JYgyE~C1TwYHimyZ zYptH4tG@;x4_Lk=K13$^Knh(x^)8#R`5e1$;yT#?o~Jn_dZCWT3s6w-ChF?3Edc

c9x+6cWiXbVEf%iwSW#^d)g7%bRy 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 K_6q_4R_uSNH>5WJ diff --git a/utils/stereo_ranger/__pycache__/sampling.cpython-312.pyc b/utils/stereo_ranger/__pycache__/sampling.cpython-312.pyc deleted file mode 100644 index 1e465a3b00e05a565c00a1201a332ecb927ac4d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4239 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 IW=UuN1Lusd!T*~sYOe3A(E$;5=0 zOq|$GNNn=5olSyS2zW2qU>*rCW-w+HyT6o5^FgnMs+-o#)B7?4lb=p$?RgQOvsq}JZN8X#oTBx|DB5sdoGQPMHZnzY4*YW& zDYKvmiW}s#>6p}3(IU2*gZ~8x>H`eR__jH`jSOq~w*55xEQzkp@wYFI54DfK*#$sy z;AHYwFHc;4_q*%iiL=L&1J^CI&*@*vH{+|4r`nV6T$;FiY5eMqB_8^_>m7e=e@U%Q zyz)k(XDInj|Jc>j$&Wt-1Ye$A36yt$1mC56;V4?cko}XI)PF%Xe&k!*3GqEhPB;UL; z_W3KxtKA5{(~*25oNPaf@U;(;pS+*K`GEx{2Yn~o+Y_B{Cc1hOBS*=cC%$++as0!v zk*oZe&?M1)Y~tiE{@8vbIWU;~_~(i4R}-)HkDu;OyxR@58UN(k*vQM{Zym!G{OaZ8 z;EC_9zdbhcO5(^{{M05cok)bwe|No;FHF4sMdE0edh+DS$+u4|9Y6o_*ypeFh++8g ztCuD&4vt^!MamsNN(i=W*^=cv&C=vdd~j^M`$V#LV6yL8V(>kjTJL~m^HvxcXgW61 zp1Anl*cUIWal|ir6IVWi^-T;P9X~x}2{zM?0JG4FYn?60#hfKzU54!C?? zhnrK@GtUPZualWX90rIv_p`o0z|C+vKiMz* zde}y!A4T0==i~uC)~7Nl0mD!rSSXvA7TF|p87+aWD8sEtO97G-NJficW6F=oYzjt6 z%NSXMm{uHEEbTRx(K+NDelyoVS7lVMY2wcNUm*CG}Idh4Y#F-)7!I!!~9`K9`Oq{$hm}Xbp zKGz3eW2R&Vkq5ZH$fg^=_ImQYcYvkqI62|WMo!^LF_fHT0zua6l%%MM3BvflHw;lP z6%rj0heTdgASF>LN+5lRvZu>JVrV6XRx;>B!i-jF>;aWf8={(0l2|CgRFK+$IgKZv z6I#r`D4bJpIzd~d(4vL1itAWJY*n$?hEu@i!)?iGF{iYr7S85kR&Mv>CSVIOBeTB% z%*<(F%L2RGU0#~O9Vmqz)WDWP3$_d*Kd!}MvHW$z#z*!%#rk0Dcs4o$jy+Bv&Dd$y z=VcxUF#f=vV8G?}@0qnDX6KePx4?X099O_@Z_S%|)+`Z<%W=dF5VceB`Bjnm%XUe2cSo*cH z=pLYu8H8 zOaFEf$S{NutpLnLh$d{25d@ADA@QUcm5}|P`w-quV+Elg3D!ZUyb5|3`hbE5> zP2A|780-WcVf@r*$@U{~_?bX!?ksAOvIn5KA3pzO(bV^HF zK~Oox_VfpZ(X+H`lRn<`gL(NlFF!4l}0VPDKN*CuY9PQ3kI@|_E)pj{kJeEvov+&eKiL`)`77D$*JyqdV$1&Una zcsSYF0R|LmXoDyC^6_hD5+|?n#uSeN2HyOJCkpG%9n$bPrmbubk*3kBZH9cFB#e)ae zCXB`Dd7a7Q@{@x%2-270EkvLQn1NvBfwje-Sg@`qAFcE+wrqo#pLFu&*OI3{#820aW5Mj0molZcZ$nwuFf&6(}b!7=IbHrjn&cZ;1R_70m5c)=R1*|eQA zf?A0-7g%$AB`4dq`SIHA{3#1)XsIi?Q9;U?(Rl$uYE@KVTL7eEgh@Xt11uAh5dyfc2w*7QS3M zCE6S(=MCTtg^n*=4&D4}kecn(l$OfP?=9^q4a?&iL)W7nkM`B{*PW@mrKyfjvO2{ZVJnO z|H!)0g2%2@4p;O^`weFdL*{7V10$7TZKU9_->-aRv|#lwcMNUslg0`cjutK)-hmaX z?^afY6?e+3hBUt`U(rz)uIXMCH_eNg7DP=825Lr4)!~hCgDD(}8w+sNeSz4#A4TW= zXr$m~#cv+?^#hToci!H;C$`%W-R&5i_uOrxGj1%5YfW8s9d$sjn6WZytc;sW;%3W$ zIW~V;6n^H1;^sp5^=|3e5;qse&GYW*6gjznmZ*TB8fxCkAIhnD3x~A7D_@zKkip!i zjT$PZQTMHgm8^)CtQg)oLf`OQ@qG2n?H#*fJN89)>>Dkyf35x5l!BV~@RXWT>3G$y zEUGN)J8)ZB9XI5M^?&)(YN~pd=r3IL?!Wv=P8F^PuwbFxwR{mU#`imeMI?B z51VV(N&nes+ANi_sLQfjG1`pLN{lu_m*ZuB-rZ%VUr^tR{v?;S|e{D0& z(2yu3hO=MLJ=B5p=x|!9gzLaI4V;{5GZN;M3T!T%FQ}hp1k@pgzHpkTo6yL}2>$Xp z`buai^vI$+39}SY-gVFdQ8uC3mk~nR?CTZ2N}CRJ60~_T0=sYy3d(5( z^vI%j00pyX7{WT{`uc1U^h0wuBV?a-EAW_CL@V3$A^nlUz#^d&QZIWC;OP`4Nk2mX z?K2YQXs@5K3ydLS(-NWmbb3g{>QFbzMmH_hmJ`4ejD(P+2@kT2Am~?ZxqxFuMkQof zRVX*`uu$F#8b4{7?JJ3C71ZUhRdaC3j|42!=^R|5PjiIH`}bg5J&jp_LTR@GxFM8n zCit5}h9j1csa+c~G_4WZO{dT*JQXmajG8_Q9=LwUwjm(5BCm%X9PSTpf)a=z5o$w-Y3QU1qrC9oFR3S-r#V% zpK~}51i^HnCcd~be!Xwv?1{wXixZ!n8vjKv+@6B=4z3PR#>YlJPTWAx0>FujpF^8O z*QJT0pC>P0hU-&my+P7L51Kg@rT0^_w3Y z7T|qo5wv3-^^Nz&uYGK(q%CNr@YB5fNusN7;zX@WK%;&iDm=W)^C2jO%q(*T}0?D(8-R+a&ugqmD95_b!y4Y)jT`wA`) zxMZ^4&sz}^=)|Jd%fgPMIGw-`_Mo4kS(Hbdvf1Z%1zZOiPD(~pxx9AT=W)2aoN;<) z9w>$B%q(#nf)ZZq2hE%KbJ!h-(P;PYcd($Ar>UTs@ntxLgFc9q;1q%dBlibb#&KZw z2W$$$N?=G+gU<=Bq30ZM8}0HkP-OS8cwfZgRT8@sBUE2FDe*NS69^tC{>v~lS8U&~ zwQkeqjWu-}YU~?p9^bZpV~u^&`VHH+ZsQE;FVo<1gBOQWr=~?rBNhh3ixk#`Lnt?F z-SX6vHQQ_0U1Y@Pki2-3vyVXprduV#*|a}m`=FfS4ImkG1jB?RET>A-8NMj*0)x-b zNe(bA9MHxQ2!c)13JmiCrvy5~{W>_Qlr-aX`2pfdUQ3JM1HmC? zG;c*%eOF_SY3{qFxi4iKMM@utEIoKz`$F7M7&FYjWtg9?SQc6Ci#*>Nd;Z1f^Dq9H zqIB!U@topVPQ|U9inL`_88_C)jYVi&#EtneV^!2xl?Hy$$iXHsQgG#dpyNPHKR>Dm z^f_o<+|icy?Eni!3r1RgTw8EYB{QhQ65g0A=_!fjER5zXjO9ES&3SOxH0-+U{iSy_ zrv`1joUXN})@Do~pEvDb0@YDtHE-PMsmDZ{MX_~H-&*%{x16sT+I?x?xqY{ct0Oy~ z1$xzPiPY{sy}Brlld>p3f-E=(?_sl27BghkyhUJ=!<2>(==a8YUar~jcbqUH2N z&l558(x`c9%)BaUUNs^a*)eL~&{1<2?A0eao``9yqS~sM_Mxcuq2cAXwX2{{xMoUD z6_tf`ag8aWnHOKYY-megZU2*RJvnmdM(9fDt6*g3p5L}b*X<2&88Z~e^UC6R#l8D_ z_T4F55G!09EnFPWEr?ewCKIn(-@T)E-)sAB27hz-*N5ZtD&oae@uK_s%g>a@iy#&7y-Lkrqsw@Yt^`D$Go_~kYf=p@aBbtY?cr>D`eSjkD)Y+?E|G*?3)H0Ig3>TT8}KN zl_I$yKA*Z32kk);+y>KwdpI~b`IiZT>VPjNEaMk0!0U()EaWW(zM@FqaKTwdjwH*Y z7LfgZ2J&#`!lMmvnt-CP$*Sh9Qxx3nGZ+zpisFskgVA1$_F;q)hW!~t(8qx=k&C1w z5suoQ)U4mOx$bfM*19Km^8!-rJj98eE$j=}9Q#}4yoh`W3sz!81mjVFKnkYsvG8;7 zB8wMRs3f+|pDnDiTvLreGe1rZ?rDq^aOCEm%^;9c3|bz`tBU4T5f*(gTKFKcXkMx? zUQ*G&=FFO*qD!UcN+ZP&$BP&AFFCU$UR)M0u*3=$MGF=oU*>6$FEs*R%J~~ACy2;& zK>j2=c?f`B@SC)O#|7?=VV!LfupQD3rvb3E#o63tA&FpBz z+zN{czu@V3qxwi5z~G3Vyn*`PjG=kuEphoD z`MH5E2<^ewEN?(>FvG3|JVY-6w;ef5JD`A*9$=aSEHaHBEk9%po-tNIuC;hJW6Zo9 z{a5UQ$|-p6?yCq(|2?lHToX5#yF#bnmdcFhS6|K7#{2M^%+O;P&_$LueQi82rK0jn z;YP@44r_Q3q;!Hh(CA}Z-~j>S_dDR#iWJ_G_-8#IK(@s@mU|Zz5V@U)nEf z5lufN$mT@@51zOc@PpS4;*h9O9Fn{bPXV%VR(5ZjFjkM~WBIF;h-wkRJ^yw zVq_%Q`(fe+mECM!qKp8fd<)dYNv960jJRasLC}6*&`pzTZcgs@HNwL)PJ!j_tQV#JlEOJ6dCg%BZ1oVE=8y(zrJF zj#A&H@6dO@Fi;RJSQuIKShV1=n+qdK?^oL)iq!9n)$fhg?~T8!KSeu8n5_tE-qPXFO@ly!ly&{W85Q{n6u^8dszgt96$Ee>+o4%LIWaWK_hP2UnE2k)c zZyt&~wKMwoE|Q7R`|n}ll%A67r^F&zX`l8_6eRZ~LILO$d{tRr0alfztIF=lrLywx zwM%6MeQT#ENQRe?e6 zoGA@LTI{AnXt7de9;h99Hd^uHDGJ~b`sV8B+O78xo-#{iKGDGPDGJkJ6Q7Pe!>3E3(S~T6|z5Ag#<^n_Xu*0JnE6-crJN znV}x#g(QgQ3-9Lwm1N@cp~)lgJo1x?Bd2*+*VxGE)DuharV$?@h)3_Gh_Kk(2oZ|ChY()Tk{!<}*YKigEqe~iV8c`Xe}HI8A`*$d z7b`{5duB>x_=Z{(r4}V9%Qw{CQEKlu)T$`8>Kkf(lv@7>s`4A^iLa?A{y8Xsl8c$Wee;K Jk&#U9zX3FS=wko? diff --git a/utils/stereo_ranger/__pycache__/target_store.cpython-312.pyc b/utils/stereo_ranger/__pycache__/target_store.cpython-312.pyc deleted file mode 100644 index 37b17a2bf8b3f3eae2f3b50e0b2430b20985aaea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6325 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 I77^Zm0kr+II{*Lx diff --git a/utils/stereo_ranger/__pycache__/warn_throttle.cpython-312.pyc b/utils/stereo_ranger/__pycache__/warn_throttle.cpython-312.pyc deleted file mode 100644 index 52747a404ba0e21b3cb3ef1efc2c51dedf5afc3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2032 zcmbVNZ)_7~7=PdEwL99w)-lXPh6fn+6edMSln|p~#y=mXiTEdF$))y=QRsU3-c{{V zlQAn<2ZJtv9kAd=WUJZcW)!V%x`eNOStW*f@q>}wwM|SmoW{gYKJQ((48j+Evpw(g z?|q)<{XO@*{bB7|FM{>Ebm`Dm7NHwdSY9C8Yw2Owa!5uYBx9M0;dUm(V8o#3k?cw% znH^;u$B--O7Ci|+M#${zDUT1Ad%CPEN6mBjh3W3ABTqbbb);!wIsQOm1QNf|cpC1~N`kM_Ily zX$_23I?q>*o`xjzg)?xZoc+!m$U8w+f_qEn)8?@+O5-2R7kcd51cQx_3uf)bytO@mw?G31%)sG z&B!?9lD#scu(C^L53sU3?GCwRkK&PCN)5<0X*R@FWoqX@UL*VDHL!b=tjHVu8(|XP zcaW&M9#awzKzQp!_4a^$>G)V_^tglOeBor}To$l|QVqBAjzW@#hbVf2#9JcO-r8ll zfYSe^XMp5T3d{H~*ojYvmOD#kMyq@mPT>@iRxD8j(n_=>5?w77YE0Auv6Bks%!ies zDdD%wWg#fK4DajVSVDP@5S17n$*HSGil~XWu8EAnYD(+?@xW<~h82W=_BQWseTArP zB;4K{*2Aq4RaPXKsBvYxu4sB|LXXC@R!vukqDmwjZ&OI?yMU6US7B;Ak~BC;ipHb5 zBz3G`jbP)ldM!=36-XzV^98cOo~M4P-8kD2yjHt&mftyd&wbtO3E$j9VmI5%FM%5} zxJX!6+EkLf1xz|#Hkesp08Dg(DEPe!%}MMBlXWs*p1Dw-xV$p0&KF~T^Hq7~jCtWAj1SDI z)jwfohs|%#EYEBA9cJ2TF?c&OL1@p&8X%&J@UR)KHbo~sM?pa*cT=s~={Lne5Cn1; z)%mo8Y9b~}N_$lAsK2F4t1RolReJ$QC;Bx|m*e~R>j9w{5C-e>!E1r-leicNcDv^` zKiJKl;xl~q%_6sH&R^G$GaWzroBCfH+&8j+Xn(O`$KFmU)xx2jF(phy zY?rRlOioWpJ+QYIO;LxD;f6mD5c}xA*0*iZ diff --git a/utils/stereo_yolo/__pycache__/__init__.cpython-312.pyc b/utils/stereo_yolo/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 5e4c54ec95fde7ba1ed2775c49706c3e709bfd04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 913 zcmY*X&rj1}82-B7jG(ADFZSq2gp)Bb#ET0jBPPZJ&5~xWg^aW-Y0H7z#t#`XwizJE z2Frl(LqLW>VVMIGuO|KhO}Ie6uia@X2NUDLw;cxj(!Bk?_IaQ8d7t znkdwVIL|W0_=mg!-{3b2Aw}h7USfwNL84FNGw{$jlQQxeZyVK1zz!dNs#M5v8L6v~YkY#c{Lr3MaQ}3vu#c*erb{Z`17}SFcW-#fSZVys&`} z)^KJIusDB&M>2Z(wK2JEY)zWSS#v4%U%~l}dNpMpP2=%OTbr@HhOypDF53=gjyk4Hd zi5zrj9xR^46PE(V_RPeLUQUBL{cxHTm&p4StBuoKk;Y>iwUG^+878y4dbxt9iu&Oc zj$5{X33w+5d3a&Bwzyo|Oqrzw-bs;U7Ch8HkJaY0II~L%`yDu6ATvh|1oUbtIhrHs zRja9;LT#nsC+T&Z+y|U_V0nMlSNhBP1KOx88vMaoJuubB zQmi-Hr(F}IFvs@FlF0TeQY=7ew_XUU92SPuXhdmvBgydj=4EP^EX7nl#=+MnWk86k ztnyL`xMe34kv&#$Tft=oFNCLoX3fj8a5q?1^F-{z4X_2#e4IT2Nw(88ABeH(Z7R(P z+%}6v5f%iNeTU8=4gKy6J`6pRr9K#2Fsz0{5sBkj4wCtMDzB)aK{YBUp(__T#In&C zsAby+hBRNNC~<=V|46<8WR^e59SGl@6h-}TcqrGeRz%%DL4gx=_XOSk>GCu?5?=W_ GVEzI{AEO5V diff --git a/utils/stereo_yolo/__pycache__/config.cpython-312.pyc b/utils/stereo_yolo/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 147678c9004bbebd7741384aa7f09cbea43736cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6219 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 a@dgnhXApjdi>2p~TzY;@J`=~q?fyRrl?bW; diff --git a/utils/stereo_yolo/__pycache__/detector.cpython-312.pyc b/utils/stereo_yolo/__pycache__/detector.cpython-312.pyc deleted file mode 100644 index b85d3cce5b59608d4703fceae4d24f212d8ddeaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4534 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 JziFTZ{vR?8gCYO` diff --git a/utils/stereo_yolo/__pycache__/warn_throttle.cpython-312.pyc b/utils/stereo_yolo/__pycache__/warn_throttle.cpython-312.pyc deleted file mode 100644 index 681ae272d36aefca154a556b24a7f4d577d593e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1767 zcmbVMT}&KR6u$Gb%d#x)mQsVMl%X1RwIJIBj7`&6#pn|;vHmS2lXYiGmo7W??yS0- zHQADFW%(yUpg$T=ezt+SkWfnnL(*4YSa~Q;d@z=ooqd9e#>6MjommJpJb9Bj_x#Pd z=YHp$`*Ufj9l`o&c)INtgU~}F>@2pxY`*}^2ojJN36wwwsdm~+Q;0@yA%RIEft{gE zPcIX-xUG>@kkr90{q~gJ)uG+FtX@lRE_dc{CbanuJ+q*V_Hp;8>p68|USF70Cz82= ztDDQI&E@M_raw2oq%L=;i;Kr|v$LFs(`LuHmpCwJOXHlUu8t^jt_h| zbnXI{WH~5Co`tX^YXcR4C)Yijo9WiSAJ>NG;NJOb1Nmepg1K^7r~B8WC|3<66l!%-$BKxIO2CL0R+-+v?y{jO?hIBlmRgg z<v)Tj) zj^pqXL{}2NAVq=#FSZBdSjE07?Xi%q3cn2`fqr+Dk2nS$4_sW<#f?>@J!`I-IV$V& zbXwM{pX+4%9Vy4ahgsXPb!Yh~m5Tl1tRH=E?A&zocyqS0ZtiThvVPT8k6jS#;2NF+ z%m1w*piM`PL4dtD!@HAP$Plyv-eK}a8GyNI11o$O+=1VPsnGuhJh&W;v9f)z7$8=F zKKv9A0K|E?Kc0#|a8_lVRb#ev^_ugAH z^Pfa~!GfiT=LN|RY98UU@Z+}ZxB_3scP7A(3TqF1>2baFS41TNx&vIi;u#tSmo%Y zK&TayzA=u%RfHoIG<(Q)+Pu&iTn#$%3>naDF%(5Tq8W<)+m5J;f2@b7nm-W`lqgJ^OxEqRtnBPI+o@?ip^*R) zq6dWtA^u1sY6u>T256N(z>^1JvKJC*yX91sgNY|+T0CH4oNPXM@6GIc-}mx9`FtM0 z&fxOm4Tl|oJ?{KDwnO7UWyT^%002z)TLv7=AZF{;}EL0x9te0jY z;c(EJoe3H^i4CmDxLXc~PydzATDlt?%F}UO3tFqiYGJ-!n%$ec;sj>f1+^ zxv6?-Zh!f`wYFB?n5`~s*0vwip3Rj%uGHQwlsE5H3JWZYwe_O9@}yo`Di^1kS3xaH z(s}l8d1L-Ca#P7pvFYZ_O3`c1KKu&){6Mk6we zVv!5|J*NAjo{Nk|#;#p5+b&Bgku(=aVd>W-$-Mg zBh7I#w`oLlEvckTZ^IqNnXF1#4v{4s9UC1pU5&Vksp*w9yS!jT7wkZ(tlqu>b%7 diff --git a/utils/target_tracker/__pycache__/kalman_box_filter.cpython-312.pyc b/utils/target_tracker/__pycache__/kalman_box_filter.cpython-312.pyc deleted file mode 100644 index f01aa7502de9204a981bd2cb2cc28a9ee449dd9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6297 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 Vq#If@gVcA+e?%93O93FN_%|@PqKW_j diff --git a/utils/task_runner/__init__.py b/utils/task_runner/__init__.py index 9b9897e..067ab07 100644 --- a/utils/task_runner/__init__.py +++ b/utils/task_runner/__init__.py @@ -1,9 +1,20 @@ """task_runner —— 任务编排器(预定流程.md 的天空侧任务级状态机)。 把已通的 L1(航向伺服 TrackingControlLaw)与 L2(地理引导 GuidanceLaw)按任务阶段 -编排成一条可运行的全流程:WAIT → ARM_TAKEOFF → GOTO → TRACK → APPROACH → RTL。 +编排成一条可运行的全流程: + + WAIT → ARM_TAKEOFF → GOTO → SEARCH → TRACK → RTL + + * GOTO :L2 地理引导飞向**参考点**(地面给的坐标是搜索起点,不是终点); + * SEARCH :到达参考点后原地偏航扫视,靠吊舱/视觉找目标;超时未发现 → RTL; + * TRACK :L1 角度直入 + 无距离降级(slow_approach 缓慢靠近);超时 → RTL; + * RTL :自定义返航(回起飞点 → 恢复初始航向 → 垂直降落),不爬高; + 异常/未起飞等情形兜底退回 PX4 原生 RTL。 模块划分: * state_machine.py 纯逻辑任务状态机(无 IO,可单测) - * runner.py 编排器(MAVSDK 飞行编排 + 状态机驱动 + PodBridge 闭环,A3) + * runner.py 编排器(MAVSDK 飞行编排 + 状态机驱动 + PodBridge 闭环) + * return_home.py 自定义返航纯逻辑(HomingController:转场/航向/高度) + * pod_control.py 真机吊舱识别/锁定指令下发(0x07/0x0F/0x0E/0x08) + * log.py 结构化任务日志(事件 JSONL + 遥测 CSV + 简报) """ diff --git a/utils/task_runner/log.py b/utils/task_runner/log.py index ef1341c..3e35552 100644 --- a/utils/task_runner/log.py +++ b/utils/task_runner/log.py @@ -6,15 +6,21 @@ * 事件日志(JSONL):状态机转移、飞行编排事件(解锁/起飞/返航)、异常/紧急。 低频、带语义、结构化,便于 grep / jq 定位「什么时候发生了什么」。 * 周期遥测日志(CSV):每控制周期的位姿 / GPS / 目标 / 控制输出,便于回放与画图。 + * 简洁任务简报(task_brief.log,纯文本):一行一事件(阶段切换/地面站命令/ + 飞控告警)+ 运行结束汇总(时长/最高/最近距目标/状态链),供人工阅读与归档; + 机器日志(JSONL/CSV)格式不变。 时间戳:单调毫秒(now_ms,与运行时代码同源)+ 相对时间 t_s(自首个 now_ms 起)。 -文件 IO 集中在 open / close,热路径只追加一行,不做 flush 决策。 +文件 IO 集中在 open / close;热路径只追加(并立即 flush 落盘——缓冲期间文件不可见, +地面站页面运行中需要读到最新状态与遥测,2026-10-14 实测)。 """ import csv import json +import math import time from dataclasses import dataclass +from datetime import datetime from pathlib import Path from typing import Optional @@ -60,13 +66,26 @@ class TaskLogConfig: dir: str = "logs" # 输出目录(相对工作目录) event_file: str = "task_events.jsonl" # 事件日志 telemetry_file: str = "task_telemetry.csv" # 周期遥测日志 + brief_file: str = "task_brief.log" # 人类可读任务简报(追加) telemetry_every_n: int = 1 # 每 N 周期记一次遥测(1 = 每周期) + telemetry_flush_ms: int = 1000 # 遥测落盘间隔(毫秒);0 = 每行立即 flush def _monotonic_ms() -> int: return int(time.monotonic() * 1000) +_STATE_CN = {"wait": "待命", "arm_takeoff": "起飞", "goto": "坐标引导", + "search": "搜索", "track": "追踪靠近", "approach": "接近悬停", + "rtl": "返航"} + + +def _fmt_t_s(t_s: float) -> str: + """相对时间 → mm:ss.s(简报用)。""" + m, s = divmod(max(0.0, float(t_s or 0.0)), 60.0) + return f"{int(m):02d}:{s:04.1f}" + + class TaskLogger: """结构化任务日志。用法: @@ -86,12 +105,20 @@ class TaskLogger: self._event_fp = None self._tel_fp = None self._tel_writer = None + self._brief_fp = None self._t0_ms: Optional[int] = None self._tick = 0 + self._last_tel_flush_ms: Optional[int] = None # 遥测上次落盘时刻 + # 简报汇总统计(随遥测行/事件更新,close 时写一行汇总) + self._n_events = 0 + self._last_t_s = 0.0 + self._max_rel_alt_m: Optional[float] = None + self._min_dist_m: Optional[float] = None + self._state_chain: list = [] # --- 生命周期 ----------------------------------------------------------- def open(self) -> None: - """创建输出目录并打开两个日志文件;disabled 时跳过。""" + """创建输出目录并打开日志文件;disabled 时跳过。""" if not self._cfg.enabled: return out_dir = Path(self._cfg.dir) @@ -103,11 +130,22 @@ class TaskLogger: self._tel_writer = csv.writer(self._tel_fp) if (out_dir / self._cfg.telemetry_file).stat().st_size == 0: self._tel_writer.writerow(_TELEMETRY_FIELDS) + self._brief_fp = (out_dir / self._cfg.brief_file).open( + "a", encoding="utf-8") + self._brief_fp.write( + f"===== 任务开始 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} =====\n") + self._last_tel_flush_ms = None self._event_fp.flush() self._tel_fp.flush() + self._brief_fp.flush() def close(self) -> None: - """flush 并关闭两个日志文件。""" + """写简报汇总并关闭全部日志文件。""" + if self._brief_fp is not None: + self._brief_fp.write(self._brief_summary() + "\n\n") + self._brief_fp.flush() + self._brief_fp.close() + self._brief_fp = None if self._event_fp is not None: self._event_fp.flush() self._event_fp.close() @@ -120,16 +158,32 @@ class TaskLogger: # --- 记录 --------------------------------------------------------------- def log_event(self, now_ms: int, event_type: str, **fields) -> None: - """追加一条 JSONL 事件。""" + """追加一条 JSONL 事件并**立即落盘**(事件低频且语义关键:地面站要实时看阶段)。 + + 注意:遥测行不在这里 flush——见 log_telemetry 的节流落盘说明。 + """ if self._event_fp is None: return record = {"type": event_type, "ts_ms": now_ms, "t_s": self._rel_s(now_ms)} record.update(fields) self._event_fp.write(json.dumps(record, ensure_ascii=False) + "\n") + self._event_fp.flush() + self._n_events += 1 + if self._brief_fp is not None: + self._brief_fp.write(self._brief_text(record) + "\n") + self._brief_fp.flush() def log_telemetry(self, now_ms: int, rec: TelemetryRecord) -> None: - """追加一行遥测(按 telemetry_every_n 节流)。""" + """追加一行遥测(按 telemetry_every_n 节流)。 + + 落盘策略:**写入靠 Python 缓冲、按 telemetry_flush_ms 定期 flush**, + 而不是每行 fsync 式 flush。原因:本方法在主控制循环里以 20Hz 调用, + 每行都 flush 等于每秒 20 次同步磁盘 IO,在树莓派 SD 卡上会阻塞事件 + 循环(实测 MAVSDK 回调队列因此积压告警);事件行仍即时落盘,故页面 + 的阶段显示不受影响,只有图表数据最多滞后 telemetry_flush_ms。 + close() 会做最后一次 flush,进程正常退出不丢数据。 + """ if self._tel_writer is None: return self._tick += 1 @@ -137,8 +191,75 @@ class TaskLogger: return rec.t_s = self._rel_s(now_ms) self._tel_writer.writerow([getattr(rec, f) for f in _TELEMETRY_FIELDS]) + self._flush_telemetry(now_ms) + # 简报汇总统计(只随真正落盘的行更新) + self._last_t_s = rec.t_s + if math.isfinite(rec.relative_altitude_m): + if (self._max_rel_alt_m is None + or rec.relative_altitude_m > self._max_rel_alt_m): + self._max_rel_alt_m = rec.relative_altitude_m + dist = rec.distance_to_target_m + if math.isfinite(dist) and dist > 0: + if self._min_dist_m is None or dist < self._min_dist_m: + self._min_dist_m = dist + if rec.state and (not self._state_chain + or self._state_chain[-1] != rec.state): + if len(self._state_chain) < 32: + self._state_chain.append(rec.state) + + # --- 简报(人类可读,一行一事件)---------------------------------------- + @staticmethod + def _brief_text(record: dict) -> str: + t = _fmt_t_s(record.get("t_s", 0.0)) + etype = record.get("type", "?") + if etype == "state": + to = _STATE_CN.get(record.get("to_state"), + record.get("to_state", "?")) + if record.get("from_state") == record.get("to_state"): + return f"[{t}] 任务启动(当前: {to})" + frm = _STATE_CN.get(record.get("from_state"), + record.get("from_state", "?")) + return f"[{t}] 阶段: {frm} → {to}({record.get('reason', '')})" + if etype == "command": + return f"[{t}] 地面站命令: {record.get('name', '')}" + if etype == "fc_status": + return (f"[{t}] 飞控 {record.get('severity', '')}: " + f"{str(record.get('text', '')).strip()}") + if etype == "takeoff": + return f"[{t}] 起飞至 {record.get('altitude_m', '?')} m" + if etype == "takeoff_failed": + return f"[{t}] 起飞失败: {record.get('error', '')}" + if etype == "rtl": + return f"[{t}] 开始返航" + fields = [f"{k}={v}" for k, v in record.items() + if k not in ("type", "ts_ms", "t_s")] + return f"[{t}] {etype}: {', '.join(fields)}" + + def _brief_summary(self) -> str: + parts = [f"时长 {self._last_t_s:.1f}s"] + if self._max_rel_alt_m is not None: + parts.append(f"最高 {self._max_rel_alt_m:.1f}m") + if self._min_dist_m is not None: + parts.append(f"最近距目标 {self._min_dist_m:.1f}m") + parts.append(f"事件 {self._n_events} 条") + if self._state_chain: + parts.append("状态链 " + "→".join( + _STATE_CN.get(s, s) for s in self._state_chain)) + return "----- 本次运行汇总: " + " · ".join(parts) + " -----" # --- 内部 --------------------------------------------------------------- + def _flush_telemetry(self, now_ms: int) -> None: + """按 telemetry_flush_ms 节流落盘遥测(0 = 每行立即落盘)。""" + interval = self._cfg.telemetry_flush_ms + if interval <= 0: + self._tel_fp.flush() + self._last_tel_flush_ms = now_ms + return + if (self._last_tel_flush_ms is None + or now_ms - self._last_tel_flush_ms >= interval): + self._tel_fp.flush() + self._last_tel_flush_ms = now_ms + def _rel_s(self, now_ms: int) -> float: if self._t0_ms is None: self._t0_ms = now_ms diff --git a/utils/task_runner/pod_control.py b/utils/task_runner/pod_control.py index d161d88..ecff9d2 100644 --- a/utils/task_runner/pod_control.py +++ b/utils/task_runner/pod_control.py @@ -8,7 +8,8 @@ * 0x08 目标识别关(真正退出识别) 本模块把这些指令序列封装成 PodController,供 TaskRunner 在 GOTO / RTL 阶段调用。 -SITL 用 VirtualPod(直接合成锁定态),不注入 PodController。 +另含地面站手动命令(stop_track / auto_lock / center),与 pod_console.py 按键 +u / a / c 语义对齐。SITL 用 VirtualPod(直接合成锁定态),不注入 PodController。 """ from pod_bridge.protocol import ControlCode, ControlCommand @@ -37,3 +38,18 @@ class PodController: """停止跟踪 + 关闭目标识别(RTL 前调用)。""" self._bridge.send_control(ControlCommand(code=ControlCode.STOP_TRACK)) self._bridge.send_control(ControlCommand(code=ControlCode.TARGET_DETECT_OFF)) + + def stop_track(self) -> None: + """停止跟踪(只停当前这一次,0x0E;0x0F 常驻自动锁定仍生效)。""" + self._bridge.send_control(ControlCommand(code=ControlCode.STOP_TRACK)) + + def auto_lock(self) -> None: + """发现目标自动锁定 + 识别转跟踪(0x0F + 0x10)。""" + self._bridge.send_control(ControlCommand( + code=ControlCode.AUTO_LOCK_ON_FOUND)) + self._bridge.send_control(ControlCommand( + code=ControlCode.DETECT_TO_TRACK)) + + def center(self) -> None: + """手动归中(0x2B)。""" + self._bridge.send_control(ControlCommand(code=ControlCode.CENTER)) diff --git a/utils/task_runner/return_home.py b/utils/task_runner/return_home.py new file mode 100644 index 0000000..3e1deca --- /dev/null +++ b/utils/task_runner/return_home.py @@ -0,0 +1,130 @@ +"""return_home.py —— 自定义返航(回起飞点 + 恢复初始航向 + 就地降落)的纯逻辑控制。 + +背景:PX4 自带的 RTL 会先爬到 `RTL_RETURN_ALT`(默认数十米)再飞回、再下降到 +`RTL_DESCEND_ALT` 后落地——即"飞很高再降落"。本模块给出替代方案: + + RETURN 阶段:保持在**转场高度**(取进入返航时的高度与起飞高度的较大者, + 绝不比任务高度更高)飞向起飞点,机头朝目标方向; + YAW 阶段 :到位后原地把机头转回**起飞时刻的航向**; + LAND 阶段 :交给飞控 `land()` 垂直降落(不爬升)。 + +设计约束(与项目铁律一致): + * 纯逻辑:输入为载机位置/航向/相对高度,输出速度与角速度意图,不碰 IO, + 可确定性单测;实际下发由 runner 负责。 + * NED 语义:vz 下正。目标高度高于当前 → vz 为负(爬升)。 + * 只做"航位推算"级别的控制:无距离传感器时不需要它(返航点是已知识别坐标), + 因此这里用比例控制 + 限幅即可,不引入积分项(避免返航中积分饱和)。 +""" + +import math +from dataclasses import dataclass + +from pod_bridge.guidance import haversine_distance_m, initial_bearing_deg +from visual_track_control.rate_limiter import clamp + + +def wrap_deg_180(deg: float) -> float: + """角度回绕到 [-180, 180)。""" + d = math.fmod(deg + 180.0, 360.0) + if d < 0.0: + d += 360.0 + return d - 180.0 + + +@dataclass +class ReturnConfig: + """自定义返航参数(全外置;数值为占位,须按机体/场地标定)。""" + + enabled: bool = True # False → 退回 PX4 原生 RTL + speed_mps: float = 2.0 # 转场水平速度上限 + arrival_radius_m: float = 3.0 # 距起飞点该半径内视为"已回到起飞点" + timeout_s: float = 120.0 # 转场阶段超时(超时 → 退回 PX4 RTL 兜底) + yaw_kp: float = 1.0 # 航向误差 → 偏航角速度(dps/deg) + yaw_rate_limit_dps: float = 20.0 # 偏航角速度上限 + align_kp: float = 1.0 # 恢复初始航向阶段的比例增益 + align_rate_limit_dps: float = 15.0 + yaw_align_tolerance_deg: float = 5.0 # 航向对齐容差 + yaw_align_timeout_s: float = 15.0 # 航向对齐超时(超时也进入降落) + alt_kp: float = 0.5 # 高度误差 → vz(m/s per m) + alt_limit_mps: float = 0.5 # 垂直速度上限(与 L1 vertical 限速同量级) + + +@dataclass +class HomingOutput: + """单周期归航意图(机体系前向 + 垂直 NED + 偏航角速度)。""" + + vx_mps: float = 0.0 + vz_mps: float = 0.0 + yaw_rate_dps: float = 0.0 + distance_m: float = math.inf + bearing_err_deg: float = 0.0 + arrived: bool = False + + +class HomingController: + """把载机当前位置/航向/高度 → 飞回起飞点的速度与航向意图。 + + 用法:进入返航时用起飞点经纬度、起飞时刻航向、转场高度构造一次; + 之后每周期 `update(lat, lon, yaw_rad, rel_alt_m)`。 + """ + + def __init__(self, cfg: ReturnConfig, home_lat_deg: float, + home_lon_deg: float, initial_yaw_rad: float, + transit_alt_m: float) -> None: + self._cfg = cfg + self._home_lat = float(home_lat_deg) + self._home_lon = float(home_lon_deg) + self._initial_yaw_rad = float(initial_yaw_rad) + self._transit_alt_m = float(transit_alt_m) + + @property + def home(self) -> tuple[float, float]: + return self._home_lat, self._home_lon + + @property + def initial_yaw_rad(self) -> float: + return self._initial_yaw_rad + + def update(self, lat_deg: float, lon_deg: float, yaw_rad: float, + rel_alt_m: float) -> HomingOutput: + """转场阶段单周期意图:朝起飞点飞,同时保持转场高度。""" + cfg = self._cfg + dist = haversine_distance_m(lat_deg, lon_deg, + self._home_lat, self._home_lon) + bearing = initial_bearing_deg(lat_deg, lon_deg, + self._home_lat, self._home_lon) + err = wrap_deg_180(bearing - math.degrees(yaw_rad)) + + yaw_rate = clamp(cfg.yaw_kp * err, + -cfg.yaw_rate_limit_dps, cfg.yaw_rate_limit_dps) + arrived = dist <= cfg.arrival_radius_m + # 对准门控:没对准先转,避免斜着"蹭"过去(与 L2 引导同思路) + alignment = max(0.0, math.cos(math.radians(err))) + vx = 0.0 if arrived else cfg.speed_mps * alignment + return HomingOutput( + vx_mps=vx, + vz_mps=self.alt_hold_vz(rel_alt_m), + yaw_rate_dps=yaw_rate, + distance_m=dist, + bearing_err_deg=err, + arrived=arrived, + ) + + def alt_hold_vz(self, rel_alt_m: float) -> float: + """保持转场高度(NED 下正):低于目标 → 负值(爬升),高于 → 正值(下降)。""" + cfg = self._cfg + return clamp(cfg.alt_kp * (rel_alt_m - self._transit_alt_m), + -cfg.alt_limit_mps, cfg.alt_limit_mps) + + def yaw_align(self, yaw_rad: float) -> tuple[float, float]: + """恢复初始航向:返回 (航向误差 deg, 偏航角速度 dps)。""" + cfg = self._cfg + err = wrap_deg_180(math.degrees(self._initial_yaw_rad) + - math.degrees(yaw_rad)) + rate = clamp(cfg.align_kp * err, + -cfg.align_rate_limit_dps, cfg.align_rate_limit_dps) + return err, rate + + def yaw_aligned(self, yaw_rad: float) -> bool: + err, _ = self.yaw_align(yaw_rad) + return abs(err) <= self._cfg.yaw_align_tolerance_deg diff --git a/utils/task_runner/runner.py b/utils/task_runner/runner.py index a5b997c..6d093aa 100644 --- a/utils/task_runner/runner.py +++ b/utils/task_runner/runner.py @@ -7,6 +7,8 @@ L2(地理引导 GuidanceLaw)按任务阶段编排,配合地面坐标注入 职责边界: * 状态机(TaskStateMachine)只做「状态 + 转移判据」决策; * 本编排器负责「采集快照 → 喂状态机 → 按状态执行动作」; + * 地面站命令(RTL/紧急/吊舱手动指令,经 GroundLink 队列)主循环逐拍分派; + * 飞控状态文本(STATUSTEXT,警告/异常)逐拍落任务日志(fc_status 事件); * 闭环控制复用 PodBridge(L1/L2 运行时切换走 set_heading_servo_enabled); * 飞行编排(解锁/起飞/返航)通过 duck-typed `drone`(MAVSDK System)完成, 便于 mock 单测;真实 SITL 装配见仓库根 task_runner_sitl.py。 @@ -23,13 +25,38 @@ from typing import Callable, Optional from pod_bridge.guidance import haversine_distance_m from visual_track_control.staleness_checker import is_stale +from visual_track_control.types import ControlMode, ControlOutput from .log import TaskLogger, TelemetryRecord +from .return_home import HomingController, ReturnConfig from .state_machine import (TaskInputs, TaskState, TaskStateConfig, TaskStateMachine) logger = logging.getLogger(__name__) +# 自定义返航的阶段 +_RTL_RETURN = "return" # 转场:飞回起飞点,保持转场高度 +_RTL_YAW = "yaw" # 到位:把机头转回初始航向 +_RTL_LAND = "land" # 降落:交给飞控垂直降落 +_RTL_FALLBACK = "fallback" # 兜底:退回 PX4 原生 RTL(会爬升,仅异常时使用) +_RTL_IDLE = "idle" + +# 地面站吊舱命令 → PodController 方法(与 ground_station/app.py 白名单对齐) +_POD_COMMAND_METHODS = { + "pod_detect_on": "arm_detection", + "pod_auto_lock": "auto_lock", + "pod_stop_track": "stop_track", + "pod_detect_off": "stop_tracking", + "pod_center": "center", +} + +# 搜索扫视换向周期计数中「向右」的半周期序号(0 → 右,1 → 左,交替) +_SWEEP_RIGHT, _SWEEP_LEFT = 0, 1 + +# RTL 落地判定:单次等待 in_air 遥测的上限(秒)。MAVSDK 遥测流挂起时 +# `async for` 会永久阻塞并卡死主循环,故必须有兜底(超时 → 下一拍重试)。 +_IN_AIR_TIMEOUT_S = 5.0 + def _monotonic_ms() -> int: return int(time.monotonic() * 1000) @@ -45,14 +72,14 @@ class TaskRunner: sm TaskStateMachine —— update() ground GroundLink —— latest()(地面注入坐标) drone MAVSDK System —— action.arm/takeoff/return_to_launch/land/disarm - vision_fn callable->bool —— 我方视觉第二判据(可选,require_vision 时用) """ def __init__(self, cfg, sm_cfg: TaskStateConfig, px4, bridge, sm: TaskStateMachine, - ground, drone, vision_fn: Optional[Callable[[], bool]] = None, + ground, drone, now_ms_fn: Callable[[], int] = _monotonic_ms, logger: Optional[TaskLogger] = None, - pod_control=None) -> None: + pod_control=None, land_instead_of_rtl: bool = False, + return_cfg: Optional[ReturnConfig] = None) -> None: self._cfg = cfg self._sm_cfg = sm_cfg self._px4 = px4 @@ -60,14 +87,32 @@ class TaskRunner: self._sm = sm self._ground = ground self._drone = drone - self._vision_fn = vision_fn self._now = now_ms_fn self._logger = logger # 结构化任务日志(可选,B 阶段) self._pod_control = pod_control # 吊舱识别/锁定控制(可选,真机) + self._land_instead_of_rtl = land_instead_of_rtl # 收尾改用就地降落 + self._return_cfg = return_cfg or ReturnConfig() self._centered_since_ms: Optional[int] = None self._rtl_requested = False self._emergency = False self._rtl_done = False + # SEARCH 扫视状态:当前扫向(右/左)与本次扫向的起始时刻 + self._sweep_dir = _SWEEP_RIGHT + self._sweep_since_ms: Optional[int] = None + self._pod_detection_on = False # 吊舱识别/自动锁定只开一次(防重发 0x07) + self._takeoff_done = False # 解锁/起飞只执行一次(防空中重复起飞) + self._in_air_stream = None # in_air 遥测流复用(避免每拍重新订阅) + self._rtl_entered_ms: Optional[int] = None # 进入 RTL 时刻(落地等待上限用) + # --- 自定义返航(回起飞点 → 恢复初始航向 → 降落)--- + self._home_lat: Optional[float] = None # 起飞点纬度(解锁时刻记录) + self._home_lon: Optional[float] = None + self._initial_yaw_rad: Optional[float] = None # 起飞时刻航向 + self._bridge_started = False # 是否已进入 offboard(未起飞则不能自主返航) + self._rtl_phase = _RTL_IDLE + self._return_ctrl: Optional[HomingController] = None + self._return_started_ms: Optional[int] = None + self._yaw_started_ms: Optional[int] = None + self._land_cmd_sent = False # --- 外部控制 ----------------------------------------------------------- def request_rtl(self) -> None: @@ -78,6 +123,43 @@ class TaskRunner: """外部触发紧急终止(丢控 / RC 接管 / 失控)。""" self._emergency = True + # --- 地面站命令分派 ----------------------------------------------------- + def _apply_ground_commands(self) -> None: + """排空地面站命令队列并逐条分派(主循环每拍调用)。""" + for name in self._ground.drain_commands(): + self._apply_command(name) + + def _apply_command(self, name: str) -> None: + """分派一条地面站命令;未知命令名忽略并警告。""" + if name == "rtl": + self.request_rtl() + elif name == "emergency": + self.trigger_emergency() + elif name in _POD_COMMAND_METHODS: + if self._pod_control is None: + logger.warning("[Task] 地面站命令 %s 被忽略:未配置吊舱控制(SITL)", + name) + return + try: + getattr(self._pod_control, _POD_COMMAND_METHODS[name])() + except Exception as exc: + logger.warning("[Task] 吊舱命令 %s 执行失败: %s", name, exc) + return + else: + logger.warning("[Task] 未知地面站命令: %s", name) + return + logger.info("[Task] 地面站命令: %s", name) + if self._logger is not None: + self._logger.log_event(self._now(), "command", name=name) + + def _log_fc_messages(self) -> None: + """把飞控状态文本(STATUSTEXT)落任务日志并输出控制台(每拍调用)。""" + for sev, text in self._px4.take_fc_messages(): + logger.info("[FC] %s: %s", sev, text) + if self._logger is not None: + self._logger.log_event(self._now(), "fc_status", + severity=sev, text=text) + @property def state(self) -> TaskState: return self._sm.state @@ -92,24 +174,29 @@ class TaskRunner: await self._px4.start_telemetry() # 幂等:先拿遥测,供起飞到位/距离计算 if self._logger is not None: self._logger.open() + # 记初始状态:页面「任务状态」取日志最后一个 state 事件,缺此行会显示上次旧状态 + self._logger.log_event(self._now(), "state", + from_state=self._sm.state.value, + to_state=self._sm.state.value, + reason="任务启动") period_s = 1.0 / self._sm_cfg.loop_frequency_hz try: while not stop_event.is_set(): now_ms = self._now() - inputs = self._collect_inputs(now_ms) - tr = self._sm.update(inputs, now_ms) - if tr.occurred: - logger.info("[Task] %s -> %s (%s)", - tr.from_state.value, tr.to_state.value, tr.reason) + try: + await self._iteration(now_ms) + except asyncio.CancelledError: + raise + except Exception as exc: + # 单周期异常不得让任务带着 offboard 指令静默停摆:转紧急 → 下一拍 + # 状态机转 RTL(与 bridge.run() 的按周期护栏同思路,但这里必须 + # 升级为返航,因为再没有别的循环会接管飞机)。 + logger.error("[Task] 任务周期异常,转紧急返航: %s", exc) + self._emergency = True if self._logger is not None: - self._logger.log_event( - now_ms, "state", from_state=tr.from_state.value, - to_state=tr.to_state.value, reason=tr.reason) - await self._on_enter(tr.to_state, now_ms) - out = await self._tick(now_ms) - if self._logger is not None: - self._logger.log_telemetry( - now_ms, self._build_telemetry_record(now_ms, inputs, out)) + self._logger.log_event(self._now(), "iteration_error", + error=f"{type(exc).__name__}: {exc}") + await self._safe_abort() if self._finished(): break await asyncio.sleep(period_s) @@ -118,10 +205,52 @@ class TaskRunner: if self._logger is not None: self._logger.close() + async def _iteration(self, now_ms: int) -> None: + """单周期:命令 → 快照 → 状态机 → 进入动作 → 持续动作 → 日志。""" + self._apply_ground_commands() # 地面站命令先于快照采集生效 + self._log_fc_messages() + inputs = self._collect_inputs(now_ms) + tr = self._sm.update(inputs, now_ms) + if tr.occurred: + logger.info("[Task] %s -> %s (%s)", + tr.from_state.value, tr.to_state.value, tr.reason) + if self._logger is not None: + self._logger.log_event( + now_ms, "state", from_state=tr.from_state.value, + to_state=tr.to_state.value, reason=tr.reason) + await self._on_enter(tr.to_state, now_ms) + out = await self._tick(now_ms) + if self._logger is not None: + self._logger.log_telemetry( + now_ms, self._build_telemetry_record(now_ms, inputs, out)) + + async def _safe_abort(self) -> None: + """异常兜底:直接进入 RTL 流程(不依赖状态机下一拍转移)。 + + 异常可能来自采集/控制律/日志任意环节,下一拍未必能正常跑完转移;因此在 + 兜底里显式走一次 RTL 进入动作(停机 + 返航),保证飞机不会停在 offboard + 速度指令上。 + """ + if self._sm.state is TaskState.RTL: + return + self._sm.force_state(TaskState.RTL, self._now()) + self._rtl_entered_ms = self._now() + if self._pod_control is not None: + try: + self._pod_control.stop_tracking() + except Exception: + pass + try: + # 兜底不走自定义返航:异常后不再信任自身状态/定位,直接交给 PX4 原生 RTL + await self._begin_rtl(self._now(), force_fallback=True) + except Exception as exc: + logger.error("[Task] 异常兜底返航失败: %s", exc) + # --- 输入采集 ----------------------------------------------------------- def _collect_inputs(self, now_ms: int) -> TaskInputs: """聚合 PX4 / 注入坐标 / 吊舱 / 视觉为一份 TaskInputs 快照。""" own = self._px4.own_state() + self._maybe_record_home(own) # 起飞阶段定位可能晚到:每拍补记一次 injected = self._ground.latest() target_injected = bool( @@ -135,10 +264,12 @@ class TaskRunner: own.latitude_deg, own.longitude_deg, injected.latitude_deg, injected.longitude_deg) + # 起飞到达判据带容差:悬停/高度估计通常略低于设定目标(实测 SITL 4.99m vs 5.0m), + # 无容差会差厘米误判"起飞超时"→ 真机上更明显 takeoff_reached = (own.relative_altitude_m - >= self._sm_cfg.takeoff_altitude_m) + >= self._sm_cfg.takeoff_altitude_m + - self._sm_cfg.takeoff_reached_tolerance_m) target_locked = self._bridge.pod_locked(now_ms) - vision = bool(self._vision_fn and self._vision_fn()) centered, centered_s = self._update_centered(now_ms) return TaskInputs( @@ -146,7 +277,6 @@ class TaskRunner: distance_to_target_m=distance, takeoff_reached=takeoff_reached, target_locked=target_locked, - vision_has_target=vision, target_centered=centered, centered_duration_s=centered_s, rtl_requested=self._rtl_requested, @@ -174,6 +304,12 @@ class TaskRunner: # --- 状态进入动作 ------------------------------------------------------- async def _on_enter(self, state: TaskState, now_ms: int) -> None: + """进入状态的**一次性**动作。 + + 注意:这些动作只在「首次」进入时执行——任务中途可能需要多次经过 GOTO/ + SEARCH(丢目标回搜),重复 `offboard.start()` / `arm_detection()` + (0x07 目标识别开会打断正在进行的跟踪)会造成真机自激,故以标志位守卫。 + """ if state is TaskState.ARM_TAKEOFF: await self._arm_takeoff() elif state is TaskState.GOTO: @@ -181,17 +317,50 @@ class TaskRunner: logger.error("[Task] offboard 启动失败,紧急终止") self._emergency = True return + self._bridge_started = True self._bridge.set_heading_servo_enabled(False) # L2 地理引导 - if self._pod_control is not None: - self._pod_control.arm_detection() # 真机:开启目标识别 + 自动锁定 + self._start_pod_detection() # 首次:开识别 + 自动锁定 + elif state is TaskState.SEARCH: + self._bridge.set_heading_servo_enabled(False) # 搜索不用控制律 + self._sweep_dir = _SWEEP_RIGHT # 每次进入从右向起扫 + self._sweep_since_ms = now_ms elif state is TaskState.TRACK: - self._bridge.set_heading_servo_enabled(True) # L1 航向伺服 + self._bridge.set_heading_servo_enabled(True) # L1 视觉追踪 + 缓慢靠近 elif state is TaskState.APPROACH: pass # 悬停由 _tick 持续发零速 elif state is TaskState.RTL: + self._rtl_entered_ms = now_ms if self._pod_control is not None: - self._pod_control.stop_tracking() # 真机:停止跟踪 + 识别关(bridge 停机前) - await self._start_rtl() + self._pod_control.stop_tracking() # 真机:停止跟踪 + 识别关 + await self._begin_rtl(now_ms) + + def _start_pod_detection(self) -> None: + """首次到达引导/搜索阶段时开启吊舱目标识别 + 自动锁定(只发一次)。""" + if self._pod_control is None or self._pod_detection_on: + return + self._pod_detection_on = True + self._pod_control.arm_detection() + + # --- SEARCH 扫视 -------------------------------------------------------- + def _search_yaw_rate(self, now_ms: int) -> float: + """SEARCH 扫视角速度:按 sweep_reverse_s 交替左右,0 = 单向扫视。 + + 吊舱相机刚性固连机体(无电机、不独立转动),故只能靠机头旋转改变观察 + 方向;扫视期间零线速度,飞机保持悬停在参考点附近。 + """ + rate = self._sm_cfg.sweep_rate_dps + if rate == 0.0: + return 0.0 + period_s = self._sm_cfg.sweep_reverse_s + if period_s > 0.0: + if self._sweep_since_ms is None: + self._sweep_since_ms = now_ms + elif (now_ms - self._sweep_since_ms) >= period_s * 1000.0: + self._sweep_since_ms = now_ms + self._sweep_dir = (_SWEEP_LEFT if self._sweep_dir == _SWEEP_RIGHT + else _SWEEP_RIGHT) + sign = 1.0 if self._sweep_dir == _SWEEP_RIGHT else -1.0 + return sign * rate # --- 每周期持续动作 ----------------------------------------------------- async def _tick(self, now_ms: int): @@ -199,10 +368,13 @@ class TaskRunner: state = self._sm.state if state in (TaskState.GOTO, TaskState.TRACK): return await self._bridge.step(now_ms) + elif state is TaskState.SEARCH: + # 原地偏航扫视(同时 poll 吊舱,保遥测新鲜,否则搜到也切不进 TRACK) + await self._bridge.search_step(now_ms, self._search_yaw_rate(now_ms)) elif state is TaskState.APPROACH: await self._px4.send_brake_hover(now_ms) # 悬停目标上空(保 offboard 心跳) elif state is TaskState.RTL: - await self._check_landed() + await self._tick_rtl(now_ms) return None # WAIT / ARM_TAKEOFF:无持续动作(待命 / 等起飞高度) @@ -238,6 +410,17 @@ class TaskRunner: # --- 飞行编排 ----------------------------------------------------------- async def _arm_takeoff(self) -> None: + """解锁 + 起飞到悬停高度(每次任务只执行一次)。 + + 重复执行会导致:机已在空中时再次 arm/takeoff → PX4 拒绝甚至触发保护, + 且日志看起来像"重新起飞"。故用标志位守卫,重复进入只告警不改动飞机。 + """ + if self._takeoff_done: + logger.warning("[Task] 已在飞行中,忽略重复的解锁/起飞请求") + return + self._takeoff_done = True + # 解锁前记录起飞点与初始航向(自定义返航要用;PX4 的 home 此时已固定) + self._record_home() try: await self._drone.action.arm() await self._drone.action.set_takeoff_altitude( @@ -254,9 +437,175 @@ class TaskRunner: self._logger.log_event(self._now(), "takeoff_failed", error=str(exc)) - async def _start_rtl(self) -> None: + def _record_home(self) -> bool: + """尝试记录起飞点与初始航向;成功返回 True(可重复调用,只记一次)。""" + if self._home_lat is not None: + return True try: - await self._bridge.stop() # 先零速刹车 + 停 offboard/遥测/串口 + own = self._px4.own_state() + except Exception as exc: # 遥测不可用时不阻塞起飞 + logger.warning("[Task] 读取起飞点失败: %s", exc) + return False + return self._maybe_record_home(own) + + def _maybe_record_home(self, own) -> bool: + """在**起飞前/起飞中**用首个有效定位记下起飞点与初始航向。 + + ⚠ SITL 实测踩到的坑:解锁发生在地面站坐标到达后约 0.3s,此时 `gps_info` + (2Hz)可能还没到,`position_valid` 仍为 False——若只在解锁那一刻记一次, + 起飞点就会丢失、自定义返航只能退回 PX4 RTL。故起飞阶段每拍补记。 + 只接受 WAIT / ARM_TAKEOFF 阶段的定位:进入 GOTO 后飞机已在空中, + 那时的位置不该再当作"起飞点"。 + """ + if self._home_lat is not None: + return True + if not own.position_valid: + return False + if self._sm.state not in (TaskState.WAIT, TaskState.ARM_TAKEOFF): + return False + self._home_lat = own.latitude_deg + self._home_lon = own.longitude_deg + self._initial_yaw_rad = own.yaw_rad + logger.info("[Task] 起飞点已记录: (%.7f, %.7f) 初始航向 %.1f°", + self._home_lat, self._home_lon, + math.degrees(self._initial_yaw_rad)) + return True + + # --- 返航(自定义:回起飞点 → 恢复初始航向 → 降落)--------------------- + def _custom_return_possible(self) -> bool: + """能否走自定义返航(已起飞 + 有起飞点 + 位置有效)。""" + if not self._return_cfg.enabled: + return False + if not self._bridge_started: + return False # 没进过 offboard:还在待命/起飞段 + if self._home_lat is None or self._home_lon is None: + return False + if self._initial_yaw_rad is None: + return False + try: + return bool(self._px4.own_state().position_valid) + except Exception: + return False + + async def _begin_rtl(self, now_ms: int, force_fallback: bool = False) -> None: + """进入返航:选择「自定义返航」或「就地降落」或「PX4 RTL 兜底」。""" + if self._land_instead_of_rtl: + self._rtl_phase = _RTL_LAND + await self._start_land() + return + if force_fallback or not self._custom_return_possible(): + reason = "异常兜底" if force_fallback else "未起飞/无起飞点" + logger.info("[Task] 使用 PX4 原生 RTL(%s)", reason) + await self._fallback_rtl() + return + own = self._px4.own_state() + # 转场高度:取"当前高度"与"起飞高度"的较大者 —— 不低于任务高度, + # 也不会为了返航额外爬高(这正是原生 RTL 会做的事)。 + transit_alt = max(own.relative_altitude_m, + self._sm_cfg.takeoff_altitude_m) + self._return_ctrl = HomingController( + self._return_cfg, self._home_lat, self._home_lon, + self._initial_yaw_rad, transit_alt) + self._return_started_ms = now_ms + self._rtl_phase = _RTL_RETURN + logger.info("[Task] 自定义返航:回起飞点 (%.7f, %.7f),转场高度 %.1fm," + "到位后恢复航向 %.1f° 并降落", + self._home_lat, self._home_lon, transit_alt, + math.degrees(self._initial_yaw_rad)) + if self._logger is not None: + self._logger.log_event( + now_ms, "return_home", home_lat=self._home_lat, + home_lon=self._home_lon, transit_alt_m=transit_alt, + initial_yaw_deg=math.degrees(self._initial_yaw_rad)) + + async def _return_step(self, now_ms: int) -> None: + """转场阶段:朝起飞点飞,保持转场高度。""" + cfg = self._return_cfg + if (self._return_started_ms is not None + and now_ms - self._return_started_ms >= cfg.timeout_s * 1000.0): + logger.warning("[Task] 返航转场超时 %.0fs,退回 PX4 RTL", cfg.timeout_s) + if self._logger is not None: + self._logger.log_event(now_ms, "return_timeout", + timeout_s=cfg.timeout_s) + await self._fallback_rtl() + return + own = self._px4.own_state() + if not own.position_valid: + # 定位丢失:不能自主返航,先刹停等恢复(不盲目飞) + logger.warning("[Task] 返航中定位失效,刹停等待") + await self._px4.send_brake_hover(now_ms) + return + cmd = self._return_ctrl.update(own.latitude_deg, own.longitude_deg, + own.yaw_rad, own.relative_altitude_m) + out = ControlOutput(mode=ControlMode.VELOCITY_HEADING, valid=True, + vx_body_mps=cmd.vx_mps, vz_mps=cmd.vz_mps, + yaw_rate_dps=cmd.yaw_rate_dps) + await self._px4.send_velocity_heading(out, now_ms) + if cmd.arrived: + logger.info("[Task] 已回到起飞点(距 %.1fm),开始恢复初始航向", + cmd.distance_m) + self._rtl_phase = _RTL_YAW + self._yaw_started_ms = now_ms + + async def _yaw_align_step(self, now_ms: int) -> None: + """恢复初始航向阶段:原地转回起飞时刻的航向。""" + cfg = self._return_cfg + own = self._px4.own_state() + err, rate = self._return_ctrl.yaw_align(own.yaw_rad) + # 保持转场高度(不额外爬升) + out = ControlOutput(mode=ControlMode.VELOCITY_HEADING, valid=True, + vx_body_mps=0.0, + vz_mps=self._return_ctrl.alt_hold_vz( + own.relative_altitude_m), + yaw_rate_dps=rate) + await self._px4.send_velocity_heading(out, now_ms) + + timed_out = (self._yaw_started_ms is not None + and now_ms - self._yaw_started_ms + >= cfg.yaw_align_timeout_s * 1000.0) + if abs(err) <= cfg.yaw_align_tolerance_deg: + logger.info("[Task] 航向已恢复(误差 %.1f°),开始降落", err) + elif timed_out: + logger.warning("[Task] 航向对齐超时(误差 %.1f°),仍进入降落", err) + else: + return + if self._logger is not None: + self._logger.log_event(now_ms, "yaw_restored", + err_deg=err, timed_out=timed_out) + self._rtl_phase = _RTL_LAND + await self._start_land() + + async def _start_land(self) -> None: + """退出 offboard 并垂直降落(不爬升)。""" + if self._land_cmd_sent: + return + self._land_cmd_sent = True + # 只退 offboard、保留遥测:落地判定与高度曲线都依赖遥测(SITL 实测过: + # 若在这里就全停,日志会冻结在降落前的高度,看不出下降)。 + stop_for_landing = getattr(self._bridge, "landing_stop", None) + try: + if stop_for_landing is not None: + await stop_for_landing() + else: + await self._bridge.stop() + except Exception as exc: + logger.warning("[Task] 停机异常: %s", exc) + try: + await self._drone.action.land() + except Exception as exc: + logger.warning("[Task] 降落指令失败,尝试 PX4 RTL 兜底: %s", exc) + try: + await self._drone.action.return_to_launch() + except Exception: + pass + if self._logger is not None: + self._logger.log_event(self._now(), "rtl", action="land") + + async def _fallback_rtl(self) -> None: + """兜底:停机后交给 PX4 原生 RTL(会先爬升到 RTL_RETURN_ALT)。""" + self._rtl_phase = _RTL_FALLBACK + try: + await self._bridge.stop() except Exception as exc: logger.warning("[Task] 停机异常: %s", exc) try: @@ -268,21 +617,57 @@ class TaskRunner: except Exception: pass if self._logger is not None: - self._logger.log_event(self._now(), "rtl") + self._logger.log_event(self._now(), "rtl", action="rtl") + + async def _tick_rtl(self, now_ms: int) -> None: + """RTL 状态机:按阶段推进(转场 → 恢复航向 → 降落 / 兜底)。""" + phase = self._rtl_phase + if phase == _RTL_RETURN: + await self._return_step(now_ms) + elif phase == _RTL_YAW: + await self._yaw_align_step(now_ms) + elif phase == _RTL_LAND: + await self._check_landed() + elif phase == _RTL_FALLBACK: + await self._check_landed() + # 整体落地等待上限:任何阶段都不得无限等 + if (not self._rtl_done and self._rtl_entered_ms is not None + and now_ms - self._rtl_entered_ms + >= self._sm_cfg.rtl_timeout_s * 1000.0): + logger.warning("[Task] RTL 等待落地超过 %.0fs,结束任务(无法确认落地)", + self._sm_cfg.rtl_timeout_s) + self._rtl_done = True + if self._logger is not None: + self._logger.log_event(now_ms, "rtl_timeout", + timeout_s=self._sm_cfg.rtl_timeout_s) async def _check_landed(self) -> None: - """RTL 状态每周期取一帧 in_air 遥测;落地后 disarm 并标记完成。""" + """RTL 状态每周期取一帧 in_air 遥测;落地后 disarm 并标记完成。 + + 两个要点: + * `in_air()` 是 MAVSDK 遥测流:流挂起时 `async for` 会**永久阻塞**, + 主循环随之卡死(rtl_done 永不置位)。故加超时兜底,超时下拍重试。 + * 生成器**复用**(`_in_air_stream`):每拍新建一个 `in_air()` 会在 + mavsdk_server 侧反复订阅/退订同一条流(20Hz × 整个 RTL 段),是 + 回调队列的无谓负担;复用后只订阅一次。 + """ try: - async for is_in_air in self._drone.telemetry.in_air(): - if not is_in_air and not self._rtl_done: - self._rtl_done = True - try: - await self._drone.action.disarm() - except Exception: - pass - break # 只取最新一帧 + if self._in_air_stream is None: + self._in_air_stream = self._drone.telemetry.in_air() + async with asyncio.timeout(_IN_AIR_TIMEOUT_S): + async for is_in_air in self._in_air_stream: + if not is_in_air and not self._rtl_done: + self._rtl_done = True + try: + await self._drone.action.disarm() + except Exception: + pass + break # 只取最新一帧 + except (TimeoutError, asyncio.TimeoutError): + logger.debug("[Task] in_air 遥测超时 %.1fs,下拍重试", _IN_AIR_TIMEOUT_S) except Exception: - pass + # 建流或消费失败都算流不可用:丢弃缓存,下拍重建(不永久卡在坏流上) + self._in_air_stream = None # --- 收尾 --------------------------------------------------------------- def _finished(self) -> bool: diff --git a/utils/task_runner/state_machine.py b/utils/task_runner/state_machine.py index e174807..3f946cd 100644 --- a/utils/task_runner/state_machine.py +++ b/utils/task_runner/state_machine.py @@ -5,23 +5,29 @@ L2(地理引导 GuidanceLaw)按任务阶段串成一条全流程。本模块 + 守卫」,不驱动 MAVSDK/offboard —— 那些由 runner.py(编排器)负责。每周期吃一份 TaskInputs 快照,返回是否转移 + 原因。 -状态链: - WAIT ──收到有效坐标──▶ ARM_TAKEOFF ──到起飞高度──▶ GOTO(L2 坐标引导) - │ 距目标 < switch_radius - │ 且锁定(+可选视觉) +状态链(2026-10-15 按实机作业预期重构): + WAIT ──收到有效坐标──▶ ARM_TAKEOFF ──到起飞高度──▶ GOTO(L2 飞到参考点) + │ 距参考点 < arrival_radius ▼ - APPROACH ◀──居中稳定── TRACK(L1 航向伺服) - │ - 任意状态 ──超时 / 返航 / 紧急──▶ RTL + SEARCH(原地偏航扫视找目标)──锁定(+可选视觉)──▶ TRACK + │ 搜索超时 (L1 视觉追踪 + + │ slow_approach 缓慢靠近) + ▼ │ 追踪超时 + RTL ◀────────────────────────────────┘ 设计要点(铁律对齐): * 纯逻辑:输入/输出都是 dataclass,时间用外部传入的 now_ms(单调毫秒), 不碰时钟/IO/网络,可确定性单测。 * 全局最高优先级:emergency(丢控/RC 接管/失控)与 rtl_requested(人工返航) → 任何非 RTL 状态立即转 RTL(安全第一)。 - * GOTO→TRACK 守卫复用 L1 方案 §2.3:目标锁定 bit2 +(可选)我方视觉第二判据, - 避免 bit2 粘滞导致误入追踪。 - * TRACK 丢目标:若注入坐标仍有效 → 退回 GOTO 重新引导;否则 → RTL。 + * 「参考点」语义:地面给的坐标是**搜索起点**,不是终点——单点坐标精度不足以 + 直接覆盖目标,故进入到达半径即转 SEARCH 扫视,由吊舱/视觉去发现目标。 + * GOTO/SEARCH→TRACK 守卫复用 L1 方案 §2.3:目标锁定 bit2 +(可选)我方视觉 + 第二判据,避免 bit2 粘滞导致误入追踪。 + * TRACK 丢目标 → 回 SEARCH 重新扫视(**不回 GOTO**:飞机本就在参考点附近, + 回 GOTO 只会反复重跑进入动作,而参考点坐标本身不精确);坐标同时失效 → RTL。 + * TRACK 不因「目标居中」退出:居中正是吊舱对准目标、应当继续缓慢靠近的时刻。 + 接近的收尾只由超时 / 人工返航 / 紧急终止(无距离输入,判不出"已到目标")。 * 阈值/超时全外置(TaskStateConfig),代码不写死数值。 """ @@ -35,9 +41,10 @@ class TaskState(Enum): WAIT = "wait" # 待命:等地面坐标 ARM_TAKEOFF = "arm_takeoff" # 解锁 + 起飞到悬停高度 - GOTO = "goto" # L2 坐标引导(飞向注入坐标) - TRACK = "track" # L1 航向伺服(吊舱角度追踪) - APPROACH = "approach" # 接近/悬停目标上空 + GOTO = "goto" # L2 地理引导:飞向参考点(地面给的搜索起点) + SEARCH = "search" # 到点后原地偏航扫视搜索目标(吊舱/视觉) + TRACK = "track" # L1 视觉追踪 + 缓慢靠近(slow_approach 保守速度) + APPROACH = "approach" # 到点悬停(真机可选收尾;SITL/常规流程不用) RTL = "rtl" # 返航 + 落地 + 上锁(终态,reset() 才回 WAIT) @@ -45,15 +52,19 @@ class TaskState(Enum): class TaskStateConfig: """状态机配置(全外置;数值均为占位,须台架/实飞标定后回填)。""" - switch_radius_m: float = 30.0 # GOTO→TRACK 切换半径(米) - require_vision: bool = False # GOTO→TRACK 是否需视觉第二判据(S3 起开) + arrival_radius_m: float = 30.0 # GOTO→SEARCH 到达参考点半径(米) arm_takeoff_timeout_s: float = 60.0 # 起飞阶段超时 - goto_timeout_s: float = 120.0 # 坐标引导超时 - track_timeout_s: float = 120.0 # 追踪超时 - center_hold_s: float = 3.0 # TRACK→APPROACH 居中稳定时长(秒) - approach_hover_s: float = 10.0 # APPROACH 悬停时长(到点 → RTL) + goto_timeout_s: float = 120.0 # 飞向参考点超时 + search_timeout_s: float = 20.0 # 搜索超时(未发现目标 → 返航) + track_timeout_s: float = 20.0 # 追踪/靠近段上限(到点 → 返航) + approach_hover_s: float = 10.0 # APPROACH 悬停时长(到点 → RTL;已非主流程) takeoff_altitude_m: float = 5.0 # 起飞目标高度(runner 起飞 + takeoff_reached 判据) + takeoff_reached_tolerance_m: float = 0.5 # 起飞到达判据容差(低于目标不超过此值也算到达; + # 悬停/高度估计误差下避免差厘米误判"起飞超时") center_threshold_deg: float = 1.0 # 脱靶量居中阈值(|off_target| < 此值判居中) + sweep_rate_dps: float = 15.0 # SEARCH 原地扫视角速度(度/秒) + sweep_reverse_s: float = 4.0 # SEARCH 扫视换向周期(秒);0 = 单向扫视 + rtl_timeout_s: float = 180.0 # RTL 等待落地上限(秒);超时即结束任务,防无限等待 loop_frequency_hz: float = 20.0 # runner 主循环频率(bridge.step 节拍) @@ -66,12 +77,11 @@ class TaskInputs: """ target_injected: bool = False # 地面坐标有效注入(新鲜、未超龄) - distance_to_target_m: float = math.inf # 到注入目标的水平距离(GOTO 用) + distance_to_target_m: float = math.inf # 到参考点的水平距离(GOTO 用) takeoff_reached: bool = False # 已到达起飞目标高度 - target_locked: bool = False # 吊舱锁定(bit2 + 遥测新鲜) - vision_has_target: bool = False # 我方视觉第二判据(可选,require_vision 时必看) - target_centered: bool = False # 目标居中(脱靶量长期≈0) - centered_duration_s: float = 0.0 # 居中持续时长(外部累加,秒) + target_locked: bool = False # 目标有效(吊舱 bit2 + 遥测新鲜 + LOS 一致性,见 bridge) + target_centered: bool = False # 目标居中(脱靶量≈0)——仅供日志/诊断 + centered_duration_s: float = 0.0 # 居中持续时长(外部累加,秒)——不再作转移判据 rtl_requested: bool = False # 人工返航命令 emergency: bool = False # 紧急(丢控 / RC 接管 / 失控) @@ -103,6 +113,15 @@ class TaskStateMachine: self._state = TaskState.WAIT self._entered_ms = 0 + def force_state(self, state: TaskState, now_ms: int) -> None: + """强制落到某状态(仅供编排器异常兜底;不产生 TaskTransition)。 + + 正常转移一律走 update()。此入口用于「主循环抛异常、必须立刻进 RTL」这类 + 非判据驱动的安全路径——此时再喂一拍快照不可靠。 + """ + self._state = state + self._entered_ms = now_ms + def update(self, inputs: TaskInputs, now_ms: int) -> TaskTransition: """吃一份快照,返回转移结果;无转移时 occurred=False、to_state 保持原状态。""" state = self._state @@ -127,23 +146,31 @@ class TaskStateMachine: elif state is TaskState.GOTO: if not inputs.target_injected: return self._goto(TaskState.RTL, "注入坐标失效", now_ms) - if (inputs.distance_to_target_m <= self._cfg.switch_radius_m - and self._lock_ok(inputs)): - return self._goto(TaskState.TRACK, "进入追踪半径且目标锁定", now_ms) + # 到参考点即结束地理引导 → 转扫视搜索(参考点是搜索起点,不是终点) + if inputs.distance_to_target_m <= self._cfg.arrival_radius_m: + return self._goto(TaskState.SEARCH, "已到参考点,开始搜索", now_ms) if self._elapsed_s(now_ms) >= self._cfg.goto_timeout_s: - return self._goto(TaskState.RTL, "坐标引导超时", now_ms) + return self._goto(TaskState.RTL, "飞向参考点超时", now_ms) + + elif state is TaskState.SEARCH: + # 扫视中一旦发现并锁定目标 → 进追踪(缓慢靠近) + if self._lock_ok(inputs): + return self._goto(TaskState.TRACK, "搜索发现目标并锁定", now_ms) + if not inputs.target_injected: + return self._goto(TaskState.RTL, "注入坐标失效", now_ms) + if self._elapsed_s(now_ms) >= self._cfg.search_timeout_s: + return self._goto(TaskState.RTL, "搜索超时未发现目标", now_ms) elif state is TaskState.TRACK: if not inputs.target_locked: - # 丢目标:坐标仍有效 → 退回 GOTO 重新引导;否则终止返航。 + # 丢目标:坐标仍有效 → 回参考点附近重新扫视;否则终止返航。 if inputs.target_injected: - return self._goto(TaskState.GOTO, "目标丢失,退回坐标引导", now_ms) + return self._goto(TaskState.SEARCH, "目标丢失,重新搜索", now_ms) return self._goto(TaskState.RTL, "目标丢失且坐标失效", now_ms) - if (inputs.target_centered - and inputs.centered_duration_s >= self._cfg.center_hold_s): - return self._goto(TaskState.APPROACH, "目标居中稳定", now_ms) + # 注意:不再有「居中稳定 → APPROACH」分支。居中表示吊舱已对准目标, + # 正是应当继续按 slow_approach 缓慢靠近的时刻,不是终止条件。 if self._elapsed_s(now_ms) >= self._cfg.track_timeout_s: - return self._goto(TaskState.RTL, "追踪超时", now_ms) + return self._goto(TaskState.RTL, "追踪/靠近超时", now_ms) elif state is TaskState.APPROACH: if self._elapsed_s(now_ms) >= self._cfg.approach_hover_s: @@ -154,15 +181,13 @@ class TaskStateMachine: # --- 内部辅助 ----------------------------------------------------------- def _lock_ok(self, inputs: TaskInputs) -> bool: - """GOTO→TRACK 的目标锁定判据(复用 L1 方案 §2.3)。 + """进 TRACK 的目标判据(GOTO/SEARCH→TRACK)。 - 需吊舱锁定 bit2;若 require_vision 还需我方视觉第二判据(bit2 粘滞兜底)。 + `target_locked` 已由编排层合成:吊舱 bit2 + 遥测新鲜 + **LOS 运动学一致性** + (见 bridge.pod_locked / los_consistency)。状态机不再重复做视觉判据 —— + 吊舱已取代双目+YOLO 成为唯一目标源,"我方视觉"不复存在。 """ - if not inputs.target_locked: - return False - if self._cfg.require_vision and not inputs.vision_has_target: - return False - return True + return inputs.target_locked def _elapsed_s(self, now_ms: int) -> float: """进入当前状态的时长(秒)。_entered_ms=0(未打戳)时返回 0。""" diff --git a/utils/tests/ground_station/app_test.py b/utils/tests/ground_station/app_test.py new file mode 100644 index 0000000..3f1d100 --- /dev/null +++ b/utils/tests/ground_station/app_test.py @@ -0,0 +1,299 @@ +"""app_test.py —— 地面站 Flask 端点测试(test_client + 真实 UDP 回环监听)。 + +覆盖:POST /api/inject 合法/非法(UDP 实收断言)、局部坐标注入换算与拒绝、 +POST /api/command 白名单校验与 UDP 下发、POST /api/continuous 服务端持续注入启停、 +GET /api/link 链路目标读写、GET /api/state 空日志目录兜底与 continuous 标志。 +""" + +import json +import math +import socket +import sys +import time +from pathlib import Path + +import pytest + +_ROOT = Path(__file__).resolve().parents[3] # utils/tests/ground_station → 仓库根 +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +from ground_station.app import create_app # noqa: E402 + + +@pytest.fixture +def udp_listener(): + """真实 UDP 回环监听:返回 (port, recv_one)。""" + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.bind(("127.0.0.1", 0)) + s.settimeout(1.5) + + def recv_one(): + data, _addr = s.recvfrom(2048) + return json.loads(data.decode("utf-8")) + + yield s.getsockname()[1], recv_one + s.close() + + +@pytest.fixture +def client(tmp_path, udp_listener): + port, _recv = udp_listener + app = create_app(udp_port=port, log_dir=str(tmp_path), + inject_interval_s=0.05) # 测试用短周期 + app.config["TESTING"] = True + return app.test_client() + + +# --- /api/inject ------------------------------------------------------------ +def test_inject_ok_sends_udp(client, udp_listener): + _port, recv_one = udp_listener + r = client.post("/api/inject", json={"lat": 1.5, "lon": 2.5, "alt": 10}) + assert r.get_json()["ok"] is True + assert recv_one() == {"lat": 1.5, "lon": 2.5, "alt": 10.0} + + +def test_inject_missing_lat_400(client): + r = client.post("/api/inject", json={"lon": 2.5}) + assert r.status_code == 400 + assert r.get_json()["ok"] is False + + +def test_inject_non_numeric_400(client): + r = client.post("/api/inject", json={"lat": "x", "lon": 2.5}) + assert r.status_code == 400 + + +# --- 局部坐标注入 ------------------------------------------------------------- +_TEL_HEADER = ("t_s,state,pos_valid,fix_type,num_satellites,latitude_deg," + "longitude_deg,relative_altitude_m,yaw_deg,target_injected," + "distance_to_target_m,target_locked,off_target_x_deg," + "off_target_y_deg,mode,yaw_rate_dps,vx_body_mps,vz_mps\n") + + +def _write_telemetry(log_dir, lat=47.0, lon=8.0, pos_valid="True"): + with open(Path(log_dir) / "task_telemetry.csv", "w", encoding="utf-8") as f: + f.write(_TEL_HEADER) + f.write(f"1.0,wait,{pos_valid},3,10,{lat},{lon},0,0,False,inf,False," + "0,0,,0,0,0\n") + + +def test_inject_local_converts_from_vehicle_position(client, udp_listener, tmp_path): + _write_telemetry(tmp_path, lat=47.0, lon=8.0) + _port, recv_one = udp_listener + r = client.post("/api/inject", json={"north_m": 30, "east_m": 15, "alt": 0}) + j = r.get_json() + assert j["ok"] is True + exp_lat = 47.0 + 30 / 111320.0 + exp_lon = 8.0 + 15 / (111320.0 * math.cos(math.radians(47.0))) + msg = recv_one() + assert msg["lat"] == pytest.approx(exp_lat, abs=1e-9) + assert msg["lon"] == pytest.approx(exp_lon, abs=1e-9) + assert j["local"]["north_m"] == 30 and j["local"]["east_m"] == 15 + assert j["local"]["origin_lat"] == pytest.approx(47.0) + + +def test_inject_local_rejected_without_telemetry(client): + r = client.post("/api/inject", json={"north_m": 30, "east_m": 15}) + assert r.status_code == 400 + assert "无法换算" in r.get_json()["error"] + + +def test_inject_local_rejected_when_pos_invalid(client, tmp_path): + _write_telemetry(tmp_path, pos_valid="False") + r = client.post("/api/inject", json={"north_m": 30, "east_m": 15}) + assert r.status_code == 400 + + +def test_inject_local_bad_numbers_400(client, tmp_path): + _write_telemetry(tmp_path) + assert client.post("/api/inject", + json={"north_m": "x", "east_m": 15}).status_code == 400 + + +def test_inject_local_home_origin_uses_session_start(client, udp_listener, tmp_path): + # 同一段:起始有效定位 47.0/8.0(≈起飞点),之后移动到 47.5/8.5 + with open(tmp_path / "task_telemetry.csv", "w", encoding="utf-8") as f: + f.write(_TEL_HEADER) + f.write("1.0,wait,True,3,10,47.0,8.0,0,0,False,inf,False,0,0,,0,0,0\n") + f.write("60.0,goto,True,3,10,47.5,8.5,0,0,False,inf,False,0,0,,0,0,0\n") + _port, recv_one = udp_listener + + j = client.post("/api/inject", + json={"north_m": 0, "east_m": 0, "origin": "home"}).get_json() + assert j["ok"] is True and j["local"]["origin"] == "home" + assert j["local"]["origin_lat"] == pytest.approx(47.0) # 段首而非最后行 + assert j["local"]["origin_lon"] == pytest.approx(8.0) + recv_one() + + j2 = client.post("/api/inject", + json={"north_m": 0, "east_m": 0}).get_json() # current + assert j2["local"]["origin"] == "current" + assert j2["local"]["origin_lat"] == pytest.approx(47.5) # 当前位置 + recv_one() + + +def test_inject_local_home_origin_skips_invalid_rows(client, tmp_path): + with open(tmp_path / "task_telemetry.csv", "w", encoding="utf-8") as f: + f.write(_TEL_HEADER) + f.write("0.5,wait,False,0,0,0.0,0.0,0,0,False,inf,False,0,0,,0,0,0\n") + f.write("1.0,wait,True,3,10,47.0,8.0,0,0,False,inf,False,0,0,,0,0,0\n") + j = client.post("/api/inject", + json={"north_m": 30, "east_m": 15, + "origin": "home"}).get_json() + assert j["ok"] is True + assert j["local"]["origin_lat"] == pytest.approx(47.0) # 取首个有效定位 + + +def test_inject_local_bad_origin_400(client, tmp_path): + _write_telemetry(tmp_path) + r = client.post("/api/inject", + json={"north_m": 1, "east_m": 1, "origin": "mars"}) + assert r.status_code == 400 + + +def test_inject_local_home_origin_rejected_without_telemetry(client): + r = client.post("/api/inject", + json={"north_m": 30, "east_m": 15, "origin": "home"}) + assert r.status_code == 400 + assert "起飞点" in r.get_json()["error"] + + +# --- /api/command ----------------------------------------------------------- +def test_command_whitelist_ok_sends_udp(client, udp_listener): + _port, recv_one = udp_listener + r = client.post("/api/command", json={"cmd": "rtl"}) + assert r.get_json() == {"ok": True, "sent": {"cmd": "rtl"}} + assert recv_one() == {"cmd": "rtl"} + + +def test_command_all_whitelist_entries_accepted(client, udp_listener): + _port, recv_one = udp_listener + for cmd in ("rtl", "emergency", "pod_detect_on", "pod_auto_lock", + "pod_stop_track", "pod_detect_off", "pod_center"): + r = client.post("/api/command", json={"cmd": cmd}) + assert r.get_json()["ok"] is True + assert recv_one() == {"cmd": cmd} + + +def test_command_unknown_rejected_no_udp(client, udp_listener): + _port, recv_one = udp_listener + r = client.post("/api/command", json={"cmd": "reboot"}) + assert r.status_code == 400 + assert r.get_json()["ok"] is False + with pytest.raises(socket.timeout): + recv_one() # 未下发任何 UDP → 监听超时即证明 + + +def test_command_non_string_rejected(client): + r = client.post("/api/command", json={"cmd": 1}) + assert r.status_code == 400 + + +# --- /api/continuous -------------------------------------------------------- +def test_continuous_requires_prior_inject(client): + r = client.post("/api/continuous", json={"on": True}) + assert r.status_code == 400 + assert r.get_json()["ok"] is False + + +def test_continuous_non_bool_rejected(client): + r = client.post("/api/continuous", json={"on": "yes"}) + assert r.status_code == 400 + + +def test_continuous_sends_repeatedly_and_stops(client, udp_listener): + _port, recv_one = udp_listener + client.post("/api/inject", json={"lat": 1.0, "lon": 2.0}) + assert recv_one() == {"lat": 1.0, "lon": 2.0, "alt": 0.0} # 手动单发 + + r = client.post("/api/continuous", json={"on": True}) + assert r.get_json() == {"ok": True, "continuous": True} + assert recv_one() == {"lat": 1.0, "lon": 2.0, "alt": 0.0} # 服务端自动重发 + assert recv_one() == {"lat": 1.0, "lon": 2.0, "alt": 0.0} + + r = client.post("/api/continuous", json={"on": False}) + assert r.get_json() == {"ok": True, "continuous": False} + time.sleep(0.2) + while True: # 排空在途包 + try: + recv_one() + except socket.timeout: + break + with pytest.raises(socket.timeout): + recv_one() # 停止后不再发 + + +def test_continuous_uses_latest_inject(client, udp_listener): + _port, recv_one = udp_listener + client.post("/api/inject", json={"lat": 1.0, "lon": 2.0}) + recv_one() + client.post("/api/inject", json={"lat": 3.0, "lon": 4.0}) # 中途改坐标 + recv_one() + + client.post("/api/continuous", json={"on": True}) + assert recv_one() == {"lat": 3.0, "lon": 4.0, "alt": 0.0} + client.post("/api/continuous", json={"on": False}) + + +# --- /api/link -------------------------------------------------------------- +def test_link_get_default_and_options(client, udp_listener): + port, _recv = udp_listener + j = client.get("/api/link").get_json() + assert j["host"] == "127.0.0.1" + assert j["port"] == port + assert any(o["host"] == "127.0.0.1" for o in j["options"]) + + +def test_link_set_bad_host_or_port_400(client): + assert client.post("/api/link", json={"host": " "}).status_code == 400 + assert client.post("/api/link", + json={"host": "1.2.3.4", "port": "x"}).status_code == 400 + assert client.post("/api/link", + json={"host": "1.2.3.4", "port": 70000}).status_code == 400 + + +def test_link_switch_redirects_inject_and_command(client, udp_listener): + _port_a, recv_a = udp_listener + s_b = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s_b.bind(("127.0.0.1", 0)) + s_b.settimeout(1.5) + port_b = s_b.getsockname()[1] + try: + r = client.post("/api/link", json={"host": "127.0.0.1", "port": port_b}) + assert r.get_json() == {"ok": True, "host": "127.0.0.1", "port": port_b} + + client.post("/api/inject", json={"lat": 1.0, "lon": 2.0}) + data, _addr = s_b.recvfrom(2048) + assert json.loads(data.decode("utf-8")) == {"lat": 1.0, "lon": 2.0, + "alt": 0.0} + client.post("/api/command", json={"cmd": "rtl"}) + data, _addr = s_b.recvfrom(2048) + assert json.loads(data.decode("utf-8")) == {"cmd": "rtl"} + + with pytest.raises(socket.timeout): + recv_a() # 旧目标不再收到任何包 + finally: + s_b.close() + + +# --- /api/state ------------------------------------------------------------- +def test_state_empty_log_dir(client): + j = client.get("/api/state").get_json() + assert j["state"] == "unknown" + assert j["telemetry"] is None + assert j["continuous"] is False + + +def test_video_disabled_by_default(client): + assert client.get("/api/state").get_json()["video"] == { + "enabled": False, "online": False} + assert client.get("/video_feed").status_code == 503 + + +def test_state_reports_continuous(client): + client.post("/api/inject", json={"lat": 1.0, "lon": 2.0}) + client.post("/api/continuous", json={"on": True}) + assert client.get("/api/state").get_json()["continuous"] is True + client.post("/api/continuous", json={"on": False}) + assert client.get("/api/state").get_json()["continuous"] is False diff --git a/utils/tests/ground_station/video_source_test.py b/utils/tests/ground_station/video_source_test.py new file mode 100644 index 0000000..0bf7492 --- /dev/null +++ b/utils/tests/ground_station/video_source_test.py @@ -0,0 +1,138 @@ +"""video_source_test.py —— RTSP→MJPEG 视频源单元测试(注入假采集器,不依赖真流)。 + +覆盖:拉帧→JPEG 编码缓存、宽度限制(等比缩放)、断线重连(重新调用工厂)、 +打开失败退避重试、online 判定、MJPEG 分块格式。 +""" + +import sys +import time +from pathlib import Path + +import numpy as np + +_ROOT = Path(__file__).resolve().parents[3] # utils/tests/ground_station → 仓库根 +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +from ground_station.video_source import RtspVideoSource # noqa: E402 + + +class FakeCapture: + """假采集器:可注入读失败次数;帧为纯色小图。""" + + def __init__(self, fail_reads=0, height=60, width=120): + self.fail_reads = fail_reads + self._frame = np.full((height, width, 3), 128, dtype=np.uint8) + self.released = False + + def isOpened(self): + return True + + def read(self): + if self.fail_reads > 0: + self.fail_reads -= 1 + return False, None + return True, self._frame + + def release(self): + self.released = True + + +def _wait(pred, timeout_s=4.0, step=0.02): + t0 = time.monotonic() + while time.monotonic() - t0 < timeout_s: + if pred(): + return True + time.sleep(step) + return False + + +def _decode(jpeg): + import cv2 + return cv2.imdecode(np.frombuffer(jpeg, dtype=np.uint8), cv2.IMREAD_COLOR) + + +def test_frames_encoded_and_online(): + src = RtspVideoSource("rtsp://fake", width=120, fps=50, + capture_factory=lambda: FakeCapture()) + assert src.online is False # 尚无帧 → 离线 + src.start() + try: + assert _wait(lambda: src.latest()[1] is not None) + seq, jpeg = src.latest() + assert seq >= 1 + assert jpeg[:2] == b"\xff\xd8" # JPEG SOI 头 + assert _decode(jpeg).shape[1] == 120 # 宽度不超限 + assert src.online is True + finally: + src.stop() + + +def test_downscale_to_max_width(): + src = RtspVideoSource("rtsp://fake", width=60, fps=50, + capture_factory=lambda: FakeCapture(height=60, width=120)) + src.start() + try: + assert _wait(lambda: src.latest()[1] is not None) + img = _decode(src.latest()[1]) + assert img.shape[1] == 60 and img.shape[0] == 30 # 等比缩到 60 宽 + finally: + src.stop() + + +def test_reconnect_after_read_failure(): + opened = [] + + def factory(): + cap = FakeCapture(fail_reads=(10 ** 6 if not opened else 0)) + opened.append(cap) + return cap + + src = RtspVideoSource("rtsp://fake", width=120, fps=50, + capture_factory=factory) + src.start() + try: + assert _wait(lambda: src.latest()[1] is not None, timeout_s=5.0) + assert len(opened) >= 2 # 首采集器读失败 → 重新打开 + assert opened[0].released is True + finally: + src.stop() + + +def test_open_failure_keeps_retrying(): + calls = [] + + class ClosedCapture: + def isOpened(self): + return False + + def release(self): + pass + + def factory(): + calls.append(1) + return ClosedCapture() + + src = RtspVideoSource("rtsp://fake", capture_factory=factory) + src.start() + try: + assert _wait(lambda: len(calls) >= 2, timeout_s=4.0) # 1s 退避后重试 + assert src.latest()[1] is None + finally: + src.stop() + + +def test_mjpeg_stream_chunks(): + src = RtspVideoSource("rtsp://fake", width=120, fps=50, + capture_factory=lambda: FakeCapture()) + src.start() + try: + assert _wait(lambda: src.latest()[1] is not None) + gen = src.mjpeg_stream() + chunk = next(gen) + assert chunk.startswith(b"--frame\r\nContent-Type: image/jpeg\r\n") + assert b"Content-Length: " in chunk + assert chunk.endswith(b"\r\n") + gen.close() + finally: + src.stop() diff --git a/utils/tests/pod_bridge/bridge_test.py b/utils/tests/pod_bridge/bridge_test.py index 84c1818..8542323 100644 --- a/utils/tests/pod_bridge/bridge_test.py +++ b/utils/tests/pod_bridge/bridge_test.py @@ -6,6 +6,7 @@ start/stop 生命周期与停机刹车、run() 主循环异常护栏与故障恢 """ import asyncio +import math import time from dataclasses import replace @@ -29,6 +30,7 @@ class FakePx4: self.offboard_start_ok = True self.velocity_calls = [] self.brake_calls = [] + self.yaw_calls = [] def own_state(self): return replace(self._state) @@ -54,6 +56,10 @@ class FakePx4: self.brake_calls.append(time_boot_ms) return True + async def send_yaw_rate(self, yaw_rate_dps): + self.yaw_calls.append(yaw_rate_dps) + return True + class FakePod: def __init__(self): @@ -159,6 +165,116 @@ def test_attitude_downlink_throttled(): assert len(pod.sent) == 2 +def test_control_law_throttled_to_own_frequency(): + """生效律按自身频率推进:L2 配置 10Hz,外部 20Hz 调用时控制律只跑 10 次/秒。 + + 链路维持(表3 姿态下发)不受此节流影响——否则吊舱解算会断流。 + """ + px4 = FakePx4(_valid_state(1000)) + pod = FakePod() + cfg = BridgeConfig() + cfg.heading_servo.enabled = False # L2 地理引导(10Hz) + cfg.guidance.frequency_hz = 10.0 + cfg.link.attitude_downlink_hz = 20.0 + cfg.ground_inject.enabled = True + ground = GroundTarget(latitude_deg=0.01, longitude_deg=0.0, time_ms=1000) + b = PodBridge(cfg, px4, pod, now_ms_fn=lambda: 1000, + ground_target_fn=lambda: ground) + + # 统计控制律实际推进次数(包装 update,不改变行为) + law_updates = {"n": 0} + real_update = b._guidance.update + + def counting_update(inp, now): + law_updates["n"] += 1 + return real_update(inp, now) + + b._guidance.update = counting_update + + ticks = 10 # 20Hz 调用 10 拍 = 500ms + + async def scenario(): + for i in range(ticks): + await b.step(1000 + i * 50) + asyncio.run(scenario()) + + # 10Hz 下 500ms 只应推进 5 拍控制律;其余 5 拍重发上一拍输出保心跳 + assert law_updates["n"] == 5 + assert len(px4.velocity_calls) == ticks # offboard 设定点每拍都在下发 + assert len(pod.sent) == ticks # 表3 姿态下发不受控制律节流影响 + + +def test_control_law_throttle_resets_on_law_switch(): + """切律后立即出第一拍(不被上一拍的节流压住)。""" + px4 = FakePx4(_valid_state(1000)) + pod = FakePod() + cfg = BridgeConfig() + cfg.guidance.frequency_hz = 10.0 + b = _bridge(px4, pod, cfg) + + async def scenario(): + await b.step(1000) + b.set_heading_servo_enabled(True) + await b.step(1010) # 切律后 10ms 即应推进 + asyncio.run(scenario()) + assert b._last_law_ms == 1010 + + +def test_stop_is_idempotent(): + """重复 stop() 只真正停机一次(真机在环实测:RTL 与收尾各调一次)。""" + px4 = FakePx4(_valid_state(1000)) + pod = FakePod() + b = _bridge(px4, pod) + + async def scenario(): + await b.start() + await b.stop() + await b.stop() # 第二次应为 no-op + asyncio.run(scenario()) + + assert pod.closed is True + assert px4.offboard_stopped is True + assert len(px4.brake_calls) == 1 # 刹车只发一次 + + +def test_start_after_stop_reenables_stop(): + """停机后重新 start() 可再次生效(标志位复位)。""" + px4 = FakePx4(_valid_state(1000)) + pod = FakePod() + b = _bridge(px4, pod) + + async def scenario(): + await b.start() + await b.stop() + assert b._stopped is True + await b.start() + assert b._stopped is False + await b.stop() + asyncio.run(scenario()) + assert len(px4.brake_calls) == 2 # 两次停机各发一次刹车 + + +def test_landing_stop_keeps_telemetry_and_pod_open(): + """降落前只退 offboard:遥测与串口保留,供落地判定与高度曲线。""" + px4 = FakePx4(_valid_state(1000)) + pod = FakePod() + b = _bridge(px4, pod) + + async def scenario(): + await b.start() + await b.landing_stop() + assert b._landing_stopped is True + assert b._stopped is False # 未全停 + assert px4.offboard_stopped is True + assert px4.telemetry_stopped is False # 遥测仍在 + assert pod.closed is False # 串口仍在 + await b.stop() # 收尾才全停 + asyncio.run(scenario()) + assert px4.telemetry_stopped is True + assert pod.closed is True + assert len(px4.brake_calls) == 2 # 两次停机各发一次刹车 + + # --- 吊舱帧入站缓存 -------------------------------------------------------- def test_target_frame_updates_latest_and_flips_to_velocity(): px4 = FakePx4(_valid_state(1000)) @@ -423,6 +539,12 @@ def _locked_off_target(x=0.0, y=0.0, frame_yaw=0.0, frame_pitch=0.0, laser=50.0) frame_yaw_deg=frame_yaw, frame_pitch_deg=frame_pitch) +def _set_pose(px4, now_ms: int, yaw_deg: float): + """更新假飞控姿态与时间戳(保证控制律的姿态超龄判定通过)。""" + px4._state = replace(px4._state, yaw_rad=math.radians(yaw_deg), + time_ms=now_ms, position_time_ms=now_ms) + + def test_l1_off_target_beta_drives_yaw_rate(): px4 = FakePx4(_valid_state(1000)) pod = FakePod() @@ -503,26 +625,73 @@ def test_l1_no_telemetry_degrades_and_keeps_heartbeat(): assert len(px4.brake_calls) == 1 -def test_l1_require_vision_blocks_without_source(): - # §2.3 第二判据:require_vision=True 但未接视觉源 → fail-safe 视为无目标 → 刹车 - px4 = FakePx4(_valid_state(1000)) +def test_l1_los_consistent_keeps_tracking(): + """机头右转多少、脱靶量就左移多少(目标在世界系静止)→ 判一致,正常跟踪。""" + clock = {"ms": 1_000_000} + px4 = FakePx4(_valid_state(clock["ms"])) pod = FakePod() - pod.queue(_locked_off_target(x=10.0)) - b = _bridge(px4, pod, _l1_config(require_vision=True)) - out = asyncio.run(b.step(1000)) - assert out.mode.name == "BRAKE_HOVER" - assert px4.velocity_calls == [] + b = PodBridge(_l1_config(), px4, pod, now_ms_fn=lambda: clock["ms"]) + async def scenario(): + for i in range(80): # 4s,20°/s 右转、脱靶量 −20°/s + clock["ms"] += 50 + yaw = 20.0 * (i + 1) * 0.05 + beta = 10.0 - 20.0 * (i + 1) * 0.05 + _set_pose(px4, clock["ms"], yaw) + pod.queue(_locked_off_target(x=beta)) + out = await b.step(clock["ms"]) + return out -def test_l1_require_vision_passes_with_source(): - px4 = FakePx4(_valid_state(1000)) + out = asyncio.run(scenario()) + assert b._los_consistent is True + assert out.mode.name == "VELOCITY_HEADING" + assert b.pod_locked(clock["ms"]) is True + + +def test_l1_los_frozen_angle_blocks_tracking(): + """机头转了 60° 而脱靶量几乎不变 → 角度冻结/幽灵目标 → 判丢(刹车)。""" + clock = {"ms": 1_000_000} + px4 = FakePx4(_valid_state(clock["ms"])) pod = FakePod() - pod.queue(_locked_off_target(x=10.0)) - b = PodBridge(_l1_config(require_vision=True), px4, pod, - now_ms_fn=lambda: 0, vision_target_fn=lambda: True) - out = asyncio.run(b.step(1000)) + b = PodBridge(_l1_config(), px4, pod, now_ms_fn=lambda: clock["ms"]) + + async def scenario(): + out = None + for i in range(80): # 4s,右转 20°/s,脱靶量恒定 10° + clock["ms"] += 50 + yaw = 20.0 * (i + 1) * 0.05 + _set_pose(px4, clock["ms"], yaw) + pod.queue(_locked_off_target(x=10.0)) + out = await b.step(clock["ms"]) + return out + + out = asyncio.run(scenario()) + assert b._los_consistent is False + assert out.mode.name == "BRAKE_HOVER" # 控制律被喂 tracked=False + assert b.pod_locked(clock["ms"]) is False # 状态机侧也判为丢目标 + + +def test_l1_los_disabled_falls_back_to_bit2(): + """关掉一致性判据 → 退回"只信 bit2"(角度冻结也不再判丢)。""" + clock = {"ms": 1_000_000} + px4 = FakePx4(_valid_state(clock["ms"])) + pod = FakePod() + b = PodBridge(_l1_config(los_consistency_enabled=False), px4, pod, + now_ms_fn=lambda: clock["ms"]) + + async def scenario(): + out = None + for i in range(80): + clock["ms"] += 50 + yaw = 20.0 * (i + 1) * 0.05 + _set_pose(px4, clock["ms"], yaw) + pod.queue(_locked_off_target(x=10.0)) + out = await b.step(clock["ms"]) + return out + + out = asyncio.run(scenario()) + assert b._los_consistent is True assert out.mode.name == "VELOCITY_HEADING" - assert out.yaw_rate_dps == pytest.approx(10.0, abs=1e-9) # --- 地面坐标注入(A2)------------------------------------------------------- diff --git a/utils/tests/pod_bridge/ground_link_test.py b/utils/tests/pod_bridge/ground_link_test.py index 930cf86..6a39ac7 100644 --- a/utils/tests/pod_bridge/ground_link_test.py +++ b/utils/tests/pod_bridge/ground_link_test.py @@ -1,12 +1,14 @@ -"""ground_link_test.py —— 地面坐标注入通道单元测试。 +"""ground_link_test.py —— 地面站链路单元测试。 覆盖:parse_payload 解析(合法/缺字段/非法 JSON/非对象/类型错误)、 -is_valid 新鲜度判定(未注入/新鲜/超龄边界)。 +parse_message 双通道(坐标 + {"cmd"} 命令)、命令队列 drain、 +is_valid 新鲜度判定(未注入/新鲜/超龄边界)、run() 真 UDP 回环。 """ import pytest -from pod_bridge.ground_link import GroundInjectConfig, GroundLink, GroundTarget +from pod_bridge.ground_link import (GroundCommand, GroundInjectConfig, + GroundLink, GroundTarget) # --- parse_payload ---------------------------------------------------------- @@ -92,3 +94,108 @@ def test_latest_returns_injected_target(): _inject(link, 1000) assert link.latest().latitude_deg == pytest.approx(1.0) assert link.latest().time_ms == 1000 + + +# --- parse_message(坐标 + 命令双通道)--------------------------------------- +def test_parse_message_command(): + m = GroundLink.parse_message(b'{"cmd": "rtl"}') + assert isinstance(m, GroundCommand) + assert m.name == "rtl" + + +def test_parse_message_command_normalized(): + m = GroundLink.parse_message(b'{"cmd": " RTL "}') + assert isinstance(m, GroundCommand) + assert m.name == "rtl" + + +def test_parse_message_cmd_takes_precedence_over_coords(): + m = GroundLink.parse_message(b'{"cmd": "emergency", "lat": 1.0, "lon": 2.0}') + assert isinstance(m, GroundCommand) + assert m.name == "emergency" + + +def test_parse_message_coords_still_work(): + m = GroundLink.parse_message(b'{"lat": 1.0, "lon": 2.0}') + assert isinstance(m, GroundTarget) + assert m.latitude_deg == pytest.approx(1.0) + + +def test_parse_message_empty_cmd_falls_back_to_coords(): + m = GroundLink.parse_message(b'{"cmd": "", "lat": 1.0, "lon": 2.0}') + assert isinstance(m, GroundTarget) + + +def test_parse_message_non_string_cmd_falls_back_to_coords(): + m = GroundLink.parse_message(b'{"cmd": 5, "lat": 1.0, "lon": 2.0}') + assert isinstance(m, GroundTarget) + + +def test_parse_message_invalid_returns_none(): + assert GroundLink.parse_message(b"not json") is None + + +# --- drain_commands --------------------------------------------------------- +def test_drain_commands_empty(): + assert _link().drain_commands() == [] + + +def test_drain_commands_fifo_and_clear(): + link = _link() + link._commands.append("rtl") + link._commands.append("pod_center") + assert link.drain_commands() == ["rtl", "pod_center"] + assert link.drain_commands() == [] # 取后清空 + + +def test_commands_queue_drops_oldest_when_full(): + link = _link() + for i in range(20): + link._commands.append(f"c{i}") + cmds = link.drain_commands() + assert len(cmds) == 16 # maxlen=16 + assert cmds[0] == "c4" and cmds[-1] == "c19" + + +# --- run() 真 UDP 回环(坐标 + 命令)----------------------------------------- +def test_run_receives_coords_and_commands(): + import asyncio + import socket as socket_mod + + probe = socket_mod.socket(socket_mod.AF_INET, socket_mod.SOCK_DGRAM) + probe.bind(("127.0.0.1", 0)) + port = probe.getsockname()[1] + probe.close() + + link = GroundLink(GroundInjectConfig(host="127.0.0.1", port=port), + now_ms_fn=lambda: 1234) + addr = ("127.0.0.1", port) + + async def scenario(): + stop = asyncio.Event() + task = asyncio.ensure_future(link.run(stop)) + udp = socket_mod.socket(socket_mod.AF_INET, socket_mod.SOCK_DGRAM) + try: + for _ in range(200): # 重发防 bind 竞态 + udp.sendto(b'{"lat": 1.5, "lon": 2.5}', addr) + await asyncio.sleep(0.01) + if link.latest().time_ms == 1234: + break + udp.sendto(b'{"cmd": "rtl"}', addr) + for _ in range(100): + if link._commands: + break + await asyncio.sleep(0.01) + finally: + udp.close() + stop.set() + task.cancel() # 收包循环阻塞在 sock_recvfrom,须 cancel 唤醒(同 SITL 入口) + try: + await task + except asyncio.CancelledError: + pass + + asyncio.run(scenario()) + assert link.latest().latitude_deg == pytest.approx(1.5) + assert link.latest().time_ms == 1234 + assert link.drain_commands() == ["rtl"] diff --git a/utils/tests/pod_bridge/los_consistency_test.py b/utils/tests/pod_bridge/los_consistency_test.py new file mode 100644 index 0000000..8d6a618 --- /dev/null +++ b/utils/tests/pod_bridge/los_consistency_test.py @@ -0,0 +1,141 @@ +"""los_consistency_test.py —— 吊舱角度运动学一致性判据的纯逻辑测试。 + +背景:本吊舱无电机、相机刚性固连机体,机头转 Δψ 时真实目标脱靶量必变 ≈−Δψ。 +判据 `residual = Δβ + Δψ`:超限且激励足够 → 判不一致(角度冻结/幽灵目标)。 +""" + +import pytest + +from pod_bridge.los_consistency import (LosConsistencyConfig, + LosConsistencyMonitor, wrap_deg_180) + + +def _mon(cfg=None): + return LosConsistencyMonitor(cfg or LosConsistencyConfig()) + + +def _drive(mon, n, dt_ms, yaw0=0.0, beta0=0.0, yaw_rate=0.0, beta_rate=0.0, + t0=0): + """按固定角速度喂 n 拍,返回最后一次状态。""" + status = None + for i in range(n): + t = t0 + (i + 1) * dt_ms + yaw = yaw0 + yaw_rate * (i + 1) * dt_ms / 1000.0 + beta = beta0 + beta_rate * (i + 1) * dt_ms / 1000.0 + status = mon.update(t, yaw, beta) + return status + + +# --- 条件开关 --------------------------------------------------------------- +def test_disabled_always_consistent(): + mon = _mon(LosConsistencyConfig(enabled=False)) + status = _drive(mon, 100, 50, yaw_rate=30.0, beta_rate=0.0) + assert status.consistent is True + assert status.excited is False + + +def test_no_history_is_not_excited(): + mon = _mon() + status = mon.update(1000, 0.0, 10.0) + assert status.consistent is True + assert status.excited is False + + +def test_insufficient_span_is_not_excited(): + """窗口未积累到 3/4 之前不做判断(避免一开始就误判丢目标)。""" + mon = _mon(LosConsistencyConfig(window_ms=3000)) + status = _drive(mon, 20, 50, yaw_rate=40.0, beta_rate=0.0) # 仅 1s + assert status.excited is False + assert status.consistent is True + + +# --- 一致:机头转、脱靶量反向跟随 ------------------------------------------- +def test_consistent_rotation_is_ok(): + """右转 20°/s 且 β 以 −20°/s 变化 → 残差≈0。""" + mon = _mon() + status = _drive(mon, 80, 50, yaw_rate=20.0, beta_rate=-20.0) # 4s + assert status.excited is True + assert status.consistent is True + assert status.residual_deg == pytest.approx(0.0, abs=0.5) + + +def test_consistent_left_rotation_is_ok(): + """左转(负角速度)且 β 正向跟随 → 同样一致(符号对称)。""" + mon = _mon() + status = _drive(mon, 80, 50, yaw_rate=-20.0, beta_rate=20.0) + assert status.excited is True + assert status.consistent is True + + +def test_target_motion_within_residual_tolerance_is_ok(): + """目标自身在动(β 变化与 −Δψ 不完全相等)但残差在容差内 → 仍判一致。""" + mon = _mon(LosConsistencyConfig(max_residual_deg=8.0)) + # 4s 内 Δψ=80°,β 只反向变 76° → 残差 4°(< 8°) + status = _drive(mon, 80, 50, yaw_rate=20.0, beta_rate=-19.0) + assert status.excited is True + assert status.consistent is True + + +# --- 不一致:角度冻结/幽灵目标 --------------------------------------------- +def test_frozen_beta_while_yawing_is_inconsistent(): + """机头转 80° 而脱靶量纹丝不动 → 角度冻结 → 判不一致。 + + 比较窗口为 3s(默认),故 4s 数据里只比较最后 3s:Δψ = 20°/s × 3s = 60°。 + """ + mon = _mon() + status = _drive(mon, 80, 50, yaw_rate=20.0, beta_rate=0.0) + assert status.excited is True + assert status.consistent is False + assert status.frozen is True + assert status.residual_deg == pytest.approx(60.0, abs=2.0) # 窗口内 Δψ + + +def test_small_yaw_change_does_not_judge_even_if_residual_big(): + """转得不够多(< 激励门槛)时不判不一致 —— 避免误杀。""" + mon = _mon(LosConsistencyConfig(min_yaw_change_deg=20.0)) + # 窗口内只转 8°,虽然 β 没动(残差 8°)但未达激励门槛 + status = _drive(mon, 80, 50, yaw_rate=2.0, beta_rate=0.0) + assert status.excited is False + assert status.consistent is True + + +# --- 历史管理 --------------------------------------------------------------- +def test_reset_clears_history(): + mon = _mon() + _drive(mon, 80, 50, yaw_rate=20.0, beta_rate=0.0) + assert mon._samples # 有历史 + mon.reset() + assert not mon._samples + status = mon.update(9999, 0.0, 0.0) + assert status.excited is False + + +def test_old_samples_dropped(): + """超出历史保留期的样本被丢弃,不会永久污染判定。""" + mon = _mon(LosConsistencyConfig(window_ms=1000, history_factor=2.0)) + _drive(mon, 40, 50, yaw_rate=50.0, beta_rate=0.0) # 2s + # 再喂一拍"很久之后",旧样本应被清掉 + status = mon.update(60_000, 0.0, 0.0) + assert status.excited is False + + +# --- 角度回绕 --------------------------------------------------------------- +@pytest.mark.parametrize("deg,expect", [ + (0.0, 0.0), (179.0, 179.0), (181.0, -179.0), + (-181.0, 179.0), (359.0, -1.0), +]) +def test_wrap_deg_180(deg, expect): + assert wrap_deg_180(deg) == pytest.approx(expect) + + +def test_wrap_around_north_is_consistent(): + """航向跨过 ±180 与 β 同步变化时,不应因回绕误判。""" + mon = _mon() + # 从 yaw=170 转到 190(即 -170),β 从 10 到 -10 + for i in range(80): + t = (i + 1) * 50 + yaw = 170.0 + 20.0 * (i + 1) * 0.05 + beta = 10.0 - 20.0 * (i + 1) * 0.05 + status = mon.update(t, wrap_deg_180(yaw), beta) + assert status.excited is True + assert status.consistent is True diff --git a/utils/tests/pod_bridge/px4_gateway_test.py b/utils/tests/pod_bridge/px4_gateway_test.py index 6e83885..722d3e5 100644 --- a/utils/tests/pod_bridge/px4_gateway_test.py +++ b/utils/tests/pod_bridge/px4_gateway_test.py @@ -43,11 +43,13 @@ class FakeOffboard: class FakeTelemetry: """各遥测流:先吐预设消息,再挂起(由 stop_telemetry 取消)。""" - def __init__(self, position=None, attitude=None, gps=None, velocity=None): + def __init__(self, position=None, attitude=None, gps=None, velocity=None, + status=None): self._position = position or [] self._attitude = attitude or [] self._gps = gps or [] self._velocity = velocity or [] + self._status = status or [] async def _stream(self, msgs): for m in msgs: @@ -66,6 +68,9 @@ class FakeTelemetry: def velocity_ned(self): return self._stream(self._velocity) + def status_text(self): + return self._stream(self._status) + class FakeDrone: def __init__(self, telemetry=None): @@ -197,6 +202,35 @@ def test_start_telemetry_populates_cache_then_stop(): asyncio.run(scenario()) +# --- 飞控状态文本(STATUSTEXT)---------------------------------------------- +class Sev(Enum): + INFO = 1 + WARNING = 2 + + +def test_update_fc_status_and_take_messages(): + gw = Px4Gateway(FakeDrone()) + gw.update_fc_status(SimpleNamespace(type=Sev.WARNING, text="Low battery")) + gw.update_fc_status(SimpleNamespace(type="INFO", text="Takeoff detected")) + assert gw.take_fc_messages() == [("WARNING", "Low battery"), + ("INFO", "Takeoff detected")] + assert gw.take_fc_messages() == [] # 取后清空 + + +def test_status_text_subscription_captures_messages(): + telemetry = FakeTelemetry(status=[ + SimpleNamespace(type=Sev.WARNING, text="EKF variance")]) + gw = Px4Gateway(FakeDrone(telemetry)) + + async def scenario(): + await gw.start_telemetry() + await asyncio.sleep(0.05) + assert gw.take_fc_messages() == [("WARNING", "EKF variance")] + await gw.stop_telemetry() + + asyncio.run(scenario()) + + def test_default_own_state_is_invalid(): s = OwnState() assert s.has_position is False and s.position_valid is False diff --git a/utils/tests/stereo_ranger/__pycache__/config_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_ranger/__pycache__/config_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 512adf7dd97a50de7faceda2ffb06618d753b83b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13539 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 diff --git a/utils/tests/stereo_ranger/__pycache__/consistency_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_ranger/__pycache__/consistency_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index d496e09c465d38688eadc9ae4120719ffcfb7153..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14578 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 qJ#9 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 diff --git a/utils/tests/stereo_ranger/__pycache__/geometry_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_ranger/__pycache__/geometry_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 9c90b6565133a326af1999c4b2d76ec994cb6bb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33605 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 W)R@QqPXjLJ5$9(C#rY5`>;DJubskp$ diff --git a/utils/tests/stereo_ranger/__pycache__/matching_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_ranger/__pycache__/matching_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 1bd698d44710b88e3fcb820c2d9aa8aacb52e182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21391 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 XstNfzeIL~Ye9a$6{k{V}#_j(BQaeLK diff --git a/utils/tests/stereo_ranger/__pycache__/stereo_ranger_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_ranger/__pycache__/stereo_ranger_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 7327cc590b872b2ef25989e406988920ccd9162f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80459 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

mE`@C{#ssw@ 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 diff --git a/utils/tests/stereo_ranger/__pycache__/warn_throttle_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_ranger/__pycache__/warn_throttle_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 81acf037efed15ef81f672003a73c199c567d486..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11571 zcmeGiTWl29_1>M?=d8U6kK!~1)+`2ZW2n~w0u@+Ic@!yqNR@idy)(1p z%{qpk$Z7WO&Y5%1oO|caeVlt9_is&25d!J&zxsIaEk7Y&V8%&48+qId$nS_o`iMp~ zUykf{3Y33@08KP4(B0qg}w>oQs|lH zMWit=oW88C7tbiwn`f(bUR_9YD~ zWZE{&q~DT;$2t*oA=rgr4}yJ9Vrj$BnaK;KQcy}B9!sSbkHb_vvWUjd;$Hzw(kH$H z2cVg+JzFy~g^_$s0}qJrf(9m|42>yE@qCOG`N{B@sT-!6X4dm+CY{TvOdn>M!q9M< z=~fgTtH(#OIWwC#tU#^+9ER1R+EpN{I*=Pubn|VdF zTD&dQxg^(RQPseg#GUk{=g^P)UuT5@mL58kHq-r?f~Kn)E9CVZ*fIShW;SQ^BWm;; zCVX2#<&9?jr@?Mjok3TTW9RUg)yli`M3!Nv=3&m9(%G1LawM-YUCbcX0SySjW&9Zi zZmX)>YFWor zxZ$vLR#06- z8Ai3(2BbvtXgaEfuICxXIFkyu-&qTCynx{Q2wp_+0|XlpY(lUZK^(yr0Fxo{#sm_! zB4|Uf4Z(H(OE#jzCr4= z43)bmw{L86xmA#B;OcHv$9M<0MWClTW|ZydB}s8F$@Ym#r2sJK^WxXWWQ|Mg* zC&{?&XaRIe+3u3RQkQrFZ)Q7S87#2`gFxVxU>?@C*Amp|4OgEn5fgcM(p*V)h+L)_ z-vG(@-du}rYS=~Gz`w&y;QWRY;(FhAX}wHJz7icEn*U=6lNWtOiR+JYQ8scWwuPrb zT;w%L3xMOAYC$YhbzpnS)H&+l*_o?#*W+`)Neg}C8i2$W)+2gYXmodYZB|}0Ow*p=(k(Y|#WKc-o9|EV>P1(ss zv6s>%GQx3b`!_}A6$5+N7R*8}uM={4#4rm(AdkT@c`BV_-N1(1fbli}E{w~qv$3sb zkN)dNyKcR4V{}H@J2^Vhdg17NG+v3u%WLPNZ4;wYa%M`|JH0<6kg5HdijtX^@ju5O z0YzkIcvj%kfIDJ zZlsE0bidAGaBASsakOjFGNzV&D#&AMeeDUKWzPka>lY3#!(S`+rTUp7w!r zK~>IrCuNqN3;KFqxA#`4(-liA@VSe61wL0mEqgA=J;&pzsZONsY0m}0+(2!qCM`4e z;hK{_)jq5v$WUZ~kW{{6WH&~>-t7FwM;?^zXb)GmxxQRgrRyo&9(~*njBzmzp{Sgm zW?XN6Y9+f1CR-iVxkZR*tPg@}hHe@vk8>I9RVa|aLO%z+psF*=(RsP0BDajMos~D< zk)yxud8cPqUbkA6u+*X!-BD zEpsRtgON=UgPsOesM@-B;PK19(MVxR^9M_234V*W}u-5GbtRqyz%1356;Ymx7-b_8;2O&#(zsB zl9&%~sf4#Y;j{E=Q>S#xzb}}KSwU6R3K>ia0_r#l`$=cAi=ZT|zdl()tJ diff --git a/utils/tests/stereo_yolo/__pycache__/config_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_yolo/__pycache__/config_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index ee6a2b5f863dc571f986ee5d2886b12648db8f62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7232 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 NSb3?=A=I+Q{QvXjxEKHc diff --git a/utils/tests/stereo_yolo/__pycache__/sbs_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/stereo_yolo/__pycache__/sbs_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 565f8808aab27a368093a785669e9b8b6b7b7e02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5139 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 QmWcbruebWedq|=G2T1A7>;M1& diff --git a/utils/tests/target_tracker/__pycache__/target_tracker_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/target_tracker/__pycache__/target_tracker_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 2a62dd2728ef7aad7e63ece2cb67a97539bc4798..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73797 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 QLxFR}r=Ak$R`v}4ABNkiq5uE@ diff --git a/utils/tests/target_tracker/__pycache__/tracklet_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/target_tracker/__pycache__/tracklet_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 78c56a80979b92ef0becd7c752c711b47eee907b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47576 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 Tx$4Twdgo1l+U$3(V)y=kmhJAa diff --git a/utils/tests/task_runner/log_test.py b/utils/tests/task_runner/log_test.py index c68cbad..866d5b1 100644 --- a/utils/tests/task_runner/log_test.py +++ b/utils/tests/task_runner/log_test.py @@ -1,7 +1,7 @@ """log_test.py —— 任务日志模块单元测试。 覆盖:open 建目录/写表头、事件 JSONL、遥测 CSV、telemetry_every_n 节流、 -disabled 时零副作用、close 收尾。 +disabled 时零副作用、close 收尾、简洁任务简报(task_brief.log)。 """ import csv @@ -121,3 +121,84 @@ def test_append_keeps_existing_rows(tmp_path): assert len(rows) == 2 assert rows[0]["state"] == "wait" assert rows[1]["state"] == "goto" + + +# --- 简洁任务简报(task_brief.log)------------------------------------------- +def test_brief_log_lines_and_summary(tmp_path): + logger = TaskLogger(_cfg(tmp_path)) + logger.open() + logger.log_event(1000, "state", from_state="wait", to_state="wait", + reason="任务启动") + logger.log_event(2000, "state", from_state="wait", to_state="arm_takeoff", + reason="收到地面坐标") + logger.log_event(3000, "command", name="rtl") + logger.log_event(4000, "fc_status", severity="WARNING", text="Low battery\t") + logger.log_event(5000, "takeoff", altitude_m=5.0) + logger.log_event(6000, "rtl") + logger.log_telemetry(1000, TelemetryRecord( + state="wait", relative_altitude_m=0.0, distance_to_target_m=33.5)) + logger.log_telemetry(1100, TelemetryRecord( + state="arm_takeoff", relative_altitude_m=5.2, distance_to_target_m=20.0)) + logger.log_telemetry(1200, TelemetryRecord( + state="arm_takeoff", relative_altitude_m=5.0, + distance_to_target_m=float("inf"))) + logger.close() + + text = (tmp_path / "task_brief.log").read_text(encoding="utf-8") + assert "任务开始" in text + assert "[00:00.0] 任务启动(当前: 待命)" in text + assert "[00:01.0] 阶段: 待命 → 起飞(收到地面坐标)" in text + assert "[00:02.0] 地面站命令: rtl" in text + assert "[00:03.0] 飞控 WARNING: Low battery" in text # 尾随 tab 已 strip + assert "[00:04.0] 起飞至 5.0 m" in text + assert "[00:05.0] 开始返航" in text + assert "本次运行汇总" in text + assert "时长 0.2s" in text # 最后一行遥测的 t_s + assert "最高 5.2m" in text + assert "最近距目标 20.0m" in text # inf 不计入 + assert "事件 6 条" in text + assert "状态链 待命→起飞" in text + + +def test_brief_required_no_file_when_disabled_or_closed(tmp_path): + logger = TaskLogger(_cfg(tmp_path, enabled=False)) + logger.open() + logger.log_event(1000, "rtl") + logger.close() + assert not (tmp_path / "task_brief.log").exists() + + +# --- 遥测落盘节流(2026-10-15:主循环 20Hz 不得每行同步 flush)---------------- +def test_telemetry_buffered_until_flush_interval(tmp_path): + """telemetry_flush_ms 内只写不 flush;close 兜底落盘,一行不丢。""" + logger = TaskLogger(_cfg(tmp_path, telemetry_flush_ms=1000)) + logger.open() + rec = TelemetryRecord(state="track") + logger.log_telemetry(1000, rec) # 首行:落盘一次并打戳 + for i in range(1, 19): # 其后 19 行都在 1s 内 → 不再 flush + logger.log_telemetry(1000 + i * 50, rec) + logger.close() + + with open(tmp_path / "task_telemetry.csv", encoding="utf-8") as f: + rows = list(csv.DictReader(f)) + assert len(rows) == 19 + + +def test_telemetry_flush_zero_flushes_every_row(tmp_path): + """telemetry_flush_ms=0 保留旧的「每行立即 flush」行为(兼容/调试)。""" + logger = TaskLogger(_cfg(tmp_path, telemetry_flush_ms=0)) + logger.open() + logger.log_telemetry(1000, TelemetryRecord(state="goto")) + assert logger._last_tel_flush_ms == 1000 + logger.close() + + +def test_events_still_flushed_immediately(tmp_path): + """事件(阶段切换)仍即时落盘:地面站页面实时性不受遥测节流影响。""" + logger = TaskLogger(_cfg(tmp_path, telemetry_flush_ms=10_000)) + logger.open() + logger.log_event(1000, "state", from_state="goto", to_state="search") + with open(tmp_path / "task_events.jsonl", encoding="utf-8") as f: + lines = [json.loads(x) for x in f if x.strip()] + assert len(lines) == 1 + logger.close() diff --git a/utils/tests/task_runner/pod_control_test.py b/utils/tests/task_runner/pod_control_test.py index d3075c7..0394569 100644 --- a/utils/tests/task_runner/pod_control_test.py +++ b/utils/tests/task_runner/pod_control_test.py @@ -1,7 +1,8 @@ """pod_control_test.py —— 吊舱目标识别/锁定控制器单元测试。 覆盖:arm_detection 发 0x07(识别,目标类型)+0x0F(自动锁定)、 -stop_tracking 发 0x0E(停止)+0x08(识别关)。 +stop_tracking 发 0x0E(停止)+0x08(识别关)、地面站手动命令 +stop_track(0x0E)/auto_lock(0x0F+0x10)/center(0x2B)。 """ from pod_bridge.protocol import ControlCode @@ -43,3 +44,31 @@ def test_stop_tracking_sends_stop_and_detect_off(): assert len(bridge.sent) == 2 assert bridge.sent[0].code == ControlCode.STOP_TRACK assert bridge.sent[1].code == ControlCode.TARGET_DETECT_OFF + + +def test_stop_track_sends_only_stop(): + """地面站「停跟踪」= 只发 0x0E(0x0F 常驻自动锁定仍生效,对齐按键 u)。""" + bridge = FakeBridge() + pc = PodController(bridge) + pc.stop_track() + + assert [c.code for c in bridge.sent] == [ControlCode.STOP_TRACK] + + +def test_auto_lock_sends_autolock_and_detect_to_track(): + """地面站「自动锁定」= 0x0F + 0x10(对齐按键 a)。""" + bridge = FakeBridge() + pc = PodController(bridge) + pc.auto_lock() + + assert [c.code for c in bridge.sent] == [ControlCode.AUTO_LOCK_ON_FOUND, + ControlCode.DETECT_TO_TRACK] + + +def test_center_sends_center(): + """地面站「归中」= 0x2B(对齐按键 c)。""" + bridge = FakeBridge() + pc = PodController(bridge) + pc.center() + + assert [c.code for c in bridge.sent] == [ControlCode.CENTER] diff --git a/utils/tests/task_runner/return_home_test.py b/utils/tests/task_runner/return_home_test.py new file mode 100644 index 0000000..ff73a96 --- /dev/null +++ b/utils/tests/task_runner/return_home_test.py @@ -0,0 +1,142 @@ +"""return_home_test.py —— 自定义返航(回起飞点 + 恢复初始航向)纯逻辑测试。 + +覆盖:转场速度/对准门控、到达判定、转场高度保持(含符号)、初始航向恢复、 +角度回绕、以及"不超过转场高度"的语义。 +""" + +import math + +import pytest + +from task_runner.return_home import (HomingController, ReturnConfig, + wrap_deg_180) + +HOME_LAT, HOME_LON = 47.397971, 8.546164 +_DEG_LAT_M = 111320.0 + + +def _offset(lat0, lon0, north_m, east_m): + return (lat0 + north_m / _DEG_LAT_M, + lon0 + east_m / (_DEG_LAT_M * math.cos(math.radians(lat0)))) + + +def _ctrl(cfg=None, initial_yaw_deg=0.0, transit_alt_m=5.0): + return HomingController(cfg or ReturnConfig(), HOME_LAT, HOME_LON, + math.radians(initial_yaw_deg), transit_alt_m) + + +# --- 角度回绕 --------------------------------------------------------------- +@pytest.mark.parametrize("deg,expect", [ + (0.0, 0.0), (179.0, 179.0), (181.0, -179.0), + (-181.0, 179.0), (360.0, 0.0), (-360.0, 0.0), +]) +def test_wrap_deg_180(deg, expect): + assert wrap_deg_180(deg) == pytest.approx(expect) + + +# --- 转场:朝起飞点飞 ------------------------------------------------------- +def test_far_away_aligned_commands_full_speed(): + """远在起飞点北侧、机头朝南(对准回家方向)→ 满速前进、几乎不转。""" + cfg = ReturnConfig(speed_mps=2.0) + c = _ctrl(cfg) + # 在起飞点正北 50m,机头朝南(yaw=180°)→ 应朝南飞(北向速度为负) + lat, lon = _offset(HOME_LAT, HOME_LON, 50.0, 0.0) + out = c.update(lat, lon, math.radians(180.0), 5.0) + assert out.arrived is False + assert out.distance_m == pytest.approx(50.0, rel=5e-2) + assert out.vx_mps == pytest.approx(2.0, rel=1e-2) # 对准 → 全速 + assert abs(out.yaw_rate_dps) < 1e-6 + + +def test_misaligned_gates_forward_speed(): + """机头与回家方向垂直(90°)→ 前向速度被 cos(err) 门控为 0,只转向。""" + c = _ctrl(ReturnConfig(speed_mps=2.0, yaw_rate_limit_dps=20.0)) + lat, lon = _offset(HOME_LAT, HOME_LON, 0.0, 50.0) # 起飞点正东 50m + out = c.update(lat, lon, math.radians(0.0), 5.0) # 机头朝北 + assert out.vx_mps == pytest.approx(0.0, abs=1e-3) # cos(90°)=0(含地理近似误差) + assert out.bearing_err_deg == pytest.approx(-90.0, abs=1e-2) # 回家方向在西 + assert out.yaw_rate_dps < 0.0 # 往左转(目标在西侧) + + +def test_yaw_rate_limited(): + c = _ctrl(ReturnConfig(yaw_kp=5.0, yaw_rate_limit_dps=20.0)) + lat, lon = _offset(HOME_LAT, HOME_LON, 0.0, 50.0) + out = c.update(lat, lon, math.radians(0.0), 5.0) + assert out.yaw_rate_dps == pytest.approx(-20.0) # 被限幅(左转) + + +def test_arrival_stops_forward_motion(): + """进入到达半径 → arrived,且不再有前向速度。""" + c = _ctrl(ReturnConfig(speed_mps=2.0, arrival_radius_m=3.0)) + lat, lon = _offset(HOME_LAT, HOME_LON, 1.0, 0.0) # 距起飞点 1m + out = c.update(lat, lon, math.radians(180.0), 5.0) + assert out.arrived is True + assert out.vx_mps == 0.0 + + +def test_distance_measured_to_home(): + c = _ctrl() + lat, lon = _offset(HOME_LAT, HOME_LON, 30.0, 40.0) # 3-4-5 三角形 → 50m + out = c.update(lat, lon, 0.0, 5.0) + assert out.distance_m == pytest.approx(50.0, rel=5e-2) + + +# --- 转场高度保持(NED 下正)------------------------------------------------ +def test_alt_hold_climbs_when_below_transit(): + c = _ctrl(transit_alt_m=5.0) + vz = c.alt_hold_vz(3.0) # 低于转场高度 + assert vz < 0.0 # NED 下正 → 负 = 爬升 + + +def test_alt_hold_descends_when_above_transit(): + c = _ctrl(transit_alt_m=5.0) + vz = c.alt_hold_vz(7.0) + assert vz > 0.0 # 正 = 下降 + + +def test_alt_hold_zero_at_target(): + assert _ctrl(transit_alt_m=5.0).alt_hold_vz(5.0) == pytest.approx(0.0) + + +def test_alt_hold_rate_limited(): + cfg = ReturnConfig(alt_kp=10.0, alt_limit_mps=0.5) + c = _ctrl(cfg, transit_alt_m=5.0) + assert c.alt_hold_vz(0.0) == pytest.approx(-0.5) # 限幅 + assert c.alt_hold_vz(100.0) == pytest.approx(0.5) + + +# --- 恢复初始航向 ----------------------------------------------------------- +def test_yaw_align_rate_and_direction(): + c = _ctrl(ReturnConfig(align_kp=1.0, align_rate_limit_dps=15.0), + initial_yaw_deg=0.0) + err, rate = c.yaw_align(math.radians(10.0)) # 当前偏右 10° + assert err == pytest.approx(-10.0) # 需要往左转 + assert rate == pytest.approx(-10.0) # 负 = 左转 + + +def test_yaw_align_limited(): + c = _ctrl(ReturnConfig(align_kp=10.0, align_rate_limit_dps=15.0), + initial_yaw_deg=0.0) + _, rate = c.yaw_align(math.radians(90.0)) + assert rate == pytest.approx(-15.0) + + +def test_yaw_aligned_within_tolerance(): + c = _ctrl(ReturnConfig(yaw_align_tolerance_deg=5.0), initial_yaw_deg=30.0) + assert c.yaw_aligned(math.radians(33.0)) is True # 差 3° < 5° + assert c.yaw_aligned(math.radians(40.0)) is False # 差 10° + + +def test_yaw_align_wraps_around_north(): + """初始航向 179°、当前 -179° → 误差 2°(不是 358°)。""" + c = _ctrl(initial_yaw_deg=179.0) + err, _ = c.yaw_align(math.radians(-179.0)) + assert abs(err) == pytest.approx(2.0, abs=1e-6) + + +# --- 配置默认值 ------------------------------------------------------------- +def test_default_config_is_non_climbing_and_enabled(): + cfg = ReturnConfig() + assert cfg.enabled is True + assert cfg.alt_limit_mps > 0.0 + assert cfg.speed_mps > 0.0 diff --git a/utils/tests/task_runner/runner_test.py b/utils/tests/task_runner/runner_test.py index a2c78ed..a24d471 100644 --- a/utils/tests/task_runner/runner_test.py +++ b/utils/tests/task_runner/runner_test.py @@ -25,10 +25,17 @@ class FakePx4: self.telemetry_started = 0 self.offboard_started = False self.brake_calls = [] + self.yaw_calls = [] # SEARCH 扫视下发记录 + self.velocity_calls = [] # send_velocity_heading 记录(返航转场用) + self.fc_msgs = [] def own_state(self): return replace(self._state) + def take_fc_messages(self): + msgs, self.fc_msgs = self.fc_msgs, [] + return msgs + async def start_telemetry(self): self.telemetry_started += 1 @@ -43,19 +50,26 @@ class FakePx4: pass async def send_velocity_heading(self, out, t=None): + self.velocity_calls.append(out) return True async def send_brake_hover(self, t=None): self.brake_calls.append(t) return True + async def send_yaw_rate(self, yaw_rate_dps): + self.yaw_calls.append(yaw_rate_dps) + return True + class FakeBridge: def __init__(self): self.started = False self.stopped = False + self.landing_stopped = False # landing_stop():只退 offboard、保留遥测 self.law = None # set_heading_servo_enabled 最后值 self.steps = 0 + self.search_steps = [] # SEARCH 态收到的扫视角速度 self._locked = False self._telemetry = None @@ -66,6 +80,9 @@ class FakeBridge: async def stop(self): self.stopped = True + async def landing_stop(self): + self.landing_stopped = True + def set_heading_servo_enabled(self, enabled): self.law = enabled @@ -75,6 +92,9 @@ class FakeBridge: async def step(self, now_ms): self.steps += 1 + async def search_step(self, now_ms, yaw_rate_dps): + self.search_steps.append(yaw_rate_dps) + @property def latest_telemetry(self): return self._telemetry @@ -83,10 +103,16 @@ class FakeBridge: class FakeGround: def __init__(self, target=None): self._target = target + self.commands = [] # 地面站命令队列(供 drain_commands 取走) def latest(self): return self._target or GroundTarget() + def drain_commands(self): + cmds = list(self.commands) + self.commands.clear() + return cmds + class FakeDrone: def __init__(self): @@ -96,6 +122,25 @@ class FakeDrone: self.rtl = False self.landed = False self.disarmed = False + self.telemetry = FakeDrone._Telemetry() + + class _Telemetry: + """模仿 MAVSDK `in_air()`:**异步生成器**,finalize 时取消底层流。 + + 真实实现在 finally 里 `in_air_stream.cancel()`;「取一帧就 break」只是 + 把生成器挂起,复用同一生成器即可避免反复订阅。 + """ + + def __init__(self, frames=(False,)): + self._frames = frames + self.cancelled = False + + async def in_air(self): + try: + for value in self._frames: + yield value + finally: + self.cancelled = True class _Action: def __init__(self, d): @@ -132,7 +177,8 @@ def _state(now_ms, lat=0.0, lon=0.0, rel_alt=0.0): def _make_runner(px4=None, bridge=None, ground=None, drone=None, - sm_cfg=None, vision_fn=None, pod_control=None): + sm_cfg=None, pod_control=None, + land_instead_of_rtl=False, now_ms_fn=None): cfg = BridgeConfig() cfg.ground_inject.enabled = True cfg.ground_inject.timeout_ms = 5000 @@ -143,9 +189,9 @@ def _make_runner(px4=None, bridge=None, ground=None, drone=None, TaskStateMachine(sm_cfg or TaskStateConfig()), ground or FakeGround(), drone or FakeDrone(), - vision_fn=vision_fn, - now_ms_fn=lambda: 1000, + now_ms_fn=now_ms_fn or (lambda: 1000), pod_control=pod_control, + land_instead_of_rtl=land_instead_of_rtl, ) @@ -180,6 +226,15 @@ def test_collect_takeoff_reached(): assert r._collect_inputs(1000).takeoff_reached is True +def test_collect_takeoff_reached_tolerance(): + """容差内(目标 5.0m,实际 4.6m ≥ 5.0-0.5)判到达;容差外(4.4m)判未到达。""" + sm_cfg = TaskStateConfig(takeoff_altitude_m=5.0) + px4_low = FakePx4(_state(1000, rel_alt=4.6)) + assert _make_runner(px4=px4_low, sm_cfg=sm_cfg)._collect_inputs(1000).takeoff_reached is True + px4_below = FakePx4(_state(1000, rel_alt=4.4)) + assert _make_runner(px4=px4_below, sm_cfg=sm_cfg)._collect_inputs(1000).takeoff_reached is False + + def test_collect_locked_from_bridge(): bridge = FakeBridge() bridge._locked = True @@ -188,14 +243,6 @@ def test_collect_locked_from_bridge(): assert r._collect_inputs(1000).target_locked is True -def test_collect_vision_second_judgement(): - r = _make_runner(vision_fn=lambda: True) - assert r._collect_inputs(1000).vision_has_target is True - - r2 = _make_runner(vision_fn=lambda: False) - assert r2._collect_inputs(1000).vision_has_target is False - - # --- 居中累加 --------------------------------------------------------------- def test_centered_accumulates_duration(): bridge = FakeBridge() @@ -257,6 +304,15 @@ def test_on_enter_rtl_stops_bridge_and_returns(): assert drone.rtl is True +def test_land_instead_of_rtl_lands(): + """--land 模式:收尾调 land 而不是 return_to_launch(真机就地回收)。""" + drone = FakeDrone() + r = _make_runner(drone=drone, land_instead_of_rtl=True) + asyncio.run(r._on_enter(TaskState.RTL, 1000)) + assert drone.landed is True + assert drone.rtl is False + + # --- 持续动作 --------------------------------------------------------------- def test_tick_goto_steps_bridge(): bridge = FakeBridge() @@ -281,12 +337,24 @@ class FakePodControl: def __init__(self): self.armed = False self.stopped = False + self.calls = [] def arm_detection(self): self.armed = True + self.calls.append("arm_detection") def stop_tracking(self): self.stopped = True + self.calls.append("stop_tracking") + + def stop_track(self): + self.calls.append("stop_track") + + def auto_lock(self): + self.calls.append("auto_lock") + + def center(self): + self.calls.append("center") def test_on_enter_goto_arms_detection(): @@ -309,3 +377,596 @@ def test_no_pod_control_is_safe(): r = _make_runner(pod_control=None) asyncio.run(r._on_enter(TaskState.GOTO, 1000)) asyncio.run(r._on_enter(TaskState.RTL, 1000)) + + +# --- 地面站命令分派 --------------------------------------------------------- +def test_ground_command_rtl_sets_requested(): + ground = FakeGround() + ground.commands.append("rtl") + r = _make_runner(ground=ground) + + r._apply_ground_commands() + assert r._collect_inputs(1000).rtl_requested is True + + +def test_ground_command_emergency_sets_flag(): + ground = FakeGround() + ground.commands.append("emergency") + r = _make_runner(ground=ground) + + r._apply_ground_commands() + assert r._collect_inputs(1000).emergency is True + + +def test_ground_command_pod_dispatch(): + pod_control = FakePodControl() + ground = FakeGround() + ground.commands.extend(["pod_detect_on", "pod_auto_lock", "pod_stop_track", + "pod_detect_off", "pod_center"]) + r = _make_runner(ground=ground, pod_control=pod_control) + + r._apply_ground_commands() + assert pod_control.calls == ["arm_detection", "auto_lock", "stop_track", + "stop_tracking", "center"] + + +def test_ground_command_pod_without_control_is_ignored(): + """SITL(无 pod_control)→ 吊舱命令记警告忽略,不抛异常。""" + ground = FakeGround() + ground.commands.append("pod_center") + r = _make_runner(ground=ground, pod_control=None) + + r._apply_ground_commands() # 不抛异常即通过 + + +def test_ground_command_unknown_is_ignored(): + ground = FakeGround() + ground.commands.append("unknown_cmd") + r = _make_runner(ground=ground) + + r._apply_ground_commands() # 不抛异常即通过 + + +def test_run_drains_ground_commands(): + """run() 主循环每拍排空命令队列:RTL 命令 → 状态机转 RTL → 调 return_to_launch。""" + ground = FakeGround() + ground.commands.append("rtl") + drone = FakeDrone() + r = _make_runner(ground=ground, drone=drone) + + async def scenario(): + stop = asyncio.Event() + task = asyncio.ensure_future(r.run(stop)) + try: + for _ in range(100): + if drone.rtl: + break + await asyncio.sleep(0.01) + finally: + stop.set() + await task + + asyncio.run(scenario()) + assert r.state is TaskState.RTL + assert drone.rtl is True + + +# --- 飞控状态文本落日志 ----------------------------------------------------- +class FakeLogger: + def __init__(self): + self.events = [] + + def open(self): + pass + + def close(self): + pass + + def log_event(self, now_ms, event_type, **fields): + self.events.append((event_type, fields)) + + def log_telemetry(self, now_ms, rec): + pass + + +def test_fc_messages_logged_as_events(): + px4 = FakePx4(_state(1000)) + px4.fc_msgs = [("WARNING", "Low battery"), ("INFO", "Takeoff detected")] + logger = FakeLogger() + r = _make_runner(px4=px4) + r._logger = logger + + r._log_fc_messages() + assert [e[0] for e in logger.events] == ["fc_status", "fc_status"] + assert logger.events[0][1] == {"severity": "WARNING", "text": "Low battery"} + assert px4.fc_msgs == [] # 已排空,不重复记录 + + +# --- SEARCH 搜索扫视(2026-10-15 新增)-------------------------------------- +def test_on_enter_search_disables_servo_and_resets_sweep(): + bridge = FakeBridge() + r = _make_runner(bridge=bridge) + r._sweep_dir = 9 # 脏值,应被重置 + asyncio.run(r._on_enter(TaskState.SEARCH, 1000)) + assert bridge.law is False # 搜索不用控制律(不由 L1/L2 出指令) + assert r._sweep_dir == 0 # 每次进搜索从右向起扫 + assert r._sweep_since_ms == 1000 + + +def test_search_yaw_rate_sweeps_both_directions(): + """扫视按 sweep_reverse_s 左右交替,幅值为配置角速度。""" + sm_cfg = TaskStateConfig(sweep_rate_dps=15.0, sweep_reverse_s=4.0) + r = _make_runner(sm_cfg=sm_cfg) + assert r._search_yaw_rate(1000) == pytest.approx(15.0) # 先向右 + assert r._search_yaw_rate(4999) == pytest.approx(15.0) # 未到换向点 + assert r._search_yaw_rate(5000) == pytest.approx(-15.0) # 换向左 + assert r._search_yaw_rate(9000) == pytest.approx(15.0) # 再换回右 + + +def test_search_yaw_rate_zero_disables_sweep(): + sm_cfg = TaskStateConfig(sweep_rate_dps=0.0, sweep_reverse_s=4.0) + r = _make_runner(sm_cfg=sm_cfg) + assert r._search_yaw_rate(1000) == 0.0 + + +def test_search_yaw_rate_continuous_when_reverse_disabled(): + """sweep_reverse_s=0 → 单向持续扫视(不换向)。""" + sm_cfg = TaskStateConfig(sweep_rate_dps=10.0, sweep_reverse_s=0.0) + r = _make_runner(sm_cfg=sm_cfg) + assert r._search_yaw_rate(1000) == pytest.approx(10.0) + assert r._search_yaw_rate(600_000) == pytest.approx(10.0) + + +def test_tick_search_sends_sweep_step(): + bridge = FakeBridge() + sm_cfg = TaskStateConfig(sweep_rate_dps=12.0, sweep_reverse_s=4.0) + r = _make_runner(bridge=bridge, sm_cfg=sm_cfg) + r._sm.force_state(TaskState.SEARCH, 1000) + asyncio.run(r._tick(1000)) + assert bridge.search_steps == [pytest.approx(12.0)] + assert bridge.steps == 0 # 搜索不走控制律 step + + +def test_search_step_polls_pod_so_lock_can_be_fresh(): + """SEARCH 必须 poll 吊舱:否则遥测超龄,搜到目标也切不进 TRACK。""" + from pod_bridge.bridge import PodBridge + from pod_bridge.protocol import Telemetry + + class FakePod: + def __init__(self): + self.polls = 0 + + def poll(self): + self.polls += 1 + return [Telemetry(status1=0b100, off_target_x_deg=1.0, + off_target_y_deg=0.0)] + + def send(self, data): + pass + + def close(self): + pass + + pod = FakePod() + px4 = FakePx4(_state(1000)) + cfg = BridgeConfig() + cfg.link.require_laser_valid = False # 本吊舱无激光(与部署配置一致) + bridge = PodBridge(cfg, px4, pod, now_ms_fn=lambda: 1000, + ground_target_fn=lambda: None) + asyncio.run(bridge.search_step(1000, -15.0)) + + assert pod.polls == 1 + assert bridge.pod_locked(1000) is True # 本拍锁定位已刷新 + assert px4.yaw_calls == [pytest.approx(-15.0)] # 扫视角速度已下发 + + +# --- 进入动作只执行一次(防重入导致的自激)---------------------------------- +class _CountingPodControl: + def __init__(self): + self.arm_calls = 0 + self.stop_calls = 0 + + def arm_detection(self): + self.arm_calls += 1 + + def stop_tracking(self): + self.stop_calls += 1 + + def stop_track(self): + pass + + def auto_lock(self): + pass + + def center(self): + pass + + +def test_pod_detection_armed_only_once_across_reentry(): + """丢目标回 SEARCH 再进 TRACK 时,不得重发 0x07/0x0F(会打断正在进行的跟踪)。""" + pod_control = _CountingPodControl() + r = _make_runner(pod_control=pod_control) + asyncio.run(r._on_enter(TaskState.GOTO, 1000)) + asyncio.run(r._on_enter(TaskState.SEARCH, 2000)) + asyncio.run(r._on_enter(TaskState.TRACK, 3000)) + asyncio.run(r._on_enter(TaskState.SEARCH, 4000)) # 丢目标回搜索 + asyncio.run(r._on_enter(TaskState.TRACK, 5000)) # 再次锁定 + assert pod_control.arm_calls == 1 + + +def test_rtl_stops_pod_tracking_even_after_reentry(): + pod_control = _CountingPodControl() + r = _make_runner(pod_control=pod_control) + asyncio.run(r._on_enter(TaskState.GOTO, 1000)) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + assert pod_control.stop_calls == 1 + + +# --- 主循环异常兜底:必须返航,不能静默停摆 --------------------------------- +def test_iteration_exception_triggers_rtl(): + """主循环单周期异常 → 必须转紧急返航,不能只停机静默停摆。 + + 异常注入在**每拍都跑**的输入采集路径(own_state),与当前状态无关。 + """ + px4 = FakePx4(_state(1000)) + drone = FakeDrone() + r = _make_runner(px4=px4, drone=drone) + calls = {"n": 0} + + def boom(): + calls["n"] += 1 + raise RuntimeError("模拟瞬态异常(遥测/串口抖动)") + + px4.own_state = boom + + async def scenario(): + stop = asyncio.Event() + task = asyncio.ensure_future(r.run(stop)) + for _ in range(300): + if drone.rtl: + break + await asyncio.sleep(0.005) + stop.set() + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + asyncio.run(scenario()) + assert calls["n"] >= 1 + assert drone.rtl is True # 异常兜底确实触发了返航 + assert r.state is TaskState.RTL + + +def test_iteration_exception_records_event(): + """异常兜底要在任务日志留痕,便于事后定位。""" + px4 = FakePx4(_state(1000)) + drone = FakeDrone() + r = _make_runner(px4=px4, drone=drone) + logger = FakeLogger() + r._logger = logger + + def boom(): + raise RuntimeError("串口拔线") + + px4.own_state = boom + + async def scenario(): + stop = asyncio.Event() + task = asyncio.ensure_future(r.run(stop)) + for _ in range(300): + if drone.rtl: + break + await asyncio.sleep(0.005) + stop.set() + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + asyncio.run(scenario()) + types = [e[0] for e in logger.events] + assert "iteration_error" in types + assert "rtl" in types # 兜底返航事件也落盘 + + +def test_safe_abort_is_noop_when_already_rtl(): + """已在 RTL 时兜底不重复触发返航(避免重复 RTL 指令)。""" + bridge = FakeBridge() + drone = FakeDrone() + r = _make_runner(bridge=bridge, drone=drone) + r._sm.force_state(TaskState.RTL, 0) + asyncio.run(r._safe_abort()) + assert drone.rtl is False + assert bridge.stopped is False + + +# --- in_air 遥测流复用(降低服务端订阅churn)-------------------------------- +def test_in_air_stream_reused_across_ticks(): + """in_air 流只订阅一次:每拍新建会在服务端反复订阅/退订同一条流。 + + 注意必须在**同一个事件循环**内连续调用(与生产一致:主循环是单 loop); + 跨 loop 复用 async generator 会被 asyncio 拒绝,实现里也就丢弃重建。 + """ + drone = FakeDrone() + calls = {"n": 0} + + class CountingTelemetry: + def in_air(self): + calls["n"] += 1 + return FakeDrone._Telemetry().in_air() + + drone.telemetry = CountingTelemetry() + r = _make_runner(drone=drone) + + async def scenario(): + for _ in range(5): + await r._check_landed() + + asyncio.run(scenario()) + assert calls["n"] == 1 # 只订阅一次 + assert r.rtl_done is True # in_air=False → 判落地并 disarm + + +def test_in_air_stream_rebuilt_when_broken(): + """流建立/消费失败时丢弃缓存,下拍重建(不永久卡在坏流上)。""" + drone = FakeDrone() + calls = {"n": 0} + + def broken(): + calls["n"] += 1 + raise RuntimeError("stream broken") + + class FlakyTelemetry: + def in_air(self): + return broken() + + drone.telemetry = FlakyTelemetry() + r = _make_runner(drone=drone) + + async def scenario(): + await r._check_landed() + assert calls["n"] == 1 + assert r._in_air_stream is None # 已丢弃 + await r._check_landed() + assert calls["n"] == 2 # 重建后再试 + + asyncio.run(scenario()) + + +# --- RTL 落地等待上限(真机在环发现:判不出落地会无限等)-------------------- +def test_rtl_timeout_ends_task_when_landing_unobservable(): + """in_air 始终不报落地 → 超过 rtl_timeout_s 后结束任务,不无限等待。""" + now = {"ms": 1_000_000} + drone = FakeDrone() + + class AlwaysAirborne: + async def in_air(self): + while True: + yield True + + drone.telemetry = AlwaysAirborne() + sm_cfg = TaskStateConfig(rtl_timeout_s=5.0) + r = _make_runner(drone=drone, sm_cfg=sm_cfg, + now_ms_fn=lambda: now["ms"]) + r._rtl_entered_ms = now["ms"] + r._rtl_phase = "land" + + async def scenario(): + now["ms"] += 4_000 # 未到上限:不应结束 + await r._tick_rtl(now["ms"]) + assert r.rtl_done is False + now["ms"] += 2_000 # 超过上限:结束任务 + await r._tick_rtl(now["ms"]) + assert r.rtl_done is True + + asyncio.run(scenario()) + + +def test_rtl_timeout_not_triggered_after_landing(): + """已判落地后不再触发超时逻辑(rtl_done 保持)。""" + now = {"ms": 1_000_000} + drone = FakeDrone() # 默认 in_air 首帧 False + sm_cfg = TaskStateConfig(rtl_timeout_s=1.0) + r = _make_runner(drone=drone, sm_cfg=sm_cfg, + now_ms_fn=lambda: now["ms"]) + r._rtl_entered_ms = now["ms"] + r._rtl_phase = "land" + + async def scenario(): + await r._tick_rtl(now["ms"]) + assert r.rtl_done is True + now["ms"] += 60_000 # 大幅超过上限 + await r._tick_rtl(now["ms"]) + assert r.rtl_done is True + + asyncio.run(scenario()) + + +# --- 自定义返航:回起飞点 → 恢复初始航向 → 降落 ----------------------------- +def _armed_runner(px4=None, drone=None, return_cfg=None): + """走完 解锁→GOTO(记录起飞点 + 进入 offboard)的 runner。""" + px4 = px4 or FakePx4(_state(1000, lat=0.0, lon=0.0)) + r = _make_runner(px4=px4, drone=drone or FakeDrone()) + if return_cfg is not None: + r._return_cfg = return_cfg + asyncio.run(r._on_enter(TaskState.ARM_TAKEOFF, 1000)) + asyncio.run(r._on_enter(TaskState.GOTO, 1000)) + return r, px4 + + +def test_custom_return_selected_after_takeoff(): + """已起飞 + 有起飞点 → 进入 RTL 走自定义返航,不调 PX4 RTL。""" + drone = FakeDrone() + r, _ = _armed_runner(drone=drone) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + assert r._rtl_phase == "return" + assert drone.rtl is False # 不再交给 PX4 原生 RTL + assert r._bridge_started is True + + +def test_custom_return_records_home_and_initial_yaw(): + r, px4 = _armed_runner() + assert r._home_lat == pytest.approx(0.0) + assert r._home_lon == pytest.approx(0.0) + assert r._initial_yaw_rad == pytest.approx(0.0) + + +def test_return_step_commands_velocity_toward_home(): + px4 = FakePx4(_state(1000, lat=0.0, lon=0.0)) + r, _ = _armed_runner(px4=px4) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + + # 飞到起飞点正北 ~111m,机头朝南(对准回家方向) + px4._state = _state(3000, lat=0.001, lon=0.0, rel_alt=5.0) + px4._state.yaw_rad = 3.141592653589793 + px4.velocity_calls.clear() + asyncio.run(r._tick_rtl(3000)) + + assert r._rtl_phase == "return" + assert len(px4.velocity_calls) == 1 + out = px4.velocity_calls[0] + assert out.vx_body_mps > 0.0 # 朝起飞点前进 + assert out.vz_mps == pytest.approx(0.0) # 转场高度 5m = 当前高度 + + +def test_return_arrival_switches_to_yaw_phase(): + px4 = FakePx4(_state(1000, lat=0.0, lon=0.0)) + r, _ = _armed_runner(px4=px4) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + + px4._state = _state(3000, lat=0.000005, lon=0.0, rel_alt=5.0) # ~0.5m + asyncio.run(r._tick_rtl(3000)) + assert r._rtl_phase == "yaw" + + +def test_yaw_phase_restores_heading_then_lands(): + drone = FakeDrone() + px4 = FakePx4(_state(1000, lat=0.0, lon=0.0)) + r, _ = _armed_runner(px4=px4, drone=drone) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + r._rtl_phase = "yaw" + r._yaw_started_ms = 2000 + + px4._state = _state(3000, lat=0.0, lon=0.0, rel_alt=5.0) # 航向已回 0 + px4.velocity_calls.clear() + asyncio.run(r._tick_rtl(3000)) + + assert len(px4.velocity_calls) == 1 # 先发了一拍"原地保持"指令 + assert r._rtl_phase == "land" + assert drone.landed is True # 航向对齐后降落 + assert r._land_cmd_sent is True + + +def test_landing_keeps_telemetry_running(): + """降落阶段只退 offboard、保留遥测(否则日志冻结在降落前高度)。""" + drone = FakeDrone() + bridge = FakeBridge() + r, _ = _armed_runner(drone=drone) + r._bridge = bridge + asyncio.run(r._start_land()) + assert bridge.landing_stopped is True + assert bridge.stopped is False # 尚未全停:遥测/串口仍在 + assert drone.landed is True + + +def test_land_phase_completes_on_landed(): + drone = FakeDrone() # in_air -> False + r, _ = _armed_runner(drone=drone) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + r._rtl_phase = "land" + asyncio.run(r._tick_rtl(3000)) + assert r.rtl_done is True + assert drone.disarmed is True + + +def test_return_timeout_falls_back_to_px4_rtl(): + drone = FakeDrone() + r, px4 = _armed_runner(drone=drone) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + + px4._state = _state(3000, lat=0.001, lon=0.0, rel_alt=5.0) + # 超过转场超时 + late = 2000 + int(r._return_cfg.timeout_s * 1000) + 1000 + asyncio.run(r._tick_rtl(late)) + assert r._rtl_phase == "fallback" + assert drone.rtl is True # 退回 PX4 原生 RTL + + +def test_custom_return_disabled_uses_px4_rtl(): + from task_runner.return_home import ReturnConfig + + drone = FakeDrone() + r, _ = _armed_runner(drone=drone, + return_cfg=ReturnConfig(enabled=False)) + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + assert r._rtl_phase == "fallback" + assert drone.rtl is True + + +def test_rtl_before_takeoff_uses_px4_rtl(): + """未起飞(还在待命/地面)时不能自主返航 → 退回 PX4 RTL。""" + drone = FakeDrone() + r = _make_runner(drone=drone) # 未记录起飞点、未进 offboard + asyncio.run(r._on_enter(TaskState.RTL, 2000)) + assert r._rtl_phase == "fallback" + assert drone.rtl is True + + +def test_safe_abort_prefers_px4_fallback(): + """异常兜底不再信任自身状态:强制走 PX4 RTL。""" + drone = FakeDrone() + r, _ = _armed_runner(drone=drone) + asyncio.run(r._safe_abort()) + assert r._rtl_phase == "fallback" + assert drone.rtl is True + + +# --- 起飞点记录时机(SITL 实测:解锁时 gps_info 可能还没到)------------------ +def test_home_recorded_when_position_becomes_valid_during_takeoff(): + """解锁瞬间定位无效 → 起飞阶段每拍补记,不丢起飞点。""" + px4 = FakePx4(_state(1000, lat=0.0, lon=0.0)) + px4._state.position_valid = False + r = _make_runner(px4=px4) + r._sm.force_state(TaskState.ARM_TAKEOFF, 0) + + r._collect_inputs(1000) + assert r._home_lat is None # 定位无效 → 暂不记录 + + px4._state = _state(1000, lat=0.0012, lon=0.0034) + r._collect_inputs(1100) + assert r._home_lat == pytest.approx(0.0012) # 补记成功 + assert r._home_lon == pytest.approx(0.0034) + + +def test_home_not_recorded_once_airborne(): + """进入 GOTO(已在空中)后不再把当前位置当作起飞点。""" + px4 = FakePx4(_state(1000, lat=0.005, lon=0.0)) + r = _make_runner(px4=px4) + r._sm.force_state(TaskState.GOTO, 0) + r._collect_inputs(1000) + assert r._home_lat is None + + +def test_home_recorded_only_once(): + px4 = FakePx4(_state(1000, lat=0.0, lon=0.0)) + r = _make_runner(px4=px4) + r._sm.force_state(TaskState.ARM_TAKEOFF, 0) + r._collect_inputs(1000) + first = (r._home_lat, r._home_lon, r._initial_yaw_rad) + + px4._state = _state(1000, lat=0.009, lon=0.009) # 位置已变(飞机在爬升/移动) + r._collect_inputs(1100) + assert (r._home_lat, r._home_lon, r._initial_yaw_rad) == first + + +def test_record_home_returns_false_without_fix(): + px4 = FakePx4(_state(1000)) + px4._state.position_valid = False + r = _make_runner(px4=px4) + assert r._record_home() is False + assert r._home_lat is None + + diff --git a/utils/tests/task_runner/state_machine_test.py b/utils/tests/task_runner/state_machine_test.py index 95e8b1f..42e1a60 100644 --- a/utils/tests/task_runner/state_machine_test.py +++ b/utils/tests/task_runner/state_machine_test.py @@ -1,8 +1,12 @@ """state_machine_test.py —— 任务级状态机单元测试。 -覆盖:完整状态链 WAIT→ARM_TAKEOFF→GOTO→TRACK→APPROACH→RTL、各状态超时、 -GOTO→TRACK 的锁定/视觉守卫、TRACK 丢目标退回 GOTO、紧急/返航全局终止、 -RTL 终态与 reset。 +覆盖(2026-10-15 按「到参考点 → 视觉搜索 → 缓慢靠近」重构后的契约): + * 完整状态链 WAIT→ARM_TAKEOFF→GOTO→SEARCH→TRACK→(超时)RTL + * GOTO 只按「到参考点半径」退出(不看锁定);SEARCH 才看锁定/视觉守卫 + * 搜索超时 → RTL;追踪(靠近)超时 → RTL + * TRACK 丢目标 → 回 SEARCH(不回 GOTO);坐标同时失效 → RTL + * TRACK **不**因「目标居中」退出(居中即应继续靠近) + * 紧急/返航全局终止、RTL 终态与 reset、force_state 兜底 """ import pytest @@ -19,6 +23,25 @@ def _sm(cfg=None) -> TaskStateMachine: return TaskStateMachine(cfg or TaskStateConfig()) +def _to_search(cfg=None, t0: int = 1000): + """推进到 SEARCH 态:WAIT→ARM_TAKEOFF→GOTO→SEARCH。返回 (sm, now_ms)。""" + sm = _sm(cfg) + sm.update(_inp(target_injected=True), t0) + sm.update(_inp(target_injected=True, takeoff_reached=True), t0 + 1000) + tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0), t0 + 2000) + assert tr.to_state is TaskState.SEARCH + return sm, t0 + 2000 + + +def _to_track(cfg=None, t0: int = 1000): + """推进到 TRACK 态:WAIT→…→SEARCH→TRACK。返回 (sm, now_ms)。""" + sm, t = _to_search(cfg, t0) + tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0, + target_locked=True), t + 1000) + assert tr.to_state is TaskState.TRACK + return sm, t + 1000 + + # --- 初始态与 reset --------------------------------------------------------- def test_initial_state_is_wait(): assert _sm().state is TaskState.WAIT @@ -32,6 +55,13 @@ def test_reset_returns_to_wait(): assert sm.state is TaskState.WAIT +def test_force_state_jumps_without_transition(): + """force_state 供异常兜底直接落状态(不产生 TaskTransition)。""" + sm = _sm() + sm.force_state(TaskState.RTL, 5000) + assert sm.state is TaskState.RTL + + # --- WAIT ------------------------------------------------------------------- def test_wait_without_coords_stays(): tr = _sm().update(_inp(), 1000) @@ -60,12 +90,11 @@ def test_arm_takeoff_timeout_goes_rtl(): cfg = TaskStateConfig(arm_takeoff_timeout_s=60.0) sm = _sm(cfg) sm.update(_inp(target_injected=True), 1000) - # 60s 未到高度 → 超时返航 tr = sm.update(_inp(target_injected=True), 1000 + 61_000) assert tr.occurred and tr.to_state is TaskState.RTL -# --- GOTO ------------------------------------------------------------------- +# --- GOTO:只按「到参考点半径」退出(不看锁定)------------------------------- def test_goto_far_away_stays(): sm = _sm() sm.update(_inp(target_injected=True), 1000) @@ -76,45 +105,31 @@ def test_goto_far_away_stays(): assert sm.state is TaskState.GOTO -def test_goto_within_radius_and_locked_goes_track(): +def test_goto_within_radius_goes_search_even_if_locked(): + """到参考点即转搜索——参考点是搜索起点,锁定与否不影响该转移。""" sm = _sm() sm.update(_inp(target_injected=True), 1000) sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0, target_locked=True), 3000) - assert tr.occurred and tr.to_state is TaskState.TRACK + assert tr.occurred and tr.to_state is TaskState.SEARCH + assert "参考点" in tr.reason -def test_goto_within_radius_but_unlocked_stays(): +def test_goto_within_radius_unlocked_still_goes_search(): sm = _sm() sm.update(_inp(target_injected=True), 1000) sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0, target_locked=False), 3000) - assert not tr.occurred - assert sm.state is TaskState.GOTO - - -def test_goto_requires_vision_when_enabled(): - cfg = TaskStateConfig(require_vision=True) - sm = _sm(cfg) - sm.update(_inp(target_injected=True), 1000) - sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - # 有锁定但无视觉第二判据 → 不切换 - tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0, - target_locked=True, vision_has_target=False), 3000) - assert not tr.occurred - # 补上视觉 → 切换 - tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0, - target_locked=True, vision_has_target=True), 4000) - assert tr.occurred and tr.to_state is TaskState.TRACK + assert tr.occurred and tr.to_state is TaskState.SEARCH def test_goto_loses_coords_goes_rtl(): sm = _sm() sm.update(_inp(target_injected=True), 1000) sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - tr = sm.update(_inp(target_injected=False, distance_to_target_m=10.0), 3000) + tr = sm.update(_inp(target_injected=False, distance_to_target_m=100.0), 3000) assert tr.occurred and tr.to_state is TaskState.RTL @@ -123,87 +138,113 @@ def test_goto_timeout_goes_rtl(): sm = _sm(cfg) sm.update(_inp(target_injected=True), 1000) sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - tr = sm.update(_inp(target_injected=True, distance_to_target_m=100.0), 2000 + 121_000) + tr = sm.update(_inp(target_injected=True, distance_to_target_m=100.0), + 2000 + 121_000) assert tr.occurred and tr.to_state is TaskState.RTL -# --- TRACK ------------------------------------------------------------------ -def test_track_centered_stable_goes_approach(): - sm = _sm(TaskStateConfig(center_hold_s=3.0)) - sm.update(_inp(target_injected=True), 1000) - sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - sm.update(_inp(target_injected=True, distance_to_target_m=10.0, - target_locked=True), 3000) - assert sm.state is TaskState.TRACK - # 居中但未满时长 → 停留 - tr = sm.update(_inp(target_injected=True, target_locked=True, - target_centered=True, centered_duration_s=1.0), 4000) +# --- SEARCH:搜索发现目标才进 TRACK ----------------------------------------- +def test_search_sees_target_goes_track(): + sm, t = _to_search() + tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0, + target_locked=True), t + 1000) + assert tr.occurred and tr.to_state is TaskState.TRACK + assert "发现目标" in tr.reason + + +def test_search_without_target_stays(): + sm, t = _to_search() + tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0, + target_locked=False), t + 1000) assert not tr.occurred - # 居中稳定满 3s → APPROACH - tr = sm.update(_inp(target_injected=True, target_locked=True, - target_centered=True, centered_duration_s=3.0), 5000) - assert tr.occurred and tr.to_state is TaskState.APPROACH + assert sm.state is TaskState.SEARCH -def test_track_lost_with_coords_goes_back_to_goto(): - sm = _sm() - sm.update(_inp(target_injected=True), 1000) - sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - sm.update(_inp(target_injected=True, distance_to_target_m=10.0, - target_locked=True), 3000) - tr = sm.update(_inp(target_injected=True, target_locked=False), 4000) - assert tr.occurred and tr.to_state is TaskState.GOTO - - -def test_track_lost_without_coords_goes_rtl(): - sm = _sm() - sm.update(_inp(target_injected=True), 1000) - sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - sm.update(_inp(target_injected=True, distance_to_target_m=10.0, - target_locked=True), 3000) - tr = sm.update(_inp(target_injected=False, target_locked=False), 4000) +def test_search_timeout_goes_rtl(): + cfg = TaskStateConfig(search_timeout_s=20.0) + sm, t = _to_search(cfg) + tr = sm.update(_inp(target_injected=True, distance_to_target_m=10.0), + t + 21_000) assert tr.occurred and tr.to_state is TaskState.RTL + assert "搜索超时" in tr.reason + + +def test_search_loses_coords_goes_rtl(): + sm, t = _to_search() + tr = sm.update(_inp(target_injected=False), t + 1000) + assert tr.occurred and tr.to_state is TaskState.RTL + + +# --- TRACK:缓慢靠近,只受超时/返航/紧急控制 --------------------------------- +def test_track_centered_does_not_exit(): + """目标居中**不再**触发退出:居中正是应当继续缓慢靠近的时刻。""" + sm, t = _to_track() + tr = sm.update(_inp(target_injected=True, target_locked=True, + target_centered=True, centered_duration_s=99.0), t + 1000) + assert not tr.occurred + assert sm.state is TaskState.TRACK + + +def test_track_holds_while_locked(): + sm, t = _to_track() + for dt in (1000, 2000, 3000): + tr = sm.update(_inp(target_injected=True, target_locked=True), t + dt) + assert not tr.occurred + assert sm.state is TaskState.TRACK def test_track_timeout_goes_rtl(): - cfg = TaskStateConfig(track_timeout_s=120.0) - sm = _sm(cfg) - sm.update(_inp(target_injected=True), 1000) - sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - sm.update(_inp(target_injected=True, distance_to_target_m=10.0, - target_locked=True), 3000) - tr = sm.update(_inp(target_injected=True, target_locked=True), 3000 + 121_000) + cfg = TaskStateConfig(track_timeout_s=20.0) + sm, t = _to_track(cfg) + tr = sm.update(_inp(target_injected=True, target_locked=True), t + 21_000) + assert tr.occurred and tr.to_state is TaskState.RTL + assert "超时" in tr.reason + + +def test_track_lost_with_coords_goes_back_to_search(): + """丢目标回 SEARCH 重新扫视(不回 GOTO:飞机本就在参考点附近)。""" + sm, t = _to_track() + tr = sm.update(_inp(target_injected=True, target_locked=False), t + 1000) + assert tr.occurred and tr.to_state is TaskState.SEARCH + assert "重新搜索" in tr.reason + + +def test_track_lost_without_coords_goes_rtl(): + sm, t = _to_track() + tr = sm.update(_inp(target_injected=False, target_locked=False), t + 1000) assert tr.occurred and tr.to_state is TaskState.RTL -# --- APPROACH --------------------------------------------------------------- +def test_reacquire_after_loss_returns_to_track(): + """丢目标→SEARCH→重新锁定→TRACK 全链路可循环。""" + sm, t = _to_track() + tr = sm.update(_inp(target_injected=True, target_locked=False), t + 1000) + assert tr.to_state is TaskState.SEARCH + tr = sm.update(_inp(target_injected=True, target_locked=True), t + 2000) + assert tr.to_state is TaskState.TRACK + + +# --- APPROACH(保留态:真机可选收尾)---------------------------------------- def test_approach_hover_done_goes_rtl(): - cfg = TaskStateConfig(center_hold_s=0.0, approach_hover_s=10.0) + cfg = TaskStateConfig(approach_hover_s=10.0) sm = _sm(cfg) - sm.update(_inp(target_injected=True), 1000) - sm.update(_inp(target_injected=True, takeoff_reached=True), 2000) - sm.update(_inp(target_injected=True, distance_to_target_m=10.0, - target_locked=True), 3000) - sm.update(_inp(target_injected=True, target_locked=True, target_centered=True, - centered_duration_s=0.0), 4000) - assert sm.state is TaskState.APPROACH - # 悬停未满 10s → 停留 - tr = sm.update(_inp(target_injected=True, target_locked=True), 4000 + 5000) + sm.force_state(TaskState.APPROACH, 1000) + tr = sm.update(_inp(), 1000 + 5000) assert not tr.occurred - # 满 10s → RTL - tr = sm.update(_inp(target_injected=True, target_locked=True), 4000 + 10_000) + tr = sm.update(_inp(), 1000 + 10_000) assert tr.occurred and tr.to_state is TaskState.RTL # --- 全局终止(紧急 / 返航)------------------------------------------------- -@pytest.mark.parametrize("state_kw", [ - {}, # WAIT - {"target_injected": True}, # 会先转 ARM_TAKEOFF -]) -def test_emergency_goes_rtl_from_any_state(state_kw): - sm = _sm() - sm.update(_inp(**state_kw), 1000) - tr = sm.update(_inp(emergency=True), 2000) +def test_emergency_from_search_goes_rtl(): + sm, t = _to_search() + tr = sm.update(_inp(emergency=True), t + 1000) + assert tr.occurred and tr.to_state is TaskState.RTL + + +def test_emergency_from_track_goes_rtl(): + sm, t = _to_track() + tr = sm.update(_inp(target_locked=True, emergency=True), t + 1000) assert tr.occurred and tr.to_state is TaskState.RTL @@ -218,7 +259,6 @@ def test_rtl_is_terminal(): sm = _sm() sm.update(_inp(rtl_requested=True), 1000) assert sm.state is TaskState.RTL - # 终态不再转移(连紧急/坐标也不动) tr = sm.update(_inp(target_injected=True, emergency=True), 2000) assert not tr.occurred assert sm.state is TaskState.RTL @@ -231,3 +271,11 @@ def test_transition_dataclass_defaults(): assert tr.to_state is TaskState.WAIT assert tr.occurred is False assert tr.reason == "" + + +# --- 配置默认值(搜索/靠近超时口径)------------------------------------------ +def test_default_config_has_search_and_pursuit_timeouts(): + cfg = TaskStateConfig() + assert cfg.search_timeout_s == pytest.approx(20.0) + assert cfg.track_timeout_s == pytest.approx(20.0) + assert cfg.sweep_rate_dps > 0.0 diff --git a/utils/tests/visual_track_control/__pycache__/frame_transform_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/visual_track_control/__pycache__/frame_transform_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 1b9b9a83fb3926109dea3812f52ba544e31f5cec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11989 zcmeHNYitzP6`t9heRv;!V4Fac&~1$4EDzheUSmvH$0!g9Bv7TKYUNs*ES?3M*t?rM zV~pKh$&i#7wFr}hkTj%qq^Mbkw80e(R!bDrzx{);3(+c4s(>Hl4+J7b%CDYt=efJ% z-6bMzr83$*bMCq4-Z^vU%)Q?|Xa3>!x)?at{r>cUu$^J9V?jME3Ujv`nA42FbTI-e zSo+y63q4y0xE>434A{EttX$*ha?l!QmlMv`{;~mAmka8+mzgfNU<2wA>_EMO18BM6 z1X?j{@s|y*W*KH5i3~(}i9}-JzBn1+rKl)1CWd`qj=l~*ABjs5Da!AS3&Xq==VMX9 zH-G%p{I6bG_{*=CK00-M^oYx~`2OD(-aEeZ$_G9Y-4`X%SWk3g&-(RE2m8dqNI&-7 z^8()!k4Yrn--P{(O{Q1vNhGkpr9T{9eEW@C7snQlO)icfUHbHsrRfhAM&DVw@R9HD z?*|)OZe9G%w$QDM$8KHx?ajA8UK;yge*EOZ;a3;`cr9+lJ2XBGA!Bi0)FFV{6xF3Fr#?w1%fE7f}atZT+tp-gaHqVo81ru*hY&ibZk^n@QPV_H@zN)0T7@W>0t0l62qm z>{*Ge3?5o_-#pZ!j&xBE)il9FtN!mj)TNGF5f62%dcq22_VftVLQT@6%%0woXHT!% zlX^CrJz)lQrQE}ezjn`}JoDauYQN0^-m;*oBg6M9QGj_0N(XWmGjBMkSYU45e5BXV36d$_Mn|kK9@{}koMftbgw_#>j~+_& zM`95PYW(HIizcl?@-W)v#tX`0-C``X8Vha4Lc6gLkS&(46v!@R^|x78l&=(M&T1CS zDuqNGFkja3-Osjf>HZ0c?}vTE_J|be?uiRgULf&UbR+sx_n_3*FLqOy-)*)7=r+av ziQ&v@Y{QR4Nt_>!4Do#;-zP+4QlB(TLSRM({KXvrgGZUqSFKC8oU@*LZn|Z5)l)NE z?dXoN(0I+~H4mTOeRB6@E-+CCB;5++Obn9GA0Qj-i%Qb-+ciU8~Fbu;i|q080HGw$YV9R98<0=cBo85GNN6^DfH4LuH+K)9@pp58xBtvQFjeFLbXYnp8|v@Ib( z6^Gy~*@D%EeoZWpw6Ro|U}9`xVo81ru*hk+ib)&8Mz8Q)>KnbndiqAMu$~U4%e0N2 zOK=-DdJn0O#Bc|=eB>M@Z}hbBT*fys2(g@R=5F-#dou|qXug@f(esq-8AgvY@r

    EfxYk1x3!t2zjVxClm13Hq4~ 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 Yz9O#y=#)$D6uacbcc*#eMU?e_18GavKmY&$ diff --git a/utils/tests/visual_track_control/__pycache__/mavsdk_gateway_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/visual_track_control/__pycache__/mavsdk_gateway_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 0351912350aba14304b90d225f1a1cb7b53bc0f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37585 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 diff --git a/utils/tests/visual_track_control/__pycache__/rate_limiter_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/visual_track_control/__pycache__/rate_limiter_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index c204b9fc7a086cbb3f853670863a404f1747fca0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17673 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 diff --git a/utils/tests/visual_track_control/__pycache__/staleness_checker_test.cpython-312-pytest-9.1.1.pyc b/utils/tests/visual_track_control/__pycache__/staleness_checker_test.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 228501eb88e1c98eab11fcfc6f3a3ba3361fe6c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6043 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 CDt@;B diff --git a/utils/visual_track_control/__pycache__/__init__.cpython-312.pyc b/utils/visual_track_control/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index f4b882a39acc3c27ef6c3cec093c5ddbbea6f644..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1865 zcmZuxNpBQK6z(2-?D33eyt3GYP(V;L84+{f6h)CNJ0Vgqu`uFLvQ%oiYsL*#U8CwA zh;Qte7)X@O5(=P@NQi_OP=GjIIOUdq&=TUnY@D0~A*7u0s%C6F34Qqb)$4xS_ui{N zlF7IM&&-vX!zWq|~|1m49CERV>%IL@(7NYN^W9Jycsw1D30v z>vN24;2ZHq-iQ12xsCPnO?Z=*h`zx8v=mku#k3 z>n46lMxdzbjkDDo=c~V7tIqvczI3BB^`La&%<|&YQP(e6j0DoMj}TjUffNjl zP6iXgLzVMW%Zulq96uS4mu~-7n*Rz7WiqJr*~#khQ>EFfl}mS!q**GdXVaZ_CNuCV zX9Hkg{`M9`E6!a)fhBV^sD-04uC()C6z47$A6}{6{|53opb{Aitbn58+x{G^UCGZmFZiRpQcO47b|xcN>`_s7cW##Jt*I~SGs$$dhc5G-uKm|Tjh(_SCgJP z3tyHOFUJw8%-^oe&Xj-ox_tF?ap3_%NJ0$4T1G5a`)=}R%LUO>)F>4Zj8W}t|HvAD1+3X=< zNk{p9YQl1)%`iDs^Ejp*rsHAfplwsOSCLw)EbJNrj-<6mibB4w>V~&Ubgiyu*o6kb z9IaJ$e@HC{>Rqc#)O91mp<(Afv0^u92d?nKl%AheV#A(;R0ghXH3UmFk`0A)0|ROt z4hd(H1cG?f0d4!J`w3+S#GsdB6wbXPnwxsjduj}&BVlsXb?P-i*TXhAj#@AqK78vK zWAxa5_*IhvU!yj&cpkjPT%YKnQ}NfNUGHH!8Yb#ab*;d)X6@rbkS_#bNBubFS$Tg# z1Yv5J=A`9NXFs)Ig2K3VsW&OzC!3&QvQxo&1qlU71#Jq{btqdDL>0sov@3wy(U8pw zHYn&NIt87K zj;@n=nWfHL=cdvRJN~nSI$)S=SX`Y+m}(qsO-7h%T-cYzgzamuhnJ;=UH`i$bhTmc ztCmxf9Y%x6JoRe~YRWWut6VB>EXD}AX*`3HjDOxAerFt361vHUt-u<$VbqBug-3S= z)DOlBfy?~ySKYPagt(p?5b}0@GK{^;g;QYkJ(&ToYGeHm0RC(;P4llv!fbxtZwGIl*Sc0V?1^6nQA(?rh=fWKS%6Oj|GG7YZ&1+t`y1poj5 diff --git a/utils/visual_track_control/__pycache__/command_sender.cpython-312.pyc b/utils/visual_track_control/__pycache__/command_sender.cpython-312.pyc deleted file mode 100644 index bb2691a9084800d9bcd41bfa864524335300a6d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2049 zcmb7FOKcNI7@pnrE5@NbO4as&R8NtgCtI} zh3sL6-@zc+#-g1_vL}(`khxo&<>6M{k>43}dR@^!7=-lH4Kg;b9~&(_yh_GTRN}+> zkw?bdrKS9sF?*-9a9B@WBjZ<=^2f;R0$Du8w=^~JyIPcRSd2(rSd6HH3fA~0zWnF} z8M#|YPgkZcFXazEjVIi0y0?6Itb8`P$`O5XmfT4jM-uw|;ZpvhQ8@V`@;*5tebgJ% zWYr6Mn}u1rg=ED zueF1(!awV%zrYxv0=qVL4#-VvVdymW|N`)!o{u%x5G#R;8%BA?Q5*Xn1`$vu0(1ZUP}T7au_#y#*CCxxTN<5MJa zp8rM+sWOzR&c=gZ^y8X#zhB~wJ44RXnHFj z3xh-Ot9hX5Knx@kWA1Q2GF~>?3?}1o2B_zizP?^X#1gMhUn2Q&;NIf!W%j`6yNPZx zMVl&z`cxW)|EHsMdk;{xAB*8uQ4@OtiX;mXRwD9N5T(`=)q)|l=U`BciXlP6VqicB zSggTTGkHNZvu_@Z7451fVr^*SE3leP<9dprfhLUpj-x+aYo6C^FiWsD##eMGW(A5K zvrfH=q6uL&P`}Es3!yjBun*R)3tfp{AdA^6j+rvpQFpQtsvOTIldb+Pn>oO4H(b9=`>BaT5hE9F#*!AgINJep!@) zk^Ms-yvD$4n>JBrYoIY1sX9=D=Q-X0^8f|A^1TUo7i_8n&&*ToV++Qk>mTch+hq9M zs_?)5ZbwI((B9eI_T7IYXD#F=Xqv_f=5;3KUMvpCLcelQ#zUJ|f&0?>`*0Xj0a)<< zkGw1KI2m1hUEcH^cz9Bux}c}$jqD+K0xdK?vt+mtLRs>*ieytYWpHT2Ye|A_^DBvY z8K0NM^v}r$che;Z5fNSuq39Nbup&i6bnFp?AK)g`S8O2F5MD>U&<6ukX%x92JSy+d z>ASQs!BZvG;D^kFFuez6Lp7v+3r!MLZLHlh*;PZ(Ro$qC*~u((oZXY%T1C)3`E+~k zv$?HRYI@SqS)={38}??ml^8y+tLPL2NeMvy4lOna0yQ(m z^a5h|3r0mdm}S)hVJ1ftwp^GpcYR?o@D=_L7H9$0PoSxC48tt5PR91v2E=^+JK9DN a4@zb?K4aH0o{9Dvg09AaVVN3CkN*O(`tr^I diff --git a/utils/visual_track_control/__pycache__/config.cpython-312.pyc b/utils/visual_track_control/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 35c56186b8bca9ea5b873fb90d77b95cc4ddd68b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14696 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 diff --git a/utils/visual_track_control/__pycache__/line_of_sight.cpython-312.pyc b/utils/visual_track_control/__pycache__/line_of_sight.cpython-312.pyc deleted file mode 100644 index 3553ad15bee8b53622f0d8319f7d0fce9046bb25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1901 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 Weqp$~UjL2$5sKPC-6CqCvi&dA%|NXH diff --git a/utils/visual_track_control/__pycache__/pid_controller.cpython-312.pyc b/utils/visual_track_control/__pycache__/pid_controller.cpython-312.pyc deleted file mode 100644 index 7fb683deca38cd81d6bae29674b3f9595b89f891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4658 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 diff --git a/utils/visual_track_control/__pycache__/staleness_checker.cpython-312.pyc b/utils/visual_track_control/__pycache__/staleness_checker.cpython-312.pyc deleted file mode 100644 index e4535a702b0c801a5fab61005104ca51932a01ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1033 zcmZuwO-vI(6rOD>t>rIj;(-{m2`6j-4JIZAy~vrEz=h2)5+b)^iVlX{a{wzvq z`7J*xm>29~DV(?%*`KARDm6x42@6Eh@Z{~a7d@V1xB9PhH z$m3EYLI>125zDc0#DWn)0%}77gBig9!wAEK4eb`;T%W1U*j-X%_Wpt}eS1vpU(nb3 zbK8At;GG(q$)^Hpa6(_{)#4wr(NQh3mK&YRrv?xFJyxq4-%yj|>^+-}RYN_xzgLaD z$quiu1h->?Jv^@EZMOO@r|flzoI)JDi{mAaLL{-?>ET7MfY08P6^DpDSe7}y1M@CS z>N>1&5Ix$b&8PCI8A0OZ`t0CRHawv3Z0U(*&Hq|okJq^c_Dl3u-3Cr_sG?JSRW|L7ZFsY8b&_6C7-y~fjJKUWj<`kzJb8b0Z;*p-vxva|}It8v> zB863g6)*Ad0QIRDz@q-qHBb@c1CU)+2oa{+;A4Eq1^XxVQ)rgY;A%ps1=Se6SD3Q^ zF96Ge{Cy&}@eX^P)uRKML^88Eti4)RLzCM4dOkIjon6(27oc6mJLglg#ly3W|KzaJ zw16XRVq{ede99z(nS@_|z6yX9JF6wbx$)^-f3RK~9D@jO^mrUlQo}<~?X<{u|2TZW z4EX<;9|dM)5-%YskfKlv@Xn3{TZYuwGc^=1VxQTVqjh)QW;QozY$&eHCn#}d`ifVg z2+)(}D8{Wz02QGi6N)l1fs&~O0l%A@Zrkq@=@D_bn;eS6&PxL31R{BGt%7C6?p2(k zY(I;C_Wvo?*`2mHWv;M6=(C~^NTyx&qr(bhxjc1Y&s?=%d1>EV9mAg!!SP#CdPDFQOew$YB@wOy{|4ziK6$p^(S2{B>Mx2*17rs diff --git a/utils/visual_track_control/__pycache__/tracking_control_law.cpython-312.pyc b/utils/visual_track_control/__pycache__/tracking_control_law.cpython-312.pyc deleted file mode 100644 index 19ae3d7be75965011baa0ddd8b9f3e0f6074925e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13663 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 diff --git a/utils/visual_track_control/__pycache__/types.cpython-312.pyc b/utils/visual_track_control/__pycache__/types.cpython-312.pyc deleted file mode 100644 index 09cc3b10a8ec0f458763ddc78a076d2abad29571..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4643 zcmbVQYitu&79QK0(}K+AO)NX0@}4uN3LfAX8p*H$0_cw z0TSXANZh7vC<(7VNVWk=RZ2q$LE0aymG%$FNHrs^mToI992x@@M|mku$LWK*klw2g z8N3D^>V_a4GJ1_6lh;IaI%p1AycQj$r?yg@v6JFVym8c^`tp{LUNiJsX7pNlJ?Srj ze(R`S9XqN`@3m=G8(8f>v)VPQ1FWS#vpV8s&hpqD9Yy7bkL8DZ^531ykA0c#9aa+8 zmC56|)18^I%em3x`N@8z>x)MG=^oGB?9ZO<{82h$wJKlTP{u!GHm_XCD1Arr(oyBg z*<9~cM&R3cfe-un=lm;IHg*J}F<+1s1)u*JwA7sW(W+u<% zzy0{(#EE?Om&({hKPF-`CFH=CqISwX1E7O+T3Tl(b69D z1;UINYY*~`+4J3n?mlpjTUNmIaO&j}&L{f(L0>c~8`}bWkb^$j9gc-$+7}ZePMvIN zX%_>LurKJ>GK&0YSw=nH0O|n6Q(hfpxSrE_4SI^Fi`i$KrI}`F=D6NzkZsy0+8N>a z*bCrIO)oW9u~dHr4@SiG^5cv}z+PWJHO#6f7prN;JKw2QM20-#gB#d9!R0zDBq7J%@r();%kv(_n*50jr;cm&g6umKB*8p!DQ*=YReu=arxjaQL zN!Qj%+mlr(x-MN^BW+9gQ*_NQJGn=1RMQ@VoR#(l=)mW32E^R7M|9wgm-M4>{^R2X zD#DwKMF1rL+RXGfb86CCymsCJwh}E-OUXzz!7Y&BXj2oMCJAol>|~_LTLGnF*3#Vs z367eY?j~;)(MW1@CT}&-%C+}uh*qI#3y4;!X|+VFLM>kHtdYy+aDtc()>q^-DwR=g zkLJ3Dfl3sQXk}DEBaTbQ!b1G@nT5Pv0?y@zfcsYOa6LSVX{+6e2&FACIMB;+`0{3mZ@IVAefa* zhc6i5*tUoOuPwvS(KDR+WgOq;0~W-#L7_#2I8+CgwOWggS+l9-TcJ@YE)W%cP{V9! zi(Fb%$TV_!QP%>M%YC945P{`kxkmvKX7o&tF~@!Huy7#B7GQ-z(kf{Wi2ehlS*lt^ z9<}zUT-w10BmRIGXAiNVS$ceq{x0esXWhKuK|`*PiWXL5>(?k6QTztQ^C*6cVhxHF zDAuAt_9Co9@jDcFbP6w__&tjCD3C4*8&S*wkyY4-0_TaI2hl}6qG_Y$k)5hvGIH=< z-FnHL*pQ;vr|TC=JCiS@=!H`zih1S^U6YKZ=w;~!M)D+GDVj+yS_~{A37D|>!Qy4u zL;{jlyq0sfDU=3asSZJad$+>FgOqymV#jL`N(d!_ZdX5_597VWLLB|L% zgPICDMt~XA2 zS_-H8n5cIBS*;ZllN~q?*9T>CKuN$12~~wCvX?*0jDN25or0SMysIP=1?ikJei6Z| z@050TK#=G-#J(NjNKs+*n!lctxbk9HJ%D)ctI;E4m>gtwHtpT*+Rms>)q(jdlYmmW zBR5otB1^!KroCh)hrvtsFkG|7ndiYz_9M8IK{yxj&AxAN3Ub{7+#_50IqXPoa179T zM!Yf&R|0;EY$RBuu@G1Uf`q9<1qV|H0R`DMV{tOUACBKI+lx?xKm!P=(qvX3JXyB; zBR-%gDlzdmYv#ojLj2(m1(fhA3W6)IVT)kM>)0Z=(uA!|C^n;Tp>U%hjCTRHwxHOG zVh)hJf_*67fdA1r2m}(7(K1y|)i3|PZq41gHIgf_GDWXRSJnci0LQ3(P+g0h1X}6& zTJ?*7ZPcXbMGu}L$HK+HMV>{BS+VZ>73=S=STD6C-y--#Sjs(miHZ;yi^o|?$iHkW z-Z%NEfb2s+td(gVuABn?$wa*#nBcAy2=6B(O}#FWl@q2#kdmx&_R3|;%j&i7Rbe|g zLedgF1Y*jd)9D`R%{uz0N=oPa7uE1js^N*rsB1_re?q~}lX8=8NwW0`1wT`jl-~A} r-lltbiURT20O}M`7ZF3Pj+vrBXa-c*5krk`=@bQm7#``NMOOb`zn3_x diff --git a/utils/visual_track_control/mavsdk_gateway.py b/utils/visual_track_control/mavsdk_gateway.py index 6cf7bea..6b0c460 100644 --- a/utils/visual_track_control/mavsdk_gateway.py +++ b/utils/visual_track_control/mavsdk_gateway.py @@ -30,7 +30,13 @@ class MavsdkCommandGateway: self._started = False async def start(self) -> bool: - """进入 offboard:PX4 要求先流式发送首条设定点再请求启动。""" + """进入 offboard:PX4 要求先流式发送首条设定点再请求启动。 + + 幂等:已启动(或重入)时**不重发** offboard.start(),避免任务状态机重入 + 动作(丢目标后回 SEARCH/参考点)反复重启 offboard 造成模式抖动。 + """ + if self._started: + return True try: await self._offboard.set_velocity_body( self._velocity_body(0.0, 0.0, 0.0, 0.0)) @@ -42,6 +48,23 @@ class MavsdkCommandGateway: logger.info("[MavsdkGateway] offboard 已启动") return True + async def send_yaw_rate(self, yaw_rate_dps: float, + forward_mps: float = 0.0, + down_mps: float = 0.0) -> bool: + """原地偏航扫视:零前向/侧向速度 + 指定偏航角速度(SEARCH 态扫视用)。 + + 与 send_brake_hover 同类,但带角速度,故机头缓慢旋转、位置保持—— + 吊舱相机刚性固连机体,转弯即改变观察方向,以此覆盖参考点周边。 + 异常一律捕获返回 False(与其它指令一致,不向外抛)。 + """ + try: + await self._offboard.set_velocity_body( + self._velocity_body(forward_mps, 0.0, down_mps, yaw_rate_dps)) + except Exception as exc: + logger.warning("[MavsdkGateway] 扫视指令发送失败: %s", exc) + return False + return True + async def send_velocity_heading(self, output: ControlOutput, time_boot_ms: Optional[int] = None) -> bool: try: diff --git a/工作进度.md b/工作进度.md index 440dfe6..04b886a 100644 --- a/工作进度.md +++ b/工作进度.md @@ -1,6 +1,6 @@ # 工作进度 -> 更新日期:2026-10-13 +> 更新日期:**2026-10-21** > 项目:无人机视觉目标追踪与控制系统 > (`reference-c++/` C++ 参考实现 + `utils/` Python 移植与新实现) > @@ -8,6 +8,9 @@ > 架构全貌与必须遵守的铁律见 `.workbuddy/memory/ARCHITECTURE.md`; > 「预定流程」全链路的操作指南与待办清单见 `预定流程-操作指南.md`; > 权威规格见 `docs/` 下各技术路线 md(代码注释用 `§章节号` 反向引用)。 +> +> ⚠ **架构口径(2026-10-19)**:光电吊舱已取代双目 + YOLO,成为**唯一目标源**; +> 双目三包已**归档到 `legacy/`**(见 §1.8),运行链路测试 446 条、归档测试 175 条。 --- @@ -17,22 +20,28 @@ | C++ 源工程 | Python 目标包 | 状态 | |-----------|--------------|------| -| `reference-c++/target_tracker` | `utils/target_tracker` | ✅ 完成(28 条用例) | -| `reference-c++/visual_track_control` | `utils/visual_track_control` | ✅ 完成(81 条 + L1 增量 5 条 = 86) | +| `reference-c++/target_tracker` | ~~`utils/target_tracker`~~ → **`legacy/utils/target_tracker`** | ✅ 完成(28 条用例,**已归档**) | +| `reference-c++/visual_track_control` | `utils/visual_track_control` | ✅ 完成(86 条,运行链路复用) | - 逐构件对照移植:测试断言逐条搬运(`EXPECT_DOUBLE_EQ` → 精确 `==`, `EXPECT_NEAR` → `pytest.approx`)。 -- 两包**互不依赖**,唯一转换点在胶水层 `utils/demo_track_control.py`。 +- `visual_track_control` 与追踪器**互不依赖**,唯一转换点是胶水层 + (胶水 demo 已归档到 `legacy/utils/demo_track_control.py`)。 - MAVLink 部分改为 **MAVSDK 语义映射**(offboard API,非字节透传), 三分支映射保留 C++ A2 回归语义。 - 行为修复项(C1/C2/C4、A1/A2/A4、S6)已在测试文件头注明对应章节。 -### 1.2 双目链路新实现(非移植,按规格新写) +### 1.2 双目链路实现(非移植,按规格新写)—— 📦 已归档 -| Python 包 | 规格来源 | 状态 | +> 2026-10-20 起三包与配套测试/demo/配置/权重/规格全部移到 `legacy/`(默认不跑测试)。 +> 原因:吊舱已成为唯一目标源,这套链路无生产调用方;保留是为了日后接回"独立旁证/距离源"。 +> 详见 `legacy/README.md`。 + +| Python 包 | 原位置 → 现位置 | 状态 | |----------|---------|------| -| `utils/stereo_ranger`(稀疏目标测距) | `docs/双目测距技术路线.md` | ✅ 完成(111 条) | -| `utils/stereo_yolo`(双目 YOLO 检测前端) | `docs/双目YOLO目标识别需求说明.md` | ✅ 完成(36 条) | +| `stereo_ranger`(稀疏目标测距) | `utils/` → `legacy/utils/` | ✅ 完成(111 条,随包归档) | +| `stereo_yolo`(双目 YOLO 检测前端) | `utils/` → `legacy/utils/` | ✅ 完成(36 条,随包归档) | +| `target_tracker`(多目标追踪) | `utils/` → `legacy/utils/` | ✅ 完成(28 条,随包归档) | - `stereo_ranger` 按规格五步流程实现,**纯标准库**,无第三方依赖; 越界即有效性门、视差域权威,偏差项已在其包 README 记录。 @@ -49,65 +58,123 @@ 支持运行时 L1/L2 切换(切换即 reset 清积分,防输出跳变)。 - **硬件结论(厂商答复 + 实测)**:本吊舱**不允许二次开发**;无激光 → **表4 永不出**, 目标坐标改走地面注入;`0x0F` 自动锁定是常驻模式(`0x0E` 只停当次,`0x08` 才退出识别); - bit2 锁定位目标丢失后不复位,必须加第二判据(我方视觉)。 + bit2 锁定位目标丢失后不复位 → **第二判据已改为 LOS 运动学一致性** + (`pod_bridge/los_consistency.py`,2026-10-19 取代旧「我方视觉」方案)。 - **S1 台架标定已完成(10-09)**:`beta_sign=+1`、`alpha_sign=-1` 已回填 `pod_bridge.json`,并有标定回归测试守护(`test_deployed_config_has_calibrated_signs`)。 - 配套工具:`virtual_pod`(模拟吊舱,无电机语义)、`verify_px4`(仅飞控验证,增量④)、 `pod_sitl_test.py`(L1 SITL 全流程)。 -### 1.4 预定流程全链路(`utils/task_runner`,10-12 落地) +### 1.4 预定流程全链路(`utils/task_runner`,10-12 落地;10-15 状态链重构;10-19 自定义返航 + LOS 判据) -「地面给坐标 → 天空解锁/接受坐标引导/追踪打击」全链路,**已在 PX4 SITL 端到端跑通(约 1m24s)**: +「地面给坐标 → 天空飞向参考点 → 搜索 → 缓慢靠近 → 自定义返航」全链路, +**已在 PX4 SITL 端到端跑通**,并完成**真吊舱(COM5)+ SITL** 硬件在环验证: + +状态链(10-15 重构后):`WAIT → ARM_TAKEOFF → GOTO(L2) → SEARCH(原地扫视) → TRACK(L1 缓慢靠近) → RTL` | 模块 | 文件 | 作用 | |------|------|------| -| 任务状态机 | `utils/task_runner/state_machine.py` | 6 状态纯逻辑转移:WAIT→ARM_TAKEOFF→GOTO(L2)→TRACK(L1)→APPROACH→RTL | -| 编排器 | `utils/task_runner/runner.py` | 状态机驱动 + 快照采集 + 状态动作(起飞/切律/返航) | +| 任务状态机 | `utils/task_runner/state_machine.py` | 状态纯逻辑转移;参考点=搜索起点,搜索超时/追踪超时→RTL | +| 编排器 | `utils/task_runner/runner.py` | 状态机驱动 + 快照采集 + 状态动作(起飞/扫视/切律/返航)+ 异常兜底返航 | +| 返航控制 | `utils/task_runner/return_home.py` | 自定义返航:回起飞点 → 恢复初始航向 → 垂直降落(不爬高) | | 日志模块 | `utils/task_runner/log.py` | 事件 JSONL + 遥测 CSV 双层结构化日志 | -| 吊舱控制 | `utils/task_runner/pod_control.py` | 真机吊舱识别/锁定指令(0x07/0x0F/0x0E/0x08) | +| 吊舱控制 | `utils/task_runner/pod_control.py` | 真机吊舱识别/锁定指令(0x07/0x0F/0x0E/0x08,只下发一次) | | 地面注入 | `utils/pod_bridge/ground_link.py` | UDP 接收地面站坐标(超龄判无效,替代表4) | +| 目标有效性 | `utils/pod_bridge/los_consistency.py` | **LOS 运动学一致性**第二判据(10-19 取代 `require_vision`) | | SITL 入口 | `task_runner_sitl.py` | 仿真全流程(VirtualPod + 虚拟目标) | | 真机入口 | `task_runner_real.py` | 真串口吊舱 + UDP 注入 + 吊舱控制 | | 地面站 | `ground_station/` | 坐标注入 + 状态可视化(Flask) | +「缓慢靠近」由既有 `TrackingControlLaw` 的无距离降级通道实现:`distance.no_distance_action += slow_approach` + `no_distance_approach_limit_mps=0.2`(SITL 实测 `vx_body` 稳定 0.2 m/s), +不需要新写控制律。 + +**自定义返航(10-19 新增)**:RTL 不再交给 PX4 原生 `return_to_launch()`(会先爬到 +`RTL_RETURN_ALT=30m` 再降),改为 `utils/task_runner/return_home.py` 驱动: +`RETURN`(保持转场高度飞回起飞点)→ `YAW`(原地转回起飞时刻航向)→ `LAND`(垂直降落)。 +SITL 实测:全程 4.91–5.06 m,距起飞点 16.1→1.6 m,航向恢复到 4° 内,高度 5.01→-0.03 m。 +兜底(未起飞/无起飞点/转场超时/主循环异常)仍退回 PX4 原生 RTL;`--land` 就地降落。 + +**目标有效性第二判据(10-19 换成 LOS 一致性)**:吊舱取代双目后"我方视觉"不存在, +旧 `require_vision` 已删除(要么恒假、要么自证)。改为 `pod_bridge/los_consistency.py`: +本吊舱无电机、相机刚性固连 → 机头转 Δψ 时真实目标脱靶量必变 ≈−Δψ;以 `Δβ+Δψ` +(3s 窗口 / 20° 激励 / 8° 残差)判"角度是活的还是冻住的",超限即 `tracked=False` +且 `pod_locked()` False(状态机随之退出 TRACK 重搜)。 +⚠ 局限:只证"角度活着",不证"是那个目标";锁错物体但角度正常变化无法识别。 + ### 1.5 测试 -**483 条用例全部通过(实测,2026-10-13)**: +**运行链路 446 条全部通过(实测,2026-10-20;归档包另 175 条)**: | 测试组 | 用例数 | 覆盖内容 | |--------|-------|---------| -| `tests/stereo_ranger` | 111 | 规格 §11.4 构件级 + 全流程 | | `tests/visual_track_control` | 86 | 对应 C++ gtest + L1 角度直入增量 | -| `tests/pod_bridge` | 173 | 协议/网关/桥接/L1 + virtual_pod/ground_link | -| `tests/task_runner` | 49 | 状态机/编排器/日志/吊舱控制 | -| `tests/stereo_yolo` | 36 | 假检测器驱动 | -| `tests/target_tracker` | 28 | 对应 C++ gtest 用例 | -| **合计** | **483** | 较 09-15(256)新增 227 | +| `tests/pod_bridge` | 208 | 协议/网关/桥接/L1 + virtual_pod/ground_link + 节流 + **LOS 一致性** | +| `tests/task_runner` | 122 | 状态机(含 SEARCH)/编排器/日志/吊舱控制/自定义返航 | +| `tests/ground_station` | 30 | 地面站页面与日志解读 | +| **运行链路合计** | **446** | 默认 `cd utils && pytest` 只跑这些 | +| (归档)`legacy/utils/tests/{stereo_ranger,stereo_yolo,target_tracker}` | 175 | 见 `legacy/README.md`,默认不跑 | -- stereo_ranger / stereo_yolo 测试**不需要**真实图像、模型权重或硬件。 +> **架构口径(2026-10-19 / 10-20)**:双目 + YOLO 三包已**归档到 `legacy/`**; +> 运行链路(`task_runner_*` / `pod_bridge`)不依赖它们 —— 吊舱是唯一目标源。 +> 目标有效性第二判据为 **LOS 运动学一致性**(`pod_bridge/los_consistency.py`), +> `require_vision` 已删除。 + +- 归档的双目测试**不需要**真实图像、模型权重或硬件。 - ⚠ codegraph 报告 `AssociateDetections` / `SelectPrimary` 等“无测试”属调用图误报,勿据此判断。 -### 1.6 仿真 / 演示 +### 1.6 仿真 / 运行入口 | 脚本 | 内容 | 结果 | |------|------|------| -| `utils/demo_track_control.py --speedup 10` | 模拟检测 → 追踪 → 控制律 → 发送,20 Hz 时间线 | ✅ | -| `utils/demo_stereo_ranger.py` | 合成框对,六场景 | ✅ | -| `utils/demo_stereo_yolo.py` | 真模型推理 + 合成素材五场景 | ✅ | -| `task_runner_sitl.py`(PX4 SITL) | 预定流程全链路 6 状态 + 地面站联动 | ✅ 端到端跑通(10-12,约 1m24s) | +| `task_runner_sitl.py`(PX4 SITL) | 全链路 `goto→search→track→rtl` + 自定义返航 | ✅ 端到端跑通(10-19 复跑) | +| `task_runner_real.py`(真吊舱 COM5 + SITL 飞控) | 同上,真串口吊舱在环 | ✅ 无目标安全路径 + 强制锁定 TRACK 路径(10-19) | +| `l1_sitl_test.py`(PX4 SITL) | 只覆盖 L1 控制律闭环(原 `pod_sitl_test.py`) | ✅ SITL 实跑 | +| (归档)`legacy/utils/demo_*.py` | 双目/追踪演示 | 📦 移入 legacy,默认不跑 | -- SITL 集成脚本:`test.py`(视觉闭环早前版本)/ `pod_sitl_test.py`(L1 吊舱版)已就位; - 当前全流程实跑走 `task_runner_sitl.py`。 +- 共享件 `now_ms` / `wait_until_ready` / `TelemetryHub` 等已从"测试脚本"拆到 + `utils/sitl_support.py`,两个入口不再 import 测试文件。 +- 吊舱硬件在环实测(10-19,COM5/CH340):表2-2 **60.0 Hz**、`self_check=0x00`; + 表3 20Hz 下发不打断上行;表2-1 指令帧校验和与协议样例一致。 + 无目标时 `status1=0x00` 恒定(bit2=0)→ 全链路走"搜索 20s 超时→返航"安全路径。 + ⚠ **仍未验证**:吊舱真实目标识别(bit2 0→1),需视场内放目标。 ### 1.7 文档与索引 -- `docs/` 现有 10 份:双目测距技术路线、双目 YOLO 需求说明、视觉跟踪控制律技术路线、 +- `docs/` 现有 7 份(3 份双目规格已归档到 `legacy/docs/`):视觉跟踪控制律技术路线、 视觉追踪技术路线、物理追踪思路、吊舱解算验证方案(模拟飞控喂表3)、树莓派 4B 部署注意事项、 - 双目相机模组到位作业指导、**L1 航向伺服实现方案**、光电吊舱控制协议 G 版-PB(docx)。 -- 根目录:`预定流程.md`(需求)+ `预定流程-操作指南.md`(操作指南与待办,链路权威)。 -- `.workbuddy/memory/ARCHITECTURE.md` + `MEMORY.md`:完整架构参考与精简铁律。 -- `.codegraph/` 索引 140 文件 / 2337 节点,可直接 codegraph 查询。 -- 相机标定已起步:根目录 `camera.py`(棋盘格采集脚本)+ `stereo_calib_params.npz`(未入库)。 + **L1 航向伺服实现方案**、光电吊舱控制协议 G 版-PB(docx)。 +- 根目录:`预定流程.md`(需求)+ `预定流程-操作指南.md`(操作指南与待办,链路权威)+ `README.md`(总览)。 +- `legacy/README.md`(归档说明)、`tools/README.md`(临时脚本说明)。 +- `.workbuddy/memory/ARCHITECTURE.md` + `MEMORY.md`:完整架构参考与精简铁律(已对齐新架构)。 +- `.codegraph/` 已于 10-20 重建(旧索引过期,清理后重新索引)。 +- 相机标定脚本已移到 `tools/`;标定结果归档在 `legacy/stereo_calib_params.npz`。 + +### 1.8 仓库结构精简(2026-10-20 完成,2026-10-21 提交) + +**动机**:子项目堆积了大量文件,其中双目/YOLO 一套在"吊舱取代双目"后已无生产调用方, +旧架构脚本与生成物也混在根目录,影响阅读与使用。 + +**做了什么**: + +| 处理 | 内容 | +|---|---| +| 归档 `legacy/` | `stereo_yolo` / `stereo_ranger` / `target_tracker` 三包 + 175 条测试 + 3 个 demo + `stereo_*.json` + 素材 + `models/yolo26n-drone.pt` + 三份双目规格 + `stereo_calib_params.npz` | +| 删除 | `test.py`(旧架构双目闭环,未实跑)、`trajectory_plot.png`、`新建 文本文档.txt`、空目录、全部 `__pycache__` / `.pytest_cache` | +| 移入 `tools/` | `camera.py` / `camera_test.py` / `video_test.py`(台架相机脚本) | +| 拆分改名 | `pod_sitl_test.py` → `utils/sitl_support.py`(入口共享件)+ `l1_sitl_test.py`(L1 测试);两个入口不再 import 测试文件 | +| 从索引剔除 | 59 个历史误跟踪的 `.pyc`(已被 `.gitignore` 覆盖) | +| 新增 `.gitignore` | `__pycache__/`、`.pytest_cache/`、`logs/`、`.codegraph/`、`.workbuddy/`、`.idea/`、`legacy/models/*.pt` 等 | +| 依赖清单 | `requirements.txt` 按实际 import 重写(去 `ultralytics`/`matplotlib`,补 `flask`);`requirements-pod.txt` 更新条数 | +| 文档 | 所有现行文档同步新路径/新计数(README、操作指南、ARCHITECTURE、MEMORY、utils/README、L1 方案、树莓派部署);新增 `legacy/README.md`、`tools/README.md` | +| codegraph | 删除过期索引并重建(143 文件 / 2667 节点 / 6470 边) | + +**结果**: + +- 根目录从 18 个文件减到 **10 个**(3 个入口/测试脚本 + 4 份 md + 2 份 requirements + `.gitignore`) +- `utils/` 从 6 个包 + 3 个 demo 减到 **4 个包 + `sitl_support.py`**(体积 3.39MB → 1.78MB) +- 测试口径:运行链路 **446 passed**;归档 **175 passed**(`cd legacy/utils && PYTHONPATH="../../utils" pytest`) +- 运行链路代码对已归档包的引用:**0 处** --- @@ -121,15 +188,21 @@ ### 2.2 数值参数未标定(阻塞飞行决策) -- 预定流程链路:`switch_radius_m` / `center_threshold_deg` / `center_hold_s` / - `takeoff_altitude_m` 与 `heading.gain`、`guidance.speed_kp/yaw_kp` 均为占位 - (`beta_sign`/`alpha_sign` 已按 S1 标定回填);**标定回填前不得实飞**。 -- `utils/config/stereo_ranger.json` 几何参数为占位;解禁路径见 - `docs/双目相机模组到位作业指导.md` P0→P6(已起步标定采图,未走完)。 +- **状态机**(`build_sm_config()`):`arrival_radius_m` / `goto_timeout_s` / `search_timeout_s` / + `track_timeout_s` / `sweep_rate_dps` / `sweep_reverse_s` / `takeoff_altitude_m` 均为占位。 +- **返航**(`build_return_config()`):`speed_mps` / `arrival_radius_m` / `yaw_align_tolerance_deg` / + `alt_kp` 及各类超时均为占位。 +- **控制律**:`heading.gain`、`guidance.speed_kp/yaw_kp` 为占位 + (`beta_sign`/`alpha_sign` 已按 S1 标定回填;LOS 判据阈值 `los_min_yaw_change_deg` / + `los_max_residual_deg` 亦待台架实测调整)。 +- **标定回填前不得实飞**。 +- (归档)`legacy/utils/config/stereo_ranger.json` 几何参数为占位;若日后接回双目, + 解禁路径见 `legacy/docs/双目相机模组到位作业指导.md` P0→P6(标定未走完)。 ### 2.3 任务级状态机规格对齐 -- 「预定流程」版状态机(6 态)已实现并 SITL 跑通; +- 「预定流程」版状态机已实现并 SITL + 真吊舱在环跑通(`WAIT→ARM_TAKEOFF→GOTO→SEARCH→ + TRACK→RTL`,2026-10-19 口径); - 但 `docs/物理追踪思路.md` 引用的 S5~S8 版规格(`docs/状态机设计.md` 等)仍缺失, 两套状态集是否对齐/合并需总项目侧确认(见 §3.1)。 @@ -154,50 +227,54 @@ 未构建、未执行 ctest。C++ 测试用例文件齐全(11 个), 需先 `cmake .. && make && ctest`。若以 Python 侧为验证基准,此点可暂不处理。 -### 3.3 环境不一致(可立即修) +### 3.3 环境口径(已统一,曾不一致) -`utils/README.md` 声明 conda env `pytorch`(`D:\program file\conda\envs\pytorch`), -但实测该环境 **pytest 缺失**;全部测试通过隔离 venv -(`C:\Users\yimiao\.workbuddy\binaries\python\envs\drone-test`,基于该 conda 解释器 -`--system-site-packages` 仅加装 pytest)完成,**未改动 conda 环境**。 -建议择机 `pip install pytest` 回填,使 README 与实际一致。 +**现行唯一口径**:conda env `pytorch`(`D:\program file\conda\envs\pytorch\python.exe`), +pytest 9.1.1 可用,全量 621 passed 直接跑。 +> 历史:曾一度认为该环境缺 pytest、改走隔离 venv `drone-test`。该结论**已过时**, +> 隔离 venv 不再需要;文中若见旧 venv 路径一律以 conda `pytorch` 为准。 ### 3.4 真机安全红线 标定回填前**不得实飞**;S3 起必须:不装桨 / 空旷场地 / RC 随时可切手动 / 有人值守。 -### 3.5 帧周期算力不可外推 +### 3.5 帧周期算力不可外推(归档项) `stereo_yolo` 实测帧周期 25~35 ms 为台架开发机数据,与 30 fps 预算同量级, -结论不可外推到机载平台。 +结论不可外推到机载平台(**这也是当前不接双目、距离优先用雷达的原因之一**)。 +该包已归档到 `legacy/`。 --- ## 4. 下一步(建议顺序) -1. **真机增量验证 ②~⑤**——对齐 `预定流程-操作指南.md` §2.4,③ 干跑优先, - 先把 β→yaw_rate 符号/量级钉死。 -2. **占位参数标定回填**——状态机 4 项 + L1/L2 增益。 -3. P1:摄像头接真视频流;`TaskStateConfig` 外置 JSON;地面站与 runner 实时打通。 -4. P2:`.gitignore` 补齐(`logs/`、`__pycache__/`、`.codegraph/` 等);conda 回填 pytest; - C++ `cmake && ctest`。 -5. 相机模组到位后按作业指导走 P0→P6;确认 §3.1 四份上游规格可获取位置。 +1. **接距离源(激光雷达)**:给 `PodBridge` 加距离注入点 → 填 `distance_valid` → + 打开 `d_exp_m` 距离 PID,并把 TRACK 的"到点"判据从纯超时改为距离判据。 + (低算力、收益最大;双目暂不接。) +2. **真机目标识别验证**:视场内放目标(人/无人机)+ 0x07,确认 bit2 0→1 + 与 TRACK 真实闭环;顺带复核 LOS 判据阈值是否合适。 +3. **占位参数标定回填**:状态机 6 项 + 返航 6 项 + L1/L2 增益 + LOS 两阈值。 +4. P1:摄像头接真视频流;`TaskStateConfig`/`ReturnConfig` 外置 JSON;地面站与 runner 实时打通。 +5. P2:C++ `cmake && ctest`;codegraph 定期 `sync`(本次已重建)。 +6. 相机模组到位后按作业指导走 P0→P6(脚本在 `tools/`,文档在 `legacy/docs/`); + 确认 §3.1 四份上游规格可获取位置。 --- ## 5. 验证命令(实测所用) ```bash -# Python 全部测试(483 条) +PY="D:/program file/conda/envs/pytorch/python.exe" + +# 运行链路测试(446 条) cd "D:/ProgramData/code_drone-test/utils" -"C:/Users/yimiao/.workbuddy/binaries/python/envs/drone-test/Scripts/python.exe" -m pytest -q +"$PY" -m pytest -q -# 三个演示 -"C:/Users/yimiao/.workbuddy/binaries/python/envs/drone-test/Scripts/python.exe" demo_track_control.py --speedup 10 -"C:/Users/yimiao/.workbuddy/binaries/python/envs/drone-test/Scripts/python.exe" demo_stereo_ranger.py -"C:/Users/yimiao/.workbuddy/binaries/python/envs/drone-test/Scripts/python.exe" demo_stereo_yolo.py +# 归档的可选后端测试(175 条,按需) +cd "D:/ProgramData/code_drone-test/legacy/utils" +PYTHONPATH="../../utils" "$PY" -m pytest -q -# SITL 全流程 / 地面站联动 / 真机:见 预定流程-操作指南.md §五「一键命令汇总」 +# 吊舱台架 / SITL 全流程 / 地面站 / 真机:见 预定流程-操作指南.md §五「一键命令汇总」 ``` --- @@ -206,16 +283,21 @@ cd "D:/ProgramData/code_drone-test/utils" | 工作项 | 状态 | 备注 | |--------|------|------| -| C++ `target_tracker` → Python | ✅ 完成 | 28 条用例 | -| C++ `visual_track_control` → Python | ✅ 完成 | 86 条(含 L1 增量) | -| `stereo_ranger` 双目测距 | ✅ 完成 | 111 条;几何参数待标定 | -| `stereo_yolo` 检测前端 | ✅ 完成 | 36 条;真实素材待联调 | -| 三路 demo 仿真验证 | ✅ 完成 | — | +| C++ `target_tracker` → Python | ✅ 完成 | 28 条用例;**已归档 legacy/** | +| C++ `visual_track_control` → Python | ✅ 完成 | 86 条(含 L1 增量);运行链路复用 | +| `stereo_ranger` 双目测距 | 📦 归档(10-20) | 111 条随包;几何参数待标定 | +| `stereo_yolo` 检测前端 | 📦 归档(10-20) | 36 条随包;真实素材待联调 | +| 三路 demo 仿真验证 | 📦 归档(10-20) | 移入 `legacy/utils/` | | `pod_bridge` 桥接 + L1 航向伺服 | ✅ 完成(10-09) | S1 符号已标定;增益待标 | -| **预定流程全链路(task_runner)** | ✅ **SITL 跑通(10-12)** | 已提交 `ded17d7` | -| 真机增量验证 ②~⑤ | ❌ 未开始 | P0;实飞前必须 | -| 状态机/L1/L2 占位参数标定 | ❌ 未开始 | 回填前不得实飞 | +| **预定流程全链路(task_runner)** | ✅ **SITL 跑通(10-12);10-15 重构;10-19 真吊舱在环** | 状态链含 SEARCH + 自定义返航 | +| LOS 运动学一致性判据 | ✅ 完成(10-19) | 取代 `require_vision`;阈值待台架调整 | +| 自定义返航(回点+复位航向+垂直降落) | ✅ SITL 验证(10-19) | 全程 4.91~5.06 m,不爬高 | +| 真机增量验证 ①②③③b | ✅ 已过 | 串口链路/干跑/真吊舱+SITL 全流程 | +| 真机增量验证 ④⑤(仅飞控 / 实飞地面联调) | ❌ 未开始 | P0;实飞前必须 | +| 吊舱**真实目标识别**(bit2 0→1) | ❌ 未验证 | 需视场内放目标 | +| 距离源接入(激光雷达) | ❌ 未开始 | 打开距离 PID 与到点判据 | +| 状态机/返航/L1/L2 占位参数标定 | ❌ 未开始 | 回填前不得实飞 | | S5~S8 版状态机(物理追踪思路) | ⚠ 规格缺失 | 待对齐(§3.1) | -| 相机模组标定 P0~P6 | ⏳ 起步 | `camera.py` 采集中 | +| 相机模组标定 P0~P6 | 📦 归档 | 脚本移到 `tools/`,标定数据在 `legacy/` | | C++ 侧构建与 ctest | ⚠ 未执行 | 无 build 目录 | -| conda 环境回填 pytest | ⚠ 待办 | 测试现走隔离 venv | +| ~~conda 环境回填 pytest~~ | ✅ 已解决 | 运行链路测试统一走 conda `pytorch`(446 条) | diff --git a/预定流程-操作指南.md b/预定流程-操作指南.md index fe94135..0b12448 100644 --- a/预定流程-操作指南.md +++ b/预定流程-操作指南.md @@ -1,29 +1,43 @@ -# 预定流程 · 操作指南与待办事项 +# 预定流程 · 操作指南与待办事项 -> 更新日期:2026-10-12 -> 覆盖:任务级状态机全链路(`utils/task_runner/`)+ 地面站页面 + 真机/仿真入口 +> 更新日期:**2026-10-19** +> 覆盖:任务级状态机全链路(`utils/task_runner/`)+ 吊舱桥接与 LOS 判据 + 地面站页面与控制命令通道 + 真机/仿真入口 > -> 本链路对应 `预定流程.md` 的「地面给坐标 → 天空解锁/接受坐标引导/追踪打击」, -> 已在 **PX4 SITL 端到端跑通**。架构与铁律见 `.workbuddy/memory/MEMORY.md` / `ARCHITECTURE.md`。 +> 本链路对应 `预定流程.md` 的「地面给坐标 → 天空解锁/接受坐标引导/搜索/缓慢靠近」, +> 已在 **PX4 SITL 端到端跑通**,并完成**真吊舱(COM5)+ SITL** 硬件在环验证。 +> 架构与铁律见 `.workbuddy/memory/MEMORY.md` / `ARCHITECTURE.md`。 +> +> ⚠ 架构口径:吊舱取代双目 + YOLO 成为**唯一目标源**;`require_vision` 已删除, +> 第二判据改为 **LOS 运动学一致性**(见 §2.6)。 --- ## 一、本次落地了什么 +> **2026-10-15 按实机作业预期重构状态链**:参考点 = 搜索起点(不再是终点), +> 到点后原地扫视搜索,锁定后按 `slow_approach` **缓慢靠近**,超时返航。 +> +> 新状态链:`WAIT → ARM_TAKEOFF → GOTO(L2 飞向参考点) → SEARCH(原地扫视) → TRACK(L1 缓慢靠近) → RTL` + | 模块 | 文件 | 作用 | |------|------|------| -| 任务状态机 | `utils/task_runner/state_machine.py` | 6 状态纯逻辑转移:WAIT→ARM_TAKEOFF→GOTO(L2)→TRACK(L1)→APPROACH→RTL | -| 编排器 | `utils/task_runner/runner.py` | 状态机驱动 + 采集快照 + 状态动作(起飞/切律/返航) | -| 日志模块 | `utils/task_runner/log.py` | 事件 JSONL + 遥测 CSV 双层结构化日志 | -| 吊舱控制 | `utils/task_runner/pod_control.py` | 真机吊舱识别/锁定指令(0x07/0x0F/0x0E/0x08) | +| 任务状态机 | `utils/task_runner/state_machine.py` | 状态纯逻辑转移:到点→SEARCH 扫视;搜索超时/追踪超时→RTL;TRACK 丢目标→回 SEARCH | +| 编排器 | `utils/task_runner/runner.py` | 状态机驱动 + 采集快照 + 状态动作(起飞/扫视/切律/返航)+ **主循环异常兜底返航** | +| 日志模块 | `utils/task_runner/log.py` | 事件 JSONL + 遥测 CSV(机器日志)+ 简洁任务简报 `task_brief.log` | +| 吊舱控制 | `utils/task_runner/pod_control.py` | 真机吊舱识别/锁定指令(0x07/0x0F/0x0E/0x08);识别开 **只下发一次**(防重入自激) | | 地面注入 | `utils/pod_bridge/ground_link.py` | UDP 接收地面站坐标(超龄判无效) | | SITL 入口 | `task_runner_sitl.py` | 仿真全流程(VirtualPod + 虚拟目标) | | 真机入口 | `task_runner_real.py` | 真串口吊舱 + UDP 注入 + 吊舱控制 | -| 地面站 | `ground_station/app.py` + `templates/index.html` | 坐标注入 + 状态可视化(Flask) | +| 地面站 | `ground_station/app.py` + `templates/index.html` | 坐标注入 + 任务/吊舱控制 + 状态可视化(Flask) | +| 命令通道 | `ground_link.py`(队列)+ `runner.py`(分派)+ `app.py`(`/api/command`) | 页面 `{"cmd":…}` → UDP 14555 → runner 逐拍分派 | 控制律本体复用既有 `visual_track_control.TrackingControlLaw`(L1 角度直入)与 `pod_bridge.GuidanceLaw`(L2 地理引导),**运行时通过 `bridge.set_heading_servo_enabled()` 切换**。 +**「缓慢靠近」不需要新写控制律**:既有 `TrackingControlLaw` 的无距离降级通道已实现—— +`distance.no_distance_action = "slow_approach"` + `no_distance_approach_limit_mps = 0.2`。 +SITL 实测 TRACK 段 `vx_body` 稳定 **0.2 m/s**,且经加速度限幅平滑起步。 + --- ## 二、操作指南 @@ -32,11 +46,12 @@ ```bash cd "D:/ProgramData/code_drone-test/utils" -"C:/Users/yimiao/.workbuddy/binaries/python/envs/drone-test/Scripts/python.exe" -m pytest -q -# 预期:483 passed +"D:/program file/conda/envs/pytorch/python.exe" -m pytest -q +# 预期:446 passed(2026-10-20,运行链路;归档 175 条另计) ``` -> ⚠ conda `pytorch` 环境缺 pytest(见 §三 待办 P2-2),测试用隔离 venv `drone-test`。 +> 环境口径:项目默认 Python 环境为 conda env `pytorch`(含 pytest/flask/mavsdk/pyserial), +> 运行与测试统一使用;「conda 缺 pytest,测试走隔离 venv」的旧口径已废止。 ### 2.2 SITL 仿真全流程(独立模式,内置虚拟目标自动注入坐标) @@ -44,41 +59,96 @@ cd "D:/ProgramData/code_drone-test/utils" ```bash # 1. WSL 内启动 PX4 SITL -cd ~/PX4-Autopilot && make px4_sitl jmavsim +cd ~/PX4-Autopilot && make px4_sitl gz_x500 # 2. Windows 侧跑全流程(内置虚拟目标 home 北30m/东15m) cd "D:/ProgramData/code_drone-test" -"D:/program file/conda/envs/pytorch/python.exe" task_runner_sitl.py --conn udp://:14540 +"D:/program file/conda/envs/pytorch/python.exe" task_runner_sitl.py --conn udpin://0.0.0.0:14540 ``` -预期:`wait → arm_takeoff → goto → track → approach → rtl`,全程约 1m24s。 +预期:`wait → arm_takeoff → goto → search → track → rtl`。 + +> **返航已改为自定义流程(2026-10-19)**:不再调 PX4 原生 `return_to_launch()`。 +> 原生 RTL 会先爬到 `RTL_RETURN_ALT`(本 SITL 实测 **30 m**)、再降到 +> `RTL_DESCEND_ALT`(10 m)后落地,即"飞很高再降落"。自定义流程改为: +> **保持转场高度(取当前高度与起飞高度的较大者,绝不额外爬高)飞回起飞点 → +> 原地把机头转回起飞时刻航向 → 垂直降落**。SITL 实测全程高度 4.91–5.06 m。 +> +> 参数在 `task_runner_{sitl,real}.py` 的 `build_return_config()`(`ReturnConfig`); +> 兜底:未起飞/无起飞点/转场超时/主循环异常 → 退回 PX4 原生 RTL; +> `--land` → 就地降落(不返航)。 ### 2.3 SITL 仿真(地面站联动模式,坐标由地面站 UDP 注入) ```bash # 终端 A:无人机(--ground-station 表示坐标走 UDP 接收,不再内置自动注入) -"D:/program file/conda/envs/pytorch/python.exe" task_runner_sitl.py --conn udp://:14540 --ground-station +"D:/program file/conda/envs/pytorch/python.exe" task_runner_sitl.py --conn udpin://0.0.0.0:14540 --ground-station -# 终端 B:地面站页面 -"D:/program file/conda/envs/pytorch/python.exe" ground_station/app.py --udp-port 14555 --log-dir logs -# 浏览器打开 http://127.0.0.1:5000,输入坐标点「注入坐标」 +# 终端 B:地面站页面(--rtsp-url 可选:接入吊舱实时视频,断流自动重连) +"D:/program file/conda/envs/pytorch/python.exe" ground_station/app.py --udp-port 14555 --log-dir logs --rtsp-url rtsp://192.168.144.119:8554/video +# 浏览器打开 http://127.0.0.1:5000,输入坐标点「注入坐标」(或勾选「持续注入」自动重发) ``` ⚠ 地面站需**持续**发坐标(每 0.5~1s 一次),否则 `ground_inject.timeout_ms`(默认 5s) -超龄后 GOTO 判「坐标失效」返航。页面上手动点一次只发一次,联调时建议用脚本循环注入。 +超龄后 GOTO 判「坐标失效」返航。页面勾选「持续注入」即可(**服务端后台线程**每 0.5s 重发, +切窗口/最小化不影响;实测浏览器会节流后台标签页的 JS 定时器 0.5s→约 1 次/分钟,故不能放页面侧); +取消勾选或关闭地面站程序才停止。 -### 2.4 真机联调(增量流程,标定前不得实飞) +### 2.4 地面站页面控制功能(2026-10-14 新增) + +页面除坐标注入外,还支持以下控制(均走**同一 UDP 端口** 14555,消息 `{"cmd": ""}`, +由 `GroundLink` 入队、`TaskRunner` 主循环逐拍分派;未知命令记警告忽略): + +| 区块 | 控件 | 命令 | 语义 | +|------|------|------|------| +| 链路目标 | 目标徽标 + 下拉选择 + 「切换目标」 | — | 坐标/命令的 UDP 目标地址(默认「本地回环」127.0.0.1:14555 跑仿真;可选本机网卡或自定义 IP,运行时切换,2026-10-14 新增) | +| 任务日志 | 滚动事件列表 | — | 阶段切换 / 地面站命令 / 飞控状态文本(STATUSTEXT,警告按级别着色)实时显示(读 `/api/state`,2026-10-14 新增) | +| 坐标注入 | 坐标形式(经纬度 / 局部北东 m;局部可选原点) | — | 局部按选定原点换算为经纬度后注入——「起飞点(home)」= 本次任务运行段起始位置(≈起飞点),「当前坐标」= 注入时刻无人机位置;页面显示换算结果与所用原点(2026-10-14 新增) | +| 坐标注入 | 「持续注入」勾选 | — | 服务端后台线程每 0.5s 重发当前坐标(替代循环脚本,与页面可见性无关) | +| 吊舱视频 | 「吊舱视频」面板 | — | 服务端 `--rtsp-url` 拉 RTSP → MJPEG 推流(浏览器原生显示);断流自动重连 | +| 任务控制 | 请求返航 RTL | `rtl` | `runner.request_rtl()`:人工返航,任何状态立即转 RTL | +| 任务控制 | 紧急终止 | `emergency` | `runner.trigger_emergency()`:最高优先级立即返航 | +| 吊舱控制 | 开识别 | `pod_detect_on` | 0x07(目标类型)+0x0F(对齐 pod_console 按键 d) | +| 吊舱控制 | 自动锁定 | `pod_auto_lock` | 0x0F+0x10(对齐按键 a,识别已开时可用) | +| 吊舱控制 | 停跟踪 | `pod_stop_track` | 0x0E:仅解当前锁定,0x0F 常驻自动锁定仍生效(对齐按键 u) | +| 吊舱控制 | 关识别 | `pod_detect_off` | 0x0E+0x08:彻底退出识别(对齐按键 s) | +| 吊舱控制 | 归中 | `pod_center` | 0x2B(对齐按键 c) | + +说明:RTL/紧急在 SITL 可直接验证。**注意 `rtl` 命令触发的是自定义返航** +(回起飞点→恢复初始航向→垂直降落,不爬高;见 §2.2 说明),不是 PX4 原生 RTL。 +吊舱命令在 SITL(VirtualPod 无控制口)会被忽略并记警告, +真机经 runner → 吊舱串口下发(2026-10-19 台架实测:0x2B/0x07/0x0F/0x10/0x0E/0x08 全达真吊舱、校验和正确)。 + +### 2.5 真机联调(增量流程,标定前不得实飞) 按顺序逐级验证,每级过了再进下一级: | 级 | 动作 | 命令 | 通过判据 | |----|------|------|---------| -| ① 纯软单测 | 全量 pytest | `pytest -q` | 483 passed | +| ① 纯软单测 | 全量 pytest | `pytest -q` | 446 passed(2026-10-20) | | ② 串口回环 | 吊舱自环 | 台架工具 | 收发一致 | -| ③ 仅吊舱 | 干跑(不发 offboard) | `python utils/pod_bridge/run_bridge.py --config utils/config/pod_bridge.json --port /dev/ttyAMA0 --dry-run` | β→yaw_rate 符号/量级正确 | +| ③ 仅吊舱 | 干跑(不发 offboard) | `python utils/pod_bridge/run_bridge.py --config utils/config/pod_bridge.json --port COM5 --dry-run` | β→yaw_rate 符号/量级正确 | +| ③b 吊舱控制联测 | SITL 飞控 + 真吊舱直连本机(TTL 转 USB) | `python task_runner_real.py --port COM5 --conn udpin://0.0.0.0:14540 --log-dir logs/x` + 页面吊舱按钮 | 命令记录无失败、吊舱按指令响应 | | ④ 仅飞控 | 飞控遥测/offboard | — | 遥测新鲜、offboard 可启停 | | ⑤ 地面联调 | 真机全流程 | `python task_runner_real.py --target-type 1` | 完整走完 | +**2026-10-19 真吊舱在环实测结论**(COM5 / CH340,SITL 飞控): + +| 项 | 结果 | +|----|------| +| 表2-2 上行 | **60.0 Hz**,6s 359 帧;`self_check=0x00` 无告警 | +| 表3 下行 | 20Hz 持续下发时上行仍稳 60.0 Hz,**下行不打断上行** | +| 表2-1 指令 | 0x2B/0x07/0x0F/0x10/0x0E/0x08 均下发成功,帧校验和与协议样例一致(0x2B→`a6`) | +| 状态位 | 视场内无目标时 `status1=0x00` 恒定(**bit2 恒 0 = 搜索态**,符合预期) | +| 脱靶量 | 搜索态恒 `(0.00, 0.00)` —— 与文档「丢失表现为 0」一致 | +| 自身 IMU | `att_roll/att_pitch` 恒 0;`frame_*` 恒 0(本版无电机,符合厂商确认) | +| 全流程(无目标) | `arm_takeoff → goto → search(20s 超时) → rtl → 落地` ✅ | +| 全流程(强制锁定混合) | `goto → search → track(20s) → rtl`;TRACK 段 `vx` 0.05→**0.2 m/s**,距离 29.5→26.2 m ✅ | +| 扫视实测 | SEARCH 20s 内机头 yaw 跨度 **65°**,水平距离 26.6–29.9 m **无漂移**,vz 恒 0 ✅ | + +⚠ **仍未验证**:吊舱真实目标识别(bit2 由 0→1)。需视场内放置目标(人或无人机) +并下发 0x07,才能验证「搜索发现目标→锁定→缓慢靠近」的真实闭环。 + 真机全流程前置: 1. 填 `utils/config/pod_bridge.json` 的 `pod_serial.port`(如 `/dev/ttyAMA0`)、`px4.connection_string`; 2. 地面站往 `ground_inject.port`(默认 14555)持续发 `{"lat":..,"lon":..,"alt":..}`。 @@ -90,15 +160,19 @@ python task_runner_real.py --port /dev/ttyAMA0 # 覆盖吊舱串口 python task_runner_real.py --conn serial:///dev/ttyS1:57600 # 覆盖飞控连接串 ``` -### 2.5 硬件铁律(真机必读,来自厂商确认 + 实测) +### 2.6 硬件铁律(真机必读,来自厂商确认 + 实测) - 🔴 **本吊舱无激光 → 表4 目标经纬度永不出**,目标坐标必须走地面注入(`ground_link`), `guidance` 吃注入坐标而非表4。 - **0x0F「发现目标自动锁定」是常驻模式**,`0x0E` 只停当前这次跟踪、不退出自动锁定; 只有 `0x08 目标识别关` 才真正退出识别。 - **bit2 锁定位在目标丢失后不复位**,且脱靶量无无效标志位(丢失表现为 0)——单靠 bit2 - 会朝过期坐标继续飞,必须加第二判据(我方视觉,`require_vision`)。 -- **自动解锁(`--auto-unlock`)在本设备禁用**(表4 恒不发导致误解锁),解锁一律用按键 `u`。 + 会朝过期坐标继续飞。**第二判据已改为 LOS 运动学一致性**(`pod_bridge/los_consistency.py`, + 2026-10-19 取代旧的 `require_vision`):本吊舱无电机、相机刚性固连,机头转 Δψ 时真实 + 目标的脱靶量必变 ≈−Δψ;机头已明显转动而脱靶量不动 → 判丢(触发退出 TRACK 重搜)。 + ⚠ 它只能证明「角度是活的」,不能证明「是那个目标」。 +- **自动解锁(`--auto-unlock`)在本设备禁用**(表4 恒不发导致误解锁),解锁一律用 `0x0E`: + 台架走 `pod_console` 按键 `u`,地面站页面走「停跟踪」按钮(等效通道)。 --- @@ -106,30 +180,39 @@ python task_runner_real.py --conn serial:///dev/ttyS1:57600 # 覆盖飞控连 ### P0(实飞前必须) -- [ ] **真机增量验证**:按 §2.4 的 ②~⑤ 逐级跑,尤其 ③ 干跑确认 β→yaw_rate 符号/量级。 -- [ ] **状态机参数标定**:`task_runner_real.py` 里 `build_sm_config()` 的 - `switch_radius_m`(GOTO→TRACK 切换半径)、`center_threshold_deg`(居中阈值)、 - `center_hold_s`、`takeoff_altitude_m` 全是占位值,须台架标定后回填。 +- [ ] **真机增量验证**:按 §2.5 的 ④⑤ 继续(①②③③b 已于 2026-10-19 通过)。 +- [ ] **吊舱真实目标识别验证**:视场内放目标(人/无人机)+ 0x07,确认 bit2 0→1 与 + TRACK 真实闭环;这是目前唯一没走通的一环(其余均为强制锁定模拟)。 +- [ ] **状态机参数标定**:`task_runner_real.py` 的 `build_sm_config()` 中 + `arrival_radius_m`(到参考点转搜索的半径)、`search_timeout_s`、`track_timeout_s`、 + `sweep_rate_dps` / `sweep_reverse_s`(扫视)、`takeoff_altitude_m` 全是占位值。 +- [ ] **返航参数标定**:`build_return_config()` 的 `speed_mps`、`arrival_radius_m`、 + `yaw_align_tolerance_deg`、`alt_kp`、`timeout_s` 均为占位值。 - [ ] **L1/L2 增益标定**:`pod_bridge.json` 里 `heading.gain`、`guidance.speed_kp/yaw_kp` 等仍为占位,`beta_sign`/`alpha_sign` 已按 S1 标定(+1/-1),其余待标定。 +- [ ] **LOS 判据阈值标定**:`los_window_ms=3000`、`los_min_yaw_change_deg=20`、 + `los_max_residual_deg=8` 需按真机噪声与目标机动特性复核(过紧误判丢目标、过松漏判幽灵)。 ### P1(落地后尽快) -- [ ] **摄像头可视化接真视频流**:当前地面站是「脱靶量模拟视野」,真机需接吊舱视频流 - (吊舱视频输出 → 采集 → 页面叠加追踪框)。 -- [ ] **任务级状态机配置 JSON 化**:`TaskStateConfig` 目前是 dataclass 默认值 + 脚本内构造, - 应外置到配置(铁律「配置全外置」)。 +- [x] **摄像头可视化接真视频流**(2026-10-14 完成):服务端 RTSP→MJPEG 接入页面「吊舱视频」面板 + (`--rtsp-url`;断流自动重连,拔插网线自愈已实测);追踪框叠加未做(吊舱 OSD 已烧录脱靶量)。 +- [ ] **接入距离源(激光雷达,优先)**:给 `PodBridge` 加距离注入点 → 填 `distance_valid` → + 打开 `d_exp_m` 距离 PID,并把 TRACK 的「到点」判据从纯超时改为距离判据。 + 代价低(串口读数),直接改善"贴近";**双目暂不接**(Pi 4 上 YOLO 开销大)。 +- [ ] **任务级配置 JSON 化**:`TaskStateConfig` 与 `ReturnConfig` 目前是 dataclass 默认值 + + 脚本内构造,应外置到配置(铁律「配置全外置」)。 - [ ] **地面站与 runner 状态实时打通**:当前地面站读日志文件(秒级延迟),可改为 runner 直接 UDP 上报遥测到地面站(低延迟)。 ### P2(工程收尾) -- [ ] **仓库补 `.gitignore`**:`logs/`、`__pycache__/`、`.codegraph/`、`models/*.pt`、 - `stereo_calib_params.npz` 等长期未跟踪项,提交时显式列路径、勿 `git add .`。 -- [ ] **conda `pytorch` 回填 pytest**:README 声明有 pytest 但实测缺失,测试目前走 - 隔离 venv `drone-test`;回填后使 README 与实际一致。 +- [x] **`.gitignore` 已补齐**(10-20):`logs/`、`__pycache__/`、`.pytest_cache/`、`.codegraph/`、`.workbuddy/`、`legacy/models/*.pt` 等。 +- [x] **conda `pytorch` 回填 pytest**(pytest 9.1.1 可用;2026-10-20 运行链路 446 通过, + 运行与测试统一走 conda 环境)。 - [ ] **C++ 侧构建验证**:`reference-c++/target_tracker` 与 `visual_track_control` 无 `build/`,若以 C++ 为验收基准需 `cmake .. && make && ctest` 跑 11 个测试。 +- [ ] **codegraph 重新索引**:当前索引早于 10-15/10-19 重构(行号偏移),需 `codegraph sync`。 --- @@ -137,27 +220,61 @@ python task_runner_real.py --conn serial:///dev/ttyS1:57600 # 覆盖飞控连 | 段 | 关键字段 | 说明 | |----|---------|------| -| `pod_serial` | `port` | 吊舱串口设备号(真机填 `/dev/ttyAMA0`) | -| `px4` | `connection_string` | 飞控连接串(SITL `udp://:14540`,真机串口) | -| `link` | `require_laser_valid` | 无激光吊舱必须 `false` | +| `pod_serial` | `port` | 吊舱串口设备号(真机 `/dev/ttyAMA0`,USB 转串口常见 `COM5`/`/dev/ttyUSB0`) | +| `px4` | `connection_string` | 飞控连接串(SITL `udpin://0.0.0.0:14540`,真机串口) | +| `link` | `require_laser_valid` | 无激光吊舱必须 `false`(本机恒 `false`) | | `heading_servo` | `enabled` / `beta_sign` / `alpha_sign` | L1 开关 + 符号(±1,已标定 +1/-1) | -| `ground_inject` | `enabled` / `port` / `timeout_ms` | 地面坐标注入开关 / UDP 端口 / 超龄门限 | +| `heading_servo` | `los_consistency_enabled` / `los_window_ms` / `los_min_yaw_change_deg` / `los_max_residual_deg` | **目标有效性第二判据**(LOS 运动学一致性,2026-10-19 取代 `require_vision`) | +| `ground_inject` | `enabled` / `port` / `timeout_ms` | 地面坐标注入开关 / UDP 端口 / 超龄门限;**真机入口会用它统一 `guidance.target_stale_ms`** | + +> 任务级与返航参数不在 JSON 内,见入口脚本的 `build_sm_config()` / `build_return_config()` +> (JSON 化仍在 P1 待办)。 + +### 4.1 链路负荷与遥测速率(2026-10-15) + +遥测速率**不是越高越好**:PX4 的消息速率由最后一次 `SET_MESSAGE_INTERVAL` 决定, +多个订阅方用不同值会互相覆盖;速率越高 MAVSDK 回调队列压力越大。 + +| 流 | 速率 | 消费者/依据 | +|----|------|------------| +| `attitude_euler` | 20 Hz | 控制律(`attitude_stale_ms=300`) | +| `position` | 10 Hz | 起飞到位 / 到参考点距离 | +| `position_velocity_ned` | 10 Hz | 仅虚拟吊舱几何(SITL) | +| `gps_info` / `velocity_ned` / `in_air` | 2 Hz | 状态量,变化极慢 | + +速率集中定义在 `utils/sitl_support.py` 的 `RATE_*` 常量(唯一来源,各订阅方共用)。 + +**任务日志落盘策略**:事件(阶段切换/命令/告警)**即时 flush**(页面实时性不受影响); +遥测 CSV 按 `TaskLogConfig.telemetry_flush_ms`(默认 1000ms)节流落盘 —— 主循环 +20Hz 若每行同步 flush,等于每秒 20 次磁盘 IO,在树莓派 SD 卡上会阻塞事件循环。 +`close()` 做最后 flush,正常退出不丢数据。 --- ## 五、一键命令汇总 ```bash -# 测试 -cd utils && "C:/Users/yimiao/.workbuddy/binaries/python/envs/drone-test/Scripts/python.exe" -m pytest -q +PY="D:/program file/conda/envs/pytorch/python.exe" + +# 运行链路测试(446 条) +cd "D:/ProgramData/code_drone-test/utils" && "$PY" -m pytest -q + +# 吊舱串口自检 / S2 干跑 +"$PY" utils/pod_bridge/listen_pod.py COM5 --seconds 5 +"$PY" utils/pod_bridge/run_bridge.py --port COM5 --dry-run --duration 10 # SITL 独立全流程 -"D:/program file/conda/envs/pytorch/python.exe" task_runner_sitl.py --conn udp://:14540 +"$PY" task_runner_sitl.py --conn udpin://0.0.0.0:14540 # SITL 地面站联动 -"D:/program file/conda/envs/pytorch/python.exe" task_runner_sitl.py --conn udp://:14540 --ground-station -"D:/program file/conda/envs/pytorch/python.exe" ground_station/app.py --udp-port 14555 --log-dir logs +"$PY" task_runner_sitl.py --conn udpin://0.0.0.0:14540 --ground-station +"$PY" ground_station/app.py --udp-port 14555 --log-dir logs --rtsp-url rtsp://192.168.144.119:8554/video -# 真机全流程(填好配置后) -"D:/program file/conda/envs/pytorch/python.exe" task_runner_real.py --target-type 1 +# 真机全流程(真吊舱串口 + UDP 注入 + PX4) +"$PY" task_runner_real.py --port COM5 --conn udpin://0.0.0.0:14540 --target-type 1 +"$PY" task_runner_real.py --port /dev/ttyAMA0 --target-type 1 # 树莓派 +"$PY" task_runner_real.py --port COM5 --land # 收尾就地降落 ``` + +> 真机入口的 `rtl`/超时/异常收尾都走**自定义返航**(回起飞点→恢复初始航向→垂直降落); +> 仅"未起飞/无起飞点/转场超时/主循环异常"退回 PX4 原生 RTL。