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.

/odometry /camera/image /detections /odometry /set_mode, /arm /fly_to camera_driver15 Hz images detectorfinds the orange target plannerclient of everything px4_bridgeflight controller sim
topicserviceaction A dot is a message. Camera frames are drawn at the driver's 15 Hz.
Top down, meters. Dashed: the mission's waypoints. Ring: the target at (11, 7).
0.0 s
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. 1

      ROS talker to Python listenerFast DDS

      received 'Hello World: 1', 'Hello World: 2', 'Hello World: 3'

    2. 2

      Python talker to ROS listenerFast DDS

      I heard: "Hello from pure Python: 0"

    3. 3

      Python client to ROS serviceFast DDS

      {'a': 2, 'b': 3} gave {'sum': 5} in 55.91 ms

    4. 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. 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.

    1. Take off and climb to 5 m.

    2. Reset the odometry origin.

    3. Send camera frames to the detector.

    4. Switch the flight controller to position hold.

    5. 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.

    Publisher
    Subscription

    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.

    PublisherSubscriptionMatchedReceivedQoS eventrclpy says
    reliablereliableyes20 of 20nonewarning
    reliablebest effortyes20 of 20nonewarning
    best effortreliableno0 of 20reliabilityerror
    best effortbest effortyes20 of 20nonewarning

    Late joiners.

    One message published, then a subscription joins. Only transient local on both sides replays it.

    PublisherLate subscriptionReceivedrclpy says
    transient localvolatile0WARNING: Publisher liveliness is system_default and subscription liveliness is system_default;
    transient localtransient local1WARNING: Publisher liveliness is system_default and subscription liveliness is system_default;
    volatilevolatile0WARNING: Publisher liveliness is system_default and subscription liveliness is system_default;
    volatiletransient local0ERROR: 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.

    HistoryReliabilityReceivedShareIn order
    keep_last 1reliable28 of 3009%yes
    keep_last 1best effort27 of 3009%yes
    keep_last 10reliable35 of 30012%yes
    keep_last 10best effort35 of 30012%yes
    keep_last 100reliable126 of 30042%yes
    keep_last 100best effort125 of 30042%yes
    keep_allreliable300 of 300100%yes
    keep_allbest effort300 of 300100%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).

    Drawn from the experiment's schedule; the tick counts and gaps in the table are measured.
    ExecutorTicks in 3 sMean gap (ms)Worst gap (ms)
    SingleThreadedExecutor3092.9317.8
    MultiThreadedExecutor, one group3092.9341.7
    MultiThreadedExecutor, separate groups6050.186.6
    Calling a service from inside a callback.
    SetupOutcomeRound trip (ms)
    call() in a callback, SingleThreadedExecutordeadlock (killed after 6 s)
    call_async() with a done callback, SingleThreadedExecutorreturned2.16
    call() in a callback, MultiThreadedExecutor, reentrant groupreturned8.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

        talkerpy_pubsub
        $ 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"
        listenerpy_pubsub
        $ 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

        talkercpp_pubsub
        $ 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)
        listenercpp_pubsub
        $ 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

        servicepy_srvcli
        $ ros2 run py_srvcli service
        [INFO] [1791453674.228579964] [minimal_service]: Incoming request
        a: 2 b: 3
        clientpy_srvcli
        $ ros2 run py_srvcli client 2 3
        [INFO] [1791453674.295977509] [minimal_client_async]: Result of add_two_ints: for 2 + 3 = 5

        C++ service and client

        servercpp_srvcli
        $ 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)
        clientcpp_srvcli
        $ ros2 run cpp_srvcli client 2 3
        [INFO] [1791453679.521816238] [rclcpp]: Sum: 5

        Read it, run it.

        ROS 2 Jazzyros2_ws
        $ 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
        Pure Pythonpython -m week05
        $ python -m week05 docs
        $ python -m week05 docs --ros