pymavlink 是 MAVLink 协议的 Python 实现,同时它还是一个 MAVLink 协议代码实现的自动生成工具,目前支持的语言有 C
、C++11
、Python
、Java
、Javascript
、Typescript
、C#
、wlua
、Obj-C
。 本文是 pymavlink 源码剖析 文章的第一篇,内容可以分为两部分:一是概述了 pymavlink 的实现代码自动生成的基本流程;二是仔细描述了 pymavlink 的XML 文件的数据解析流程,而本篇的题目即以此命名。
如果对 MAVLink 协议还不太熟悉请参考文章目录下方 “相关” 里面给出的链接。
pymavlink
的代码 clone 下来之后的文件目录结构如下
由 MAVLink 的官方文档可以知道 Tools/mavgen.py
是pymavlink 代码生成的入口程序。其内容如下
代码段1
#!/usr/bin/env python'''
parse a MAVLink protocol XML file and generate a python implementationCopyright 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, pathsys.path.insert(0, path.dirname(path.dirname(path.dirname(path.abspath(__file__)))))from pymavlink.generator import mavgen
from pymavlink.generator import mavparsefrom argparse import ArgumentParserparser = 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)
py是什么文件、由其内容可以看出它实际上是对
generator/mavgen.py
的简单调用, 所以这里主要关注的是 generator/mavgen.py
的内容,mavgen.py
所在文件夹 generator
包含了自动生成代码所需的主要文件,其文件夹结构如下:
可以看到在文件夹下有以 mavgen_
开头后面跟着编程语言名称的几个 .py
文件、XML结构定义文件mavschema.xsd
、几个以编程语言名称命名的文件夹,以及一些其他文件。 mavgen.py
包含了入口函数 mavgen
,其主要执行以下几个项内容:
XML
文件预处理: 检查输入的XML
文件是否有效XML
的数据,同时把<include>
标签引入的XML
文件添加到待处理列表里下面对每一部分分别进行介绍。
在mavgen
这个函数里,定义了内部函数 mavgen_validate
用来完成了上一节中提到的 XML
文件的有效性校验。mavgen_validate
函数的内容如下:
代码段 2
def mavgen_validate(xmlfile):"""Uses lxml to validate an XML file. We define mavgen_validatehere because it relies on the XML libs that were loaded in mavgen(), so it can't be called standalone"""xmlvalid = Truetry: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 = Falsereturn xmlvalidexcept etree.XMLSchemaError:return Falseexcept etree.DocumentInvalid as err:sys.exit('ERROR: %s' % str(err.error_log))return True
这段代码主要对XML
的有效性做了三个方面的检验:
(1)是否是有效的XML
格式的文件
(2)是否符合 mavschema.xsd
文件的定义
(3)是否在标签的属性里含有不允许的字符。这里的不允许字符串主要是用来检查是否和目标语言的关键字冲突。
解析XML
的调用的是 mavparse.py
中定义的 MAVXML
类,
142 xml.append(mavparse.MAVXML(fname, opts.wire_protocol))
Xml文件?下面分析 MAVXML
类。MAVXML
类的代码框架如下
代码段 3
class MAVXML(object):'''parse a mavlink XML file'''def __init__(self, filename, wire_protocol_version=PROTOCOL_0_9):#initial codedef __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
指定的文件进行解析。crc_extra
等。这里我们看下调用MAVXML
构造函数后的对象的属性列表
可以看到MAVXML
中从 XML 文件中解析出了很多信息,下面具体地描述解析过程。
首先是获得当前处理 XML 文件名及版本号,及初始化message
、 enum
和include
属性为空,并设置了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
说出三种解析XML文档的方式、然后依据协议的版本,设置一些 flag,分别是:
protocol_marker
: 标志消息开始的字节sort_fields
:是否对payload 域进行排序little_endian
:字符的是否按最小字节先发送的传输顺序crc_extra
: 是否在checksum
的计算中加入 crc_extracrc_struct
: 是否把结构体包括在crc
的计算中command_24bit
: message id 的范围是否允许超过256allow_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)
这里先看一下 Python 的 xml.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_datap.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)
如何在XML中调用资源文件?下面是上面所定义的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>
此时调用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'
。
后处理主要包括:
MAV_CMD
开头的且参数个数不足 7 的枚举类型把参数按默认值补足 7 个。crc_extra
netty源码剖析,最后把这些值放到以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_length
。
但我们注意到这是针对 MAVLink v1 的判断,对于 MAVLink v2 并不成立(参考下图),红色加绿色的部分的长度明显大于 8 个字节。
在完成XML 数据解析之后就是目标代码生成。
最后一步,根据 opts.language
所指定的语言生成目标代码
164 opts.language = opts.language.lower()
165 if opts.language == 'python':
166 from . import mavgen_python
167 mavgen_python.generate(opts.output, xml)
168 elif opts.language == 'c':
169 from . import mavgen_c
170 mavgen_c.generate(opts.output, xml)
171 elif opts.language == 'wlua':
172 from . import mavgen_wlua
173 mavgen_wlua.generate(opts.output, xml)
174 elif opts.language == 'cs':
175 from . import mavgen_cs
176 mavgen_cs.generate(opts.output, xml)
177 elif opts.language == 'javascript':
178 from . import mavgen_javascript
179 mavgen_javascript.generate(opts.output, xml)
180 elif opts.language == 'typescript':
181 from . import mavgen_typescript
182 mavgen_typescript.generate(opts.output, xml)
183 elif opts.language == 'objc':
184 from . import mavgen_objc
185 mavgen_objc.generate(opts.output, xml)
186 elif opts.language == 'swift':
187 from . import mavgen_swift
188 mavgen_swift.generate(opts.output, xml)
189 elif opts.language == 'java':
190 from . import mavgen_java
191 mavgen_java.generate(opts.output, xml)
192 elif opts.language == 'c++11':
193 from . import mavgen_cpp11
194 mavgen_cpp11.generate(opts.output, xml)
195 else:
196 print("Unsupported language %s" % opts.language)
pyi文件、这里我们看一下变量 opts
和 xml
的内容
opts
:
definitions: ['/mnt/d/github_/mavl...ommon.xml']
error_limit: 200
language: 'c'
output: '/mnt/d/github_/mavlink_cpp'
strict_units: False
validate: False
wire_protocol: '2.0'
xml
:
[<pymavlink.generator.mavparse.MAVXML object at 0x7f5a30ec1fd0>]
把xml
列表里面唯一一个MAVXML object
展开后有
allow_extensions: True
basename: 'common'
basename_upper: 'COMMON'
command_24bit: True
crc_extra: True
crc_struct: True
enum: [<pymavlink.generator...a30ed02b0>, <pymavlink.generator...a30ed06d8>, <pymavlink.generator...a30edf208>, <pymavlink.generator...a30edf278>, <pymavlink.generator...a30edf470>, <pymavlink.generator...a30edf898>, <pymavlink.generator...a30edfa90>, <pymavlink.generator...a30edfba8>, <pymavlink.generator...a30edfc18>, <pymavlink.generator...a30ee50b8>, <pymavlink.generator...a30ef0be0>, <pymavlink.generator...a30efb3c8>, <pymavlink.generator...a30efb630>, <pymavlink.generator...a30efba58>, ...]
filename: '/mnt/d/github_/mavlink/message_definitions/v1.0/common.xml'
include: []
largest_payload: 255
little_endian: True
message: [<pymavlink.generator...a30058dd8>, <pymavlink.generator...a3005e2e8>, <pymavlink.generator...a3005e9b0>, <pymavlink.generator...a3005eb00>, <pymavlink.generator...a3005edd8>, <pymavlink.generator...a30063208>, <pymavlink.generator...a300634a8>, <pymavlink.generator...a30063630>, <pymavlink.generator...a30063cf8>, <pymavlink.generator...a30063eb8>, <pymavlink.generator...a300671d0>, <pymavlink.generator...a30067358>, <pymavlink.generator...a300675f8>, <pymavlink.generator...a30067978>, ...]
message_crcs: {0: 50, 1: 124, 2: 137, 4: 237, 5: 217, 6: 104, 7: 119, 8: 117, 11: 89, 20: 214, 21: 159, 22: 220, 23: 168, 24: 24, ...}
message_flags: {0: 0, 1: 0, 2: 0, 4: 3, 5: 1, 6: 0, 7: 0, 8: 0, 11: 1, 20: 3, 21: 3, 22: 0, 23: 3, 24: 0, ...}
message_lengths: {0: 9, 1: 31, 2: 12, 4: 14, 5: 28, 6: 3, 7: 32, 8: 36, 11: 6, 20: 20, 21: 2, 22: 25, 23: 23, 24: 52, ...}
message_min_lengths: {0: 9, 1: 31, 2: 12, 4: 14, 5: 28, 6: 3, 7: 32, 8: 36, 11: 6, 20: 20, 21: 2, 22: 25, 23: 23, 24: 30, ...}
message_names: {0: 'HEARTBEAT', 1: 'SYS_STATUS', 2: 'SYSTEM_TIME', 4: 'PING', 5: 'CHANGE_OPERATOR_CONTROL', 6: 'CHANGE_OPERATOR_CONTROL_ACK', 7: 'AUTH_KEY', 8: 'LINK_NODE_STATUS', 11: 'SET_MODE', 20: 'PARAM_REQUEST_READ', 21: 'PARAM_REQUEST_LIST', 22: 'PARAM_VALUE', 23: 'PARAM_SET', 24: 'GPS_RAW_INT', ...}
message_target_component_ofs: {0: 0, 1: 0, 2: 0, 4: 13, 5: 0, 6: 0, 7: 0, 8: 0, 11: 0, 20: 3, 21: 1, 22: 0, 23: 5, 24: 0, ...}
message_target_system_ofs: {0: 0, 1: 0, 2: 0, 4: 12, 5: 0, 6: 0, 7: 0, 8: 0, 11: 4, 20: 2, 21: 0, 22: 0, 23: 4, 24: 0, ...}
parse_time: 'Fri Mar 27 2020'
protocol_marker: 253
sort_fields: True
version: 3
wire_protocol_version: '2.0'
__len__: 1
具体的生成过程见:
链接 >>> pymavlink 源码剖析(二)之生成代码
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态