pymavlink 源碼剖析(一)之XML文件的數據解析


相關:

  1. MAVLink 協議解析之原理篇;
  2. MAVLink 協議解析之XML定義篇
  3. pymavlink 源碼剖析(二)之生成代碼

1 引言

pymavlinkMAVLink 協議的 Python 實現,同時它還是一個 MAVLink 協議代碼實現的自動生成工具,目前支持的語言有 CC++11PythonJavaJavascriptTypescriptC#wluaObj-C。 本文是 pymavlink 源碼剖析 文章的第一篇,內容可以分爲兩部分:一是概述了 pymavlink 的實現代碼自動生成的基本流程;二是仔細描述了 pymavlink 的XML 文件的數據解析流程,而本篇的題目即以此命名。
如果對 MAVLink 協議還不太熟悉請參考文章目錄下方 “相關” 裏面給出的鏈接。

2 pymavlink 的代碼自動生成方法

pymavlink 的代碼 clone 下來之後的文件目錄結構如下
pymavlink 文件目錄結構
MAVLink官方文檔可以知道 Tools/mavgen.py 是pymavlink 代碼生成的入口程序。其內容如下

代碼段1

#!/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

'''

# allow running mavgen from within the tree without installing
if __name__ == "__main__" and __package__ in ('', None):
   from os import sys, path
   sys.path.insert(0, path.dirname(path.dirname(path.dirname(path.abspath(__file__)))))

from pymavlink.generator import mavgen
from pymavlink.generator import mavparse

from argparse import ArgumentParser

parser = ArgumentParser(description="This tool generate implementations from MAVLink message definitions")
parser.add_argument("-o", "--output", default="mavlink", help="output directory.")
parser.add_argument("--lang", dest="language", choices=mavgen.supportedLanguages, default=mavgen.DEFAULT_LANGUAGE, help="language of generated code [default: %(default)s]")
parser.add_argument("--wire-protocol", choices=[mavparse.PROTOCOL_0_9, mavparse.PROTOCOL_1_0, mavparse.PROTOCOL_2_0], default=mavgen.DEFAULT_WIRE_PROTOCOL, help="MAVLink protocol version. [default: %(default)s]")
parser.add_argument("--no-validate", action="store_false", dest="validate", default=mavgen.DEFAULT_VALIDATE, help="Do not perform XML validation. Can speed up code generation if XML files are known to be correct.")
parser.add_argument("--error-limit", default=mavgen.DEFAULT_ERROR_LIMIT, help="maximum number of validation errors to display")
parser.add_argument("--strict-units", action="store_true", dest="strict_units", default=mavgen.DEFAULT_STRICT_UNITS, help="Perform validation of units attributes.")
parser.add_argument("definitions", metavar="XML", nargs="+", help="MAVLink definitions")
args = parser.parse_args()

mavgen.mavgen(args, args.definitions)

由其內容可以看出它實際上是對 generator/mavgen.py 的簡單調用, 所以這裏主要關注的是 generator/mavgen.py 的內容,mavgen.py 所在文件夾 generator 包含了自動生成代碼所需的主要文件,其文件夾結構如下:
generator 文件夾結構
可以看到在文件夾下有以 mavgen_ 開頭後面跟着編程語言名稱的幾個 .py 文件、XML結構定義文件mavschema.xsd 、幾個以編程語言名稱命名的文件夾,以及一些其他文件。 mavgen.py 包含了入口函數 mavgen ,其主要執行以下幾個項內容:

  1. XML 文件預處理: 檢查輸入的XML 文件是否有效
  2. 解析 XML 的數據,同時把<include> 標籤引入的XML 文件添加到待處理列表裏
  3. 生成指定編程語言的 MAVLink 協議代碼

下面對每一部分分別進行介紹。

3 XML 文件的數據解析

3.1 XML 文件預處理

mavgen 這個函數裏,定義了函數 mavgen_validate 完成了上一節中提到的 XML 文件的有效性校驗。mavgen_validate 函數的內容如下:

代碼段 2

    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

這段代碼主要對XML 的有效性做了三個方面的檢驗:
(1)是否是有效的XML 格式的文件
(2)是否符合 mavschema.xsd 文件的定義
(3)是否在標籤的屬性裏含有不允許的字符。這裏的不允許字符串主要是用來檢查是否和目標語言的關鍵字衝突。

3.2 解析 XML 的數據

解析XML 的調用的是 mavparse.py 中定義的 MAVXML 類,

142  xml.append(mavparse.MAVXML(fname, opts.wire_protocol))

下面分析 MAVXML 類。MAVXML 類的代碼框架如下

代碼段 3

class MAVXML(object):
    '''parse a mavlink XML file'''
    def __init__(self, filename, wire_protocol_version=PROTOCOL_0_9):
    	#initial code
    def __str__(self):
    	return "MAVXML for %s from %s (%u message, %u enums)" % (
            self.basename, self.filename, len(self.message), len(self.enum))

可以看到 MAVXML 類只是重載了__init____str__,並沒有更多的方法定義,同時注意到 __str__ 作用是把XML 的一些文件信息給返回。下面主要分析一下類初始化函數 __init____init__ 函數的定義如 代碼塊 3 中所示。第一個參數是文件名,第二個參數爲協議的版本號,默認爲0.9 版本。__init__ 的步驟可以分爲如下幾個步驟:

  1. 依據協議版本初始化一些版本特徵變量,包括協議標誌位,是否對數據幀進行排序,是否有校驗等。
  2. 定義start_elementend_elementchar_data 分別作爲xml.parsers.expat 模塊中的XMLParserType 對象的StartElementHandler,EndElementHandler, CharacterDataHandler的實現。然後開始對filename 指定的文件進行解析。
  3. 對解析完的數據進行後處理,包括對 payload 排序,計算crc_extra等。

這裏我們看下調用MAVXML 構造函數後的對象的屬性列表
MAVXML 屬性列表
可以看到MAVXML 中從 XML 文件中解析出了很多信息,下面具體地描述解析過程。

3.2.1 依據協議版本初始化一些版本特徵變量

首先是獲得當前處理 XML 文件名及版本號,及初始化messageenuminclude屬性爲空,並設置了parse_time 屬性爲當前的日期。

代碼段 4

186         self.filename = filename
187         self.basename = os.path.basename(filename)
188         if self.basename.lower().endswith(".xml"):
189             self.basename = self.basename[:-4]
190         self.basename_upper = self.basename.upper()
191         self.message = []
192         self.enum = []
193         # we use only the day for the parse_time, as otherwise
194         # it causes a lot of unnecessary cache misses with ccache
195         self.parse_time = time.strftime("%a %b %d %Y")
196         self.version = 2
197         self.include = []
198         self.wire_protocol_version = wire_protocol_version

然後依據協議的版本,設置一些 flag,分別是:

  • protocol_marker: 標誌消息開始的字節
  • sort_fields:是否對payload 域進行排序
  • little_endian :字符的是否按最小字節先發送的傳輸順序
  • crc_extra: 是否在checksum 的計算中加入 crc_extra
  • crc_struct: 是否把結構體包括在crc的計算中
  • command_24bit: message id 的範圍是否允許超過256
  • allow_extension: 是否允許message 消息中存在擴展域

代碼段 5

200         # setup the protocol features for the requested protocol version
201         if wire_protocol_version == PROTOCOL_0_9:
202             self.protocol_marker = ord('U')
203             self.sort_fields = False
204             self.little_endian = False
205             self.crc_extra = False
206             self.crc_struct = False
207             self.command_24bit = False
208             self.allow_extensions = False
209         elif wire_protocol_version == PROTOCOL_1_0:
210             self.protocol_marker = 0xFE
211             self.sort_fields = True
212             self.little_endian = True
213             self.crc_extra = True
214             self.crc_struct = False
215             self.command_24bit = False
216             self.allow_extensions = False
217         elif wire_protocol_version == PROTOCOL_2_0:
218             self.protocol_marker = 0xFD
219             self.sort_fields = True
220             self.little_endian = True
221             self.crc_extra = True
222             self.crc_struct = True
223             self.command_24bit = True
224             self.allow_extensions = True
225         else:
226             print("Unknown wire protocol version")
227             print("Available versions are: %s %s %s" % (PROTOCOL_0_9, PROTOCOL_1_0, PROTOCOL_2_0))
228             raise MAVParseError('Unknown MAVLink wire protocol version %s' % wire_protocol_version)

3.2.2 解析 XML 文件

這裏先看一下 Pythonxml.parsers.expat 模塊的官方文檔中給出的例子

代碼段 5

import xml.parsers.expat

# 3 handler functions
def start_element(name, attrs):
   print('Start element:', name, attrs)
def end_element(name):
   print('End element:', name)
def char_data(data):
   print('Character data:', repr(data))

p = xml.parsers.expat.ParserCreate()

p.StartElementHandler = start_element
p.EndElementHandler = end_element
p.CharacterDataHandler = char_data

p.Parse("""<?xml version="1.0"?>
<parent id="top"><child1 name="paul">Text goes here</child1>
<child2 name="fred">More text</child2>
</parent>""", 1)

結果輸出爲:

Start element: parent {'id': 'top'}
Start element: child1 {'name': 'paul'}
Character data: 'Text goes here'
End element: child1
Character data: '\n'
Start element: child2 {'name': 'fred'}
Character data: 'More text'
End element: child2
Character data: '\n'
End element: parent

從上面的輸出可以看出,xml.parsers.expat 解析流程爲:把 XML 文件從頭到尾的所有標籤按順序遍歷,對於標籤的入口調用 StartElementHandler 方法,對於標籤中的文本調用CharacterDataHandler方法,標籤結束時則調用EndElementHandler方法。

MAVXML 中定義了start_element(name, attrs)對應於標籤入口函數的、char_data(data)對應於標籤內文本處理函數,end_element(name)對應於標籤出口函數。下面是標籤入口函數start_element 的定義:

代碼段 6

239         def start_element(name, attrs):
240             """ """
241             in_element_list.append(name)
242             in_element = '.'.join(in_element_list)
243             #print in_element
244             if in_element == "mavlink.messages.message":                                                                           245                 check_attrs(attrs, ['name', 'id'], 'message')
246                 self.message.append(MAVType(attrs['name'], attrs['id'], p.CurrentLineNumber))
247             elif in_element == "mavlink.messages.message.extensions":
248                 self.message[-1].extensions_start = len(self.message[-1].fields)
249             elif in_element == "mavlink.messages.message.field":
250                 check_attrs(attrs, ['name', 'type'], 'field')
251                 print_format = attrs.get('print_format', None)
252                 enum = attrs.get('enum', '')
253                 display = attrs.get('display', '')
254                 units = attrs.get('units', '')
255                 if units:
256                     units = '[' + units + ']'
257                 new_field = MAVField(attrs['name'], attrs['type'], print_format, self, enum=enum, display=display, units=units)
258                 if self.message[-1].extensions_start is None or self.allow_extensions:
259                     self.message[-1].fields.append(new_field)
260             elif in_element == "mavlink.enums.enum":
261                 check_attrs(attrs, ['name'], 'enum')
262                 self.enum.append(MAVEnum(attrs['name'], p.CurrentLineNumber))
263             elif in_element == "mavlink.enums.enum.entry":
264                 check_attrs(attrs, ['name'], 'enum entry')
265                 # determine value and if it was automatically assigned (for possible merging later)
266                 if 'value' in attrs:
267                     value = eval(attrs['value'])
268                     autovalue = False
269                 else:
270                     value = self.enum[-1].highest_value + 1
271                     autovalue = True
272                 # check lowest value
273                 if (self.enum[-1].start_value is None or value < self.enum[-1].start_value):
274                     self.enum[-1].start_value = value
275                 # check highest value
276                 if (value > self.enum[-1].highest_value):
277                     self.enum[-1].highest_value = value
278                 # append the new entry
279                 self.enum[-1].entry.append(MAVEnumEntry(attrs['name'], value, '', False, autovalue, self.filename, p.CurrentLineNum    ber))
280             elif in_element == "mavlink.enums.enum.entry.param":
281                 check_attrs(attrs, ['index'], 'enum param')
282                 self.enum[-1].entry[-1].param.append(
283                                                 MAVEnumParam(attrs['index'],
284                                                         label=attrs.get('label', ''), units=attrs.get('units', ''),
285                                                         enum=attrs.get('enum', ''), increment=attrs.get('increment', ''),
286                                                         minValue=attrs.get('minValue', ''),
287                                                         maxValue=attrs.get('maxValue', ''), default=attrs.get('default', '0'),
288                                                         reserved=attrs.get('reserved', False) ))

下面是出口函數end_element的定義

代碼段 7

298         def end_element(name):
299             """"""
300             in_element_list.pop()

下面是標籤內文本處理函數的定義,

代碼段 8

302         def char_data(data):
303             in_element = '.'.join(in_element_list)
304             if in_element == "mavlink.messages.message.description":
305                 self.message[-1].description += data
306             elif in_element == "mavlink.messages.message.field":
307                 if self.message[-1].extensions_start is None or self.allow_extensions:
308                     self.message[-1].fields[-1].description += data
309             elif in_element == "mavlink.enums.enum.description":
310                 self.enum[-1].description += data
311             elif in_element == "mavlink.enums.enum.entry.description":
312                 self.enum[-1].entry[-1].description += data
313             elif in_element == "mavlink.enums.enum.entry.param":
314                 self.enum[-1].entry[-1].param[-1].description += data
315             elif in_element == "mavlink.version":
316                 self.version = int(data)
317             elif in_element == "mavlink.include":
318                 self.include.append(data)

下面是上面所定義的handlers函數的綁定及XML 的解析函數的調用:

代碼段 9

320         f = open(filename, mode='rb')
321         p = xml.parsers.expat.ParserCreate()
322         p.StartElementHandler = start_element
323         p.EndElementHandler = end_element
324         p.CharacterDataHandler = char_data
325         p.ParseFile(f)
326         f.close()

在解析過程中,通過in_element_list 維護着當前所解析的路徑。例如,假設目前解析到 common.xml 下圖紅色箭頭所示的標籤處,
在這裏插入圖片描述
此時的in_element_list 的爲['mavlink','messages','message'],當進一步解析,進入

<field type="uint8_t" name="system_status" enum="MAV_STATE"> System status flag. </field>

此時調用StartElementHandlerstart_element , 並傳入參數name=field, attrs={'type':'uint8_t', 'name':'system_status', 'enum':'MAV_STATE'},
那麼有in_element=mavlink.messages.message.field ,於是會創建一個MAVField 對象添加到messagefields裏面。接着調用 CharacterDataHandlerchar_data 方法,此時傳入data='System status flag.',然後該值會被添加到start_element 所創建的MAVField對象的descritption屬性中。當處理完時,調用EndElementHandlerend_element 方法,從in_element_list 中彈出'field'

3.2.3 對解析後結果的後處理

後處理主要包括:

  1. 對以MAV_CMD 開頭的且參數個數不足 7 的枚舉類型把參數按默認值補足 7 個。
  2. 如果** MAVLink ** 是 2.0 之前的版本則剔除消息 ID 大於 256 的消息
  3. 把消息的域按照大字節在前小字節在後進行重排(對於數組則按照其數據類型的大小,而不是按照其數組大小)。計算數據的總長度。
  4. 依據重排後的域計算crc_extra

最後把這些值放到以message_ 開頭的變量裏面

代碼段 10

# 消息序號, 比如 heartbeat 爲 0
427             key = m.id
# 上面提到的 crc_extra
428             self.message_crcs[key] = m.crc_extra
# 發送出去的消息總長度(包括extension部分)
429             self.message_lengths[key] = m.wire_length
# 不包括 extension 部分的消息的長度
430             self.message_min_lengths[key] = m.wire_min_length
# 消息的名稱,例如 heartbeat 就是消息的名稱
431             self.message_names[key] = m.name
# 消息中是否顯示包含 target_system 和 target_component 的 flag
432             self.message_flags[key] = m.message_flags
# target_system 在消息重排之後的域中的偏移量,無論是否含有 taregt_system 域都默認爲0
433             self.message_target_system_ofs[key] = m.target_system_ofs
# target_system 在消息重排之後在域中的偏離量,無論是否含有target_system 都默認爲0
434             self.message_target_component_ofs[key] = m.target_component_ofs

最後檢查了數據的長度是否超出常見的 radio 的傳輸長度(64)

代碼段 11

439             if m.wire_length+8 > 64:
440                 print("Note: message %s is longer than 64 bytes long (%u bytes), which can cause fragmentation since many radio mod    ems use 64 bytes as maximum air transfer unit." % (m.name, m.wire_length+8))

如下圖所示,這裏的 8 是 PAYLOAD 的以外的字節,wire_length 算出的是PAYLOAD 的長度, 總長度是 8 + wire_lengthMAVLink v1 Frame
但我們注意到這是針對 MAVLink v1 的判斷,對於 MAVLink v2 並不成立(參考下圖)。
在這裏插入圖片描述
在完成XML 數據解析之後就是目標代碼生成,見下面的鏈接。

目標代碼生成

鏈接 >>> pymavlink 源碼剖析(二)之生成代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章