Обработчик протокола обмена данными BlackBox

Базовые идеи языка описания протокола и топологии обмена данными  BlackBox 

Ручное написание кода сериализации и десериализации данных, особенно между гетерогенными устройствами на различных языках программирования процесс трудоемкий,  часто приводящий к сложно устранимым ошибкам. Самым работоспособным решением такой проблемы является создание DSL — языка описания протокола обмена данными, описание, на этом языке протокола обмена, и затем, базируясь на этом описании — генерация кода на необходимых языках программирования. Давно доступны разнообразные решения по данной технологии, вот для примера некоторых из них.

Protocol Buffers
Cap’n Proto
FlatBuffers
ZCM
MAVLink
Thrift

Изучив эти, и многие другие варианты реализации, было принято решение попытаться создать систему, которая воплотит и дополнит всё лучшее, отбросив обнаруженные недостатки.

Язык описания протокола обмена  BlackBox   базируется на конструкциях популярного языка программирования JAVA . Для JAVA существует большое количество удобных, продвинутых сред разработки. Например Android Studio,  вполне достаточно.

В целом система и взаимосвязь компонентов выглядит так

На самом верхнем уровне иерархии, для описания устройств (Host), способных принимать, обрабатывать и отправлять пакеты используется JAVA конструкция class.

С помощью implements описываются языки программирования на которых будет сгенерирован исходный код для устройства. В  BlackBox  поддерживает следующие языки програмирования:

Аннотация Язык
InC C
InCPP C++
InCS C#
InKT Kotlin
InRUST RUST

Host может включать в себя несколько интерфейсов обмена, которые описываются с помощью конструкции interface.

Интерфейс просто объединяет в себе некоторый набор пакетов, описанных в его теле конструкцией class. Конструкция  extends  на интерфейсе позволяет интерфейсу множественно наследовать пакеты от других интерфейсов.

Внутри, класс описания пакета содержит только поля, которые описывают данные передаваемые в пакете и константы которые только хранятся.  Конструкция implements на классе описания пакета, позволяет селективно вставлять пакет в отдельные интерфейсы перечисленные после implements . Конструкция extends на классе описания пакета, позволяет пакету наследовать поля от других пакетов. Имена пакетов должны быть

  • уникальными,
  • не начинаться или зачанчиваться подчёркиваниями _
  • и не совпадать с ключевыми словами всех поддерживаемых языков програмирования

У каждого пакета есть уникальный ID. Который, присваевается аннотацией   @Id( 123 )  . Если при описании протокола, пакету ID не присвоен, система сделает это автоматически во время кодогенерации. Обратите внимание, что кодогенератор возвращает преобразованный исходник описания протокола, в котором расставлены все недостающие ID. Используйте в своей работе именно этот, возвращённый файл описания протокола

 

Существует три типа пакетов

  • пустой пакет — не содержит в себе каких либо данных, однако факт его получения является информационным событием.
  • пакет с предопределенными полями — вне зависимости от выставленных значений полей имеет постоянную длину и жестко заданный набор полей.
  • пакет с НЕ  обязательными полями — в зависимости от заполненности имеет переменный размер, поскольку помимо обязательных полей, содержит поля которые в случае если они не заполнены не передаются.

 

 BlackBox  позволяет описывать константы:
Именованный набор констант с уникальными положительными, целочисленными значениями описываются через поля JAVA конструкции enum.  Уникальные значения ( в пределах enum)  таким константам, присваиваются автоматически.
В случае когда необходимо иметь контроль над целочисленными  значениями констант  их описывают  в виде static final полей  enum с инициализацией необходимыми значениями.
Аннотация @BitFlags на enum позволяет указать, что данные константы является битовыми флагами. Это влияет на генерируемые значения констант и на создаваемый код их обработки. Вместе с этим  BlackBox   предоставляет более удобную замену битовым флагам: битовые поля. enums могут быть использованы как типы данных полей пакета. Описания enum должны находиться корне описания протокола,  вне какого либо контекста.
Обычные, примитивные константы, в том числе имеюшие не целочисленные значения, при необходимости, также могут быть объявлены в виде static final  полей непосредственно в  классе описания пакета.

Аннотации полей пакета вида  @A, @V, @X, @I    хранят метаинформацию о закономерностях изменения значений поля пакета. Располагая этой информацией, кодогенератор, включает алгоритм кодировки Base 128 Varint  (используемый, например, в protocol buffer), что позволяет ощутимо, сжать отправляемые данные с минимальной затратой ресурсов.  Достигается это путем отбрасывания, старших, не заполненных разрядов на передающей стороне, и восстановлением их на стороне принимающей.

На графике показана зависимость количества требуемых байт от величины пересылаемого значения

 

Данные пересылаемые между узлами можно разделить на два типа.

  1. Случайные, имеющие во всём диапазоне равномерно распределенные значения. Шум.

    Такие данные передаются как есть, сжатие или какое либо кодирование этого типа данных — напрасная трата вычислительных ресурсов. Поля с таким типом значений не имеют аннотаций либо маркируются аннотацией   @I   .
  2. Данные, в изменениях которых есть некоторые закономерности, неравномерности или градиенты распределения.

C помощью аннотаций  @A, @V, @X  фиксируют три варианта распределения значений относительно наиболее вероятного.

Редкие флуктуации, возможны только в сторону увеличения, в направлении бОльших значений относительно наиболее вероятного val.
Флуктуации, возможны в обе в стороны относительно наиболее вероятного val.
Флуктуации, возможны только в сторону меньших значений относительно наиболее вероятного val.

Наиболее вероятное значение val передается аргументом к аннотации.

 Примеры использования аннотаций распределения данных

Языковая конструкция

Описание

@I long  field обязательное поле, данные поля перед отправкой не обрабатывается(плохо сжимаемо), и может принимать значения в диапазоне long
@A byte field обязательное поле, данные сжимаются, поле может принимать значения в диапазоне от 0 до 255. Фактически аналогия типу uint8_t в С.
@I (-1000) byte field обязательное поле, не сжимаются, поле может принимать значения в диапазоне от -1128 до -873
@X_ short  field НЕ обязательное поле принимает значения в диапазоне от -32 768 до 32 767 перед отправкой будет сжато.
@A (1000)  short field обязательное поле, поле принимает значения в диапазоне от    1 000 до 66 535 перед отправкой будет сжато
@V_ short field nullable поле принимает значения в диапазоне от    —65 535  до 0 перед отправкой будет сжато
@I(-11 / 75) short field Обязательное поле с равномерно распределенными значениями в заданном диапазоне.

 

 Описание массивов

Для описания массивов используются следующие аннотации

@D При создании массива его параметры фиксируются и сразу отводится всё минимально необходимое пространство под данные.  Используется в тех случаях когда известно, что массив вероятнее всего будет полностью заполнен данными. Даже если данные не вставлены — место под них отводится, но нет дополнительных затрат на отслеживание наполненности данными.
@D_ При создании массива его параметры фиксируются, но место под данные, в зафиксированных пределах, отводится только по мере их поступления. Используется для разреженных массивов,  когда известно, что массив вероятнее всего будет мало заполнен.  Есть дополнительные затраты связанные с отслеживание наполненности данными.

 

 

Языковая конструкция

Описание

@D(1 | 2 | 3)  int  field1; Обязательный многомерный массив с предопределенными измерениями
1 х 2 х 3.
Возвращает примитивы.
@D(1 | 2 | 3) @__(4)  int   field1; Многомерный массив с предопределенными измерениями
1 х 2 x 3
Возвращает массивы предопределенной длины 4.
@D_(1 | -2 | -3)  int  field1; Многомерный массив с предопределенным первым измерением 1 и переменными остальными измерениями.  Место под данные, в пределах максимальных значений измерений, отводится только по мере добавления в массив.
Возвращает примитивы.
@D(1 | 2 |  3) @__(-4) int   field1; Многомерный массив с предопределенными измерениями
1 х 2 x 3
Возвращает массивы раВной, переменной1 до 4) длиной
@D(1 | 2 ) @__(~3)  int  field1; Многомерный массив с предопределенными измерениями
1 х 2
Возвращает массивы раЗной, переменной1 до 3) длиной
@A(337) String field Возвращает строку — максимум 337 двухбайтовых символов.
Аннотация типа @V делает строку однобайтной
@V String [] field Возвращает содержимое строки — массив однобайтовых символов. Максимальная  длина строк по умолчанию до 127 символов.
@X_(3 / 45) @__(12) byte [] field НЕОбязательное поле возвращает массив предопределенной длины 12. Значения которого в заданном диапазоне, с неравномерным распределением в обе стороны относительно середины диапазона.
@__(-45) int  field НЕОбязательное поле.
Возвращает массив длинной от 1 до 45
@A @D1 | 2 | 3 ) byte field Обязательное поле многомерный массив с предопределенными измерениями
1 х 2 х 3.

Возвращает примитивы с неравномерным распределением значений вверх.
@A_ @D1 | 2 | 3 ) byte field НЕОбязательное поле многомерный массив с предопределенными измерениями
1 х 2 х 3.
 При создании массива отводится всё необходимое пространство.
Возвращает примитивы с неравномерным распределением значений вверх.
@A_ @D_1 | 2 | 3 ) byte field НЕОбязательное поле многомерный массив с предопределенными измерениями
1 х 2 х 3.
 При создании массива фиксируются его параметры, а необходимое пространство под данные отводится по мере заполнения.
Возвращает примитивы с неравномерным распределением значений вверх!
@B) byte field Обязательное битовое поле. Длина поля 3 бита
@B_12 | 67 ) byte field НЕОбязательное битовое поле. Длина поля в битах будет вычислена на основе переданного диапазона допустимых значений.

 

 

 

 

 

 

 

 

 

Помимо полей с массивами примитивов   BlackBox   поддерживает возможность хранить в поле другие объекты(пакеты),  а также массивы объектов. Поддерживаются вложенные объекты необходимой глубины, кроме циклических, бесконечных вложенностей. Для этого в качестве типа данных необходимо указать описанный в спецификации другой пакет

Помимо оптимизации трафика,  BlackBox  позволяет учитывать топологию, качество каналов передачи данных и генерировать соответствующий исходный код.
Для описания каналов обмена между хостами используется конструкция class. которая должна находится в корне, рядом с описанием устройств, и содержать в себе:

  1. тип протокола обмена
  2. список интерфейсов, которыми хосты взаимодействуют по данному каналу.

  • Канал с протоколом типа StdProtocol, передает только непосредственно данные пакета. Такой тип допустимо использовать для передачи поверх уже имеющих встроенную защиту от ошибок протоколов типа TCP/IP, либо локальных, малошумных, качественных каналов типа синхронных SPI/I2C шин, либо для обычной выгрузки в файл данных из памяти, для последующего восстановления.
  • Для ненадежных каналов с высокой вероятностью ошибок, к примеру UART по радиоканалу, используется улучшенный, помехозащищенный вариант протокол типа AdvProtocol.  Он содержит в себе алгоритм обнаружения ошибок CRC и Byte (0x55) stuffing framing для быстрого восстановления в случие сбоя.

В целом файл описания выглядит так

в качастве шаблона можно скачать LedBlinkProject.java .