module config: add generate_actuators_metadata.py script

This commit is contained in:
Beat Küng 2021-11-02 16:39:27 +01:00 committed by Daniel Agar
parent 36d9635518
commit 36296794c8
3 changed files with 731 additions and 7 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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