Path: blob/master/Tools/autotest/logger_metadata/parse.py
9685 views
#!/usr/bin/env python312'''3AP_FLAKE8_CLEAN4'''56import argparse7import copy8import os9import re10import sys1112import emit_html13import emit_rst14import emit_xml15import emit_md1617import enum_parse18from enum_parse import EnumDocco1920topdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../')21topdir = os.path.realpath(topdir)2223# Regular expressions for finding message information in code comments24re_loggermessage = re.compile(r"@LoggerMessage\s*:\s*([\w,]+)", re.MULTILINE)25re_commentline = re.compile(r"\s*//")26re_description = re.compile(r"\s*//\s*@Description\s*:\s*(.*)")27re_url = re.compile(r"\s*//\s*@URL\s*:\s*(.*)")28re_field = re.compile(r"\s*//\s*@Field\s*:\s*(\w+):\s*(.*)")29re_fieldbits = re.compile(r"\s*//\s*@FieldBits\s*:\s*(\w+):\s*(.*)")30re_fieldbitmaskenum = re.compile(r"\s*//\s*@FieldBitmaskEnum\s*:\s*(\w+):\s*(.*)")31re_fieldvalueenum = re.compile(r"\s*//\s*@FieldValueEnum\s*:\s*(\w+):\s*(.*)")32re_vehicles = re.compile(r"\s*//\s*@Vehicles\s*:\s*(.*)")3334# Regular expressions for finding message definitions in structure format35re_start_messagedef = re.compile(r"^\s*{?\s*LOG_[A-Z0-9_]+_[MSGTA]+[A-Z0-9_]*\s*,")36re_deffield = r'[\s\\]*"?([\w\-#?%]+)"?\s*'37re_full_messagedef = re.compile(r'\s*LOG_\w+\s*,\s*\w+\([^)]+\)[\s\\]*,' +38f'{re_deffield},{re_deffield},' +39r'[\s\\]*"?([\w,]+)"?[\s\\]*,' +40f'{re_deffield},{re_deffield}',41re.MULTILINE)42re_names_define = re.compile(r'#define\s+(\w+_LABELS)\s+"([\w,]+)"')43re_fmt_define = re.compile(r'#define\s+(\w+_FMT)\s+"([\w\-#?%]+)"')44re_units_define = re.compile(r'#define\s+(\w+_UNITS)\s+"([\w\-#?%]+)"')45re_mults_define = re.compile(r'#define\s+(\w+_MULTS)\s+"([\w\-#?%]+)"')4647# Regular expressions for finding message definitions in Write calls48re_start_writecall = re.compile(r"\s*[AP:]*logger[\(\)]*.Write[StreamingCrcl]*\(")49re_writefield = r'\s*"([\w\-#?%,]+)"\s*'50re_full_writecall = re.compile(r'\s*[AP:]*logger[\(\)]*.Write[StreamingCrcl]*\(' +51f'{re_writefield},{re_writefield},{re_writefield},({re_writefield},{re_writefield})?',52re.MULTILINE)5354# Regular expression for extracting unit and multipliers from structure55re_units_mults_struct = re.compile(r"^\s*{\s*'([\w\-#?%!/])',"+r'\s*"?([\w\-#?%./]*)"?\s*}')5657# TODO: validate URLS actually return 2005859# Lookup tables are populated by reading LogStructure.h60log_fmt_lookup = {}61log_units_lookup = {}62log_mult_lookup = {}6364# Lookup table to convert multiplier to prefix65mult_prefix_lookup = {660: "",671: "",681e-1: "d", # deci-691e-2: "c", # centi-701e-3: "m", # milli-711e-6: "μ", # micro-721e-9: "n" # nano-73}747576class LoggerDocco(object):7778vehicle_map = {79"Rover": "Rover",80"Sub": "ArduSub",81"Copter": "ArduCopter",82"Plane": "ArduPlane",83"Tracker": "AntennaTracker",84"Blimp": "Blimp",85}8687def __init__(self, vehicle):88self.vehicle = vehicle89self.doccos = []90self.emitters = [91emit_html.HTMLEmitter(),92emit_rst.RSTEmitter(),93emit_xml.XMLEmitter(),94emit_md.MDEmitter(),95]96self.msg_fmts_list = {}97self.msg_names_list = {}98self.msg_units_list = {}99self.msg_mults_list = {}100101class Docco(object):102103def __init__(self, name):104self.name = name105self.url = None106if isinstance(name, list):107self.description = [None] * len(name)108else:109self.description = None110self.fields = {}111self.fields_order = []112self.vehicles = None113self.bits_enums = []114115def add_name(self, name):116# If self.name/description aren't lists, convert them117if isinstance(self.name, str):118self.name = [self.name]119self.description = [self.description]120# Replace any existing empty descriptions with empty strings121for i in range(0, len(self.description)):122if self.description[i] is None:123self.description[i] = ""124# Extend the name and description lists125if isinstance(name, list):126self.name.extend(name)127self.description.extend([None] * len(name))128else:129self.name.append(name)130self.description.append(None)131132def set_description(self, desc):133if isinstance(self.description, list):134for i in range(0, len(self.description)):135if self.description[i] is None:136self.description[i] = desc137else:138self.description = desc139140def set_url(self, url):141self.url = url142143def ensure_field(self, field):144if field not in self.fields:145self.fields[field] = {}146self.fields_order.append(field)147148def set_field_description(self, field, description):149if field in self.fields:150raise ValueError("Already have field %s in %s" %151(field, self.name))152self.ensure_field(field)153self.fields[field]["description"] = description154155def get_field_description(self, field):156if field not in self.fields:157return None158return self.fields[field].get('description', None)159160def set_field_bits(self, field, bits):161bits = bits.split(",")162count = 0163entries = []164for bit in bits:165entries.append(EnumDocco.EnumEntry(bit, 1 << count, None))166count += 1167bitmask_name = self.name + field168self.bits_enums.append(EnumDocco.Enumeration(bitmask_name, entries))169self.ensure_field(field)170self.fields[field]["bitmaskenum"] = bitmask_name171172def set_fieldbitmaskenum(self, field, bits):173self.ensure_field(field)174self.fields[field]["bitmaskenum"] = bits175176def set_fieldvalueenum(self, field, bits):177self.ensure_field(field)178self.fields[field]["valueenum"] = bits179180def set_vehicles(self, vehicles):181self.vehicles = vehicles182183def set_field_names(self, fields):184''' Check that the field ordering matches the defined fields '''185fields = fields.split(",")186# First check that the number of fields match187if len(fields) != len(self.fields_order):188print(f"Error: Mismatch in number of fields in message {self.name}: ", file=sys.stderr, end='')189print(f"{len(self.fields_order)} vs {len(fields)}", file=sys.stderr)190sys.exit(1)191# Now check that each field name matches192err = False193for idx in range(0, len(fields)):194if fields[idx] != self.fields_order[idx]:195print(f"Error: Field order mismatch in log message {self.name}: ", file=sys.stderr, end='')196print(f"field={idx+1}: {fields[idx]} vs {self.fields_order[idx]}", file=sys.stderr)197err = True198# Exit if we had any name mismatch errors199if err:200sys.exit(1)201202def set_fmts(self, fmts):203# If no fields are defined, do nothing204if len(self.fields_order) == 0:205return206# Make sure lengths match up207if len(fmts) != len(self.fields_order):208print(f"Number of fmts don't match fields: msg={self.name} fmts={fmts} num_fields={len(self.fields_order)} {self.fields_order}") # noqa:E501209return210# Loop through the list211for idx in range(0, len(fmts)):212if fmts[idx] in log_fmt_lookup:213self.fields[self.fields_order[idx]]["fmt"] = log_fmt_lookup[fmts[idx]]214else:215print(f"Unrecognised format character: {fmts[idx]} in message {self.name}")216217def set_units(self, units, mults):218# If no fields are defined, do nothing219if len(self.fields_order) == 0:220return221# Make sure lengths match up222if len(units) != len(self.fields_order) or len(units) != len(mults):223print(f"Number of units/mults/fields don't match: msg={self.name} units={units} mults={mults} num_fields={len(self.fields_order)}") # noqa:E501224return225# Loop through the list226for idx in range(0, len(units)):227# Get the index into fields from field_order228f = self.fields_order[idx]229# Convert unit char to base unit230if units[idx] in log_units_lookup:231baseunit = log_units_lookup[units[idx]]232else:233print(f"Unrecognised units character: {units[idx]} in message {self.name}")234continue235# Do nothing if this field has no unit defined236if baseunit == "":237continue238# Convert mult char to value239if mults[idx] in log_mult_lookup:240mult = log_mult_lookup[mults[idx]]241mult_num = float(mult)242else:243print(f"Unrecognised multiplier character: {mults[idx]} in message {self.name}")244continue245# Check if the defined format for this field contains its own multiplier246# If so, the presented value will be the base-unit directly247if 'fmt' in self.fields[f] and self.fields[f]['fmt'].endswith("* 100"):248self.fields[f]["units"] = baseunit249elif 'fmt' in self.fields[f] and "latitude/longitude" in self.fields[f]['fmt']:250self.fields[f]["units"] = baseunit251# Check if we have a defined prefix for this multiplier252elif mult_num in mult_prefix_lookup:253self.fields[f]["units"] = f"{mult_prefix_lookup[mult_num]}{baseunit}"254# If all else fails, set the unit as the multiplier and base unit together255else:256self.fields[f]["units"] = f"{mult} {baseunit}"257258def populate_lookups(self):259# Initialise the lookup tables260# Read the contents of the LogStructure.h file261structfile = os.path.join(topdir, "libraries", "AP_Logger", "LogStructure.h")262with open(structfile) as f:263lines = f.readlines()264f.close()265# Initialise current section to none266section = "none"267# Loop through the lines in the file268for line in lines:269# Look for the start of fmt/unit/mult info270if line.startswith("Format characters"):271section = "fmt"272elif line.startswith("const struct UnitStructure"):273section = "units"274elif line.startswith("const struct MultiplierStructure"):275section = "mult"276# Read formats from code comment, e.g.:277# b : int8_t278elif section == "fmt":279if "*/" in line:280section = "none"281else:282parts = line.split(":")283log_fmt_lookup[parts[0].strip()] = parts[1].strip()284# Read units or multipliers from C struct definition, e.g.:285# { '2', 1e2 }, or { 'J', "W.s" },286elif section != "none":287if "};" in line:288section = "none"289else:290u = re_units_mults_struct.search(line)291if u is not None and section == "units":292log_units_lookup[u.group(1)] = u.group(2)293if u is not None and section == "mult":294log_mult_lookup[u.group(1)] = u.group(2)295296def search_for_files(self, dirs_to_search):297_next = []298for _dir in dirs_to_search:299_dir = os.path.join(topdir, _dir)300for entry in os.listdir(_dir):301filepath = os.path.join(_dir, entry)302if os.path.isdir(filepath):303_next.append(filepath)304continue305(name, extension) = os.path.splitext(filepath)306if extension not in [".cpp", ".h"]:307continue308self.files.append(filepath)309if len(_next):310self.search_for_files(_next)311312def parse_messagedef(self, messagedef):313# Merge concatenated strings and remove comments314messagedef = re.sub(r'"\s+"', '', messagedef)315messagedef = re.sub(r'//[^\n]*', '', messagedef)316# Extract details from a structure definition317d = re_full_messagedef.search(messagedef)318if d is not None:319self.msg_fmts_list[d.group(1)] = d.group(2)320self.msg_names_list[d.group(1)] = d.group(3)321self.msg_units_list[d.group(1)] = d.group(4)322self.msg_mults_list[d.group(1)] = d.group(5)323return324# Extract details from a WriteStreaming call325d = re_full_writecall.search(messagedef)326if d is not None:327if d.group(1) in self.msg_fmts_list:328return329if d.group(5) is None:330self.msg_names_list[d.group(1)] = d.group(2)331self.msg_fmts_list[d.group(1)] = d.group(3)332else:333self.msg_names_list[d.group(1)] = d.group(2)334self.msg_fmts_list[d.group(1)] = d.group(6)335self.msg_units_list[d.group(1)] = d.group(3)336self.msg_mults_list[d.group(1)] = d.group(5)337return338# Didn't parse339# print(f"Unable to parse: {messagedef}")340341def search_messagedef_start(self, line, prevmessagedef=""):342# Look for the start of a structure definition343d = re_start_messagedef.search(line)344if d is not None:345messagedef = line346if "}" in line:347self.parse_messagedef(messagedef)348return ""349else:350return messagedef351# Look for a new call to WriteStreaming352d = re_start_writecall.search(line)353if d is not None:354messagedef = line355if ";" in line:356self.parse_messagedef(messagedef)357return ""358else:359return messagedef360# If we didn't find a new one, continue with any previous state361return prevmessagedef362363def parse_file(self, filepath):364with open(filepath) as f:365# print("Opened (%s)" % filepath)366lines = f.readlines()367f.close()368369def debug(x):370pass371# if filepath == "/home/pbarker/rc/ardupilot/libraries/AP_HAL/AnalogIn.h":372# debug = print373state_outside = "outside"374state_inside = "inside"375messagedef = ""376state = state_outside377docco = None378for line in lines:379debug(f"{state}: {line}")380if messagedef:381messagedef = messagedef + line382if "}" in line or ";" in line:383self.parse_messagedef(messagedef)384messagedef = ""385if state == state_outside:386# Check for start of a message definition387messagedef = self.search_messagedef_start(line, messagedef)388389# Check for fmt/unit/mult #define390u = re_names_define.search(line)391if u is not None:392self.msg_names_list[u.group(1)] = u.group(2)393u = re_fmt_define.search(line)394if u is not None:395self.msg_fmts_list[u.group(1)] = u.group(2)396u = re_units_define.search(line)397if u is not None:398self.msg_units_list[u.group(1)] = u.group(2)399u = re_mults_define.search(line)400if u is not None:401self.msg_mults_list[u.group(1)] = u.group(2)402403# Check for the @LoggerMessage tag indicating the start of the docco block404m = re_loggermessage.search(line)405if m is None:406continue407name = m.group(1)408if "," in name:409name = name.split(",")410state = state_inside411docco = LoggerDocco.Docco(name)412elif state == state_inside:413# If this line is not a comment, then this is the end of the docco block414if not re_commentline.match(line):415state = state_outside416if docco.vehicles is None or self.vehicle in docco.vehicles:417self.finalise_docco(docco)418messagedef = self.search_messagedef_start(line)419continue420# Check for an multiple @LoggerMessage lines in this docco block421m = re_loggermessage.search(line)422if m is not None:423name = m.group(1)424if "," in name:425name = name.split(",")426docco.add_name(name)427continue428# Find and extract data from the various docco fields429m = re_description.match(line)430if m is not None:431docco.set_description(m.group(1))432continue433m = re_url.match(line)434if m is not None:435docco.set_url(m.group(1))436continue437m = re_field.match(line)438if m is not None:439docco.set_field_description(m.group(1), m.group(2))440continue441m = re_fieldbits.match(line)442if m is not None:443docco.set_field_bits(m.group(1), m.group(2))444continue445m = re_fieldbitmaskenum.match(line)446if m is not None:447docco.set_fieldbitmaskenum(m.group(1), m.group(2))448continue449m = re_fieldvalueenum.match(line)450if m is not None:451docco.set_fieldvalueenum(m.group(1), m.group(2))452continue453m = re_vehicles.match(line)454if m is not None:455docco.set_vehicles([x.strip() for x in m.group(1).split(',')])456continue457print("Unknown field (%s)" % str(line))458sys.exit(1)459460def parse_files(self):461for _file in self.files:462self.parse_file(_file)463464def emit_output(self):465# expand things like PIDR,PIDQ,PIDA into multiple doccos466new_doccos = []467for docco in self.doccos:468if isinstance(docco.name, list):469for name, desc in zip(docco.name, docco.description):470tmpdocco = copy.copy(docco)471tmpdocco.name = name472tmpdocco.description = desc473new_doccos.append(tmpdocco)474else:475new_doccos.append(docco)476new_doccos = sorted(new_doccos, key=lambda x : x.name)477478# Try to attach the formats/units/multipliers479for docco in new_doccos:480# Check that the field names are correctly ordered481if docco.name in self.msg_names_list:482if "LABELS" in self.msg_names_list[docco.name]:483if self.msg_names_list[docco.name] in self.msg_names_list:484docco.set_field_names(self.msg_names_list[self.msg_names_list[docco.name]])485else:486docco.set_field_names(self.msg_names_list[docco.name])487else:488print(f"No field names found for message {docco.name}")489# Apply the Formats to the docco490if docco.name in self.msg_fmts_list:491if "FMT" in self.msg_fmts_list[docco.name]:492if self.msg_fmts_list[docco.name] in self.msg_fmts_list:493docco.set_fmts(self.msg_fmts_list[self.msg_fmts_list[docco.name]])494else:495docco.set_fmts(self.msg_fmts_list[docco.name])496else:497print(f"No formats found for message {docco.name}")498# Get the Units499units = None500if docco.name in self.msg_units_list:501if "UNITS" in self.msg_units_list[docco.name]:502if self.msg_units_list[docco.name] in self.msg_units_list:503units = self.msg_units_list[self.msg_units_list[docco.name]]504else:505units = self.msg_units_list[docco.name]506# Get the Multipliers507mults = None508if docco.name in self.msg_mults_list:509if "MULTS" in self.msg_mults_list[docco.name]:510if self.msg_mults_list[docco.name] in self.msg_mults_list:511mults = self.msg_mults_list[self.msg_mults_list[docco.name]]512else:513mults = self.msg_mults_list[docco.name]514# Apply the units/mults to the docco515if units is not None and mults is not None:516docco.set_units(units, mults)517elif units is not None or mults is not None:518print(f"Cannot find matching units/mults for message {docco.name}")519520# every field must have a description. Things like521# FieldBitmaskEnum can create the field object but not fill522# description in.523for docco in new_doccos:524for field in docco.fields:525if docco.get_field_description(field) is None:526raise ValueError(f"{docco.name}.{field} missing description")527528enums_by_name = {}529for enum in self.enumerations:530enums_by_name[enum.name] = enum531for emitter in self.emitters:532emitter.emit(new_doccos, enums_by_name)533534def run(self):535self.populate_lookups()536self.enumerations = enum_parse.EnumDocco(self.vehicle).get_enumerations()537self.files = []538self.search_for_files([self.vehicle_map[self.vehicle], "libraries"])539self.parse_files()540self.emit_output()541542def finalise_docco(self, docco):543self.doccos.append(docco)544self.enumerations += docco.bits_enums545546547if __name__ == '__main__':548parser = argparse.ArgumentParser(description="Parse parameters.")549parser.add_argument("-v", "--verbose", dest='verbose', action='store_true', default=False, help="show debugging output")550parser.add_argument("--vehicle", required=True, help="Vehicle type to generate for")551552args = parser.parse_args()553554s = LoggerDocco(args.vehicle)555556if args.vehicle not in s.vehicle_map:557print("Invalid vehicle (choose from: %s)" % str(s.vehicle_map.keys()))558sys.exit(1)559560s.run()561562563