Docs menu
ROS 2 in this workspace
Nodes, topics, parameters, launch files and sourcing, as this workspace uses them, with the commands for inspecting a running car.
ROS 2 is a framework for running many small programs that exchange messages. The car runs about a dozen of them at once. This page covers the parts of ROS 2 the rest of the docs rely on. Lecture 1 (notes), Tutorial 1 (notes) and Lab 1 of the course teach them from scratch, and the site’s hands-on ROS 2 primer builds a first package step by step.
Nodes and topics
A node is one running program with one job: read the LiDAR, choose a steering angle, or talk to the motor controller. Nodes never call each other’s functions. They exchange messages on named channels called topics. A node publishes on a topic without knowing who listens, and subscribes to a topic without knowing who sends.
gap_follow does not know that urg_node exists: it only needs some node to publish laser scans on /scan.Because nodes only share topics, one node can be replaced without touching the others. The simulator works this way: racerbot_sim publishes its own /scan and /odom, and the driving nodes above it run unchanged. Any topic can also be inspected from a terminal while the car runs.
Every message has a fixed type that both sides agree on. The workspace uses standard types:
| Topic | Message type | Contents |
|---|---|---|
/scan |
sensor_msgs/LaserScan |
One LiDAR sweep: ranges, angle_min, angle_increment |
/drive, /teleop |
ackermann_msgs/AckermannDriveStamped |
A steering angle in radians and a speed in m/s |
/odom |
nav_msgs/Odometry |
The car’s estimated pose and velocity |
/joy |
sensor_msgs/Joy |
Gamepad buttons and axes |
Parameters
A parameter is a named setting a node reads when it starts, such as gap_follow’s max_speed. Each package keeps its parameters in YAML files under config/, which the launch file loads, and the YAML’s values replace the defaults declared in the code. To change how a node behaves, edit its YAML, not its Python.
Some parameters can also change while the node runs, through ros2 param set or the dashboard. A change made that way lasts until the node restarts.
Launch files
A launch file is a Python script that starts a set of nodes with their parameters. ros2 launch <package> <file> runs it, and one Ctrl+C stops every node it started.
ros2 launch f1tenth_stack bringup_launch.pyA single node can also be started on its own with ros2 run <package> <executable>. The workspace uses this for tools such as the doctor.
Packages and colcon
A package is one unit of code that is built and installed together. Every package in src/ has a package.xml that names it and its dependencies. The workspace’s own Python packages share one layout:
src/<package>/
├── package.xml name and dependencies
├── setup.py, setup.cfg build settings and the executables it installs
├── resource/<package> empty marker file
├── <package>/ the Python code: the node, plus logic that imports no ROS
├── launch/ launch files
├── config/ YAML parameters
└── test/ testscolcon build builds every package into install/. ROS 2 runs what is in install/, not what is in src/. See Set up the workspace for the build commands.
Anatomy of a node
gap_follow_node shows every piece a node in this workspace has. These excerpts are from gap_follow_node.pyGitHub, shortened.
A class that is a node. It subclasses rclpy.node.Node and gives the node its name:
class GapFollowNode(Node):
def __init__(self):
super().__init__('gap_follow_node')Parameters, declared with a default, then read. The default in the code is a fallback. The YAML loaded by the launch file wins, so min_gap_distance is declared as 1.0 here but runs at 2.0, the value in gap_follow.yaml:
self.declare_parameter('scan_topic', '/scan')
self.declare_parameter('min_gap_distance', 1.0)
...
self.scan_topic = self.get_parameter('scan_topic').valuePublishers and subscribers. Each names a message type, a topic, and for a subscriber the function to call when a message arrives. The last argument is the queue depth; see QoS and timing.
self.drive_pub = self.create_publisher(AckermannDriveStamped, self.drive_topic, 10)
self.scan_sub = self.create_subscription(
LaserScan, self.scan_topic, self.scan_callback, 10)
self.odom_sub = self.create_subscription(
Odometry, self.odom_topic, self.odom_callback, 10)Callbacks. A callback runs each time its topic delivers a message. A small one stores what it needs for later:
def odom_callback(self, msg: Odometry):
self.current_speed = float(msg.twist.twist.linear.x)
self.last_odom_time = self.get_clock().now()scan_callback is the large one: each scan runs the whole follow-the-gap pipeline and ends with self.drive_pub.publish(...).
main, the entry point. rclpy.spin waits for messages and calls the callbacks until the node is stopped:
def main(args=None):
rclpy.init(args=args)
node = GapFollowNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()How ROS finds it. setup.py registers main as an executable named gap_follow_node, which is what ros2 run gap_follow gap_follow_node and the launch file start:
entry_points={
'console_scripts': [
'gap_follow_node = gap_follow.gap_follow_node:main',
],
},Node(
package='gap_follow',
executable='gap_follow_node',
name='gap_follow_node',
output='screen',
parameters=[gap_follow_config, ...],
)The parameters list is applied in order, so later entries override earlier ones: the launch file loads the YAML first, then any value given on the command line, such as max_speed:=1.0.
Sourcing
Every new terminal needs two commands before any ros2 command, in this order:
source /opt/ros/jazzy/setup.bash
source ~/racerbot-ws/install/setup.bashThe first makes ROS 2 itself available. The second adds the workspace’s built packages on top of it. A terminal that skipped either reports Package ... not found for a package that exists. Sourcing sets environment variables in the current shell only, so it does not carry over to a new terminal, SSH session or tmux pane.
ROS domains
ROS 2 nodes find each other automatically on the local network. Two cars, or a laptop and a car, on the same network can join one graph by accident, and then one car’s LB could drive the other. The ROS_DOMAIN_ID environment variable separates graphs: nodes only see nodes with the same ID.
Before running two cars together, check that each car sees only its own gamepad:
ros2 topic info /joyPublisher count: 1 is correct. If it reads 2, give the cars different ROS_DOMAIN_ID values. The tests use the same mechanism to stay off the car’s graph. See Run the tests.
Inspect a running car
These commands only read. They are safe to run at any time, in any sourced terminal.
| Command | Shows |
|---|---|
ros2 node list |
Every running node |
ros2 topic list |
Every topic |
ros2 topic hz /scan |
How often a topic publishes. /scan runs at about 40 Hz. |
ros2 topic echo /drive --field drive |
The messages on a topic, here only the drive field |
ros2 topic info /drive |
How many nodes publish and subscribe |
ros2 param get /gap_follow_node max_speed |
One parameter’s current value |
ros2 param list /gap_follow_node |
Every parameter of a node |
Checked against
car 1 commit 0265263
and
car 2 commit b81b291, before the two workspaces merged.