Ros2 Basics Struggle in Real Robot project Part 2

Hi the construct team I hope all are fine,

I am a beginner in Ros2 and I am struggled for more than 1 week working on the real robot project part 2 the find wall behavior I wrote my code and I am sure that it is correct but it dose not work so I would appreciate if you can tell me where is the mistake or what should I do if my code is not correct the problem or issue is that the robot is not aligned to the right wall

this is the code of wall_finder.py :

from rclpy.qos import ReliabilityPolicy, QoSProfile
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.executors import MultiThreadedExecutor
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
from custom_interfaces.srv import FindWall
import rclpy
from rclpy.node import Node
import numpy as np
import time

class WallFinderServer(Node):
    def __init__(self):
        super().__init__('wall_finder_server')
        
        self.group = ReentrantCallbackGroup()

        self.wall_finder_srv = self.create_service(
            FindWall, 
            "find_wall", 
            self.find_wall_callback, 
            callback_group=self.group)

        self.twist_publisher = self.create_publisher(
            Twist, 
            "cmd_vel",
            10,
            callback_group=self.group
        )
        
        self.sub_laser = self.create_subscription(
            LaserScan,
            "scan",
            self.laser_callback,
            QoSProfile(depth=10, reliability=ReliabilityPolicy.BEST_EFFORT),
            callback_group=self.group
        )
        self.initial_configuration = False
        self.turn_sign = +1
        self.laser_min_idx = 0
        self.laser_min = -1.0
        self.angle_to_rotate = 0.0
        self.laser_front = 0.0
        self.n_ranges = 0.0
        self.front_laser_idx = 0
        self.right_laser_idx = 0
        
        self.dist_tol = 0.03
        self.laser_idx_tol = 6
        
        self.vel_angular_z = 0.2
        self.vel_linear_x = 0.05

        self.first_turn_finished = False
        self.second_turn_finished = False

    def laser_callback(self, msg):
        
        if not self.initial_configuration:
            self.n_ranges = round((2 * np.pi / msg.angle_increment) + 1)
            self.front_laser_idx = int(self.n_ranges/2)
            self.right_laser_idx = int(self.n_ranges/4)
            self.initial_configuration = True
            self.get_logger().info('Front laser idx: "%s"' % str(self.front_laser_idx))
            self.get_logger().info('Right laser idx: "%s"' % str(self.right_laser_idx))
        
        self.laser_min_idx = np.argmin(msg.ranges[:])
        self.laser_min = msg.ranges[self.laser_min_idx]
        min_range_angle = (2 * np.pi * (self.laser_min_idx + 1)) / self.n_ranges
        if not self.first_turn_finished:
            self.angle_to_rotate = min_range_angle - np.pi
            if self.angle_to_rotate < 0:
                self.turn_sign = -1    
            self.get_logger().info('Angle to rotate: "%s"' % str(self.angle_to_rotate * 180 / np.pi))
        elif not self.second_turn_finished:
            self.angle_to_rotate = min_range_angle - np.pi/2
            self.get_logger().info('Angle to rotate 2: "%s"' % str(self.angle_to_rotate * 180 / np.pi))
        # self.get_logger().info('Angle to rotate: "%s"' % str(self.laser_min_idx ))
        self.laser_front = msg.ranges[self.front_laser_idx]
        
            
    def find_wall_callback(self, request, response):
        msg = Twist()
        
        while (self.laser_min < 0):
            self.get_logger().info('WAITING FOR IDENTIFYING THE NEAREST WALL')
            time.sleep(0.5)

        while (abs(self.laser_min_idx - self.front_laser_idx) > self.laser_idx_tol):
            msg.angular.z = self.turn_sign * self.vel_angular_z
            self.twist_publisher.publish(msg)
            time.sleep(0.5)
            self.get_logger().info('FIRST TURN')
        
        self.first_turn_finished = True
        msg.angular.z = 0.0
        self.twist_publisher.publish(msg)
        self.get_logger().info('First turn finished, the robot is looking the nearest wall')

        linear_x_sign = 1
        if self.laser_front < 0.3:
            linear_x_sign = -1
        while abs(self.laser_front - 0.3) > self.dist_tol:
            msg.linear.x = linear_x_sign * self.vel_linear_x
            msg.angular.z = 0.0
            self.twist_publisher.publish(msg)
            time.sleep(0.5)
            self.get_logger().info('LINEAR MOTION')
            
        msg.linear.x = 0.0
        self.twist_publisher.publish(msg)
        self.get_logger().info('The robot 30 cm away from the wall')

        while (abs(self.laser_min_idx - self.right_laser_idx) > self.laser_idx_tol):
            msg.angular.z = self.vel_angular_z
            self.twist_publisher.publish(msg)
            time.sleep(0.5)
            self.get_logger().info('SECOND TURN')

        self.second_turn_finished = True
        msg.angular.z = 0.0
        self.twist_publisher.publish(msg)
        self.get_logger().info('The robot is aligned with the right wall')

        response.wallfound = True
        
        return response


def main(args=None):
    # initialize the ROS communication
    rclpy.init(args=args)
    wall_finder_node = WallFinderServer()
    # Create a MultiThreadedExecutor with 3 threads
    executor = MultiThreadedExecutor(num_threads=3)
    # Add the node to the executor
    executor.add_node(wall_finder_node)
    try:
        # Spin the executor
        executor.spin()
    finally:
        # Shutdown the executor
        executor.shutdown()
        wall_finder_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

I hope you can help me :blush:

Best Regards,

Ghassan

Please anyone can help me

Hi @ghassan11 ,

I felt I have to point this out: Why are you importing the whole of numpy to use just the argmin function and the constant pi? I am not sure if you are excited to use numpy library, but this is bad programming.

You can actually define pi as self.pi = 3.141593 and use self.pi instead of np.pi. You do not require pi precision beyond 6 decimal places. Seriously!

Also, you are finding the index of minimum of ranges using argmin and then finding the minimum value. You can do both using built-in python functions itself.

self.laser_min = min(msg.ranges)   # finds min value
self.laser_min_idx = msg.ranges.index(self.laser_min) # index of min value

The above two lines will do the same thing as the following lines (these lines are unnecessarily complicated)

self.laser_min_idx = np.argmin(msg.ranges[:])   # index of min value
self.laser_min = msg.ranges[self.laser_min_idx]   # finds min value

You must do all robot controls within the service callback, not within the scan callback.
Use the scan callback just to get the current scan ranges. Do not implement any robot turns/moves inside the scan callback. So move all robot control logic inside service callback.

Rework with the service callback after moving all robot control code into this callback. You will then have easier control of the robot.

Regards,
Girish

2 Likes

Thankyou Girish man you are better than the computer your brain is FANTASTIC I don’t know how to thank you every time you help thanks Girish I hope to be like you one day :wink: I am 15 years old :heart_eyes:
Best Regards,

1 Like

Hi @ghassan11 ,

Glad to know that reply was helpful to you.

That’s awesome! I wish I had learnt robotics when I was 15 (anyways, not too late for me though :smile:).

Regards,
Girish

1 Like

Hi Girish sorry again for annoying :sweat_smile:
I have another issue I am in the project section 2.2

where I should call the wall_finder.py code which is the server I should call it as a client from the wall_following.py so I will create a client in the wall_following.py so the result should be that the server when it will be called by the client it will align the robot to the correct position after that it will start the wall_following.py behavior the problem is that when the client call the robot the server align the robot to the correct position but the robot then stop from moving we can say the robot don’t have any information if the server finished yes or no.

this is the wall_finder.py :

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

class WallFinderServer(Node):
    def __init__(self):
        super().__init__('wall_finder_server')

        self.group = ReentrantCallbackGroup()

        self.wall_aligned = False  # Flag to indicate if the wall is aligned

        self.wall_finder_srv = self.create_service(
            FindWall, 
            'find_wall', 
            self.find_wall_callback)

        self.twist_publisher = self.create_publisher(
            Twist, 
            '/cmd_vel',
            10)

        self.sub_laser = self.create_subscription(
            LaserScan,
            "/scan",
            self.laser_callback,
            QoSProfile(depth=10, reliability=ReliabilityPolicy.BEST_EFFORT),
            callback_group=self.group
        )

        self.initial_configuration = False
        self.turn_sign = +1
        self.laser_min_idx = 0
        self.laser_min = -1.0
        self.angle_to_rotate = 0.0
        self.laser_front = 0.0
        self.n_ranges = 0
        self.front_laser_idx = 0
        self.right_laser_idx = 0

        self.dist_tol = 0.03
        self.laser_idx_tol = 6

        self.vel_angular_z = 0.2
        self.vel_linear_x = 0.05

        self.first_turn_finished = False
        self.second_turn_finished = False

    def laser_callback(self, msg):
        if not self.initial_configuration:
            self.n_ranges = len(msg.ranges)
            self.front_laser_idx = int(self.n_ranges / 2)
            # Calculate the right laser index based on the front laser index
            self.right_laser_idx = (self.front_laser_idx + int(self.n_ranges / 4)) % self.n_ranges
            self.initial_configuration = True
            self.get_logger().info('Front laser idx: "%s"' % str(self.front_laser_idx))
            self.get_logger().info('Right laser idx: "%s"' % str(self.right_laser_idx))

        self.laser_min = min(msg.ranges)
        self.laser_min_idx = msg.ranges.index(self.laser_min)
        min_range_angle = (2 * math.pi * (self.laser_min_idx + 1)) / self.n_ranges

        if not self.first_turn_finished:
            self.angle_to_rotate = min_range_angle - math.pi
            if self.angle_to_rotate < 0:
                self.turn_sign = -1
            self.get_logger().info('Angle to rotate: "%s"' % str(self.angle_to_rotate * 180 / math.pi))
        elif not self.second_turn_finished:
            self.angle_to_rotate = min_range_angle - (3 * math.pi / 2)
            self.get_logger().info('Angle to rotate 2: "%s"' % str(self.angle_to_rotate * 180 / math.pi))

        self.laser_front = msg.ranges[self.front_laser_idx]

    def move_robot(self, linear_x, angular_z, duration):
        # Move the robot with the given velocities for the specified duration
        msg = Twist()
        msg.linear.x = linear_x
        msg.angular.z = angular_z

        start_time = time.time()
        while time.time() - start_time < duration:
            self.twist_publisher.publish(msg)
            time.sleep(0.1)
        msg.linear.x = 0.0
        msg.angular.z = 0.0
        self.twist_publisher.publish(msg)

    def find_wall_callback(self, request, response):
        while self.laser_min < 0:
            self.get_logger().info('WAITING FOR IDENTIFYING THE NEAREST WALL')
            time.sleep(0.5)

        while abs(self.laser_min_idx - self.front_laser_idx) > self.laser_idx_tol:
            turn_sign = self.turn_sign
            self.move_robot(0.0, turn_sign * self.vel_angular_z, 0.5)
            self.get_logger().info('FIRST TURN')

        self.first_turn_finished = True
        self.move_robot(0.0, 0.0, 0.5)
        self.get_logger().info('First turn finished, the robot is looking at the nearest wall')

        linear_x_sign = 1 if self.laser_front < 0.3 else -1
        while abs(self.laser_front - 0.3) > self.dist_tol:
            self.move_robot(linear_x_sign * self.vel_linear_x, 0.0, 0.5)
            self.get_logger().info('LINEAR MOTION')

        self.move_robot(0.0, 0.0, 0.5)
        self.get_logger().info('The robot is 30 cm away from the wall')

        while abs(self.laser_min_idx - self.right_laser_idx) > self.laser_idx_tol:
            self.move_robot(0.0, self.vel_angular_z, 0.5)
            self.get_logger().info('SECOND TURN')

        self.second_turn_finished = True
        self.move_robot(0.0, 0.0, 0.5)
        self.get_logger().info('The robot is aligned with the right wall')

        self.wall_aligned = True
        response.wallfound = True
        return response


def main(args=None):
    # initialize the ROS communication
    rclpy.init(args=args)
    wall_finder_node = WallFinderServer()
    # Create a MultiThreadedExecutor with 3 threads
    executor = MultiThreadedExecutor(num_threads=3)
    # Add the node to the executor
    executor.add_node(wall_finder_node)
    try:
        # Spin the executor
        executor.spin()
    finally:
        # Shutdown the executor
        executor.shutdown()
        wall_finder_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

and this is the wall_following.py :

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
from custom_interfaces.srv import FindWall
import time

class WallFollower(Node):
    def __init__(self):
        super().__init__('wall_follower')
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        self.subscription_ = self.create_subscription(
            LaserScan,
            '/scan',
            self.laser_callback,
            10
        )
        self.wall_distance_ = 0.3
        self.front_wall_distance_ = 0.5
        self.laser_forwar = 0
        self.wall_aligned = False

        # Wait until the wall is aligned by calling the /find_wall service
        self.wait_for_wall_alignment()

    def wait_for_wall_alignment(self):
        self.get_logger().info('Waiting for wall alignment...')
        while not self.is_wall_aligned():
            rclpy.spin_once(self)  # Process any available messages
            time.sleep(0.5)

    def is_wall_aligned(self):
        # Create a synchronous service client to call the /find_wall service
        find_wall_client = self.create_client(FindWall, 'find_wall')

        # Wait for the service to be available
        if not find_wall_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().error('Service not available')
            return False

        # Create a request to the service (the request message has no fields)
        request = FindWall.Request()

        # Call the service synchronously
        response = find_wall_client.call(request)

        if response is not None and response.wallfound:
            self.get_logger().info('Wall found! Robot is aligned with the wall.')
            return True
        else:
            self.get_logger().info('Wall not found yet.')
            return False

    def laser_callback(self, msg):
        if not self.is_wall_aligned():
            return
        else:

            ranges = msg.ranges
            right_ray_index = int(len(ranges) * 3 / 4)  # Index of the rightmost laser ray
            self.laser_forward = msg.ranges[359]

        # Check if there is a wall in front
            if self.laser_forward < self.front_wall_distance_:
                self.update_velocity(0.0, 0.25)  # Turn fast to the left
                return

        # Calculate the distance to the wall on the right
            right_distance = ranges[right_ray_index]

        # Adjust the linear and angular velocities based on the distance to the wall
            if right_distance > self.wall_distance_ + 0.05:
                self.update_velocity(0.12, -0.25)  # Approach the wall
            elif right_distance < self.wall_distance_ - 0.05:
                self.update_velocity(0.12, 0.25)  # Move away from the wall
            else:
                self.update_velocity(0.15, 0.0)  # Maintain the current distance

    def update_velocity(self, linear, angular):
        # Create a Twist message with the provided linear and angular velocities
        msg = Twist()
        msg.linear.x = linear
        msg.angular.z = angular
        self.publisher_.publish(msg)

def main(args=None):
    rclpy.init(args=args)
    wall_follower = WallFollower()
    rclpy.spin(wall_follower)
    wall_follower.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Regards,
Ghassan

Any one can help please :blush:

Hi @ghassan11 ,

Firstly, please learn how to format a paragraph.

This paragraph seems to be just one sentence. I could not understand it easily. I spent about 15 minutes just to understand what you have written there.
Also please use English grammar and punctuation.

As the rosject instructions say, the service is synchronous.
The find_wall service is called only once to position the robot, before starting the wall following procedure.

First, test if the service server works correctly. Then make a service client to call the service to check if programmatic service call works fine.
Then incorporate the service client code into your main wall_following program.
The wall following program will call the find_wall service to position the robot close to the wall.
Once the service is complete and returns success, the wall following algorithm will start.

In case your find_wall service does not complete or the robot does not stop next to a wall, then you need to work and fix your find_wall service server program code logic.

I hope this is understandable.

Regards,
Girish

Hello Girish,

I apologize for my bad English.

The server code is working well, and so is the client. My problem is that when I run the main code “wall_following.py” and add the client to it, after the client calls the server and the server performs its job, the robot doesn’t exhibit the wall-following behavior, so the client don’t have any information to know that the server finished i provided the both codes up.

Regards,
Ghassan

@girishkumar.kannan :grin:

Hi @ghassan11 ,

Some changes for the wall_following.py program:

  1. You can move the find_wall_client declaration into __init__ function.
  2. You can also move the wait_for_service section into __init__ function.

The reason why the wall_following behavior is not actuating is because, once the service client completes, the code does not have a handle to trigger the next event.
The fix is quite simple, you need to use a wall_timer (timer_callback).
So once you have initialized all the necessary things in the __init__ function, use this timer callback to trigger the service call with a boolean flag. Once the find_wall service is complete, set the boolean flag to true and move on to wall following behavior procedure. This method will help you in the future to add the action section of the rosject (for odometry calculation).

Also, just as I have told you before, do not overload the scan callback with robot control functions. Use the scan callback just to get the current scan ranges and store it in a class variable.
Move the robot control algorithm into the timer callback (from the scan callback).

I hope this helps.

Regards,
Girish

HI @girishkumar.kannan thank you for your reply

I tried your solution but still it dose not do the wall_following behavior,
I have a great and bad news the great is that the client sent to the server successfully and the robot is aligned.
The bad is that the robot did not start the wall_following behavior it just keep the robot to align it self again and again without stopping.
Please I hope you help me if you need further information I will answer

This is the code after your advices wall_folowing.py:

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
from custom_interfaces.srv import FindWall

class WallFollower(Node):
    def __init__(self):
        super().__init__('wall_follower')
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        self.subscription_ = self.create_subscription(
            LaserScan,
            '/scan',
            self.laser_callback,
            10
        )
        self.wall_distance_ = 0.3
        self.front_wall_distance_ = 0.5
        self.laser_forward = 0
        self.wall_aligned = False

        # Create the find_wall service client
        self.find_wall_client = self.create_client(FindWall, 'find_wall')

        # Wait for the service to become available
        if not self.find_wall_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().error('Service not available')
        else:
            self.get_logger().info('Service is available')

        self.timer = self.create_timer(1.0, self.timer_callback)

    def timer_callback(self):
        if not self.wall_aligned:
            # Call the find_wall service
            request = FindWall.Request()
            future = self.find_wall_client.call_async(request)
            future.add_done_callback(self.find_wall_response_callback)

    def find_wall_response_callback(self, future):
        try:
            response = future.result()
            if response and response.wallfound:
                self.get_logger().info('Wall found! Robot is aligned with the wall.')
                self.wall_aligned = True
                self.wall_following_behavior()

            else:
                self.get_logger().info('Wall not found yet.')
        except Exception as e:
            self.get_logger().error('Service call failed: %s' % str(e))

        if self.wall_aligned:
            self.wall_following_behavior()

    def wall_following_behavior(self):
        ranges = getattr(self, 'ranges', [])
        if not ranges:
            return

        right_ray_index = int(len(ranges) * 3 / 4)  # Index of the rightmost laser ray
        self.laser_forward = ranges[0]

        # Check if there is a wall in front
        if self.laser_forward < self.front_wall_distance_:
            self.update_velocity(0.0, 0.25)  # Turn fast to the left
            return

        right_distance = ranges[right_ray_index]

        # Adjust the linear and angular velocities based on the distance to the wall
        if right_distance > self.wall_distance_ + 0.05:
            self.update_velocity(0.12, -0.25)  # Approach the wall
        elif right_distance < self.wall_distance_ - 0.05:
            self.update_velocity(0.12, 0.25)  # Move away from the wall
        else:
            self.update_velocity(0.15, 0.0)  # Maintain the current distance

    def laser_callback(self, msg):
        # Store the scan ranges in a class variable
        self.ranges = msg.ranges

    def update_velocity(self, linear, angular):
        # Create a Twist message with the provided linear and angular velocities
        msg = Twist()
        msg.linear.x = linear
        msg.angular.z = angular
        self.publisher_.publish(msg)

def main(args=None):
    rclpy.init(args=args)
    wall_follower = WallFollower()

    executor = rclpy.executors.MultiThreadedExecutor(num_threads=3)
    executor.add_node(wall_follower)

    try:
        executor.spin()
    finally:
        executor.shutdown()
        wall_follower.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

I hope you can reply as early as possible

Regards,
Ghassan :blush:

Thanks @girishkumar.kannan I solved and finished part 2 it worked well but am sure I will need you in Part 3 :joy: :sweat_smile:
Regards,
Ghassan

Hi @ghassan11 ,

I was just typing out a very long reply! You saved me time!

Glad that you have solved it yourself!

For part 3, please try to solve yourself. Or try to find the issue in the forums, chances are you will find multiple copies of the same issue.
If your issue is truly different, then post here!

Regards,
Girish

1 Like

@girishkumar.kannan
I will try to do it by myself Part 3
Wish you a happy weekend :grin:

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