Path: blob/master/Tools/autotest/param_metadata/param_parse.py
9552 views
#!/usr/bin/env python312'''Generates parameter metadata files suitable for consumption by3ground control stations and various web services45AP_FLAKE8_CLEAN67'''89import copy10import os11import re12import sys13from argparse import ArgumentParser1415from param import (Library, Parameter, Vehicle, known_group_fields,16known_param_fields, required_param_fields, required_library_param_fields, known_units)17from htmlemit import HtmlEmit18from rstemit import RSTEmit19from rstlatexpdfemit import RSTLATEXPDFEmit20from xmlemit import XmlEmit21from mdemit import MDEmit22from jsonemit import JSONEmit2324parser = ArgumentParser(description="Parse ArduPilot parameters.")25parser.add_argument("-v", "--verbose", dest='verbose', action='store_true', default=False, help="show debugging output")26parser.add_argument("--vehicle", required=True, help="Vehicle type to generate for")27parser.add_argument("--no-emit",28dest='emit_params',29action='store_false',30default=True,31help="don't emit parameter documentation, just validate")32parser.add_argument("--legacy-params",33dest='emit_legacy_params',34action='store_true',35default=None,36help="include legacy parameters in output (default depends on format)")37parser.add_argument("--no-legacy-params",38dest='emit_legacy_params',39action='store_false',40default=None,41help="don't include legacy parameters in output (default depends on format)")42parser.add_argument("--format",43dest='output_format',44action='store',45default='all',46choices=['all', 'html', 'rst', 'rstlatexpdf', 'wiki', 'xml', 'json', 'edn', 'md'],47help="what output format to use")4849args = parser.parse_args()505152# Regular expressions for parsing the parameter metadata5354prog_param = re.compile(r"@Param(?:{([^}]+)})?: (\w+).*((?:\n[ \t]*// @(\w+)(?:{([^}]+)})?: ?(.*))+)(?:\n[ \t\r]*\n|\n[ \t]+[A-Z]|\n\-\-\]\])", re.MULTILINE) # noqa5556# match e.g @Value: 0=Unity, 1=Koala, 17=Liability57prog_param_fields = re.compile(r"[ \t]*// @(\w+): ?([^\r\n]*)")58# match e.g @Value{Copter}: 0=Volcano, 1=Peppermint59prog_param_tagged_fields = re.compile(r"[ \t]*// @(\w+){([^}]+)}: ([^\r\n]*)")6061prog_groups = re.compile(r"@Group: *(\w*).*((?:\n[ \t]*// @(Path): (\S+))+)", re.MULTILINE)6263apm_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../')646566def find_vehicle_parameter_filepath(vehicle_name):67apm_tools_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../Tools/')6869vehicle_name_to_dir_name_map = {70"Copter": "ArduCopter",71"Plane": "ArduPlane",72"Tracker": "AntennaTracker",73"Sub": "ArduSub",74}7576# first try ArduCopter/Parameters.cpp77for top_dir in apm_path, apm_tools_path:78path = os.path.join(top_dir, vehicle_name, "Parameters.cpp")79if os.path.exists(path):80return path8182# then see if we can map e.g. Copter -> ArduCopter83if vehicle_name in vehicle_name_to_dir_name_map:84path = os.path.join(top_dir, vehicle_name_to_dir_name_map[vehicle_name], "Parameters.cpp")85if os.path.exists(path):86return path8788raise ValueError("Unable to find parameters file for (%s)" % vehicle_name)899091def debug(str_to_print):92"""Debug output if verbose is set."""93if args.verbose:94print(str_to_print)959697def lua_applets():98'''return list of Library objects for lua applets and drivers'''99lua_lib = Library("", reference="Lua Script", not_rst=True, check_duplicates=True)100dirs = ["libraries/AP_Scripting/applets", "libraries/AP_Scripting/drivers"]101paths = []102for d in dirs:103for root, dirs, files in os.walk(os.path.join(apm_path, d)):104for file in files:105if not file.endswith(".lua"):106continue107f = os.path.join(root, file)108debug("Adding lua path %s" % f)109# the library is expected to have the path as a relative path from within110# a vehicle directory111f = f.replace(apm_path, "../")112paths.append(f)113setattr(lua_lib, "Path", ','.join(paths))114return lua_lib115116117libraries = []118119if args.vehicle != "AP_Periph":120# AP_Vehicle also has parameters rooted at "", but isn't referenced121# from the vehicle in any way:122ap_vehicle_lib = Library("", reference="VEHICLE") # the "" is tacked onto the front of param name123setattr(ap_vehicle_lib, "Path", os.path.join('..', 'libraries', 'AP_Vehicle', 'AP_Vehicle.cpp'))124libraries.append(ap_vehicle_lib)125126libraries.append(lua_applets())127128error_count = 0129current_param = None130current_file = None131132133def error(str_to_print):134"""Show errors."""135global error_count136error_count += 1137if current_file is not None:138print("Error in %s" % current_file)139if current_param is not None:140print("At param %s" % current_param)141print(str_to_print)142143144truename_map = {145"Rover": "Rover",146"ArduSub": "Sub",147"ArduCopter": "Copter",148"ArduPlane": "Plane",149"AntennaTracker": "Tracker",150"AP_Periph": "AP_Periph",151"Blimp": "Blimp",152}153valid_truenames = frozenset(truename_map.values())154truename = truename_map.get(args.vehicle, args.vehicle)155156documentation_tags_which_are_comma_separated_nv_pairs = frozenset([157'Values',158'Bitmask',159])160161vehicle_path = find_vehicle_parameter_filepath(args.vehicle)162163basename = os.path.basename(os.path.dirname(vehicle_path))164path = os.path.normpath(os.path.dirname(vehicle_path))165reference = basename # so links don't break we use ArduCopter166vehicle = Vehicle(truename, path, reference=reference)167debug('Found vehicle type %s' % vehicle.name)168169170def process_vehicle(vehicle):171debug("===\n\n\nProcessing %s" % vehicle.name)172current_file = vehicle.path+'/Parameters.cpp'173174f = open(current_file)175p_text = f.read()176f.close()177group_matches = prog_groups.findall(p_text)178179debug(group_matches)180for group_match in group_matches:181library_name = group_match[0].strip()182fields_text = group_match[1].strip()183lib = Library(library_name)184fields = prog_param_fields.findall(fields_text)185for field in fields:186field_name = field[0].strip()187field_value = field[1].strip()188if field_name in known_group_fields:189setattr(lib, field_name, field_value)190else:191error(f"group: unknown parameter metadata field '{field_name}'")192if not any(lib.name == parsed_l.name for parsed_l in libraries):193libraries.append(lib)194195param_matches = []196param_matches = prog_param.findall(p_text)197198for param_match in param_matches:199(only_vehicles, param_name, field_text) = (param_match[0].strip(),200param_match[1].strip(),201param_match[2].strip())202if len(only_vehicles):203only_vehicles_list = [x.strip() for x in only_vehicles.split(",")]204for only_vehicle in only_vehicles_list:205if only_vehicle not in valid_truenames:206raise ValueError("Invalid only_vehicle %s" % only_vehicle)207if vehicle.truename not in only_vehicles_list:208continue209p = Parameter(vehicle.reference+":"+param_name, current_file)210debug(p.name + ' ')211global current_param212current_param = p.name213fields = prog_param_fields.findall(field_text)214p.__field_text = field_text215field_list = []216for field in fields:217(field_name, field_value) = (field[0].strip(), field[1].strip())218field_list.append(field_name)219if field_name in known_param_fields:220value = re.sub('@PREFIX@', "", field_value).rstrip()221if hasattr(p, field_name):222if field_name in documentation_tags_which_are_comma_separated_nv_pairs:223# allow concatenation of (e.g.) bitmask fields224x = eval("p.%s" % field_name)225x += ", "226x += value227value = x228else:229error("%s already has field %s" % (p.name, field_name))230setattr(p, field_name, value)231elif field_name in frozenset(["CopyFieldsFrom", "CopyValuesFrom"]):232setattr(p, field_name, field_value)233else:234error(f"param: unknown parameter metadata field '{field_name}'")235236if (getattr(p, 'Values', None) is not None and237getattr(p, 'Bitmask', None) is not None):238error("Both @Values and @Bitmask present")239240vehicle.params.append(p)241current_file = None242debug("Processed %u params" % len(vehicle.params))243244245process_vehicle(vehicle)246247debug("Found %u documented libraries" % len(libraries))248249libraries = list(libraries)250251alllibs = libraries[:]252253254def all_vehicles(vehicle_list: list) -> bool:255return len(vehicle_list) and vehicle_list[0].lower() == "all-vehicles"256257258def applicable_to_vehicle(vehicle: str, vehicle_list: list) -> bool:259return vehicle in vehicle_list or all_vehicles(vehicle_list)260261262def process_library(vehicle, library, pathprefix=None):263'''process one library'''264paths = library.Path.split(',')265for path in paths:266path = path.strip()267global current_file268current_file = path269debug("\n Processing file '%s'" % path)270if pathprefix is not None:271libraryfname = os.path.join(pathprefix, path)272elif path.find('/') == -1:273libraryfname = os.path.join(vehicle.path, path)274else:275libraryfname = os.path.normpath(os.path.join(apm_path + '/libraries/' + path))276if path and os.path.exists(libraryfname):277f = open(libraryfname)278p_text = f.read()279f.close()280else:281error("Path %s not found for library %s (fname=%s)" % (path, library.name, libraryfname))282continue283284param_matches = prog_param.findall(p_text)285debug("Found %u documented parameters" % len(param_matches))286for param_match in param_matches:287(only_vehicles, param_name, field_text) = (param_match[0].strip(),288param_match[1].strip(),289param_match[2].strip())290if len(only_vehicles):291only_vehicles_list = [x.strip() for x in only_vehicles.split(",")]292for only_vehicle in only_vehicles_list:293if only_vehicle not in valid_truenames:294raise ValueError("Invalid only_vehicle %s" % only_vehicle)295if not applicable_to_vehicle(vehicle.name, only_vehicles_list):296continue297p = Parameter(library.name+param_name, current_file)298debug(p.name + ' ')299global current_param300current_param = p.name301fields = prog_param_fields.findall(field_text)302p.__field_text = field_text303field_list = []304for field in fields:305(field_name, field_value) = (field[0].strip(), field[1].strip())306field_list.append(field_name)307if field_name in known_param_fields:308value = re.sub('@PREFIX@', library.name, field_value)309if hasattr(p, field_name):310if field_name in documentation_tags_which_are_comma_separated_nv_pairs:311# allow concatenation of (e.g.) bitmask fields312x = eval("p.%s" % field_name)313x += ", "314x += value315value = x316else:317error("%s already has field %s" % (p.name, field_name))318setattr(p, field_name, value)319elif field_name in frozenset(["CopyFieldsFrom", "CopyValuesFrom"]):320setattr(p, field_name, field_value)321else:322error(f"param: unknown parameter metadata field '{field_name}'")323324debug("matching %s" % field_text)325fields = prog_param_tagged_fields.findall(field_text)326# a parameter is considered to be vehicle-specific if327# there does not exist a Values: or Values{VehicleName}328# for that vehicle but @Values{OtherVehicle} exists.329seen_values_or_bitmask_for_this_vehicle = False330seen_values_or_bitmask_for_other_vehicle = False331for field in fields:332(field_name, only_for_vehicles, field_value) = (field[0].strip(),333field[1].strip(),334field[2].strip())335only_for_vehicles = only_for_vehicles.split(",")336only_for_vehicles = [some_vehicle.strip() for some_vehicle in only_for_vehicles]337if not all_vehicles(only_for_vehicles):338delta = set(only_for_vehicles) - set(truename_map.values())339if len(delta):340error("Unknown vehicles (%s)" % delta)341debug("field_name=%s vehicle=%s field[1]=%s only_for_vehicles=%s\n" %342(field_name, vehicle.name, field[1], str(only_for_vehicles)))343if field_name not in known_param_fields:344error(f"tagged param: unknown parameter metadata field '{field_name}'")345continue346if not applicable_to_vehicle(vehicle.name, only_for_vehicles):347if len(only_for_vehicles) and field_name in documentation_tags_which_are_comma_separated_nv_pairs:348seen_values_or_bitmask_for_other_vehicle = True349continue350351append_value = False352if field_name in documentation_tags_which_are_comma_separated_nv_pairs:353if applicable_to_vehicle(vehicle.name, only_for_vehicles):354if seen_values_or_bitmask_for_this_vehicle:355append_value = hasattr(p, field_name)356seen_values_or_bitmask_for_this_vehicle = True357else:358if seen_values_or_bitmask_for_this_vehicle:359continue360append_value = hasattr(p, field_name)361362value = re.sub('@PREFIX@', library.name, field_value)363if append_value:364setattr(p, field_name, getattr(p, field_name) + ',' + value)365else:366setattr(p, field_name, value)367368if (getattr(p, 'Values', None) is not None and369getattr(p, 'Bitmask', None) is not None):370error("Both @Values and @Bitmask present")371372if (getattr(p, 'Values', None) is None and373getattr(p, 'Bitmask', None) is None):374# values and Bitmask available for this vehicle375if seen_values_or_bitmask_for_other_vehicle:376# we've (e.g.) seen @Values{Copter} when we're377# processing for Rover, and haven't seen either378# @Values: or @Vales{Rover} - so we omit this379# parameter on the assumption that it is not380# applicable for this vehicle.381continue382383if getattr(p, 'Vector3Parameter', None) is not None:384params_to_add = []385for axis in 'X', 'Y', 'Z':386new_p = copy.copy(p)387new_p.change_name(p.name + "_" + axis)388for a in ["Description"]:389if hasattr(new_p, a):390current = getattr(new_p, a)391setattr(new_p, a, current + " (%s-axis)" % axis)392params_to_add.append(new_p)393else:394params_to_add = [p]395396for p in params_to_add:397p.path = path # Add path. Later deleted - only used for duplicates398if library.check_duplicates and library.has_param(p.name):399error("Duplicate parameter %s in %s" % (p.name, library.name))400continue401library.params.append(p)402403group_matches = prog_groups.findall(p_text)404debug("Found %u groups" % len(group_matches))405debug(group_matches)406done_groups = dict()407for group_match in group_matches:408group = group_match[0].strip()409debug("Group: %s" % group)410do_append = True411if group in done_groups:412# this is to handle cases like the RangeFinder413# parameters, where the wasp stuff gets tack into the414# same RNGFND1_ group415lib = done_groups[group]416do_append = False417else:418lib = Library(group)419done_groups[group] = lib420421fields = prog_param_fields.findall(group_match[1].strip())422for field in fields:423(field_name, field_value) = (field[0].strip(), field[1].strip())424if field_name in known_group_fields:425setattr(lib, field_name, field_value)426elif field_name in ["CopyFieldsFrom", "CopyValuesFrom"]:427setattr(p, field_name, field_value)428else:429error(f"unknown parameter metadata field '{field_name}'")430if not any(lib.Path == parsed_l.Path for parsed_l in libraries):431if do_append:432lib.set_name(library.name + lib.name)433debug("Group name: %s" % lib.name)434process_library(vehicle, lib, os.path.dirname(libraryfname))435if do_append:436alllibs.append(lib)437438current_file = None439440441for library in libraries:442debug("===\n\n\nProcessing library %s" % library.name)443444if hasattr(library, 'Path'):445process_library(vehicle, library)446else:447error("Skipped: no Path found")448449debug("Processed %u documented parameters" % len(library.params))450451452def natural_sort_key(libname):453"""Natural sort key used for sorting alphanumeric strings"""454# splitting string into parts (numeric , non-numeric)455parts = re.split(r'(\d+)', libname)456return tuple((int(p) if p.isdigit() else p) for p in parts)457458459# sort libraries by name460alllibs = sorted(alllibs, key=lambda x: natural_sort_key(x.name))461462libraries = alllibs463464465def is_number(numberString):466try:467float(numberString)468return True469except ValueError:470return False471472473def clean_param(param):474if (hasattr(param, "Values")):475valueList = param.Values.split(",")476new_valueList = []477for i in valueList:478(start, sep, end) = i.partition(":")479if sep != ":":480raise ValueError("Expected a colon separator in (%s)" % (i,))481if len(end) == 0:482raise ValueError("Expected a colon-separated string, got (%s)" % i)483end = end.strip()484start = start.strip()485new_valueList.append(":".join([start, end]))486param.Values = ",".join(new_valueList)487488if hasattr(param, "Vector3Parameter"):489del param.Vector3Parameter490491492def do_copy_values(vehicle_params, libraries, param):493if not hasattr(param, "CopyValuesFrom"):494return495496# so go and find the values...497wanted_name = param.CopyValuesFrom498if hasattr(param, 'Vector3Parameter'):499suffix = param.name[-2:]500wanted_name += suffix501502del param.CopyValuesFrom503for x in vehicle_params:504name = x.name505(v, name) = name.split(":")506if name != wanted_name:507continue508param.Values = x.Values509return510511for lib in libraries:512for x in lib.params:513if x.name != wanted_name:514continue515param.Values = x.Values516return517518error("Did not find value to copy (%s wants %s)" %519(param.name, wanted_name))520521522def do_copy_fields(vehicle_params, libraries, param):523do_copy_values(vehicle_params, libraries, param)524525if not hasattr(param, 'CopyFieldsFrom'):526return527528# so go and find the values...529wanted_name = param.CopyFieldsFrom530del param.CopyFieldsFrom531532if hasattr(param, 'Vector3Parameter'):533suffix = param.name[-2:]534wanted_name += suffix535536for x in vehicle_params:537name = x.name538(v, name) = name.split(":")539if name != wanted_name:540continue541for field in dir(x):542if hasattr(param, field):543# override544continue545if field.startswith("__") or field in frozenset(["name", "real_path"]):546# internal methods like __ne__547continue548setattr(param, field, getattr(x, field))549return550551for lib in libraries:552for x in lib.params:553if x.name != wanted_name:554continue555for field in dir(x):556if hasattr(param, field):557# override558continue559if field.startswith("__") or field in frozenset(["name", "real_path"]):560# internal methods like __ne__561continue562setattr(param, field, getattr(x, field))563return564565error("Did not find value to copy (%s wants %s)" %566(param.name, wanted_name))567568569def validate(param, is_library=False):570"""571Validates the parameter meta data.572"""573global current_file574current_file = param.real_path575global current_param576current_param = param.name577# Validate values578if (hasattr(param, "Range")):579rangeValues = param.__dict__["Range"].split(" ")580if (len(rangeValues) != 2):581error("Invalid Range values for %s (%s)" %582(param.name, param.__dict__["Range"]))583return584min_value = rangeValues[0]585max_value = rangeValues[1]586if not is_number(min_value):587error("Min value not number: %s %s" % (param.name, min_value))588return589if not is_number(max_value):590error("Max value not number: %s %s" % (param.name, max_value))591return592# Check for duplicate in @value field593if (hasattr(param, "Values")):594valueList = param.__dict__["Values"].split(",")595values = []596for i in valueList:597i = i.replace(" ", "")598values.append(i.partition(":")[0])599600# Make sure all values are numbers601for value in values:602if not is_number(value):603error("Value not number: \"%s\"" % value)604605if (len(values) != len(set(values))):606error("Duplicate values found" + str({x for x in values if values.count(x) > 1}))607608# Validate units609if (hasattr(param, "Units")):610if (param.__dict__["Units"] != "") and (param.__dict__["Units"] not in known_units):611error("unknown units field '%s'" % param.__dict__["Units"])612# Validate User613if (hasattr(param, "User")):614if param.User.strip() not in ["Standard", "Advanced"]:615error("unknown user (%s)" % param.User.strip())616617# Validate description618if (hasattr(param, "Description")):619if not param.Description or not param.Description.strip():620error("Empty Description (%s)" % param)621622# Check range and values don't contradict one another623if (hasattr(param, "Range") and hasattr(param, "Values")):624# Get the min and max of values625valueList = param.__dict__["Values"].split(",")626values = [float(v.replace(" ", "").partition(":")[0]) for v in valueList]627minValue = min(values)628maxValue = max(values)629630# Get min and max range631rangeValues = param.__dict__["Range"].split(" ")632minRange = float(rangeValues[0])633maxRange = float(rangeValues[1])634635# Check values are within range636if minValue < minRange:637error("Range of %f to %f and value of: %f" % (minRange, maxRange, minValue))638if maxValue > maxRange:639error("Range of %f to %f and value of: %f" % (minRange, maxRange, maxValue))640641# Validate increment642if (hasattr(param, "Increment")):643if not is_number(param.Increment):644error("Increment not number: \"%s\"" % param.Increment)645646required_fields = required_param_fields647if is_library:648required_fields = required_library_param_fields649for req_field in required_fields:650if not getattr(param, req_field, False):651error("missing parameter metadata field '%s' in %s" % (req_field, param.__field_text))652653654# handle CopyFieldsFrom and CopyValuesFrom:655for param in vehicle.params:656do_copy_fields(vehicle.params, libraries, param)657for library in libraries:658for param in library.params:659do_copy_fields(vehicle.params, libraries, param)660661for param in vehicle.params:662clean_param(param)663664for param in vehicle.params:665validate(param)666667# Find duplicate names in library and fix up path668for library in libraries:669param_names_seen = set()670param_names_duplicate = set()671# Find duplicates:672for param in library.params:673if param.name in param_names_seen: # is duplicate674param_names_duplicate.add(param.name)675param_names_seen.add(param.name)676# Fix up path for duplicates677for param in library.params:678if param.name in param_names_duplicate:679param.path = param.path.rsplit('/')[-1].rsplit('.')[0]680else:681# not a duplicate, so delete attribute.682del param.path683684for library in libraries:685for param in library.params:686clean_param(param)687688for library in libraries:689for param in library.params:690validate(param, is_library=True)691692if not args.emit_params:693sys.exit(error_count)694695all_emitters = {696'json': JSONEmit,697'xml': XmlEmit,698'html': HtmlEmit,699'rst': RSTEmit,700'rstlatexpdf': RSTLATEXPDFEmit,701'md': MDEmit,702}703704try:705from ednemit import EDNEmit706all_emitters['edn'] = EDNEmit707except ImportError:708# if the user wanted edn only then don't hide any errors709if args.output_format == 'edn':710raise711712if args.verbose:713print("Unable to emit EDN, install edn_format and pytz if edn is desired")714715# filter to just the ones we want to emit:716emitters_to_use = []717for emitter_name in all_emitters.keys():718if args.output_format == 'all' or args.output_format == emitter_name:719emitters_to_use.append(emitter_name)720721# actually invoke each emitter:722for emitter_name in emitters_to_use:723emit = all_emitters[emitter_name]()724725emit.emit_legacy_params = args.emit_legacy_params726if emit.emit_legacy_params is None:727if emitter_name in ('rst', 'rstlatexpdf'):728# do not emit legacy parameters to the Wiki729emit.emit_legacy_params = False730else:731emit.emit_legacy_params = True732733emit.emit(vehicle)734735emit.start_libraries()736737# create a single parameter list for all SIM_ parameters (for rst to use)738sim_params = []739for library in libraries:740if library.name.startswith("SIM_"):741sim_params.extend(library.params)742sim_params = sorted(sim_params, key=lambda x : x.name)743744for library in libraries:745if library.params:746# we sort the parameters in the SITL library to avoid747# rename, and on the assumption that an asciibetical sort748# gives a good layout:749if emitter_name == 'rst':750if library.not_rst:751continue752if library.name == 'SIM_':753library = copy.deepcopy(library)754library.params = sim_params755elif library.name.startswith('SIM_'):756continue757emit.emit(library)758759emit.close()760761sys.exit(error_count)762763764