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.
A fully set-up Workspace ends up with four folders inside it. This is what it looks like once everything has been created and built at least once:
archo_ws/
โโโ src/
โโโ build/
โโโ install/
โโโ log/
This is a diagram of a folder structure, not something you type into a terminal. You do not create all four of these folders yourself. In fact, only one of them is yours to make โ the other three are generated automatically. Here's the split:
| Folder | Who creates it? | Role | How much do you edit it by hand? |
|---|---|---|---|
src/ | You (with mkdir) | Home of all your code; each Package is a subfolder here | Almost always โ 95% of your work happens here |
build/ | Generated by colcon build | Temporary compilation files | Almost never |
install/ | Generated by colcon build | Final, runnable version of the project after building | Only for sourcing |
log/ | Generated by colcon build | Reports from every build and run | Only when debugging a build error |
You create only the src folder. Everything else appears later, on its own. Run this in your terminal:
mkdir creates a directory; -p also creates any missing parent folders along the
way (here, archo_ws itself, before creating src inside it) and does not
complain if the folder already exists. ~ is your home directory. At this point, your
Workspace looks like this:
archo_ws/
โโโ src/
That's it โ no build, install, or log yet, because nothing has been built.
Once there's at least one Package inside src (you'll create one in the next section),
running the build tool from inside archo_ws:
colcon build
automatically creates build/, install/, and log/ for you. You never
create these three by hand, and you should not normally edit files inside them โ they get regenerated
every time you build. This is covered in more detail once you build your first Package later in this chapter.
Right after mkdir -p ~/archo_ws/src, running ls ~/archo_ws should show only
src. If you see build, install, or log already, it
means colcon build was run at some point โ that's fine, it just means you're further along
than this step assumes.
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.
Right now, since you've only created the folder and haven't built anything yet, your Workspace should
still look empty inside src:
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 the next section you'll create your first Package inside src, and later in this chapter
you'll run colcon build for the first time โ that's the moment build,
install, and log will appear next to src, matching the four-folder
diagram shown earlier.
In your own terminal, run: cd ~/archo_ws, then pwd, then tree -L 1.
Confirm you only see src so far โ if build, install, or
log already show up, you've built ahead of this point, which is fine.
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.
None of these folders exist on your system yet โ you haven't created a single Package so far, and ROS 2
does not create them for you automatically. A Package is created with the ros2 pkg create
command. Let's create the ARCHO team's first Package, archo_bringup, as a Python Package:
Run this from inside ~/archo_ws/src โ that's where every Package lives. ros2 pkg
create generates a new folder named after the Package, already containing the standard files a
Python ROS 2 Package needs (package.xml, setup.py, and so on). You do not create
those files by hand; the command does. Now go into the 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.
ros2 pkg create did not write any Node code for us โ it only generated the empty
scaffolding (folders and boilerplate files). We still have to write the Node ourselves. Create a new
file at ~/archo_ws/src/archo_bringup/archo_bringup/simple_node.py with the content shown
below, then open setup.py and add one line inside entry_points so ROS 2 knows
this file contains a runnable Node:
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.