有顺序的的启动节点,暂停节点,关闭节点是ROS1的一个痛点。因为在ROS1中节点启动是无序的。ROS1系统设计时并没有考虑节点启动时可能存在的互相依赖。
但在实际生产使用环境中,某些节点能正常工作是需要其他一些节点已经启动了的。
比如:需要定位功能能正常运行,前提是map_server节点已经正常加载地图文件并且已经把地图话题发布出来。
又或者你想从建图功能切到导航功能。在ROS1中,需要先将建图功能的程序杀干净。然后再启动导航程序。在ROS2中,各个节点的状态是可管理的。
在这个场景里,大可让建图程序休眠,而不用杀掉。切换功能时只需要激活相应功能的节点即可。
ROS2中引入了节点生命周期管理的概念,正是为了处理上面描述的问题。这项功能的实现需要做下面两件事情。
继承LifecycleNode
来实现自己的节点
将节点名称注册到Lifecycle Manager
中,由它来管理各个节点的状态
实现一个功能通常需要一些节点互相配合来实现。这样的话,我们可以将某一个功能涉及到的节点使用一个Lifecycle Manager
程序来管理。从而实现了起停一项功能的效果。
ROS2中的节点类型 ROS2中提供了两种节点类型。
Node()
是和ROS1中一样的节点基类
LifecycleNode()
是可管理状态的节点基类
这里详细说说LifecycleNode()
节点类型。LifecycleNode
类型的节点可以处于下面几种状态:
Unconfigured
Inactive
Active
Finalized
每一种状态下,可以让节点运行不同的代码。
Unconfigured 状态 当一个节点被创建时,它首先会进入到Unconfigured 状态 。 该状态下可通过执行onConfigure()
切换到Inactive
; 该状态下可通过执行onShutdown()
切换到Finalized
。
Inactive 状态 在这个状态下,将不响应话题,服务,action和参数等数据交互。处于该状态说明节点已经被配置好了。下一步激活就可以正常执行功能代码了。 该状态下可通过执行onCleanup()
切换到Unconfigured
; 该状态下可通过执行onActivate()
切换到active
; 该状态下可通过执行onShutdown()
切换到Finalized
。
Active 状态 该状态是节点的运行状态,可以接受话题消息,响应服务请求和action请求。数据接收到后进行处理然后输出结果。主要的功能代码应该在该状态下执行。 该状态下可通过执行onDeactivate()
切换到inactive
; 该状态下可通过执行onShutdown()
切换到Finalized
。
Finalized 状态 该状态是节点待销毁前的一个状态。在该状态下执行destroy()
将释放节点的资源。
下图演示了各个状态之间是如何切换的。
图片来源于:http://design.ros2.org/articles/node_lifecycle.html 图中,蓝色部分是表示节点状态,黄色部分是状态转换需执行的函数。
需要注意的是,LifecycleNode
类型节点目前只可以在C++中使用
从图上可以看出,LifecycleNode
类型节点切换状态是通过执行一系列的函数实现的。这些函数在继承LifecycleNode
类型节点时是需要重新实现的。
这些切换状态的函数接口声明在/opt/ros/galactic/include/rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp
文件中。 主要有下面几个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 RCLCPP_LIFECYCLE_PUBLIC virtual CallbackReturnon_configure (const State & previous_state) ; RCLCPP_LIFECYCLE_PUBLIC virtual CallbackReturnon_cleanup (const State & previous_state) ; RCLCPP_LIFECYCLE_PUBLIC virtual CallbackReturnon_shutdown (const State & previous_state) ; RCLCPP_LIFECYCLE_PUBLIC virtual CallbackReturnon_activate (const State & previous_state) ; RCLCPP_LIFECYCLE_PUBLIC virtual CallbackReturnon_deactivate (const State & previous_state) ; RCLCPP_LIFECYCLE_PUBLIC virtual CallbackReturnon_error (const State & previous_state) ;
每一个状态转换函数可以返回三种状态:
1 2 3 4 5 6 7 8 9 10 11 enum class CallbackReturn : uint8_t { SUCCESS = lifecycle_msgs::msg::Transition::TRANSITION_CALLBACK_SUCCESS, FAILURE = lifecycle_msgs::msg::Transition::TRANSITION_CALLBACK_FAILURE, ERROR = lifecycle_msgs::msg::Transition::TRANSITION_CALLBACK_ERROR };
在任何状态下,执行状态转换函数返回错误(ERROR
)时,节点都会执行on_error()
。如果执行成功则节点转换到Unconfigured
状态。如果执行失败则节点转换到Finalized
状态。
每一个状态转移函数都有需要实现的功能。onConfigure() 顾名思义,该函数主要负责配置好节点,包括初始化节点内要使用的资源,读取参数,新建话题发布器订阅器,服务,action等等。
onConfigure
运行成功,节点将从Unconfigured
切换到Inactive
状态。如果运行失败则仍然处于Unconfigured
状态。如果运行返回错误则进行错误处理(即onError()
)。
onCleanup() 主要用于清除节点的状态。使节点内各状态数据恢复到节点刚刚创建的时候。
onCleanup
运行成功,节点将从Inactive
切换到Unconfigured
状态。如果运行返回错误则进行错误处理(即onError()
)。
onActivate() 在该函数里一般做一些节点功能运行前的最后准备工作。比如激活话题订阅器发布器等等。
onActivate
运行成功,节点将从Inactive
切换到Active
状态。如果运行返回错误则进行错误处理(即onError()
)。
onDeactivate() 在该函数里执行的操作一般与onActivate()
相反。比如复位话题订阅器发布器等等。
onDeactivate
运行成功,节点将从Active
切换到Inactive
状态。如果运行返回错误则进行错误处理(即onError()
)。
onShutdown() 在这里主要执行节点销毁前的一些操作。除了Finalized
状态外,其他任何状态下都可以运行该函数使节点状态切换至Finalized
状态。
onShutdown
运行成功,节点将目前状态切换到Finalized
状态。如果运行返回错误则进行错误处理(即onError()
)。
onError() 任何其他状态转移函数执行错误都将执行这个函数。在这里执行一些异常的应对策略。
onError
运行成功,节点将切换到Unconfigured
状态。如果运行返回错误则进入到Finalized
状态。
如何建立一个LifecycleNode
节点 下面是一个简单的示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 #include <chrono> #include <iostream> #include <memory> #include <string> #include <thread> #include <utility> #include "lifecycle_msgs/msg/transition.hpp" #include "rclcpp/rclcpp.hpp" #include "rclcpp/publisher.hpp" #include "rclcpp_lifecycle/lifecycle_node.hpp" #include "rclcpp_lifecycle/lifecycle_publisher.hpp" #include "rcutils/logging_macros.h" #include "std_msgs/msg/string.hpp" using namespace std::chrono_literals;class ManagedScan : public rclcpp_lifecycle::LifecycleNode{ public : explicit ManagedScan (const std::string & node_name, bool intra_process_comms = false ) : rclcpp_lifecycle::LifecycleNode(node_name, rclcpp::NodeOptions().use_intra_process_comms(intra_process_comms)) { } void publish () { static size_t count = 0 ; auto msg = std::make_unique <std_msgs::msg::String>(); msg->data = "Lifecycle HelloWorld #" + std::to_string (++count); if (!pub_->is_activated ()) { RCLCPP_INFO ( get_logger (), "Lifecycle publisher is currently inactive. Messages are not published." ); } else { RCLCPP_INFO ( get_logger (), "Lifecycle publisher is active. Publishing: [%s]" , msg->data.c_str ()); } pub_->publish (std::move (msg)); } rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_configure (const rclcpp_lifecycle::State &) { pub_ = this ->create_publisher <std_msgs::msg::String>("managed_scan" , 10 ); timer_ = this ->create_wall_timer ( 1 s, std::bind (&ManagedScan::publish, this )); RCLCPP_INFO (get_logger (), "on_configure() is called." ); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_activate (const rclcpp_lifecycle::State &) { pub_->on_activate (); RCUTILS_LOG_INFO_NAMED (get_name (), "on_activate() is called." ); std::this_thread::sleep_for (2 s); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_deactivate (const rclcpp_lifecycle::State &) { pub_->on_deactivate (); RCUTILS_LOG_INFO_NAMED (get_name (), "on_deactivate() is called." ); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_cleanup (const rclcpp_lifecycle::State &) { timer_.reset (); pub_.reset (); RCUTILS_LOG_INFO_NAMED (get_name (), "on cleanup is called." ); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_shutdown (const rclcpp_lifecycle::State & state) { timer_.reset (); pub_.reset (); RCUTILS_LOG_INFO_NAMED ( get_name (), "on shutdown is called from state %s." , state.label ().c_str ()); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; } rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_error (const rclcpp_lifecycle::State &) { RCUTILS_LOG_INFO_NAMED (get_name (), "something went wrong!" ); return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::FAILURE; } private : std::shared_ptr<rclcpp_lifecycle::LifecyclePublisher<std_msgs::msg::String>> pub_; std::shared_ptr<rclcpp::TimerBase> timer_; }; int main (int argc, char * argv[]) { setvbuf (stdout, NULL , _IONBF, BUFSIZ); rclcpp::init (argc, argv); rclcpp::executors::SingleThreadedExecutor exe; std::shared_ptr<ManagedScan> lc_node = std::make_shared <ManagedScan>("managed_scan_node" ); exe.add_node (lc_node->get_node_base_interface ()); exe.spin (); rclcpp::shutdown (); return 0 ; }
通过下面的方式获取完整的lifecycle_node_demo
工程。
1 git clone https://gitee.com/shoufei/lifecycle_node_demo.git
需要注意的是,LifecycleNode 类型节点中的话题发布器需要按下面的方式定义。
1 std::shared_ptr<rclcpp_lifecycle::LifecyclePublisher<std_msgs::msg::String>> pub_;
话题订阅器则与普通Node类型节点一样。
编译并source了该工程后就可以用下面的命令启动该示例。
1 ros2 launch lifecycle_node_demo lifecycle_demo.launch.py
用 ros2 topic list
命令查看,可以发现有下面这个话题。
1 /lifecycle_node_demo_node/transition_event
但是没有代码中定义的managed_scan
话题。这是因为节点启动后并没有执行on_configure
函数。它还处于Unconfigured
状态。而发布器的定义是在on_configure
函数中的。
通过下面的命令可获取节点状态
1 ros2 lifecycle get /lifecycle_node_demo_node
此时它返回
当我们打印/lifecycle_node_demo_node/transition_event
话题内容时,发现并没有任何数据。那是因为目前并没有发生状态转换。
使用下面的命令来设置节点状态
1 ros2 lifecycle set /lifecycle_node_demo_node configure
可以设置的状态有下面几个
configure
cleanup
activate
deactivate
shutdown
转换状态成功后将打印
1 Transitioning successful
/lifecycle_node_demo_node/transition_event
话题也打印了下面的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 timestamp: 0 transition: id : 0 label: '' start_state: id : 1 label: unconfigured goal_state: id : 10 label: configuring --- timestamp: 0 transition: id : 0 label: '' start_state: id : 10 label: configuring goal_state: id : 2 label: inactive ---
managed_scan
话题也能用ros2 topic list
看到了。
如果需要重新启动一个节点,可以按下面的步骤进行操作:
将节点切换到inactive
状态
然后将节点切换到unconfigured
状态
接着将节点切换到inactive
状态
最后再切换到active
状态
如何管理LifecycleNode
节点的状态 LifecycleNode
节点提供了切换状态的服务,所以可以通过外部程序通过服务请求的方式来管理LifecycleNode
节点的状态切换。
在Navigation2中的nav2_lifecycle_manager
功能包实现了对LifecycleNode
节点的管理。
以Navigation2中的navigation_launch.py文件为例来分析一下nav2_lifecycle_manager
功能包如何管理多个节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescriptionfrom launch.actions import DeclareLaunchArgument, SetEnvironmentVariablefrom launch.substitutions import LaunchConfigurationfrom launch_ros.actions import Nodefrom nav2_common.launch import RewrittenYaml def generate_launch_description ():bringup_dir = get_package_share_directory('nav2_bringup' ) namespace = LaunchConfiguration('namespace' ) use_sim_time = LaunchConfiguration('use_sim_time' ) autostart = LaunchConfiguration('autostart' ) params_file = LaunchConfiguration('params_file' ) lifecycle_nodes = ['controller_server' , 'planner_server' ,'recoveries_server' ,'bt_navigator' ,'waypoint_follower' ] remappings = [('/tf' , 'tf' ), ('/tf_static' , 'tf_static' )] param_substitutions = { 'use_sim_time' : use_sim_time,'autostart' : autostart} configured_params = RewrittenYaml( source_file=params_file, root_key=namespace, param_rewrites=param_substitutions, convert_types=True ) return LaunchDescription([SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM' , '1' ), DeclareLaunchArgument( 'namespace' , default_value='' ,description='Top-level namespace' ), DeclareLaunchArgument( 'use_sim_time' , default_value='false' ,description='Use simulation (Gazebo) clock if true' ), DeclareLaunchArgument( 'autostart' , default_value='true' ,description='Automatically startup the nav2 stack' ), DeclareLaunchArgument( 'params_file' ,default_value=os.path.join(bringup_dir, 'params' , 'nav2_params.yaml' ), description='Full path to the ROS2 parameters file to use' ), Node( package='nav2_controller' , executable='controller_server' , output='screen' , parameters=[configured_params], remappings=remappings), Node( package='nav2_planner' , executable='planner_server' , name='planner_server' , output='screen' , parameters=[configured_params], remappings=remappings), Node( package='nav2_recoveries' , executable='recoveries_server' , name='recoveries_server' , output='screen' , parameters=[configured_params], remappings=remappings), Node( package='nav2_bt_navigator' , executable='bt_navigator' , name='bt_navigator' , output='screen' , parameters=[configured_params], remappings=remappings), Node( package='nav2_waypoint_follower' , executable='waypoint_follower' , name='waypoint_follower' , output='screen' , parameters=[configured_params], remappings=remappings), Node( package='nav2_lifecycle_manager' , executable='lifecycle_manager' , name='lifecycle_manager_navigation' , output='screen' , parameters=[{'use_sim_time' : use_sim_time}, {'autostart' : autostart}, {'node_names' : lifecycle_nodes}]), ])
其中lifecycle_nodes
变量定义了需要管理的节点。注意,这些节点都是继承于LifecycleNode
。
1 2 3 4 5 6 7 8 9 lifecycle_nodes = ['controller_server' , 'planner_server' ,'recoveries_server' ,'bt_navigator' ,'waypoint_follower' ]
然后将需要管理的节点名称传入nav2_lifecycle_manager
包。其中autostart
参数决定了是否自动启动这些节点并把状态切换到Active
状态。
参考:http://design.ros2.org/articles/node_lifecycle.html