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. הוא לא עובד עבור קבצים גדולים מספיק כך שקובץ ה-tmp מנצל את המקום הפנוי של מערכת הקבצים; שמור אותו לרשומות קטנות.

14.3.3.3. מלכודת הספרייה האיטית

ה-VFS של MicroPython אינו מאנדקס את תוכן הספריות באופן שבו מערכת קבצים שולחנית עושה זאת. 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() כנגד כל ספרייה בודדת נשאר זול, ולולאת הפריימים של היישום אינה נתקעת על פעולות קבצים ככל שההתקנה מתיישנת.

אותו עיקרון חל על לכידות תמונה, עקבות חיישן, או כל דבר אחר שהיישום כותב עבורו קובץ לכל אירוע. אם קצב האירועים גבוה, העץ צריך להיות עמוק יותר (year/month/day/hour, או year/month/day/hour/minute) כך שכל ספריית עלה תישאר קטנה. אם קצב האירועים נמוך, עץ year/month מספיק.

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>/ – לכידות תמונה או וידאו שהיישום מבצע. אותה צורת עץ, אותה סיבה.

פריסה זו עולה ליישום בערך עשרים שורות קוד וחוסכת ממנו את מצבי הכשל שמפילים את המצלמה חודשים לתוך התקנה.