==================================================== Exercises ==================================================== This page contains four take-home exercises that reinforce the concepts from Lecture 8. Each exercise asks you to **write code from scratch** based on a specification -- no starter code is provided. All files should be created inside your ``~/enpm605_ws/src/first_pkg/`` workspace. .. dropdown:: Exercise 1 -- Periodic Logger :icon: gear :class-container: sd-border-primary :class-title: sd-font-weight-bold **Goal** Practice creating an OOP-based ROS 2 node with a timer callback, proper spin/shutdown lifecycle, and the ROS 2 logger. .. raw:: html
**Specification** Create the file ``first_pkg/periodic_logger.py`` that implements the following. 1. **``PeriodicLogger(Node)``** class: - ``__init__(self, node_name: str, period: float = 1.0)`` that calls ``super().__init__(node_name)``, stores ``_period`` and ``_count = 0``, and creates a timer with the given period. - ``_timer_callback(self) -> None``: logs ``"[] Tick at "`` using the ROS 2 logger and increments ``_count``. Retrieve the current ROS 2 time with ``self.get_clock().now().to_msg()``. 2. **``scripts/run_periodic_logger.py``** entry point: - Initialize ``rclpy``, instantiate ``PeriodicLogger`` with node name ``"periodic_logger"`` and period ``0.5``. - Wrap the spin in a ``try/except KeyboardInterrupt`` block. - Destroy the node and call ``rclpy.shutdown()`` in the ``finally`` block. 3. Register the entry point in ``setup.py``: .. code-block:: python 'periodic_logger = scripts.run_periodic_logger:main', **Expected behavior** Running ``ros2 run first_pkg periodic_logger`` should produce approximately two log lines per second: .. code-block:: text [INFO] [] [periodic_logger]: [0] Tick at sec: 1741200001 nanosec: 123456789 [INFO] [] [periodic_logger]: [1] Tick at sec: 1741200001 nanosec: 623456789 [INFO] [] [periodic_logger]: [2] Tick at sec: 1741200002 nanosec: 123456789 **Verification commands** .. code-block:: console ros2 node list # should show /periodic_logger ros2 node info /periodic_logger ros2 topic hz /rosout # should show ~2 Hz .. dropdown:: Exercise 2 -- String Publisher :icon: gear :class-container: sd-border-primary :class-title: sd-font-weight-bold **Goal** Practice creating a publisher that sends ``std_msgs/String`` messages, registering it as a node executable, and inspecting it with ROS 2 introspection tools. .. raw:: html
**Specification** Create the file ``first_pkg/string_publisher.py`` that implements the following. 1. **``StringPublisher(Node)``** class: - ``__init__(self, node_name: str)`` that calls ``super().__init__(node_name)``, creates a publisher on the topic ``"greeting"`` with message type ``std_msgs/msg/String`` and queue depth ``10``, initializes ``_count = 0``, creates a ``String()`` message object, and creates a timer with period ``1.0`` s. - ``_timer_callback(self) -> None``: sets ``self._message.data`` to ``f"Hello from ROS 2 -- message {self._count}"``, publishes the message, logs it with the ROS 2 logger, and increments ``_count``. 2. **Entry point** ``scripts/run_string_publisher.py``: - Standard lifecycle: ``rclpy.init()``, instantiate ``StringPublisher("string_publisher")``, ``rclpy.spin()``, ``destroy_node()``, ``rclpy.shutdown()``. 3. Register in ``setup.py``: .. code-block:: python 'string_publisher = scripts.run_string_publisher:main', **Verification** After building and running, in a second terminal: .. code-block:: console ros2 topic list -t # /greeting [std_msgs/msg/String] ros2 topic echo /greeting # prints each message ros2 topic hz /greeting # should show ~1.0 Hz rqt_graph # node -> topic visible .. dropdown:: Exercise 3 -- String Subscriber :icon: gear :class-container: sd-border-primary :class-title: sd-font-weight-bold **Goal** Practice creating a subscriber with a named callback, running both publisher and subscriber simultaneously, and verifying end-to-end communication. .. raw:: html
**Specification** Create the file ``first_pkg/string_subscriber.py`` that implements the following. 1. **``StringSubscriber(Node)``** class: - ``__init__(self, node_name: str)`` that calls ``super().__init__(node_name)``, creates a subscription on topic ``"greeting"`` with message type ``std_msgs/msg/String``, queue depth ``10``, and a named callback. - ``_subscriber_callback(self, msg: String) -> None``: logs ``f"Received: {msg.data}"`` using the ROS 2 logger. 2. **Entry point** ``scripts/run_string_subscriber.py``: - Standard lifecycle matching Exercise 2. 3. Register in ``setup.py``: .. code-block:: python 'string_subscriber = scripts.run_string_subscriber:main', **Expected behavior** Run the publisher from Exercise 2 in Terminal 1 and this subscriber in Terminal 2: .. code-block:: text [INFO] [] [string_subscriber]: Received: Hello from ROS 2 -- message 0 [INFO] [] [string_subscriber]: Received: Hello from ROS 2 -- message 1 [INFO] [] [string_subscriber]: Received: Hello from ROS 2 -- message 2 **Verification** .. code-block:: console ros2 topic info /greeting -v # shows publisher and subscriber counts rqt_graph # both nodes connected via /greeting .. dropdown:: Exercise 4 -- QoS Mismatch Investigation :icon: gear :class-container: sd-border-primary :class-title: sd-font-weight-bold **Goal** Observe the effects of QoS incompatibility first-hand and use introspection tools to diagnose the problem. .. raw:: html
**Specification** This exercise has two parts. **Part A -- Create a BEST_EFFORT publisher** Modify a copy of ``string_publisher.py`` saved as ``first_pkg/qos_publisher.py``. Change the publisher to use an explicit ``QoSProfile`` with ``ReliabilityPolicy.BEST_EFFORT``, ``DurabilityPolicy.VOLATILE``, ``HistoryPolicy.KEEP_LAST``, and ``depth=10``. Keep the topic name ``"qos_test"`` and message type ``std_msgs/msg/String``. Register as ``'qos_publisher = ...'`` in ``setup.py``. **Part B -- Create a RELIABLE subscriber** Create ``first_pkg/qos_subscriber.py`` with a ``QoSProfile`` using ``ReliabilityPolicy.RELIABLE`` and all other policies at their defaults. Subscribe to ``"qos_test"``. Register as ``'qos_subscriber = ...'`` in ``setup.py``. **Investigation steps** 1. Run both nodes simultaneously. 2. Observe that the subscriber callback is never invoked. 3. Run ``ros2 topic info /qos_test -v`` and note the QoS printed for publisher and subscriber. 4. Run ``ros2 doctor --report | grep qos`` and record the output. 5. Fix the mismatch by changing the subscriber to ``ReliabilityPolicy.BEST_EFFORT`` and confirm that messages are now received. **Written reflection (include as a comment block at the top of qos_subscriber.py)** Answer these questions in 3-5 sentences: - What did ``ros2 topic info -v`` show before and after the fix? - Why does DDS produce no error when QoS is incompatible? - In what real-world scenario would a QoS mismatch cause a hard-to-debug failure?