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.
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.
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/
| Folder | Role | How much do you edit it by hand? |
|---|---|---|
src/ | Home of all your code; each Package is a subfolder here | Almost always โ 95% of your work happens here |
build/ | Temporary compilation files | Almost never |
install/ | Final, runnable version of the project after building | Only for sourcing |
log/ | Reports from every build and run | Only when debugging a build error |
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.
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.
Open a terminal and run these commands in order:
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.
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.
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
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:
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
| File | Role |
|---|---|
package.xml | The Package's identity card: name, version, description, maintainer, dependencies |
setup.py | Tells Python and ROS the Package's name, which files to install, and which Nodes are runnable |
setup.cfg | Specifies where Python executable files get installed |
__init__.py | Tells 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++
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.
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.
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:
| Part | Meaning |
|---|---|
simple_node (left side) | The name you use with ros2 run |
archo_bringup.simple_node | The file path: archo_bringup/simple_node.py |
:main | When 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.
| Level | Use |
|---|---|
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 |
main() Function โ The Heart of ExecutionThe order of the five lines inside main() matters a great deal, and this same pattern always repeats:
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.
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
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
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:
The Node stays running after printing the message โ this is normal, because rclpy.spin(node)
is still executing. Press Ctrl+C to stop it.
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.
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.
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.
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.
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.
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.
source install/setup.bash after opening a new terminal.SimpleNode) with the ROS network Node name (simple_node).rclpy.spin() and being surprised the Node shuts down immediately.colcon build instead of --packages-select on large projects, wasting a lot of time.