مشروع متكامل
كل فصل حتى الآن عالج أمرا واحدا. المشروع يجمعها، وتظهر فيه مشكلة جديدة: كيف نبني شيئا كبيرا دون أن نضيع فيه؟ الجواب في هذا الفصل بسيط: نكتب ما نريد قبل أن نبرمج، ونرتّب الملفات، ونضيف أمرا واحدا في كل مرة، ونسجّل ما فعلناه.
- نعرف ما يطلبه المنهج من مشروع فردي ومشروع زوجي.
- نكتب فكرة ومخطّطا صندوقيا ومواصفات تقبل الفحص.
- نبني مثالا كاملا من لوحتين مرتّبا في شجرة ملفات واضحة، خطوة بعد خطوة.
- نفصل بين عطل في العتاد وعطل في البرنامج، بمقياس متعدّد وراسم ذبذبات.
- نوثّق ما كُتب بمساعدة الذكاء الاصطناعي، ونكتب سجلّ العمل وكتاب المشروع.
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 استعمال الذكاء الاصطناعي: واجب التوثيق – שימוש ב-AI: חובת תיעוד (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.