找回密码
 立即注册
首页 业界区 安全 ROS1 noetic 中将 Unitree G1 基于 Gazebo/RViz 关节联 ...

ROS1 noetic 中将 Unitree G1 基于 Gazebo/RViz 关节联动

凳舒 2026-1-13 21:40:00
博客地址:https://www.cnblogs.com/zylyehuo/
基础:ROS1 noetic 中将 Unitree G1 的 URDF 导入 Gazebo/RViz


  • RViz 只是“眼睛”,Gazebo 才是“身体”。
  • RViz 滑条 (joint_state_publisher):它只是在广播“数据”:“我现在假设关节在 30 度”。RViz 听到了,就画给你看。
  • Gazebo:它模拟的是真实的物理电机。它听不到“假设”,它只接受“电机电压/扭矩指令”。
目前的连接是断开的: 滑条 GUI ---> /joint_states ---> RViz (更新画面) Gazebo (不仅没收到指令,而且现在的模型里连“电机”都没装,只有空壳)
要解决这个问题,需要做三件事


  • 给 URDF 装电机:添加  标签。
  • 配置控制器:生成 controllers.yaml。
  • 写个转换桥梁:把滑条的信号“翻译”成电机指令发给 Gazebo。
效果预览


运行 rviz_gazebo.py


运行 rviz_gazebo_fixed.py


第一步:安装必要的 ROS 控制包
  1. sudo apt-get install ros-noetic-ros-control ros-noetic-ros-controllers ros-noetic-gazebo-ros-control
复制代码
第二步:运行一键配置脚本

~/g1_ws/src/g1_description/scripts/rviz_gazebo.py

这份生成的机器人会由于没有控制代码而倒地
  1. #!/usr/bin/env python3import osimport sysimport xml.etree.ElementTree as ET# ================= 路径配置 =================SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))PKG_ROOT = os.path.dirname(SCRIPT_DIR)PKG_NAME = "g1_description"# 文件路径SRC_URDF = os.path.join(PKG_ROOT, "urdf/g1_29dof.urdf")DST_URDF = os.path.join(PKG_ROOT, "urdf/g1_complete.urdf")YAML_FILE = os.path.join(PKG_ROOT, "config/controllers.yaml")BRIDGE_SCRIPT = os.path.join(PKG_ROOT, "scripts/bridge_node.py")LAUNCH_GAZEBO = os.path.join(PKG_ROOT, "launch/1_gazebo.launch")LAUNCH_RVIZ = os.path.join(PKG_ROOT, "launch/2_rviz.launch")# 颜色映射COLOR_MAP = {    "white": "Gazebo/Grey",    "grey":  "Gazebo/Grey",    "dark":  "Gazebo/DarkGrey",    "black": "Gazebo/FlatBlack"}print(f"正在生成【全物理走路调试版】环境...")def generate_files():    # 1. 目录检查    os.makedirs(os.path.join(PKG_ROOT, "config"), exist_ok=True)    os.makedirs(os.path.join(PKG_ROOT, "launch"), exist_ok=True)        if not os.path.exists(SRC_URDF):        print(f"错误: 找不到 {SRC_URDF}")        sys.exit(1)    tree = ET.parse(SRC_URDF)    root = tree.getroot()    joint_names = []    # ================= 1. URDF 物理配置 (Floating Base) =================        # 1.1 移除所有 World 固定锚点 (彻底自由)    for link in root.findall('link'):        if link.get('name') == 'world': root.remove(link)    for joint in root.findall('joint'):        if joint.get('name') == 'world_anchor': root.remove(joint)    # 1.2 确保 base_link 存在 (作为浮动基座的根)    has_base = False    for link in root.findall('link'):        if link.get('name') == 'base_link': has_base = True    if not has_base:        base_link = ET.Element('link', name='base_link')        # 给 base_link 一个微小的惯性,防止 Gazebo 忽略它        inertial = ET.SubElement(base_link, 'inertial')        ET.SubElement(inertial, 'mass', value="0.01")        ET.SubElement(inertial, 'inertia', ixx="0.001", ixy="0", ixz="0", iyy="0.001", iyz="0", izz="0.001")        root.insert(0, base_link)                # 连接 base_link -> pelvis        joint = ET.Element('joint', name='base_to_pelvis', type='fixed')        ET.SubElement(joint, 'parent', link='base_link')        ET.SubElement(joint, 'child', link='pelvis')        ET.SubElement(joint, 'origin', xyz="0 0 0", rpy="0 0 0")        root.insert(1, joint)    # 1.3 物理参数精细化 (摩擦力与接触刚度)    # 清理所有旧的 gazebo 标签    for gazebo in root.findall('gazebo'):        root.remove(gazebo)    for link in root.findall('link'):        name = link.get('name')                # A. 惯性修复        inertial = link.find('inertial')        if inertial:            mass = inertial.find('mass')            if mass and float(mass.get('value')) < 0.05: mass.set('value', '0.05')            inertia = inertial.find('inertia')            if inertia:                for ax in ['ixx','iyy','izz']:                     if float(inertia.get(ax)) < 0.0001: inertia.set(ax, '0.0001')        # B. Gazebo 属性生成        gz = ET.SubElement(root, 'gazebo', reference=name)                # 开启重力!(这是走路的前提)        ET.SubElement(gz, 'turnGravityOff').text = 'false'        ET.SubElement(gz, 'selfCollide').text = 'false' # 暂时关闭自碰撞防止炸机        # 颜色匹配        visual = link.find('visual')        mat_name = "white"        if visual:            mat = visual.find('material')            if mat is not None: mat_name = mat.get('name', 'white').lower()        ET.SubElement(gz, 'material').text = COLOR_MAP.get(mat_name, "Gazebo/Grey")        # C. 【关键】脚底摩擦力增强        # 假设 link 名字里带 ankle_roll 的是脚底接触件        if "ankle_roll" in name:            # 高摩擦力,防止打滑            ET.SubElement(gz, 'mu1').text = "1.5"             ET.SubElement(gz, 'mu2').text = "1.5"            # 接触刚度 (kp) 和 阻尼 (kd)            # kp 越大越硬,kd 越大越不弹            ET.SubElement(gz, 'kp').text = "1000000.0"             ET.SubElement(gz, 'kd').text = "100.0"            # 最小穿透深度,防止抖动            ET.SubElement(gz, 'minDepth').text = "0.001"            ET.SubElement(gz, 'maxVel').text = "0.0" # 防止高速弹射    # ================= 2. 插件与电机 =================    # ros_control    gz_c = ET.SubElement(root, 'gazebo')    pl_c = ET.SubElement(gz_c, 'plugin', name="gazebo_ros_control", filename="libgazebo_ros_control.so")    ET.SubElement(pl_c, 'robotNamespace').text = f"/{PKG_NAME}"    ET.SubElement(pl_c, 'legacyModeNS').text = "true"    # P3D (真值反馈,用于做闭环控制)    gz_p3d = ET.SubElement(root, 'gazebo')    pl_p3d = ET.SubElement(gz_p3d, 'plugin', name="p3d_base_controller", filename="libgazebo_ros_p3d.so")    ET.SubElement(pl_p3d, 'alwaysOn').text = "true"    ET.SubElement(pl_p3d, 'updateRate').text = "100.0"    ET.SubElement(pl_p3d, 'bodyName').text = "pelvis"    ET.SubElement(pl_p3d, 'topicName').text = "ground_truth_odom"    ET.SubElement(pl_p3d, 'frameName').text = "world"    ET.SubElement(pl_p3d, 'xyzOffsets').text = "0 0 0"    ET.SubElement(pl_p3d, 'rpyOffsets').text = "0 0 0"    # 电机接口    for joint in root.findall('joint'):        if joint.get('type') in ['revolute', 'continuous', 'prismatic']:            name = joint.get('name')            joint_names.append(name)                        # 去重            is_dup = False            for tr in root.findall('transmission'):                if tr.get('name') == f"{name}_trans": is_dup = True                        if not is_dup:                trans = ET.SubElement(root, 'transmission', name=f"{name}_trans")                ET.SubElement(trans, 'type').text = "transmission_interface/SimpleTransmission"                j = ET.SubElement(trans, 'joint', name=name)                ET.SubElement(j, 'hardwareInterface').text = "hardware_interface/PositionJointInterface"                act = ET.SubElement(trans, 'actuator', name=f"{name}_motor")                ET.SubElement(act, 'mechanicalReduction').text = "1"                ET.SubElement(act, 'hardwareInterface').text = "hardware_interface/PositionJointInterface"    tree.write(DST_URDF, encoding='utf-8', xml_declaration=True)    # ================= 3. PID 参数 (支撑级) =================    with open(YAML_FILE, 'w') as f:        f.write(f"{PKG_NAME}:\n")        f.write("  joint_state_controller:\n")        f.write("    type: joint_state_controller/JointStateController\n")        f.write("    publish_rate: 100\n\n")                for jn in joint_names:            f.write(f"  {jn}_controller:\n")            f.write("    type: position_controllers/JointPositionController\n")            f.write(f"    joint: {jn}\n")            # 这里的 PID 是控制器层面的,可以稍大            f.write("    pid: {p: 800.0, i: 0.0, d: 20.0}\n")                # Gazebo 物理引擎 PID (核心)        f.write("\n  gazebo_ros_control:\n    pid_gains:\n")        for jn in joint_names:            if "ankle" in jn or "knee" in jn or "hip" in jn:                # 腿部:需要大力矩支撑体重,但阻尼要适中防止震荡                # P=800~1000 足够支撑 G1 这种体型的机器人                f.write(f"      {jn}: {{p: 1000.0, i: 0.1, d: 50.0}}\n")            else:                # 手臂:不需要太大                f.write(f"      {jn}: {{p: 300.0, i: 0.0, d: 5.0}}\n")    # ================= 4. Bridge Script =================    with open(BRIDGE_SCRIPT, 'w') as f:        f.write(f"""#!/usr/bin/env python3import rospyfrom sensor_msgs.msg import JointStatefrom std_msgs.msg import Float64class Bridge:    def __init__(self):        rospy.init_node('rviz_bridge')        self.pubs = {{}}        self.ns = "/{PKG_NAME}"        rospy.Subscriber("/gui_commands", JointState, self.cb)    def cb(self, msg):        for i, n in enumerate(msg.name):            if n not in self.pubs: self.pubs[n] = rospy.Publisher(f"{{self.ns}}/{{n}}_controller/command", Float64, queue_size=10)            self.pubs[n].publish(msg.position[i])if __name__ == '__main__': Bridge(); rospy.spin()""")    os.chmod(BRIDGE_SCRIPT, 0o755)    # ================= 5. Launch Gazebo (Walk Ready) =================    ctl_str = " ".join([f"{jn}_controller" for jn in joint_names])    urdf_rel = "urdf/g1_complete.urdf"    yaml_rel = "config/controllers.yaml"        with open(LAUNCH_GAZEBO, 'w') as f:        f.write(f"""                             """)    # ================= 6. Launch RViz =================    with open(LAUNCH_RVIZ, 'w') as f:        f.write(f"""            """)    print("\n[完成] 调试环境已就绪!")    print("-------------------------------------------------------")    print("1. python3 scripts/rviz_gazebo.py")    print("2. cd ~/g1_ws && catkin_make && source devel/setup.bash")    print("3. roslaunch g1_description 1_gazebo.launch")    print("   -> 注意:Gazebo 启动是暂停的。点击 Play 后,机器人会下落到地面。")    print("   -> 如果参数合适,它会保持蹲姿平衡;如果不合适,它会倒。")    print("-------------------------------------------------------")if __name__ == "__main__":    generate_files()
复制代码
~/g1_ws/src/g1_description/scripts/rviz_gazebo_fixed.py

这份生成的机器人会固定在世界系中
  1. #!/usr/bin/env python3import osimport sysimport xml.etree.ElementTree as ET# ================= 路径配置 =================SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))PKG_ROOT = os.path.dirname(SCRIPT_DIR)PKG_NAME = "g1_description"# 文件路径SRC_URDF = os.path.join(PKG_ROOT, "urdf/g1_29dof.urdf")DST_URDF = os.path.join(PKG_ROOT, "urdf/g1_complete.urdf")YAML_FILE = os.path.join(PKG_ROOT, "config/controllers.yaml")BRIDGE_SCRIPT = os.path.join(PKG_ROOT, "scripts/bridge_node.py")LAUNCH_GAZEBO = os.path.join(PKG_ROOT, "launch/1_gazebo.launch")LAUNCH_RVIZ = os.path.join(PKG_ROOT, "launch/2_rviz.launch")# 颜色映射表 (URDF材质名 -> Gazebo材质名)COLOR_MAP = {    "white": "Gazebo/Grey",       # 银灰色    "grey":  "Gazebo/Grey",    "dark":  "Gazebo/DarkGrey",   # 深黑色    "black": "Gazebo/FlatBlack"}print(f"正在生成【完美静止 + 颜色修复版】配置文件...")def generate_files():    os.makedirs(os.path.join(PKG_ROOT, "config"), exist_ok=True)    os.makedirs(os.path.join(PKG_ROOT, "launch"), exist_ok=True)        if not os.path.exists(SRC_URDF):        print(f"错误: 找不到 {SRC_URDF}")        sys.exit(1)    tree = ET.parse(SRC_URDF)    root = tree.getroot()    joint_names = []    # ================= 1. 结构修复 (Base Link & World Anchor) =================    # 1.1 补全 base_link    has_base = False    for link in root.findall('link'):        if link.get('name') == 'base_link': has_base = True    if not has_base:        base_link = ET.Element('link', name='base_link')        root.insert(0, base_link)        joint = ET.Element('joint', name='base_to_pelvis', type='fixed')        ET.SubElement(joint, 'parent', link='base_link')        ET.SubElement(joint, 'child', link='pelvis')        ET.SubElement(joint, 'origin', xyz="0 0 0", rpy="0 0 0")        root.insert(1, joint)    # 1.2 添加 World 锚点 (固定在 0.79m)    has_world = False    for link in root.findall('link'):         if link.get('name') == 'world': has_world = True    if not has_world:        world_link = ET.Element('link', name='world')        root.insert(0, world_link)        anchor = ET.Element('joint', name='world_anchor', type='fixed')        ET.SubElement(anchor, 'parent', link='world')        ET.SubElement(anchor, 'child', link='base_link')        ET.SubElement(anchor, 'origin', xyz="0 0 0.79", rpy="0 0 0")        root.insert(1, anchor)    # ================= 2. 颜色与物理修复 (核心) =================    # 先清理所有现有的 gazebo 标签,避免冲突    for gazebo in root.findall('gazebo'):        root.remove(gazebo)    # 遍历所有 Link,重新生成属性    for link in root.findall('link'):        name = link.get('name')        if name == 'world': continue        # --- A. 物理惯性清洗 ---        inertial = link.find('inertial')        if inertial:            mass = inertial.find('mass')            if mass:                if float(mass.get('value')) < 0.01: mass.set('value', '0.01')            inertia = inertial.find('inertia')            if inertia:                for ax in ['ixx','iyy','izz']:                     if float(inertia.get(ax)) < 0.001: inertia.set(ax, '0.001')        # --- B. 生成 Gazebo 标签 (颜色 + 物理) ---        gz = ET.SubElement(root, 'gazebo', reference=name)                # B1. 物理属性 (无重力,无碰撞)        ET.SubElement(gz, 'turnGravityOff').text = 'true'        ET.SubElement(gz, 'selfCollide').text = 'false'        # B2. 颜色自动匹配        # 查找该 link 下的 visual -> material -> name        visual = link.find('visual')        mat_name = "white" # 默认值        if visual:            mat = visual.find('material')            if mat is not None:                mat_name = mat.get('name', 'white').lower()                # 根据字典映射颜色        gz_color = COLOR_MAP.get(mat_name, "Gazebo/Grey")        ET.SubElement(gz, 'material').text = gz_color    # ================= 3. 插件与电机 =================    gz_c = ET.SubElement(root, 'gazebo')    pl_c = ET.SubElement(gz_c, 'plugin', name="gazebo_ros_control", filename="libgazebo_ros_control.so")    ET.SubElement(pl_c, 'robotNamespace').text = f"/{PKG_NAME}"    ET.SubElement(pl_c, 'legacyModeNS').text = "true"    for joint in root.findall('joint'):        if joint.get('type') in ['revolute', 'continuous', 'prismatic']:            name = joint.get('name')            joint_names.append(name)                        # 检查重复            is_dup = False            for tr in root.findall('transmission'):                if tr.get('name') == f"{name}_trans": is_dup = True                        if not is_dup:                trans = ET.SubElement(root, 'transmission', name=f"{name}_trans")                ET.SubElement(trans, 'type').text = "transmission_interface/SimpleTransmission"                j = ET.SubElement(trans, 'joint', name=name)                ET.SubElement(j, 'hardwareInterface').text = "hardware_interface/PositionJointInterface"                act = ET.SubElement(trans, 'actuator', name=f"{name}_motor")                ET.SubElement(act, 'mechanicalReduction').text = "1"                ET.SubElement(act, 'hardwareInterface').text = "hardware_interface/PositionJointInterface"    tree.write(DST_URDF, encoding='utf-8', xml_declaration=True)    # ================= 4. PID Config (柔和防抖) =================    with open(YAML_FILE, 'w') as f:        f.write(f"{PKG_NAME}:\n")        f.write("  joint_state_controller:\n")        f.write("    type: joint_state_controller/JointStateController\n")        f.write("    publish_rate: 100\n\n")        for jn in joint_names:            f.write(f"  {jn}_controller:\n")            f.write("    type: position_controllers/JointPositionController\n")            f.write(f"    joint: {jn}\n")            # 这里的 PID 可以小一点,因为 Gazebo 物理层有自己的 PID            f.write("    pid: {p: 100.0, i: 0.0, d: 1.0}\n")                # Gazebo 物理 PID (P=50, D=0.5 足够驱动失重关节)        f.write("\n  gazebo_ros_control:\n    pid_gains:\n")        for jn in joint_names:            f.write(f"      {jn}: {{p: 50.0, i: 0.0, d: 0.5}}\n")    # ================= 5. Bridge Script =================    with open(BRIDGE_SCRIPT, 'w') as f:        f.write(f"""#!/usr/bin/env python3import rospyfrom sensor_msgs.msg import JointStatefrom std_msgs.msg import Float64class Bridge:    def __init__(self):        rospy.init_node('rviz_bridge')        self.pubs = {{}}        self.ns = "/{PKG_NAME}"        rospy.Subscriber("/gui_commands", JointState, self.cb)    def cb(self, msg):        for i, n in enumerate(msg.name):            if n not in self.pubs: self.pubs[n] = rospy.Publisher(f"{{self.ns}}/{{n}}_controller/command", Float64, queue_size=10)            self.pubs[n].publish(msg.position[i])if __name__ == '__main__': Bridge(); rospy.spin()""")    os.chmod(BRIDGE_SCRIPT, 0o755)    # ================= 6. Launch Gazebo =================    ctl_str = " ".join([f"{jn}_controller" for jn in joint_names])    urdf_rel = "urdf/g1_complete.urdf"    yaml_rel = "config/controllers.yaml"        with open(LAUNCH_GAZEBO, 'w') as f:        f.write(f"""                                """)    # ================= 7. Launch RViz =================    with open(LAUNCH_RVIZ, 'w') as f:        f.write(f"""            """)    print("-------------------------------------------------------")    print("1. python3 scripts/rviz_gazebo.py")    print("2. cd ~/g1_ws && catkin_make && source devel/setup.bash")    print("3. roslaunch g1_description 1_gazebo.launch")    print("4. roslaunch g1_description 2_rviz.launch")    print("-------------------------------------------------------")if __name__ == "__main__":    generate_files()
复制代码
第三步:运行配置脚本
  1. python3 ~/g1_ws/src/g1_description/scripts/rviz_gazebo.py# python3 ~/g1_ws/src/g1_description/scripts/rviz_gazebo_fixed.py
复制代码
第四步:编译并运行
  1. cd ~/g1_wscatkin_makesource devel/setup.bash
复制代码
终端 1 (仿真)
  1. roslaunch g1_description 1_gazebo.launch
复制代码
终端 2 (控制)
  1. roslaunch g1_description 2_rviz.launch
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

2026-2-9 00:27:16

举报

2026-2-13 19:53:07

举报

2026-2-16 20:31:08

举报

您需要登录后才可以回帖 登录 | 立即注册