14.2.2.1. Заморозка скриптов в прошивку

Замороженный модуль – это файл .py, скомпилированный в байт-код и связанный с образом прошивки на этапе сборки. Среда выполнения импортирует замороженный модуль прямо из флеш-памяти, никогда не обращаясь к файловой системе на диске. Для готового к поставке продукта это правильное место для кода приложения: конечному пользователю нечего удалять, устаревший .py на SD-карте ничего не переопределит, и камера выполняет один и тот же код при каждой загрузке независимо от того, что (если вообще что-то) находится на её накопителях.

На этой странице описывается последовательность запуска, которой следует камера, а затем то, как manifest.py и директива freeze встраивают приложение в сборку.

14.2.2.1.1. Последовательность запуска

Что и когда выполняется на камере, выходящей из сброса:

  • Загрузчик. При включении питания открывается короткое окно DFU, которое IDE использует для отправки обновлений прошивки. Окно закрывается через несколько секунд, и загрузчик передаёт управление MicroPython. Выполняющийся скрипт может повторно войти в это окно по запросу, вызвав machine.bootloader().

  • Инициализация замороженной файловой системы. Прежде чем выполнится какой-либо код приложения, среда выполнения поднимает файловые системы. Внутренняя флеш-память монтируется в /flash (и форматируется как пустая, если там ничего нет). Если присутствует SD-карта и во внутренней флеш-памяти не существует файла-маркера с именем SKIPSD, SD-карта монтируется в /sdcard. ROMFS, если сборка включает его, монтируется автоматически в /rom. Рабочий каталог устанавливается в каталог загрузки (/sdcard, если карта смонтирована, иначе /flash), а sys.path заполняется значениями /flash, /flash/lib, /sdcard, /sdcard/lib, /rom и /rom/lib. Настройка, размещённая во флеш-памяти, выполняется замороженным модулем _boot.py – это инфраструктура порта и платы, а не точка подключения приложения. Приложения не настраивают _boot.py; это делает сборка. Размещение файла SKIPSD во флеш-памяти из IDE – это поддерживаемый способ заставить камеру загружаться из внутренней флеш-памяти вместо SD-карты.

  • Настройка перед REPL. boot.py выполняется при каждом программном сбросе – холодная загрузка, Ctrl-D из REPL, завершение выполняющегося скрипта и восстановление сторожевым таймером – перед тем, как REPL станет доступен. Его задача – подготовить окружение, в котором работает остальная часть системы: ту настройку, которая должна быть на месте, чтобы могли функционировать REPL, приложение и любой инструмент восстановления. Это не то место, где живёт само приложение. Точкой входа приложения является main.py.

  • Основной цикл. main.py – это основной цикл приложения. Выполняется один раз при холодной загрузке, сразу после boot.py. Не выполняется повторно при последующих программных сбросах – вместо этого камера переходит в REPL. Эта асимметрия важна для разработки (нажатие Ctrl-D переходит в REPL без повторного запуска цикла, чтобы разработчик мог исследовать состояние), но не для производства: установленная в полевых условиях камера видит включение питания, срабатывание сторожевого таймера и аппаратные сбросы, которые все являются аппаратными сбросами, повторно входящими в путь холодной загрузки и снова запускающими main.py.

14.2.2.1.2. Заморозка в прошивку

Набор замороженных модулей платы объявляется в boards/<TARGET>/manifest.py в дереве прошивки. Манифест – это небольшой файл Python, который вызывает несколько директив:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") – встраивает один файл foo.py в сборку.

  • package("mylib", base_path="...") – встраивает многофайловый пакет Python, сохраняя его структуру каталогов относительно заданного базового пути.

  • include("...") – подключает другой файл манифеста. Манифесты плат используют это для совместного использования общих наборов модулей.

  • require("logging") – подключает именованный вышестоящий модуль micropython-lib по имени.

Минимальный манифест приложения добавляет одну строку freeze на каждый скрипт верхнего уровня и одну строку package на каждый пакет, от которого зависит приложение.

14.2.2.1.2.1. Где находится исходный код

Исходный код приложения находится в scripts/libraries/ в дереве прошивки, рядом с модулями, которые сборка уже замораживает. Переменная манифеста $(OMV_LIB_DIR) раскрывается в этот путь, поэтому записи манифеста остаются короткими. Редактирование манифеста уже является операцией внутри дерева, поэтому хранение исходного кода внутри дерева избавляет от необходимости жонглировать отдельным репозиторием проекта при разрешении путей.

Типичная структура для приложения, которое поставляет единственный main.py плюс вспомогательный пакет:

scripts/libraries/
    main.py
    my_lib/
        __init__.py
        helpers.py

А в boards/<TARGET>/manifest.py платы – одна строка freeze для скрипта и одна строка package для пакета:

freeze("$(OMV_LIB_DIR)/", "main.py")
package("my_lib", base_path="$(OMV_LIB_DIR)/my_lib")

Однофайловые скрипты – здесь main.py, но то же правило применяется к boot.py или любому отдельному вспомогательному модулю – используют freeze. Многофайловые пакеты используют package. Добавление ещё одного скрипта – это ещё одна строка freeze; добавление ещё одного пакета – это ещё одна строка package.

14.2.2.1.2.2. Сборка и прошивка

Когда манифест на месте, соберите прошивку точно так, как описано в главе о прошивке:

make -j$(nproc) -C lib/micropython/mpy-cross   # once, builds the cross-compiler
make -j$(nproc) TARGET=<TARGET>                # builds the firmware

Результат попадает в build/<TARGET>/bin/:

build/<TARGET>/bin/
    firmware.bin     # flash through the IDE
    romfs0.img       # flash through the IDE in a separate step

Прошивка файлов .bin и .img через IDE даёт камеру, чьё приложение является частью сборки.

Приведённая выше последовательность запуска – это то, что делает встраивание эффективным: среда выполнения разрешает boot.py и main.py в замороженные копии, прежде чем вообще обратится к файловой системе, поэтому поставляемая камера выполняет код сборки, даже если на SD-карте хранится устаревший boot.py, оставшийся со времён разработки.

14.2.2.1.2.3. Порядок поиска

Семантика переопределения различается для пути выполнения boot.py / main.py и для обычных инструкций import. Понимание того, что есть что, важно как для производства, так и для разработки:

  • Для boot.py и main.py: среда выполнения сначала ищет замороженную копию, затем файловую систему. Замороженный boot.py нельзя переопределить, разместив его на SD-карте – тот, у кого в руках камера, не может изменить точку входа без перепрошивки.

  • Для import foo: среда выполнения сначала просматривает sys.path – который охватывает /flash, /sdcard, /rom и их подкаталоги lib – затем замороженные модули. Файл foo.py с тем же именем во флеш-памяти или на SD действительно переопределяет замороженный foo. Это удобство для разработки: разместите исправленный модуль на карте, выполните программный сброс, увидьте изменение без перепрошивки.

Готовый к поставке продукт, который хочет подавить поведение «файловая система переопределяет замороженное» для импортов, может очистить sys.path в начале boot.py:

import sys

sys.path.clear()

При пустом sys.path все импорты разрешаются только из замороженных модулей; ничто во флеш-памяти, на SD или в ROMFS не может их затенить.

14.2.2.1.2.4. Проблема ресурсов

Заморозка отлично подходит для кода. Она не подходит для крупных бинарных ресурсов: файлов моделей машинного обучения, таблиц меток, конфигурации JSON, шаблонов изображений. Встраивание их в виде литералов Python раздувает исходный код, медленно перекомпилируется и расходует контейнер байт-кода на данные, которые интерпретатор всё равно собирается прочитать в сыром виде. Страница Создание образа ROMFS описывает доступную только для чтения файловую систему во флеш-памяти, которая заполняет этот пробел.