ROS 2 Learning Path  ยท  Illustrated Textbook

Chapter 2: The ROS 2 Development Environment

From a mental map to your first real Node on your own system
Audience: Beginner to robotics engineer
Prerequisite: Chapter 1 (ROS 2 Architecture)
Running project: ARCHO robot
Reading time: 70โ€“90 minutes
What's in this chapter 2.1From map to city โ€” why this chapter is different 2.2Workspace โ€” the project's factory 2.3Package โ€” the organized unit of code 2.4Your first real Node โ€” line by line 2.5Building and running with colcon 2.6Observing a Node from outside 2.7Summary, glossary, and exercises

2.1From Map to City

By the time you finished Chapter 1, you had ARCHO's architectural map in your head: you knew what a Node was, what a Topic was, when to use a Service, and when to use an Action. But a map, by itself, never moves a robot. Now it's time to actually enter the city: open a folder on your own system, write a Python file, and for the first time see the word Node โ€” which until now was just an abstract concept โ€” become a real, living process that you can see in the terminal, stop, and run again.

This chapter has three stops: first we learn what a Workspace is and why every project needs one; then we move into Package, the smallest buildable unit in ROS 2; and finally we read the ARCHO team's first real Node line by line, build it, and run it on the system.

flowchart RL A["Workspace
Main project folder"] --> B["Package
Organized unit of code"] B --> C["Node
simple_node.py"] C --> D["colcon build"] D --> E["ros2 run"] E --> F["Chapter 3: Real Publisher and Subscriber"] style A fill:#eef0ff,stroke:#3d4bf5,color:#211f1a style F fill:#eafaf3,stroke:#0e9e6e,color:#211f1a,font-weight:bold

2.2Workspace โ€” The Project's Factory

The problem: a messy folder called Desktop

Imagine it's the ARCHO team's first day of writing code. One team member, without much thought, dumps everything straight onto the Desktop: camera.cpp, main.py, lidar_driver.cpp, motor.py, map.yaml. A week later, nobody knows which file belongs to which part of the robot, which version is the latest, or which one should even be compiled. This is exactly the wall every team runs into, sooner or later, without a clear folder structure.

The solution is simple: create a main folder and keep everything organized inside it. ROS 2 has formalized exactly this idea under the name Workspace.

๐Ÿ“– Definition: Workspace

A Workspace is the main folder that holds all of a project's ROS packages, build files, and final outputs. Each independent project (say, ARCHO versus a different robotic arm) usually has its own separate Workspace.

When you create a Workspace, you'll see these four folders:

archo_ws/
โ”œโ”€โ”€ src/
โ”œโ”€โ”€ build/
โ”œโ”€โ”€ install/
โ””โ”€โ”€ log/
FolderRoleHow much do you edit it by hand?
src/Home of all your code; each Package is a subfolder hereAlmost always โ€” 95% of your work happens here
build/Temporary compilation filesAlmost never
install/Final, runnable version of the project after buildingOnly for sourcing
log/Reports from every build and runOnly when debugging a build error
๐Ÿง  Simple Analogy: The Factory

Think of the Workspace as a factory. src is where the engineers design things; build is the factory's assembly line that turns raw parts into products; install is the warehouse holding the finished, ready-to-ship product; and log is the factory's daily report ledger. If a product comes out defective one day, you first check the log reports to see where in the production line the problem occurred.

๐ŸŒ Multiple Workspaces for Multiple Purposes

In real projects, you usually have more than one Workspace on your system at the same time โ€” for example, ~/archo_ws for the robot's main code, and ~/simulation_ws for simulation experiments that aren't yet ready to merge with the main code. This separation keeps unfinished experiments from mixing with stable code.

Now let's move to your own system

Open a terminal and run these commands in order:

dev@archo:~$ cd ~/archo_ws dev@archo:~/archo_ws$ tree -L 1 . โ”œโ”€โ”€ build โ”œโ”€โ”€ install โ”œโ”€โ”€ log โ””โ”€โ”€ src 5 directories, 0 files dev@archo:~/archo_ws$ ls src archo_bringup
โš ๏ธ A Note on Uppercase and Lowercase

If you mistype tree -L 1 as, say, tree -l 1 (lowercase l), Linux thinks 1 is a folder name and prints [error opening dir]. The uppercase -L means "only show folders down to this depth" โ€” this is one of the most common first-day typos.

Easy Exercise

In your own terminal, run: cd ~/archo_ws, then pwd, then tree -L 1 and ls src. Note the output of each, and say which folder is likely to see the most daily changes.

2.3Package โ€” The Organized Unit of Code

If the Workspace is the factory, a Package is one of the units inside that factory. A real robot like ARCHO usually has several independent Packages, not one giant folder:

archo_ws/
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ archo_description   # Physical model and URDF
    โ”œโ”€โ”€ archo_bringup       # Launches the whole system
    โ”œโ”€โ”€ archo_control       # Motor and motion control
    โ”œโ”€โ”€ archo_navigation     # Nav2 configuration and path planning
    โ”œโ”€โ”€ archo_sensors       # Camera, LiDAR, IMU drivers
    โ””โ”€โ”€ archo_interfaces    # Custom Message/Service/Action definitions
๐Ÿ“– Definition: Package

A Package is the smallest organized, buildable unit in ROS 2. A Package can contain Nodes, Python or C++ code, Launch files, Parameter definitions, Messages, Services, Actions, URDF files, RViz configurations, and tests.

Go into the ARCHO team's first Package and look at its structure:

dev@archo:~$ cd ~/archo_ws/src/archo_bringup dev@archo:~/archo_ws/src/archo_bringup$ tree -L 3 archo_bringup/ โ”œโ”€โ”€ archo_bringup/ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ””โ”€โ”€ simple_node.py โ”œโ”€โ”€ package.xml โ”œโ”€โ”€ resource/ โ”œโ”€โ”€ setup.cfg โ”œโ”€โ”€ setup.py โ””โ”€โ”€ test/
โš ๏ธ Why Do We Have Two Folders With the Same Name?

Seeing archo_bringup/archo_bringup/ is not a mistake. The outer folder is the ROS 2 Package itself; the inner folder is the Python module that the actual Nodes live inside.

ROS Package
โ””โ”€โ”€ Python Module
    โ””โ”€โ”€ ROS Nodes

Important Package Files

FileRole
package.xmlThe Package's identity card: name, version, description, maintainer, dependencies
setup.pyTells Python and ROS the Package's name, which files to install, and which Nodes are runnable
setup.cfgSpecifies where Python executable files get installed
__init__.pyTells Python this folder is a module; it must exist even if empty

To figure out whether a Package was built with Python or C++, just check this:

grep build_type package.xml
# <build_type>ament_python</build_type>   โ†’ Python
# <build_type>ament_cmake</build_type>    โ†’ usually C++
๐Ÿ”ง Engineering Perspective: Package vs. Node

Don't confuse the two. A Workspace can hold several Packages, and a Package can hold several Nodes โ€” for example, archo_sensors could contain camera_node, imu_node, and battery_node all at once. But in professional projects, it's better for each Package to stay focused on a single area of responsibility rather than growing too big and disorganized.

Intermediate Exercise

Inside archo_bringup, run grep build_type package.xml and cat setup.py. In setup.py, look for the line with console_scripts and say what name it registers for running the Node.

2.4Your First Real Node โ€” Line by Line

Inside ARCHO's setup.py, you'll see this line:

entry_points={
    'console_scripts': [
        'simple_node = archo_bringup.simple_node:main',
    ],
},

This one line carries three important messages you should be able to unpack separately:

PartMeaning
simple_node (left side)The name you use with ros2 run
archo_bringup.simple_nodeThe file path: archo_bringup/simple_node.py
:mainWhen the Node runs, the main() function should be called

Now let's open the contents of simple_node.py:

import rclpy
from rclpy.node import Node


class SimpleNode(Node):
    def __init__(self):
        super().__init__('simple_node')
        self.get_logger().info('Hello from ARCHO โ€” first node alive!')


def main(args=None):
    rclpy.init(args=args)
    node = SimpleNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Now let's go through it line by line โ€” the way an experienced engineer would review a new teammate's code.

import rclpy

rclpy is the core ROS 2 library for Python โ€” short for ROS Client Library for Python. Its C++ counterpart is called rclcpp. Without rclpy, your Python program is just an ordinary script with no way to connect to the ROS 2 network.

from rclpy.node import Node

Here we import the ready-made Node class โ€” the same class we discussed in Chapter 1. This class already provides capabilities like creating Publishers, Subscribers, Services, Actions, Parameters, Timers, and Loggers; we simply inherit from it.

class SimpleNode(Node): and super().__init__('simple_node')

There's an important distinction here that a lot of beginners mix up: SimpleNode is the Python class name, but simple_node (inside super().__init__) is the name this Node is known by on the ROS 2 network. That's why, when you later run ros2 node list, you'll see /simple_node, not SimpleNode.

self.get_logger().info(...)

This line prints a message using ROS's built-in logging system. You could use Python's print() instead, but the Logger has several advantages: it shows the Node's name, marks the message's severity level, timestamps it, and lets you follow the messages on the /rosout Topic as well.

LevelUse
debug()Technical detail, for development only
info()Normal status and ordinary messages
warning()Possible problem ahead
error()An error that occurred
fatal()A very serious error that makes continuing impossible

The main() Function โ€” The Heart of Execution

The order of the five lines inside main() matters a great deal, and this same pattern always repeats:

flowchart TB A["rclpy.init()
Connect the program to the ROS 2 network"] --> B["node = SimpleNode()
Actually create the Node"] B --> C["rclpy.spin(node)
Keep the Node alive and listening for events"] C --> D["node.destroy_node()
After Ctrl+C, clean shutdown"] D --> E["rclpy.shutdown()
Disconnect from ROS 2"] style A fill:#eef0ff,stroke:#3d4bf5,color:#211f1a style C fill:#fdf3e4,stroke:#c8862c,color:#211f1a,font-weight:bold style E fill:#eafaf3,stroke:#0e9e6e,color:#211f1a
๐Ÿง  Simple Analogy: The Phone Operator

Think of rclpy.spin(node) like hiring a phone operator. Without spin(), the operator walks into the office, says one sentence, and immediately goes home โ€” the Node gets created, the message gets printed, and it shuts down right away. But spin() says: "stay by the phone and wait for calls." Those "calls" are exactly what later show up as Messages, Service requests, Timer firings, or an Action Goal.

And finally, at the end of the file, if __name__ == '__main__': main() is just a standard Python idiom: if this file is run directly (not through ros2 run), main() still gets called.

2.5Building and Running with colcon

When a file inside src changes, ROS still doesn't know about the installed version yet. We need to build the Workspace from the root:

cd ~/archo_ws
colcon build --packages-select archo_bringup
๐Ÿ“– What is colcon?

colcon is the official ROS 2 build tool. Its job is to find all Packages inside src, check their dependencies, compile them, build the install folder, and register the executable files. Running colcon build alone builds every Package; adding --packages-select builds just one, saving a lot of time on large projects.

After building, you need to "activate" the Workspace so the terminal knows where these Packages live:

source install/setup.bash
โš ๏ธ Important Note

Every new terminal you open needs you to run cd ~/archo_ws and source install/setup.bash again. Forgetting this step is the most common cause of a "Package not found" message in the first few days.

Now run the Node:

dev@archo:~/archo_ws$ ros2 run archo_bringup simple_node [INFO] [simple_node]: Hello from ARCHO โ€” first node alive!

The Node stays running after printing the message โ€” this is normal, because rclpy.spin(node) is still executing. Press Ctrl+C to stop it.

2.6Observing a Node from Outside

This section covers one of the important habits you should build from the very start: check every Node you run from a second terminal as well. Keep the first terminal open (the Node is still running), open a second terminal, and:

source /opt/ros/jazzy/setup.bash
cd ~/archo_ws
source install/setup.bash
ros2 node list

You should see /simple_node. For more complete information:

ros2 node info /simple_node

The output shows a list of this Node's Subscribers, Publishers, Service Servers and Clients, and Action Servers and Clients โ€” for now, they're all empty, since this Node only prints a message. But you'll probably see two internal ROS Topics as well: /parameter_events and /rosout.

๐ŸŒ Even Logging Travels Over a Topic

When you call self.get_logger().info(...), the message is not only printed in the terminal but also published on the /rosout Topic. If you run this in a second terminal:

ros2 topic echo /rosout

You'll see the same log messages live. This shows that even the logging system is built on the same Publish/Subscribe infrastructure from Chapter 1 โ€” in ROS 2, almost everything, even internal logging, relies on the same handful of core concepts.

$ ros2 run archo_bringup simple_node [INFO] [simple_node]: Hello from ARCHO... (node stays alive โ€” spin) Terminal 1 โ€” Running the Node $ ros2 node list /simple_node $ ros2 topic echo /rosout msg: "Hello from ARCHO..." Terminal 2 โ€” Live Inspection
Figure 2.1 โ€” Always inspect a Node from a second terminal as well; this habit later becomes essential for debugging complex Nodes.
Harder Exercise

Run the Node in the first terminal, then run ros2 node info /simple_node in a second terminal. Note the full output and explain why this Node's Publishers and Subscribers (aside from ROS's internal ones) are empty.

2.7Chapter 2 Summary

You've walked the whole path so far: you created a Workspace, found a Package inside it and dissected it, read ARCHO's first real Node line by line, built it with colcon build, ran it with ros2 run, and inspected it live from a second terminal. This was the first time you touched Chapter 1's abstract concepts on your own real system.

โœ… Learning Checkpoint
  • I can explain what a Workspace is and why every project needs one.
  • I know the role of each of the src, build, install, and log folders.
  • I can tell a Package apart from a Node and explain how they relate.
  • I can explain a simple rclpy file line by line.
  • I know why rclpy.spin() is needed and what happens without it.
  • I can build, run, and inspect a Node from a second terminal.

Connection to the Main Project

ARCHO Project now has its first Package and first real Node โ€” it still only prints a "hello" message, but the Workspace and Package infrastructure is ready for us to turn this quiet Node into a real Publisher in the next chapter.

What the Next Chapter Adds

In Chapter 3, we'll turn this same simple_node into a Publisher that publishes a Message on a real Topic every second, and build a separate Subscriber to receive it. For the first time, you'll see with your own eyes two independent Nodes โ€” exactly as described in Chapter 1 โ€” talking to each other.

Chapter 2 Glossary

Workspace
The main folder holding all the Packages, build files, and outputs of a ROS 2 project.
Package
The smallest organized, buildable unit in ROS 2; can contain Nodes, Launch files, Parameters, and more.
colcon
The official ROS 2 build tool that compiles and installs the Packages inside src.
rclpy
The ROS 2 client library for Python; the bridge between a Python program and the ROS 2 network.
spin()
The function that keeps a Node alive and waiting for ROS events (Messages, Services, Timers, Actions).
Logger
ROS's built-in logging system, which publishes messages on the /rosout Topic in addition to printing them in the terminal.
entry_points
The part of setup.py that links a Node's runnable name to its file and main function.

Common Chapter 2 Mistakes โ€” Summary