data/method/mavlink/pymavlink/generator/mavgen.py

373 lines
16 KiB
Python

#!/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 <include> 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")