1285 lines
53 KiB
Python
1285 lines
53 KiB
Python
#! /usr/bin/python
|
|
|
|
"""
|
|
This script generates markdown files for the MAVLink message definition XML at:
|
|
https://github.com/mavlink/mavlink/tree/master/message_definitions/v1.0
|
|
|
|
The files can be imported into a markdown SSG to display the messages as HTML
|
|
|
|
The script runs on Python 3.
|
|
The following libraries must be imported:
|
|
- bs4.
|
|
- lxml
|
|
|
|
The file is run in mavlink/doc/ with no arguments.
|
|
It writes the files to /messages/
|
|
It can also be run for a specific dialect, if specified.
|
|
It can also be imported and used to get information about the XML.
|
|
"""
|
|
|
|
# import lxml.etree as ET
|
|
# import requests
|
|
from bs4 import BeautifulSoup as bs
|
|
import re
|
|
import os # for walk
|
|
|
|
import argparse # for command line parsing
|
|
|
|
MAXIMUM_INCLUDE_FILE_NESTING = 5
|
|
|
|
|
|
class MAVXML(object):
|
|
'''Represents a MAVLink XML file'''
|
|
|
|
def __init__(self, filename):
|
|
self.filename = filename
|
|
self.basename = os.path.basename(filename)
|
|
if self.basename.lower().endswith(".xml"):
|
|
self.basename = self.basename[:-4]
|
|
self.basename_upper = self.basename.upper()
|
|
self.messages = {} # dict
|
|
self.enums = {}
|
|
self.commands = {}
|
|
self.includes = []
|
|
self.dialect = None
|
|
self.version = None
|
|
|
|
# Read the XML file
|
|
with open(self.filename, 'r') as f:
|
|
xml_content = f.read()
|
|
# Initialize BeautifulSoup with the XML content
|
|
soup = bs(xml_content, 'xml')
|
|
|
|
# Extract dialect
|
|
dialect = soup.find('dialect')
|
|
if dialect:
|
|
self.dialect = dialect.text
|
|
|
|
# Extract version
|
|
version = soup.find('version')
|
|
if version:
|
|
self.version = version.text
|
|
|
|
# Extract includes
|
|
includes = soup.find_all('include')
|
|
for include in includes:
|
|
self.includes.append(include.text[:-4])
|
|
|
|
# Extract and reorder messages
|
|
messages = soup.find_all('message')
|
|
for message in messages:
|
|
item = MAVMessage(message, self.basename)
|
|
self.messages[item.name] = item
|
|
# reorder messages by id
|
|
sorted_items = sorted(self.messages.items(),
|
|
key=lambda item: item[1].id)
|
|
# Clear the original dictionary and rebuild it with the sorted items
|
|
self.messages.clear()
|
|
self.messages.update(sorted_items)
|
|
|
|
# Extact all ENUM except MAV_CMD
|
|
# Define a custom filter function to exclude "MAV_CMD"
|
|
def exclude_mav_cmd(tag):
|
|
return tag.name == 'enum' and tag.get('name') != 'MAV_CMD'
|
|
filtered_enums = soup.find_all(exclude_mav_cmd)
|
|
for enum in filtered_enums:
|
|
# print(f"debug: enumTestDalect: {self.basename}")
|
|
item = MAVEnum(enum, self.basename)
|
|
self.enums[item.name] = item
|
|
# reorder the enum values
|
|
for enumName in self.enums.keys():
|
|
# reorder the enum values - sort the entries based on the 'value' property
|
|
mav_enum_entries = self.enums[enumName].entries.values()
|
|
sorted_entries = sorted(
|
|
mav_enum_entries, key=lambda entry: entry.value)
|
|
# Create a new dictionary with the sorted entries
|
|
sorted_enum_entries = {
|
|
entry.name: entry for entry in sorted_entries}
|
|
# Clear the original dictionary and rebuild it with the sorted items
|
|
self.enums[enumName].entries.clear()
|
|
self.enums[enumName].entries.update(sorted_enum_entries)
|
|
|
|
# Extract Commands (MAV_CMD) and reorder
|
|
mav_cmd_enum = soup.find('enum', attrs={'name': 'MAV_CMD'})
|
|
if mav_cmd_enum:
|
|
mav_commands = mav_cmd_enum.find_all('entry')
|
|
for command in mav_commands:
|
|
item = MAVCommand(command, self.basename)
|
|
self.commands[item.name] = item
|
|
# reorder commands by id
|
|
# Sort the items of the dictionary based on the id property of the value (second element)
|
|
sorted_items = sorted(self.commands.items(),
|
|
key=lambda item: item[1].value)
|
|
# Clear the original dictionary and rebuild it with the sorted items
|
|
self.commands.clear()
|
|
self.commands.update(sorted_items)
|
|
|
|
def mergeIn(self, mergeXML):
|
|
"""Merge a passed file into this file"""
|
|
# print(f"debug: mergeIn {mergeXML.basename} into {self.basename}")
|
|
|
|
# merge messages
|
|
for messageName in mergeXML.messages.keys():
|
|
if messageName in self.messages:
|
|
# print(f"debug: mergeIn {messageName} already present, skip")
|
|
continue
|
|
else:
|
|
# print(f"debug: mergeIn {messageName} added from {mergeXML.basename}")
|
|
self.messages[messageName] = mergeXML.messages[messageName]
|
|
# reorder messages by id
|
|
# Sort the items of the dictionary based on the id property of the value (second element)
|
|
sorted_items = sorted(self.messages.items(),
|
|
key=lambda item: item[1].id)
|
|
# Clear the original dictionary and rebuild it with the sorted items
|
|
self.messages.clear()
|
|
self.messages.update(sorted_items)
|
|
|
|
# merge commands
|
|
for commandName in mergeXML.commands.keys():
|
|
if commandName in self.commands:
|
|
# print(f"debug: mergeIn {commandName} already present, skip")
|
|
continue
|
|
else:
|
|
# print(f"debug: mergeIn {commandName} added from {mergeXML.basename}")
|
|
self.commands[commandName] = mergeXML.commands[commandName]
|
|
# reorder commands by id now imported
|
|
# Sort the items of the dictionary based on the id property of the value (second element)
|
|
sorted_items = sorted(self.commands.items(),
|
|
key=lambda item: item[1].value)
|
|
# Clear the original dictionary and rebuild it with the sorted items
|
|
self.commands.clear()
|
|
self.commands.update(sorted_items)
|
|
|
|
# merge enums
|
|
for enumName in mergeXML.enums.keys():
|
|
if enumName in self.enums:
|
|
# print(f"TODO need to merge the values: debug: mergeIn {enumName} already present, skip")
|
|
for enumValue in mergeXML.enums[enumName].entries.keys():
|
|
|
|
if enumValue in self.enums[enumName].entries:
|
|
# print(f"{enumValue} - skip: already present")
|
|
pass
|
|
else:
|
|
# add value from lower level that hasn't been replaced
|
|
self.enums[enumName].entries[enumValue] = mergeXML.enums[enumName].entries[enumValue]
|
|
|
|
# reorder the enum values now imported - sort the entries based on the 'value' property
|
|
mav_enum_entries = self.enums[enumName].entries.values()
|
|
sorted_entries = sorted(
|
|
mav_enum_entries, key=lambda entry: entry.value)
|
|
# Create a new dictionary with the sorted entries
|
|
sorted_enum_entries = {
|
|
entry.name: entry for entry in sorted_entries}
|
|
# Clear the original dictionary and rebuild it with the sorted items
|
|
self.enums[enumName].entries.clear()
|
|
self.enums[enumName].entries.update(sorted_enum_entries)
|
|
# continue
|
|
else:
|
|
# print(f"debug: mergeIn {enumName} added from {mergeXML.basename}")
|
|
# Enum is new, so just merge it
|
|
self.enums[enumName] = mergeXML.enums[enumName]
|
|
|
|
def getMarkdown(self):
|
|
"""Generate Markdown for this XML file"""
|
|
# markdownText = f"# {self.basename}\n\n"
|
|
markdownText = ""
|
|
|
|
intro_text = self.get_top_level_docs(self.basename)
|
|
markdownText += intro_text
|
|
|
|
# Generate dialect and version if present
|
|
if self.dialect:
|
|
markdownText += f"**Protocol dialect:** {self.dialect}\n\n"
|
|
if self.version:
|
|
markdownText += f"**Protocol version:** {self.version}\n\n"
|
|
|
|
# Generate include files docs
|
|
markdownText += "## MAVLink Include Files\n\n"
|
|
if self.includes:
|
|
base_path = '../messages/'
|
|
# Create a list of formatted strings
|
|
for include in self.includes:
|
|
# markdownText+="\n"
|
|
markdownText += f"- [{include}.xml]({base_path}{include}.md)\n"
|
|
else:
|
|
markdownText += " None\n"
|
|
markdownText += "\n"
|
|
|
|
# Get counts of entities
|
|
entity_summary = "## Summary\n\n"
|
|
entity_summary += "Type | Defined | Included\n"
|
|
entity_summary += "--- | --- | ---\n"
|
|
|
|
|
|
matching_count = 0
|
|
non_matching_count = 0
|
|
for message in self.messages.values():
|
|
if message.basename == self.basename:
|
|
matching_count += 1
|
|
else:
|
|
non_matching_count += 1
|
|
result_string = result_string = f"{'[Messages](#messages)' if matching_count + non_matching_count > 0 else 'Messages'} | {matching_count} | {non_matching_count}\n"
|
|
entity_summary += result_string
|
|
|
|
matching_count = 0
|
|
non_matching_count = 0
|
|
for enum in self.enums.values():
|
|
if enum.basename == self.basename:
|
|
matching_count += 1
|
|
else:
|
|
non_matching_count += 1
|
|
result_string = result_string = f"{'[Enums](#enumerated-types)' if matching_count + non_matching_count > 0 else 'Enums'} | {matching_count} | {non_matching_count}\n"
|
|
entity_summary += result_string
|
|
|
|
matching_count = 0
|
|
non_matching_count = 0
|
|
for commands in self.commands.values():
|
|
if commands.basename == commands.basename:
|
|
matching_count += 1
|
|
else:
|
|
non_matching_count += 1
|
|
result_string = result_string = f"{'[Commands](#mav_commands)' if matching_count + non_matching_count > 0 else 'Commands'} | {matching_count} | {non_matching_count}\n\n"
|
|
entity_summary += result_string
|
|
|
|
entity_summary += "The following sections list all entities in the dialect (both included and defined in this file).\n\n"
|
|
markdownText += entity_summary
|
|
|
|
if len(self.messages):
|
|
markdownText += "## Messages\n\n"
|
|
for message in self.messages.values():
|
|
# Get markdown assuming base dialect of this XML
|
|
markdownText += message.getMarkdown(self.basename)
|
|
|
|
if len(self.enums):
|
|
markdownText += "## Enumerated Types\n\n"
|
|
for enum in self.enums.values():
|
|
markdownText += enum.getMarkdown(self.basename)
|
|
|
|
if len(self.commands):
|
|
markdownText += "## Commands (MAV_CMD) {#mav_commands}\n\n"
|
|
for command in self.commands.values():
|
|
markdownText += command.getMarkdown(self.basename)
|
|
|
|
return markdownText
|
|
|
|
def get_top_level_docs(self, filename):
|
|
# Inject top level heading and other details.
|
|
# print('FILENAME (prefix): %s' % filename)
|
|
insert_text = '<!-- THIS FILE IS AUTO-GENERATED: https://github.com/mavlink/mavlink/blob/master/doc/mavlink_xml_to_markdown.py -->\n\n'
|
|
if filename == 'common':
|
|
insert_text += """
|
|
# MAVLINK Common Message Set (common.xml)
|
|
|
|
The MAVLink *common* message set contains *standard* definitions that are managed by the MAVLink project.
|
|
The definitions cover functionality that is considered useful to most ground control stations and autopilots.
|
|
MAVLink-compatible systems are expected to use these definitions where possible (if an appropriate message exists) rather than rolling out variants in their own [dialects](../messages/README.md).
|
|
|
|
The original definitions are defined in [common.xml](https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/common.xml).
|
|
"""
|
|
elif filename == 'minimal':
|
|
insert_text += """
|
|
# MAVLink Minimal Set (minimal.xml)
|
|
|
|
The MAVLink *minimal* set contains the minimal set of definitions for a viable MAVLink system.
|
|
|
|
The message set is defined in [minimal.xml](https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/minimal.xml) and is managed by the MAVLink project.
|
|
|
|
> **Tip** The minimal set is included (imported into) other xml definition files, including the [MAVLink Common Message Set (common.xml)](minimal.md).
|
|
|
|
"""
|
|
elif filename == 'standard':
|
|
insert_text += """
|
|
# Dialect: MAVLINK Standard Message Set (standard.xml)
|
|
|
|
The MAVLink *standard* message set contains *standard* definitions that are managed by the MAVLink project.
|
|
The definitions are those that are expected to be implemented in all flight stacks/ground stations
|
|
AND are likely to be implemented in a compatible way.
|
|
The original definitions are defined in [standard.xml](https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/standard.xml).
|
|
"""
|
|
|
|
elif filename == 'ardupilotmega':
|
|
insert_text += """
|
|
# Dialect: ArduPilotMega
|
|
|
|
These messages define the ArduPilot specific message set, which is custom to [http://ardupilot.org](http://ardupilot.org).
|
|
|
|
This topic is a human-readable form of the XML definition file: [ardupilotmega.xml](https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/ardupilotmega.xml).
|
|
|
|
> **Warning** The ArduPilot MAVLink fork of [ardupilotmega.xml](https://github.com/ArduPilot/mavlink/blob/master/message_definitions/v1.0/ardupilotmega.xml) may contain messages that have not yet been merged into this documentation.
|
|
"""
|
|
elif filename == 'development':
|
|
insert_text += """
|
|
# Dialect: development
|
|
|
|
This dialect contains messages that are proposed for inclusion in the [standard set](standard.md), in order to ease development of prototype implementations.
|
|
They should be considered a 'work in progress' and not included in production builds.
|
|
|
|
This topic is a human-readable form of the XML definition file: [development.xml](https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/development.xml).
|
|
"""
|
|
elif filename == 'all':
|
|
insert_text += """
|
|
# Dialect: all
|
|
|
|
This dialect is intended to `include` all other [dialects](../messages/README.md) in the mavlink/mavlink repository (including [external dialects](https://github.com/mavlink/mavlink/tree/master/external/dialects#mavlink-external-dialects)).
|
|
|
|
Dialects that are in **all.xml** are guaranteed to not have clashes in messages, enums, enum ids, and MAV_CMDs.
|
|
This ensure that:
|
|
|
|
- Systems based on these dialects can co-exist on the same MAVLink network.
|
|
- A Ground Station might (optionally) use libraries generated from **all.xml** to communicate using any of the dialects.
|
|
|
|
> **Warning** New dialect files in the official repository must be added to **all.xml** and restrict themselves to using ids in their own allocated range.
|
|
A few older dialects are not included because these operate in completely closed networks or because they are only used for tests.
|
|
|
|
This topic is a human-readable form of the XML definition file: [all.xml](https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/all.xml).
|
|
"""
|
|
else:
|
|
insert_text += '\n# Dialect: %s' % filename.rsplit('.', 1)[0]
|
|
insert_text += '\n\n*This is a human-readable form of the XML definition file: [%s](https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/%s).*' % (
|
|
filename, filename)
|
|
insert_text += """
|
|
|
|
<span id="mav2_extension_field"></span>
|
|
|
|
> **Note**
|
|
> - MAVLink 2 [extension fields](../guide/define_xml_element.md#message_extensions) are displayed in blue.
|
|
> - Entities from dialects are displayed only as headings (with link to original)
|
|
|
|
<style>
|
|
span.ext {
|
|
color: blue;
|
|
}
|
|
span.warning {
|
|
color: red;
|
|
}
|
|
</style>
|
|
"""
|
|
|
|
return insert_text
|
|
|
|
|
|
class MAVDeprecated(object):
|
|
def __init__(self, soup):
|
|
# name, type, print_format, xml, description='', enum='', display='', units='', instance=False
|
|
self.since = soup.get('since')
|
|
self.replaced_by = soup.get('replaced_by')
|
|
self.description = soup.text
|
|
if self.description:
|
|
self.description = fix_add_implicit_links_items(self.description)
|
|
|
|
# self.debug()
|
|
|
|
def getMarkdown(self):
|
|
markdown = "**DEPRECATED:**"
|
|
markdown += f" Replaced By {fix_add_implicit_links_items(self.replaced_by)} " if self.replaced_by else ''
|
|
markdown += f"({self.since})" if self.since else ''
|
|
markdown += f" — {self.description})" if self.description else ''
|
|
markdown = f'<span class="warning">{markdown.strip()}</span>'
|
|
return markdown
|
|
|
|
def debug(self):
|
|
print(
|
|
f"debug:Deprecated: since({self.since}), replaced_by({fix_add_implicit_links_items(self.replaced_by)}), description({self.description})")
|
|
|
|
|
|
class MAVWip(object):
|
|
def __init__(self, soup=None):
|
|
# <wip/>
|
|
# self.wip = True
|
|
self.description = None
|
|
if soup:
|
|
self.description = soup.text
|
|
|
|
# self.debug()
|
|
|
|
def getMarkdown(self):
|
|
if self.description:
|
|
print(f"TODO: MAVWIP: desc not printed: {self.name}")
|
|
markdown = '**WORK IN PROGRESS**: Do not use in stable production environments (it may change).'
|
|
markdown = f'<span class="warning">{markdown.strip()}</span>'
|
|
return markdown
|
|
|
|
def debug(self):
|
|
print(f"debug:MAVWip: desc({self.description})")
|
|
|
|
|
|
class MAVField(object):
|
|
def __init__(self, soup, parent, extension):
|
|
# name, type, print_format, xml, description='', enum='', display='', units='', instance=False
|
|
self.name = None
|
|
self.type = None
|
|
self.units = None
|
|
self.enum = None
|
|
self.display = None
|
|
self.instance = None
|
|
self.print_format = None
|
|
self.invalid = None
|
|
self.default = None
|
|
self.minValue = None
|
|
self.maxValue = None
|
|
self.multiplier = None
|
|
self.extension = extension
|
|
for attr, value in soup.attrs.items():
|
|
# We do it this way to catch all of them. New additions will throw debug
|
|
if attr == 'name':
|
|
self.name = value
|
|
elif attr == 'type':
|
|
self.type = value
|
|
elif attr == 'units':
|
|
self.units = value
|
|
elif attr == 'enum':
|
|
self.enum = value
|
|
elif attr == 'display':
|
|
self.display = value
|
|
elif attr == 'instance':
|
|
self.instance = True
|
|
elif attr == 'print_format':
|
|
self.print_format = value
|
|
elif attr == 'invalid':
|
|
self.invalid = value
|
|
elif attr == 'default':
|
|
self.default = value
|
|
elif attr == 'minValue':
|
|
self.minValue = value
|
|
elif attr == 'maxValue':
|
|
self.maxValue = value
|
|
elif attr == 'multiplier':
|
|
self.multiplier = value
|
|
else:
|
|
print(
|
|
f"Debug: MAVField: Unexpected attribute: {attr}, Value: {value}")
|
|
|
|
# self.name_upper = self.name.upper()
|
|
self.description = soup.contents # may need further processing
|
|
if not self.description:
|
|
self.description = None
|
|
# print(f"DEBUG: field desc not defined: {self.name}")
|
|
elif len(self.description) == 1:
|
|
self.description = self.description[0] # Expected
|
|
else:
|
|
print(
|
|
f"DEBUG: field desc multiple array problem: {self.name} (len: {len(self.description)} )")
|
|
for item in self.description:
|
|
print(f" DEBUG: {item}")
|
|
|
|
# Tell the message what field types it has - needed for table rendering
|
|
# parent.fieldnames.add('name')
|
|
# parent.fieldnames.add('type')
|
|
parent.fieldnames.add('description')
|
|
if self.units:
|
|
parent.fieldnames.add('units')
|
|
if self.enum:
|
|
parent.fieldnames.add('enum')
|
|
if self.display:
|
|
parent.fieldnames.add('display')
|
|
if self.print_format:
|
|
parent.fieldnames.add('print_format')
|
|
if self.instance:
|
|
parent.fieldnames.add('instance')
|
|
if self.minValue:
|
|
parent.fieldnames.add('minValue')
|
|
if self.maxValue:
|
|
parent.fieldnames.add('minValue')
|
|
if self.default:
|
|
parent.fieldnames.add('default')
|
|
if self.invalid:
|
|
parent.fieldnames.add('invalid')
|
|
if self.multiplier:
|
|
parent.fieldnames.add('multiplier')
|
|
# self.debug()
|
|
|
|
def debug(self):
|
|
print(
|
|
f"Debug_Field- name ({self.name}), type ({self.type}), desc({self.description}), units({self.units}), display({self.display}), instance({self.instance}), multiplier({self.multiplier})")
|
|
# TODO - display, instance, are not output.
|
|
|
|
|
|
class MAVMessage(object):
|
|
def __init__(self, soup, basename):
|
|
self.name = soup['name']
|
|
self.id = int(soup['id'])
|
|
self.name_lower = self.name.lower()
|
|
self.basename = basename
|
|
# self.linenumber = linenumber
|
|
|
|
self.deprecated = None
|
|
self.wip = None
|
|
self.fields = []
|
|
self.fieldnames = set()
|
|
if self.basename == 'development':
|
|
self.wip = MAVWip()
|
|
|
|
# iterate the fields of our message
|
|
extension = None
|
|
for child in soup.children:
|
|
if child.name: # Check if the child is a tag (not a text node)
|
|
if child.name == 'extensions':
|
|
extension = True
|
|
elif child.name == 'field':
|
|
self.fields.append(MAVField(child, self, extension))
|
|
elif child.name == 'description':
|
|
# Will do more processing this.
|
|
self.description = child.contents
|
|
if len(self.description) == 1:
|
|
self.description = self.description[0]
|
|
self.description = tidyDescription(self.description)
|
|
self.description = fix_add_implicit_links_items(
|
|
self.description)
|
|
else:
|
|
print(
|
|
f"DEBUG: message desc multiple array problem: {self.name}")
|
|
pass
|
|
elif child.name == 'deprecated':
|
|
self.deprecated = MAVDeprecated(child)
|
|
elif child.name == 'wip':
|
|
self.wip = MAVWip(child)
|
|
else:
|
|
print(f"MAVMessage: Unexpected tag: {child.name}")
|
|
|
|
# fields = soup.find_all('field')
|
|
# for field in fields:
|
|
# self.fields.append(MAVField(field, self))
|
|
|
|
# self.debug()
|
|
|
|
def getMarkdown(self, currentDialect):
|
|
"""
|
|
Return markdown for a message.
|
|
"""
|
|
message = f"### {self.name} ({self.id})"
|
|
|
|
# Add marker after name if there are additions
|
|
if self.basename is not currentDialect or self.deprecated or self.wip:
|
|
message += " —"
|
|
|
|
# From dialect to heading if in dialect
|
|
if self.basename is not currentDialect:
|
|
# With basename (dialect name) test
|
|
message += f" \[from: [{self.basename}](../messages/{self.basename}.md#{self.name})\]"
|
|
|
|
if self.deprecated:
|
|
message += " [DEP]"
|
|
elif self.wip:
|
|
message += " [WIP]"
|
|
# message+=f"Included from [{self.basename}](../messages/{self.basename}.md#{self.name})\n\n" # With basename (dialect name) test
|
|
message += ' {#' + self.name + '}\n\n'
|
|
|
|
# If dialect, that's it. After this is assuming current dialect
|
|
if self.basename is not currentDialect:
|
|
return message
|
|
|
|
if self.deprecated:
|
|
message += self.deprecated.getMarkdown()+"\n\n"
|
|
if self.wip:
|
|
message += self.wip.getMarkdown()+"\n\n"
|
|
|
|
message += self.description + '\n\n'
|
|
# message+=self.description + f" ({self.basename})\n\n" # With dialect test
|
|
|
|
# Test code for building this using the table builder
|
|
# Note, might need to modify for new max/min stuff
|
|
tableHeadings = []
|
|
tableHeadings.append('Field Name')
|
|
tableHeadings.append('Type')
|
|
valueHeading = False
|
|
unitsHeading = False
|
|
multiplierHeading = False
|
|
if any(field in self.fieldnames for field in ('units',)):
|
|
unitsHeading = True
|
|
tableHeadings.append('Units')
|
|
if any(field in self.fieldnames for field in ('multiplier',)):
|
|
multiplierHeading = True
|
|
tableHeadings.append('Multiplier')
|
|
if any(field in self.fieldnames for field in ('enum', 'invalid, maxValue, minValue, default')):
|
|
valueHeading = True
|
|
tableHeadings.append('Values')
|
|
tableHeadings.append('Description')
|
|
|
|
tableRows = []
|
|
for field in self.fields:
|
|
row = []
|
|
nameText = f"<span class='ext'>{field.name}</span> <a href='#mav2_extension_field'>++</a>" if field.extension else f"{field.name}"
|
|
row.append(nameText)
|
|
row.append(f"`{field.type}`")
|
|
if unitsHeading:
|
|
row.append(f"{field.units if field.units else ''}")
|
|
if multiplierHeading:
|
|
row.append(f"{field.multiplier if field.multiplier else ''}")
|
|
if valueHeading:
|
|
# Values: #invalid, default, minValue, maxValue.
|
|
values = []
|
|
invalidText = f'invalid:{field.invalid}' if field.invalid else ''
|
|
values.append(invalidText)
|
|
defaultText = f'default:{field.default}' if field.default else ''
|
|
values.append(defaultText)
|
|
minValueText = f'min:{field.minValue}' if field.minValue else ''
|
|
values.append(minValueText)
|
|
maxValueText = f'max:{field.maxValue}' if field.maxValue else ''
|
|
values.append(maxValueText)
|
|
|
|
enumText = f"{fix_add_implicit_links_items(field.enum) if field.enum else ''}"
|
|
values.append(enumText)
|
|
|
|
# single elements only get one space
|
|
valueText = "".join(
|
|
f"{elem} " if elem else "" for elem in values)
|
|
row.append(valueText.strip())
|
|
|
|
descriptionText = f"{fix_add_implicit_links_items(tidyDescription(field.description,'table'))}" if field.description else ''
|
|
instanceText = '<br>Messages with same value are from the same source (instance).' if field.instance else ''
|
|
descriptionText += instanceText
|
|
row.append(descriptionText.strip())
|
|
tableRows.append(row)
|
|
|
|
# print("debugtablerows")
|
|
# print(tableRows)
|
|
|
|
message += generateMarkdownTable(tableHeadings, tableRows)
|
|
message += "\n\n"
|
|
return message
|
|
|
|
def debug(self):
|
|
print(
|
|
f"debug:message: name({self.name}, id({self.id}), description({self.description}), deprecated({self.deprecated})")
|
|
|
|
|
|
class MAVEnumEntry(object):
|
|
def __init__(self, soup, basename):
|
|
# name, value, description='', end_marker=False, autovalue=False, origin_file='', origin_line=0, has_location=False
|
|
self.name = soup['name']
|
|
self.value = int(soup.get('value')) if soup.get('value') else print(
|
|
f"TODO MISSING VALUE in MAVEnumEntry: {self.name}")
|
|
self.basename = basename
|
|
self.description = soup.findChild('description', recursive=False)
|
|
self.description = self.description.text if self.description else None
|
|
self.deprecated = soup.findChild('deprecated', recursive=False)
|
|
self.deprecated = MAVDeprecated(
|
|
self.deprecated) if self.deprecated else None
|
|
self.wip = soup.findChild('wip', recursive=False)
|
|
self.wip = MAVWip(self.wip) if self.wip else None
|
|
# self.autovalue = autovalue # True if value was *not* specified in XML
|
|
|
|
def getMarkdown(self, currentDialect):
|
|
"""Return markdown for an enum entry"""
|
|
deprString = f"<b>{self.deprecated.getMarkdown()}" if self.deprecated else ""
|
|
if self.wip:
|
|
print(f"TODO: WIP in Enum Entry: {self.name}")
|
|
importedNote = ""
|
|
if self.basename is not currentDialect:
|
|
importedNote = " — \[from: [{self.basename}](../messages/{self.basename}.md#{self.name})\]"
|
|
if self.basename is not currentDialect:
|
|
print(
|
|
f"TODO/Debug: Check rendering - imported merged enum value {self.name}")
|
|
desc = fix_add_implicit_links_items(tidyDescription(
|
|
self.description, 'table')) if self.description else ""
|
|
string = f"<a id='{self.name}'></a>{self.value} | [{self.name}](#{self.name}) | {desc}{importedNote}{deprString} \n"
|
|
return string
|
|
|
|
|
|
class MAVEnum(object):
|
|
def __init__(self, soup, basename):
|
|
# name, linenumber, description='', bitmask=False
|
|
self.basename = basename # dialect declared in
|
|
self.name = None
|
|
self.bitmask = None
|
|
self.entries = {}
|
|
|
|
for attr, value in soup.attrs.items():
|
|
if attr == 'name':
|
|
self.name = value
|
|
elif attr == 'bitmask':
|
|
self.bitmask = True
|
|
else:
|
|
print(
|
|
f"Debug: MAVEnum: Unexpected attribute: {attr}, Value: {value}")
|
|
|
|
self.description = soup.findChild('description', recursive=False)
|
|
self.description = tidyDescription(
|
|
self.description.text) if self.description else None
|
|
self.deprecated = soup.findChild('deprecated', recursive=False)
|
|
self.deprecated = MAVDeprecated(
|
|
self.deprecated) if self.deprecated else None
|
|
if self.basename == 'development':
|
|
self.wip = MAVWip()
|
|
else:
|
|
self.wip = soup.findChild('wip', recursive=False)
|
|
self.wip = MAVWip(self.wip) if self.wip else None
|
|
self.bitmask = soup.get('bitmask')
|
|
enumEntries = soup.find_all('entry')
|
|
for entry in enumEntries:
|
|
enumVal = MAVEnumEntry(entry, self.basename)
|
|
self.entries[enumVal.name] = enumVal
|
|
|
|
# self.debug()
|
|
|
|
def getMarkdown(self, currentDialect):
|
|
"""Return markdown for a whole enum"""
|
|
|
|
string = f"### {self.name}"
|
|
|
|
# Add marker after name if there are additions
|
|
if self.basename is not currentDialect or self.deprecated or self.wip:
|
|
string += " —"
|
|
|
|
if self.basename is not currentDialect:
|
|
# With basename (dialect name) test
|
|
string += f" \[from: [{self.basename}](../messages/{self.basename}.md#{self.name})\]"
|
|
|
|
if self.deprecated:
|
|
string += " [DEP]"
|
|
elif self.wip:
|
|
string += " [WIP]"
|
|
# message+=f"Included from [{self.basename}](../messages/{self.basename}.md#{self.name})\n\n" # With basename (dialect name) test
|
|
string += ' {#' + self.name + '}\n\n'
|
|
|
|
# If dialect, that's it. After this is assuming current dialect
|
|
if self.basename is not currentDialect:
|
|
return string
|
|
|
|
if self.deprecated:
|
|
string += self.deprecated.getMarkdown()+"\n\n"
|
|
|
|
if self.wip:
|
|
string += self.wip.getMarkdown() + "\n\n"
|
|
|
|
# if self.name=="MAV_FRAME":
|
|
# pass
|
|
# self.debug()
|
|
|
|
string += "(Bitmask) " if self.bitmask else ""
|
|
string += f"{fix_add_implicit_links_items(self.description)}" if self.description else ""
|
|
if self.bitmask or self.description:
|
|
string += "\n\n"
|
|
string += "Value | Name | Description\n--- | --- | ---\n"
|
|
for entry in self.entries.values():
|
|
string += entry.getMarkdown(self.basename)
|
|
string += "\n"
|
|
|
|
return string
|
|
|
|
def debug(self):
|
|
print(
|
|
f"debug:MAVEnum: name({self.name}), bitmask({self.bitmask}), deprecated({self.deprecated}), wip({self.wip}), basename({self.basename})")
|
|
|
|
|
|
class MAVCommandParam(object):
|
|
def __init__(self, soup, parent):
|
|
# name, value, description='', end_marker=False, autovalue=False, origin_file='', origin_line=0, has_location=False
|
|
|
|
self.index = None
|
|
self.label = None
|
|
self.units = None
|
|
self.minValue = None
|
|
self.maxValue = None
|
|
self.increment = None
|
|
self.enum = None
|
|
self.description = None
|
|
self.reserved = None
|
|
self.default = None
|
|
self.multiplier = None
|
|
|
|
for attr, value in soup.attrs.items():
|
|
# We do it this way to catch all of them. New additions will throw debug
|
|
if attr == 'index':
|
|
self.index = int(value)
|
|
elif attr == 'label':
|
|
self.label = value
|
|
elif attr == 'units':
|
|
self.units = value
|
|
elif attr == 'minValue':
|
|
self.minValue = value
|
|
elif attr == 'maxValue':
|
|
self.maxValue = value
|
|
elif attr == 'enum':
|
|
self.enum = value
|
|
elif attr == 'increment':
|
|
self.increment = value
|
|
elif attr == 'reserved':
|
|
self.reserved = True # TODO is it ever reserved by default, and if so make happen
|
|
elif attr == 'default':
|
|
self.default = value # TODO is it ever default by default, and if so make happen?
|
|
elif attr == 'multiplier':
|
|
self.multiplier = value
|
|
else:
|
|
print(
|
|
f"Debug: MAVCommandParam: Unexpected attribute: {attr}, Value: {value}")
|
|
|
|
if soup.text:
|
|
self.description = soup.text
|
|
if self.description:
|
|
self.description = tidyDescription(self.description, "table")
|
|
# no deprecated or wip supported
|
|
# self.autovalue = autovalue # True if value was *not* specified in XML
|
|
|
|
# Add fields to display in parent.
|
|
parent.param_fieldnames.add('index')
|
|
if self.label:
|
|
parent.param_fieldnames.add('label')
|
|
if self.units:
|
|
parent.param_fieldnames.add('units')
|
|
if self.minValue:
|
|
parent.param_fieldnames.add('minValue')
|
|
if self.maxValue:
|
|
parent.param_fieldnames.add('maxValue')
|
|
if self.increment:
|
|
parent.param_fieldnames.add('increment')
|
|
if self.enum:
|
|
parent.param_fieldnames.add('enum')
|
|
if self.multiplier:
|
|
parent.param_fieldnames.add('multiplier')
|
|
|
|
class MAVCommand(object):
|
|
def __init__(self, soup, basename):
|
|
# name, value, description='', end_marker=False, autovalue=False, origin_file='', origin_line=0, has_location=False
|
|
pass
|
|
self.name = soup['name']
|
|
self.value = int(soup.get('value')) if soup.get(
|
|
'value') else "TODO MISSING VALUE"
|
|
self.basename = basename
|
|
self.description = soup.description.text if soup.description else None
|
|
if self.description:
|
|
self.description = tidyDescription(self.description)
|
|
self.deprecated = soup.findChild('deprecated', recursive=False)
|
|
self.deprecated = MAVDeprecated(
|
|
self.deprecated) if self.deprecated else None
|
|
if self.basename == 'development':
|
|
self.wip = MAVWip()
|
|
else:
|
|
self.wip = soup.findChild('wip', recursive=False)
|
|
self.wip = MAVWip(self.wip) if self.wip else None
|
|
# self.autovalue = autovalue # True if value was *not* specified in XML
|
|
self.param_fieldnames = set()
|
|
self.params = []
|
|
params = soup.find_all('param')
|
|
for param in params:
|
|
# TODO: Decide if we want to add entries for non-existing param values
|
|
self.params.append(MAVCommandParam(param, self))
|
|
|
|
def getMarkdown(self, currentDialect):
|
|
"""Return markdown for a command (entry)"""
|
|
|
|
string = f"### {self.name} ({self.value})"
|
|
|
|
# Add marker after name if there are additions
|
|
if self.basename is not currentDialect or self.deprecated or self.wip:
|
|
string += " —"
|
|
|
|
# From dialect to heading if in dialect
|
|
if self.basename is not currentDialect:
|
|
# With basename (dialect name) test
|
|
string += f" \[from: [{self.basename}](../messages/{self.basename}.md#{self.name})\]"
|
|
if self.deprecated:
|
|
string += " [DEP]"
|
|
elif self.wip:
|
|
string += " [WIP]"
|
|
string += ' {#' + self.name + '}\n\n'
|
|
|
|
# If dialect, that's it. After this is assuming current dialect
|
|
if self.basename is not currentDialect:
|
|
return string
|
|
|
|
if self.deprecated:
|
|
string += self.deprecated.getMarkdown() + "\n\n"
|
|
if self.wip:
|
|
string += self.wip.getMarkdown() + "\n\n"
|
|
|
|
string += f"{fix_add_implicit_links_items(self.description)}\n\n" if self.description else ""
|
|
tableHeadings = []
|
|
tableHeadings.append('Param (Label)')
|
|
tableHeadings.append('Description')
|
|
valueHeading = False
|
|
unitsHeading = False
|
|
multiplierHeading = False
|
|
if any(field in self.param_fieldnames for field in ('enum', 'minValue', 'maxValue', 'increment')):
|
|
valueHeading = True
|
|
tableHeadings.append('Values')
|
|
if 'units' in self.param_fieldnames:
|
|
unitsHeading = True
|
|
tableHeadings.append('Units')
|
|
if 'multiplier' in self.param_fieldnames:
|
|
multiplierHeading = True
|
|
tableHeadings.append('Multiplier')
|
|
tableRows = []
|
|
|
|
for param in self.params:
|
|
row = []
|
|
row.append(
|
|
f"{param.index} ({param.label})" if param.label else str(param.index))
|
|
row.append(param.description if param.description else "")
|
|
|
|
if valueHeading:
|
|
valString = " "
|
|
if param.enum:
|
|
valString = fix_add_implicit_links_items(param.enum)
|
|
elif param.minValue or param.maxValue or param.increment:
|
|
if param.minValue:
|
|
valString += f"min: {param.minValue}"
|
|
if param.maxValue:
|
|
valString += f" max: {param.maxValue}"
|
|
if param.increment:
|
|
valString += f" inc: {param.increment}"
|
|
valString = valString.strip()
|
|
row.append(valString)
|
|
|
|
if unitsHeading:
|
|
unitsString = " "
|
|
if param.units:
|
|
unitsString = param.units
|
|
row.append(unitsString)
|
|
|
|
if multiplierHeading:
|
|
multiplierString = " "
|
|
if param.multiplier:
|
|
multiplierString = param.multiplier
|
|
row.append(multiplierString)
|
|
|
|
tableRows.append(row)
|
|
|
|
# print("debugtablerows")
|
|
# print(tableRows)
|
|
|
|
string += generateMarkdownTable(tableHeadings, tableRows)
|
|
string += "\n\n"
|
|
|
|
return string
|
|
|
|
|
|
def tidyDescription(desc_string, type="markdown"):
|
|
"""
|
|
Helper method to remove odd whitepace etc from a description string.
|
|
Different behaviour if the string is to be used in normal markdown or in a table.
|
|
"""
|
|
if "\n" not in desc_string:
|
|
desc_string = desc_string.strip()
|
|
# print(f"debug1strp|{desc_string}|")
|
|
return desc_string
|
|
if type == "markdown":
|
|
# print(f"debug2|{desc_string}|")
|
|
desc = desc_string
|
|
desc.strip()
|
|
lines = desc.splitlines()
|
|
first_line = lines[0].strip()
|
|
new_string = first_line + "\n\n"
|
|
for line in lines[1:]:
|
|
new_string += line.strip() + "\n"
|
|
desc_string = new_string.strip()
|
|
# print(f"debug3|{desc_string}|")
|
|
return desc_string
|
|
if type == "table":
|
|
lines = desc_string.strip().splitlines()
|
|
new_string = "<br>".join(line.strip() for line in lines)
|
|
# for line in lines:
|
|
# new_string += line.strip() + "<br>"
|
|
|
|
return new_string.strip()
|
|
|
|
|
|
def fix_add_implicit_links_items(input_text):
|
|
if not type(input_text) is str:
|
|
# Its not something we can handle
|
|
return input_text
|
|
|
|
# Makes screaming snake case into anchors (helper method). Special fix for MAV_CMD.
|
|
# I don't remember this regexp but it appears to work
|
|
# print("fix_add_implicit_link was called")
|
|
def make_text_to_link(matchobj):
|
|
# print("make_entry_to_link was called: %s" % matchobj.group(0))
|
|
item_string = matchobj.group(2)
|
|
item_url = item_string
|
|
if item_string == 'MAV_CMD':
|
|
item_url = 'mav_commands'
|
|
returnString = f"{matchobj.group(1)}[{item_string}](#{item_url}){matchobj.group(3)}"
|
|
return returnString
|
|
|
|
linked_md = re.sub(
|
|
r'([\`\(\s,]|^)([A-Z]{2,}(?:_[A-Z0-9]+)+)([\`\)\s\.,:]|$)', make_text_to_link, input_text, flags=re.DOTALL)
|
|
return linked_md
|
|
|
|
|
|
def generateMarkdownTable(headings, rows):
|
|
"""Generates a markdown table from an array containing headings and array containing array for every row."""
|
|
string = ""
|
|
pattern = " | ".join(headings) + "\n"
|
|
string += pattern
|
|
# Generate column marker pattern
|
|
field_count = len(headings)
|
|
pattern = ("--- | ") * (field_count - 1) + "---\n"
|
|
string += pattern
|
|
for row in rows:
|
|
# single elements only get one space
|
|
pattern = "| ".join(f"{elem} " if elem else "" for elem in row) + "\n"
|
|
# print('debug: ROW:')
|
|
# print(row)
|
|
# print(pattern)
|
|
string += pattern
|
|
return string
|
|
|
|
|
|
class XMLFiles(object):
|
|
def __init__(self, dialect=None, source_dir="."):
|
|
self.xml_dialects = dict()
|
|
self.source_dir = source_dir
|
|
if not dialect:
|
|
raise ValueError(
|
|
"XMLFiles requires XML dialect name or list of dialect names")
|
|
|
|
dialectNames = []
|
|
if isinstance(dialect, list):
|
|
dialectNames = dialect
|
|
else:
|
|
dialectNames.append(dialect)
|
|
|
|
for dialect in dialectNames:
|
|
xmlFileName = f"{self.source_dir}{dialect}.xml"
|
|
print(f"Importing: {xmlFileName}")
|
|
xmlParser = MAVXML(xmlFileName)
|
|
self.xml_dialects[dialect] = xmlParser
|
|
|
|
self.expand_includes()
|
|
self.update_includes() # TODO - make this optional based on a setting?
|
|
|
|
"""
|
|
# Build a dialect tree for better rendering of included items
|
|
# Dict at top level so we can get self.dialectTree['ardupilotmega']
|
|
# and get a tree we can iterate to print the structure
|
|
# That could be a dict or an array. Probably dict allows more efficiency
|
|
# Might be better if separate to XMLFiles so accessible from where printed.
|
|
# Or we can fetch it with a getter.
|
|
self.dialectTree = {}
|
|
for dialectName in self.xml_dialects.keys():
|
|
print(dialectName)
|
|
if dialectName not in self.dialectTree.keys():
|
|
includes = self.xml_dialects[dialectName].includes
|
|
|
|
self.dialectTree[dialectName]=set()
|
|
for dialect in includes:
|
|
tempDict = {} # {all: {common {stnadard: {minimal: None}} }, common: {} }
|
|
"""
|
|
|
|
def generateDocs(self, output_dir="."):
|
|
for xmlfile in self.xml_dialects.values():
|
|
xmlString = xmlfile.getMarkdown()
|
|
|
|
# Create outputdir if it does not exist
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
|
|
# output_file = f"{output_dir}{xmlfile.filename}.md"
|
|
output_file = f"{output_dir}{xmlfile.basename}.md"
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
print(f"Generating: {output_file}")
|
|
f.write(xmlString)
|
|
|
|
def generateIndexDoc(self, output_dir="."):
|
|
# File for index
|
|
index_file_name = "README.md"
|
|
|
|
# Create outputdir if it does not exist
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
index_file_name = f"{output_dir}{index_file_name}"
|
|
|
|
#initialise text for index file.
|
|
index_text="""<!-- THIS FILE IS AUTO-GENERATED FROM XML: https://github.com/mavlink/mavlink/blob/master/doc/mavlink_xml_to_markdown.py (Do not update mavlink-devguide) -->
|
|
# XML Definition Files & Dialects
|
|
|
|
MAVLink definitions files can be found in [mavlink/message definitions](https://github.com/mavlink/mavlink/blob/master/message_definitions/).
|
|
These can roughly be divided into:
|
|
|
|
- [Standard definitions](#standard-definitions) - core definitions shared by many flight stacks
|
|
- [Test definitions](#test-definitions) - definitions to support testing and validation
|
|
- [Dialects](#dialects) - *protocol-* and *vendor-specific* messages, enums and commands
|
|
|
|
## Standard Definitions
|
|
|
|
The following XML definition files are considered standard/core (i.e. not dialects):
|
|
|
|
- [minimal.xml](minimal.md) - the minimum set of entities (messages, enums, MAV_CMD) required to set up a MAVLink network.
|
|
- [standard.xml](standard.md) - the standard set of entities that are implemented by almost all flight stacks (at least 2, in a compatible way).
|
|
This `includes` [minimal.xml](minimal.md).
|
|
- [common.xml](common.md) - the set of entities that have been implemented in at least one core flight stack.
|
|
This `includes` [standard.xml](minimal.md)
|
|
|
|
> **Note** We are still working towards moving the truly standard entities from **common.xml** to **standard.xml**
|
|
Currently you should include [common.xml](common.md)
|
|
|
|
In addition:
|
|
|
|
- [development.xml](development.md) - XML definitions that are _proposed_ for inclusion in the standard definitions.
|
|
These are work in progress.
|
|
|
|
## Test Definitions
|
|
|
|
The following definitions are used for testing and dialect validation:
|
|
|
|
- [all.xml](all.md) - This includes all other XML files, and is used to verify that there are no ID clashes (and can potentially be used by GCS to communicate with any core dialect).
|
|
- [test.xml](test.md) - Test XML definition file.
|
|
|
|
## Dialects {#dialects}
|
|
|
|
MAVLink *dialects* are XML definition files that define *protocol-* and *vendor-specific* messages, enums and commands.
|
|
|
|
> **Note** Vendor forks of MAVLink may contain XML entities that have not yet been pushed into the main repository (and will not be documented).
|
|
|
|
Dialects may *include* other MAVLink XML files, which may in turn contain other XML files (up to 5 levels of XML file nesting are allowed - see `MAXIMUM_INCLUDE_FILE_NESTING` in [mavgen.py](https://github.com/ArduPilot/pymavlink/blob/master/generator/mavgen.py#L44)).
|
|
A typical pattern is for a dialect to include [common.xml](../messages/common.md) (containing the *MAVLink standard definitions*), extending it with vendor or protocol specific messages.
|
|
|
|
The dialect definitions are:
|
|
|
|
"""
|
|
|
|
for xmlfile in self.xml_dialects.keys():
|
|
index_text+=f"- [{xmlfile}.xml]({xmlfile}.md)\n"
|
|
|
|
#Write the index
|
|
with open(index_file_name, 'w') as content_file:
|
|
print(f"Generating: {index_file_name}")
|
|
content_file.write(index_text)
|
|
|
|
|
|
def expand_includes(self):
|
|
"""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
|
|
includes_to_add = set()
|
|
for name in self.xml_dialects.keys():
|
|
for incl in self.xml_dialects[name].includes:
|
|
# print(incl)
|
|
if incl not in self.xml_dialects:
|
|
# new include
|
|
# print(f"debug: {incl} not in {self.xml_dialects.keys()}")
|
|
includes_to_add.add(incl)
|
|
includeadded = True
|
|
for incl in includes_to_add:
|
|
xmlFileName = f"{self.source_dir}{incl}.xml"
|
|
print(f"Importing included file: {xmlFileName}")
|
|
xmlParser = MAVXML(xmlFileName)
|
|
self.xml_dialects[incl] = xmlParser
|
|
return includeadded
|
|
|
|
for i in range(MAXIMUM_INCLUDE_FILE_NESTING):
|
|
if not expand_oneiteration():
|
|
break
|
|
|
|
def update_includes(self):
|
|
"""Update dialects and merge with included files,
|
|
starting with the bottom level (files with no or fewer includes).
|
|
Includes were already found and parsed into xml list in expand_includes().
|
|
"""
|
|
|
|
# 1: Mark files that don't have includes as "done"
|
|
done = []
|
|
for xmldialect in self.xml_dialects.values():
|
|
if len(xmldialect.includes) == 0:
|
|
done.append(xmldialect.basename)
|
|
# print(f"\nFile with no includes found (ENDPOINT): {xmldialect.basename}" )
|
|
if len(done) == 0:
|
|
print("\nERROR in includes tree, no base found!")
|
|
exit(1)
|
|
|
|
# 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 xmldialect in self.xml_dialects.values():
|
|
if xmldialect.basename in done:
|
|
# print(f"Debug: {xmldialect.basename}: already done, skip")
|
|
continue
|
|
# check if all its includes were already done
|
|
all_includes_done = True
|
|
for i in xmldialect.includes:
|
|
if i not in done:
|
|
all_includes_done = False
|
|
# print(f"debug: {i} not done in {xmldialect.basename}")
|
|
break
|
|
if not all_includes_done:
|
|
# print(f"{xmldialect.basename}: not all includes ready, skip")
|
|
continue
|
|
|
|
# Found file where all includes are done
|
|
done.append(xmldialect.basename)
|
|
# print(f"{xmldialect.basename}: all includes ready, add" )
|
|
# now update it with the facts from all it's includes
|
|
for i in xmldialect.includes:
|
|
# TODO - merge my includes
|
|
# get the corresponding XML file:
|
|
dialectToMerge = self.xml_dialects[i]
|
|
# print(f"debug: merging {dialectToMerge.basename} ({i}) into {xmldialect.basename}")
|
|
# check that it matches
|
|
# update the merge
|
|
xmldialect.mergeIn(dialectToMerge)
|
|
|
|
"""
|
|
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 # WHY DO THIS?
|
|
"""
|
|
|
|
if len(done) == len(self.xml_dialects):
|
|
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 main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Markdown Generator for MAVLink Docs from XML")
|
|
|
|
parser.add_argument("-d", "--source_dir", default="../message_definitions/v1.0/",
|
|
help="Path to XML definition directory")
|
|
parser.add_argument("-i", "--input_dialect", default=None,
|
|
help="Name of XML dialect, e.g. 'common' (if not specified, does all dialects)")
|
|
parser.add_argument("-o", "--output", default="./messages/",
|
|
help="Path to Markdown output directory")
|
|
args = parser.parse_args()
|
|
# print(args.source_dir)
|
|
# print(args.input_dialect)
|
|
# print(args.output)
|
|
|
|
files = None
|
|
# xml_dialects = [] #The list of dialects to generate markdown for
|
|
if args.input_dialect:
|
|
files = XMLFiles(dialect=args.input_dialect,
|
|
source_dir=args.source_dir)
|
|
else:
|
|
all_files = os.listdir(args.source_dir)
|
|
xml_dialects = [file[:-4]
|
|
for file in all_files if file.endswith('.xml')]
|
|
files = XMLFiles(dialect=xml_dialects, source_dir=args.source_dir)
|
|
# xml_dialects.append(f"{args.source_dir}{args.input_dialect}.xml")
|
|
# print(xml_dialects)
|
|
|
|
files.generateDocs(args.output)
|
|
files.generateIndexDoc(args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|