ROS Basics - Uvic-Robotics-Club/uvic-rover-2027 GitHub Wiki
Nodes are responsible for a single, modular purpose. This means that a node should generally avoid sharing responsibilities as that defeats the communicative/network purpose of robotics systems, where different logical, object-oriented programs share information and data with one another. Since robotics systems is compromised of signals, from wheel motors, to embedded system micro-controllers, to a controller β all of these different responsibilities at different structural levels need their own nodes for maintainability, testability, reusability, and fault isolation. However there can be situations where nodes can be monolithic (e.g., tightly coupled sensor fusion code that never needs to be reused separately).
A node can hold any number of publishers and subscribers simultaneously, each to different topic. Most nodes are both in practice (e.g., a filter node subscribes to raw sensor data AND publishes filtered output).
A topic acts as a bus between any number of nodes. Basically itβs the middleman between a subscriber and publisher. A publisher releases data via a topic name, and the subscriber, looking for that topic name, can retrieve the information. Basically all publisherβs need to publish their information to a topic, and any subscriber node can connect to that topic to retrieve that information.
A minor concept to understand is that a node can be BOTH a subscriber and a publisher, it just depends on what itβs responsibilities are.
For ROS2 Topics, if you call -- verbose (or -v) to ros2 topic info <topic_name>, it will return additional details like Node names of publishers and subscribers, topic type, and QoS profiles (ROS2 DDS specific).
There is also a way to view the rate at which data is published using: ros2 topic hz <topic_name>
There is also a way to view the bandwidth used by the topic: ros2 topic bw <topic_name>
These are just the specific data object type (a schema/contract/struct definition) that the topic receives and sends. It could be custom messages that can be some string, a vector, coords, etc. It is a verification to ensure correct data is getting sent.
Message type compatibility is checked at discovery time, so if a publisher and subscriber on the same topic name have different message types, then they won't connect.
Publishers and subscribers are how nodes actually send and receive data over topics. A publisher writes data to a topic, a subscriber reads from it. They never interact directly.
Minimal Publisher (C++)
#include "rclcpp/rclcpp.hpp" // Core ROS2 C++ library
#include "std_msgs/msg/string.hpp" // Built-in ROS2 String message type
// Our publisher node inherits from rclcpp::Node which gives it all ROS2 functionality
class MinimalPublisher : public rclcpp::Node {
public:
// "minimal_publisher" is the name this node registers with in the ROS2 network
MinimalPublisher() : Node("minimal_publisher") {
// Create a publisher on the topic named "topic" using String messages
// The 10 is the queue size β how many messages to buffer if the subscriber
// isn't consuming them fast enough before dropping old ones
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
// Create a timer that fires a callback every 500ms
timer_ = this->create_wall_timer(
std::chrono::milliseconds(500),
// This is a lambda β an anonymous inline function that runs on each timer tick
// [this] captures the current object so we can access publisher_ inside it
[this]() {
auto message = std_msgs::msg::String();
message.data = "Hello from the rover";
publisher_->publish(message); // Send the message to the topic
});
}
private:
// SharedPtr is ROS2's way of managing memory β you'll see this pattern everywhere
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv); // Initialize ROS2 β always the first call
rclcpp::spin(std::make_shared<MinimalPublisher>()); // Keep the node alive and processing callbacks
rclcpp::shutdown(); // Clean up when done β always the last call
return 0;
}Minimal Subscriber (C++)
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MinimalSubscriber : public rclcpp::Node {
public:
MinimalSubscriber() : Node("minimal_subscriber") {
// Create a subscription to the same "topic" the publisher is writing to
// The message type must match the publisher's β std_msgs::msg::String here
// 10 is the queue size, same concept as the publisher
subscriber_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10,
// This lambda is the callback β it fires every time a new message arrives
// msg is a shared pointer to the received message
[this](const std_msgs::msg::String::SharedPtr msg) {
// RCLCPP_INFO logs to the ROS2 console β equivalent to printf but
// routed through the ROS2 logging system so it shows up in ros2 logs
RCLCPP_INFO(this->get_logger(), "Received: '%s'", msg->data.c_str());
});
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>()); // Keeps node alive, waiting for messages
rclcpp::shutdown();
return 0;
}QoS (Quality of Service) controls how publishers and subscribers handle message delivery. This is worth understanding because it directly affects reliability and latency.
-
RELIABLE-- guarantees delivery, retransmits lost messages, TCP-like behaviour. Use for critical data like commands. -
BEST_EFFORT-- no retransmission, drops messages if the network can't keep up. UDP-like behaviour. Use for high-frequency sensor data or camera feeds where a dropped frame is acceptable.
-
VOLATILE-- subscriber only receives messages published after it connects. Default for most cases. -
TRANSIENT_LOCAL-- subscriber receives the last message even if it connects after it was published. Useful for state information.
One important constraint: publisher and subscriber QoS profiles must be compatible. A RELIABLE publisher and a BEST_EFFORT subscriber WON'T CONNECT.
Parameters are configurable values exposed by a node at runtime. Instead of hardcoding values like motor speed limits or camera resolution, you declare them as parameters so they can be changed without recompiling. In ROS2 the standard approach is a YAML file, which lets you configure multiple nodes and their parameters in one place.
Declaring parameters in the node (C++)
#include "rclcpp/rclcpp.hpp"
class DriveNode : public rclcpp::Node {
public:
DriveNode() : Node("drive_node") {
// Declare the parameter with a default value
// If no YAML file provides this value, 1.0 is used as the fallback
this->declare_parameter<double>("max_speed", 1.0);
// Retrieve the parameter value at runtime
double max_speed = this->get_parameter("max_speed").as_double();
RCLCPP_INFO(this->get_logger(), "Max speed: %f", max_speed);
}
};Setting parameters via YAML
# params.yaml
# The top level key must match the node name exactly
drive_node:
ros__parameters:
max_speed: 0.5