14.2.2.1. تجميد البرامج النصية داخل البرنامج الثابت

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

تتناول هذه الصفحة تسلسل بدء التشغيل الذي تتّبعه الكاميرا، ثم كيف يقوم manifest.py وتوجيه freeze بدمج التطبيق في عملية البناء.

14.2.2.1.1. تسلسل بدء التشغيل

ما الذي يعمل، ومتى، على كاميرا تخرج من إعادة الضبط:

  • محمّل الإقلاع. يدخل التشغيل نافذة DFU قصيرة يستخدمها OpenMV 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، وعودة البرنامج النصي قيد التشغيل، والتعافي من المراقب (watchdog) -- قبل أن يصبح الـ REPL قابلًا للوصول. مهمته هي تحضير البيئة التي يعمل فيها بقية النظام: نوع الإعداد الذي يحتاج الـ REPL والتطبيق وأي أدوات تعافٍ إلى وجوده ليعمل. وهو ليس المكان الذي يقيم فيه التطبيق نفسه. فـ main.py هو نقطة دخول التطبيق.

  • الحلقة الرئيسية. main.py هو الحلقة الرئيسية للتطبيق. يعمل مرة واحدة عند الإقلاع البارد، فور انتهاء boot.py. ولا يُعاد تشغيله عند عمليات إعادة الضبط الناعمة اللاحقة -- بل تنتقل الكاميرا إلى الـ REPL بدلًا من ذلك. هذا التباين مهم للتطوير (إذ ينقل Ctrl-D إلى الـ REPL دون إعادة تشغيل الحلقة، حتى يتمكن المطوّر من فحص الحالة) لكنه ليس مهمًا للإنتاج: فالكاميرا الموزّعة ميدانيًا ترى عمليات التشغيل، والمراقب، وعمليات إعادة الضبط الصلبة، وهي جميعًا عمليات إعادة ضبط للعتاد تعيد الدخول إلى مسار الإقلاع البارد وتشغّل main.py مجددًا.

14.2.2.1.2. التجميد داخل البرنامج الثابت

تُعلَن مجموعة الوحدات المجمّدة الخاصة باللوحة في boards/<TARGET>/manifest.py ضمن شجرة البرنامج الثابت. والـ manifest هو ملف Python صغير يستدعي حفنة من التوجيهات:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") -- يدمج ملف foo.py واحدًا في البناء.

  • package("mylib", base_path="...") -- يدمج حزمة Python متعددة الملفات، مع الحفاظ على تخطيط مجلداتها تحت المسار الأساسي المعطى.

  • include("...") -- يستدعي ملف manifest آخر. تستخدمه manifest اللوحات لمشاركة مجموعات الوحدات المشتركة.

  • require("logging") -- يستدعي وحدة micropython-lib مُسمّاة من المصدر الأصلي بالاسم.

إن أبسط manifest للتطبيق يضيف سطر freeze واحدًا لكل برنامج نصي من المستوى الأعلى وسطر package واحدًا لكل حزمة يعتمد عليها التطبيق.

14.2.2.1.2.1. أين يقيم الكود المصدري

يقيم الكود المصدري للتطبيق تحت scripts/libraries/ في شجرة البرنامج الثابت، إلى جانب الوحدات التي يجمّدها البناء مسبقًا. ويتوسّع متغير الـ manifest المسمّى $(OMV_LIB_DIR) إلى ذلك المسار، لكي تبقى مدخلات الـ manifest قصيرة. وبما أن تحرير الـ manifest عملية داخل الشجرة أصلًا، فإن إبقاء الكود المصدري داخل الشجرة يتجنّب التعامل مع مستودع مشروع منفصل في عملية حل المسارات.

تخطيط نموذجي لتطبيق يشحن ملف 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. البناء والوميض (flashing)

بمجرد وضع الـ manifest في مكانه، ابنِ البرنامج الثابت تمامًا كما يصف فصل البرنامج الثابت

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 يضخّم الكود المصدري، ويعيد الترجمة ببطء، ويهدر حاوية الـ bytecode على بيانات سيقرؤها المفسّر خامًا على أي حال. تتناول صفحة بناء صورة ROMFS نظام الملفات المقيم في الفلاش للقراءة فقط الذي يسدّ هذه الفجوة.