接着前面的博客,我们先看下如何创建工作空间开始。
一. 工作空间
我们可以手动或者在终端中以命令行方式去创建工作空间(类似于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 found 或 package not found。
- 若想仅在当前终端生临时效,能够找到可执行文件的位置,则执行如下命令
python
source install/setup.bash
没有执行如上命令之前,我们使用如下命令
python
ros2 pkg executables
查不到刚生成的可执行文件

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


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

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

别忘了执行如下命令以生效。
python
source ~/.bashrc
后面每次再打开新终端,执行查找可执行程序命令时,就能查找带了,不必每个新终端都要去source install/setup.bash

五. 运行可执行程序,以及一些常用的ROS2查找功能语句
-
例举ROS2已安装或编译的包
ros2 pkg list

-
查指定包的可执行程序
ros2 pkg executables 包名

-
查所有可执行程序
ros2 pkg executables
-
查看正在运行的节点
ros2 node list

-
运行节点
ros2 run <包名> <可执行程序名>

-
若想按顺序运行某个包里的所有节点,执行如下命令:
for exe in (ros2 pkg executables 包名); do ros2 run 包名 exe; done
这边运行下如上几个包中的可执行程序,看下效果


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