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
00045
00046 class ParameterGenerator(object):
00047 """Automatic config file and header generator"""
00048
00049 def __init__(self, parent=None, group=""):
00050 """Constructor for ParamGenerator"""
00051 self.enums = []
00052 self.parameters = []
00053 self.childs = []
00054 self.parent = parent
00055 if group:
00056 self.group = group
00057 else:
00058 self.group = "gen"
00059 self.group_variable = filter(str.isalnum, self.group)
00060
00061 if len(sys.argv) != 4:
00062 eprint("ParameterGenerator: Unexpected amount of args, did you try to call this directly? You shouldn't do this!")
00063
00064 self.dynconfpath = sys.argv[1]
00065 self.share_dir = sys.argv[2]
00066 self.cpp_gen_dir = sys.argv[3]
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 = entry_strings[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 - Global parameters, vectors and maps can not have a default, min or max value
00114
00115 :param self:
00116 :param name: The Name of you new parameter
00117 :param paramtype: The C++ type of this parameter. Can be any of ['std::string', 'int', 'bool', 'float',
00118 'double'] or std::vector<...> or std::map<std::string, ...>
00119 :param description: Choose an informative documentation string for this parameter.
00120 :param level: (optional) Passed to dynamic_reconfigure
00121 :param edit_method: (optional) Passed to dynamic_reconfigure
00122 :param default: (optional) default value
00123 :param min: (optional)
00124 :param max: (optional)
00125 :param configurable: (optional) Should this parameter be dynamic configurable
00126 :param global_scope: (optional) If true, parameter is searched in global ('/') namespace instead of private (
00127 '~') ns
00128 :param constant: (optional) If this is true, the parameter will not be fetched from param server,
00129 but the default value is kept.
00130 :return: None
00131 """
00132 configurable = self._make_bool(configurable)
00133 global_scope = self._make_bool(global_scope)
00134 constant = self._make_bool(constant)
00135 newparam = {
00136 'name': name,
00137 'type': paramtype,
00138 'default': default,
00139 'level': level,
00140 'edit_method': edit_method,
00141 'description': description,
00142 'min': min,
00143 'max': max,
00144 'is_vector': False,
00145 'is_map': False,
00146 'configurable': configurable,
00147 'constant': constant,
00148 'global_scope': global_scope,
00149 }
00150 self._perform_checks(newparam)
00151 self.parameters.append(newparam)
00152
00153 def _perform_checks(self, param):
00154 """
00155 Will test some logical constraints as well as correct types.
00156 Throws Exception in case of error.
00157 :param self:
00158 :param param: Dictionary of one param
00159 :return:
00160 """
00161
00162 if param['type'].strip() == "std::string" and (param['max'] is not None or param['min'] is not None):
00163 eprint(param['name'], "Max or min specified for for variable of type string")
00164
00165 in_type = param['type'].strip()
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" % param['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'], "Wrong syntax used for setting up std::map<... , ...>: You provided '%s' with "
00182 "parameter %s" % in_type)
00183 ptype = ptype[1].strip()
00184 if ptype == "std::string":
00185 eprint(param['name'], "Max and min can not be specified for variable of type %s" % param['type'])
00186
00187 pattern = r'^[a-zA-Z][a-zA-Z0-9_]*$'
00188 if not re.match(pattern, param['name']):
00189 eprint(param['name'], "The name of field does not follow the ROS naming conventions, "
00190 "see http://wiki.ros.org/ROS/Patterns/Conventions")
00191 if param['configurable'] and (
00192 param['global_scope'] or param['is_vector'] or param['is_map'] or param['constant']):
00193 eprint(param['name'], "Global Parameters, vectors, maps and constant params can not be declared configurable! ")
00194 if param['global_scope'] and param['default'] is not None:
00195 eprint(param['name'], "Default values for global parameters should not be specified in node! ")
00196 if param['constant'] and param['default'] is None:
00197 eprint(param['name'], "Constant parameters need a default value!")
00198 if param['name'] in [p['name'] for p in self.parameters]:
00199 eprint(param['name'],"Parameter with the same name exists already")
00200 if param['edit_method'] == '':
00201 param['edit_method'] = '""'
00202 elif param['edit_method'] != '""':
00203 param['configurable'] = True
00204
00205
00206 if param['is_vector']:
00207 ptype = in_type[12:-1].strip()
00208 self._test_primitive_type(param['name'], ptype)
00209 param['type'] = 'std::vector<{}>'.format(ptype)
00210 elif param['is_map']:
00211 ptype = in_type[9:-1].split(',')
00212 if len(ptype) != 2:
00213 eprint(param['name'], "Wrong syntax used for setting up std::map<... , ...>: You provided '%s' with "
00214 "parameter %s" % in_type)
00215 ptype[0] = ptype[0].strip()
00216 ptype[1] = ptype[1].strip()
00217 if ptype[0] != "std::string":
00218 eprint(param['name'], "Can not setup map with %s as key type. Only std::map<std::string, "
00219 "...> are allowed" % ptype[0])
00220 self._test_primitive_type(param['name'], ptype[0])
00221 self._test_primitive_type(param['name'], ptype[1])
00222 param['type'] = 'std::map<{},{}>'.format(ptype[0], ptype[1])
00223 else:
00224
00225 self._test_primitive_type(param['name'], in_type)
00226 param['pytype'] = self._pytype(in_type)
00227
00228 @staticmethod
00229 def _pytype(drtype):
00230 """Convert C++ type to python type"""
00231 return {'std::string': "str", 'int': "int", 'double': "double", 'bool': "bool"}[drtype]
00232
00233 @staticmethod
00234 def _test_primitive_type(name, drtype):
00235 """
00236 Test whether parameter has one of the accepted C++ types
00237 :param name: Parametername
00238 :param drtype: Typestring
00239 :return:
00240 """
00241 primitive_types = ['std::string', 'int', 'bool', 'float', 'double']
00242 if drtype not in primitive_types:
00243 raise TypeError("'%s' has type %s, but allowed are: %s" % (name, drtype, primitive_types))
00244
00245 @staticmethod
00246 def _get_cvalue(param, field):
00247 """
00248 Helper function to convert strings and booleans to correct C++ syntax
00249 :param param:
00250 :return: C++ compatible representation
00251 """
00252 value = param[field]
00253 if param['type'] == 'std::string':
00254 value = '"{}"'.format(param[field])
00255 elif param['type'] == 'bool':
00256 value = str(param[field]).lower()
00257 return str(value)
00258
00259 @staticmethod
00260 def _get_pyvalue(param, field):
00261 """
00262 Helper function to convert strings and booleans to correct C++ syntax
00263 :param param:
00264 :return: C++ compatible representation
00265 """
00266 value = param[field]
00267 if param['type'] == 'std::string':
00268 value = '"{}"'.format(param[field])
00269 elif param['type'] == 'bool':
00270 value = str(param[field]).capitalize()
00271 return str(value)
00272
00273 @staticmethod
00274 def _get_cvaluelist(param, field):
00275 """
00276 Helper function to convert python list of strings and booleans to correct C++ syntax
00277 :param param:
00278 :return: C++ compatible representation
00279 """
00280 values = param[field]
00281 assert(isinstance(values, list))
00282 form = ""
00283 for value in values:
00284 if param['type'] == 'std::vector<std::string>':
00285 value = '"{}"'.format(value)
00286 elif param['type'] == 'std::vector<bool>':
00287 value = str(value).lower()
00288 else:
00289 value = str(value)
00290 form += value + ','
00291
00292 return form[:-1]
00293
00294 @staticmethod
00295 def _get_cvaluedict(param, field):
00296 """
00297 Helper function to convert python dict of strings and booleans to correct C++ syntax
00298 :param param:
00299 :return: C++ compatible representation
00300 """
00301 values = param[field]
00302 assert(isinstance(values, dict))
00303 form = ""
00304 for key, value in values.items():
00305 if param['type'] == 'std::map<std::string,std::string>':
00306 pair = '{{"{}","{}"}}'.format(key, value)
00307 elif param['type'] == 'std::map<std::string,bool>':
00308 pair = '{{"{}",{}}}'.format(key, str(value).lower())
00309 else:
00310 pair = '{{"{}",{}}}'.format(key, str(value))
00311 form += pair + ','
00312
00313 return form[:-1]
00314
00315 def generate(self, pkgname, nodename, classname):
00316 """
00317 Main working Function, call this at the end of your .params file!
00318 :param self:
00319 :param pkgname: Name of the catkin package
00320 :param nodename: Name of the Node that will hold these params
00321 :param classname: This should match your file name, so that cmake will detect changes in config file.
00322 :return: Exit Code
00323 """
00324 self.pkgname = pkgname
00325 self.nodename = nodename
00326 self.classname = classname
00327
00328 if self.parent:
00329 eprint("You should not call generate on a group! Call it on the main parameter generator instead!")
00330
00331 self._generatecfg()
00332 self._generatecpp()
00333
00334 return 0
00335
00336 def _generatecfg(self):
00337 """
00338 Generate .cfg file for dynamic reconfigure
00339 :param self:
00340 :return:
00341 """
00342 templatefile = os.path.join(self.dynconfpath, "templates", "ConfigType.h.template")
00343 with open(templatefile, 'r') as f:
00344 template = f.read()
00345
00346 param_entries = self._generate_param_entries()
00347 print(param_entries)
00348
00349 param_entries = "\n".join(param_entries)
00350 template = Template(template).substitute(pkgname=self.pkgname, nodename=self.nodename,
00351 classname=self.classname, params=param_entries)
00352
00353 cfg_file = os.path.join(self.share_dir, "cfg", self.classname + ".cfg")
00354 try:
00355 if not os.path.exists(os.path.dirname(cfg_file)):
00356 os.makedirs(os.path.dirname(cfg_file))
00357 except OSError:
00358
00359 pass
00360 with open(cfg_file, 'w') as f:
00361 f.write(template)
00362 os.chmod(cfg_file, 509)
00363
00364 def _generatecpp(self):
00365 """
00366 Generate C++ Header file, holding the parameter struct.
00367 :param self:
00368 :return:
00369 """
00370
00371
00372 templatefile = os.path.join(self.dynconfpath, "templates", "Parameters.h.template")
00373 with open(templatefile, 'r') as f:
00374 template = f.read()
00375
00376 param_entries = []
00377 string_representation = []
00378 from_server = []
00379 non_default_params = []
00380 from_config = []
00381 test_limits = []
00382
00383 params = self._get_parameters()
00384
00385
00386 for param in params:
00387 name = param['name']
00388
00389
00390 if param["global_scope"]:
00391 namespace = 'globalNamespace'
00392 else:
00393 namespace = 'privateNamespace'
00394 full_name = '{} + "{}"'.format(namespace, param["name"])
00395
00396
00397 if param["default"] is None:
00398 default = ""
00399 non_default_params.append(Template(' << "\t" << $namespace << "$name" << " ($type) '
00400 '\\n"\n').substitute(
00401 namespace=namespace, name=name, type=param["type"]))
00402 else:
00403 if param['is_vector']:
00404 default = ', {}'.format(str(param['type']) + "{" + self._get_cvaluelist(param, "default") + "}")
00405 elif param['is_map']:
00406 default = ', {}'.format(str(param['type']) + "{" + self._get_cvaluedict(param, "default") + "}")
00407 else:
00408 default = ', {}'.format(str(param['type']) + "{" + self._get_cvalue(param, "default") + "}")
00409
00410
00411 if param['constant']:
00412 param_entries.append(Template(' static constexpr auto ${name} = $default; /*!< ${description} '
00413 '*/').substitute(type=param['type'], name=name,
00414 description=param['description'],
00415 default=self._get_cvalue(param, "default")))
00416 from_server.append(Template(' testConstParam($paramname);').substitute(paramname=full_name))
00417 else:
00418 param_entries.append(Template(' ${type} ${name}; /*!< ${description} */').substitute(
00419 type=param['type'], name=name, description=param['description']))
00420 from_server.append(Template(' getParam($paramname, $name$default);').substitute(
00421 paramname=full_name, name=name, default=default, description=param['description']))
00422
00423
00424 if param['configurable']:
00425 from_config.append(Template(' $name = config.$name;').substitute(name=name))
00426
00427
00428 if param['min'] is not None:
00429 test_limits.append(Template(' testMin<$type>($paramname, $name, $min);').substitute(
00430 paramname=full_name, name=name, min=param['min'], type=param['type']))
00431 if param['max'] is not None:
00432 test_limits.append(Template(' testMax<$type>($paramname, $name, $max);').substitute(
00433 paramname=full_name, name=name, max=param['max'], type=param['type']))
00434
00435
00436 string_representation.append(Template(' << "\t" << p.$namespace << "$name:" << p.$name << '
00437 '"\\n"\n').substitute(namespace=namespace, name=name))
00438
00439 param_entries = "\n".join(param_entries)
00440 string_representation = "".join(string_representation)
00441 non_default_params = "".join(non_default_params)
00442 from_server = "\n".join(from_server)
00443 from_config = "\n".join(from_config)
00444 test_limits = "\n".join(test_limits)
00445
00446 content = Template(template).substitute(pkgname=self.pkgname, ClassName=self.classname,
00447 parameters=param_entries, fromConfig=from_config,
00448 fromParamServer=from_server, string_representation=string_representation,
00449 non_default_params=non_default_params, nodename=self.nodename,
00450 test_limits=test_limits)
00451
00452 header_file = os.path.join(self.cpp_gen_dir, self.classname + "Parameters.h")
00453 try:
00454 if not os.path.exists(os.path.dirname(header_file)):
00455 os.makedirs(os.path.dirname(header_file))
00456 except OSError:
00457
00458 pass
00459 with open(header_file, 'w') as f:
00460 f.write(content)
00461
00462 def _get_parameters(self):
00463 """
00464 Returns parameter of this and all childs
00465 :return: list of all parameters
00466 """
00467 params = self.parameters
00468 for child in self.childs:
00469 params.extend(child._get_parameters())
00470 return params
00471
00472 def _generate_param_entries(self):
00473 """
00474 Generates the entries for the cfg-file
00475 :return: list of param entries as string
00476 """
00477 param_entries = []
00478 dynamic_params = [p for p in self.parameters if p["configurable"]]
00479
00480 if self.parent:
00481 param_entries.append(Template("$group_variable = $parent.add_group('$group')").substitute(
00482 group_variable=self.group_variable,
00483 group=self.group,
00484 parent=self.parent.group_variable))
00485
00486 for enum in self.enums:
00487 param_entries.append(Template("$name = gen.enum([").substitute(
00488 name=enum['name'],
00489 parent=self.group))
00490 i = 0
00491 for value in enum['values']:
00492 param_entries.append(
00493 Template(" gen.const(name='$name', type='$type', value=$value, descr='$descr'),")
00494 .substitute(name=value, type="int", value=i, descr=""))
00495 i += 1
00496 param_entries.append(Template(" ], '$description')").substitute(description=enum["description"]))
00497
00498 for param in dynamic_params:
00499 content_line = Template("$group_variable.add(name = '$name', paramtype = '$paramtype', level = $level, "
00500 "description = '$description', edit_method=$edit_method").substitute(
00501 group_variable=self.group_variable,
00502 name=param["name"],
00503 paramtype=param['pytype'],
00504 level=param['level'],
00505 edit_method=param['edit_method'],
00506 description=param['description'])
00507 if param['default'] is not None:
00508 content_line += Template(", default=$default").substitute(default=self._get_pyvalue(param, "default"))
00509 if param['min'] is not None:
00510 content_line += Template(", min=$min").substitute(min=param['min'])
00511 if param['max'] is not None:
00512 content_line += Template(", max=$max").substitute(max=param['max'])
00513 content_line += ")"
00514 param_entries.append(content_line)
00515
00516 for child in self.childs:
00517 param_entries.extend(child._generate_param_entries())
00518 return param_entries
00519
00520 @staticmethod
00521 def _make_bool(param):
00522 if isinstance(param, bool):
00523 return param
00524 else:
00525
00526 return bool(param)