文章目錄
相關:
1 引言
pymavlink 是 MAVLink 協議的 Python 實現,同時它還是一個 MAVLink 協議代碼實現的自動生成工具,目前支持的語言有 C
、C++11
、Python
、Java
、Javascript
、Typescript
、C#
、wlua
、Obj-C
。 本文是 pymavlink 源碼剖析 文章的第一篇,內容可以分爲兩部分:一是概述了 pymavlink 的實現代碼自動生成的基本流程;二是仔細描述了 pymavlink 的XML 文件的數據解析流程,而本篇的題目即以此命名。
如果對 MAVLink 協議還不太熟悉請參考文章目錄下方 “相關” 裏面給出的鏈接。
2 pymavlink 的代碼自動生成方法
pymavlink
的代碼 clone 下來之後的文件目錄結構如下
由 MAVLink 的官方文檔可以知道 Tools/mavgen.py
是pymavlink 代碼生成的入口程序。其內容如下
#!/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
包含了自動生成代碼所需的主要文件,其文件夾結構如下:
可以看到在文件夾下有以 mavgen_
開頭後面跟着編程語言名稱的幾個 .py
文件、XML結構定義文件mavschema.xsd
、幾個以編程語言名稱命名的文件夾,以及一些其他文件。 mavgen.py
包含了入口函數 mavgen
,其主要執行以下幾個項內容:
XML
文件預處理: 檢查輸入的XML
文件是否有效- 解析
XML
的數據,同時把<include>
標籤引入的XML
文件添加到待處理列表裏 - 生成指定編程語言的 MAVLink 協議代碼
下面對每一部分分別進行介紹。
3 XML 文件的數據解析
3.1 XML 文件預處理
在mavgen
這個函數裏,定義了函數 mavgen_validate
完成了上一節中提到的 XML
文件的有效性校驗。mavgen_validate
函數的內容如下:
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
類的代碼框架如下
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__
的步驟可以分爲如下幾個步驟:
- 依據協議版本初始化一些版本特徵變量,包括協議標誌位,是否對數據幀進行排序,是否有校驗等。
- 定義
start_element
、end_element
、char_data
分別作爲xml.parsers.expat
模塊中的XMLParserType
對象的StartElementHandler
,EndElementHandler
,CharacterDataHandler
的實現。然後開始對filename
指定的文件進行解析。 - 對解析完的數據進行後處理,包括對 payload 排序,計算
crc_extra
等。
這裏我們看下調用MAVXML
構造函數後的對象的屬性列表
可以看到MAVXML
中從 XML 文件中解析出了很多信息,下面具體地描述解析過程。
3.2.1 依據協議版本初始化一些版本特徵變量
首先是獲得當前處理 XML 文件名及版本號,及初始化message
、 enum
和include
屬性爲空,並設置了parse_time
屬性爲當前的日期。
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_extracrc_struct
: 是否把結構體包括在crc
的計算中command_24bit
: message id 的範圍是否允許超過256allow_extension
: 是否允許message 消息中存在擴展域
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 文件
這裏先看一下 Python 的 xml.parsers.expat
模塊的官方文檔中給出的例子
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
的定義:
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
的定義
298 def end_element(name):
299 """"""
300 in_element_list.pop()
下面是標籤內文本處理函數的定義,
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
的解析函數的調用:
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>
此時調用StartElementHandler
即 start_element
, 並傳入參數name=field, attrs={'type':'uint8_t', 'name':'system_status', 'enum':'MAV_STATE'}
,
那麼有in_element=mavlink.messages.message.field
,於是會創建一個MAVField
對象添加到message
的fields
裏面。接着調用 CharacterDataHandler
即 char_data
方法,此時傳入data='System status flag.'
,然後該值會被添加到start_element
所創建的MAVField
對象的descritption
屬性中。當處理完時,調用EndElementHandler
即 end_element
方法,從in_element_list
中彈出'field'
。
3.2.3 對解析後結果的後處理
後處理主要包括:
- 對以
MAV_CMD
開頭的且參數個數不足 7 的枚舉類型把參數按默認值補足 7 個。 - 如果** MAVLink ** 是 2.0 之前的版本則剔除消息 ID 大於 256 的消息
- 把消息的域按照大字節在前小字節在後進行重排(對於數組則按照其數據類型的大小,而不是按照其數組大小)。計算數據的總長度。
- 依據重排後的域計算
crc_extra
最後把這些值放到以message_
開頭的變量裏面
# 消息序號, 比如 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)
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_length
。
但我們注意到這是針對 MAVLink v1 的判斷,對於 MAVLink v2 並不成立(參考下圖)。
在完成XML 數據解析之後就是目標代碼生成,見下面的鏈接。
目標代碼生成
鏈接 >>> pymavlink 源碼剖析(二)之生成代碼