ROS1从入门到精通 11:Gazebo仿真(打造虚拟机器人世界)

【ROS1从入门到精通】第11篇:Gazebo仿真(打造虚拟机器人世界)

🎯 本文目标:深入学习Gazebo物理仿真环境,掌握机器人模型导入、传感器仿真、环境建模、插件开发等技术,能够构建完整的机器人仿真系统,实现在虚拟环境中测试和验证机器人算法。

📑 目录

  1. Gazebo仿真概述
  2. Gazebo环境搭建
  3. URDF到SDF转换
  4. 传感器仿真
  5. 物理引擎配置
  6. 环境建模
  7. Gazebo插件开发
  8. ros_control集成
  9. 仿真调试与优化
  10. 实战案例:完整仿真系统
  11. 总结与最佳实践

1. Gazebo仿真概述

1.1 什么是Gazebo?

Gazebo是一个开源的3D机器人仿真器,提供了:

  • 物理仿真:基于ODE、Bullet等物理引擎
  • 传感器仿真:相机、激光、IMU、GPS等
  • 环境建模:室内外场景构建
  • 机器人控制:与ROS深度集成
  • 可视化界面:3D渲染和交互

Gazebo架构 Gazebo Server Physics Engine Sensor System Rendering Engine ODE Bullet DART Simbody Camera LiDAR IMU GPS OGRE OptiX ROS Interface Topics Services Plugins

1.2 Gazebo与ROS集成

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo与ROS集成基础
"""

import rospy
from gazebo_msgs.msg import ModelStates, LinkStates
from gazebo_msgs.srv import (
    SpawnModel, DeleteModel, GetModelState,
    SetModelState, GetWorldProperties,
    ApplyBodyWrench, ApplyJointEffort
)
from geometry_msgs.msg import Pose, Twist, Wrench
import tf

class GazeboROSInterface:
    """Gazebo ROS接口"""

    def __init__(self):
        rospy.init_node('gazebo_ros_interface')

        # 订阅Gazebo状态
        self.model_states_sub = rospy.Subscriber(
            '/gazebo/model_states', ModelStates, self.model_states_callback)
        self.link_states_sub = rospy.Subscriber(
            '/gazebo/link_states', LinkStates, self.link_states_callback)

        # Gazebo服务
        self.spawn_model_srv = rospy.ServiceProxy(
            '/gazebo/spawn_urdf_model', SpawnModel)
        self.delete_model_srv = rospy.ServiceProxy(
            '/gazebo/delete_model', DeleteModel)
        self.get_model_state_srv = rospy.ServiceProxy(
            '/gazebo/get_model_state', GetModelState)
        self.set_model_state_srv = rospy.ServiceProxy(
            '/gazebo/set_model_state', SetModelState)

        # 等待服务可用
        rospy.wait_for_service('/gazebo/spawn_urdf_model')

        # 存储状态
        self.models = {}
        self.links = {}

        rospy.loginfo("Gazebo ROS interface initialized")

    def model_states_callback(self, msg):
        """模型状态回调"""
        for i, name in enumerate(msg.name):
            self.models[name] = {
                'pose': msg.pose[i],
                'twist': msg.twist[i]
            }

    def link_states_callback(self, msg):
        """连杆状态回调"""
        for i, name in enumerate(msg.name):
            self.links[name] = {
                'pose': msg.pose[i],
                'twist': msg.twist[i]
            }

    def spawn_model(self, model_name, model_xml, initial_pose=None,
                   reference_frame="world"):
        """生成模型"""
        if initial_pose is None:
            initial_pose = Pose()
            initial_pose.orientation.w = 1.0

        try:
            resp = self.spawn_model_srv(
                model_name=model_name,
                model_xml=model_xml,
                robot_namespace="",
                initial_pose=initial_pose,
                reference_frame=reference_frame
            )

            if resp.success:
                rospy.loginfo(f"Successfully spawned model: {model_name}")
            else:
                rospy.logerr(f"Failed to spawn model: {resp.status_message}")

            return resp.success

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def delete_model(self, model_name):
        """删除模型"""
        try:
            resp = self.delete_model_srv(model_name=model_name)

            if resp.success:
                rospy.loginfo(f"Successfully deleted model: {model_name}")
            else:
                rospy.logerr(f"Failed to delete model: {resp.status_message}")

            return resp.success

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def get_model_pose(self, model_name, reference_frame="world"):
        """获取模型位姿"""
        try:
            resp = self.get_model_state_srv(
                model_name=model_name,
                relative_entity_name=reference_frame
            )

            if resp.success:
                return resp.pose
            else:
                rospy.logwarn(f"Failed to get model state: {resp.status_message}")
                return None

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return None

    def set_model_pose(self, model_name, pose, twist=None,
                      reference_frame="world"):
        """设置模型位姿"""
        from gazebo_msgs.msg import ModelState

        model_state = ModelState()
        model_state.model_name = model_name
        model_state.pose = pose
        model_state.twist = twist if twist else Twist()
        model_state.reference_frame = reference_frame

        try:
            resp = self.set_model_state_srv(model_state=model_state)

            if resp.success:
                rospy.loginfo(f"Successfully set model state: {model_name}")
            else:
                rospy.logerr(f"Failed to set model state: {resp.status_message}")

            return resp.success

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def apply_force(self, body_name, force, torque=None, duration=None):
        """施加力和力矩"""
        wrench = Wrench()
        wrench.force.x = force[0]
        wrench.force.y = force[1]
        wrench.force.z = force[2]

        if torque:
            wrench.torque.x = torque[0]
            wrench.torque.y = torque[1]
            wrench.torque.z = torque[2]

        try:
            apply_wrench_srv = rospy.ServiceProxy(
                '/gazebo/apply_body_wrench', ApplyBodyWrench)

            resp = apply_wrench_srv(
                body_name=body_name,
                reference_frame="world",
                reference_point=Point(),
                wrench=wrench,
                start_time=rospy.Time(),
                duration=rospy.Duration(duration) if duration else rospy.Duration(-1)
            )

            return resp.success

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

2. Gazebo环境搭建

2.1 安装和配置

bash 复制代码
# 安装Gazebo(ROS Noetic默认Gazebo 11)
sudo apt-get install ros-noetic-gazebo-ros-pkgs ros-noetic-gazebo-ros-control

# 设置Gazebo模型路径
echo "export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:~/gazebo_models" >> ~/.bashrc
echo "export GAZEBO_RESOURCE_PATH=$GAZEBO_RESOURCE_PATH:~/gazebo_worlds" >> ~/.bashrc
source ~/.bashrc

# 下载模型库
cd ~
git clone https://github.com/osrf/gazebo_models.git

# 测试Gazebo
roslaunch gazebo_ros empty_world.launch

2.2 Launch文件配置

xml 复制代码
<!-- launch/gazebo_simulation.launch -->
<launch>
  <!-- Gazebo参数 -->
  <arg name="paused" default="false"/>
  <arg name="use_sim_time" default="true"/>
  <arg name="gui" default="true"/>
  <arg name="headless" default="false"/>
  <arg name="debug" default="false"/>
  <arg name="world_name" default="$(find my_robot_gazebo)/worlds/my_world.world"/>

  <!-- 启动Gazebo -->
  <include file="$(find gazebo_ros)/launch/empty_world.launch">
    <arg name="world_name" value="$(arg world_name)"/>
    <arg name="debug" value="$(arg debug)"/>
    <arg name="gui" value="$(arg gui)"/>
    <arg name="paused" value="$(arg paused)"/>
    <arg name="use_sim_time" value="$(arg use_sim_time)"/>
    <arg name="headless" value="$(arg headless)"/>
  </include>

  <!-- 加载机器人描述 -->
  <param name="robot_description"
         command="$(find xacro)/xacro $(find my_robot_description)/urdf/robot.urdf.xacro"/>

  <!-- 生成机器人 -->
  <node name="spawn_robot" pkg="gazebo_ros" type="spawn_model"
        args="-urdf -param robot_description -model my_robot -x 0 -y 0 -z 0.5"
        respawn="false" output="screen"/>

  <!-- Robot State Publisher -->
  <node name="robot_state_publisher" pkg="robot_state_publisher"
        type="robot_state_publisher">
    <param name="publish_frequency" value="50"/>
  </node>

  <!-- Joint State Publisher -->
  <node name="joint_state_publisher" pkg="joint_state_publisher"
        type="joint_state_publisher">
    <param name="use_gui" value="false"/>
  </node>

</launch>

2.3 Gazebo配置管理

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo配置管理器
"""

import os
import yaml
import xml.etree.ElementTree as ET

class GazeboConfig:
    """Gazebo配置管理"""

    def __init__(self):
        self.physics_engines = {
            'ode': {
                'type': 'ode',
                'max_step_size': 0.001,
                'real_time_factor': 1.0,
                'real_time_update_rate': 1000,
                'gravity': [0, 0, -9.8]
            },
            'bullet': {
                'type': 'bullet',
                'max_step_size': 0.001,
                'real_time_factor': 1.0,
                'real_time_update_rate': 1000,
                'gravity': [0, 0, -9.8],
                'bullet': {
                    'solver': {
                        'type': 'sequential_impulse',
                        'iters': 50,
                        'sor': 1.3
                    }
                }
            }
        }

    def create_world_file(self, world_name, config):
        """创建世界文件"""
        world = ET.Element('sdf', version='1.6')
        world_elem = ET.SubElement(world, 'world', name=world_name)

        # 添加物理引擎配置
        physics = ET.SubElement(world_elem, 'physics',
                               name='default_physics',
                               type=config.get('physics_engine', 'ode'))

        ET.SubElement(physics, 'max_step_size').text = str(
            config.get('max_step_size', 0.001))
        ET.SubElement(physics, 'real_time_factor').text = str(
            config.get('real_time_factor', 1.0))
        ET.SubElement(physics, 'real_time_update_rate').text = str(
            config.get('real_time_update_rate', 1000))

        gravity = ET.SubElement(physics, 'gravity')
        g = config.get('gravity', [0, 0, -9.8])
        gravity.text = f"{g[0]} {g[1]} {g[2]}"

        # 添加场景
        scene = ET.SubElement(world_elem, 'scene')
        ET.SubElement(scene, 'ambient').text = '0.4 0.4 0.4 1'
        ET.SubElement(scene, 'background').text = '0.7 0.7 0.7 1'
        ET.SubElement(scene, 'shadows').text = '1'

        # 添加光源
        self.add_light(world_elem, 'sun', 'directional',
                      [0, 0, 10], [0.8, 0.8, 0.8, 1])

        # 添加地面
        self.add_ground(world_elem)

        return world

    def add_light(self, parent, name, light_type, pose, diffuse):
        """添加光源"""
        light = ET.SubElement(parent, 'light', name=name, type=light_type)

        pose_elem = ET.SubElement(light, 'pose')
        pose_elem.text = f"{pose[0]} {pose[1]} {pose[2]} 0 0 0"

        diffuse_elem = ET.SubElement(light, 'diffuse')
        diffuse_elem.text = f"{diffuse[0]} {diffuse[1]} {diffuse[2]} {diffuse[3]}"

        ET.SubElement(light, 'cast_shadows').text = '1'

    def add_ground(self, parent):
        """添加地面"""
        model = ET.SubElement(parent, 'model', name='ground_plane')
        ET.SubElement(model, 'static').text = 'true'

        link = ET.SubElement(model, 'link', name='link')

        # 碰撞
        collision = ET.SubElement(link, 'collision', name='collision')
        geom = ET.SubElement(collision, 'geometry')
        plane = ET.SubElement(geom, 'plane')
        ET.SubElement(plane, 'normal').text = '0 0 1'
        ET.SubElement(plane, 'size').text = '100 100'

        # 视觉
        visual = ET.SubElement(link, 'visual', name='visual')
        geom = ET.SubElement(visual, 'geometry')
        plane = ET.SubElement(geom, 'plane')
        ET.SubElement(plane, 'normal').text = '0 0 1'
        ET.SubElement(plane, 'size').text = '100 100'

        material = ET.SubElement(visual, 'material')
        script = ET.SubElement(material, 'script')
        ET.SubElement(script, 'uri').text = 'file://media/materials/scripts/gazebo.material'
        ET.SubElement(script, 'name').text = 'Gazebo/Grey'

    def save_world(self, world, filename):
        """保存世界文件"""
        tree = ET.ElementTree(world)
        tree.write(filename, encoding='utf-8', xml_declaration=True)
        print(f"World file saved to: {filename}")

3. URDF到SDF转换

3.1 Gazebo标签扩展

xml 复制代码
<!-- URDF with Gazebo tags -->
<robot name="gazebo_robot">

  <!-- Link Gazebo properties -->
  <gazebo reference="base_link">
    <material>Gazebo/Blue</material>
    <gravity>true</gravity>
    <self_collide>false</self_collide>
    <mu1>1.0</mu1>
    <mu2>1.0</mu2>
    <kp>1e6</kp>
    <kd>1.0</kd>
  </gazebo>

  <!-- Joint Gazebo properties -->
  <gazebo reference="joint1">
    <provideFeedback>true</provideFeedback>
    <implicitSpringDamper>true</implicitSpringDamper>
    <springStiffness>0.0</springStiffness>
    <springReference>0.0</springReference>
    <stopCfm>0.0</stopCfm>
    <stopErp>0.2</stopErp>
  </gazebo>

  <!-- Transmission for ros_control -->
  <transmission name="joint1_trans">
    <type>transmission_interface/SimpleTransmission</type>
    <joint name="joint1">
      <hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
    </joint>
    <actuator name="joint1_motor">
      <mechanicalReduction>1</mechanicalReduction>
      <hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
    </actuator>
  </transmission>

</robot>

3.2 Gazebo属性生成器

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo属性生成器
"""

import xml.etree.ElementTree as ET

class GazeboTags:
    """Gazebo标签生成器"""

    # Gazebo材质列表
    GAZEBO_MATERIALS = [
        'Gazebo/White', 'Gazebo/Grey', 'Gazebo/Black',
        'Gazebo/Red', 'Gazebo/Green', 'Gazebo/Blue',
        'Gazebo/Yellow', 'Gazebo/Orange', 'Gazebo/Purple',
        'Gazebo/Turquoise', 'Gazebo/Wood', 'Gazebo/WoodFloor',
        'Gazebo/Bricks', 'Gazebo/Grass', 'Gazebo/Rocky'
    ]

    def __init__(self):
        self.gazebo_tags = []

    def add_link_properties(self, link_name, properties):
        """添加连杆属性"""
        gazebo = ET.Element('gazebo', reference=link_name)

        # 材质
        if 'material' in properties:
            ET.SubElement(gazebo, 'material').text = properties['material']

        # 重力
        if 'gravity' in properties:
            ET.SubElement(gazebo, 'gravity').text = str(properties['gravity']).lower()

        # 摩擦系数
        if 'mu1' in properties or 'mu2' in properties:
            surface = ET.SubElement(gazebo, 'surface')
            friction = ET.SubElement(surface, 'friction')
            ode = ET.SubElement(friction, 'ode')

            if 'mu1' in properties:
                ET.SubElement(ode, 'mu').text = str(properties['mu1'])
            if 'mu2' in properties:
                ET.SubElement(ode, 'mu2').text = str(properties['mu2'])

        # 接触系数
        if 'kp' in properties or 'kd' in properties:
            if 'surface' not in [child.tag for child in gazebo]:
                surface = ET.SubElement(gazebo, 'surface')
            else:
                surface = gazebo.find('surface')

            contact = ET.SubElement(surface, 'contact')
            ode_contact = ET.SubElement(contact, 'ode')

            if 'kp' in properties:
                ET.SubElement(ode_contact, 'kp').text = str(properties['kp'])
            if 'kd' in properties:
                ET.SubElement(ode_contact, 'kd').text = str(properties['kd'])

        # 自碰撞
        if 'self_collide' in properties:
            ET.SubElement(gazebo, 'self_collide').text = str(
                properties['self_collide']).lower()

        self.gazebo_tags.append(gazebo)
        return gazebo

    def add_joint_properties(self, joint_name, properties):
        """添加关节属性"""
        gazebo = ET.Element('gazebo', reference=joint_name)

        # 反馈
        if 'provide_feedback' in properties:
            ET.SubElement(gazebo, 'provideFeedback').text = str(
                properties['provide_feedback']).lower()

        # 弹簧阻尼
        if 'implicit_spring_damper' in properties:
            ET.SubElement(gazebo, 'implicitSpringDamper').text = str(
                properties['implicit_spring_damper']).lower()

        # 弹簧参数
        if 'spring_stiffness' in properties:
            ET.SubElement(gazebo, 'springStiffness').text = str(
                properties['spring_stiffness'])
        if 'spring_reference' in properties:
            ET.SubElement(gazebo, 'springReference').text = str(
                properties['spring_reference'])

        # 停止参数
        if 'stop_cfm' in properties:
            ET.SubElement(gazebo, 'stopCfm').text = str(properties['stop_cfm'])
        if 'stop_erp' in properties:
            ET.SubElement(gazebo, 'stopErp').text = str(properties['stop_erp'])

        self.gazebo_tags.append(gazebo)
        return gazebo

    def add_transmission(self, joint_name, actuator_name=None,
                        transmission_type='SimpleTransmission',
                        hardware_interface='EffortJointInterface',
                        mechanical_reduction=1.0):
        """添加传动装置(用于ros_control)"""
        transmission = ET.Element('transmission', name=f"{joint_name}_trans")

        # 传动类型
        ET.SubElement(transmission, 'type').text = f"transmission_interface/{transmission_type}"

        # 关节
        joint = ET.SubElement(transmission, 'joint', name=joint_name)
        ET.SubElement(joint, 'hardwareInterface').text = f"hardware_interface/{hardware_interface}"

        # 执行器
        actuator_name = actuator_name or f"{joint_name}_motor"
        actuator = ET.SubElement(transmission, 'actuator', name=actuator_name)
        ET.SubElement(actuator, 'mechanicalReduction').text = str(mechanical_reduction)
        ET.SubElement(actuator, 'hardwareInterface').text = f"hardware_interface/{hardware_interface}"

        return transmission

    def generate_gazebo_urdf(self, urdf_file, output_file):
        """生成带Gazebo标签的URDF"""
        tree = ET.parse(urdf_file)
        root = tree.getroot()

        # 添加Gazebo标签
        for tag in self.gazebo_tags:
            root.append(tag)

        # 保存文件
        tree.write(output_file, encoding='utf-8', xml_declaration=True)
        print(f"Gazebo URDF saved to: {output_file}")

4. 传感器仿真

4.1 相机传感器

xml 复制代码
<!-- Camera sensor in URDF -->
<gazebo reference="camera_link">
  <sensor type="camera" name="camera_sensor">
    <update_rate>30.0</update_rate>
    <camera name="camera">
      <horizontal_fov>1.3962634</horizontal_fov>
      <image>
        <width>640</width>
        <height>480</height>
        <format>R8G8B8</format>
      </image>
      <clip>
        <near>0.02</near>
        <far>300</far>
      </clip>
      <noise>
        <type>gaussian</type>
        <mean>0.0</mean>
        <stddev>0.007</stddev>
      </noise>
    </camera>

    <plugin name="camera_controller" filename="libgazebo_ros_camera.so">
      <alwaysOn>true</alwaysOn>
      <updateRate>30.0</updateRate>
      <cameraName>robot/camera</cameraName>
      <imageTopicName>image_raw</imageTopicName>
      <cameraInfoTopicName>camera_info</cameraInfoTopicName>
      <frameName>camera_link</frameName>
      <hackBaseline>0.07</hackBaseline>
      <distortionK1>0.0</distortionK1>
      <distortionK2>0.0</distortionK2>
      <distortionK3>0.0</distortionK3>
      <distortionT1>0.0</distortionT1>
      <distortionT2>0.0</distortionT2>
    </plugin>
  </sensor>
</gazebo>

4.2 激光传感器

xml 复制代码
<!-- Laser sensor in URDF -->
<gazebo reference="laser_link">
  <sensor type="ray" name="laser_sensor">
    <pose>0 0 0 0 0 0</pose>
    <visualize>true</visualize>
    <update_rate>10</update_rate>
    <ray>
      <scan>
        <horizontal>
          <samples>720</samples>
          <resolution>0.5</resolution>
          <min_angle>-3.14159</min_angle>
          <max_angle>3.14159</max_angle>
        </horizontal>
      </scan>
      <range>
        <min>0.10</min>
        <max>30.0</max>
        <resolution>0.01</resolution>
      </range>
      <noise>
        <type>gaussian</type>
        <mean>0.0</mean>
        <stddev>0.01</stddev>
      </noise>
    </ray>

    <plugin name="laser_controller" filename="libgazebo_ros_laser.so">
      <topicName>/scan</topicName>
      <frameName>laser_link</frameName>
    </plugin>
  </sensor>
</gazebo>

4.3 传感器管理器

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo传感器管理器
"""

class SensorManager:
    """传感器管理器"""

    def __init__(self):
        self.sensors = {}

    def create_camera_sensor(self, name, link_name, config):
        """创建相机传感器"""
        sensor = {
            'type': 'camera',
            'name': name,
            'link': link_name,
            'update_rate': config.get('update_rate', 30.0),
            'horizontal_fov': config.get('horizontal_fov', 1.3962634),
            'image_width': config.get('width', 640),
            'image_height': config.get('height', 480),
            'image_format': config.get('format', 'R8G8B8'),
            'clip_near': config.get('near', 0.02),
            'clip_far': config.get('far', 300),
            'noise_type': config.get('noise_type', 'gaussian'),
            'noise_mean': config.get('noise_mean', 0.0),
            'noise_stddev': config.get('noise_stddev', 0.007)
        }

        self.sensors[name] = sensor
        return self.generate_camera_xml(sensor)

    def create_laser_sensor(self, name, link_name, config):
        """创建激光传感器"""
        sensor = {
            'type': 'ray',
            'name': name,
            'link': link_name,
            'update_rate': config.get('update_rate', 10.0),
            'samples': config.get('samples', 720),
            'resolution': config.get('resolution', 0.5),
            'min_angle': config.get('min_angle', -3.14159),
            'max_angle': config.get('max_angle', 3.14159),
            'min_range': config.get('min_range', 0.10),
            'max_range': config.get('max_range', 30.0),
            'range_resolution': config.get('range_resolution', 0.01),
            'noise_type': config.get('noise_type', 'gaussian'),
            'noise_mean': config.get('noise_mean', 0.0),
            'noise_stddev': config.get('noise_stddev', 0.01)
        }

        self.sensors[name] = sensor
        return self.generate_laser_xml(sensor)

    def create_imu_sensor(self, name, link_name, config):
        """创建IMU传感器"""
        sensor = {
            'type': 'imu',
            'name': name,
            'link': link_name,
            'update_rate': config.get('update_rate', 100.0),
            'angular_velocity_noise': config.get('angular_velocity_noise', 0.0001),
            'linear_acceleration_noise': config.get('linear_acceleration_noise', 0.0001),
            'angular_velocity_bias': config.get('angular_velocity_bias', 0.0),
            'linear_acceleration_bias': config.get('linear_acceleration_bias', 0.0)
        }

        self.sensors[name] = sensor
        return self.generate_imu_xml(sensor)

    def create_gps_sensor(self, name, link_name, config):
        """创建GPS传感器"""
        sensor = {
            'type': 'gps',
            'name': name,
            'link': link_name,
            'update_rate': config.get('update_rate', 10.0),
            'position_noise': config.get('position_noise', 0.001),
            'velocity_noise': config.get('velocity_noise', 0.0001),
            'reference_latitude': config.get('reference_latitude', 49.9),
            'reference_longitude': config.get('reference_longitude', 8.9),
            'reference_altitude': config.get('reference_altitude', 0.0),
            'reference_heading': config.get('reference_heading', 0.0)
        }

        self.sensors[name] = sensor
        return self.generate_gps_xml(sensor)

    def generate_camera_xml(self, sensor):
        """生成相机XML"""
        gazebo = ET.Element('gazebo', reference=sensor['link'])
        sensor_elem = ET.SubElement(gazebo, 'sensor',
                                   type='camera',
                                   name=sensor['name'])

        ET.SubElement(sensor_elem, 'update_rate').text = str(sensor['update_rate'])

        camera = ET.SubElement(sensor_elem, 'camera', name='camera')
        ET.SubElement(camera, 'horizontal_fov').text = str(sensor['horizontal_fov'])

        image = ET.SubElement(camera, 'image')
        ET.SubElement(image, 'width').text = str(sensor['image_width'])
        ET.SubElement(image, 'height').text = str(sensor['image_height'])
        ET.SubElement(image, 'format').text = sensor['image_format']

        clip = ET.SubElement(camera, 'clip')
        ET.SubElement(clip, 'near').text = str(sensor['clip_near'])
        ET.SubElement(clip, 'far').text = str(sensor['clip_far'])

        noise = ET.SubElement(camera, 'noise')
        ET.SubElement(noise, 'type').text = sensor['noise_type']
        ET.SubElement(noise, 'mean').text = str(sensor['noise_mean'])
        ET.SubElement(noise, 'stddev').text = str(sensor['noise_stddev'])

        # 添加ROS插件
        plugin = ET.SubElement(sensor_elem, 'plugin',
                              name='camera_controller',
                              filename='libgazebo_ros_camera.so')
        ET.SubElement(plugin, 'alwaysOn').text = 'true'
        ET.SubElement(plugin, 'updateRate').text = str(sensor['update_rate'])
        ET.SubElement(plugin, 'cameraName').text = sensor['name']
        ET.SubElement(plugin, 'imageTopicName').text = 'image_raw'
        ET.SubElement(plugin, 'cameraInfoTopicName').text = 'camera_info'
        ET.SubElement(plugin, 'frameName').text = sensor['link']

        return gazebo

    def generate_laser_xml(self, sensor):
        """生成激光XML"""
        gazebo = ET.Element('gazebo', reference=sensor['link'])
        sensor_elem = ET.SubElement(gazebo, 'sensor',
                                   type='ray',
                                   name=sensor['name'])

        ET.SubElement(sensor_elem, 'pose').text = '0 0 0 0 0 0'
        ET.SubElement(sensor_elem, 'visualize').text = 'true'
        ET.SubElement(sensor_elem, 'update_rate').text = str(sensor['update_rate'])

        ray = ET.SubElement(sensor_elem, 'ray')
        scan = ET.SubElement(ray, 'scan')
        horizontal = ET.SubElement(scan, 'horizontal')
        ET.SubElement(horizontal, 'samples').text = str(sensor['samples'])
        ET.SubElement(horizontal, 'resolution').text = str(sensor['resolution'])
        ET.SubElement(horizontal, 'min_angle').text = str(sensor['min_angle'])
        ET.SubElement(horizontal, 'max_angle').text = str(sensor['max_angle'])

        range_elem = ET.SubElement(ray, 'range')
        ET.SubElement(range_elem, 'min').text = str(sensor['min_range'])
        ET.SubElement(range_elem, 'max').text = str(sensor['max_range'])
        ET.SubElement(range_elem, 'resolution').text = str(sensor['range_resolution'])

        noise = ET.SubElement(ray, 'noise')
        ET.SubElement(noise, 'type').text = sensor['noise_type']
        ET.SubElement(noise, 'mean').text = str(sensor['noise_mean'])
        ET.SubElement(noise, 'stddev').text = str(sensor['noise_stddev'])

        # 添加ROS插件
        plugin = ET.SubElement(sensor_elem, 'plugin',
                              name='laser_controller',
                              filename='libgazebo_ros_laser.so')
        ET.SubElement(plugin, 'topicName').text = f"/{sensor['name']}/scan"
        ET.SubElement(plugin, 'frameName').text = sensor['link']

        return gazebo

5. 物理引擎配置

5.1 物理参数优化

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo物理引擎配置优化
"""

import rospy
from gazebo_msgs.srv import SetPhysicsProperties, GetPhysicsProperties
from geometry_msgs.msg import Vector3

class PhysicsOptimizer:
    """物理引擎优化器"""

    def __init__(self):
        rospy.init_node('physics_optimizer')

        # 物理引擎服务
        self.get_physics_srv = rospy.ServiceProxy(
            '/gazebo/get_physics_properties', GetPhysicsProperties)
        self.set_physics_srv = rospy.ServiceProxy(
            '/gazebo/set_physics_properties', SetPhysicsProperties)

        rospy.wait_for_service('/gazebo/get_physics_properties')

    def get_physics_properties(self):
        """获取物理属性"""
        try:
            resp = self.get_physics_srv()

            print("Current Physics Properties:")
            print(f"  Time step: {resp.time_step}")
            print(f"  Max update rate: {resp.max_update_rate}")
            print(f"  Gravity: {resp.gravity}")
            print(f"  Auto disable bodies: {resp.ode_config.auto_disable_bodies}")
            print(f"  SOR PGS iters: {resp.ode_config.sor_pgs_iters}")
            print(f"  Contact surface layer: {resp.ode_config.contact_surface_layer}")
            print(f"  Contact max correcting vel: {resp.ode_config.contact_max_correcting_vel}")
            print(f"  CFM: {resp.ode_config.cfm}")
            print(f"  ERP: {resp.ode_config.erp}")

            return resp

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return None

    def set_physics_properties(self, config):
        """设置物理属性"""
        try:
            # 获取当前属性
            current = self.get_physics_srv()

            # 更新属性
            time_step = config.get('time_step', current.time_step)
            max_update_rate = config.get('max_update_rate', current.max_update_rate)

            gravity = config.get('gravity',
                                [current.gravity.x, current.gravity.y, current.gravity.z])

            # ODE配置
            ode_config = current.ode_config
            ode_config.auto_disable_bodies = config.get('auto_disable_bodies',
                                                        ode_config.auto_disable_bodies)
            ode_config.sor_pgs_iters = config.get('sor_pgs_iters',
                                                  ode_config.sor_pgs_iters)
            ode_config.contact_surface_layer = config.get('contact_surface_layer',
                                                          ode_config.contact_surface_layer)
            ode_config.contact_max_correcting_vel = config.get('contact_max_correcting_vel',
                                                              ode_config.contact_max_correcting_vel)
            ode_config.cfm = config.get('cfm', ode_config.cfm)
            ode_config.erp = config.get('erp', ode_config.erp)

            # 调用服务
            resp = self.set_physics_srv(
                time_step=time_step,
                max_update_rate=max_update_rate,
                gravity=Vector3(x=gravity[0], y=gravity[1], z=gravity[2]),
                ode_config=ode_config
            )

            if resp.success:
                rospy.loginfo("Physics properties updated successfully")
            else:
                rospy.logerr(f"Failed to update physics: {resp.status_message}")

            return resp.success

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def optimize_for_realtime(self):
        """优化实时性能"""
        config = {
            'time_step': 0.001,           # 1ms时间步
            'max_update_rate': 1000,       # 1000Hz更新率
            'sor_pgs_iters': 20,          # 减少迭代次数
            'contact_surface_layer': 0.001,
            'contact_max_correcting_vel': 100.0,
            'cfm': 0.0,
            'erp': 0.2
        }

        return self.set_physics_properties(config)

    def optimize_for_accuracy(self):
        """优化精度"""
        config = {
            'time_step': 0.0001,          # 0.1ms时间步
            'max_update_rate': 10000,      # 10kHz更新率
            'sor_pgs_iters': 100,         # 增加迭代次数
            'contact_surface_layer': 0.0001,
            'contact_max_correcting_vel': 10.0,
            'cfm': 0.0,
            'erp': 0.8
        }

        return self.set_physics_properties(config)

    def set_gravity(self, gravity_vector):
        """设置重力"""
        config = {'gravity': gravity_vector}
        return self.set_physics_properties(config)

    def enable_zero_gravity(self):
        """启用零重力"""
        return self.set_gravity([0, 0, 0])

    def set_moon_gravity(self):
        """设置月球重力"""
        return self.set_gravity([0, 0, -1.62])

    def set_mars_gravity(self):
        """设置火星重力"""
        return self.set_gravity([0, 0, -3.71])

6. 环境建模

6.1 世界文件创建

xml 复制代码
<!-- worlds/my_world.world -->
<?xml version="1.0"?>
<sdf version="1.6">
  <world name="my_world">

    <!-- 物理引擎 -->
    <physics type="ode">
      <real_time_update_rate>1000</real_time_update_rate>
      <max_step_size>0.001</max_step_size>
      <real_time_factor>1</real_time_factor>
      <ode>
        <solver>
          <type>quick</type>
          <iters>50</iters>
          <precon_iters>0</precon_iters>
          <sor>1.400000</sor>
        </solver>
      </ode>
    </physics>

    <!-- 场景 -->
    <scene>
      <ambient>0.4 0.4 0.4 1</ambient>
      <background>0.7 0.7 0.7 1</background>
      <shadows>true</shadows>
    </scene>

    <!-- 光源 -->
    <light name="sun" type="directional">
      <cast_shadows>true</cast_shadows>
      <pose>0 0 10 0 0 0</pose>
      <diffuse>0.8 0.8 0.8 1</diffuse>
      <specular>0.2 0.2 0.2 1</specular>
      <attenuation>
        <range>1000</range>
        <constant>0.9</constant>
        <linear>0.01</linear>
        <quadratic>0.001</quadratic>
      </attenuation>
      <direction>-0.5 0.1 -0.9</direction>
    </light>

    <!-- 地面 -->
    <model name="ground_plane">
      <static>true</static>
      <link name="link">
        <collision name="collision">
          <geometry>
            <plane>
              <normal>0 0 1</normal>
              <size>100 100</size>
            </plane>
          </geometry>
          <surface>
            <friction>
              <ode>
                <mu>100</mu>
                <mu2>50</mu2>
              </ode>
            </friction>
          </surface>
        </collision>
        <visual name="visual">
          <cast_shadows>false</cast_shadows>
          <geometry>
            <plane>
              <normal>0 0 1</normal>
              <size>100 100</size>
            </plane>
          </geometry>
          <material>
            <script>
              <uri>file://media/materials/scripts/gazebo.material</uri>
              <name>Gazebo/Grey</name>
            </script>
          </material>
        </visual>
      </link>
    </model>

  </world>
</sdf>

6.2 环境构建器

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo环境构建器
"""

import random
import math

class WorldBuilder:
    """世界构建器"""

    def __init__(self):
        self.models = []
        self.lights = []
        self.physics_config = {}

    def create_office_environment(self):
        """创建办公室环境"""
        # 地面
        self.add_ground()

        # 墙壁
        self.add_walls(10, 10, 3)

        # 办公桌
        desks = [
            (2, 2, 0, 0),
            (2, -2, 0, 0),
            (-2, 2, 0, 3.14),
            (-2, -2, 0, 3.14)
        ]
        for x, y, z, yaw in desks:
            self.add_desk(x, y, z, yaw)

        # 椅子
        chairs = [
            (2.5, 2, 0, 3.14),
            (2.5, -2, 0, 3.14),
            (-2.5, 2, 0, 0),
            (-2.5, -2, 0, 0)
        ]
        for x, y, z, yaw in chairs:
            self.add_chair(x, y, z, yaw)

        # 障碍物
        self.add_random_obstacles(10, 10, 10)

    def create_outdoor_environment(self):
        """创建室外环境"""
        # 地形
        self.add_terrain()

        # 树木
        self.add_trees(20, 20, 20)

        # 建筑物
        self.add_buildings(5)

        # 道路**作者**:机器人开发工程师
**最后更新**:2024年12月

        self.add_roads()

    def add_ground(self, size=100):
        """添加地面"""
        ground = {
            'name': 'ground_plane',
            'static': True,
            'geometry': {
                'type': 'plane',
                'normal': [0, 0, 1],
                'size': [size, size]
            },
            'material': 'Gazebo/Grey',
            'friction': {'mu': 100, 'mu2': 50}
        }
        self.models.append(ground)

    def add_walls(self, width, length, height):
        """添加墙壁"""
        walls = [
            # 前墙
            {'pos': [width/2, 0, height/2], 'size': [0.1, length, height]},
            # 后墙
            {'pos': [-width/2, 0, height/2], 'size': [0.1, length, height]},
            # 左墙
            {'pos': [0, length/2, height/2], 'size': [width, 0.1, height]},
            # 右墙
            {'pos': [0, -length/2, height/2], 'size': [width, 0.1, height]}
        ]

        for i, wall in enumerate(walls):
            model = {
                'name': f'wall_{i}',
                'static': True,
                'pose': wall['pos'] + [0, 0, 0],
                'geometry': {
                    'type': 'box',
                    'size': wall['size']
                },
                'material': 'Gazebo/Wood'
            }
            self.models.append(model)

    def add_desk(self, x, y, z, yaw):
        """添加办公桌"""
        desk = {
            'name': f'desk_{len(self.models)}',
            'static': False,
            'pose': [x, y, z + 0.4, 0, 0, yaw],
            'links': [
                # 桌面
                {
                    'name': 'top',
                    'geometry': {'type': 'box', 'size': [1.2, 0.6, 0.02]},
                    'mass': 10.0,
                    'material': 'Gazebo/Wood'
                },
                # 桌腿
                {
                    'name': 'leg1',
                    'pose': [0.5, 0.25, -0.4, 0, 0, 0],
                    'geometry': {'type': 'cylinder', 'radius': 0.02, 'length': 0.8},
                    'mass': 1.0,
                    'material': 'Gazebo/Grey'
                },
                {
                    'name': 'leg2',
                    'pose': [0.5, -0.25, -0.4, 0, 0, 0],
                    'geometry': {'type': 'cylinder', 'radius': 0.02, 'length': 0.8},
                    'mass': 1.0,
                    'material': 'Gazebo/Grey'
                },
                {
                    'name': 'leg3',
                    'pose': [-0.5, 0.25, -0.4, 0, 0, 0],
                    'geometry': {'type': 'cylinder', 'radius': 0.02, 'length': 0.8},
                    'mass': 1.0,
                    'material': 'Gazebo/Grey'
                },
                {
                    'name': 'leg4',
                    'pose': [-0.5, -0.25, -0.4, 0, 0, 0],
                    'geometry': {'type': 'cylinder', 'radius': 0.02, 'length': 0.8},
                    'mass': 1.0,
                    'material': 'Gazebo/Grey'
                }
            ]
        }
        self.models.append(desk)

    def add_chair(self, x, y, z, yaw):
        """添加椅子"""
        chair = {
            'name': f'chair_{len(self.models)}',
            'static': False,
            'pose': [x, y, z + 0.25, 0, 0, yaw],
            'links': [
                # 座位
                {
                    'name': 'seat',
                    'geometry': {'type': 'box', 'size': [0.4, 0.4, 0.04]},
                    'mass': 2.0,
                    'material': 'Gazebo/Blue'
                },
                # 靠背
                {
                    'name': 'back',
                    'pose': [-0.18, 0, 0.25, 0, 0, 0],
                    'geometry': {'type': 'box', 'size': [0.04, 0.4, 0.5]},
                    'mass': 1.0,
                    'material': 'Gazebo/Blue'
                }
            ]
        }
        self.models.append(chair)

    def add_random_obstacles(self, area_width, area_length, count):
        """添加随机障碍物"""
        for i in range(count):
            # 随机位置
            x = random.uniform(-area_width/2, area_width/2)
            y = random.uniform(-area_length/2, area_length/2)
            z = 0

            # 随机大小
            size = [
                random.uniform(0.1, 0.5),
                random.uniform(0.1, 0.5),
                random.uniform(0.5, 1.5)
            ]

            # 随机形状
            shape_type = random.choice(['box', 'cylinder', 'sphere'])

            obstacle = {
                'name': f'obstacle_{i}',
                'static': False,
                'pose': [x, y, z + size[2]/2, 0, 0, random.uniform(0, 2*math.pi)],
                'geometry': {'type': shape_type},
                'mass': random.uniform(1, 10),
                'material': random.choice(['Gazebo/Red', 'Gazebo/Green',
                                          'Gazebo/Blue', 'Gazebo/Yellow'])
            }

            if shape_type == 'box':
                obstacle['geometry']['size'] = size
            elif shape_type == 'cylinder':
                obstacle['geometry']['radius'] = size[0]/2
                obstacle['geometry']['length'] = size[2]
            else:  # sphere
                obstacle['geometry']['radius'] = size[0]/2

            self.models.append(obstacle)

    def add_trees(self, area_width, area_length, count):
        """添加树木"""
        for i in range(count):
            x = random.uniform(-area_width/2, area_width/2)
            y = random.uniform(-area_length/2, area_length/2)

            tree = {
                'name': f'tree_{i}',
                'static': True,
                'pose': [x, y, 0, 0, 0, 0],
                'links': [
                    # 树干
                    {
                        'name': 'trunk',
                        'pose': [0, 0, 1, 0, 0, 0],
                        'geometry': {'type': 'cylinder', 'radius': 0.1, 'length': 2},
                        'material': 'Gazebo/Wood'
                    },
                    # 树冠
                    {
                        'name': 'crown',
                        'pose': [0, 0, 3, 0, 0, 0],
                        'geometry': {'type': 'sphere', 'radius': 1},
                        'material': 'Gazebo/Green'
                    }
                ]
            }
            self.models.append(tree)

7. Gazebo插件开发

7.1 模型插件

cpp 复制代码
// src/model_plugin.cpp
#include <gazebo/gazebo.hh>
#include <gazebo/physics/physics.hh>
#include <gazebo/common/common.hh>
#include <ros/ros.h>
#include <std_msgs/Float64.h>
#include <geometry_msgs/Twist.h>

namespace gazebo {

class ModelPlugin : public ModelPlugin {
public:
    ModelPlugin() : ModelPlugin() {}

    virtual void Load(physics::ModelPtr _model, sdf::ElementPtr _sdf) {
        // 存储模型指针
        this->model = _model;

        // 解析SDF参数
        if (_sdf->HasElement("updateRate")) {
            this->update_rate = _sdf->Get<double>("updateRate");
        } else {
            this->update_rate = 100.0;
        }

        // 初始化ROS
        if (!ros::isInitialized()) {
            int argc = 0;
            char **argv = NULL;
            ros::init(argc, argv, "gazebo_model_plugin",
                     ros::init_options::NoSigintHandler);
        }

        // 创建ROS节点句柄
        this->rosNode.reset(new ros::NodeHandle("gazebo"));

        // 创建订阅者
        ros::SubscribeOptions so = ros::SubscribeOptions::create<geometry_msgs::Twist>(
            "/" + this->model->GetName() + "/cmd_vel",
            1,
            boost::bind(&ModelPlugin::OnCmdVel, this, _1),
            ros::VoidPtr(),
            &this->rosQueue
        );
        this->cmdVelSub = this->rosNode->subscribe(so);

        // 创建发布者
        this->posePub = this->rosNode->advertise<geometry_msgs::Pose>(
            "/" + this->model->GetName() + "/pose", 1);

        // 启动ROS异步线程
        this->rosQueueThread = std::thread(
            std::bind(&ModelPlugin::QueueThread, this));

        // 连接更新事件
        this->updateConnection = event::Events::ConnectWorldUpdateBegin(
            std::bind(&ModelPlugin::OnUpdate, this));

        ROS_INFO("Model plugin loaded for: %s", this->model->GetName().c_str());
    }

    void OnUpdate() {
        // 获取模型位姿
        ignition::math::Pose3d pose = this->model->WorldPose();

        // 发布位姿
        geometry_msgs::Pose msg;
        msg.position.x = pose.Pos().X();
        msg.position.y = pose.Pos().Y();
        msg.position.z = pose.Pos().Z();
        msg.orientation.x = pose.Rot().X();
        msg.orientation.y = pose.Rot().Y();
        msg.orientation.z = pose.Rot().Z();
        msg.orientation.w = pose.Rot().W();

        this->posePub.publish(msg);

        // 应用速度命令
        if (this->cmd_vel_received) {
            this->model->SetLinearVel(
                ignition::math::Vector3d(
                    this->cmd_linear_x,
                    this->cmd_linear_y,
                    this->cmd_linear_z
                )
            );

            this->model->SetAngularVel(
                ignition::math::Vector3d(
                    this->cmd_angular_x,
                    this->cmd_angular_y,
                    this->cmd_angular_z
                )
            );
        }
    }

    void OnCmdVel(const geometry_msgs::Twist::ConstPtr& msg) {
        this->cmd_linear_x = msg->linear.x;
        this->cmd_linear_y = msg->linear.y;
        this->cmd_linear_z = msg->linear.z;
        this->cmd_angular_x = msg->angular.x;
        this->cmd_angular_y = msg->angular.y;
        this->cmd_angular_z = msg->angular.z;
        this->cmd_vel_received = true;
    }

    void QueueThread() {
        static const double timeout = 0.01;
        while (this->rosNode->ok()) {
            this->rosQueue.callAvailable(ros::WallDuration(timeout));
        }
    }

private:
    physics::ModelPtr model;
    event::ConnectionPtr updateConnection;

    std::unique_ptr<ros::NodeHandle> rosNode;
    ros::Subscriber cmdVelSub;
    ros::Publisher posePub;
    ros::CallbackQueue rosQueue;
    std::thread rosQueueThread;

    double update_rate;
    bool cmd_vel_received = false;
    double cmd_linear_x = 0, cmd_linear_y = 0, cmd_linear_z = 0;
    double cmd_angular_x = 0, cmd_angular_y = 0, cmd_angular_z = 0;
};

GZ_REGISTER_MODEL_PLUGIN(ModelPlugin)

}

7.2 Python插件接口

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo插件Python接口
"""

import rospy
from geometry_msgs.msg import Twist, Pose
from std_msgs.msg import Float64
import threading

class GazeboPluginInterface:
    """Gazebo插件接口"""

    def __init__(self, model_name):
        self.model_name = model_name

        # 发布者
        self.cmd_vel_pub = rospy.Publisher(
            f'/{model_name}/cmd_vel', Twist, queue_size=1)

        # 订阅者
        self.pose_sub = rospy.Subscriber(
            f'/{model_name}/pose', Pose, self.pose_callback)

        # 状态
        self.current_pose = None
        self.lock = threading.Lock()

    def pose_callback(self, msg):
        """位姿回调"""
        with self.lock:
            self.current_pose = msg

    def set_velocity(self, linear, angular):
        """设置速度"""
        cmd = Twist()
        cmd.linear.x = linear[0]
        cmd.linear.y = linear[1]
        cmd.linear.z = linear[2]
        cmd.angular.x = angular[0]
        cmd.angular.y = angular[1]
        cmd.angular.z = angular[2]

        self.cmd_vel_pub.publish(cmd)

    def get_pose(self):
        """获取位姿"""
        with self.lock:
            return self.current_pose

    def move_forward(self, speed=1.0):
        """前进"""
        self.set_velocity([speed, 0, 0], [0, 0, 0])

    def move_backward(self, speed=1.0):
        """后退"""
        self.set_velocity([-speed, 0, 0], [0, 0, 0])

    def turn_left(self, speed=1.0):
        """左转"""
        self.set_velocity([0, 0, 0], [0, 0, speed])

    def turn_right(self, speed=1.0):
        """右转"""
        self.set_velocity([0, 0, 0], [0, 0, -speed])

    def stop(self):
        """停止"""
        self.set_velocity([0, 0, 0], [0, 0, 0])

8. ros_control集成

8.1 控制器配置

yaml 复制代码
# config/controllers.yaml
joint_state_controller:
  type: joint_state_controller/JointStateController
  publish_rate: 50

position_controllers:
  joint1_position_controller:
    type: effort_controllers/JointPositionController
    joint: joint1
    pid: {p: 100.0, i: 0.01, d: 10.0}

  joint2_position_controller:
    type: effort_controllers/JointPositionController
    joint: joint2
    pid: {p: 100.0, i: 0.01, d: 10.0}

velocity_controllers:
  left_wheel_velocity_controller:
    type: effort_controllers/JointVelocityController
    joint: left_wheel_joint
    pid: {p: 100.0, i: 0.01, d: 10.0}

  right_wheel_velocity_controller:
    type: effort_controllers/JointVelocityController
    joint: right_wheel_joint
    pid: {p: 100.0, i: 0.01, d: 10.0}

trajectory_controller:
  type: position_controllers/JointTrajectoryController
  joints:
    - joint1
    - joint2
    - joint3
  gains:
    joint1: {p: 100, d: 1, i: 1, i_clamp: 1}
    joint2: {p: 100, d: 1, i: 1, i_clamp: 1}
    joint3: {p: 100, d: 1, i: 1, i_clamp: 1}

8.2 控制器管理

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
ros_control控制器管理
"""

import rospy
from controller_manager_msgs.srv import (
    LoadController, UnloadController,
    SwitchController, ListControllers
)
from std_msgs.msg import Float64
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint

class ControllerManager:
    """控制器管理器"""

    def __init__(self):
        rospy.init_node('controller_manager_interface')

        # 控制器管理服务
        self.load_controller_srv = rospy.ServiceProxy(
            '/controller_manager/load_controller', LoadController)
        self.unload_controller_srv = rospy.ServiceProxy(
            '/controller_manager/unload_controller', UnloadController)
        self.switch_controller_srv = rospy.ServiceProxy(
            '/controller_manager/switch_controller', SwitchController)
        self.list_controllers_srv = rospy.ServiceProxy(
            '/controller_manager/list_controllers', ListControllers)

        # 等待服务
        rospy.wait_for_service('/controller_manager/load_controller')

        # 控制器命令发布者
        self.command_publishers = {}

    def load_controller(self, controller_name):
        """加载控制器"""
        try:
            resp = self.load_controller_srv(name=controller_name)
            if resp.ok:
                rospy.loginfo(f"Controller '{controller_name}' loaded successfully")
            else:
                rospy.logerr(f"Failed to load controller '{controller_name}'")
            return resp.ok
        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def unload_controller(self, controller_name):
        """卸载控制器"""
        try:
            resp = self.unload_controller_srv(name=controller_name)
            if resp.ok:
                rospy.loginfo(f"Controller '{controller_name}' unloaded successfully")
            else:
                rospy.logerr(f"Failed to unload controller '{controller_name}'")
            return resp.ok
        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def start_controller(self, controller_name):
        """启动控制器"""
        try:
            resp = self.switch_controller_srv(
                start_controllers=[controller_name],
                stop_controllers=[],
                strictness=1  # BEST_EFFORT
            )
            if resp.ok:
                rospy.loginfo(f"Controller '{controller_name}' started")
            else:
                rospy.logerr(f"Failed to start controller '{controller_name}'")
            return resp.ok
        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def stop_controller(self, controller_name):
        """停止控制器"""
        try:
            resp = self.switch_controller_srv(
                start_controllers=[],
                stop_controllers=[controller_name],
                strictness=1  # BEST_EFFORT
            )
            if resp.ok:
                rospy.loginfo(f"Controller '{controller_name}' stopped")
            else:
                rospy.logerr(f"Failed to stop controller '{controller_name}'")
            return resp.ok
        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return False

    def list_controllers(self):
        """列出控制器"""
        try:
            resp = self.list_controllers_srv()

            print("Controllers:")
            for controller in resp.controller:
                status = "running" if controller.state == "running" else "stopped"
                print(f"  - {controller.name}: {controller.type} [{status}]")

            return resp.controller

        except rospy.ServiceException as e:
            rospy.logerr(f"Service call failed: {e}")
            return []

    def send_joint_command(self, controller_name, command):
        """发送关节命令"""
        topic = f"/{controller_name}/command"

        if topic not in self.command_publishers:
            self.command_publishers[topic] = rospy.Publisher(
                topic, Float64, queue_size=1)

        msg = Float64()
        msg.data = command
        self.command_publishers[topic].publish(msg)

    def send_trajectory(self, controller_name, joint_names, points):
        """发送轨迹"""
        topic = f"/{controller_name}/command"

        if topic not in self.command_publishers:
            self.command_publishers[topic] = rospy.Publisher(
                topic, JointTrajectory, queue_size=1)

        trajectory = JointTrajectory()
        trajectory.header.stamp = rospy.Time.now()
        trajectory.joint_names = joint_names

        for point_data in points:
            point = JointTrajectoryPoint()
            point.positions = point_data['positions']
            point.velocities = point_data.get('velocities', [])
            point.accelerations = point_data.get('accelerations', [])
            point.time_from_start = rospy.Duration(point_data['time'])
            trajectory.points.append(point)

        self.command_publishers[topic].publish(trajectory)

9. 仿真调试与优化

9.1 仿真性能分析

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Gazebo仿真性能分析
"""

import rospy
from rosgraph_msgs.msg import Clock
from gazebo_msgs.msg import PerformanceMetrics
import time
import psutil
import matplotlib.pyplot as plt

class SimulationPerformanceAnalyzer:
    """仿真性能分析器"""

    def __init__(self):
        rospy.init_node('simulation_performance_analyzer')

        # 订阅时钟
        self.clock_sub = rospy.Subscriber('/clock', Clock, self.clock_callback)

        # 性能数据
        self.real_times = []
        self.sim_times = []
        self.real_time_factors = []
        self.cpu_usage = []
        self.memory_usage = []

        # 开始时间
        self.start_real_time = time.time()
        self.start_sim_time = None

    def clock_callback(self, msg):
        """时钟回调"""
        current_sim_time = msg.clock.to_sec()
        current_real_time = time.time()

        if self.start_sim_time is None:
            self.start_sim_time = current_sim_time
            return

        # 计算经过的时间
        elapsed_sim = current_sim_time - self.start_sim_time
        elapsed_real = current_real_time - self.start_real_time

        # 计算实时因子
        if elapsed_real > 0:
            rtf = elapsed_sim / elapsed_real
        else:
            rtf = 0

        # 记录数据
        self.real_times.append(elapsed_real)
        self.sim_times.append(elapsed_sim)
        self.real_time_factors.append(rtf)

        # 系统资源使用
        self.cpu_usage.append(psutil.cpu_percent())
        self.memory_usage.append(psutil.virtual_memory().percent)

        # 定期输出
        if len(self.real_times) % 100 == 0:
            self.print_statistics()

    def print_statistics(self):
        """打印统计信息"""
        if not self.real_time_factors:
            return

        current_rtf = self.real_time_factors[-1]
        avg_rtf = sum(self.real_time_factors) / len(self.real_time_factors)
        min_rtf = min(self.real_time_factors)
        max_rtf = max(self.real_time_factors)

        avg_cpu = sum(self.cpu_usage) / len(self.cpu_usage)
        avg_mem = sum(self.memory_usage) / len(self.memory_usage)

        print("\n" + "="*50)
        print("SIMULATION PERFORMANCE")
        print("="*50)
        print(f"Current RTF: {current_rtf:.3f}")
        print(f"Average RTF: {avg_rtf:.3f}")
        print(f"Min RTF: {min_rtf:.3f}")
        print(f"Max RTF: {max_rtf:.3f}")
        print(f"Average CPU: {avg_cpu:.1f}%")
        print(f"Average Memory: {avg_mem:.1f}%")
        print("="*50)

    def plot_performance(self):
        """绘制性能图表"""
        fig, axes = plt.subplots(3, 1, figsize=(10, 8))

        # 实时因子
        axes[0].plot(self.real_times, self.real_time_factors)
        axes[0].set_ylabel('Real Time Factor')
        axes[0].set_title('Simulation Performance')
        axes[0].grid(True)
        axes[0].axhline(y=1.0, color='r', linestyle='--', label='Real-time')
        axes[0].legend()

        # CPU使用率
        axes[1].plot(self.real_times, self.cpu_usage)
        axes[1].set_ylabel('CPU Usage (%)')
        axes[1].grid(True)

        # 内存使用率
        axes[2].plot(self.real_times, self.memory_usage)
        axes[2].set_xlabel('Time (s)')
        axes[2].set_ylabel('Memory Usage (%)')
        axes[2].grid(True)

        plt.tight_layout()
        plt.show()

    def save_data(self, filename):
        """保存数据"""
        data = {
            'real_times': self.real_times,
            'sim_times': self.sim_times,
            'real_time_factors': self.real_time_factors,
            'cpu_usage': self.cpu_usage,
            'memory_usage': self.memory_usage
        }

        import pickle
        with open(filename, 'wb') as f:
            pickle.dump(data, f)

        print(f"Performance data saved to: {filename}")

10. 实战案例:完整仿真系统

10.1 移动机器人仿真系统

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
完整的移动机器人仿真系统
"""

import rospy
import tf2_ros
from geometry_msgs.msg import Twist, PoseStamped
from sensor_msgs.msg import LaserScan, Image
from nav_msgs.msg import Odometry, Path
import numpy as np

class MobileRobotSimulation:
    """移动机器人仿真系统"""

    def __init__(self, robot_name="mobile_robot"):
        rospy.init_node('mobile_robot_simulation')

        self.robot_name = robot_name

        # Gazebo接口
        self.gazebo_interface = GazeboROSInterface()

        # 控制器管理
        self.controller_manager = ControllerManager()

        # 传感器管理
        self.sensor_manager = SensorManager()

        # 初始化机器人
        self.spawn_robot()
        self.setup_controllers()
        self.setup_sensors()

        # 导航系统
        self.setup_navigation()

        # 控制循环
        self.control_timer = rospy.Timer(
            rospy.Duration(0.1), self.control_callback)

    def spawn_robot(self):
        """生成机器人"""
        # 读取URDF
        with open('mobile_robot.urdf', 'r') as f:
            robot_xml = f.read()

        # 初始位置
        initial_pose = Pose()
        initial_pose.position.x = 0
        initial_pose.position.y = 0
        initial_pose.position.z = 0.5
        initial_pose.orientation.w = 1.0

        # 生成机器人
        success = self.gazebo_interface.spawn_model(
            self.robot_name, robot_xml, initial_pose)

        if success:
            rospy.loginfo("Robot spawned successfully")
        else:
            rospy.logerr("Failed to spawn robot")

    def setup_controllers(self):
        """设置控制器"""
        # 加载控制器
        controllers = [
            'joint_state_controller',
            'left_wheel_velocity_controller',
            'right_wheel_velocity_controller'
        ]

        for controller in controllers:
            self.controller_manager.load_controller(controller)
            self.controller_manager.start_controller(controller)

    def setup_sensors(self):
        """设置传感器"""
        # 激光传感器
        laser_config = {
            'update_rate': 10,
            'samples': 720,
            'min_angle': -3.14159,
            'max_angle': 3.14159,
            'min_range': 0.1,
            'max_range': 10.0
        }
        self.sensor_manager.create_laser_sensor(
            'laser', 'laser_link', laser_config)

        # 相机传感器
        camera_config = {
            'update_rate': 30,
            'width': 640,
            'height': 480,
            'horizontal_fov': 1.3962634
        }
        self.sensor_manager.create_camera_sensor(
            'camera', 'camera_link', camera_config)

        # IMU传感器
        imu_config = {
            'update_rate': 100,
            'angular_velocity_noise': 0.0001,
            'linear_acceleration_noise': 0.0001
        }
        self.sensor_manager.create_imu_sensor(
            'imu', 'imu_link', imu_config)

    def setup_navigation(self):
        """设置导航"""
        # 速度发布者
        self.cmd_vel_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1)

        # 传感器订阅
        self.scan_sub = rospy.Subscriber('/scan', LaserScan, self.scan_callback)
        self.odom_sub = rospy.Subscriber('/odom', Odometry, self.odom_callback)

        # 目标订阅
        self.goal_sub = rospy.Subscriber('/move_base_simple/goal',
                                         PoseStamped, self.goal_callback)

        # 路径发布
        self.path_pub = rospy.Publisher('/path', Path, queue_size=1)

        # 状态
        self.current_pose = None
        self.current_scan = None
        self.goal_pose = None
        self.path = Path()

    def scan_callback(self, msg):
        """激光扫描回调"""
        self.current_scan = msg

        # 简单避障
        if self.current_scan:
            self.obstacle_avoidance()

    def odom_callback(self, msg):
        """里程计回调"""
        self.current_pose = msg.pose.pose

        # 更新路径
        pose_stamped = PoseStamped()
        pose_stamped.header = msg.header
        pose_stamped.pose = msg.pose.pose
        self.path.poses.append(pose_stamped)

        # 发布路径
        self.path.header = msg.header
        self.path_pub.publish(self.path)

    def goal_callback(self, msg):
        """目标回调"""
        self.goal_pose = msg.pose
        rospy.loginfo(f"New goal: ({msg.pose.position.x:.2f}, "
                     f"{msg.pose.position.y:.2f})")

    def obstacle_avoidance(self):
        """简单避障"""
        if not self.current_scan:
            return

        # 分析激光数据
        ranges = np.array(self.current_scan.ranges)

        # 前方扇区(-30度到+30度)
        front_sector = ranges[330:390]

        # 检测障碍物
        min_distance = np.min(front_sector[np.isfinite(front_sector)])

        cmd = Twist()

        if min_distance < 0.5:  # 障碍物太近
            # 停止并转向
            cmd.linear.x = 0.0
            cmd.angular.z = 0.5
            rospy.logwarn(f"Obstacle detected at {min_distance:.2f}m, turning...")
        else:
            # 正常前进
            cmd.linear.x = 0.5
            cmd.angular.z = 0.0

        self.cmd_vel_pub.publish(cmd)

    def control_callback(self, event):
        """控制回调"""
        if self.goal_pose and self.current_pose:
            # 简单的点对点导航
            dx = self.goal_pose.position.x - self.current_pose.position.x
            dy = self.goal_pose.position.y - self.current_pose.position.y

            distance = np.sqrt(dx**2 + dy**2)
            angle = np.arctan2(dy, dx)

            # 获取当前朝向
            q = self.current_pose.orientation
            _, _, yaw = tf.transformations.euler_from_quaternion(
                [q.x, q.y, q.z, q.w])

            angle_error = angle - yaw

            # 归一化角度
            while angle_error > np.pi:
                angle_error -= 2*np.pi
            while angle_error < -np.pi:
                angle_error += 2*np.pi

            cmd = Twist()

            if distance < 0.1:  # 到达目标
                cmd.linear.x = 0.0
                cmd.angular.z = 0.0
                self.goal_pose = None
                rospy.loginfo("Goal reached!")
            elif abs(angle_error) > 0.2:  # 需要转向
                cmd.linear.x = 0.0
                cmd.angular.z = 0.5 * np.sign(angle_error)
            else:  # 前进
                cmd.linear.x = min(0.5, distance)
                cmd.angular.z = 0.5 * angle_error

            self.cmd_vel_pub.publish(cmd)

def main():
    """主函数"""
    simulation = MobileRobotSimulation()

    try:
        rospy.spin()
    except KeyboardInterrupt:
        rospy.loginfo("Shutting down simulation...")

if __name__ == '__main__':
    main()

11. 总结与最佳实践

11.1 本文总结

通过本文的学习,你已经掌握了:

  1. Gazebo基础:理解Gazebo架构和ROS集成
  2. 环境搭建:配置Gazebo仿真环境
  3. URDF转换:添加Gazebo特定标签
  4. 传感器仿真:配置各类传感器
  5. 物理引擎:优化物理仿真参数
  6. 环境建模:创建仿真世界
  7. 插件开发:自定义Gazebo插件
  8. ros_control:集成控制系统
  9. 调试优化:性能分析和优化
  10. 完整系统:构建完整仿真系统

11.2 最佳实践

方面 建议 原因
模型简化 使用简单碰撞模型 提高仿真性能
更新频率 传感器10-30Hz 平衡精度和性能
物理步长 0.001秒或更小 确保稳定性
实时因子 监控RTF 评估性能
插件使用 优先使用现有插件 减少开发时间
资源管理 及时删除不用的模型 节省内存

11.3 常见问题解决

  1. 仿真卡顿

    • 降低更新频率
    • 简化碰撞模型
    • 减少传感器数量
  2. 物体穿透

    • 减小物理步长
    • 增加迭代次数
    • 调整接触参数
  3. 传感器噪声

    • 配置噪声模型
    • 使用滤波器
    • 调整采样率
  4. 控制不稳定

    • 调整PID参数
    • 检查惯性参数
    • 验证关节限制

11.4 下一步学习

在下一篇文章中,我们将学习:

  • 导航与路径规划:move_base框架
  • SLAM建图:gmapping和cartographer
  • 定位算法:AMCL粒子滤波
  • 避障策略:动态窗口法

版权声明:本文为原创文章,转载请注明出处

💡 学习建议:Gazebo仿真是机器人开发的重要工具,可以在安全的虚拟环境中测试算法。建议先从简单的模型开始,逐步增加复杂度。重点掌握传感器配置和物理参数调优,这对仿真真实性至关重要。


下一篇预告:《【ROS1从入门到精通】第12篇:导航与路径规划(让机器人自主导航)》

敬请期待!

相关推荐
kyle~2 小时前
导航---Nav2导航框架概览
c++·机器人·ros2·导航
Deepoch2 小时前
从“飞行相机”到“空中智能体”:无人机如何重构行业生产力
人工智能·科技·机器人·无人机·开发板·具身模型·deepoc
具身智能之心3 小时前
北大&智源研究院最新!RoboOS-NeXT:“记忆 + 分层架构” 实现通用多机器人协作
机器人·具身智能
具身智能之心3 小时前
西湖大学最新!RobustVLA:面向VLA模型的鲁棒性感知强化后训练方法(优于SOTA方案)
机器人·具身智能·vla模型
万俟淋曦20 小时前
【论文速递】2025年第40周(Sep-28-Oct-04)(Robotics/Embodied AI/LLM)
人工智能·深度学习·ai·机器人·大模型·论文·具身智能
万俟淋曦21 小时前
【论文速递】2025年第38周(Sep-14-20)(Robotics/Embodied AI/LLM)
人工智能·深度学习·机器学习·机器人·大模型·论文·具身智能
诗远Yolanda1 天前
【EI检索会议】第二届国际人工智能创新研讨会(IS-AII 2026)
图像处理·人工智能·深度学习·机器学习·计算机视觉·机器人
万俟淋曦1 天前
【论文速递】2025年第37周(Sep-07-13)(Robotics/Embodied AI/LLM)
人工智能·ai·机器人·大模型·论文·robotics·具身智能
万俟淋曦1 天前
【论文速递】2025年第42周(Oct-12-18)(Robotics/Embodied AI/LLM)
人工智能·ai·机器人·大模型·论文·robotics·具身智能