Problem while launching service server and service client at the same time

Hello,

I am working on the Rosject of the ROS2 In 5 Days Python course. I created and tested both the wall_following node and the wall_finding service separately using the terminal to launch the controller and to call the service server and both scripts are working as expected. However, my current task is to launch both the the service server node and the robot controller (the ‘wall_following’) node at the same time. In addition I have to modify the ‘wall_following’ node in order to create a synchronous service client to call the ‘wall_finding’ service server. But I am having trouble to make the code work. I am not sure what I am doing wrong. But I think there is a problem with the way I am sending the synchronous call in my main function and checking for the response in my ‘motion’ callback function.
My robot controller code is below:

import sys
from threading import Thread

# import python ros client
import rclpy
# import the ROS2 python libraries
from rclpy.node import Node
# import Twist module from geometry_msgs.msg
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
from custom_interface.srv import FindWall
from std_srvs.srv import Empty
# import Quality of Service library, to set the correct profile and reliability to read sensor data.
from rclpy.qos import ReliabilityPolicy, QoSProfile


class Wall_Following(Node):

    def __init__(self):
        # naming the class constructor (node name)
        super().__init__('wall_following')

        # creating client for the /find_wall service server
        self.cli = self.create_client(FindWall, 'find_wall')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        # self.req = FindWall.Request()
        self.req = Empty.Request()

        # creating the publisher object
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        # creating the subscriber object
        self.subscriber_ = self.create_subscription(
            LaserScan,
            '/scan',
            self.read_measurement,
            # is the most used to read LaserScan data and some sensor data.
            QoSProfile(depth=10, reliability=ReliabilityPolicy.BEST_EFFORT)
        )
        # define the timer period for 0.5 seconds
        self.timer_period = 0.5
        # declare variable to store all the laser messages
        self.laser_msg = LaserScan()
        # declare the variable to save the front laser scan reading
        self.forward_laser = 0.0
        # declare the variable to save the right laser scan reading
        self.right_laser = 0.0
        # declare the velocity twist message
        self.cmd = Twist()
        # create time
        self.timer = self.create_timer(self.timer_period, self.motion)

    def stop_robot(self):
        self.get_logger().info('STOPPING ROBOT...')
        self.cmd.linear.x = 0.0
        self.cmd.angular.z = 0.0
        self.publisher_.publish(self.cmd)

    def rotate_left(self):
        self.get_logger().info(
            f'Moving away from right wall - distance to right wall = {self.right_laser} m')
        self.cmd.linear.x = 0.1  # making sure that the robot is not moving forward
        self.cmd.angular.z = 0.2  # rotate counterclockwise to point to the closest wall
        self.publisher_.publish(self.cmd)

    def rotate_right(self):
        self.get_logger().info(
            f'Moving away from right wall - distance to right wall = {self.right_laser} m')
        self.cmd.linear.x = 0.1  # making sure that the robot is not moving forward
        self.cmd.angular.z = -0.2  # rotate counterclockwise to point to the closest wall
        self.publisher_.publish(self.cmd)

    def move_forward(self):
        self.get_logger().info('MOVING ROBOT FORWARD...')
        self.cmd.linear.x = 0.1
        self.cmd.angular.z = 0.0
        self.publisher_.publish(self.cmd)

    def hard_left_rotation(self):
        self.get_logger().info(
            f'Getting close to front wall - changing direction - distance to front wall = {self.forward_laser} m')
        self.cmd.linear.x = 0.05
        self.cmd.angular.z = 0.4
        self.publisher_.publish(self.cmd)

    def send_request(self):
        self.future = self.cli.call(self.req)

    def motion(self):

        if self.future.result():
            # check if the robot is far from the front wall
            if self.forward_laser > 0.5:
                # if the robot is far from the front wall and far from the right wall, move closer to the right wall
                if self.right_laser > 0.3:
                    self.rotate_right()
                # if the robot is far from the front wall and close to the right wall, move away from the right wall
                elif self.right_laser < 0.3:
                    self.rotate_left()
                # if the robot is far from the front wall is at a distance between 0.2 m and 0.3 m from the right wall, keep moving forward
                else:
                    self.move_forward()
            # if the robot is close to a front wall, increase angular velocity while still going forward
            else:
                self.hard_left_rotation()

            self.get_logger().info(
                f'Front laser measurement : {self.forward_laser} m')
            self.get_logger().info(
                f'Right laser measurement : {self.right_laser} m')
        else:
            self.get_logger().info('Server response is FALSE')

    def read_measurement(self, msg):
        self.laser_msg = msg
        self.forward_laser = self.laser_msg.ranges[359]
        self.right_laser = self.laser_msg.ranges[179]


def main(args=None):
    # initialize the ROS communication
    rclpy.init(args=args)
    # declare the node constructor
    wall_following = Wall_Following()

    spin_thread = Thread(target=rclpy.spin, args=(wall_following,))
    spin_thread.start()

    wall_following.send_request()

    wall_following.destroy_node()
    # shutdown the ROS communication
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Moreover, my launch file is the following:

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='wall_follower',
            executable='wall_finding',
            output='screen',
            arguments=['--ros-args', '--log-level', 'info']),

        Node(
            package='wall_follower',
            executable='wall_following_v2',
            output='screen',
            arguments=['--ros-args', '--log-level', 'info'])
    ])

And, this is the terminal output when I launch the launch file:

ros2 launch wall_follower main.launch.py
[INFO] [launch]: All log files can be found below /home/user/.ros/log/2022-12-25-11-32-42-413569-3_xterm-9801
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [wall_finding-1]: process started with pid [9898]
[INFO] [wall_following_v2-2]: process started with pid [9902]
[wall_finding-1] [INFO] [1671967965.949427122] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967966.243057828] [find_wall_service]: read laser measurement
[wall_following_v2-2] Traceback (most recent call last):
[wall_following_v2-2]   File "/home/user/ros2_ws/install/wall_follower/lib/wall_follower/wall_following_v2", line33, in <module>
[wall_following_v2-2]     sys.exit(load_entry_point('wall-follower==0.0.0', 'console_scripts', 'wall_following_v2')())
[wall_following_v2-2]   File "/home/user/ros2_ws/install/wall_follower/lib/python3.8/site-packages/wall_follower/wall_following_v2.py", line 174, in main
[wall_following_v2-2]     wall_following.send_request()
[wall_following_v2-2]   File "/home/user/ros2_ws/install/wall_follower/lib/python3.8/site-packages/wall_follower/wall_following_v2.py", line 132, in send_request
[wall_following_v2-2]     self.future = self.cli.call(self.req)
[wall_following_v2-2]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/client.py", line 80, in call
[wall_following_v2-2]     raise TypeError()
[wall_following_v2-2] TypeError
[wall_following_v2-2] Exception in thread Thread-1:
[wall_following_v2-2] Traceback (most recent call last):
[wall_following_v2-2]   File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
[wall_following_v2-2]     self.run()
[wall_following_v2-2]   File "/usr/lib/python3.8/threading.py", line 870, in run
[wall_following_v2-2]     self._target(*self._args, **self._kwargs)
[wall_following_v2-2]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/__init__.py", line 191, in spin
[wall_following_v2-2]     executor.spin_once()
[wall_following_v2-2]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 711, in spin_once
[wall_following_v2-2]     raise handler.exception()
[wall_following_v2-2]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/task.py", line 239, in __call__
[wall_following_v2-2]     self._handler.send(None)
[wall_following_v2-2]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 426, in handler
[wall_following_v2-2]     await call_coroutine(entity, arg)
[wall_following_v2-2]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 340, in _execute_timer
[wall_following_v2-2]     await await_or_execute(tmr.callback)
[wall_following_v2-2]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 118, in await_or_execute
[wall_following_v2-2]     return callback(*args)
[wall_following_v2-2]   File "/home/user/ros2_ws/install/wall_follower/lib/python3.8/site-packages/wall_follower/wall_following_v2.py", line 136, in motion
[wall_following_v2-2]     if self.future.result():
[wall_following_v2-2] AttributeError: 'Wall_Following' object has no attribute 'future'
[wall_finding-1] [INFO] [1671967966.762792541] [find_wall_service]: read laser measurement
[ERROR] [wall_following_v2-2]: process has died [pid 9902, exit code 1, cmd '/home/user/ros2_ws/install/wall_follower/lib/wall_follower/wall_following_v2 --ros-args --log-level info --ros-args'].
[wall_finding-1] [INFO] [1671967967.110928746] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967967.442750430] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967967.744603054] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967968.077909535] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967968.373987608] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967968.648504300] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967969.051760631] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967969.335983051] [find_wall_service]: read laser measurement
[wall_finding-1] [INFO] [1671967969.622519798] [find_wall_service]: read laser measurement
^C[WARNING] [launch]: user interrupted with ctrl-c (SIGINT)
[wall_finding-1] Traceback (most recent call last):
[wall_finding-1]   File "/home/user/ros2_ws/install/wall_follower/lib/wall_follower/wall_finding", line 33, in <module>
[wall_finding-1]     sys.exit(load_entry_point('wall-follower==0.0.0', 'console_scripts', 'wall_finding')())
[wall_finding-1]   File "/home/user/ros2_ws/install/wall_follower/lib/python3.8/site-packages/wall_follower/wall_finder.py", line 148, in main
[wall_finding-1]     executor.spin()
[wall_finding-1]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 290, in spin
[wall_finding-1]     self.spin_once()
[wall_finding-1]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 756, in spin_once
[wall_finding-1]     self._spin_once_impl(timeout_sec)
[wall_finding-1]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 742, in _spin_once_impl
[wall_finding-1]     handler, entity, node = self.wait_for_ready_callbacks(
[wall_finding-1]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 689, in wait_for_ready_callbacks
[wall_finding-1]     return next(self._cb_iter)
[wall_finding-1]   File "/opt/ros/foxy/lib/python3.8/site-packages/rclpy/executors.py", line 586, in _wait_for_ready_callbacks
[wall_finding-1]     _rclpy.rclpy_wait(wait_set, timeout_nsec)
[wall_finding-1] KeyboardInterrupt
[ERROR] [wall_finding-1]: process has died [pid 9898, exit code -2, cmd '/home/user/ros2_ws/install/wall_follower/lib/wall_follower/wall_finding --ros-args --log-level info --ros-args'].

Thank you for your help

Hi @e.na.hatem ,

After going through your code, I firstly noticed the following line:

Are you experiencing the problem because you have the wrong line commented? You should use the FindWall service message. You are using Empty instead.

I also noticed that you are running the wall_following node as a separate thread. You need not do that if you make the service client as a node. So you would be able to add wall_following and findwall_service_client as two nodes into the MultiThreadedExecutor.
(You are also not using MultiThreadedExecutor - this is very important as you will require a minimum of 4 threads for the main program).

Why does your launch file have 2 versions of wall_follower? It should just have the wall_follower and the findwall_service_server programs (as of now). Later your launch file will also include the action_server node.

I did not go through your output much - I saw your code and expected to see a few errors - so that is not surprising.

Summary:

  1. Change service request message type and call service properly.
  2. Modify the main code to include MultiThreadedExecutor.
  3. Individually check if a service call to FindWall_Service_Server is working properly - call multiple times one-after-another (not simultaneously - since the service is synchronous).

Sorry, I could not help you much but just give you some tips form my experience.
Best way to check if your code is working is to check if each part is working individually.
Especially for service, create a service server and a client - to just test the service functionality.

Regards,
Girish

1 Like

Hello @girishkumar.kannan ,

Thank you for your response.

For this part of the code, I was experimenting with the Empty service interface since I was obtaining an error with the custom FindWall service interface. But in the end I got the same error with the Empty service interface too. So I modified the code again as you mentioned and I am now using the custom FindWall service interface.

Moreover, I was running the wall_following node as a separate thread because I was following the ROS2 tutorial for Synchronous vs Asynchronous service clients and this is how they did synchronous calls. I remodified the code as you mentioned and I am now using the MultiThreadedExecutor for the wall_following service client node as well (I was using it for the findwall_service_server node only since the code was getting stuck in the while loop and the laser measurements were not being updated in the service server callback function). However, I didn’t really understand why I would need to use the MultiThreadedExecutor for the service client node (wall_following). But either way, I did it just to test the code to see the results.

My launch file currently launches the wall_finding service server node and the wall_following_v2 service client node. I ended the name of the script with _v2 because that way I can still have a copy of the wall_following script that I created for the first section of the Rosject.

In addition, I tested the FindWall_Service_Server and I think the code works but it’s not very consistent (the control law is based on the closest measurement index coming from the laser sensor measurements which may overshoot from time to time even with + or - 3 degrees of error allowance). I have to develop it more to make it more robust (by using statistical methods to find the closest wall as you mentioned to me earlier in a previous post, is there any tutorial that you know of that can help me with this task ?) but for the moment I think it’s good enough to test the service server node.

So, I launched the service server and the service client nodes with the modifications as you mentioned and now the robot is currently using the service server to find the wall. However, the robot doesn’t start to follow the wall after the response from the service server is done. And, I am not really sure why. I added the motion callback function to the MultiThreadedExecutor but it is as if the callback function is not being run because I am not obtaining anything from the get_logger() in the terminal. You can find the modified code of the service client node below:

import sys
from threading import Thread

# import python ros client
import rclpy
# import the ROS2 python libraries
from rclpy.node import Node
# import Twist module from geometry_msgs.msg
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
from custom_interface.srv import FindWall
from std_srvs.srv import Empty
# import Quality of Service library, to set the correct profile and reliability to read sensor data.
from rclpy.qos import ReliabilityPolicy, QoSProfile
from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor


class Wall_Following(Node):

    def __init__(self):
        # naming the class constructor (node name)
        super().__init__('wall_following')

        self.group = ReentrantCallbackGroup()

        # creating client for the /find_wall service server
        self.cli = self.create_client(
            FindWall, 'find_wall', callback_group=self.group)
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.req = FindWall.Request()
        # self.req = Empty.Request()

        # creating the publisher object
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        # creating the subscriber object
        self.subscriber_ = self.create_subscription(
            LaserScan,
            '/scan',
            self.read_measurement,
            # is the most used to read LaserScan data and some sensor data.
            QoSProfile(depth=10, reliability=ReliabilityPolicy.BEST_EFFORT),
            callback_group=self.group
        )
        # define the timer period for 0.5 seconds
        self.timer_period = 0.5
        # declare variable to store all the laser messages
        self.laser_msg = LaserScan()
        # declare the variable to save the front laser scan reading
        self.forward_laser = 0.0
        # declare the variable to save the right laser scan reading
        self.right_laser = 0.0
        # declare the velocity twist message
        self.cmd = Twist()
        # create time
        self.timer = self.create_timer(
            self.timer_period, self.motion, callback_group=self.group)

    def stop_robot(self):
        self.get_logger().info('STOPPING ROBOT...')
        self.cmd.linear.x = 0.0
        self.cmd.angular.z = 0.0
        self.publisher_.publish(self.cmd)

    def rotate_left(self):
        self.get_logger().info(
            f'Moving away from right wall - distance to right wall = {self.right_laser} m')
        self.cmd.linear.x = 0.1  # making sure that the robot is not moving forward
        self.cmd.angular.z = 0.2  # rotate counterclockwise to point to the closest wall
        self.publisher_.publish(self.cmd)

    def rotate_right(self):
        self.get_logger().info(
            f'Moving away from right wall - distance to right wall = {self.right_laser} m')
        self.cmd.linear.x = 0.1  # making sure that the robot is not moving forward
        self.cmd.angular.z = -0.2  # rotate counterclockwise to point to the closest wall
        self.publisher_.publish(self.cmd)

    def move_forward(self):
        self.get_logger().info('MOVING ROBOT FORWARD...')
        self.cmd.linear.x = 0.1
        self.cmd.angular.z = 0.0
        self.publisher_.publish(self.cmd)

    def hard_left_rotation(self):
        self.get_logger().info(
            f'Getting close to front wall - changing direction - distance to front wall = {self.forward_laser} m')
        self.cmd.linear.x = 0.05
        self.cmd.angular.z = 0.4
        self.publisher_.publish(self.cmd)

    def send_request(self):
        self.get_logger().info('SENDING REQUEST...')
        self.future = self.cli.call(self.req)

    def motion(self):

        if self.future.result() is True:
            # check if the robot is far from the front wall
            if self.forward_laser > 0.5:
                # if the robot is far from the front wall and far from the right wall, move closer to the right wall
                if self.right_laser > 0.3:
                    self.rotate_right()
                # if the robot is far from the front wall and close to the right wall, move away from the right wall
                elif self.right_laser < 0.3:
                    self.rotate_left()
                # if the robot is far from the front wall is at a distance between 0.2 m and 0.3 m from the right wall, keep moving forward
                else:
                    self.move_forward()
            # if the robot is close to a front wall, increase angular velocity while still going forward
            else:
                self.hard_left_rotation()

            self.get_logger().info(
                f'Front laser measurement : {self.forward_laser} m')
            self.get_logger().info(
                f'Right laser measurement : {self.right_laser} m')
        else:
            self.get_logger().info('Server response is FALSE')

    def read_measurement(self, msg):
        self.laser_msg = msg
        self.forward_laser = self.laser_msg.ranges[359]
        self.right_laser = self.laser_msg.ranges[179]


# def main(args=None):
#     # initialize the ROS communication
#     rclpy.init(args=args)
#     # declare the node constructor
#     wall_following = Wall_Following()

#     spin_thread = Thread(target=rclpy.spin, args=(wall_following,))
#     spin_thread.start()

#     wall_following.send_request()

#     wall_following.destroy_node()
#     # shutdown the ROS communication
#     rclpy.shutdown()

def main(args=None):  # check the main function of a service node
    rclpy.init(args=args)
    try:
        wall_following = Wall_Following()
        wall_following.send_request()
        executor = MultiThreadedExecutor(num_threads=6)
        executor.add_node(wall_following)
        try:
            executor.spin()
        finally:
            executor.shutdown()
            wall_following.destroy_node()

    finally:
        rclpy.shutdown()


if __name__ == '__main__':
    main()

You can also find the final output from the terminal as well as an image showing the final position of the robot after the service client response is received:

[wall_finding-1] [INFO] [1672141465.425518444] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.426367581] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.427212142] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.429122142] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.511971553] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.520218935] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.522537664] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.523457028] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.525037160] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.525851671] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.533478629] [find_wall_service]: Closest measurement index = 197
[wall_finding-1] [INFO] [1672141465.534953856] [find_wall_service]: Closest measurement index = 180
[wall_finding-1] [INFO] [1672141465.536143311] [find_wall_service]: The closest wallis currently on the right - Stopping robot...
[wall_finding-1] [INFO] [1672141465.537564975] [find_wall_service]: STOPPING ROBOT...

I am not really sure what I am still doing wrong though. Do you think there is still a problem with my main() function ?

Thank you

Hi @e.na.hatem ,

Good! :+1:

No, you don’t have to use MultiThreadedExecutor. In fact, since the service is synchronous, the client can call the service only once. Only after the service finishes, another call from the client can be made.

Ok, now I understand what you have done. I did this rosject differently, so I got a little confused.

That is actually what I implemented when I did this project. I basically used the laser scan readings from the front of the robot to determine if the robot is facing an obstacle or a wall.

So now, coming to your code:
After you send a request to the FindWall Service, you must wait for the service to complete and return you the result - until then, your callback should not start.
The best way to check this is to implement a boolean flag that checks if the service is completed.
Something like (pseudocode):

call findwall service
if findwall.result = False:
    sleep(1.0)
else:
    start wall_following

Remember: Callbacks, as the name states, gets called back every time. So do not try to have a while loop inside callbacks. They will only block your code.

Nothing big, but you should not put wall_following.send_request() inside main(). This line should be in your motion() callback.

Also, you need to define ReentrantCallbackGroup() separately for each callback function.
So if you have, for example, 3 callbacks, then you will have:

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

You should not re-use the same callback group. This is a (beginner’s) mistake - even I did this mistake and learned from it.

Regards,
Girish

1 Like

Thank you for your explanation @girishkumar.kannan !
I made some progress with my code thanks to you. The request is now being sent to the service server from the callback function and the robot performs the wall finding procedure. However, there is still something I am doing wrong because the wall following procedure is not starting.

I was sending the request with the following line of code:

self.future = self.cli.call(self.req)

Then, I was checking the result to see if I should start the wall following procedure as follows:

if self.future.result() is True:
     self.wall_following()

However, I was getting an error stating that ‘future is not an attribute of the class’.

So, I defined the future variable in the class constructor as follows:

self.future = FindWall.Response()

And I am checking the for the server response as follows:

 if self.future.result() is False:
            self.get_logger().info('Service response not received yet')
            time.sleep(1)
        else:
            self.wall_following()

And, when I run the code now, I get the following error after 10-20 seconds of the robot being ready for the wall following procedure:

[wall_finding-1] [INFO] [1672474115.768644655] [find_wall_service]: The closest wall is currently on the right - Stopping robot...
[wall_finding-1] [INFO] [1672474115.770155218] [find_wall_service]: STOPPING ROBOT...
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
[wall_following_v2-2] The following exception was never retrieved: 'FindWall_Response' object has no attribute 'result'
^C[WARNING] [launch]: user interrupted with ctrl-c (SIGINT)

I should point out that I did not receive the message “Service response not received yet” that should be sent to the terminal in case the response of the service server is False.

I am not sure what I am doing wrong. Should I declare the future variable in the constructor as a different variable ? Or is my way of checking for the response of the service server incorrect ?

You can also find my modified code below:

import sys
import time
from threading import Thread

# import python ros client
import rclpy
# import the ROS2 python libraries
from rclpy.node import Node
# import Twist module from geometry_msgs.msg
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
from custom_interface.srv import FindWall
from std_srvs.srv import Empty
# import Quality of Service library, to set the correct profile and reliability to read sensor data.
from rclpy.qos import ReliabilityPolicy, QoSProfile
from rclpy.callback_groups import ReentrantCallbackGroup, MutuallyExclusiveCallbackGroup
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor


class Wall_Following(Node):

    def __init__(self):
        # naming the class constructor (node name)
        super().__init__('wall_following')

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

        # creating client for the /find_wall service server
        self.cli = self.create_client(
            FindWall, 'find_wall', callback_group=self.group1)
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.req = FindWall.Request()
        self.future = FindWall.Response()

        # creating the publisher object
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        # creating the subscriber object
        self.subscriber_ = self.create_subscription(
            LaserScan,
            '/scan',
            self.read_measurement,
            # is the most used to read LaserScan data and some sensor data.
            QoSProfile(depth=10, reliability=ReliabilityPolicy.BEST_EFFORT),
            callback_group=self.group2
        )
        # define the timer period for 0.5 seconds
        self.timer_period = 0.5
        # declare variable to store all the laser messages
        self.laser_msg = LaserScan()
        # declare the variable to save the front laser scan reading
        self.forward_laser = 0.0
        # declare the variable to save the right laser scan reading
        self.right_laser = 0.0
        # declare the velocity twist message
        self.cmd = Twist()
        # create time
        self.timer = self.create_timer(
            self.timer_period, self.motion, callback_group=self.group3)
        # create Boolean for synchronous request
        self.request_sent = False

    def stop_robot(self):
        self.get_logger().info('STOPPING ROBOT...')
        self.cmd.linear.x = 0.0
        self.cmd.angular.z = 0.0
        self.publisher_.publish(self.cmd)

    def rotate_left(self):
        self.get_logger().info(
            f'Moving away from right wall - distance to right wall = {self.right_laser} m')
        self.cmd.linear.x = 0.1  # making sure that the robot is not moving forward
        self.cmd.angular.z = 0.2  # rotate counterclockwise to point to the closest wall
        self.publisher_.publish(self.cmd)

    def rotate_right(self):
        self.get_logger().info(
            f'Moving away from right wall - distance to right wall = {self.right_laser} m')
        self.cmd.linear.x = 0.1  # making sure that the robot is not moving forward
        self.cmd.angular.z = -0.2  # rotate counterclockwise to point to the closest wall
        self.publisher_.publish(self.cmd)

    def move_forward(self):
        self.get_logger().info('MOVING ROBOT FORWARD...')
        self.cmd.linear.x = 0.1
        self.cmd.angular.z = 0.0
        self.publisher_.publish(self.cmd)

    def hard_left_rotation(self):
        self.get_logger().info(
            f'Getting close to front wall - changing direction - distance to front wall = {self.forward_laser} m')
        self.cmd.linear.x = 0.05
        self.cmd.angular.z = 0.4
        self.publisher_.publish(self.cmd)

    def send_request(self):
        self.get_logger().info('SENDING REQUEST...')
        self.future = self.cli.call(self.req)

    def wall_following(self):

        self.get_logger().info('ENTERING WALL_FOLLOWING FUNCTION...')
        # check if the robot is far from the front wall
        if self.forward_laser > 0.5:
            # if the robot is far from the front wall and far from the right wall, move closer to the right wall
            if self.right_laser > 0.3:
                self.rotate_right()
            # if the robot is far from the front wall and close to the right wall, move away from the right wall
            elif self.right_laser < 0.3:
                self.rotate_left()
            # if the robot is far from the front wall is at a distance between 0.2 m and 0.3 m from the right wall, keep moving forward
            else:
                self.move_forward()
            # if the robot is close to a front wall, increase angular velocity while still going forward
        else:
            self.hard_left_rotation()

        # self.get_logger().info(
        #     f'Front laser measurement : {self.forward_laser} m')
        # self.get_logger().info(
        #     f'Right laser measurement : {self.right_laser} m')

    def motion(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()

        if self.future.result() is False:
            self.get_logger().info('Service response not received yet')
            time.sleep(1)
        else:
            self.wall_following()

    def read_measurement(self, msg):
        self.laser_msg = msg
        self.forward_laser = self.laser_msg.ranges[359]
        self.right_laser = self.laser_msg.ranges[179]


# def main(args=None):
#     # initialize the ROS communication
#     rclpy.init(args=args)
#     # declare the node constructor
#     wall_following = Wall_Following()

#     spin_thread = Thread(target=rclpy.spin, args=(wall_following,))
#     spin_thread.start()

#     wall_following.send_request()

#     wall_following.destroy_node()
#     # shutdown the ROS communication
#     rclpy.shutdown()

def main(args=None):  # check the main function of a service node
    rclpy.init(args=args)
    try:
        wall_following = Wall_Following()
        executor = MultiThreadedExecutor(num_threads=4)
        executor.add_node(wall_following)
        try:
            executor.spin()
        finally:
            executor.shutdown()
            wall_following.destroy_node()

    finally:
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Thanks again!

Hi @e.na.hatem ,

Since you have already declared the service response with the following line:

self.future = FindWall.Response()

This is correct, by the way!

All you have to do to get the result is (in your case):

# self.future.result() will not work !!!
result = self.future.wallfound # this is either True or False

So the way you would check is also simple:

# if self.future.result() is False: # delete this line
if not self.future.wallfound: # use this line instead
    self.get_logger().info('Service response not received yet')
    time.sleep(1)
else:
    self.wall_following()

I hope this gets your problem fixed.

Let me know if you still have issues.

Regards,
Girish

PS: When your error says that 'FindWall_Response' object has no attribute 'result' - it literally means that your object has no such variable/attribute !

Some Additional Advice:

  1. You do not have to name the variables exactly as specified in tutorial notes. You can use self.result instead of self.future for your service result.
  2. You do not have to copy-paste my example lines “as-is”. You can rename self.group1 as self.client_cbg (cbg is short for call back group). self.group2 as self.scan_sub_cbg and self.group3 as self.timer_cbg
  3. Also you do not have to declare the groups with name. You can directly do self.cli = self.create_client(FindWall, 'find_wall', callback_group=ReentrantCallbackGroup())
  4. You can extend your main() function’s exception handling block to a try-except-finally from just try-finally - this will help you print something in case you have an exception in the try block.

That’s all for now! :smile:

1 Like

Thank you so much for your help @girishkumar.kannan the code finally works now!
I just need some clarification for when we should set each callback function in a node to a different ReentrantCallbackGroup(). Is it best practice to always use
the MultiThreadedExecutor ?

Thank you

Hi @e.na.hatem ,

Glad you got it working!

Refer this link: Using Callback Groups — ROS 2 Documentation: Foxy documentation

You have two callback group types:

  1. MutuallyExclusiveCallbackGroup: More like serialized (non-parallel, a.k.a, sequential), that gets executed one after another and not simultaneously.
    Use this if you have callbacks that need not be run together, that is, when your main program is not dependent on more than one callback at any point of time.
    A (valid) example: a service callback followed by a odometry callback.
  2. ReentrantCallbackGroup: This is parallel (non-sequential), that gets executed together (although it gets polled since there is no true parallelism in computation [I believe]).
    Use this if you have callbacks that needs to be run together, that is, when your main program is dependent on more than one callback at any point of time.
    A (valid) example: a laser scan callback and odometry callback.

I believe, each of the above two groups is a global group, more like thread containers. So when you allocate one callback from one or any node, they get added into that global set of callbacks and gets called back when required.
Therefore, you specify one callback group type for each callback you define in your program.
[This is what I believe, although I may be wrong or not exactly right.]

Not exactly. If you have just one timer callback (for publisher) and one subscriber callback where the rate of publishing is not too fast and the rate of subscription is also not too fast, you can work without MultiThreadedExecutor. But if you do not use MultiThreadedExecutor, your program chooses SingleThreadedExecutor by default, even if you do not explicitly specify SingleThreadedExecutor anywhere in your code.

But, when you have more than 2 callbacks, specifically, when you want your callbacks to run in parallel, then the only best option is to use MultiThreadedExecutor. [Again, this is what I believe, I may be wrong.]

I hope I clarified your doubts.

Let me know if you are still unclear of something.

Regards,
Girish

1 Like

Thank you so much for your help @girishkumar.kannan !

1 Like

I found this thread after trying to troubleshoot my own rosject at the same step as @e.na.hatem. I think that I’ve implemented the suggested changes from @girishkumar.kannan, but something is still missing. I would expect to receive a message on the terminal during this method:

    def send_request(self):
        self.result = self.client.call(self.req)
        if not self.result.wallfound: 
            self.get_logger().info('Service response not received yet')
            time.sleep(1)
        else:
            self.wall_hugger()

However, don’t see anything on the terminal. The wall_follower.py correctly makes a request to /find_wall, but after wall_finder.py has finished, wall_follower.py doesn’t move on to the movement logic for the actual wall following.

wall_follower.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
import time
from findwall.srv import FindWall


class wall_hugger(Node):
    def __init__(self):
        super().__init__('wall_hugger_node')
        #create publisher object
        self.publisher_ = self.create_publisher(Twist, 'cmd_vel', 10)
        
        #create subscriber object
        self.subscriber = self.create_subscription(LaserScan, 'scan', self.Laser_Scan_callback, QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE))
        
        # define the timer period for 0.5 seconds
        self.timer_period = 0.5
        
        # Create client for FindWall Service
        self.client = self.create_client(FindWall, 'find_wall')

        # 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 variable to save the received info
        self.front_laser = 0.0
        
        
        self.right_laser = 0.0
        self.right_laser_255 = 0.0
        self.right_laser_285 = 0.0

        self.left_laser = 0.0
        
        self.back_laser = 0.0
        
        #difference between the lasers on 15 deg tilt to determine robot orientation w.r.t. wall
        self.right_laser_diff = 0.0
        
        #angle between the right wall and the forward motion of the robot.  0 deg means moving // with the wall
        self.theta_wall = 0.0
        
        #turning correction factor
        self.correction_factor = 0
        
        #average distance from the right wall
        self.right_avg_dist = 0
        
        # create a Twist message
        self.movement = Twist()
        self.timer = self.create_timer(self.timer_period, self.motion)
        
        # Flag to indicate if the service has completed
        #self.service_done = False  

        self.req = FindWall.Request()
        self.result = FindWall.Response()
        
        

    def send_request(self):
        self.result = self.client.call(self.req)
        if not self.result.wallfound: 
            self.get_logger().info('Service response not received yet')
            time.sleep(1)
        else:
            self.wall_hugger()
            


        
    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[360]
        # self.right_laser = msg.ranges[180]
        # self.left_laser = msg.ranges[540]
        # self.back_laser = msg.ranges[0]
        # self.right_laser_285 = msg.ranges[195]
        # self.right_laser_255 = msg.ranges[165]

    def motion(self):
        if self.result.wallfound:  # 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.5:
                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.265 and self.right_avg_dist < 0.3 and self.right_avg_dist > 0.22:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(-0.2)
            #If angled towards the wall and within the sweet spot 
            elif self.right_laser_diff < 0.00 and self.right_avg_dist > 0.265 and self.right_avg_dist < 0.3 and self.right_avg_dist > 0.22:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(0.2)    
            #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 = 0.2
            #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 = 0.0                
            #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 = -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 = 0.0                  
            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)
    wall_hugger_node = wall_hugger()
    wall_hugger_node.send_request()  # Call the service
    wall_hugger_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Could you point me in the right direction?

Thank you.

Hi @pharva ,

Glad to know that you found my post helpful!

Ok, so the wall_follower program makes the correct request to /find_wall service.
I am assuming here that your /find_wall service also works, since you said wall_finder.py finished.
Now, since the wall_follower.py does not move the robot, I have some questions for you:

  1. Does the wall_finder.py program return the service response to your wall_follower.py program after the service has completed?
  2. You don’t have a self.wall_hugger() function in your code, should that be changed to self.motion()? Because wall_hugger is your class name and not a function name in your program.
    def send_request(self):
        self.result = self.client.call(self.req)
        if not self.result.wallfound: 
            self.get_logger().info('Service response not received yet')
            time.sleep(1)
        else:
            # self.wall_hugger() # <--- this line is wrong
            self.motion() # <--- this line is correct

Let me know if this fixes your problem.

Regards,
Girish

  1. I think it does. Inside the custom_service_callback starting on line 29. I set response.wallfind = self.ready, which is a variable that I’ve set to be True when the end of my movement logic is complete.

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
import sys

# 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.srv = self.create_service(FindWall, 'find_wall', self.custom_service_callback)
        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))
        self.aligned = False
        self.approach = False
        self.ready = False
        self.service_called = False

    def custom_service_callback(self, request, response):
        # Logic to handle service request and set response
        self.service_called = True

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

    def laser_scan_callback(self, msg):
        if self.service_called == True:
        
            self.movement = Twist()
            
            ## 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]

            self.left_laser = msg.ranges[90]
            self.left_laser_75 = msg.ranges[75]
            self.left_laser_105 = msg.ranges[105]
       
            self.back_laser = msg.ranges[180]

            self.laser_min = min(msg.ranges)
            self.laser_max = max(msg.ranges)
            
            # Real Bot Laser Ranges
            # self.front_laser = msg.ranges[360]
            # self.front_laser_10 = msg.ranges[370]
            # self.front_laser_350 = msg.ranges[350]

            # self.right_laser = msg.ranges[180]
            # self.right_laser_285 = msg.ranges[195]
            # self.right_laser_255 = msg.ranges[165]

            # self.left_laser = msg.ranges[540]
            # self.left_laser_75 = msg.ranges[525]
            # self.left_laser_105 = msg.ranges[555]
       
            # self.back_laser = msg.ranges[0]

            # 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('Min Range "%s"' % str(self.laser_min))
            #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(self.aligned))
            self.get_logger().info('Approach Status "%s"' % str(self.approach))
            self.get_logger().info('Ready Status "%s"' % str(self.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 self.aligned == False and self.theta_front_wall > 45 and self.theta_front_wall < -45:
                self.movement.angular.z = 0.3
            elif self.aligned == False and self.theta_front_wall < 45 and self.theta_front_wall > 5:
                self.movement.angular.z = self.correction_factor_front*(-0.2)
            elif self.aligned == False and self.theta_front_wall > -45 and self.theta_front_wall < -5:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            else:
                self.movement.angular.z = 0.0
                self.aligned = True
            
            if self.aligned == True and self.approach == False and self.front_laser > 0.5:
                self.movement.linear.x = 0.05
            elif self.aligned == True and self.approach == False and self.front_laser <= 0.5:
                self.movement.linear.x = 0.0
                self.approach = True

            if self.approach == True and self.right_laser*0.70 > self.laser_min:
                self.movement.angular.z = 0.3
            elif self.approach == True and self.right_laser*0.95 > self.laser_min:
                self.movement.angular.z = self.correction_factor_right*(0.2)
            elif self.approach == True:
                self.movement.angular.z = 0.0
                self.ready = True
                
            # Check if ready to shut down
            if self.ready == True:
                self.get_logger().info("Bot in position.")
                self.get_logger().info('Ready Status "%s"' % str(self.ready))
                self.service_called = False
                
                
                
            # Publishing the cmd_vel values to a Topic
            self.publisher_.publish(self.movement)
            
     

        

def main(args=None):
    rclpy.init(args=args)
    service = WallFinderService()
    rclpy.spin(service)
    WallFinderService.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

  1. Ah, yes. You’re right. I’ll fix that.

Even though the “else” statement was wrong, shouldn’t I still be seeing “Service response not received yet” in the terminal?

Hi @pharva ,

That’s because your service client code is getting blocked.
You have not implemented MultiThreadedExecutor along with ReentrantCallbackGroup.
What is happening in your code is that, the Laser Scan callback is blocking your Service Client Callbacks.
Therefore, your program is unable to allocate system resources for your service callback.
Make laser scan and service callbacks as ReentrantCallbacks and use a MultiThreadedExecutor, your program should work fine (if everything else is correct).

Regards,
Girish

I think that I’ve implemented the MultiThreadExecutor and ReentrantCallbackGroup correctly. I think I’m getting closer to the solution. I’ve noticed that the findwall.srv boolean “wallfound” is not consistent between the wall_finder.py and wall_following.py.

In one terminal I launch the wall_finder.py and it waits to have a request sent to it via wall_follower.py. wall_finder outputs messages to let me know where it’s at in the process. When the robot is aligned, it tells me the value of the findwall.srv boolean “wallfound.”

[wall_finder-1] [INFO] [1708632699.605161396] [find_wall]: Aligned Status "True"
[wall_finder-1] [INFO] [1708632699.605589549] [find_wall]: Approach Status "True"
[wall_finder-1] [INFO] [1708632699.606051218] [find_wall]: Ready Status "False"
[wall_finder-1] [INFO] [1708632699.606660228] [find_wall]: Bot in position.
[wall_finder-1] [INFO] [1708632699.607421664] [find_wall]: Ready Status "True"
[wall_finder-1] [INFO] [1708632699.607994525] [find_wall]: wallfound "findwall.srv.FindWall_Response(wallfound=True)"

In another terminal, I launch the wall_follower.py which calls wall_finder and waits for the wallfound boolean to be True before executing the motion() method. What’s confusing to me now is that while both terminals are running simultaneously, wallfound value is different in each terminal.

[wall_following-1] [INFO] [1708632726.628005000] [wall_hugger_node]: wallfound "findwall.srv.FindWall_Response(wallfound=False)"
[wall_following-1] [INFO] [1708632726.628612399] [wall_hugger_node]: Service response not received yet
[wall_following-1] [INFO] [1708632727.128237055] [wall_hugger_node]: wallfound "findwall.srv.FindWall_Response(wallfound=False)"
[wall_following-1] [INFO] [1708632727.128945573] [wall_hugger_node]: Service response not received yet

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
import sys

# 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.srv = self.create_service(FindWall, 'find_wall', self.custom_service_callback)
        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))
        self.aligned = False
        self.approach = False
        self.ready = False
        self.service_called = False
        self.response = FindWall.Response()

    def custom_service_callback(self, request, response):
        # Logic to handle service request and set response
        self.service_called = True

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

    def laser_scan_callback(self, msg):
        if self.service_called == True:
        
            self.movement = Twist()
            
            ## 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]

            self.left_laser = msg.ranges[90]
            self.left_laser_75 = msg.ranges[75]
            self.left_laser_105 = msg.ranges[105]
       
            self.back_laser = msg.ranges[180]

            self.laser_min = min(msg.ranges)
            self.laser_max = max(msg.ranges)
            
            # Real Bot Laser Ranges
            # self.front_laser = msg.ranges[360]
            # self.front_laser_10 = msg.ranges[370]
            # self.front_laser_350 = msg.ranges[350]

            # self.right_laser = msg.ranges[180]
            # self.right_laser_285 = msg.ranges[195]
            # self.right_laser_255 = msg.ranges[165]

            # self.left_laser = msg.ranges[540]
            # self.left_laser_75 = msg.ranges[525]
            # self.left_laser_105 = msg.ranges[555]
       
            # self.back_laser = msg.ranges[0]

            # 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('Min Range "%s"' % str(self.laser_min))
            #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(self.aligned))
            self.get_logger().info('Approach Status "%s"' % str(self.approach))
            self.get_logger().info('Ready Status "%s"' % str(self.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 self.aligned == False and self.theta_front_wall > 45 and self.theta_front_wall < -45:
                self.movement.angular.z = 0.3
            elif self.aligned == False and self.theta_front_wall < 45 and self.theta_front_wall > 5:
                self.movement.angular.z = self.correction_factor_front*(-0.2)
            elif self.aligned == False and self.theta_front_wall > -45 and self.theta_front_wall < -5:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            else:
                self.movement.angular.z = 0.0
                self.aligned = True
            
            if self.aligned == True and self.approach == False and self.front_laser > 0.5:
                self.movement.linear.x = 0.05
            elif self.aligned == True and self.approach == False and self.front_laser <= 0.5:
                self.movement.linear.x = 0.0
                self.approach = True

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

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

        

def main(args=None):
    rclpy.init(args=args)
    service = WallFinderService()
    rclpy.spin(service)
    WallFinderService.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
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.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
        
        

    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()
        
        

        if self.response.wallfound is False:
            self.get_logger().info('wallfound "%s"' % str(self.response))
            self.get_logger().info('Service response not received yet')
            time.sleep(1)
        else:
            self.motion()

        
    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[360]
        # self.right_laser = msg.ranges[180]
        # self.left_laser = msg.ranges[540]
        # self.back_laser = msg.ranges[0]
        # self.right_laser_285 = msg.ranges[195]
        # self.right_laser_255 = msg.ranges[165]

    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.5:
                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.265 and self.right_avg_dist < 0.3 and self.right_avg_dist > 0.22:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(-0.2)
            #If angled towards the wall and within the sweet spot 
            elif self.right_laser_diff < 0.00 and self.right_avg_dist > 0.265 and self.right_avg_dist < 0.3 and self.right_avg_dist > 0.22:
                self.movement.linear.x = 0.03
                self.movement.angular.z = self.correction_factor*(0.2)    
            #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 = 0.2
            #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 = 0.0                
            #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 = -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 = 0.0                  
            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()

Hi @pharva ,

I think there is something lost in between.

Your wallfound changes to True after the robot has aligned towards the wall, which is good!
Now, your wallfound value is not changing to True in your wall_following.py program output, so there can be only two things:

  1. The result is either not returned correctly from the service server
    [OR]
  2. The result is not captured correctly by the service client in the wall follower program.

Looking into your wall_finder.py, in the custom_service_callback() function:

    def custom_service_callback(self, request, response):
        # Logic to handle service request and set response
        self.service_called = True

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

You are not calling the method that performs the service inside the service callback.
You have put the logic inside laser_scan_callback() which is not the right way to do.
You use subscribers for sensors only for data acquisition and never for robot control.

Looking into your wall_following.py, in the begin() function:
The way you have implemented the service client is correct.
The way you are checking for wallfound response is correct.
So there is no issue on the client side.

So the problem what I suspect is, in your wall_finder.py program’s, custom_service_callback() function.
So when you started the wall_finder.py and made a request to the service server from the wall_following.py program, self.response.wallfound = self.ready line got called immediately and sent a service response as False since self.ready is initialized with False and your callback is not waiting on a task to complete.
Therefore, when you made a request to the service server externally from another terminal, the “wall finding” process is now complete and has set the self.ready to True which in turn has set the result.wallfound to True, which returns True to your external service call.
To verify this, try doing this:

  1. Start wall finder service in one terminal.
  2. Make a request to the service from another terminal - this should and will fail returning wallfound as False. This action will trigger the service callback.
  3. Now start the wall following in another new terminal. This time the wall following service should return success (True).
    If this is the case, then do what I have indicated above.

To re-summarize, you need to remove the robot control logic from laser_scan_callback() of the wall_finder.py and make it into a separate function. Then call this function from your service_callback() function and return True as a result only when the new function completes.

I hope this is clear and clarifies your issue / doubt. Let me know if anything is still unclear.

Regards,
Girish

I think I’m starting to understand the problem you’re trying to get me to see (I apologize that I am not grasping this very quickly).

I’ve made the changes you recommended and have added a few debug notifications to let me know where the code is at. motion_finder is where all of the motion logic is organized while laser_scan_callback just hosts the laser readings now.

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
import sys
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 WallFinderService(Node):

    def __init__(self):
        super().__init__('find_wall')
        self.srv = self.create_service(FindWall, 'find_wall', self.custom_service_callback)
        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))
        self.aligned = False
        self.approach = False
        self.ready = False
        self.service_called = False
        self.response = FindWall.Response()
        self.movement = Twist()
        self.timer_period = 0.5
        self.timer = self.create_timer(self.timer_period, self.motion_finder)

    def custom_service_callback(self, request, response):
        # Logic to handle service request and set response
        self.service_called = True
        self.motion_finder()
        self.get_logger().info('step 1')
        if self.ready == True:
            self.get_logger().info('step 3')
            self.response.wallfound = self.ready
        self.get_logger().info('response.wallfound: "%s"' % str(response.wallfound))
        self.get_logger().info('step 2')
        return response
        

    def laser_scan_callback(self, msg):            
        ## 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]

        self.left_laser = msg.ranges[90]
        self.left_laser_75 = msg.ranges[75]
        self.left_laser_105 = msg.ranges[105]
   
        self.back_laser = msg.ranges[180]

        self.laser_min = min(msg.ranges)
        self.laser_max = max(msg.ranges)
        
        # Real Bot Laser Ranges
        # self.front_laser = msg.ranges[360]
        # self.front_laser_10 = msg.ranges[370]
        # self.front_laser_350 = msg.ranges[350]

        # self.right_laser = msg.ranges[180]
        # self.right_laser_285 = msg.ranges[195]
        # self.right_laser_255 = msg.ranges[165]

        # self.left_laser = msg.ranges[540]
        # self.left_laser_75 = msg.ranges[525]
        # self.left_laser_105 = msg.ranges[555]
   
        # self.back_laser = msg.ranges[0]

    def motion_finder(self):
        if self.service_called == True:
            # 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('Min Range "%s"' % str(self.laser_min))
            #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(self.aligned))
            self.get_logger().info('Approach Status "%s"' % str(self.approach))
            self.get_logger().info('Ready Status "%s"' % str(self.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 self.aligned == False and self.theta_front_wall > 45 and self.theta_front_wall < -45:
                self.movement.angular.z = 0.3
            elif self.aligned == False and self.theta_front_wall < 45 and self.theta_front_wall > 5:
                self.movement.angular.z = self.correction_factor_front*(-0.2)
            elif self.aligned == False and self.theta_front_wall > -45 and self.theta_front_wall < -5:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            else:
                self.movement.angular.z = 0.0
                self.aligned = True
            
            if self.aligned == True and self.approach == False and self.front_laser > 0.5:
                self.movement.linear.x = 0.05
            elif self.aligned == True and self.approach == False and self.front_laser <= 0.5:
                self.movement.linear.x = 0.0
                self.approach = True

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

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

        

def main(args=None):
    rclpy.init(args=args)
    service = WallFinderService()
    rclpy.spin(service)
    WallFinderService.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

    def custom_service_callback(self, request, response):
        # Logic to handle service request and set response
        self.service_called = True
        self.motion_finder()
        self.get_logger().info('step 1')
        if self.ready == True:
            self.get_logger().info('step 3')
            self.response.wallfound = self.ready
        self.get_logger().info('response.wallfound: "%s"' % str(response.wallfound))
        self.get_logger().info('step 2')
        return response

I’ve learned that inside this custom_service_callback it steps through the commands without waiting for them to complete. This is how I know:

[wall_finder-1] [INFO] [1708737556.608345180] [find_wall]: Aligned Status "False"
[wall_finder-1] [INFO] [1708737556.609133696] [find_wall]: Approach Status "False"
[wall_finder-1] [INFO] [1708737556.609831368] [find_wall]: Ready Status "False"
[wall_finder-1] [INFO] [1708737556.614945394] [find_wall]: step 1
[wall_finder-1] [INFO] [1708737556.616074409] [find_wall]: response.wallfound: "False"
[wall_finder-1] [INFO] [1708737556.617053093] [find_wall]: step 2

I can tell that it has completely run through all of the steps in custom_service_callback while the bot is still completing motion_finder. How can I get the custom_service_callback to wait until motion_finder is complete before moving on to the next command, “if self.ready == True: ?”

@girishkumar.kannan I imagine trying to read through someone else’s code is quite tedious. I appreciate your help and the time you’ve given me.

Hi @pharva ,

I am glad that I was able to paint you a picture of your situation (with my long explanation)!

Perfect. Much appreciated!

Yes, so what I suspected was happening in your code was right!

Ok, now coming to your issue. You need some conceptual understanding of services and/or actions in ROS2.

So, Services and Actions in ROS2 can be any combination of the following properties:

  1. Blocking or Non-Blocking
  2. Synchronous or Asynchronous
  3. Single Instance-only or Multiple Instances
  4. Sequential or Parallel

It really depends on how the programmer has intended the Service or Action to be.

That said, coming specifically to your case about the Wall Finder Logic, we have the following characteristics:

  1. The wall finder service is required only once, when the robot starts the wall following lap loop.
  2. When wall finder logic happens, wall follower should not work.
  3. This wall finder logic has no need for use when the robot is doing wall following.

So the wall finder logic is: Blocking, Synchronous, Single Instance-only and Sequential.
So, your service callback must be Blocking all other robot processes but not any of the sensor subscribers (in this case laser scan callback).
So, your Laser Scan and Service callback must be Reentrant Callback types, but when the wall finder logic (service callback) happens, no other robot control should take over.

So, your new logic function motion_finder, when it is called inside the service callback, should be blocking all other tasks of the robot (except laser) and the service callback should not complete (read as: return) the function before the blocking function (motion_finder) completes. Only after this the service callback should return the response.
So when a service request is made and a response is returned immediately, then the service callback is not running a blocking function.

Therefore, your wall finder function should:

  1. Read the service request
  2. Perform the blocking function
  3. Return the response ONLY when the blocking function completes and has provided success or failure (boolean) result.

I hope this clarified all your doubts. (Technically, you should have a much better understanding now!)
Sorry for the long post again!

No problem!

Regards,
Girish

@girishkumar.kannan

I finally got it. Your guidance was critical to my success. I used ReentrantCallbackGroup() to group the service callback and the laser callback (is this the right way?). I set up a while loop that contained the movement logic inside of the service callback. This ensured that the service callback wouldn’t move on until the movement logic was completed. Is this what you were talking about when you mentioned a blocking function?

Thank you again for your help.

Could you explain why self.response returns this:

findwall.srv.FindWall_Response(wallfound=False)

and response.wallfound returns “True?”

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.mutuallyexclusive_group_1 = MutuallyExclusiveCallbackGroup()
        #self.mutuallyexclusive_group_2 = MutuallyExclusiveCallbackGroup()

        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('Min Range "%s"' % str(self.laser_min))
            #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.theta_front_wall > 45 and self.theta_front_wall < -45:
                self.movement.angular.z = 0.3
            elif aligned == False and self.theta_front_wall < 45 and self.theta_front_wall > 5:
                self.movement.angular.z = self.correction_factor_front*(-0.2)
            elif aligned == False and self.theta_front_wall > -45 and self.theta_front_wall < -5:
                self.movement.angular.z = self.correction_factor_front*(0.2)
            else:
                self.movement.angular.z = 0.0
                aligned = True
            
            if aligned == True and approach == False and self.front_laser > 0.5:
                self.movement.linear.x = 0.05
            elif aligned == True and approach == False and self.front_laser <= 0.5:
                self.movement.linear.x = 0.0
                approach = True

            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
                
            # 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]

        self.left_laser = msg.ranges[90]
        self.left_laser_75 = msg.ranges[75]
        self.left_laser_105 = msg.ranges[105]
   
        self.back_laser = msg.ranges[180]

        self.laser_min = min(msg.ranges)
        self.laser_max = max(msg.ranges)
        
        # Real Bot Laser Ranges
        # self.front_laser = msg.ranges[360]
        # self.front_laser_10 = msg.ranges[370]
        # self.front_laser_350 = msg.ranges[350]

        # self.right_laser = msg.ranges[180]
        # self.right_laser_285 = msg.ranges[195]
        # self.right_laser_255 = msg.ranges[165]

        # self.left_laser = msg.ranges[540]
        # self.left_laser_75 = msg.ranges[525]
        # self.left_laser_105 = msg.ranges[555]
   
        # self.back_laser = msg.ranges[0]

    

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

if __name__ == '__main__':
    main()

Hi @pharva ,

Yay!

Yes, that is right.

Exactly. Any loop that you use within a callback that is not thread-compatible will be a blocking function. This can be anything like for, while or do...while.

After going through your most recent code, these are my observations:

  1. Your self.response (class variable) is not the same as response local variable inside the service callback (custom_service_callback). In programming terminology, these two variables are completely independent and not within same scope with respect to each other.
  1. In your service callback (custom_service_callback), you are setting response.wallfound = ready where ready = True after your approach is True. Therefore, your self.response variable is unaffected. The only change happens to response.wallfound and NOT to self.response.wallfound.
  1. In your above post, you have commented the following line:

So you are not making any changes to the (freshly)-initialized variable self.response, since its declaration time in the following line:

Any unaltered message declaration defaults to zeros for numbers (int, float) and defaults to False for booleans.

Therefore, your response.wallfound returns True, but self.response.wallfound returns False since it is never modified from the moment it was declared in your current (most recent) program code.

You do not require the self.response class variable. You may get rid of it.
Also, you are using a lot of boolean flags. Try avoiding that. They are unnecessary. Just use a print statement instead (self.get_logger().info("blah blah")), since you are already on a blocking function, those sub-tasks have to complete in sequence.
Also, you have way too much if...else conditionals. You can condense them further by optimizing your variables.
For Example, the following line can be changed to :

if ((aligned) and (not approach) and (self.front_laser > 0.5)):
which is shorter and more pythonic.

For optimized flag handling, you can use two tricks:

  1. Comparing a (Python) List of Booleans.
  2. Comparing to a integer value of a binary “byte” or “word”.

If you need more info on this step please make a new topic (do not continue on this issue thread post).

I hope this post should finally solve all your issues. Don’t hesitate to reply if you still have issues.

Regards,
Girish