Turtlebot_3 Extreme Latency Trouble With Following Input

Working on the rosject ROS2 Basics in 5 Days, third module. The simulation runs flawlessly. When I get to the real robot, I adjust my laser indexes to account for the change in /scan. When I launch my main.launch.py, sometimes the turtlebot just says still with a logger, “SENDING REQUEST” (within wall_following).

    def send_request(self):
        self.get_logger().info('SENDING REQUEST...')
        self.response = self.client.call(self.req)
        

Other times, it will actually start the wall_finding service. I have loggers inserted throughout the code to let me know where it is. Sometimes turtlebot gets to the first motion of the wall_finder logic and begins to rotate to align the front with the closest wall. Sometimes, it gets aligned and outputs, “Aligned status True.” Now, if it outputs that message, I know it shouldn’t be rotating because I have a command telling it to stop angular movement, yet it still rotates.

if aligned == False and self.front_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif aligned == False and self.front_laser*0.90 > self.laser_min:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            elif aligned == False and self.front_laser*0.90 < self.laser_min:
                self.movement.angular.z = 0.0
                aligned = True
                self.get_logger().info('Aligned Status "%s"' % str(aligned))

The very next command it has is to approach the wall with linear.x so I know its not within this section of code because it has no linear movement. Turtlebot is still spinning.

if aligned == True and approach == False and self.front_laser > 0.3:
                self.movement.linear.x = 0.05
            elif aligned == True and approach == False and self.front_laser <= 0.3:
                self.movement.linear.x = 0.0
                approach = True
                self.get_logger().info('Approach Status "%s"' % str(approach))

When this happens, I enter ctrl+C into the terminal to stop the program, yet turtlebot is still spinning. Sometimes it will start a linear movement and drive across the arena and wont respond to joystick input for a good while.

The cameras seem to have a connection problem as well. Last night I consistently saw one or both cameras blank out and attempt to reconnect during my reserved time. Today, one camera is just frozen.

It would seem that the turtlebot isn’t processing commands within a reasonable time of receiving them. When I log into the project, I assume I’m logged into a virtual session on a server somewhere. Is the server close to the turtlebot?

45 minutes of submitting the same code it worked once. The wall alignment worked fine, the wall following worked fine, and the odometry recorded as it should, only to hit an obstacle placed near the wall and have the turtlebot get hung up and stall out. Is this the expected behavior?

Hi @pharva ,

I can understand your issue, I have experienced the same (long back, when I was doing the same project).

The problem is not actually in your service code logic, but it is in the publisher declaration for /cmd_vel. The queue_size for the QoS parameter should be a reasonably big number but not too big, try something between 10 and 100 while checking the real robot behavior. Try a value of say, 50 or 80.
This is because Python is quite slow at processing compared to C++.

Try this and let me know if this solved your issue.

Regards,
Girish

@girishkumar.kannan Thank you for your reply. What you’re saying makes a lot of sense and seems to be exactly what’s happening. However, your suggestion didn’t seem to work. Let me confirm that I’ve adjusted the correct value:

Inside of wall_finder:

class WallFinderService(Node):

    def __init__(self):
        super().__init__('find_wall')
        self.reentrant_group_1 = ReentrantCallbackGroup()

        self.srv = self.create_service(FindWall, 'find_wall', self.custom_service_callback, callback_group=self.reentrant_group_1)
        self.publisher_ = self.create_publisher(Twist, 'cmd_vel', 80)
        self.subscriber = self.create_subscription(LaserScan, 'scan', self.laser_scan_callback, QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE), callback_group=self.reentrant_group_1)

self.publisher_ = self.create_publisher(Twist, ‘cmd_vel’, 80) <–{changed to “80” from “10”}

I tried 50, 65, 80, and 15. There was no change in the robot’s behavior. The IDE showed me that the robot was reaching milestones that should trigger different velocities. For example:

[wall_finder-1] [INFO] [1710357529.555517760] [find_wall]: Aligned Status “True”
[wall_finder-1] [INFO] [1710357559.237756629] [find_wall]: Approach Status “True”

## Move logic
            if aligned == False and self.front_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif aligned == False and self.front_laser*0.90 > self.laser_min:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            elif aligned == False and self.front_laser*0.90 < self.laser_min:
                self.movement.angular.z = 0.0
                aligned = True
                self.get_logger().info('Aligned Status "%s"' % str(aligned))
            
            if aligned == True and approach == False and self.front_laser > 0.3:
                self.movement.linear.x = 0.05
            elif aligned == True and approach == False and self.front_laser <= 0.3:
                self.movement.linear.x = 0.0
                approach = True
                self.get_logger().info('Approach Status "%s"' % str(approach))

When Alignment Status is True, there should be no rotation, but the robot continued to rotate before it started moving forward inside the second part of the logic.

When the Approach Status is True, the forward motion should stop, but the robot just continued into the wall.

Any other thoughts?

Hi @pharva ,

I am not sure why that did not work out for you. Probably something changed in the hardware configuration. That should have worked.

I do have another solution though, a directly opposite idea: set the /cmd_vel topic QoS queue_size to exactly 1.

self.publisher_ = self.create_publisher(Twist, ‘cmd_vel’, 1)

The reason for this change is that we are configuring it to not have any other messages in the queue, except the most recent message only.

This could work. If this does not, then you might need to take a deeper look into your service code and try to eliminate if there is anything else that publishes into angular speed.

The better option to debug this on the real robot is with 3 terminals open along with the real robot stream. So you have 4 distinct windows side-by-side like 4 quadrants of a graph.
Terminal 1: echo /cmd_vel
Terminal 2: wall finder service server
Terminal 3: wall finder service call

Start the wall finder service and echo cmd_vel together. Make sure the robot is in a favorable position for wall finder service to succeed, then make the service request on the third terminal. Since you have debug messages displayed on your service server, you would know exactly when the service server sets the angular speed to 0 and how the robot speed has changed by looking at your cmd_vel echo.

Let me know if that works for you [the above idea of setting QoS queue_size to 1].

Regards,
Girish

Hi @girishkumar.kannan,
I set up the terminal configuration as you suggested, and I could see the published velocities as the robot stepped through the code. As soon as “aligned” = True, /cmd_vel switched from

image

to

image

as it should.

The robot continues to rotate with an angular velocity. I’ve tried setting the queue_size to 1, 50, 100, 150, and 250 (!!!). No change. I’ve adjusted it in the wall_finder.py and wall_following.py. No change. I’ve tried publishing to the topic immediately after setting linear.z = 0.0. No change.

image

I’m not sure how to approach this. Does the “while” loop output too many messages? If I’d adjusted the queue size to 1, then shouldn’t the robot only be acting on the very last command it received?

Here is my wall_finder.py

from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
from findwall.srv import FindWall
from rclpy.qos import ReliabilityPolicy, QoSProfile
import rclpy
from rclpy.node import Node
import math
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup

# To launch the simulation:
    # source ~/simulation_ws/install/setup.bash
    # ros2 launch turtlebot3_gazebo main_turtlebot3_lab.launch.xml

# To control simualted robot:
    # ros2 run teleop_twist_keyboard teleop_twist_keyboard

class WallFinderService(Node):

    def __init__(self):
        super().__init__('find_wall')
        self.reentrant_group_1 = ReentrantCallbackGroup()

        self.srv = self.create_service(FindWall, 'find_wall', self.custom_service_callback, callback_group=self.reentrant_group_1)
        self.publisher_ = self.create_publisher(Twist, 'cmd_vel', 10)
        self.subscriber = self.create_subscription(LaserScan, 'scan', self.laser_scan_callback, QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE), callback_group=self.reentrant_group_1)
        
        self.response = FindWall.Response()
        self.movement = Twist()


    def custom_service_callback(self, request, response):
        wall_found = False
        aligned = False
        approach = False
        ready = False

        while wall_found == False: 
            # Calculated Variables
            self.front_laser_diff = self.front_laser_10 - self.front_laser_350
            self.right_laser_diff = self.right_laser_285 - self.right_laser_255
        
            self.theta_right_wall = math.degrees(math.atan2(self.right_laser_285 - self.right_laser_255*math.cos(math.radians(15)),self.right_laser*math.sin(math.radians(15))))
            self.theta_front_wall = math.degrees(math.atan2(self.front_laser_350-self.front_laser*math.cos(math.radians(10)),self.front_laser*math.sin(math.radians(10))))

            self.correction_factor_front = abs(self.theta_front_wall)/90
            self.correction_factor_right = abs(self.theta_right_wall)/90

            self.front_laser_avg = (self.front_laser+self.front_laser_10+self.front_laser_350)/3
            self.right_laser_avg = (self.right_laser+self.right_laser_285+self.right_laser_255)/3


            # print laser readings
            #self.get_logger().info(
            #f"Min Laser: {self.laser_min:.3f},"
            #f"R Laser {self.right_laser:.3f},"
            #)
            #self.get_logger().info('Min Range "%s"' % str(self.laser_min), 'Front Range "%s"' % str(self.front_laser))
            #self.get_logger().info('Front Range "%s"' % str(self.front_laser))
            #self.get_logger().info('Front 10 Range "%s"' % str(self.front_laser_10))
            #self.get_logger().info('Front 350 Range "%s"' % str(self.front_laser_350))
            #self.get_logger().info('Front Theta "%s"' % str(self.theta_front_wall))
            #self.get_logger().info('Wall Located "%s"' % str(self.wall_located))
            #self.get_logger().info('Aligned Status "%s"' % str(aligned))
            #self.get_logger().info('Approach Status "%s"' % str(approach))
            #self.get_logger().info('Ready Status "%s"' % str(ready))
        
        
            #Correction Factor Chop
            if self.correction_factor_front > 0.99:
                self.correction_factor_front = 0.99

            if self.correction_factor_right > 0.99:
                self.correction_factor_right = 0.99

            ## Move logic
            if aligned == False and self.front_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif aligned == False and self.front_laser*0.90 > self.laser_min:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            elif aligned == False and self.front_laser*0.90 < self.laser_min:
                self.movement.angular.z = 0.0
                aligned = True
                #self.publisher_.publish(self.movement)
                self.get_logger().info('Aligned Status "%s"' % str(aligned))
                
            
            if aligned == True and approach == False and self.front_laser > 0.3:
                self.movement.linear.x = 0.05
            elif aligned == True and approach == False and self.front_laser <= 0.3:
                self.movement.linear.x = 0.0
                approach = True
                #self.publisher_.publish(self.movement)
                self.get_logger().info('Approach Status "%s"' % str(approach))
                

            if approach == True and self.right_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif approach == True and self.right_laser*0.95 > self.laser_min:
                self.movement.angular.z = self.correction_factor_right*(0.2)
            elif approach == True:
                self.movement.angular.z = 0.0
                ready = True
                #self.publisher_.publish(self.movement)
                self.get_logger().info('Ready Status "%s"' % str(ready))
                
                
            # Check if ready to shut down
            if ready == True:
                response.wallfound = ready
                self.get_logger().info("Bot in position.")
                self.get_logger().info('Ready Status inside motion finder "%s"' % str(ready))
                wall_found = True
                break
                

            # Publishing the cmd_vel values to a Topic
            self.publisher_.publish(self.movement)


        #self.response.wallfound = ready
        self.get_logger().info('response.wallfound: "%s"' % str(response.wallfound))
        
        return response
        

    def laser_scan_callback(self, msg):  
        #self.get_logger().info('MULTITHREADING REENTRANT Receiving laser scan')          
        ## Save the Laser readings
        # Simulation Laser Ranges
        #self.front_laser = msg.ranges[0]
        #self.front_laser_10 = msg.ranges[10]
        #self.front_laser_350 = msg.ranges[350]

        #self.right_laser = msg.ranges[270]
        #self.right_laser_285 = msg.ranges[285]
        #self.right_laser_255 = msg.ranges[255]

        # Real Bot Laser Ranges
        self.front_laser = msg.ranges[359]
        self.front_laser_10 = msg.ranges[369]
        self.front_laser_350 = msg.ranges[349]

        self.right_laser = msg.ranges[179]
        self.right_laser_285 = msg.ranges[194]
        self.right_laser_255 = msg.ranges[164]

        self.laser_min = min(msg.ranges)

def main(args=None):
    rclpy.init(args=args)
    wall_finder_node = WallFinderService()
    
    # Use MultiThreadedExecutor
    executor = MultiThreadedExecutor(num_threads=2)
    executor.add_node(wall_finder_node)
    
    try:
        executor.spin()
    finally:
        wall_finder_node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

Hi @pharva ,

I took a look into your code for quite sometime and realized that there are no logic flow issues.

At this point, I am guessing that the hardware is having a slow processing speed. Probably it is ROS Bridge latency, which I am not sure or unaware of.

I will tag an admin here @roalgoal who can give you a better insight into what is happening.

Regards,
Girish

Hi @pharva, from what I understand, you have repeated issues (in different reserved sessions) where the TurtleBot3 is not responding as it should, with an apparent delay.

This is indeed very strange behavior because I have not heard of this until now.

We even had 3 presentations yesterday, 2 of them about this exact project and 1 of them about ROS 2 autonomous navigation (where this delay would become obvious), and everything ran smoothly, which only adds to the mystery of what’s happening here :thinking:.

Let me ask you some questions:

  1. Is your internet connection being detected as low by the rosject? This is a red WiFi icon with an exclamation mark that appears in the bottom blue bar.
  2. Do the joystick or teleop_twist_keyboard present a similar issue? We have to make sure that what is telling the robot to keep twisting is not the code you are running. In this case, the turtlebot3 only cares about the received /cmd_vel, that’s why I ask.
  3. Does this issue happen to you in every session? Are there other issues regarding the operation of the robot, like the lidar or the odometry, that you noticed?
  4. What are the nodes that you are launching? are there any more in addition to the wall finder server? Maybe isolating it from the main wall follower node can help with debugging.

This is indeed very strange behavior because I have not heard of this until now.

We even had 3 presentations yesterday, 2 of them about this exact project and 1 of them about ROS 2 autonomous navigation (where this delay would become obvious), and everything ran smoothly, which only adds to the mystery of what’s happening here :thinking:.

I am brand new to python and writing code. I am sure that there is nothing wrong with the turtlebot and I’ve done something wrong in my code. My lack of experience means that I don’t know how to approach a problem like this and I rely heavily on this forum and the experience and knowledge of people like you. I apologize for the ignorance of my answers and hope we can resolve this issue.

  1. Is your internet connection being detected as low by the rosject? This is a red WiFi icon with an exclamation mark that appears in the bottom blue bar.

I can’t comment on all of my connections, but the last one was fine (no red icon).

  1. Do the joystick or teleop_twist_keyboard present a similar issue? We have to make sure that what is telling the robot to keep twisting is not the code you are running. In this case, the turtlebot3 only cares about the received /cmd_vel, that’s why I ask.

If I haven’t submitted my wall_finder service the telop_twist_keyboard responds almost immediately. However, if the robot is in the middle of my wall_finder service, the robot doesn’t respond to the input for quite some time. After following @girishkumar.kannan suggestion about monitoring the /cmd_vel in a terminal all on its own, I could see the joystick input show immediately in /cmd_vel, but the robot would not respond to that input. During the wall_finder service, I would notice that the turtlebot wasn’t behaving how I expected it to and would enter “ctrl+c” into the terminal that was running the wall_finder service to stop the service. Turtlebot would continue executing the wall_finder service for 5-10 more seconds. During that time (after canceling the service) I would try to reorient the turtlebot with the joystick. I could see the input immediately with “echo /cmd_vel”, but the turtlebot was still executing the wall_finder service. After a few seconds, the turtlebot would respond to my joystick inputs.

  1. Does this issue happen to you in every session? Are there other issues regarding the operation of the robot, like the lidar or the odometry, that you noticed?

Yes, it happens every session. I haven’t noticed any issues with /LaserScan or /scan (I’ve forgotten what its called exactly). I haven’t gotten to the odometry readings in the real robot because I never make it past the wall_finder service.

In my initial post I mentioned that it worked one time. Thinking back on that “success,” I think that I got lucky in how I placed the turtlebot initially and it was able to move into the wall following movement even though the wall_finder service didn’t execute accurately. Inside of the wall following movement and odometry action server, things seem to run smoothly. Again, I’ve only seen the odometry run once, and the wall following twice (once for section 1, and the second is detailed above).

  1. What are the nodes that you are launching? are there any more in addition to the wall finder server? Maybe isolating it from the main wall follower node can help with debugging.

I hope I’m answering this question correctly: I’m launching the WallFinderService node, the OdomActionServer node, and the wall_hugger node (this is the code that calls the finder service, sends a goal to the odom action server, and contains the logic for wall following), totaling 3 nodes. This all works flawlessly in the simulation.

I’m not expecting you to proofread the code below, but for reference, I’ve included all three files and the launch file.

odom_recorder.py

import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from rclpy.qos import ReliabilityPolicy, QoSProfile
from odom_interface.action import OdomRecord
from nav_msgs.msg import Odometry
from geometry_msgs.msg import Point
from std_msgs.msg import Float64
import time
from rclpy.executors import MultiThreadedExecutor

class OdomActionServer(Node):
    def __init__(self):
        super().__init__('odom_action_server')
        self._action_server = ActionServer(self, OdomRecord, 'record_odom', self.execute_callback)
        self.distance_publisher = self.create_publisher(Float64, 'total_distance', 10)
        self.subscriber = self.create_subscription(Odometry, '/odom', self.odom_callback, QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE))
        self.execute_goal = False
        self.stored_init_meas = False
        self.first_odom = Point()
        self.last_odom = Point()
        self.odom_record = []
        self.total_distance = 0.0
        self.last_x = 0.0
        self.last_y = 0.0
        self.timer_odom_record = self.create_timer(1.0, self.update_odom_record)  # make the update_odom_record update only every 1 second
        
    def odom_callback(self, msg):
        if self.stored_init_meas == False and self.execute_goal is True:
            self.first_odom.x = msg.pose.pose.position.x
            self.first_odom.y = msg.pose.pose.position.y
            self.odom_record.append(Point(x=self.first_odom.x, y=self.first_odom.y, z=0.0))
            self.stored_init_meas = True
           
        self.last_odom.x = msg.pose.pose.position.x
        self.last_odom.y = msg.pose.pose.position.y

    def update_odom_record(self):
        if self.execute_goal is True:
            self.odom_record.append(Point(x=self.last_odom.x, y=self.last_odom.y, z=0.0))
        

    def execute_callback(self, goal_handle):
        self.get_logger().info('Executing goal...')
        self.execute_goal = True
        self.total_distance = 0.0
        self.first_odom = Point()
        self.last_odom = Point()
        self.odom_record = []
        self.stored_init_meas = False
        #self.timer_publish_feedback = self.create_timer(1.0, lambda: self.publish_feedback())   # make the publish_feedback update only every 1 second
        while self.execute_goal is True:
            time.sleep(1)
            self.publish_feedback()
        
        self.get_logger().info('exited while loop within execute_callback')
        goal_handle.succeed()
        result = OdomRecord.Result()
        result.list_of_odoms = self.odom_record
        self.get_logger().info(f'Action succeeded. Total distance traveled: {self.total_distance}')
        return result


    def publish_feedback(self):
        if len(self.odom_record) >= 2:
            #self.last_x = self.odom_record[-2].x
            #self.last_y = self.odom_record[-2].y
            
            dist_p1_p2 = ((self.odom_record[-1].x - self.odom_record[-2].x) ** 2 + (self.odom_record[-1].y - self.odom_record[-2].y) ** 2) ** 0.5
            self.total_distance += dist_p1_p2
            #self.get_logger().info(
            #f"self.last_x: {self.last_x:.3f},"
            #f"self.last_odom.x {self.last_odom.x:.3f},"
            #f"self.last_y: {self.last_y:.3f},"
            #f"self.last_odom.y {self.last_odom.y:.3f},"
            #f"self.total_distance {self.total_distance:.3f},"
            #f"dist_p1_p2: {dist_p1_p2:.3f},"
            #)

        dist_to_start = ((self.odom_record[-1].x - self.first_odom.x) ** 2 + (self.odom_record[-1].y - self.first_odom.y) ** 2) ** 0.5

        if dist_to_start < 0.075 and len(self.odom_record) >= 60:
            self.get_logger().info('Robot has returned to the initial position.')
            self.execute_goal = False
            self.stored_init_meas = False
            
        self.get_logger().info(
            f"self.odom_record list length: {len(self.odom_record)}, "
            f"total_distance: {self.total_distance:.3f}, "
            f"dist_to_start: {dist_to_start:.3f}"
        )
        #self.get_logger().info(
        #    f"first_odom: {self.first_odom},"
        #    f"last_odom: {self.last_odom}"    
        #)
       
        feedback_msg = OdomRecord.Feedback()
        feedback_msg.current_total = self.total_distance
        self.distance_publisher.publish(Float64(data=self.total_distance))
       
                

def main(args=None):
    rclpy.init(args=args)
    odom_action_server = OdomActionServer()
    executor = MultiThreadedExecutor(num_threads=4)
    executor.add_node(odom_action_server)
    try:
        executor.spin()
    finally:
        executor.shutdown()
        odom_action_server.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

wall_finder.py

from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
from findwall.srv import FindWall
from rclpy.qos import ReliabilityPolicy, QoSProfile
import rclpy
from rclpy.node import Node
import math
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup

# To launch the simulation:
    # source ~/simulation_ws/install/setup.bash
    # ros2 launch turtlebot3_gazebo main_turtlebot3_lab.launch.xml

# To control simualted robot:
    # ros2 run teleop_twist_keyboard teleop_twist_keyboard

class WallFinderService(Node):

    def __init__(self):
        super().__init__('find_wall')
        self.reentrant_group_1 = ReentrantCallbackGroup()

        self.srv = self.create_service(FindWall, 'find_wall', self.custom_service_callback, callback_group=self.reentrant_group_1)
        self.publisher_ = self.create_publisher(Twist, 'cmd_vel', 10)
        self.subscriber = self.create_subscription(LaserScan, 'scan', self.laser_scan_callback, QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE), callback_group=self.reentrant_group_1)
        
        self.response = FindWall.Response()
        self.movement = Twist()


    def custom_service_callback(self, request, response):
        wall_found = False
        aligned = False
        approach = False
        ready = False

        while wall_found == False: 
            # Calculated Variables
            self.front_laser_diff = self.front_laser_10 - self.front_laser_350
            self.right_laser_diff = self.right_laser_285 - self.right_laser_255
        
            self.theta_right_wall = math.degrees(math.atan2(self.right_laser_285 - self.right_laser_255*math.cos(math.radians(15)),self.right_laser*math.sin(math.radians(15))))
            self.theta_front_wall = math.degrees(math.atan2(self.front_laser_350-self.front_laser*math.cos(math.radians(10)),self.front_laser*math.sin(math.radians(10))))

            self.correction_factor_front = abs(self.theta_front_wall)/90
            self.correction_factor_right = abs(self.theta_right_wall)/90

            self.front_laser_avg = (self.front_laser+self.front_laser_10+self.front_laser_350)/3
            self.right_laser_avg = (self.right_laser+self.right_laser_285+self.right_laser_255)/3


            # print laser readings
            #self.get_logger().info(
            #f"Min Laser: {self.laser_min:.3f},"
            #f"R Laser {self.right_laser:.3f},"
            #)
            #self.get_logger().info('Min Range "%s"' % str(self.laser_min), 'Front Range "%s"' % str(self.front_laser))
            #self.get_logger().info('Front Range "%s"' % str(self.front_laser))
            #self.get_logger().info('Front 10 Range "%s"' % str(self.front_laser_10))
            #self.get_logger().info('Front 350 Range "%s"' % str(self.front_laser_350))
            #self.get_logger().info('Front Theta "%s"' % str(self.theta_front_wall))
            #self.get_logger().info('Wall Located "%s"' % str(self.wall_located))
            #self.get_logger().info('Aligned Status "%s"' % str(aligned))
            #self.get_logger().info('Approach Status "%s"' % str(approach))
            #self.get_logger().info('Ready Status "%s"' % str(ready))
        
        
            #Correction Factor Chop
            if self.correction_factor_front > 0.99:
                self.correction_factor_front = 0.99

            if self.correction_factor_right > 0.99:
                self.correction_factor_right = 0.99

            ## Move logic
            if aligned == False and self.front_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif aligned == False and self.front_laser*0.90 > self.laser_min:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            elif aligned == False and self.front_laser*0.90 < self.laser_min:
                self.movement.angular.z = 0.0
                aligned = True
                self.publisher_.publish(self.movement)
                self.get_logger().info('Aligned Status "%s"' % str(aligned))
                
            
            if aligned == True and approach == False and self.front_laser > 0.3:
                self.movement.linear.x = 0.05
            elif aligned == True and approach == False and self.front_laser <= 0.3:
                self.movement.linear.x = 0.0
                approach = True
                #self.publisher_.publish(self.movement)
                self.get_logger().info('Approach Status "%s"' % str(approach))
                

            if approach == True and self.right_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif approach == True and self.right_laser*0.95 > self.laser_min:
                self.movement.angular.z = self.correction_factor_right*(0.2)
            elif approach == True:
                self.movement.angular.z = 0.0
                ready = True
                #self.publisher_.publish(self.movement)
                self.get_logger().info('Ready Status "%s"' % str(ready))
                
                
            # Check if ready to shut down
            if ready == True:
                response.wallfound = ready
                self.get_logger().info("Bot in position.")
                self.get_logger().info('Ready Status inside motion finder "%s"' % str(ready))
                wall_found = True
                break
                

            # Publishing the cmd_vel values to a Topic
            self.publisher_.publish(self.movement)


        #self.response.wallfound = ready
        self.get_logger().info('response.wallfound: "%s"' % str(response.wallfound))
        
        return response
        

    def laser_scan_callback(self, msg):  
        #self.get_logger().info('MULTITHREADING REENTRANT Receiving laser scan')          
        ## Save the Laser readings
        # Simulation Laser Ranges
        #self.front_laser = msg.ranges[0]
        #self.front_laser_10 = msg.ranges[10]
        #self.front_laser_350 = msg.ranges[350]

        #self.right_laser = msg.ranges[270]
        #self.right_laser_285 = msg.ranges[285]
        #self.right_laser_255 = msg.ranges[255]

        # Real Bot Laser Ranges
        self.front_laser = msg.ranges[359]
        self.front_laser_10 = msg.ranges[369]
        self.front_laser_350 = msg.ranges[349]

        self.right_laser = msg.ranges[179]
        self.right_laser_285 = msg.ranges[194]
        self.right_laser_255 = msg.ranges[164]

        self.laser_min = min(msg.ranges)

def main(args=None):
    rclpy.init(args=args)
    wall_finder_node = WallFinderService()
    
    # Use MultiThreadedExecutor
    executor = MultiThreadedExecutor(num_threads=2)
    executor.add_node(wall_finder_node)
    
    try:
        executor.spin()
    finally:
        wall_finder_node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

wall_following.py

import rclpy
# import the ROS2 python libraries
from rclpy.node import Node
# import the Twist module from geometry_msgs interface
from geometry_msgs.msg import Twist
# import the LaserScan module from sensor_msgs interface
from sensor_msgs.msg import LaserScan
from rclpy.qos import ReliabilityPolicy, QoSProfile
import math
from findwall.srv import FindWall
from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor
from rclpy.action import ActionClient
from odom_interface.action import OdomRecord
import time

# To launch the simulation:
    # source ~/simulation_ws/install/setup.bash
    # ros2 launch turtlebot3_gazebo main_turtlebot3_lab.launch.xml

# To control simualted robot:
    # ros2 run teleop_twist_keyboard teleop_twist_keyboard


class wall_hugger(Node):
    def __init__(self):
        super().__init__('wall_hugger_node')

        self.group1 = ReentrantCallbackGroup()
        self.group2 = ReentrantCallbackGroup()
        self.group3 = ReentrantCallbackGroup()

        self.publisher_ = self.create_publisher(Twist, 'cmd_vel', 10)
        self.subscriber = self.create_subscription(LaserScan, 'scan', self.Laser_Scan_callback, QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE), callback_group=self.group2)
        self.client = self.create_client(FindWall, 'find_wall', callback_group=self.group1)
        self._action_client = ActionClient(self, OdomRecord, 'record_odom')
               
        self.req = FindWall.Request()
        self.response = FindWall.Response()
                
        # checks once per second if a Service matching the type and name of the client is available.
        while not self.client.wait_for_service(timeout_sec=1.0):
            # if it is not available, a message is displayed
            self.get_logger().info('service not available, waiting again...')
       
        # define the motion variables
        self.front_laser = 0.0
        self.right_laser = 0.0
        self.left_laser = 0.0
        self.back_laser = 0.0
        self.right_laser_285 = 0.0      #Laser 15 deg CW of the right laser
        self.right_laser_255 = 0.0      #laser 15 deg CCW of the right laser
        self.right_laser_diff = 0.0     #difference between the lasers on 15 deg tilt to determine robot orientation w.r.t. wall
        self.theta_wall = 0.0           #angle between the right wall and the forward motion of the robot.  0 deg means moving // with the wall
        self.correction_factor = 0      #turning correction factor
        self.right_avg_dist = 0         #average distance from the right wall
             
        # create a Twist message
        self.movement = Twist()
        self.timer_period = 0.5
        self.timer = self.create_timer(self.timer_period, self.begin, callback_group=self.group3)
        
        # create Boolean for synchronous request
        self.request_sent = False

        self.action_goal_sent = False
        
        
# Wall Finder Service Callbacks

    def send_request(self):
        self.get_logger().info('SENDING REQUEST...')
        self.response = self.client.call(self.req)
        
    def begin(self):
        
        # sending synchronous request
        if self.request_sent is False:
            # making sure that the request will not be called again in the callback function
            self.request_sent = True
            self.send_request()   

        elif self.response.wallfound is True:
            if self.action_goal_sent is False:
                self.get_logger().info('wallfound "%s"' % str(self.response))
                self.get_logger().info('Service Complete')
                self.send_goal()
                self.action_goal_sent = True
            #time.sleep(5)
            self.motion()

# Action Client Callbacks

    def send_goal(self):
        goal_msg = OdomRecord.Goal()
        # Set any goal parameters if needed
        self._action_client.wait_for_server()
        self._send_goal_future = self._action_client.send_goal_async(goal_msg, feedback_callback=self.feedback_callback)
        self._send_goal_future.add_done_callback(self.goal_response_callback)

    def goal_response_callback(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().info('Goal rejected :(')
            return

        self.get_logger().info('Goal accepted :)')
        self._get_result_future = goal_handle.get_result_async()
        self._get_result_future.add_done_callback(self.get_result_callback)

    def get_result_callback(self, future):
        result = future.result().result
        self.get_logger().info('Total Distance: {1}'.format(result.current_total))

    def feedback_callback(self, feedback_msg):
        feedback = feedback_msg.feedback.current_total
        self.get_logger().info('Received feedback: Current Total Distance: {0}'.format(feedback))


# Wall Follower Callbacks

    def Laser_Scan_callback(self, msg):
        # save the Laser readings
        # Simulation
        #self.front_laser = msg.ranges[0]
        #self.right_laser = msg.ranges[270]
        #self.left_laser = msg.ranges[90]
        #self.back_laser = msg.ranges[180]
        #self.right_laser_285 = msg.ranges[285]
        #self.right_laser_255 = msg.ranges[255]
        
        # Real Robot
        self.front_laser = msg.ranges[359]
        self.right_laser = msg.ranges[179]
        self.left_laser = msg.ranges[539]
        self.back_laser = msg.ranges[0]
        self.right_laser_285 = msg.ranges[194]
        self.right_laser_255 = msg.ranges[164]

    def motion(self):
        #if self.service_done:  # Check if the service has completed
            # print laser readings for debug
            #self.get_logger().info('Front Laser "%s"' % str(self.front_laser))
            #self.get_logger().info('Right Laser Avg: "%s"' % str(self.right_avg_dist))
            #self.get_logger().info('Angle: "%s"' % str(self.right_laser_diff))
            #self.get_logger().info('Right 255: "%s"' % str(self.right_laser_255))
            #self.get_logger().info('Right 285: "%s"' % str(self.right_laser_285))
            #self.get_logger().info('Theta: "%s"' % str(self.theta_wall))
            #self.get_logger().info('Correcction: "%s"' % str(self.correction_factor))
            
            
            #Calculated Variables
            self.right_laser_diff = self.right_laser_285 - self.right_laser_255
            
            self.theta_wall = math.degrees(math.atan2(self.right_laser_285 - self.right_laser_255*math.cos(math.radians(15)),self.right_laser*math.sin(math.radians(15))))
            
            self.correction_factor = abs(self.theta_wall)/90
            
            self.right_avg_dist = (self.right_laser+self.right_laser_285+self.right_laser_255)/3
            
            #Correction Factor Chop
            if self.correction_factor > 0.99:
                self.correction_factor = 0.99
            ## Move logic
            # Front proximity
            if self.front_laser < 0.4:
                self.movement.angular.z = 0.4
            #If angled away from the wall and within sweet spot 
            elif self.right_laser_diff > 0.00 and self.right_avg_dist < 0.3 and self.right_avg_dist > 0.22:
                #self.get_logger().info('Sweet Spot Turning right')
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(-0.3)
            #If angled towards the wall and within the sweet spot 
            elif self.right_laser_diff < 0.00 and self.right_avg_dist < 0.3 and self.right_avg_dist > 0.22:
                #self.get_logger().info('Sweet Spot Turning left')
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(0.3)    
            #If angled towards the wall and too close to the wall 
            elif self.right_laser_diff < 0.00 and self.right_laser_285 < 0.22:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(0.4)
            #If low agnled away from the wall and too close to the wall
            #elif self.right_laser_diff > 0.0 and self.right_laser_diff <= 0.15 and self.right_laser_255 < 0.22:
                #self.movement.linear.x = 0.03
                #self.movement.angular.z = 0.2 
            #If angled away from the wall and too close to the wall 
            elif self.right_laser_diff >= 0.05 and self.right_laser_255 < 0.22:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(0.1)                
            #If angled away from the wall and too far away from the wall 
            elif self.right_laser_diff > 0.00 and self.right_laser_255 > 0.3:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(-0.4) 
            #If angled towards the wall and too far away from the wall 
            elif self.right_laser_diff < -0.05 and self.right_laser_285 > 0.3:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(-0.1)                   
            else:
                self.movement.linear.x = 0.03
                self.movement.angular.z = 0.0
            
            # Publishing the cmd_vel values to a Topic
            self.publisher_.publish(self.movement)



def main(args=None):
    rclpy.init(args=args)
    try:
        wall_hugger_node = wall_hugger()
        executor = MultiThreadedExecutor(num_threads=4)
        executor.add_node(wall_hugger_node)
        try:
            executor.spin()
        finally:
            executor.shutdown()
            wall_hugger_node.destroy_node()

    finally:
        rclpy.shutdown()

if __name__ == '__main__':
    main()

main.launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='wall_follower',
            executable='wall_finder',
            output='screen'),
        Node(
            package='wall_follower',
            executable='wall_following',
            output='screen'),
        Node(
            package='wall_follower',
            executable='odom_recorder',
            output='screen'),
    ])

HI @pharva ,

Since everything works as expected in the simulation, I suspect just one thing here.
A laser scan value size mismatch between simulation and real robot, however I am not 100% sure and need more information from you.

Execute the following command in a terminal in the simulation and on the real robot:

ros2 topic echo /scan # <--- use this command on ROS2 Foxy & Galactic
ros2 topic echo /scan --once   # <--- use this command on ROS2 Humble

Hit Ctrl + C after getting 1 or 2 messages if you are on ROS2 Foxy or Galactic.
You will get a message output similar to what you see immediately below:

header:                # <--- not required
  stamp:               # <--- not required
    sec: 0             # <--- not required
    nanosec: 0         # <--- not required
  frame_id: ''         # <--- not required
angle_min: 0.0         # <--- REQUIRED
angle_max: 0.0         # <--- REQUIRED
angle_increment: 0.0   # <--- REQUIRED
time_increment: 0.0    # <--- not required
scan_time: 0.0         # <--- not required
range_min: 0.0         # <--- not required
range_max: 0.0         # <--- not required
ranges: []             # <--- not required
intensities: []        # <--- not required

Copy only the lines that indicate angle_min, angle_max, angle_increment for simulation and real robot. If the values are different for simulation and real robot, please post them as shown below:

Simulation:

angle_min: <float_value>
angle_max: <float_value>
angle_increment: <float_value>

Real Robot:

angle_min: <float_value>
angle_max: <float_value>
angle_increment: <float_value>

The reason why I suspect this is that, the laser scanner configuration on the real robot was different on this project when I worked on it (long back). Since you have hard-coded the indices of the laser scan values in your code, I think the laser scan index mismatch is what is making the robot perform a different behavior.

I hope this helps.

Regards,
Girish

@girishkumar.kannan Indeed the index is different! I learned this during the first wall following unit.
Within my code, I have commented out a section for simulation with laser messages referencing the simulation laser info, and I have another section for the real robot.

The simulation min angle (starting angle) is 0 and the max angle is 6.28 (2*pi) the angle increment is set so that there are a total of 360 entries.

Turtlebot min angle is -3.14 and the max angle is 3.14. The angle increment is set so that there are 720 entries.

For example, the laser pointing forward in the simulation is .ranges[0] and on the turtlebot it is .ranges[360].

I have accounted for and fixed this during the first module. I don’t believe that this is my problem.

Hi @pharva ,

Oh yes, you have indicated that in your code. I see that now. I kind of forgot after going through the codes.

Ok, so I have one final suggestion here. Instead of continuously rotating to the desired direction, do a scan->turn->stop->check behavior. This way you first scan for the lowest scan value (this is your closest wall angle), then turn for 1 second, then stop the robot. Check if the front laser is equal to the minimum value of the scan, so you can match the closest wall in front of the robot. This way you are restricting the robot from overshooting the desired heading angle, so that it does not go into continuous spinning mode.

Try this and let me know.

Regards,
Girish

I have found the problem. My professor suggested looking into how frequently I was publishing to the /cmd_vel topic. We found this command:

ros2 topic hz /cmd_vel

In the simulation, the frequency was 600-700 Hz. Much more than what I need. On the real turtlebot, the frequency was 1600 Hz on average!

I added a time.sleep(0.25) at the end of my while loop and the code ran smoothly. I think this is along the same thoughts as what @girishkumar.kannan suggested:

Instead of continuously rotating to the desired direction, do a scan->turn->stop->check behavior.

The turtlebot responded instantly to /cmd_vel inputs after that change.

        while wall_found == False: 
            # Calculated Variables
            self.front_laser_diff = self.front_laser_10 - self.front_laser_350
            self.right_laser_diff = self.right_laser_285 - self.right_laser_255
        
            self.theta_right_wall = math.degrees(math.atan2(self.right_laser_285 - self.right_laser_255*math.cos(math.radians(15)),self.right_laser*math.sin(math.radians(15))))
            self.theta_front_wall = math.degrees(math.atan2(self.front_laser_350-self.front_laser*math.cos(math.radians(10)),self.front_laser*math.sin(math.radians(10))))

            self.correction_factor_front = abs(self.theta_front_wall)/90
            self.correction_factor_right = abs(self.theta_right_wall)/90

            self.front_laser_avg = (self.front_laser+self.front_laser_10+self.front_laser_350)/3
            self.right_laser_avg = (self.right_laser+self.right_laser_285+self.right_laser_255)/3
 
            #Correction Factor Chop
            if self.correction_factor_front > 0.99:
                self.correction_factor_front = 0.99

            if self.correction_factor_right > 0.99:
                self.correction_factor_right = 0.99

            ## Move logic
            if aligned == False and self.front_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.35
            elif aligned == False and self.front_laser*0.90 > self.laser_min:
                self.movement.angular.z = 0.2
            elif aligned == False and self.front_laser*0.90 < self.laser_min:
                self.movement.angular.z = 0.0
                aligned = True
                self.publisher_.publish(self.movement)
                self.get_logger().info('Aligned Status "%s"' % str(aligned))
                
            
            if aligned == True and approach == False and self.front_laser > 0.3:
                self.movement.linear.x = 0.05
            elif aligned == True and approach == False and self.front_laser <= 0.3:
                self.movement.linear.x = 0.0
                approach = True
                #self.publisher_.publish(self.movement)
                self.get_logger().info('Approach Status "%s"' % str(approach))
                

            if approach == True and self.right_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif approach == True and self.right_laser*0.95 > self.laser_min:
                self.movement.angular.z = self.correction_factor_right*(0.2)
            elif approach == True:
                self.movement.angular.z = 0.0
                ready = True
                #self.publisher_.publish(self.movement)
                self.get_logger().info('Ready Status "%s"' % str(ready))
                
                
            # Check if ready to shut down
            if ready == True:
                response.wallfound = ready
                self.get_logger().info("Bot in position.")
                self.get_logger().info('Ready Status inside motion finder "%s"' % str(ready))
                wall_found = True
                break
                

            # Publishing the cmd_vel values to a Topic
            self.publisher_.publish(self.movement)
            time.sleep(0.25)

The reason, I think, I never had issues with the wall_following part of the movement logic is because I have this within the initialization of the wall_follower node:

 self.timer_period = 0.5
 self.timer = self.create_timer(self.timer_period, self.begin, callback_group=self.group3)

Hi @pharva ,

Glad to know that you have found out that the problem was the rate of publishing.

This thought never occurred to me - to check the frequency using the hz command.
It is also surprising to know that the real robot publishes at 1600 Hz average, into /cmd_vel topic!
For this kind of robot, I would say, 10 Hz itself is way too fast!

Anyways, glad to know that I could be of some help! Thanks for sharing this information!

Regards,
Girish

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.