Exercises#
This page contains four take-home exercises that reinforce the concepts from Lecture 11. 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/ workspace
in the appropriate packages (frame_demo, tf2_demo, or
robot_control_demo).
Exercise 1 – Quaternion Utility Library
Goal
Build a Python module that converts between orientation
representations and verify it against scipy.spatial.transform.
Specification
Create the file frame_demo/frame_demo/quat_utils.py that
implements the following functions.
``axis_angle_to_quaternion(axis, angle)``:
Input: a 3-element unit vector
axisand a rotationanglein radians.Output: a tuple
(w, x, y, z)using the half-angle formula:\[w = \cos\!\left(\frac{\theta}{2}\right), \quad (x, y, z) = \sin\!\left(\frac{\theta}{2}\right) \cdot \mathbf{u}\]Raise
ValueErrorif the axis is not a unit vector (tolerance:1e-6).
``quaternion_to_euler(w, x, y, z)``:
Input: a unit quaternion
(w, x, y, z).Output: a tuple
(roll, pitch, yaw)in radians.Use the standard ZYX (Tait-Bryan) convention.
``quaternion_multiply(q1, q2)``:
Input: two quaternions as
(w, x, y, z)tuples.Output: the Hamilton product
q1 * q2as a(w, x, y, z)tuple.
``normalize_quaternion(w, x, y, z)``:
Input: a quaternion (not necessarily unit).
Output: the corresponding unit quaternion.
Raise
ValueErrorif the magnitude is zero.
Write a main() function that:
Computes the quaternion for a \(90°\) rotation about the \(x\)-axis using
axis_angle_to_quaternion.Computes the quaternion for a \(90°\) rotation about the \(z\)-axis.
Composes them (z-rotation applied first, then x-rotation) using
quaternion_multiply.Converts the result to Euler angles.
Prints all intermediate and final values.
Verifies the result against
scipy.spatial.transform.Rotationand printsPASSorFAIL.
Verification
cd ~/enpm605_ws && colcon build --symlink-install --packages-select frame_demo
source install/setup.bash
ros2 run frame_demo quat_utils
Exercise 2 – Static Transform Broadcaster Node
Goal
Write a ROS 2 node that publishes a static transform from
base_link to a custom sensor frame using parameters loaded
from a YAML file.
Specification
Create frame_demo/frame_demo/sensor_frame_publisher.py.
``SensorFramePublisher(Node)`` class:
__init__: declares the following parameters:parent_frame(string, default"base_link")child_frame(string, default"sensor_link")translation(double array, default[0.0, 0.0, 0.0])rotation_rpy(double array, default[0.0, 0.0, 0.0])
Converts the roll-pitch-yaw values to a quaternion using
scipy.spatial.transform.Rotation.from_euler("xyz", [r, p, y]).Creates a
StaticTransformBroadcasterand publishes the transform once.Logs the published transform at
infolevel.
YAML parameter file
config/sensor_frame.yaml:/sensor_frame_publisher: ros__parameters: parent_frame: "base_link" child_frame: "camera_link" translation: [0.1, 0.0, 0.25] rotation_rpy: [0.0, -0.2618, 0.0]
Launch file
launch/sensor_frame.launch.py:Starts the
sensor_frame_publishernode with the YAML parameter file.
Register the entry point in
setup.pyand install the config and launch directories.
Expected behavior
The node starts, publishes the static transform, and keeps running.
ros2 run tf2_ros tf2_echo base_link camera_linkshows the transform.ros2 run rqt_tf_tree rqt_tf_tree --force-discovershowsbase_link→camera_linkin the tree.
Verification
cd ~/enpm605_ws && colcon build --symlink-install --packages-select frame_demo
source install/setup.bash
# Terminal 1
ros2 launch frame_demo sensor_frame.launch.py
# Terminal 2
ros2 run tf2_ros tf2_echo base_link camera_link
# Terminal 3
ros2 run rqt_tf_tree rqt_tf_tree --force-discover
Exercise 3 – Transform Listener and Logger
Goal
Write a node that periodically looks up the transform between two frames and logs the position and orientation (as Euler angles).
Specification
Create frame_demo/frame_demo/frame_logger.py.
``FrameLogger(Node)`` class:
__init__: declares two parameters:target_frame(string, default"odom")source_frame(string, default"base_link")
Creates a
Bufferand aTransformListener.Creates a timer at 1 Hz.
Timer callback
_timer_callback(self):Calls
self._tf_buffer.lookup_transform(target, source, rclpy.time.Time(), timeout=Duration(seconds=0.1)).Extracts the translation
(x, y, z).Converts the quaternion to Euler angles using
scipy.spatial.transform.Rotation.Logs translation and Euler angles (degrees) at
infolevel.Catches
TransformExceptionand logs a warning instead.
Expected behavior
With the ROSbot simulation running, the node logs the robot’s pose in the
odomframe once per second.Moving the robot (e.g., with
teleop_twist_keyboard) shows the logged values changing.
Verification
# Terminal 1
ros2 launch rosbot_gazebo empty_world.launch.py
# Terminal 2
cd ~/enpm605_ws && colcon build --symlink-install --packages-select frame_demo
source install/setup.bash
ros2 run frame_demo frame_logger
# Terminal 3
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -p stamped:=true
Exercise 4 – Proportional Go-to-Goal Controller
Goal
Implement a two-phase proportional controller that drives a differential-drive robot to a goal pose (position + final heading) using odometry feedback.
Specification
Create robot_control_demo/robot_control_demo/goto_goal.py.
``GoToGoal(Node)`` class:
__init__: declares the following parameters with defaults:goal_x(double,3.0)goal_y(double,2.0)goal_heading(double,1.57) — desired final yaw in radiansk_rho(double,0.5) — linear gaink_alpha(double,1.0) — angular gain (drive phase)k_heading(double,1.5) — angular gain (rotate phase)position_tolerance(double,0.05) — metersheading_tolerance(double,0.05) — radians
Creates a publisher on
/cmd_vel(geometry_msgs/msg/TwistStamped).Subscribes to
/odometry/filtered(nav_msgs/msg/Odometry).Creates a timer at 20 Hz for the control loop.
Tracks the current phase:
DRIVEorROTATE.
Odometry callback
_odom_callback(self, msg):Extracts
x,yfrommsg.pose.pose.position.Extracts yaw from the quaternion using
scipy.spatial.transform.Rotation.
Control loop
_control_loop(self):DRIVE phase:
Compute distance error: \(\rho = \sqrt{(x_g - x)^2 + (y_g - y)^2}\)
Compute heading error to goal: \(\alpha = \text{atan2}(y_g - y, x_g - x) - \psi\)
Normalize \(\alpha\) to \([-\pi, \pi]\).
Set
linear.x = k_rho * rhoandangular.z = k_alpha * alpha.Clamp linear velocity to
[0, 0.5]m/s and angular velocity to[-1.0, 1.0]rad/s.If \(\rho <\)
position_tolerance, switch toROTATEphase.
ROTATE phase:
Compute heading error: \(e_\psi = \psi_{\text{goal}} - \psi\)
Normalize to \([-\pi, \pi]\).
Set
linear.x = 0.0andangular.z = k_heading * e_psi.Clamp angular velocity to
[-1.0, 1.0]rad/s.If \(|e_\psi| <\)
heading_tolerance, publish a zero-velocity command, log"Goal reached!", and stop the timer.
Register the entry point in
setup.py.
Expected behavior
The robot drives toward the goal position with a smooth curved trajectory (simultaneous linear + angular motion).
Once within
position_toleranceof the goal, the robot stops translating and rotates in place to the desired heading.Velocities decrease as the robot approaches the goal.
Verification
# Terminal 1
ros2 launch rosbot_gazebo empty_world.launch.py
# Terminal 2
cd ~/enpm605_ws && colcon build --symlink-install --packages-select robot_control_demo
source install/setup.bash
ros2 run robot_control_demo goto_goal --ros-args \
-p goal_x:=3.0 -p goal_y:=2.0 -p goal_heading:=1.57
# Optional: visualize in RViz2 with Odometry display on /odometry/filtered