bridge_client.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
00026 import threading
00027 import re
00028 import time
00029 import socket
00030 import urllib2
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 data_item import Event, SimpleCondition, Sample, ThreeDSample
00042 from mtconnect_adapter import Adapter
00043 from long_pull import LongPull
00044 
00045 # Import ROS Python modules
00046 import roslib
00047 import rospy
00048 import actionlib
00049 
00050 ## @class GenericActionClient
00051 ## @brief The GenericActionClient
00052 ## launches a ROS node that will stream XML data from the machine tool and will launch a ROS
00053 ## action client if the machine tool requests an action.  For example, if the machine tool 
00054 ## requests a MaterialLoad action, it will change the MaterialLoad Event tag from 'READY' to
00055 ## 'ACTIVE'.  Once this change is captured by this class, the class launches a ROS MaterialLoadAction
00056 ## client.  Subsequently, the MaterialLoadAction server must be executed to process the action.
00057 ## If not, the system waits until the MaterialLoadAction server is available.
00058 ##
00059 ## Actions and goals are specified by a configuration file that must be included with the
00060 ## main program during execution. If this file is not provided, the node will terminate
00061 ## with an error message indicating the need for this file.
00062 ##
00063 ## Command line example:
00064 ##
00065 ##     bridge_client.py -i bridge_client_config.yaml
00066 ##     bridge_client.py -input bridge_client_config.yaml
00067 ##
00068 ## The class contains the following methods:
00069 ## setup_topic_data -- utilizes introspection to set up class instance variables.
00070 ## action_client -- function triggered by the xml_callback that executes a ROS action.
00071 ## xml_callback -- parses xml stream and launches action client.  Completes 'READY' handshake with machine tool.
00072 class GenericActionClient(object):
00073     ## @brief Constructor for a GenericActionClient
00074     def __init__(self):
00075         # Initialize ROS generic client node
00076         rospy.init_node('ActionClient')
00077         
00078         # Setup MTConnect to ROS Conversion
00079         self.config = bridge_library.obtain_dataMap()
00080         self.msg_parameters = ['url', 'url_port', 'machine_tool', 'xml_namespace', 'adapter_port', 'service']
00081         self.url = self.config[self.msg_parameters[0]]
00082         self.url_port = self.config[self.msg_parameters[1]]
00083         self.mtool = self.config[self.msg_parameters[2]]
00084         self.ns = dict(m = self.config[self.msg_parameters[3]])
00085         self.port = self.config[self.msg_parameters[4]]
00086         self.service = self.config[self.msg_parameters[5]]
00087         
00088         # Check for url connectivity, dwell until system timeout
00089         bridge_library.check_connectivity((1,self.url,self.url_port))
00090         
00091         # Setup MTConnect Adapter for robot status data items
00092         self.adapter = Adapter((self.url, self.port))
00093         
00094         # Create empty lists for actions, message type handles, etc.
00095         self.lib_manifests = []
00096         self.type_handle = None
00097         self.action_list = {}
00098         self.action_goals = {}
00099         self.package = None
00100         self.xml_goal = None
00101         self.di_dict = {} # XML data item dictionary to store MTConnect Adapter Events
00102         self.handshake = None
00103         
00104         # Setup action client data per the config file
00105         self.setup_topic_data()
00106         
00107         # Add data items to the MTConnect Adapter - must be unique data items
00108         bridge_library.add_event((self.adapter, self.action_list, self.di_dict, False))
00109         
00110         # Start the adapter
00111         rospy.loginfo('Start the Robot Link adapter')
00112         self.adapter.start()
00113         
00114         # Create robot Service Server thread for each machine tool action
00115         self.action_service = []
00116         for mt_action in self.action_goals.keys():
00117             self.action_service.append(ActionService(mt_action, self.adapter, self.di_dict, self.service_handle, self.service))
00118         
00119         # Establish XML connection, read in current XML
00120         while True:
00121             try:
00122                 self.conn = HTTPConnection(self.url, self.url_port)
00123                 response = bridge_library.xml_get_response((self.url, self.url_port, self.port, self.conn, self.mtool + "/current"))
00124                 body = response.read()
00125                 break
00126             except socket.error as e:
00127                 rospy.loginfo('HTTP connection error %s' % e)
00128                 time.sleep(10)
00129         
00130         # Parse the XML and determine the current sequence and XML Event elements
00131         seq, elements = bridge_library.xml_components(body, self.ns, self.action_list)
00132 
00133         # Start a streaming XML connection
00134         response = bridge_library.xml_get_response((self.url, self.url_port, self.port, self.conn, self.mtool + "/sample?interval=1000&count=1000&from=" + seq))
00135         
00136         # Create class lock
00137         self.lock = thread.allocate_lock()
00138 
00139         # Create XML polling thread
00140         lp = LongPull(response)
00141         lp.long_pull(self.xml_callback) # Runs until user interrupts
00142         
00143         
00144 
00145     ## @brief This function captures the topic package, type, action goals, and action
00146     ## state conversion from ROS to MTConnect as required for the ROS action client.
00147     ## This task is completed for each topic specified in the configuration file.
00148     ## 
00149     ## This function then performs a relative import of the topic via the getattr(import_module)
00150     ## function.  Data is stored in the following class attributes:
00151     ## 
00152     ##     self.lib_manifests --> used to track which manifests have been loaded.
00153     ##     self.type_handle   --> used for ROS SimpleActionClient, stores package name with action messages.
00154     ##     self.action_list   --> used for ROS SimpleActionClient, stores CNC action request strings.
00155     ##     self.action_goals  --> data structure for message class instances of the topic type.
00156     def setup_topic_data(self):
00157         for package, action in self.config.items():
00158             if package not in self.msg_parameters: # Only one ROS package in config by design
00159                 # Load package manifest if unique
00160                 if package not in self.lib_manifests:
00161                     roslib.load_manifest(package)
00162                     self.lib_manifests.append(package)
00163 
00164                 # Import module
00165                 rospy.loginfo('Importing --> ' + package + '.msg')
00166                 self.type_handle = import_module(package + '.msg')
00167                 self.service_handle = import_module(package + '.srv')
00168                 
00169                 # Capture package for action client
00170                 self.package = package
00171                 
00172                 for action_req in action.keys():
00173                     # Capture action name and action goal xml tag for action client callback reference
00174                     goal_tag = self.config[package][action_req].keys()[0]
00175                     self.action_list[action_req] = {goal_tag : self.config[package][action_req][goal_tag].keys()}
00176                     
00177                     # Capture ROS Action parameters
00178                     self.action_goals[action_req] = None
00179         return    
00180     
00181     ## @brief ROS Action client function that compiles and sends the action goal to the
00182     ## dedicated ROS action server.  The function will block until the server becomes available.
00183     ## After the goal result is received from the dedicated ROS action server, the function
00184     ## sets the robot data item via the MTConnect adapter.
00185     ## 
00186     ## @param cb_data: tuple containing the following parameters:
00187     ## @param name: string containing the data item name for the robot action
00188     ## @param goals: dictionary of data_item:goal pairs in str:str or str:float format
00189     ## @param handle: Python <module 'mtconnect_msgs.msg'>
00190     def action_client(self, cb_data):
00191         # Execute ROS Action
00192         rospy.init_node('ActionClient')
00193         
00194         # Unpack action client data
00195         name, goals, handle = cb_data
00196         rospy.loginfo('Launched %s Action CLient' % name)
00197         
00198         # Creates the SimpleActionClient, passing the type of the action (MaterialLoadAction) to the constructor.
00199         # i.e. name = 'MaterialLoad', action_type = mtconnect_msgs.msg.MaterialLoadAction
00200         action_type = getattr(handle, name + 'Action')
00201         client = actionlib.SimpleActionClient(name + 'Client', action_type)
00202         
00203         # Waits until the action server has started up and started listening for goals.
00204         rospy.loginfo('Waiting for %s Dedicated Action Server' % name)
00205         client.wait_for_server()
00206         rospy.loginfo('%s Dedicated Action Server Activated' % name)
00207 
00208         # Creates a MaterialLoad goal to send to the action server.
00209         # i.e. goal = mtconnect_msgs.msg.MaterialLoad()
00210         goal_handle = getattr(handle, name + 'Goal')
00211         goal = goal_handle()
00212         
00213         # Check to make sure the action goal has been set
00214         if self.action_goals[name] == None:
00215             # Ping the current url for the goal Event
00216             goal_tag = self.action_list[name].keys()[0]
00217             response = bridge_library.xml_get_response((self.url, self.url_port, self.port, self.conn, 
00218                                                         self.mtool + "/current?path=//DataItem[@type='" + goal_tag.upper() +"']"))
00219             body = response.read()
00220             root = ElementTree.fromstring(body)
00221 
00222             # Set the action goals
00223             self.action_goals = bridge_library.set_goal(name, self.action_list[name], root, self.ns, self.action_goals)
00224             
00225         # Check goal source for required attributes
00226         for attrib, attrib_type, action_goal in zip(goal_handle.__slots__, goal_handle._slot_types, self.action_goals[name]):
00227             if bridge_library.type_check(attrib_type, action_goal) == False:
00228                 rospy.logerr('INCOMPATIBLE GOAL TYPES ROS MSG: %s --> XML: %s' % (attrib_type, type(action_goal)))
00229                 sys.exit()
00230         
00231         # Set the goal attributes from XML data
00232         try:
00233             for attrib, xml_goal in zip(goal_handle.__slots__, self.action_goals[name]):
00234                 setattr(goal, attrib, xml_goal)
00235         except Exception as e:
00236             rospy.logerr("ROS Action %s Client failed: %s" % (name, e))
00237         
00238         # Sends the goal to the action server.
00239         rospy.loginfo('Sending the goal')
00240         name_conv = bridge_library.split_event(name)
00241         client.send_goal(goal, done_cb = None, active_cb = bridge_library.action_cb((self.adapter, self.di_dict, name_conv, 'ACTIVE')))
00242         
00243         
00244         # Waits for the server to finish performing the action.
00245         rospy.loginfo('Waiting for result')
00246         client.wait_for_result()
00247         
00248         # Obtain result state
00249         result = client.get_state()
00250         
00251         # Prints out the result of the executing action
00252         rospy.loginfo('Returning the result --> %s' % result)
00253         
00254         # Set the Robot XML data item
00255         data_item = bridge_library.split_event(name)
00256         
00257         # Submit converted result to host via MTConnect adapter
00258         if result == 3: # SUCCEEDED from actionlib_msgs.msg.GoalStatus
00259             rospy.loginfo('Sending COMPLETE flag')
00260             bridge_library.action_cb((self.adapter, self.di_dict, data_item, 'COMPLETE'))
00261             return name
00262         elif result == 4: # ABORTED from actionlib_msgs.msg.GoalStatus
00263             rospy.loginfo('%s ABORTED, terminating action request' % data_item)
00264             bridge_library.action_cb((self.adapter, self.di_dict, data_item, 'FAIL'))
00265             return None
00266 
00267     ## @brief Callback function that launches a ROS action client if the machine
00268     ## tool data item tag is 'ACTIVE'.  Once the action is completed, it completes
00269     ## the handshake signal between the machine tool and the robot via the MTConnect
00270     ## adapter.
00271     ## 
00272     ## @param chunk: xml data, read from response.read()
00273     def xml_callback(self, chunk):
00274         #rospy.loginfo('*******************In PROCESS_XML callback***************')
00275         self.lock.acquire()
00276         try:
00277             # Only grab XML elements for machine tool action requests 
00278             _, elements, self.action_goals = bridge_library.xml_components(chunk, self.ns, self.action_list, get_goal = True, action_goals = self.action_goals)
00279 
00280             if elements:
00281                 # Check for existing handshake, execute it first
00282                 if self.handshake:
00283                     for e in elements:
00284                         if self.handshake == e.attrib['name'] and e.text == 'READY':
00285                             # Send hand shake signal
00286                             bridge_library.action_cb((self.adapter, self.di_dict, e.attrib['name'], 'READY'))
00287                             self.handshake = None
00288                             elements.remove(e)
00289                     if self.handshake != None:
00290                         rospy.logerr('DID NOT RECEIVE READY HANDSHAKE FROM MACHINE TOOL') 
00291                 # Process remaining action requests
00292                 for e in elements:
00293                     # Remove XML namespace string from the element tag for hash tables
00294                     action_text = re.findall(r'(?<=\})\w+',e.tag)[0]
00295                     
00296                     # Check if machine tool is requesting an action, if so, run action client
00297                     if e.text == 'ACTIVE':
00298                         self.handshake = self.action_client((action_text, self.action_goals[action_text], self.type_handle))
00299                         #self.handshake = e.attrib['name']
00300                     
00301         except Exception as e:
00302             rospy.logerr("Generic Action Client: Process XML callback failed: %s, releasing lock" % e)
00303         finally:
00304             self.lock.release()
00305         #rospy.loginfo('*******************Done with PROCESS_XML callback***************')
00306         return
00307 
00308 
00309 class ActionService(GenericActionClient):
00310     def __init__(self, mt_action, adapt, data_item_dict, service_handle, service_state):
00311         
00312         self.mt_action = mt_action
00313         self.adapt = adapt
00314         self.data_item_dict = data_item_dict
00315         self.service_hndle = service_handle
00316         self.service_state = service_state
00317         
00318         rospy.loginfo('STARTED %s SERVICE SERVER THREAD' % mt_action)
00319         self.ss_thread = threading.Thread(target = self.action_service_server)
00320         self.ss_thread.daemon = True
00321         self.ss_thread.start()
00322         
00323     def action_service_server(self):
00324         self.as_name = bridge_library.split_event(self.mt_action)
00325         action_service_class = getattr(self.service_hndle, self.service_state)
00326         s = rospy.Service(self.mt_action + '/' + 'set_mtconnect_state', action_service_class, self.robot_state_callback)
00327         rospy.spin()
00328         return
00329     
00330     def robot_state_callback(self, request):
00331         rospy.loginfo('SERVICE REQUEST --> %s' % request.state_flag)
00332         response_service_class = getattr(self.service_hndle, self.service_state + 'Response')
00333         if request.state_flag == -1: # NOT_READY
00334             try:
00335                 bridge_library.action_cb((self.adapt, self.data_item_dict, self.as_name, 'NOT_READY'))
00336                 return response_service_class(True)
00337             except:
00338                 return response_service_class(False)
00339         elif request.state_flag == 0: # READY
00340             try:
00341                 bridge_library.action_cb((self.adapt, self.data_item_dict, self.as_name, 'READY'))
00342                 return response_service_class(True)
00343             except:
00344                 return response_service_class(False)
00345 
00346 
00347 if __name__ == '__main__':
00348     try:
00349         rospy.loginfo('Starting Generic Action Client')
00350         robot_action = GenericActionClient()
00351     except rospy.ROSInterruptException:
00352         rospy.loginfo('program interrupted before completion')
00353                 


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