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 ## @package bridge_library.py 00020 ## This module contains functions utilized by the MTConnect to ROS bridge nodes. 00021 ## Currently, this module contains methods for HTTP connectivity verification, 00022 ## XML parsing, importing configuration files, and several MTConnect Adapter 00023 ## related functions. 00024 00025 # Import standard Python modules 00026 import sys 00027 import os 00028 import optparse 00029 import yaml 00030 import re 00031 import time 00032 import urllib2 00033 from xml.etree import ElementTree 00034 00035 # Import custom Python modules for MTConnect Adapter interface 00036 path, file = os.path.split(__file__) 00037 sys.path.append(os.path.realpath(path) + '/src') 00038 from data_item import Event, SimpleCondition, Sample, ThreeDSample 00039 00040 # Import ROS Python modules 00041 import rospy 00042 00043 ## @brief obtain_dataMap Function documentation. 00044 ## 00045 ## This function utilizes python option parser to determine the option filename. 00046 ## Once the file name is obtained, the .yaml file contents are stored in a dictionary. 00047 ## Program terminates if the option file is not available, or if it is in an 00048 ## incorrect YAML format. 00049 ## 00050 ## This function does not take any arguments. 00051 ## 00052 ## @return: dataMap, dictionary of node parameters 00053 def obtain_dataMap(): 00054 ## @brief determine_config_file_name Function documentation 00055 ## 00056 ## In order to execute a ROS bridge node a configuration file in YAML 00057 ## format is required. This function allows the following input 00058 ## options for the configuration file: 00059 ## -i, --input 00060 def determine_config_file_name(): 00061 parser = optparse.OptionParser() 00062 parser.add_option('-i', '--input', 00063 dest="input_filename", 00064 default=None, 00065 ) 00066 options, remainder = parser.parse_args() 00067 00068 if not options.input_filename: 00069 print('ERROR: Must provide .yaml configuration file') 00070 sys.exit(0) 00071 return options.input_filename if options.input_filename else None 00072 00073 fn = determine_config_file_name() 00074 00075 # Read file contents and store into dataMap dictionary 00076 try: 00077 with open(fn) as f: 00078 dataMap = yaml.load(f) 00079 if dataMap == {}: 00080 sys.exit(0) 00081 except IOError as e: 00082 print('({})'.format(e)) 00083 sys.exit(0) 00084 return dataMap 00085 00086 ## @brief check_connectivity Function documentation 00087 ## 00088 ## The purpose of this function is to determine if an HTTP connection is available. 00089 ## It will continue to try to make a connection up to a user specified time. 00090 ## 00091 ## This function takes the following arguments: 00092 ## @param data: data is a tuple containing the following parameters: 00093 ## 00094 ## @param tout: int, allowable time in seconds before the open request times out 00095 ## @param url: string, url that will be opened 00096 ## @param url_port: int, url port that will be concatenated to the url string 00097 def check_connectivity(data): 00098 # Unpack function arguments 00099 tout, url, url_port = data 00100 00101 # Verify url availability, dwell until time out 00102 current = time.time() 00103 time_out = current + 20.0 00104 rospy.loginfo('Checking for URL availability') 00105 while time_out > current: 00106 try: 00107 response = urllib2.urlopen('http://' + url + ':' + str(url_port) + '/current', timeout = tout) 00108 rospy.loginfo('Connection available') 00109 break 00110 except urllib2.URLError as err: 00111 current = time.time() 00112 pass 00113 else: 00114 rospy.loginfo('System Time Out: URL Unavailable, check if the MTConnect Agent is running') 00115 sys.exit() 00116 return 00117 00118 ## @brief xml_get_response Function documentation 00119 ## 00120 ## This function determines if an HTTP connection can be made. If so, it returns a response 00121 ## to a user specified "GET" request. 00122 ## 00123 ## This function takes the following arguments: 00124 ## @param data: data is a tuple containing the following parameters: 00125 ## 00126 ## @param url: string, url that will be opened 00127 ## @param url_port: int, url port that will be concatenated to the url string 00128 ## @param port: int, Adapter port used by MTConnect adapter.py module 00129 ## @param conn: Python httplib.HTTPConnection 00130 ## @param req: string, user specified selector url 00131 ## 00132 ## @return: response, "GET" response 00133 def xml_get_response(data): 00134 # Unpack data 00135 url, url_port, port, conn, req = data 00136 00137 # Get response from url 00138 rospy.logdebug('Attempting HTTP connection on url: %s:%s\tPort:%s' % (url, url_port, port)) 00139 conn.request("GET", req) 00140 response = conn.getresponse() 00141 if response.status != 200: 00142 rospy.logerror("Request failed: %s - %d" % (response.reason, response.status)) 00143 sys.exit(0) 00144 else: 00145 rospy.logdebug('Request --> %s, Status --> %s' % (response.reason, response.status)) 00146 return response 00147 00148 ## @brief xml_components Function documentation 00149 ## 00150 ## This function finds all elements in the updated XML. If an action goal is required, 00151 ## the string acquired from the XML is parsed and returned with the appropriate type. 00152 ## For example, if the goal is "'ALUMINUM 6061-T6', 5.00, 2.50", the function will 00153 ## convert this string into the following list ['ALUMINUM 6061-T6', 5.00, 2.50] which contains 00154 ## the following types: [str, float, float] 00155 ## 00156 ## This function takes the following arguments: 00157 ## @param xml: XML data, read from response.read() 00158 ## @param ns: dictionary, xml namespace dictionary 00159 ## @param tag_list: dictionary, xml tag stored as tag:goal or tag:tag pairs 00160 ## @param get_goal: boolean, optional parameter, used when a action goal is required 00161 ## @param action_goals: dictionary, optional parameter, stored action goals by XML tag key 00162 ## 00163 ## Function returns: 00164 ## @return: nextSeq, int, next XML sequence for streaming XML via longpull.py 00165 ## @return: elements, Python Element object 00166 ## @return: [optional] action_goals, dictionary of unchanged xml_tag:goal pairs 00167 ## @return: [optional] request_goal, dictionary of updated xml_tag:goal pairs 00168 def xml_components(xml, ns, tag_list, get_goal = False, action_goals = None): 00169 # Extract XML Event elements 00170 root = ElementTree.fromstring(xml) 00171 header = root.find('.//m:Header', namespaces = ns) 00172 nextSeq = header.attrib['nextSequence'] 00173 00174 elements = [] 00175 find_goal = None 00176 00177 for tag, goals in tag_list.items(): 00178 # Create a list of XML elements 00179 element_list = root.findall('.//m:' + tag, namespaces = ns) 00180 if element_list: # Element list is not empty 00181 for e in element_list: 00182 elements.append(e) 00183 00184 # Check if a goal must be captured 00185 if get_goal == True: 00186 request_goal = set_goal(tag, goals, root, ns, action_goals) 00187 00188 if get_goal == True and request_goal is not None: 00189 return nextSeq, elements, request_goal 00190 elif get_goal == True and request_goal == None: 00191 return nextSeq, elements, action_goals 00192 else: 00193 return nextSeq, elements 00194 00195 ## @brief set_goal Function documentation 00196 ## 00197 ## This function extracts the machine tool request goal from the Event tag specified in the configuration file. 00198 ## For example, during MaterialLoad the request goal tag is Material which is an Event that stores the material 00199 ## specifications: material type, material length, material diameter. 00200 ## 00201 ## The function takes the following arguments: 00202 ## @param tag: string of the Event tag, i.e. 'MaterialLoad' 00203 ## @param goals: dictionary of the goal (str):goal attributes (list of strings) 00204 ## @param root: XML ElementTree object that converted the XML chunk in string format to an ElementTree object 00205 ## @param ns: dictionary, XML namespace dictionary 00206 ## @param action_goals: dictionary, stored action goals by XML tag key 00207 ## 00208 ## The function returns: 00209 ## @return: action_goals, dictionary, hash table of XML tag:goal pairs 00210 ## @return: None type if the action goal tag is not present in the XML 00211 def set_goal(tag, goals, root, ns, action_goals): 00212 goal_tag = goals.keys()[0] 00213 find_goal = root.findall('.//m:' + goal_tag, namespaces = ns) 00214 00215 if find_goal: 00216 rospy.loginfo('GOAL-ELEMENT SET--> %s' % find_goal[0].text) 00217 goal_conv = [] 00218 tokens = find_goal[0].text.split(", ") 00219 for item in tokens: 00220 try: 00221 goal_conv.append(float(item)) 00222 except ValueError: 00223 goal_conv.append(item) 00224 action_goals[tag] = goal_conv 00225 rospy.loginfo('Action Goals Set --> %s' % action_goals) 00226 00227 return action_goals 00228 else: 00229 return None 00230 00231 ## @brief add_event Function documentation 00232 ## 00233 ## This function creates instances of the Adapter Event class for 00234 ## each of the XML tags provided to the function. 00235 ## 00236 ## This function takes the following arguments: 00237 ## @param data: data is a tuple containing the following parameters: 00238 ## 00239 ## @param adapter: Adapter class object 00240 ## @param tag_list: list of XML tags culled from node configuration file 00241 ## @param di_dict: dictionary {string:string}, stored Adapter Event class instances for each XML tag 00242 ## @param init: boolean, user specified boolean to determine if the Adapter Events must be initialized 00243 def add_event(data): 00244 # Unpack function arguments 00245 adapter, tag_list, di_dict, init = data 00246 00247 for tag in tag_list: 00248 # Change tag to XML [name] attribute format if necessary 00249 if not tag.islower(): 00250 data_item = split_event(tag) 00251 else: 00252 data_item = tag 00253 00254 # Add Event to the MTConnect adapter 00255 di_dict[data_item] = Event(data_item) 00256 adapter.add_data_item(di_dict[data_item]) 00257 00258 # Output success 00259 rospy.loginfo('Added XML data_item --> %s' % data_item) 00260 00261 if init == True: 00262 # Set initial states for robot actions 00263 adapter.begin_gather() 00264 for data_item, event in di_dict.items(): 00265 event.set_value('READY') 00266 adapter.complete_gather() 00267 return 00268 00269 ## @brief split_event Function documentation 00270 ## 00271 ## This function converts a data item Event string from CamelCase to camel_case 00272 ## 00273 ## This function takes and returns the following arguments: 00274 ## @param xml_tag: string, Event data item in CamelCase format 00275 ## @return: data_item, string, Event data item in camel_case format 00276 def split_event(xml_tag): 00277 tokens = re.findall(r'([A-Z][a-z]*)', xml_tag) 00278 tokenlist = [val.lower() for val in tokens] 00279 data_item = tokenlist[0] + '_' + tokenlist[1] 00280 return data_item 00281 00282 ## @brief action_cb Function documentation 00283 ## 00284 ## This function sets the value of an Adapter Event. It is used to port 00285 ## XML tag changes back to machine tool. 00286 ## 00287 ## This function takes the following arguments: 00288 ## @param data: data is a tuple containing the following parameters: 00289 ## 00290 ## @param adapter: Adapter class object 00291 ## @param di_dict: dictionary {string:string}, stored Adapter Event class instances for each XML tag 00292 ## @param data_item: string, data item used to locate the Adapter Event 00293 ## @param state: string, Event will be changed to this value 00294 def action_cb(data): 00295 # Unpack function arguments 00296 adapter, di_dict, data_item, state = data 00297 00298 # Respond that goal is accepted 00299 #rospy.loginfo("Changing %s to '%s'" % (data_item, state)) 00300 adapter.begin_gather() 00301 di_dict[data_item].set_value(state) 00302 adapter.complete_gather() 00303 return 00304 00305 ## @brief type_check Function documentation 00306 ## 00307 ## This function checks the goal types and converts them to Python 00308 ## standard types. It then verifies if the goal type matches the type 00309 ## specified in the goal message. 00310 ## 00311 ## This function takes the following arguments: 00312 ## @param goal_type: string, goal_type from goal msg.__slot_types 00313 ## @param request: varies, actual goal value that is a float, int, string, etc. 00314 ## @return boolean: True for a match, False for an error 00315 def type_check(goal_type, request): 00316 type_dict = {'bool': bool, 'int8': int, 'uint8': int, 00317 'int16': int, 'unit16': int, 'int32': int, 00318 'unit32': int, 'int64': long, 'unint64': long, 00319 'float32': float, 'float64': float, 'string': str} 00320 return type_dict[goal_type] == type(request)