ROS工作空间、功能包、节点创建

接着前面的博客,我们先看下如何创建工作空间开始。

一. 工作空间

我们可以手动或者在终端中以命令行方式去创建工作空间(类似于pycharm的一个工程文件夹,visual studio解决方案所在文件夹),方便我们对后面的功能包,节点做整体管理。

然后再执行命令

复制代码
colcon build 

可看到一个空文件夹在生成后自动生成了build、install、log文件夹。

我们也可以用命令行方式去创建文件夹

复制代码
mkdir -p DemoWorkspace

然后我们还是继续命令

复制代码
colcon build 

可看到一个工作空间就创建完毕了。

注意,我们还需在这个文件夹下手动创建src文件夹,用来存放一些后面要写的代码,脚本,需手动存放到此。后面我们所有工作都在DemoWorkspace工作空间下进行。

build文件夹 是编译过程中产生的中间临时文件,几乎不用管

install文件夹 是编译后真正能用的文件,ROS2 运行时加载的就是这里的文件,里面包含:

  • 可执行文件(节点、命令)
  • 库文件(.so/.dll)
  • 配置文件、消息 / 服务定义
  • 启动文件(launch)
  • 环境脚本(setup.bash

log文件夹 是编译日志文件,方便排查问题,几乎不用管

一个工作空间中可以有一堆包,每个包可以按照不同功能来区分,所有包都必须直接放在src文件夹下面,并列排布,不可以嵌套。执行一次colcon build, 能够对所有包进行编译。

一个功能包中可以写任意数量的可执行文件exe, 每个exe又能单/多节点

二. 功能包

1. 我们创建一个python示例包1,终端执行如下语句

复制代码
ros2 pkg create pythonpackagedemo1 --build-type ament_python --dependencies rclpy --node-name pythonexecute1

执行后,可看到包已经创建

ros2 pkg create是官方命令,用于创建新的功能包;

--dependencies rcply 声明依赖,rcply是ros2的python客户端库(必须依赖,用于编写节点)

--dependencies rclpy 声明依赖:rclpy是 ROS2 的 Python 客户端库(必须依赖,用于编写节点)。

2. 我们再创建一个C++版本的功能包,执行如下语句

复制代码
ros2 pkg create cpp_packagedemo1 --build-type ament_cmake --dependencies rclcpp --node-name cppexecute1

执行后可看到包已经被创建

三. 节点

接下来我们要去在各包中去创建程序,在程序中实现具体的功能。

1. 以前面创建的第一个python包为例, 我们用pycharm打开如下自动创建的文件pythonexecute1.py

将其中代码改为如下:

python 复制代码
import rclpy                                     # ROS2 Python接口库
from rclpy.node import Node                      # ROS2 节点类
import time

"""
创建一个pyHelloNode1节点
"""
class pyHelloNode1(Node):
    def __init__(self, name):
        super().__init__(name)                     # ROS2节点父类初始化
        while rclpy.ok():                          # ROS2系统是否正常运行
            self.get_logger().info("Hello python node1")  # ROS2日志输出
            time.sleep(1)                        # 休眠控制循环时间

def main(args=None):                               # ROS2节点主入口main函数
    rclpy.init(args=args)                          # ROS2 Python接口初始化
    node = pyHelloNode1("Hello_python_node1")            # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                               # 循环等待ROS2退出
    node.destroy_node()                            # 销毁节点对象
    rclpy.shutdown()                               # 关闭ROS2 Python接口

    

可看到在这个Py文件中仅创建了一个节点。如果想要在此包下增加一个可执行文件,则可以在这个目录下再手动添加一个py文件pythonexecute2_includetwonode_parallel.py,然后进行编辑。

python 复制代码
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor

# ============= 节点 1 =============
class Node1(Node):
    def __init__(self):
        super().__init__("node1")
        self.create_timer(1.0, self.callback)

    def callback(self):
        self.get_logger().info("节点 1 运行中")

# ============= 节点 2 =============
class Node2(Node):
    def __init__(self):
        super().__init__("node2")
        self.create_timer(1.0, self.callback)

    def callback(self):
        self.get_logger().info("节点 2 运行中")

# ============= 主函数(同时运行 2 个节点) =============
def main(args=None):
    rclpy.init(args=args)

    node1 = Node1()
    node2 = Node2()

    # 多线程,让两个节点同时跑
    executor = MultiThreadedExecutor()
    executor.add_node(node1)
    executor.add_node(node2)

    try:
        executor.spin()
    finally:
        executor.shutdown()
        node1.destroy_node()
        node2.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

在这个目录下再手动添加一个py文件pythonexecute2_includetwonode_sequence.py,然后进行编辑。

python 复制代码
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor

# ============= 节点 1 =============
class Node1(Node):
    def __init__(self):
        super().__init__("node1")
        self.create_timer(1.0, self.callback)

    def callback(self):
        self.get_logger().info("节点 1 运行中")

# ============= 节点 2 =============
class Node3(Node):
    def __init__(self):
        super().__init__("node3")
        self.create_timer(1.0, self.callback)

    def callback(self):
        self.get_logger().info("节点 3 运行中")

# ============= 主函数(同时运行 2 个节点) =============
def main(args=None):
    rclpy.init(args=args)

    # ============= 串行执行:先运行节点1 =============
    node1 = Node1()
    print("开始运行节点1(运行3秒后自动关闭)")
    rclpy.spin_once(node1, timeout_sec=3.0)  # 运行3秒
    node1.destroy_node()

    # ============= 再运行节点2 =============
    node3 = Node3()
    print("开始运行节点3(运行3秒后自动关闭)")
    rclpy.spin_once(node3, timeout_sec=3.0)  # 运行3秒
    node3.destroy_node()
   # 以上是自动串行,适合自动化、批处理任务

    # 以下是手动串行,适合调试、分步执行
    # 运行节点1
    node1 = Node1()
    print("=== 节点1 运行中,按 Ctrl+C 停止节点1 ===")
    try:
        rclpy.spin(node1)
    except KeyboardInterrupt:
        pass
    node1.destroy_node()

    # 运行节点2
    node3 = Node3()
    print("\n=== 节点3 运行中,按 Ctrl+C 停止节点3 ===")
    try:
        rclpy.spin(node3)
    except KeyboardInterrupt:
        pass
    node3.destroy_node()
    
    # 关闭
    rclpy.shutdown()

if __name__ == '__main__':
    main()

这几个文件在文件夹中的目录如下:

接下里要在自动生成的setup.py里注册这两个新可执行文件,博主将setup.py文件修改成如下:

python 复制代码
from setuptools import find_packages, setup

package_name = 'pythonpackagedemo1'

setup(
    name=package_name,
    version='0.0.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='jetson',
    maintainer_email='yahboom@168.com',
    description='TODO: Package description',
    license='TODO: License declaration',
    extras_require={
        'test': [
            'pytest',
        ],
    },
    entry_points={
        'console_scripts': [
            'pythonexecute_onenode = pythonpackagedemo1.pythonexecute1:main',
            'pythonexecute_twonodeparallel = pythonpackagedemo1.pythonexecute2_includetwonode_parallel:main',
            'pythonexecute_twonodesequence = pythonpackagedemo1.pythonexecute2_includetwonode_sequence:main',
        ],
    },
)

2. 我们接下来再去配置下上面用c++方式创建的包中节点,修改cppexecute1.cpp中代码如下:

cpp 复制代码
#include "rclcpp/rclcpp.hpp"

// ====================== 节点 1 ======================
class Node1 : public rclcpp::Node
{
public:
    Node1() : Node("cpp_node1")
    {
        // 创建 1s 定时器
        timer_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&Node1::callback, this));
        
        RCLCPP_INFO(this->get_logger(), "cpp节点1 已启动");
    }

private:
    void callback()
    {
        RCLCPP_INFO(this->get_logger(), "cpp节点1 运行中...");
    }
    rclcpp::TimerBase::SharedPtr timer_;
};

// ====================== 节点 2 ======================
class Node2 : public rclcpp::Node
{
public:
    Node2() : Node("cppnode2")
    {
        timer_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&Node2::callback, this));
        
        RCLCPP_INFO(this->get_logger(), "cpp节点2 已启动");
    }

private:
    void callback()
    {
        RCLCPP_INFO(this->get_logger(), "cpp节点2 运行中...");
    }
    rclcpp::TimerBase::SharedPtr timer_;
};

// ====================== 主函数 ======================
int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);

    // 同时运行两个节点(并行)
    auto node1 = std::make_shared<Node1>();
    auto node2 = std::make_shared<Node2>();

    // 多线程执行器(和 Python 的 MultiThreadedExecutor 一样)
    rclcpp::executors::MultiThreadedExecutor executor;
    executor.add_node(node1);
    executor.add_node(node2);

    executor.spin();  // 启动

    rclcpp::shutdown();
    return 0;
}

CMakeLists.txt中语句中语句如下:

cpp 复制代码
cmake_minimum_required(VERSION 3.8)
project(cpp_packagedemo1)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)

add_executable(cppexecute1 src/cppexecute1.cpp)
target_include_directories(cppexecute1 PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
target_compile_features(cppexecute1 PUBLIC c_std_99 cxx_std_17)  # Require C99 and C++17
ament_target_dependencies(
  cppexecute1
  "rclcpp"
)

install(TARGETS cppexecute1
  DESTINATION lib/${PROJECT_NAME})

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

后面博主主要以python方式作为后续的演示。

3. 编译功能包,以生成可执行文件

如果指定仅编译某个包,可执行如下语句

cpp 复制代码
colcon build --packages-select cpp_packagedemo1

可看到此时install文件夹下只有一个cpp的可执行文件

可在终端运行该可执行程序,执行效果如下:

按ctrl + c即可停止运行。

执行如下语句,即可以对工作空间里的所有包进行编译

cpp 复制代码
colcon build   

即可对完成对所有库的编译,可看到python版本的可执行文件其实是py脚本,我们人选其中一个打开看下。

python 复制代码
#!/usr/bin/python3
# EASY-INSTALL-ENTRY-SCRIPT: 'pythonpackagedemo1==0.0.0','console_scripts','pythonexecute_onenode'
import re
import sys

# for compatibility with easy_install; see #2198
__requires__ = 'pythonpackagedemo1==0.0.0'

try:
    from importlib.metadata import distribution
except ImportError:
    try:
        from importlib_metadata import distribution
    except ImportError:
        from pkg_resources import load_entry_point


def importlib_load_entry_point(spec, group, name):
    dist_name, _, _ = spec.partition('==')
    matches = (
        entry_point
        for entry_point in distribution(dist_name).entry_points
        if entry_point.group == group and entry_point.name == name
    )
    return next(matches).load()


globals().setdefault('load_entry_point', importlib_load_entry_point)


if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(load_entry_point('pythonpackagedemo1==0.0.0', 'console_scripts', 'pythonexecute_onenode')())

注意: colcon build过程并不会去清空install文件夹内内容,再重新生成,如下可看到pythonexecute_onenode仍存在。

注:节点可看成是类对象,节点自己不是可执行程序,不会自己运行起来,其是存在于可执行程序中。 很多同学都会混淆,这边博主要说明下,能运行的只有可执行程序,不是节点。节点是程序运行后,在内存里创建的对象 / 实例,运行可执行程序 → 程序内部创建节点 → 节点才活了,节点是运行后的产物,不能直接运行

四. 设置环境变量

编译成功后,为了让系统能够找到我们的功能包和可执行文件,还需要设置环境变量。ROS2 需要环境变量来找到自己的组件、你的功能包、以及运行所需的全部配置,不设置环境变量,ROS2 根本无法正常工作。它能告诉系统:

  • ROS2 的可执行文件在哪里
  • ROS2 的库文件在哪里
  • 你的功能包放在哪里
  • 使用哪个版本的 ROS2(如 Humble / Iron)

没有这个 "导航地图",系统就会报错: command not foundpackage not found

  1. 若想仅在当前终端生临时效,能够找到可执行文件的位置,则执行如下命令
python 复制代码
source install/setup.bash   

没有执行如上命令之前,我们使用如下命令

python 复制代码
ros2 pkg executables

查不到刚生成的可执行文件

一旦执行了source install/setup.bash 命令,再去查找时,便能找到了

但此时重新打开一个终端,执行如上查找可执行程序命令,发现又找不到了

  1. 若避免每次打开新终端后,都要去更新一下,想要永久生效,可执行如下命令,在里面将包的安装文件夹路径填进去

别忘了执行如下命令以生效。

python 复制代码
source ~/.bashrc

后面每次再打开新终端,执行查找可执行程序命令时,就能查找带了,不必每个新终端都要去source install/setup.bash

五. 运行可执行程序,以及一些常用的ROS2查找功能语句

  1. 例举ROS2已安装或编译的包

    ros2 pkg list

  1. 查指定包的可执行程序

    ros2 pkg executables 包名

  1. 查所有可执行程序

    ros2 pkg executables

  2. 查看正在运行的节点

    ros2 node list

  1. 运行节点

    ros2 run <包名> <可执行程序名>

  1. 若想按顺序运行某个包里的所有节点,执行如下命令:

    for exe in (ros2 pkg executables 包名); do ros2 run 包名 exe; done

这边运行下如上几个包中的可执行程序,看下效果

下一篇博客我们再继续深入,此篇暂就到这儿。

相关推荐
济6176 小时前
【ROS2 Humble 开发专栏】Ubuntu22.04 基于 OpenCV 实现颜色阈值分割与目标坐标定位|附完整工程源码
嵌入式硬件·嵌入式·ros2·机器人开发·机器人方向
MIXLLRED1 天前
Ubuntu22.04 + ROS2 Humble 安装部署 PCT Planner
ubuntu·ros2·三维路径规划·pct
某林2122 天前
Wheeltec 机器人多模态交互系统:从硬件死锁到纯软件异步驱动的重构实录
ros2·架构重构·技术复盘·c++底层排错·大模型qwen落地
济6172 天前
ROS开发专栏---基于图像视觉的目标追踪实验--适配Ubuntu 22.04
嵌入式硬件·嵌入式·ros2·机器人开发·机器人方向
济6172 天前
ROS开发专栏---视觉图像数据的获取实验--适配Ubuntu 22.04
嵌入式硬件·嵌入式·ros2·机器人开发·机器人方向
小烤箱3 天前
ROS2 学习资源与学习方法
学习·ros·学习方法·ros2
济6175 天前
ROS开发专栏---基于 NAV2 实现仿真环境自主导航实验--适配Ubuntu 22.04
嵌入式硬件·嵌入式·ros2·机器人方向
济6175 天前
ROS开发专栏---基于开源导航插件 wp_map_tools 多航点巡航导航实验--适配Ubuntu 22.04
ubuntu·嵌入式·ros2·机器人开发·机器人方向
济6178 天前
ROS开发专栏---基于激光雷达的自动避障---适配Ubuntu 22.04
嵌入式·ros2·机器人开发·机器人方向