generate.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 import datetime
00004 from copy import deepcopy
00005 import logging
00006 import xml.etree.ElementTree as et
00007 import os
00008 import pystache
00009 import re
00010 import string
00011 import sys
00012 import subprocess
00013 import urllib2
00014 
00015 # SDK 3.12.6: https://github.com/Parrot-Developers/arsdk_manifests/blob/d7640c80ed7147971995222d9f4655932a904aa8/release.xml
00016 LIBARCOMMANDS_GIT_OWNER = "Parrot-Developers"
00017 LIBARCOMMANDS_GIT_HASH = "ab28dab91845cd36c4d7002b55f70805deaff3c8"
00018 
00019 # From XML types to ROS primitive types
00020 ROS_TYPE_MAP = {
00021     "bool": "bool",
00022     "u8": "uint8",
00023     "i8": "int8",
00024     "u16": "uint16",
00025     "i16": "int16",
00026     "u32": "uint32",
00027     "i32": "int32",
00028     "u64": "uint64",
00029     "i64": "int64",
00030     "float": "float32",
00031     "double": "float64",
00032     "string": "string",
00033     "enum": "enum"
00034 }
00035 
00036 # From XML types to BebopSDK union defined in ARCONTROLLER_Dictionary.h
00037 BEBOP_TYPE_MAP = {
00038     "bool": "U8",
00039     "u8": "U8",
00040     "i8": "I8",
00041     "u16": "U16",
00042     "i16": "I16",
00043     "u32": "U32",
00044     "i32": "I32",
00045     "u64": "U64",
00046     "i64": "I64",
00047     "float": "Float",
00048     "double": "Double",
00049     "string": "String",
00050     "enum": "I32"
00051 }
00052 
00053 # From XML types to Dynamic Reconfigure Types
00054 DYN_TYPE_MAP = {
00055     "bool": "bool_t",
00056     "u8": "int_t",
00057     "i8": "int_t",
00058     "u16": "int_t",
00059     "i16": "int_t",
00060     "u32": "int_t",
00061     "i32": "int_t",
00062     "u64": "int_t",
00063     "i64": "int_t",
00064     "float": "double_t",
00065     "double": "double_t",
00066     "string": "str_t",
00067     "enum": "enum"
00068 }
00069 
00070 C_TYPE_MAP = {
00071     "bool": "bool",
00072     "u8": "int32_t",
00073     "i8": "int32_t",
00074     "u16": "int32_t",
00075     "i16": "int32_t",
00076     "u32": "int32_t",
00077     "i32": "int32_t",
00078     "u64": "int32_t",
00079     "i64": "int32_t",
00080     "float": "double",  # for rosparam
00081     "double": "double",
00082     "string": "std::string",
00083     "enum": "int32_t"
00084 }
00085 
00086 blacklist_settings_keys = set(["wifiSecurity"])
00087 
00088 min_max_regex = re.compile('\[([0-9\.\-]+)\:([0-9\.\-]+)\]')
00089 rend = pystache.Renderer()
00090 
00091 def get_xml_url(filename):
00092     return rend.render_path("templates/url.mustache",
00093         {"repo_owner": LIBARCOMMANDS_GIT_OWNER, "hash": LIBARCOMMANDS_GIT_HASH, "filename": filename})
00094 
00095 def load_from_url(url):
00096     f = urllib2.urlopen(url)
00097     data = f.read()
00098     f.close()
00099     return data
00100 
00101 def is_state_tag(name):
00102     return (not name.find("State") == -1) and (name.find("Settings") == -1)
00103 
00104 def is_settings_tag(name):
00105     return (not name.find("Settings") == -1) and (name.find("State") == -1)
00106 
00107 def strip_text(text):
00108     return re.sub("\s\s+", " ", text.strip().replace('\n', '').replace('\r', '')).replace('"', '').replace("'", "")
00109 
00110 def cap_word(text):
00111     return text.lower().title()
00112 
00113 def guess_min_max(arg_comment):
00114     m = min_max_regex.search(arg_comment)
00115     if m:
00116         logging.info("  ... [min:max]")
00117         return [float(m.group(1)), float(m.group(2))]
00118     elif (arg_comment.lower().find("m/s2") != -1):
00119         logging.info("  ... acc (m/s2)")
00120         return [0.0, 5.0]
00121     elif (arg_comment.lower().find("m/s") != -1):
00122         logging.info("  ... speed (m/s)")
00123         return [0.0, 10.0]
00124     elif (arg_comment.lower().find("in meters") != -1) or (arg_comment.lower().find("in m") != -1):
00125         logging.info("  ... meters")
00126         return [0, 160]
00127     elif (arg_comment.lower().find("in degree/s") != -1):
00128         logging.info("  ... rotations speed degrees/s")
00129         return [0, 900.0]
00130     elif (arg_comment.lower().find("in degree") != -1):
00131         logging.info("  ... degrees")
00132         return [-180.0, 180.0]
00133     elif (arg_comment.lower().find("1") != -1) and (arg_comment.lower().find("0") != -1):
00134         logging.info("  ... bool")
00135         return [0, 1]
00136     elif (arg_comment.lower().find("latitude") != -1):
00137         logging.info("  ... latitude")
00138         return [-90.0, 90.0]
00139     elif (arg_comment.lower().find("longitude") != -1):
00140         logging.info("  ... longitude")
00141         return [-180.0, 180.0]
00142     elif (arg_comment.lower().find("[rad/s]") != -1):
00143         logging.info("  ... angular speed (rad/s)")
00144         return [0.0, 5.0]
00145     elif (arg_comment.lower().find("channel") != -1):
00146         logging.info("  ... unknown int")
00147         return [0, 50]
00148     elif (arg_comment.lower().find("second") != -1):
00149         logging.info("  ... time (s)")
00150         return [0, 120]
00151 
00152     return []
00153 
00154 def today():
00155     return datetime.datetime.now().strftime("%Y-%m-%d")
00156 
00157 def generate_states(xml_filename):
00158     xml_url = get_xml_url(xml_filename)
00159     project = xml_filename.split(".")[0]
00160 
00161     logging.info("XML Filename: %s" % (xml_filename, ))
00162     logging.info("Fetching source XML file for project %s " % (project, ))
00163     logging.info("URL: %s" % (xml_url, ))
00164     xml = load_from_url(xml_url)
00165     xml_root = et.fromstring(xml)
00166 
00167     # iterate all <class> tags
00168     logging.info("Iterating all State <class> tags ...")
00169 
00170     generator = os.path.basename(__file__)
00171     generator_git_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip()
00172 
00173     d_cpp = dict({
00174             "url": xml_url,
00175             "project": project,
00176             "date": today(),
00177             "generator": generator,
00178             "generator_git_hash": generator_git_hash,
00179             "queue_size": 10,  # 5Hz
00180             "frame_id": "base_link",
00181             "cpp_class": list()
00182         })
00183     d_msg = dict()
00184 
00185     for cl in xml_root.iter("class"):
00186         if not is_state_tag(cl.attrib["name"]):
00187             continue
00188 
00189         # Iterate all cmds
00190         # Generate one .msg and one C++ class for each of them
00191         for cmd in cl.iter("cmd"):
00192             # .msg
00193             msg_name = cap_word(project) + cl.attrib["name"] + cmd.attrib["name"]
00194 
00195             comment_el = cmd.find("comment")
00196             msg_file_comment = ""
00197             if not comment_el is None:
00198                 msg_file_comment = comment_el.attrib["desc"]
00199 
00200             d = dict({
00201                 "url": xml_url,
00202                 "msg_filename": msg_name,
00203                 "date": today(),
00204                 "generator": generator,
00205                 "generator_git_hash": generator_git_hash,
00206                 "msg_file_comment": strip_text(msg_file_comment),
00207                 "msg_field": list()
00208             })
00209 
00210             # C++ class
00211             cpp_class_dict_key = rend.render_path("templates/dictionary_key.mustache",
00212                 {"project": project.upper(), "class": cl.attrib["name"].upper(), "cmd": cmd.attrib["name"].upper()})
00213             # cmd.attrib["name"] and cl.attrib["name"] are already in CamelCase
00214             cpp_class_name = msg_name
00215             cpp_class_instance_name = project.lower() + "_" + cl.attrib["name"].lower() + "_" + cmd.attrib["name"].lower() + "_ptr";
00216             cpp_class_param_name = "states/enable_" + cl.attrib["name"].lower() + "_" + cmd.attrib["name"].lower()
00217             topic_name = "states/" + project + "/" + cl.attrib["name"] + "/" + cmd.attrib["name"]
00218 
00219             arg_list = []
00220             for arg in cmd.iter("arg"):
00221                 # .msg
00222                 f_name = arg.attrib["name"]
00223                 f_type = ROS_TYPE_MAP[arg.attrib.get("type", "bool")]
00224                 f_comment = strip_text(arg.text)
00225                 f_enum_list = list()
00226                 if (f_type == "enum"):
00227                     f_type = "uint8"
00228                     counter = 0
00229                     for enum in arg.iter("enum"):
00230                         f_enum_list.append({
00231                             "constant_name": f_name + "_" + enum.attrib["name"],
00232                             "constant_value": counter,
00233                             "constant_comment": strip_text(enum.text)
00234                             })
00235                         counter += 1
00236 
00237                 d["msg_field"].append({
00238                     "msg_field_type": f_type,
00239                     "msg_field_name": f_name,
00240                     "msg_field_comment": f_comment,
00241                     "msg_field_enum": deepcopy(f_enum_list)
00242                 })
00243 
00244                 # C++ class
00245                 arg_list.append({
00246                     "cpp_class_arg_key": cpp_class_dict_key + "_" + arg.attrib["name"].upper(),
00247                     "cpp_class_arg_name": f_name,
00248                     "cpp_class_arg_sdk_type": BEBOP_TYPE_MAP[arg.attrib.get("type", "bool")]
00249                     })
00250 
00251             d_msg[msg_name] = deepcopy(d)
00252 
00253             # C++ class
00254             d_cpp["cpp_class"].append({
00255                 "cpp_class_name": cpp_class_name,
00256                 "cpp_class_comment": strip_text(msg_file_comment),
00257                 "cpp_class_instance_name": cpp_class_instance_name,
00258                 "cpp_class_param_name": cpp_class_param_name,
00259                 "topic_name": topic_name,
00260                 "latched": "true",
00261                 "cpp_class_msg_type": msg_name,
00262                 "key": cpp_class_dict_key,
00263                 "cpp_class_arg": deepcopy(arg_list)
00264                 })
00265 
00266     logging.info("... Done iterating, writing results to file")
00267     # .msg write
00268     for k, d in d_msg.items():
00269         msg_filename = "%s.msg" % k
00270         logging.info("Writing %s" % (msg_filename, ))
00271         with open(msg_filename, "w") as msg_file:
00272             msg_file.write(rend.render_path("templates/msg.mustache", d))
00273 
00274     header_file_name = "%s_state_callbacks.h" % (project.lower(), )
00275     logging.info("Writing %s" % (header_file_name, ))
00276     with open(header_file_name, "w") as header_file:
00277         header_file.write(rend.render_path("templates/state_callbacks.h.mustache", d_cpp))
00278 
00279     include_file_name = "%s_state_callback_includes.h" % (project.lower(), )
00280     logging.info("Writing %s" % (include_file_name, ))
00281     with open(include_file_name, "w") as include_file:
00282         include_file.write(rend.render_path("templates/state_callback_includes.h.mustache", d_cpp))
00283 
00284     with open("callbacks_common.h", "w") as header_file:
00285         header_file.write(rend.render_path("templates/callbacks_common.h.mustache", d_cpp))
00286 
00287     rst_file_name = "%s_states_param_topic.rst" % (project.lower(), )
00288     logging.info("Writing %s" % (rst_file_name, ))
00289     with open(rst_file_name, "w") as rst_file:
00290         rst_file.write(rend.render_path("templates/states_param_topic.rst.mustache", d_cpp))
00291 
00292 def generate_settings(xml_filename):
00293     xml_url = get_xml_url(xml_filename)
00294     project = xml_filename.split(".")[0]
00295 
00296     logging.info("Fetching source XML file for project %s " % (project, ))
00297     logging.info("URL: %s" % (xml_url, ))
00298     xml = load_from_url(xml_url)
00299     xml_root = et.fromstring(xml)
00300 
00301     generator = os.path.basename(__file__)
00302     generator_git_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip()
00303 
00304     # make sure that the name of the config file matches the third argument
00305     # of gen.generate()
00306     d_cfg = dict({
00307             "cfg_filename": "Bebop%s.cfg" % (project.title(), ),
00308             "url": xml_url,
00309             "project": project.title(),
00310             "date": today(),
00311             "generator": generator,
00312             "generator_git_hash": generator_git_hash,
00313             "cfg_class": list(),
00314             "cpp_class": list()
00315         })
00316 
00317     for cl in xml_root.iter("class"):
00318         if not is_settings_tag(cl.attrib["name"]):
00319             continue
00320 
00321         # At the moment the XML file is not 100% consistent between Settings and SettingsChanged and inner Commands
00322         # 1. Check if `class["name"]State` exists
00323         if not xml_root.findall(".//class[@name='%s']" % (cl.attrib["name"] + "State", )):
00324             logging.warning("No State Class for %s " % (cl.attrib["name"], ))
00325             continue
00326 
00327         # Iterate all cmds
00328         # generate one C++ class for each command
00329         cfg_class_d = {
00330             "cfg_class_name": cl.attrib["name"].lower(),
00331             "cfg_class_comment": strip_text(cl.text),
00332             "cfg_cmd": list()
00333         }
00334         for cmd in cl.iter("cmd"):
00335             # 2. Check if `cmd["name"]Changed` exists
00336             if not xml_root.findall(".//cmd[@name='%s']" % (cmd.attrib["name"] + "Changed", )):
00337                 logging.warning("No Changed CMD for %s " % (cmd.attrib["name"], ))
00338                 continue
00339 
00340             # blacklist
00341             if strip_text(cmd.attrib["name"]) in blacklist_settings_keys:
00342                 logging.warning("Key %s is blacklisted!" % (cmd.attrib["name"], ))
00343                 continue
00344 
00345             comment_el = cmd.find("comment")
00346             cmd_comment = ""
00347             if not comment_el is None:
00348                 cmd_comment = comment_el.attrib["desc"]
00349 
00350 
00351             # .cfg
00352             cfg_cmd_d = {
00353                 "cfg_cmd_comment": strip_text(cmd_comment),
00354                 "cfg_arg": list()
00355             }
00356 
00357             # C++
00358             # We are iterating classes with names ending in "Setting". For each of these classes
00359             # there exists a corresponding class with the same name + "State" (e.g PilotingSetting and PilottingSettingState)
00360             # The inner commands of the corresponding class are also follow a similar conention, they end in "CHANGED".
00361             # We create cfg files based on Settings, and ROS param updates based on SettingsChanged
00362             cpp_class_dict_key = rend.render_path("templates/dictionary_key.mustache",
00363                 {"project": project.upper(), "class": cl.attrib["name"].upper() + "STATE", "cmd": cmd.attrib["name"].upper() + "CHANGED"} )
00364             # cmd.attrib["name"] and cl.attrib["name"] are already in CamelCase
00365             cpp_class_name = cl.attrib["name"] + cmd.attrib["name"]
00366             cpp_class_comment = strip_text(cmd_comment)
00367             cpp_class_instance_name = project.lower() + "_" + cl.attrib["name"].lower() + "_" + cmd.attrib["name"].lower() + "_ptr";
00368             cpp_class_params = list()
00369 
00370             counter = 0
00371             # generate one dyamic reconfigure variable per arg
00372             for arg in cmd.iter("arg"):
00373                 # .cfg
00374                 arg_name = cl.attrib["name"] + cmd.attrib["name"] + cap_word(arg.attrib["name"])
00375                 arg_type = DYN_TYPE_MAP[arg.attrib.get("type", "bool")]
00376                 arg_comment = strip_text(arg.text)
00377 
00378                 arg_enum_list = list()
00379                 minmax_list = list()
00380                 arg_default = 0
00381                 arg_min = 0.0
00382                 arg_max = 0.0
00383                 counter = 0
00384                 need_enum_cast = False
00385                 if (arg_type == "enum"):
00386                     need_enum_cast = True
00387                     arg_type = "int_t"
00388                     for enum in arg.iter("enum"):
00389                         arg_enum_list.append({
00390                             "constant_name": arg_name + "_" + enum.attrib["name"],
00391                             "constant_value": counter,
00392                             "constant_comment": strip_text(enum.text)
00393                             })
00394                         counter += 1
00395                 elif not arg_type == "str_t":
00396                     # No min/max values defined in XML, guessing the type and propose a value:
00397                     logging.info("Guessing type of \"%s\"" % (arg_name))
00398                     logging.info("  from: %s" % (arg_comment))
00399                     minmax_list = guess_min_max(arg_comment)
00400                     if (len(minmax_list) == 2):
00401                         [arg_min, arg_max] = minmax_list
00402                         logging.info("  min: %s max: %s" % (arg_min, arg_max))
00403                     else:
00404                         logging.warning("  Can not guess [min:max] values for this arg, skipping it")
00405 
00406                     # We create a fake enum for int_t types that only accept bool values
00407                     # The source XML should have defined them as bool_t
00408                     # Since these are fake enums (no defines in SDK), we don't need int->enum casting
00409                     if arg_type == "int_t" and arg_min == 0 and arg_max == 1:
00410                         arg_enum_list.append({
00411                             "constant_name": arg_name + "_OFF",
00412                             "constant_value": 0,
00413                             "constant_comment": "Disabled"
00414                             })
00415                         arg_enum_list.append({
00416                             "constant_name": arg_name + "_ON",
00417                             "constant_value": 1,
00418                             "constant_comment": "Enabled"
00419                             })
00420                         counter = 2
00421 
00422                 # either we found minmax or the arg is of type enum
00423                 if len(minmax_list) or need_enum_cast or arg_type == "str_t":
00424                     # hack
00425                     if arg_type == "str_t":
00426                         arg_min = "''"
00427                         arg_max = "''"
00428                         arg_default = "''"
00429 
00430                     cfg_cmd_d["cfg_arg"].append({
00431                         "cfg_arg_type": arg_type,
00432                         "cfg_arg_name": arg_name,
00433                         "cfg_arg_comment": arg_comment,
00434                         "cfg_arg_default": arg_default,
00435                         "cfg_arg_min": arg_min,
00436                         "cfg_arg_max": arg_max,
00437                         # Render once trick: http://stackoverflow.com/a/10118092
00438                         "cfg_arg_enum": {'items' : deepcopy(arg_enum_list)} if len(arg_enum_list) else [],
00439                         "enum_max": counter - 1
00440                         })
00441 
00442                     # generate c enum type
00443                     if (need_enum_cast):
00444                         enum_cast = "static_cast<eARCOMMANDS_%s_%s_%s_%s>" % (project.upper(), cl.attrib["name"].upper(), cmd.attrib["name"].upper(), arg.attrib["name"].upper())
00445                     else:
00446                         enum_cast = ""
00447 
00448                     cpp_class_params.append({
00449                         "cpp_class_arg_key": cpp_class_dict_key + "_" + arg.attrib["name"].upper(),
00450                         "cpp_class_param_name": arg_name,
00451                         "cpp_class_comment": cpp_class_comment,
00452                         "cpp_class_param_enum_cast": enum_cast,
00453                         "cpp_class_param_type": C_TYPE_MAP[arg.attrib.get("type", "bool")],
00454                         "cpp_class_param_sdk_type": BEBOP_TYPE_MAP[arg.attrib.get("type", "bool")]
00455                         })
00456 
00457             # Skip cmds with no arguments
00458             if len(cfg_cmd_d["cfg_arg"]):
00459                 cfg_class_d["cfg_cmd"].append(deepcopy(cfg_cmd_d))
00460                 d_cfg["cpp_class"].append({
00461                     "cpp_class_dict_key": cpp_class_dict_key,
00462                     "cpp_class_name": cpp_class_name,
00463                     "cpp_class_instance_name": cpp_class_instance_name,
00464                     "cpp_class_params": deepcopy(cpp_class_params)
00465                     })
00466 
00467         d_cfg["cfg_class"].append(deepcopy(cfg_class_d))
00468 
00469 
00470     logging.info("... Done iterating, writing results to file")
00471 
00472     # .cfg write
00473     cfg_file_name = d_cfg["cfg_filename"]
00474     logging.info("Writing %s" % (cfg_file_name, ))
00475     with open(cfg_file_name, "w") as cfg_file:
00476         cfg_file.write(rend.render_path("templates/cfg.mustache", d_cfg))
00477 
00478     header_file_name = "%s_setting_callbacks.h" % (project.lower(), )
00479     logging.info("Writing %s" % (header_file_name, ))
00480     with open(header_file_name, "w") as header_file:
00481         header_file.write(rend.render_path("templates/setting_callbacks.h.mustache", d_cfg))
00482 
00483     include_file_name = "%s_setting_callback_includes.h" % (project.lower(), )
00484     logging.info("Writing %s" % (include_file_name, ))
00485     with open(include_file_name, "w") as include_file:
00486         include_file.write(rend.render_path("templates/setting_callback_includes.h.mustache", d_cfg))
00487 
00488     rst_file_name = "%s_settings_param.rst" % (project.lower(), )
00489     logging.info("Writing %s" % (rst_file_name, ))
00490     with open(rst_file_name, "w") as rst_file:
00491         rst_file.write(rend.render_path("templates/settings_param.rst.mustache", d_cfg))
00492 
00493 def main():
00494     # Setup stuff
00495     logging.basicConfig(level="INFO")
00496 
00497     generate_states("common.xml")
00498     generate_states("ardrone3.xml")
00499     #generate_settings("common_commands.xml")
00500     generate_settings("ardrone3.xml")
00501 
00502     generator = os.path.basename(__file__)
00503     generator_git_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip()
00504     with open("last_build_info", "w") as last_build_file:
00505         last_build_file.write(rend.render_path(
00506             "templates/last_build_info.mustache",
00507             {
00508                 "source_hash": LIBARCOMMANDS_GIT_HASH,
00509                 "date": datetime.datetime.now(),
00510                 "generator": generator,
00511                 "generator_git_hash": generator_git_hash
00512             }))
00513 
00514 if __name__ == "__main__":
00515     main()
00516 


bebop_driver
Author(s): Mani Monajjemi
autogenerated on Mon Aug 21 2017 02:36:39