Документация Engee

Генерация, сборка, загрузка и запуск модели

Страница в процессе разработки.

В статье описана программная цепочка работы модели в независимом режиме: получение таргетом сгенерированного Си-кода, сборка проекта целевой платформы, загрузка и запуск результата. Интерактивное выполнение и XCP рассматриваются отдельно в статье Выполнение модели в интерактивном режиме.

Методы для управления работой модели

Для независимого режима достаточно реализовать четыре метода BaseTarget:

  1. generate_executable_code — подготовить проект целевой платформы.

  2. compile_model — собрать проект и получить артефакт.

  3. upload_model — перенести артефакт на устройство или подготовить его к запуску.

  4. start_model — подтвердить запуск или запустить артефакт, если платформа требует отдельного старта.

Эти методы вызываются инфраструктурой Target Hardware последовательно, поэтому каждый метод должен сохранять состояние, необходимое для следующего шага: путь к проекту, путь к build-директории, путь к прошивке, имя исполняемого файла, настройки загрузчика и т.п.

Основные API

Основные модули, используемые в коде таргета:

  • targets.base_target — контракт класса таргета и методы для управления работой модели (запуск, выполнение, остановка).

  • targets.base_models — модели данных, которые получает и возвращает таргет.

  • targets.code_info — низкоуровневое описание сгенерированного Си-кода модели.

В коде таргета для независимого режима выполнения используются следующие API:

  • BaseTarget задает обязательный интерфейс таргета. От него наследуется пользовательский класс. Даже если таргет не поддерживает интерактивный режим, методы start_stream и change_param все равно должны быть реализованы, но для таргета независимого режима выполнения они должны возвращать понятную ошибку.

  • EngeeModel содержит идентификатор модели. На практике чаще всего используется model.name: по нему называют директорию проекта, скетч, бинарный файл или прошивку.

  • ModelSettings содержит настройки запуска модели. Для генерации проекта особенно важны model_settings.cmi и model_settings.is_ext_mode. В этой главе рассматривается сценарий независимого режима выполнения, поэтому is_ext_mode должен быть False или должен явно отклоняться.

  • ModelCode содержит сгенерированный код модели. Для Си-таргетов используется model_code.c_code: в нем есть словарь исходных файлов и поле c_code_info с метаданными.

  • TargetResponse — стандартный ответ методов таргета. Для методов независимого режима выполнения достаточно вернуть TargetResponse(detail="…​"), где detail кратко описывает выполненный этап.

  • CMIParser читает параметры блока EDM-Target из model_settings.cmi. Метод get_target_block_options превращает значения маски блока EDM-Target в Pydantic-модель настроек пользовательской платформы.

  • create_codegen_info преобразует подробный CodeInfo в более удобный для шаблонов CodeGenInfo: имена Си-функций init/step/terminate, базовый период, список step-функций, времени остановки модели и другие значения рантайм.

  • render_template_to_file генерирует Jinja2-шаблон в файл проекта: например, main.c, .ino, CMakeLists.txt или Makefile.

  • recreate_dir удаляет и заново создает директорию проекта. Это удобно для чистой генерации, но используйте его только для директории, созданной таргетом.

  • store_model_src сохраняет Си-исходники модели в проекте. Сгенерированный Engee main.c пропускается, потому что таргет создает собственную точку входа из шаблона.

  • store_filed_sources сохраняет пользовательские исходные файлы, вложенные в блоки C Function модели. Используйте его, если целевая платформа должна поддерживать пользовательский Си-код из модели.

  • CMakeWrapper — готовая обертка для CMake-сборки. Ее можно использовать, если целевая платформа собирается через CMakeLists.txt. Для Arduino, vendor SDK или собственной системы сборки обычно пишут свой builder-класс.

Генерация

Метод generate_executable_code связывает кодогенерацию Engee с пользовательской платформой. Он не должен компилировать проект. Его задача — создать на диске полный проект, который затем сможет собрать compile_model.

Обычно метод делает следующее:

  1. Сохраняет model_settings, model_code и другие данные, которые понадобятся следующим шагам.

  2. Читает параметры блока EDM-Target через CMIParser.

  3. Проверяет, что выбран поддерживаемый режим выполнения.

  4. Создает чистую директорию проекта.

  5. Преобразует model_code.c_code.c_code_info в CodeGenInfo.

  6. Рендерит рантайм-шаблоны: main.c, .ino, CMakeLists.txt, Makefile или файлы проекта IDE.

  7. Сохраняет Си-исходники модели через store_model_src.

  8. Копирует драйверы, заголовки, файлы компоновки, файлы запуска, SDK-обвязку и другие платформенные файлы.

  9. Сохраняет пользовательские C Function-файлы через store_filed_sources, если они поддерживаются.

  10. Возвращает TargetResponse(detail="generate_executable_code").

Пример структуры generate_executable_code

def generate_executable_code(
    self,
    model: EngeeModel,
    model_settings: ModelSettings,
    model_code: ModelCode,
) -> TargetResponse:
    if model_settings.is_ext_mode:
        raise TargetException("MyTarget supports only independent execution.")

    self.model_settings = model_settings
    self.target_block = self.cmi_parser.get_target_block_options(
        model_settings.cmi,
        MyTargetBlock,
        self.__class__.__name__,
    )

    self.project_path = Path(self.target_block.codegen_folder) / model.name
    recreate_dir(self.project_path)

    project_src = self.project_path / "src"
    project_src.mkdir(parents=True, exist_ok=True)

    codegen_info = create_codegen_info(
        model_code.c_code.c_code_info,
        model_settings.is_ext_mode,
    )

    render_template_to_file(
        self.main_template,
        project_src / "main.c",
        codegen_info.model_dump(),
    )

    store_model_src(model_code.c_code, project_src, codegen_info)
    store_filed_sources(model_settings.cmi, project_src)

    return TargetResponse(detail="generate_executable_code")

Jinja2-шаблоны

Jinja2 — это шаблонизатор: он берет текстовый файл с подстановками и управляющими конструкциями, получает словарь значений и формирует итоговый файл. В таргетах Jinja2 нужен для генерации C/C++/CMake-кода, который зависит от конкретной модели.

Шаблон нужен потому, что имена функций модели, базовый период, список step-функций и времени остановок заранее неизвестны. Они появляются только после кодогенерации Engee. Таргет получает эти данные через CodeGenInfo и передает их в шаблон.

В шаблонах обычно используются:

  • {{ name }} — подстановка значения;

  • {% for item in items %} …​ {% endfor %} — цикл;

  • {% if condition %} …​ {% endif %} — условный фрагмент.

Шаблон должен описывать только платформенную обвязку. Код модели уже находится в файлах, которые сохраняет store_model_src.

Пример фрагмента шаблона main.c

#include "{{ model_name }}.h"

static unsigned long step_number = 0;

int main(void)
{
    platform_timer_init({{ base_rate_us }}UL);
    {{ init.cname }}();

    while (1) {
        unsigned long start_time = platform_time_us();

{% for step in steps %}
        if ((step_number % {{ step.base_rate_scale }}UL) == 0UL) {
            {{ step.cname }}();
        }
{% endfor %}

        step_number++;

{% if stop_time %}
        if ((step_number * {{ base_rate_us }}UL) > {{ stop_time_us }}UL) {
            {{ terminate.cname }}();
            break;
        }
{% endif %}

        platform_wait_until_next_tick(start_time, {{ base_rate_us }}UL);
    }

    return 0;
}

Пример значений, которые передаются в шаблон

codegen_info = create_codegen_info(
    model_code.c_code.c_code_info,
    model_settings.is_ext_mode,
)

context = codegen_info.model_dump()
render_template_to_file("templates/main.c", "build/src/main.c", context)

Рантайм-шаблон

Рантайм-шаблон независимого режима выполнения должен содержать следующие сведения:

  • где находится точка входа программы или прошивки;

  • когда вызывается функция инициализации модели;

  • как вызываются все step-функции;

  • как выдерживается базовый период;

  • как обрабатываются несколько шагов расчета;

  • что происходит при переполнении (overrun);

  • как обрабатывается конечное время моделирования;

  • когда вызывается функция завершения модели;

  • какие платформенные драйверы и заголовки доступны пользовательскому Си-коду.

Подробнее планировщик и выполнение модели рассмотрены в статье Выполнение модели в независимом режиме.

Python-код таргета создает проект, а поведение модели во время выполнения задается C/C++ рантайм-шаблоном.

Сборка

Метод compile_model запускает платформенный тулчейн и превращает сгенерированный проект в артефакт, который можно загрузить или запустить.

Возможные варианты:

  • arduino-cli compile;

  • CMake + cross compiler;

  • Make или Ninja;

  • vendor CLI/SDK;

  • сборка локального исполняемого файла;

  • сборка прошивки .hex, .bin, .elf.

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

Пример сборки через CMakeWrapper

def compile_model(self, model: EngeeModel) -> TargetResponse:
    if self.project_path is None or not self.project_path.exists():
        raise TargetException(
            "Project was not generated. Call generate_executable_code first."
        )

    self.cmake.build_all(str(self.project_path))
    self.artifact_path = self.project_path / "build" / "my_firmware.elf"

    if not self.artifact_path.exists():
        raise TargetException(f"Build artifact not found: {self.artifact_path}")

    return TargetResponse(detail="compile_model")

Пример вызов собственного builder

def compile_model(self, model: EngeeModel) -> TargetResponse:
    self.builder.build_project(model.name)
    self.artifact_path = self.builder.get_artifact_path(model.name)
    return TargetResponse(detail="compile_model")

Загрузка

Метод upload_model переносит результат сборки туда, где он должен выполняться. Для микроконтроллера это обычно прошивка через программатор. Для Linux-устройства — копирование по SSH. Для локального приложения этот шаг может ничего не делать, если бинарный файл уже находится на нужной машине.

Важно отделять загрузку от сборки: пользовательские ошибки тулчейна должны возникать в compile_model, а ошибки связи с устройством, программатором или удаленным хостом — в upload_model.

Пример загрузки прошивки

def upload_model(self, model: EngeeModel) -> TargetResponse:
    if self.artifact_path is None or not self.artifact_path.exists():
        raise TargetException("Build artifact not found. Compile model first.")

    self.loader.flash(self.artifact_path)
    return TargetResponse(detail="upload_model")

Пример локального приложения без отдельной загрузки

def upload_model(self, model: EngeeModel) -> TargetResponse:
    return TargetResponse(detail="upload_model")

Запуск

Метод start_model вызывается после загрузки. В режиме независимого режима выполнения возможны два варианта:

  • прошивка уже стартовала после загрузки, и метод только возвращает успешный ответ;

  • собранный артефакт является приложением, и метод должен запустить процесс.

Если процесс запускается из start_model, таргет должен сохранить handle процесса, чтобы позже stop_model мог его остановить. Если платформа не поддерживает удаленную остановку модели в независимом режиме, stop_model может только вернуть успешный ответ, но это поведение нужно описать в README таргета.

Пример прошивки, которая выполняется после загрузки

async def start_model(self, model: EngeeModel) -> TargetResponse:
    return TargetResponse(detail="start_model")

Пример запуска локального процесса

async def start_model(self, model: EngeeModel) -> TargetResponse:
    if self.artifact_path is None or not self.artifact_path.exists():
        raise TargetException("Executable not found. Compile model first.")

    self.current_process = subprocess.Popen([str(self.artifact_path)])
    return TargetResponse(detail="start_model")

Проверочный список

Для таргета независимого режима выполнения проверьте:

  • Блок EDM-Target успешно читается через CMIParser;

  • проект создается в директории таргета;

  • рантайм-шаблон использует CodeGenInfo, а не жестко заданные имена функций модели;

  • store_model_src сохраняет исходники модели в ожидаемое место;

  • драйверы, заголовки и файлы сборки попадают в проект;

  • compile_model проверяет наличие проекта и итогового артефакта;

  • upload_model не смешивает загрузку со сборкой;

  • start_model явно описывает поведение платформы: подтверждение запуска или старт процесса;

  • ошибки тулчейна, загрузки и запуска превращаются в понятные исключения таргета.