Feedback Control

This document explains how to implement feedback control in Python in a ROS2 Humble environment and create a program to control a mobile robot. The node publishes geometry_msgs/Twist type messages to the /kachaka/manual_control/cmd_vel topic and subscribes to nav_msgs/Odometry type messages from the /kachaka/odometry/odometry topic.

Learning Objectives

By the end of this lesson, you will be able to:

  • Understand the concept of feedback control and how it differs from feedforward control
  • Use sensor data (odometry) to control robot movement
  • Implement position-based and angle-based control
  • Create control loops that respond to the robot’s actual state

1. Program Overview

This program demonstrates feedback control by:

  • Controlling a mobile robot with feedback control
  • Using odometry data to obtain the robot’s current position and orientation
  • Controlling the robot’s movement based on target positions and angles
  • Implementing basic movements such as straight, rotate, and stop with position feedback

2. What is Feedback Control

Feedback control is a control method that measures the output of the control target and performs control based on the difference (error) from the target value. It uses sensor data to understand the current state and performs control to approach the target state.

In simple terms: Feedback control is like driving a car - you continuously look at where you are, compare it to where you want to go, and adjust your steering and speed accordingly. This allows the system to correct for disturbances and uncertainties that feedforward control cannot handle.

Features of Feedback control

  • Error Detection: Detects the difference between the target value and the actual value
  • Error Correction: Determines control input to correct the detected error
  • Adaptability: Can respond to unexpected disturbances
  • Precision: Can achieve higher accuracy than feedforward control

When to use Feedback Control:

  • When precise positioning is required
  • In environments with uncertainties or disturbances
  • When the robot needs to adapt to changing conditions
  • For tasks that require monitoring the robot’s actual state

Difference from Feedforward control

Feedforward control performs control based only on input without measuring the output of the control target. On the other hand, feedback control measures the output and performs control based on the difference from the target value.

3. Prerequisites

  • ROS2 Humble is installed
  • Python3 is available
  • ROS2 workspace (e.g., ~/ros2_ws) is set up

Required Packages

  • rclpy: ROS2 Python client library
  • geometry_msgs: Package for handling geometric messages such as position and velocity
  • nav_msgs: Package for handling navigation-related messages
  • tf_transformations: Library for coordinate transformations and quaternion operations
  • numpy: Library for numerical calculations

4. Creating a Package

First, create a package in the src directory of the ROS2 workspace.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python kachaka_feedback_control --dependencies rclpy geometry_msgs nav_msgs tf_transformations numpy --node-name kachaka_feedback_control

This command generates a package with the following structure:

kachaka_feedback_control/
├── resource/
│   └── kachaka_feedback_control
├── kachaka_feedback_control/
│   ├── __init__.py
│   └── kachaka_feedback_control.py
├── test/
├── package.xml
└── setup.py

5. Node Implementation

Create a kachaka_feedback_control.py file with the following content in the kachaka_feedback_control directory within the kachaka_feedback_control package.

kachaka_feedback_control.py

#!/usr/bin/env python3
import math
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from nav_msgs.msg import Odometry
import numpy as np
from tf_transformations import euler_from_quaternion

# Imports for QoS settings
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy

class KachakaFeedbackControl(Node):
    def __init__(self):
        super().__init__('kachaka_feedback_control')

        # Define a QoS profile with BEST_EFFORT reliability
        # QoS (Quality of Service) settings control how messages are delivered
        # BEST_EFFORT means messages may be dropped if the system is busy
        # This is appropriate for odometry data where real-time performance is more
        # important than guaranteed delivery of every message
        qos_profile = QoSProfile(
            history=QoSHistoryPolicy.KEEP_LAST,  # Keep only the last N messages
            depth=10,  # Keep the last 10 messages in the queue
            reliability=QoSReliabilityPolicy.BEST_EFFORT  # Allow message loss for speed
        )

        # Publisher for velocity commands
        self.cmd_vel_pub = self.create_publisher(Twist, '/kachaka/manual_control/cmd_vel', 10)

        # Subscriber for odometry data with BEST_EFFORT QoS
        self.odom_sub = self.create_subscription(
            Odometry,
            '/kachaka/odometry/odometry',
            self.callback_odom,
            qos_profile
        )

        # Initialize position variables to None
        # These will be set when we receive the first odometry message
        self.x = None   # X position in meters
        self.y = None   # Y position in meters
        self.yaw = None # Orientation angle in radians

        # Wait for the first odometry message to be received
        # This is important because we need initial position data before we can
        # calculate movements. Without this wait, the control methods would fail
        # because they depend on knowing the starting position.
        while self.x is None or self.y is None or self.yaw is None:
            # Process callbacks to receive the odometry message
            rclpy.spin_once(self, timeout_sec=0.1)

    def callback_odom(self, msg):
        """
        Callback function that is called whenever a new odometry message is received.
        
        Odometry provides the robot's estimated position and orientation based on
        wheel encoders and other sensors. This callback updates our knowledge of
        where the robot currently is.
        
        Parameters:
            msg: Odometry message containing pose (position + orientation) information
        """
        # Extract the X coordinate from the odometry message
        # The message structure is: msg.pose.pose.position.x
        self.x = msg.pose.pose.position.x
        # Extract the Y coordinate
        self.y = msg.pose.pose.position.y
        # Extract the yaw (rotation around Z-axis) from the quaternion
        # Quaternions are used to represent 3D rotations, but we only need the yaw angle
        self.yaw = self.get_yaw_from_quaternion(msg.pose.pose.orientation)

    def get_yaw_from_quaternion(self, quaternion):
        """
        Convert a quaternion to Euler angles and extract the yaw angle.
        
        Quaternions are a way to represent 3D rotations without gimbal lock issues.
        For a mobile robot moving on a flat surface, we only need the yaw angle
        (rotation around the vertical Z-axis), which tells us which direction
        the robot is facing.
        
        Parameters:
            quaternion: Quaternion object with x, y, z, w components
        
        Returns:
            yaw: Rotation angle around Z-axis in radians [0 to 2π]
        """
        # Convert quaternion to Euler angles (roll, pitch, yaw)
        # The function returns a tuple: (roll, pitch, yaw)
        e = euler_from_quaternion((quaternion.x, quaternion.y, quaternion.z, quaternion.w))
        # Return only the yaw angle (index 2)
        return e[2]

    def go_straight(self, distance, velocity=0.3):
        """
        Move straight for the specified distance using feedback control.
        
        This method uses the robot's current position (from odometry) to determine
        when it has traveled the target distance. This is feedback control because
        we're measuring the output (actual distance traveled) and adjusting until
        we reach the target.
        
        Parameters:
            distance: Target distance to travel in meters
            velocity: Speed at which to travel [m/s] (default: 0.3 m/s)
        """
        vel = Twist()
        # Record the starting position
        # We'll compare the current position to this to calculate distance traveled
        x0 = self.x
        y0 = self.y
        
        # Continue moving until we've traveled the target distance
        # Calculate Euclidean distance: sqrt((x-x0)² + (y-y0)²)
        # This gives us the straight-line distance from the starting point
        while np.sqrt((self.x - x0)**2 + (self.y - y0)**2) < distance:
            # Set velocity: forward motion, no rotation
            vel.linear.x = velocity
            vel.angular.z = 0.0
            # Publish the velocity command
            self.cmd_vel_pub.publish(vel)
            # Process callbacks to update position from odometry
            rclpy.spin_once(self, timeout_sec=0.1)
        
        # Stop when we've reached the target distance
        self.stop()

    def turn_right(self, angle_degree, yawrate=-0.5):
        """
        Turn right by the specified angle using feedback control.
        
        This method measures the actual rotation angle and continues turning until
        the target angle is reached. The angle difference calculation handles
        the wrap-around at 2π radians (360 degrees).
        
        Parameters:
            angle_degree: Target rotation angle in degrees
            yawrate: Rotation speed [rad/s] (default: -0.5 rad/s, negative = right turn)
        """
        vel = Twist()
        # Record the starting orientation
        yaw0 = self.yaw
        # Convert target angle from degrees to radians
        target_angle = math.radians(angle_degree)
        
        # Continue turning until we've rotated the target angle
        # The angle difference calculation uses atan2 to handle wrap-around correctly
        # This ensures the calculation works even if we cross the 0/2π boundary
        while abs(math.atan2(math.sin(self.yaw - yaw0), math.cos(self.yaw - yaw0))) < target_angle:
            # Set velocity: no forward motion, rotate right (negative angular velocity)
            vel.linear.x = 0.0
            vel.angular.z = yawrate
            # Publish the velocity command
            self.cmd_vel_pub.publish(vel)
            # Process callbacks to update orientation from odometry
            rclpy.spin_once(self, timeout_sec=0.1)
        
        # Stop when we've reached the target angle
        self.stop()

    def turn_left(self, angle_degree, yawrate=0.5):
        """
        Turn left by the specified angle (in degrees) using a positive yaw rate.
        """
        vel = Twist()
        yaw0 = self.yaw
        target_angle = math.radians(angle_degree)
        # Normalize the angle difference using atan2
        while abs(math.atan2(math.sin(self.yaw - yaw0), math.cos(self.yaw - yaw0))) < target_angle:
            vel.linear.x = 0.0
            vel.angular.z = yawrate
            self.cmd_vel_pub.publish(vel)
            rclpy.spin_once(self, timeout_sec=0.1)
        self.stop()

    def stop(self):
        """Stop the robot by publishing zero velocities."""
        vel = Twist()
        vel.linear.x = 0.0
        vel.angular.z = 0.0
        self.cmd_vel_pub.publish(vel)

def main(args=None):
    rclpy.init(args=args)
    kachaka_feedback_control = KachakaFeedbackControl()

    try:
        # Example movement commands: go straight, turn left, then turn right
        kachaka_feedback_control.go_straight(1.0)
        kachaka_feedback_control.turn_left(90)
        kachaka_feedback_control.turn_right(90)
    finally:
        kachaka_feedback_control.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

6. Editing setup.py

If you are not using –node-name, set the entry point in setup.py at the package root.

    entry_points={
        'console_scripts': [
            'kachaka_feedback_control = kachaka_feedback_control.kachaka_feedback_control:main'
        ],

7. Building and Running the Package

Building the Package

cd ~/ros2_ws
colcon build --packages-select kachaka_feedback_control

Loading Environment Settings

source install/setup.bash

Running the Node

ros2 run kachaka_feedback_control kachaka_feedback_control

8. Detailed Code Explanation

Classes and Methods

  • KachakaFeedbackControl: Main node class
    • __init__: Node initialization, publisher and subscriber setup
    • callback_odom: Callback function when receiving odometry data
    • get_yaw_from_quaternion: Function to get yaw angle from quaternion
    • go_straight: Function to move straight for a specified distance
    • turn_right: Function to rotate right by a specified angle
    • turn_left: Function to rotate left by a specified angle
    • stop: Function to stop the robot

Control Parameters

  • distance: Distance to move straight [m]
  • velocity: Forward speed [m/s]
  • angle_degree: Angle to rotate [degrees]
  • yawrate: Rotation speed [rad/s]

Movement Sequence

The program executes the following movement sequence:

  1. Move straight 1.0 meters
  2. Rotate left 90 degrees
  3. Rotate right 90 degrees

Implementation Points of Feedback Control

  1. Error Calculation:

    • Calculate the difference (distance) between target position and current position
    • Calculate the difference (angle) between target direction and current orientation
  2. Comparison with Target Value:

    • The go_straight method calculates the distance between current position and starting position and compares it with the target distance
    • The turn_right and turn_left methods calculate the difference between current yaw angle and starting yaw angle and compare it with the target angle
  3. Target Reaching Determination:

    • Determine that the target has been reached when the error in distance and angle is below the tolerance
    • Stop the robot after reaching the target
  4. QoS Profile Settings:

    • Set a QoS profile with BEST_EFFORT reliability for the odometry data subscriber
    • This allows for data delay or loss and prioritizes real-time performance

Exercises

Moving to Target Position

Let’s implement a function to move the robot to a specified coordinate using feedback control.

Triangular Trajectory

Let’s implement a function to make the robot draw a square trajectory using feedback control.

Content

Try creating your favorite trajectory!