פרויקט מסכם
עד עכשיו כל פרק עסק בדבר אחד. הפרויקט מחבר אותם, ומתעוררת בו בעיה חדשה: איך בונים משהו גדול בלי ללכת לאיבוד בו? התשובה בפרק הזה פשוטה: כותבים מה שרוצים לפני שמתכנתים, מסדרים את הקבצים, מוסיפים דבר אחד בכל פעם, ורושמים מה עשינו.
- נכיר את מה שתכנית הלימודים דורשת מפרויקט יחיד ומפרויקט זוגי.
- נכתוב רעיון, תרשים מלבנים ומפרט שאפשר לבדוק.
- נבנה דוגמה שלמה משני לוחות, מסודרת בעץ קבצים ברור, צעד אחר צעד.
- נפריד בין תקלה בחומרה לתקלה בתוכנה, בעזרת רב-מודד ומשקף תנודות.
- נתעד מה נכתב בעזרת בינה מלאכותית, ונכתוב יומן עבודה וספר פרויקט.
21.1 מה דורשת תכנית הלימודים? (Project Requirements)
| סוג | הנדרש |
|---|---|
| פרויקט יחיד | מיקרו-בקר שמתחבר למערכת אחרת דרך Bluetooth או WiFi או NFC או RFID או ZigBee. |
| פרויקט זוגי | שני מיקרו-בקרים לפחות, ואחד מהם מחובר לאינטרנט. |
בדוגמה שאנחנו בונים כאן יש שני לוחות, ושניהם מגיעים למתווך MQTT באינטרנט, ולכן היא עומדת בתנאי של פרויקט זוגי. מה שישתנה בפרויקט שלכם הוא החיישנים והמטרה, והדרך אחת.
21.2 תכנון: רעיון, תרשים מלבנים ומפרט (Planning)
הרעיון, במשפט: מפקח על חממה קטנה: לוח בחממה קורא טמפרטורה, לחות ואור ומפעיל מאוורר, ולוח במשרד מציג את המצב ומחליט מתי המאוורר פועל.
מפרט: כל שורה צריכה להיות ניתנת לבדיקה בכן או לא. המשפט "הפרויקט מהיר" אינו מפרט, והמשפט "נשלחת קריאה כל 10 שניות" הוא מפרט:
| המפרט | איך בודקים | |
|---|---|---|
| 1 | צומת החיישנים שולח קריאה כל 10 שניות. | קוראים את הנושא במחשב ומודדים את הזמן. |
| 2 | המסך מציג טמפרטורה, לחות, אור ומצב המאוורר. | משווים את המסך לקריאה. |
| 3 | המאוורר פועל מעל 30 מעלות, ונעצר מתחת ל-27. | מחממים את החיישן בקרבת היד ועוקבים. |
| 4 | אחרי שתיקה של 30 שניות המסך כותב NO DATA. | מנתקים את הזנת צומת החיישנים. |
| 5 | ההודעות חוזרות מעצמן אחרי הפסקה ברשת, בלי ללחוץ על דבר. | מכבים את הנתב לדקה ומדליקים אותו. |
21.3 עץ הקבצים (Project Tree)
מחליטים איפה כל דבר נמצא לפני שכותבים. הכלל: מה ששני הלוחות חולקים נמצא ב-shared, מה ששייך ללוח אחד נמצא בתיקייה שלו, והקובץ main.py קצר וקורא לאחרים. בלוח עצמו מעתיקים יחד את קבצי shared ואת קבצי תיקיית הצומת אל התיקייה הראשית, כי התוכנית מחפשת שם את הספריות שלה.
main.py נושאים את השמות שכתובים לידם, ולכן משנים את שמם בהעתקה ללוח.הקובץ המשותף gh_link.py: שמות הנושאים במקום אחד, ולכן אי אפשר שיהיה ביניהם הבדל בין שני הלוחות. החיבור והחיבור מחדש הם מפרק 16.
# gh_link.py - the greenhouse MQTT link, the same on both nodes.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import json
import time
from umqtt.simple import MQTTClient
import wifi
BROKER = "broker.hivemq.com"
DATA = b"school/gh1/data"
FAN_SET = b"school/gh1/fan/set"
FAN_STATE = b"school/gh1/fan/state"
class Link:
def __init__(self, client_id, topics, handler):
self.client = MQTTClient(client_id, BROKER)
self.topics = topics
self.handler = handler
self.client.set_callback(self._deliver)
def _deliver(self, topic, msg):
try:
obj = json.loads(msg)
except ValueError:
print("bad message on", topic)
return
self.handler(topic, obj)
def connect(self, tries=4):
wifi.connect()
wait = 1
for _ in range(tries):
try:
self.client.connect()
for topic in self.topics:
self.client.subscribe(topic)
return True
except OSError:
print("no broker, waiting", wait, "s")
time.sleep(wait)
wait *= 2
return False
def send(self, topic, obj):
msg = json.dumps(obj).encode()
try:
self.client.publish(topic, msg)
except OSError:
if self.connect():
self.client.publish(topic, msg)
def poll(self):
try:
self.client.check_msg()
except OSError:
self.connect()gh_link.py ולא בכל main.py?21.4 בנייה צעד אחר צעד (Building Step by Step)
לא כותבים הכול ואז מנסים. מוסיפים מנגנון אחד, מנסים אותו לבדו, ולא מתקדמים עד שהוא עובד. וכשמופיעה תקלה אנחנו יודעים איפה לחפש: במה שהוספנו אחרון.
| צעד | המנגנון החדש | המפרט | יודעים שזה עובד כאשר |
|---|---|---|---|
| 1 | החיישנים, בלי רשת | — | מופיעות קריאות סבירות בחלון ההדפסה. |
| 2 | פרסום למתווך | 1 | הקריאות מופיעות במחשב. |
| 3 | המסך מציג את מה שמגיע | 2 | המסך משתנה עם הטמפרטורה. |
| 4 | ההחלטה ופקודת המאוורר | 3 | המאוורר פועל ומצבו חוזר למסך. |
| 5 | שתיקה והפסקה ברשת | 4, 5 | מנתקים את ההזנה או את הרשת, והמכשיר מתנהג כפי שכתבנו. |
צעד 1: החיישנים:
None והתוכנית אינה נעצרת.# gh_sense.py - the sensors of the greenhouse, as one reading.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import dht
from machine import Pin
import board
class Sensors:
def __init__(self):
self.air = dht.DHT22(Pin(board.DHT))
self.ldr = board.analog_in(board.LDR)
def read(self):
try:
self.air.measure()
except OSError:
return None
return {"t": self.air.temperature(),
"h": self.air.humidity(),
"light": self.ldr.read_u16() * 100 // 65535}import time
from gh_sense import Sensors
sensors = Sensors()
for _ in range(3):
print(sensors.read())
time.sleep(10){'t': 24, 'light': 50, 'h': 60}
{'t': 24, 'light': 50, 'h': 60}
{'t': 24, 'light': 60, 'h': 60}צעדים 2 ו-3: פרסום ותצוגה:
# gh_view.py - what the greenhouse display shows, on a 16x2 LCD.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import board
from i2c_lcd import LCD
class View:
def __init__(self):
self.lcd = LCD(board.i2c())
def data(self, d, fan):
self.lcd.line(0, "T %.1fC H %.0f%%" % (d["t"], d["h"]))
self.lcd.line(1, "L %d%% Fan %s"
% (d["light"], "ON" if fan else "off"))
def no_data(self):
self.lcd.line(0, "NO DATA")
self.lcd.line(1, "check sensor")import time
from gh_view import View
from gh_link import Link, DATA
view = View()
def on_message(topic, obj):
print("got", obj)
view.data(obj, False)
link = Link("gh1-display", (DATA,), on_message)
link.connect()
for _ in range(25):
link.poll()
time.sleep(1)got {'h': 60, 't': 24.5, 'light': 50}
got {'h': 58, 't': 25.0, 'light': 61}
[LCD]
|T 25.0C H 58% |
|L 61% Fan off |
...צעדים 4 ו-5: שני הצמתים בצורתם הסופית:
main.py בצומת החיישנים: מפרסם כל 10 שניות, מבצע את הפקודה ומחזיר את המצב. פקודת המאוורר באה מן העולם המדומה.import time
import machine
from machine import Pin, WDT
import board
from gh_sense import Sensors
from gh_link import Link, DATA, FAN_SET, FAN_STATE
SEND_EVERY_MS = 10000
fan = Pin(board.LED, Pin.OUT)
sensors = Sensors()
def on_message(topic, cmd):
fan.value(1 if cmd["on"] else 0)
link.send(FAN_STATE, {"on": fan.value()})
print("fan:", fan.value())
link = Link("gh1-sensor", (FAN_SET,), on_message)
if not link.connect():
time.sleep(30)
machine.reset()
guard = WDT(timeout=8000)
next_send = time.ticks_ms()
while True:
guard.feed()
link.poll()
if time.ticks_diff(time.ticks_ms(), next_send) >= 0:
next_send = time.ticks_add(next_send, SEND_EVERY_MS)
data = sensors.read()
if data:
link.send(DATA, data)
else:
print("sensor failed")
time.sleep_ms(100)fan: 1
[mqtt_pub]
school/gh1/data {"t": 31, "light": 60, "h": 60}
school/gh1/data {"t": 31, "light": 60, "h": 60}
school/gh1/fan/state {"on": 1}
school/gh1/data {"t": 31, "light": 60, "h": 60}
...WDT(timeout=8000) בחרנו כדי שיתאים לשני הלוחות: הפסק המרבי ב-RP2040 לפי התיעוד הוא 8388 ms. יוצרים את כלב השמירה אחרי החיבור הראשון, כי החיבור לוקח יותר מזה, ואי אפשר לעצור כלב שמירה אחרי שהופעל. אם התוכנית נתקעת או שההמתנה למתווך מתארכת, כלב השמירה מאתחל את הלוח, וזה מקובל במכשיר שעובד לבד.עכשיו main.py בצומת התצוגה. שימו לב בו ל-want ול-have: הראשון הוא מה שאנחנו רוצים, והשני הוא מה שהלוח האחר אומר. כל עוד הם שונים חוזרים על הפקודה עם כל קריאה. לכן אם הודעה אבדה, היא תתוקן אחרי 10 שניות. זה חוג סגור דרך הרשת, ברעיון של פרק 20: מודדים, משווים ומתקנים. בפלטים שלהלן ההודעות שותקות אחרי t = 25, כאילו החיישן התנתק.
import time
from gh_view import View
from gh_link import Link, DATA, FAN_SET, FAN_STATE
FAN_ON = 30.0
FAN_OFF = 27.0
STALE_MS = 30000
view = View()
want = False
have = False
seen = time.ticks_ms()
stale = False
def on_data(d):
global want, seen, stale
seen = time.ticks_ms()
stale = False
if d["t"] > FAN_ON:
want = True
elif d["t"] < FAN_OFF:
want = False
if want != have:
print("T", d["t"], "ask fan", want)
link.send(FAN_SET, {"on": want})
view.data(d, have)
def on_message(topic, obj):
global have
if topic == DATA:
on_data(obj)
elif topic == FAN_STATE:
have = bool(obj["on"])
print("fan is", have)
link = Link("gh1-display", (DATA, FAN_STATE), on_message)
link.connect()
while True:
link.poll()
late = time.ticks_diff(time.ticks_ms(), seen)
if late > STALE_MS and not stale:
stale = True
print("silent for", late // 1000, "s")
view.no_data()
time.sleep_ms(100)T 31.0 ask fan True
fan is True
T 26.0 ask fan False
fan is False
silent for 30 s
[mqtt_pub]
school/gh1/fan/set {"on": true}
school/gh1/fan/set {"on": false}
[LCD]
|NO DATA |
|check sensor |
...21.5 איתור תקלות: חומרה או תוכנה? (Troubleshooting)
השאלה הראשונה היא איפה התקלה, ולא איך מתקנים אותה. מחלקים את השרשרת לשני חצאים, בודקים באמצע, ויודעים איזה חצי תקין. בפרויקט שלנו המתווך הוא האמצע הטבעי: אילו קריאות מגיעות אליו?
mosquitto_sub -h broker.hivemq.com -t "school/gh1/#" -v
אם הקריאות מופיעות, צומת החיישנים תקין והתקלה בצד התצוגה. אם לא, ההפך. כדי לבדוק את כל החומרה כותבים תוכנית קטנה שאינה יודעת דבר על הפרויקט:
import board
import dht
from machine import Pin
import wifi
def lcd():
return 0x27 in board.i2c().scan()
def air():
s = dht.DHT22(Pin(board.DHT))
s.measure()
return s.humidity() > 0
def light():
return 1000 < board.analog_in(board.LDR).read_u16() < 64500
def net():
return wifi.connect(5).isconnected()
for name, test in (("lcd", lcd), ("air", air),
("light", light), ("net", net)):
try:
ok = test()
except OSError:
ok = False
print("%-6s %s" % (name, "PASS" if ok else "FAIL"))lcd FAIL air PASS light FAIL net FAIL
כשהכול מחובר התוכנית נותנת: lcd PASS, air PASS, light PASS, net PASS. כל שורה FAIL פירושה תקלה בחומרה או בחיווט, ולכן לא מחפשים ב-main.py לפני שמתקנים אותה.
כשחושדים בחומרה לוקחים כלי מדידה. הרב-מודד נותן ערך אחד קבוע, ומתאים למה שאינו משתנה מהר. משקף התנודות מצייר את המתח לאורך הזמן, ומתאים למה שמשתנה:
| התסמין | בודקים קודם | הכלי |
|---|---|---|
| הלוח אינו עובד או מתאתחל | מתח 3V3 על ההדק, ואיכות ההזנה כשמפעילים עומס | רב-מודד, ומשקף תנודות לצניחה רגעית |
| חיישן שאינו משתנה | המתח על ההדק עצמו ולא על החיישן, והמשכיות החוטים | רב-מודד |
scan() ריק | ההזנה, החלפת SDA ו-SCL, ונגדי המשיכה: שני הקווים גבוהים במנוחה | רב-מודד, ואחר כך משקף תנודות לפולסים |
| מאוורר שאינו מסתובב וההדק משתנה | המתח על הבסיס, הזנת המנוע, והארקה המשותפת | רב-מודד |
| PWM אינו פועל כמצופה | הצורה, התדר ומחזור העבודה | משקף תנודות. הרב-מודד מציג את הממוצע בלבד |
| ההודעה אינה מגיעה והחומרה תקינה | שם הנושא, מזהה הלקוח, ו-check_msg() בלולאה | התוכנית וההדפסה |
הוא מציג בקירוב את הממוצע, כי הרב-מודד איטי מן הפולסים. זה אינו אומר שההדק נמצא במתח הזה: משקף התנודות מראה שהוא קופץ בין 0 ל-3.3 V.
mosquitto_sub מציגה קריאות חדשות. איפה התקלה?21.6 שימוש בבינה מלאכותית: חובת תיעוד (Using AI)
מותר להשתמש בכלי בינה מלאכותית בפרויקט, אבל תכנית הלימודים מחייבת אותנו (פרק 8 בה) לתעד מה נכתב בעזרתם. אין די לומר בסוף "השתמשנו": רושמים זאת כשזה קורה, ב-docs/ai_log.md.
## 2026-11-14 - gh_link.py, function connect() Tool and date: (name of the tool), 2026-11-14 Asked: "retry MQTT connect with growing waits in MicroPython" Took: the loop with wait *= 2 Changed: removed a call that does not exist in umqtt.simple Checked: ran with the broker off, read every line, can explain it
- מבינים כל שורה: בהצגה עשויים לבקש מאיתנו להסביר כל שורה. מה שאיננו מסוגלים להסביר אינו נכנס לפרויקט.
- מריצים ובודקים: הכלי כותב פקודות שנראות נכונות ואינן קיימות, או שהן ישנות, כמו
adc.width()בסעיף 6.4. המקור הוא תיעוד מיקרו-פייתון והניסוי. - לא שולחים סודות: הקובץ
secrets.pyלא עובר לשום כלי ולא לשום שיתוף. - רושמים את השינוי: מה ששינינו אחרי הכלי חשוב, כי הוא מראה מה הבנו.
gh_view.py כמות שהיא בלי שינוי, והיא עבדה מהפעם הראשונה. מה היא רושמת?21.7 יומן עבודה וספר פרויקט (Logbook and Project Book)
יומן עבודה כותבים בכל מפגש, בכמה שורות. אחרי חודש לא נזכור למה החלפנו את הנגד ההוא, והיומן זוכר. הוא גם חומר הגלם של ספר הפרויקט.
## 2026-11-12 (2 hours) - Sara, Omar Goal: stage 3, the display shows a real reading. Did: wired the LCD, wrote gh_view.py. Problem: LCD lit, no text. scan() returned []. Cause: SDA and SCL swapped. Found with scan(), fixed the wires. Next: stage 4, the fan command.
ספר פרויקט כותבים לקורא שאינו יודע דבר על הפרויקט שלנו. זה סדר מוצע לחלקיו:
| החלק | מה בו | מאיפה |
|---|---|---|
| הרעיון והמטרה | הבעיה ומה שהמכשיר עושה | 21.2 |
| התרשים והמפרט | תרשים המלבנים וטבלת המפרט | 21.2 |
| החומרה | השרטוטים, רשימת החלקים וההדקים | המעגלים שבנינו |
| התוכנה | עץ הקבצים, תפקיד כל קובץ והנושאים | 21.3 |
| הבדיקה | טבלת המפרט ותוצאת כל שורה | 21.4 |
| תקלות ומה שלמדנו | החשוב שקרה ואיך ניתחנו אותו | יומן העבודה |
| שימוש בבינה מלאכותית | סיכום היומן | ai_log.md |
עיקרי הפרק:
- כותבים את הרעיון, התרשים והמפרט לפני התוכנה, וכל פריט במפרט ניתן לבדיקה בכן או לא.
- עץ הקבצים מחליט איפה כל דבר, ולמה ששני הלוחות חולקים יש מקום אחד.
- מנגנון אחד בכל צעד, ולכן יודעים איפה לחפש כשיש תקלה.
- מחלקים את השרשרת לשני חצאים, ומפרידים חומרה מתוכנה בבדיקה עצמית ובכלי מדידה.
- מתעדים כל שימוש בבינה מלאכותית כשהוא קורה, ומבינים כל שורה שלוקחים.
- יומן העבודה נכתב בכל מפגש, ומתוכו נולד ספר הפרויקט.
פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| Link(id, topics, handler) | חיבור של צומת, מן הספרייה gh_link.py. |
| link.send(topic, obj) | פרסום מילון בתבנית JSON, עם חיבור מחדש בעת הצורך. |
| link.poll() | בדיקת הודעות נכנסות בלי להמתין. |
| WDT(timeout=8000) | כלב שמירה, עם פסק זמן שמתאים לשני הלוחות. |
| mosquitto_sub -t "a/#" -v | קריאת כל מה שמתפרסם תחת נושא, מן המחשב. |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| מפרט | Specification | المواصفات |
| צומת | Node | العقدة |
| עץ קבצים | File Tree | شجرة الملفات |
| רב-מודד | Multimeter | المقياس المتعدّد |
| משקף תנודות | Oscilloscope | راسم الذبذبات |
| יומן עבודה | Logbook | سجلّ العمل |
| ספר פרויקט | Project Book | كتاب المشروع |
| חממה | Greenhouse | الدفيئة |
want אינו משתנה, והוא שווה ל-have.שאלות לתרגול:
(א) המכשיר יפה.
(ב) הנורה נדלקת תוך שנייה מרגע שחושך.
(ג) הוא צורך מעט אנרגיה.
shared, והשני ב-node_display."up" ובו מספר השניות מאז ההפעלה. איזה קובץ משנים? ולמה gh_view.py נשאר כמו שהוא?gh_sense.py, בהוספת time.ticks_ms() // 1000. המסך קורא את השדות שהוא מכיר, ושדה חדש אינו משפיע עליו.FAN_OFF ב-gh_display_main.py. תחום ההיסטרזיס גדל, ולכן התנודה גדלה וההחלפות פוחתות.connect() אם הוא נכשל כולו? ומה קורה אם המתווך מתנתק באמצע העבודה?(א) האם צריך קריאה מהירה יותר לפי פרק 20?
(ב) מה מונע מאיתנו לקרוא כל שנייה בחיישן DHT22?
(ב) איזה מנגנון מפרק 16 מתאים?
set_last_will: המתווך שולח הודעה בשם הלוח כשהחיבור שלו נפסק, ולא מיד, אלא אחרי שפג פסק הזמן של keepalive.