קבצים ונתוני JSON
כל מה שנמצא במשתנים נעלם כשהחשמל מתנתק, ורק קובץ נשאר. בפרק הזה נלמד את הבקר לזכור: את ההגדרות שלו, את קריאות החיישנים ואת מספר ההפעלות. נלמד גם מה המחיר של הזיכרון הזה, כי כתיבה ב-flash אינה בחינם.
- נקרא קובץ, נכתוב אליו ונוסיף לו, וננהל קבצים בעזרת הפקודות של
os. - נשמור הגדרות בקובץ JSON שהפסקת חשמל באמצע הכתיבה אינה פוגעת בו.
- נרשום קריאות חיישן בקובץ CSV וננתח אותן על הלוח עצמו.
- נקטין את מספר הכתיבות ל-flash בעזרת חוצץ, ונבין מה מפסידים בתמורה.
- נחבר כרטיס 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 סוגרת את הקובץ ביציאה מהבלוק, גם אם אירעה בו שגיאה. זה חשוב, כי חלק מהכתיבה עלול להישאר בזיכרון ולא להגיע ל-flash לפני הסגירה.
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 |
|---|---|
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 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
בפרק 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, שמתחבר לאפיק 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): מספר שמתקבל מפונקציה חד-כיוונית. מהסיסמה קל לחשב את ערך הגיבוב, אבל מערך הגיבוב אי אפשר לחזור לסיסמה. כדי להשוות, מחשבים את ערך הגיבוב של מה שהמשתמש הקליד ומשווים בין שני הערכים. מלח (salt) הוא בתים אקראיים שמוסיפים לפני החישוב, וכך לשתי סיסמאות זהות יש שני ערכי גיבוב שונים.
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.