البرمجة بلغة بايثون ومكتبة gpiozero
نترك الآن المتحكّم وننتقل إلى حاسوب كامل بحجم بطاقة. على Raspberry Pi 4/5 لا توجد مايكروبايثون ولا machine، بل لينكس وبايثون العادية. الأفكار التي تعلّمناها تبقى كما هي: طرف ومُخرَج ومُدخَل وحدث. لكن طريقة التثبيت والتشغيل والتوقيت تتغيّر، وهذا الفصل يبيّن ما يتغيّر وما يبقى.
- نجهّز نظام التشغيل ونتصل بالباي عبر SSH، ونعرف ما يختلف بين الباي 4 والباي 5.
- نعمل داخل بيئة افتراضية، ونفهم لماذا يرفض
pipالعمل خارجها. - نميّز بين ترقيم BCM والترقيم الفيزيائي، ونلتزم بجهد 3.3 V.
- نستعمل
LEDوButtonوPWMLEDوServoوDistanceSensorوالأحداث. - نشغّل برنامجنا تلقائيا عند الإقلاع بخدمة systemd.
22.1 تجهيز النظام والاتصال – התקנת המערכת וחיבור מרחוק (Setup and SSH)
قارنّا في القسم 1.2 بين اللوحات، ورأينا أن الباي حاسوب وليس متحكّما. عليه نظام التشغيل (מערכת הפעלה) Raspberry Pi OS، وبرنامجنا واحد من عشرات البرامج التي تعمل معا. التثبيت يتم مرة واحدة:
- نثبّت على حاسوبنا برنامج Raspberry Pi Imager من الموقع الرسمي، ونختار فيه طراز اللوحة والنظام وبطاقة microSD.
- قبل الكتابة نملأ إعدادات التخصيص: اسم الجهاز، المستخدم وكلمة المرور، شبكة WiFi، وتفعيل SSH. قد تختلف أسماء الشاشات بين الإصدارات.
- نضع البطاقة في الباي ونشغّله، وننتظر انتهاء الإقلاع الأول، ثم نتصل من حاسوبنا بسطر الأوامر (שורת פקודה):
ssh student@mypi.local sudo apt update sudo apt full-upgrade python --version sudo shutdown -h now
أوامر النظام في هذا الفصل لا تُنفَّذ في أثناء بناء الكتاب، لأنها تحتاج إلى باي حقيقي. أما برامج gpiozero التي تظهر تحتها شاشة فقد نُفّذت فعلا.
| Pi 4 | Pi 5 | |
|---|---|---|
| المصدر الذي توصي به الشركة | 5 V, 3 A | 5 V, 5 A |
| الأطراف | 40 طرفا، بالترتيب نفسه | 40 طرفا، بالترتيب نفسه |
| من يتحكّم بالأطراف | الشريحة الرئيسية | شريحة منفصلة: RP1 |
مكتبة gpiozero | تعمل | تعمل |
مكتبة RPi.GPIO | تعمل | لا تعمل |
sudo shutdown -h now، وننتظر توقّف الضوء الأخضر.22.2 البيئة الافتراضية – סביבה וירטואלית (Virtual Environment)
على المتحكّم كنا ننسخ المكتبة إلى اللوحة وكفى. أما على الباي فالنظام نفسه يعتمد على بايثون، ولو غيّرنا مكتبة يستعملها لتعطّل شيء آخر. لذلك يمنع Raspberry Pi OS، منذ إصدار Bookworm، تثبيت المكتبات بـpip مباشرة في بايثون النظام:
$ pip install smbus2 error: externally-managed-environment
sudo apt install python3-gpiozero mkdir ~/project cd ~/project python -m venv --system-site-packages env source env/bin/activate pip install smbus2 python blink.py deactivate
الخيار --system-site-packages يجعل البيئة ترى مكتبات النظام أيضا، ومنها gpiozero التي ثبّتناها بـapt. بعد التفعيل يظهر الاسم (env) في بداية السطر، ومن تلك اللحظة يكتب pip داخل المجلد env فقط. وفي الملف pi_i2c.py من الفصل 6 استعملنا smbus2، وهذا هو المكان الصحيح لتثبيتها.
python app.py، فظهر الخطأ أن المكتبة smbus2 غير موجودة، مع أننا ثبّتناها أمس. ما السبب؟22.3 ترقيم الأطراف والجهد – מספור הדקים ומתח (BCM Numbering)
لكل طرف في الباي رقمان: الرقم الفيزيائي (פיזי) هو موضعه على الموصل، من 1 إلى 40. ورقم BCM هو رقمه في الشريحة. مكتبة gpiozero تستعمل رقم BCM، فالأمر LED(17) يعني GPIO17، وهو الطرف الفيزيائي 11.
pinout يطبع الخريطة كاملة على الباي.نختار القيمة القياسية القريبة 330 Ω، فيصبح التيار نحو 3.94 mA.
22.4 أول برنامجين: الثنائي والزر – נורית ולחצן (LED, Button)
المكتبة gpiozero لا تبدأ من الطرف كما تفعل machine، بل من الجهاز: نقول «ثنائي باعث للضوء على 17»، فتتكفّل بالباقي. كل مُخرَج على الشاشات التالية جاء من تشغيل المكتبة الحقيقية على أطراف مُحاكاة، ولذلك هو مثالي ولا يدّعي قياسا على لوحة.
on() وoff()، والخاصية is_lit تخبرنا بالحالة.from gpiozero import LED
import time
led = LED(17)
for i in range(3):
led.on()
print("on, is_lit =", led.is_lit)
time.sleep(0.5)
led.off()
print("off, is_lit =", led.is_lit)
time.sleep(0.5)on, is_lit = True off, is_lit = False on, is_lit = True off, is_lit = False on, is_lit = True off, is_lit = False
والآن الزر. الصنف Button يفعّل مقاومة الرفع الداخلية من تلقاء نفسه، ثم يترجم المعنى لنا: is_pressed تعطي True عند الضغط، مع أن الطرف ينزل عندها إلى 0. لا حاجة إلى قلب القيمة كما فعلنا في الفصل 3.
from gpiozero import LED, Button
import time
led = LED(17)
button = Button(2)
for step in range(5):
pressed = button.is_pressed
led.value = pressed
print("t=%.1f pressed=%s lit=%s" % (step * 0.5, pressed, led.is_lit))
time.sleep(0.5)t=0.0 pressed=False lit=False t=0.5 pressed=True lit=True t=1.0 pressed=True lit=True t=1.5 pressed=False lit=False t=2.0 pressed=False lit=False
RPi.GPIO التي نجدها في أمثلة كثيرة على الإنترنت؟ لأنها تتعامل مباشرة مع مسجّلات الأطراف في الشريحة الرئيسية، وعلى الباي 5 انتقلت الأطراف إلى الشريحة RP1، فتتوقف المكتبة برسالة خطأ. أما gpiozero فتختار من تلقاء نفسها الطبقة المناسبة للوحة، وعلى الباي 5 هي lgpio. لذلك يعمل البرنامج نفسه على اللوحتين.22.5 الأحداث والانتظار – אירועים והמתנה (Events, pause)
حلقة الفحص تعمل، لكن قد تفوتها ضغطة قصيرة بين قراءتين. البديل هو الحدث (אירוע): نعطي المكتبة دالة، وهي تستدعيها عند الضغط.
pressed تُستدعى وحدها في كل منهما.from gpiozero import LED, Button
import time
led = LED(17)
button = Button(2)
count = 0
def pressed():
global count
count += 1
led.toggle()
print("press %d, lit=%s" % (count, led.is_lit))
button.when_pressed = pressed
for second in range(4):
time.sleep(1)
print("done, %d presses" % count)press 1, lit=True press 2, lit=False done, 2 presses
تشبه هذه الفكرة المقاطعة في الفصل 5، لكن الفرق كبير: الدالة هنا ليست مقاطعة حقيقية، بل تعمل في خيط (תהליכון) خلفي من بايثون. لذلك يجوز فيها ما كنا نتجنّبه في المقاطعة، مثل الطباعة وإنشاء القوائم. ثمن ذلك أن زمن الاستجابة أقل ثباتا.
حلقة for في البرنامج أعلاه موجودة للعرض فقط: هي تُبقي البرنامج حيا بضع ثوان ثم ينتهي، فنرى المُخرَج. في المشروع الحقيقي نضع مكانها pause()، وهي تنتظر إلى الأبد. لذلك لم نشغّل البرنامج التالي عند بناء الكتاب، فهو لا ينتهي:
bounce_time يعالج ارتداد التلامس، وpause() يُبقي البرنامج ينتظر الأحداث.from gpiozero import LED, Button from signal import pause led = LED(17) button = Button(2, bounce_time=0.05) button.when_pressed = led.toggle pause()
button.when_pressed = pressed بلا قوسين. مع القوسين تُستدعى الدالة مرة واحدة الآن، وتُسند نتيجتها None، فلا يحدث شيء عند الضغط.pause() من البرنامج الأخير. ماذا يحدث عند تشغيله؟22.6 السطوع والمحرّك المؤازر والمسافة – בהירות, סרוו ומרחק (PWMLED, Servo, DistanceSensor)
برنامج السطوع يعمل هنا على أطراف مُحاكاة تحفظ قيمة PWM فقط، فلا ضوء ولا نبضات. أما DistanceSensor فيحتاج إلى قياس زمن حقيقي، ولذلك لم يُنفَّذ برنامج الموقف في بناء الكتاب: مرّ بالترجمة فقط، وليس تحته شاشة.
value بين 0 و1، بدل duty_u16 بين 0 و65535. وتوجد أيضا led.pulse()، وهي تنبض في الخلفية.from gpiozero import PWMLED
import time
led = PWMLED(18, frequency=1000)
for percent in (0, 25, 50, 75, 100):
led.value = percent / 100
print("duty %3d%% value %.2f" % (percent, led.value))
time.sleep(1)duty 0% value 0.00 duty 25% value 0.25 duty 50% value 0.50 duty 75% value 0.75 duty 100% value 1.00
from gpiozero import Servo, DistanceSensor
import time
servo = Servo(18)
sensor = DistanceSensor(echo=24, trigger=23, max_distance=1)
while True:
metres = sensor.distance
print("%.2f m" % metres)
if metres < 0.2:
servo.max()
else:
servo.min()
time.sleep(0.5)نوصل تغذية المحرّك المؤازر بمصدر 5 V مستقل، مع أرضي مشترك، ونأخذ من الطرف إشارته فقط. وServo يولّد النبضات برمجيا، فقد يرتجف المحرّك قليلا، لأن لينكس قد يؤخّر البرنامج.
ولا يوجد في الباي محوّل تماثلي رقمي. لقراءة مقياس جهد أو مقاومة ضوئية نستعمل المحوّل الخارجي ADS1115 عبر I2C، كما فعلنا في الفصل 6.
22.7 ما الذي تغيّر عن مايكروبايثون؟ – ההבדלים ממיקרו-פייתון
| مايكروبايثون على المتحكّم | بايثون على الباي | |
|---|---|---|
| التشغيل | برنامج واحد يبدأ من main.py | برنامج بين برامج كثيرة، نبدؤه بأنفسنا أو بخدمة |
| الوصول إلى الأطراف | machine.Pin | gpiozero |
| رقم الطرف | رقم الشريحة، عبر board.py | BCM |
| مقاومة الرفع للزر | نطلبها: Pin.PULL_UP | تلقائية في Button |
| حدث عند الضغط | Pin.irq، مقاطعة قصيرة | when_pressed، خيط خلفي |
| قراءة تماثلية | ADC داخلي | محوّل خارجي فقط |
| تثبيت المكتبات | نسخ أو mip | pip في بيئة افتراضية، أو apt |
| دقة التوقيت | أعلى، فلا نظام يقاطع البرنامج | أقل: لينكس يوزّع الزمن بين البرامج |
| الإطفاء | نفصل الكهرباء | نطفئ النظام أولا |
السطر قبل الأخير يفسّر توزيع الأدوار في الفصل التالي: التوقيت الدقيق والقرب من الحسّاس للمتحكّم، والتخزين والشبكة وصفحات الويب للباي.
22.8 التشغيل التلقائي: الخدمة – הרצה אוטומטית (systemd)
على المتحكّم يبدأ main.py وحده عند التغذية. على الباي لا يحدث شيء من هذا: إذا أغلقنا نافذة SSH توقف البرنامج، وبعد إعادة الإقلاع لن يعمل. الحل هو الخدمة (שירות): ملف يصف لمدير النظام systemd كيف يشغّل برنامجنا. نحفظ برنامج pause() من القسم 22.5 باسم button_led.py ونكتب:
[Unit] Description=Button and LED After=multi-user.target [Service] User=student WorkingDirectory=/home/student/project ExecStart=/home/student/project/env/bin/python -u button_led.py Restart=on-failure [Install] WantedBy=multi-user.target
sudo nano /etc/systemd/system/button-led.service sudo systemctl daemon-reload sudo systemctl enable --now button-led systemctl status button-led journalctl -u button-led -f sudo systemctl stop button-led
ExecStart: يشير مباشرة إلى بايثون داخل البيئة، فلا حاجة إلىactivate.User: اسم المستخدم الذي اخترناه في التخصيص. لم يعد الاسمpiمضمونا.Restart=on-failure: يعيد التشغيل إذا انهار البرنامج.-u: يمنع تأخيرprint، فنرى المُخرَج فورا فيjournalctl.
أهمّ أفكار الفصل:
- الباي حاسوب عليه لينكس: نطفئه بأمر، ولا نفصل كهرباءه.
- كل مشروع في بيئة افتراضية، و
--system-site-packagesلرؤيةgpiozero. gpiozeroتستعمل رقم BCM، وتعمل على الباي 4 والباي 5. أماRPi.GPIOفلا تعمل على الباي 5.- كل طرف 3.3 V فقط، ولا محوّل تماثليا في الباي.
- الأحداث تعمل في خيط خلفي، و
pause()يُبقي البرنامج حيا. - خدمة systemd هي نظير
main.pyعلى الباي.
| الأمر | عمله |
|---|---|
| LED(17) | ثنائي باعث للضوء على GPIO17: on, off, toggle, is_lit, value. |
| Button(2, bounce_time=0.05) | زر مع مقاومة رفع داخلية، ومعالجة ارتداد التلامس. |
| button.is_pressed | True عند الضغط. |
| button.when_pressed = f | تُستدعى f عند الضغط. ونظيرها when_released. |
| from signal import pause | pause() تنتظر الأحداث إلى الأبد. |
| PWMLED(18).value = 0.5 | سطوع بين 0 و1. |
| Servo(18) | محرّك مؤازر: min, mid, max, value. |
| DistanceSensor(echo=24, trigger=23) | distance بالأمتار. وعلى ECHO مجزّئ جهد. |
| python -m venv --system-site-packages env | بيئة افتراضية ترى مكتبات النظام. |
| systemctl enable --now NAME | تشغيل الخدمة الآن وعند كل إقلاع. |
| journalctl -u NAME -f | متابعة مُخرَج الخدمة. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| نظام التشغيل | מערכת הפעלה | Operating System |
| سطر الأوامر | שורת פקודה | Command Line |
| البيئة الافتراضية | סביבה וירטואלית | Virtual Environment |
| الترقيم الفيزيائي | מספור פיזי | Physical Numbering |
| الحدث | אירוע | Event |
| الخيط | תהליכון | Thread |
| الخدمة | שירות | Service |
source env/bin/activate ثم نعيد التشغيل.LED(18).journalctl -u button-led يبيّن السبب. ونسينا systemctl enable.أسئلة للتمرين:
--system-site-packages؟ ولماذا نحتاج إليه هنا؟gpiozero المثبّتة بـapt دون تثبيتها مرة أخرى.from gpiozero import LED, Button from signal import pause led = LED(17) button = Button(2) button.when_pressed = led.on button.when_released = led.off pause()
import RPi.GPIO as GPIO ويعمل على الباي 4، ويفشل على الباي 5. لماذا؟ وما الحل الذي نفضّله؟RPi.GPIO لا تصل إليها. نعيد كتابته بـgpiozero.