Week 5, ROS 2: nodes and communication
ROS 2, from the wire up.
The official tutorials, a simulated drone wired together with topics, services and an action, measured QoS and executor experiments, and then ROS 2 taken apart: a pure Python DDS participant that speaks RTPS, CDR and REP 2011 type hashes well enough to talk to real ROS 2 nodes.
- stack
- ROS 2 Jazzy, Fast DDS and Cyclone DDS, in a container
- drone
- camera_driver, detector, planner and px4_bridge: 2 topics in the pipeline, odometry and battery, 2 services and 1 action
- from scratch
- RTPS discovery and reliability, CDR, REP 2011 type hashes and the QoS rules, in pure Python
- 1,210 of 1,210REP 2011 type hashes identical to every installed interface
- 1,265,625QoS pairs judged exactly like
rclpy, reason strings included - 300 of 300random CDR messages identical to
rclpy.serialization - 4 directionswith the real tutorial nodes, plus Eclipse Cyclone DDS
- 11.8 sto find the target and land at (11.13, 7.04) m
The drone, wired up by ROS 2.
A replay of ros2 launch ugs_drone drone.launch.py, recorded off the wire by this repo's pure Python participant: 79 topics and services, 179 camera frames (42.1 MB), odometry, detections and the action's feedback. The planner flies a survey leg, spots the target, cancels the goal and retargets.
- phase
- waiting
- altitude
- 0.0 m
- speed
- 0.0 m/s
- /fly_to goal
- none
- distance to go
- battery
/detections, in the 320 x 240 image
Talking to ROS 2 without ROS 2.
week05/participant.py is a DDS participant in pure Python: SPDP and SEDP discovery, reliable RTPS with heartbeats and acknowledgements, DATA_FRAG reassembly, CDR, and ROS 2's naming and type hash conventions. Pointed at the unmodified tutorial nodes, it holds up in every direction.
- 1
ROS talker to Python listenerFast DDS
received 'Hello World: 1', 'Hello World: 2', 'Hello World: 3'
- 2
Python talker to ROS listenerFast DDS
I heard: "Hello from pure Python: 0"
- 3
Python client to ROS serviceFast DDS
{'a': 2, 'b': 3} gave {'sum': 5} in 55.91 ms
- 4
ROS clients, Python and C++ to Python serviceFast DDS
py_srvcli: Result of add_two_ints: for 7 + 35 = 42; cpp_srvcli: Sum: 42
- 5
Cyclone DDS talker to Python listenerEclipse Cyclone DDS
received 3 messages
- 1,210 of 1,210 type hashes
- RIHS01 hashes computed from the .msg, .srv and .action files match every interface installed in Jazzy.
- 1,265,625 of 1,265,625 QoS pairs
- Every combination of policies, compared with
rclpy.qos.qos_check_compatible, verdict and reason string. - 300 of 300 read back
- rclpy deserializes this repo's CDR; ours matches rclpy's except where Fast CDR leaves the alignment padding unwritten (300 of 300 messages had leftover bytes there).
Topic, service or action?
The lecture's quick check. A topic is a stream nobody answers; a service is a quick question with one answer; an action is a long job with progress you can cancel.
Take off and climb to 5 m.
Reset the odometry origin.
Send camera frames to the detector.
Switch the flight controller to position hold.
Land, with the option to abort.
QoS playground.
Offered must be at least requested. Pick a publisher and a subscription; the verdict and reason below come from a JavaScript port of week05/qos.py, which agrees with rclpy on all 1,265,625 combinations. The page's tests hold the port to the Python exactly.
Measured in ROS 2.
Twenty messages from a publisher to a subscription, for each reliability pair. The incompatible pair never matches and both sides get an incompatible QoS event.
| Publisher | Subscription | Matched | Received | QoS event | rclpy says |
|---|---|---|---|---|---|
reliable | reliable | yes | 20 of 20 | none | warning |
reliable | best effort | yes | 20 of 20 | none | warning |
best effort | reliable | no | 0 of 20 | reliability | error |
best effort | best effort | yes | 20 of 20 | none | warning |
Late joiners.
One message published, then a subscription joins. Only transient local on both sides replays it.
| Publisher | Late subscription | Received | rclpy says |
|---|---|---|---|
transient local | volatile | 0 | WARNING: Publisher liveliness is system_default and subscription liveliness is system_default; |
transient local | transient local | 1 | WARNING: Publisher liveliness is system_default and subscription liveliness is system_default; |
volatile | volatile | 0 | WARNING: Publisher liveliness is system_default and subscription liveliness is system_default; |
volatile | transient local | 0 | ERROR: Volatile publisher and transient local subscription; |
Depth against a slow reader.
300 messages at 1 kHz into a callback that takes 10 ms. Keep last drops the oldest; keep all received 300 of 300.
| History | Reliability | Received | Share | In order |
|---|---|---|---|---|
keep_last 1 | reliable | 28 of 300 | 9% | yes |
keep_last 1 | best effort | 27 of 300 | 9% | yes |
keep_last 10 | reliable | 35 of 300 | 12% | yes |
keep_last 10 | best effort | 35 of 300 | 12% | yes |
keep_last 100 | reliable | 126 of 300 | 42% | yes |
keep_last 100 | best effort | 125 of 300 | 42% | yes |
keep_all | reliable | 300 of 300 | 100% | yes |
keep_all | best effort | 300 of 300 | 100% | yes |
One slow callback, three executors.
A 50 ms timer shares a node with a subscription whose callback sleeps 300 ms; a message arrives every 500 ms for 3 s. In one mutually exclusive group the timer waits (317.8 ms worst gap). In separate groups on a MultiThreadedExecutor it keeps time (86.6 ms).
| Executor | Ticks in 3 s | Mean gap (ms) | Worst gap (ms) |
|---|---|---|---|
| SingleThreadedExecutor | 30 | 92.9 | 317.8 |
| MultiThreadedExecutor, one group | 30 | 92.9 | 341.7 |
| MultiThreadedExecutor, separate groups | 60 | 50.1 | 86.6 |
| Setup | Outcome | Round trip (ms) |
|---|---|---|
| call() in a callback, SingleThreadedExecutor | deadlock (killed after 6 s) | |
| call_async() with a done callback, SingleThreadedExecutor | returned | 2.16 |
| call() in a callback, MultiThreadedExecutor, reentrant group | returned | 8.2 |
On the wire.
Seven packets from the tutorials' captures (120 and 113 packets), dissected by week05/dissect.py. Pick a packet, then hover or tap any byte to see which field it belongs to.
Hover a byte.
300 of 300random AllTypes messages serialize like rclpy, except for alignment padding: Fast CDR skips it without writing, so rclpy's bytes carry leftover memory there.
11.1 bytespast the real CDR, on average: serialize_message returns rmw_fastrtps's size estimate as the length.
55.91 msfor the pure Python client's AddTwoInts request to the ROS tutorial service and back.
The tutorials, as they ran.
The official Jazzy publisher and subscriber and service and client tutorials, in Python and C++, built with colcon in ros2_ws/. Real terminal output.
Python publisher and subscriber
$ ros2 run py_pubsub talker
[INFO] [1791453655.322132765] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [1791453655.793275863] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [1791453656.293566002] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [1791453656.790751232] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [1791453657.289835813] [minimal_publisher]: Publishing: "Hello World: 4"
[INFO] [1791453657.790507140] [minimal_publisher]: Publishing: "Hello World: 5"
[INFO] [1791453658.290440483] [minimal_publisher]: Publishing: "Hello World: 6"
[INFO] [1791453658.789300099] [minimal_publisher]: Publishing: "Hello World: 7"
[INFO] [1791453659.293271916] [minimal_publisher]: Publishing: "Hello World: 8"
[INFO] [1791453659.789386692] [minimal_publisher]: Publishing: "Hello World: 9"$ ros2 run py_pubsub listener
[INFO] [1791453658.827121219] [minimal_subscriber]: I heard: "Hello World: 7"
[INFO] [1791453659.294193846] [minimal_subscriber]: I heard: "Hello World: 8"
[INFO] [1791453659.789361251] [minimal_subscriber]: I heard: "Hello World: 9"C++ publisher and subscriber
$ ros2 run cpp_pubsub talker
[INFO] [1791453662.295314204] [minimal_publisher]: Publishing: 'Hello, world! 0'
[INFO] [1791453662.794442358] [minimal_publisher]: Publishing: 'Hello, world! 1'
[INFO] [1791453663.296214788] [minimal_publisher]: Publishing: 'Hello, world! 2'
[INFO] [1791453663.794429471] [minimal_publisher]: Publishing: 'Hello, world! 3'
[INFO] [1791453664.300196873] [minimal_publisher]: Publishing: 'Hello, world! 4'
[INFO] [1791453664.803686898] [minimal_publisher]: Publishing: 'Hello, world! 5'
[INFO] [1791453665.358817095] [minimal_publisher]: Publishing: 'Hello, world! 6'
[INFO] [1791453665.800605722] [minimal_publisher]: Publishing: 'Hello, world! 7'
[INFO] [1791453666.294541520] [minimal_publisher]: Publishing: 'Hello, world! 8'
[INFO] [1791453666.795882821] [minimal_publisher]: Publishing: 'Hello, world! 9'
[INFO] [1791453667.294731294] [minimal_publisher]: Publishing: 'Hello, world! 10'
[INFO] [1791453667.796901255] [minimal_publisher]: Publishing: 'Hello, world! 11'
[INFO] [1791453668.294770488] [minimal_publisher]: Publishing: 'Hello, world! 12'
[INFO] [1791453668.550617175] [rclcpp]: signal_handler(SIGINT/SIGTERM)$ ros2 run cpp_pubsub listener
[INFO] [1791453667.296619829] [minimal_subscriber]: I heard: 'Hello, world! 10'
[INFO] [1791453667.798144009] [minimal_subscriber]: I heard: 'Hello, world! 11'
[INFO] [1791453668.297059478] [minimal_subscriber]: I heard: 'Hello, world! 12'
[INFO] [1791453669.285844633] [rclcpp]: signal_handler(SIGINT/SIGTERM)Python service and client
$ ros2 run py_srvcli service
[INFO] [1791453674.228579964] [minimal_service]: Incoming request
a: 2 b: 3$ ros2 run py_srvcli client 2 3
[INFO] [1791453674.295977509] [minimal_client_async]: Result of add_two_ints: for 2 + 3 = 5C++ service and client
$ ros2 run cpp_srvcli server
[INFO] [1791453678.141454767] [rclcpp]: Ready to add two ints.
[INFO] [1791453679.521176122] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [1791453679.521353770] [rclcpp]: sending back response: [5]
[INFO] [1791453680.314121896] [rclcpp]: signal_handler(SIGINT/SIGTERM)$ ros2 run cpp_srvcli client 2 3
[INFO] [1791453679.521816238] [rclcpp]: Sum: 5Read it, run it.
- week05/README.md: the pure Python DDS stack, and how each piece was checked.
- ros2_ws/: the tutorials, the drone and the lab, as ROS 2 packages.
- docs/week05/NOTES.md: the lecture notes.
- docs/week05/RESULTS.md: every number on this page.
$ colcon build --symlink-install && source install/setup.bash
$ ros2 launch ugs_drone drone.launch.py
$ ros2 launch ugs_drone drone.launch.py cancel_after_s:=2.0
$ python -m week05 docs
$ python -m week05 docs --ros