generate.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 import datetime
4 from copy import deepcopy
5 import logging
6 import xml.etree.ElementTree as et
7 import os
8 import pystache
9 import re
10 import string
11 import sys
12 import subprocess
13 import urllib2
14 
15 # SDK 3.12.6: https://github.com/Parrot-Developers/arsdk_manifests/blob/d7640c80ed7147971995222d9f4655932a904aa8/release.xml
16 LIBARCOMMANDS_GIT_OWNER = "Parrot-Developers"
17 LIBARCOMMANDS_GIT_HASH = "ab28dab91845cd36c4d7002b55f70805deaff3c8"
18 
19 # From XML types to ROS primitive types
20 ROS_TYPE_MAP = {
21  "bool": "bool",
22  "u8": "uint8",
23  "i8": "int8",
24  "u16": "uint16",
25  "i16": "int16",
26  "u32": "uint32",
27  "i32": "int32",
28  "u64": "uint64",
29  "i64": "int64",
30  "float": "float32",
31  "double": "float64",
32  "string": "string",
33  "enum": "enum"
34 }
35 
36 # From XML types to BebopSDK union defined in ARCONTROLLER_Dictionary.h
37 BEBOP_TYPE_MAP = {
38  "bool": "U8",
39  "u8": "U8",
40  "i8": "I8",
41  "u16": "U16",
42  "i16": "I16",
43  "u32": "U32",
44  "i32": "I32",
45  "u64": "U64",
46  "i64": "I64",
47  "float": "Float",
48  "double": "Double",
49  "string": "String",
50  "enum": "I32"
51 }
52 
53 # From XML types to Dynamic Reconfigure Types
54 DYN_TYPE_MAP = {
55  "bool": "bool_t",
56  "u8": "int_t",
57  "i8": "int_t",
58  "u16": "int_t",
59  "i16": "int_t",
60  "u32": "int_t",
61  "i32": "int_t",
62  "u64": "int_t",
63  "i64": "int_t",
64  "float": "double_t",
65  "double": "double_t",
66  "string": "str_t",
67  "enum": "enum"
68 }
69 
70 C_TYPE_MAP = {
71  "bool": "bool",
72  "u8": "int32_t",
73  "i8": "int32_t",
74  "u16": "int32_t",
75  "i16": "int32_t",
76  "u32": "int32_t",
77  "i32": "int32_t",
78  "u64": "int32_t",
79  "i64": "int32_t",
80  "float": "double", # for rosparam
81  "double": "double",
82  "string": "std::string",
83  "enum": "int32_t"
84 }
85 
86 blacklist_settings_keys = set(["wifiSecurity"])
87 
88 min_max_regex = re.compile('\[([0-9\.\-]+)\:([0-9\.\-]+)\]')
89 rend = pystache.Renderer()
90 
91 def get_xml_url(filename):
92  return rend.render_path("templates/url.mustache",
93  {"repo_owner": LIBARCOMMANDS_GIT_OWNER, "hash": LIBARCOMMANDS_GIT_HASH, "filename": filename})
94 
95 def load_from_url(url):
96  f = urllib2.urlopen(url)
97  data = f.read()
98  f.close()
99  return data
100 
101 def is_state_tag(name):
102  return (not name.find("State") == -1) and (name.find("Settings") == -1)
103 
104 def is_settings_tag(name):
105  return (not name.find("Settings") == -1) and (name.find("State") == -1)
106 
107 def strip_text(text):
108  return re.sub("\s\s+", " ", text.strip().replace('\n', '').replace('\r', '')).replace('"', '').replace("'", "")
109 
110 def cap_word(text):
111  return text.lower().title()
112 
113 def guess_min_max(arg_comment):
114  m = min_max_regex.search(arg_comment)
115  if m:
116  logging.info(" ... [min:max]")
117  return [float(m.group(1)), float(m.group(2))]
118  elif (arg_comment.lower().find("m/s2") != -1):
119  logging.info(" ... acc (m/s2)")
120  return [0.0, 5.0]
121  elif (arg_comment.lower().find("m/s") != -1):
122  logging.info(" ... speed (m/s)")
123  return [0.0, 10.0]
124  elif (arg_comment.lower().find("in meters") != -1) or (arg_comment.lower().find("in m") != -1):
125  logging.info(" ... meters")
126  return [0, 160]
127  elif (arg_comment.lower().find("in degree/s") != -1):
128  logging.info(" ... rotations speed degrees/s")
129  return [0, 900.0]
130  elif (arg_comment.lower().find("in degree") != -1):
131  logging.info(" ... degrees")
132  return [-180.0, 180.0]
133  elif (arg_comment.lower().find("1") != -1) and (arg_comment.lower().find("0") != -1):
134  logging.info(" ... bool")
135  return [0, 1]
136  elif (arg_comment.lower().find("latitude") != -1):
137  logging.info(" ... latitude")
138  return [-90.0, 90.0]
139  elif (arg_comment.lower().find("longitude") != -1):
140  logging.info(" ... longitude")
141  return [-180.0, 180.0]
142  elif (arg_comment.lower().find("[rad/s]") != -1):
143  logging.info(" ... angular speed (rad/s)")
144  return [0.0, 5.0]
145  elif (arg_comment.lower().find("channel") != -1):
146  logging.info(" ... unknown int")
147  return [0, 50]
148  elif (arg_comment.lower().find("second") != -1):
149  logging.info(" ... time (s)")
150  return [0, 120]
151 
152  return []
153 
154 def today():
155  return datetime.datetime.now().strftime("%Y-%m-%d")
156 
157 def generate_states(xml_filename):
158  xml_url = get_xml_url(xml_filename)
159  project = xml_filename.split(".")[0]
160 
161  logging.info("XML Filename: %s" % (xml_filename, ))
162  logging.info("Fetching source XML file for project %s " % (project, ))
163  logging.info("URL: %s" % (xml_url, ))
164  xml = load_from_url(xml_url)
165  xml_root = et.fromstring(xml)
166 
167  # iterate all <class> tags
168  logging.info("Iterating all State <class> tags ...")
169 
170  generator = os.path.basename(__file__)
171  generator_git_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip()
172 
173  d_cpp = dict({
174  "url": xml_url,
175  "project": project,
176  "date": today(),
177  "generator": generator,
178  "generator_git_hash": generator_git_hash,
179  "queue_size": 10, # 5Hz
180  "frame_id": "base_link",
181  "cpp_class": list()
182  })
183  d_msg = dict()
184 
185  for cl in xml_root.iter("class"):
186  if not is_state_tag(cl.attrib["name"]):
187  continue
188 
189  # Iterate all cmds
190  # Generate one .msg and one C++ class for each of them
191  for cmd in cl.iter("cmd"):
192  # .msg
193  msg_name = cap_word(project) + cl.attrib["name"] + cmd.attrib["name"]
194 
195  comment_el = cmd.find("comment")
196  msg_file_comment = ""
197  if not comment_el is None:
198  msg_file_comment = comment_el.attrib["desc"]
199 
200  d = dict({
201  "url": xml_url,
202  "msg_filename": msg_name,
203  "date": today(),
204  "generator": generator,
205  "generator_git_hash": generator_git_hash,
206  "msg_file_comment": strip_text(msg_file_comment),
207  "msg_field": list()
208  })
209 
210  # C++ class
211  cpp_class_dict_key = rend.render_path("templates/dictionary_key.mustache",
212  {"project": project.upper(), "class": cl.attrib["name"].upper(), "cmd": cmd.attrib["name"].upper()})
213  # cmd.attrib["name"] and cl.attrib["name"] are already in CamelCase
214  cpp_class_name = msg_name
215  cpp_class_instance_name = project.lower() + "_" + cl.attrib["name"].lower() + "_" + cmd.attrib["name"].lower() + "_ptr";
216  cpp_class_param_name = "states/enable_" + cl.attrib["name"].lower() + "_" + cmd.attrib["name"].lower()
217  topic_name = "states/" + project + "/" + cl.attrib["name"] + "/" + cmd.attrib["name"]
218 
219  arg_list = []
220  for arg in cmd.iter("arg"):
221  # .msg
222  f_name = arg.attrib["name"]
223  f_type = ROS_TYPE_MAP[arg.attrib.get("type", "bool")]
224  f_comment = strip_text(arg.text)
225  f_enum_list = list()
226  if (f_type == "enum"):
227  f_type = "uint8"
228  counter = 0
229  for enum in arg.iter("enum"):
230  f_enum_list.append({
231  "constant_name": f_name + "_" + enum.attrib["name"],
232  "constant_value": counter,
233  "constant_comment": strip_text(enum.text)
234  })
235  counter += 1
236 
237  d["msg_field"].append({
238  "msg_field_type": f_type,
239  "msg_field_name": f_name,
240  "msg_field_comment": f_comment,
241  "msg_field_enum": deepcopy(f_enum_list)
242  })
243 
244  # C++ class
245  arg_list.append({
246  "cpp_class_arg_key": cpp_class_dict_key + "_" + arg.attrib["name"].upper(),
247  "cpp_class_arg_name": f_name,
248  "cpp_class_arg_sdk_type": BEBOP_TYPE_MAP[arg.attrib.get("type", "bool")]
249  })
250 
251  d_msg[msg_name] = deepcopy(d)
252 
253  # C++ class
254  d_cpp["cpp_class"].append({
255  "cpp_class_name": cpp_class_name,
256  "cpp_class_comment": strip_text(msg_file_comment),
257  "cpp_class_instance_name": cpp_class_instance_name,
258  "cpp_class_param_name": cpp_class_param_name,
259  "topic_name": topic_name,
260  "latched": "true",
261  "cpp_class_msg_type": msg_name,
262  "key": cpp_class_dict_key,
263  "cpp_class_arg": deepcopy(arg_list)
264  })
265 
266  logging.info("... Done iterating, writing results to file")
267  # .msg write
268  for k, d in d_msg.items():
269  msg_filename = "%s.msg" % k
270  logging.info("Writing %s" % (msg_filename, ))
271  with open(msg_filename, "w") as msg_file:
272  msg_file.write(rend.render_path("templates/msg.mustache", d))
273 
274  header_file_name = "%s_state_callbacks.h" % (project.lower(), )
275  logging.info("Writing %s" % (header_file_name, ))
276  with open(header_file_name, "w") as header_file:
277  header_file.write(rend.render_path("templates/state_callbacks.h.mustache", d_cpp))
278 
279  include_file_name = "%s_state_callback_includes.h" % (project.lower(), )
280  logging.info("Writing %s" % (include_file_name, ))
281  with open(include_file_name, "w") as include_file:
282  include_file.write(rend.render_path("templates/state_callback_includes.h.mustache", d_cpp))
283 
284  with open("callbacks_common.h", "w") as header_file:
285  header_file.write(rend.render_path("templates/callbacks_common.h.mustache", d_cpp))
286 
287  rst_file_name = "%s_states_param_topic.rst" % (project.lower(), )
288  logging.info("Writing %s" % (rst_file_name, ))
289  with open(rst_file_name, "w") as rst_file:
290  rst_file.write(rend.render_path("templates/states_param_topic.rst.mustache", d_cpp))
291 
292 def generate_settings(xml_filename):
293  xml_url = get_xml_url(xml_filename)
294  project = xml_filename.split(".")[0]
295 
296  logging.info("Fetching source XML file for project %s " % (project, ))
297  logging.info("URL: %s" % (xml_url, ))
298  xml = load_from_url(xml_url)
299  xml_root = et.fromstring(xml)
300 
301  generator = os.path.basename(__file__)
302  generator_git_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip()
303 
304  # make sure that the name of the config file matches the third argument
305  # of gen.generate()
306  d_cfg = dict({
307  "cfg_filename": "Bebop%s.cfg" % (project.title(), ),
308  "url": xml_url,
309  "project": project.title(),
310  "date": today(),
311  "generator": generator,
312  "generator_git_hash": generator_git_hash,
313  "cfg_class": list(),
314  "cpp_class": list()
315  })
316 
317  for cl in xml_root.iter("class"):
318  if not is_settings_tag(cl.attrib["name"]):
319  continue
320 
321  # At the moment the XML file is not 100% consistent between Settings and SettingsChanged and inner Commands
322  # 1. Check if `class["name"]State` exists
323  if not xml_root.findall(".//class[@name='%s']" % (cl.attrib["name"] + "State", )):
324  logging.warning("No State Class for %s " % (cl.attrib["name"], ))
325  continue
326 
327  # Iterate all cmds
328  # generate one C++ class for each command
329  cfg_class_d = {
330  "cfg_class_name": cl.attrib["name"].lower(),
331  "cfg_class_comment": strip_text(cl.text),
332  "cfg_cmd": list()
333  }
334  for cmd in cl.iter("cmd"):
335  # 2. Check if `cmd["name"]Changed` exists
336  if not xml_root.findall(".//cmd[@name='%s']" % (cmd.attrib["name"] + "Changed", )):
337  logging.warning("No Changed CMD for %s " % (cmd.attrib["name"], ))
338  continue
339 
340  # blacklist
341  if strip_text(cmd.attrib["name"]) in blacklist_settings_keys:
342  logging.warning("Key %s is blacklisted!" % (cmd.attrib["name"], ))
343  continue
344 
345  comment_el = cmd.find("comment")
346  cmd_comment = ""
347  if not comment_el is None:
348  cmd_comment = comment_el.attrib["desc"]
349 
350 
351  # .cfg
352  cfg_cmd_d = {
353  "cfg_cmd_comment": strip_text(cmd_comment),
354  "cfg_arg": list()
355  }
356 
357  # C++
358  # We are iterating classes with names ending in "Setting". For each of these classes
359  # there exists a corresponding class with the same name + "State" (e.g PilotingSetting and PilottingSettingState)
360  # The inner commands of the corresponding class are also follow a similar conention, they end in "CHANGED".
361  # We create cfg files based on Settings, and ROS param updates based on SettingsChanged
362  cpp_class_dict_key = rend.render_path("templates/dictionary_key.mustache",
363  {"project": project.upper(), "class": cl.attrib["name"].upper() + "STATE", "cmd": cmd.attrib["name"].upper() + "CHANGED"} )
364  # cmd.attrib["name"] and cl.attrib["name"] are already in CamelCase
365  cpp_class_name = cl.attrib["name"] + cmd.attrib["name"]
366  cpp_class_comment = strip_text(cmd_comment)
367  cpp_class_instance_name = project.lower() + "_" + cl.attrib["name"].lower() + "_" + cmd.attrib["name"].lower() + "_ptr";
368  cpp_class_params = list()
369 
370  counter = 0
371  # generate one dyamic reconfigure variable per arg
372  for arg in cmd.iter("arg"):
373  # .cfg
374  arg_name = cl.attrib["name"] + cmd.attrib["name"] + cap_word(arg.attrib["name"])
375  arg_type = DYN_TYPE_MAP[arg.attrib.get("type", "bool")]
376  arg_comment = strip_text(arg.text)
377 
378  arg_enum_list = list()
379  minmax_list = list()
380  arg_default = 0
381  arg_min = 0.0
382  arg_max = 0.0
383  counter = 0
384  need_enum_cast = False
385  if (arg_type == "enum"):
386  need_enum_cast = True
387  arg_type = "int_t"
388  for enum in arg.iter("enum"):
389  arg_enum_list.append({
390  "constant_name": arg_name + "_" + enum.attrib["name"],
391  "constant_value": counter,
392  "constant_comment": strip_text(enum.text)
393  })
394  counter += 1
395  elif not arg_type == "str_t":
396  # No min/max values defined in XML, guessing the type and propose a value:
397  logging.info("Guessing type of \"%s\"" % (arg_name))
398  logging.info(" from: %s" % (arg_comment))
399  minmax_list = guess_min_max(arg_comment)
400  if (len(minmax_list) == 2):
401  [arg_min, arg_max] = minmax_list
402  logging.info(" min: %s max: %s" % (arg_min, arg_max))
403  else:
404  logging.warning(" Can not guess [min:max] values for this arg, skipping it")
405 
406  # We create a fake enum for int_t types that only accept bool values
407  # The source XML should have defined them as bool_t
408  # Since these are fake enums (no defines in SDK), we don't need int->enum casting
409  if arg_type == "int_t" and arg_min == 0 and arg_max == 1:
410  arg_enum_list.append({
411  "constant_name": arg_name + "_OFF",
412  "constant_value": 0,
413  "constant_comment": "Disabled"
414  })
415  arg_enum_list.append({
416  "constant_name": arg_name + "_ON",
417  "constant_value": 1,
418  "constant_comment": "Enabled"
419  })
420  counter = 2
421 
422  # either we found minmax or the arg is of type enum
423  if len(minmax_list) or need_enum_cast or arg_type == "str_t":
424  # hack
425  if arg_type == "str_t":
426  arg_min = "''"
427  arg_max = "''"
428  arg_default = "''"
429 
430  cfg_cmd_d["cfg_arg"].append({
431  "cfg_arg_type": arg_type,
432  "cfg_arg_name": arg_name,
433  "cfg_arg_comment": arg_comment,
434  "cfg_arg_default": arg_default,
435  "cfg_arg_min": arg_min,
436  "cfg_arg_max": arg_max,
437  # Render once trick: http://stackoverflow.com/a/10118092
438  "cfg_arg_enum": {'items' : deepcopy(arg_enum_list)} if len(arg_enum_list) else [],
439  "enum_max": counter - 1
440  })
441 
442  # generate c enum type
443  if (need_enum_cast):
444  enum_cast = "static_cast<eARCOMMANDS_%s_%s_%s_%s>" % (project.upper(), cl.attrib["name"].upper(), cmd.attrib["name"].upper(), arg.attrib["name"].upper())
445  else:
446  enum_cast = ""
447 
448  cpp_class_params.append({
449  "cpp_class_arg_key": cpp_class_dict_key + "_" + arg.attrib["name"].upper(),
450  "cpp_class_param_name": arg_name,
451  "cpp_class_comment": cpp_class_comment,
452  "cpp_class_param_enum_cast": enum_cast,
453  "cpp_class_param_type": C_TYPE_MAP[arg.attrib.get("type", "bool")],
454  "cpp_class_param_sdk_type": BEBOP_TYPE_MAP[arg.attrib.get("type", "bool")]
455  })
456 
457  # Skip cmds with no arguments
458  if len(cfg_cmd_d["cfg_arg"]):
459  cfg_class_d["cfg_cmd"].append(deepcopy(cfg_cmd_d))
460  d_cfg["cpp_class"].append({
461  "cpp_class_dict_key": cpp_class_dict_key,
462  "cpp_class_name": cpp_class_name,
463  "cpp_class_instance_name": cpp_class_instance_name,
464  "cpp_class_params": deepcopy(cpp_class_params)
465  })
466 
467  d_cfg["cfg_class"].append(deepcopy(cfg_class_d))
468 
469 
470  logging.info("... Done iterating, writing results to file")
471 
472  # .cfg write
473  cfg_file_name = d_cfg["cfg_filename"]
474  logging.info("Writing %s" % (cfg_file_name, ))
475  with open(cfg_file_name, "w") as cfg_file:
476  cfg_file.write(rend.render_path("templates/cfg.mustache", d_cfg))
477 
478  header_file_name = "%s_setting_callbacks.h" % (project.lower(), )
479  logging.info("Writing %s" % (header_file_name, ))
480  with open(header_file_name, "w") as header_file:
481  header_file.write(rend.render_path("templates/setting_callbacks.h.mustache", d_cfg))
482 
483  include_file_name = "%s_setting_callback_includes.h" % (project.lower(), )
484  logging.info("Writing %s" % (include_file_name, ))
485  with open(include_file_name, "w") as include_file:
486  include_file.write(rend.render_path("templates/setting_callback_includes.h.mustache", d_cfg))
487 
488  rst_file_name = "%s_settings_param.rst" % (project.lower(), )
489  logging.info("Writing %s" % (rst_file_name, ))
490  with open(rst_file_name, "w") as rst_file:
491  rst_file.write(rend.render_path("templates/settings_param.rst.mustache", d_cfg))
492 
493 def main():
494  # Setup stuff
495  logging.basicConfig(level="INFO")
496 
497  generate_states("common.xml")
498  generate_states("ardrone3.xml")
499  #generate_settings("common_commands.xml")
500  generate_settings("ardrone3.xml")
501 
502  generator = os.path.basename(__file__)
503  generator_git_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip()
504  with open("last_build_info", "w") as last_build_file:
505  last_build_file.write(rend.render_path(
506  "templates/last_build_info.mustache",
507  {
508  "source_hash": LIBARCOMMANDS_GIT_HASH,
509  "date": datetime.datetime.now(),
510  "generator": generator,
511  "generator_git_hash": generator_git_hash
512  }))
513 
514 if __name__ == "__main__":
515  main()
516 
def guess_min_max(arg_comment)
Definition: generate.py:113
def cap_word(text)
Definition: generate.py:110
def load_from_url(url)
Definition: generate.py:95
def generate_states(xml_filename)
Definition: generate.py:157
def today()
Definition: generate.py:154
def is_settings_tag(name)
Definition: generate.py:104
def get_xml_url(filename)
Definition: generate.py:91
def main()
Definition: generate.py:493
def strip_text(text)
Definition: generate.py:107
def generate_settings(xml_filename)
Definition: generate.py:292
def is_state_tag(name)
Definition: generate.py:101


bebop_driver
Author(s): Mani Monajjemi
autogenerated on Mon Jun 10 2019 12:58:56