Генерация, сборка, загрузка и запуск модели
|
Страница в процессе разработки. |
В статье описана программная цепочка работы модели в независимом режиме: получение таргетом сгенерированного Си-кода, сборка проекта целевой платформы, загрузка и запуск результата. Интерактивное выполнение и XCP рассматриваются отдельно в статье Выполнение модели в интерактивном режиме.
Методы для управления работой модели
Для независимого режима достаточно реализовать четыре метода BaseTarget:
-
generate_executable_code— подготовить проект целевой платформы. -
compile_model— собрать проект и получить артефакт. -
upload_model— перенести артефакт на устройство или подготовить его к запуску. -
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сохраняет Си-исходники модели в проекте. Сгенерированный Engeemain.cпропускается, потому что таргет создает собственную точку входа из шаблона. -
store_filed_sourcesсохраняет пользовательские исходные файлы, вложенные в блоки C Function модели. Используйте его, если целевая платформа должна поддерживать пользовательский Си-код из модели. -
CMakeWrapper— готовая обертка для CMake-сборки. Ее можно использовать, если целевая платформа собирается черезCMakeLists.txt. Для Arduino, vendor SDK или собственной системы сборки обычно пишут свой builder-класс.
Генерация
Метод generate_executable_code связывает кодогенерацию Engee с пользовательской платформой. Он не должен компилировать проект. Его задача — создать на диске полный проект, который затем сможет собрать compile_model.
Обычно метод делает следующее:
-
Сохраняет
model_settings,model_codeи другие данные, которые понадобятся следующим шагам. -
Читает параметры блока EDM-Target через
CMIParser. -
Проверяет, что выбран поддерживаемый режим выполнения.
-
Создает чистую директорию проекта.
-
Преобразует
model_code.c_code.c_code_infoвCodeGenInfo. -
Рендерит рантайм-шаблоны:
main.c,.ino,CMakeLists.txt,Makefileили файлы проекта IDE. -
Сохраняет Си-исходники модели через
store_model_src. -
Копирует драйверы, заголовки, файлы компоновки, файлы запуска, SDK-обвязку и другие платформенные файлы.
-
Сохраняет пользовательские C Function-файлы через
store_filed_sources, если они поддерживаются. -
Возвращает
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явно описывает поведение платформы: подтверждение запуска или старт процесса; -
ошибки тулчейна, загрузки и запуска превращаются в понятные исключения таргета.