[Python, API, Программирование микроконтроллеров, Разработка для интернета вещей] Опыт написания IDL для embedded

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
24-Фев-2021 10:32

ПредисловиеЯ при работе с микроконтроллерами часто сталкивался с бинарными протоколами. Особенно, когда есть несколько контроллеров. Или же используется bluetooth low energy и необходимо написать код для обработки бинарных данных в характеристике. Помимо кода всегда требуется понятная документация.Всегда возникает вопрос - а можно ли описать как-то протокол и сгенерировать на все платформы код и документацию? В этом может помочь IDL.1. Что такое IDLОпределение IDL довольно простое и уже представлено на wikipedia
IDL, или язык описания интерфейсов (англ. Interface Description Language или Interface Definition Language) — язык спецификаций для описания интерфейсов, синтаксически похожий на описание классов в языке C++.
Самое главное в IDL - он должен хорошо описывать интерфейс взаимодействия, API, протокол. Он должен быть достаточно понятен, чтобы служить другим инженерам документацией.Бонус также является - генерация документации, структур, кода.2. МотивацияВ процессе работы я попробовал разные кодогенераторы и IDL. Среди тех, что попробовал были - QFace (https://github.com/Pelagicore/qface), swagger (Это не IDL, а API development tool). Также существует коммерческое решение проблемы: https://www.protlr.com/.Swagger больше подходит к REST API. Поэтому сразу был отметён. Однако его можно использовать если применяется cbor (бинарный аналог json с кучей крутых фич).В QFace давно не было коммитов, хотелось некоторых "наворотов" для применения в embedded, возникли сложности при написании шаблона. Он не ищет символы сам, не умеет считать поля enum-ов.Бесплатные решения было найти сложно, чтобы можно было комфортно использовать при разработке бинарных протоколов.Поэтому я отказался от генераторов кода и IDL в пользу написания некоторых "автоматизаций" в коде, позволяющих проще писать адаптер протокола. Но протокол с коллегами продолжили описывать при помощи QFace. Решил в свободное время попробовать сделать что-то более или менее годное.2.1 Обзор QFaceIDL, которая являлась источником вдохновения, имеет простой синтаксис:
module <module> <version>
import <module> <version>
interface <Identifier> {
    <type> <identifier>
    <type> <operation>(<parameter>*)
    signal <signal>(<parameter>*)
}
struct <Identifier> {
    <type> <identifier>;
}
enum <Identifier> {
    <name> = <value>,
}
flag <Identifier> {
    <name> = <value>,
}
Для генерации используется jinja2. Пример:
{% for module in system.modules %}
    {%- for interface in module.interfaces -%}
    INTERFACE, {{module}}.{{interface}}
    {% endfor -%}
    {%- for struct in module.structs -%}
    STRUCT , {{module}}.{{struct}}
    {% endfor -%}
    {%- for enum in module.enums -%}
    ENUM   , {{module}}.{{enum}}
    {% endfor -%}
{% endfor %}
Концепция интересная. Можно было просто "подпилить" для комфорта "напильником", что конечно и сделал мой коллега. Но мне показалось интересным взять библиотеку sly и просто написать IDL с нужными фичами.3. Обзор slyПочему именно sly - библиотека очень проста для описания грамматики.Сначала надо написать лексер. Он токенизирует код чтобы далее было проще обрабатывать парсером. Код из документации:
class CalcLexer(Lexer):
    # Set of token names.   This is always required
    tokens = { ID, NUMBER, PLUS, MINUS, TIMES,
               DIVIDE, ASSIGN, LPAREN, RPAREN }
    # String containing ignored characters between tokens
    ignore = ' \t'
    # Regular expression rules for tokens
    ID      = r'[a-zA-Z_][a-zA-Z0-9_]*'
    NUMBER  = r'\d+'
    PLUS    = r'\+'
    MINUS   = r'-'
    TIMES   = r'\*'
    DIVIDE  = r'/'
    ASSIGN  = r'='
    LPAREN  = r'\('
    RPAREN  = r'\)'
Нужно наследовать класс Lexer, в переменную tokens - добавить свои использованные токены. Само определение токенов делается в теле класса - достаточно просто описать регулярное выражение, соответсвующее токену.Парсер - делает работу по преобразованию набора токенов по определенным правилам. С помощью его и осуществляется основная работа. В случае компиляторов - преобразование в байт-код/объектный файл итд. Для интерпретаторов - можно сразу выполнять вычисления. При реализации кодогенератора - можно преобразовать в дерево классов.Также парсер задается очень простым способом (пример из документации):
class CalcParser(Parser):
    # Get the token list from the lexer (required)
    tokens = CalcLexer.tokens
    # Grammar rules and actions
    @_('expr PLUS term')
    def expr(self, p):
        return p.expr + p.term
    @_('expr MINUS term')
    def expr(self, p):
        return p.expr - p.term
    @_('term')
    def expr(self, p):
        return p.term
    @_('term TIMES factor')
    def term(self, p):
        return p.term * p.factor
    @_('term DIVIDE factor')
    def term(self, p):
        return p.term / p.factor
    @_('factor')
    def term(self, p):
        return p.factor
    @_('NUMBER')
    def factor(self, p):
        return p.NUMBER
    @_('LPAREN expr RPAREN')
    def factor(self, p):
        return p.expr
Каждый метод класса отвечает за парсинг конкретной конструкции. В декораторе @_ указывается правило, которое обрабатывается. Имя метода sly распознает как название правила.В этом примере сразу происходят вычисления.Подробнее можно прочитать в официальной документации: https://sly.readthedocs.io/en/latest/sly.html4. Процесс созданияВ самом начале программа получает yml файл с настройками. Затем при помощи sly преобразовывает код в древо классов. Далее выполняются вычисления и поиски объектов. После вычисления - передается в jinja2 шаблон и дерево символов.Читать как был определен список токенов для лексера может быть скучно, поэтому перейдем сразу к парсеру.Вначале определили, что модуль состоит из списка термов:
@_('term term')
    def term(self, p):
        t0 = p.term0
        t1 = p.term1
        t0.extend(t1)
        return t0
Затем определим, что терм состоит из определений структуры, энумератора или интерфейса разделенные символом ";"(SEPARATOR):
@_('enum_def SEPARATOR')
    def term(self, p):
        return [p.enum_def]
    @_('statement SEPARATOR')
    def term(self, p):
        return [p.statement]
    @_('interface SEPARATOR')
    def term(self, p):
        return [p.interface]
    @_('struct SEPARATOR')
    def term(self, p):
        return [p.struct]
Здесь терм сразу паковался в массив для удобства. Чтобы список термов (term term правило) работал уже сразу с листами и собрал в один лист.Ниже представлен набор правил для описания структуры:
@_('STRUCT NAME LBRACE struct_items RBRACE')
    def struct(self, p):
        return Struct(p.NAME, p.struct_items, lineno=p.lineno)
    @_('decorator_item STRUCT NAME LBRACE struct_items RBRACE')
    def struct(self, p):
        return Struct(p.NAME, p.struct_items, lineno=p.lineno, tags=p.decorator_item)
    @_('struct_items struct_items')
    def struct_items(self, p):
        si0 = p.struct_items0
        si0.extend(p.struct_items1)
        return si0
    @_('type_def NAME SEPARATOR')
    def struct_items(self, p):
        return [StructField(p.type_def, p.NAME, lineno=p.lineno)]
    @_('type_def NAME COLON NUMBER SEPARATOR')
    def struct_items(self, p):
        return [StructField(p.type_def, p.NAME, bitsize=p.NUMBER, lineno=p.lineno)]
    @_('decorator_item type_def NAME SEPARATOR')
    def struct_items(self, p):
        return [StructField(p.type_def, p.NAME, lineno=p.lineno, tags=p.decorator_item)]
    @_('decorator_item type_def NAME COLON NUMBER SEPARATOR')
    def struct_items(self, p):
        return [StructField(p.type_def, p.NAME, bitsize=p.NUMBER, lineno=p.lineno, tags=p.decorator_item)]
Если описать простым языком правила - структура (struct) содержит поля структур (struct_items). А поля структур могут определяться как:
  • тип (type_def), имя (NAME), разделитель (SEPARATOR)
  • тип (type_def), имя, двоеточие (COLON), число (NUMBER - для битфилда, означает количество бит), разделитель
  • список декораторов (decorator_item), тип, имя, разделитель
  • список декораторов, тип, имя, двоеточие (COLON), число (NUMBER - для битфилда), разделитель
Новшество относительно QFace (однако есть в protlr) - была введена возможность описывать специальные условные ссылки на структуры. Было решено назвать эту фичу - alias.
@_('DECORATOR ALIAS NAME COLON expr struct SEPARATOR')
    def term(self, p):
        return [Alias(p.NAME, p.expr, p.struct), p.struct]
Это было сделано чтобы поддерживалась следующая конструкция:
enum Opcode {
    Start =  0x00,
    Stop = 0x01
};
@alias Payload: Opcode.Start
struct StartPayload {
    ...
};
@alias Payload: Opcode.Stop
struct StopPayload {
    ...
};
struct Message {
    Opcode opcode: 8;
    Payload<opcode> payload;
};
Данная конструкция обозначает, что если opcode = Opcode.Start (0x00) - payload будет соответствовать структуре StartPayload. Если opcode = Opcode.Stop (0x01) - payload будет иметь структуру StopPayload. То есть создаем ссылку структуры с определенными условиями.Следующее что было сделано - отказался от объявления модуля. Показалось это избыточным так как - имя файла уже содержит имя модуля, а версию писать бессмысленно так как есть git. Хороший протокол имеет прямую и обратную совместимость и в версии нуждаться не должен. Был выкинут тип flag так как есть enum, и добавил возможность описания битфилдов. Убрал возможность определения сигналов так как пока что низкоуровневого примера, демонстрирующего пользу, не было.Была добавлена возможность python-подобных импортов. Чтобы можно было импортировать из другого модуля только конкретный символ. Это полезно для генерации документации.Для вычислений был создан класс - Solvable. Его наследует каждый объект, которому есть что посчитать. Например, для SymbolType (тип поля класса или интерфейса). В данном классе этот метод ищет по ссылке тип, чтобы добавить его в поле reference. Чтобы в jinja можно было сразу на месте обратиться к полям enum или структуры. Класс Solvable должен искать во вложенных символах вычислимые и вызывать solve. Т.е. вычисления происходят рекурсивно.Пример реализации метода solve для структуры:
def solve(self, scopes: list):
        scopes = scopes + [self]
        for i in self.items:
            if isinstance(i, Solvable):
                i.solve(scopes=scopes)
Как видно, в методе solve есть аргумент - scopes. Этот аргумент отвечает за видимость символов. Пример использования:
struct SomeStruct {
    i32  someNumber;
    @setter: someNumber;
    void setInteger(i32 integer);
};
Как видно из примера - это позволяет производить поиск символа someNumber в области видимости структуры, вместо явного указания SomeStruct.someNumber.ЗаключениеПо сравнению с QFace мне удалось - упростить написание шаблона за счет поиска типов, вычисления перечислений. Также полезно иметь импорт символов и возможность условно ссылаться на разные структуры.В папке examples/uart - находится пример генерации заголовков, кода и html документации. Пример иллюстрирует типичный uart протокол с применением новых фич. Подразумевается, что функции типа put_u32 итд - определит сам пользователь исходя из порядка байт и архитектуры MCU.Ознакомиться подробнее с реализацией можно по ссылке: https://gitlab.com/volodyaleo/volk-idlP.S.Это моя первая статья на Хабр. Буду рад получить отзывы - интересна ли данная тематика или нет. Если у кого-то есть хорошие примеры кодо+доко-генераторов бинарных протоколов для Embedded, было бы интересно прочитать в комментариях. Или какая-то успешная практика внедрения похожих систем для описания бинарных протоколов.В данном проекте я не обращал особого внимания на скорость работы. Некоторые вещи делал чтобы "быстрее решить задачу". Было важнее получить рабочий код, который можно уже пробовать применять к разным проектам.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_python, #_api, #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_razrabotka_dlja_interneta_veschej (Разработка для интернета вещей), #_python3, #_embedded, #_codegen, #_codegeneration, #_idl, #_python, #_api, #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
)
, #_razrabotka_dlja_interneta_veschej (
Разработка для интернета вещей
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 19-Май 11:54
Часовой пояс: UTC + 5