通过ROS与navigation教程学习，了解如何为机器人整合navigation包，配置TF，实现导航，发布里程，增加传感器等
ROS与navigation教程-目录 说明： 介绍如何为机器人整合导航包，实现有效控制和自主导航等功能 目录： ROS与navigation教程-目录 ROS与navigation教程-设置机器人使用TF ROS与navigation教程-基本导航调试指南 ROS与navigation教程-安装和配置导航包 ROS与navigation教程-结合RVIZ与导航包 ROS与navigation教程-发布里程计消息 ROS与navigation教程-发布传感器数据 ROS与navigation教程-编写自定义全局路径规划 ROS与navigation教程-stage仿真 ROS与navigation教程-示例-激光发布(C++) ROS与navigation教程-示例-里程发布(C++) ROS与navigation教程-示例-点云发布(C++) ROS与navigation教程-示例-机器人TF设置(C++) ROS与navigation教程-示例-导航目标设置(C++) ROS与navigation教程-turtlebot-整合导航包简明指南 ROS与navigation教程-turtlebot-SLAM地图构建 ROS与navigation教程-turtlebot-现有地图的自主导航 参考: http://wiki.ros.org/navigation/Tutorials
ROS与navigation教程-设置机器人使用TF 说明： 介绍如何配置机器人让其使用TF 注意：本教程假设您已完成ROS教程和tf基础教程。 本教程的代码在robot_setup_tf_tutorial包中可用 步骤： (1)变换配置 许多ROS包需要使用tf软件库发布机器人的变换树。在抽象层，变换树根据不同坐标系之间的平移和旋转来定义偏移量。为了使这更具体，考虑一个简单的机器人的例子，它具有安装在其顶部的单个激光器的移动基座。在提及机器人时，我们定义两个坐标系：一个对应于机器人底座的中心点，一个坐标系安装在基座顶部的激光中心点。我们还给他们名字，以供参考。我们将调用附加到移动基础“base_link”的坐标系（对于导航，它重要的是将其放置在机器人的旋转中心），我们也会调用附加到激光“base_laser”的坐标系。有关框架命名约定，请参见REP 105 在这一点上，我们假设激光器的一些数据以与激光中心点的距离的形式来表达。换句话说，我们在“base_laser”坐标系中有一些数据。现在假设我们想要获取这些数据并使用它来帮助移动基地避免活动空间的障碍。要做到这一点，我们需要一种将我们从“base_laser”坐标系转换成“base_link”坐标系的激光扫描的方法。实质上，我们需要定义“base_laser”和“base_link”坐标系之间的关系。 在定义这种关系时，假设我们知道激光器在移动基座的中心点之上10厘米和向前20厘米处安装。这给了我们一个将“base_link”坐标系与“base_laser”坐标系相关联的平移值和偏移值。具体来说，我们知道要从“base_link”坐标系到“base_laser”坐标系的数据，我们必须应用（x：0.1m，y：0.0m，z：0.2m）的转换，并从“ base_laser“坐标系到”base_link“坐标系，我们必须应用相反的变换（x：-0.1m，y：0.0m，z：-0.20m）。 我们可以选择自己管理这种关系，这意味着在必要时存储和应用帧之间的适当变换，但随着坐标系的数量增加，这成为一个真正的痛苦。幸运的是，我们不用自己做这个工作。相反，我们将使用tf定义“base_link”和“base_laser”之间的关系，并让它管理两个坐标系之间的变换。 要使用tf定义和存储“base_link”和“base_laser”坐标系之间的关系，我们需要将它们添加到一个变换树中。在概念上，变换树中的每个节点对应于坐标系，每个方向对应于需要应用从当前节点移动到其子节点的变换。Tf使用树结构来确保只有一个遍历将任何两个坐标系连接在一起，并假设树中的所有方向都是从父节点引导到子节点。 要为我们的简单示例创建一个变换树，我们将创建两个节点，一个用于“base_link”坐标系，一个用于“base_laser”坐标系。为了创建它们之间的方向，我们首先需要决定哪个节点是父节点，哪些节点是子节点。记住，这种区别很重要，因为tf假定所有转换都从父对象移动到子对象。让我们选择“base_link”坐标系作为父节点，因为随着其他部分/传感器被添加到机器人，通过遍历“base_link”框架，它们将最有意义地与“base_laser”坐标系相关。这意味着连接“base_link”和“base_laser”的变换应为（x：0.1m，y：0.0m，z：0.2m）。使用这个变换树设置，将在“base_laser”坐标系中接收到的激光扫描转换为“base_link”坐标系就像调用tf库一样简单。我们的机器人可以使用这些信息来理解“base_link”框架中的激光扫描，并安全地计划避开环境中的障碍物。 (2)编写代码 希望上面的例子有助于在概念层面了解tf。现在，我们必须使用变换树并用代码创建它。在这个例子中，假设你已熟悉ROS，因此如果任何术语或概念不熟悉，请确保查看ROS文档。 假设我们有一个高级别的任务描述在“base_laser”坐标系中获取点并将其转换为“base_link”坐标系。我们需要做的第一件事是创建一个负责在系统中发布转换的节点。接下来，我们必须创建一个节点来监听通过ROS发布的变换数据，并应用它来转换一个点。我们将首先为源代码创建一个包，然后我们给它一个简单的名字，如“robot_setup_tf”我们将依赖于roscpp，tf和geometry_msgs。 $ cd %TOP_DIR_YOUR_CATKIN_WS%/src $ catkin_create_pkg robot_setup_tf roscpp tf geometry_msgs 请注意，您必须运行上面的命令（例如~/ros，您可能已为之前的教程创建）。 在fuerte，groovy和hydro中的替代方法：在navigation_tutorials包中有一个标准的robot_setup_tf_tutorial包。您可能希望通过以下方式安装 (%YOUR_ROS_DISTRO% can be { fuerte, groovy } .)： $ sudo apt-get install ros-%YOUR_ROS_DISTRO%-navigation-tutorials (3)广播变换 现在我们有了我们的软件包，我们需要创建一个节点来完成在ROS 上广播base_laser → base_link变换的工作。在刚刚创建的robot_setup_tf包中，启动您喜欢的编辑器，并将以下代码粘贴到src/tf_broadcaster.cpp文件中。 #include <ros/ros.h> #include <tf/transform_broadcaster.h> int main(int argc, char** argv){ ros::init(argc, argv, "robot_tf_publisher"); ros::NodeHandle n; ros::Rate r(100); tf::TransformBroadcaster broadcaster; while(n.ok()){ broadcaster.sendTransform( tf::StampedTransform( tf::Transform(tf::Quaternion(0, 0, 0, 1), tf::Vector3(0.1, 0.0, 0.2)), ros::Time::now(),"base_link", "base_laser")); r.sleep(); } } 现在，我们来更详细看看发布base_link → base_laser变换相关的代码 #include <tf/transform_broadcaster.h> tf包提供了一个 tf::TransformBroadcaster的实现，以帮助使发布变换的任务更容易。要使用TransformBroadcaster，我们需要包含tf/transform_broadcaster.h头文件。 tf::TransformBroadcaster broadcaster; 在这里，我们创建一个TransformBroadcaster对象，我们稍后将通过网络发送base_link → base_laser变换。 broadcaster.sendTransform( tf::StampedTransform( tf::Transform(tf::Quaternion(0, 0, 0, 1), tf::Vector3(0.1, 0.0, 0.2)), ros::Time::now(),"base_link", "base_laser")); 这是实际工作完成的地方。使用TransformBroadcaster发送变换需要五个参数。首先，我们传递旋转变换，它由btQuaternion指定，需要在两个坐标系之间进行任何旋转。在这种情况下，我们不要施加旋转，所以我们发送一个由俯仰，滚动和偏航值等于零的btQuaternion。第二，btVector3可以应用到任何变换的，所以我们创建一个btVector3对应的激光的x偏移10厘米和Z偏移距离机器人基座20厘米。第三，我们需要给出发布的时间戳，我们将用ros::Time::now()。第四，我们需要传递我们创建的链接的父节点的名称，在这种情况下是“base_link”。第五，我们需要传递我们创建的链接的子节点的名称，在这种情况下是“base_laser”。 (4)使用变换 以上，我们创建了一个通过ROS发布base_laser → base_link变换的节点。现在，我们要编写一个节点，将使用该变换在“base_laser”坐标系中取一点，并将其转换为“base_link”坐标系中的一个点。我们将首先将下面的代码粘贴到一个文件中，并进行更详细的解释。在robot_setup_tf包中，创建一个名为src/tf_listener.cpp的文件，并粘贴以下内容： #include <ros/ros.h> #include <geometry_msgs/PointStamped.h> #include <tf/transform_listener.h> void transformPoint(const tf::TransformListener& listener){ //we'll create a point in the base_laser frame that we'd like to transform to the base_link frame geometry_msgs::PointStamped laser_point; laser_point.header.frame_id = "base_laser"; //we'll just use the most recent transform available for our simple example laser_point.header.stamp = ros::Time(); //just an arbitrary point in space laser_point.point.x = 1.0; laser_point.point.y = 0.2; laser_point.point.z = 0.0; try{ geometry_msgs::PointStamped base_point; listener.transformPoint("base_link", laser_point, base_point); ROS_INFO("base_laser: (%.2f, %.2f. %.2f) -----> base_link: (%.2f, %.2f, %.2f) at time %.2f", laser_point.point.x, laser_point.point.y, laser_point.point.z, base_point.point.x, base_point.point.y, base_point.point.z, base_point.header.stamp.toSec()); } catch(tf::TransformException& ex){ ROS_ERROR("Received an exception trying to transform a point from \"base_laser\" to \"base_link\": %s", ex.what()); } } int main(int argc, char** argv){ ros::init(argc, argv, "robot_tf_listener"); ros::NodeHandle n; tf::TransformListener listener(ros::Duration(10)); //we'll transform a point once every second ros::Timer timer = n.createTimer(ros::Duration(1.0), boost::bind(&transformPoint, boost::ref(listener))); ros::spin(); } 现在我们将一步一步解释 #include <tf/transform_listener.h> 在这里，我们包含tf/transform_listener.h 头文件，我们需要创建一个tf::TransformListener。一个TransformListener对象自动订阅了ROS变换消息主题和管理所有将在网络中广播的变换数据。 void transformPoint(const tf::TransformListener& listener){ 我们将创建一个函数，给定一个TransformListener，在“base_laser”坐标系中取一点，并将其转换为“base_link”坐标系。这个函数将作为在我们程序的main()中创建的ros::Timer的回调，并且每秒都会触发。 //we'll create a point in the base_laser frame that we'd like to transform to the base_link frame geometry_msgs::PointStamped laser_point; laser_point.header.frame_id = "base_laser"; //we'll just use the most recent transform available for our simple example laser_point.header.stamp = ros::Time(); //just an arbitrary point in space laser_point.point.x = 1.0; laser_point.point.y = 0.2; laser_point.point.z = 0.0; 在这里，我们将创建一个geometry_msgs::PointStamped的点。消息名称结尾处的“Stamped”只是意味着它包含一个头，允许我们将时间戳和frame_id与消息相关联。我们将laser_point消息的Stamp字段设置为ros::Time()，它是一个特殊的时间值，允许我们向TransformListener询问最新的可用变换。对于标题的frame_id字段，我们将把它设置为“base_laser”，因为我们在“base_laser”坐标系中创建一个点。最后，我们将设置一些数据，例如x：1.0，y：0.2和z：0.0的点。 try{ geometry_msgs::PointStamped base_point; listener.transformPoint("base_link", laser_point, base_point); ROS_INFO("base_laser: (%.2f, %.2f. %.2f) -----> base_link: (%.2f, %.2f, %.2f) at time %.2f", laser_point.point.x, laser_point.point.y, laser_point.point.z, base_point.point.x, base_point.point.y, base_point.point.z, base_point.header.stamp.toSec()); } 现在我们将“base_laser”坐标系中的点转换成“base_link”坐标系的点。为此，我们将使用TransformListener对象，调用transformPoint()，并使用三个参数：我们要将点转换为（“base_link”在我们的例子中）坐标系的名称，我们正在转换的点，以及存储变换点。所以在调用transformPoint()之后，base_point保存与laser_point相同的信息，包含变换后的信息。 catch(tf::TransformException& ex){ ROS_ERROR("Received an exception trying to transform a point from "base_laser" to "base_link": %s", ex.what()); } 如果由于某种原因base_laser → base_link变换不可用（也许tf_broadcaster没有运行），当我们调用transformPoint()时，TransformListener可能会引发异常。为了确保我们优雅地处理这个问题，我们将捕获异常，并为用户打印一个错误。 (5)编译代码 现在我们已经写了一个例子，我们需要构建它。打开由roscreate-pkg自动生成的CMakeLists.txt文件，并将以下行添加到文件的底部。 add_executable(tf_broadcaster src/tf_broadcaster.cpp) add_executable(tf_listener src/tf_listener.cpp) target_link_libraries(tf_broadcaster ${catkin_LIBRARIES}) target_link_libraries(tf_listener ${catkin_LIBRARIES}) 接下来，确保保存文件并构建包。 $ cd %TOP_DIR_YOUR_CATKIN_WS% $ catkin_make (6)运行代码 让我们来看看ROS实际发生了什么。对于本节，您将需要打开三个终端。 新开终端，运行roscore。 roscore 新开终端，运行tf_broadcaster rosrun robot_setup_tf tf_broadcaster 新开终端，运行tf_listener，将我们的模拟点从“base_laser”坐标系变换为“base_link”坐标系一个点 rosrun robot_setup_tf tf_listener 如果一切正常，您应该看到以下输出显示一个点从“base_laser”坐标系转换到“base_link”坐标系。 [ INFO] 1248138528.200823000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138528.19 [ INFO] 1248138529.200820000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138529.19 [ INFO] 1248138530.200821000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138530.19 [ INFO] 1248138531.200835000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138531.19 [ INFO] 1248138532.200849000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138532.19 恭喜，您刚刚为平面激光器编写了一个成功的变换广播。下一步是将我们用于此示例的PointStamped替换为ROS中的传感器流。
ROS与navigation教程-基本导航调整指南 说明： 介绍如何调整机器人上的ROS导航包 步骤： (1) 机器人导航需要那些准备? 在调整新机器人上的导航包时遇到的大部分问题都在本地规划器调谐参数之外的区域。机器人的里程计，定位，传感器以及有效运行导航的其他先决条件常常会出错。所以，我做的第一件事是确保机器人本身正在准备好导航。这包括三个组件检查：距离传感器，里程计和定位。 距离传感器 如果机器人没有从其距离传感器（例如激光器）获取信息，那么导航将不起作用。我将确保我可以在rviz中查看传感器信息，它看起来相对正确，并以预期的速度进入。 里程计 通常我会很难使机器人正确定位。它将不断迷失，我会花费大量的时间调试AMCL的参数，发现真正的罪魁祸首是机器人的测距。因此，我总是运行两个健全检查，以确保我相信机器人的里程计。 第一个测试检查角速度是否合理。我打开rviz，将坐标系设置为“odom”，显示机器人提供的激光扫描，将该主题的衰减时间设置为高（类似20秒），并执行原地旋转。然后，我看看扫描出来的边线在随后的旋转中如何相互匹配。理想情况下，每次扫描将刚好落在相互的顶端，会重叠在一起，但是有些旋转漂移是预期的，所以我只是确保扫描之间误差，不会超过一度或两度以上。 第二个测试检查线速度是否合理。机器人放置在与距离墙壁几米远地方，然后以上面相同的方式设置rviz。接着我将驱动机器人向墙壁前进，从rviz中聚合的激光扫描看看扫描出来边线的厚度。理想情况下，墙体应该看起来像一个扫描，但我只是确保它的厚度不超过几厘米。如果显示扫描边线的分散在半米以上，但有些可能是错误的测距。 其他的测试角速和线速方法： 线速标定：http://www.ncnynl.com/archives/201701/1217.html 角速标定：http://www.ncnynl.com/archives/201701/1218.html 线速标定：http://www.ncnynl.com/archives/201707/1812.html 角速标定：http://www.ncnynl.com/archives/201707/1813.html 定位 假设里程计和激光扫描仪都能合理地执行，建图和调整AMCL通常并不会太糟糕。首先，我将运行gmapping或karto，并操纵机器人来生成地图。然后，我将使用该地图与AMCL，并确保机器人保持定位。如果我运行的机器人的距离不是很好，我会用AMCL的测距模型参数玩一下。对整个系统的一个很好的测试是确保激光扫描和地图可以在rviz的“地图”坐标系中可视化，并且激光扫描与环境地图很好地匹配。 (2)代价地图 一旦我的机器人满足导航的先决条件，我想确保代价地图的设置和配置正确。 我会在如何配置ROS导航教程和costmap_2d文档详细说明，但是我会给出一些我经常做的事情的提示。 如下建议可用来调整代价地图： 确保根据传感器实际发布的速率为每个观测源设置expected_update_rate参数。我通常在这里给出相当的容忍度，把检查的期限提高到我期望的两倍，但是当传感器低于预期速率时，它很容易从导航中接收警告。 为系统适当设置transform_tolerance参数。查看使用tf从“base_link”坐标系到“map”坐标系的转换的预期延迟。我通常使用tf_monitor查看系统的延迟，并将参数保守地设置为关闭。另外，如果tf_monitor报告的延迟足够大，我可能会随时看看导致延迟的原因。这有时会导致我发现关于给定机器人的变换如何发布的问题。 在缺乏处理能力的机器人上，我会考虑关闭map_update_rate参数。然而，在这样做时，我考虑到这将导致传感器数据快速进入代价地图的延迟，这反过来会降低机器人对障碍物的反应速度。 该publish_frequency参数是在rviz可视化costmap有用。然而，特别是对于大型全局地图，该参数可能导致事情运行缓慢。在生产系统中，我考虑降低成本图发布的速度，当我需要可视化非常大的地图时，我确定设置速度真的很低。 是否对代价地图使用voxel_grid或costmap模型的决定在很大程度上取决于机器人具有的传感器套件。调整代价地图为基于3D-based代价地图更多是涉及未知空间的考虑。如果我正在使用的机器人只有一个平面激光，我总是使用costmap模型的地图。 有时，它只能在里程坐标系中运行导航。要做到这一点，我发现最容易做的事情就是我的复制local_costmap_params.yaml文件覆盖global_costmap_params.yaml文件并更改了地图宽度和高度比如10米。如果需要独立的定位性能来调整导航，这是一个简单易行的方法 我倾向于根据机器人的尺寸和处理能力选择我使用的地图的分辨率。在具有很多处理能力并需要适应狭窄空间的机器人，如PR2，我会使用细粒度的地图...将分辨率设置为0.025米。对于像roomba这样的东西，我可能会以高达0.1米的分辨率去降低计算量。 Rviz是验证代价地图正常工作的好方法。我通常从代价地图中查看障碍物数据，并确保在操纵杆控制下驱动机器人时，它与地图和激光扫描相一致。这是对传感器数据以合理的方式进入代价地图的合理检查。如果我决定用机器人跟踪未知的空间，主要是这些机器人正在使用voxel_grid模型的代价地图，我一定要看看未知的空间可视化，看看未知的空间被以合理的方式清除。是否正确地从代价地图中清除障碍物的一个很好的检查方法是简单地走在机器人的前面，看看它是否都成功地看到我，并清除我。 当导航包仅运行costmap时，检查系统负载是一个好主意。这意味着提高move_base节点，但不会发送目标点并查看负载。如果计算机在这个时候陷入僵局，我知道如果我想要运行规划器的话，我需要做一些CPU参数调整。 (3) 局部规划器 如果通过代价地图的操作令人满意，我将继续调整局部规划器参数。在具有合理加速度限制的机器人上，我通常使用dwa_local_planner，对于那些具有较低加速度限制的机器人，并且可以从每个步骤考虑到加速限制的，我将使用base_local_planner。调整dwa_local_planner比调整base_local_planner更为愉快，因为它的参数是动态可配置的。为导航包添加dynamic_reconfigure也已经在计划中。针对规划器的提示： 对于给定的机器人最重要的是，正确设置了加速度限制参数。如果这些参数关闭，只能期望来自机器人的次优行为。如果我不知道机器人的加速度极限是什么，我会花点时间写出一个脚本，让电机以最大平移和旋转速度命令运行一段时间，看看报告的里程计速度（假设里程会给出合理的估计），并从中得出加速度极限。合理设置这些参数可以节省很多时间。 如果我正在使用的机器人具有低加速度限制，我确保我正在运行base_local_planner，其中dwa设置为false。将dwa设置为true后，我还将确保根据处理能力将vx_samples参数更新为8到15之间的值。这将允许在展示中生成非圆形曲线。 如果我正在使用的机器人的定位并不是很好，我将确保将目标公差参数设置得比我想象的要高一些。如果机器人具有较高的最小旋转速度以避免在目标点的振荡，那么我也将提高旋转公差。 如果我使用低分辨率的CPU原因，我有时会提高sim_granularity参数，以节省一些周期。 我实际上很少发现自己改变了path_distance_bias和goal_distance_bias（对于base_local_planner这些参数被称为pdist_scale和gdist_scale）参数在计划者上非常多。当我这样做时，通常是因为我试图限制本地规划器自由，让计划的路径与除NavFn以外的全局规划器合作。将path_distance_bias参数增大，将使机器人更紧密地跟随路径，同时以快速向目标向前移动。如果这个值设置得太高，机器人将会拒绝移动，因为移动的代价大于停留在路径上的位置。简单来说就是让实际移动更接近全局路径还是本地路径。 如果我想以聪明的方式介绍代价函数，我将确保将meter_scoring参数设置为true。这使得它在代理函数中的距离以米而不是单元格，也意味着我可以调整一个地图分辨率的代价函数，并且当我移动到他人时期望合理的行为。此外，您现在可以通过将publish_cost_grid参数设置为true来显示本地计划程序在rviz中生成的代价函数。（这不管怎么说，从来没有把它放入文档，我会很快到那个时候）。考虑到以米为单位的代价函数，我可以计算出移动1米的成本与目标平衡的成本的折衷。这倾向于给我一个如何调整东西的体面的想法。 轨迹从其端点得分。这意味着将sim_time参数设置为不同的值可能对机器人的行为有很大的影响。我通常将此参数设置在1-2秒之间，其中设置更高可以导致稍微平滑的轨迹，确保乘以sim_period的最小速度小于目标的两倍。否则，机器人将倾向于在其目标位置的范围之外的位置旋转，而不是朝向目标移动。 精确的轨迹模拟也取决于距离测距的合理速度估计。这来自于dwa_local_planner和base_local_planner都使用这种速度估计以及机器人的加速度限制来确定规划周期的可行速度空间。虽然来自测距的速度估计不一定是完美的，但确保其至少接近得到最佳行为是重要的。 ROS导航调整指南 如果您的机器人正在导航准备就绪，而您即将通过优化机器人的导航行为的过程，此处是由郑开宇创建的ROS导航调整指南。它讨论了包括设置速度和加速度的组件，全局规划器，本地规划器（特别是DWA本地规划），代价地图，AMCL（定位）， recovery behaviors等。这是一个用于改进SCITOS G5机器人导航系统的视频演示，基于本指南中提出的想法。本指南绝对不完整（如AMCL部分需要更多讨论），并可能包含错误。如有问题，随时向这个Github Repository提交问题或提出请求！ 参考： https://github.com/zkytony/ROSNavigationGuide/blob/master/main.pdf
ROS与navigation教程-安装和配置导航包 说明： 介绍如何在机器人整合导航包。包括使用TF发送变换，发布里程信息，发布传感器数据以及基本导航包配置。 (1)机器人配置： 机器人配置图 为使用导航包需要机器人以特定方式配置。上图显示了此配置的概述。白色组件是已实现的必需组件，灰色组件是已实现的可选组件，必须为每个机器人平台创建蓝色组件。以下部分提供了导航包的先决条件以及如何满足每个要求的说明。 ROS 导航堆栈假设机器人正在使用ROS。有关如何在机器人上安装ROS的说明，请参阅ROS文档。 变换配置（其他变换） 导航包要求机器人使用tf发布关于坐标系之间的关系的信息。有关设置此配置的详细教程，请参见：变换配置 传感器信息（传感器源） 导航包使用来自传感器的信息来避开地图上的障碍物，它假定这些传感器在ROS上发布 sensor_msgs/LaserScan 或 sensor_msgs/PointCloud消息。有关通过ROS发布这些消息的信息，请参阅“ROS发布传感器流”教程。此外，还有一些传感器已经有相关处理的ROS驱动程序。 支持的传感器及其相应驱动程序的链接如下所示： 符合SCIP2.2标准的Hokuyo激光设备以及Hokuyo型号04LX，30LX- urg_node SICK LMS2xx激光器 - sicktoolbox_wrapper 里程信息（里程计源） 导航包需要使用TF和nav_msgs/Odometry消息来发布里程信息。有关发布测距信息的教程可以在这里找到：在ROS上发布测距信息。 odometry支持的平台及其相应驱动程序的链接如下所示： Videre Erratic: erratic_player PR2: pr2_mechanism_controllers 基本控制器（基本控制器） 导航包假设它向机器人的“cmd_vel”话题，发送geometry_msgs/Twist类型的消息速度命令。这意味着必须有一个订阅“cmd_vel”话题的节点能够执行 (vx, vy, vtheta) <==> (cmd_vel.linear.x, cmd_vel.linear.y, cmd_vel.angular.z) 的速度并将其转换为电机命令以发送到移动基站。 支持的基础控制平台及其相应驱动程序的链接如下： Videre Erratic: erratic_player PR2: pr2_mechanism_controllers 建图（map_server） 导航包不需要地图操作，但为了本教程的目的，我们假设您有一个。有关创建您的环境地图的详细信息，请参阅构建地图教程。 (2)导航包设置 假定上述机器人设置的所有要求已经得到满足。这意味着机器人必须使用TF发布坐标系信息，接收所有能被导航包使用的传感器sensor_msgs/LaserScan或sensor_msgs/PointCloud的消息，并使用tf和nav_msgs/Odometry 发布里程消息，同时也发布速度命令到移动基座。如果您的机器人中没有满足这些要求，请参阅上面的“机器人配置”部分，了解完成它们的说明。 创建包 本教程的第一步是创建一个包，其中我们将存储导航包的所有配置和启动文件。此软件包将依赖于用于满足上述“机器人配置”部分以及包含导航包高级接口的move_base软件包的任何软件包。因此，创建命令为： catkin_create_pkg my_robot_name_2dnav move_base my_tf_configuration_dep my_odom_configuration_dep my_sensor_configuration_dep 此命令将创建一个具有必要依赖关系的包，以便在机器人上运行导航包。 创建机器人配置启动文件 现在我们有一个工作空间用于我们所有的配置和启动文件，我们将创建一个roslaunch文件，它将启动所有硬件和发布机器人需要TF变换。启动您最喜欢的编辑器，并将以下代码段粘贴到名为my_robot_configuration.launch的文件中。当然，您应该随意将文本“my_robot”替换为实际机器人的名称。我们还必须对启动文件进行类似的更改，如下所述，因此请确保您阅读本节的其余部分。 <launch> <node pkg="sensor_node_pkg" type="sensor_node_type" name="sensor_node_name" output="screen"> </node> <node pkg="odom_node_pkg" type="odom_node_type" name="odom_node" output="screen"> </node> <node pkg="transform_configuration_pkg" type="transform_configuration_type" name="transform_configuration_name" output="screen"> </node> </launch> 现在我们有一个启动文件的模板，但我们需要填写我们的特定机器人。我们将在下面的每一部分中详细介绍需要做的更改 <launch> <node pkg="sensor_node_pkg" type="sensor_node_type" name="sensor_node_name" output="screen"> 在本节中，我们将介绍机器人用于导航的任何传感器。将“sensor_node_pkg”替换为传感器的ROS驱动程序的包名称，传感器驱动程序类型“sensor_node_type”，传感器节点所需名称的“sensor_node_name”以及带有任何参数的“sensor_param”您的节点可能需要。请注意，如果您有多个传感器用于将信息发送到导航包，则应在此处启动所有传感器。 <node pkg="odom_node_pkg" type="odom_node_type" name="odom_node" output="screen"> </node> 在本部分中，我们将为基座启动里程计。再次，您需要将pkg，type，name和param规范替换为与实际启动的节点名称。 <node pkg="transform_configuration_pkg" type="transform_configuration_type" name="transform_configuration_name" output="screen"> </node> 在本节中，我们将为机器人启动变换配置。再次，您需要将pkg，type，name和param规范替换为与实际启动的节点名称。 代价地图配置(local_costmap) & (global_costmap) 导航包使用两个成本地图来存储有关地图的障碍物信息。一个成本地图用于全局规划，意味着在整个环境中创建长期计划，另一个用于本地规划和避障。有一些配置选项，我们希望两个代价地图都使用，一些我们要在每种代价地图上单独设置。因此，下面有三个部分用于代价地图的配置：常用配置选项，全局配置选项和本地配置选项。 注意：以下部分仅涵盖了代价地图的基本配置选项。有关各种选项的文档，请参阅costmap_2d文档 通用配置 (local_costmap) & (global_costmap) 导航包使用代价地图存储有关障碍物的信息。为了正确执行此操作，我们需要为costmaps指定应该监听传感器主题和更新频率。我们创建一个名为costmap_common_params.yaml的文件，如下所示，并填写： obstacle_range: 2.5 raytrace_range: 3.0 footprint: [[x0, y0], [x1, y1], ... [xn, yn]] #robot_radius: ir_of_robot inflation_radius: 0.55 observation_sources: laser_scan_sensor point_cloud_sensor laser_scan_sensor: {sensor_frame: frame_name, data_type: LaserScan, topic: topic_name, marking: true, clearing: true} point_cloud_sensor: {sensor_frame: frame_name, data_type: PointCloud, topic: topic_name, marking: true, clearing: true} 让我们将上面的文件分解说明。 obstacle_range: 2.5 raytrace_range: 3.0 这些参数设置了代价地图中的障碍物信息的阈值。 "obstacle_range" 参数确定最大范围传感器读数，这将导致障碍物被放入代价地图中。在这里，我们把它设置在2.5米，这意味着机器人只会更新其地图包含距离移动基座2.5米以内的障碍物的信息。“raytrace_range”参数确定了用于清除指定范围外的空间。将其设置为3.0米，这意味着机器人将尝试清除3米外的空间。 footprint: [[x0, y0], [x1, y1], ... [xn, yn]] #robot_radius: ir_of_robot inflation_radius: 0.55 在这里，我们设置机器人的占用面积或机器人的半径是圆形。在指定占地面积的情况下，假设机器人的中心处于（0.0,0.0），并且支持顺时针和逆时针规格。我们还将设定代价地图的膨胀半径。膨胀半径应设定为在代价地图的与障碍物保持安全的最大距离。例如，将膨胀半径设定为0.55米，意味着机器人针对相同的障碍物获取的所有路径都保持距离障碍物0.55米或更远。 observation_sources: laser_scan_sensor point_cloud_sensor “observe_sources”传感器源参数定义把信息传递给代价地图的传感器列表，以空格分隔每个传感器，每个传感器分别定义。 laser_scan_sensor: {sensor_frame: frame_name, data_type: LaserScan, topic: topic_name, marking: true, clearing: true} 设置上述传感器源observation_sources的传感器信息。以laser_scan_sensor作为例子，应将“frame_name”参数设置为传感器坐标系的名称，“data_type”参数应根据话题使用的消息设置为LaserScan或PointCloud，并将“topic_name”设置为传感器发布数据的话题。“marking”和“clearing”参数确定传感器是否向代价地图添加障碍物信息或从代价地图中清除障碍物信息，或者同时都做。 全局代价地图配置（global_costmap） 我们将在下面创建一个文件，用于存储特定于全局代价地图的配置选项。打开一个带有global_costmap_params.yaml文件的编辑器，并粘贴到以下文本中： global_costmap: global_frame: /map robot_base_frame: base_link update_frequency: 5.0 static_map: true “global_frame”参数定义了代价地图应该运行的坐标系，在这种情况下，我们将选择/map坐标系。“robot_base_frame”参数定义了代价地图应该为机器人的基座的坐标系。“update_frequency”参数决定了代价地图更新的频率（以Hz为单位）。该“static_map”参数确定是否由map_server提供的地图服务来进行代价地图的初始化。如果您没有使用现有的地图或地图服务器，请将static_map参数设置为false。 本地代价地图配置（local_costmap） 我们将在下面创建一个文件，它将存储特定于本地代价地图的配置选项。打开一个带有文件local_costmap_params.yaml的编辑器并粘贴到以下文本中： local_costmap: global_frame: odom robot_base_frame: base_link update_frequency: 5.0 publish_frequency: 2.0 static_map: false rolling_window: true width: 6.0 height: 6.0 resolution: 0.05 “global_frame”，“robot_base_frame”，“update_frequency”和“static_map”参数与上述“全局代价地图配置”部分中描述的相同。“publish_frequency”参数确定代价地图发布可视化信息的速率（以Hz为单位）。将“rolling_window”参数设置为true意味着当机器人移动时，保持机器人在本地代价地图中心。“宽度”，“高度”和“分辨率”参数设置本地代价地图的宽度（米），高度（米）和分辨率（米/单元格）。请注意，这个网格的分辨率与静态地图的分辨率不同，但大多数时候我们倾向设置为相同值。 完整的配置选项 此最低配置应该可以运行，但是有关可用于代价地图的配置选项的更多详细信息，请参阅costmap_2d文档。 基本本地规划器配置/base_local_planner base_local_planner负责把上层规划器计算的速度指令发送给移动基座。我们需要根据我们的机器人的规格来设置一些配置选项，以便开始运行。打开一个名为base_local_planner_params.yaml的文件，并粘贴以下文本： TrajectoryPlannerROS: max_vel_x: 0.45 min_vel_x: 0.1 max_vel_theta: 1.0 min_in_place_vel_theta: 0.4 acc_lim_theta: 3.2 acc_lim_x: 2.5 acc_lim_y: 2.5 holonomic_robot: true 上述参数的第一部分定义了机器人的速度限制。第二部分定义了机器人的加速度限制。 注意：本节仅介绍TrajectoryPlanner的基本配置选项。有关各种选项的文档，请参阅base_local_planner文档。 创建导航包的启动文件 现在我们已经有了所有的配置文件，我们需要把所有的东西放在导航包的启动文件中。打开一个带有move_base.launch文件的编辑器并粘贴以下文本： <launch> <master auto="start"/> <node name="map_server" pkg="map_server" type="map_server" args="$(find my_map_package)/my_map.pgm my_map_resolution"/> <include file="$(find amcl)/examples/amcl_omni.launch" /> <node pkg="move_base" type="move_base" respawn="false" name="move_base" output="screen"> <rosparam file="$(find my_robot_name_2dnav)/costmap_common_params.yaml" command="load" ns="global_costmap" /> <rosparam file="$(find my_robot_name_2dnav)/costmap_common_params.yaml" command="load" ns="local_costmap" /> <rosparam file="$(find my_robot_name_2dnav)/local_costmap_params.yaml" command="load" /> <rosparam file="$(find my_robot_name_2dnav)/global_costmap_params.yaml" command="load" /> <rosparam file="$(find my_robot_name_2dnav)/base_local_planner_params.yaml" command="load" /> </node> </launch> 您需要对此文件进行的唯一更改是将地图服务器更改为指向已创建的地图，如果您有差分驱动器机器人，则将“amcl_omni.launch”更改为“amcl_diff.launch”。有关创建地图的教程，请参阅构建地图。 AMCL配置（amcl） AMCL有许多配置选项会影响本地化的性能。有关AMCL的更多信息，请参阅amcl文档。 运行导航包 现在我们已经完成了所有设置，我们可以运行导航包。为此，我们需要打开机器人的两个终端。在一个终端中，我们将启动my_robot_configuration.launch文件，另一个终端将启动我们刚创建的move_base.launch文件。 终端1，执行 roslaunch my_robot_name_2dnav my_robot_configuration.launch 终端2，执行 roslaunch my_robot_name_2dnav move_base.launch 恭喜，导航包现在应该正在运行。有关通过图形界面将目标发送到导航包的信息，请参阅rviz和导航教程。如果要使用代码将目标发送到导航包，请参阅“ 发送简单导航目标”教程。 故障排除 对于运行导航堆栈时遇到的常见问题，请参阅导航堆栈疑难解答页面。 参考资料 http://wiki.ros.org/navigation/Tutorials/RobotSetup/
ROS与navigation教程-结合RVIZ与导航包 说明： 介绍如何结合rviz来使用导航包，包括初始化位态，将目标发送给机器人以及查看导航包可视化信息 概观： rviz是一个强大的可视化工具，可以用于许多不同的目的。本教程至少对rviz进行了一些熟悉，可以在这里找到rviz文档。 设置rviz 以下视频显示如何设置rviz以使用导航包。这包括为amcl定位系统设置机器人的姿态，显示导航包提供的所有可视化信息，以及使用rviz将目标发送到导航包。navstack发布的每个可视化话题的讨论可以在下面找到。 截图： 视频：(此为youtbe视频，请翻墙查看) 2D导航目标/2D Nav Goal 话题：move_base_simple/goal 类型：geometry_msgs/PoseStamped 说明：允许用户设置机器人目标位姿，并发送到导航包。 2D姿势估计/2D Pose Estimate 话题：initialpose 类型： geometry_msgs/PoseWithCovarianceStamped 说明：允许用户设置机器人的初始化位姿。 静态地图/Static Map 话题：map 类型：nav_msgs/GetMap 类型：nav_msgs/OccupancyGrid 说明：显示由map_server提供的静态地图（如果存在） 粒子云/Particle Cloud 话题：particlecloud 类型：geometry_msgs/PoseArray 说明：显示机器人定位系统中使用的粒子云。粒子云的分布代表了定位系统对机器人姿态评估的不确定性。一个非常分散的粒子云层反映了很高的不确定性，而一个浓缩的粒子云代表着低的不确定性。换句话说越集中定位越准。 机器人本体/Robot Footprint 话题：local_costmap/robot_footprint 类型：geometry_msgs/PolygonStamped 说明：显示机器人的占用空间 障碍/Obstacles 话题：local_costmap/obstacles 类型：nav_msgs/GridCells 说明：显示导航包在其本地代价地图中看到的障碍。为了使机器人避免碰撞，机器人本体绝不应与包含障碍物的单元相交。 障碍膨胀区/Inflated Obstacles 话题：local_costmap/inflated_obstacles 类型：nav_msgs/GridCells 说明：在导航包的代价地图中显示由机器人与障碍区之间一个内切半径范围的空间。为了机器人避免碰撞，机器人的中心点不应与障碍膨胀区重叠。 未知空间/Unknown Space 话题：local_costmap/unknown_space 类型：nav_msgs/GridCells 说明：显示导航包的costmap_2d中包含的任何未知空间。 全局规划/Global Plan 话题：TrajectoryPlannerROS/global_plan 类型：nav_msgs/Path 说明：代价地图中显示达到目标的全局规划路径 本地规划/Local Plan 话题：TrajectoryPlannerROS/local_plan 类型：nav_msgs/Path 说明：代价地图中显示达到目标本地规划的路径。 规划器规划/Planner Plan 话题：NavfnROS/plan 类型：nav_msgs/Path 说明：显示由全局规划器计算出机器人的完整规划。 当前目标/Current Goal 话题：current_goal 类型：geometry_msgs/PoseStamped 说明：显示导航包尝试实现的目标位势
ROS与navigation教程-发布里程计消息 说明： 介绍导航包发布里程信息的示例。 它涵盖了通过ROS发布nav_msgs/Odometry消息，以及从“odom”坐标系到tf上的“base_link”坐标系的变换。 (1)发布里程信息 导航包使用TF来确定机器人在地图中的位置，并将传感器数据与静态地图相关联。然而，TF不提供关于机器人的速度的任何信息。因此，导航包要求任何里程源都通过ROS发布TF变换和nav_msgs/Odometry消息。本教程解释了nav_msgs/Odometry消息，并提供了用于发布消息并通过ROS和tf进行变换的示例代码。 (2)nav_msgs/Odometry消息 nav_msgs/Odometry消息存储了在自由空间中机器人的位置和速度的估计值： # This represents an estimate of a position and velocity in free space. # The pose in this message should be specified in the coordinate frame given by header.frame_id. # The twist in this message should be specified in the coordinate frame given by the child_frame_id Header header string child_frame_id geometry_msgs/PoseWithCovariance pose geometry_msgs/TwistWithCovariance twist 该消息中的pose对应于机器人在坐标系中的估计位置以及用于该位态估计的确定性的可选协方差。该消息中的twist对应于机器人在子坐标系中的速度，通常是移动基站的坐标系，以及用于该速度估计的确定性的可选协方差。 (3)使用tf发布Odometry变换 如变换配置教程中所述，“TF”软件库负责管理与机器人相关的变换树的各坐标系之间的关系。因此，任何里程源都必须发布有关其管理的坐标系的信息。 (4)编写代码 在本节中，我们将编写一些用于通过ROS发布 nav_msgs/Odometry 消息的示例代码，并使用tf进行变换，这种变换只是在一个循环中驱动的仿真的机器人。我们首先将代码全部显示出来，下面是一个一个一个的解释。 将依赖关系添加到包的manifest.xml中 <depend package="tf"/> <depend package="nav_msgs"/> 示例代码： #include <ros/ros.h> #include <tf/transform_broadcaster.h> #include <nav_msgs/Odometry.h> int main(int argc, char** argv){ ros::init(argc, argv, "odometry_publisher"); ros::NodeHandle n; ros::Publisher odom_pub = n.advertise<nav_msgs::Odometry>("odom", 50); tf::TransformBroadcaster odom_broadcaster; double x = 0.0; double y = 0.0; double th = 0.0; double vx = 0.1; double vy = -0.1; double vth = 0.1; ros::Time current_time, last_time; current_time = ros::Time::now(); last_time = ros::Time::now(); ros::Rate r(1.0); while(n.ok()){ ros::spinOnce(); // check for incoming messages current_time = ros::Time::now(); //compute odometry in a typical way given the velocities of the robot double dt = (current_time - last_time).toSec(); double delta_x = (vx * cos(th) - vy * sin(th)) * dt; double delta_y = (vx * sin(th) + vy * cos(th)) * dt; double delta_th = vth * dt; x += delta_x; y += delta_y; th += delta_th; //since all odometry is 6DOF we'll need a quaternion created from yaw geometry_msgs::Quaternion odom_quat = tf::createQuaternionMsgFromYaw(th); //first, we'll publish the transform over tf geometry_msgs::TransformStamped odom_trans; odom_trans.header.stamp = current_time; odom_trans.header.frame_id = "odom"; odom_trans.child_frame_id = "base_link"; odom_trans.transform.translation.x = x; odom_trans.transform.translation.y = y; odom_trans.transform.translation.z = 0.0; odom_trans.transform.rotation = odom_quat; //send the transform odom_broadcaster.sendTransform(odom_trans); //next, we'll publish the odometry message over ROS nav_msgs::Odometry odom; odom.header.stamp = current_time; odom.header.frame_id = "odom"; //set the position odom.pose.pose.position.x = x; odom.pose.pose.position.y = y; odom.pose.pose.position.z = 0.0; odom.pose.pose.orientation = odom_quat; //set the velocity odom.child_frame_id = "base_link"; odom.twist.twist.linear.x = vx; odom.twist.twist.linear.y = vy; odom.twist.twist.angular.z = vth; //publish the message odom_pub.publish(odom); last_time = current_time; r.sleep(); } } 让我们详细分解代码的重要部分 #include <tf/transform_broadcaster.h> #include <nav_msgs/Odometry.h> 由于我们将一个从“odom”坐标系变换到“base_link”坐标系和nav_msgs/Odometry消息，我们需要包括相关的头文件。 ros::Publisher odom_pub = n.advertise<nav_msgs::Odometry>("odom", 50); tf::TransformBroadcaster odom_broadcaster; 我们需要创建一个 ros::Publisher 和一个 tf::TransformBroadcaster来分别使用ROS和tf发送消息。 double x = 0.0; double y = 0.0; double th = 0.0; 我们假设机器人起始于“odom”坐标系的起点 double vx = 0.1; double vy = -0.1; double vth = 0.1; 这里我们将设置一些速度，这将导致“base_link”坐标系在“odom”坐标系中以x方向 0.1m/s速度，y方向为-0.1m/s速度，th方向为0.1rad/s弧度移动。这或多或少会导致我们的仿真机器人兜一圈。 ros::Rate r(1.0); 在本例中，我们将以1Hz的速率发布里程信息，使得显示更简洁些，大多数系统将以更高的速率发布里程信息。 //compute odometry in a typical way given the velocities of the robot double dt = (current_time - last_time).toSec(); double delta_x = (vx * cos(th) - vy * sin(th)) * dt; double delta_y = (vx * sin(th) + vy * cos(th)) * dt; double delta_th = vth * dt; x += delta_x; y += delta_y; th += delta_th; 在这里，我们将根据我们设定的恒定速度更新我们的里程信息。当然，真正的里程系统会整合计算速度。 //since all odometry is 6DOF we'll need a quaternion created from yaw geometry_msgs::Quaternion odom_quat = tf::createQuaternionMsgFromYaw(th); 我们通常会尝试在我们的系统中使用所有消息的3D版本，以允许2D和3D组件在适当的情况下协同工作，并将消息数量保持在最低限度。因此，有必要将我们的yaw(偏航值)变为四元数。幸运的是，tf提供的功能允许从yaw(偏航值)容易地创建四元数，并且可以方便地获取四元数的偏航值。 //first, we'll publish the transform over tf geometry_msgs::TransformStamped odom_trans; odom_trans.header.stamp = current_time; odom_trans.header.frame_id = "odom"; odom_trans.child_frame_id = "base_link"; 在这里，我们将创建一个TransformStamped消息，通过tf发送。我们要发布在current_time时刻的从"odom"坐标系到“base_link”坐标系的变换。因此，我们将相应地设置消息头和child_frame_id，确保使用“odom”作为父坐标系，将“base_link”作为子坐标系。 odom_trans.transform.translation.x = x; odom_trans.transform.translation.y = y; odom_trans.transform.translation.z = 0.0; odom_trans.transform.rotation = odom_quat; //send the transform odom_broadcaster.sendTransform(odom_trans); 将我们的里程数据中填入变换消息中，然后使用TransformBroadcaster发送变换。 //next, we'll publish the odometry message over ROS nav_msgs::Odometry odom; odom.header.stamp = current_time; odom.header.frame_id = "odom"; 我们还需要发布nav_msgs/Odometry消息，以便导航包可以从中获取速度信息。我们将消息的header设置为current_time和“odom”坐标系。 //set the position odom.pose.pose.position.x = x; odom.pose.pose.position.y = y; odom.pose.pose.position.z = 0.0; odom.pose.pose.orientation = odom_quat; //set the velocity odom.child_frame_id = "base_link"; odom.twist.twist.linear.x = vx; odom.twist.twist.linear.y = vy; odom.twist.twist.angular.z = vth; 这将使用里程数据填充消息，并发送出去。我们将消息的child_frame_id设置为“base_link”坐标系，因为我们要发送速度信息到这个坐标系。
ROS与navigation教程-发布传感器数据 说明： 介绍如何通过ROS发送两种类型的传感器数据， 分别是sensor_msgs/LaserScan消息和sensor_msgs/PointCloud消息。 发布传感器数据 通过ROS从传感器正确发布数据对于导航包安全运行非常重要。如果导航包没有收到来自机器人传感器的信息，那么它将被盲目地驾驶，并且很有可能会撞到东西。有许多传感器可为导航包提供信息：激光器，相机，声纳，红外线，碰撞传感器等。但是，当前的导航包只接受使用sensor_msgs/LaserScan消息类型或sensor_msgs/PointCloud消息类型。以下教程将提供两种类型消息的典型设置和使用示例。 ROS消息头 像其他类型消息一样， sensor_msgs/LaserScan和sensor_msgs/PointCloud消息包含TF坐标系和时间关联的消息。为了标准化此信息的发送方式， Header消息类型用作所有消息中的字段 Header类型中有三个字段如下所示。seq字段对应于标识符的消息由发布者发送时候自动增加。stamp字段存储与数据相关联的时间信息。以激光为例，stamp可能对应于扫描发生时的时间。frame_id字段存储与数据相关联的TF坐标系的消息。以激光为例，应该是设置为进行扫描的坐标系。 #Standard metadata for higher-level flow data types #sequence ID: consecutively increasing ID uint32 seq #Two-integer timestamp that is expressed as: # * stamp.secs: seconds (stamp_secs) since epoch # * stamp.nsecs: nanoseconds since stamp_secs # time-handling sugar is provided by the client library time stamp #Frame this data is associated with # 0: no frame # 1: global frame string frame_id 发布激光消息 LaserScan消息 对于具有激光扫描仪的机器人，ROS在sensor_msgs包提供特殊的消息类型，叫LaserScan，用于保存有关扫描的信息。主要从扫描仪返回的数据可以格式化成这种消息类型，就可以使代码更容易处理。在谈论如何生成和发布这些消息之前，让我们来看看消息规范： # # Laser scans angles are measured counter clockwise, with 0 facing forward # (along the x-axis) of the device frame # Header header float32 angle_min # start angle of the scan [rad] float32 angle_max # end angle of the scan [rad] float32 angle_increment # angular distance between measurements [rad] float32 time_increment # time between measurements [seconds] float32 scan_time # time between scans [seconds] float32 range_min # minimum range value [m] float32 range_max # maximum range value [m] float32[] ranges # range data [m] (Note: values < range_min or > range_max should be discarded) float32[] intensities # intensity data [device-specific units] 上面的注释就能明白字段表达的意思，为了更直观认识，写一个简单的激光扫描发布器来进行演示。 编写代码发布激光/LaserScan消息 通过ROS发布LaserScan消息是相当简单的。我们将首先提供下面的示例代码，然后逐行解释代码。 #include <ros/ros.h> #include <sensor_msgs/LaserScan.h> int main(int argc, char** argv){ ros::init(argc, argv, "laser_scan_publisher"); ros::NodeHandle n; ros::Publisher scan_pub = n.advertise<sensor_msgs::LaserScan>("scan", 50); unsigned int num_readings = 100; double laser_frequency = 40; double ranges[num_readings]; double intensities[num_readings]; int count = 0; ros::Rate r(1.0); while(n.ok()){ //generate some fake data for our laser scan for(unsigned int i = 0; i < num_readings; ++i){ ranges[i] = count; intensities[i] = 100 + count; } ros::Time scan_time = ros::Time::now(); //populate the LaserScan message sensor_msgs::LaserScan scan; scan.header.stamp = scan_time; scan.header.frame_id = "laser_frame"; scan.angle_min = -1.57; scan.angle_max = 1.57; scan.angle_increment = 3.14 / num_readings; scan.time_increment = (1 / laser_frequency) / (num_readings); scan.range_min = 0.0; scan.range_max = 100.0; scan.ranges.resize(num_readings); scan.intensities.resize(num_readings); for(unsigned int i = 0; i < num_readings; ++i){ scan.ranges[i] = ranges[i]; scan.intensities[i] = intensities[i]; } scan_pub.publish(scan); ++count; r.sleep(); } } 下面一一进行解释 #include <sensor_msgs/LaserScan.h> 这里包含sensor_msgs/LaserScan的头文件 ros::Publisher scan_pub = n.advertise<sensor_msgs::LaserScan>("scan", 50); 此代码创建一个ros::Publisher，用于使用ROS发送LaserScan消息 unsigned int num_readings = 100; double laser_frequency = 40; double ranges[num_readings]; double intensities[num_readings]; 这里设置消息的长度，便于填充一些虚拟数据。一个真正的应用程序将从他们的激光扫描仪中获取数据 //generate some fake data for our laser scan for(unsigned int i = 0; i < num_readings; ++i){ ranges[i] = count; intensities[i] = 100 + count; } ros::Time scan_time = ros::Time::now(); 用每秒增加1的值填充虚拟激光数据 //populate the LaserScan message sensor_msgs::LaserScan scan; scan.header.stamp = scan_time; scan.header.frame_id = "laser_frame"; scan.angle_min = -1.57; scan.angle_max = 1.57; scan.angle_increment = 3.14 / num_readings; scan.time_increment = (1 / laser_frequency) / (num_readings); scan.range_min = 0.0; scan.range_max = 100.0; scan.ranges.resize(num_readings); scan.intensities.resize(num_readings); for(unsigned int i = 0; i < num_readings; ++i){ scan.ranges[i] = ranges[i]; scan.intensities[i] = intensities[i]; } 创建一个scan_msgs::LaserScan消息，并填写我们生成的数据，并准备发送 scan_pub.publish(scan); 通过ROS发布消息 通过ROS发布点云/PointClouds PointCloud消息 为了存储和共享关于空间上多个点的数据，ROS提供了一个sensor_msgs/PointCloud消息。如下所示，此消息旨在支持三维点阵列以及作为通道存储的任何相关数据。例如，PointCloud可以发送一个“强度”通道，保存有关云中每个点的强度值的信息。我们将在下一部分中探索使用ROS发送PointCloud的示例。 #This message holds a collection of 3d points, plus optional additional information about each point. #Each Point32 should be interpreted as a 3d point in the frame given in the header Header header geometry_msgs/Point32[] points #Array of 3d points ChannelFloat32[] channels #Each channel should have the same number of elements as points array, and the data in each channel should correspond 1:1 with each point 编写代码发布PointCloud消息 用ROS发布PointCloud是非常简单的。我们将在下面详细介绍一个简单的例子，然后详细讨论重要的部分。 #include <ros/ros.h> #include <sensor_msgs/PointCloud.h> int main(int argc, char** argv){ ros::init(argc, argv, "point_cloud_publisher"); ros::NodeHandle n; ros::Publisher cloud_pub = n.advertise<sensor_msgs::PointCloud>("cloud", 50); unsigned int num_points = 100; int count = 0; ros::Rate r(1.0); while(n.ok()){ sensor_msgs::PointCloud cloud; cloud.header.stamp = ros::Time::now(); cloud.header.frame_id = "sensor_frame"; cloud.points.resize(num_points); //we'll also add an intensity channel to the cloud cloud.channels.resize(1); cloud.channels[0].name = "intensities"; cloud.channels[0].values.resize(num_points); //generate some fake data for our point cloud for(unsigned int i = 0; i < num_points; ++i){ cloud.points[i].x = 1 + count; cloud.points[i].y = 2 + count; cloud.points[i].z = 3 + count; cloud.channels[0].values[i] = 100 + count; } cloud_pub.publish(cloud); ++count; r.sleep(); } } 下面进行逐行解释 #include <sensor_msgs/PointCloud.h> 包含sensor_msgs/PointCloud消息头 ros::Publisher cloud_pub = n.advertise<sensor_msgs::PointCloud>("cloud", 50); 创建ros::Publisher, 用于发送PointCloud消息 sensor_msgs::PointCloud cloud; cloud.header.stamp = ros::Time::now(); cloud.header.frame_id = "sensor_frame"; 填写PointCloud消息的header，分别是发布信息的相关坐标系和时间戳 cloud.points.resize(num_points); 设置点云中的点数，以便我们可以用虚拟数据填充它们 //we'll also add an intensity channel to the cloud cloud.channels.resize(1); cloud.channels[0].name = "intensities"; cloud.channels[0].values.resize(num_points); 向PointCloud添加一个称为“强度”的频道，并将其大小设置为与云中我们将拥有的点数相匹配。 //generate some fake data for our point cloud for(unsigned int i = 0; i < num_points; ++i){ cloud.points[i].x = 1 + count; cloud.points[i].y = 2 + count; cloud.points[i].z = 3 + count; cloud.channels[0].values[i] = 100 + count; } 用一些虚拟数据填充PointCloud消息。另外，用伪数据填充强度信道。 cloud_pub.publish(cloud); 使用ROS 发布PointCloud
ROS与navigation教程-编写自定义全局路径规划 说明： 介绍在ROS中编写和使用全局路径规划器的步骤，提供从编写路径计划程序类开始直到将其部署为插件的所有步骤。 简介： 首先要知道的是，为了向ROS添加一个新的全局路径规划器，新的路径规划器必须遵守nav_core包中定义的nav_core::BaseGlobalPlanner C ++接口。一旦编写了全局路径规划器，它必须作为插件添加到ROS中，以便它可以被move_base包使用。 我将使用Turtlebot作为机器人的一个例子来部署新的路径规划器。有关如何将真正的GA规划程序集成为ROS插件的教程，参考在ROS中添加遗传算法全局路径规划器作为插件。此链接中还提供了视频教程 (1)编写路径规划类 Class Header/类头文件 第一步是为遵循nav_core::BaseGlobalPlanner的路径规划器编写一个新类。在carrot_planner.h中可以找到一个类似的例子作为参考。为此，您需要创建一个将在本例中调用的global_planner.h头文件。我将介绍添加插件的最小代码，这是添加任何全局规划程序的必要和常见步骤。 最小头文件global_planner.h定义如下： /** include the libraries you need in your planner here */ /** for global path planner interface */ #include <ros/ros.h> #include <costmap_2d/costmap_2d_ros.h> #include <costmap_2d/costmap_2d.h> #include <nav_core/base_global_planner.h> #include <geometry_msgs/PoseStamped.h> #include <angles/angles.h> #include <base_local_planner/world_model.h> #include <base_local_planner/costmap_model.h> using std::string; #ifndef GLOBAL_PLANNER_CPP #define GLOBAL_PLANNER_CPP namespace global_planner { class GlobalPlanner : public nav_core::BaseGlobalPlanner { public: GlobalPlanner(); GlobalPlanner(std::string name, costmap_2d::Costmap2DROS* costmap_ros); /** overridden classes from interface nav_core::BaseGlobalPlanner **/ void initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros); bool makePlan(const geometry_msgs::PoseStamped& start, const geometry_msgs::PoseStamped& goal, std::vector<geometry_msgs::PoseStamped>& plan ); }; }; #endif 解释头文件的不同部分 #include <ros/ros.h> #include <costmap_2d/costmap_2d_ros.h> #include <costmap_2d/costmap_2d.h> #include <nav_core/base_global_planner.h> #include <geometry_msgs/PoseStamped.h> #include <angles/angles.h> #include <base_local_planner/world_model.h> #include <base_local_planner/costmap_model.h> 需要包含路径规划器所需的核心ROS库。路径规划器需要使用<costmap_2d/costmap_2d_ros.h>和<costmap_2d/costmap_2d.h>, 而costmap_2d::Costmap2D类作为输入的地图类。当定义为插件时，路径规划器类将自动访问此地图。没有必要订阅costmap2d来获取ROS的代价地图。<nav_core/base_global_planner.h> 用于导入接口nav_core::BaseGlobalPlanner，这是插件必须要继承的父类。 namespace global_planner { class GlobalPlanner : public nav_core::BaseGlobalPlanner { 为您的类定义命名空间是一个很好的做法，尽管不是必需的。在这里，我们将为GlobalPlanner类定义命名空间为global_planner。命名空间用于定义对类的完整引用，如global_planner::GlobalPlanner。然后定义类GlobalPlanner并继承接口nav_core::BaseGlobalPlanner。在nav_core::BaseGlobalPlanner中定义的所有方法都必须在新类GlobalPlanner中总重写。 public: GlobalPlanner(); GlobalPlanner(std::string name, costmap_2d::Costmap2DROS* costmap_ros); /** overriden classes from interface nav_core::BaseGlobalPlanner **/ void initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros); bool makePlan(const geometry_msgs::PoseStamped& start, const geometry_msgs::PoseStamped& goal, std::vector<geometry_msgs::PoseStamped>& plan ); 构造函数GlobalPlanner(std::string name, costmap_2d::Costmap2DROS* costmap_ros)用于初始化代价地图，即将用于规划的代价地图（costmap_ros）以及规划器的名称（name）。默认构造函数GlobalPlanner()即使用默认值作为初始化。方法initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros)是BaseGlobalPlanner的初始化函数，用于初始化costmap，参数为用于规划的代价地图（costmap_ros）和规划器的名称(name)。 对于carrot_planner的具体情况，初始化方法实现如下： void CarrotPlanner::initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros){ if(!initialized_){ costmap_ros_ = costmap_ros; //initialize the costmap_ros_ attribute to the parameter. costmap_ = costmap_ros_->getCostmap(); //get the costmap_ from costmap_ros_ // initialize other planner parameters ros::NodeHandle private_nh("~/" + name); private_nh.param("step_size", step_size_, costmap_->getResolution()); private_nh.param("min_dist_from_robot", min_dist_from_robot_, 0.10); world_model_ = new base_local_planner::CostmapModel(*costmap_); initialized_ = true; } else ROS_WARN("This planner has already been initialized... doing nothing"); } 接着，bool makePlan（start，goal，plan）的方法必须要重写。最终规划将存储在方法的参数std::vector<geometry_msgs::PoseStamped>& plan 中。该规划将通过插件自动发布为话题。carrot_planner的makePlan方法的参考。 Class Implementation/类实现 接下来，我将介绍在实施全局规划插件时要考虑的主要问题。我将不会描述完整的路径规划算法。为了说明的目的，我将使用makePlan（）方法的路径规划的例子（以后可以改进）。这是全局规划（global_planner.cpp）的最小代码实现，它总是生成一个静态计划。 #include <pluginlib/class_list_macros.h> #include "global_planner.h" //register this planner as a BaseGlobalPlanner plugin PLUGINLIB_EXPORT_CLASS(global_planner::GlobalPlanner, nav_core::BaseGlobalPlanner) using namespace std; //Default Constructor namespace global_planner { GlobalPlanner::GlobalPlanner (){ } GlobalPlanner::GlobalPlanner(std::string name, costmap_2d::Costmap2DROS* costmap_ros){ initialize(name, costmap_ros); } void GlobalPlanner::initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros){ } bool GlobalPlanner::makePlan(const geometry_msgs::PoseStamped& start, const geometry_msgs::PoseStamped& goal, std::vector<geometry_msgs::PoseStamped>& plan ){ plan.push_back(start); for (int i=0; i<20; i++){ geometry_msgs::PoseStamped new_goal = goal; tf::Quaternion goal_quat = tf::createQuaternionFromYaw(1.54); new_goal.pose.position.x = -2.5+(0.05*i); new_goal.pose.position.y = -3.5+(0.05*i); new_goal.pose.orientation.x = goal_quat.x(); new_goal.pose.orientation.y = goal_quat.y(); new_goal.pose.orientation.z = goal_quat.z(); new_goal.pose.orientation.w = goal_quat.w(); plan.push_back(new_goal); } plan.push_back(goal); return true; } }; 可以根据计划人员的要求和规格实施构造函数。在这个具体的说明性例子中不考虑它们的实现。有几件重要的事情要考虑： 1.注册规划器作为BaseGlobalPlanner插件：这是通过指令PLUGINLIB_EXPORT_CLASS(global_planner::GlobalPlanner, nav_core::BaseGlobalPlanner)完成的。为此，必须包含库 #include <pluginlib/class_list_macros.h> 2.makePlan()方法实现：在开始和目标参数用来获取初始位置和目标位置。在该说明性实现中，以起始位置（plan.push_back(start))）作为开始的规划变量。然后，在for循环中，将在规划中静态插入20个新的虚拟位置，然后将目标位置作为最后一个位置插入规划。然后，此规划路径将发送到move_base全局规划模块，该模块将通过ROS话题nav_msgs/Path进行发布，然后将由本地规划模块接收。 现在，您的全局规划类已经完成，您已准备好进行第二步，即为全局规划类创建插件，将其集成到move_base包的全局规划模块nav_core::BaseGlobalPlanner中。 编译 要编译上面创建的全局规划库，必须将其添加到CMakeLists.txt中。添加代码： add_library(global_planner_lib src/path_planner/global_planner/global_planner.cpp) 然后在终端中运行catkin_make，在catkin工作空间目录中生成二进制文件。这将在lib目录中创建库文件 ~/catkin_ws/devel/lib/libglobal_planner_lib。观察到“lib”附加到CMakeLists.txt中声明的库名global_planner_lib (2)编写你的插件 基本上，重要的是按照插件描述页面中所述创建一个新插件所需的所有步骤。有五个步骤： 插件注册 首先，您需要通过导出将全局规划类注册为插件。为了允许一个类被动态加载，它必须被标记为一个导出的类。这是通过特殊宏PLUGINLIB_EXPORT_CLASS完成的。这个宏可以放在构成插件库的任何源（.cpp）文件中，通常放在导出类的.cpp文件的末尾。以上已经在global_planner.cpp中完成了该指令 PLUGINLIB_EXPORT_CLASS(global_planner::GlobalPlanner, nav_core::BaseGlobalPlanner) 这将使global_planner::GlobalPlanner类注册为move_base包中nav_core::BaseGlobalPlanner类的插件。 插件描述文件 第二步是在描述文件中描述该插件。插件描述文件是一个XML文件，用于以机器可读格式存储有关插件的所有重要信息。它包含有关插件库的信息，插件的名称，插件的类型等。以我们的全局规划类为例，您需要创建一个新文件并将其保存在您的包中的某个位置（在我们的case_ global_planner包），并给它一个名称，例如global_planner_plugin.xml。插件描述文件（global_planner_plugin.xml）的内容将如下所示： <library path="lib/libglobal_planner_lib"> <class name="global_planner/GlobalPlanner" type="global_planner::GlobalPlanner" base_class_type="nav_core::BaseGlobalPlanner"> <description>This is a global planner plugin by iroboapp project.</description> </class> </library> 在第一行 <library path="lib/libglobal_planner_lib">我们指定插件库的路径。我们的例子，路径是lib/libglobal_planner_lib，其中lib是目录中的文件夹 ~/catkin_ws/devel/（参见上面的编译部分）。在此行中，我们首先指定我们将在move_base启动文件中使用的global_planner插件的名称作为参数，在此行中， 它指定了要在nav_core中使用的全局规划器。通常使用命名空间（global_planner）后跟斜杠，然后使用类（ GlobalPlanner）的名称来指定插件的名称。如果不指定名称，那么名称将等于该类型，在这种情况下将是global_planner::GlobalPlanner。它建议指定名称以避免混淆。 该类型指定了实现插件的类的名称，在我们的例子中是global_planner::GlobalPlanner，而base_class_type指定了实现插件的基类的名称，在我们的例子中是nav_core::BaseGlobalPlanner。所述<description> 标记提供关于插件的简要说明。有关插件描述文件及其相关标签/属性的详细说明，请参阅以下文档。 为什么我们需要这个文件？除了代码宏，我们需要这个文件，以允许ROS系统自动发现，加载。插件描述文件还包含重要信息，如插件的描述，不适合加在宏定义里。 注册插件到ROS包系统 为了让pluginlib在所有ROS程序包上查询系统上的所有可用插件，每个程序包都必须明确指定其导出的插件，哪些程序包库包含这些插件。插件提供者必须在其导出标记块中的package.xml中指向其插件描述文件。请注意，如果您有其他出口，他们都必须在同一个导出字段中。在我们的全局规划示例中，相关行将如下所示： <export> <nav_core plugin="${prefix}/global_planner_plugin.xml" /> </export> 在${PREFIX}/自动确定global_planner_plugin.xml文件完整的路径。有关导出插件的详细讨论，请参阅以下文档。 注意：为了使上述导出命令正常工作，提供程序包必须直接依赖于包含插件接口的程序包，这在全局规划程序的情况下是nav_core。因此，global_planner软件包在其package.xml中必须包含以下内容： <build_depend>nav_core</build_depend> <run_depend>nav_core</run_depend> 这将告诉编译器关于nav_core包的依赖性 在ROS包系统中，查询可用插件 可以通过rospack查询ROS包系统，以查看任何给定的包可用的插件。例如： rospack plugins --attrib=plugin nav_core 这将返回从nav_core包导出的所有插件。这是一个执行的例子： akoubaa@anis-vbox:~/catkin_ws$ rospack plugins --attrib=plugin nav_core rotate_recovery /opt/ros/hydro/share/rotate_recovery/rotate_plugin.xml navfn /opt/ros/hydro/share/navfn/bgp_plugin.xml base_local_planner /opt/ros/hydro/share/base_local_planner/blp_plugin.xml move_slow_and_clear /opt/ros/hydro/share/move_slow_and_clear/recovery_plugin.xml global_planner /home/akoubaa/catkin_ws/src/global_planner/global_planner_plugin.xml dwa_local_planner /opt/ros/hydro/share/dwa_local_planner/blp_plugin.xml clear_costmap_recovery /opt/ros/hydro/share/clear_costmap_recovery/ccr_plugin.xml carrot_planner /opt/ros/hydro/share/carrot_planner/bgp_plugin.xml 请注意，我们的插件现在可以在global_planner软件包下使用，并且在/home/akoubaa/catkin_ws/src/global_planner/global_planner_plugin.xml文件中指定。您还可以观察nav_core软件包中已经存在的其他插件，其中包括carrot_planner/CarrotPlanner和navfn，其实现Dijkstra算法。 现在，你的插件就可以使用了。 (3)在Turtlebot上运行插件 在Turtlebot上运行您的规划器有几个步骤。首先，您需要将包含您的全局规划程序（在我们的示例中为global_planner）的软件包复制到Turtlebot的catkin工作区（例如catkin_ws）。然后，您需要运行catkin_make将您的插件导出到您的turtlebot ROS环境中。其次，您需要对move_base配置进行一些修改，以指定要使用的新计划。为此，请按照以下三个步骤： 1.在ROS Hydro版本中，转到此文件夹/opt/ros/hydro/share/turtlebot_navigation/launch/includes，其他版本方法通用。 $ roscd turtlebot_navigation/ $ cd launch/includes/ 2.打开文件 move_base.launch.xml（您可能需要sudo打开并能够保存），并将新的规划器添加为全局规划器的参数，如下所示： ...... <node pkg="move_base" type="move_base" respawn="false" name="move_base" output="screen"> .... 保存并关闭move_base.launch.xml。需要注意的是，规划器的名称是global_planner/GlobalPlanner，跟指定在global_planner_plugin.xml文件名称一致。现在，您已准备好使用新的计划。 3.你现在必须启动你的Turtlebot。您需要启动minimal.launch，3dsensor.launch，amcl.launch.xml和move_base.launch.xml。以下是可用于此目的的启动文件的示例。 <launch> <include file="$(find turtlebot_bringup)/launch/minimal.launch"></include> <include file="$(find turtlebot_bringup)/launch/3dsensor.launch"> <arg name="rgb_processing" value="false" /> <arg name="depth_registration" value="false" /> <arg name="depth_processing" value="false" /> <arg name="scan_topic" value="/scan" /> </include> <arg name="map_file" default="your_map_folder/your_map_file.yaml"/> <node name="map_server" pkg="map_server" type="map_server" args="$(arg map_file)" /> <arg name="initial_pose_x" default="0.0"/> <arg name="initial_pose_y" default="0.0"/> <arg name="initial_pose_a" default="0.0"/> <include file="$(find turtlebot_navigation)/launch/includes/amcl.launch.xml"> <arg name="initial_pose_x" value="$(arg initial_pose_x)"/> <arg name="initial_pose_y" value="$(arg initial_pose_y)"/> <arg name="initial_pose_a" value="$(arg initial_pose_a)"/> </include> <include file="$(find turtlebot_navigation)/launch/includes/move_base.launch.xml"/> </launch> 请注意，在使用此启动文件启动您的turtlebot时，将会考虑在move_base.launch.xml文件中所做的更改。 (4)用RVIZ测试计划 在你启动Turtlebot之后，你可以使用这个命令启动rviz（在新的终端） $ roslaunch turtlebot_rviz_launchers view_navigation.launch --screen 效果图： 您现在可以通过点击按钮"Add"（在底部）添加要在rviz中显示的所有信息。您将看到以下窗口： 例如，如果要显示全局路径，请单击"By topic"，在“move_base”类别下选择"/global_plan" ，然后选择"Path"，如果需要，请添加显示名称，然后单击“OK”。您可以以相同的方式添加本地路径。 现在点击 “2D nav goal”按钮（在顶部），并选择目标位置。你现在可以看到你的机器人走向目标。 测试地图： willow_garage_map.pgm willow_garage_map.yaml 参考： http://wiki.ros.org/navigation/Tutorials/Writing%20A%20Global%20Path%20Planner%20As%20Plugin%20in%20ROS
ROS与navigation教程-stage仿真 说明： 介绍如何在stage仿真器下使用navigation包 代码： https://github.com/ros-planning/navigation_tutorials 简介： 这个代码包含launch文件，用于在stage中运行navigation包 launch/move_base_amcl_10cm.launch: 在分辨率为10CM的地图运行导航包并带有amcl launch/move_base_amcl_5cm.launch: 在分辨率为5CM的地图运行导航包并带有amcl launch/move_base_amcl_2.5cm.launch: 在分辨率为2.5CM的地图运行导航包并带有amcl launch/move_base_fake_localization_10cm.launch:在分辨率为10CM的地图运行导航包并带有fake_localization launch/move_base_fake_localization_5cm.launch:在分辨率为5CM的地图运行导航包并带有fake_localization launch/move_base_fake_localization_2.5cm.launch: 在分辨率为10CM的地图运行导航包并带有fake_localization launch/move_base_multi_robot.launch: 运行导航包并带有多个机器人 launch/move_base_gmapping_5cm.launch: 在分辨率为5CM的地图运行导航包并带有gmapping 参考： http://wiki.ros.org/navigation_stage
ROS与navigation教程-示例-激光发布(C++) 说明： 介绍如何实际编程发布激光数据 代码： 参考github仓库 安装： 新建目录 $ mkdir -p ~/catkin_ws/src 下载并编译 $ cd ~/catkin_ws/src $ git clone https://github.com/ros-planning/navigation_tutorials $ cd .. $ catkin_make 测试 新终端，执行roscore $ roscore 新终端，执行 $ rosrun laser_scan_publisher_tutorial laser_scan_publisher