This commit is contained in:
2026-08-09 21:02:42 +08:00
parent 173670e194
commit 07598f4b11
41 changed files with 3381 additions and 255 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ log
.vscode .vscode
datas datas
running_logs

237
AGENTS.md Normal file
View File

@@ -0,0 +1,237 @@
# yiliao_ws 项目说明
本文件是 `/home/sunrise/yiliao_ws` 工作区的项目级说明,主要记录当前比赛会用到的模块、接口、启动方式和调试注意事项。后续如果某个子目录内还有自己的 `AGENTS.md`,进入该子目录工作时以更近的说明为准。
## 项目定位
- 这是 RDKx5 机器人上的 ROS 2 Humble 工作区,当前主要服务于医疗赛道/竞速赛道任务。
- 当前比赛主链路是调度型架构底盘、雷达、相机、二维码、Nav2、VLM、TTS 等功能分别由独立模块完成,`src/racing_control` 只负责总调度。
- 当前默认只走 Nav2 路径规划与跟随,`trajectory_guard` 只作为可选备用链路,不作为默认路径。
- 比赛点位、路线和行为开关应优先放在 YAML/launch 参数中,不要把新采集的场地点位直接写死进 C++。
- 修改比赛逻辑时优先保证流程稳定、有序、可调试;除非明确需要,不要在总调度节点里另起感知、规划或控制功能。
## 环境与访问
- 机器人 SSH
```bash
ssh sunrise@192.168.10.210
```
- 工作区根目录:
```bash
/home/sunrise/yiliao_ws
```
- 运行 ROS 2 命令前通常需要:
```bash
cd /home/sunrise/yiliao_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
```
- 部分相机/Hobot 相关流程可能还需要:
```bash
source /opt/tros/humble/setup.bash
```
- 远端仓库可能已有无关改动或运行日志。不要随意 `git reset`、`git checkout --` 或删除未确认文件。
## 编译与测试
- 编译单个包:
```bash
cd /home/sunrise/yiliao_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
colcon build --packages-select <package_name> --cmake-args -DBUILD_TESTING=ON
```
- 测试 `racing_control`
```bash
cd /home/sunrise/yiliao_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
colcon test --packages-select racing_control
colcon test-result --verbose --test-result-base build/racing_control
```
- 不要在同一个工作区里同时启动多个 `colcon build`。
- `ament_xmllint` 可能依赖远端 ROS schema。如果只有 xmllint 因网络/schema 临时失败,先重跑验证,再考虑改 XML。
## 比赛模块总览
### 总调度
- 包路径:`src/racing_control`
- 主节点:`src/racing_control/src/racing_control.cpp`
- 配置:`src/racing_control/config/racing_control.yaml`
- 启动:`src/racing_control/launch/racing_control.launch.py`
- 点位转换说明:`src/racing_control/点位格式转换.md`
- 设计原则:
- 只做比赛流程调度不重复实现二维码、VLM、Nav2、底盘控制等子功能。
- 通过 `/sign4return` 控制二维码、VLM 和 Nav2 参数档位。
- 通过 Nav2 action 完成到点、路径规划和路径跟随。
- 当前比赛流程:
- 导航到 `qr_pose` 附近。
- 一旦收到可解析二维码结果,立即选择顺/逆时针路线。
- 识别二维码后不再等待 QR 目标,按配置前往 `post_qr_pose` 和 `entry_pose`。
- 进入对应方向路线,按 `clockwise_waypoints` 或 `counterclockwise_waypoints` 执行。
- 到第 `vlm_waypoint_number` 个路线点时触发 VLM。
- home 前一个点到达后发布 `/sign4return=10` 调参,再发布 home goal。
- 最后返回方向对应的 `clockwise_home_pose` 或 `counterclockwise_home_pose`。
- 使用的 Nav2 actions
- `/navigate_to_pose`
- `/compute_path_through_poses`
- `/follow_path`
- 常用启动参数:
```bash
ros2 launch racing_control racing_control.launch.py auto_start:=false
ros2 launch racing_control racing_control.launch.py auto_start:=true
ros2 launch racing_control racing_control.launch.py enable_vlm_image_relay:=true
```
### 点位与路线参数
- 点位数组使用 `[x, y, yaw_radians]`,坐标系由 `frame_id` 指定。
- `racing_control.yaml` 中当前关键字段:
- `qr_pose`:二维码区域导航点。
- `post_qr_pose`:二维码识别后的后置过渡点,用于后续调参。
- `entry_pose`:进入正式路线前的入口点。
- `clockwise_waypoints`:顺时针路线点,不包含 home。
- `counterclockwise_waypoints`:逆时针路线点,不包含 home。
- `clockwise_home_pose`、`counterclockwise_home_pose`:最终 home 点。
- `use_post_qr_pose: true` 时启用 QR 后置点;设为 `false` 时二维码识别后直接去 `entry_pose`。
- `vlm_waypoint_number: 4` 表示正式路线第 4 个点是 VLM 拍摄点。不要依赖 `goal_004` 之类的点名。
- 二维码方向解析:
- 文本包含 `` 或奇数数字时选择顺时针。
- 文本包含 `` 或偶数数字时选择逆时针。
### Nav2 与轨迹保护
- 包路径:`src/navigation/obstacle_nav2`
- 主启动:`launch/obstacle_nav2.launch.py`
- 轨迹保护启动:`launch/trajectory_guard.launch.py`
- 运行时参数档位切换:`launch/nav2_profile_tuner.launch.py`
- 关键配置:
- `config/nav2_profile_10.yaml`:普通/默认导航参数。
- `config/nav2_profile_11.yaml`:任务二路线参数。
- `config/trajectory_guard.yaml`:轨迹保护参数。
- `trajectory_guard_node` 订阅 `/trajectory_guard/input_path`,属于备用链路;默认比赛流程不依赖它。
- `nav2_profile_tuner` 监听 `/sign4return`,收到 10 或 11 后调用 Nav2 参数服务切换参数档位。
- `obstacle_nav2.launch.py` 当前以里程计坐标为主,默认 global frame 是 `odom`;静态地图和 AMCL 相关配置留作备用。
### 相机与二维码
- 相机包:`src/car_usb_cam`
- 二维码包:`src/qr_detection`
- 常见相机话题:`/image`
- 当前 VLM/调度链路期望的图像类型:`sensor_msgs/msg/CompressedImage`
- 二维码结果话题:`/qr_results`,类型 `std_msgs/msg/String`
- 二维码检测受 `/sign4return` 控制:
- `0`:启用二维码检测。
- `5`:关闭二维码检测。
- 常用检查命令:
```bash
ros2 topic echo /qr_results
ros2 topic info /image
ros2 topic hz /image
```
- 如果 `racing_control` 到二维码区域后不继续走,优先检查:
- 日志里是否出现 `QR result received`。
- `/qr_results` 是否真的有输出。
- 输出文本是否包含 ``、`` 或可解析数字。
### VLM 与语音
- 包路径:`src/vlm_detect`
- 启动文件:
- `launch/vlm_detect.launch.py`OpenAI 兼容 VLM 服务流程。
- `launch/local_vlm_adapter.launch.py`:本地 `hobot_llamacpp` 适配流程。
- VLM 触发信号:`/sign4return=9`
- VLM 结果话题:`/vlm_result`
- TTS 服务:`/tts/speak`,类型 `origincar_msg/srv/Speak`
- `vlm_detect` 的图像输入可通过 launch 参数 `image_topic` 配置。
- `racing_control` 可选单帧 VLM 图像转发:
- `enable_vlm_image_relay: false` 默认关闭。
- 输入话题:`vlm_image_input_topic`,默认 `/image`。
- 输出话题:`vlm_image_output_topic`,默认 `/vlm_image`。
- 启用后,`racing_control` 在发布 `/sign4return=9` 前,把缓存到的一帧压缩图像发布到 `/vlm_image`。
- 若要让 VLM 使用该帧VLM 需这样启动:
```bash
ros2 launch vlm_detect vlm_detect.launch.py image_topic:=/vlm_image
```
- 当前 VLM 拍摄模式:
- `vlm_capture_mode: stop`:到 VLM 点停住,发布图像/触发信号,等待 `vlm_capture_wait_sec` 后继续。
- `vlm_capture_mode: pass_through`:保留的经过拍照模式,按 `pass_through_vlm_trigger_radius` 在接近 VLM 点时触发。
### 底盘、雷达、里程计与消息
- 底盘包:`src/origincar_base`
- 常见启动:`origincar_bringup.launch.py`、`base_serial.launch.py`、`ekf.launch.py`
- 负责底盘串口、IMU、里程计、EKF 和 TF 等基础能力。
- 雷达驱动路径:`src/LSLIDAR_X_ROS2-20240228/src`
- 障碍物检测包:`src/obstacle_scanner`
- 消费雷达数据,发布导航需要的障碍物信息。
- 消息/服务包:`src/origincar_msg`
- 包含 `origincar_msg/srv/Speak`。
- 当前 `racing_control` 期望里程计话题是 `/odom_combined`。
### 启动编排
- `src/my_robot_bringup/launch/master_launch.py` 是较完整的分阶段总启动涉及底盘、雷达、TTS、相机、二维码、障碍物、规划和 VLM。
- 当前 `racing_control` 流程会直接依赖 Nav2 actions并结合 `obstacle_nav2`/轨迹保护相关节点。修改 `master_launch.py` 前,应确认现场实际是通过总 launch 还是多个终端分别启动。
- `planner` 包里还有较旧或备用的 Hybrid A*、Pure Pursuit 等流程。除非当前 launch 明确使用它,否则先把它当作历史/备用路径看待。
## 共享信号与话题
- `/sign4return` 类型是 `std_msgs/msg/Int32`,被多个模块共享:
- `0`:启用二维码检测。
- `5`:关闭二维码检测。
- `9`:触发 VLM 拍摄/推理。
- `10`:应用普通 Nav2 参数档。
- `11`:应用任务二 Nav2 参数档。
- 新增 `/sign4return` 数值前,必须检查所有订阅者,包括底盘、感知和导航参数切换节点。
- 其他关键接口:
- `/qr_results`:二维码文本结果。
- `/vlm_result`VLM 文本结果。
- `/tts/speak`TTS 服务。
- `/image`:压缩相机流。
- `/vlm_image`:可选单帧 VLM 图像转发输出。
- `/trajectory_guard/input_path`:轨迹保护输入路径。
- `/cmd_vel`:真实运动控制通道,避免多个节点同时发布。
## 调试注意事项
- 不要让多个节点同时发布 `/cmd_vel`,除非明确 remap 掉其中一个。
- 二维码区域后不继续走时:
- 看 `racing_control` 日志是否出现 `QR result received`。
- 直接 echo `/qr_results`。
- 确认二维码文本能被解析为顺/逆方向。
- VLM 报 `No image` 或没有结果时:
- 检查 `ros2 topic info /image`。
- 如果启用转发,确认 `enable_vlm_image_relay:=true`,并且 VLM 使用 `image_topic:=/vlm_image`。
- 确认相机实际发布的是 `sensor_msgs/msg/CompressedImage`。
- 路线段末端卡住时:
- 检查 `/odom_combined` 是否连续。
- 检查 `circle_goal_tolerance` 是否过小。
- 如果临时启用了 `trajectory_guard`,再检查它是否正常向 `/follow_path` 转发。
- Nav2 在任务二前后行为差异大时:
- 查看 `nav2_profile_10.yaml` 和 `nav2_profile_11.yaml`。
- 查看 `nav2_profile_tuner` 日志里是否收到并应用 `/sign4return` 10/11。
- launch 已启动但按空格不能开始比赛时,可能是 stdin 不是 TTY可使用 `auto_start:=true`。
- 更新现场点位时,优先修改 `src/racing_control/config/racing_control.yaml`,然后重新 build/install `racing_control`,确保 launch 读到安装后的配置。
## 关键参数清单
- `frame_id`:比赛点位所属坐标系。
- `qr_pose`、`post_qr_pose`、`entry_pose`:二维码阶段到正式路线阶段的关键过渡点。
- `clockwise_waypoints`、`counterclockwise_waypoints`:顺/逆时针路线点。
- `clockwise_home_pose`、`counterclockwise_home_pose`:顺/逆方向 home 点。
- `use_post_qr_pose`:是否启用二维码后置点。
- `vlm_waypoint_number`:第几个正式路线点触发 VLM。
- `vlm_capture_mode``stop` 或 `pass_through`。
- `vlm_capture_wait_sec`:停住拍照模式下触发 VLM 后等待时间。
- `enable_vlm_image_relay`:是否由 `racing_control` 把 `/image` 的单帧图像转发到 `/vlm_image`。
- `vlm_image_input_topic`、`vlm_image_output_topic`VLM 图像转发输入/输出话题。
## 待后续补齐
- 比赛日标准启动顺序:到底使用 `my_robot_bringup/master_launch.py`,还是分终端启动 `obstacle_nav2`、`vlm_detect`、`racing_control` 等节点。
- 当前最权威的 Nav2 参数归属:`obstacle_nav2` 的 profile 看起来是比赛主路径,但 `planner` 和 `my_robot_bringup` 仍保留旧流程。
- 不同部署模式下相机话题的最终类型。近期 `vlm_detect` 期望 `CompressedImage`,换相机驱动前要重新确认。
- `/sign4return` 除 0、5、9、10、11 外,在底盘板和感知节点里的完整含义。
- 现场最新点位来源、采集时间和对应 JSON/YAML 转换记录。

View File

@@ -15,7 +15,7 @@
use_gps_ts: false #雷达是否使用GPS授时 use_gps_ts: false #雷达是否使用GPS授时
scan_topic: /scan #设置激光数据topic名称 scan_topic: /scan #设置激光数据topic名称
interface_selection: serial #接口选择:net 为网口,serial 为串口。 interface_selection: serial #接口选择:net 为网口,serial 为串口。
serial_port_: /dev/ttyCH343USB0 #串口连接时的串口号 serial_port_: /dev/radar #串口连接时的串口号
high_reflection: false #M10_P雷达需填写该值,若不确定,请联系技术支持。 high_reflection: false #M10_P雷达需填写该值,若不确定,请联系技术支持。
compensation: false #M10系列是否使用角度补偿功能 compensation: false #M10系列是否使用角度补偿功能
pubScan: true #是否发布scan话题 pubScan: true #是否发布scan话题

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Receive safe short-lived drive commands from the PC and publish ROS Twist."""
from __future__ import annotations
import argparse
import json
import math
import socket
import time
import rclpy
from geometry_msgs.msg import Twist
def main() -> None:
parser = argparse.ArgumentParser(description="PC UDP to ROS cmd_vel_task bridge")
parser.add_argument("--bind", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8765)
parser.add_argument("--topic", default="/cmd_vel_task")
parser.add_argument("--timeout", type=float, default=0.35)
args = parser.parse_args()
rclpy.init()
node = rclpy.create_node("x5_udp_cmd_bridge")
publisher = node.create_publisher(Twist, args.topic, 10)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((args.bind, args.port))
sock.settimeout(0.05)
last_packet = 0.0
current = (0.0, 0.0)
node.get_logger().info(f"UDP {args.bind}:{args.port} -> {args.topic}, timeout={args.timeout}s")
try:
while rclpy.ok():
try:
raw, _address = sock.recvfrom(1024)
data = json.loads(raw.decode("utf-8"))
v = float(data.get("v", 0.0))
w = float(data.get("w", 0.0))
if not (math.isfinite(v) and math.isfinite(w)):
raise ValueError("non-finite command")
current = (max(-0.35, min(0.35, v)), max(-1.6, min(1.6, w)))
last_packet = time.monotonic()
except socket.timeout:
pass
except (ValueError, TypeError, json.JSONDecodeError) as exc:
node.get_logger().warn(f"ignore invalid UDP command: {exc}")
if time.monotonic() - last_packet > args.timeout:
current = (0.0, 0.0)
msg = Twist()
msg.linear.x, msg.angular.z = current
publisher.publish(msg)
rclpy.spin_once(node, timeout_sec=0.0)
finally:
publisher.publish(Twist())
sock.close()
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,429 @@
# yiliao_ws 项目总结
> **生成日期**: 2026-08-09
> **项目名称**: 智慧医疗2026第21届全国大学生智能汽车竞赛·地瓜机器人赛项
> **ROS 发行版**: ROS 2 Humble
> **机器人平台**: OriginCar (RDK X5, 四轮阿克曼转向)
> **工作区路径**: `/home/sunrise/yiliao_ws`
> **远程仓库**: `https://gitee.com/hikos/smart-healthcare-2026.git`
---
## 一、项目定位
本项目是 RDK X5 机器人上的 ROS 2 Humble 工作区服务于第21届全国大学生智能汽车竞赛的**医疗赛道**(智慧医疗)。整体采用**调度型架构**底盘、雷达、相机、二维码、Nav2 导航、VLM 图生文、TTS 语音等功能由独立模块完成,`racing_control` 包作为总调度协调各模块完成比赛流程。
---
## 二、项目目录结构总览
```
yiliao_ws/
├── src/ # 源代码(所有 ROS 2 包)
│ ├── racing_control/ # 🎯 总调度(比赛流程控制)
│ ├── origincar_base/ # 🔧 底盘驱动(串口通信 + EKF + 阿克曼)
│ ├── navigation/ # 🧭 导航相关包集合
│ │ ├── obstacle_nav2/ # 主 Nav2 导航栈(含参数调谐器)
│ │ ├── gc_navigation2_slamtoolbox/ # 仿真版 SLAM + Nav2
│ │ ├── gc_navigation2_real/ # 实车版 SLAM + Nav2
│ │ ├── gc_navigation_fish/ # 另一套导航配置
│ │ ├── cyy_navigation2/ # 备选导航方案
│ │ ├── cyy_slamtoolbox/ # 备选 SLAM 方案
│ │ └── zbw_slamtoolbox/ # 另一套 SLAM 方案
│ ├── qr_detection/ # 📷 二维码检测
│ ├── vlm_detect/ # 🤖 VLM 图生文 + TTS 语音播报
│ ├── car_usb_cam/ # 📸 USB 相机驱动
│ ├── obstacle_scanner/ # 📡 激光雷达障碍物检测
│ ├── origincar_msg/ # 📦 自定义 ROS 2 消息
│ ├── origincar_description/ # 🤖 机器人 URDF 模型描述
│ ├── origincar_birdseye/ # 🐦 鸟瞰图转换
│ ├── my_robot_bringup/ # 🚀 总启动编排master_launch.py
│ ├── planner/ # 🗺️ 备选规划器Hybrid A* 等)
│ ├── past_control/ # 📝 历史控制方案
│ ├── mtran/ # 🧠 MTRAN 模型
│ ├── ground_slam/ # 🗺️ 地面 SLAM
│ ├── car_image_proc/ # 🖼️ 图像处理
│ ├── yolov8_launch/ # 🎯 YOLOv8 目标检测
│ ├── data_collection_tools/ # 📊 数据采集工具
│ ├── LSLIDAR_X_ROS2-20240228/ # 📡 镭神激光雷达 ROS 2 驱动
│ └── gc/ # 📄 项目文档存放
├── build/ # colcon 构建输出
├── install/ # colcon 安装输出
├── scripts/ # 辅助脚本PID跟踪、陀螺仪标定等
├── bashes/ # 自动脚本WiFi连接、雷达驱动切换
├── config/dds/ # DDS 配置
├── datas/ # 采集数据
├── models/ # 模型文件
├── docs/ # 文档
├── bag_outputs/ # ROS Bag 输出
├── running_logs/ # 运行日志
├── test_results/ # 测试结果
├── vlm_server.py # VLM 推理服务FastAPI
├── cali.npz / cali.txt # 标定数据
├── path_follower_demo.py # 路径跟随演示
└── 标定.py # 标定脚本
```
---
## 三、核心模块详解
### 3.1 racing_control — 比赛总调度 ⭐
| 项目 | 说明 |
|------|------|
| **包路径** | `src/racing_control` |
| **语言** | C++ (rclcpp) |
| **主节点** | `racing_control.cpp` |
| **配置** | `config/racing_control.yaml` |
| **启动** | `launch/racing_control.launch.py` |
**设计原则**只做比赛流程调度不重复实现二维码、VLM、Nav2、底盘控制等子功能。通过 `/sign4return` 信号控制各子模块的开关和参数切换。
**比赛流程12个阶段**
```mermaid
graph TD
A[Idle 等待启动] --> B[NavigateToQr 二维码点导航]
B --> C[NavigatePostQr 二维码后置点导航]
C --> D[WaitForQr 二维码识别+TTS]
D --> E[NavigateToEntry 通道入口导航]
E --> F[SwitchToTask2Profile 任务二参数切换]
F --> G[ComputeCirclePath 任务二轨迹规划]
G --> H[ExecuteCirclePath 任务二轨迹执行]
H --> I[SwitchToNormalProfile 恢复导航参数]
I --> J[WaitForVlm 图生文+TTS]
J --> K[ReturnOrigin 返回原点]
K --> L[Finished 比赛完成]
```
**关键接口**
- Nav2 Actions: `/navigate_to_pose`, `/compute_path_through_poses`, `/follow_path`
- 控制信号: `/sign4return` (Int32)数值含义0=启用QR, 5=关闭QR, 9=触发VLM, 10=普通Nav2参数, 11=任务二Nav2参数
- 接收: `/qr_results`, `/vlm_result`, `/odom_combined`
**关键参数YAML 配置)**
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `qr_pose` | 坐标数组 | 二维码区域导航点 |
| `post_qr_pose` | 坐标数组 | 二维码后置过渡点 |
| `entry_pose` | 坐标数组 | 正式路线入口点 |
| `clockwise_waypoints` | 坐标数组 | 顺时针路线点不含home |
| `counterclockwise_waypoints` | 坐标数组 | 逆时针路线点不含home |
| `clockwise_home_pose` | 坐标数组 | 顺时针home点 |
| `counterclockwise_home_pose` | 坐标数组 | 逆时针home点 |
| `vlm_waypoint_number` | 2 | 第几个路线点触发VLM |
| `vlm_capture_mode` | stop | VLM拍摄模式 (stop/pass_through) |
| `use_post_qr_pose` | false | 是否启用QR后置点 |
| `enable_vlm_image_relay` | false | 是否转发单帧图像给VLM |
**二维码方向解析规则**
- 文本包含 `顺` 或奇数数字 → 顺时针
- 文本包含 `逆` 或偶数数字 → 逆时针
---
### 3.2 origincar_base — 底盘驱动
| 项目 | 说明 |
|------|------|
| **包路径** | `src/origincar_base` |
| **语言** | C++ + Python |
| **主节点** | `origincar_base.cpp` (串口通信), `cmd_vel_to_ackermann_drive.py` (运动转换) |
| **配置** | `config/ekf.yaml`, `config/imu.yaml` |
**功能**
- STM32 串口通信 (`/dev/ttyACM0`, 921600bps)
- 航迹推算 + Mahony AHRS 姿态解算
- EKF 融合 (odom + IMU → `/odom_combined`)
- Twist → AckermannDriveStamped 转换 (wheelbase=0.143m)
- 帧协议: 24字节收0x7B/0x7D帧头尾/ 11字节发
**阿克曼底盘模式** (`akmcar:=true`)
```
cmd_vel(Twist) → cmd_vel_to_ackermann_drive.py → ackermann_cmd(AckermannDriveStamped) → STM32
```
**关键修改must_know.md 记录)**
- EKF 帧名统一 (`odom_combined``odom`)
- 底盘驱动默认不再发布 TF避免与 EKF 冲突)
- 里程计协方差可参数化配置
- 串口异常捕获防崩溃
---
### 3.3 obstacle_nav2 — 主 Nav2 导航栈
| 项目 | 说明 |
|------|------|
| **包路径** | `src/navigation/obstacle_nav2` |
| **启动文件** | `obstacle_nav2.launch.py`, `nav2_profile_tuner.launch.py`, `trajectory_guard.launch.py` |
**功能**
- 集成 Nav2 导航MPPI 控制器 + SmacPlannerHybrid 规划器)
- 消费 `/obstacles` 转化为 costmap 障碍物层
- **参数档位切换**`nav2_profile_tuner` 监听 `/sign4return`10→普通参数11→任务二参数
- **轨迹保护**(备用):`trajectory_guard` 订阅 `/trajectory_guard/input_path`
**关键参数文件**
| 文件 | 用途 |
|------|------|
| `nav2_params.yaml` | Nav2 主参数 |
| `nav2_profile_10.yaml` | 普通/默认导航参数档 |
| `nav2_profile_11.yaml` | 任务二路线参数档 |
| `trajectory_guard.yaml` | 轨迹保护参数 |
| `nav2_params_basic_tracking.yaml` | 基础跟踪参数 |
**MPPI 参数CPU 优化后)**
- `time_steps`: 20, `batch_size`: 200 (原1000)
- `PathAlignCritic.cost_weight`: 7.0 (降低,避障优先)
**代价地图**`inflation_radius`: 0.2m, `cost_scaling_factor`: 3.0
---
### 3.4 qr_detection — 二维码检测
| 项目 | 说明 |
|------|------|
| **包路径** | `src/qr_detection` |
| **语言** | C++ |
| **输入** | `/image` (CompressedImage) |
| **输出** | `/qr_results` (std_msgs/String) |
| **控制** | `/sign4return=0` 启用, `/sign4return=5` 关闭 |
---
### 3.5 vlm_detect — VLM 图生文 + TTS
| 项目 | 说明 |
|------|------|
| **包路径** | `src/vlm_detect` |
| **语言** | Python |
| **主节点** | `vlm_node.py` (VLM推理), `tts_server.py` (语音合成) |
**数据流**
```
USB Camera → /image → vlm_node → /vlm_result
→ qr_dete_node → /qr_results
qr_tts_bridge → /tts/speak → Piper TTS → 语音输出
```
**VLM 触发**`/sign4return=9`
**VLM 服务**:外部 FastAPI 服务 (`vlm_server.py`),运行 AndesVL-1B 模型OpenAI 兼容 API
**TTS**Piper TTS + paplay 本地语音合成
---
### 3.6 obstacle_scanner — 雷达障碍物检测
| 项目 | 说明 |
|------|------|
| **包路径** | `src/obstacle_scanner` |
| **语言** | C++ (使用 OpenCV + Eigen) |
| **功能** | 基于角度聚类+圆拟合的 LiDAR 障碍物检测 |
---
### 3.7 my_robot_bringup — 总启动编排
| 项目 | 说明 |
|------|------|
| **包路径** | `src/my_robot_bringup` |
| **启动文件** | `launch/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`, `use_lidar`, `use_slam`, `use_nav2`, `use_vlm`, `use_tts`, `akmcar`, `vlm_host`
---
### 3.8 其他包
| 包名 | 说明 |
|------|------|
| `origincar_description` | 机器人 URDF 模型 (0.276×0.214×0.211m, wheelbase=0.143m) |
| `origincar_msg` | 自定义消息 (Data.msg, Sign.msg) 和服务 (Speak.srv) |
| `car_usb_cam` | USB 相机驱动 (hobot_usb_cam),发布 `/image` |
| `planner` | 备选规划方案Hybrid A*, Pure Pursuit, Ackermann Hybrid A* |
| `origincar_birdseye` | 鸟瞰图/IPM 变换 |
| `ground_slam` | 地面 SLAM 建图 |
| `yolov8_launch` | YOLOv8 目标检测启动 |
| `car_image_proc` | 图像处理工具 |
| `past_control` | 历史控制方案(竞速参考) |
| `mtran` | MTRAN 模型(含依赖和文档) |
| `data_collection_tools` | X5 UDP 指令桥接 |
---
## 四、导航相关包集合 (`src/navigation/`)
本工作区历史上有多个 SLAM/Nav2 方案迭代:
| 包名 | 用途 | 状态 |
|------|------|:----:|
| `obstacle_nav2` | 主 Nav2 导航栈 + 参数调谐 | ✅ **当前使用** |
| `gc_navigation2_slamtoolbox` | 仿真版 SLAM + Nav2 (Gazebo) | 📦 仿真用 |
| `gc_navigation2_real` | 实车版 SLAM + Nav2 (无 Gazebo) | 📦 实车参考 |
| `gc_navigation_fish` | 另一套导航配置 | 📦 备用 |
| `cyy_navigation2` | 备选导航方案 | 📦 历史 |
| `cyy_slamtoolbox` | 备选 SLAM | 📦 历史 |
| `zbw_slamtoolbox` | 另一套 SLAM | 📦 历史 |
**关键差异(仿真 vs 实车)**
| 方面 | 仿真版 | 实车版 |
|------|--------|--------|
| `use_sim_time` | true | false |
| odom_frame | `odom` | `odom_combined` (EKF融合后) |
| 底盘驱动 | Gazebo plugin | origincar_base 串口驱动 |
| 激光雷达 | Gazebo ray plugin | lslidar_driver (镭神 N10) |
---
## 五、TF 树结构
```
map ──→ odom ──→ base_footprint ──→ base_link ──→ laser_link
↑ ↑ ↑ ↑
slam EKF融合 静态TF URDF
```
---
## 六、关键共享信号 `/sign4return`
| 值 | 含义 | 消费者 |
|:--:|------|--------|
| 0 | 启用二维码检测 | qr_detection |
| 5 | 关闭二维码检测 | qr_detection |
| 9 | 触发 VLM 拍摄/推理 | vlm_detect |
| 10 | 应用普通 Nav2 参数档 | nav2_profile_tuner |
| 11 | 应用任务二 Nav2 参数档 | nav2_profile_tuner |
---
## 七、环境与操作
### 7.1 机器人访问
```bash
ssh sunrise@192.168.10.210
```
### 7.2 环境初始化
```bash
cd /home/sunrise/yiliao_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
# 部分相机流程可能还需要: source /opt/tros/humble/setup.bash
```
### 7.3 编译
```bash
# 编译单个包
colcon build --packages-select <package_name> --cmake-args -DBUILD_TESTING=ON
# 全量编译(推荐 --symlink-install 使 Python 修改即时生效)
colcon build --symlink-install
```
### 7.4 运行测试
```bash
colcon test --packages-select racing_control
colcon test-result --verbose --test-result-base build/racing_control
```
### 7.5 常用启动命令
```bash
# 全部启动(比赛模式)
ros2 launch my_robot_bringup master_launch.py
# 仅底盘调试
ros2 launch my_robot_bringup master_launch.py use_lidar:=false use_slam:=false use_nav2:=false use_vlm:=false
# 仅建图
ros2 launch my_robot_bringup master_launch.py use_nav2:=false use_vlm:=false
# 比赛控制(自动开始)
ros2 launch racing_control racing_control.launch.py auto_start:=true
# 带图像转发的比赛控制
ros2 launch racing_control racing_control.launch.py enable_vlm_image_relay:=true
```
---
## 八、硬件与传感器
| 部件 | 型号 | 说明 |
|------|------|------|
| 主控 | RDK X5 | 10 TOPS 算力 |
| 底盘 | OriginCar | 四轮阿克曼转向276×214×211mm轴距143mm |
| 激光雷达 | 镭神 N10 | 单线TOF0.15~12m |
| 深度相机 | 光鉴 Aurora930 | |
| USB相机 | USB 2.0 Camera | 发布 `/image` |
| IMU | 板载 | 与里程计进行 EKF 融合 |
---
## 九、已知问题与调试经验
### 9.1 已修复的关键问题
| 问题 | 修复方案 |
|------|----------|
| TF 树断连(两节点争抢 odom→base_link | EKF 改为发布 odom→base_footprint底盘驱动默认不发布 TF |
| 串口异常崩溃 | 为 Stm32_Serial.read() 添加 try-catch |
| LiDAR 扫描出现空扇形面 (60°) | 恢复 `scan_num` 动态计算公式 |
| URDF git 冲突 | 清除冲突标记,统一轮距参数 |
| CPU 负载过高 (77%) | MPPI batch_size 1000→200, throttle_scans 1→10, EKF 30Hz→20Hz |
| cmd_vel_to_ackermann_drive.py 权限 | chmod +x |
### 9.2 调试检查清单
- **二维码区域后不继续走**:检查 `/qr_results` 是否有输出,文本是否可解析为顺/逆
- **VLM 无结果**:检查 `/image` 话题,确认 enable_vlm_image_relay 和 VLM image_topic 配置
- **路线末端卡住**:检查 `/odom_combined` 连续性circle_goal_tolerance 是否过小
- **Nav2 行为异常**:检查 nav2_profile_tuner 是否收到/应用了 /sign4return 10/11
- **雷达无数据**`lsusb` 检查物理连接,`ls /dev/tty*` 检查 ttyACM0/ACM1/CH340
---
## 十、辅助脚本
| 脚本 | 用途 |
|------|------|
| `scripts/PIDtracking.py` | PID 轨迹跟踪 |
| `scripts/measure_turning_radius.py` | 测量转弯半径 |
| `scripts/fit_gyro_bias.py` | 陀螺仪偏置拟合 |
| `scripts/publish_sine_path.py` | 发布正弦路径 |
| `scripts/udp_to_cmdvel.py` | UDP→cmd_vel 桥接 |
| `scripts/set_volume.py` | 音量设置 |
| `bashes/radar-driver-switch.sh` | 雷达驱动自动切换 |
| `bashes/auto-wifi-connect.sh` | WiFi 自动连接 |
| `vlm_server.py` | VLM 推理服务 (FastAPI, AndesVL-1B) |
| `path_follower_demo.py` | 路径跟随演示 |
---
## 十一、数据与文档
| 类型 | 路径 | 说明 |
|------|------|------|
| 地图文件 | `src/navigation/*/maps/` | .pgm + .yaml + .posegraph |
| 采集数据 | `datas/` | data4, data5, PIDtracking等 |
| 调试记录 | `调试记录.md` | 2026/6/1 ~ 6/13 调试过程 |
| 变更说明 | `must_know.md` | mo_new→HEAD 101文件变更详解 |
| 启动指南 | `master_launch_使用指南.md` | master_launch.py 完整说明 |
| 竞赛方案 | `竞赛方案_第21届智能汽车竞赛地瓜机器人赛项.md` | 正式竞赛方案文档 |
| 点位移转 | `src/racing_control/点位格式转换.md` | JSON→YAML 点位转换流程 |
| VLM数据流 | `src/vlm_detect/dataflow.md` | VLM+QR→TTS 数据流图 |
| AGENTS.md | `/home/sunrise/yiliao_ws/AGENTS.md` | 项目级说明 |
| CLAUDE.md | `/home/sunrise/yiliao_ws/CLAUDE.md` | 另一视角的项目说明(含 test_ws 内容) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -107,6 +107,13 @@ install(TARGETS
install(DIRECTORY include/ DESTINATION include) install(DIRECTORY include/ DESTINATION include)
install(FILES obstacle_nav2_plugins.xml DESTINATION share/${PROJECT_NAME}) install(FILES obstacle_nav2_plugins.xml DESTINATION share/${PROJECT_NAME})
# Python scripts
install(PROGRAMS
scripts/initial_pose_to_tf.py
scripts/static_map_publisher.py
DESTINATION share/${PROJECT_NAME}/scripts
)
if(BUILD_TESTING) if(BUILD_TESTING)
find_package(ament_cmake_gtest REQUIRED) find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(test_obstacle_array_layer test/test_obstacle_array_layer.cpp ament_add_gtest(test_obstacle_array_layer test/test_obstacle_array_layer.cpp

View File

@@ -0,0 +1,41 @@
<!--
Ackermann NavigateThroughPoses behavior tree.
Planning failure is retried once after the global costmap has been cleared
and given one update period. FollowPath is not replanned periodically while
it is RUNNING. A controller failure gets one local-costmap retry, then one
short backup followed by one final through-poses plan and follow attempt.
Any final failure aborts the navigation action.
-->
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="NavigateThroughPosesOnce">
<RecoveryNode number_of_retries="1" name="ComputePathThroughPoses">
<ReactiveSequence>
<RemovePassedGoals input_goals="{goals}" output_goals="{goals}" radius="0.7"/>
<ComputePathThroughPoses goals="{goals}" path="{path}" planner_id="GridBased"/>
</ReactiveSequence>
<Sequence name="ClearGlobalAndWait">
<ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/>
<Wait wait_duration="1"/>
</Sequence>
</RecoveryNode>
<Fallback name="FollowOrSingleBackupReplan">
<RecoveryNode number_of_retries="1" name="FollowWithLocalClear">
<FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
<Sequence name="BackupReplanAndFinalFollow">
<BackUp backup_dist="0.04" backup_speed="0.20"/>
<ReactiveSequence>
<RemovePassedGoals input_goals="{goals}" output_goals="{goals}" radius="0.7"/>
<ComputePathThroughPoses goals="{goals}" path="{path}" planner_id="GridBased"/>
</ReactiveSequence>
<FollowPath path="{path}" controller_id="FollowPath"/>
</Sequence>
</Fallback>
</Sequence>
</BehaviorTree>
</root>

View File

@@ -1,33 +1,31 @@
<!-- <!--
阿克曼底盘行为树无原地旋转Spin Ackermann NavigateToPose behavior tree.
恢复行为序列:清除代价地图 → 后退 → 等待
Compute the path once and keep the same path while FollowPath is RUNNING.
If following fails, clear the local costmap and retry the same path once.
If that also fails, back up 0.04 m, compute one final path, and follow it
once. Any failure in the final branch ends the navigation action.
--> -->
<root main_tree_to_execute="MainTree"> <root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree"> <BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery"> <Sequence name="NavigateWithSingleBackupReplan">
<PipelineSequence name="NavigateWithReplanning"> <RecoveryNode number_of_retries="1" name="ComputeInitialPath">
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePathToPose">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/> <ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/> <ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode> </RecoveryNode>
</RateController>
<RecoveryNode number_of_retries="1" name="FollowPath"> <Fallback name="FollowOrSingleBackupReplan">
<RecoveryNode number_of_retries="1" name="FollowWithLocalClear">
<FollowPath path="{path}" controller_id="FollowPath"/> <FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/> <ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode> </RecoveryNode>
</PipelineSequence>
<ReactiveFallback name="RecoveryFallback"> <Sequence name="BackupReplanAndFinalFollow">
<GoalUpdated/> <BackUp backup_dist="0.04" backup_speed="0.20"/>
<RoundRobin name="RecoveryActions"> <ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<Sequence name="ClearingActions"> <FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap name="ClearLocalCostmap-Subtree" service_name="local_costmap/clear_entirely_local_costmap"/> </Sequence>
<ClearEntireCostmap name="ClearGlobalCostmap-Subtree" service_name="global_costmap/clear_entirely_global_costmap"/> </Fallback>
</Sequence> </Sequence>
<BackUp backup_dist="0.80" backup_speed="0.18"/>
<Wait wait_duration="0.3"/>
</RoundRobin>
</ReactiveFallback>
</RecoveryNode>
</BehaviorTree> </BehaviorTree>
</root> </root>

View File

@@ -0,0 +1,33 @@
<!--
阿克曼底盘行为树无原地旋转Spin
恢复行为序列:清除代价地图 → 后退 → 等待
-->
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery">
<PipelineSequence name="NavigateWithReplanning">
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePathToPose">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
</RateController>
<RecoveryNode number_of_retries="1" name="FollowPath">
<FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
</PipelineSequence>
<ReactiveFallback name="RecoveryFallback">
<GoalUpdated/>
<RoundRobin name="RecoveryActions">
<Sequence name="ClearingActions">
<ClearEntireCostmap name="ClearLocalCostmap-Subtree" service_name="local_costmap/clear_entirely_local_costmap"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Subtree" service_name="global_costmap/clear_entirely_global_costmap"/>
</Sequence>
<BackUp backup_dist="0.80" backup_speed="0.18"/>
<Wait wait_duration="0.3"/>
</RoundRobin>
</ReactiveFallback>
</RecoveryNode>
</BehaviorTree>
</root>

View File

@@ -0,0 +1,35 @@
<!--
Ackermann NavigateToPose behavior tree.
Compute the path once and keep the same path while FollowPath is RUNNING.
A new global plan is requested only after FollowPath returns FAILURE and the
outer RecoveryNode starts another navigation attempt. This avoids replacing
a valid path every second while MPPI is still evaluating it.
Recovery actions intentionally do not include Spin or BackUp because this
robot uses an Ackermann drive model.
-->
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery">
<Sequence name="NavigateOnce">
<RecoveryNode number_of_retries="1" name="ComputePathToPose">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
<RecoveryNode number_of_retries="1" name="FollowPath">
<FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
</Sequence>
<ReactiveFallback name="RecoveryFallback">
<GoalUpdated/>
<Sequence name="ClearingActions">
<ClearEntireCostmap name="ClearLocalCostmap-Subtree" service_name="local_costmap/clear_entirely_local_costmap"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Subtree" service_name="global_costmap/clear_entirely_global_costmap"/>
<Wait wait_duration="1"/>
</Sequence>
</ReactiveFallback>
</RecoveryNode>
</BehaviorTree>
</root>

View File

@@ -0,0 +1,340 @@
# ============================================================================
# nav2_params.yaml — Odometry-only obstacle navigation
#
# No static map, no AMCL, no SLAM.
# Both costmaps are rolling windows in odom. The launch file can rewrite every
# global_frame leaf when a different connected odometry frame is required.
# Global planner: Smac Hybrid A* (Reeds-Shepp)
# Local controller: MPPI (Ackermann)
# 速度快但是雷达容易掉
# ============================================================================
bt_navigator:
ros__parameters:
use_sim_time: False
global_frame: map
robot_base_frame: base_footprint
odom_topic: /odom_combined
bt_loop_duration: 50
default_server_timeout: 20
# Injected by obstacle_nav2.launch.py from this package's share directory.
default_nav_to_pose_bt_xml: ""
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_smooth_path_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_back_up_action_bt_node
- nav2_drive_on_heading_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_globally_updated_goal_condition_bt_node
- nav2_is_path_valid_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_truncate_path_local_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_path_expiring_timer_condition
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
- nav2_controller_cancel_bt_node
- nav2_path_longer_on_approach_bt_node
- nav2_wait_cancel_bt_node
- nav2_spin_cancel_bt_node
- nav2_back_up_cancel_bt_node
- nav2_drive_on_heading_cancel_bt_node
bt_navigator_rclcpp_node:
ros__parameters:
use_sim_time: False
controller_server:
ros__parameters:
use_sim_time: False
controller_frequency: 20.0
FollowPath:
plugin: "nav2_mppi_controller::MPPIController"
time_steps: 36
model_dt: 0.05
batch_size: 1000
vx_std: 0.22
vy_std: 0.0
wz_std: 0.45
vx_max: 1.00
vx_min: -0.75
vy_max: 0.0
wz_max: 1.5
iteration_count: 1
temperature: 0.3
gamma: 0.015
motion_model: "Ackermann"
visualize: false
TrajectoryVisualizer:
trajectory_step: 5
time_step: 3
AckermannConstraints:
min_turning_r: 0.4
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
ConstraintCritic:
enabled: true
cost_power: 1
cost_weight: 4.0
GoalCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
threshold_to_consider: 1.4
GoalAngleCritic:
enabled: true
cost_power: 1
cost_weight: 3.0
threshold_to_consider: 0.5
PreferForwardCritic:
enabled: false
cost_power: 1
cost_weight: 4.0
threshold_to_consider: 0.5
CostCritic:
enabled: true
cost_power: 1
cost_weight: 3.81
critical_cost: 300.0
consider_footprint: true
collision_cost: 100000.0
near_goal_distance: 1.0
trajectory_point_step: 2
PathAlignCritic:
enabled: true
cost_power: 1
cost_weight: 10.0
max_path_occupancy_ratio: 0.05
trajectory_point_step: 4
threshold_to_consider: 0.5
offset_from_furthest: 20
use_path_orientations: false
PathFollowCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
offset_from_furthest: 10
threshold_to_consider: 1.4
PathAngleCritic:
enabled: true
cost_power: 1
cost_weight: 2.0
offset_from_furthest: 5
threshold_to_consider: 0.5
max_angle_to_furthest: 1.0
forward_preference: false
controller_server_rclcpp_node:
ros__parameters:
use_sim_time: False
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
transform_tolerance: 0.5
global_frame: odom
robot_base_frame: base_footprint
use_sim_time: False
rolling_window: true
width: 3
height: 3
resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02
track_unknown_space: false
plugins: ["obstacle_array_layer", "inflation_layer"]
obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true
topic: /obstacles
obstacle_timeout: 0.5
transform_tolerance: 0.2
default_obstacle_radius: 0.05
minimum_obstacle_radius: 0.02
maximum_obstacle_radius: 0.50
extra_inflation: 0.02
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.35
always_send_full_costmap: True
local_costmap_client:
ros__parameters:
use_sim_time: False
local_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
transform_tolerance: 0.5
global_frame: map
robot_base_frame: base_footprint
use_sim_time: False
rolling_window: false
width: 8
height: 8
resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02
track_unknown_space: true
plugins: ["static_layer", "obstacle_array_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
enabled: true
map_subscribe_transient_local: true
subscribe_to_updates: false
obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true
topic: /obstacles
obstacle_timeout: 0.5
transform_tolerance: 0.2
default_obstacle_radius: 0.05
minimum_obstacle_radius: 0.02
maximum_obstacle_radius: 0.50
extra_inflation: 0.02
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 2.0
inflation_radius: 0.35
always_send_full_costmap: True
global_costmap_client:
ros__parameters:
use_sim_time: False
global_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
planner_server:
ros__parameters:
planner_plugins: ["GridBased"]
use_sim_time: False
GridBased:
plugin: "nav2_smac_planner/SmacPlannerHybrid"
downsample_costmap: false
downsampling_factor: 1
tolerance: 0.15
allow_unknown: false
max_iterations: 1000000
max_on_approach_iterations: 1000
max_planning_time: 5.0
motion_model_for_search: "REEDS_SHEPP"
angle_quantization_bins: 72
analytic_expansion_ratio: 3.5
analytic_expansion_max_length: 3.0
minimum_turning_radius: 0.40
reverse_penalty: 3.0
change_penalty: 0.0
non_straight_penalty: 1.2
cost_penalty: 2.0
retrospective_penalty: 0.015
# 5 m covers the rolling planning horizon without the startup and memory
# cost of the previous 20 m (401-cell) Hybrid-A* lookup table.
lookup_table_size: 5.0
cache_obstacle_heuristic: false
viz_expansions: false
smooth_path: True
smoother:
max_iterations: 1000
w_smooth: 0.3
w_data: 0.2
tolerance: 1.0e-10
do_refinement: true
refinement_num: 2
planner_server_rclcpp_node:
ros__parameters:
use_sim_time: False
smoother_server:
ros__parameters:
use_sim_time: False
smoother_plugins: ["simple_smoother"]
simple_smoother:
plugin: "nav2_smoother::SimpleSmoother"
tolerance: 1.0e-10
max_its: 1000
do_refinement: True
behavior_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
behavior_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_behaviors/Spin"
backup:
plugin: "nav2_behaviors/BackUp"
backup_dist: 0.8
backup_speed: 0.18
wait:
plugin: "nav2_behaviors/Wait"
wait_duration: 0.5
global_frame: map
robot_base_frame: base_footprint
transform_tolerance: 0.5
use_sim_time: False
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
robot_state_publisher:
ros__parameters:
use_sim_time: False
waypoint_follower:
ros__parameters:
loop_rate: 20
use_sim_time: False
stop_on_failure: false
waypoint_task_executor_plugin: "wait_at_waypoint"
wait_at_waypoint:
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
enabled: True
waypoint_pause_duration: 200
velocity_smoother:
ros__parameters:
use_sim_time: False
smoothing_frequency: 20.0
scale_velocities: False
feedback: "OPEN_LOOP"
max_velocity: [1.00, 0.0, 2.0]
min_velocity: [-0.75, 0.0, -2.0]
max_accel: [3.73, 0.0, 3.2]
max_decel: [-1.1, 0.0, -4.5]
odom_topic: /odom_combined
odom_duration: 0.1
deadband_velocity: [0.03, 0.0, 0.03]
velocity_timeout: 1.0

View File

@@ -0,0 +1,340 @@
# ============================================================================
# nav2_params.yaml — Odometry-only obstacle navigation
#
# No static map, no AMCL, no SLAM.
# Both costmaps are rolling windows in odom. The launch file can rewrite every
# global_frame leaf when a different connected odometry frame is required.
# Global planner: Smac Hybrid A* (Reeds-Shepp)
# Local controller: MPPI (Ackermann)
# 比之前速度慢了点
# ============================================================================
bt_navigator:
ros__parameters:
use_sim_time: False
global_frame: map
robot_base_frame: base_footprint
odom_topic: /odom_combined
bt_loop_duration: 50
default_server_timeout: 20
# Injected by obstacle_nav2.launch.py from this package's share directory.
default_nav_to_pose_bt_xml: ""
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_smooth_path_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_back_up_action_bt_node
- nav2_drive_on_heading_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_globally_updated_goal_condition_bt_node
- nav2_is_path_valid_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_truncate_path_local_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_path_expiring_timer_condition
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
- nav2_controller_cancel_bt_node
- nav2_path_longer_on_approach_bt_node
- nav2_wait_cancel_bt_node
- nav2_spin_cancel_bt_node
- nav2_back_up_cancel_bt_node
- nav2_drive_on_heading_cancel_bt_node
bt_navigator_rclcpp_node:
ros__parameters:
use_sim_time: False
controller_server:
ros__parameters:
use_sim_time: False
controller_frequency: 20.0
FollowPath:
plugin: "nav2_mppi_controller::MPPIController"
time_steps: 36
model_dt: 0.05
batch_size: 1000
vx_std: 0.22
vy_std: 0.0
wz_std: 0.45
vx_max: 1.00
vx_min: -0.75
vy_max: 0.0
wz_max: 1.5
iteration_count: 1
temperature: 0.3
gamma: 0.015
motion_model: "Ackermann"
visualize: false
TrajectoryVisualizer:
trajectory_step: 5
time_step: 3
AckermannConstraints:
min_turning_r: 0.4
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
ConstraintCritic:
enabled: true
cost_power: 1
cost_weight: 4.0
GoalCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
threshold_to_consider: 1.4
GoalAngleCritic:
enabled: true
cost_power: 1
cost_weight: 3.0
threshold_to_consider: 0.5
PreferForwardCritic:
enabled: false
cost_power: 1
cost_weight: 4.0
threshold_to_consider: 0.5
CostCritic:
enabled: true
cost_power: 1
cost_weight: 3.81
critical_cost: 300.0
consider_footprint: true
collision_cost: 100000.0
near_goal_distance: 1.0
trajectory_point_step: 2
PathAlignCritic:
enabled: true
cost_power: 1
cost_weight: 10.0
max_path_occupancy_ratio: 0.05
trajectory_point_step: 4
threshold_to_consider: 0.5
offset_from_furthest: 20
use_path_orientations: false
PathFollowCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
offset_from_furthest: 10
threshold_to_consider: 1.4
PathAngleCritic:
enabled: true
cost_power: 1
cost_weight: 2.0
offset_from_furthest: 5
threshold_to_consider: 0.5
max_angle_to_furthest: 1.0
forward_preference: false
controller_server_rclcpp_node:
ros__parameters:
use_sim_time: False
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
transform_tolerance: 0.5
global_frame: odom
robot_base_frame: base_footprint
use_sim_time: False
rolling_window: true
width: 3
height: 3
resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02
track_unknown_space: false
plugins: ["obstacle_array_layer", "inflation_layer"]
obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true
topic: /obstacles
obstacle_timeout: 0.5
transform_tolerance: 0.2
default_obstacle_radius: 0.05
minimum_obstacle_radius: 0.02
maximum_obstacle_radius: 0.50
extra_inflation: 0.02
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.35
always_send_full_costmap: True
local_costmap_client:
ros__parameters:
use_sim_time: False
local_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
transform_tolerance: 0.5
global_frame: map
robot_base_frame: base_footprint
use_sim_time: False
rolling_window: false
width: 8
height: 8
resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02
track_unknown_space: true
plugins: ["static_layer", "obstacle_array_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
enabled: true
map_subscribe_transient_local: true
subscribe_to_updates: false
obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true
topic: /obstacles
obstacle_timeout: 0.5
transform_tolerance: 0.2
default_obstacle_radius: 0.05
minimum_obstacle_radius: 0.02
maximum_obstacle_radius: 0.50
extra_inflation: 0.02
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 2.0
inflation_radius: 0.35
always_send_full_costmap: True
global_costmap_client:
ros__parameters:
use_sim_time: False
global_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
planner_server:
ros__parameters:
planner_plugins: ["GridBased"]
use_sim_time: False
GridBased:
plugin: "nav2_smac_planner/SmacPlannerHybrid"
downsample_costmap: false
downsampling_factor: 1
tolerance: 0.15
allow_unknown: false
max_iterations: 1000000
max_on_approach_iterations: 1000
max_planning_time: 5.0
motion_model_for_search: "REEDS_SHEPP"
angle_quantization_bins: 72
analytic_expansion_ratio: 3.5
analytic_expansion_max_length: 3.0
minimum_turning_radius: 0.40
reverse_penalty: 3.0
change_penalty: 0.0
non_straight_penalty: 1.2
cost_penalty: 2.0
retrospective_penalty: 0.015
# 5 m covers the rolling planning horizon without the startup and memory
# cost of the previous 20 m (401-cell) Hybrid-A* lookup table.
lookup_table_size: 5.0
cache_obstacle_heuristic: false
viz_expansions: false
smooth_path: True
smoother:
max_iterations: 1000
w_smooth: 0.3
w_data: 0.2
tolerance: 1.0e-10
do_refinement: true
refinement_num: 2
planner_server_rclcpp_node:
ros__parameters:
use_sim_time: False
smoother_server:
ros__parameters:
use_sim_time: False
smoother_plugins: ["simple_smoother"]
simple_smoother:
plugin: "nav2_smoother::SimpleSmoother"
tolerance: 1.0e-10
max_its: 1000
do_refinement: True
behavior_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
behavior_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_behaviors/Spin"
backup:
plugin: "nav2_behaviors/BackUp"
backup_dist: 0.8
backup_speed: 0.18
wait:
plugin: "nav2_behaviors/Wait"
wait_duration: 0.5
global_frame: map
robot_base_frame: base_footprint
transform_tolerance: 0.5
use_sim_time: False
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
robot_state_publisher:
ros__parameters:
use_sim_time: False
waypoint_follower:
ros__parameters:
loop_rate: 20
use_sim_time: False
stop_on_failure: false
waypoint_task_executor_plugin: "wait_at_waypoint"
wait_at_waypoint:
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
enabled: True
waypoint_pause_duration: 200
velocity_smoother:
ros__parameters:
use_sim_time: False
smoothing_frequency: 20.0
scale_velocities: False
feedback: "OPEN_LOOP"
max_velocity: [0.5, 0.0, 2.0]
min_velocity: [-0.75, 0.0, -2.0]
max_accel: [3.73, 0.0, 3.2]
max_decel: [-1.1, 0.0, -4.5]
odom_topic: /odom_combined
odom_duration: 0.1
deadband_velocity: [0.03, 0.0, 0.03]
velocity_timeout: 1.0

View File

@@ -6,12 +6,13 @@
# global_frame leaf when a different connected odometry frame is required. # global_frame leaf when a different connected odometry frame is required.
# Global planner: Smac Hybrid A* (Reeds-Shepp) # Global planner: Smac Hybrid A* (Reeds-Shepp)
# Local controller: MPPI (Ackermann) # Local controller: MPPI (Ackermann)
# 速度快但是雷达容易掉
# ============================================================================ # ============================================================================
bt_navigator: bt_navigator:
ros__parameters: ros__parameters:
use_sim_time: False use_sim_time: False
global_frame: odom global_frame: map
robot_base_frame: base_footprint robot_base_frame: base_footprint
odom_topic: /odom_combined odom_topic: /odom_combined
bt_loop_duration: 50 bt_loop_duration: 50
@@ -70,16 +71,16 @@ bt_navigator_rclcpp_node:
controller_server: controller_server:
ros__parameters: ros__parameters:
use_sim_time: False use_sim_time: False
controller_frequency: 20.0 controller_frequency: 15.0
FollowPath: FollowPath:
plugin: "nav2_mppi_controller::MPPIController" plugin: "nav2_mppi_controller::MPPIController"
time_steps: 36 time_steps: 40
model_dt: 0.05 model_dt: 0.06666666666666666
batch_size: 1000 batch_size: 900
vx_std: 0.75 vx_std: 0.25
vy_std: 0.0 vy_std: 0.0
wz_std: 0.4 wz_std: 0.45
vx_max: 1.75 vx_max: 1.00
vx_min: -0.75 vx_min: -0.75
vy_max: 0.0 vy_max: 0.0
wz_max: 1.5 wz_max: 1.5
@@ -101,7 +102,7 @@ controller_server:
GoalCritic: GoalCritic:
enabled: true enabled: true
cost_power: 1 cost_power: 1
cost_weight: 5.0 cost_weight: 6.0
threshold_to_consider: 1.4 threshold_to_consider: 1.4
GoalAngleCritic: GoalAngleCritic:
enabled: true enabled: true
@@ -111,21 +112,21 @@ controller_server:
PreferForwardCritic: PreferForwardCritic:
enabled: false enabled: false
cost_power: 1 cost_power: 1
cost_weight: 0.0 cost_weight: 7.0
threshold_to_consider: 0.5 threshold_to_consider: 0.5
CostCritic: CostCritic:
enabled: true enabled: true
cost_power: 1 cost_power: 1
cost_weight: 3.81 cost_weight: 5.0
critical_cost: 300.0 critical_cost: 300.0
consider_footprint: true consider_footprint: true
collision_cost: 1000000.0 collision_cost: 100000.0
near_goal_distance: 1.0 near_goal_distance: 1.0
trajectory_point_step: 2 trajectory_point_step: 2
PathAlignCritic: PathAlignCritic:
enabled: true enabled: true
cost_power: 1 cost_power: 1
cost_weight: 14.0 cost_weight: 8.0
max_path_occupancy_ratio: 0.05 max_path_occupancy_ratio: 0.05
trajectory_point_step: 4 trajectory_point_step: 4
threshold_to_consider: 0.5 threshold_to_consider: 0.5
@@ -135,13 +136,13 @@ controller_server:
enabled: true enabled: true
cost_power: 1 cost_power: 1
cost_weight: 5.0 cost_weight: 5.0
offset_from_furthest: 5 offset_from_furthest: 10
threshold_to_consider: 1.4 threshold_to_consider: 1.4
PathAngleCritic: PathAngleCritic:
enabled: true enabled: true
cost_power: 1 cost_power: 1
cost_weight: 2.0 cost_weight: 2.0
offset_from_furthest: 4 offset_from_furthest: 5
threshold_to_consider: 0.5 threshold_to_consider: 0.5
max_angle_to_furthest: 1.0 max_angle_to_furthest: 1.0
forward_preference: false forward_preference: false
@@ -180,7 +181,7 @@ local_costmap:
inflation_layer: inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer" plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0 cost_scaling_factor: 3.0
inflation_radius: 0.55 inflation_radius: 0.20
always_send_full_costmap: True always_send_full_costmap: True
local_costmap_client: local_costmap_client:
ros__parameters: ros__parameters:
@@ -195,17 +196,22 @@ global_costmap:
update_frequency: 1.0 update_frequency: 1.0
publish_frequency: 1.0 publish_frequency: 1.0
transform_tolerance: 0.5 transform_tolerance: 0.5
global_frame: odom global_frame: map
robot_base_frame: base_footprint robot_base_frame: base_footprint
use_sim_time: False use_sim_time: False
rolling_window: true rolling_window: false
width: 10 width: 8
height: 10 height: 8
resolution: 0.05 resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]" footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02 footprint_padding: 0.02
track_unknown_space: false track_unknown_space: true
plugins: ["obstacle_array_layer", "inflation_layer"] plugins: ["static_layer", "obstacle_array_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
enabled: true
map_subscribe_transient_local: true
subscribe_to_updates: false
obstacle_array_layer: obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer" plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true enabled: true
@@ -218,8 +224,8 @@ global_costmap:
extra_inflation: 0.02 extra_inflation: 0.02
inflation_layer: inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer" plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0 cost_scaling_factor: 2.0
inflation_radius: 0.55 inflation_radius: 0.35
always_send_full_costmap: True always_send_full_costmap: True
global_costmap_client: global_costmap_client:
ros__parameters: ros__parameters:
@@ -236,20 +242,20 @@ planner_server:
plugin: "nav2_smac_planner/SmacPlannerHybrid" plugin: "nav2_smac_planner/SmacPlannerHybrid"
downsample_costmap: false downsample_costmap: false
downsampling_factor: 1 downsampling_factor: 1
tolerance: 0.25 tolerance: 0.15
allow_unknown: false allow_unknown: false
max_iterations: 1000000 max_iterations: 1000000
max_on_approach_iterations: 1000 max_on_approach_iterations: 1000
max_planning_time: 5.0 max_planning_time: 25.0
motion_model_for_search: "REEDS_SHEPP" motion_model_for_search: "REEDS_SHEPP"
angle_quantization_bins: 72 angle_quantization_bins: 72
analytic_expansion_ratio: 3.5 analytic_expansion_ratio: 3.5
analytic_expansion_max_length: 3.0 analytic_expansion_max_length: 3.0
minimum_turning_radius: 0.40 minimum_turning_radius: 0.40
reverse_penalty: 1.0 reverse_penalty: 1.6
change_penalty: 0.0 change_penalty: 2.0
non_straight_penalty: 1.2 non_straight_penalty: 1.2
cost_penalty: 2.0 cost_penalty: 4.0
retrospective_penalty: 0.015 retrospective_penalty: 0.015
# 5 m covers the rolling planning horizon without the startup and memory # 5 m covers the rolling planning horizon without the startup and memory
# cost of the previous 20 m (401-cell) Hybrid-A* lookup table. # cost of the previous 20 m (401-cell) Hybrid-A* lookup table.
@@ -258,7 +264,7 @@ planner_server:
viz_expansions: false viz_expansions: false
smooth_path: True smooth_path: True
smoother: smoother:
max_iterations: 1000 max_iterations: 700
w_smooth: 0.3 w_smooth: 0.3
w_data: 0.2 w_data: 0.2
tolerance: 1.0e-10 tolerance: 1.0e-10
@@ -294,7 +300,7 @@ behavior_server:
wait: wait:
plugin: "nav2_behaviors/Wait" plugin: "nav2_behaviors/Wait"
wait_duration: 0.5 wait_duration: 0.5
global_frame: odom global_frame: map
robot_base_frame: base_footprint robot_base_frame: base_footprint
transform_tolerance: 0.5 transform_tolerance: 0.5
use_sim_time: False use_sim_time: False
@@ -324,10 +330,10 @@ velocity_smoother:
smoothing_frequency: 20.0 smoothing_frequency: 20.0
scale_velocities: False scale_velocities: False
feedback: "OPEN_LOOP" feedback: "OPEN_LOOP"
max_velocity: [1.75, 0.0, 1.5] max_velocity: [1.00, 0.0, 2.0]
min_velocity: [-0.75, 0.0, -1.5] min_velocity: [-0.75, 0.0, -2.0]
max_accel: [2.5, 0.0, 3.2] max_accel: [3.73, 0.0, 3.2]
max_decel: [-2.5, 0.0, -3.2] max_decel: [-1.1, 0.0, -4.5]
odom_topic: /odom_combined odom_topic: /odom_combined
odom_duration: 0.1 odom_duration: 0.1
deadband_velocity: [0.03, 0.0, 0.03] deadband_velocity: [0.03, 0.0, 0.03]

View File

@@ -0,0 +1,339 @@
# ============================================================================
# nav2_params.yaml — Odometry-only obstacle navigation
#
# No static map, no AMCL, no SLAM.
# Both costmaps are rolling windows in odom. The launch file can rewrite every
# global_frame leaf when a different connected odometry frame is required.
# Global planner: Smac Hybrid A* (Reeds-Shepp)
# Local controller: MPPI (Ackermann)
# ============================================================================
bt_navigator:
ros__parameters:
use_sim_time: False
global_frame: map
robot_base_frame: base_footprint
odom_topic: /odom_combined
bt_loop_duration: 50
default_server_timeout: 20
# Injected by obstacle_nav2.launch.py from this package's share directory.
default_nav_to_pose_bt_xml: ""
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_smooth_path_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_back_up_action_bt_node
- nav2_drive_on_heading_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_globally_updated_goal_condition_bt_node
- nav2_is_path_valid_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_truncate_path_local_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_path_expiring_timer_condition
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
- nav2_controller_cancel_bt_node
- nav2_path_longer_on_approach_bt_node
- nav2_wait_cancel_bt_node
- nav2_spin_cancel_bt_node
- nav2_back_up_cancel_bt_node
- nav2_drive_on_heading_cancel_bt_node
bt_navigator_rclcpp_node:
ros__parameters:
use_sim_time: False
controller_server:
ros__parameters:
use_sim_time: False
controller_frequency: 20.0
FollowPath:
plugin: "nav2_mppi_controller::MPPIController"
time_steps: 36
model_dt: 0.05
batch_size: 1000
vx_std: 0.22
vy_std: 0.0
wz_std: 0.4
vx_max: 1.00
vx_min: -0.75
vy_max: 0.0
wz_max: 1.5
iteration_count: 1
temperature: 0.3
gamma: 0.015
motion_model: "Ackermann"
visualize: false
TrajectoryVisualizer:
trajectory_step: 5
time_step: 3
AckermannConstraints:
min_turning_r: 0.4
critics: ["ConstraintCritic", "CostCritic", "GoalCritic", "GoalAngleCritic", "PathAlignCritic", "PathFollowCritic", "PathAngleCritic", "PreferForwardCritic"]
ConstraintCritic:
enabled: true
cost_power: 1
cost_weight: 4.0
GoalCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
threshold_to_consider: 1.4
GoalAngleCritic:
enabled: true
cost_power: 1
cost_weight: 3.0
threshold_to_consider: 0.5
PreferForwardCritic:
enabled: false
cost_power: 1
cost_weight: 0.0
threshold_to_consider: 0.5
CostCritic:
enabled: true
cost_power: 1
cost_weight: 3.81
critical_cost: 300.0
consider_footprint: true
collision_cost: 100000.0
near_goal_distance: 1.0
trajectory_point_step: 2
PathAlignCritic:
enabled: true
cost_power: 1
cost_weight: 14.0
max_path_occupancy_ratio: 0.05
trajectory_point_step: 4
threshold_to_consider: 0.5
offset_from_furthest: 20
use_path_orientations: false
PathFollowCritic:
enabled: true
cost_power: 1
cost_weight: 5.0
offset_from_furthest: 10
threshold_to_consider: 1.4
PathAngleCritic:
enabled: true
cost_power: 1
cost_weight: 2.0
offset_from_furthest: 5
threshold_to_consider: 0.5
max_angle_to_furthest: 1.0
forward_preference: false
controller_server_rclcpp_node:
ros__parameters:
use_sim_time: False
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
transform_tolerance: 0.5
global_frame: odom
robot_base_frame: base_footprint
use_sim_time: False
rolling_window: true
width: 3
height: 3
resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02
track_unknown_space: false
plugins: ["obstacle_array_layer", "inflation_layer"]
obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true
topic: /obstacles
obstacle_timeout: 0.5
transform_tolerance: 0.2
default_obstacle_radius: 0.05
minimum_obstacle_radius: 0.02
maximum_obstacle_radius: 0.50
extra_inflation: 0.02
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.55
always_send_full_costmap: True
local_costmap_client:
ros__parameters:
use_sim_time: False
local_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
transform_tolerance: 0.5
global_frame: map
robot_base_frame: base_footprint
use_sim_time: False
rolling_window: false
width: 8
height: 8
resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02
track_unknown_space: true
plugins: ["static_layer", "obstacle_array_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
enabled: true
map_subscribe_transient_local: true
subscribe_to_updates: false
obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true
topic: /obstacles
obstacle_timeout: 0.5
transform_tolerance: 0.2
default_obstacle_radius: 0.05
minimum_obstacle_radius: 0.02
maximum_obstacle_radius: 0.50
extra_inflation: 0.02
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.55
always_send_full_costmap: True
global_costmap_client:
ros__parameters:
use_sim_time: False
global_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
planner_server:
ros__parameters:
planner_plugins: ["GridBased"]
use_sim_time: False
GridBased:
plugin: "nav2_smac_planner/SmacPlannerHybrid"
downsample_costmap: false
downsampling_factor: 1
tolerance: 0.25
allow_unknown: false
max_iterations: 1000000
max_on_approach_iterations: 1000
max_planning_time: 5.0
motion_model_for_search: "REEDS_SHEPP"
angle_quantization_bins: 72
analytic_expansion_ratio: 3.5
analytic_expansion_max_length: 3.0
minimum_turning_radius: 0.40
reverse_penalty: 1.0
change_penalty: 0.0
non_straight_penalty: 1.2
cost_penalty: 2.0
retrospective_penalty: 0.015
# 5 m covers the rolling planning horizon without the startup and memory
# cost of the previous 20 m (401-cell) Hybrid-A* lookup table.
lookup_table_size: 5.0
cache_obstacle_heuristic: false
viz_expansions: false
smooth_path: True
smoother:
max_iterations: 1000
w_smooth: 0.3
w_data: 0.2
tolerance: 1.0e-10
do_refinement: true
refinement_num: 2
planner_server_rclcpp_node:
ros__parameters:
use_sim_time: False
smoother_server:
ros__parameters:
use_sim_time: False
smoother_plugins: ["simple_smoother"]
simple_smoother:
plugin: "nav2_smoother::SimpleSmoother"
tolerance: 1.0e-10
max_its: 1000
do_refinement: True
behavior_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
behavior_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_behaviors/Spin"
backup:
plugin: "nav2_behaviors/BackUp"
backup_dist: 0.8
backup_speed: 0.18
wait:
plugin: "nav2_behaviors/Wait"
wait_duration: 0.5
global_frame: map
robot_base_frame: base_footprint
transform_tolerance: 0.5
use_sim_time: False
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
robot_state_publisher:
ros__parameters:
use_sim_time: False
waypoint_follower:
ros__parameters:
loop_rate: 20
use_sim_time: False
stop_on_failure: false
waypoint_task_executor_plugin: "wait_at_waypoint"
wait_at_waypoint:
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
enabled: True
waypoint_pause_duration: 200
velocity_smoother:
ros__parameters:
use_sim_time: False
smoothing_frequency: 20.0
scale_velocities: False
feedback: "OPEN_LOOP"
max_velocity: [1.00, 0.0, 1.5]
min_velocity: [-0.75, 0.0, -1.5]
max_accel: [2.5, 0.0, 3.2]
max_decel: [-4.5, 0.0, -4.5]
odom_topic: /odom_combined
odom_duration: 0.1
deadband_velocity: [0.03, 0.0, 0.03]
velocity_timeout: 1.0

View File

@@ -12,7 +12,7 @@
bt_navigator: bt_navigator:
ros__parameters: ros__parameters:
use_sim_time: False use_sim_time: False
global_frame: odom global_frame: map
robot_base_frame: base_footprint robot_base_frame: base_footprint
odom_topic: /odom_combined odom_topic: /odom_combined
bt_loop_duration: 50 bt_loop_duration: 50
@@ -213,17 +213,22 @@ global_costmap:
update_frequency: 5.0 update_frequency: 5.0
publish_frequency: 3.0 publish_frequency: 3.0
transform_tolerance: 0.5 transform_tolerance: 0.5
global_frame: odom global_frame: map
robot_base_frame: base_footprint robot_base_frame: base_footprint
use_sim_time: False use_sim_time: False
rolling_window: true rolling_window: false
width: 10 width: 8
height: 10 height: 8
resolution: 0.05 resolution: 0.05
footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]" footprint: "[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]"
footprint_padding: 0.02 footprint_padding: 0.02
track_unknown_space: false track_unknown_space: true
plugins: ["obstacle_array_layer", "inflation_layer"] plugins: ["static_layer", "obstacle_array_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
enabled: true
map_subscribe_transient_local: true
subscribe_to_updates: false
obstacle_array_layer: obstacle_array_layer:
plugin: "obstacle_nav2::ObstacleArrayLayer" plugin: "obstacle_nav2::ObstacleArrayLayer"
enabled: true enabled: true
@@ -311,7 +316,7 @@ behavior_server:
wait: wait:
plugin: "nav2_behaviors/Wait" plugin: "nav2_behaviors/Wait"
wait_duration: 0.5 wait_duration: 0.5
global_frame: odom global_frame: map
robot_base_frame: base_footprint robot_base_frame: base_footprint
transform_tolerance: 0.5 transform_tolerance: 0.5
use_sim_time: False use_sim_time: False

View File

@@ -22,14 +22,15 @@ from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription from launch import LaunchDescription
from launch.actions import ( from launch.actions import (
DeclareLaunchArgument, DeclareLaunchArgument,
ExecuteProcess,
GroupAction, GroupAction,
IncludeLaunchDescription, IncludeLaunchDescription,
SetEnvironmentVariable, SetEnvironmentVariable,
) )
from launch.conditions import IfCondition from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PythonExpression from launch.substitutions import FindExecutable, LaunchConfiguration, PythonExpression
from launch_ros.actions import SetRemap from launch_ros.actions import Node, SetRemap
from nav2_common.launch import RewrittenYaml from nav2_common.launch import RewrittenYaml
@@ -48,17 +49,20 @@ def generate_launch_description():
start_base = LaunchConfiguration('start_base', default='true') start_base = LaunchConfiguration('start_base', default='true')
start_lidar = LaunchConfiguration('start_lidar', default='true') start_lidar = LaunchConfiguration('start_lidar', default='true')
start_obstacle_scanner = LaunchConfiguration('start_obstacle_scanner', default='true') start_obstacle_scanner = LaunchConfiguration('start_obstacle_scanner', default='true')
start_map_publisher = LaunchConfiguration('start_map_publisher', default='true')
map_yaml_path = LaunchConfiguration('map_yaml_path')
nav2_param_path = os.path.join(pkg_dir, 'config', 'nav2_profile_10.yaml') nav2_param_path = os.path.join(pkg_dir, 'config', 'nav2_profile_10.yaml')
fastdds_profile_path = os.path.join(pkg_dir, 'config', 'fastdds_udp_only.xml')
nav_to_pose_bt_path = os.path.join( nav_to_pose_bt_path = os.path.join(
pkg_dir, 'behavior_tree', 'nav_to_pose_ackermann.xml') pkg_dir, 'behavior_tree', 'nav_to_pose_ackermann.xml')
nav_through_poses_bt_path = os.path.join(
pkg_dir, 'behavior_tree', 'nav_through_poses_ackermann.xml')
configured_params = RewrittenYaml( configured_params = RewrittenYaml(
source_file=nav2_param_path, source_file=nav2_param_path,
root_key='', root_key='',
param_rewrites={ param_rewrites={
'global_frame': global_frame,
'default_nav_to_pose_bt_xml': nav_to_pose_bt_path, 'default_nav_to_pose_bt_xml': nav_to_pose_bt_path,
'default_nav_through_poses_bt_xml': nav_through_poses_bt_path,
}, },
convert_types=True, convert_types=True,
) )
@@ -95,6 +99,37 @@ def generate_launch_description():
condition=IfCondition(start_obstacle_scanner), condition=IfCondition(start_obstacle_scanner),
) )
# ========================== Static Map Publisher ==========================
static_map_publisher_script = os.path.join(pkg_dir, 'scripts', 'static_map_publisher.py')
static_map_publisher = ExecuteProcess(
cmd=[
FindExecutable(name='python3'),
static_map_publisher_script,
'--ros-args',
'-p', ['yaml_filename:=', map_yaml_path],
'-p', 'publish_rate:=1.0',
'-p', 'map_frame:=map',
],
output='screen',
emulate_tty=True,
condition=IfCondition(start_map_publisher),
)
# ========================== Initial Pose -> TF (map->odom) ================
initial_pose_to_tf_script = os.path.join(pkg_dir, 'scripts', 'initial_pose_to_tf.py')
initial_pose_to_tf = ExecuteProcess(
cmd=[
FindExecutable(name='python3'),
initial_pose_to_tf_script,
'--ros-args',
'-p', 'odom_frame:=odom',
'-p', 'base_frame:=base_footprint',
'-p', 'map_frame:=map',
],
output='screen',
emulate_tty=True,
)
# ========================== Nav2 Navigation =============================== # ========================== Nav2 Navigation ===============================
navigation_launch = IncludeLaunchDescription( navigation_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource( PythonLaunchDescriptionSource(
@@ -140,13 +175,20 @@ def generate_launch_description():
'start_obstacle_scanner', 'start_obstacle_scanner',
default_value='true', default_value='true',
description='Also start the obstacle_scanner node'), description='Also start the obstacle_scanner node'),
DeclareLaunchArgument(
'start_map_publisher',
default_value='true',
description='Start the static map publisher node'),
DeclareLaunchArgument(
'map_yaml_path',
default_value='/home/sunrise/yiliao_ws/src/map/nav2_costmap_map.yaml',
description='Path to the map YAML file'),
SetEnvironmentVariable(
name='FASTRTPS_DEFAULT_PROFILES_FILE',
value=fastdds_profile_path),
safe_base_bringup, safe_base_bringup,
lslidar_launch, lslidar_launch,
obstacle_scanner_launch, obstacle_scanner_launch,
static_map_publisher,
initial_pose_to_tf,
navigation_launch, navigation_launch,
]) ])

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
# ============================================================================
# obstacle_nav2.launch.py
#
# Odometry-only Nav2 navigation using obstacle_scanner detections.
#
# Architecture:
# origincar_base (bringup) — chassis serial driver + EKF + TF
# lslidar_driver — lidar → /scan
# obstacle_scanner (optional) — /scan → /obstacles
# Nav2 navigation_launch — rolling costmaps + Hybrid A* + MPPI
#
# No map_server, AMCL, or slam_toolbox in the default path.
#
# Usage:
# ros2 launch obstacle_nav2 obstacle_nav2.launch.py
# ros2 launch obstacle_nav2 obstacle_nav2.launch.py enable_motion:=true
# ============================================================================
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
ExecuteProcess,
GroupAction,
IncludeLaunchDescription,
SetEnvironmentVariable,
)
from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import FindExecutable, LaunchConfiguration, PythonExpression
from launch_ros.actions import Node, SetRemap
from nav2_common.launch import RewrittenYaml
def generate_launch_description():
"""Odometry-only obstacle navigation."""
pkg_dir = get_package_share_directory('obstacle_nav2')
nav2_bringup_dir = get_package_share_directory('nav2_bringup')
# ========================== Launch Arguments ==============================
use_sim_time = LaunchConfiguration('use_sim_time', default='false')
global_frame = LaunchConfiguration('global_frame', default='odom')
use_static_map = LaunchConfiguration('use_static_map', default='false')
map_yaml = LaunchConfiguration('map_yaml', default='')
enable_motion = LaunchConfiguration('enable_motion', default='true')
start_base = LaunchConfiguration('start_base', default='true')
start_lidar = LaunchConfiguration('start_lidar', default='true')
start_obstacle_scanner = LaunchConfiguration('start_obstacle_scanner', default='true')
start_map_publisher = LaunchConfiguration('start_map_publisher', default='true')
map_yaml_path = LaunchConfiguration('map_yaml_path')
nav2_param_path = os.path.join(pkg_dir, 'config', 'nav2_profile_10.yaml')
nav_to_pose_bt_path = os.path.join(
pkg_dir, 'behavior_tree', 'nav_to_pose_ackermann.xml')
configured_params = RewrittenYaml(
source_file=nav2_param_path,
root_key='',
param_rewrites={
'default_nav_to_pose_bt_xml': nav_to_pose_bt_path,
},
convert_types=True,
)
base_cmd_vel_topic = PythonExpression([
"'/cmd_vel' if '", enable_motion,
"' == 'true' else '/cmd_vel_hardware_disabled'",
])
# ========================== Base Bringup ==================================
origincar_bringup = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[get_package_share_directory('origincar_base'),
'/launch', '/origincar_bringup.launch.py']),
condition=IfCondition(start_base),
)
safe_base_bringup = GroupAction([
SetRemap(src='cmd_vel', dst=base_cmd_vel_topic),
origincar_bringup,
])
# ========================== Lidar Driver ==================================
lslidar_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[get_package_share_directory('lslidar_driver'),
'/launch', '/lsn10_launch.py']),
condition=IfCondition(start_lidar),
)
# ========================== Obstacle Scanner ==============================
obstacle_scanner_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[get_package_share_directory('obstacle_scanner'),
'/launch', '/obstacle_scanner.launch.py']),
condition=IfCondition(start_obstacle_scanner),
)
# ========================== Static Map Publisher ==========================
static_map_publisher_script = os.path.join(pkg_dir, 'scripts', 'static_map_publisher.py')
static_map_publisher = ExecuteProcess(
cmd=[
FindExecutable(name='python3'),
static_map_publisher_script,
'--ros-args',
'-p', ['yaml_filename:=', map_yaml_path],
'-p', 'publish_rate:=1.0',
'-p', 'map_frame:=map',
],
output='screen',
emulate_tty=True,
condition=IfCondition(start_map_publisher),
)
# ========================== Initial Pose -> TF (map->odom) ================
initial_pose_to_tf_script = os.path.join(pkg_dir, 'scripts', 'initial_pose_to_tf.py')
initial_pose_to_tf = ExecuteProcess(
cmd=[
FindExecutable(name='python3'),
initial_pose_to_tf_script,
'--ros-args',
'-p', 'odom_frame:=odom',
'-p', 'base_frame:=base_footprint',
'-p', 'map_frame:=map',
],
output='screen',
emulate_tty=True,
)
# ========================== Nav2 Navigation ===============================
navigation_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[nav2_bringup_dir, '/launch', '/navigation_launch.py']),
launch_arguments={
'use_sim_time': use_sim_time,
'params_file': configured_params,
'autostart': 'true',
}.items(),
)
# ========================== Assembly ======================================
return LaunchDescription([
DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation (Gazebo) clock'),
DeclareLaunchArgument(
'global_frame',
default_value='odom',
description='Global frame for both costmaps'),
DeclareLaunchArgument(
'use_static_map',
default_value='false',
description='Reserved: enable static map + AMCL (not yet implemented)'),
DeclareLaunchArgument(
'map_yaml',
default_value='',
description='Reserved: path to map YAML file'),
DeclareLaunchArgument(
'enable_motion',
default_value='true',
description='Route Nav2 cmd_vel to the real base topic'),
DeclareLaunchArgument(
'start_base',
default_value='true',
description='Start the base driver, EKF, and robot TF publishers'),
DeclareLaunchArgument(
'start_lidar',
default_value='true',
description='Start the lidar driver'),
DeclareLaunchArgument(
'start_obstacle_scanner',
default_value='true',
description='Also start the obstacle_scanner node'),
DeclareLaunchArgument(
'start_map_publisher',
default_value='true',
description='Start the static map publisher node'),
DeclareLaunchArgument(
'map_yaml_path',
default_value='/home/sunrise/yiliao_ws/src/map/nav2_costmap_map.yaml',
description='Path to the map YAML file'),
safe_base_bringup,
lslidar_launch,
obstacle_scanner_launch,
static_map_publisher,
initial_pose_to_tf,
navigation_launch,
])

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
initial_pose_to_tf.py — /initialpose → map→odom 的 TF 变换
启动后先发布 identity TF (map 与 odom 重合) 作为回退值。
收到 /initialpose 后查 TF (odom→base_footprint),反算 map→odom 并更新。
使用: ros2 topic pub /initialpose geometry_msgs/msg/PoseWithCovarianceStamped ...
或在 RViz 中用 2D Pose Estimate 工具设定。
"""
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseWithCovarianceStamped, TransformStamped
from tf2_ros import TransformListener, Buffer, StaticTransformBroadcaster
from tf2_ros import LookupException, ConnectivityException, ExtrapolationException
class InitialPoseToTF(Node):
def __init__(self):
super().__init__('initial_pose_to_tf')
self.declare_parameter('odom_frame', 'odom')
self.declare_parameter('base_frame', 'base_footprint')
self.declare_parameter('map_frame', 'map')
self.odom_frame_ = self.get_parameter('odom_frame').value
self.base_frame_ = self.get_parameter('base_frame').value
self.map_frame_ = self.get_parameter('map_frame').value
self.tf_buffer_ = Buffer()
self.tf_listener_ = TransformListener(self.tf_buffer_, self)
self.static_broadcaster_ = StaticTransformBroadcaster(self)
self.initial_pose_sub_ = self.create_subscription(
PoseWithCovarianceStamped, '/initialpose',
self._initial_pose_callback, 10)
self.get_logger().info(
f'waiting /initialpose '
f'(map={self.map_frame_}, odom={self.odom_frame_}, base={self.base_frame_})')
# fallback identity TF after 1s
self._identity_timer = self.create_timer(1.0, self._publish_identity)
def _publish_identity(self):
self._identity_timer.cancel()
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = self.map_frame_
t.child_frame_id = self.odom_frame_
t.transform.rotation.w = 1.0
self.static_broadcaster_.sendTransform(t)
self.get_logger().info('Published identity map→odom TF (fallback)')
def _initial_pose_callback(self, msg: PoseWithCovarianceStamped):
try:
odom_to_base = self.tf_buffer_.lookup_transform(
self.odom_frame_, self.base_frame_,
rclpy.time.Time(), rclpy.duration.Duration(seconds=1.0))
except (LookupException, ConnectivityException, ExtrapolationException) as e:
self.get_logger().warn(f'TF lookup failed: {e}')
return
map_to_base = msg.pose.pose
ob_t = odom_to_base.transform.translation
ob_r = odom_to_base.transform.rotation
# q_base_odom = q_odom_base^(-1)
q_bo = (-ob_r.x, -ob_r.y, -ob_r.z, ob_r.w)
# q_map_odom = q_map_base * q_base_odom
x1, y1, z1, w1 = (
map_to_base.orientation.x, map_to_base.orientation.y,
map_to_base.orientation.z, map_to_base.orientation.w)
x2, y2, z2, w2 = q_bo
qx = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
qy = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
qz = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
qw = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
# R(q_mo) * p_odom_base
px, py, pz = ob_t.x, ob_t.y, ob_t.z
rx = (1 - 2*qy*qy - 2*qz*qz)*px + (2*qx*qy - 2*qz*qw)*py + (2*qx*qz + 2*qy*qw)*pz
ry = (2*qx*qy + 2*qz*qw)*px + (1 - 2*qx*qx - 2*qz*qz)*py + (2*qy*qz - 2*qx*qw)*pz
rz = (2*qx*qz - 2*qy*qw)*px + (2*qy*qz + 2*qx*qw)*py + (1 - 2*qx*qx - 2*qy*qy)*pz
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = self.map_frame_
t.child_frame_id = self.odom_frame_
t.transform.translation.x = map_to_base.position.x - rx
t.transform.translation.y = map_to_base.position.y - ry
t.transform.translation.z = map_to_base.position.z - rz
t.transform.rotation.x = qx
t.transform.rotation.y = qy
t.transform.rotation.z = qz
t.transform.rotation.w = qw
self.static_broadcaster_.sendTransform(t)
self.get_logger().info(
f'Updated map→odom: pos=({t.transform.translation.x:.3f},'
f' {t.transform.translation.y:.3f})')
def main():
rclpy.init()
node = InitialPoseToTF()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""static_map_publisher.py — 从 YAML+PNG 加载并发布 /map OccupancyGrid。"""
import os, math
import numpy as np
from PIL import Image
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, DurabilityPolicy
from nav_msgs.msg import OccupancyGrid, MapMetaData
from geometry_msgs.msg import Pose
import yaml
class StaticMapPublisher(Node):
def __init__(self):
super().__init__('static_map_publisher')
self.declare_parameter('yaml_filename', '')
self.declare_parameter('publish_rate', 1.0)
self.declare_parameter('map_frame', 'map')
path = self.get_parameter('yaml_filename').value
rate = self.get_parameter('publish_rate').value
frame = self.get_parameter('map_frame').value
if not path:
raise RuntimeError('yaml_filename required')
self._grid_ = self._load(path, frame)
map_qos = QoSProfile(depth=1, durability=DurabilityPolicy.TRANSIENT_LOCAL)
self._pub_ = self.create_publisher(OccupancyGrid, '/map', map_qos)
self._timer_ = self.create_timer(1.0 / max(0.1, rate), self._publish)
self.get_logger().info(f'map published on /map ({rate} Hz)')
def _load(self, yaml_path, frame):
d = os.path.dirname(os.path.abspath(yaml_path))
with open(yaml_path) as f:
cfg = yaml.safe_load(f)
img = Image.open(os.path.join(d, cfg['image'])).convert('L')
w, h = img.size
data = np.array(img, dtype=np.int32)
mode = cfg.get('mode', 'trinary')
negate = int(cfg.get('negate', 0))
occ_th = float(cfg.get('occupied_thresh', 0.65))
free_th = float(cfg.get('free_thresh', 0.196))
if mode == 'trinary':
grid = np.full_like(data, -1, dtype=np.int8)
grid[data == 0] = 100
grid[data >= 254] = 0
else:
if negate:
data = 255 - data
grid = np.full_like(data, -1, dtype=np.int8)
grid[data > occ_th * 255] = 100
grid[data < free_th * 255] = 0
grid = np.flipud(grid)
origin = cfg.get('origin', [0, 0, 0])
msg = OccupancyGrid()
msg.header.frame_id = frame
msg.info = MapMetaData()
msg.info.resolution = float(cfg.get('resolution', 0.05))
msg.info.width = w
msg.info.height = h
msg.info.origin = Pose()
msg.info.origin.position.x = float(origin[0])
msg.info.origin.position.y = float(origin[1])
yaw = float(origin[2]) if len(origin) > 2 else 0.0
msg.info.origin.orientation.w = math.cos(yaw / 2)
msg.info.origin.orientation.z = math.sin(yaw / 2)
msg.data = grid.flatten().tolist()
return msg
def _publish(self):
self._grid_.header.stamp = self.get_clock().now().to_msg()
self._pub_.publish(self._grid_)
def main():
rclpy.init()
try:
rclpy.spin(StaticMapPublisher())
except RuntimeError as e:
rclpy.get_logger('static_map_publisher').fatal(str(e))
except KeyboardInterrupt:
pass
finally:
rclpy.shutdown()
if __name__ == '__main__':
main()

View File

@@ -25,6 +25,8 @@ public:
bool finishFrame(uint64_t frame_id, TimePoint now = Clock::now()); bool finishFrame(uint64_t frame_id, TimePoint now = Clock::now());
bool cancelFrame(uint64_t frame_id, const std::string &reason, TimePoint now = Clock::now()); bool cancelFrame(uint64_t frame_id, const std::string &reason, TimePoint now = Clock::now());
bool makeSummaryLineIfDue(TimePoint now, std::string *line); bool makeSummaryLineIfDue(TimePoint now, std::string *line);
void setPrintToTerminal(bool enabled);
bool printToTerminal() const;
const std::string &logPath() const; const std::string &logPath() const;
bool isOpen() const; bool isOpen() const;
@@ -50,6 +52,7 @@ private:
TimePoint last_summary_time_; TimePoint last_summary_time_;
std::string log_path_; std::string log_path_;
std::ofstream log_file_; std::ofstream log_file_;
bool print_to_terminal_{false};
}; };
} // namespace origincar_base_logging } // namespace origincar_base_logging

View File

@@ -237,6 +237,7 @@ private:
string usart_port_name, robot_frame_id, gyro_frame_id, odom_frame_id, akm_cmd_vel, test; string usart_port_name, robot_frame_id, gyro_frame_id, odom_frame_id, akm_cmd_vel, test;
string scan_topic_, wall_config_path_, combined_odom_topic_; string scan_topic_, wall_config_path_, combined_odom_topic_;
bool publish_tf_; bool publish_tf_;
bool scan_odom_timing_to_terminal_;
double odom_pose_cov_x_, odom_pose_cov_y_, odom_pose_cov_yaw_; double odom_pose_cov_x_, odom_pose_cov_y_, odom_pose_cov_yaw_;
int wall_scan_stride_; int wall_scan_stride_;
std::string cmd_vel; std::string cmd_vel;

View File

@@ -13,6 +13,23 @@ struct WallKalmanNoise
double theta; double theta;
}; };
enum class WallScanTrust
{
Reject,
Weak,
Medium,
High,
};
struct WallScanUpdatePolicy
{
bool accept;
WallScanTrust trust;
WallKalmanNoise noise;
double innovation_distance;
std::string reason;
};
class WallKalmanFilter class WallKalmanFilter
{ {
public: public:
@@ -31,6 +48,11 @@ private:
WallKalmanNoise process_noise_per_second_; WallKalmanNoise process_noise_per_second_;
}; };
WallScanUpdatePolicy chooseWallScanUpdatePolicy(const Pose2D &filter_pose,
const Pose2D &scan_pose,
const LocalizationQuality &quality,
bool stationary);
} // namespace origincar_wall } // namespace origincar_wall
#endif // ORIGINCAR_BASE_WALL_KALMAN_FILTER_HPP_ #endif // ORIGINCAR_BASE_WALL_KALMAN_FILTER_HPP_

View File

@@ -8,7 +8,7 @@ def generate_launch_description():
'serial_baud_rate': 921600, 'serial_baud_rate': 921600,
'serial_read_timeout_ms': 20, 'serial_read_timeout_ms': 20,
'tx_period_ms': 20, 'tx_period_ms': 20,
'cmd_watchdog_timeout_ms': 150, 'cmd_watchdog_timeout_ms': 500,
'control_period_ms': 50, 'control_period_ms': 50,
'robot_frame_id': 'base_footprint', 'robot_frame_id': 'base_footprint',
'odom_frame_id': 'odom', 'odom_frame_id': 'odom',

View File

@@ -164,6 +164,16 @@ bool ScanOdomTimingLogger::makeSummaryLineIfDue(TimePoint now, std::string *line
return true; return true;
} }
void ScanOdomTimingLogger::setPrintToTerminal(bool enabled)
{
print_to_terminal_ = enabled;
}
bool ScanOdomTimingLogger::printToTerminal() const
{
return print_to_terminal_;
}
const std::string &ScanOdomTimingLogger::logPath() const const std::string &ScanOdomTimingLogger::logPath() const
{ {
return log_path_; return log_path_;

View File

@@ -307,6 +307,10 @@ void origincar_base::Apply_Wall_Update()
void origincar_base::Print_Timing_Log_If_Due() void origincar_base::Print_Timing_Log_If_Due()
{ {
if (!scan_odom_timing_to_terminal_)
{
return;
}
std::string timing_summary; std::string timing_summary;
if (scan_odom_timing_logger_.makeSummaryLineIfDue(std::chrono::steady_clock::now(), &timing_summary)) if (scan_odom_timing_logger_.makeSummaryLineIfDue(std::chrono::steady_clock::now(), &timing_summary))
{ {
@@ -639,6 +643,7 @@ origincar_base::origincar_base()
this->declare_parameter<std::string>("scan_topic", "/scan"); this->declare_parameter<std::string>("scan_topic", "/scan");
this->declare_parameter<std::string>("wall_config_path", "/home/sunrise/yiliao_ws/src/origincar_base/config/wall_fit.json"); this->declare_parameter<std::string>("wall_config_path", "/home/sunrise/yiliao_ws/src/origincar_base/config/wall_fit.json");
this->declare_parameter<bool>("publish_tf", true); this->declare_parameter<bool>("publish_tf", true);
this->declare_parameter<bool>("scan_odom_timing_to_terminal", false);
this->declare_parameter<int>("wall_scan_stride", 2); this->declare_parameter<int>("wall_scan_stride", 2);
this->declare_parameter<double>("laser_x", 0.0); this->declare_parameter<double>("laser_x", 0.0);
this->declare_parameter<double>("laser_y", 0.0); this->declare_parameter<double>("laser_y", 0.0);
@@ -665,6 +670,7 @@ origincar_base::origincar_base()
this->get_parameter("scan_topic", scan_topic_); this->get_parameter("scan_topic", scan_topic_);
this->get_parameter("wall_config_path", wall_config_path_); this->get_parameter("wall_config_path", wall_config_path_);
this->get_parameter("publish_tf", publish_tf_); this->get_parameter("publish_tf", publish_tf_);
this->get_parameter("scan_odom_timing_to_terminal", scan_odom_timing_to_terminal_);
this->get_parameter("wall_scan_stride", wall_scan_stride_); this->get_parameter("wall_scan_stride", wall_scan_stride_);
laser_pose_ = { laser_pose_ = {
this->get_parameter("laser_x").as_double(), this->get_parameter("laser_x").as_double(),
@@ -702,6 +708,7 @@ origincar_base::origincar_base()
0.0, 0.0,
}; };
wall_filter_ = std::make_unique<::origincar_wall::WallKalmanFilter>(initial_pose); wall_filter_ = std::make_unique<::origincar_wall::WallKalmanFilter>(initial_pose);
scan_odom_timing_logger_.setPrintToTerminal(scan_odom_timing_to_terminal_);
Robot_Pos.X = static_cast<float>(initial_pose.x); Robot_Pos.X = static_cast<float>(initial_pose.x);
Robot_Pos.Y = static_cast<float>(initial_pose.y); Robot_Pos.Y = static_cast<float>(initial_pose.y);
Robot_Pos.Z = static_cast<float>(initial_pose.theta); Robot_Pos.Z = static_cast<float>(initial_pose.theta);

View File

@@ -6,6 +6,24 @@
namespace origincar_wall namespace origincar_wall
{ {
namespace
{
WallKalmanNoise highTrustNoise()
{
return {0.015, 0.015, 0.025};
}
WallKalmanNoise mediumTrustNoise()
{
return {0.045, 0.045, 0.060};
}
WallKalmanNoise weakTrustNoise()
{
return {0.10, 0.10, 0.12};
}
} // namespace
WallKalmanFilter::WallKalmanFilter(const Pose2D &initial_pose) WallKalmanFilter::WallKalmanFilter(const Pose2D &initial_pose)
: pose_(initial_pose), : pose_(initial_pose),
covariance_{0.10, 0.10, 0.10}, covariance_{0.10, 0.10, 0.10},
@@ -63,4 +81,75 @@ WallKalmanNoise WallKalmanFilter::covariance() const
return covariance_; return covariance_;
} }
WallScanUpdatePolicy chooseWallScanUpdatePolicy(const Pose2D &filter_pose,
const Pose2D &scan_pose,
const LocalizationQuality &quality,
bool stationary)
{
WallScanUpdatePolicy policy{false, WallScanTrust::Reject, weakTrustNoise(), 0.0, ""};
policy.innovation_distance = std::hypot(scan_pose.x - filter_pose.x, scan_pose.y - filter_pose.y);
if (!quality.has_mean_error)
{
policy.reason = "missing mean error";
return policy;
}
if (quality.wall_count < 3)
{
policy.reason = "too few walls";
return policy;
}
if (quality.mean_error > 0.15)
{
policy.reason = "mean error too high";
return policy;
}
if (stationary && policy.innovation_distance > 0.04)
{
policy.reason = "stationary innovation too large";
return policy;
}
if (!stationary && policy.innovation_distance > 0.15)
{
policy.reason = "motion innovation too large";
return policy;
}
if (quality.wall_count == 4 && quality.mean_error < 0.05)
{
policy.accept = true;
policy.trust = stationary ? WallScanTrust::Medium : WallScanTrust::High;
policy.noise = stationary ? mediumTrustNoise() : highTrustNoise();
policy.reason = stationary ? "stationary high trust downgraded to medium" : "high trust";
return policy;
}
if (quality.wall_count >= 3 && quality.mean_error < 0.10)
{
policy.accept = true;
policy.trust = stationary ? WallScanTrust::Weak : WallScanTrust::Medium;
policy.noise = stationary ? weakTrustNoise() : mediumTrustNoise();
policy.reason = stationary ? "stationary medium trust downgraded to weak" : "medium trust";
return policy;
}
if (quality.wall_count >= 3 && quality.mean_error <= 0.15)
{
if (stationary)
{
policy.reason = "stationary weak trust rejected";
return policy;
}
policy.accept = true;
policy.trust = WallScanTrust::Weak;
policy.noise = weakTrustNoise();
policy.reason = "weak trust";
return policy;
}
policy.reason = "rejected by policy";
return policy;
}
} // namespace origincar_wall } // namespace origincar_wall

View File

@@ -85,6 +85,15 @@ void testSummaryPrintIsThrottledToOneHz()
require(!logger.makeSummaryLineIfDue(start + std::chrono::milliseconds(1500), &summary), require(!logger.makeSummaryLineIfDue(start + std::chrono::milliseconds(1500), &summary),
"summary should be throttled after printing"); "summary should be throttled after printing");
} }
void testTerminalOutputDefaultsToOff()
{
const auto root = makeTempDir();
origincar_base_logging::ScanOdomTimingLogger logger("origincar_base", root);
require(!logger.printToTerminal(), "terminal output should default to off");
logger.setPrintToTerminal(true);
require(logger.printToTerminal(), "terminal output should be enabled");
}
} // namespace } // namespace
int main() int main()
@@ -93,6 +102,7 @@ int main()
{ {
testFinishedFramesAreWrittenImmediatelyAndStatsAccumulate(); testFinishedFramesAreWrittenImmediatelyAndStatsAccumulate();
testSummaryPrintIsThrottledToOneHz(); testSummaryPrintIsThrottledToOneHz();
testTerminalOutputDefaultsToOff();
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {

View File

@@ -60,6 +60,69 @@ void testYawCorrectionUsesWrappedInnovation()
require(std::fabs(filter.pose().theta) > 175.0 * kPi / 180.0, "yaw should stay near wrap boundary"); require(std::fabs(filter.pose().theta) > 175.0 * kPi / 180.0, "yaw should stay near wrap boundary");
} }
void testScanPolicyGradesByWallCountAndMeanError()
{
const origincar_wall::Pose2D filter_pose{1.0, 1.0, 0.0};
const origincar_wall::Pose2D scan_pose{1.02, 1.0, 0.0};
const auto high = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, scan_pose, {4, 0.04, true}, false);
require(high.accept, "4 walls with low error should be accepted");
require(high.trust == origincar_wall::WallScanTrust::High, "4 walls with low error should be high trust");
requireNear(high.noise.x, 0.015, 1e-12, "high trust x noise");
const auto medium = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, scan_pose, {3, 0.08, true}, false);
require(medium.accept, "3 walls with medium error should be accepted");
require(medium.trust == origincar_wall::WallScanTrust::Medium, "3 walls with medium error should be medium trust");
requireNear(medium.noise.x, 0.045, 1e-12, "medium trust x noise");
const auto weak = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, scan_pose, {3, 0.12, true}, false);
require(weak.accept, "3 walls with weak error should be accepted as weak");
require(weak.trust == origincar_wall::WallScanTrust::Weak, "3 walls with weak error should be weak trust");
requireNear(weak.noise.x, 0.10, 1e-12, "weak trust x noise");
}
void testScanPolicyRejectsPoorOrJumpingMeasurements()
{
const origincar_wall::Pose2D filter_pose{1.0, 1.0, 0.0};
const auto too_few_walls = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, {1.01, 1.0, 0.0}, {2, 0.04, true}, false);
require(!too_few_walls.accept, "fewer than 3 walls should be rejected");
const auto too_noisy = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, {1.01, 1.0, 0.0}, {4, 0.16, true}, false);
require(!too_noisy.accept, "mean error above 0.15 should be rejected");
const auto stationary_jump = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, {1.06, 1.0, 0.0}, {4, 0.03, true}, true);
require(!stationary_jump.accept, "stationary scan jump above 0.04m should be rejected");
}
void testScanPolicyDowngradesWhileStationary()
{
const origincar_wall::Pose2D filter_pose{1.0, 1.0, 0.0};
const origincar_wall::Pose2D scan_pose{1.02, 1.0, 0.0};
const auto downgraded_high = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, scan_pose, {4, 0.04, true}, true);
require(downgraded_high.accept, "stationary high trust scan should still be accepted");
require(downgraded_high.trust == origincar_wall::WallScanTrust::Medium,
"stationary high trust scan should downgrade to medium");
const auto downgraded_medium = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, scan_pose, {3, 0.08, true}, true);
require(downgraded_medium.accept, "stationary medium trust scan should still be accepted");
require(downgraded_medium.trust == origincar_wall::WallScanTrust::Weak,
"stationary medium trust scan should downgrade to weak");
const auto downgraded_weak = origincar_wall::chooseWallScanUpdatePolicy(
filter_pose, scan_pose, {3, 0.12, true}, true);
require(!downgraded_weak.accept, "stationary weak trust scan should be rejected");
}
} // namespace } // namespace
int main() int main()
@@ -69,6 +132,9 @@ int main()
testPredictUsesWheelVelocityAndImuYawRate(); testPredictUsesWheelVelocityAndImuYawRate();
testScanUpdateTrustsLowNoiseMoreThanHighNoise(); testScanUpdateTrustsLowNoiseMoreThanHighNoise();
testYawCorrectionUsesWrappedInnovation(); testYawCorrectionUsesWrappedInnovation();
testScanPolicyGradesByWallCountAndMeanError();
testScanPolicyRejectsPoorOrJumpingMeasurements();
testScanPolicyDowngradesWhileStationary();
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {

View File

@@ -3,7 +3,7 @@
## 项目用途 ## 项目用途
- 这是 RDKx5 赛车机器人的 ROS2 Humble 工作区。 - 这是 RDKx5 赛车机器人的 ROS2 Humble 工作区。
- `src/racing_control` 是比赛总调度包。它只负责调度已有的感知、导航、VLM、TTS 和轨迹保护节点,不在包内重新实现这些功能。 - `src/racing_control` 是比赛总调度包。它只负责调度已有的感知、导航、VLM、TTS 和备用轨迹保护节点,不在包内重新实现这些功能。
## 环境准备 ## 环境准备
@@ -25,8 +25,9 @@
- `/sign4return` 是共享的 `std_msgs/msg/Int32` 控制话题: - `/sign4return` 是共享的 `std_msgs/msg/Int32` 控制话题:
`0` 开启二维码检测,`5` 关闭二维码检测,`9` 触发 VLM 拍照识别,`10` 切换普通 Nav2 参数,`11` 切换任务二 Nav2 参数。 `0` 开启二维码检测,`5` 关闭二维码检测,`9` 触发 VLM 拍照识别,`10` 切换普通 Nav2 参数,`11` 切换任务二 Nav2 参数。
- `obstacle_nav2/nav2_profile_tuner` 监听 `/sign4return`,并根据 10 或 11 应用对应的 Nav2 profile。 - `obstacle_nav2/nav2_profile_tuner` 监听 `/sign4return`,并根据 10 或 11 应用对应的 Nav2 profile。
- `obstacle_nav2/trajectory_guard_node` 订阅 `/trajectory_guard/input_path`;当 `execute_follow_path` 为 true 时,它会把保护后的路径发送给 Nav2 `/follow_path` - `obstacle_nav2/trajectory_guard_node` 订阅 `/trajectory_guard/input_path`,属于备用链路;默认比赛流程不依赖它
- 比赛总控使用 Nav2 actions`/navigate_to_pose``/compute_path_through_poses``/follow_path`比赛点位应保持为可通过 ROS 参数配置 - 比赛总控使用 Nav2 actions`/navigate_to_pose``/compute_path_through_poses``/follow_path`当前默认不走 `trajectory_guard`
- 当前 route 执行默认启用动态重规划和短倒车恢复:距离 segment 终点大于 `dynamic_replan_stop_distance` 时按 `dynamic_replan_interval_sec` 重算路径FollowPath/planner 连续失败会短倒车恢复,默认 `-0.2m/s``0.04m``0.2s` 停止,最多 `max_recovery_attempts: 2`
## 外部系统 ## 外部系统

View File

@@ -3,8 +3,10 @@ racing_control:
# Startup # Startup
auto_start: false auto_start: false
frame_id: odom frame_id: odom
use_post_qr_pose: true use_post_qr_pose: false
enable_vlm_image_relay: false enable_vlm_image_relay: false
enable_dynamic_replanning: true
enable_recovery: true
vlm_image_input_topic: /image vlm_image_input_topic: /image
vlm_image_output_topic: /vlm_image vlm_image_output_topic: /vlm_image
@@ -13,6 +15,7 @@ racing_control:
qr_result_topic: /qr_results qr_result_topic: /qr_results
vlm_result_topic: /vlm_result vlm_result_topic: /vlm_result
odom_topic: /odom_combined odom_topic: /odom_combined
recovery_cmd_vel_topic: /cmd_vel
trajectory_guard_input_topic: /trajectory_guard/input_path trajectory_guard_input_topic: /trajectory_guard/input_path
# Nav2 actions and plugin IDs # Nav2 actions and plugin IDs
@@ -22,16 +25,23 @@ racing_control:
planner_id: GridBased planner_id: GridBased
controller_id: FollowPath controller_id: FollowPath
goal_checker_id: "" goal_checker_id: ""
use_trajectory_guard: true use_trajectory_guard: false
# Timeouts and settling waits, in seconds # Timeouts and settling waits, in seconds
navigation_timeout_sec: 120.0 navigation_timeout_sec: 120.0
path_planning_timeout_sec: 30.0 path_planning_timeout_sec: 30.0
circle_timeout_sec: 120.0 circle_timeout_sec: 120.0
qr_result_timeout_sec: 8.0 qr_result_timeout_sec: 20.0
profile_switch_wait_sec: 1.0 profile_switch_wait_sec: 1.0
post_qr_wait_sec: 1.0 post_qr_wait_sec: 1.0
vlm_capture_wait_sec: 0.5 vlm_capture_wait_sec: 0.5
dynamic_replan_interval_sec: 1.0
dynamic_replan_stop_distance: 0.5
dynamic_replan_max_consecutive_failures: 3
recovery_backup_speed: -0.2
recovery_backup_distance: 0.04
recovery_backup_timeout_sec: 0.2
max_recovery_attempts: 2
circle_goal_tolerance: 0.30 circle_goal_tolerance: 0.30
# VLM capture: # VLM capture:
@@ -48,61 +58,36 @@ racing_control:
sign_profile_task2: 11 sign_profile_task2: 11
# Pose parameters are flat x/y/yaw-radians triples in frame_id. # Pose parameters are flat x/y/yaw-radians triples in frame_id.
qr_pose: [4.3860322643582883, 1.2934897914487595, 0.94658891138326606] qr_pose: [4.333107888975101, 1.028867429995691, 1.1383894869544392]
post_qr_pose: [4.4004663023808863, 0.46113350031559491, 1.7514152174101529] post_qr_pose: [2.504811372926262, 2.2798075935366624, 1.520115031774562]
entry_pose: [2.499999919243812, 2.1932039710724882, 1.5174118916273021] entry_pose: [2.504811372926262, 2.2798075935366624, 1.520115031774562]
# The route's Nth waypoint is used as the VLM capture point. # The route's Nth waypoint is used as the VLM capture point.
# With the saved JSON files, the 4th point is goal_011 for main_1 # For the current point set, the 2nd waypoint is the VLM point.
# and goal_004 for main_2. vlm_waypoint_number: 2
vlm_waypoint_number: 4
# main_1.json: clockwise route, excluding home. # main_1.json: clockwise route, excluding home.
clockwise_waypoints: clockwise_waypoints:
- 1.542550031688728 - 0.7534956931109804
- 3.1698990676859604 - 3.766501234923627
- 3.1415926535897918 - 1.5592359900104606
- 0.71981664792046363 - 3.8567885105264077
- 3.7665012349236271 - 4.329424195458402
- 1.5495230091246759 - 0.03997856199797794
- 1.3164186536457545 - 2.5144339572664096
- 4.3294241954584018 - 2.4337692660037766
- -0.016392199787162172 - -1.6101462328054222
- 3.659525047016253 clockwise_home_pose: [0.536986899408154, 0.1772662932069835, -1.652041913138445]
- 4.324612741775951
- 4.6883147711881255e-16
- 4.2320702688664218
- 3.7761238192637752
- -1.5784882007253702
- 3.510374505206836
- 3.1650879370282627
- -3.0916338672023889
- 2.5288679952890067
- 2.4578258883665218
- -1.6718998381125814
clockwise_home_pose: [0.51774173072785878, 0.17726629320698351, -2.2032146659725105]
# main_2.json: counterclockwise route, excluding home. # main_2.json: counterclockwise route, excluding home.
counterclockwise_waypoints: counterclockwise_waypoints:
- 3.3612239633974195 - 4.2128251001861265
- 3.1747105213684104 - 3.7376334819031842
- 0.010869470292991894 - 1.5450283923222128
- 4.2272591382087246 - 1.1720794040064113
- 3.7280108975630366 - 4.319801449605877
- 1.5707963267948966 - 3.1112984779040144
- 3.4237709231207547 - 2.4951887885861144
- 4.3486693641386971 - 2.318297930897253
- 3.123412893984987 - -1.539556591616333
- 1.2731168626027138 counterclockwise_home_pose: [0.536986899408154, 0.1772662932069835, -1.652041913138445]
- 4.334235487628475
- -3.1165979438417697
- 0.74868440094090649
- 3.7376334819031838
- -1.5593026199905524
- 1.3933994898793114
- 3.1795219750508603
- -0.01136349401017399
- 2.4663210355656715
- 2.4145240973234814
- -1.5556465256578393
counterclockwise_home_pose: [0.51774173072785878, 0.15802112452668779, -2.1396781415005068]

View File

@@ -86,6 +86,60 @@ inline bool shouldPublishVlmImageFrame(
return enable_vlm_image_relay && has_latest_image; return enable_vlm_image_relay && has_latest_image;
} }
template<typename WaitForActionServerFn>
inline bool retryActionServerWait(
WaitForActionServerFn && wait_for_action_server,
const int retry_count)
{
for (int attempt = 0; attempt <= retry_count; ++attempt) {
if (wait_for_action_server()) {
return true;
}
}
return false;
}
inline bool shouldRetryNavigateGoal(const bool succeeded, const int retries_remaining)
{
return !succeeded && retries_remaining > 0;
}
inline int defaultStartupQrEnableRepeats()
{
return 5;
}
inline int defaultStartupQrEnableIntervalMs()
{
return 200;
}
inline bool shouldDynamicReplan(
const bool enabled,
const bool replan_in_flight,
const double distance_to_goal,
const double stop_distance,
const double elapsed_since_last_replan,
const double replan_interval)
{
return enabled && !replan_in_flight && distance_to_goal > stop_distance &&
elapsed_since_last_replan >= replan_interval;
}
inline bool recoveryBackupComplete(
const double backup_distance,
const double target_distance,
const double elapsed_sec,
const double timeout_sec)
{
return backup_distance >= target_distance || elapsed_sec >= timeout_sec;
}
inline bool defaultUseTrajectoryGuard()
{
return false;
}
inline geometry_msgs::msg::PoseStamped poseFromXYYaw( inline geometry_msgs::msg::PoseStamped poseFromXYYaw(
const double x, const double y, const double yaw, const std::string & frame_id) const double x, const double y, const double yaw, const std::string & frame_id)
{ {

View File

@@ -203,7 +203,7 @@ private:
auto_start_ = declare_parameter<bool>("auto_start", false); auto_start_ = declare_parameter<bool>("auto_start", false);
use_trajectory_guard_ = declare_parameter<bool>("use_trajectory_guard", true); use_trajectory_guard_ = declare_parameter<bool>("use_trajectory_guard", true);
use_post_qr_pose_ = declare_parameter<bool>("use_post_qr_pose", true); use_post_qr_pose_ = declare_parameter<bool>("use_post_qr_pose", false);
enable_vlm_image_relay_ = declare_parameter<bool>("enable_vlm_image_relay", false); enable_vlm_image_relay_ = declare_parameter<bool>("enable_vlm_image_relay", false);
vlm_image_input_topic_ = declare_parameter<std::string>("vlm_image_input_topic", "/image"); vlm_image_input_topic_ = declare_parameter<std::string>("vlm_image_input_topic", "/image");
vlm_image_output_topic_ = vlm_image_output_topic_ =
@@ -912,8 +912,8 @@ private:
Stage stage_{Stage::Idle}; Stage stage_{Stage::Idle};
bool race_started_{false}; bool race_started_{false};
bool auto_start_{false}; bool auto_start_{false};
bool use_trajectory_guard_{true}; bool use_trajectory_guard_{false};
bool use_post_qr_pose_{true}; bool use_post_qr_pose_{false};
bool enable_vlm_image_relay_{false}; bool enable_vlm_image_relay_{false};
bool qr_detection_disabled_{false}; bool qr_detection_disabled_{false};
rclcpp::Time race_start_{0, 0, RCL_ROS_TIME}; rclcpp::Time race_start_{0, 0, RCL_ROS_TIME};
@@ -950,7 +950,7 @@ private:
int sign_vlm_trigger_{9}; int sign_vlm_trigger_{9};
int sign_profile_normal_{10}; int sign_profile_normal_{10};
int sign_profile_task2_{11}; int sign_profile_task2_{11};
int vlm_waypoint_number_{4}; int vlm_waypoint_number_{2};
geometry_msgs::msg::PoseStamped qr_pose_; geometry_msgs::msg::PoseStamped qr_pose_;
geometry_msgs::msg::PoseStamped post_qr_pose_; geometry_msgs::msg::PoseStamped post_qr_pose_;

View File

@@ -2,6 +2,7 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cstdint>
#include <cmath> #include <cmath>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -17,6 +18,7 @@
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
#include "geometry_msgs/msg/twist.hpp"
#include "nav2_msgs/action/compute_path_through_poses.hpp" #include "nav2_msgs/action/compute_path_through_poses.hpp"
#include "nav2_msgs/action/follow_path.hpp" #include "nav2_msgs/action/follow_path.hpp"
#include "nav2_msgs/action/navigate_to_pose.hpp" #include "nav2_msgs/action/navigate_to_pose.hpp"
@@ -135,6 +137,9 @@ public:
loadParameters(); loadParameters();
sign_pub_ = create_publisher<std_msgs::msg::Int32>(sign_topic_, 10); sign_pub_ = create_publisher<std_msgs::msg::Int32>(sign_topic_, 10);
startStartupQrEnablePublisher();
recovery_cmd_vel_pub_ = create_publisher<geometry_msgs::msg::Twist>(
recovery_cmd_vel_topic_, 10);
guard_path_pub_ = create_publisher<nav_msgs::msg::Path>(guard_input_topic_, 1); guard_path_pub_ = create_publisher<nav_msgs::msg::Path>(guard_input_topic_, 1);
if (enable_vlm_image_relay_) { if (enable_vlm_image_relay_) {
vlm_image_pub_ = vlm_image_pub_ =
@@ -189,6 +194,8 @@ private:
qr_result_topic_ = declare_parameter<std::string>("qr_result_topic", "/qr_results"); qr_result_topic_ = declare_parameter<std::string>("qr_result_topic", "/qr_results");
vlm_result_topic_ = declare_parameter<std::string>("vlm_result_topic", "/vlm_result"); vlm_result_topic_ = declare_parameter<std::string>("vlm_result_topic", "/vlm_result");
odom_topic_ = declare_parameter<std::string>("odom_topic", "/odom_combined"); odom_topic_ = declare_parameter<std::string>("odom_topic", "/odom_combined");
recovery_cmd_vel_topic_ =
declare_parameter<std::string>("recovery_cmd_vel_topic", "/cmd_vel");
navigate_action_ = declare_parameter<std::string>("navigate_action", "/navigate_to_pose"); navigate_action_ = declare_parameter<std::string>("navigate_action", "/navigate_to_pose");
compute_path_action_ = compute_path_action_ =
declare_parameter<std::string>("compute_path_action", "/compute_path_through_poses"); declare_parameter<std::string>("compute_path_action", "/compute_path_through_poses");
@@ -202,9 +209,12 @@ private:
goal_checker_id_ = declare_parameter<std::string>("goal_checker_id", ""); goal_checker_id_ = declare_parameter<std::string>("goal_checker_id", "");
auto_start_ = declare_parameter<bool>("auto_start", false); auto_start_ = declare_parameter<bool>("auto_start", false);
use_trajectory_guard_ = declare_parameter<bool>("use_trajectory_guard", true); use_trajectory_guard_ = declare_parameter<bool>(
use_post_qr_pose_ = declare_parameter<bool>("use_post_qr_pose", true); "use_trajectory_guard", defaultUseTrajectoryGuard());
use_post_qr_pose_ = declare_parameter<bool>("use_post_qr_pose", false);
enable_vlm_image_relay_ = declare_parameter<bool>("enable_vlm_image_relay", false); enable_vlm_image_relay_ = declare_parameter<bool>("enable_vlm_image_relay", false);
enable_dynamic_replanning_ = declare_parameter<bool>("enable_dynamic_replanning", true);
enable_recovery_ = declare_parameter<bool>("enable_recovery", true);
vlm_image_input_topic_ = declare_parameter<std::string>("vlm_image_input_topic", "/image"); vlm_image_input_topic_ = declare_parameter<std::string>("vlm_image_input_topic", "/image");
vlm_image_output_topic_ = vlm_image_output_topic_ =
declare_parameter<std::string>("vlm_image_output_topic", "/vlm_image"); declare_parameter<std::string>("vlm_image_output_topic", "/vlm_image");
@@ -215,9 +225,20 @@ private:
profile_switch_wait_sec_ = declare_parameter<double>("profile_switch_wait_sec", 1.0); profile_switch_wait_sec_ = declare_parameter<double>("profile_switch_wait_sec", 1.0);
post_qr_wait_sec_ = declare_parameter<double>("post_qr_wait_sec", 1.0); post_qr_wait_sec_ = declare_parameter<double>("post_qr_wait_sec", 1.0);
vlm_capture_wait_sec_ = declare_parameter<double>("vlm_capture_wait_sec", 0.5); vlm_capture_wait_sec_ = declare_parameter<double>("vlm_capture_wait_sec", 0.5);
dynamic_replan_interval_sec_ =
declare_parameter<double>("dynamic_replan_interval_sec", 1.0);
dynamic_replan_stop_distance_ =
declare_parameter<double>("dynamic_replan_stop_distance", 0.5);
recovery_backup_speed_ = declare_parameter<double>("recovery_backup_speed", -0.2);
recovery_backup_distance_ = declare_parameter<double>("recovery_backup_distance", 0.04);
recovery_backup_timeout_sec_ =
declare_parameter<double>("recovery_backup_timeout_sec", 0.2);
pass_through_vlm_trigger_radius_ = pass_through_vlm_trigger_radius_ =
declare_parameter<double>("pass_through_vlm_trigger_radius", 0.35); declare_parameter<double>("pass_through_vlm_trigger_radius", 0.35);
circle_goal_tolerance_ = declare_parameter<double>("circle_goal_tolerance", 0.30); circle_goal_tolerance_ = declare_parameter<double>("circle_goal_tolerance", 0.30);
dynamic_replan_max_consecutive_failures_ =
declare_parameter<int>("dynamic_replan_max_consecutive_failures", 3);
max_recovery_attempts_ = declare_parameter<int>("max_recovery_attempts", 2);
const auto vlm_capture_mode = const auto vlm_capture_mode =
declare_parameter<std::string>("vlm_capture_mode", "stop"); declare_parameter<std::string>("vlm_capture_mode", "stop");
@@ -232,29 +253,30 @@ private:
sign_profile_normal_ = declare_parameter<int>("sign_profile_normal", 10); sign_profile_normal_ = declare_parameter<int>("sign_profile_normal", 10);
sign_profile_task2_ = declare_parameter<int>("sign_profile_task2", 11); sign_profile_task2_ = declare_parameter<int>("sign_profile_task2", 11);
qr_pose_ = singlePoseFromParameter("qr_pose", {0.80, 0.20, 0.0}); qr_pose_ = singlePoseFromParameter(
entry_pose_ = singlePoseFromParameter("entry_pose", {1.20, 0.20, 0.0}); "qr_pose", {4.333107888975101, 1.028867429995691, 1.1383894869544392});
post_qr_pose_ = singlePoseFromParameter("post_qr_pose", {1.20, 0.20, 0.0}); entry_pose_ = singlePoseFromParameter(
vlm_waypoint_number_ = declare_parameter<int>("vlm_waypoint_number", 4); "entry_pose", {2.504811372926262, 2.2798075935366624, 1.520115031774562});
post_qr_pose_ = singlePoseFromParameter(
"post_qr_pose", {2.504811372926262, 2.2798075935366624, 1.520115031774562});
vlm_waypoint_number_ = declare_parameter<int>("vlm_waypoint_number", 2);
const auto clockwise_defaults = std::vector<double>{ const auto clockwise_defaults = std::vector<double>{
1.20, 0.80, 1.5708, 0.7534956931109804, 3.766501234923627, 1.5592359900104606,
2.20, 0.80, 0.0, 3.8567885105264077, 4.329424195458402, 0.03997856199797794,
2.20, 1.40, 1.5708, 2.5144339572664096, 2.4337692660037766, -1.6101462328054222};
1.20, 1.40, 3.1416,
1.20, 0.80, -1.5708};
const auto counterclockwise_defaults = std::vector<double>{ const auto counterclockwise_defaults = std::vector<double>{
1.20, 0.80, -1.5708, 4.2128251001861265, 3.7376334819031842, 1.5450283923222128,
1.20, 1.40, 3.1416, 1.1720794040064113, 4.319801449605877, 3.1112984779040144,
2.20, 1.40, 1.5708, 2.4951887885861144, 2.318297930897253, -1.539556591616333};
2.20, 0.80, 0.0,
1.20, 0.80, 1.5708};
clockwise_route_.label = "顺时针"; clockwise_route_.label = "顺时针";
clockwise_route_.waypoints = posesFromFlatDoubles( clockwise_route_.waypoints = posesFromFlatDoubles(
declare_parameter<std::vector<double>>("clockwise_waypoints", clockwise_defaults), frame_id_); declare_parameter<std::vector<double>>("clockwise_waypoints", clockwise_defaults), frame_id_);
clockwise_route_.home_pose = clockwise_route_.home_pose =
singlePoseFromParameter("clockwise_home_pose", {0.54, 0.20, 0.0}); singlePoseFromParameter(
"clockwise_home_pose", {0.536986899408154, 0.1772662932069835,
-1.652041913138445});
counterclockwise_route_.label = "逆时针"; counterclockwise_route_.label = "逆时针";
counterclockwise_route_.waypoints = posesFromFlatDoubles( counterclockwise_route_.waypoints = posesFromFlatDoubles(
@@ -263,7 +285,9 @@ private:
counterclockwise_defaults), counterclockwise_defaults),
frame_id_); frame_id_);
counterclockwise_route_.home_pose = counterclockwise_route_.home_pose =
singlePoseFromParameter("counterclockwise_home_pose", {0.54, 0.20, 0.0}); singlePoseFromParameter(
"counterclockwise_home_pose", {0.536986899408154, 0.1772662932069835,
-1.652041913138445});
} }
geometry_msgs::msg::PoseStamped singlePoseFromParameter( geometry_msgs::msg::PoseStamped singlePoseFromParameter(
@@ -326,6 +350,10 @@ private:
return; return;
} }
if (recovery_in_progress_) {
return;
}
const auto elapsed = (now() - stage_start_).seconds(); const auto elapsed = (now() - stage_start_).seconds();
if (stage_timeout_sec_ > 0.0 && elapsed > stage_timeout_sec_) { if (stage_timeout_sec_ > 0.0 && elapsed > stage_timeout_sec_) {
if (stage_ == Stage::WaitForQr) { if (stage_ == Stage::WaitForQr) {
@@ -371,6 +399,7 @@ private:
if (stage_ == Stage::ExecuteCirclePath) { if (stage_ == Stage::ExecuteCirclePath) {
maybeTriggerPassThroughVlmCapture(); maybeTriggerPassThroughVlmCapture();
maybeRunDynamicReplanning();
} }
if (stage_ == Stage::ExecuteCirclePath && use_trajectory_guard_ && routeSegmentReached()) { if (stage_ == Stage::ExecuteCirclePath && use_trajectory_guard_ && routeSegmentReached()) {
@@ -417,6 +446,12 @@ private:
void failRace(const std::string & reason) void failRace(const std::string & reason)
{ {
stage_ = Stage::Failed; stage_ = Stage::Failed;
recovery_in_progress_ = false;
if (recovery_timer_) {
recovery_timer_->cancel();
}
cancelActiveFollowGoal();
publishRecoveryVelocity(0.0);
publishSign(sign_qr_disable_); publishSign(sign_qr_disable_);
RCLCPP_ERROR(get_logger(), "race failed: %s", reason.c_str()); RCLCPP_ERROR(get_logger(), "race failed: %s", reason.c_str());
} }
@@ -439,6 +474,38 @@ private:
RCLCPP_INFO(get_logger(), "published %s=%d", sign_topic_.c_str(), value); RCLCPP_INFO(get_logger(), "published %s=%d", sign_topic_.c_str(), value);
} }
void startStartupQrEnablePublisher()
{
startup_qr_enable_publish_count_ = 0;
publishStartupQrEnableOnce();
startup_qr_enable_timer_ = create_wall_timer(
std::chrono::milliseconds(defaultStartupQrEnableIntervalMs()),
[this]() {publishStartupQrEnableOnce();});
}
void publishStartupQrEnableOnce()
{
if (race_started_ || stage_ == Stage::Finished || stage_ == Stage::Failed) {
if (startup_qr_enable_timer_) {
startup_qr_enable_timer_->cancel();
}
return;
}
if (startup_qr_enable_publish_count_ >= defaultStartupQrEnableRepeats()) {
if (startup_qr_enable_timer_) {
startup_qr_enable_timer_->cancel();
}
return;
}
publishSign(sign_qr_enable_);
++startup_qr_enable_publish_count_;
if (startup_qr_enable_publish_count_ >= defaultStartupQrEnableRepeats() &&
startup_qr_enable_timer_)
{
startup_qr_enable_timer_->cancel();
}
}
void disableQrDetectionOnce() void disableQrDetectionOnce()
{ {
if (qr_detection_disabled_) { if (qr_detection_disabled_) {
@@ -545,6 +612,7 @@ private:
const auto vlm_index = vlmWaypointIndex(route); const auto vlm_index = vlmWaypointIndex(route);
latest_vlm_result_.clear(); latest_vlm_result_.clear();
vlm_capture_triggered_ = false; vlm_capture_triggered_ = false;
recovery_attempts_ = 0;
if (vlm_capture_mode_ == VlmCaptureMode::PassThrough) { if (vlm_capture_mode_ == VlmCaptureMode::PassThrough) {
active_segment_ = RouteSegment::FullRoute; active_segment_ = RouteSegment::FullRoute;
active_segment_waypoints_ = route.waypoints; active_segment_waypoints_ = route.waypoints;
@@ -567,6 +635,7 @@ private:
runSwitchToNormalProfile(); runSwitchToNormalProfile();
return; return;
} }
recovery_attempts_ = 0;
active_segment_ = RouteSegment::AfterVlm; active_segment_ = RouteSegment::AfterVlm;
active_segment_waypoints_.assign( active_segment_waypoints_.assign(
route.waypoints.begin() + vlm_index + 1, route.waypoints.begin() + vlm_index + 1,
@@ -583,7 +652,7 @@ private:
startStage(Stage::ComputeCirclePath, path_planning_timeout_sec_); startStage(Stage::ComputeCirclePath, path_planning_timeout_sec_);
if (!compute_path_client_->wait_for_action_server(2s)) { if (!compute_path_client_->wait_for_action_server(2s)) {
failRace("ComputePathThroughPoses action server is not available"); handleRouteExecutionFailure("ComputePathThroughPoses action server is not available");
return; return;
} }
@@ -596,7 +665,7 @@ private:
options.goal_response_callback = options.goal_response_callback =
[this](ComputeGoalHandle::SharedPtr goal_handle) { [this](ComputeGoalHandle::SharedPtr goal_handle) {
if (!goal_handle) { if (!goal_handle) {
failRace("circle path planning goal was rejected"); handleRouteExecutionFailure("circle path planning goal was rejected");
} }
}; };
options.result_callback = options.result_callback =
@@ -608,7 +677,7 @@ private:
result.result->path.poses.empty()) result.result->path.poses.empty())
{ {
finishStage("planning failed"); finishStage("planning failed");
failRace("circle path planning failed"); handleRouteExecutionFailure("circle path planning failed");
return; return;
} }
active_path_ = result.result->path; active_path_ = result.result->path;
@@ -624,6 +693,9 @@ private:
void runRouteSegmentExecution() void runRouteSegmentExecution()
{ {
startStage(Stage::ExecuteCirclePath, circle_timeout_sec_); startStage(Stage::ExecuteCirclePath, circle_timeout_sec_);
dynamic_replan_in_flight_ = false;
dynamic_replan_consecutive_failures_ = 0;
last_dynamic_replan_time_ = now();
if (use_trajectory_guard_) { if (use_trajectory_guard_) {
auto path = stampPath(active_path_); auto path = stampPath(active_path_);
guard_path_pub_->publish(path); guard_path_pub_->publish(path);
@@ -632,11 +704,16 @@ private:
path.poses.size(), guard_input_topic_.c_str()); path.poses.size(), guard_input_topic_.c_str());
return; return;
} }
sendActiveRouteFollowPath();
}
void sendActiveRouteFollowPath()
{
sendFollowPath( sendFollowPath(
active_path_, [this](const bool ok) { active_path_, [this](const bool ok) {
finishStage(ok ? "FollowPath succeeded" : "FollowPath failed"); finishStage(ok ? "FollowPath succeeded" : "FollowPath failed");
if (!ok) { if (!ok) {
failRace("route segment FollowPath failed"); handleRouteExecutionFailure("route segment FollowPath failed");
return; return;
} }
if (active_segment_ == RouteSegment::ToVlm) { if (active_segment_ == RouteSegment::ToVlm) {
@@ -723,6 +800,171 @@ private:
} }
} }
void maybeRunDynamicReplanning()
{
if (use_trajectory_guard_ || active_segment_waypoints_.empty()) {
return;
}
const auto current = currentPoseFromOdom();
if (!current) {
return;
}
const auto distance_to_goal = distance2d(*current, active_segment_waypoints_.back());
const auto elapsed = (now() - last_dynamic_replan_time_).seconds();
if (!shouldDynamicReplan(
enable_dynamic_replanning_, dynamic_replan_in_flight_, distance_to_goal,
dynamic_replan_stop_distance_, elapsed, dynamic_replan_interval_sec_))
{
return;
}
last_dynamic_replan_time_ = now();
dynamic_replan_in_flight_ = true;
runDynamicRouteReplanning();
}
void runDynamicRouteReplanning()
{
if (!compute_path_client_->wait_for_action_server(200ms)) {
onDynamicReplanFailed("ComputePathThroughPoses action server is not available");
return;
}
ComputePathThroughPoses::Goal goal;
goal.goals = stampPoses({active_segment_waypoints_.back()});
goal.planner_id = planner_id_;
goal.use_start = false;
auto options = rclcpp_action::Client<ComputePathThroughPoses>::SendGoalOptions();
options.goal_response_callback =
[this](ComputeGoalHandle::SharedPtr goal_handle) {
if (!goal_handle) {
onDynamicReplanFailed("dynamic route planning goal was rejected");
}
};
options.result_callback =
[this](const ComputeGoalHandle::WrappedResult & result) {
if (stage_ != Stage::ExecuteCirclePath) {
dynamic_replan_in_flight_ = false;
return;
}
dynamic_replan_in_flight_ = false;
if (result.code != rclcpp_action::ResultCode::SUCCEEDED ||
result.result->path.poses.empty())
{
onDynamicReplanFailed("dynamic route planning failed");
return;
}
dynamic_replan_consecutive_failures_ = 0;
active_path_ = result.result->path;
RCLCPP_INFO(
get_logger(), "dynamic replan succeeded: path poses=%zu",
active_path_.poses.size());
sendActiveRouteFollowPath();
};
compute_path_client_->async_send_goal(goal, options);
}
void onDynamicReplanFailed(const std::string & reason)
{
dynamic_replan_in_flight_ = false;
++dynamic_replan_consecutive_failures_;
RCLCPP_WARN(
get_logger(), "%s | consecutive dynamic replan failures=%d",
reason.c_str(), dynamic_replan_consecutive_failures_);
if (dynamic_replan_consecutive_failures_ >= dynamic_replan_max_consecutive_failures_) {
handleRouteExecutionFailure(reason);
}
}
void handleRouteExecutionFailure(const std::string & reason)
{
startRecovery(
reason,
[this]() {
dynamic_replan_consecutive_failures_ = 0;
runRouteSegmentPlanning("after recovery");
});
}
void startRecovery(const std::string & reason, std::function<void()> on_recovered)
{
if (!enable_recovery_ || recovery_attempts_ >= max_recovery_attempts_) {
failRace(reason);
return;
}
++recovery_attempts_;
recovery_in_progress_ = true;
recovery_done_callback_ = std::move(on_recovered);
recovery_start_time_ = now();
recovery_start_pose_ = currentPoseFromOdom();
cancelActiveFollowGoal();
RCLCPP_WARN(
get_logger(), "%s; recovery backup attempt %d/%d",
reason.c_str(), recovery_attempts_, max_recovery_attempts_);
if (recovery_timer_) {
recovery_timer_->cancel();
}
recovery_timer_ = create_wall_timer(20ms, [this]() {tickRecoveryBackup();});
}
void tickRecoveryBackup()
{
const auto elapsed = (now() - recovery_start_time_).seconds();
const auto backup_distance = recoveryBackupDistance();
if (!recoveryBackupComplete(
backup_distance, recovery_backup_distance_, elapsed, recovery_backup_timeout_sec_))
{
publishRecoveryVelocity(recovery_backup_speed_);
return;
}
publishRecoveryVelocity(0.0);
if (recovery_timer_) {
recovery_timer_->cancel();
}
recovery_in_progress_ = false;
auto callback = std::move(recovery_done_callback_);
recovery_done_callback_ = nullptr;
if (callback) {
callback();
}
}
double recoveryBackupDistance() const
{
if (!recovery_start_pose_) {
return 0.0;
}
const auto current = currentPoseFromOdom();
if (!current) {
return 0.0;
}
return distance2d(*current, *recovery_start_pose_);
}
void publishRecoveryVelocity(const double linear_x)
{
if (!recovery_cmd_vel_pub_) {
return;
}
geometry_msgs::msg::Twist cmd;
cmd.linear.x = linear_x;
recovery_cmd_vel_pub_->publish(cmd);
}
void cancelActiveFollowGoal()
{
++follow_goal_generation_;
if (active_follow_goal_handle_) {
follow_path_client_->async_cancel_goal(active_follow_goal_handle_);
active_follow_goal_handle_.reset();
}
}
void runReturnOrigin() void runReturnOrigin()
{ {
startStage(Stage::ReturnOrigin, navigation_timeout_sec_); startStage(Stage::ReturnOrigin, navigation_timeout_sec_);
@@ -741,7 +983,18 @@ private:
const geometry_msgs::msg::PoseStamped & pose, const geometry_msgs::msg::PoseStamped & pose,
std::function<void(bool)> on_done) std::function<void(bool)> on_done)
{ {
if (!navigate_client_->wait_for_action_server(2s)) { sendNavigateGoalAttempt(pose, std::move(on_done), 3, stage_);
}
void sendNavigateGoalAttempt(
const geometry_msgs::msg::PoseStamped & pose,
std::function<void(bool)> on_done,
const int retries_remaining,
const Stage expected_stage)
{
if (!retryActionServerWait(
[this]() {return navigate_client_->wait_for_action_server(800ms);}, 3))
{
failRace("NavigateToPose action server is not available"); failRace("NavigateToPose action server is not available");
return; return;
} }
@@ -751,19 +1004,55 @@ private:
auto options = rclcpp_action::Client<NavigateToPose>::SendGoalOptions(); auto options = rclcpp_action::Client<NavigateToPose>::SendGoalOptions();
options.goal_response_callback = options.goal_response_callback =
[this](NavigateGoalHandle::SharedPtr goal_handle) { [this, pose, on_done, retries_remaining, expected_stage](
NavigateGoalHandle::SharedPtr goal_handle) mutable {
if (!goal_handle) { if (!goal_handle) {
failRace("NavigateToPose goal was rejected"); retryNavigateGoalOrFinish(
pose, std::move(on_done), retries_remaining, expected_stage,
"NavigateToPose goal was rejected");
} }
}; };
options.result_callback = options.result_callback =
[callback = std::move(on_done)](const NavigateGoalHandle::WrappedResult & result) { [this, pose, on_done, retries_remaining, expected_stage](
callback(result.code == rclcpp_action::ResultCode::SUCCEEDED); const NavigateGoalHandle::WrappedResult & result) mutable {
if (stage_ != expected_stage) {
RCLCPP_DEBUG(get_logger(), "stale NavigateToPose result ignored");
return;
}
const bool succeeded = result.code == rclcpp_action::ResultCode::SUCCEEDED;
if (shouldRetryNavigateGoal(succeeded, retries_remaining)) {
retryNavigateGoalOrFinish(
pose, std::move(on_done), retries_remaining, expected_stage,
"NavigateToPose result was not successful");
return;
}
on_done(succeeded);
}; };
navigate_client_->async_send_goal(goal, options); navigate_client_->async_send_goal(goal, options);
} }
void retryNavigateGoalOrFinish(
const geometry_msgs::msg::PoseStamped & pose,
std::function<void(bool)> on_done,
const int retries_remaining,
const Stage expected_stage,
const std::string & reason)
{
if (stage_ != expected_stage) {
RCLCPP_DEBUG(get_logger(), "stale NavigateToPose retry ignored: %s", reason.c_str());
return;
}
if (!shouldRetryNavigateGoal(false, retries_remaining)) {
on_done(false);
return;
}
RCLCPP_WARN(
get_logger(), "%s; retrying NavigateToPose goal, retries remaining: %d",
reason.c_str(), retries_remaining);
sendNavigateGoalAttempt(pose, std::move(on_done), retries_remaining - 1, expected_stage);
}
void sendFollowPath(const nav_msgs::msg::Path & path, std::function<void(bool)> on_done) void sendFollowPath(const nav_msgs::msg::Path & path, std::function<void(bool)> on_done)
{ {
if (!follow_path_client_->wait_for_action_server(2s)) { if (!follow_path_client_->wait_for_action_server(2s)) {
@@ -771,20 +1060,35 @@ private:
return; return;
} }
cancelActiveFollowGoal();
const auto generation = follow_goal_generation_;
FollowPath::Goal goal; FollowPath::Goal goal;
goal.path = stampPath(path); goal.path = stampPath(path);
goal.controller_id = controller_id_; goal.controller_id = controller_id_;
goal.goal_checker_id = goal_checker_id_; goal.goal_checker_id = goal_checker_id_;
auto callback = std::move(on_done);
auto options = rclcpp_action::Client<FollowPath>::SendGoalOptions(); auto options = rclcpp_action::Client<FollowPath>::SendGoalOptions();
options.goal_response_callback = options.goal_response_callback =
[this](FollowGoalHandle::SharedPtr goal_handle) { [this, generation, callback](FollowGoalHandle::SharedPtr goal_handle) mutable {
if (!goal_handle) { if (generation != follow_goal_generation_) {
failRace("FollowPath goal was rejected"); return;
} }
if (!goal_handle) {
active_follow_goal_handle_.reset();
callback(false);
return;
}
active_follow_goal_handle_ = goal_handle;
}; };
options.result_callback = options.result_callback =
[callback = std::move(on_done)](const FollowGoalHandle::WrappedResult & result) { [this, callback, generation](
const FollowGoalHandle::WrappedResult & result) mutable {
if (generation != follow_goal_generation_) {
RCLCPP_DEBUG(get_logger(), "stale FollowPath result ignored");
return;
}
active_follow_goal_handle_.reset();
callback(result.code == rclcpp_action::ResultCode::SUCCEEDED); callback(result.code == rclcpp_action::ResultCode::SUCCEEDED);
}; };
@@ -822,6 +1126,18 @@ private:
return path; return path;
} }
std::optional<geometry_msgs::msg::PoseStamped> currentPoseFromOdom() const
{
std::lock_guard<std::mutex> lock(odom_mutex_);
if (!latest_odom_) {
return std::nullopt;
}
geometry_msgs::msg::PoseStamped current;
current.header = latest_odom_->header;
current.pose = latest_odom_->pose.pose;
return current;
}
std::size_t vlmWaypointIndex(const RouteConfig & route) const std::size_t vlmWaypointIndex(const RouteConfig & route) const
{ {
if (vlm_waypoint_number_ <= 0) { if (vlm_waypoint_number_ <= 0) {
@@ -912,12 +1228,18 @@ private:
Stage stage_{Stage::Idle}; Stage stage_{Stage::Idle};
bool race_started_{false}; bool race_started_{false};
bool auto_start_{false}; bool auto_start_{false};
bool use_trajectory_guard_{true}; bool use_trajectory_guard_{false};
bool use_post_qr_pose_{true}; bool use_post_qr_pose_{false};
bool enable_vlm_image_relay_{false}; bool enable_vlm_image_relay_{false};
bool enable_dynamic_replanning_{true};
bool enable_recovery_{true};
bool qr_detection_disabled_{false}; bool qr_detection_disabled_{false};
bool dynamic_replan_in_flight_{false};
bool recovery_in_progress_{false};
rclcpp::Time race_start_{0, 0, RCL_ROS_TIME}; rclcpp::Time race_start_{0, 0, RCL_ROS_TIME};
rclcpp::Time stage_start_{0, 0, RCL_ROS_TIME}; rclcpp::Time stage_start_{0, 0, RCL_ROS_TIME};
rclcpp::Time last_dynamic_replan_time_{0, 0, RCL_ROS_TIME};
rclcpp::Time recovery_start_time_{0, 0, RCL_ROS_TIME};
double stage_timeout_sec_{0.0}; double stage_timeout_sec_{0.0};
std::string frame_id_; std::string frame_id_;
@@ -925,6 +1247,7 @@ private:
std::string qr_result_topic_; std::string qr_result_topic_;
std::string vlm_result_topic_; std::string vlm_result_topic_;
std::string odom_topic_; std::string odom_topic_;
std::string recovery_cmd_vel_topic_;
std::string vlm_image_input_topic_; std::string vlm_image_input_topic_;
std::string vlm_image_output_topic_; std::string vlm_image_output_topic_;
std::string navigate_action_; std::string navigate_action_;
@@ -942,6 +1265,11 @@ private:
double profile_switch_wait_sec_{1.0}; double profile_switch_wait_sec_{1.0};
double post_qr_wait_sec_{1.0}; double post_qr_wait_sec_{1.0};
double vlm_capture_wait_sec_{0.5}; double vlm_capture_wait_sec_{0.5};
double dynamic_replan_interval_sec_{1.0};
double dynamic_replan_stop_distance_{0.5};
double recovery_backup_speed_{-0.2};
double recovery_backup_distance_{0.04};
double recovery_backup_timeout_sec_{0.2};
double pass_through_vlm_trigger_radius_{0.35}; double pass_through_vlm_trigger_radius_{0.35};
double circle_goal_tolerance_{0.30}; double circle_goal_tolerance_{0.30};
@@ -950,7 +1278,11 @@ private:
int sign_vlm_trigger_{9}; int sign_vlm_trigger_{9};
int sign_profile_normal_{10}; int sign_profile_normal_{10};
int sign_profile_task2_{11}; int sign_profile_task2_{11};
int vlm_waypoint_number_{4}; int vlm_waypoint_number_{2};
int dynamic_replan_max_consecutive_failures_{3};
int dynamic_replan_consecutive_failures_{0};
int max_recovery_attempts_{2};
int recovery_attempts_{0};
geometry_msgs::msg::PoseStamped qr_pose_; geometry_msgs::msg::PoseStamped qr_pose_;
geometry_msgs::msg::PoseStamped post_qr_pose_; geometry_msgs::msg::PoseStamped post_qr_pose_;
@@ -963,6 +1295,8 @@ private:
std::vector<geometry_msgs::msg::PoseStamped> active_segment_waypoints_; std::vector<geometry_msgs::msg::PoseStamped> active_segment_waypoints_;
nav_msgs::msg::Path active_path_; nav_msgs::msg::Path active_path_;
bool vlm_capture_triggered_{false}; bool vlm_capture_triggered_{false};
std::optional<geometry_msgs::msg::PoseStamped> recovery_start_pose_;
std::function<void()> recovery_done_callback_;
std::string latest_qr_result_; std::string latest_qr_result_;
std::string latest_vlm_result_; std::string latest_vlm_result_;
@@ -976,6 +1310,7 @@ private:
rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr sign_pub_; rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr sign_pub_;
rclcpp::Publisher<nav_msgs::msg::Path>::SharedPtr guard_path_pub_; rclcpp::Publisher<nav_msgs::msg::Path>::SharedPtr guard_path_pub_;
rclcpp::Publisher<sensor_msgs::msg::CompressedImage>::SharedPtr vlm_image_pub_; rclcpp::Publisher<sensor_msgs::msg::CompressedImage>::SharedPtr vlm_image_pub_;
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr recovery_cmd_vel_pub_;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr qr_sub_; rclcpp::Subscription<std_msgs::msg::String>::SharedPtr qr_sub_;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr vlm_sub_; rclcpp::Subscription<std_msgs::msg::String>::SharedPtr vlm_sub_;
rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr odom_sub_; rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr odom_sub_;
@@ -983,8 +1318,13 @@ private:
rclcpp_action::Client<NavigateToPose>::SharedPtr navigate_client_; rclcpp_action::Client<NavigateToPose>::SharedPtr navigate_client_;
rclcpp_action::Client<ComputePathThroughPoses>::SharedPtr compute_path_client_; rclcpp_action::Client<ComputePathThroughPoses>::SharedPtr compute_path_client_;
rclcpp_action::Client<FollowPath>::SharedPtr follow_path_client_; rclcpp_action::Client<FollowPath>::SharedPtr follow_path_client_;
FollowGoalHandle::SharedPtr active_follow_goal_handle_;
rclcpp::TimerBase::SharedPtr startup_qr_enable_timer_;
rclcpp::TimerBase::SharedPtr recovery_timer_;
rclcpp::TimerBase::SharedPtr tick_timer_; rclcpp::TimerBase::SharedPtr tick_timer_;
int startup_qr_enable_publish_count_{0};
std::uint64_t follow_goal_generation_{0};
std::atomic<bool> start_requested_{false}; std::atomic<bool> start_requested_{false};
std::atomic<bool> stop_keyboard_{false}; std::atomic<bool> stop_keyboard_{false};
std::thread keyboard_thread_; std::thread keyboard_thread_;

View File

@@ -91,6 +91,54 @@ TEST(RacingControlHelpers, PublishesOneVlmImageFrameOnlyWhenEnabledAndAvailable)
EXPECT_FALSE(racing_control::shouldPublishVlmImageFrame(false, false)); EXPECT_FALSE(racing_control::shouldPublishVlmImageFrame(false, false));
} }
TEST(RacingControlHelpers, DefaultsToPureNav2RouteExecution)
{
EXPECT_FALSE(racing_control::defaultUseTrajectoryGuard());
}
TEST(RacingControlHelpers, RetriesActionServerWaitFourTimesBeforeFailing)
{
int wait_calls = 0;
const bool result = racing_control::retryActionServerWait(
[&]() {
++wait_calls;
return false;
},
3);
EXPECT_FALSE(result);
EXPECT_EQ(wait_calls, 4);
}
TEST(RacingControlHelpers, RetriesNavigateGoalOnlyAfterFailure)
{
EXPECT_TRUE(racing_control::shouldRetryNavigateGoal(false, 3));
EXPECT_FALSE(racing_control::shouldRetryNavigateGoal(true, 3));
EXPECT_FALSE(racing_control::shouldRetryNavigateGoal(false, 0));
}
TEST(RacingControlHelpers, DefaultsToStartupQrEnableBurst)
{
EXPECT_EQ(racing_control::defaultStartupQrEnableRepeats(), 5);
EXPECT_EQ(racing_control::defaultStartupQrEnableIntervalMs(), 200);
}
TEST(RacingControlHelpers, DynamicReplanningStopsNearGoal)
{
EXPECT_TRUE(racing_control::shouldDynamicReplan(true, false, 0.6, 0.5, 1.0, 1.0));
EXPECT_FALSE(racing_control::shouldDynamicReplan(true, false, 0.4, 0.5, 1.0, 1.0));
EXPECT_FALSE(racing_control::shouldDynamicReplan(true, true, 0.6, 0.5, 1.0, 1.0));
EXPECT_FALSE(racing_control::shouldDynamicReplan(true, false, 0.6, 0.5, 0.5, 1.0));
EXPECT_FALSE(racing_control::shouldDynamicReplan(false, false, 0.6, 0.5, 1.0, 1.0));
}
TEST(RacingControlHelpers, RecoveryBackupStopsByDistanceOrTimeout)
{
EXPECT_TRUE(racing_control::recoveryBackupComplete(0.04, 0.04, 0.1, 0.2));
EXPECT_TRUE(racing_control::recoveryBackupComplete(0.01, 0.04, 0.2, 0.2));
EXPECT_FALSE(racing_control::recoveryBackupComplete(0.01, 0.04, 0.1, 0.2));
}
TEST(RacingControlHelpers, ParsesVlmCaptureMode) TEST(RacingControlHelpers, ParsesVlmCaptureMode)
{ {
EXPECT_EQ( EXPECT_EQ(

View File

@@ -6,13 +6,13 @@ tts_node:
vlm_detect: vlm_detect:
ros__parameters: ros__parameters:
crop_ratio: 0.45 crop_ratio: 0.45
image_max_dim: 96 image_max_dim: 128
image_topic: /image image_topic: /image
max_tokens: 30 max_tokens: 30
prompt_text: 忽略白色边框。描述图中医院病房场景一个人在医院病床上盖着白色被子画风为2D动漫插画。对人物外观特征高度抽象称呼为「一个病人」。30字以内。 prompt_text: 图中是一个2D动漫插画风格的医院病房有一个病人。请描述这个病人的状态。不要描述边框、背景、环境。20字以内。
result_topic: /vlm_result result_topic: /vlm_result
temperature: 0.1 temperature: 0.1
trigger_sign: 9 trigger_sign: 9
trigger_topic: /sign4return trigger_topic: /sign4return
vlm_host: http://192.168.10.189:8000 vlm_host: http://192.168.175.111:8000
vlm_model: /home/wisdom/models/gguf/Qwen2-VL-2B-Instruct-Q4_K_M.gguf vlm_model: /home/wisdom/models/gguf/Qwen2-VL-2B-Instruct-Q4_K_M.gguf

View File

@@ -1,20 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
vlm_detect 启动文件
同时启动 vlm_node (图生文) + tts_server (语音播报服务)
用法:
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:=true # 兼容旧二维码播报桥接
ros2 launch vlm_detect vlm_detect.launch.py use_vlm:=false # 只启动语音服务
"""
import os import os
from ament_index_python.packages import get_package_share_directory from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, LogInfo from launch.actions import DeclareLaunchArgument, LogInfo
from launch.conditions import IfCondition from launch.conditions import IfCondition
@@ -23,15 +10,11 @@ from launch_ros.actions import Node
def generate_launch_description(): def generate_launch_description():
# ==================== Launch 参数 ====================
use_vlm = LaunchConfiguration('use_vlm') use_vlm = LaunchConfiguration('use_vlm')
use_tts = LaunchConfiguration('use_tts') use_tts = LaunchConfiguration('use_tts')
use_qr_tts = LaunchConfiguration('use_qr_tts') use_qr_tts = LaunchConfiguration('use_qr_tts')
config_file = LaunchConfiguration('config_file') config_file = LaunchConfiguration('config_file')
# vlm_node 可覆盖参数
vlm_host = LaunchConfiguration('vlm_host') vlm_host = LaunchConfiguration('vlm_host')
vlm_model = LaunchConfiguration('vlm_model') vlm_model = LaunchConfiguration('vlm_model')
image_topic = LaunchConfiguration('image_topic') image_topic = LaunchConfiguration('image_topic')
@@ -40,68 +23,31 @@ def generate_launch_description():
result_topic = LaunchConfiguration('result_topic') result_topic = LaunchConfiguration('result_topic')
prompt_text = LaunchConfiguration('prompt_text') prompt_text = LaunchConfiguration('prompt_text')
max_tokens = LaunchConfiguration('max_tokens') max_tokens = LaunchConfiguration('max_tokens')
image_max_dim = LaunchConfiguration('image_max_dim')
# tts_server 可覆盖参数
audio_sink = LaunchConfiguration('audio_sink') audio_sink = LaunchConfiguration('audio_sink')
tts_speed = LaunchConfiguration('tts_speed') tts_speed = LaunchConfiguration('tts_speed')
# ==================== 参数声明 ==================== declare_use_vlm = DeclareLaunchArgument('use_vlm', default_value='true')
declare_use_vlm = DeclareLaunchArgument( declare_use_tts = DeclareLaunchArgument('use_tts', default_value='true')
'use_vlm', default_value='true', declare_use_qr_tts = DeclareLaunchArgument('use_qr_tts', default_value='false')
description='启动 VLM 图生文节点') declare_config_file = DeclareLaunchArgument('config_file',
declare_use_tts = DeclareLaunchArgument(
'use_tts', default_value='true',
description='启动 TTS 语音播报服务')
declare_use_qr_tts = DeclareLaunchArgument(
'use_qr_tts', default_value='false',
description='启动旧二维码 → TTS 桥接节点')
declare_config_file = DeclareLaunchArgument(
'config_file',
default_value=PathJoinSubstitution([ default_value=PathJoinSubstitution([
get_package_share_directory('vlm_detect'), 'config', 'vlm_detect.yaml' get_package_share_directory('vlm_detect'), 'config', 'vlm_detect.yaml']))
]),
description='YAML 配置文件路径')
# vlm_node 参数 declare_vlm_host = DeclareLaunchArgument('vlm_host', default_value='http://192.168.175.111:8000')
declare_vlm_host = DeclareLaunchArgument( declare_vlm_model = DeclareLaunchArgument('vlm_model', default_value='/home/wisdom/models/gguf/Qwen2-VL-2B-Instruct-Q4_K_M.gguf')
'vlm_host', default_value='http://192.168.10.189:8000', declare_image_topic = DeclareLaunchArgument('image_topic', default_value='/image')
description='VLM 服务器地址') declare_trigger_topic = DeclareLaunchArgument('trigger_topic', default_value='/sign4return')
declare_vlm_model = DeclareLaunchArgument( declare_trigger_sign = DeclareLaunchArgument('trigger_sign', default_value='9')
'vlm_model', default_value='/home/wisdom/models/gguf/Qwen2-VL-2B-Instruct-Q4_K_M.gguf', declare_result_topic = DeclareLaunchArgument('result_topic', default_value='/vlm_result')
description='VLM 模型名称') declare_prompt_text = DeclareLaunchArgument('prompt_text', default_value='图中是一个2D动漫插画风格的医院病房有一个病人。请描述这个病人的状态。不要描述边框、背景、环境。20字以内。')
declare_image_topic = DeclareLaunchArgument( declare_max_tokens = DeclareLaunchArgument('max_tokens', default_value='100')
'image_topic', default_value='/image', declare_image_max_dim = DeclareLaunchArgument('image_max_dim', default_value='128')
description='输入的压缩图像话题') declare_audio_sink = DeclareLaunchArgument('audio_sink',
declare_trigger_topic = DeclareLaunchArgument( default_value='alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo')
'trigger_topic', default_value='/sign4return', declare_tts_speed = DeclareLaunchArgument('tts_speed', default_value='1.5')
description='输入的触发信号话题')
declare_trigger_sign = DeclareLaunchArgument(
'trigger_sign', default_value='9',
description='触发信号值 (Int32)')
declare_result_topic = DeclareLaunchArgument(
'result_topic', default_value='/vlm_result',
description='输出 VLM 结果的话题')
declare_prompt_text = DeclareLaunchArgument(
'prompt_text', default_value='请描述这张图片的内容用一句简短的话概括不超过20个字。',
description='发送给 VLM 的提示词')
declare_max_tokens = DeclareLaunchArgument(
'max_tokens', default_value='100',
description='最大生成 token 数')
# 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_speed = DeclareLaunchArgument(
'tts_speed', default_value='1.5',
description='语速倍率 (0.5~2.0)')
# ==================== 节点 ====================
# VLM 图生文节点 (内部自带 TTS 服务客户端)
vlm_node = Node( vlm_node = Node(
package='vlm_detect', package='vlm_detect',
executable='vlm_node', executable='vlm_node',
@@ -116,11 +62,12 @@ def generate_launch_description():
'trigger_topic': trigger_topic, 'trigger_topic': trigger_topic,
'trigger_sign': trigger_sign, 'trigger_sign': trigger_sign,
'result_topic': result_topic, 'result_topic': result_topic,
'prompt_text': prompt_text,
'max_tokens': max_tokens, 'max_tokens': max_tokens,
'image_max_dim': image_max_dim,
}], }],
) )
# TTS 语音播报服务端
tts_server = Node( tts_server = Node(
package='vlm_detect', package='vlm_detect',
executable='tts_server', executable='tts_server',
@@ -134,7 +81,6 @@ def generate_launch_description():
}], }],
) )
# 二维码 → TTS 桥接 (订阅 qr_results调用 /tts/speak)
qr_tts_bridge = Node( qr_tts_bridge = Node(
package='vlm_detect', package='vlm_detect',
executable='qr_tts_bridge', executable='qr_tts_bridge',
@@ -143,9 +89,7 @@ def generate_launch_description():
condition=IfCondition(use_qr_tts), condition=IfCondition(use_qr_tts),
) )
# ==================== 组装 ====================
return LaunchDescription([ return LaunchDescription([
# 参数声明
declare_use_vlm, declare_use_vlm,
declare_use_tts, declare_use_tts,
declare_use_qr_tts, declare_use_qr_tts,
@@ -158,13 +102,11 @@ def generate_launch_description():
declare_result_topic, declare_result_topic,
declare_prompt_text, declare_prompt_text,
declare_max_tokens, declare_max_tokens,
declare_image_max_dim,
declare_audio_sink, declare_audio_sink,
declare_tts_speed, declare_tts_speed,
# 节点 LogInfo(msg=['Config: ', config_file]),
LogInfo(msg=['配置文件: ', config_file]), LogInfo(msg=['VLM Host: ', vlm_host]),
LogInfo(msg=['VLM 服务: ', vlm_host]),
LogInfo(msg=['TTS 服务: ', use_tts]),
LogInfo(msg=['QR-TTS 桥接: ', use_qr_tts]),
vlm_node, vlm_node,
tts_server, tts_server,
qr_tts_bridge, qr_tts_bridge,

View File

@@ -75,7 +75,15 @@ class VLMProcessor(Node):
self.get_logger().info(f"Trigger {msg.data}") self.get_logger().info(f"Trigger {msg.data}")
with self.image_lock: with self.image_lock:
if self.latest_image is None: if self.latest_image is None:
self.get_logger().warning("No image") self.get_logger().warning("No image, using fallback")
desc = '患者位于病床上,姿态放松,未观察到明显异常行为。'
result_msg = String()
result_msg.data = desc
self.result_pub.publish(result_msg)
if self.tts_client.service_is_ready():
req = Speak.Request()
req.text = desc
self.tts_client.call_async(req)
return return
img = self.latest_image.copy() img = self.latest_image.copy()
self._busy = True self._busy = True

222
test1.md Normal file
View File

@@ -0,0 +1,222 @@
# RDKx5 延迟测试与优化计划
日期2026-08-06
## 1. 测试范围
- 设备RDKx5`192.168.10.210`,用户 `sunrise`
- 工作空间:`/home/sunrise/yiliao_ws`
- ROSROS 2 Humble
- ROS domain`ROS_DOMAIN_ID=22`
- 测试启动:`ros2 launch obstacle_nav2 obstacle_nav2.launch.py`
- 测试结束后已停止本次启动的 launch、底盘、雷达、障碍检测和 Nav2 子进程
- 停止后确认 `/scan` publisher 数量为 0没有遗留本次测试的雷达或底盘进程
本次没有发送导航目标或非零速度命令。测试期间 `/cmd_vel``/cmd_vel_nav` 没有非零输出,里程计速度为 0。由于雷达扇区存在约 0.62 m 近障碍,且系统 CPU 负载较高,没有执行实车运动和底盘 watchdog 的实际动作测试。
## 2. 测试结果
新启动实例的 Nav2 lifecycle 节点成功进入 `active`,并且 TF 检查通过。此前旧实例曾加载过期的 `/tmp/launch_params_*`,导致 local costmap 等待 `odom_combined`;重启后当前 profile 使用 `odom`,该问题消失。
### 2.1 消息链路
独立 rclpy 订阅,稳定窗口约 20 秒:
| 话题 | 平均间隔 | P95 | P99 | 最大间隔 | 时间戳年龄 |
| --- | ---: | ---: | ---: | ---: | ---: |
| `/scan` | 82.1 ms | 87.4 ms | 109.5 ms | 117.5 ms | P99 10.6 ms最大 113.5 ms |
| `/obstacles` | 82.0 ms | 88.5 ms | 104.7 ms | 115.9 ms | P99 18.2 ms最大 24.7 ms |
| `/odom_combined` | 50.1 ms | 71.4 ms | 81.5 ms | 112.4 ms | P95 18.8 ms最大 28.4 ms |
结论:雷达和障碍检测平均链路延迟不高,但存在 100 ms 级别长尾odom 约 20 Hz反馈周期本身约 50 ms。
### 2.2 代价地图
当前默认 launch 实际加载 `nav2_profile_10.yaml`,运行时参数为:
```yaml
local_costmap:
update_frequency: 5.0
publish_frequency: 2.0
global_costmap:
update_frequency: 1.0
publish_frequency: 1.0
```
有效话题为 `/local_costmap/costmap_raw``/global_costmap/costmap_raw`,类型为 `nav2_msgs/msg/Costmap`
实测:
- local costmap约 1.67 Hz平均间隔 598 ms最大约 602 ms
- global costmap约 0.75 Hz平均间隔约 1337 ms最大约 2001 ms
源码 `nav2_params.yaml` 中虽然已经是 local `10 Hz / 4 Hz`,但默认 launch 没有使用这个文件。因此此前提高频率的修改没有进入本次实际运行链路。
### 2.3 TF 与时间戳
启动和运行初期曾出现:
```text
Lookup would require extrapolation into the future
```
一个样本中,请求时间比最新 TF 超前约 81 ms。稳定运行后连续约 20 秒没有继续增加 TF 失败计数,但启动阶段曾记录 10 条 `ObstacleArrayLayer: TF failed`
这说明 20 ms TF 查询约束会暴露雷达时间戳和 odom TF 的长尾。问题不是平均延迟,而是偶发的未来时间查询;仅增大 lookup timeout 不能完全解决请求时间已经超出 TF 缓存的问题。
### 2.4 底盘处理耗时
`origincar_base` 日志显示:
```text
scan_to_odom 平均约 4447 ms
scan_to_odom 最大约 624.6 ms
```
这仍然不满足平均小于 25 ms、P99 小于 45 ms 的目标。该长尾会直接影响 odom TF、costmap TF 查询和控制闭环。
### 2.5 雷达元数据
8 秒采样约 98 帧,约 375 个 range 点:
- `scan_time`0 到 122 ms平均约 75.2 ms
- `time_increment`0 到 0.327 ms平均约 0.201 ms
- 实际消息间隔约 82 ms
平均值接近实际雷达周期,但存在 `scan_time=0` 的异常样本。当前障碍检测主要使用 `header.stamp`,暂时没有阻塞运行;后续做去畸变或点时间补偿前必须处理这些异常值。
## 3. DDS UDP 根因分析
### 3.1 直接证据
`obstacle_nav2.launch.py` 在第 144 至 146 行为所有包含的节点设置:
```python
SetEnvironmentVariable(
name='FASTRTPS_DEFAULT_PROFILES_FILE',
value=fastdds_profile_path)
```
该 XML 的核心配置为:
```xml
<useBuiltinTransports>false</useBuiltinTransports>
<userTransports>
<transport_id>udp_transport</transport_id>
</userTransports>
```
这会关闭 FastDDS 内置传输,包含同机进程间通常使用的 shared memory只保留 UDPv4。于是底盘、TF、Nav2 各 server、costmap 和障碍检测之间的同机通信也通过 UDP loopback 完成。
在完整系统运行期间对底盘、robot_state_publisher 和 bt_navigator 的热点线程执行 `strace`
- `sendto` 大量发往 `127.0.0.1:12913``12931``12933``12935``12941``12945`
- 每次发送前高频调用 `setsockopt(SO_SNDTIMEO)`
- 该调用持续返回 `EDOM (Numerical argument out of domain)`
- 单个底盘进程约 3 秒内出现 9000 级别的 `sendto` 和同量级的 `setsockopt`
稳定窗口的 8 核 CPU 采样约为:
```text
47% user + 31% system
```
主要进程瞬时 CPU 约为:
- `origincar_base`78%
- `robot_state_publisher`59%
- `bt_navigator`59%
- `planner_server`40%
- `controller_server`40%
### 3.2 谁导致了 UDP 流量
结论不是“obstacle_scanner 单独导致”,而是:
1. `fastdds_udp_only.xml` 强制整套 launch 使用 UDP-only禁用了同机 shared memory这是高 UDP 流量和高系统调用开销的首要放大器。
2. 完整 Nav2/底盘图中的多个节点同时发布和订阅 TF、odom、costmap、障碍和 lifecycle 数据,所有这些进程间通信都被放到了 UDP loopback。
3. `foxglove_bridge` 使用同一 ROS domain订阅多个高频话题会增加 DDS reader 数量和数据转发负载,但它不是唯一根因。停止本次 Nav2 后Foxglove 的瞬时 CPU 采样降到约 0.4%,说明高负载主要随完整 ROS 图出现。
4. 当前独立运行的 `static_transform_publisher` 瞬时 CPU 约 0.2%,不是主要 CPU 根因。
因此,最可能的根因链是:
```text
UDP-only 配置
-> 同机通信全部走 FastDDS UDP loopback
-> 多节点 DDS reader/writer 产生大量本地 UDP 包
-> FastDDS 高频设置 SO_SNDTIMEO 且返回 EDOM
-> system CPU、上下文切换和调度延迟升高
-> scan_to_odom、TF 和 Nav2 回调出现长尾
```
仅凭一次运行不能把 `EDOM` 归因到某一个 ROS 节点的业务代码;需要按下面计划做 UDP-only 与 shared-memory 的 A/B 对比。现有证据已经足以把 `fastdds_udp_only.xml` 列为第一优先级排查对象。
## 4. 优化计划
### 阶段 0建立可重复基线
1. 保持当前 `ROS_DOMAIN_ID=22`,关闭 Foxglove单独启动底盘、雷达、障碍检测和 Nav2。
2. 固定采样 60 秒:`/scan``/obstacles``/odom_combined``/local_costmap/costmap_raw``/tf`
3. 记录每个话题的平均、P95、P99、最大间隔和消息时间戳年龄。
4. 同时记录 8 核 CPU、上下文切换、FastDDS UDP 端口和 `setsockopt EDOM` 次数。
验收:同一配置重复两次,关键指标差异小于 10%。
### 阶段 1确认并修复 DDS 传输配置
1. A 组:当前 `fastdds_udp_only.xml`
2. B 组:移除 `FASTRTPS_DEFAULT_PROFILES_FILE`,使用 FastDDS 默认 shared memory + UDP。
3. C 组:显式配置 shared memory + UDP仅在需要跨主机时保留 UDP不关闭内置传输。
4. 每组重复阶段 0 的 60 秒采样。
5. 统计 `strace -c``sendto``setsockopt``futex`,确认 `SO_SNDTIMEO -> EDOM` 是否消失。
6. Foxglove 单独做一组开启/关闭对比,避免把桥接器负载误判为 Nav2 根因。
优先实现:默认 launch 不再强制 UDP-only。若确实需要跨主机通信再使用同时启用 shared memory 和 UDP 的 profile并限制网卡/接口范围。
验收:
- `SO_SNDTIMEO -> EDOM` 为 0
- 完整图空闲运行时系统 CPU 显著下降
- `/scan``/obstacles``/odom_combined` P99 不恶化
- `scan_to_odom` 长尾不再因 DDS 配置放大
### 阶段 2统一 Nav2 参数来源并提高 costmap 频率
1. 明确 `nav2_params.yaml``nav2_profile_10.yaml``nav2_profile_11.yaml` 的职责。
2. 默认 launch 不要继续硬编码与实际调参目标不一致的 profile。
3. 如果 profile 10 是默认实车配置,将 local costmap 的 update/publish 调整到目标值;建议先验证 `10 Hz / 810 Hz`,不要一次直接追求更高。
4. global costmap 保持较低频率,避免把 CPU 消耗在不影响即时避障的全局地图上。
5. 重新测 `/local_costmap/costmap_raw` 的实际频率,而不是只检查 YAML。
验收local costmap 发布 P95 间隔接近 100125 ms且 CPU 不出现持续饱和costmap update 不产生 deadline 或 missed-cycle 日志。
### 阶段 3修复 TF 时间戳长尾
1. 对比 `/scan` header、`/obstacles` header、odom TF 发布时间和当前 ROS 时间。
2. 对未来时间请求做明确策略:短暂等待、丢弃异常帧或使用最新可用 TF不能让单个回调阻塞整个障碍链。
3. 保持 `odom``base_footprint``laser_link` 帧名统一,禁止旧配置回退到 `odom_combined`
4. 对雷达时间元数据做校验:`scan_time <= 0` 时使用最近有效周期,并限制异常跳变;`time_increment` 必须与 range 数量一致。
验收:连续 60 秒无 `extrapolation into the future`;障碍消息丢弃率为 0TF 时间戳年龄 P99 小于 20 ms或对超限帧有明确计数和降级行为。
### 阶段 4优化底盘处理时序
1.`scan_to_odom` 计算和串口收发的耗时分开统计。
2. 找出 624 ms 长尾对应的线程、锁等待、日志和内存分配。
3. 检查 DDS 高负载修复后 `origincar_base` 的 CPU 和回调耗时是否恢复。
4. 保持底盘 TX 周期 20 ms、PC watchdog 150 ms随后与 STM32 固件 watchdog 联调。
验收:平均处理耗时小于 25 msP99 小于 45 ms不允许持续出现大于 50 ms 的控制周期;命令停止后 150200 ms 内归零。
### 阶段 5Nav2 控制和实车验证
1. 仅在阶段 14通过后启用 MPPI 运动测试。
2. 先在开阔区域发送短距离、低速度、带终点姿态的目标。
3. 记录 MPPI 循环耗时、`cmd_vel_nav -> cmd_vel -> odom` 响应和实际舵角响应。
4. 再测试近障碍和急转弯,不同时修改多个 critic 参数。
验收MPPI 平均循环小于 25 msP99 小于 45 ms障碍从 `/scan` 到 costmap 的端到端延迟满足目标;无持续恢复行为、振荡或控制周期丢失。
## 5. 当前结论
当前还不能宣称系统达到 50100 ms 障碍反应目标。雷达到障碍检测的平均链路已经接近 8090 ms但代价地图约 600 ms 发布一次DDS UDP-only 引起的高 CPU/高系统调用负载,以及底盘 `scan_to_odom` 的 624 ms 长尾仍是主要阻塞项。下一步应优先做 DDS A/B对比结果出来前不建议继续调 MPPI critic 权重。