برمجة المتحكّمات بلغة مايكروبايثون · الفصل 22 — البرمجة بلغة بايثون ومكتبة gpiozeroתכנות ב-Python וספריית gpiozero · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 22

البرمجة بلغة بايثون ومكتبة gpiozero

תכנות ב-Python וספריית gpiozero – Python and gpiozero
3 ساعات تعليمية

نترك الآن المتحكّم وننتقل إلى حاسوب كامل بحجم بطاقة. على 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، وبرنامجنا واحد من عشرات البرامج التي تعمل معا. التثبيت يتم مرة واحدة:

  1. نثبّت على حاسوبنا برنامج Raspberry Pi Imager من الموقع الرسمي، ونختار فيه طراز اللوحة والنظام وبطاقة microSD.
  2. قبل الكتابة نملأ إعدادات التخصيص: اسم الجهاز، المستخدم وكلمة المرور، شبكة WiFi، وتفعيل SSH. قد تختلف أسماء الشاشات بين الإصدارات.
  3. نضع البطاقة في الباي ونشغّله، وننتظر انتهاء الإقلاع الأول، ثم نتصل من حاسوبنا بسطر الأوامر (שורת פקודה):
الاتصال والتحديث والإطفاء. استبدلوا الاسمين بما اخترتموه في التخصيص.
ssh student@mypi.local
sudo apt update
sudo apt full-upgrade
python --version
sudo shutdown -h now
SSH (SSH): بروتوكول يفتح سطر أوامر الباي على حاسوب آخر عبر الشبكة، فلا نحتاج إلى شاشة ولوحة مفاتيح موصولتين بالباي.

أوامر النظام في هذا الفصل لا تُنفَّذ في أثناء بناء الكتاب، لأنها تحتاج إلى باي حقيقي. أما برامج gpiozero التي تظهر تحتها شاشة فقد نُفّذت فعلا.

Pi 4Pi 5
المصدر الذي توصي به الشركة5 V, 3 A5 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
البيئة الافتراضية (סביבה וירטואלית): مجلد فيه نسخة خاصة بالمشروع من بايثون ومكتباته، لا تؤثر في النظام ولا في المشاريع الأخرى.
مشروع جديد: نثبّت gpiozero من النظام، وننشئ البيئة، ونفعّلها، ونثبّت فيها.
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، وهذا هو المكان الصحيح لتثبيتها.

سؤال 22.1: فتحنا نافذة SSH جديدة وكتبنا python app.py، فظهر الخطأ أن المكتبة smbus2 غير موجودة، مع أننا ثبّتناها أمس. ما السبب؟

22.3 ترقيم الأطراف والجهد – מספור הדקים ומתח (BCM Numbering)

لكل طرف في الباي رقمان: الرقم الفيزيائي (פיזי) هو موضعه على الموصل، من 1 إلى 40. ورقم BCM هو رقمه في الشريحة. مكتبة gpiozero تستعمل رقم BCM، فالأمر LED(17) يعني GPIO17، وهو الطرف الفيزيائي 11.

123V35V34GPIO25V56GPIO3GND78GPIO4GPIO14910GNDGPIO151112GPIO17GPIO181314GPIO27GND1516GPIO22GPIO2317183V3GPIO24
أول 18 طرفا من الموصل: الأرقام في الدوائر فيزيائية، والأسماء حولها حسب BCM. والأمر pinout يطبع الخريطة كاملة على الباي.
تحذير: أطراف الباي تعمل على 3.3 V فقط. طرفا 5V على الموصل هما للتغذية، ولا نوصلهما بأي طرف آخر. وجهد 5 V على طرف إدخال قد يُتلف الشريحة، ولا تصليح لها: تُستبدل اللوحة كلها.
GPIO17 (11)GPIO2 (3)GND (9)330 ΩLEDالزر
ثنائي باعث للضوء على GPIO17، وزر بين GPIO2 والأرضي.
مثال 22.1
نريد تيارا نحو 4 mA في ثنائي باعث للضوء جهده الأمامي النموذجي 2 V، موصول بطرف جهده 3.3 V. ما المقاومة؟
الحل:
R = VPIN − VFI
R = 3.3 − 20.004
R = 325 Ω

نختار القيمة القياسية القريبة 330 Ω، فيصبح التيار نحو 3.94 mA.

R = 330 Ω
سؤال 22.2: الطرف الفيزيائي 12: ما رقمه حسب BCM؟ وماذا نكتب لإنشاء ثنائي باعث للضوء عليه؟

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)
Raspberry Pi 4/5 · Python 3 · simulated bus
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.

الزر يُضغط عند 0.5 ثانية ويُترك عند 1.5 ثانية، والثنائي يتبعه.
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)
Raspberry Pi 4/5 · Python 3 · simulated bus
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
ملاحظة: الطرف GPIO2 هو أيضا خط SDA لناقل I2C. إذا استعملنا الناقل في المشروع نفسه ننقل الزر إلى طرف آخر، مثل GPIO27.
ملاحظة: لماذا لا نستعمل 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)
Raspberry Pi 4/5 · Python 3 · simulated bus
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، فلا يحدث شيء عند الضغط.
سؤال 22.3: حذفنا السطر 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)
Raspberry Pi 4/5 · Python 3 · simulated bus
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 يولّد النبضات برمجيا، فقد يرتجف المحرّك قليلا، لأن لينكس قد يؤخّر البرنامج.

HC-SR04ECHOGNDGPIO24GND1 kΩ2 kΩ5 V3.3 V
مجزّئ جهد على ECHO. أما TRIG فهو مُدخَل في الحسّاس، ويوصل مباشرة بـGPIO23، وتغذية الحسّاس من 5V.
مثال 22.2
خرج ECHO في الحسّاس HC-SR04 يبلغ 5 V والطرف يتحمّل 3.3 V. نستعمل R1 = 1 kΩ وR2 = 2 kΩ. ما الجهد على الطرف؟
الحل:
V = 5 · R2R1 + R2
V = 5 · 21 + 2
V = 3.33 V
V = 3.33 V

ولا يوجد في الباي محوّل تماثلي رقمي. لقراءة مقياس جهد أو مقاومة ضوئية نستعمل المحوّل الخارجي ADS1115 عبر I2C، كما فعلنا في الفصل 6.

22.7 ما الذي تغيّر عن مايكروبايثون؟ – ההבדלים ממיקרו-פייתון

مايكروبايثون على المتحكّمبايثون على الباي
التشغيلبرنامج واحد يبدأ من main.pyبرنامج بين برامج كثيرة، نبدؤه بأنفسنا أو بخدمة
الوصول إلى الأطرافmachine.Pingpiozero
رقم الطرفرقم الشريحة، عبر board.pyBCM
مقاومة الرفع للزرنطلبها: Pin.PULL_UPتلقائية في Button
حدث عند الضغطPin.irq، مقاطعة قصيرةwhen_pressed، خيط خلفي
قراءة تماثليةADC داخليمحوّل خارجي فقط
تثبيت المكتباتنسخ أو mippip في بيئة افتراضية، أو apt
دقة التوقيتأعلى، فلا نظام يقاطع البرنامجأقل: لينكس يوزّع الزمن بين البرامج
الإطفاءنفصل الكهرباءنطفئ النظام أولا

السطر قبل الأخير يفسّر توزيع الأدوار في الفصل التالي: التوقيت الدقيق والقرب من الحسّاس للمتحكّم، والتخزين والشبكة وصفحات الويب للباي.

22.8 التشغيل التلقائي: الخدمة – הרצה אוטומטית (systemd)

على المتحكّم يبدأ main.py وحده عند التغذية. على الباي لا يحدث شيء من هذا: إذا أغلقنا نافذة SSH توقف البرنامج، وبعد إعادة الإقلاع لن يعمل. الحل هو الخدمة (שירות): ملف يصف لمدير النظام systemd كيف يشغّل برنامجنا. نحفظ برنامج pause() من القسم 22.5 باسم button_led.py ونكتب:

/etc/systemd/system/button-led.service
[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
سؤال 22.4: الخدمة لا تعمل بعد الإقلاع. ما الأمر الذي يبيّن لنا سبب الفشل؟ وما الأمر الذي نسيناه إذا لم تبدأ وحدها بعد الإقلاع مع أنها تعمل يدويا؟

أهمّ أفكار الفصل:

الأمرعمله
LED(17)ثنائي باعث للضوء على GPIO17: on, off, toggle, is_lit, value.
Button(2, bounce_time=0.05)زر مع مقاومة رفع داخلية، ومعالجة ارتداد التلامس.
button.is_pressedTrue عند الضغط.
button.when_pressed = fتُستدعى f عند الضغط. ونظيرها when_released.
from signal import pausepause() تنتظر الأحداث إلى الأبد.
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
الإجابات النهائية لأسئلة الفصل
22.1 البيئة لم تُفعَّل في هذه النافذة. نكتب source env/bin/activate ثم نعيد التشغيل.
22.2 GPIO18، ونكتب LED(18).
22.3 ينتهي البرنامج فورا بعد السطر الأخير، فلا يبقى من يستجيب للزر.
22.4 journalctl -u button-led يبيّن السبب. ونسينا systemctl enable.

أسئلة للتمرين:

المستوى الأول — أساسي
1.
اعتمادا على رسم الموصل: ما رقم BCM للطرف الفيزيائي 16؟ وما الرقم الفيزيائي لـGPIO27؟
الإجابات: GPIO23، والطرف الفيزيائي 13.
2.
ما الذي يفعله الخيار --system-site-packages؟ ولماذا نحتاج إليه هنا؟
الإجابات: يجعل البيئة ترى مكتبات النظام، فنستعمل gpiozero المثبّتة بـapt دون تثبيتها مرة أخرى.
3.
ثنائي باعث للضوء جهده الأمامي النموذجي 1.8 V مع مقاومة 470 Ω على طرف الباي. ما التيار تقريبا؟
الإجابات: I = 3.19 mA.
المستوى الثاني — تطبيق
4.
اكتبوا برنامجا يضيء الثنائي ما دام الزر مضغوطا، بالأحداث وبدون حلقة.
الإجابات:
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()
5.
برنامج من الإنترنت يبدأ بالسطر import RPi.GPIO as GPIO ويعمل على الباي 4، ويفشل على الباي 5. لماذا؟ وما الحل الذي نفضّله؟
الإجابات: الأطراف في الباي 5 على الشريحة RP1، وRPi.GPIO لا تصل إليها. نعيد كتابته بـgpiozero.
المستوى الثالث — تفكير
6.
نريد مجزّئ جهد لخرج 5 V بالمقاومتين R1 = 2.2 kΩ وR2 = 3.3 kΩ. ما الجهد على الطرف؟ وهل هو آمن؟
الإجابات: V = 3 V، وهو أقل من 3.3 V فهو آمن.
7.
زميلكم يريد توليد إشارة بتوقيت دقيق جدا لتشغيل محرّك خطوي (מנוע צעד) مع قراءة ثلاثة حسّاسات تماثلية. أين تضعون هذا العمل: على الباي أم على متحكّم؟ علّلوا.
الإجابات: على متحكّم: توقيته أثبت لأن لينكس قد يؤخّر البرنامج، وفيه محوّل داخلي. والباي يتلقّى منه النتائج.
لوحة القوانين: كل قوانين هذا الفصل مجموعة مع رسومها وجداول رموزها في الفصل 25 — لوحة القوانين الشاملة. (נוסחאון הספר)