14.3.3. نظافة نظام الملفات

تمتلئ وحدة التخزين على ذاكرة الفلاش وبطاقة SD في كاميرا مشحونة بملفات لن يقوم أي مشغّل بتنظيفها يدويًا. هناك قراران بشأن وحدة التخزين هذه يبقيان مع المنتج طوال عمره: أي سطح يحتفظ بأي نوع من البيانات، وكيفية هيكلة الأدلة بحيث تستمر عمليات الملفات في العمل مع تراكم السجلات لدى التطبيق.

14.3.3.1. أين توضع الأشياء

تنتقل الشيفرة والأصول ضمن الوحدات المجمّدة وROMFS التي تثبّتها عملية البناء وقت الشحن. أما حالة التطبيق -- أي شيء يكتبه التطبيق أثناء التشغيل، وأي شيء ينمو، وأي شيء يتغير بين عمليات الإقلاع -- فيجب أن يعيش في مكان آخر. تكشف الكاميرا عن سطحين قابلين للكتابة لهذا الغرض:

  • ذاكرة الفلاش الداخلية عند /flash: نظام ملفات صغير قابل للكتابة يُركّب قبل تشغيل أي شيفرة تطبيق. وهو المكان المناسب لـالسجلات الصغيرة ذات الحجم الثابت التي تنجو من إعادة التشغيل: الإعدادات التي يحدّثها التطبيق أثناء التشغيل، وآخر معايرة معروفة، وعدّاد متجدد، وملف علامة من سطر واحد يقول "تم تجهيز هذه الكاميرا." دورات كتابة محدودة -- تتحمل ذاكرة الفلاش الداخلية الحديثة آلافًا إلى عشرات الآلاف من عمليات الكتابة لكل قطاع، وليس الملايين، لذا يجب أن تكون عمليات الكتابة غير متكررة، لا لكل إطار.

  • بطاقة SD عند /sdcard: نظام ملفات أكبر قابل للكتابة يُركّب عند وجود بطاقة. وهو المكان المناسب لـالملفات الضخمة المتغيرة: التقاطات الصور والفيديو، وملفات السجل، وبيانات الضبط الدقيق للنموذج، وأي شيء قد ينمو إلى ميغابايتات أو غيغابايتات. سعة كتابة أعلى من ذاكرة الفلاش الداخلية لكنها لا تزال محدودة؛ وهي قابلة للإزالة والاستبدال، وهي السطح الأكثر عرضة للاختفاء بينما يكون التطبيق في منتصف الكتابة.

الإجابة الصحيحة عن أين تكتب شيئًا ما هي دائمًا تقريبًا "الفلاش للسجلات الصغيرة الثابتة، وSD لكل شيء آخر." والاثنان ليسا قابلين للتبادل: فالتطبيق الذي يخربش ملف سجله المتجدد إلى /flash سيستنزف قدرة احتمال الكتابة لذاكرة الفلاش في نشر كان سيكون على ما يرام على بطاقة SD.

14.3.3.2. تعامل مع كليهما على أنه قابل للفشل

يمكن أن يفشل كل من /flash و/sdcard. فبطاقة SD يمكن إخراجها، وذاكرة الفلاش يمكن أن تتلف بسبب انقطاع التيار في منتصف الكتابة، وكلاهما يمكن أن ينفد مساحته، وأي عملية على أيٍّ منهما يمكن أن ترفع OSError لأسباب لن يحظى التطبيق بفرصة لتشخيصها في الميدان.

هناك نمطان يجعلان التطبيق ينجو من ذلك:

  • غلّف عمليات التركيب والعمليات بكتل try. كل استدعاء لـ open() و os.listdir() و os.rename() على مسارات بيانات المستخدم قابل للفشل. التقط OSError، وسجّله، وارجع إلى بديل محدد -- اكتب إلى /flash إذا اختفت /sdcard، وتجاوز العملية إذا لم يكن أيٌّ منهما متاحًا.

  • عمليات كتابة ذرّية للملفات التي يجب أن تنجو من انقطاع التيار. اكتب إلى مسار مؤقت، وأغلق المقبض، ثم استخدم os.rename() فوق الاسم الحي. فإما أن تنجح إعادة التسمية ويصبح الملف هو النسخة الجديدة، أو لا تنجح فيبقى الملف هو النسخة القديمة. لا توجد حالة ثالثة يكون فيها الملف مكتوبًا نصفيًا:

    import os
    
    def write_config_atomic(path, contents):
        tmp = path + '.tmp'
        with open(tmp, 'w') as f:
            f.write(contents)
            f.flush()
        os.rename(tmp, path)
    

    يعمل النمط على كل من الفلاش وSD. لكنه لا يعمل مع الملفات الكبيرة بما يكفي بحيث يستهلك الملف المؤقت المساحة الحرة لنظام الملفات؛ احتفظ به للسجلات الصغيرة.

14.3.3.3. فخ الدليل البطيء

لا يفهرس MicroPython VFS محتويات الأدلة بالطريقة التي يفعلها نظام ملفات سطح المكتب. تجتاز os.listdir() و os.stat() جدول الملفات الأساسي خطيًا. فالدليل الذي يحتوي على مئة ملف لا بأس به؛ أما الدليل الذي يحتوي على عشرة آلاف ملف فبطيء إلى درجة عدم القابلية للاستخدام، إذ يستغرق كل استدعاء لـ os.listdir() ثوانٍ، وكل استدعاء لـ open() يتحقق من الجدول في طريقه عبره.

التطبيقات التي تكتب السجلات أو الالتقاطات إلى القرص تصطدم بهذا الأمر أسرع من غيرها. فمخطط ساذج مثل /sdcard/logs/<timestamp>.log يفتح ملفًا جديدًا واحدًا كل دقيقة سيملأ دليل logs/ بنصف مليون ملف في سنة من النشر. وقبل ذلك بزمن طويل يبدأ التطبيق بتفويت معدل إطاراته لأن كل فتح ملف يستغرق وقتًا أطول من الفاصل الزمني للإطار.

النمط الصحيح هو تقسيم الملفات عبر شجرة من الأدلة الفرعية المؤرّخة بحيث لا يحتفظ أي دليل واحد بأكثر من بضع مئات من الإدخالات أبدًا:

import os
import time

LOG_ROOT = '/sdcard/logs'

def log_path(now=None):
    if now is None:
        now = time.localtime()
    year, month, day, hour = now[0], now[1], now[2], now[3]
    directory = '{}/{:04d}/{:02d}/{:02d}'.format(
        LOG_ROOT, year, month, day)
    _makedirs(directory)
    return '{}/{:02d}.log'.format(directory, hour)

def _makedirs(path):
    # os.makedirs equivalent -- create each level if missing
    parts = path.split('/')
    for i in range(2, len(parts) + 1):
        sub = '/'.join(parts[:i])
        try:
            os.mkdir(sub)
        except OSError:
            pass

أصبحت سنة من التسجيل بمعدل ملف واحد لكل ساعة موزّعة الآن عبر 365 دليلًا يوميًا، يحتوي كل منها على 24 ملفًا كحد أقصى؛ ويبقى استدعاء os.listdir() على أي دليل واحد رخيصًا، ولا تتعثّر حلقة الإطارات في التطبيق على عمليات الملفات مع تقادم النشر.

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

14.3.3.4. المسارات الخاصة بكل جهاز

في أسطول من أكثر من كاميرا واحدة، تحتاج ملفات السجل إلى تحديد الوحدة المادية التي أتت منها. يعيد machine.unique_id() معرّفًا للأجهزة مدمجًا في الكاميرا في المصنع؛ وهو القيمة نفسها عبر عمليات إعادة التشغيل، وعبر تحديثات البرنامج الثابت، وعبر تبديلات بطاقة SD. ضمّنه في مسار السجل أو في سجلات السجل، وعندها يمكن لمشغّل ينظر إلى كومة من بطاقات SD أو سجل مركزي أن يميّز أيها هو أيّ:

import binascii
import machine

UNIT_ID = binascii.hexlify(machine.unique_id()).decode()

LOG_ROOT = '/sdcard/logs/' + UNIT_ID

بدمجه مع نمط الأدلة الفرعية المؤرّخة، يصبح التخطيط /sdcard/logs/<unit-id>/2026/06/09/14.log -- ساعة واحدة من سجلات وحدة واحدة، في دليل ضحل بما يكفي لاجتيازه، على مسار يسمّي الوحدة على نظام الملفات نفسه.

14.3.3.5. تجميع كل ذلك معًا

تبدو وحدة التخزين القابلة للكتابة في كاميرا مشحونة تقريبًا هكذا:

  • /flash -- الإعدادات، والمعايرة، وعلامة التجهيز. تُكتب نادرًا، وتُقرأ كثيرًا. نمط إعادة التسمية الذرّية لأي ملف يؤدي فقدانه إلى تعطيل الإقلاع التالي.

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log -- السجل التشغيلي. يُكتب باستمرار، ويُدوّر بحسب المسار، ولا يُكتب أبدًا عبر دليل يحتوي على آلاف الأشقاء.

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ -- التقاطات الصور أو الفيديو التي يجريها التطبيق. الشكل الشجري نفسه، وللسبب نفسه.

يكلّف هذا التخطيط التطبيق نحو عشرين سطرًا من الشيفرة، وينقذه من أنماط الفشل التي تُسقط الكاميرا بعد أشهر من النشر.