ros_topic_listener.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 # Software License Agreement (BSD License)
00003 #
00004 # Copyright (c) 2008, Willow Garage, Inc.
00005 # All rights reserved.
00006 #
00007 # Redistribution and use in source and binary forms, with or without
00008 # modification, are permitted provided that the following conditions
00009 # are met:
00010 #
00011 #  * Redistributions of source code must retain the above copyright
00012 #    notice, this list of conditions and the following disclaimer.
00013 #  * Redistributions in binary form must reproduce the above
00014 #    copyright notice, this list of conditions and the following
00015 #    disclaimer in the documentation and/or other materials provided
00016 #    with the distribution.
00017 #  * Neither the name of Willow Garage, Inc. nor the names of its
00018 #    contributors may be used to endorse or promote products derived
00019 #    from this software without specific prior written permission.
00020 #
00021 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00022 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00023 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00024 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00025 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00026 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00027 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00028 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00029 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00030 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00031 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00032 # POSSIBILITY OF SUCH DAMAGE.
00033 #
00034 # Revision $Id$
00035 
00036 # ###############################################################################################################################################
00037 # xxd -i ros_topic_listener.py | sed 's/unsigned/const/g' | sed 's/ros_topic_listener_py/ROS_TOPIC_LISTENER_SCRIPT/g' > RosTopicListenerPython.h
00038 # ###############################################################################################################################################
00039 
00040 # make sure we aren't using floor division
00041 from __future__ import division, print_function
00042 
00043 NAME='rostopic'
00044 
00045 import cogni_topic_listener
00046 
00047 import os
00048 import sys
00049 import math
00050 import socket
00051 import time
00052 import traceback
00053 import yaml
00054 import xmlrpclib
00055 
00056 from operator import itemgetter
00057 from urlparse import urlparse
00058 
00059 import genpy
00060 
00061 import roslib.message
00062 import rosgraph
00063 
00064 import rospy
00065 
00066 class ROSTopicException(Exception):
00067     """
00068     Base exception class of rostopic-related errors
00069     """
00070     pass
00071 class ROSTopicIOException(ROSTopicException):
00072     """
00073     rostopic errors related to network I/O failures
00074     """
00075     pass
00076 
00077 def _check_master():
00078     """
00079     Make sure that master is available
00080     :raises: :exc:`ROSTopicException` If unable to successfully communicate with master
00081     """
00082     try:
00083         rosgraph.Master('/rostopic').getPid()
00084     except socket.error:
00085         raise ROSTopicIOException("Unable to communicate with master!")
00086     
00087 def _master_get_topic_types(master):
00088     try:
00089         val = master.getTopicTypes()
00090     except xmlrpclib.Fault:
00091         #TODO: remove, this is for 1.1
00092         sys.stderr.write("WARNING: rostopic is being used against an older version of ROS/roscore\n")
00093         val = master.getPublishedTopics('/')
00094     return val
00095 
00096 def _sleep(duration):
00097     rospy.rostime.wallsleep(duration)
00098 
00099 
00100    
00101 def msgevalgen(pattern):
00102     """
00103     Generates a function that returns the relevant field (aka 'subtopic') of a Message object
00104     :param pattern: subtopic, e.g. /x. Must have a leading '/' if specified, ``str``
00105     :returns: function that converts a message into the desired value, ``fn(Message) -> value``
00106     """
00107     if not pattern or pattern == '/':
00108         return None
00109     def msgeval(msg):
00110         # I will probably replace this with some less beautiful but more efficient
00111         try:
00112             return eval('msg'+'.'.join(pattern.split('/')))
00113         except AttributeError as e:
00114             sys.stdout.write("no field named [%s]"%pattern+"\n")
00115             return None
00116     return msgeval
00117     
00118 def _get_topic_type(topic):
00119     """
00120     subroutine for getting the topic type
00121     :returns: topic type, real topic name and fn to evaluate the message instance
00122     if the topic points to a field within a topic, e.g. /rosout/msg, ``(str, str, fn)``
00123     """
00124     try:
00125         val = _master_get_topic_types(rosgraph.Master('/rostopic'))
00126     except socket.error:
00127         raise ROSTopicIOException("Unable to communicate with master!")
00128 
00129     # exact match first, followed by prefix match
00130     matches = [(t, t_type) for t, t_type in val if t == topic]
00131     if not matches:
00132         matches = [(t, t_type) for t, t_type in val if topic.startswith(t+'/')]
00133         # choose longest match
00134         matches.sort(key=itemgetter(0), reverse=True)
00135     if matches:
00136         t, t_type = matches[0]
00137         if t_type == rosgraph.names.ANYTYPE:
00138             return None, None, None
00139         return t_type, t, msgevalgen(topic[len(t):])
00140     else:
00141         return None, None, None
00142 
00143 # NOTE: this is used externally by rxplot
00144     
00145 def get_topic_type(topic, blocking=False):
00146     """
00147     Get the topic type.
00148 
00149     :param topic: topic name, ``str``
00150     :param blocking: (default False) block until topic becomes available, ``bool``
00151     
00152     :returns: topic type, real topic name and fn to evaluate the message instance
00153       if the topic points to a field within a topic, e.g. /rosout/msg. fn is None otherwise. ``(str, str, fn)``
00154     :raises: :exc:`ROSTopicException` If master cannot be contacted
00155     """
00156     topic_type, real_topic, msg_eval = _get_topic_type(topic)
00157     if topic_type:
00158         return topic_type, real_topic, msg_eval
00159     elif blocking:
00160         sys.stderr.write("WARNING: topic [%s] does not appear to be published yet\n"%topic)
00161         while not rospy.is_shutdown():
00162             topic_type, real_topic, msg_eval = _get_topic_type(topic)
00163             if topic_type:
00164                 return topic_type, real_topic, msg_eval
00165             else:
00166                 _sleep(0.1)
00167     return None, None, None
00168 
00169 def get_topic_class(topic, blocking=False):
00170     """
00171     Get the topic message class
00172     :returns: message class for topic, real topic
00173       name, and function for evaluating message objects into the subtopic
00174       (or ``None``). ``(Message, str, str)``
00175     :raises: :exc:`ROSTopicException` If topic type cannot be determined or loaded
00176     """
00177     topic_type, real_topic, msg_eval = get_topic_type(topic, blocking=blocking)
00178     if topic_type is None:
00179         return None, None, None
00180     msg_class = roslib.message.get_message_class(topic_type)
00181     if not msg_class:
00182         raise ROSTopicException("Cannot load message class for [%s]. Are your messages built?"%topic_type)
00183     return msg_class, real_topic, msg_eval
00184 
00185 
00186 
00187 class CallbackEcho(object):
00188     """
00189     Callback instance that can print callback data in a variety of
00190     formats. Used for all variants of rostopic echo
00191     """
00192 
00193     def __init__(self, topic, msg_eval=None, plot=False, filter_fn=None,
00194                  echo_clear=False, echo_all_topics=False,
00195                  offset_time=False, count=None,
00196                  field_filter_fn=None):
00197         """
00198         :param plot: if ``True``, echo in plotting-friendly format, ``bool``
00199         :param filter_fn: function that evaluates to ``True`` if message is to be echo'd, ``fn(topic, msg)``
00200         :param echo_all_topics: (optional) if ``True``, echo all messages in bag, ``bool``
00201         :param offset_time: (optional) if ``True``, display time as offset from current time, ``bool``
00202         :param count: number of messages to echo, ``None`` for infinite, ``int``
00203         :param field_filter_fn: filter the fields that are strified for Messages, ``fn(Message)->iter(str)``
00204         """
00205         if topic and topic[-1] == '/':
00206             topic = topic[:-1]
00207         self.topic = topic
00208         self.msg_eval = msg_eval
00209         self.plot = plot
00210         self.filter_fn = filter_fn
00211 
00212         self.prefix = ''
00213         self.suffix = '' if not plot else ''# same as YAML document separator, bug #3291
00214         
00215         self.echo_all_topics = echo_all_topics
00216         self.offset_time = offset_time
00217 
00218         # done tracks when we've exceeded the count
00219         self.done = False
00220         self.max_count = count
00221         self.count = 0
00222 
00223         # determine which strifying function to use
00224         if plot:
00225             #TODOXXX: need to pass in filter function
00226             self.str_fn = _str_plot
00227             self.sep = ''
00228         else:
00229             #TODOXXX: need to pass in filter function
00230             self.str_fn = self.custom_strify_message
00231             if echo_clear:
00232                 self.prefix = '\033[2J\033[;H'
00233 
00234         self.field_filter=field_filter_fn
00235         
00236         # first tracks whether or not we've printed anything yet. Need this for printing plot fields.
00237         self.first = True
00238 
00239         # cache
00240         self.last_topic = None
00241         self.last_msg_eval = None
00242 
00243     def custom_strify_message(self, val, indent='', time_offset=None, current_time=None, field_filter=None, type_information=None):
00244         # ensure to print uint8[] as array of numbers instead of string
00245         if type_information and type_information.startswith('uint8['):
00246             val = [ord(x) for x in val]
00247         return genpy.message.strify_message(val, indent=indent, time_offset=time_offset, current_time=current_time, field_filter=field_filter)
00248 
00249     def callback(self, data, callback_args, current_time=None):
00250         """
00251         Callback to pass to rospy.Subscriber or to call
00252         manually. rospy.Subscriber constructor must also pass in the
00253         topic name as an additional arg
00254         :param data: Message
00255         :param topic: topic name, ``str``
00256         :param current_time: override calculation of current time, :class:`genpy.Time`
00257         """
00258         topic = callback_args['topic']
00259         type_information = callback_args.get('type_information', None)
00260         if self.filter_fn is not None and not self.filter_fn(data):
00261             return
00262 
00263         if self.max_count is not None and self.count >= self.max_count:
00264             self.done = True
00265             return
00266         
00267         try:
00268             msg_eval = self.msg_eval
00269             if topic == self.topic:
00270                 pass
00271             elif self.topic.startswith(topic + '/'):
00272                 # self.topic is actually a reference to topic field, generate msgeval
00273                 if topic == self.last_topic:
00274                     # use cached eval
00275                     msg_eval = self.last_msg_eval
00276                 else:
00277                     # generate msg_eval and cache
00278                     self.last_msg_eval = msg_eval = msgevalgen(self.topic[len(topic):])
00279                     self.last_topic = topic
00280             elif not self.echo_all_topics:
00281                 return
00282 
00283             if msg_eval is not None:
00284                 data = msg_eval(data)
00285                 
00286             # data can be None if msg_eval returns None
00287             if data is not None:
00288                 # NOTE: we do all prints using direct writes to sys.stdout, which works better with piping
00289                 
00290                 self.count += 1
00291                 str_output = ""
00292                                 
00293                 if self.offset_time:
00294                     str_output = self.prefix+self.str_fn(data, time_offset=rospy.get_rostime(),current_time=current_time, field_filter=self.field_filter, type_information=type_information)+self.suffix
00295                 else:
00296                     str_output = self.prefix+self.str_fn(data,current_time=current_time, field_filter=self.field_filter, type_information=type_information)+self.suffix
00297                 # print(str_output)
00298                 cogni_topic_listener.internal_topic_update(topic, str_output)
00299 
00300 
00301 
00302 
00303                 # if self.offset_time:
00304                 #     sys.stdout.write(self.prefix+\
00305                 #                      self.str_fn(data, time_offset=rospy.get_rostime(),
00306                 #                                  current_time=current_time, field_filter=self.field_filter, type_information=type_information) + \
00307                 #                      self.suffix + '\n')
00308                 # else:
00309                 #     sys.stdout.write(self.prefix+\
00310                 #                      self.str_fn(data,
00311                 #                                  current_time=current_time, field_filter=self.field_filter, type_information=type_information) + \
00312                 #                      self.suffix + '\n')
00313 
00314                 # we have to flush in order before piping to work
00315                 sys.stdout.flush()
00316             # #2778 : have to check count after incr to set done flag
00317             if self.max_count is not None and self.count >= self.max_count:
00318                 self.done = True
00319 
00320         except IOError:
00321             self.done = True
00322         except:
00323             # set done flag so we exit
00324             self.done = True
00325             traceback.print_exc()
00326             
00327 
00328 
00329 
00330 
00331 def _rostopic_echo(topic, callback_echo, bag_file=None, echo_all_topics=False):
00332     """
00333     Print new messages on topic to screen.
00334     
00335     :param topic: topic name, ``str``
00336     :param bag_file: name of bag file to echo messages from or ``None``, ``str``
00337     """
00338     # we have to init a node regardless and bag echoing can print timestamps
00339 
00340     if bag_file:
00341         # initialize rospy time due to potential timestamp printing
00342         rospy.rostime.set_rostime_initialized(True)        
00343         _rostopic_echo_bag(callback_echo, bag_file)
00344     else:
00345         _check_master()
00346         rospy.init_node(NAME, anonymous=True)
00347         msg_class, real_topic, msg_eval = get_topic_class(topic, blocking=True)
00348         if msg_class is None:
00349             # occurs on ctrl-C
00350             return
00351         callback_echo.msg_eval = msg_eval
00352 
00353         # extract type information for submessages
00354         type_information = None
00355         if len(topic) > len(real_topic):
00356             subtopic = topic[len(real_topic):]
00357             subtopic = subtopic.strip('/')
00358             if subtopic:
00359                 fields = subtopic.split('/')
00360                 submsg_class = msg_class
00361                 while fields:
00362                     field = fields[0].split('[')[0]
00363                     del fields[0]
00364                     index = submsg_class.__slots__.index(field)
00365                     type_information = submsg_class._slot_types[index]
00366                     if fields:
00367                         submsg_class = roslib.message.get_message_class(type_information)
00368 
00369         use_sim_time = rospy.get_param('/use_sim_time', False)
00370         sub = rospy.Subscriber(real_topic, msg_class, callback_echo.callback, {'topic': topic, 'type_information': type_information})
00371 
00372         if use_sim_time:
00373             # #2950: print warning if nothing received for two seconds
00374 
00375             timeout_t = time.time() + 2.
00376             while time.time() < timeout_t and \
00377                     callback_echo.count == 0 and \
00378                     not rospy.is_shutdown() and \
00379                     not callback_echo.done:
00380                 _sleep(0.1)
00381 
00382             if callback_echo.count == 0 and \
00383                     not rospy.is_shutdown() and \
00384                     not callback_echo.done:
00385                 sys.stderr.write("WARNING: no messages received and simulated time is active.\nIs /clock being published?\n")
00386 
00387         # while not rospy.is_shutdown() and not callback_echo.done:
00388         #     _sleep(0.1)
00389 
00390 
00391             
00392 ##########################################################################################
00393 # COMMAND PROCESSING #####################################################################
00394     
00395 def _rostopic_cmd_echo(argv):
00396     def expr_eval(expr):
00397         def eval_fn(m):
00398             return eval(expr)
00399         return eval_fn
00400 
00401     args = argv[2:]
00402     from optparse import OptionParser
00403     parser = OptionParser(usage="usage: %prog echo [options] /topic", prog=NAME)
00404     parser.add_option("-b", "--bag",
00405                       dest="bag", default=None,
00406                       help="echo messages from .bag file", metavar="BAGFILE")
00407     parser.add_option("-p", 
00408                       dest="plot", default=False,
00409                       action="store_true",
00410                       help="echo in a plotting friendly format")
00411     parser.add_option("--filter", 
00412                       dest="filter_expr", default=None,
00413                       metavar="FILTER-EXPRESSION",
00414                       help="Python expression to filter messages that are printed. Expression can use Python builtins as well as m (the message) and topic (the topic name).")
00415     parser.add_option("--nostr", 
00416                       dest="nostr", default=False,
00417                       action="store_true",
00418                       help="exclude string fields")
00419     parser.add_option("--noarr",
00420                       dest="noarr", default=False,
00421                       action="store_true",
00422                       help="exclude arrays")
00423     parser.add_option("-c", "--clear",
00424                       dest="clear", default=False,
00425                       action="store_true",
00426                       help="clear screen before printing next message")
00427     parser.add_option("-a", "--all",
00428                       dest="all_topics", default=False,
00429                       action="store_true",
00430                       help="display all message in bag, only valid with -b option")
00431     parser.add_option("-n", 
00432                       dest="msg_count", default=None, metavar="COUNT",
00433                       help="number of messages to echo")
00434     parser.add_option("--offset",
00435                       dest="offset_time", default=False,
00436                       action="store_true",
00437                       help="display time as offsets from current time (in seconds)")
00438 
00439     (options, args) = parser.parse_args(args)
00440     if len(args) > 1:
00441         parser.error("you may only specify one input topic")
00442     if options.all_topics and not options.bag:
00443         parser.error("Display all option is only valid when echoing from bag files")
00444     if options.offset_time and options.bag:
00445         parser.error("offset time option is not valid with bag files")
00446     if options.all_topics:
00447         topic = ''
00448     else:
00449         if len(args) == 0:
00450             parser.error("topic must be specified")        
00451         topic = rosgraph.names.script_resolve_name('rostopic', args[0])
00452         # suppressing output to keep it clean
00453         #if not options.plot:
00454         #    print "rostopic: topic is [%s]"%topic
00455         
00456     filter_fn = None
00457     if options.filter_expr:
00458         filter_fn = expr_eval(options.filter_expr)
00459 
00460     try:
00461         msg_count = int(options.msg_count) if options.msg_count else None
00462     except ValueError:
00463         parser.error("COUNT must be an integer")
00464         
00465     field_filter_fn = create_field_filter(options.nostr, options.noarr)
00466     callback_echo = CallbackEcho(topic, None, plot=options.plot,
00467                                  filter_fn=filter_fn,
00468                                  echo_clear=options.clear, echo_all_topics=options.all_topics,
00469                                  offset_time=options.offset_time, count=msg_count,
00470                                  field_filter_fn=field_filter_fn)
00471     try:
00472         _rostopic_echo(topic, callback_echo, bag_file=options.bag)
00473     except socket.error:
00474         sys.stderr.write("Network communication failed. Most likely failed to communicate with master.\n")
00475 
00476 def create_field_filter(echo_nostr, echo_noarr):
00477     def field_filter(val):
00478         fields = val.__slots__
00479         field_types = val._slot_types
00480         for f, t in zip(val.__slots__, val._slot_types):
00481             if echo_noarr and '[' in t:
00482                 continue
00483             elif echo_nostr and 'string' in t:
00484                 continue
00485             yield f
00486     return field_filter
00487 
00488 
00489 
00490 def cogni_topic_echo(topic, callback_echo):
00491     """
00492     Print new messages on topic to screen.
00493     
00494     :param topic: topic name, ``str``
00495     :param bag_file: name of bag file to echo messages from or ``None``, ``str``
00496     """
00497     # we have to init a node regardless and bag echoing can print timestamps
00498 
00499 
00500     _check_master()
00501     rospy.init_node(NAME, anonymous=True)
00502     msg_class, real_topic, msg_eval = get_topic_class(topic, blocking=True)
00503     if msg_class is None:
00504         # occurs on ctrl-C
00505         return
00506     callback_echo.msg_eval = msg_eval
00507 
00508     # extract type information for submessages
00509     type_information = None
00510     if len(topic) > len(real_topic):
00511         subtopic = topic[len(real_topic):]
00512         subtopic = subtopic.strip('/')
00513         if subtopic:
00514             fields = subtopic.split('/')
00515             submsg_class = msg_class
00516             while fields:
00517                 field = fields[0].split('[')[0]
00518                 del fields[0]
00519                 index = submsg_class.__slots__.index(field)
00520                 type_information = submsg_class._slot_types[index]
00521                 if fields:
00522                     submsg_class = roslib.message.get_message_class(type_information)
00523 
00524     use_sim_time = rospy.get_param('/use_sim_time', False)
00525     sub = rospy.Subscriber(real_topic, msg_class, callback_echo.callback, {'topic': topic, 'type_information': type_information})
00526     _subscribers[topic] = sub
00527     if use_sim_time:
00528         # #2950: print warning if nothing received for two seconds
00529 
00530         timeout_t = time.time() + 2.
00531         while time.time() < timeout_t and \
00532                 callback_echo.count == 0 and \
00533                 not rospy.is_shutdown() and \
00534                 not callback_echo.done:
00535             _sleep(0.1)
00536 
00537         if callback_echo.count == 0 and \
00538                 not rospy.is_shutdown() and \
00539                 not callback_echo.done:
00540             sys.stderr.write("WARNING: no messages received and simulated time is active.\nIs /clock being published?\n")
00541 
00542     # while not rospy.is_shutdown() and not callback_echo.done:
00543     #     _sleep(0.1)
00544 
00545 
00546 _active_listeners = {}
00547 _subscribers = {}
00548 
00549 def update_topics():
00550         # Replace with topics from C++
00551         topics = cogni_topic_listener.internal_get_topics()
00552 
00553         # Add new topics to listeners
00554         for new_topic in topics:
00555                 if new_topic not in _active_listeners:
00556                         add_topic_listener(new_topic)
00557                         # print("New topic added '" + new_topic + "'")
00558 
00559         # Remove remove topics
00560         for active_topic in _active_listeners.keys():
00561                 if active_topic not in topics:
00562                         _subscribers[active_topic].unregister()
00563                         del _active_listeners[active_topic]
00564                         del _subscribers[active_topic]
00565                         # print("Topic removed '" + active_topic + "'")
00566 
00567 
00568 def add_topic_listener(topic):
00569         callback_echo = CallbackEcho(topic, None)
00570         cogni_topic_echo(topic, callback_echo)
00571         _active_listeners[str(topic)] = callback_echo
00572 
00573 def is_interruption_request():
00574         return cogni_topic_listener.internal_interruption_requested()
00575 
00576 # add_topic_listener("/scan/ranges[0:1]")
00577 # add_topic_listener("/scan/ranges[1:2]")
00578 # add_topic_listener("/scan/ranges[2:3]")
00579 
00580 while True:
00581         _sleep(1)
00582         update_topics() 
00583         if (is_interruption_request()):
00584                 break


scriptable_monitor
Author(s):
autogenerated on Wed Aug 26 2015 16:21:30