turtle_herder.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # License: BSD
00004 #   https://raw.github.com/robotics-in-concert/concert_services/license/LICENSE
00005 #
00006 ##############################################################################
00007 # About
00008 ##############################################################################
00009 
00010 # Simple script to manage spawning and killing of turtles across multimaster
00011 # boundaries. Typically turtlesim clients would connect to the kill and
00012 # spawn services directly to instantiate themselves, but since we can't
00013 # flip service proxies, this is not possible. So this node is the inbetween
00014 # go-to node and uses a rocon service pair instead.
00015 #
00016 # It supplements this relay role with a bit of herd management - sets up
00017 # random start locations and feeds back aliased names when running with
00018 # a concert.
00019 
00020 ##############################################################################
00021 # Imports
00022 ##############################################################################
00023 
00024 import copy
00025 import math
00026 import os
00027 import random
00028 import tempfile
00029 
00030 import concert_service_utilities
00031 import gateway_msgs.msg as gateway_msgs
00032 import gateway_msgs.srv as gateway_srvs
00033 import rocon_launch
00034 import rospy
00035 import rocon_gateway_utils
00036 import rocon_python_comms
00037 import std_msgs.msg as std_msgs
00038 import turtlesim.srv as turtlesim_srvs
00039 
00040 ##############################################################################
00041 # Utilities
00042 ##############################################################################
00043 
00044 
00045 def prepare_launch_configurations(turtles):
00046     """
00047     :param Turtle[] turtles:
00048     """
00049     port = 11
00050     launch_text = '<concert>\n'
00051     for turtle in turtles:
00052         launch_text += '  <launch title="%s:114%s" package="concert_service_turtlesim" name="turtle.launch" port="114%s">\n' % (turtle.unique_name, str(port), str(port))
00053         launch_text += '    <arg name="turtle_name" value="%s"/>\n' % turtle.unique_name
00054         launch_text += '    <arg name="turtle_concert_whitelist" value="%s"/>\n' % str(turtle.concert_whitelist)  # e.g. [Turtle Concert, Turtle Teleop Concert, Concert Tutorial]
00055         launch_text += '    <arg name="turtle_rapp_whitelist" value="%s"/>\n' % str(turtle.rapp_whitelist)  # e.g. [rocon_apps, turtle_concert]
00056         launch_text += '  </launch>\n'
00057         port = port + 1
00058     launch_text += '</concert>\n'
00059     temp = tempfile.NamedTemporaryFile(mode='w+t', delete=False)
00060     #print("\n" + console.green + rocon_launch_text + console.reset)
00061     temp.write(launch_text)
00062     #rospy.logwarn("Turtle Herder: rocon launch text\n%s" % launch_text)
00063     temp.close()  # unlink it later
00064     launch_configurations = rocon_launch.parse_rocon_launcher(temp.name, "--screen")
00065     try:
00066         os.unlink(temp.name)
00067     except OSError:
00068         rospy.logerr("Turtle Herder : failed to unlink the rocon launcher.")
00069     return launch_configurations
00070 
00071 
00072 ##############################################################################
00073 # Turtle
00074 ##############################################################################
00075 
00076 class Turtle(object):
00077     """
00078     Holds parameterised information used for customising a turtle spawning.
00079     """
00080     __slots__ = [
00081                  'name',
00082                  'unique_name',
00083                  'rapp_whitelist',
00084                  'concert_whitelist'
00085                 ]
00086 
00087     def __init__(self, name, rapp_whitelist, concert_whitelist):
00088         self.name = name
00089         self.unique_name = name  # this gets manipulated later
00090         self.rapp_whitelist = rapp_whitelist
00091         self.concert_whitelist = concert_whitelist
00092 
00093 ##############################################################################
00094 # Turtle Herder
00095 ##############################################################################
00096 
00097 
00098 class TurtleHerder(object):
00099     '''
00100       Shepherds the turtles!
00101 
00102       @todo get alised names from the concert client list if the topic is available
00103 
00104       @todo watchdog for killing turtles that are no longer connected.
00105     '''
00106     __slots__ = [
00107         'turtles',              # list of turtle name strings
00108         '_kill_turtle_service_client',
00109         '_spawn_turtle_service_client',
00110         '_gateway_flip_service',
00111         '_processes',
00112         '_temporary_files',     # temporary files that have to be unlinked later
00113         'is_disabled',          # flag set when service manager tells it to shut down.
00114         '_terminal',            # terminal to use to spawn concert clients
00115         '_shutdown_subscriber'
00116     ]
00117 
00118     def __init__(self):
00119         self.turtles = []
00120         self._processes = []
00121         self._temporary_files = []
00122         self.is_disabled = False
00123         # herding backend
00124         rospy.wait_for_service('kill')  # could use timeouts here
00125         rospy.wait_for_service('spawn')
00126         self._kill_turtle_service_client = rospy.ServiceProxy('kill', turtlesim_srvs.Kill, persistent=True)
00127         self._spawn_turtle_service_client = rospy.ServiceProxy('spawn', turtlesim_srvs.Spawn, persistent=True)
00128         # kill the default turtle that turtlesim starts with
00129         try:
00130             unused_response = self._kill_turtle_service_client("turtle1")
00131         except rospy.ServiceException:
00132             rospy.logerr("Turtle Herder : failed to contact the internal kill turtle service")
00133         except rospy.ROSInterruptException:
00134             rospy.loginfo("Turtle Herder : shutdown while contacting the internal kill turtle service")
00135             return
00136         self._shutdown_subscriber = rospy.Subscriber('shutdown', std_msgs.Empty, self.shutdown)
00137         # gateway
00138         gateway_namespace = rocon_gateway_utils.resolve_local_gateway()
00139         rospy.wait_for_service(gateway_namespace + '/flip')
00140         self._gateway_flip_service = rospy.ServiceProxy(gateway_namespace + '/flip', gateway_srvs.Remote)
00141         # set up a terminal type for spawning
00142         try:
00143             self._terminal = rocon_launch.create_terminal()
00144         except (rocon_launch.UnsupportedTerminal, rocon_python_comms.NotFoundException) as e:
00145             rospy.logwarn("Turtle Herder : cannot find a suitable terminal, falling back to spawning inside the current one [%s]" % str(e))
00146             self._terminal = rocon_launch.create_terminal(rocon_launch.terminals.active)
00147 
00148     def _spawn_simulated_turtles(self, turtles):
00149         """
00150         Very important to have checked that the turtle names are unique
00151         before calling this method.
00152 
00153         :param Turtle[] turtles:
00154         """
00155         for turtle in turtles:
00156             internal_service_request = turtlesim_srvs.SpawnRequest(
00157                                                 random.uniform(3.5, 6.5),
00158                                                 random.uniform(3.5, 6.5),
00159                                                 random.uniform(0.0, 2.0 * math.pi),
00160                                                 turtle.unique_name)
00161             try:
00162                 unused_internal_service_response = self._spawn_turtle_service_client(internal_service_request)
00163                 self.turtles.append(turtle)
00164             except rospy.ServiceException:  # communication failed
00165                 rospy.logerr("TurtleHerder : failed to contact the internal spawn turtle service")
00166                 continue
00167             except rospy.ROSInterruptException:
00168                 rospy.loginfo("TurtleHerder : shutdown while contacting the internal spawn turtle service")
00169                 continue
00170 
00171     def _launch_turtle_clients(self, turtles):
00172         """
00173         :param Turtle[] turtles:
00174         """
00175         # spawn the turtle concert clients
00176         launch_configurations = prepare_launch_configurations(turtles)
00177         for launch_configuration in launch_configurations:
00178             rospy.loginfo("Turtle Herder : launching turtle concert client %s on port %s" %
00179                       (launch_configuration.name, launch_configuration.port))
00180             #print("%s" % launch_configuration)
00181             process, meta_roslauncher = self._terminal.spawn_roslaunch_window(launch_configuration)
00182             self._processes.append(process)
00183             self._temporary_files.append(meta_roslauncher)
00184 
00185     def spawn_turtles(self, turtles):
00186         """
00187         :param Turtle[] turtles:
00188         """
00189         uniquely_named_turtles = self._establish_unique_names(turtles)
00190         self._spawn_simulated_turtles(uniquely_named_turtles)
00191         self._launch_turtle_clients(uniquely_named_turtles)
00192         self._send_flip_rules(uniquely_named_turtles, cancel=False)
00193 
00194     def _establish_unique_names(self, turtles):
00195         """
00196         Make sure the turtle names don't clash with currently spawned turtles.
00197         If they do, postfix them with an incrementing counter.
00198 
00199         :param Turtle[] turtles: list of new turtle names to uniquify.
00200         :returns: uniquified names for the turtles.
00201         :rtype Turtle[]: updated turtles
00202         """
00203         for turtle in turtles:
00204             name_extension = ''
00205             count = 0
00206             while turtle.name + name_extension in [turtle.name for turtle in self.turtles]:
00207                 name_extension = '_' + str(count)
00208                 count = count + 1
00209             turtle.unique_name = turtle.name + name_extension
00210         return turtles
00211 
00212     def _send_flip_rules(self, turtles, cancel):
00213         """
00214         :param Turtle[] turtles:
00215         """
00216         for turtle in turtles:
00217             rules = []
00218             rule = gateway_msgs.Rule()
00219             rule.node = ''
00220             rule.type = gateway_msgs.ConnectionType.SUBSCRIBER
00221             # could resolve this better by looking up the service info
00222             rule.name = "/services/turtlesim/%s/cmd_vel" % turtle.unique_name
00223             rules.append(copy.deepcopy(rule))
00224             rule.type = gateway_msgs.ConnectionType.PUBLISHER
00225             rule.name = "/services/turtlesim/%s/pose" % turtle.unique_name
00226             rules.append(copy.deepcopy(rule))
00227             # send the request
00228             request = gateway_srvs.RemoteRequest()
00229             request.cancel = cancel
00230             remote_rule = gateway_msgs.RemoteRule()
00231             remote_rule.gateway = turtle.unique_name
00232             for rule in rules:
00233                 remote_rule.rule = rule
00234                 request.remotes.append(copy.deepcopy(remote_rule))
00235             try:
00236                 self._gateway_flip_service(request)
00237             except rospy.ServiceException:  # communication failed
00238                 rospy.logerr("TurtleHerder : failed to send flip rules")
00239                 return
00240             except rospy.ROSInterruptException:
00241                 rospy.loginfo("TurtleHerder : shutdown while contacting the gateway flip service")
00242                 return
00243 
00244     def _ros_service_manager_disable_callback(self, msg):
00245         self.is_disabled = True
00246 
00247     def shutdown(self, msg=None):
00248         """
00249           - Send unflip requests
00250           - Cleanup turtles on the turtlesim canvas.
00251           - Shutdown spawned terminals
00252 
00253         :todo: this should go in a service manager callable ros callback where we can
00254         call disable on this service and bring it down without having to SIGINT it.
00255         """
00256         # cleaning turtles is probably not really important since
00257         # we always shutdown turtlesim and turtle_herder together.
00258         # for name in self.turtles:
00259         #     try:
00260         #         unused_internal_service_response = self._kill_turtle_service_client(name)
00261         #     except rospy.ServiceException:  # communication failed
00262         #         break  # quietly fail
00263         #     except rospy.ROSInterruptException:
00264         #         break  # quietly fail
00265 
00266         self._terminal.shutdown_roslaunch_windows(processes=self._processes,
00267                                                   hold=False)
00268         for temporary_file in self._temporary_files:
00269             #print("Unlinking %s" % temporary_file.name)
00270             try:
00271                 os.unlink(temporary_file.name)
00272             except OSError as e:
00273                 rospy.logerr("Turtle Herder : failed to unlink temporary file [%s]" % str(e))
00274 
00275 ##############################################################################
00276 # Launch point
00277 ##############################################################################
00278 
00279 if __name__ == '__main__':
00280 
00281     rospy.init_node('turtle_herder')
00282     (service_name, unused_service_description, service_priority, unused_service_id) = concert_service_utilities.get_service_info()
00283     turtles = []
00284     turtle_parameters = rospy.get_param('/services/' + service_name + '/turtles', {})
00285     # should check that turtle_parameters is a dict here
00286     for name, parameters in turtle_parameters.items():
00287         try:
00288             turtles.append(Turtle(name, parameters['rapp_whitelist'], parameters['concert_whitelist']))
00289         except KeyError as e:
00290             rospy.logerr("TurtleHerder : not all turtle parameters found for %s (req'd even if empty)[%s]" % (name, str(e)))
00291     rospy.loginfo("TurtleHerder : spawning turtles: %s" % [turtle.name for turtle in turtles])
00292 
00293     turtle_herder = TurtleHerder()
00294     turtle_herder.spawn_turtles(turtles)
00295     while not rospy.is_shutdown() and not turtle_herder.is_disabled:
00296         rospy.sleep(0.3)
00297     turtle_herder.shutdown()


concert_service_turtlesim
Author(s): Daniel Stonier
autogenerated on Thu Jun 6 2019 21:35:22