bridge_publisher.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 """
00004    Copyright 2013 Southwest Research Institute
00005  
00006    Licensed under the Apache License, Version 2.0 (the "License");
00007    you may not use this file except in compliance with the License.
00008    You may obtain a copy of the License at
00009  
00010      http://www.apache.org/licenses/LICENSE-2.0
00011  
00012    Unless required by applicable law or agreed to in writing, software
00013    distributed under the License is distributed on an "AS IS" BASIS,
00014    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
00015    See the License for the specific language governing permissions and
00016    limitations under the License.
00017    """
00018 
00019 # Import standard Python modules
00020 import sys
00021 import os
00022 import optparse
00023 import yaml
00024 import operator
00025 import thread # for thread locking
00026 import re
00027 import time
00028 import urllib2
00029 from Queue import Queue
00030 from threading import Thread, Timer
00031 from importlib import import_module
00032 from httplib import HTTPConnection
00033 from xml.etree import ElementTree
00034 
00035 # Import custom ROS-MTConnect function library
00036 import bridge_library
00037 
00038 # Import custom Python modules for MTConnect Adapter interface
00039 path, file = os.path.split(__file__)
00040 sys.path.append(os.path.realpath(path) + '/src')
00041 from long_pull import LongPull
00042 
00043 # Import ROS Python modules
00044 import roslib
00045 import rospy
00046 
00047 ## @class BridgePublisher
00048 ## @brief The BridgePublisher
00049 ## parses XML data from an HTTP connection and starts a ROS publishing
00050 ## thread that publishes topic data at 10 hertz.  Topics and topic parameters are specified
00051 ## by a configuration file that must be included with the main program during execution.
00052 ## If this file is not provided, the node will terminate with an error message indicating
00053 ## the need for this file.
00054 ##
00055 ## Command line example:
00056 ##
00057 ##     bridge_publisher.py -i bridge_publisher_config.yaml
00058 ##     bridge_publisher.py -input bridge_publisher_config.yaml
00059 ##
00060 ## The class contains the following methods:
00061 ##
00062 ## setup_topic_data -- utilizes introspection to set up topic instance variables.
00063 ## init_di_dict -- creates hash table of data_item:None pairs.
00064 ## process_xml -- parses xml elements and updates the di_current and di_changed dictionaries.
00065 ## topic_publisher -- gets and sets attributes for a ROS message. Publishes ROS message.
00066 ## xml_callback -- callback function invoked by longpull, executes process_xml function and stores copy of di_changed into XML_queue.
00067 ## ros_publisher -- function that uses a Timer thread to publish ROS topic.  Extracts data from XML_queue and executes topic_publisher.
00068 class BridgePublisher():
00069     ## @brief Constructor for a BridgePublisher
00070     def __init__(self):
00071         # Initialize the ROS publisher node
00072         rospy.init_node('bridge_publisher')
00073         
00074         # Setup MTConnect to ROS Conversion
00075         self.config = bridge_library.obtain_dataMap()
00076         self.msg_parameters = ['url', 'url_port', 'machine_tool', 'xml_namespace']
00077         self.url = self.config[self.msg_parameters[0]]
00078         self.url_port = self.config[self.msg_parameters[1]]
00079         self.mtool = self.config[self.msg_parameters[2]]
00080         self.ns = dict(m = self.config[self.msg_parameters[3]])
00081         
00082         # Check for url connectivity, dwell until system timeout
00083         bridge_library.check_connectivity((1,self.url,self.url_port))
00084         
00085         # Create the data lists for the topic names, types, manifests, data items, ROS publishers, and ROS messages
00086         self.topic_name_list = []
00087         self.topic_type_list = []
00088         self.lib_manifests = []
00089         self.data_items = []
00090         self.pub = []
00091         self.msg = []
00092         self.data_hold = None
00093         
00094         # Setup topic, data items, pub, etc. via configuration file
00095         self.setup_topic_data()
00096         
00097         # Hash table of persistent CNC event states, only updates when state changes
00098         self.di_current = self.init_di_dict()
00099         
00100         # Hash table to store changed CNC event states
00101         self.di_changed = None
00102         
00103         # Establish XML connection, read in current XML
00104         while True:
00105             try:
00106                 self.conn = HTTPConnection(self.url, self.url_port)
00107                 response = bridge_library.xml_get_response((self.url, self.url_port, None, self.conn, self.mtool + "/current"))
00108                 body = response.read()
00109                 break
00110             except socket.error as e:
00111                 rospy.loginfo('HTTP connection error %s' % e)
00112                 time.sleep(10)
00113         
00114         # Parse the XML and determine the current sequence and XML Event elements
00115         seq, elements = bridge_library.xml_components(body, self.ns, self.di_current)
00116         
00117         # Use XML to establish current data item state dictionary
00118         for di in self.di_current.keys():
00119             name = bridge_library.split_event(di)
00120             for e in elements:
00121                 rospy.loginfo('Element %s --> %s' % (e.tag, e.text))
00122                 if name == e.attrib['name']:
00123                     self.di_current[di] = e.text
00124         
00125         # Confirm that data items in config file are present in XML
00126         for di_set in self.data_items:
00127             if not set(di_set).issubset(set(self.di_current.keys())):
00128                 rospy.logerr('ERROR: INCORRECT EVENTS IN TOPIC CONFIG OR XML AGENT FILE')
00129                 sys.exit()
00130         
00131         # Start a streaming XML connection
00132         response = bridge_library.xml_get_response((self.url, self.url_port, None, self.conn, self.mtool + "/sample?interval=1000&count=1000&from=" + seq))
00133         
00134         # Create queue for streaming XML
00135         self.XML_queue = Queue()
00136         
00137         # Create a ROS publishing thread
00138         self.ros_publisher()
00139         
00140         # Streams data from the agent...
00141         lp = LongPull(response)
00142         lp.long_pull(self.xml_callback) # Runs until user interrupts
00143 
00144     ## @brief This function captures the topic name, type, and member
00145     ## attributes that are required for the ROS publisher.  This task
00146     ## is completed for each topic specified in the configuration file.
00147     ## 
00148     ## This function then performs a relative import of the topic
00149     ## via the getattr(import_module) function.  Data is stored in the
00150     ## following class attributes:
00151     ##    
00152     ##     self.data_items   --> used for ROS data structure tracking, stores list of data items for each topic.
00153     ##     self.pub          --> used to setup ROS topic publishers.
00154     ##     self.msg          --> data structure for msg class instances of the topic type.
00155     ##     self.topic_name_list  --> used for configuration file key reference.
00156     ##     self.topic_type_list  --> used for module import and config file key reference.
00157     def setup_topic_data(self):
00158         for topic_name, type_name in self.config.items():
00159             if topic_name not in self.msg_parameters:
00160                 # TOPIC --> topic name, such as 'chatter'
00161                 # TOPIC TYPE --> ROS package and message type, such as mtconnect_msgs/CncStatus
00162                 tn = type_name.keys()[0]
00163                 tokens = tn.split('/')
00164                 package = tokens[0]
00165                 topic_type_name = tokens[1]
00166                 
00167                 # Load package manifest if unique
00168                 if package not in self.lib_manifests:
00169                     roslib.load_manifest(package)
00170                     self.lib_manifests.append(package)
00171     
00172                 # Import module and create topic type class,
00173                 #    i.e. append <class 'mtconnect_msgs.msg._RobotStates.RobotStates'>
00174                 rospy.loginfo('Class Instance --> ' + package + '.msg.' + topic_type_name)
00175                 type_handle = getattr(import_module(package + '.msg'), topic_type_name)
00176                 
00177                 # Append list of data items specified in the ROS topic message
00178                 self.data_items.append(self.config[topic_name][type_name.keys()[0]].keys())
00179                 
00180                 # Create ROS publisher
00181                 self.pub.append(rospy.Publisher(topic_name, type_handle))
00182                 
00183                 # Instance of topic type message class
00184                 self.msg.append(type_handle())
00185                 
00186                 # Capture topic name and type for self.config reference
00187                 self.topic_name_list.append(topic_name)
00188                 self.topic_type_list.append(type_name.keys()[0]) # Only one type per topic
00189         
00190         rospy.loginfo('TOPIC NAME LIST --> %s' % self.topic_name_list)
00191         
00192         return
00193     
00194     ## @brief Initialize the data item dictionary with data item : None pairs.
00195     ## Dictionary used to record only changed event states.
00196     ## @return A dictionary of data_item:None pairs for every data item in the configuration file.
00197     ## Even if multiple topics are used, all of the data items are stored in a single hash table.
00198     def init_di_dict(self):
00199         return {di:None for di_list in self.data_items for di in di_list}
00200     
00201     ## @brief Function determines if an event of interest has changed and updates
00202     ## the current and changed data item dictionaries.  Events that will be
00203     ## published are specified by the user in a .yaml file.
00204     ## @param xml: xml data, read from response.read()
00205     ## @return integer representing the next sequence to be added to XPath expression for streaming
00206     ## @return dictionary of changed data items that includes the ROS attribute value for the ROS msg data structure.
00207     def process_xml(self, xml):
00208         nextSeq, elements = bridge_library.xml_components(xml, self.ns, self.di_current)
00209 
00210         # Create local data item dictionary to track event changes
00211         local_di = self.init_di_dict()
00212 
00213         # Loop through XML elements, update the di_changed and di_current
00214         di_list = [di_val for di_list in self.data_items for di_val in di_list]
00215         
00216         if elements:
00217             rospy.logdebug('***********XML -->*************\n%s' % xml)
00218             for e in elements:
00219                 for d_item in di_list:
00220                     if e.attrib['name'] == bridge_library.split_event(d_item):
00221                         local_di[d_item] = e.text
00222                         self.di_current[d_item] = e.text # Update di_current with new state
00223                         break
00224         return nextSeq, local_di
00225     
00226     ## @brief Publishes the MTConnect message.  This function is a generic publisher.
00227     ## If the machine tool xml changed, the message attribute is updated with this value.
00228     ## Otherwise the message attribute is updated with the value stored in the di_current dictionary.
00229     ## @param cb_data: tuple containing the following parameters:
00230     ## @param topic_name: string containing the name of the ROS topic
00231     ## @param topic_type: string containing the name of the topic type
00232     ## @param pub: ROS publisher object for the topic_name and topic_type
00233     ## @param msg: ROS message class instance for the topic_name and topic_type
00234     ## @param data_items: list containing a string of data items for the specified topic
00235     def topic_publisher(self, cb_data):
00236         # Unpack function arguments
00237         topic_name, topic_type, pub, msg, data_items = cb_data
00238         
00239         msg.header.stamp = rospy.Time.now()
00240         
00241         for di_name, value in self.di_changed.items():
00242             # Convert tag text from MTConnect to ROS and set attribute value
00243             if di_name in data_items:
00244                 if value is not None:
00245                     # Check for conversion key error
00246                     conv_keys = self.config[topic_name][topic_type][di_name].keys()
00247                     if value not in conv_keys:
00248                         rospy.logerr("CONVERSION ERROR IN TOPIC CONFIG FILE: XML is '%s' --> CONFIG is %s" % (value, conv_keys))
00249                         os._exit(0)
00250                     
00251                     # Convert from MTConnect to ROS format from di_changed
00252                     ros_tag = self.config[topic_name][topic_type][di_name][self.di_changed[di_name]]
00253                 else:
00254                     # Convert from MTConnect to ROS format via di_current
00255                     ros_tag = self.config[topic_name][topic_type][di_name][self.di_current[di_name]]
00256                 
00257                 # Obtain the state value via operator.attrgetter object: 'door_state.CLOSED'
00258                 name = bridge_library.split_event(di_name)
00259                 state_object = operator.attrgetter(name + '.' + ros_tag)
00260                 state_value = state_object(msg)
00261                 
00262                 # From the msg class, obtain the message attribute using data item name: attrib = msg.door_state
00263                 attrib = getattr(msg, name)
00264                 
00265                 # Set the attribute to the converted ROS value: setattr(msg, 'door_state.val', 1) --> msg.door_state.val = 1
00266                 setattr(attrib, attrib.__slots__[0], state_value)
00267 
00268         pub.publish(msg)
00269         return
00270     
00271     ## @brief Processes the xml chunk provided by the LongPull class instance and stores the result
00272     ## into di_changed since the tags have changed.  di_changed is then stored into a queue
00273     ## to be used by ros_publisher.  The queue is used to ensure capture of updated tags while
00274     ## ros_publisher is blocked publishing a message.
00275     ## @param chunk: xml data, read from response.read()
00276     def xml_callback(self, chunk):
00277         #rospy.loginfo('*******************In PROCESS_XML callback***************')
00278         try:
00279             _, self.di_changed = self.process_xml(chunk)
00280             if self.di_changed is not None:
00281                 self.XML_queue.put(self.di_changed)
00282                 rospy.logdebug('PUTTING XML INTO QUEUE %s\tNUMBER OF QUEUED OBJECTS %s' % (self.XML_queue, self.XML_queue.qsize()))
00283                 if self.XML_queue.qsize() > 1:
00284                     rospy.loginfo('STORED XML INTO QUEUE, WAITING ON ROS PUBLISHER, QUEUE SIZE --> %s' % self.XML_queue.qsize())
00285         except Exception as e:
00286             rospy.logerr("Bridge Publisher: Process XML callback failed: %s, releasing lock" % e)
00287         finally:
00288             pass
00289         #rospy.loginfo('*******************Done with PROCESS_XML callback***************')
00290         return
00291     
00292     ## @brief Starts a publishing thread at 10 hertz.  Data required for the ROS publisher
00293     ## is obtained from the XML queue.  If data is not available, the function will not launch
00294     ## the ROS publisher.  If the queue is empty, data publishes the previous message.
00295     def ros_publisher(self):
00296         #rospy.loginfo('-------------------In ROS_PUBLISHER callback---------------')
00297 
00298         # Create timer thread
00299         ROSpub = Timer(0.1, self.ros_publisher)
00300         ROSpub.daemon = True
00301         ROSpub.start()
00302         
00303         # Pull from queue if XML is available, if not use stored value
00304         if self.XML_queue.empty() and self.data_hold is not None:
00305             data = self.data_hold
00306         elif not self.XML_queue.empty():
00307             data = self.XML_queue.get()
00308         else:
00309             # XML data is not available, do not publish
00310             data = None
00311         
00312         try:
00313             if data != None:
00314                 # Publish all topics specified in the configuration file
00315                 publishers = []
00316                 for topic_name, topic_type, pub, message, di in zip(self.topic_name_list, self.topic_type_list,
00317                                                                 self.pub, self.msg, self.data_items):
00318                     # Publish topic
00319                     rospy.init_node('bridge_publisher')
00320                     publishers.append(self.topic_publisher((topic_name, topic_type, pub, message, di)))
00321                 
00322                 # Reset current data items to None
00323                 self.di_changed = self.init_di_dict()
00324         except Exception as e:
00325             rospy.logerr("Bridge Publisher: ROS-Publisher callback failed: %s, releasing lock" % e)        
00326         finally:
00327             if data != self.data_hold:
00328                 # Update the persistent variable and close the queue
00329                 self.data_hold = data
00330                 self.XML_queue.task_done()
00331             else:
00332                 pass
00333 
00334         #rospy.loginfo('-------------------Done with ROS_PUBLISHER callback---------------')
00335         return
00336 
00337 if __name__ == '__main__':
00338     try:
00339         rospy.loginfo('Launching Bridge Publisher -- Publishing Machine Tool Messages')
00340         mtc_parse = BridgePublisher()
00341     except rospy.ROSInterruptException:
00342         sys.exit(0)


mtconnect_ros_bridge
Author(s): Stephen L. Wiedmann
autogenerated on Mon Jan 6 2014 11:30:45