BlackBox 的基本思想 .
手动编写序列化和数据反序列化代码,尤其是不同编程语言的异构设备之间的编写是一个耗时的过程,通常会导致难以消除的错误。解决这个问题最有效的方法是创建一个 DSL (通过它正式描述协议),然后创建一个程序,以所需的编程语言为基础,为各种目标平台生成基于此描述的源代码。以这种方式准备好的解决方案的例子有很多
Protocol Buffers
Cap’n Proto
FlatBuffers
ZCM
MAVLink
Thrift
在研究了这些以及其他许多实施方法之后,我决定创建一个系统来实施和补充优点,消除已发现的缺点。
描述 BlackBox 交换协议的语言 是基于流行编程语言JAVA的结构。JAVA拥有大量方便的IDE。比如 Android Studio 。
一般来说,系统和组件的互连如下所示:
在层次结构的最高层 ,可以接收,处理和发送数据包的设备( 主机)被描述为JAVA class 构造。
1 2 3 4 5 6 7 8 |
class STM32 implements InC { } class I9100 implements InJAVA, InC { } class PC implements InC { } |
在implements语句中,您正在请求您想要生成源代码的编程语言。
该主机 可以包括由来描述一些交换接口 interface 声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class STM32 implements InC { interface Security extends I9100.TokenExchange {} } class I9100 implements InJAVA, InC { interface TokenExchange extends PC.Common {} } class PC implements InC { interface ToSTM32 extends PowerControl, Common {} interface ToI9100 extends PowerControl {} interface PowerControl {} interface Common extends PowerControl {} } |
该接口组合了一组包(由class 构造描述 )。接口 extends 语句允许接口从其他接口继承多个数据包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class STM32 implements InC { interface Security extends I9100.TokenExchange { @id(7) class AUTH_KEY { @I(0 / 32) String key; } @id(4) class PING { @A_ long time_usec; @A_(-24) int seq; @X byte target_system; @V byte target_component; } } } |
软件包描述类只包含 描述软件包传输数据的字段。包描述类中的implements结构允许有选择地将包插入到implements语句后列出的接口中 。 包描述类的 extends语句允许包继承其他包的字段。包名称必须是唯一的。
的 黑盒子 允许以描述常量:
命名 具有独特的常数的组 的整数值 通过的领域中描述 enum构建体。 自动分配这些常量的唯一值(在 enum 内)。
在需要控制常量整数值的情况下,它们以 enum static final 字段的形式描述,并通过所需的值进行初始化。在 enum 上注解 @BitFlags
允许指定这些常量是位标志。这会影响生成的常量值以及为其处理生成的代码。
与此同时, BlackBox 为比特标志提供了一个更方便的替换:比特字段。
enum可以用作包字段的参考数据类型。该 enum 应在描述文件的根被声明,任何上下文之外。
如果需要的话,普通个体常数(包括非整数值)可以直接在包声明类中声明为static final字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
class I9100 implements InJAVA { interface Main { class HeartBeat { int beat; boolean led_state; AutoEnumeratedConstants field_auto_constant; static final byte on = 1, // predefined constants within pack off = 0; static final double double1 = 1.0, // predefined constants within pack double2 = 0.5; static final String string_const1 = "string_const1", string_const2 = "string_const2"; } } } enum AutoEnumeratedConstants {// constant set with auto enumerated unique values unique_auto_const1, unique_auto_const2, unique_auto_const3; static final int //predefined constants mask_const1 = 0x80, mask_const2 = 0x04; static final byte //predefined constants const1 = 1, const2 = 2, const3 = 3; } @BitFlags enum OnlyStaticConstants { ; static final int // predefined constants ONLY mask_const1 = 0x80, mask_const2 = 0x04; } |
注意!尽管JAVA中的源代码生成元素 被转换为带注释的基元,名为SlimEnum。为了方便使用它们,已经为IntelliJ IDE创建了插件SlimEnum。不要忘记从IntelliJ插件库安装它。
包@A,@V,@X,@I 字段的注释 存储有关更改字段值模式的元信息。基于此注释信息代码生成器会引发Base 128 Varint算法 (例如,在协议缓冲区中使用),该算法允许有限且有限的负载压缩发送数据。这是通过排除发送端较旧的未填充位并在接收端恢复它们来实现的。
该图显示了字节数与传输值的关系
节点之间发送的数据可以分为两种类型。
- 随机值,均匀分布在所有范围内。 几乎是一个噪音。
这样的数据按原样传输,压缩或这种类型数据的任何编码都是计算资源的浪费。具有该类型值的字段没有任何注释或由 @I描述 - 数据在值的变化中具有某种模式/梯度。
这些字段用 @A,@V,@X注释 表示相对于最可能的值分布的三种变体。
![]() |
只有在相对于最可能的价值val而言较大值的方向上才有可能出现罕见波动。 |
相对于最可能的价值val,在两个方向都可能出现罕见的波动。 | |
只有在相对于最可能值val的较小值的方向上才可能出现波动。 |
最可能的值 – val 作为参数传递给注释。
语言构造 |
描述 |
@I byte field | 必填字段,发送前的字段数据不会被编码(压缩性较差),并且可以取值范围从 -128 到 127 |
@A byte field | 必填字段,数据被压缩,字段可以取值范围从 0 到 255 。实际上,这 与 C uint8_t 类型是类比的 。 |
@I (-1000) byte field | 必填字段(不要压缩),字段可以取值范围从 -1128 到 -873 |
@X_ short field | 的 可为空的(可选) 字段取值的范围在 从 -32 768 到 32 767。 将在发送时被压缩。 |
@A (1000) short field | 可空(可选) 字段取值在 – 65,535 到 0之间 将在发送时被压缩。 |
@V_ short field | 可空字段取-65 535 到 0 之间的值 将在发送时被压缩。 |
@I(-11 / 75) short field | 在指定范围内均匀分布值的必需字段。 |
带数组的字段说明
以下注释用于描述数组
@D | 此注释类型表示具有预定义维度的数组,并预先分配所有数据空间。在已知数组最有可能完全填充数据的情况下使用。即使数据没有设置 – 分配空间,但没有资源浪费跟踪数据的完整性。 |
@D_ | 此注释类型表示具有预定义参数的数组,但数据空间已设置在预定义范围内,仅在插入数据时分配。用于 稀疏 数组,当知道该数组最可能填充不足时。跟踪数据的完整性会产生额外的成本。 |
语言构造 |
描述 |
@D(1 | 2 | 3) int field1; | 预定义维度为 1 x 2 x 3的强制多维数组。 返回基元。 |
@D(1 | 2 | 3) int [] field1; | 具有预定义维度的多维数组 1 x 2 返回预定义 长度为 3的数组。 |
@D(1 | 2 | -3) int [] field1; | 具有预定义维度 1 x 2的多维数组 返回等长,可变( 1 到 3 )长度的数组 |
@D(1 | 2 | ~3) int [] field1; | 具有预定义维度的多维数组 1 x 2 返回不同 变量( 1 到 3 )长度的数组 |
@A @D( 1 | 2 | 3 ) byte field | 具有预定义维度 1 x 2 x 3的必需字段多维数组。 返回值不均匀分布向上的基元。 |
@A_ @D( 1 | 2 | 3 ) byte field | 可选字段是预定义维度为1 x 2 x 3的多维数组 。 创建数组时,会分配所有必需的空间。 向上返回值不等分布的基元。 |
@A(337) String field | 返回一个字符串,每个 字符的最大长度为337个2字节。 (注释 @V 表示一个字符串 1字节的字符串) |
@V String [] field | 返回字符串的内容 – 一个单字节字符数组。行的最大长度最多为127个字符。 |
@X_(3 / 45) @D( 12) byte [] field | 可选字段返回一个预定义长度为 12 的数组。数组的值在给定的范围内,相对于范围的中间在两个方向上分布不均匀。 |
@D(-45) int [] field | 可选字段。 返回从1 到 45的长度数组 |
@B( 3 ) byte field | 强制位字段。字段长度3位 |
@B_( 12 | 67 ) byte field | 可选位域。以位为单位的字段长度将根据传输的允许值范围计算。 |
@D_(1 | -2 | -3) int field1; | 具有预定第一维度1 而其他维度可变的多维阵列 。数据的位置在维度的最大值内,仅在添加到数组时被分配。 返回原语。 |
除了优化流量, BlackBox 还 允许您考虑数据传输通道的拓扑结构和质量,并生成相应的源代码。主机之间的通信通道通过顶层类 声明进行描述 。它包含:
- 交换协议的类型
- 设备通过给定通道交互的两个连接接口的列表。
1 2 3 4 5 6 7 |
class PC_STM32_Channel extends SimpleProtocol PC.ToSTM32, STM32.Security {} class PC_I9100_Channel extends AdvancedProtocol implements PC.ToI9100, I9100.TokenExchange {} class STM32 implements InC {...} class I9100 implements InJAVA, InC {....} |
- 所述 SimpleProtocol协议类型意味着直接而没有任何变换发送的分组数据。此类型可用于通过传输协议传输具有内置错误保护(如 TCP / IP ) 或本地,低噪声,高质量通道(如同步 SPI / I2C 总线)的数据,或用于从存储器到内存的简单转储数据该文件,以供以后恢复。
- 对于不可靠的,具有高错误概率的噪声信道,如 无线电上的UART ,使用改进的AdvancedProtocol 协议的噪声保护版本 。 它使用 CRC16和Byte(0x55)填充帧进行错误后的快速恢复。
您可以下载 LedBlinkProject.java 并将其用作模板。