ملفات بيان (manifest) في MicroPython

ملخص

يوفر MicroPython ميزة تتيح "تجميد" (freezing) شيفرة Python داخل البرنامج الثابت، كبديل عن تحميل الشيفرة من نظام الملفات.

ولهذا الفوائد التالية:

  • تُترجَم الشيفرة مسبقًا إلى رمز بايت (bytecode)، مما يتجنب الحاجة إلى ترجمة شيفرة Python المصدرية عند وقت التحميل.

  • يمكن تنفيذ رمز البايت مباشرة من ROM (أي ذاكرة الفلاش) بدلًا من نسخه إلى RAM. وبالمثل تُحمَّل أي كائنات ثابتة (سلاسل نصية، صفوف tuples، إلخ) من ROM أيضًا. وقد يؤدي ذلك إلى توفّر قدر أكبر بكثير من الذاكرة لتطبيقك.

  • على الأجهزة التي لا تحتوي على نظام ملفات، يكون هذا هو السبيل الوحيد لتحميل شيفرة Python.

أثناء التطوير، لا يُنصح عمومًا بالتجميد لأنه سيُبطئ بشكل كبير دورة التطوير لديك، إذ سيتطلب كل تحديث إعادة كتابة البرنامج الثابت بالكامل. ومع ذلك، قد يظل من المفيد تجميد بعض الاعتماديات التي نادرًا ما تتغير بشكل انتقائي (مثل المكتبات الخارجية).

تكون طريقة سرد ملفات Python المراد تجميدها داخل البرنامج الثابت عبر "بيان" (manifest)، وهو ملف Python يُفسَّر بواسطة عملية البناء. عادةً ما تكتب ملف بيان كجزء من تعريف لوحة، لكن يمكنك أيضًا كتابة ملف بيان مستقل واستخدامه مع تعريف لوحة موجود.

يمكن لملفات البيان أن تُعرّف اعتماديات على مكتبات من micropython-lib بالإضافة إلى ملفات Python في نظام الملفات، وكذلك على ملفات بيان أخرى.

كتابة ملفات البيان

ملف البيان هو ملف Python يحتوي على سلسلة من استدعاءات الدوال. راجع الدوال المتاحة المعرّفة أدناه.

يمكن لأي مسارات تُستخدم في ملفات البيان أن تتضمن المتغيرات التالية. وكلها تُحَل إلى مسارات مطلقة.

  • $(MPY_DIR) -- المسار إلى مستودع micropython.

  • $(MPY_LIB_DIR) -- المسار إلى الوحدة الفرعية micropython-lib. يُفضَّل استخدام require().

  • $(PORT_DIR) -- المسار إلى المنفذ (port) الحالي (مثل ports/stm32)

  • $(BOARD_DIR) -- المسار إلى اللوحة الحالية (مثل ports/stm32/boards/OPENMV4)

ينبغي ألا تكون ملفات البيان المخصصة داخل مستودع MicroPython الرئيسي. يجب أن تحتفظ بها في نظام التحكم بالإصدارات مع بقية مشروعك.

عادةً ما يحتاج البيان المستخدم لترجمة البرنامج الثابت إلى تضمين بيان المنفذ، الذي قد يتضمن وحدات مجمّدة لازمة لعمل اللوحة. إذا كنت تريد فقط إضافة وحدات إضافية إلى لوحة موجودة، فضمّن بيان اللوحة (والذي سيضمّن بدوره بيان المنفذ).

البناء باستخدام بيان مخصص

يمكن تحديد بيانك في سطر أوامر make بـ:

$ make BOARD=MYBOARD FROZEN_MANIFEST=/path/to/my/project/manifest.py

ينطبق هذا على جميع المنافذ، بما في ذلك تلك المبنية على CMake (مثل rp2)، إذ سيمرر غلاف Makefile هذا إلى بناء CMake.

إضافة بيان إلى تعريف لوحة

إذا كان لديك تعريف لوحة مخصص، فيمكنك جعله يتضمن بيانك المخصص تلقائيًا. على المنافذ المبنية على make (معظم المنافذ)، اضبط المتغير FROZEN_MANIFEST في ملف mpconfigboard.mk لديك.

FROZEN_MANIFEST ?= $(BOARD_DIR)/manifest.py

على المنافذ المبنية على CMake (مثل rp2)، استخدم بدلًا من ذلك mpconfigboard.cmake

set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)

دوال عالية المستوى

هذه هي الدوال التي ستستخدمها عادةً. فهي تضيف شيفرة إلى المجموعة التي تُترجَم مسبقًا إلى رمز بايت وتُجمَّد في صورة البرنامج الثابت:

  • تُجمّد module وpackage مصدرك المحلي الخاص — ملفًا واحدًا أو دليل حزمة كاملًا على التوالي.

  • تُجمّد require حزمة منشورة (واعتمادياتها) من micropython-lib، بالاسم.

  • تسحب include بيانًا آخر بحيث تُضاف وحداته المجمّدة أيضًا.

  • add_library وmetadata دالتان مساعدتان (لتسجيل مسارات بحث إضافية لأجل require، وللإعلان عن بيانات وصفية للحزمة).

يقوم بيان البرنامج الثابت النموذجي أولًا بـ include لبيان المنفذ أو اللوحة (بحيث تبقى الوحدات التي تحتاجها اللوحة مجمّدة)، ثم يضيف أسطر module/package/require الخاصة به.

ملاحظة: يمكن ضبط الوسيط المفتاحي opt على الدوال المختلفة، وهو يتحكم في مستوى التحسين (optimisation) الذي يستخدمه المترجم المتقاطع (cross-compiler). راجع micropython.opt_level().

add_library(library, library_path, prepend=False)

تسجيل المسار إلى library (مكتبة) خارجية مسماة.

استخدم هذا عندما تريد أن تحلّ require الحزم من دليل غير micropython-lib — على سبيل المثال مجموعتك الخاصة من المشغّلات (drivers)، أو نسخة مسحوبة من مكتبة خارجية.

سيُبحَث في المسار library_path تلقائيًا عند استخدام require. افتراضيًا تُضاف المكتبة المضافة إلى نهاية قائمة المكتبات المراد البحث فيها. مرّر True لـ الإضافة في المقدمة بحيث تُضاف إلى بداية القائمة.

إضافةً إلى ذلك، يمكن طلب المكتبة المضافة صراحةً باستخدام require("name", library="library").

package(package_path, files=None, base_path='.', opt=None)

تجميد حزمة كاملة — دليل من ملفات .py (مع حزم فرعية اختياريًا) — بحيث يمكن استيرادها بـ import <package>. استخدم module بدلًا من ذلك لملف مستقل واحد.

هذا يكافئ نسخ دليل "package_path" إلى الجهاز (باستثناء أنها شيفرة مجمّدة).

في أبسط الحالات، لتجميد حزمة "foo" في الدليل الحالي:

package("foo")

سيضمّن تَكراريًا جميع ملفات .py في foo، وسيُجمَّد على هيئة foo/**/*.py.

إذا لم تكن الحزمة في الدليل نفسه الذي يوجد فيه ملف البيان، فاستخدم base_path:

package("foo", base_path="path/to/libraries")

يمكنك استخدام المتغيرات أعلاه، مثل $(PORT_DIR) في base_path.

لتقييد الأمر بملفات معينة في الحزمة استخدم files (ملاحظة: ينبغي أن تكون المسارات نسبية للحزمة): package("foo", files=["bar/baz.py"]).

module(module_path, base_path='.', opt=None)

تجميد ملف .py مستقل واحد بحيث يمكن استيراده باسمه (module("foo.py") يجعل import foo يعمل). استخدم package لدليل/حزمة.

إذا كان الملف في الدليل الحالي:

module("foo.py")

وإلا فاستخدم base_path لتحديد موقع الملف:

module("foo.py", base_path="src/drivers")

يمكنك استخدام المتغيرات أعلاه، مثل $(PORT_DIR) في base_path.

require(name, library=None)

طلب حزمة بالاسم (واعتمادياتها) من micropython-lib.

هكذا تُجمَّد امتدادات المكتبة القياسية ومشغّلات المجتمع: تُجلَب الحزمة المسماة من الوحدة الفرعية micropython-lib وتُجمَّد مع كل ما تعتمد عليه. استخدم module أو package بدلًا من ذلك لتجميد مصدرك الخاص بدلًا من حزمة منشورة.

اختياريًا حدّد library (سلسلة نصية) للإشارة إلى حزمة من مكتبة سبق تسجيلها بـ add_library. وإلا فستُستخدم قائمة مسارات المكتبات.

include(manifest_path)

تضمين بيان آخر. هكذا تُؤلَّف البيانات (manifests): ينبغي لبيان برنامج ثابت مخصص أن يقوم بـ include لبيان المنفذ (أو اللوحة) بحيث تبقى الوحدات التي تحتاجها اللوحة مجمّدة، ثم يضيف مدخلاته الخاصة.

عادةً ما يحتاج البيان المستخدم لترجمة البرنامج الثابت إلى تضمين بيان المنفذ، الذي قد يتضمن وحدات مجمّدة لازمة لعمل اللوحة.

يمكن أن يكون الوسيط manifest سلسلة نصية (اسم ملف) أو كائنًا قابلًا للتكرار من السلاسل النصية.

تُحَل المسارات النسبية بالنسبة إلى ملف البيان الحالي.

إذا كان المسار إلى دليل، فإنه يتضمن ضمنيًا ملف manifest.py الموجود داخل ذلك الدليل.

يمكنك استخدام المتغيرات أعلاه، مثل $(PORT_DIR) في manifest_path.

metadata(description=None, version=None, license=None, author=None)

تعريف بيانات وصفية لملف البيان هذا. هذا مفيد لبيانات حزم micropython-lib.

تُستهلَك هذه الحقول عند نشر حزمة إلى / تثبيتها من micropython-lib عبر mip؛ وهي ليست لازمة في بيان برنامج ثابت للوحة.

دوال منخفضة المستوى

هذه الدوال موثّقة لأجل الاكتمال، لكن باستثناء freeze_as_str يمكن الوصول إلى كل الوظائف عبر الدوال عالية المستوى.

تختلف دوال freeze* فقط في كيفية تخزين الشيفرة:

  • تخزّن freeze_as_mpy / freeze_mpy رمز البايت المترجَم مسبقًا (.mpy) في الفلاش. تعمل الشيفرة مباشرة من الفلاش، وتستخدم قدرًا أدنى من RAM، وتُستورَد بسرعة. وهذا ما تستخدمه module وpackage وrequire داخليًا.

  • تجمّد freeze_as_str بدلًا من ذلك مصدر Python، الذي يُترجَم إلى رمز بايت عند وقت الاستيراد (مستخدمًا RAM، ومتطلبًا المترجم الموجود على الجهاز). هذه هي القدرة الوحيدة التي لا تكشفها الدوال عالية المستوى، ولذلك فهي الاستثناء المذكور أعلاه.

freeze(path, script=None, opt=0)

البدائية الأساسية التي تُبنى عليها الدوال عالية المستوى؛ يُفضَّل استخدام تلك. جمّد المدخل المحدد بـ path، مع تحديد نوعه تلقائيًا. سيُترجَم برنامج .py نصي إلى .mpy أولًا ثم يُجمَّد، وسيُجمَّد ملف .mpy مباشرة.

يجب أن يكون path دليلًا، وهو الدليل الأساسي لبدء البحث عن الملفات. عند استيراد الوحدات المجمّدة الناتجة، سيبدأ اسم الوحدة بعد path، أي أن path يُستبعَد من اسم الوحدة.

إذا كان path نسبيًا، فإنه يُحَل إلى manifest.py الحالي.

إذا كان script بقيمة None، فستُجمَّد جميع الملفات في path.

إذا كان script قابلًا للتكرار، فإن freeze() تُستدعى على جميع عناصر القابل للتكرار (مع تمرير path وopt نفسهما).

إذا كان script سلسلة نصية، فإنه يحدد الملف أو الدليل المراد تجميده، ويمكن أن يتضمن أدلة إضافية قبل الملف أو الدليل الأخير. سيُبحَث عن الملف أو الدليل في path. إذا كان script دليلًا فستُجمَّد جميع الملفات في ذلك الدليل.

opt هو مستوى التحسين المراد تمريره إلى mpy-cross عند ترجمة .py إلى .mpy. هذه المستويات موصوفة في micropython.opt_level().

freeze_as_str(path)

تجميد path المعطى وجميع برامج .py النصية ضمنه كسلسلة نصية، سيُترجَم عند الاستيراد. استخدم هذا فقط عندما يجب أن تبقى الشيفرة المجمّدة مصدر Python؛ فهو يكلّف RAM عند وقت الاستيراد مقارنةً بنُسَخ .mpy.

freeze_as_mpy(path, script=None, opt=0)

تجميد المدخل عبر ترجمة برامج .py النصية أولًا إلى ملفات .mpy، ثم تجميد ملفات .mpy الناتجة. وهذا ما تفعله module وpackage خلف الكواليس. راجع freeze() لمزيد من التفاصيل حول الوسائط.

freeze_mpy(path, script=None, opt=0)

تجميد المدخل، الذي يجب أن يكون ملفات .mpy تُجمَّد مباشرة (دون خطوة ترجمة). راجع freeze() لمزيد من التفاصيل حول الوسائط.

أمثلة

لتجميد ملف واحد من الدليل الحالي يكون متاحًا بـ import mydriver، استخدم:

module("mydriver.py")

لتجميد دليل من الملفات في دليل فرعي "mydriver" من الدليل الحالي يكون متاحًا بـ import mydriver، استخدم:

package("mydriver")

لتجميد مكتبة "hmac" من micropython-lib، استخدم:

require("hmac")

فيما يلي مثال أكثر اكتمالًا لملف manifest.py مخصص (للوحة لها بيانها الافتراضي الخاص):

# Include the board's default manifest.
include("$(BOARD_DIR)/manifest.py")
# Add a custom driver
module("mydriver.py")
# Add aiorepl from micropython-lib
require("aiorepl")

بعد ذلك يمكن ترجمة اللوحة بـ

$ cd ports/stm32
$ make BOARD=MYBOARD FROZEN_MANIFEST=~/src/myproject/manifest.py

لاحظ أن معظم اللوحات ليس لها manifest.py خاص بها، بل تستخدم بيان المنفذ مباشرة، وفي هذه الحالة ينبغي لبيانك فقط أن يقوم بـ include("$(PORT_DIR)/boards/manifest.py") بدلًا من ذلك.