parameter_generator_catkin.py
Go to the documentation of this file.
00001 # Copyright (c) 2016, Claudio Bandera
00002 # All rights reserved.
00003 #
00004 # Redistribution and use in source and binary forms, with or without
00005 # modification, are permitted provided that the following conditions are met:
00006 #     * Redistributions of source code must retain the above copyright
00007 #       notice, this list of conditions and the following disclaimer.
00008 #     * Redistributions in binary form must reproduce the above copyright
00009 #       notice, this list of conditions and the following disclaimer in the
00010 #       documentation and/or other materials provided with the distribution.
00011 #     * Neither the name of the organization nor the
00012 #       names of its contributors may be used to endorse or promote products
00013 #       derived from this software without specific prior written permission.
00014 #
00015 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
00016 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00017 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
00018 # DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
00019 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
00020 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00021 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
00022 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
00023 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
00024 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
00025 #
00026 # Author: Claudio Bandera
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 # TODO add group
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]  # Make sure we only get 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         # Check type
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             # Pytype and defaults can only be applied to primitives
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         # remove last ','
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         # remove last ','
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             # Stupid error, sometimes the directory exists anyway
00359             pass
00360         with open(cfg_file, 'w') as f:
00361             f.write(template)
00362         os.chmod(cfg_file, 509)  # entspricht 775 (octal)
00363 
00364     def _generatecpp(self):
00365         """
00366         Generate C++ Header file, holding the parameter struct.
00367         :param self:
00368         :return:
00369         """
00370 
00371         # Read in template file
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         # Create dynamic parts of the header file for every parameter
00386         for param in params:
00387             name = param['name']
00388 
00389             # Adjust key for parameter server
00390             if param["global_scope"]:
00391                 namespace = 'globalNamespace'
00392             else:
00393                 namespace = 'privateNamespace'
00394             full_name = '{} + "{}"'.format(namespace, param["name"])
00395 
00396             # Test for default value
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             # Test for constant value
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             # Test for configurable params
00424             if param['configurable']:
00425                 from_config.append(Template('    $name = config.$name;').substitute(name=name))
00426 
00427             # Test limits
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             # Add debug output
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             # Stupid error, sometimes the directory exists anyway
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             # Pray and hope that it is a string
00526             return bool(param)


rosparam_handler
Author(s): Claudio Bandera
autogenerated on Wed Aug 23 2017 02:49:31