功能模块:木鸢通讯协议 - Ligcox/BTP_DM GitHub Wiki
前言
木鸢通讯协议是一个针对RoboMaster赛场的机器人通讯协议,在木鸢通讯协议概述一节,我们简单介绍了木鸢通讯协议基本数据帧和用法。在木鸢通讯协议用户手册中,我们详尽地介绍如何开始使用木鸢通讯协议。这一章,我们着重围绕木鸢通讯协议的设计和实现展开说明。
数据帧类型定义
木鸢通讯协议基本的数据帧在BTPDM中通过OrderedDict实现。当前机器人状态保留在connection对象的STATUS属性中。数据帧包含了"HEAD"、"D_ADDR"、"ID"、"LEN"、"DATA"、"SUM_CHECK"、"ADD_CHECK"六个字段,分别对应BCP中的数据帧字段,其中,DATA字段是一个bytearray类型的变量。
- 通俗地说OrderedDict是python中的一个有序字典。
- 在python3.6后,dict类型是默认是有序的。
- 为了保证各版本python解释器的通用性,BTPDM的实现中依旧保留了OrderedDict作为BCP数据帧定义。
BCP运行方式
木鸢通讯协议的BTPDM实现位于src/connection.py下
USART对象
BCP使用pyserial处理串口设备及相关请求。该模块封装了串口的访问。它为在Windows、OSX、Linux、BSD(可能是任何POSIX兼容系统)和IronPython上运行的Python提供后端。使用“serial”的模块会自动选择合适的设备。
在connection类的构造函数实现了对串口对象及相关线程的初始化。包含了
- self.device: 串口对象
- self.tx_thread: 串口发送线程
- self.rx_thread: 串口接收线程
robot类对象
在BTPDM运行时(更准确地说是当scheduler确定时),BCP数据的前两个字段(即HEAD和D_ADDR)就已经被确定下来,后续发送的所有BCP数据帧的前两个部分不再被更改。
而各个机器人发送的数据帧在某种程度上是相似的,但又有一定的差异。因此,公共部分的数据发送,例如云台数据方法,直接在Robot基类中实现;而该机器人特有的方法,如哨兵机器人的底盘控制,可以在Robot类的派生类Sentry中实现。
以云台任务的BCP数据帧为例,gimbal方法的实现为:
def gimbal(self, yaw_angle, pitch_angle):
'''
@brief: 控制云台偏转
'''
self.setID("gimbal")
self.setDATA("hh", (int(yaw_angle*1000), int(pitch_angle*1000)))
self.conn.send(self.getInfo())
可以看到,gimbal方法所做的事情为:设置该BCP数据帧的ID为gimbal所对应的数据(即0x02);根据BCP,云台数据的yaw和pitch会将放大1000倍取整,设置为该帧BCP数据的DATA位,在执行self.setDATA
方法时,整个数据包的全部内容(包括和校验和附加校验,计算过程在self.setDATA
调用)全部确定;然后调用该帧数据的getInfo
方法返回十六进制的bytearrays,再将结果返回给connection
对象的send
函数写入串口对象。
而将哨兵机器人上云台对象sentry_up的云台向yaw正方向偏移1.5°,pitch正方向偏移1.5°可以调用
sentry_up.gimbal(1.5, 1.5)
connection对象会将bytearray(b'\xff\x02\x02\x02\xdc\x05\xdc\x05\xc7X')
写入的串口对象。
BCP模块间的关系
实例化后的
Connection
对象作为类属性被传入到Robot
对象中,Robot
的所有操作都是基于对Connection
类方法的调用实现的。简单的说,Connection
负责处理设备的读写,Robot
及其派生类负责连接决策数据和具体的BCP实现。
BCP的BTPDM实现主要包括的Connection
和Robot
两个类,其中Connection
主要处理串口相关的请求,Robot
及其派生类主要包含了具体机器人所需要的功能帧。
BCP各个模块的耦合关系如下:
Connection
类
USART数据接收
在BTPDM开始运行时,程序会启用self.rx_thread
线程,线程重复调用rx_function
方法。rx_function
会当前串口设备中的_全部_信息读取并逐个遍历尝试组成BCP数据包。若无法组成BCP数据包,会直接将数据丢弃;若成功组成BCP数据包,则调用bcpAnalysis
方法,将数据更新到robot
对象的STATUS
变量的相应位置。
USART数据发送
在BTPDM开始运行时,程序会启用self.tx_thread
线程,线程重复调用tx_function
方法。当需要写入数据到USART设备中时,程序会首先将需要写入的数据存入tx_queue
中,tx_queue
是一个数据缓存区,这样的目的是为了防止短时重复写入不同大量数据对程序的阻塞。当tx_function
线程发现tx_queue
中的数据非空时,或逐个遍历tx_queue
的元素,将其写入USART设备中。
Robot
及其派生类
Robot
及其派生类是由SerialInfo
派生而来。SerialInfo
类仅实现了一个返回bytearray类型数据的getInfo
方法。Robot
及其派生类仅需要在内部直接调用该方法即可返回数据写入到USART设备中。
在Robot
及其派生类中,除了实现各类功能帧外,还实现了心跳帧和设备故障帧。在每次主线程进入和关键设备读取信息时,都会调用心跳帧和设备故障帧。这样做的目的当BTPDM发生离线或异常时,能够快速的发现问题。
数据和BCP数据帧的转化
- BCP数据帧的转化在/Sample/BCPSample.ipynb提供了基本的演示样例
- 木鸢通讯协议借鉴了大量匿名通讯协议的方法,在设计过程中也可能存在缺陷,欢迎通过issues和email的方式交流和讨论
在木鸢通讯协议概述一节,我们已经介绍过,BCP数据的本质实际上一串十六进制的字节流。那么使用像串口对象中写入的是b'\xff\x02\x02\x02\xdc\x05\xdc\x05\xc7X'
就不显得奇怪了。但是数据和BCP数据帧之间的对应关系是什么,仍然是接下来我们需要讨论的关键。
从数字到十六进制数
一个十六进制位能够表示0~255之间任意的一个整数值,在USART设备链路中,这些数字会变成高低电平传输。python作为一个弱类型的语言,变量的内存空间由解释器自行分配。那么十六进制传输的过程中就会带来一系列的问题:一个数据究竟需要多少位(例如1应该被表示为0x01或者是0x00 0x01)来保存?超出255的数字应该如何表示?
如果熟悉偏底层的语言,比如C/C++,这些问题会迎刃而解。例如定义变量int a = 1;
,变量a
会以占用4个字节(具体依据编译器实现的)的int类型数据0x01 0x00 0x00 0x01保存在内存中;定义变量uint8_t a = 1;
,变量a
会以占用1字节的u8类型数据0x01保存在内存中。如果能实现特定类型的数据和C/C++中数据类型的对应关系,就能够将一个数据转换为16进制数据在USART链路中传输。
实例分析
在python中提供了标准库struct模块,该模块执行Python值和表示为Python bytes对象的C结构之间的转换。在实现具体数据转化过程中,只需要指定固定格式化字符,即可完成Python到C数据结构的转化。
在上述的字节流b'\xff\x02\x02\x02\xdc\x05\xdc\x05\xc7X'
中0xff
代表帧头为FF,0x02
代表哨兵机器人上云台ID为02,0x02
代表云台功能码为0x02,0x02
代表该帧数据长度为2,\xc7X
代表和校验和附加校验。
在BCP中,指定所有的数据帧为小段模式,即高字节在后,低字节在前。云台决策数据1.5放大1000倍数字1500对应的十六进制值为0x05dc,该数据转化为C的数据short
类型,占用2个字节长度。因此将数据表示为\xdc\x05
。在该帧的DATA位的\xdc\x05\xdc\x05
表示云台yaw和pitch数据为1500。
在实现过程中setDATA("hh", (int(yaw_angle*1000), int(pitch_angle*1000)))
方法,参数<hh
(隐式地添加了<
)代表传入的数据需要被转换为short类型,后面为需要转化的数据格式
取整还是保留小数?
在具体实现的过程中,可能注意到,云台数据并没有直接发送,而是对数据放大1000倍后再取整发送。我们不妨尝试对比两种做法。
同样对数字1.57使用ee
和hh
格式化为2字节的浮点数和整数
struct.pack("<ee", *[1.59, 1.59])
struct.pack("<hh", *[1500, 1500])
结果为b'\\>\\>'
和\xdc\x05\xdc\x05
不论编译器中使用浮点数的具体实现,对于'f','d'和'e'格式化后的代码,分别使用IEEE 754中binary32,binary64或binary16格式。而IEEE 754 binary16“半精度”类型是在2008年IEEE 754修订版中引入的。它有一个符号位、一个5位整数位和11位精度(显式存储10位),并且可以表示近似6.1e-05和6.5e+04精度之间的数字。C编译器并不广泛支持这种类型:在典型机器上,无符号短整型可用于存储,但不能用于数学运算。
简单地说,由于浮点数的表示需要在内存中保留数字得整数部分和小数部分,这不可避免地需要更多的内存空间。此外,由于不确定尾数、C编译器支持等等一系列的原因,在相同的数据量下,直接发送浮点数的_效率_是显著小于整数的。因此,直接进行浮点数传输不是一个明智的选择。
在BCP的实现中,需要传输浮点数的部分,在保留一定精度的前提下,均使用了放缩后传输的方式。在BCP的接收设备上在根据BCP的定义反算出对应数据。
需要注意的是,对数据的范围做成限制是必要的。放缩(特别是放大)后的数据可能超出保存数据所需要的范围。这可能导致struct提示一个错误,进而导致该BCP数据帧丢失。