[Python, API, Программирование микроконтроллеров, Разработка для интернета вещей] Опыт написания IDL для embedded
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
ПредисловиеЯ при работе с микроконтроллерами часто сталкивался с бинарными протоколами. Особенно, когда есть несколько контроллеров. Или же используется 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] Import or from import, that is the question
- [Совершенный код, .NET, API, C#, Микросервисы] Паттерн CQRS: теория и практика в рамках ASP.Net Core 5
- [Python, Мозг] Мой опыт работы с EEG гарнитурой EMOTIV EPOC+
- [Разработка веб-сайтов, Python, API] Как я сделал веб-фреймворк без MVC — Pipe Framework
- [Python, .NET, История IT] Языку программирования Python исполнилось 30 лет
- [Информационная безопасность, Антивирусная защита, Разработка под MacOS] На 30 тысячах компьютеров с macOS нашли странный зловред, который ждёт команду
- [C, Программирование микроконтроллеров] АЦП преобразования в указанные моменты времени на STM32
- [Системное администрирование, Python, *nix, Серверное администрирование, DevOps] Improving Ansible (перевод)
- [Python] Pythonista. Привет, Python
- [Python, Машинное обучение, Искусственный интеллект, Natural Language Processing] Краткость — сестра таланта: Как сделать Transformer/Summarizer на Trax
Теги для поиска: #_python, #_api, #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_razrabotka_dlja_interneta_veschej (Разработка для интернета вещей), #_python3, #_embedded, #_codegen, #_codegeneration, #_idl, #_python, #_api, #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
), #_razrabotka_dlja_interneta_veschej (
Разработка для интернета вещей
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:21
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
ПредисловиеЯ при работе с микроконтроллерами часто сталкивался с бинарными протоколами. Особенно, когда есть несколько контроллеров. Или же используется bluetooth low energy и необходимо написать код для обработки бинарных данных в характеристике. Помимо кода всегда требуется понятная документация.Всегда возникает вопрос - а можно ли описать как-то протокол и сгенерировать на все платформы код и документацию? В этом может помочь IDL.1. Что такое IDLОпределение IDL довольно простое и уже представлено на wikipedia IDL, или язык описания интерфейсов (англ. Interface Description Language или Interface Definition Language) — язык спецификаций для описания интерфейсов, синтаксически похожий на описание классов в языке C++.
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>, } {% 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 %} 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'\)' 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 @_('term term')
def term(self, p): t0 = p.term0 t1 = p.term1 t0.extend(t1) return t0 @_('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] @_('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)]
@_('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; }; def solve(self, scopes: list):
scopes = scopes + [self] for i in self.items: if isinstance(i, Solvable): i.solve(scopes=scopes) struct SomeStruct {
i32 someNumber; @setter: someNumber; void setInteger(i32 integer); }; =========== Источник: habr.com =========== Похожие новости:
Программирование микроконтроллеров ), #_razrabotka_dlja_interneta_veschej ( Разработка для интернета вещей ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:21
Часовой пояс: UTC + 5