14.3.3. ความสะอาดของระบบไฟล์

พื้นที่เก็บข้อมูลแบบ Flash และ SD บนกล้องที่จัดส่งแล้วจะเต็มไปด้วยไฟล์ที่ไม่มีผู้ดูแลระบบคนไหนล้างออกด้วยมือ การตัดสินใจสองประการเกี่ยวกับพื้นที่เก็บข้อมูลนั้นจะอยู่กับผลิตภัณฑ์ตลอดอายุการใช้งาน: พื้นผิวใดเก็บข้อมูลประเภทใด และ โครงสร้างไดเรกทอรีจัดการอย่างไรเพื่อให้การดำเนินการไฟล์ยังคงทำงานได้เมื่อแอปพลิเคชันสะสมระเบียนข้อมูล

14.3.3.1. ตำแหน่งที่เหมาะสม

โค้ดและสินทรัพย์ถูกเก็บไว้ในโมดูลที่ตรึงไว้และ ROMFS ที่การ build กำหนดในเวลาที่จัดส่ง สถานะ ของแอปพลิเคชัน -- สิ่งที่แอปพลิเคชันเขียนขณะรันไทม์ สิ่งที่เติบโตขึ้น สิ่งที่เปลี่ยนแปลงระหว่างการบูต -- ต้องอยู่ที่อื่น กล้องมีพื้นผิวที่เขียนได้สองแห่งสำหรับสิ่งนี้:

  • Internal flash ที่ /flash: ระบบไฟล์ที่เขียนได้ขนาดเล็กที่ถูก mount ก่อนโค้ดแอปพลิเคชันใดๆ จะรัน เหมาะสำหรับ ระเบียนขนาดคงที่ขนาดเล็กที่รอดชีวิตจากการรีบูต: การตั้งค่าที่แอปพลิเคชันอัปเดตขณะรันไทม์ การสอบเทียบล่าสุดที่ทราบ ตัวนับแบบหมุนเวียน ไฟล์มาร์กเกอร์หนึ่งบรรทัดที่บอกว่า "กล้องนี้ถูกจัดเตรียมแล้ว" รอบการเขียนมีจำกัด -- Internal flash สมัยใหม่รองรับการเขียนหลายพันถึงหลายหมื่นครั้งต่อเซกเตอร์ ไม่ใช่หลายล้านครั้ง ดังนั้นการเขียนต้องไม่บ่อยครั้ง ไม่ใช่ทุกเฟรม

  • SD card ที่ /sdcard: ระบบไฟล์ที่เขียนได้ขนาดใหญ่ที่ถูก mount เมื่อมีการ์ดอยู่ เหมาะสำหรับ ไฟล์ขนาดใหญ่ที่แปรผัน: การบันทึกภาพและวิดีโอ ไฟล์บันทึก ข้อมูลการปรับแต่งโมเดล สิ่งใดก็ตามที่อาจเติบโตถึงเมกะไบต์หรือกิกะไบต์ ความจุการเขียนสูงกว่า Internal flash แต่ก็ยังจำกัด ถอดออกได้ เปลี่ยนได้ และเป็นพื้นผิวที่มีแนวโน้มสูงสุดที่จะหายไปเมื่อแอปพลิเคชันกำลังเขียนอยู่

คำตอบที่ถูกต้องสำหรับ ตำแหน่งที่จะเขียนบางอย่าง คือ "flash สำหรับระเบียนคงที่ขนาดเล็ก, SD สำหรับทุกอย่างอื่น" ทั้งสองไม่สามารถสลับกันได้: แอปพลิเคชันที่เขียนไฟล์บันทึกแบบหมุนเวียนลงใน /flash จะเผาผลาญความทนทานการเขียนของ flash ในการ deploy ที่จะทำงานได้ดีบน SD

14.3.3.2. ถือว่าทั้งสองล้มเหลวได้

/flash และ /sdcard ทั้งคู่อาจล้มเหลวได้ SD card สามารถถูกดึงออก flash อาจเสียหายจากการตัดไฟกลางการเขียน ทั้งคู่อาจหมดพื้นที่ และการดำเนินการใดๆ ที่ทั้งคู่สามารถยก OSError ด้วยเหตุผลที่แอปพลิเคชันจะไม่มีโอกาสวินิจฉัยในภาคสนาม

สองรูปแบบช่วยให้แอปพลิเคชันรอดชีวิตจากสิ่งนั้น:

  • ห่อ mount และการดำเนินการใน try block. ทุก open(), os.listdir(), os.rename() ต่อเส้นทางข้อมูลผู้ใช้อาจล้มเหลว ดัก OSError บันทึกมัน และกลับไปใช้ทางเลือกที่กำหนด -- เขียนลงใน /flash ถ้า /sdcard หายไป ข้ามการดำเนินการถ้าทั้งคู่ไม่พร้อมใช้งาน

  • การเขียนแบบ Atomic สำหรับไฟล์ที่ต้องรอดชีวิตจากการตัดไฟ. เขียนไปยังเส้นทางชั่วคราว ปิด handle แล้ว os.rename() ทับชื่อที่ใช้งานอยู่ ไม่ว่าการ 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)
    

    รูปแบบนี้ทำงานทั้งบน flash และ SD แต่ ไม่ ทำงานสำหรับไฟล์ขนาดใหญ่พอที่ไฟล์ tmp ใช้พื้นที่ว่างของระบบไฟล์หมด สงวนไว้สำหรับระเบียนขนาดเล็ก

14.3.3.3. กับดักไดเรกทอรีช้า

MicroPython VFS ไม่ได้สร้างดัชนีเนื้อหาไดเรกทอรีแบบเดียวกับระบบไฟล์ Desktop os.listdir() และ os.stat() เดินผ่านตารางไฟล์พื้นฐานตามลำดับเชิงเส้น ไดเรกทอรีที่มีไฟล์หนึ่งร้อยไฟล์ก็ดี แต่ไดเรกทอรีที่มีหนึ่งหมื่นไฟล์ช้าจนใช้ไม่ได้ โดย os.listdir() ทุกครั้งใช้เวลาหลายวินาทีและ open() ทุกครั้งตรวจสอบกับตารางระหว่างทาง

แอปพลิเคชันที่เขียนบันทึกหรือการบันทึกภาพลงดิสก์พบปัญหานี้เร็วที่สุด โครงสร้าง /sdcard/logs/<timestamp>.log แบบง่ายที่เปิดไฟล์ใหม่หนึ่งไฟล์ต่อนาทีจะเติมไดเรกทอรี logs/ ด้วยไฟล์ห้าแสนไฟล์ภายในหนึ่งปีของการ deploy ก่อนถึงเวลานั้น แอปพลิเคชันจะเริ่มพลาดอัตราเฟรมเพราะการเปิดไฟล์ทุกครั้งใช้เวลานานกว่าช่วงเวลาเฟรม

รูปแบบที่ถูกต้องคือแบ่งไฟล์ข้ามต้นไม้ของไดเรกทอรีย่อยตามวันที่เพื่อให้ไม่มีไดเรกทอรีใดไดเรกทอรีหนึ่งมีมากกว่าสองสามร้อยรายการ:

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() ต่อไดเรกทอรีใดก็ตามยังคงถูก และ frame loop ของแอปพลิเคชันไม่หยุดชะงักบนการดำเนินการไฟล์เมื่อการ deploy อายุมากขึ้น

หลักการเดียวกันนี้ใช้กับการบันทึกภาพ การติดตาม sensor หรืออะไรก็ตามที่แอปพลิเคชันเขียนไฟล์ต่อเหตุการณ์ หากอัตราเหตุการณ์สูง ต้นไม้ควรลึกกว่า (ปี/เดือน/วัน/ชั่วโมง หรือ ปี/เดือน/วัน/ชั่วโมง/นาที) เพื่อให้ไดเรกทอรีใบแต่ละใบยังคงเล็ก หากอัตราเหตุการณ์ต่ำ ต้นไม้ปี/เดือนก็เพียงพอ

14.3.3.4. เส้นทางเฉพาะอุปกรณ์

ในกองยานที่มีกล้องมากกว่าหนึ่งตัว ไฟล์บันทึกต้องระบุหน่วยทางกายภาพที่มาจากใด machine.unique_id() คืนตัวระบุฮาร์ดแวร์ที่ฝังอยู่ในกล้องจากโรงงาน มันเป็นค่าเดิมข้ามการรีบูต ข้ามการอัปเดต firmware และข้ามการสลับ SD card ฝังมันในเส้นทางบันทึกหรือในระเบียนบันทึก และผู้ดูแลระบบที่มองดูกองการ์ด 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 -- การตั้งค่า, การสอบเทียบ, มาร์กเกอร์การจัดเตรียม เขียนไม่บ่อย อ่านบ่อย รูปแบบ Atomic-rename สำหรับไฟล์ใดๆ ที่การสูญเสียจะทำลายการบูตครั้งถัดไป

  • /sdcard/logs/<unit-id>/<year>/<month>/<day>/<hour>.log -- บันทึกการดำเนินงาน เขียนต่อเนื่อง หมุนเวียนตามเส้นทาง ไม่เขียนผ่านไดเรกทอรีที่มีพี่น้องนับพัน

  • /sdcard/captures/<unit-id>/<year>/<month>/<day>/ -- การบันทึกภาพหรือวิดีโอที่แอปพลิเคชันทำ โครงสร้างต้นไม้เดียวกัน เหตุผลเดียวกัน

เลย์เอาต์นั้นต้องใช้โค้ดแอปพลิเคชันประมาณยี่สิบบรรทัด และช่วยหลีกเลี่ยงรูปแบบความล้มเหลวที่ทำให้กล้องหยุดทำงานหลายเดือนหลังจาก deploy