الملفات وبيانات JSON
كل ما في المتغيّرات يضيع عند انقطاع التيار. الملف يبقى. في هذا الفصل نجعل المتحكّم يتذكّر: إعداداته، وقراءات حسّاساته، وعدد مرات تشغيله. ونتعلّم أيضا ثمن هذه الذاكرة، فالكتابة في الفلاش ليست مجانية.
- نقرأ ملفا ونكتب فيه ونضيف إليه، وندير الملفات بأوامر
os. - نحفظ الإعدادات في ملف JSON لا يفسده انقطاع التيار أثناء الكتابة.
- نسجّل قراءات الحسّاس في ملف CSV ونحلّلها على اللوحة.
- نقلّل عدد مرات الكتابة في الفلاش بمخزن مؤقّت، ونعرف ما نخسره مقابل ذلك.
- نوصل بطاقة SD، ونحفظ كلمة مرور ببصمتها، ونعرف ما لا تحلّه البصمة.
12.1 نظام الملفات على اللوحة – מערכת הקבצים על הלוח (Filesystem)
جزء من ذاكرة الفلاش (זיכרון פלאש) في اللوحة مخصّص لنظام ملفات (מערכת קבצים). هناك يعيش boot.py وmain.py ومكتباتنا، كما رأينا في الفصل 1. والبرنامج يفتح ملفاته بالأمر نفسه الذي نعرفه من بايثون: open.
| الوضع | معناه | إذا كان الملف موجودا | إذا لم يكن موجودا |
|---|---|---|---|
"r" | قراءة، وهو الافتراضي | نقرأ من أوله | خطأ OSError |
"w" | كتابة | يُمحى محتواه فورا | يُنشأ |
"a" | إضافة | نكتب في آخره | يُنشأ |
"rb"، "wb" | بايتات بدل النص | كما سبق | كما سبق |
f.read().with open("note.txt", "w") as f:
count = f.write("line one\n")
f.write("line two\n")
print("characters in the first write:", count)
with open("note.txt", "a") as f:
f.write("line three\n")
with open("note.txt") as f:
for line in f:
print("->", line.strip())characters in the first write: 9 -> line one -> line two -> line three
العبارة with تغلق الملف عند الخروج من الكتلة، حتى لو وقع خطأ داخلها. وهذا مهم، لأن جزءا من الكتابة قد يبقى في الذاكرة ولا يصل إلى الفلاش قبل الإغلاق.
f.write يأخذ نصا فقط في الوضع النصي. لكتابة عدد نكتب "%.2f" % value أو str(value). وفي "w" يُمحى الملف عند الفتح، قبل أن نكتب أي شيء.12.2 إدارة الملفات بالوحدة os – ניהול קבצים
| الأمر | عمله |
|---|---|
| os.listdir() | قائمة بأسماء الملفات في المجلد الحالي. |
| os.stat(path)[6] | حجم الملف بالبايت. وإذا لم يكن الملف موجودا، خطأ OSError. |
| os.remove(path) | حذف ملف. |
| os.rename(old, new) | تغيير الاسم. |
| os.mkdir(path) | إنشاء مجلد. |
| os.statvfs("/") | معلومات نظام الملفات: الخانة 0 حجم الكتلة، والخانة 3 عدد الكتل الحرة، وحاصل ضربهما هو المساحة الحرة. |
import os
def texts():
return sorted(n for n in os.listdir() if n.endswith(".txt"))
def exists(path):
try:
os.stat(path)
return True
except OSError:
return False
for name in ("a.txt", "b.txt"):
with open(name, "w") as f:
f.write("12345")
print("files:", texts())
print("size of a.txt:", os.stat("a.txt")[6], "bytes")
os.rename("b.txt", "c.txt")
print("after rename:", texts())
os.remove("a.txt")
print("after remove:", texts())
print("a.txt exists:", exists("a.txt"))files: ['a.txt', 'b.txt'] size of a.txt: 5 bytes after rename: ['a.txt', 'c.txt'] after remove: ['c.txt'] a.txt exists: False
os.path المعروفة من بايثون، ولذلك كتبنا الدالة exists بأنفسنا.12.3 صيغة JSON – פורמט JSON (JSON)
| في بايثون | في JSON |
|---|---|
dict | {"a": 1} |
list، tuple | [1, 2] |
str | "text"، بعلامتي تنصيص مزدوجتين فقط |
int، float | 28، 28.5 |
True، False، None | true، false، null |
dumps من كائن إلى نص، وloads من نص إلى كائن.import json
node = {"name": "lab-1", "limit": 28.5, "on": True, "pins": [2, 13]}
text = json.dumps(node)
print(text)
back = json.loads(text)
print("limit + 1.5 =", "%.1f" % (back["limit"] + 1.5))
print("second pin =", back["pins"][1])
try:
json.loads("{'limit': 28}")
except ValueError:
print("single quotes are not JSON"){"limit": 28.5, "on": true, "name": "lab-1", "pins": [2, 13]}
limit + 1.5 = 30.0
second pin = 13
single quotes are not JSONdumps هنا الوسيط indent، فالنص يخرج في سطر واحد.json.loads("[1, 2]")[1] + 1؟12.4 ملف الإعدادات – קובץ הגדרות
العتبة وفترة القياس واسم الجهاز لا نريد أن نكتبها داخل البرنامج: نريد تغييرها دون تعديل الشيفرة. نضعها في ملف، ونواجه خطرين: أن يكون الملف غير موجود، وأن ينقطع التيار أثناء كتابته. المكتبة التالية تعالج الخطرين:
# settings.py - keep a dictionary of settings in a JSON file.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import json
import os
def load(path, defaults):
data = dict(defaults)
try:
with open(path) as f:
data.update(json.load(f))
except (OSError, ValueError):
pass
return data
def save(path, data):
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f)
os.rename(tmp, path)load: نبدأ من القيم الافتراضية، ونضع فوقها ما في الملف. وإذا كان الملف مفقودا أو معطوبا، تبقى القيم الافتراضية ولا يتوقف البرنامج. وإذا أضفنا لاحقا إعدادا جديدا، تأتي قيمته من الافتراضيات.save: نكتب أولا في ملف مؤقّت، ثم نغيّر اسمه. إذا انقطع التيار أثناء الكتابة، يبقى الملف القديم سليما.
import settings
PATH = "config.json"
DEFAULTS = {"limit": 30, "period_s": 60}
cfg = settings.load(PATH, DEFAULTS)
print("no file yet :", cfg["limit"], cfg["period_s"])
cfg["limit"] = 28
settings.save(PATH, cfg)
cfg = settings.load(PATH, DEFAULTS)
print("after save :", cfg["limit"], cfg["period_s"])
with open(PATH, "w") as f:
f.write('{"limit": 2')
cfg = settings.load(PATH, DEFAULTS)
print("broken file :", cfg["limit"], cfg["period_s"])no file yet : 30 60 after save : 28 60 broken file : 30 60
open(path, "w") مباشرة في save؟12.5 سجلّ البيانات في ملف CSV – רישום נתונים (Data Logger)
سجلّ البيانات (רישום נתונים) هو برنامج يحفظ قراءات الحسّاس على مدى الزمن. وأبسط صيغة له CSV: صف في كل سطر، والأعمدة تفصلها فواصل. وهذا الملف يفتحه أي برنامج جداول مباشرة بعد نسخه إلى الحاسوب.
قبل أن نبدأ نسأل: هل تكفي المساحة؟
والمساحة الحرة الفعلية تختلف بين لوحة وأخرى، فنقرؤها بالأمر os.statvfs. والتحليل يتم على اللوحة نفسها: نقرأ الملف سطرا سطرا ولا نستعمل f.read()، لأن السجلّ قد يكون أكبر من الذاكرة كلها.
readline.PATH = "log.csv"
with open(PATH, "w") as f:
f.write("t_s,volts\n0,1.20\n60,1.35\n120,1.80\n180,1.65\n")
count = 0
total = 0.0
high = 0.0
with open(PATH) as f:
f.readline()
for line in f:
value = float(line.split(",")[1])
count += 1
total += value
high = max(high, value)
print("rows:", count)
print("mean: %.3f V" % (total / count))
print("max : %.2f V" % high)rows: 4 mean: 1.500 V max : 1.80 V
"a" يكمل بعد إعادة التشغيل ولا يُمحى. أما الزمن من time.ticks_ms فيبدأ من جديد، فهو لا يصلح تاريخا. للتاريخ الحقيقي نحتاج إلى ساعة خارجية أو إلى الشبكة.12.6 اهتراء الفلاش: لا نكتب في كل ثانية – שחיקת flash (Flash Wear)
خلايا الفلاش تتحمّل عددا محدودا من دورات المحو والكتابة، وهذا العدد مذكور في ورقة بيانات شريحة الذاكرة، ويختلف من شريحة إلى أخرى. والكتابة المتكرّرة تستهلكها، حتى إذا وزّع نظام الملفات الكتابة على الخلايا. الحل هو المخزن المؤقّت (חוצץ): نجمع الصفوف في قائمة، ونكتبها معا في فتحة واحدة للملف.
import board
import os
import time
from convert import to_volts
PATH = "log.csv"
FLUSH_EVERY = 4
pot = board.analog_in(board.POT)
rows = []
for n in range(10):
rows.append("%d,%.2f\n" % (n, to_volts(pot.read_u16())))
if len(rows) >= FLUSH_EVERY:
with open(PATH, "a") as f:
f.write("".join(rows))
print("wrote", len(rows), "rows")
rows.clear()
time.sleep(1)
print("still in memory:", len(rows), "rows")
print("file size:", os.stat(PATH)[6], "bytes")wrote 4 rows wrote 4 rows still in memory: 2 rows file size: 56 bytes
| الطريقة | مرات فتح الملف في اليوم | أقصى خسارة عند انقطاع التيار |
|---|---|---|
| كتابة كل ثانية | 86400 | لا شيء تقريبا |
| صف كل دقيقة، وكتابة كل 10 صفوف | 144 | 10 صفوف، أي 10 دقائق من القياس |
القاعدة: نقرّر كم بيانات نقبل أن نخسر، ونكتب بهذا القدر فقط. وملف الإعدادات نكتبه عند تغيّر القيمة فقط، ولا نكتبه في حلقة.
while True يكتب في ملف في كل دورة بلا sleep. يعمل جيدا في المختبر، ويستهلك الفلاش بصمت. وتوثيق مايكروبايثون يوصي بنظام LittleFS لأنه أقوى أمام التلف، ويذكر أن نظام FAT لا يتحمّل انقطاع التيار أثناء الكتابة.12.7 الملف boot.py – הקובץ boot.py
عرفنا في الفصل 1 أن اللوحة تشغّل boot.py ثم main.py. نضع في boot.py ما يجب أن يكون جاهزا قبل المشروع، مثل مجلد السجلات، أو تركيب بطاقة SD. وهو ينتهي دائما.
import os
try:
os.mkdir("logs")
except OSError:
passوهذا main.py فيه طريق نجاة: إذا كان الزر مضغوطا عند التشغيل لا يبدأ المشروع، فنصل إلى اللوحة بسهولة ونصلح الملفات:
import board
from machine import Pin
if Pin(board.BTN, Pin.IN, Pin.PULL_UP).value() == 0:
print("safe mode: the project did not start")
else:
import appهذان الملفان لم يُشغَّلا في بناء الكتاب، لأن المُحاكي لا يعيد تشغيل اللوحة، وقد مرّا بالترجمة فقط.
12.8 بطاقة SD – כרטיס SD
للسجلات الطويلة نستعمل بطاقة SD، وهي تتصل بناقل SPI الذي عرفناه في الفصل 10. هنا تفترق اللوحتان: على ESP32 الصنف machine.SDCard جزء من البرنامج الثابت، وعلى Pico W نحتاج إلى المكتبة sdcard من micropython-lib، ونثبّتها كما في الفصل 11.
import machine
import os
import vfs
vfs.mount(machine.SDCard(slot=2), "/sd")
print(os.listdir("/sd"))
vfs.umount("/sd")import board
import os
import sdcard
import vfs
from machine import Pin
cs = Pin(board.CS, Pin.OUT, value=1)
sd = sdcard.SDCard(board.spi(), cs)
vfs.mount(vfs.VfsFat(sd), "/sd")
print(os.listdir("/sd"))
vfs.umount("/sd")هذان البرنامجان لم يُشغَّلا في بناء الكتاب، لأن المُحاكي لا يملك بطاقة ولا الصنف SDCard. والبطاقة تحتاج إلى تهيئة FAT، وهي تُفسد بسهولة إذا انقطع التيار أثناء الكتابة. لذلك نغلق الملف ونفصلها بالأمر vfs.umount قبل إخراجها.
12.9 كلمة المرور في ملف: البصمة – סיסמאות ב-hash (Hash)
إذا طلب جهازنا كلمة مرور، فأين نحفظها؟ ليس نصا في ملف. نحفظ بصمتها (ערך גיבוב, hash): عدد ناتج عن دالة تعمل في اتجاه واحد، فمن كلمة المرور نحسب البصمة بسهولة، ومن البصمة لا نستطيع العودة. وللمقارنة نحسب بصمة ما كتبه المستخدم ونقارن بين البصمتين. والملح (מלח) بايتات عشوائية نضيفها قبل الحساب، فتصبح لكلمتي مرور متماثلتين بصمتان مختلفتان.
import binascii
import hashlib
def digest(password, salt):
h = hashlib.sha256(salt + password.encode())
return binascii.hexlify(h.digest()).decode()
salt = b"salt-one"
stored = digest("blue-river-42", salt)
print(stored)
print("right password:", digest("blue-river-42", salt) == stored)
print("wrong password:", digest("1234", salt) == stored)
print("other salt :", digest("blue-river-42", b"salt-two") == stored)08cf86f399462d7b65c50c238a191145ed1e3fb227cf95eca7fe58e4e4cfaad2 right password: True wrong password: False other salt : False
os.urandom. على اللوحة نكتب salt = os.urandom(8)، فيكون لكل كلمة مرور ملح عشوائي، ونحفظه في الملف بجانب البصمة: هو ليس سرّا.والآن ما لا تحلّه البصمة:
- كلمة مرور WiFi: اللوحة تحتاج إلى النص نفسه لترسله إلى الشبكة، فلا يمكن حفظها كبصمة. لذلك تبقى في secrets.py، وكل من يصل إلى اللوحة يقرؤها.
- من يمسك اللوحة: يستطيع قراءة ملفاتها عبر USB، أو تغيير البصمة في الملف، أو تغيير البرنامج نفسه.
- كلمة المرور الضعيفة: SHA-256 سريعة، فتجربة قائمة من كلمات المرور الشائعة تبقى ممكنة لمن حصل على الملف.
- الطريق: كلمة مرور تمرّ عبر الشبكة بلا تشفير تُقرأ في الطريق، مهما حفظناها بعناية.
hashlib.sha256 موجودة دائما حسب توثيق مايكروبايثون، أما غيرها فيتعلّق بالبناء. ولا يجوز أن نترك في ملفات المشروع كلمة مرور حقيقية مثل تلك التي في المثال.أهمّ أفكار الفصل:
- الوضع
"w"يمحو عند الفتح، و"a"يضيف، وwithيغلق دائما. - الإعدادات في JSON مع قيم افتراضية، والكتابة في ملف مؤقّت ثم تغيير الاسم.
- السجلّ في CSV، ونحلّله سطرا سطرا لأنه قد يكون أكبر من الذاكرة.
- الفلاش يهترئ، فنكتب دفعات، ونقرّر مسبقا كم بيانات نقبل أن نخسر.
- البصمة مع الملح تحمي من قراءة كلمة المرور في الملف، ولا تحمي ممن يمسك اللوحة.
أوامر الفصل:
| الأمر | عمله |
|---|---|
| open(path, mode) | فتح ملف بالوضع "r" أو "w" أو "a". |
| f.read(), f.readline(), f.write(text) | قراءة الكل، قراءة سطر، كتابة نص. |
| json.dumps(obj), json.loads(text) | من كائن إلى نص، ومن نص إلى كائن. |
| json.dump(obj, f), json.load(f) | الأمران نفسهما مع ملف. |
| settings.load(path, defaults), settings.save(path, d) | إعدادات آمنة، من مكتبتنا settings.py. |
| machine.SDCard(slot=2) | بطاقة على ESP32 بنمط SPI. |
| vfs.mount(dev, "/sd"), vfs.umount("/sd") | تركيب نظام ملفات وفصله. |
| hashlib.sha256(data).digest() | بصمة SHA-256 من بايتات. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| نظام الملفات | מערכת קבצים | Filesystem |
| ذاكرة الفلاش | זיכרון פלאש | Flash Memory |
| سجلّ البيانات | רישום נתונים | Data Logger |
| المخزن المؤقّت | חוצץ | Buffer |
| الاهتراء | שחיקה | Wear |
| البصمة | ערך גיבוב | Hash |
| الملح | מלח | Salt |
"w" يمحو الملف عند الفتح. فإذا انقطع التيار قبل انتهاء الكتابة، ضاعت الإعدادات القديمة والجديدة معا.أسئلة للتمرين:
open("a.txt", "w").close(). ماذا بقي فيه؟"w" محا المحتوى عند الفتح، والملف الآن فارغ.t = 24.5 في ملف نصي؟f.write("%.1f" % t)، لأن write يأخذ نصا.json.dumps وjson.dump؟DEFAULTS مفتاحا جديدا، وعلى اللوحة ملف قديم لا يعرفه. ماذا يعيد settings.load؟s = settings.load(p, {"boots": 0})، ثم s["boots"] += 1، ثم settings.save(p, s).os.stat(path)[6] بالحد. إذا تجاوزه: نحذف القديم إن وُجد، ونستعمل os.rename.