#!/usr/bin/env python '''parse a MAVLink protocol XML file and generate a python implementation Copyright Andrew Tridgell 2011 Released under GNU GPL version 3 or later General process: - each filename passed in: - may be validated, based on --validate - is parsed using mavparse.MAVXML into an xml document and appended to a list, "xml" - expand_includes is called to do a breadth-first search of the xml documents structure inferred by the tags in each document, expanding the xml list from its base (just the ones on the commandline) to the entire structure - update_includes works on the xml list created by expand_includes - any xml document with no includes is added to the "done" list (there must be at least one of these) - it repeatedly calls update_one_iteration - each iteration is intended to include the crcs and other information from includes into the xml document doing the include ''' from __future__ import print_function import sys if sys.version_info <= (3,10): from future import standard_library standard_library.install_aliases() from builtins import object import os import re import sys from . import mavparse # XSD schema file schemaFile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mavschema.xsd") # Set defaults for generating MAVLink code DEFAULT_WIRE_PROTOCOL = mavparse.PROTOCOL_1_0 DEFAULT_LANGUAGE = 'Python' DEFAULT_ERROR_LIMIT = 200 DEFAULT_VALIDATE = True DEFAULT_STRICT_UNITS = False MAXIMUM_INCLUDE_FILE_NESTING = 5 # List the supported languages. This is done globally because it's used by the GUI wrapper too # Right now, 'JavaScript' ~= 'JavaScript_Stable', in the future it may be made equivalent to 'JavaScript_NextGen' supportedLanguages = ["Ada", "C", "CS", "JavaScript", "JavaScript_Stable","JavaScript_NextGen", "TypeScript", "Python2", "Python3", "Python", "Lua", "WLua", "ObjC", "Swift", "Java", "C++11"] def mavgen(opts, args): """Generate mavlink message formatters and parsers (C and Python ) using options and args where args are a list of xml files. This function allows python scripts under Windows to control mavgen using the same interface as shell scripts under Unix""" xml = [] all_files = set() # Enable validation by default, disabling it if explicitly requested if opts.validate: try: from lxml import etree with open(schemaFile, 'r') as f: xmlschema_root = etree.parse(f) if not opts.strict_units: # replace the strict "SI_Unit" list of known unit strings with a more generic "xs:string" type for elem in xmlschema_root.iterfind('xs:attribute[@name="units"]', xmlschema_root.getroot().nsmap): elem.set("type", "xs:string") xmlschema = etree.XMLSchema(xmlschema_root) except ImportError: print("WARNING: Failed to import lxml module etree. Are lxml, libxml2 and libxslt installed? XML validation will not be performed", file=sys.stderr) opts.validate = False except etree.XMLSyntaxError as err: print("WARNING: XML Syntax Errors detected in %s XML schema file. XML validation will not be performed" % schemaFile, file=sys.stderr) print(str(err.error_log), file=sys.stderr) opts.validate = False except Exception as e: print("Exception:", e) print("WARNING: Unable to load XML validator libraries. XML validation will not be performed", file=sys.stderr) opts.validate = False def expand_includes(): """Expand includes. Root files already parsed objects in the xml list.""" def expand_oneiteration(): '''takes the list of xml files to process and finds includes which have not already been turned into xml documents added to xml files to process, turns them into xml documents and adds them to the xml files list. Returns false if no more documents were added. ''' includeadded = False for x in xml[:]: for i in x.include: fname = os.path.abspath(os.path.join(os.path.dirname(x.filename), i)) # Only parse new include files if fname in all_files: continue # Validate XML file with XSD file if possible. if opts.validate: print("Validating %s" % fname) if not mavgen_validate(fname): print("ERROR Validation of %s failed" % fname) exit(1) else: print("Validation skipped for %s." % fname) # Parsing print("Parsing %s" % fname) xml.append(mavparse.MAVXML(fname, opts.wire_protocol)) all_files.add(fname) includeadded = True return includeadded for i in range(MAXIMUM_INCLUDE_FILE_NESTING): if not expand_oneiteration(): break if mavparse.check_duplicates(xml): return False if opts.validate and mavparse.check_missing_enum(xml): return False return True def update_includes(): """Update dialects with crcs etc of included files. Included files were already found and parsed into xml list in expand_includes(). """ # 1: Mark files that don't have includes as "done" done = [] for x in xml: #print("\n",x) if len(x.include) == 0: done.append(x) #print("\nFile with no includes found (ENDPOINT): %s" % x.filename ) if len(done) == 0: print("\nERROR in includes tree, no base found!") exit(1) #print("\n",done) # 2: Update all 'not done' files for which all includes have # been done. Returns True if any updates were made def update_oneiteration(): initial_done_length = len(done) for x in xml: #print("\nCHECK %s" % x.filename) if x in done: #print(" already done, skip") continue #check if all its includes were already done all_includes_done = True for i in x.include: fname = os.path.abspath(os.path.join(os.path.dirname(x.filename), i)) if fname not in [d.filename for d in done]: all_includes_done = False break if not all_includes_done: #print(" not all includes ready, skip") continue #Found file where all includes are done done.append(x) #print(" all includes ready, add" ) #now update it with the facts from all it's includes for i in x.include: fname = os.path.abspath(os.path.join(os.path.dirname(x.filename), i)) #print(" include file %s" % i ) #Find the corresponding x for ix in xml: if ix.filename != fname: continue #print(" add %s" % ix.filename ) x.message_crcs.update(ix.message_crcs) x.message_lengths.update(ix.message_lengths) x.message_min_lengths.update(ix.message_min_lengths) x.message_flags.update(ix.message_flags) x.message_target_system_ofs.update(ix.message_target_system_ofs) x.message_target_component_ofs.update(ix.message_target_component_ofs) x.message_names.update(ix.message_names) x.largest_payload = max(x.largest_payload, ix.largest_payload) break if len(done) == len(xml): return False # finished if len(done) == initial_done_length: # we've made no progress print("ERROR include tree can't be resolved, no base found!") exit(1) return True for i in range(MAXIMUM_INCLUDE_FILE_NESTING): #print("\nITERATION "+str(i)) if not update_oneiteration(): break def mavgen_validate(xmlfile): """Uses lxml to validate an XML file. We define mavgen_validate here because it relies on the XML libs that were loaded in mavgen(), so it can't be called standalone""" xmlvalid = True try: with open(xmlfile, 'r') as f: xmldocument = etree.parse(f) xmlschema.assertValid(xmldocument) forbidden_names_re = re.compile("^(break$|case$|class$|catch$|const$|continue$|debugger$|default$|delete$|do$|else$|\ export$|extends$|finally$|for$|function$|if$|import$|in$|instanceof$|let$|new$|\ return$|super$|switch$|this$|throw$|try$|typeof$|var$|void$|while$|with$|yield$|\ enum$|await$|implements$|package$|protected$|static$|interface$|private$|public$|\ abstract$|boolean$|byte$|char$|double$|final$|float$|goto$|int$|long$|native$|\ short$|synchronized$|transient$|volatile$).*", re.IGNORECASE) for element in xmldocument.iter('enum', 'entry', 'message', 'field'): if forbidden_names_re.search(element.get('name')): print("Validation error:", file=sys.stderr) print("Element : %s at line : %s contains forbidden word" % (element.tag, element.sourceline), file=sys.stderr) xmlvalid = False return xmlvalid except etree.XMLSchemaError: return False except etree.DocumentInvalid as err: sys.exit('ERROR: %s' % str(err.error_log)) return True # Process all XML files, validating them as necessary. for fname in args: # only add each dialect file argument once. if fname in all_files: continue all_files.add(fname) if opts.validate: print("Validating %s" % fname) if not mavgen_validate(fname): return False else: print("Validation skipped for %s." % fname) print("Parsing %s" % fname) xml.append(mavparse.MAVXML(fname, opts.wire_protocol)) # expand includes if not expand_includes(): return False update_includes() print("Found %u MAVLink message types in %u XML files" % ( mavparse.total_msgs(xml), len(xml))) # convert language option to lowercase and validate opts.language = opts.language.lower() if opts.language == 'python': # We support generating type annotations starting with # python3.6. Type annotations were introduced in 3.5, but # useful things like variable annotations are only supported # starting with 3.6. from . import mavgen_python mavgen_python.generate(opts.output, xml, enable_type_annotations=sys.version_info >= (3, 6)) elif opts.language == 'python2': from . import mavgen_python mavgen_python.generate(opts.output, xml, enable_type_annotations=False) elif opts.language == 'python3': from . import mavgen_python mavgen_python.generate(opts.output, xml, enable_type_annotations=True) elif opts.language == 'c': from . import mavgen_c mavgen_c.generate(opts.output, xml) elif opts.language == 'lua': from . import mavgen_lua mavgen_lua.generate(opts.output, xml) elif opts.language == 'wlua': from . import mavgen_wlua mavgen_wlua.generate(opts.output, xml) elif opts.language == 'cs': from . import mavgen_cs mavgen_cs.generate(opts.output, xml) elif (opts.language == 'javascript' ) or ( opts.language == 'javascript_stable' ): from . import mavgen_javascript_stable as mavgen_javascript mavgen_javascript.generate(opts.output, xml) elif opts.language == 'javascript_nextgen': from . import mavgen_javascript mavgen_javascript.generate(opts.output, xml) elif opts.language == 'typescript': from . import mavgen_typescript mavgen_typescript.generate(opts.output, xml) elif opts.language == 'objc': from . import mavgen_objc mavgen_objc.generate(opts.output, xml) elif opts.language == 'swift': from . import mavgen_swift mavgen_swift.generate(opts.output, xml) elif opts.language == 'java': from . import mavgen_java mavgen_java.generate(opts.output, xml) elif opts.language == 'c++11': from . import mavgen_cpp11 mavgen_cpp11.generate(opts.output, xml) elif opts.language == 'ada': if opts.wire_protocol != mavparse.PROTOCOL_1_0: raise DeprecationWarning("Error! Mavgen_Ada only supports protocol version 1.0") else: from . import mavgen_ada mavgen_ada.generate(opts.output, xml) else: print("Unsupported language %s" % opts.language) return True # build all the dialects in the dialects subpackage class Opts(object): def __init__(self, output, wire_protocol=DEFAULT_WIRE_PROTOCOL, language=DEFAULT_LANGUAGE, validate=DEFAULT_VALIDATE, error_limit=DEFAULT_ERROR_LIMIT, strict_units=DEFAULT_STRICT_UNITS): self.wire_protocol = wire_protocol self.error_limit = error_limit self.language = language self.output = output self.validate = validate self.strict_units = strict_units def mavgen_python_dialect(dialect, wire_protocol, with_type_annotations): '''generate the python code on the fly for a MAVLink dialect''' dialects = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'dialects') mdef = os.getenv("MDEF", default=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'message_definitions')) legacy_path = "python2" if not with_type_annotations else "" if wire_protocol == mavparse.PROTOCOL_0_9: py = os.path.join(dialects, 'v09', legacy_path, dialect + '.py') xml = os.path.join(dialects, 'v09', dialect + '.xml') if not os.path.exists(xml): xml = os.path.join(mdef, 'v0.9', dialect + '.xml') elif wire_protocol == mavparse.PROTOCOL_1_0: py = os.path.join(dialects, 'v10', legacy_path, dialect + '.py') xml = os.path.join(dialects, 'v10', dialect + '.xml') if not os.path.exists(xml): xml = os.path.join(mdef, 'v1.0', dialect + '.xml') else: py = os.path.join(dialects, 'v20', legacy_path, dialect + '.py') xml = os.path.join(dialects, 'v20', dialect + '.xml') if not os.path.exists(xml): xml = os.path.join(mdef, 'v1.0', dialect + '.xml') if with_type_annotations: opts = Opts(py, wire_protocol, language="Python3") else: opts = Opts(py, wire_protocol, language='Python2') # Python 2 to 3 compatibility try: import StringIO as io except ImportError: import io # throw away stdout while generating stdout_saved = sys.stdout sys.stdout = io.StringIO() try: xml = os.path.relpath(xml) if not mavgen(opts, [xml]): sys.stdout.seek(0) stdout_saved.write(sys.stdout.getvalue()) sys.stdout = stdout_saved return False except Exception: sys.stdout = stdout_saved raise sys.stdout = stdout_saved return True if __name__ == "__main__": raise DeprecationWarning("Executable was moved to pymavlink.tools.mavgen")