90 % of the semester. A named, typed channel — nobody addresses anybody.
No arrow goes box to box — everything goes through the name.
publish() does not block and returns nothing: no acknowledgement, and no error when nobody listenspublish() and the callback — fall behind and messages are dropped silentlyros2 node info · rqt_graph show what actually connectedrviz2 mid-run and it just connects — the publisher never learns about it docs.ros.org/en/jazzy/Tutorials/Intermediate/RViz/RViz-User-Guide/RViz-User-Guide.html
docs.ros.org/en/jazzy/Concepts/Intermediate/About-Discovery.html
It waits — they connect when the publisher appears.
ros2 launch may bring nodes up in any orderros2 topic info — not by reading the log| Source | Type | Topic |
|---|---|---|
| 2D lidar | sensor_msgs/msg/LaserScan |
/scan |
| 3D lidar, depth | sensor_msgs/msg/PointCloud2 |
/points |
| Camera | sensor_msgs/msg/Image + CameraInfo |
/camera/image_raw |
| IMU | sensor_msgs/msg/Imu |
/imu/data |
| GNSS | sensor_msgs/msg/NavSatFix |
/gps/fix |
| Wheels, joints | sensor_msgs/msg/JointState |
/joint_states |
Header — a stamp and a frame; without it nothing can be fusedImage alone is useless for geometry: CameraInfo carries the intrinsics| Meaning | Type | Topic |
|---|---|---|
| Where the robot is | nav_msgs/msg/Odometry |
/odom |
| The map it built | nav_msgs/msg/OccupancyGrid |
/map |
| Where to drive | geometry_msgs/msg/Twist |
/cmd_vel |
The topic names are conventions, not rules — but every third-party package expects them.
/odom and /map mean$ ros2 interface show sensor_msgs/msg/LaserScan
std_msgs/Header header # when, and in which frame
float32 angle_min # start angle of the scan [rad]
float32 angle_increment # angular distance between measurements [rad]
float32[] ranges # the measurements [m]
/scan can come from Gazebo · a real lidar driver · ros2 bag play · your own test node. Your code does not change.
A custom type isolates you: no third-party package can read it. Invent one only when no standard type fits.
sensor_msgs/msg/LaserScan → RViz draws it, ros2 bag records it, slam_toolbox maps with itnav_msgs/msg/Odometry and subscribe to geometry_msgs/msg/Twist → Nav2 can drive your robotsensor_msgs/msg/Imu → robot_localization fuses it with your odometryapt install eachYour robot joins the ecosystem by publishing the right message. That is the entire entry fee.
| Package | Consumes → produces |
|---|---|
| Nav2 | a goal pose → /cmd_vel: costmaps, planner, controller, recovery |
| slam_toolbox | /scan → a map and a pose, with loop closure |
| ros2_control | a body twist → wheel commands; encoders → odometry |
| robot_localization | wheel odometry + IMU + GNSS → one fused pose |
| image_pipeline | a raw camera stream → calibrated, rectified, depth |
| MoveIt 2 | the same job for arms: inverse kinematics, planning, collision checking |
Already installed with it: RViz · Gazebo and the ros_gz bridge · rosbag2 · teleop_twist_keyboard
diff_drive_controller turns a body twist intoWe meet it again as soon as we model our own robot: describe the wheels, pick the controller.
$ ros2 topic echo /scan --once # one message, printed
$ ros2 topic hz /scan
average rate: 9.984 # how often it really arrives
$ ros2 topic bw /camera/image_raw
27.31 MB/s # what a camera actually costs you
$ ros2 topic info /scan --verbose
Publisher count: 1 # who is on the other end
Subscription count: 2
There is no file that says how the system is wired. There is only the live graph — and these commands.
list -t first — the name is usually right and the type is notros2 topic list shows /scan. ros2 topic echo /scan prints nothing, and no error appears anywhere. What is wrong?The topic exists, but nothing is publishing to it.
ros2 topic list shows a name as soon as anyone mentions it — a subscriber alone is enoughros2 topic info /scan --verbose prints the publisher count. Zero publishers, no data, no error/scan while the driver publishes /scan_raw
rqt_graph — and you see who is actually connected to whom$ ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.2}, angular: {z: 0.5}}"
The robot drives an arc. No code written.
echo--once sends a single message and exits; without -r the default is 1 Hz$ ros2 bag record --topics /scan /odom /cmd_vel # or -a for everything
$ ros2 bag info rosbag2_2026_09_07-14_22_10 # topics, duration, message count
$ ros2 bag play rosbag2_2026_09_07-14_22_10 # replay it as live data
Swap the ends of the graph. Two things change in your configuration:
use_sim_time — in simulation every node takes its clock from the simulator; on the robot it is the wall clock again
Two computers. A Raspberry Pi runs ROS 2; a small control board runs the motors.
Why two? Linux is good at graphs and networking, and bad at hitting a deadline. Driving motors is a small loop that must run on time, every time — so it gets its own board.
Sensors. 360° laser scanner · IMU on the control board · encoders inside the two servo motors · battery monitor.
It publishes: /scan · /odom · /imu · /joint_states · /battery_state
It subscribes to one motion topic: /cmd_vel
Twist Message to Turning Wheels In simulation the velocity you ask for is the velocity you get. On the robot there is firmware you
did not write, a cable in between, and wheels that slip.
| What it is | When | |
|---|---|---|
| Topic | stream, no reply | sensor data, commands |
| Service | request → response | a quick one-off call |
| Action | goal → feedback → result | a long task |
| Parameter | a named setting of a node | change behaviour, no rebuild |
ros2 service list -t · ros2 action list -t — the same introspection you already use on topics docs.ros.org/en/jazzy/Concepts/Basic/About-Services.html · docs.ros.org/en/jazzy/Concepts/Basic/About-Actions.html
$ ros2 pkg create --build-type ament_python my_pkg --dependencies rclpy sensor_msgs
One command, and you get:
ws/src/my_pkg/
├── package.xml # name, license, dependencies — this is what makes it a ROS package
├── setup.py # what to install, and which script becomes a runnable node
└── my_pkg/
└── my_node.py # your code
package.xml is why rosdep install can fetch your dependencies on someone else's machinebuild/, install/, log/ are generated — they never go into git docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.html
| Build type | You write | Use it for |
|---|---|---|
ament_python |
setup.py |
Python nodes — everything in Lab 0 |
ament_cmake |
CMakeLists.txt |
C++ nodes |
colcon builds and ros2 run can start docs.ros.org/en/jazzy/How-To-Guides/Ament-CMake-Documentation.html
$ ros2 param list /my_node
$ ros2 param get /my_node speed
$ ros2 param set /my_node speed 0.5
$ colcon build --packages-select my_pkg --symlink-install
$ source install/setup.bash
$ ros2 run my_pkg my_node
docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html
docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html
Code is installed; configuration is read.
colcon build copies (or links) your package into install/, and ros2 run starts what is there — not what is in src/source install/setup.bash is what tells this shell where install/ is docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html
$ ros2 launch my_robot bringup.launch.py
Three terminals' worth of nodes, from one file:
| Node it starts | What that node does |
|---|---|
robot_state_publisher |
publishes the robot's shape and joint positions |
lidar_driver |
reads the sensor, publishes /scan |
diff_drive_controller |
turns /cmd_vel into wheel commands |
--show-args lists everything you can override from the command line docs.ros.org/en/jazzy/Tutorials/Intermediate/Launch/Launch-Main.html
ros2 run <pkg> <node> # start a node
ros2 node list · ros2 node info # who is in the graph
ros2 topic list -t # what is being published
ros2 topic echo · hz · info # look at the data
ros2 topic pub # publish by hand
rqt_graph # the graph, drawn
colcon build --packages-select # build one package
source install/setup.bash # make it visible to ros2
sudo apt install ros-jazzy-desktopsource /opt/ros/jazzy/setup.bash in every new terminal, or once in .bashrcmkdir -p ~/ws/src, then run colcon build from ~/wssource ~/ws/install/setup.bash — now ros2 run finds your own packagesros2 run demo_nodes_cpp talker here, ros2 run demo_nodes_py listener thereSame version for all of us: Jazzy on Ubuntu 24.04, or the course container on any other OS.
Not graded. No git, no README.
Range · Imu · Temperature · BatteryState · LaserScanros2 topic list · echo · hzstd_msgs/String — the rest of the course runs on sensor_msgs docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html · docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries.html
Lecture 1 — Gazebo and ROS 2
Gazebo: physics, plugins, worlds and sensors · models from Fuel · running without a window · the ROS 2 ↔ Gazebo bridge · RViz · simulation time
ros2 launch simple_bot bot.launch.py — a robot that already drives from /cmd_vel