Robotics StackExchange | Archived questions

Client call to service from another ROS2 package hangs

I am trying to write a client for a service defined in a separate ROS2 package, but it is hanging waiting for the service to become available.

The other package is a servo package for an AWS DeepRacer car, found here. In my workspace, I git clone this package and build it, then it works just fine. I run this package in one terminal, and then I can run another terminal and call one of the package's services directly:

ros2 service call /servo_pkg/get_led_state deepracer_interfaces_pkg/srv/GetLedCtrlSrv  

This call to the service returns as expected.

So now I want to create my own package called gg_led_pkg with a single client to call this get_led_state service based on a timer every 5 seconds. I am using a ReentrantCallbackGroup for the client and the timer, with a MultiThreadedExecutor to spin the node. I set up my setup.py and my package.xml files, dependencies are fine, and everything builds. The client executable runs, but it hangs when trying to call the service.

What am I doing wrong? Is it not possible to call a service from a client defined in a different package but in the same workspace?

Here is the output from my client call

~/environment/deepracerws $ ros2 run ggled_pkg client

[INFO] [1655305042.750526039] [ledminimalclient_async]: Beginning demo, end with CTRL-C

[INFO] [1655305047.741162292] [ledminimalclient_async]: Client sending request

[INFO][1655305052.741094414] [ledminimalclient_async]: Client sending request

And so on and so on...

My client code:

import sys 
from time import sleep 
from threading import Thread

#import the two service interfaces we need to use 
from deepracer_interfaces_pkg.srv import SetLedCtrlSrv, GetLedCtrlSrv

# this imports the ROS2 Client Library for Python 
import rclpy
# the main object for ROS nodes 
from rclpy.node import Node
# import a multi-threaded executor to be used when spinning the node.  
# This allows "parallel" execution of threads
# but the python GIL is still present (so not truly parallel) 
from rclpy.executors import MultiThreadedExecutor
# the re-entrant callback group allows the executor to schedule and execute the group's callbacks
# in any way it sees fit.  This is not necessarily thread safe 
from rclpy.callback_groups import ReentrantCallbackGroup


class LedMinimalClientAsync(Node):

    def __init__(self, client_cb_group, timer_cb_group, manual_call):
        # first initialize 
        super().__init__('led_minimal_client_async')

        self.client_cb_group = client_cb_group
        self.timer_cb_group = timer_cb_group
        self.manual_call = manual_call

        # create a CLI interface for this getter method
        # we set the client to use the GetLedCtrlSrv defined in the deepracer interfaces package
        # we define the name as get_led_ctrl_srv
        # we define a callback group here (going to make it reentrant)
        self.get_led_status = self.create_client(GetLedCtrlSrv, 
                                                'get_led_ctrl_srv', 
                                                callback_group=self.client_cb_group)

        # # wait for the client to become available (this is where it is locking)
        # while not self.get_led_status.wait_for_service(timeout_sec=1.0):
        #     self.get_logger().info('service not available, waiting again...')

        # make a simple request payload, which is empty
        self.req = GetLedCtrlSrv.Request()   


    def start(self):
        '''a method to call the server request outside the __init__()'''
        # if we are not making manual calls to the server
        # then this is false and will use the timer to make calls every second
        if not self.manual_call:
            self.call_timer = self.create_timer(5, 
                                                self.timer_cb, 
                                                callback_group=self.timer_cb_group)


    def call_srv(self, delay: float = 1):
        '''
        this method is called manually and imposes a delay with sleep.  
        Then is calls the private method _call_srv() that actually makes the service call
        '''
        sleep(delay)
        self._call_srv()


    def _call_srv(self):
        '''
        this is the method that actually makes the service call
        '''
        self.get_logger().info('Client sending request')

        # is this a synchronous request?
        self.reponse = self.get_led_status.call(self.req)
        self.get_logger().info('Client received response ' + self.reponse)

        # not sure if we need this one of the synchronous one
        # self.future = self.get_led_status.call_async(self.req)
        # self.get_logger().info('Client received response ' + self.reponse)


    def timer_cb(self):
        '''
        if a timer is used to make the service call, and therefore not a manual call
        then this is the method used.  The timer will handle the wait period
        '''
        self._call_srv()


    # def _call_srv_async(self):
        # '''
        # this is the ansynchronous call to the service, making a future object
        # This future object can be called by an external method like so:
        # if minimal_client.future.done():
        #     response = minimal_client.future.result()
        # '''

    #     self.future = self.get_led_status.call_async(self.req)


def call_srv_manually(client_node):
    '''
    this is a way to have the client call the service manually
    '''
    client_node.call_srv()
    client_node.get_logger().info('Test finished successfully.\n')



def run(client_cb_group, timer_cb_group, manual_call):
    '''
    the main function for running the various pieces of the code
    '''
    # initialize the ros2 client library for python
    rclpy.init()

    # instatiate a client node with the callback groups (default to None)
    client_node = LedMinimalClientAsync(client_cb_group, timer_cb_group, manual_call)

    # make a multithreaded executor to spin up parallel threads
    executor = MultiThreadedExecutor()
    executor.add_node(client_node)

    # make a thread to call the service manually.  This currently isn't used
    call_thread = Thread(target=call_srv_manually, args=(client_node, ), daemon=True)

    # if there is a manual call to the service, then 
    if manual_call:
        call_thread.start()
    # added this else case for moving the callback outside the __init__ definition
    else:
        client_node.start()

    try:
        print("")
        client_node.get_logger().info('Beginning demo, end with CTRL-C')
        executor.spin()
    except KeyboardInterrupt:
        client_node.get_logger().info('KeyboardInterrupt, shutting down.\n')
    client_node.destroy_node()
    rclpy.shutdown()

    if manual_call:
        call_thread.join()


def main(args=None):
    # make a single instance of a re-entrant callback group, 
    # assign it to both the timer and the client node
    # let the timer callback make the requests to the server node, instead of manual
    cb_group = ReentrantCallbackGroup()
    run(client_cb_group=cb_group, timer_cb_group=cb_group, manual_call=False)



if __name__ == '__main__':
    main()

I am using ROS2 Foxy on an Ubuntu 20.04 x86 machine (AWS Robomaker)

Asked by brieb on 2022-06-15 12:27:47 UTC

Comments

Answers