From a5cee5e14266992c98d4323eae06360df56a57b1 Mon Sep 17 00:00:00 2001 From: Orange <2314753575@qq.com> Date: Thu, 23 Jul 2026 14:51:51 +0800 Subject: [PATCH] =?UTF-8?q?vlm=E5=92=8Ctts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- master_launch_使用指南.md | 227 + must_know.md | 3 + src/my_robot_bringup/launch/master_launch.py | 24 +- src/origincar_description/CMakeLists.txt | 4 - src/origincar_msg/CMakeLists.txt | 1 + src/origincar_msg/srv/Speak.srv | 4 + src/planner/config/odom_hybrid_astar.yaml | 3 +- src/planner/config/odom_pure_pursuit.yaml | 24 +- src/planner/config/real_hybrid_astar.yaml | 1 + src/planner/config/real_pure_pursuit.yaml | 20 +- .../launch/odom_hybrid_astar.launch.py | 86 +- .../launch/real_hybrid_astar.launch.py | 100 + .../ackermann_cmd_bridge.cpython-313.pyc | Bin 0 -> 8688 bytes .../evaluate_navigation_test.cpython-313.pyc | Bin 0 -> 8157 bytes .../navigation_test_recorder.cpython-313.pyc | Bin 0 -> 31161 bytes .../__pycache__/odom_map_tf.cpython-313.pyc | Bin 0 -> 13033 bytes ...topology_pure_pursuit_node.cpython-313.pyc | Bin 0 -> 50793 bytes .../scripts/goal_reach_debug_collector.py | 36 +- src/planner/scripts/preflight_check.py | 2 +- src/planner/scripts/send_goal.py | 4 +- .../scripts/topology_pure_pursuit_node.py | 338 +- src/vlm_detect/config/vlm_detect.yaml | 18 - src/vlm_detect/launch/vlm_detect.launch.py | 80 +- src/vlm_detect/setup.py | 2 + .../test_publisher.cpython-310.pyc | Bin 2810 -> 2834 bytes .../__pycache__/tts_node.cpython-310.pyc | Bin 2551 -> 2421 bytes .../__pycache__/vlm_node.cpython-310.pyc | Bin 4263 -> 4282 bytes src/vlm_detect/vlm_detect/qr_tts_bridge.py | 66 + src/vlm_detect/vlm_detect/test_publisher.py | 2 +- src/vlm_detect/vlm_detect/tts_node.py | 70 +- src/vlm_detect/vlm_detect/tts_server.py | 90 + src/vlm_detect/vlm_detect/vlm_node.py | 46 +- .../goal_reach_debug_20260722_202420.json | 11585 ++++++++++++++++ tf_bag/metadata.yaml | 26 + tf_bag/tf_bag_0.db3 | Bin 0 -> 311296 bytes 35 files changed, 12694 insertions(+), 168 deletions(-) create mode 100644 master_launch_使用指南.md create mode 100644 src/origincar_msg/srv/Speak.srv create mode 100644 src/planner/scripts/__pycache__/ackermann_cmd_bridge.cpython-313.pyc create mode 100644 src/planner/scripts/__pycache__/evaluate_navigation_test.cpython-313.pyc create mode 100644 src/planner/scripts/__pycache__/navigation_test_recorder.cpython-313.pyc create mode 100644 src/planner/scripts/__pycache__/odom_map_tf.cpython-313.pyc create mode 100644 src/planner/scripts/__pycache__/topology_pure_pursuit_node.cpython-313.pyc create mode 100644 src/vlm_detect/vlm_detect/qr_tts_bridge.py create mode 100644 src/vlm_detect/vlm_detect/tts_server.py create mode 100644 test_results/goal_reach_debug_20260722_202420.json create mode 100644 tf_bag/metadata.yaml create mode 100644 tf_bag/tf_bag_0.db3 diff --git a/master_launch_使用指南.md b/master_launch_使用指南.md new file mode 100644 index 0000000..dc8ccf7 --- /dev/null +++ b/master_launch_使用指南.md @@ -0,0 +1,227 @@ +# master_launch.py 使用指南 + +## 启动顺序 + +``` +t=0s 底盘驱动 + EKF + TF + IMU (origincar_bringup) +t=2s 激光雷达 (lslidar_driver) +t=5s SLAM 建图 (slam_toolbox) +t=8s Nav2 导航栈 +t=10s VLM 图生文 + TTS 语音播报 +``` + +每个阶段依赖前一个阶段就绪,属于**严格时间线启动**,不可调换。 + +--- + +## 参数一览 + +| 参数 | 默认值 | 说明 | +|---|---|---| +| `use_base` | true | 底盘驱动 + EKF + TF | +| `use_lidar` | true | 激光雷达 (lsn10) | +| `use_slam` | true | slam_toolbox 建图 | +| `use_nav2` | true | Nav2 导航栈 | +| `use_vlm` | true | VLM 图生文 + TTS | +| `use_tts` | true | TTS 语音播报开关 | +| `akmcar` | true | 阿克曼底盘 (false=差速) | +| `carto_slam` | false | 使用 Cartographer 替代 EKF | +| `vlm_host` | http://192.168.10.189:8000 | VLM 推理服务地址 | +| `use_sim_time` | false | 使用仿真时间 | + +--- + +## 常用命令 + +### 全部启动 +```bash +ros2 launch my_robot_bringup master_launch.py +``` + +### 只调试底盘 +```bash +ros2 launch my_robot_bringup master_launch.py use_lidar:=false use_slam:=false use_nav2:=false use_vlm:=false +``` + +### 只建图(底盘 + 雷达 + SLAM) +```bash +ros2 launch my_robot_bringup master_launch.py use_nav2:=false use_vlm:=false +``` + +### 只启动 VLM(调试图生文) +```bash +ros2 launch my_robot_bringup master_launch.py use_base:=false use_lidar:=false use_slam:=false use_nav2:=false +``` + +### 底盘 + 雷达 + SLAM + 导航(不加 VLM) +```bash +ros2 launch my_robot_bringup master_launch.py use_vlm:=false use_tts:=false +``` + +### 更换 VLM 服务地址 +```bash +ros2 launch my_robot_bringup master_launch.py vlm_host:=http://192.168.10.200:8000 +``` + +### 差分底盘模式 +```bash +ros2 launch my_robot_bringup master_launch.py akmcar:=false +``` + +### 使用 Cartographer 替代 EKF +```bash +ros2 launch my_robot_bringup master_launch.py carto_slam:=true +``` + +### 使用自定义 SLAM 参数 +```bash +ros2 launch my_robot_bringup master_launch.py slam_params_file:=/path/to/my_slam.yaml +``` + +### 使用自定义 Nav2 参数 +```bash +ros2 launch my_robot_bringup master_launch.py nav2_params_file:=/path/to/my_nav2.yaml +``` + +--- + +## 前置条件 + +1. **VLM 服务** — WSL 中先启动 `python vlm_server.py`(如需使用 VLM) +2. **Portproxy 转发** — Windows 上管理员 PowerShell 执行 `setup_vlm_forward.ps1`(仅需一次,IP 不变则不用重跑) +3. **重刷环境** — 启动前清理残留进程: + ```bash + pkill -f ros2; pkill -f espeak; pkill -f ffplay + ``` + +--- + +## 网络架构(VLM 相关) + +``` +机器人 (192.168.10.x) + ↓ http://192.168.10.189:8000 +Windows 宿主机 (IP 需固定) + ↓ netsh portproxy → 127.0.0.1:8000 +WSL2 vlm_server.py +``` + +- Windows IP 不变 → 一切正常 +- Windows IP 变化 → 重跑 `setup_vlm_forward.ps1`,启动时传 `vlm_host:=http://新IP:8000` + + +## 改动的地方 + master_launch.py 变更内容 + + 阶段: 1 (t=0s) + 原来: 底盘 + EKF + TF + IMU + 现在: 不变 + ──────────────────────────────────────── + 阶段: 2 (t=2s) + 原来: 激光雷达 + 现在: 不变 + ──────────────────────────────────────── + 阶段: 3 (t=4s) + 原来: — + 现在: 障碍物检测 + (obstacle_scanner.launch.py) + ──────────────────────────────────────── + 阶段: 4 (t=5s) + 原来: SLAM Toolbox + 现在: Map Server + AMCL 定位 (预建地图替代 + SLAM) + ──────────────────────────────────────── + 阶段: 5 (t=7s) + 原来: Nav2 导航栈 + 现在: Hybrid A* 路径规划 + Pure Pursuit + 轨迹跟随 + ──────────────────────────────────────── + 阶段: 6 (t=10s) + +● Bash(rm -f /c/Users/DHQ/master_launch_new.py) + ⎿  Done + +● 修改完成。以下是改动总结: + + master_launch.py 变更内容 + + ┌───────────┬───────────────────────┬────────────────────────────────────────────┐ + │ 阶段 │ 原来 │ 现在 │ + ├───────────┼───────────────────────┼────────────────────────────────────────────┤ + │ 1 (t=0s) │ 底盘 + EKF + TF + IMU │ 不变 │ + ├───────────┼───────────────────────┼────────────────────────────────────────────┤ + │ 2 (t=2s) │ 激光雷达 │ 不变 │ + ├───────────┼───────────────────────┼────────────────────────────────────────────┤ + │ 3 (t=4s) │ — │ 障碍物检测 (obstacle_scanner.launch.py) │ + ├───────────┼───────────────────────┼────────────────────────────────────────────┤ + │ 4 (t=5s) │ SLAM Toolbox │ Map Server + AMCL 定位 (预建地图替代 SLAM) │ + ├───────────┼───────────────────────┼────────────────────────────────────────────┤ + │ 5 (t=7s) │ Nav2 导航栈 │ Hybrid A* 路径规划 + Pure Pursuit 轨迹跟随 │ + ├───────────┼───────────────────────┼────────────────────────────────────────────┤ + │ 6 (t=10s) │ VLM + TTS │ 不变 │ + └───────────┴───────────────────────┴────────────────────────────────────────────┘ + + 具体改动点 + + 1. 移除:cyy_slamtoolbox、gc_navigation2_slamtoolbox、slam_toolbox 节点、nav2_bringup launch + 2. 新增:obstacle_scanner 节点(发布 /obstacles) + 3. 新增:map_server + amcl + lifecycle_manager 用于定位 + 4. 新增:grid_astar_theta_node(路径规划,发布 /plan) + 5. 新增:topology_pure_pursuit_node.py(轨迹跟随,发布 /cmd_vel) + 6. 话题对齐:planner 的 obstacles_topic 参数覆盖为 /obstacles(与 obstacle_scanner 发布的 topic 一致) + 7. 原文件已备份为 master_launch.py.bak + + 常用命令 + source /opt/ros/humble/setup.bash + source ~/yiliao_ws/install/setup.bash + 全部启动 + + ros2 launch my_robot_bringup master_launch.py + + 只调试底盘 + + ros2 launch my_robot_bringup master_launch.py use_lidar:=false use_obstacle:=false use_planner:=false use_vlm:=false + + 底盘 + 雷达 + 定位 + 规划跟踪(不开障碍物检测和 VLM) + + ros2 launch my_robot_bringup master_launch.py use_obstacle:=false use_vlm:=false + + 只启动 VLM(调试图生文) + + ros2 launch my_robot_bringup master_launch.py use_base:=false use_lidar:=false use_obstacle:=false use_planner:=false + + 底盘 + 雷达 + 障碍物检测 + 规划跟踪(不加 VLM) + + ros2 launch my_robot_bringup master_launch.py use_vlm:=false use_tts:=false + + 只做障碍物检测(不开规划不开 VLM) + + ros2 launch my_robot_bringup master_launch.py use_planner:=false use_vlm:=false + + 只做路径规划+跟踪(不开障碍物检测,靠预建地图) + + ros2 launch my_robot_bringup master_launch.py use_obstacle:=false use_vlm:=false + + 更换 VLM 服务地址 + + ros2 launch my_robot_bringup master_launch.py vlm_host:=http://192.168.10.200:8000 + + 差分底盘模式 + + ros2 launch my_robot_bringup master_launch.py akmcar:=false + + 使用 Cartographer 替代 EKF + + ros2 launch my_robot_bringup master_launch.py carto_slam:=true + + 更换地图文件 + + ros2 launch my_robot_bringup master_launch.py map_file:=/path/to/my_map.yaml + + 更换规划器/跟踪器/AMCL 参数文件 + + ros2 launch my_robot_bringup master_launch.py planner_params_file:=/path/to/planner.yaml + ros2 launch my_robot_bringup master_launch.py tracker_params_file:=/path/to/tracker.yaml + ros2 launch my_robot_bringup master_launch.py amcl_params_file:=/path/to/amcl.yaml + + diff --git a/must_know.md b/must_know.md index 8d88d6f..044959a 100644 --- a/must_know.md +++ b/must_know.md @@ -236,4 +236,7 @@ cd /home/sunrise/yiliao_ws && colcon build --packages-select gc_navigation2_slam # 查看完整提交历史 cd /home/sunrise/yiliao_ws && git log --oneline 436e8a2..HEAD + +# 启动图生文 + ``` diff --git a/src/my_robot_bringup/launch/master_launch.py b/src/my_robot_bringup/launch/master_launch.py index ee5e81c..722bdf6 100755 --- a/src/my_robot_bringup/launch/master_launch.py +++ b/src/my_robot_bringup/launch/master_launch.py @@ -130,21 +130,21 @@ def generate_launch_description(): # ================================================================ # 阶段 4: USB 摄像头 + 二维码识别 (t=4s) - # usb_cam: 驱动 USB 摄像头,发布 /image_raw + # car_usb_cam: 使用 hobot_usb_cam 驱动 USB 摄像头,发布 /image_raw # qr_detect: 订阅 /image_raw,识别二维码,发布 qr_results # ================================================================ - usb_camera = Node( - package='usb_cam', - executable='usb_cam_node_exe', - name='usb_cam', - output='screen', + usb_camera = IncludeLaunchDescription( + PythonLaunchDescriptionSource([ + FindPackageShare('car_usb_cam'), '/launch', '/hobot_usb_cam.launch.py' + ]), condition=IfCondition(use_qr), - parameters=[{ - 'video_device': camera_device, - 'image_size': [640, 480], - 'pixel_format': 'YUYV', - 'framerate': 30.0, - }], + launch_arguments={ + 'usb_video_device': camera_device, + 'usb_image_width': '640', + 'usb_image_height': '480', + 'usb_framerate': '30', + 'usb_pixel_format': 'yuyv2rgb', + }.items(), ) qr_detect = Node( diff --git a/src/origincar_description/CMakeLists.txt b/src/origincar_description/CMakeLists.txt index 5bc73fc..21934a5 100644 --- a/src/origincar_description/CMakeLists.txt +++ b/src/origincar_description/CMakeLists.txt @@ -22,11 +22,7 @@ find_package(ament_cmake REQUIRED) # find_package( REQUIRED) install( -<<<<<<< HEAD DIRECTORY launch urdf rviz meshes world -======= - DIRECTORY launch urdf rviz meshes world ->>>>>>> mo_new DESTINATION share/${PROJECT_NAME} ) diff --git a/src/origincar_msg/CMakeLists.txt b/src/origincar_msg/CMakeLists.txt index 8697fbf..d64086a 100644 --- a/src/origincar_msg/CMakeLists.txt +++ b/src/origincar_msg/CMakeLists.txt @@ -25,6 +25,7 @@ find_package(std_msgs REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "msg/Data.msg" "msg/Sign.msg" + "srv/Speak.srv" DEPENDENCIES std_msgs ADD_LINTER_TESTS ) diff --git a/src/origincar_msg/srv/Speak.srv b/src/origincar_msg/srv/Speak.srv new file mode 100644 index 0000000..57fc17e --- /dev/null +++ b/src/origincar_msg/srv/Speak.srv @@ -0,0 +1,4 @@ +string text +--- +bool success +string message diff --git a/src/planner/config/odom_hybrid_astar.yaml b/src/planner/config/odom_hybrid_astar.yaml index d3c7ce3..0f0537c 100644 --- a/src/planner/config/odom_hybrid_astar.yaml +++ b/src/planner/config/odom_hybrid_astar.yaml @@ -46,9 +46,10 @@ grid_astar_theta_planner: yaw_bins: 36 primitive_length: 0.12 collision_sample_step: 0.03 - goal_xy_tolerance: 0.06 + goal_xy_tolerance: 0.10 goal_yaw_tolerance: 0.174533 require_goal_yaw: false + snap_final_xy_to_goal: true use_grid_heuristic: true heuristic_weight: 1.35 goal_yaw_heuristic_weight: 0.0 diff --git a/src/planner/config/odom_pure_pursuit.yaml b/src/planner/config/odom_pure_pursuit.yaml index d411b26..ebd9e07 100644 --- a/src/planner/config/odom_pure_pursuit.yaml +++ b/src/planner/config/odom_pure_pursuit.yaml @@ -14,18 +14,28 @@ topology_pure_pursuit: cancel_topic: /navigation_cancel control_rate: 20.0 - lookahead_distance: 0.40 + lookahead_distance: 0.28 + min_lookahead_distance: 0.10 + lookahead_curvature_gain: 0.80 + lookahead_error_gain: 2.00 + heading_correction_gain: 0.80 + cross_track_correction_gain: 1.20 + cross_track_speed_gain: 1.80 + min_tracking_speed_ratio: 0.35 + path_reacquire_distance: 0.45 goal_tolerance: 0.06 goal_yaw_tolerance: 0.174533 align_goal_yaw: false final_alignment_offset: 0.25 final_alignment_speed: 0.06 - linear_speed: 0.60 - min_linear_speed: 0.08 - reverse_speed: 0.25 - slowdown_distance: 0.35 - curvature_slowdown_gain: 0.10 - max_angular_speed: 2.80 + linear_speed: 0.45 + min_linear_speed: 0.06 + reverse_speed: 0.18 + slowdown_distance: 0.45 + curvature_slowdown_gain: 0.06 + min_curvature_speed_ratio: 0.70 + min_goal_slowdown_ratio: 0.35 + max_angular_speed: 3.20 min_turning_radius: 0.25 latency_compensation: true diff --git a/src/planner/config/real_hybrid_astar.yaml b/src/planner/config/real_hybrid_astar.yaml index 4ded55e..3d64e9f 100644 --- a/src/planner/config/real_hybrid_astar.yaml +++ b/src/planner/config/real_hybrid_astar.yaml @@ -47,6 +47,7 @@ grid_astar_theta_planner: # Point navigation does not require the car to finish at a specific yaw. # Set true when final docking orientation is required. require_goal_yaw: false + snap_final_xy_to_goal: true use_grid_heuristic: true heuristic_weight: 1.25 goal_yaw_heuristic_weight: 0.0 diff --git a/src/planner/config/real_pure_pursuit.yaml b/src/planner/config/real_pure_pursuit.yaml index a479cf2..63ac86a 100644 --- a/src/planner/config/real_pure_pursuit.yaml +++ b/src/planner/config/real_pure_pursuit.yaml @@ -14,16 +14,26 @@ topology_pure_pursuit: cancel_topic: /navigation_cancel control_rate: 20.0 - lookahead_distance: 0.45 + lookahead_distance: 0.30 + min_lookahead_distance: 0.12 + lookahead_curvature_gain: 0.70 + lookahead_error_gain: 1.60 + heading_correction_gain: 0.70 + cross_track_correction_gain: 1.00 + cross_track_speed_gain: 1.50 + min_tracking_speed_ratio: 0.35 + path_reacquire_distance: 0.45 goal_tolerance: 0.15 goal_yaw_tolerance: 0.174533 align_goal_yaw: false - linear_speed: 0.20 + linear_speed: 0.18 min_linear_speed: 0.06 - reverse_speed: 0.10 + reverse_speed: 0.08 slowdown_distance: 0.70 - curvature_slowdown_gain: 0.25 - max_angular_speed: 1.20 + curvature_slowdown_gain: 0.12 + min_curvature_speed_ratio: 0.65 + min_goal_slowdown_ratio: 0.35 + max_angular_speed: 1.50 min_turning_radius: 0.40 latency_compensation: true diff --git a/src/planner/launch/odom_hybrid_astar.launch.py b/src/planner/launch/odom_hybrid_astar.launch.py index 84313e0..408ce16 100644 --- a/src/planner/launch/odom_hybrid_astar.launch.py +++ b/src/planner/launch/odom_hybrid_astar.launch.py @@ -41,11 +41,21 @@ def generate_launch_description(): planner_min_turning_radius = LaunchConfiguration("planner_min_turning_radius") tracker_min_turning_radius = LaunchConfiguration("tracker_min_turning_radius") lookahead_distance = LaunchConfiguration("lookahead_distance") + min_lookahead_distance = LaunchConfiguration("min_lookahead_distance") + lookahead_curvature_gain = LaunchConfiguration("lookahead_curvature_gain") + lookahead_error_gain = LaunchConfiguration("lookahead_error_gain") + heading_correction_gain = LaunchConfiguration("heading_correction_gain") + cross_track_correction_gain = LaunchConfiguration("cross_track_correction_gain") + cross_track_speed_gain = LaunchConfiguration("cross_track_speed_gain") + min_tracking_speed_ratio = LaunchConfiguration("min_tracking_speed_ratio") + path_reacquire_distance = LaunchConfiguration("path_reacquire_distance") linear_speed = LaunchConfiguration("linear_speed") min_linear_speed = LaunchConfiguration("min_linear_speed") reverse_speed = LaunchConfiguration("reverse_speed") slowdown_distance = LaunchConfiguration("slowdown_distance") curvature_slowdown_gain = LaunchConfiguration("curvature_slowdown_gain") + min_curvature_speed_ratio = LaunchConfiguration("min_curvature_speed_ratio") + min_goal_slowdown_ratio = LaunchConfiguration("min_goal_slowdown_ratio") max_angular_speed = LaunchConfiguration("max_angular_speed") goal_tolerance = LaunchConfiguration("goal_tolerance") goal_yaw_tolerance = LaunchConfiguration("goal_yaw_tolerance") @@ -54,6 +64,7 @@ def generate_launch_description(): final_alignment_speed = LaunchConfiguration("final_alignment_speed") require_goal_yaw = LaunchConfiguration("require_goal_yaw") planner_goal_xy_tolerance = LaunchConfiguration("planner_goal_xy_tolerance") + snap_final_xy_to_goal = LaunchConfiguration("snap_final_xy_to_goal") use_grid_heuristic = LaunchConfiguration("use_grid_heuristic") heuristic_weight = LaunchConfiguration("heuristic_weight") goal_yaw_heuristic_weight = LaunchConfiguration("goal_yaw_heuristic_weight") @@ -83,6 +94,30 @@ def generate_launch_description(): tracker_min_turning_radius, value_type=float ) lookahead_distance_value = ParameterValue(lookahead_distance, value_type=float) + min_lookahead_distance_value = ParameterValue( + min_lookahead_distance, value_type=float + ) + lookahead_curvature_gain_value = ParameterValue( + lookahead_curvature_gain, value_type=float + ) + lookahead_error_gain_value = ParameterValue( + lookahead_error_gain, value_type=float + ) + heading_correction_gain_value = ParameterValue( + heading_correction_gain, value_type=float + ) + cross_track_correction_gain_value = ParameterValue( + cross_track_correction_gain, value_type=float + ) + cross_track_speed_gain_value = ParameterValue( + cross_track_speed_gain, value_type=float + ) + min_tracking_speed_ratio_value = ParameterValue( + min_tracking_speed_ratio, value_type=float + ) + path_reacquire_distance_value = ParameterValue( + path_reacquire_distance, value_type=float + ) linear_speed_value = ParameterValue(linear_speed, value_type=float) min_linear_speed_value = ParameterValue(min_linear_speed, value_type=float) reverse_speed_value = ParameterValue(reverse_speed, value_type=float) @@ -90,6 +125,12 @@ def generate_launch_description(): curvature_slowdown_gain_value = ParameterValue( curvature_slowdown_gain, value_type=float ) + min_curvature_speed_ratio_value = ParameterValue( + min_curvature_speed_ratio, value_type=float + ) + min_goal_slowdown_ratio_value = ParameterValue( + min_goal_slowdown_ratio, value_type=float + ) max_angular_speed_value = ParameterValue(max_angular_speed, value_type=float) goal_tolerance_value = ParameterValue(goal_tolerance, value_type=float) goal_yaw_tolerance_value = ParameterValue(goal_yaw_tolerance, value_type=float) @@ -104,6 +145,9 @@ def generate_launch_description(): planner_goal_xy_tolerance_value = ParameterValue( planner_goal_xy_tolerance, value_type=float ) + snap_final_xy_to_goal_value = ParameterValue( + snap_final_xy_to_goal, value_type=bool + ) use_grid_heuristic_value = ParameterValue(use_grid_heuristic, value_type=bool) heuristic_weight_value = ParameterValue(heuristic_weight, value_type=float) goal_yaw_heuristic_weight_value = ParameterValue( @@ -225,6 +269,7 @@ def generate_launch_description(): "min_turning_radius": planner_min_turning_radius_value, "require_goal_yaw": require_goal_yaw_value, "goal_xy_tolerance": planner_goal_xy_tolerance_value, + "snap_final_xy_to_goal": snap_final_xy_to_goal_value, "use_grid_heuristic": use_grid_heuristic_value, "heuristic_weight": heuristic_weight_value, "goal_yaw_heuristic_weight": goal_yaw_heuristic_weight_value, @@ -251,11 +296,21 @@ def generate_launch_description(): "cmd_vel_topic": "/planner_cmd_vel", "min_turning_radius": tracker_min_turning_radius_value, "lookahead_distance": lookahead_distance_value, + "min_lookahead_distance": min_lookahead_distance_value, + "lookahead_curvature_gain": lookahead_curvature_gain_value, + "lookahead_error_gain": lookahead_error_gain_value, + "heading_correction_gain": heading_correction_gain_value, + "cross_track_correction_gain": cross_track_correction_gain_value, + "cross_track_speed_gain": cross_track_speed_gain_value, + "min_tracking_speed_ratio": min_tracking_speed_ratio_value, + "path_reacquire_distance": path_reacquire_distance_value, "linear_speed": linear_speed_value, "min_linear_speed": min_linear_speed_value, "reverse_speed": reverse_speed_value, "slowdown_distance": slowdown_distance_value, "curvature_slowdown_gain": curvature_slowdown_gain_value, + "min_curvature_speed_ratio": min_curvature_speed_ratio_value, + "min_goal_slowdown_ratio": min_goal_slowdown_ratio_value, "max_angular_speed": max_angular_speed_value, "goal_tolerance": goal_tolerance_value, "goal_yaw_tolerance": goal_yaw_tolerance_value, @@ -308,15 +363,25 @@ def generate_launch_description(): ), DeclareLaunchArgument( "lookahead_distance", - default_value="0.40", + default_value="0.28", description="Pure Pursuit lookahead in meters.", ), - DeclareLaunchArgument("linear_speed", default_value="0.60"), - DeclareLaunchArgument("min_linear_speed", default_value="0.08"), - DeclareLaunchArgument("reverse_speed", default_value="0.25"), - DeclareLaunchArgument("slowdown_distance", default_value="0.35"), - DeclareLaunchArgument("curvature_slowdown_gain", default_value="0.10"), - DeclareLaunchArgument("max_angular_speed", default_value="2.80"), + DeclareLaunchArgument("min_lookahead_distance", default_value="0.10"), + DeclareLaunchArgument("lookahead_curvature_gain", default_value="0.80"), + DeclareLaunchArgument("lookahead_error_gain", default_value="2.00"), + DeclareLaunchArgument("heading_correction_gain", default_value="0.80"), + DeclareLaunchArgument("cross_track_correction_gain", default_value="1.20"), + DeclareLaunchArgument("cross_track_speed_gain", default_value="1.80"), + DeclareLaunchArgument("min_tracking_speed_ratio", default_value="0.35"), + DeclareLaunchArgument("path_reacquire_distance", default_value="0.45"), + DeclareLaunchArgument("linear_speed", default_value="0.45"), + DeclareLaunchArgument("min_linear_speed", default_value="0.06"), + DeclareLaunchArgument("reverse_speed", default_value="0.18"), + DeclareLaunchArgument("slowdown_distance", default_value="0.45"), + DeclareLaunchArgument("curvature_slowdown_gain", default_value="0.06"), + DeclareLaunchArgument("min_curvature_speed_ratio", default_value="0.70"), + DeclareLaunchArgument("min_goal_slowdown_ratio", default_value="0.35"), + DeclareLaunchArgument("max_angular_speed", default_value="3.20"), DeclareLaunchArgument("goal_tolerance", default_value="0.06"), DeclareLaunchArgument("goal_yaw_tolerance", default_value="0.174533"), DeclareLaunchArgument("align_goal_yaw", default_value="false"), @@ -329,9 +394,14 @@ def generate_launch_description(): ), DeclareLaunchArgument( "planner_goal_xy_tolerance", - default_value="0.06", + default_value="0.10", description="Hybrid A* final XY tolerance in meters.", ), + DeclareLaunchArgument( + "snap_final_xy_to_goal", + default_value="true", + description="Publish the final path XY exactly at the requested goal XY without forcing yaw.", + ), DeclareLaunchArgument( "use_grid_heuristic", default_value="true", diff --git a/src/planner/launch/real_hybrid_astar.launch.py b/src/planner/launch/real_hybrid_astar.launch.py index 89f8dc0..b69ee38 100644 --- a/src/planner/launch/real_hybrid_astar.launch.py +++ b/src/planner/launch/real_hybrid_astar.launch.py @@ -32,9 +32,27 @@ def generate_launch_description(): enable_motion = LaunchConfiguration("enable_motion") wheelbase = LaunchConfiguration("wheelbase") max_steering_angle = LaunchConfiguration("max_steering_angle") + lookahead_distance = LaunchConfiguration("lookahead_distance") + min_lookahead_distance = LaunchConfiguration("min_lookahead_distance") + lookahead_curvature_gain = LaunchConfiguration("lookahead_curvature_gain") + lookahead_error_gain = LaunchConfiguration("lookahead_error_gain") + heading_correction_gain = LaunchConfiguration("heading_correction_gain") + cross_track_correction_gain = LaunchConfiguration("cross_track_correction_gain") + cross_track_speed_gain = LaunchConfiguration("cross_track_speed_gain") + min_tracking_speed_ratio = LaunchConfiguration("min_tracking_speed_ratio") + path_reacquire_distance = LaunchConfiguration("path_reacquire_distance") + linear_speed = LaunchConfiguration("linear_speed") + min_linear_speed = LaunchConfiguration("min_linear_speed") + reverse_speed = LaunchConfiguration("reverse_speed") + slowdown_distance = LaunchConfiguration("slowdown_distance") + curvature_slowdown_gain = LaunchConfiguration("curvature_slowdown_gain") + min_curvature_speed_ratio = LaunchConfiguration("min_curvature_speed_ratio") + min_goal_slowdown_ratio = LaunchConfiguration("min_goal_slowdown_ratio") + max_angular_speed = LaunchConfiguration("max_angular_speed") use_grid_heuristic = LaunchConfiguration("use_grid_heuristic") heuristic_weight = LaunchConfiguration("heuristic_weight") goal_yaw_heuristic_weight = LaunchConfiguration("goal_yaw_heuristic_weight") + snap_final_xy_to_goal = LaunchConfiguration("snap_final_xy_to_goal") state_log_period = LaunchConfiguration("state_log_period") search_progress_log_period_ms = LaunchConfiguration("search_progress_log_period_ms") use_sim_time = LaunchConfiguration("use_sim_time") @@ -42,11 +60,53 @@ def generate_launch_description(): enable_motion_value = ParameterValue(enable_motion, value_type=bool) wheelbase_value = ParameterValue(wheelbase, value_type=float) max_steering_angle_value = ParameterValue(max_steering_angle, value_type=float) + lookahead_distance_value = ParameterValue(lookahead_distance, value_type=float) + min_lookahead_distance_value = ParameterValue( + min_lookahead_distance, value_type=float + ) + lookahead_curvature_gain_value = ParameterValue( + lookahead_curvature_gain, value_type=float + ) + lookahead_error_gain_value = ParameterValue( + lookahead_error_gain, value_type=float + ) + heading_correction_gain_value = ParameterValue( + heading_correction_gain, value_type=float + ) + cross_track_correction_gain_value = ParameterValue( + cross_track_correction_gain, value_type=float + ) + cross_track_speed_gain_value = ParameterValue( + cross_track_speed_gain, value_type=float + ) + min_tracking_speed_ratio_value = ParameterValue( + min_tracking_speed_ratio, value_type=float + ) + path_reacquire_distance_value = ParameterValue( + path_reacquire_distance, value_type=float + ) + linear_speed_value = ParameterValue(linear_speed, value_type=float) + min_linear_speed_value = ParameterValue(min_linear_speed, value_type=float) + reverse_speed_value = ParameterValue(reverse_speed, value_type=float) + slowdown_distance_value = ParameterValue(slowdown_distance, value_type=float) + curvature_slowdown_gain_value = ParameterValue( + curvature_slowdown_gain, value_type=float + ) + min_curvature_speed_ratio_value = ParameterValue( + min_curvature_speed_ratio, value_type=float + ) + min_goal_slowdown_ratio_value = ParameterValue( + min_goal_slowdown_ratio, value_type=float + ) + max_angular_speed_value = ParameterValue(max_angular_speed, value_type=float) use_grid_heuristic_value = ParameterValue(use_grid_heuristic, value_type=bool) heuristic_weight_value = ParameterValue(heuristic_weight, value_type=float) goal_yaw_heuristic_weight_value = ParameterValue( goal_yaw_heuristic_weight, value_type=float ) + snap_final_xy_to_goal_value = ParameterValue( + snap_final_xy_to_goal, value_type=bool + ) state_log_period_value = ParameterValue(state_log_period, value_type=float) search_progress_log_period_ms_value = ParameterValue( search_progress_log_period_ms, value_type=float @@ -168,6 +228,7 @@ def generate_launch_description(): "use_grid_heuristic": use_grid_heuristic_value, "heuristic_weight": heuristic_weight_value, "goal_yaw_heuristic_weight": goal_yaw_heuristic_weight_value, + "snap_final_xy_to_goal": snap_final_xy_to_goal_value, "state_log_period": state_log_period_value, "search_progress_log_period_ms": search_progress_log_period_ms_value, }], @@ -182,6 +243,23 @@ def generate_launch_description(): "use_sim_time": use_sim_time_value, "publish_cmd_vel": enable_motion_value, "cmd_vel_topic": "/planner_cmd_vel", + "lookahead_distance": lookahead_distance_value, + "min_lookahead_distance": min_lookahead_distance_value, + "lookahead_curvature_gain": lookahead_curvature_gain_value, + "lookahead_error_gain": lookahead_error_gain_value, + "heading_correction_gain": heading_correction_gain_value, + "cross_track_correction_gain": cross_track_correction_gain_value, + "cross_track_speed_gain": cross_track_speed_gain_value, + "min_tracking_speed_ratio": min_tracking_speed_ratio_value, + "path_reacquire_distance": path_reacquire_distance_value, + "linear_speed": linear_speed_value, + "min_linear_speed": min_linear_speed_value, + "reverse_speed": reverse_speed_value, + "slowdown_distance": slowdown_distance_value, + "curvature_slowdown_gain": curvature_slowdown_gain_value, + "min_curvature_speed_ratio": min_curvature_speed_ratio_value, + "min_goal_slowdown_ratio": min_goal_slowdown_ratio_value, + "max_angular_speed": max_angular_speed_value, "state_log_period": state_log_period_value, }], ) @@ -211,6 +289,23 @@ def generate_launch_description(): ), DeclareLaunchArgument("wheelbase", default_value="0.143"), DeclareLaunchArgument("max_steering_angle", default_value="0.60"), + DeclareLaunchArgument("lookahead_distance", default_value="0.30"), + DeclareLaunchArgument("min_lookahead_distance", default_value="0.12"), + DeclareLaunchArgument("lookahead_curvature_gain", default_value="0.70"), + DeclareLaunchArgument("lookahead_error_gain", default_value="1.60"), + DeclareLaunchArgument("heading_correction_gain", default_value="0.70"), + DeclareLaunchArgument("cross_track_correction_gain", default_value="1.00"), + DeclareLaunchArgument("cross_track_speed_gain", default_value="1.50"), + DeclareLaunchArgument("min_tracking_speed_ratio", default_value="0.35"), + DeclareLaunchArgument("path_reacquire_distance", default_value="0.45"), + DeclareLaunchArgument("linear_speed", default_value="0.18"), + DeclareLaunchArgument("min_linear_speed", default_value="0.06"), + DeclareLaunchArgument("reverse_speed", default_value="0.08"), + DeclareLaunchArgument("slowdown_distance", default_value="0.70"), + DeclareLaunchArgument("curvature_slowdown_gain", default_value="0.12"), + DeclareLaunchArgument("min_curvature_speed_ratio", default_value="0.65"), + DeclareLaunchArgument("min_goal_slowdown_ratio", default_value="0.35"), + DeclareLaunchArgument("max_angular_speed", default_value="1.50"), DeclareLaunchArgument( "use_grid_heuristic", default_value="true", @@ -226,6 +321,11 @@ def generate_launch_description(): default_value="0.0", description="Weight for final yaw guidance in the Hybrid A* heuristic.", ), + DeclareLaunchArgument( + "snap_final_xy_to_goal", + default_value="true", + description="Publish the final path XY exactly at the requested goal XY without forcing yaw.", + ), DeclareLaunchArgument( "state_log_period", default_value="1.0", diff --git a/src/planner/scripts/__pycache__/ackermann_cmd_bridge.cpython-313.pyc b/src/planner/scripts/__pycache__/ackermann_cmd_bridge.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4c0cd6100a8da81701a06188ce3d0ba6dc320c5 GIT binary patch literal 8688 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 KYeHtlM*S~|5S)Yn literal 0 HcmV?d00001 diff --git a/src/planner/scripts/__pycache__/evaluate_navigation_test.cpython-313.pyc b/src/planner/scripts/__pycache__/evaluate_navigation_test.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d554516a3896ae38dc28f85d9fb2f328590bdd4a GIT binary patch literal 8157 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⋘Qi>`=#P{CO+vx%9158p_(G@^aS$~OKc#QBMUO}3Fl2ORK;PO@V~AXDKJ%2~fbk)tIL{zt&Z*=&fOpQljvs8aH(gw z+%sD4cxAO=%h&!x|BpI8=(rEt?R42UylVf%+gfqAY`VKk?(Vh76L;UwAtr73fsr_kLidC2^CYA zsbPUfc8Fxd;lvT`(^}`>)uL;`G3$yB)T2inn$BDRqznjFgAE2FHf#gU8^E4&z-+)q zg44hih&Xq1O@n*Gct8EypiR}HM{1=_|DkHPYJv<>T-A4jJqzpUN9@slbPz=$SF03* z9qZ)%N6dDF=OWNAn~g!Y!x4J-91g&@3xS@Xr#}T9$lao85xr(PfEG=GV>2?9$tJMZ zqP0wcOlzSadGxWppy4UIye7p z`8DUJ-qmmp3PM<(-01zifBW@~zp2}fhyLIPxO_OCZ&HRf^iK|xQXbDYDs4Dq50ayg z+VpqhdB56CIq|6ZFm*yjT`CFWgXGMT{4DSP$(x`3v))yU>4W5OlBEBl`)cCc=i~Wy zMY0~*2el(fo|z1XXTvy&rN#~njyfbb`9QqECqfMVdV@({VE;9jx31(CgJ*$ALmDre z0IR=|x4toP@iLP?z%W5(Mc}Tb;UiEMa$#J80q37eFqhcb894m>21v+Lm@99+GQqOU z7Z`g2%`y$|ewh;`NJUME$(0N*CI5+2ZPl1eNz_^R zn{g#iXV=~XhHxC~s71-TQ5pEkCfpJ@BD|9Z-s6uVk1r7FMFaK3 zBmVImpMl%wVsCuf#;m@%1@WDKWc}^+i$&l0zi0pP>fc^{ z;=b~w1+^diS66$*+xTf~XJPhf`@pJu+iY{RY&EyNANr%vT6(j0q|`gIku3I(6noz& zH@~@h0n(1{#yexT$JU%>*P*Spp$GnQ+sN&yU#fSjQ=hn+E4_WIvoPx2$+cr^y+z;9 z6L;WgSKnRxR%6Q=wf6Sf#bWE=$Bl<~EU3Hxxea-II_Jmkz?TE4WAHx*Q4{kM$11$9 zv+M6d^r*j$qMydj_u@(GhbOIYFViu|yF*So7HE|1u~;G-kHthDdWf^Q%0NXn z#!@zIxM5+wtIKT}ornX--Xa>tSdF2l!SGR5CauS^vp+@#-BBis>kGi_PXg5)JrC( rsry-H&@{IF4HKf=tNF5}qe9t#f9g9^x97h5)<={dJ`PfyWbFS1))Vlo literal 0 HcmV?d00001 diff --git a/src/planner/scripts/__pycache__/navigation_test_recorder.cpython-313.pyc b/src/planner/scripts/__pycache__/navigation_test_recorder.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a885a271f9d90ac9636011cc17e2baee9e2c8c9a GIT binary patch literal 31161 zcmdUYX?PsRbzl$Xz+k5500zh49$;_{PLco*kRT|M01to!Ni-nAhOF@b7=o~X8L(#{ z9=2r0kz+u%Lqf7b(AI{atuF%nX+y~I0Vm#A^d%*m?4mW0d)$WCO6)I=a(xh~Z0uyS z+4riax(CAsK_@@*Z4s=ldR6thy6V-dSMOET+_Tv%47d;M^?$^CjA4F;5$cSIz&($S zVP0f7hG#e(m)5Q0b>x}Gr;(?g*TYlaZRj%cMvt7w#G6Q3I-gFSX5I`>Lw81(g}0DA z#%^mD%d=fJ-bUi4ZhM!5cXT;u=MV%E*I}2VRLtOR}P;;!WrGUU3q+7mz#HY z<@5OjX6Y{ID&z}E*xFsxRm>N6mGC7b&UTk}d3aA(8DG{_&X)tsR>1IH&JKPB=K#Nw zbAn&xX37~Zvw-2U9?#jSm%pE{=3J0g^SF=8-aEiB0W-rqUQ^EOOf-*&8BH5a^W|KA zq&GMm=0`k%(1<58H8Bz3$1Vpwd@wNHz=wyzQP1RfAQTGnBtw)B3_l6s$sm6=%ufVD z!@o5FHDL@D)Abksy?DB5Y`Ed@OqDba;Gh z_>yEf6Xs6y;j?4oK`FC0I6k%^8H@IfO$4D5hC|`-xMbp@{8(rdP?-)*ojn`mrK~KcSr7zUzJ95g9|mAq#nq&BD|^rk66B>-Kd>t}im3P1?} z)TBg|Km!mmq~JSJ96$;AZv)IHzj$%U_@$4>th-kXyZKVcH=IoU zEC5Jr{2>Hhgs-WAC%O^%s!?Vz@oh-!^gBaogII#NL7-;Y^{3HvPKPu%m`+o;v_WN* zXpIbL8J!j$q~eV@X}i&9NI&4k3J|(}7puzSw*ftyB4Iun9Fg=B*q-A-@FoHmNkdD9 z;o#V~&mfte3XD$$Ba-HL=MAJGmH`*~v{fq@8QO0mm`PfHI>p~_ zZm`1pGt?e~{#E;0-=9J8&}S%Pz$fEE{uij1?25Y9?zlNaItB`GZc&j37!;5h`(5qOSVk}`l! zMx*Eak@J#~OleZuFr*A$ir^?gLZD72h}_BWSSad`1V<-;Ec_!d2jPs%AB5bs@IUeb zcvqNshIJJ6?0wO z?wlE&%bIy&j=ynXq5MWju+=YR2yE+>PN1-B`p?UxqN;09P8bIK6g^j#Ltx+NZ#vu&Bj&N;TsOt)cHFDA#ZP|0? zhs3D#*_IeB4~a4Bq3g&#Bu0g9wydN5Au%enwk1aALt@N%=&#OtNQ}xT*s_k<4~Y>M z1H^5KG4~-cDx+#kjP8fTn6K24vJRXbwfJ7qLt<1$`Cu!>y$QyWhs21>CgK3kei~Mv zwBo%7R2U7>G#JYs3geJhmC@K>Ck-X#^-|d2aT-cWt)#Ff#8gFN1aI|*7+r@kTovR* z36?k#L*Y`6Ar=&I)mS}^kvE{kO&p0e(inMPlM<^2EZa83aD77@hiN-#j9_Wr5Np{G zqo2xRRI%&!}-4qGJFsDYOPTpQq{Sv-d?cOhZi!SAQl+vK_omQ@Dn~IU4T$ zJPoPO!Ix;b_jhSXeR7Sh`Vf3w?=2NZLxW$WAxJN$Vf6{nm(>^z_3L*UY7Z63U{ET3D&QuF&f&k1&-02@U048p)i8=t7?pfw#52%niH@p9ErjR)@3zDLtC~0 z>aU0ctO_rqFoN}4YK(@qtZOn>g$vTs1nVEGF&a{_Hf6xr{4j+NewT*0?Sqv0ARTGV znFVxoKS<7`yeMPBDmGKls#6qpEtmQz`}6P>bT2~R~Qr%=JdU(S1`sgV!g zr+1D>854m?P&Ok1Q1wm9@o0E*Z1}RF2?3Tc7%u!W#NI?A!{Ldcu@LC{9HZgDxPKDN zABhZ>CIuo{Q0JY18;Js3vJFp+_@4@nC!pr0L|k%>ph?9Kh{3)xqP&e*Wzgc{hl~RjJFw*u$r3&5kAn3DntAl|H_(0afRq(Rv(H!%KH?l7jGPOP zkGzW`pbu;`>5~B-G+v-O4PFGLmp}aEWn1M3DkmyO{FTQlPgZi0&VSj^I2?KEva#_a z5wO(Aidh~-2OcHSe>P4<&o=BqT%q8Fap(lV6&enYpwY)6S%c$&$q2kV5*(I{*s~Yq z$0aEpV_;a2923Dn1io>^kHrJL>E2uvTy7R9%VW(Vi`xkVs%ck@~YuW zvWzG;vUhQj^R5LOCkMRC#j+Izs^n3}f(J*4CmQznMkQlp3Y5{3+3z0$ zB_V5K^bG|fLCJby3>Bnck{i1ynW2LLIeZu^TP81&7~h5HPoQ@ay&m*Vp?4a+GwAiA z$D!8;o@6{b9u7qLM=`7iohttrhBC17!{g!MCnbF-d_l4x*@IT!ABalUaUfwNJs8xy z33FQ#5a?$f#RZ8?07zde1xS5qY3x>+q9!7v`~;NZLs&F(G8jTq^pk!k&EVJox_C+M zdOyA+G&}}ogq*~vjsS~E0=p7mTAU!0S;}yDDioD$gr34r0@J{#gGA2;#>Nu>!V4fg zK0X!!1DYQ=$9P1_CE38b;g3#%VGXLv2S&!mB$mKsMRr8WLRA%UMqWe7|fXZ=IOJj|owL9#*|T8UVKB|{jRT&7WCb=Cv5N~R0^7!D2#l5Qew zmY?AZJc|TiLrmu0&J)KE9y-y+NEnhW zp=jDn$E@joWKhFHyqVx31d37A8pD7kEj>IWrGtJN?ePF2J%%9Cs1V5TCuWf$9`#a^ z#h;)dK;uL}7AXVfZp!naP?pTFHo#nCA)GgpD;Xgc5txGH`Xe>+@MgKsC8Hd-;J_OX zkB&l`VJvhuj27ZZaQrNf>kMdiCx%|BRLM~&1xJ9Y{0Nmx^1a`)qEf+$HHT)=ACN%<4cxR^FAfM zQrfbzYvVhgS!p`FYVJtNvU6qk$yIaD10ZuxFN|!g*g{`&#aBw3mwH#tdw!l@G}k8< zHwnc}OV(I%n^=5EC_WU+KRj*tjD0O9|0VC-h*+{sDA~4D6f0>LOFD#-j#xowEa!-r zb4JKH6U*VIEpc;}Xm&$`&Sx(fRxq@4E!%zlqO}K>E%wZ3 zi)GD1S@Y7VSXqZy)+Lm6#frPJXu+DdD$Y7ZwnSh{pnGHNPLbUwu>0=m(<&^O!}Gq0 z0SpGwvqSLgSZa@X4vC&)g6CMQ=(zmJ71c=?cE;FUBHJdgZGfSYU?>9&l{H)EDOBIm z7V|tJdX5O5Be4SL321=w%J~toYL8I0XQlOItg1(>>JzH^V&(laN1>LOueC>SSD<8V`p;!Sl z5qyi8z3P~8h-Qyq_S|Jm<(BsuQ-<|@C*vxPXSvqgKGD5HaPL^ls}}Qi3wgWWOEk1c*|`7t00n|I1K5zfp3hWaSKZANIb57O}2PsB2pu`F8l5;neEo z&KX4acEJtR6PvqWZ^N61Qcd>HJ+0%=2QteRr8UBL7{rja>cjnzgZv4!NCC>j&xMaoG(umD`o9+gDi(v zN_*ZmpSpkd3##%^4!(!eD(tOrj>A)SqYRIVUZ0Nt3_KRocwFei(lRm{!BUA$Av~&c zq>|0%HCYtEJP{NQU@t&k!DrcjXFWaz82IlYuH*{b2EgVMtGvt|dJRjVKms)=EP31P zzP}1MVUwm90qh%_Lf!5ID}5BLQ(TfoL8rAL?qM_-po74#B5uR`P$o-p8{TJ6NrY%G z_GXHs+LR=n2YxYe@KsqftUibSYw{*0u!&;(rML%uSz;5;ObV7?v%U!|L85EI#bJuu znTYo)79%wSP1F89+)$%sYvs~#t4}BmN)@QMXsB1Qs89%eAJPqRoF2A;CHj%JIP8l> zD}|!t8UfJvKB3&OcfL>bMtO&fps#y7QsfK`!R)!yKM>PzFJ!FR8Fj;O@9 z31(EQq&TfD2`QLCp$Yi31}m;SQ=Gc70~MW64p*Zojt<7&2Ml5uCb--r4hD$6hyPOu z%VUC-3?r~Tm;W<_km2#?7$Sq=e_)7=0i<(&1wCaT$m8OB0NXHcd<_6n*=7@$vf#Xf z<#_Ob${kr(FJ8SEXS3F<_UY)=kI#HubW{qCO3_g*II3gTnuU&-b=z0D>{wo89qFb=@n^+MZRN^9oOAT3IQl!F2}D037QN6b7-C<)eh6GKQ3pkG9_GpGeop~=r9diy*#|&z3p4{-ELZJidF3eQI866CZ@WU zba1u6R=?_6;BJ3l@dIK*r_j(D^BsYV)vyqH+w6Tn$3F7_j-s`LRZAR!sT!)D>NHC$ z&8y~aNh4FAMwbm81a6~n6D=MBhFaJ$39~2i01XUs8cw{BFYN*ONLuoaS5Ck8bFib= zM=+yOD8(t3C?KqS6`7WKoWY6edbHti(ck{%u7U$Tt7Jg)h-3zT z3^z;5oCkMo@V|{-E_yBKJ&PVr!Te{@LnHXcDVONLE~ZSmOOS1IIX0m-dkZo}Zh;4c z>B_ynXLgV1st{ZiqN_@9RW0amn-|SuO{-ATD%R{1YWA&UwN0nTtq##zD&SxBt1Y+p zetB=4bzM6+(>0eZ<~IoW4GRGwe;drWn}FdkLA`}6*6yY2>oeEQOjYeGPuzMUR=HEG zY!xb7W0m`+J62g(1lO*zo_Jxorhu)?n(Ysuap)sRmE9(EjAWDI)D5aNiIf0z2kwT| znpZ)<10821ZCsg?Vb0PXGg|M`Q#M84HR6GHJ$!j(qarP@)KzQ5(7GzrN<>Fv57?m4 zDi?l|(5WOFWl&8G1|3-`~H61O?84$KUQHjiNQ%omHcCc)OU)clP-uk8_=j|k02UjMk*{Fu=E7%Ay_i}lPO z6xhlivK|o6S_X9Yb5k6?L_Vv60G#R2?3-jtqmxUT+)^PZcK_7z4S;UmCL8TuARhk= zvPW=|OKOw1&4u^Z7JHr4_Ltzp_UmN&SC)j3C*dgdfdrT(ADbiFiIkj#mX#PS#V{`B z(X>cf6VSbjITj!boHVV`p~bhgmU;aTUs`JktMKqm@Ho|)(-8gg7X9h8lfw*SWjydT zI9sVpXENrYT~eJg<}+vt%uv}htUiZE$PDv<#bhtanJG@4VYG0Pg{H<*lGBrgCTV7GB6%spg;|fGS*v!qgK<>RTZn z4zv`v$$MZ1Pj$b>dumh5!V+Giod9FQ{W*V)_cY#LdXx8Tzdz^0zGoDuSEk^zjL+Ef zE_Ns3<~;5|kykzWJbtodMA;+4KZo!iqlX+fNdhUsOx7?z2D=i-VMP9U%s}LiXEF3i z^!_J$SJ5LYq&f^;2Tx*&-P$i(P5IwJP@W%bt78*Wuwx|2zB(1*ac7*A3FjO)8dmui z5uW2$c1@t28FrL_F*^cBl2Ca_6b^Fr1&D4uUB-U}%WrBROV|ki3d?;GI3%9+vrk8^ zUYfb|>GrvUF>7(Wv!U*5(Kij^u>s-OK&3Fb1< zTqBrk7P3X(VZnF!^$xM4Pw422`TE~BKl-b?WzZ7D$hmZ|=ICz2h9gDDW66y6GMUj} zJOCioVF{uuNKJq^If9`vhiR^1GUhm@Pb zMmC+M)5Mq2Y-xuw8nYayYt?aSt%i`P-=sNPRoqV=q4cCy*Dj?oA1N(t9;D62^lHo} zu!dC*q`)apHE;d>HQA8Y(x0wHcLUGUlcNJ)A|VS%80lJ4*1#8)oG;MOU=w}oz_U1| z(XYTBD`N(c?-Q5K1Ylbk(g3V^JPBaZ17r<(&7;s!DU80e%zk|-b5;kcXgF`3o=_HJ z5O+Ahz9%vr`P@7)y=*q|@P7j@l1%4<0XRHNP7_ZA{bM8YHmEOQnsYoHesXFOcWB5P zYIqckdE^sh&l8Vy%fEzKGn72An+3Cjei~NWl9>-DW;U|W_F@5)ZpaaEpEYTMAsRs> zJ4uh*n08?JU^~=#UH zZqGvY?b5|k!QBe;X#Spc#+YxPKE9sC*m6Zyp1JNzZ$AtQ0 zG57Il+geuc^_u5v=Jv<3eADS0WACuGtA}R}U#p%448`;5x2%9Dt_56U3+GM>MNKhw z+j_deX<26s8J1tZ?_iwe_n0*J1hgJ_-(tP<=LNRrZPxoESKhi268<@Y1oqvW14kN| zA2gI6b?D!)>%iYIYoX?d%vvwOsvlO3OfPg+s-t`jXQ&|YArzO)tZgqxUN=hG;wOpaKM>TotBm_WKxf1!W$KiqLv+0^#A=v%CYEb z1y*ZH-8bOTn0u1&D8~+y@F?|#T4=1;2D~(HD4k>AUI0yV-w+5VgUaemYmOqvPjJ(2 zic4rZ$qfS9xDz@~eX=H#)I;IhD$3q6HB3Y5Oaz=geF&Huya%~niqkBcPFo&l1+DcR zrRCo06m$@!X;4y`0o7b&MO&-JX(+Lxg-!oj$0d4|xs2wA@5$f{4%7ytuP45N_;AJ^ zASU8#9%#^-Yqdtj128D`qt-`@-V+#hsJUpUw}pn(XYVdGOhdmW2HXai;s}Oxyt#xk zp3?WHlek%ln=wAPj~0UVZ=+%LN$CMn+f^tH0rU|XR-a7{S_-KMWQ#*;8VxDB6@`KU zmsUPg18_3XTMfu}b3m>RibH+&9#z9MM5z(2i=d%T@8O{#lGe(X^^LpE@{c2o2hUf* z{~Lx;YJn}Cc#jkR=LkqK7Lo<n#9G4l#~H_&?)J+i+3 zEew%G_9BM9jNUYQGw6xv#nBr>4~LJupr61H(K&trL%#oby>^x#q_RSL7Q0 zR~Q{ZFNk@qNj2u5L`Zt#_&@|HoQg2iw*QBiV^iC{yb-nlroPA|G%_eJ9WL3*8FRoE zHxONB+U8os!a4!}nqjpa&vwsbOzWnH;{_#RL9KwB@3sjA+r)xhLcuPvV82kXUo1Ev z6dahwop^Flv2eRkxLqvVEfnq+3l9i|2gJgILg7K!n7NiPqSe40{;GE&9II^y(^(B! zcUQn3LSH?Z=Y0a}d$oC`=|HUR5lHuu^hz*O!2r@P4OiUhU@gbPqv z&v&h|O>t|kU@eyg4uFYlt-#hU>|14bgFtt^eYQR3@`|o%!Brh|`4&2EpISU6xb}%z z$Aqk7Z?*_or@$@QQtJJgZp9s+>8r@Fcvkp*@~eBxF@U^n=U#mHIZkdBq?)J#ev4 zj4i|MsJjI3u4RL`r(4+5y`GtY`vWsFtn1mRAz`0m=X9XE_$&yXj;n1zJLQ74XkHhy zmONlmY=qf~H|#;l(C?J;Ww4DU5Z z)<5Fc{M=nQ*CFr!UKkM@+l9vV<@3w@O5>qb_hB%ax-9n?J+4yp85WtY0kz^3y2iOU znWi~iLDHt40k}0~OKQ{iY#_9Bz(@$~A_S7iY@0%6N-ea=n^^HF;gKiGdx~X0L5#^V zZGr$nLPDF2On|%vzSftf&YRFpRsiuVdN%MjZaK39MB23(!4Yk;te@5(wWEmb0DM#u zrPWbN%e>n4T-UYpbD6Wzxxl>c#>nhP=MT;YZgnhdUpTYaa;sZpx2>?-BzK)mZJ*e) zH$iP_aE5GZf_DOT(1{qZ4oSDs@qY_1l)Mj7<4&v|3fWhfpF6VNQ)p-Y{5(Hjv+Bm{ z`W{RyW++UUpnQ`frwi#+R&hUxpnZ=p6z`zIzO+_>(9?QOPgo3G;Dk@o)B41^C9y{% zxu;<*3-UR+T@5gw1X)*Qp@T@$!~s@g0xNkE4A2PZD3g>A=7x-_E^k1&{wSoQ_B~*I zCi*rG?z!XA`*nDr7g(&hM?Ox84NG|4FFyPCUz`7@A1t*K)+-xPav;%XB3-CbqAc!J zVmTB6Aw3r=C)x0@6{u~Vf+MGVP^DAJOm8-mGL&TXfKOt}Np{|x0T|PJTk>iNHNoI@ZArgINhG zhP4c`YT53GRp;}eaa9VwpLkKz!Gpw*P<>{|#A~MpIhjdHY!vjRPQp{B zc7=A}8<2q!YX2ViL>~x%R0me2v>^~YU>8Gj*+e!io(r_H7uLVfQXrJ#h$3#2@)}gB zwHJf(RIo>ZB}jH!RwU&2N?t=!UMDT3J~!kwa%o_p(YctTx?_*Qh=vxKya@MN@|s_* zRqHGjGYx53o5=XL41D50gcmIYDURT^4s@YiWnhq6S3+#Rv6kt#5NNjowI#KTI|UI$ zfrorYvwkrCQyq?SrN!-A;(hDih1bz<=hARD(8wO3L`NbF(%8UXYT`pdImPY$7L*3- z-N0SV5=AN5A&MpN(r(bv5!+J8bX0c|bm&0I8rscG@M_cqd!gUn0B^tn7;^SI6W;?m z<$fnD$UFc}aisP@lTd@+lp1|7!GBHiqKxob!e&uy@MV(2P5gg@uw;Sr-Qn>mvbPhz zKr+F8iqWwUF>lGjgBQTE)oBewSbM$@v*P)ExF*4PVQd7BG=nK@Z1h}I-b@C=1>f_> zm>0zi`PhX;#*L;X{$DZa{~{WTa>_lTBIgG&UE$kO+63ImIsvz~j>2>F5`PJjBWS!( zST_yGEc_o3bNyz5iPnCNQDoy`@Id=x&$>E3GYWW- za5a=uv*5bzUUZ{y&(@)K1%>}-YJHLZ`S;=Ba%)5Je#~bcD z?l(&Bl*aa*jyntH0<(^57Rl+3XXjl%HhXNYm)PMNZ#4?V+m?0+#XE)UUDKU$NA|Vz zpT0ELiRb#-gvz!b7PTcxzhHSUpK%wh7ckC(6?5Y5V`z89Atwd8X6|7p;v7_yNE(3@ zxcVQC7Lk8xjmC=Por4O`Ns@3hxD)VrHK!siX=%|x7E!bB>F+Rwe6GGM@0wRAEY1rrS0fKzOrE zL}N{gyOfC85!a+E4X6J!_jTh^@b@SFG&ix#%yj8S&l2|!2Yl&7u|c-yMnPRYHXH$- zg5OIR4_rQQo{y4Cx8)U!jz264ivBIqpDGpN5wyPnfL5c}dG^8s?Nrw~-t16_REfm)gDGs0- z@(vxrP!277+T}E6@}zQ>P0MLW7D?r-{RYbM0?-qK%3jZYWf%P^;DS&ywqL&?XBqu8 zRpL$Lv~60Ka#}`xCe_8hX}JnDKMf_7b8K3!isn|IN##)1(a52Er2{BbGY6N6l2hVq zv$XRlSU^Fd?kd*KOY4-t=z?6dxLR@O1ArsrO>qFNPXR#`ppBYP&{3v3$P2P;sspHo z^%bBR_E&&{WSr_A)?UhGH)eF|Kv$t@H|;%`idVIsd+<9C-Jjmuc|UCM2!-_?qapPP zGqsV+;o$o3Ry_w7eB(9LfV*4GK|_>B_MW5=^~n`z*C+L=I(1*N8uAtP$SSp?fKOua z@-7sW@rE+~3jiWJj^&Hn;8cooF%P-ZNxrO!-037=#Fl)u6S=%8aSEB-cUF)%d>IY< zF9-QBUjGJ{;SIyRS0?<$4!Ah%Ly!f`J$`sx zF1i4l=7Pyrs3oo`Be$qkYg(><2MUtC-0~%9O3x;p7TO0wguIRX8fLzZULATq^ybjJ zf!^oQdkMV`cAMxrF1U`r*&(=2Pn+Yljbd%* zTeY3B+(Gc?bFNPbwVl(}mE6I2Q?uA~?5(C_vBD?7pAX*b6q=5KfqbR#iS@KXmnF{T z&1K&#xlyvNhuAx8!Q8QRBZTm%UhcK%^^eVdY&{+0X2zC3S8=oHM$>u*#w}=m1M~MT zp<>s%6(Mj{3)v37SE$;%ZbOKjfFS@xFBzCS^0W>K`6F^D5eqS8Mjeap?6N zgy%B0rXO^FRln4|y6usb{tv9@A=J&-O6Lu)SZ`VJ_Lf6J{h^rW@OnNb6)-lq1?iQ2 zxArYW#ik=d(~+3>=z1Y06)`q2$=&R}(Y;_0YxW2=dqB^$vIPKk!@qo(B3P|xUEv;EFT)(=fCJ063gxZqUW8OI z-fB4x7XXRgeS&wN8mOujs}2fP2jN};vHGx3eK=lIFV=JlHJv06n$48}O3%^jPtQIb zb5-8m38>-zoIPkjx6Rl@bD>}^Ts0Syt@-jv0QfnmiZw8xqiO5HLjUr1f!+URI+)Ni zD&K>xkVpe~K4Hd@bQ{e6VTPn#y}z_baLJi3m!5chcbp z;|zB)iC=B%b{Xz$M}NaI3n*L`<8k;CF&>_&PD_nMrG^pU7BCT^l#=4;@sF)H1j^h%DydzzH%C$pn`t!Y!IS>ZM^7i4ow9$S0DPPCVK#q@qnmI4QJ*BO4_X zvK{8?(=$(t4ln$X{U3N`79M*|-OgZSoE1c??As|{K|r`A=%*6*KoTBF_}mqQEuwb@ zJ;FD8Ftm)`G+w;g( zL;P(-^#|yE4ZW|Tw}jr;(fbB^UqSER(ECI5^w_;D${l0Fa5Lz+@CdQo@_&bke~;cj zphspkBovt*a^Ma>QUosBgrDLYnu6Olp`NzH4S#qErQZ+4bO*{3VtPc@wjD-X=5Qd!O;%(5-2#)b{jM|JXt|qZHhS-78;wC`#9FKNWZe{D2UaD!5}?>T8?muU1|^`@hBBdmlQ1DzMWwp` z5Y}8+Pbmns*5ztVM}sRr$D)M%LGd;T zKcJV{9lSIY4)7z#LvVR2KQ$SZnM7LnNmvQNRiJ$Ml0O7L{V8RrKh9~6oSTY{gfEca z1;xaqIR}4l5-bZLU^icf{}EI%!geD=#;3YI(e>b=sjp5`!S!bPB0Ezeo5 zwZP?lAj-kCZke%shK1puk-6r`Mg!>8%QKhZ$cE@FgCA8}b$ZuL8`IXDIoIva+uwHL zk$GpqFW=8$*y4MP&S?9QnOz5_Wc!!*?Tj-YKpB}oVx0iW$o!Z0E%$-eT@UsCU#&UZ z8*u&ey5Ye!`gux$e~nxPlE0OEsDim;u07Pk+-Yt)Y+$}qVL7y2|DDFP!`aMtw&^gu zU60`w%VC}VyNnKfgXM6h{<}^c`q`PCUj28gYCAFw-*f69{Jl&|M@jnk3ezxLtiy1L z9>e8Loi4-oGZFrMm!;E_{{3PSF4bY!gL!c7!hQqBUki7}`bWaUFgM|%j6VY%D64AW zZt&>2@v$LtzcjxG;UB{sK?q4%>P6^ykzOO-x@>?{DRVR^|EjECfunfDBg>W8P~a&Q zjv^3`EQ5ay6YYd%8tym`)O?l;L+x2Av5eP~D!pW}~j)uG6BcdB5OeMqT+_-}?+c?>=MA(q*pK YG6r_~a!jB9BZK*qM_^1C3P|<-AL^zFz5oCK literal 0 HcmV?d00001 diff --git a/src/planner/scripts/__pycache__/odom_map_tf.cpython-313.pyc b/src/planner/scripts/__pycache__/odom_map_tf.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9dbd5564f978f3ab89bb4a14f1788dd7aacfa52a GIT binary patch literal 13033 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 u@*LazIPh)>+i{zF%;5L&DUXk>+iGMuVR literal 0 HcmV?d00001 diff --git a/src/planner/scripts/__pycache__/topology_pure_pursuit_node.cpython-313.pyc b/src/planner/scripts/__pycache__/topology_pure_pursuit_node.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56b37c159638e787690b30b525b9f8a275450d44 GIT binary patch literal 50793 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 0: + receive_age = max(0.0, (now_ns - self.latest_odom_received_ns) * 1e-9) if msg.header.stamp.sec == 0 and msg.header.stamp.nanosec == 0: - age = 0.0 + stamp_age = None else: - age = (self.get_clock().now() - stamp).nanoseconds * 1e-9 - if age < 0.0: - age = 0.0 - if age > self.max_odom_age: + stamp = Time.from_msg(msg.header.stamp) + stamp_age = max(0.0, (self.get_clock().now() - stamp).nanoseconds * 1e-9) + if receive_age is not None and receive_age > self.max_odom_age: return { "available": False, "reason": "odom_too_old", - "age_sec": age, + "odom_receive_age_sec": receive_age, + "odom_stamp_age_sec": stamp_age, } source_frame = msg.header.frame_id or self.map_frame @@ -381,7 +386,8 @@ class GoalReachDebugCollector(Node): "x": x, "y": y, "yaw": yaw, - "odom_age_sec": age, + "odom_receive_age_sec": receive_age, + "odom_stamp_age_sec": stamp_age, "odom_linear_x": msg.twist.twist.linear.x, "odom_angular_z": msg.twist.twist.angular.z, } @@ -448,6 +454,7 @@ class GoalReachDebugCollector(Node): "goal_xy_tolerance", "goal_yaw_tolerance", "require_goal_yaw", + "snap_final_xy_to_goal", "resolution", "yaw_bins", "primitive_length", @@ -467,11 +474,21 @@ class GoalReachDebugCollector(Node): "publish_cmd_vel", "cmd_vel_topic", "lookahead_distance", + "min_lookahead_distance", + "lookahead_curvature_gain", + "lookahead_error_gain", + "heading_correction_gain", + "cross_track_correction_gain", + "cross_track_speed_gain", + "min_tracking_speed_ratio", + "path_reacquire_distance", "linear_speed", "min_linear_speed", "reverse_speed", "slowdown_distance", "curvature_slowdown_gain", + "min_curvature_speed_ratio", + "min_goal_slowdown_ratio", "min_turning_radius", "max_angular_speed", ], @@ -628,8 +645,9 @@ class GoalReachDebugCollector(Node): planner_xy = planner.get("goal_xy_tolerance") tracker_xy = tracker.get("goal_tolerance") + snap_xy = bool(planner.get("snap_final_xy_to_goal")) if isinstance(planner_xy, (int, float)) and isinstance(tracker_xy, (int, float)): - if planner_xy > tracker_xy: + if planner_xy > tracker_xy and not snap_xy: hints.append( "planner 的 goal_xy_tolerance 大于 tracker 的 goal_tolerance;" "planner 可能发布一个离原始目标较远的路径终点。" diff --git a/src/planner/scripts/preflight_check.py b/src/planner/scripts/preflight_check.py index 345d2d9..8859fa8 100755 --- a/src/planner/scripts/preflight_check.py +++ b/src/planner/scripts/preflight_check.py @@ -32,7 +32,7 @@ class PreflightCheck(Node): self.declare_parameter("map_topic", "/map") self.declare_parameter("odom_topic", "/odom_combined") self.declare_parameter("scan_topic", "/scan") - self.declare_parameter("cmd_vel_topic", "/cmd_vel") + self.declare_parameter("cmd_vel_topic", "/planner_cmd_vel") self.declare_parameter("expected_scan_frame", "laser_link") self.declare_parameter("tf_wait_sec", 3.0) self.declare_parameter("drive_command_topic", "/ackermann_cmd") diff --git a/src/planner/scripts/send_goal.py b/src/planner/scripts/send_goal.py index 3859919..1e9d586 100755 --- a/src/planner/scripts/send_goal.py +++ b/src/planner/scripts/send_goal.py @@ -39,8 +39,8 @@ def main(): parser.add_argument("yaw", type=float, nargs="?", default=0.0) parser.add_argument("--topic", default="/goal_pose") parser.add_argument("--frame", default="map") - parser.add_argument("--timeout", type=float, default=5.0) - parser.add_argument("--repeat", type=int, default=1) + parser.add_argument("--timeout", type=float, default=8.0) + parser.add_argument("--repeat", type=int, default=3) args = parser.parse_args() rclpy.init() diff --git a/src/planner/scripts/topology_pure_pursuit_node.py b/src/planner/scripts/topology_pure_pursuit_node.py index 54086d6..045e173 100755 --- a/src/planner/scripts/topology_pure_pursuit_node.py +++ b/src/planner/scripts/topology_pure_pursuit_node.py @@ -62,12 +62,22 @@ class TopologyPurePursuit(Node): self.declare_parameter("path_resolution", 0.10) self.declare_parameter("nearest_node_max_distance", 2.0) self.declare_parameter("lookahead_distance", 0.55) + self.declare_parameter("min_lookahead_distance", 0.16) + self.declare_parameter("lookahead_curvature_gain", 0.50) + self.declare_parameter("lookahead_error_gain", 1.20) + self.declare_parameter("heading_correction_gain", 0.55) + self.declare_parameter("cross_track_correction_gain", 0.80) + self.declare_parameter("cross_track_speed_gain", 1.20) + self.declare_parameter("min_tracking_speed_ratio", 0.40) + self.declare_parameter("path_reacquire_distance", 0.50) self.declare_parameter("goal_tolerance", 0.25) self.declare_parameter("linear_speed", 0.30) self.declare_parameter("min_linear_speed", 0.08) self.declare_parameter("reverse_speed", 0.12) self.declare_parameter("slowdown_distance", 0.80) self.declare_parameter("curvature_slowdown_gain", 0.18) + self.declare_parameter("min_curvature_speed_ratio", 0.35) + self.declare_parameter("min_goal_slowdown_ratio", 0.25) self.declare_parameter("max_angular_speed", 1.5) self.declare_parameter("min_turning_radius", 0.35) self.declare_parameter("goal_yaw_tolerance", 0.10) @@ -99,6 +109,35 @@ class TopologyPurePursuit(Node): self.lookahead_distance = float( self.get_parameter("lookahead_distance").value ) + self.min_lookahead_distance = max( + 0.05, + min( + self.lookahead_distance, + float(self.get_parameter("min_lookahead_distance").value), + ), + ) + self.lookahead_curvature_gain = max( + 0.0, float(self.get_parameter("lookahead_curvature_gain").value) + ) + self.lookahead_error_gain = max( + 0.0, float(self.get_parameter("lookahead_error_gain").value) + ) + self.heading_correction_gain = max( + 0.0, float(self.get_parameter("heading_correction_gain").value) + ) + self.cross_track_correction_gain = max( + 0.0, float(self.get_parameter("cross_track_correction_gain").value) + ) + self.cross_track_speed_gain = max( + 0.0, float(self.get_parameter("cross_track_speed_gain").value) + ) + self.min_tracking_speed_ratio = max( + 0.0, + min(1.0, float(self.get_parameter("min_tracking_speed_ratio").value)), + ) + self.path_reacquire_distance = max( + 0.0, float(self.get_parameter("path_reacquire_distance").value) + ) self.goal_tolerance = float(self.get_parameter("goal_tolerance").value) self.linear_speed = float(self.get_parameter("linear_speed").value) self.min_linear_speed = float(self.get_parameter("min_linear_speed").value) @@ -107,6 +146,14 @@ class TopologyPurePursuit(Node): self.curvature_slowdown_gain = float( self.get_parameter("curvature_slowdown_gain").value ) + self.min_curvature_speed_ratio = max( + 0.0, + min(1.0, float(self.get_parameter("min_curvature_speed_ratio").value)), + ) + self.min_goal_slowdown_ratio = max( + 0.0, + min(1.0, float(self.get_parameter("min_goal_slowdown_ratio").value)), + ) self.max_angular_speed = float( self.get_parameter("max_angular_speed").value ) @@ -151,6 +198,7 @@ class TopologyPurePursuit(Node): self.tf_listener = TransformListener(self.tf_buffer, self) self.latest_odom = None + self.latest_odom_received_ns = 0 self.active_path = [] self.active_node_path = [] self.active_goal_node = "" @@ -162,9 +210,11 @@ class TopologyPurePursuit(Node): self.last_cmd_linear = 0.0 self.last_cmd_angular = 0.0 - self.plan_pub = self.create_publisher( - NavPath, self.get_parameter("plan_topic").value, 10 - ) + self.plan_pub = None + if self.enable_topology_planning: + self.plan_pub = self.create_publisher( + NavPath, self.get_parameter("plan_topic").value, 10 + ) self.cmd_pub = self.create_publisher( Twist, self.get_parameter("cmd_vel_topic").value, 10 ) @@ -272,6 +322,7 @@ class TopologyPurePursuit(Node): def _odom_callback(self, msg): self.latest_odom = msg + self.latest_odom_received_ns = self.get_clock().now().nanoseconds if ( self.enable_topology_planning and self.autoplan_default_goal @@ -462,6 +513,8 @@ class TopologyPurePursuit(Node): return updated def _publish_path(self): + if self.plan_pub is None: + return path_msg = NavPath() path_msg.header.frame_id = self.map_frame path_msg.header.stamp = self.get_clock().now().to_msg() @@ -474,6 +527,12 @@ class TopologyPurePursuit(Node): def _current_pose(self, compensate=True): if self.latest_odom is None: return None + now_ns = self.get_clock().now().nanoseconds + if self.latest_odom_received_ns <= 0: + return None + age = max(0.0, (now_ns - self.latest_odom_received_ns) * 1e-9) + if age > self.max_odom_age: + return None msg = self.latest_odom source_frame = msg.header.frame_id or self.map_frame @@ -483,16 +542,6 @@ class TopologyPurePursuit(Node): vx = msg.twist.twist.linear.x wz = msg.twist.twist.angular.z - stamp = Time.from_msg(msg.header.stamp) - if msg.header.stamp.sec == 0 and msg.header.stamp.nanosec == 0: - age = 0.0 - else: - age = (self.get_clock().now() - stamp).nanoseconds * 1e-9 - if age < 0.0: - age = 0.0 - if age > self.max_odom_age: - return None - if compensate and self.latency_compensation: horizon = min(age, self.max_latency_compensation) if abs(wz) < 1e-4: @@ -587,28 +636,53 @@ class TopologyPurePursuit(Node): ) return - nearest_index = self._nearest_path_index(x, y) + projection = self._nearest_path_projection(x, y) + nearest_index = projection["index"] self.current_path_index = nearest_index - target_index = self._lookahead_index(x, y, nearest_index) - target_x, target_y, target_path_yaw = self.active_path[target_index] - nearest_x, nearest_y, _ = self.active_path[nearest_index] - cross_track_error = math.hypot(nearest_x - x, nearest_y - y) + cross_track_signed = projection["signed_error"] + cross_track_error = abs(cross_track_signed) + path_curvature = self._path_curvature(nearest_index) + lookahead_threshold = self._adaptive_lookahead( + path_curvature, cross_track_error, goal_distance + ) + target = self._path_target_from_projection(projection, lookahead_threshold) + target_index = target["index"] + target_x = target["x"] + target_y = target["y"] + target_path_yaw = target["yaw"] dx = target_x - x dy = target_y - y - path_heading = self._segment_heading(target_index, x, y) + path_heading = target["path_heading"] reverse = math.cos(normalize_angle(target_path_yaw - path_heading)) < 0.0 control_yaw = normalize_angle(yaw + math.pi) if reverse else yaw + # Track the XY curve. The path pose yaw is still used to detect reverse + # segments, but heading correction should follow the segment tangent so + # a noisy or intentionally ignored goal yaw does not pull the vehicle + # away from the planned line. + heading_error = normalize_angle(path_heading - control_yaw) local_x = math.cos(control_yaw) * dx + math.sin(control_yaw) * dy local_y = -math.sin(control_yaw) * dx + math.cos(control_yaw) * dy lookahead = max(0.05, math.hypot(local_x, local_y)) curvature = 2.0 * local_y / (lookahead * lookahead) + curvature += ( + self.heading_correction_gain + * math.sin(heading_error) + / max(self.min_lookahead_distance, lookahead) + ) + curvature += ( + -self.cross_track_correction_gain + * cross_track_signed + / max(self.min_lookahead_distance * self.min_lookahead_distance, lookahead * lookahead) + ) if self.min_turning_radius > 1e-3: max_curvature = 1.0 / self.min_turning_radius curvature = max(-max_curvature, min(max_curvature, curvature)) - speed = self._target_speed(abs(curvature), goal_distance, reverse) + speed = self._target_speed( + abs(curvature), goal_distance, reverse, cross_track_error + ) angular = speed * curvature angular = max(-self.max_angular_speed, min(self.max_angular_speed, angular)) self._publish_cmd(speed, angular) @@ -620,9 +694,13 @@ class TopologyPurePursuit(Node): nearest_index=nearest_index, target_index=target_index, cross_track_error=cross_track_error, + cross_track_signed=cross_track_signed, reverse=reverse, curvature=curvature, lookahead=lookahead, + lookahead_threshold=lookahead_threshold, + path_curvature=path_curvature, + heading_error=heading_error, ) def _align_final_yaw(self, x, y, yaw, goal_x, goal_y, goal_yaw, goal_yaw_error): @@ -670,15 +748,177 @@ class TopologyPurePursuit(Node): best_dist = dist return best_index - def _lookahead_index(self, x, y, start_index): + def _nearest_path_projection(self, x, y): + if len(self.active_path) < 2: + px, py, pyaw = self.active_path[0] + return { + "index": 0, + "t": 0.0, + "x": px, + "y": py, + "yaw": pyaw, + "path_heading": pyaw, + "signed_error": 0.0, + "distance": math.hypot(x - px, y - py), + } + + if self.path_reacquire_distance > 0.0: + nearest_vertex = self._nearest_path_index(x, y) + vx, vy, _ = self.active_path[nearest_vertex] + vertex_error = math.hypot(vx - x, vy - y) + if vertex_error > self.path_reacquire_distance: + start = 0 + else: + start = max(0, self.current_path_index - 8) + else: + start = max(0, self.current_path_index - 8) + + best = None + best_dist_sq = float("inf") + for index in range(start, len(self.active_path) - 1): + x0, y0, yaw0 = self.active_path[index] + x1, y1, yaw1 = self.active_path[index + 1] + dx = x1 - x0 + dy = y1 - y0 + seg_len_sq = dx * dx + dy * dy + if seg_len_sq <= 1e-10: + continue + t = ((x - x0) * dx + (y - y0) * dy) / seg_len_sq + t = max(0.0, min(1.0, t)) + px = x0 + t * dx + py = y0 + t * dy + err_x = x - px + err_y = y - py + dist_sq = err_x * err_x + err_y * err_y + if dist_sq >= best_dist_sq: + continue + seg_len = math.sqrt(seg_len_sq) + ux = dx / seg_len + uy = dy / seg_len + signed_error = ux * err_y - uy * err_x + heading = math.atan2(dy, dx) + yaw = yaw0 if t < 0.5 else yaw1 + best = { + "index": index, + "t": t, + "x": px, + "y": py, + "yaw": yaw, + "path_heading": heading, + "signed_error": signed_error, + "distance": math.sqrt(dist_sq), + } + best_dist_sq = dist_sq + + if best is not None: + return best + + index = min(max(0, self.current_path_index), len(self.active_path) - 1) + px, py, pyaw = self.active_path[index] + return { + "index": index, + "t": 0.0, + "x": px, + "y": py, + "yaw": pyaw, + "path_heading": pyaw, + "signed_error": 0.0, + "distance": math.hypot(x - px, y - py), + } + + def _path_target_from_projection(self, projection, lookahead_distance): + remaining = max(0.05, float(lookahead_distance)) + index = min(projection["index"], len(self.active_path) - 2) + t = max(0.0, min(1.0, projection.get("t", 0.0))) + + while index < len(self.active_path) - 1: + x0, y0, yaw0 = self.active_path[index] + x1, y1, yaw1 = self.active_path[index + 1] + dx = x1 - x0 + dy = y1 - y0 + seg_len = math.hypot(dx, dy) + if seg_len <= 1e-9: + index += 1 + t = 0.0 + continue + available = (1.0 - t) * seg_len + if remaining <= available: + ratio = t + remaining / seg_len + x = x0 + ratio * dx + y = y0 + ratio * dy + heading = math.atan2(dy, dx) + yaw = yaw0 if ratio < 0.5 else yaw1 + return { + "index": index + 1, + "x": x, + "y": y, + "yaw": yaw, + "path_heading": heading, + } + remaining -= available + index += 1 + t = 0.0 + + x, y, yaw = self.active_path[-1] + heading = self._segment_heading(len(self.active_path) - 1) + return { + "index": len(self.active_path) - 1, + "x": x, + "y": y, + "yaw": yaw, + "path_heading": heading, + } + + def _lookahead_index(self, x, y, start_index, lookahead_distance=None): + threshold = ( + self.lookahead_distance + if lookahead_distance is None + else max(0.05, float(lookahead_distance)) + ) target_index = len(self.active_path) - 1 for index in range(start_index, len(self.active_path)): px, py, _ = self.active_path[index] - if math.hypot(px - x, py - y) >= self.lookahead_distance: + if math.hypot(px - x, py - y) >= threshold: target_index = index break return target_index + def _adaptive_lookahead(self, path_curvature, cross_track_error, goal_distance): + scale = ( + 1.0 + + self.lookahead_curvature_gain * abs(path_curvature) + + self.lookahead_error_gain * max(0.0, cross_track_error) + ) + lookahead = self.lookahead_distance / max(1.0, scale) + if goal_distance < self.lookahead_distance: + lookahead = min(lookahead, max(self.min_lookahead_distance, goal_distance)) + return max(self.min_lookahead_distance, min(self.lookahead_distance, lookahead)) + + def _path_curvature(self, index): + if len(self.active_path) < 3: + return 0.0 + i0 = max(0, index - 1) + i1 = max(0, min(index, len(self.active_path) - 1)) + i2 = min(len(self.active_path) - 1, index + 1) + if i0 == i1: + i2 = min(len(self.active_path) - 1, i1 + 2) + if i1 == i2: + i0 = max(0, i1 - 2) + if i0 == i1 or i1 == i2: + return 0.0 + + x0, y0, _ = self.active_path[i0] + x1, y1, _ = self.active_path[i1] + x2, y2, _ = self.active_path[i2] + a = math.hypot(x1 - x0, y1 - y0) + b = math.hypot(x2 - x1, y2 - y1) + c = math.hypot(x2 - x0, y2 - y0) + denominator = a * b * c + if denominator <= 1e-6: + return 0.0 + cross = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0) + return 2.0 * cross / denominator + def _segment_heading(self, index, current_x=None, current_y=None): if len(self.active_path) < 2: return 0.0 @@ -716,9 +956,13 @@ class TopologyPurePursuit(Node): nearest_index=None, target_index=None, cross_track_error=None, + cross_track_signed=None, reverse=False, curvature=None, lookahead=None, + lookahead_threshold=None, + path_curvature=None, + heading_error=None, ): if self.state_log_period <= 0.0: return @@ -731,10 +975,34 @@ class TopologyPurePursuit(Node): parts = [f"tracker_state mode={mode}"] if pose is None: parts.append("pose=unavailable") + if self.latest_odom is None: + parts.append("odom=none") + elif self.latest_odom_received_ns > 0: + receive_age = max( + 0.0, + (now_ns - self.latest_odom_received_ns) * 1e-9, + ) + parts.append(f"odom_receive_age={receive_age:.3f}") else: x, y, yaw, vx, wz = pose parts.append(f"pose=({x:.3f},{y:.3f},{yaw:.3f})") parts.append(f"odom_twist=({vx:.3f},{wz:.3f})") + if self.latest_odom_received_ns > 0: + receive_age = max( + 0.0, + (now_ns - self.latest_odom_received_ns) * 1e-9, + ) + parts.append(f"odom_receive_age={receive_age:.3f}") + if self.latest_odom is not None: + stamp = Time.from_msg(self.latest_odom.header.stamp) + if ( + self.latest_odom.header.stamp.sec != 0 + or self.latest_odom.header.stamp.nanosec != 0 + ): + stamp_age = max( + 0.0, (self.get_clock().now() - stamp).nanoseconds * 1e-9 + ) + parts.append(f"odom_stamp_age={stamp_age:.3f}") parts.append(f"path_points={len(self.active_path)}") parts.append(f"completed={self.completed}") @@ -748,21 +1016,41 @@ class TopologyPurePursuit(Node): parts.append(f"target_index={target_index}") if cross_track_error is not None: parts.append(f"cross_track={cross_track_error:.3f}") + if cross_track_signed is not None: + parts.append(f"cross_track_signed={cross_track_signed:.3f}") if curvature is not None: parts.append(f"curvature={curvature:.3f}") if lookahead is not None: parts.append(f"lookahead={lookahead:.3f}") + if lookahead_threshold is not None: + parts.append(f"lookahead_threshold={lookahead_threshold:.3f}") + if path_curvature is not None: + parts.append(f"path_curvature={path_curvature:.3f}") + if heading_error is not None: + parts.append(f"heading_error={heading_error:.3f}") parts.append(f"reverse={reverse}") parts.append(f"cmd=({self.last_cmd_linear:.3f},{self.last_cmd_angular:.3f})") self.get_logger().info(" ".join(parts)) - def _target_speed(self, abs_curvature, goal_distance, reverse): + def _target_speed(self, abs_curvature, goal_distance, reverse, cross_track_error=0.0): base_speed = self.reverse_speed if reverse else self.linear_speed speed = base_speed - speed *= max(0.35, 1.0 - self.curvature_slowdown_gain * abs_curvature) + speed *= max( + self.min_curvature_speed_ratio, + 1.0 - self.curvature_slowdown_gain * abs_curvature, + ) if goal_distance < self.slowdown_distance: - ratio = max(0.25, goal_distance / max(0.05, self.slowdown_distance)) + ratio = max( + self.min_goal_slowdown_ratio, + goal_distance / max(0.05, self.slowdown_distance), + ) speed *= ratio + if cross_track_error > 0.0 and self.cross_track_speed_gain > 0.0: + tracking_ratio = max( + self.min_tracking_speed_ratio, + 1.0 - self.cross_track_speed_gain * cross_track_error, + ) + speed *= tracking_ratio speed = max(self.min_linear_speed, min(base_speed, speed)) return -speed if reverse else speed diff --git a/src/vlm_detect/config/vlm_detect.yaml b/src/vlm_detect/config/vlm_detect.yaml index 8c6674b..ebce0bf 100644 --- a/src/vlm_detect/config/vlm_detect.yaml +++ b/src/vlm_detect/config/vlm_detect.yaml @@ -1,36 +1,18 @@ # vlm_detect 参数配置 -# 使用: ros2 launch vlm_detect vlm_detect.launch.py vlm_node: ros__parameters: - # VLM 推理服务地址 vlm_host: "http://192.168.10.189:8000" - # 模型名称 (OpenAI 格式) vlm_model: "./OpenGVLab/InternVL3-1B/" - # 订阅的压缩图像话题 image_topic: "/image_mjpeg" - # 订阅的触发信号话题 trigger_topic: "/sign4return" - # 触发信号值 trigger_sign: 9 - # 发布结果的话题 result_topic: "/vlm_result" - # 发送给 VLM 的提示词 prompt_text: "描述图片中有一个病人的特征,字数控制在20字以内。" - # 最大输出 token 数 max_tokens: 100 tts_node: ros__parameters: - # VLM 推理服务地址 (需与 vlm_node 一致) - vlm_host: "http://192.168.10.189:8000" - # 音频输出设备 (PulseAudio sink) audio_sink: "alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo" - # 订阅 VLM 结果的话题 (需与 vlm_node 一致) result_topic: "/vlm_result" - # TTS 语音 (edge-tts 语音名) - tts_voice: "zh-CN-XiaoxiaoNeural" - # 临时 MP3 存储路径 - tmp_mp3_path: "/tmp/tts_out.mp3" - # 播放速度 (ffplay atempo, 范围 0.5~2.0) tts_speed: 1.5 diff --git a/src/vlm_detect/launch/vlm_detect.launch.py b/src/vlm_detect/launch/vlm_detect.launch.py index b45783e..dd416d5 100644 --- a/src/vlm_detect/launch/vlm_detect.launch.py +++ b/src/vlm_detect/launch/vlm_detect.launch.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -vlm_detect 联合启动文件 -同时启动 vlm_node (图生文) 和 tts_node (语音播报) +vlm_detect 启动文件 +同时启动 vlm_node (图生文) + tts_server (语音播报服务) + qr_tts_bridge (二维码播报) 用法: - ros2 launch vlm_detect vlm_detect.launch.py # 默认配置 - ros2 launch vlm_detect vlm_detect.launch.py vlm_host:=http://... # 覆盖 VLM 服务地址 - ros2 launch vlm_detect vlm_detect.launch.py use_tts:=false # 只启动 vlm_node + ros2 launch vlm_detect vlm_detect.launch.py # 默认全部 + ros2 launch vlm_detect vlm_detect.launch.py vlm_host:=http://... # 指定 VLM 服务地址 + ros2 launch vlm_detect vlm_detect.launch.py use_tts:=false # 关闭语音播报 + ros2 launch vlm_detect vlm_detect.launch.py use_qr_tts:=false # 关闭二维码播报桥接 + ros2 launch vlm_detect vlm_detect.launch.py use_vlm:=false # 只启动语音服务 """ import os @@ -23,7 +25,9 @@ from launch_ros.actions import Node def generate_launch_description(): # ==================== Launch 参数 ==================== + use_vlm = LaunchConfiguration('use_vlm') use_tts = LaunchConfiguration('use_tts') + use_qr_tts = LaunchConfiguration('use_qr_tts') config_file = LaunchConfiguration('config_file') @@ -37,15 +41,22 @@ def generate_launch_description(): prompt_text = LaunchConfiguration('prompt_text') max_tokens = LaunchConfiguration('max_tokens') - # tts_node 可覆盖参数 + # tts_server 可覆盖参数 audio_sink = LaunchConfiguration('audio_sink') - tts_voice = LaunchConfiguration('tts_voice') tts_speed = LaunchConfiguration('tts_speed') - # ==================== 声明参数 ==================== + # ==================== 参数声明 ==================== + declare_use_vlm = DeclareLaunchArgument( + 'use_vlm', default_value='true', + description='启动 VLM 图生文节点') + declare_use_tts = DeclareLaunchArgument( 'use_tts', default_value='true', - description='是否同时启动 TTS 语音播报节点') + description='启动 TTS 语音播报服务') + + declare_use_qr_tts = DeclareLaunchArgument( + 'use_qr_tts', default_value='true', + description='启动二维码 → TTS 桥接节点') declare_config_file = DeclareLaunchArgument( 'config_file', @@ -57,47 +68,46 @@ def generate_launch_description(): # vlm_node 参数 declare_vlm_host = DeclareLaunchArgument( 'vlm_host', default_value='http://192.168.10.189:8000', - description='VLM 推理服务地址') + description='VLM 服务器地址') declare_vlm_model = DeclareLaunchArgument( 'vlm_model', default_value='./OpenGVLab/InternVL3-1B/', description='VLM 模型名称') declare_image_topic = DeclareLaunchArgument( 'image_topic', default_value='/image_mjpeg', - description='订阅的压缩图像话题') + description='输入的压缩图像话题') declare_trigger_topic = DeclareLaunchArgument( 'trigger_topic', default_value='/sign4return', - description='订阅的触发信号话题') + description='输入的触发信号话题') declare_trigger_sign = DeclareLaunchArgument( 'trigger_sign', default_value='9', description='触发信号值 (Int32)') declare_result_topic = DeclareLaunchArgument( 'result_topic', default_value='/vlm_result', - description='发布 VLM 结果的话题') + description='输出 VLM 结果的话题') declare_prompt_text = DeclareLaunchArgument( - 'prompt_text', default_value='描述图片中有一个病人的特征,字数控制在20字以内。', + 'prompt_text', default_value='请描述这张图片的内容,用一句简短的话概括,不超过20个字。', description='发送给 VLM 的提示词') declare_max_tokens = DeclareLaunchArgument( 'max_tokens', default_value='100', - description='最大输出 token 数') + description='最大生成 token 数') - # tts_node 参数 + # tts_server 参数 declare_audio_sink = DeclareLaunchArgument( 'audio_sink', default_value='alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo', description='音频输出设备 (PulseAudio sink)') - declare_tts_voice = DeclareLaunchArgument( - 'tts_voice', default_value='zh-CN-XiaoxiaoNeural', - description='TTS 语音名称 (edge-tts)') declare_tts_speed = DeclareLaunchArgument( 'tts_speed', default_value='1.5', - description='播放速度倍率 (0.5~2.0)') + description='语速倍率 (0.5~2.0)') # ==================== 节点 ==================== + # VLM 图生文节点 (内部自带 TTS 服务客户端) vlm_node = Node( package='vlm_detect', executable='vlm_node', name='vlm_detect', output='screen', + condition=IfCondition(use_vlm), parameters=[config_file, { 'vlm_host': vlm_host, @@ -111,26 +121,35 @@ def generate_launch_description(): }], ) - tts_node = Node( + # TTS 语音播报服务端 + tts_server = Node( package='vlm_detect', - executable='tts_node', - name='tts_node', + executable='tts_server', + name='tts_server', output='screen', condition=IfCondition(use_tts), parameters=[config_file, { - 'vlm_host': vlm_host, 'audio_sink': audio_sink, - 'result_topic': result_topic, - 'tts_voice': tts_voice, 'tts_speed': tts_speed, }], ) - # ==================== 启动描述 ==================== + # 二维码 → TTS 桥接 (订阅 qr_results,调用 /tts/speak) + qr_tts_bridge = Node( + package='vlm_detect', + executable='qr_tts_bridge', + name='qr_tts_bridge', + output='screen', + condition=IfCondition(use_qr_tts), + ) + + # ==================== 组装 ==================== return LaunchDescription([ # 参数声明 + declare_use_vlm, declare_use_tts, + declare_use_qr_tts, declare_config_file, declare_vlm_host, declare_vlm_model, @@ -141,12 +160,13 @@ def generate_launch_description(): declare_prompt_text, declare_max_tokens, declare_audio_sink, - declare_tts_voice, declare_tts_speed, # 节点 LogInfo(msg=['配置文件: ', config_file]), LogInfo(msg=['VLM 服务: ', vlm_host]), - LogInfo(msg=['TTS 播报: ', use_tts]), + LogInfo(msg=['TTS 服务: ', use_tts]), + LogInfo(msg=['QR-TTS 桥接: ', use_qr_tts]), vlm_node, - tts_node, + tts_server, + qr_tts_bridge, ]) diff --git a/src/vlm_detect/setup.py b/src/vlm_detect/setup.py index 8c070ef..b4b075d 100644 --- a/src/vlm_detect/setup.py +++ b/src/vlm_detect/setup.py @@ -27,6 +27,8 @@ setup( 'vlm_node = vlm_detect.vlm_node:main', 'test_publisher = vlm_detect.test_publisher:main', 'tts_node = vlm_detect.tts_node:main', + 'tts_server = vlm_detect.tts_server:main', + 'qr_tts_bridge = vlm_detect.qr_tts_bridge:main', ], }, ) diff --git a/src/vlm_detect/vlm_detect/__pycache__/test_publisher.cpython-310.pyc b/src/vlm_detect/vlm_detect/__pycache__/test_publisher.cpython-310.pyc index 49408f792ec67724dcb25239efe7338ff0792dab..8953928b8e6251d768a450de81edee431d06cd55 100644 GIT binary patch delta 35 qcmew*I!TN-pO=@50SNBM#AiL5$UB2^{lsPMlckw8H`_7p;RFD_W(za` delta 48 zcmbOv_DhsEpO=@50SFd~I%b`l$UB2^`ov}J0tJaB8Tuvp`kA?j>8W~I1)D<|_i_RN DXRr_J diff --git a/src/vlm_detect/vlm_detect/__pycache__/tts_node.cpython-310.pyc b/src/vlm_detect/vlm_detect/__pycache__/tts_node.cpython-310.pyc index 29784a48be99c9055b182a0e3601c561d35a7443..75c715c20f663ce76fac343a96a62737f03b33b2 100644 GIT binary patch delta 1219 zcmYjQ&u<$=6rS1NySCSEVyXa1v#2P+X=68+28vRIAOeZht>mbHL9CX{#9qg{wq_^4 zR%-<*y;dliV^zsM^v0nl#DzQJPuRZzu1JUjhw^6o3yB|4Dnje;Aww@p?o_co;QDaeQ*|^gG9Z z+A8eiH?e?ko~cBc;ej^68Jem@E3i^vL*fkidWg6&FrQ#zWjMr7fazPnu(^`~b5J7o zO!CT*tIW_8nu4tU2$9lbb%rr|l`r$^BlH;V!-&_Wmw0uCufoB(SA>duL{JAc+;(os zeq!iH>?o(gP?x_-Te5rR?e2EGga?dM;$_~E)MD%ZEkiOHQ%bgSe3DjoMgalwyh55= zURu2)HS&g#hVys7Xv~YA`XRQ(&+7dvxpU|4r`tRJy^nW4XBx6e)P zxi-}^m8di2$La(`9T-y+MAl{sMqh$58mw~_quAO&N%I)glIv^f9?7<3wTaH{fkSj~ z-8Ux83<~xPNR{LUQkmEfwddtJP>xqlPcWK*iPPm5FD55wynL~uSc#NLg}9`eDLtd8 zQ4_!GOK)Flr#egnRIRmmG%hSSI^@mc;7F_)+h^(3*Av?h!XYKsz5QS~{65$_@F)x^ zIDoVEbF?1!Qf2)pSNEbAK$5E)H*YjBTLBRp)m$S14{|+$Phrd!AjjT>kZW)k&9%{( z#%xh0+~hRoeVT+RHGFu;Y*8^iTo+CAo%vkj^n~XY*>x_UgtqP;$%nqaciZzk%D0D!UCi)|6pRhHC|d(`dX8YKe#TyGyw~maZ?{ i$fW0(ypzdd>;v(uy|PdT_}&X6YD!IUmMjN)c=jJCP%r`j delta 1330 zcmYLITW=dh6rP#Ajn|jh$wp~PL=A!lt4(Z&UUI=g5Kxdnen~|k53EdPsqMyIGdoUG zYb{Gno`NC>DEpWUBU^(kg{ji(;>gw`n?yDpWa*J-4m>0B5(AIckcidIL(Eb>uHoJxw6Wum zk`PHB+SF5Qs4qtiK`h-z2N{-zsJ4e##>b$C!P8+Mft|u`?Z^69KF%`Ajg~_ygl9Kh z=7e;8tzlPPPjoI;g=o#^^ToND>Eeyq>0)8JI6F5#TPPHQcgX#Pb+@+abgWKwYOy@^ zL9OC$fRvp!udIa$(P-I?)~j}_BC4Hq9^AYf;kLyzgoEG7%Wt+XzbM zug70JI^O;<>u2TgQip=&)tYxpofufrtaHx_@^~PW z_k$B|1cfZ1+a~!u)m44< z!1R@RT=uvPD5$VD6^xp+h`hOb5S8mggnNhq6Y2>$u&aqA04-SyjPrq2_1GO9Vnb5E zOS8m|dfspu8j8&QEsVN4T=UZ4^203Y0}akEA9Vxbur$j6LC76oAoLj70bbvR;S6en zEiJQXmD?$JzmrD~Prv*5@s|%DZy(NQJ4SxJC_&AUP!-OG2(<{M)7KzX#_FEi1WalT z2T%`=w719F&%u#I{P_EyPJTJOnmzq`=gDtB1Q~t2M>^FGnesxVc)gIr{EBQc5~{2s zDqNQF2~Z*B&Zlk16JDscfF}HZ@YB3f^BnsVm)oAGh&E)nEvLyXS*G3NHQ|KDs@oJ! zQ*ik=ho;y5xW(O7$MZrh_)H(dW5J&OLXy8IPgU-Ctq=!)>X%C65RgId8YmrGl0(>p zMX>&qYLYz@z$q2jL*XRxG^JUvr$DS{!I)9fB@g*{uwqP(s$d$jnY=p%Ql>9*HTcRn z!fLQYZ{o$EN1w+F!MAjB@?A&?scknZwWe+7a5)#}Z$XS-mntvSD5&6X`ci_+##~nL X*Mpm}%a)v(jR>A5<~15Of!O~7*)>h| diff --git a/src/vlm_detect/vlm_detect/__pycache__/vlm_node.cpython-310.pyc b/src/vlm_detect/vlm_detect/__pycache__/vlm_node.cpython-310.pyc index 7c0e001deab785bdf3f2a7534ce5760504159afc..4e6191398c7e0a97092182ca11665b050b7c19dd 100644 GIT binary patch delta 268 zcmZ3kxJ!{YpO=@50SKZk;m(D0DDX`MlH?%a;Gc>aRVgo%xL&MDu%%L2N zTP9EAZZ=Ed>1C;9tzlTeyO3c4UkYyt>jKt=3^gng3@LoGSyK4tGB0FgWT;`y;!hEf zWXKZ85)5Y06rAkKldbkr4rrMs%PrR2{FKz3TO7Hm#l?x~sl`P?K=E7bC7HRY`K2Y3 zFYx?hRGEC9SDuZDk?o5D-{w!ewTz5elMDFe7)vMj^Q(&%fz)%v$LA(y=EcWrGWuzX z6=hF8%^x8f4wNZU1WB{y7o_GTW)`u7L|i~b^kfGCQ8tj?qO{4;0dc delta 290 zcmdm`xLlDppO=@50SE#O9J7{glgcP5DX`MlH?%a;Gc>c%Gc?dMw6NUl$Q;VS zxM}iq?q-u-=33Soh6TI}85Z!R@RYDFU|q;i!y>_u!aJKKg>NqNLPkc08rCfS6n;sD zEP*V+U1X&6Zz~nwOYa#10a10TGcPVb;9T+=9v?Bai?Ghya;YlrlL_z?D6PgPV(y GLAPUFIHTm<+(`q6C?132wM!a~}3wQAF(c~G0L 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 C^;t6j literal 0 HcmV?d00001