00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029 from __future__ import print_function
00030 from string import Template
00031 import sys
00032 import os
00033 import re
00034
00035
00036 def eprint(*args, **kwargs):
00037 print("************************************************", file=sys.stderr, **kwargs)
00038 print("Error when setting up parameter '{}':".format(args[0]), file=sys.stderr, **kwargs)
00039 print(*args[1:], file=sys.stderr, **kwargs)
00040 print("************************************************", file=sys.stderr, **kwargs)
00041 sys.exit(1)
00042
00043
00044 class ParameterGenerator(object):
00045 """Automatic config file and header generator"""
00046
00047 def __init__(self, parent=None, group=""):
00048 """Constructor for ParamGenerator"""
00049 self.enums = []
00050 self.parameters = []
00051 self.childs = []
00052 self.parent = parent
00053 if group:
00054 self.group = group
00055 else:
00056 self.group = "gen"
00057 self.group_variable = filter(str.isalnum, self.group)
00058
00059 if len(sys.argv) != 5:
00060 eprint(
00061 "ParameterGenerator: Unexpected amount of args, did you try to call this directly? You shouldn't do this!")
00062
00063 self.dynconfpath = sys.argv[1]
00064 self.share_dir = sys.argv[2]
00065 self.cpp_gen_dir = sys.argv[3]
00066 self.py_gen_dir = sys.argv[4]
00067
00068 self.pkgname = None
00069 self.nodename = None
00070 self.classname = None
00071
00072 def add_group(self, name):
00073 """
00074 Add a new group in the dynamic reconfigure selection
00075 :param name: name of the new group
00076 :return: a new group object that you can add parameters to
00077 """
00078 if not name:
00079 eprint("You have added a group with an empty group name. This is not supported!")
00080 child = ParameterGenerator(self, name)
00081 self.childs.append(child)
00082 return child
00083
00084 def add_enum(self, name, description, entry_strings, default=None):
00085 """
00086 Add an enum to dynamic reconfigure
00087 :param name: Name of enum parameter
00088 :param description: Informative documentation string
00089 :param entry_strings: Enum entries, must be strings! (will be numbered with increasing value)
00090 :param default: Default value
00091 :return:
00092 """
00093
00094 entry_strings = [str(e) for e in entry_strings]
00095 if default is None:
00096 default = 0
00097 else:
00098 default = entry_strings.index(default)
00099 self.add(name=name, paramtype="int", description=description, edit_method=name, default=default,
00100 configurable=True)
00101 for e in entry_strings:
00102 self.add(name=name + "_" + e, paramtype="int", description="Constant for enum {}".format(name),
00103 default=entry_strings.index(e), constant=True)
00104 self.enums.append({'name': name, 'description': description, 'values': entry_strings})
00105
00106 def add(self, name, paramtype, description, level=0, edit_method='""', default=None, min=None, max=None,
00107 configurable=False, global_scope=False, constant=False):
00108 """
00109 Add parameters to your parameter struct. Call this method from your .params file!
00110
00111 - If no default value is given, you need to specify one in your launch file
00112 - Global parameters, vectors, maps and constant params can not be configurable
00113
00114 :param self:
00115 :param name: The Name of you new parameter
00116 :param paramtype: The C++ type of this parameter. Can be any of ['std::string', 'int', 'bool', 'float',
00117 'double'] or std::vector<...> or std::map<std::string, ...>
00118 :param description: Choose an informative documentation string for this parameter.
00119 :param level: (optional) Passed to dynamic_reconfigure
00120 :param edit_method: (optional) Passed to dynamic_reconfigure
00121 :param default: (optional) default value
00122 :param min: (optional)
00123 :param max: (optional)
00124 :param configurable: (optional) Should this parameter be dynamic configurable
00125 :param global_scope: (optional) If true, parameter is searched in global ('/') namespace instead of private (
00126 '~') ns
00127 :param constant: (optional) If this is true, the parameter will not be fetched from param server,
00128 but the default value is kept.
00129 :return: None
00130 """
00131 configurable = self._make_bool(configurable)
00132 global_scope = self._make_bool(global_scope)
00133 constant = self._make_bool(constant)
00134 newparam = {
00135 'name': name,
00136 'type': paramtype,
00137 'default': default,
00138 'level': level,
00139 'edit_method': edit_method,
00140 'description': description,
00141 'min': min,
00142 'max': max,
00143 'is_vector': False,
00144 'is_map': False,
00145 'configurable': configurable,
00146 'constant': constant,
00147 'global_scope': global_scope,
00148 }
00149 self._perform_checks(newparam)
00150 self.parameters.append(newparam)
00151
00152 def _perform_checks(self, param):
00153 """
00154 Will test some logical constraints as well as correct types.
00155 Throws Exception in case of error.
00156 :param self:
00157 :param param: Dictionary of one param
00158 :return:
00159 """
00160
00161 in_type = param['type'].strip()
00162 if param['max'] is not None or param['min'] is not None:
00163 if in_type in ["std::string", "bool"]:
00164 eprint(param['name'], "Max and min can not be specified for variable of type %s" % in_type)
00165
00166 if in_type.startswith('std::vector'):
00167 param['is_vector'] = True
00168 if in_type.startswith('std::map'):
00169 param['is_map'] = True
00170
00171 if (param['is_vector']):
00172 if (param['max'] is not None or param['min'] is not None):
00173 ptype = in_type[12:-1].strip()
00174 if ptype == "std::string":
00175 eprint(param['name'], "Max and min can not be specified for variable of type %s" % in_type)
00176
00177 if (param['is_map']):
00178 if (param['max'] is not None or param['min'] is not None):
00179 ptype = in_type[9:-1].split(',')
00180 if len(ptype) != 2:
00181 eprint(param['name'],
00182 "Wrong syntax used for setting up std::map<... , ...>: You provided '%s' with "
00183 "parameter %s" % in_type)
00184 ptype = ptype[1].strip()
00185 if ptype == "std::string":
00186 eprint(param['name'], "Max and min can not be specified for variable of type %s" % in_type)
00187
00188 pattern = r'^[a-zA-Z][a-zA-Z0-9_]*$'
00189 if not re.match(pattern, param['name']):
00190 eprint(param['name'], "The name of field does not follow the ROS naming conventions, "
00191 "see http://wiki.ros.org/ROS/Patterns/Conventions")
00192 if param['configurable'] and (
00193 param['global_scope'] or param['is_vector'] or param['is_map'] or param['constant']):
00194 eprint(param['name'],
00195 "Global Parameters, vectors, maps and constant params can not be declared configurable! ")
00196 if param['global_scope'] and param['default'] is not None:
00197 eprint(param['name'], "Default values for global parameters should not be specified in node! ")
00198 if param['constant'] and param['default'] is None:
00199 eprint(param['name'], "Constant parameters need a default value!")
00200 if param['name'] in [p['name'] for p in self.parameters]:
00201 eprint(param['name'], "Parameter with the same name exists already")
00202 if param['edit_method'] == '':
00203 param['edit_method'] = '""'
00204 elif param['edit_method'] != '""':
00205 param['configurable'] = True
00206
00207
00208 if param['is_vector']:
00209 ptype = in_type[12:-1].strip()
00210 self._test_primitive_type(param['name'], ptype)
00211 param['type'] = 'std::vector<{}>'.format(ptype)
00212 elif param['is_map']:
00213 ptype = in_type[9:-1].split(',')
00214 if len(ptype) != 2:
00215 eprint(param['name'], "Wrong syntax used for setting up std::map<... , ...>: You provided '%s' with "
00216 "parameter %s" % in_type)
00217 ptype[0] = ptype[0].strip()
00218 ptype[1] = ptype[1].strip()
00219 if ptype[0] != "std::string":
00220 eprint(param['name'], "Can not setup map with %s as key type. Only std::map<std::string, "
00221 "...> are allowed" % ptype[0])
00222 self._test_primitive_type(param['name'], ptype[0])
00223 self._test_primitive_type(param['name'], ptype[1])
00224 param['type'] = 'std::map<{},{}>'.format(ptype[0], ptype[1])
00225 else:
00226
00227 self._test_primitive_type(param['name'], in_type)
00228 param['pytype'] = self._pytype(in_type)
00229
00230 @staticmethod
00231 def _pytype(drtype):
00232 """Convert C++ type to python type"""
00233 return {'std::string': "str", 'int': "int", 'double': "double", 'bool': "bool"}[drtype]
00234
00235 @staticmethod
00236 def _test_primitive_type(name, drtype):
00237 """
00238 Test whether parameter has one of the accepted C++ types
00239 :param name: Parametername
00240 :param drtype: Typestring
00241 :return:
00242 """
00243 primitive_types = ['std::string', 'int', 'bool', 'float', 'double']
00244 if drtype not in primitive_types:
00245 raise TypeError("'%s' has type %s, but allowed are: %s" % (name, drtype, primitive_types))
00246
00247 @staticmethod
00248 def _get_cvalue(param, field):
00249 """
00250 Helper function to convert strings and booleans to correct C++ syntax
00251 :param param:
00252 :return: C++ compatible representation
00253 """
00254 value = param[field]
00255 if param['type'] == 'std::string':
00256 value = '"{}"'.format(param[field])
00257 elif param['type'] == 'bool':
00258 value = str(param[field]).lower()
00259 return str(value)
00260
00261 @staticmethod
00262 def _get_pyvalue(param, field):
00263 """
00264 Helper function to convert strings and booleans to correct C++ syntax
00265 :param param:
00266 :return: C++ compatible representation
00267 """
00268 value = param[field]
00269 if param['type'] == 'std::string':
00270 value = '"{}"'.format(param[field])
00271 elif param['type'] == 'bool':
00272 value = str(param[field]).capitalize()
00273 return str(value)
00274
00275 @staticmethod
00276 def _get_cvaluelist(param, field):
00277 """
00278 Helper function to convert python list of strings and booleans to correct C++ syntax
00279 :param param:
00280 :return: C++ compatible representation
00281 """
00282 values = param[field]
00283 assert (isinstance(values, list))
00284 form = ""
00285 for value in values:
00286 if param['type'] == 'std::vector<std::string>':
00287 value = '"{}"'.format(value)
00288 elif param['type'] == 'std::vector<bool>':
00289 value = str(value).lower()
00290 else:
00291 value = str(value)
00292 form += value + ','
00293
00294 return form[:-1]
00295
00296 @staticmethod
00297 def _get_cvaluedict(param, field):
00298 """
00299 Helper function to convert python dict of strings and booleans to correct C++ syntax
00300 :param param:
00301 :return: C++ compatible representation
00302 """
00303 values = param[field]
00304 assert (isinstance(values, dict))
00305 form = ""
00306 for key, value in values.items():
00307 if param['type'] == 'std::map<std::string,std::string>':
00308 pair = '{{"{}","{}"}}'.format(key, value)
00309 elif param['type'] == 'std::map<std::string,bool>':
00310 pair = '{{"{}",{}}}'.format(key, str(value).lower())
00311 else:
00312 pair = '{{"{}",{}}}'.format(key, str(value))
00313 form += pair + ','
00314
00315 return form[:-1]
00316
00317 def generate(self, pkgname, nodename, classname):
00318 """
00319 Main working Function, call this at the end of your .params file!
00320 :param self:
00321 :param pkgname: Name of the catkin package
00322 :param nodename: Name of the Node that will hold these params
00323 :param classname: This should match your file name, so that cmake will detect changes in config file.
00324 :return: Exit Code
00325 """
00326 self.pkgname = pkgname
00327 self.nodename = nodename
00328 self.classname = classname
00329
00330 if self.parent:
00331 eprint("You should not call generate on a group! Call it on the main parameter generator instead!")
00332
00333 return self._generateImpl()
00334
00335 def _generateImpl(self):
00336 """
00337 Implementation level function. Can be overwritten by derived classes.
00338 :return:
00339 """
00340 self._generatecfg()
00341 self._generatehpp()
00342 self._generatepy()
00343
00344 return 0
00345
00346 def _generatecfg(self):
00347 """
00348 Generate .cfg file for dynamic reconfigure
00349 :param self:
00350 :return:
00351 """
00352 templatefile = os.path.join(self.dynconfpath, "templates", "ConfigType.h.template")
00353 with open(templatefile, 'r') as f:
00354 template = f.read()
00355
00356 param_entries = self._generate_param_entries()
00357
00358 param_entries = "\n".join(param_entries)
00359 template = Template(template).substitute(pkgname=self.pkgname, nodename=self.nodename,
00360 classname=self.classname, params=param_entries)
00361
00362 cfg_file = os.path.join(self.share_dir, "cfg", self.classname + ".cfg")
00363 try:
00364 if not os.path.exists(os.path.dirname(cfg_file)):
00365 os.makedirs(os.path.dirname(cfg_file))
00366 except OSError:
00367
00368 pass
00369 with open(cfg_file, 'w') as f:
00370 f.write(template)
00371 os.chmod(cfg_file, 509)
00372
00373 def _generatehpp(self):
00374 """
00375 Generate C++ Header file, holding the parameter struct.
00376 :param self:
00377 :return:
00378 """
00379
00380
00381 templatefile = os.path.join(self.dynconfpath, "templates", "Parameters.h.template")
00382 with open(templatefile, 'r') as f:
00383 template = f.read()
00384
00385 param_entries = []
00386 string_representation = []
00387 from_server = []
00388 to_server = []
00389 non_default_params = []
00390 from_config = []
00391 to_config = []
00392 test_limits = []
00393
00394 params = self._get_parameters()
00395
00396
00397 for param in params:
00398 name = param['name']
00399
00400
00401 if param["global_scope"]:
00402 namespace = 'globalNamespace'
00403 else:
00404 namespace = 'privateNamespace'
00405 full_name = '{} + "{}"'.format(namespace, param["name"])
00406
00407
00408 if param["default"] is None:
00409 default = ""
00410 non_default_params.append(Template(' << "\t" << $namespace << "$name" << " ($type) '
00411 '\\n"\n').substitute(
00412 namespace=namespace, name=name, type=param["type"]))
00413 else:
00414 if param['is_vector']:
00415 default = ', {}'.format(str(param['type']) + "{" + self._get_cvaluelist(param, "default") + "}")
00416 elif param['is_map']:
00417 default = ', {}'.format(str(param['type']) + "{" + self._get_cvaluedict(param, "default") + "}")
00418 else:
00419 default = ', {}'.format(str(param['type']) + "{" + self._get_cvalue(param, "default") + "}")
00420
00421
00422 if param['constant']:
00423 param_entries.append(Template(' static constexpr auto ${name} = $default; /*!< ${description} '
00424 '*/').substitute(type=param['type'], name=name,
00425 description=param['description'],
00426 default=self._get_cvalue(param, "default")))
00427 from_server.append(Template(' rosparam_handler::testConstParam($paramname);').substitute(paramname=full_name))
00428 else:
00429 param_entries.append(Template(' ${type} ${name}; /*!< ${description} */').substitute(
00430 type=param['type'], name=name, description=param['description']))
00431 from_server.append(Template(' success &= rosparam_handler::getParam($paramname, $name$default);').substitute(
00432 paramname=full_name, name=name, default=default, description=param['description']))
00433 to_server.append(
00434 Template(' rosparam_handler::setParam(${paramname},${name});').substitute(paramname=full_name, name=name))
00435
00436
00437 if param['configurable']:
00438 from_config.append(Template(' $name = config.$name;').substitute(name=name))
00439 to_config.append(Template(' config.$name = $name;').substitute(name=name))
00440
00441
00442 if param['is_vector']:
00443 ttype = param['type'][12:-1].strip()
00444 elif param['is_map']:
00445 ttype = param['type'][9:-1].strip()
00446 else:
00447 ttype = param['type']
00448 if param['min'] is not None:
00449 test_limits.append(Template(' rosparam_handler::testMin<$type>($paramname, $name, $min);').substitute(
00450 paramname=full_name, name=name, min=param['min'], type=ttype))
00451 if param['max'] is not None:
00452 test_limits.append(Template(' rosparam_handler::testMax<$type>($paramname, $name, $max);').substitute(
00453 paramname=full_name, name=name, max=param['max'], type=ttype))
00454
00455
00456 if param['is_vector'] or param['is_map']:
00457 string_representation.append(Template(' << "\t" << p.$namespace << "$name:" << rosparam_handler::to_string(p.$name) << '
00458 '"\\n"\n').substitute(namespace=namespace, name=name))
00459 else:
00460 string_representation.append(Template(' << "\t" << p.$namespace << "$name:" << p.$name << '
00461 '"\\n"\n').substitute(namespace=namespace, name=name))
00462
00463 param_entries = "\n".join(param_entries)
00464 string_representation = "".join(string_representation)
00465 non_default_params = "".join(non_default_params)
00466 from_server = "\n".join(from_server)
00467 to_server = "\n".join(to_server)
00468 from_config = "\n".join(from_config)
00469 to_config = "\n".join(to_config)
00470 test_limits = "\n".join(test_limits)
00471
00472 content = Template(template).substitute(pkgname=self.pkgname, ClassName=self.classname,
00473 parameters=param_entries, fromConfig=from_config,
00474 fromParamServer=from_server,
00475 string_representation=string_representation,
00476 non_default_params=non_default_params, nodename=self.nodename,
00477 test_limits=test_limits, toParamServer=to_server,
00478 toConfig=to_config)
00479
00480 header_file = os.path.join(self.cpp_gen_dir, self.classname + "Parameters.h")
00481 try:
00482 if not os.path.exists(os.path.dirname(header_file)):
00483 os.makedirs(os.path.dirname(header_file))
00484 except OSError:
00485
00486 pass
00487 with open(header_file, 'w') as f:
00488 f.write(content)
00489
00490 def _generatepy(self):
00491 """
00492 Generate Python parameter file
00493 :param self:
00494 :return:
00495 """
00496 params = self._get_parameters()
00497 paramDescription = str(params)
00498
00499
00500 templatefile = os.path.join(self.dynconfpath, "templates", "Parameters.py.template")
00501 with open(templatefile, 'r') as f:
00502 template = f.read()
00503
00504 content = Template(template).substitute(pkgname=self.pkgname, ClassName=self.classname,
00505 paramDescription=paramDescription)
00506
00507 py_file = os.path.join(self.py_gen_dir, "param", self.classname + "Parameters.py")
00508 try:
00509 if not os.path.exists(os.path.dirname(py_file)):
00510 os.makedirs(os.path.dirname(py_file))
00511 except OSError:
00512
00513 pass
00514 with open(py_file, 'w') as f:
00515 f.write(content)
00516 init_file = os.path.join(self.py_gen_dir, "param", "__init__.py")
00517 with open(init_file, 'wa') as f:
00518 f.write("")
00519
00520 def _generateyml(self):
00521 """
00522 Generate .yaml file for roslaunch
00523 :param self:
00524 :return:
00525 """
00526 params = self._get_parameters()
00527
00528 content = "### This file was generated using the rosparam_handler generate_yaml script.\n"
00529
00530 for entry in params:
00531 if not entry["constant"]:
00532 content += "\n"
00533 content += "# Name:\t" + str(entry["name"]) + "\n"
00534 content += "# Desc:\t" + str(entry["description"]) + "\n"
00535 content += "# Type:\t" + str(entry["type"]) + "\n"
00536 if entry['min'] or entry['max']:
00537 content += "# [min,max]:\t[" + str(entry["min"]) + "/" + str(entry["max"]) + "]" + "\n"
00538 if entry["global_scope"]:
00539 content += "# Lives in global namespace!\n"
00540 if entry["default"] is not None:
00541 content += str(entry["name"]) + ": " + str(entry["default"]) + "\n"
00542 else:
00543 content += str(entry["name"]) + ": \n"
00544
00545 yaml_file = os.path.join(os.getcwd(), self.classname + "Parameters.yaml")
00546
00547 with open(yaml_file, 'w') as f:
00548 f.write(content)
00549
00550 def _get_parameters(self):
00551 """
00552 Returns parameter of this and all childs
00553 :return: list of all parameters
00554 """
00555 params = self.parameters
00556 for child in self.childs:
00557 params.extend(child._get_parameters())
00558 return params
00559
00560 def _generate_param_entries(self):
00561 """
00562 Generates the entries for the cfg-file
00563 :return: list of param entries as string
00564 """
00565 param_entries = []
00566 dynamic_params = [p for p in self.parameters if p["configurable"]]
00567
00568 if self.parent:
00569 param_entries.append(Template("$group_variable = $parent.add_group('$group')").substitute(
00570 group_variable=self.group_variable,
00571 group=self.group,
00572 parent=self.parent.group_variable))
00573
00574 for enum in self.enums:
00575 param_entries.append(Template("$name = gen.enum([").substitute(
00576 name=enum['name'],
00577 parent=self.group))
00578 i = 0
00579 for value in enum['values']:
00580 param_entries.append(
00581 Template(" gen.const(name='$name', type='$type', value=$value, descr='$descr'),")
00582 .substitute(name=value, type="int", value=i, descr=""))
00583 i += 1
00584 param_entries.append(Template(" ], '$description')").substitute(description=enum["description"]))
00585
00586 for param in dynamic_params:
00587 content_line = Template("$group_variable.add(name = '$name', paramtype = '$paramtype', level = $level, "
00588 "description = '$description', edit_method=$edit_method").substitute(
00589 group_variable=self.group_variable,
00590 name=param["name"],
00591 paramtype=param['pytype'],
00592 level=param['level'],
00593 edit_method=param['edit_method'],
00594 description=param['description'])
00595 if param['default'] is not None:
00596 content_line += Template(", default=$default").substitute(default=self._get_pyvalue(param, "default"))
00597 if param['min'] is not None:
00598 content_line += Template(", min=$min").substitute(min=param['min'])
00599 if param['max'] is not None:
00600 content_line += Template(", max=$max").substitute(max=param['max'])
00601 content_line += ")"
00602 param_entries.append(content_line)
00603
00604 for child in self.childs:
00605 param_entries.extend(child._generate_param_entries())
00606 return param_entries
00607
00608 @staticmethod
00609 def _make_bool(param):
00610 if isinstance(param, bool):
00611 return param
00612 else:
00613
00614 return bool(param)
00615
00616
00617
00618 class YamlGenerator(ParameterGenerator):
00619 def _generateImpl(self):
00620 self._generateyml()
00621 return 0