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