14.2.2.1. スクリプトをファームウェアにフリーズする

フリーズされたモジュールとは、バイトコードにコンパイルされ、ビルド時にファームウェアイメージにリンクされた .py ファイルのことです。ランタイムは、ディスク上のファイルシステムを一切参照することなく、フラッシュから直接フリーズモジュールをインポートします。出荷製品にとって、これはアプリケーションコードを置くのに適切な場所です。エンドユーザーが削除できるものは何もなく、SDカード上の古い .py が上書きするものも何もなく、カメラはドライブに何が(もしあれば)入っていようと、起動のたびに同じコードを実行します。

このページでは、カメラがたどる起動シーケンスについて説明し、続いて manifest.pyfreeze ディレクティブがどのようにアプリケーションをビルドに組み込むかを説明します。

14.2.2.1.1. 起動シーケンス

リセットから立ち上がるカメラで何がいつ実行されるか:

  • ブートローダー。 電源投入時、IDEがファームウェア更新をプッシュするために使う短いDFUウィンドウに入ります。このウィンドウは数秒後に閉じ、ブートローダーが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 をカスタマイズせず、ビルドが行います。IDEからフラッシュに SKIPSD ファイルを置くのが、カメラをSDカードではなく内蔵フラッシュから起動させるためのサポートされた方法です。

  • REPL前のセットアップ。 boot.pyあらゆるソフトリセット時(コールドブート、REPLからの Ctrl-D、実行中スクリプトの終了、ウォッチドッグによる復旧)に、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 行を1つ、アプリケーションが依存するパッケージごとに package 行を1つ追加します。

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 行を1つとパッケージ用の package 行を1つ記述します:

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

単一ファイルのスクリプト(ここでは main.py ですが、同じルールが boot.py やスタンドアロンのヘルパーにも当てはまります)には freeze を使います。複数ファイルのパッケージには package を使います。スクリプトを追加するのは freeze 行を1つ増やすこと、パッケージを追加するのは package 行を1つ増やすことです。

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.pymain.py をフリーズされたコピーに解決するため、出荷されたカメラは、開発時に残された古い boot.py がSDカードにあっても、ビルドのコードを実行します。

14.2.2.1.2.3. ルックアップの順序

オーバーライドのセマンティクスは、boot.py / main.py の実行経路と通常の import 文とでは異なります。どちらがどちらなのかを知っておくことは、本番でも開発でも重要です:

  • boot.pymain.py の場合: ランタイムはまずフリーズされたコピーを探し、次にファイルシステムを探します。フリーズされた boot.py は、SDカードに1つ置いてもオーバーライドできません。カメラを手にした者は、再フラッシュなしにエントリーポイントを変更できないのです。

  • import foo の場合: ランタイムはまず sys.path を検索します。これには /flash/sdcard/rom とそれらの lib サブディレクトリが含まれます。その後にフリーズモジュールを検索します。フラッシュやSD上の同名の foo.py は、フリーズされた foo を実際にオーバーライドします。これが開発上の便利な仕組みです。修正したモジュールをカードに置き、ソフトリセットすれば、再フラッシュなしに変更を確認できます。

インポートにおいてファイルシステムがフリーズモジュールを上書きする挙動を抑制したい出荷製品は、boot.py の早い段階で sys.path をクリアできます:

import sys

sys.path.clear()

sys.path を空にすると、すべてのインポートはフリーズモジュールからのみ解決されます。フラッシュ、SD、ROMFS上のどれもそれらを覆い隠すことはできません。

14.2.2.1.2.4. アセットの問題

フリーズはコードには最適です。しかし、機械学習のモデルファイル、ラベルテーブル、JSON設定、画像テンプレートといった大きなバイナリアセットには向いていません。それらをPythonリテラルとして埋め込むと、ソースが膨れ上がり、再コンパイルが遅くなり、どのみちインタープリタがそのまま読み込むだけのデータのためにバイトコードコンテナを無駄にします。ROMFSイメージのビルド のページでは、このギャップを埋める読み取り専用のフラッシュファイルシステムについて説明します。