Creating a Node#
The Node Framework was designed to be very general. In fact, the Node Framework
itself is actually not ROS-specific! However, all of the current nodes we
have are RosNodes. RosNode is a subclass of the more general Node,
and as you can imagine from the name, these are nodes whose functions are exposed
over ROS services and topics. That means we can control all RosNodes by communicating
via ROS services. Cool! Since all of our current nodes are RosNodes, we will
only focus on those in this article.
Note
You may be wondering, “why doesn’t the Node Framework assume that all nodes are ROS Nodes if all of our nodes are ROS Nodes?” The Node Framework was designed with the future in mind. Perhaps someday, you’ll find the need to make a non-ROS Node, and the current controls software stack can grow with you!
The actual logic for a node – that is, where you write the code that does what
you want your node to do – is written in a RosNodeProvider. They provide
the functionality for a node. You would extend RosNodeProvider and override
its virtual functions to flesh out the functionality of your node.
Lifecycle of a Node#
As mentioned earlier, to create a node, you would extend RosNodeProvider, which
contains several functions that are called at different stages of a node’s life.
Let’s go through the lifecycle of a Node, in order from enable to disable.
parseConfig#
This is the first function that the Node Framework calls from your node implementation when your node is enabled. You are given the parsed YAML configuration file for your node, and you get to set variables and configure your node appropriately. For example, the HAL node might take in a mapping from motor ID’s to the location on a microcontroller, and would store this in some internal representation.
Here is the function documentation. Pay attention to what you’re expected to return:
-
virtual bool parseConfig(const YAML::Node &node) = 0#
Parses this node’s configuration from the specified input stream.
- Parameters
config – the stream with the nodes configuration
- Returns
true if the node is happy with its configuration. False if it wishes to abort the enable process because it is not happy with its configuration. Note that even after this method, this node provider may be destructed, even in this method returns true. However, after setUpNodeFunctions returns true, this node will not be destructed until prepareForDisable is called.
requiredDependencies#
This is where you will provide a list of other nodes that your node depends on. That is, nodes that you need to have enabled before your node can do its thing. This can be a constant list of dependencies, or it can vary dynamically according to the configuration. For example, if you have a node that monitors all of the available cameras, you might have the user specify a list of camera nodes in your node’s configuration, and then only depend on the camera nodes associated with the cameras they defined in config.
Here is the function documentation:
-
virtual std::shared_ptr<std::unordered_set<std::string>> requiredDependencies() = 0#
Calculates the dependencies which this node will require. This method will be called after parseConfig, allowing a node to dynamically determine what dependencies is wants. This method will be called before handleDependencies to verify that all dependencies are met.
- Returns
the set of dependency names which this node requires.
setUpNodeFunctions#
Assuming all of your node’s dependencies could be acquired, we move on to this step. Finally,
you can give your node its actual functionality. Remember, all interactions with a RosNode
are done via ROS. So, this is where you’ll define your node’s ROS publishers, subscribers, services,
action servers, and action clients. You can listen to the output of one node to generate the output of
your own. This is the really cool part of a node.
Here is the function documentation:
-
virtual bool setUpNodeFunctions(ros::NodeHandle &handle, const std::unordered_map<std::string, std::string> &deps) = 0#
Effect: creates all needed publishes, subscribers, and services for this node.
- Parameters
handle – the handle to use to create the needed ROS constructs.
deps – the map from dependency name to the node driver which satisfies the given dependency. The set of keys in the map will be equal to the set returned by requiredDependencies.
- Returns
true if the node was able to set itself up, false otherwise.
ok#
Now, your node is enabled, and it’s doing its thing. It’s receiving messages and responding to requests.
Everything is beautiful until… oh no! A fatal error occurs. At that point, you would want to shut down
your node, and the ok function can help you do that. Before and after every ROS callback, the framework
(specifically, the RosNode class) will poll this function to check if your node wants to be disabled.
If the function returns false, then the node will be scheduled for destruction.
Here is the function documentation:
-
virtual bool ok() = 0#
Determines if the node is OK. This method will be called
- Returns
true if the node is working properly. false if the node wishes to be disabled.
prepareForDisable#
As the name implies, this function is called when your node is being disabled, whether its’ due to a crash
or a normal rover shutdown. This function should “clean up” the node, and return true if the clean-up
succeeded. For example, you might have to write zero to all of the associated motors when disabling a motor
controller. If you fail to write, you might return false here.
Important
Make sure you give this function sufficient thought when designing and implementing your node. Oftentimes, cleaning up your node can be just as important as the rest of its functionality.
Here is the function documentation:
-
virtual bool prepareForDisable() = 0#
Effect: prepares this node to be disabled. After this method completes, no more methods will be called on this object, except for the desctructor. No ROS callbacks will be called either. This method may try to set a safe state before being disabled, but has no gaurentee as to the state of any of its dependencies. The dependencies might be enabled, or disabled, or may change state during this method.
- Returns
true if this node was able to disable cleanly and set a safe state. False otherwise.
Guided Example: The Addition Node#
We’re going to cook up an example to give you a better sense for how this works in practice. This node will add two unsigned integers together.
The AdditionNodeProvider#
Start by writing the skeleton for your node’s main class, a RosNodeProvider implementation:
1class AdditionNodeProvider : public cmr::RosNodeProvider {
2 public:
3 bool parseConfig(const YAML::Node& node) { return true; }
4 bool setUpNodeFunctions( ros::NodeHandle& handle,
5 const std::unordered_map<std::string, std::string>& deps) {
6 return true;
7 }
8 bool prepareForDisable() { return true; }
9 std::shared_ptr<std::unordered_set<std::string>> requiredDependencies() {
10 return std::make_shared<std::unordered_set<std::string>>();
11 }
12 bool ok() { return true; }
13};
This is the most basic form of a node provider. It does nothing, and always says that it succeeded. It doesn’t depend on any other nodes.
Creating the Addition Action#
Let’s make our addition node do what we set out to make it do: add two numbers. Recall that all
RosNodes communicate by means of dispatching actions. Some legacy code might use services still, but
we strongly prefer actions because they have no timeouts, which is dangerous.
TODO: Convert example code to use messages instead of services.