diff --git a/Tools/module_config/generate_actuators_metadata.py b/Tools/module_config/generate_actuators_metadata.py new file mode 100755 index 0000000000..b10a40bde9 --- /dev/null +++ b/Tools/module_config/generate_actuators_metadata.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +""" Script to generate actuators.json metadata from module.yaml config file(s) +""" + +import argparse +import lzma #to create .xz file +import json +import os +import sys + +from output_groups_from_timer_config import get_timer_groups, get_output_groups + +try: + import yaml +except ImportError as e: + print("Failed to import yaml: " + str(e)) + print("") + print("You may need to install it using:") + print(" pip3 install --user pyyaml") + print("") + sys.exit(1) + +parser = argparse.ArgumentParser(description='Generate actuators.json from module.yaml file(s)') + +parser.add_argument('--config-files', type=str, nargs='*', default=[], + help='YAML module config file(s)') +parser.add_argument('--output-file', type=str, action='store', + help='JSON output file', required=True) +parser.add_argument('--compress', action='store_true', help='Add a compressed output file') +parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', + help='Verbose Output') +parser.add_argument('--timer-config', type=str, action='store', + help='board-specific timer_config.cpp file') +parser.add_argument('--board', type=str, action='store', + help='board name, e.g. ') +parser.add_argument('--board-with-io', dest='board_with_io', action='store_true', + help='Indicate that the board as an IO for extra PWM', + default=False) + +args = parser.parse_args() + +compress = args.compress +verbose = args.verbose +output_file = args.output_file +timer_config_file = args.timer_config +board_with_io = args.board_with_io +board = args.board + +root_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..") +output_functions_file = os.path.join(root_dir,"src/lib/mixer_module/output_functions.yaml") + +def save_compressed(filename): + #create lzma compressed version + xz_filename=filename+'.xz' + with lzma.open(xz_filename, 'wt', preset=9) as f: + with open(filename, 'r') as content_file: + f.write(content_file.read()) + +def load_yaml_file(file_name): + with open(file_name, 'r') as stream: + try: + return yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + raise + +# functions +output_functions_yaml = load_yaml_file(output_functions_file) +output_functions = output_functions_yaml['functions'] +functions = {} + +def add_function(functions, index, name, function_obj=None): + functions[index] = { + "label": name + } + if function_obj is not None: + if function_obj.get('exclude_from_actuator_testing', False): + functions[index]['exclude-from-actuator-testing'] = True + if 'note' in function_obj: + functions[index]['note'] = function_obj['note'] + +for group_key in output_functions: + group = output_functions[group_key] + for function_name in group: + function_name_label = function_name.replace('_', ' ') + if isinstance(group[function_name], int): + add_function(functions, group[function_name], function_name_label) + elif not 'count' in group[function_name]: + add_function(functions, group[function_name]['start'], function_name_label, group[function_name]) + else: + start = group[function_name]['start'] + count = group[function_name]['count'] + for i in range(count): + add_function(functions, start+i, function_name_label+' '+str(i+1), group[function_name]) + +# outputs +outputs = [] + +def process_module_name(module_name): + if module_name == '${PWM_MAIN_OR_AUX}': + if board_with_io: return 'PWM AUX' + return 'PWM MAIN' + if '${' in module_name: + raise Exception('unhandled variable in {:}'.format(module_name)) + return module_name + +def process_param_prefix(param_prefix): + if param_prefix == '${PWM_MAIN_OR_HIL}': + if board == 'px4_sitl': return 'PWM_MAIN' + return 'HIL_ACT' + if param_prefix == '${PWM_MAIN_OR_AUX}': + if board_with_io: return 'PWM_AUX' + return 'PWM_MAIN' + if '${' in param_prefix: + raise Exception('unhandled variable in {:}'.format(param_prefix)) + return param_prefix + +def process_channel_label(module_name, channel_label, no_prefix): + if channel_label == '${PWM_MAIN_OR_HIL}': + return 'Channel' + if channel_label == '${PWM_MAIN_OR_AUX_CAP}': + return 'CAP' + if channel_label == '${PWM_MAIN_OR_AUX}': + if board_with_io: return 'AUX' + return 'MAIN' + if '${' in channel_label: + raise Exception('unhandled variable in {:}'.format(channel_label)) + if no_prefix: return channel_label + return channel_label + +def get_actuator_output(yaml_config, output_functions, timer_config_file, verbose): + """ parse the actuator_output section from the yaml config file + """ + if not 'actuator_output' in yaml_config: + return None + + + output_groups = yaml_config['actuator_output']['output_groups'] + module_name = process_module_name(yaml_config['module_name']) + group_idx = 0 + + if verbose: print('processing module: {}'.format(module_name)) + + actuator_output = { + 'label': module_name + } + if 'show_subgroups_if' in yaml_config['actuator_output']: + actuator_output['show-subgroups-if'] = yaml_config['actuator_output']['show_subgroups_if'] + + # config parameters + def get_config_params(param_list): + """ convert config parameter list (per group or per subgroup) """ + parameters = [] + for config_param in param_list: + if verbose: + print('config param: {}'.format(config_param)) + param = { + 'name': config_param['param'], + } + if 'label' in config_param: + param['label'] = config_param['label'] + if 'function' in config_param: + param['function'] = config_param['function'] + parameters.append(param) + return parameters + + parameters = get_config_params(yaml_config['actuator_output'].get('config_parameters', [])) + if len(parameters) > 0: + actuator_output['parameters'] = parameters + + subgroups = [] + + while group_idx < len(output_groups): + group = output_groups[group_idx] + group_idx += 1 + + if verbose: print("processing group: {:}".format(group)) + + # Check for generator and generate additional data. + if 'generator' in group: + if group['generator'] == 'pwm': + param_prefix = process_param_prefix(group['param_prefix']) + no_prefix = not group.get('channel_label_module_name_prefix', True) + channel_labels = [process_channel_label(module_name, label, no_prefix) + for label in group['channel_labels']] + standard_params = group.get('standard_params', []) + extra_function_groups = group.get('extra_function_groups', []) + pwm_timer_param = group.get('pwm_timer_param', None) + if 'timer_config_file' in group: + timer_config_file = os.path.join(root_dir, group['timer_config_file']) + if timer_config_file is None: + raise Exception('trying to generate pwm outputs, but --timer-config not set') + timer_groups = get_timer_groups(timer_config_file, verbose) + timer_output_groups, timer_params = get_output_groups(timer_groups, + param_prefix, channel_labels, + standard_params, extra_function_groups, pwm_timer_param, + verbose=verbose) + output_groups.extend(timer_output_groups) + else: + raise Exception('unknown generator {:}'.format(group['generator'])) + continue + + subgroup = {} + + # supported actions + if 'supported_actions' in group: + actions = {} + for action_name in group['supported_actions']: + action = group['supported_actions'][action_name] + action_name = action_name.replace('_', '-') + actions[action_name] = {} + if 'supported_if' in action: + actions[action_name]['supported-if'] = action['supported_if'] + if 'actuator_types' in action: + actions[action_name]['actuator-types'] = action['actuator_types'] + subgroup['supported-actions'] = actions + + # channels + num_channels = group['num_channels'] + no_prefix = not group.get('channel_label_module_name_prefix', True) + channel_label = process_channel_label(module_name, group['channel_label'], no_prefix) + instance_start = group.get('instance_start', 1) + instance_start_label = group.get('instance_start_label', instance_start) + channels = [] + for channel in range(num_channels): + channels.append({ + 'label': channel_label + ' ' +str(channel+instance_start_label), + 'param-index': channel+instance_start + }) + subgroup['channels'] = channels + + if 'group_label' in group: + subgroup['label'] = group['group_label'] + + + # per-channel-params + per_channel_params = [] + param_prefix = process_param_prefix(group['param_prefix']) + standard_params = group.get('standard_params', {}) + standard_params_array = [ + ( 'function', 'Function', 'FUNC', False ), + ( 'disarmed', 'Disarmed', 'DIS', False ), + ( 'min', 'Minimum', 'MIN', False ), + ( 'max', 'Maximum', 'MAX', False ), + ( 'failsafe', 'Failsafe', 'FAIL', True ), + ] + for key, label, param_suffix, advanced in standard_params_array: + show_if = None + if key in standard_params and 'show_if' in standard_params[key]: + show_if = standard_params[key]['show_if'] + + if key in standard_params or key == 'function': + param = { + 'label': label, + 'name': param_prefix+'_'+param_suffix+'${i}', + 'function': key, + } + if advanced: param['advanced'] = True + if show_if: param['show-if'] = show_if + per_channel_params.append(param) + + # TODO: support non-standard per-channel parameters + + subgroup['per-channel-parameters'] = per_channel_params + + # group config params + parameters = get_config_params(group.get('config_parameters', [])) + if len(parameters) > 0: + subgroup['parameters'] = parameters + + subgroups.append(subgroup) + + actuator_output['subgroups'] = subgroups + return actuator_output + +# Mixers +mixers = None +def get_mixers(yaml_config, output_functions, verbose): + if not 'mixer' in yaml_config: + return None + + actuator_types = {} + for actuator_type_key in yaml_config['mixer']['actuator_types']: + actuator_type_conf = yaml_config['mixer']['actuator_types'][actuator_type_key] + actuator_type = { } + if actuator_type_key != 'DEFAULT': + actuator_type['label-index-offset'] = 1 # always 1 + if 'functions' in actuator_type_conf: + function_name = actuator_type_conf['functions'] + # we expect the function to be in 'common' (this is not a requirement, just simplicity) + output_function = output_functions['common'][function_name] + actuator_type['function-min'] = output_function['start'] + actuator_type['function-max'] = output_function['start'] + output_function['count'] - 1 + + values = actuator_type_conf['actuator_testing_values'] + actuator_type['values'] = { + 'min': values['min'], + 'max': values['max'], + } + if values.get('default_is_nan', False): + actuator_type['values']['default-is-nan'] = True + else: + actuator_type['values']['default'] = values['default'] + if values.get('reversible', False): + actuator_type['values']['reversible'] = True + + # per item params + per_item_params = [] + for per_item_param in actuator_type_conf.get('per_item_parameters', []): + per_item_params.append({k.replace('_','-'): v for k, v in per_item_param.items()}) + if len(per_item_params) > 0: + actuator_type['per-item-parameters'] = per_item_params + + actuator_types[actuator_type_key] = actuator_type + + if verbose: + print('Actuator types: {}'.format(actuator_types)) + + config = [] + yaml_mixer_config = yaml_config['mixer']['config'] + select_param = yaml_mixer_config['param'] + types = yaml_mixer_config['types'] + for type_index in types: + current_type = types[type_index] + option = select_param + '==' + str(type_index) + mixer_config = { + 'option': option, + } + if 'type' in current_type: + mixer_config['type'] = current_type['type'] + actuators = [] + for actuator_conf in current_type['actuators']: + actuator = { + 'actuator-type': actuator_conf['actuator_type'], + 'required': True, # for now always set as required + } + # sanity check that actuator type exists + if actuator_conf['actuator_type'] not in actuator_types: + raise Exception('actuator type "{}" does not exist (valid: {})'.format(actuator_conf['actuator_type'], actuator_types.keys())) + + if 'group_label' in actuator_conf: + actuator['group-label'] = actuator_conf['group_label'] + else: + # infer from actuator type + if actuator_conf['actuator_type'] == 'motor': + actuator['group-label'] = 'Motors' + elif actuator_conf['actuator_type'] == 'servo': + actuator['group-label'] = 'Servos' + else: + raise Exception('Missing group label for actuator type "{}"'.format(actuator_conf['actuator_type'])) + + if 'count' in actuator_conf: # possibly dynamic size + actuator['count'] = actuator_conf['count'] + per_item_params = actuator_conf['per_item_parameters'] + params = [] + if 'standard' in per_item_params: + standard_params = per_item_params['standard'] + if 'position' in standard_params: + params.extend([ + { + 'label': 'Position X', + 'function': 'posx', + 'name': standard_params['position'][0], + }, + { + 'label': 'Position Y', + 'function': 'posy', + 'name': standard_params['position'][1], + }, + { + 'label': 'Position Z', + 'function': 'posz', + 'name': standard_params['position'][2], + 'advanced': True, + }, + ]) + if 'extra' in per_item_params: + for extra_param in per_item_params['extra']: + params.append({k.replace('_','-'): v for k, v in extra_param.items()}) + actuator['per-item-parameters'] = params + else: # fixed size + labels = [] + pos_x = [] + pos_y = [] + pos_z = [] + for instance in actuator_conf['instances']: + labels.append(instance['name']) + pos_x.append(instance['position'][0]) + pos_y.append(instance['position'][1]) + pos_z.append(instance['position'][2]) + actuator['count'] = len(labels) + actuator['item-label-prefix'] = labels + actuator['per-item-parameters'] = [ + { + 'label': 'Position X', + 'function': 'posx', + 'value': pos_x, + }, + { + 'label': 'Position Y', + 'function': 'posy', + 'value': pos_y, + }, + { + 'label': 'Position Z', + 'function': 'posz', + 'value': pos_z, + 'advanced': True, + }, + ] + + # actuator parameters + parameters = [] + for param in actuator_conf.get('parameters', []): + parameters.append({k.replace('_','-'): v for k, v in param.items()}) + actuator['parameters'] = parameters + + actuators.append(actuator) + + mixer_config['actuators'] = actuators + config.append(mixer_config) + + if verbose: + print('Mixer configs: {}'.format(config)) + + mixers = { + 'actuator-types': actuator_types, + 'config': config, + } + return mixers + + +for yaml_file in args.config_files: + yaml_config = load_yaml_file(yaml_file) + + try: + actuator_output = get_actuator_output(yaml_config, + output_functions, timer_config_file, verbose) + if actuator_output: + outputs.append(actuator_output) + + parsed_mixers = get_mixers(yaml_config, output_functions, verbose) + if parsed_mixers is not None: + if mixers is not None: + # only expected to be configured in one module + raise Exception('multiple "mixer" sections in module config files') + mixers = parsed_mixers + except Exception as e: + print('Exception while parsing {:}:'.format(yaml_file)) + raise e + +if mixers is None: + if len(outputs) > 0: + raise Exception('Missing "mixer" section in yaml configs (CONFIG_MODULES_CONTROL_ALLOCATOR not added to the build?)') + else: + # set a minimal default + mixers = { + 'actuator-types': { 'DEFAULT': { 'values': { 'min': 0, 'max': 1 } } }, + 'config': [], + } + +actuators = { + 'version': 1, + 'show-ui-if': 'SYS_CTRL_ALLOC==1', + 'outputs_v1': outputs, + 'functions_v1': functions, + 'mixer_v1': mixers, + } + +with open(output_file, 'w') as outfile: + indent = 2 if verbose else None + json.dump(actuators, outfile, indent=indent) + +if compress: + save_compressed(output_file) diff --git a/Tools/module_config/output_groups_from_timer_config.py b/Tools/module_config/output_groups_from_timer_config.py index 998fd2ea5e..f334bd5822 100755 --- a/Tools/module_config/output_groups_from_timer_config.py +++ b/Tools/module_config/output_groups_from_timer_config.py @@ -151,6 +151,9 @@ def get_output_groups(timer_groups, param_prefix="PWM_MAIN", channel_label = channel_labels[channel_type_idx] channel_type_instance = instance_start_label[channel_type_idx] + group_label = channel_label + ' ' + str(channel_type_instance) + if group_count > 1: + group_label += '-' + str(channel_type_instance+group_count-1) group = { 'param_prefix': param_prefix, 'channel_label': channel_label, @@ -159,16 +162,39 @@ def get_output_groups(timer_groups, param_prefix="PWM_MAIN", 'extra_function_groups': deepcopy(extra_function_groups), 'num_channels': group_count, 'standard_params': deepcopy(standard_params), + 'group_label': group_label, + 'channel_label_module_name_prefix': False, } - output_groups.append(group) if pwm_timer_param is not None: - timer_channels_label = channel_label + ' ' + str(channel_type_instance) - if group_count > 1: - timer_channels_label += '-' + str(channel_type_instance+group_count-1) pwm_timer_param_cp = deepcopy(pwm_timer_param) + timer_param_name = param_prefix+'_TIM'+str(timer_index) - if not dshot_support: + group['config_parameters'] = [ + { + 'param': timer_param_name, + 'function': 'primary', + } + ] + + if dshot_support: + # don't show pwm limit params when dshot enabled + + for standard_param in group['standard_params']: + group['standard_params'][standard_param]['show_if'] = timer_param_name + '>=-1' + + # indicate support for changing motor spin direction + group['supported_actions'] = { + 'set_spin_direction1': { + 'supported_if': timer_param_name + '<-1', + 'actuator_types': ['motor'] + }, + 'set_spin_direction2': { + 'supported_if': timer_param_name + '<-1', + 'actuator_types': ['motor'] + }, + } + else: # remove dshot entries if no dshot support values = pwm_timer_param_cp['values'] for key in list(values.keys()): @@ -178,8 +204,9 @@ def get_output_groups(timer_groups, param_prefix="PWM_MAIN", for descr_type in ['short', 'long']: descr = pwm_timer_param_cp['description'][descr_type] pwm_timer_param_cp['description'][descr_type] = \ - descr.replace('${label}', timer_channels_label) - timer_params[param_prefix+'_TIM'+str(timer_index)] = pwm_timer_param_cp + descr.replace('${label}', group_label) + timer_params[timer_param_name] = pwm_timer_param_cp + output_groups.append(group) instance_start += group_count instance_start_label[channel_type_idx] += group_count return (output_groups, timer_params) diff --git a/validation/module_schema.yaml b/validation/module_schema.yaml index 9fba2f88f8..3457fdcff6 100644 --- a/validation/module_schema.yaml +++ b/validation/module_schema.yaml @@ -228,12 +228,38 @@ parameters: actuator_output: type: dict schema: + show_subgroups_if: + # condition: ui only shows the groups if this condition is true + # (e.g. 'UAVCAN_ENABLE>=3') + type: string + regex: &condition_regex "^(true|false|[\\.\\-a-zA-Z0-9_]{1,16}(>|>=|==|!=|<|<=)\\-?\\d+)$" + + config_parameters: + # list of configuration parameters that apply to the whole module + type: list + minlength: 1 + schema: + type: dict + schema: + param: + # parameter name + type: string + required: true + label: + # human-readable label + type: string + function: + type: string + allowed: [ enable ] output_groups: type: list minlength: 1 schema: type: dict schema: + group_label: + # Human-readable group label, e.g. 'ESCs' + type: string generator: # Optional generator that uses additional information for # param generation (e.g. board-specific config) @@ -277,6 +303,10 @@ actuator_output: type: integer min: 0 max: 65536 + show_if: + # ui only shows the param if this condition is true + type: string + regex: *condition_regex min: type: dict schema: @@ -295,6 +325,10 @@ actuator_output: type: integer min: 0 max: 65536 + show_if: + # ui only shows the param if this condition is true + type: string + regex: *condition_regex max: type: dict schema: @@ -313,6 +347,10 @@ actuator_output: type: integer min: 0 max: 65536 + show_if: + # ui only shows the param if this condition is true + type: string + regex: *condition_regex failsafe: type: dict schema: @@ -326,6 +364,10 @@ actuator_output: type: integer min: 0 max: 65536 + show_if: + # ui only shows the param if this condition is true + type: string + regex: *condition_regex extra_function_groups: # Additional function groups to add, defined in output_functions.yaml type: list @@ -346,4 +388,184 @@ actuator_output: # Only used for 'pwm' generator, per-timer config param type: dict schema: *parameter_definition + config_parameters: + # list of configuration parameters that apply to the + # group + type: list + minlength: 1 + schema: + type: dict + schema: + param: + # parameter name + type: string + required: true + label: + # human-readable label + type: string + function: + type: string + allowed: [ primary ] + instance_start: + # The value of the first channel instance for multiple + # instances, used in '${i}'. If 0, ${i} expands to + # [0, N-1] + # Default: 1 + type: integer + instance_start_label: + # Allows to use a different instance start for + # labels vs parameter name. + # Default: equal to 'instance_start' + type: integer + + supported_actions: + # set of actuator actions supported by the driver + type: dict + keyschema: + # action name + type: string + regex: "^(beep|3d_mode_on|3d_mode_off|set_spin_direction1|set_spin_direction2)$" + valueschema: + type: dict + schema: + supported_if: + type: string + regex: *condition_regex + actuator_types: + type: list + minlength: 1 + schema: + type: string + +# Mixer (only set in control_allocator) +mixer: + type: dict + schema: + actuator_types: + type: dict + keyschema: + # actuator type name + type: string + valueschema: + type: dict + schema: + functions: + # Which output functions this maps to. Must be a name from + # output_functions.yaml + type: string + actuator_testing_values: + type: dict + schema: + min: + type: number + min: -1 + max: 1 + required: true + max: + type: number + min: -1 + max: 1 + required: true + default: + type: number + min: -1 + max: 1 + default_is_nan: + type: boolean + required: true + per_item_parameters: + type: list + minlength: 1 + schema: &mixer_parameter + type: dict + schema: + label: + type: string + name: + # param name + type: string + required: true + show_as: + type: string + allowed: [ 'bitset', 'true-if-positive' ] + show_if: + # condition + type: string + regex: *condition_regex + index_offset: + type: integer + advanced: + type: integer + function: + type: string + config: + # Airframe types + type: dict + schema: + param: + # param name to configure the airframe type + type: string + types: + type: dict + keyschema: + # parameter value to select the airframe + type: integer + valueschema: + type: dict + schema: + type: + # geometry type (used for rendering a geometry, and + # show type-specific functionality) + type: string + allowed: [ multirotor ] + actuators: + type: list + minlength: 1 + schema: + type: dict + schema: + actuator_type: + type: string + count: + # param name or fixed count + oneof: + - type: string + - type: integer + parameters: + type: list + minlength: 1 + schema: *mixer_parameter + per_item_parameters: + type: dict + schema: + standard: + type: dict + schema: + position: + type: list + minlength: 3 + maxlength: 3 + schema: + # position param names + type: string + extra: + type: list + schema: *mixer_parameter + instances: + # non-configurable actuators (fixed positions) + type: list + schema: + type: dict + schema: + name: + type: string + required: true + position: + type: list + required: true + schema: + minlength: 3 + maxlength: 3 + type: number +