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

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

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

Protocol Buffers
Cap’n Proto
FlatBuffers
ZCM
MAVLink
Thrift

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

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

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

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

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

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

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

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

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

Внимание! Именованные константы при кодогенерации в JAVA преобразуются в аннотированные примитивы SlimEnum. Для удобной работы с ними в IDE Intellij , был создан плагин SlimEnum, не забудьте установить.

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

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

 

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

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

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

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

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

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

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

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

Описание

@I byte field обязательное поле, данные поля перед отправкой не кодируется (плохо сжимаемо), и может принимать значения в диапазоне от -128 до 127
@A byte field обязательное поле, данные сжимаются, поле может принимать значения в диапазоне от 0 до 255. Фактически аналогия типу uint8_t в С.
@I (-1000) byte field обязательное поле, не сжимаются, поле может принимать значения в диапазоне от -1128 до -873
@X_ short  field nullable поле принимает значения в диапазоне от -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)  int []  field1; Многомерный массив с предопределенными измерениями
1 х 2
Возвращает массивы предопределенной длины 3.
@D_(1 | -2 | -3)  int  field1; Многомерный массив с предопределенным первым измерением 1 и переменными остальными измерениями.  Место под данные, в пределах максимальных значений измерений, отводится только по мере добавления в массив.
Возвращает примитивы.
@D(1 | 2 | -3)  int []  field1; Многомерный массив с предопределенными измерениями
1 х 2
Возвращает массивы раВной, переменной1 до 3) длиной
@D(1 | 2 | ~3)  int []  field1; Многомерный массив с предопределенными измерениями
1 х 2
Возвращает массивы раЗной, переменной1 до 3) длиной
@A(337) String field Возвращает строку — максимум 337 двухбайтовых символов.
Аннотация типа @V делает строку однобайтной
@V String [] field Возвращает содержимое строки — массив однобайтовых символов. Максимальная  длина строк по умолчанию до 127 символов.
@X_(3 / 45) @D( 12) byte [] field НЕОбязательное поле возвращает массив предопределенной длины 12. Значения которого в заданном диапазоне, с неравномерным распределением в обе стороны относительно середины диапазона.
@D(-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  позволяет учитывать топологию, качество каналов передачи данных и генерировать соответствующий исходный код.
Для описания каналов обмена между хостами используется конструкция class. которая должна находится
на самом верхнем уровне, рядом с описанием устройств,
и содержать в себе:
1) тип протокола обмена
2) список интерфейсов, которыми устройства взаимодействуют по данному каналу.

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

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

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