在 Windows 11 环境中完成 YOLOv8 训练,类别统一定义为 carnation_flower,评价指标采用 Precision、Recall、mAP@0.5 和 mAP@0.5:0.95。
第四章:视觉识别与控制系统
论文第四章的核心目标,是把康乃馨目标识别、坐标转换、机器人模型描述、阶段动作控制和 Launch 组织连接为一条完整的采收链路。YOLOv8 负责识别可采收花朵目标,输出图像像素信息; 坐标转换程序将视觉信息变为世界坐标;ROS 2 节点根据目标位姿生成关节运动;URDF 与 RViz2 / Gazebo 则承担机器人显示与仿真验证。
原始图像总数 630 张,筛选后保留 600 张;当前标注样本为 480 张,目录按 images / labels 与 train / val 分层组织。
simple_target_follower.py 订阅 /target_pose,发布 /joint_states 与 /target_marker,按既定阶段执行扶持、剪切、回撤与复位。
ROS 2 Jazzy、RViz2、Gazebo Sim 和 robot_state_publisher 共同负责模型描述、TF 计算、可视化与仿真验证。
4.1 视觉识别流程设计
第四章首先定义了感知链路:自动采收机在作业场景中识别可采收花朵,输出双目像素坐标,再交给坐标转换程序计算世界坐标, 作为机械臂控制的姿态输入。训练平台采用 Windows 11 25H2,搭配 Python 3.12、CUDA 12.7、PyTorch 2.6.0+cu124 与 Ultralytics YOLO 8.4.37。
图像采集位于真实温室花棚环境,既覆盖正午强光也覆盖阴天弱光,拍摄距离约 200 到 1500 mm,包含遮挡、重叠和复杂背景。 数据筛选后保留 600 张,当前用于标注的图像数量为 480 张。标注工具为 LabelImg 1.8.6,原始标签保存为 Pascal VOC, 后续再转换为 YOLO TXT 格式,满足训练需求。
4.2 ROS 2 平台与 WSL Ubuntu 部署
运动规划与仿真系统部署在 WSL Ubuntu 环境,工作区路径设为 /root/carnation_harvest。论文列出的软件栈包括:
Ubuntu 24.04.4 LTS、ROS 2 Jazzy、Python 3.12.3、colcon 0.20.1、RViz2 14.1.19、Gazebo Sim 8.10.0、
ros_gz_sim 1.0.18、rclpy 7.1.9、robot_state_publisher 3.3.3、tf2_ros 0.36.19、moveit_ros 2.12.4、
moveit_py 2.12.4、ultralytics 8.4.24 与 numpy 1.26.4。
这一层的作用不是做重计算,而是把检测模型输出的目标位置接入 ROS 节点通信、TF 发布、路径规划和三维显示,让整套“感知 - 定位 - 执行” 过程在同一个软件栈内闭环验证。
4.2.1 真实工作区结构
现在首页使用的源码不再只来自论文附录,而是来自你从
\\wsl.localhost\Ubuntu-24.04\root\carnation_harvest 复制出来的真实控制系统目录。
当前可确认的工作区核心位于 src 下,至少包含两个有效包:
carnation_description 与 carnation_planning;
同时还保留了 carnation_interfaces、carnation_localization、
carnation_perception、carnation_simulation、
carnation_task_manager 等后续扩展目录。
控制系统目录结构
src/
├─ carnation_description/
│ ├─ config/
│ │ └─ display.rviz
│ ├─ launch/
│ │ └─ display.launch.py
│ ├─ meshes/
│ │ ├─ base_link.stl
│ │ ├─ upper_lift_link.stl
│ │ ├─ lower_lift_link.stl
│ │ ├─ upper_arm_link.stl
│ │ └─ lower_arm_link.stl
│ └─ urdf/
│ └─ carnation_robot_v0.urdf
├─ carnation_planning/
│ ├─ carnation_planning/
│ │ └─ simple_target_follower.py
│ ├─ config/
│ │ └─ simple_target_follower.yaml
│ └─ launch/
│ └─ simple_alignment.launch.py
├─ carnation_interfaces/
├─ carnation_localization/
├─ carnation_perception/
├─ carnation_simulation/
└─ carnation_task_manager/
整个目录去掉 __pycache__ 后约 40 MB,其中绝大部分体积来自五个 STL 网格文件。
这说明把“完整工作区”直接挂到首页并不划算;更合理的做法是继续保持文章页纯文本化,把真正有用的控制逻辑、Launch 和参数文件嵌入网页。
4.3 URDF 结构
机器人描述文件以 world 为参考坐标系,并增加 world_aligned_link 作为整机姿态修正中间层。
之后通过 base_move_joint 完成整机过道平移,再由上下机械臂的升降与伸缩关节完成扶持和剪切动作。论文附录 D 给出了
URDF 源码,下面保留关键结构摘录。
附录 D | URDF 关键摘录
<?xml version="1.0"?>
<robot name="carnation_robot">
<link name="world"/>
<link name="world_aligned_link"/>
<joint name="world_align_joint" type="fixed">
<parent link="world"/>
<child link="world_aligned_link"/>
<origin xyz="0 0 0.20" rpy="1.5708 0 0"/>
</joint>
<link name="base_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="package://carnation_description/meshes/base_link.stl"
scale="0.001 0.001 0.001"/>
</geometry>
</visual>
</link>
<joint name="base_move_joint" type="prismatic">
<parent link="world_aligned_link"/>
<child link="base_link"/>
<origin xyz="0 0 0" rpy="0 0 0"/>
<axis xyz="1 0 0"/>
<limit lower="0.0" upper="5.0" effort="500.0" velocity="0.5"/>
</joint>
<joint name="upper_lift_joint" type="prismatic">
<parent link="base_link"/>
<child link="upper_lift_link"/>
<origin xyz="0 -0.12 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<limit lower="0.0" upper="0.20" effort="200.0" velocity="0.05"/>
</joint>
<joint name="upper_extend_joint" type="prismatic">
<parent link="upper_lift_link"/>
<child link="upper_arm_link"/>
<origin xyz="0 0 0.12" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit lower="0.0" upper="-0.37" effort="150.0" velocity="0.05"/>
</joint>
<joint name="lower_lift_joint" type="prismatic">
<parent link="base_link"/>
<child link="lower_lift_link"/>
<origin xyz="0 -0.05 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<limit lower="0.0" upper="0.20" effort="200.0" velocity="0.05"/>
</joint>
<joint name="lower_extend_joint" type="prismatic">
<parent link="lower_lift_link"/>
<child link="lower_arm_link"/>
<origin xyz="0 0 0.08" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit lower="0.0" upper="-0.37" effort="150.0" velocity="0.05"/>
</joint>
</robot>
4.4 分阶段动作控制
论文将采收动作拆成连续的阶段序列:base_align、lift_align、
upper_hold_extend、lower_cut_extend、lower_retract、
upper_lift_raise、upper_retract、lift_reset。
先移动整机对准花朵,再调整上下机械臂,随后执行扶持与剪切,最后回撤并复位。
控制节点通过目标位姿到关节变量的直接映射工作,结合零点偏置、比例系数和关节限位,分别计算各运动轴目标值。
代码中还为夹持和剪切预留了两个停顿时间参数:cut_pause_seconds 与 release_pause_seconds,
默认值均为 2.0 s。
源码摘录
这一节收录控制系统中的关键代码与配置文件:
simple_target_follower.py、simple_target_follower.yaml、
display.launch.py、simple_alignment.launch.py 与 URDF 关键结构。
规划参数 | simple_target_follower.yaml
这个配置文件把各关节的坐标映射、零点、缩放系数、限位和速度集中到参数层,
使 simple_target_follower 可以通过 YAML 调整,而不必反复改 Python 代码。
simple_target_follower:
ros__parameters:
# 节点循环频率,决定 joint_states 的刷新速度
publish_rate_hz: 20.0
# 要求输入目标位姿位于 world 坐标系中
expected_target_frame: world
# RViz 里目标点球体的显示尺寸
marker_scale: 0.05
# base_move_joint 负责整机沿过道方向移动
"base_move_joint.source_axis": "x"
"base_move_joint.zero_world_value": 0.0
"base_move_joint.scale": 1.0
"base_move_joint.min": 0.0
"base_move_joint.max": 7.0
"base_move_joint.speed": 0.50
"base_move_joint.initial": 0.0
# upper_lift_joint 负责上机械臂升降
"upper_lift_joint.source_axis": "z"
"upper_lift_joint.zero_world_value": 0.82
"upper_lift_joint.scale": 1.0
"upper_lift_joint.min": 0.0
"upper_lift_joint.max": 0.23
"upper_lift_joint.speed": 0.10
"upper_lift_joint.initial": 0.0
# upper_extend_joint 负责上机械臂前伸扶持
"upper_extend_joint.source_axis": "y"
"upper_extend_joint.zero_world_value": 0.35
"upper_extend_joint.scale": -1.0
"upper_extend_joint.min": -0.37
"upper_extend_joint.max": 0.0
"upper_extend_joint.speed": 0.08
"upper_extend_joint.initial": 0.0
# lower_lift_joint 负责下机械臂升降
"lower_lift_joint.source_axis": "z"
"lower_lift_joint.zero_world_value": 0.82
"lower_lift_joint.scale": 1.0
"lower_lift_joint.min": 0.0
"lower_lift_joint.max": 0.23
"lower_lift_joint.speed": 0.1
"lower_lift_joint.initial": 0.0
# lower_extend_joint 负责下机械臂伸出剪切
"lower_extend_joint.source_axis": "y"
"lower_extend_joint.zero_world_value": 0.35
"lower_extend_joint.scale": -1.0
"lower_extend_joint.min": -0.37
"lower_extend_joint.max": 0.0
"lower_extend_joint.speed": 0.08
"lower_extend_joint.initial": 0.0
附录 E | simple_target_follower.py
# 用 dataclass 描述关节映射关系与阶段动作
from dataclasses import dataclass
from tkinter import SEL
from typing import Dict, List, Optional
# ROS 2 消息与节点依赖
import rclpy
from geometry_msgs.msg import PoseStamped
from rclpy.node import Node
from sensor_msgs.msg import JointState
from visualization_msgs.msg import Marker
# 单个关节的映射配置:
# 将世界坐标中的某个轴映射到某个关节目标值
@dataclass
class JointMapping:
name: str
source_axis: str
zero_world_value: float
scale: float
minimum: float
maximum: float
speed: float
initial: float
@dataclass
class MotionPhase:
name: str
targets: Dict[str, float]
pause_seconds: float = 0.0
# 核心控制节点:
# 订阅目标位姿,按顺序生成一组关节动作
class SimpleTargetFollower(Node):
def __init__(self) -> None:
super().__init__("simple_target_follower")
# 基础运行参数
self.declare_parameter("publish_rate_hz", 20.0)
self.declare_parameter("target_topic", "/target_pose")
self.declare_parameter("joint_state_topic", "/joint_states")
self.declare_parameter("target_marker_topic", "/target_marker")
self.declare_parameter("expected_target_frame", "world")
self.declare_parameter("marker_scale", 0.05)
self.declare_parameter("phase_tolerance", 0.002)
self.declare_parameter("release_pause_seconds", 2.0)
self.declare_parameter("cut_pause_seconds", 2.0)
# 每个关节的默认映射与约束
joint_defaults = {
"base_move_joint": {
"source_axis": "x",
"zero_world_value": 0.0,
"scale": 1.0,
"min": 0.0,
"max": 7.0,
"speed": 0.60,
"initial": 0.0,
},
"upper_lift_joint": {
"source_axis": "z",
"zero_world_value": 0.82,
"scale": 1.0,
"min": 0.0,
"max": 0.23,
"speed": 0.10,
"initial": 0.0,
},
"upper_extend_joint": {
"source_axis": "y",
"zero_world_value": 0.35,
"scale": -1.0,
"min": -0.37,
"max": 0.0,
"speed": 0.08,
"initial": 0.0,
},
"lower_lift_joint": {
"source_axis": "z",
"zero_world_value": 0.82,
"scale": 1.0,
"min": 0.0,
"max": 0.23,
"speed": 0.10,
"initial": 0.0,
},
"lower_extend_joint": {
"source_axis": "y",
"zero_world_value": 0.35,
"scale": -1.0,
"min": -0.37,
"max": 0.0,
"speed": 0.08,
"initial": 0.0,
},
}
# 读取参数并转成 JointMapping 结构,便于后续统一计算
self.joint_mappings: List[JointMapping] = []
for joint_name, defaults in joint_defaults.items():
self.declare_parameter(f"{joint_name}.source_axis", defaults["source_axis"])
self.declare_parameter(f"{joint_name}.zero_world_value", defaults["zero_world_value"])
self.declare_parameter(f"{joint_name}.scale", defaults["scale"])
self.declare_parameter(f"{joint_name}.min", defaults["min"])
self.declare_parameter(f"{joint_name}.max", defaults["max"])
self.declare_parameter(f"{joint_name}.speed", defaults["speed"])
self.declare_parameter(f"{joint_name}.initial", defaults["initial"])
self.joint_mappings.append(
JointMapping(
name=joint_name,
source_axis=str(self.get_parameter(f"{joint_name}.source_axis").value),
zero_world_value=float(self.get_parameter(f"{joint_name}.zero_world_value").value),
scale=float(self.get_parameter(f"{joint_name}.scale").value),
minimum=float(self.get_parameter(f"{joint_name}.min").value),
maximum=float(self.get_parameter(f"{joint_name}.max").value),
speed=float(self.get_parameter(f"{joint_name}.speed").value),
initial=float(self.get_parameter(f"{joint_name}.initial").value),
)
)
# 将常用参数缓存到成员变量
self.publish_rate_hz = float(self.get_parameter("publish_rate_hz").value)
self.expected_target_frame = str(self.get_parameter("expected_target_frame").value)
self.marker_scale = float(self.get_parameter("marker_scale").value)
self.phase_tolerance = float(self.get_parameter("phase_tolerance").value)
self.release_pause_seconds = float(self.get_parameter("release_pause_seconds").value)
self.cut_phase_seconds = float(self.get_parameter("cut_pause_seconds").value)
# 按名称建立索引,便于后续快速访问关节配置
self.mapping_by_name: Dict[str, JointMapping] = {
mapping.name: mapping for mapping in self.joint_mappings
}
# current_positions 记录当前状态,target_positions 记录阶段目标
self.current_positions: Dict[str, float] = {
mapping.name: mapping.initial for mapping in self.joint_mappings
}
self.target_positions: Dict[str, float] = dict(self.current_positions)
# 阶段队列状态
self.sequence_active = False
self.phase_queue: List[MotionPhase] = []
self.phase_index = -1
self.phase_hold_until: Optional[float] = None
self.phase_logged_complete = False
target_topic = str(self.get_parameter("target_topic").value)
joint_state_topic = str(self.get_parameter("joint_state_topic").value)
marker_topic = str(self.get_parameter("target_marker_topic").value)
# 输入目标位姿,输出 joint_states 和 target_marker
self.create_subscription(PoseStamped, target_topic, self._target_callback, 10)
self.joint_state_publisher = self.create_publisher(JointState, joint_state_topic, 10)
self.marker_publisher = self.create_publisher(Marker, marker_topic, 10)
self.create_timer(1.0 / self.publish_rate_hz, self._timer_callback)
# 收到新目标后,先做合法性检查,再生成本轮采收阶段队列
def _target_callback(self, message: PoseStamped) -> None:
frame_id = message.header.frame_id or self.expected_target_frame
if frame_id != self.expected_target_frame:
return
if self.sequence_active:
return
coordinates = {
"x": float(message.pose.position.x),
"y": float(message.pose.position.y),
"z": float(message.pose.position.z),
}
joint_targets = self._compute_joint_targets(coordinates)
self._build_phase_queue(joint_targets)
self._publish_target_marker(message)
self._advance_phase()
# 在 RViz 中发布目标点 Marker,方便观察目标位置
def _publish_target_marker(self, message: PoseStamped) -> None:
marker = Marker()
marker.header = message.header
marker.ns = "target"
marker.id = 0
marker.type = Marker.SPHERE
marker.action = Marker.ADD
marker.pose = message.pose
marker.scale.x = self.marker_scale
marker.scale.y = self.marker_scale
marker.scale.z = self.marker_scale
marker.color.r = 0.95
marker.color.g = 0.1
marker.color.b = 0.1
marker.color.a = 1.0
self.marker_publisher.publish(marker)
# 定时器负责推进阶段状态,并按速度限制逐步逼近目标位置
def _timer_callback(self) -> None:
dt = 1.0 / self.publish_rate_hz
if self.sequence_active:
self._update_phase_progress()
# 按各关节速度上限平滑逼近目标,而不是一步跳到位
for mapping in self.joint_mappings:
current = self.current_positions[mapping.name]
target = self.target_positions[mapping.name]
max_step = mapping.speed * dt
delta = target - current
if abs(delta) <= max_step:
self.current_positions[mapping.name] = target
else:
direction = 1.0 if delta > 0.0 else -1.0
self.current_positions[mapping.name] = current + max_step * direction
# 将最新关节位置发布给 robot_state_publisher / RViz
message = JointState()
message.header.stamp = self.get_clock().now().to_msg()
message.name = [mapping.name for mapping in self.joint_mappings]
message.position = [self.current_positions[mapping.name] for mapping in self.joint_mappings]
self.joint_state_publisher.publish(message)
# 根据 x / y / z 坐标映射出各个关节的目标值,并做限位裁剪
def _compute_joint_targets(self, coordinates: Dict[str, float]) -> Dict[str, float]:
joint_targets: Dict[str, float] = {}
for mapping in self.joint_mappings:
raw_value = mapping.scale * (coordinates[mapping.source_axis] - mapping.zero_world_value)
lower = min(mapping.minimum, mapping.maximum)
upper = max(mapping.minimum, mapping.maximum)
joint_targets[mapping.name] = min(max(raw_value, lower), upper)
return joint_targets
# 回到初始姿态,但保留底盘当前对齐后的 x 位置
def _home_targets_with_base(self, base_value: float) -> Dict[str, float]:
targets = {mapping.name: mapping.initial for mapping in self.joint_mappings}
targets["base_move_joint"] = base_value
return targets
# 生成完整采收序列:
# 对准 -> 升降对齐 -> 上臂扶持 -> 下臂剪切 -> 回撤 -> 复位
def _build_phase_queue(self, joint_targets: Dict[str, float]) -> None:
base_target = joint_targets["base_move_joint"]
home_targets = self._home_targets_with_base(base_target)
base_align = dict(home_targets)
base_align["base_move_joint"] = base_target
lift_align = dict(base_align)
lift_align["upper_lift_joint"] = joint_targets["upper_lift_joint"]
lift_align["lower_lift_joint"] = joint_targets["lower_lift_joint"]
upper_hold = dict(lift_align)
upper_hold["upper_extend_joint"] = joint_targets["upper_extend_joint"]
lower_cut = dict(upper_hold)
lower_cut["lower_extend_joint"] = joint_targets["lower_extend_joint"]
lower_retract = dict(lower_cut)
lower_retract["lower_extend_joint"] = self.mapping_by_name["lower_extend_joint"].initial
upper_lift_raise = dict(lower_retract)
upper_lift_raise["upper_lift_joint"] = max(
self.mapping_by_name["upper_lift_joint"].minimum,
self.mapping_by_name["upper_lift_joint"].maximum,
)
upper_retract = dict(upper_lift_raise)
upper_retract["upper_extend_joint"] = self.mapping_by_name["upper_extend_joint"].initial
lift_reset = dict(home_targets)
lift_reset["base_move_joint"] = base_target
self.phase_queue = [
MotionPhase("base_align", base_align),
MotionPhase("lift_align", lift_align),
MotionPhase("upper_hold_extend", upper_hold),
MotionPhase("lower_cut_extend", lower_cut, pause_seconds=self.cut_phase_seconds),
MotionPhase("lower_retract", lower_retract),
MotionPhase("upper_lift_raise", upper_lift_raise),
MotionPhase("upper_retract", upper_retract, pause_seconds=self.release_pause_seconds),
MotionPhase("lift_reset", lift_reset),
]
self.phase_index = -1
self.phase_hold_until = None
self.phase_logged_complete = False
self.sequence_active = True
# 进入下一个阶段,并把该阶段目标写入 target_positions
def _advance_phase(self) -> None:
self.phase_index += 1
self.phase_hold_until = None
self.phase_logged_complete = False
if self.phase_index >= len(self.phase_queue):
self.sequence_active = False
self.phase_queue = []
return
phase = self.phase_queue[self.phase_index]
self.target_positions = dict(phase.targets)
# 判断当前阶段是否到位;如设置了 pause_seconds,则先等待再切换阶段
def _update_phase_progress(self) -> None:
if not self.sequence_active or self.phase_index >= len(self.phase_queue):
return
phase = self.phase_queue[self.phase_index]
if self._targets_reached(phase.targets):
if phase.pause_seconds > 0.0:
if self.phase_hold_until is None:
self.phase_hold_until = self.get_clock().now().nanoseconds / 1e9 + phase.pause_seconds
return
if self.get_clock().now().nanoseconds / 1e9 < self.phase_hold_until:
return
self._advance_phase()
# 使用 phase_tolerance 判断当前关节组是否已经达到目标
def _targets_reached(self, targets: Dict[str, float]) -> bool:
return all(
abs(self.current_positions[joint_name] - target_value) <= self.phase_tolerance
for joint_name, target_value in targets.items()
)
# ROS 2 节点标准入口
def main() -> None:
rclpy.init()
node = SimpleTargetFollower()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
附录 F | simple_alignment.launch.py
这个 Launch 文件负责装配整套演示链路:加载 URDF,启动
simple_target_follower 和 robot_state_publisher,
并通过 use_rviz 开关控制 RViz2 是否在 5 秒后延迟启动。
# Launch 文件:负责把规划节点、机器人状态发布和 RViz 串起来
from pathlib import Path
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, TimerAction
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
# 获取两个包的 share 目录,方便读取 urdf / yaml / rviz 配置
description_share = Path(get_package_share_directory("carnation_description"))
planning_share = Path(get_package_share_directory("carnation_planning"))
# 预设模型、RViz 配置和控制参数文件
default_model = description_share / "urdf" / "carnation_robot_v0.urdf"
default_rviz_config = description_share / "config" / "display.rviz"
default_planner_config = planning_share / "config" / "simple_target_follower.yaml"
# robot_state_publisher 直接读取 URDF 文本
robot_description = {"robot_description": default_model.read_text(encoding="utf-8")}
use_rviz = LaunchConfiguration("use_rviz")
return LaunchDescription(
[
DeclareLaunchArgument(
"use_rviz",
default_value="true",
description="Start RViz with the default robot model config.",
),
# 控制节点:订阅 /target_pose,输出 /joint_states 与 /target_marker
Node(
package="carnation_planning",
executable="simple_target_follower",
name="simple_target_follower",
parameters=[str(default_planner_config)],
output="screen",
),
# 根据 joint_states 和 URDF 计算整棵 TF 树
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
name="robot_state_publisher",
parameters=[robot_description],
output="screen",
),
# 延迟启动 RViz,给前面两个节点留出初始化时间
TimerAction(
period=5.0,
actions=[
Node(
package="rviz2",
executable="rviz2",
name="rviz2",
arguments=["-d", str(default_rviz_config)],
condition=IfCondition(use_rviz),
output="screen",
)
],
),
]
)
模型显示 | display.launch.py
这份 Launch 用于单独显示机器人模型。它支持在
joint_state_publisher_gui 与普通
joint_state_publisher 间切换,并可直接拉起 RViz2 查看 URDF 和 TF。
# Launch 文件:单独用于显示机器人模型与 TF 结构
from pathlib import Path
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
# 获取模型和 RViz 配置文件
package_share = Path(get_package_share_directory("carnation_description"))
default_model = package_share / "urdf" / "carnation_robot_v0.urdf"
default_rviz_config = package_share / "config" / "display.rviz"
# robot_state_publisher 读取 URDF 文本并发布到 /robot_description
robot_description = {"robot_description": default_model.read_text(encoding="utf-8")}
# 两个开关:
# 1. 是否用 GUI 手动调关节
# 2. 是否启动 RViz
use_joint_state_gui = LaunchConfiguration("use_joint_state_gui")
use_rviz = LaunchConfiguration("use_rviz")
return LaunchDescription(
[
DeclareLaunchArgument(
"use_joint_state_gui",
default_value="true",
description="Start joint_state_publisher_gui instead of the CLI publisher.",
),
DeclareLaunchArgument(
"use_rviz",
default_value="true",
description="Start RViz with the default robot model config.",
),
# GUI 版本的 joint_state_publisher,适合手动拖关节调姿态
Node(
package="joint_state_publisher_gui",
executable="joint_state_publisher_gui",
name="joint_state_publisher_gui",
arguments=[str(default_model)],
condition=IfCondition(use_joint_state_gui),
),
# 非 GUI 版本,适合无界面环境
Node(
package="joint_state_publisher",
executable="joint_state_publisher",
name="joint_state_publisher",
arguments=[str(default_model)],
condition=UnlessCondition(use_joint_state_gui),
),
# 依据 URDF + joint_states 发布各 link 的坐标关系
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
name="robot_state_publisher",
parameters=[robot_description],
output="screen",
),
# 打开 RViz 进行模型、TF 与 Marker 可视化
Node(
package="rviz2",
executable="rviz2",
name="rviz2",
arguments=["-d", str(default_rviz_config)],
condition=IfCondition(use_rviz),
output="screen",
),
]
)