المكتبات: تثبيتها وكتابتها
منذ الفصل الأول ونحن نكتب import. في هذا الفصل نفتح هذه الكلمة: أين يبحث عنها المتحكّم؟ كيف نثبّت مكتبة كتبها غيرنا، وكيف نعرف أنها تستحق الثقة؟ ثم ننتقل إلى الجهة الأخرى ونكتب مكتبة بأنفسنا، بحيث يمكن اختبارها دون أي شريحة، ثم نطبّق الفحص نفسه على شيفرة كتبها لنا برنامج ذكاء اصطناعي.
- نشرح دور
sys.pathوالمجلدlib، ونفسّر رسالةImportError. - نثبّت مكتبة بـ
mipوmpremoteوThonny، ونفحصها قبل الاستعمال: الرخصة والمصدر والإصدار والشيفرة. - نفصل تعريف الشريحة عن منطق البرنامج، ونمرّر الناقل عبر الباني.
- نترجم مكتبة إلى
.mpyونعرف متى يستحق ذلك. - نكتب اختبار وحدة بكائن بديل، ونبني الصنف
Buttonمع معالجة ارتداد التلامس. - نراجع شيفرة كتبها برنامج ذكاء اصطناعي دون أن نقبل أي معلومة بلا مصدر.
11.1 أين يبحث import؟ – איפה import מחפש (The Search Path)
import بدل نسخها إلى كل برنامج. وقد تُسمى الحزمة (חבילה) إذا كانت عدة ملفات مع وصف للتثبيت.عندما نكتب import convert يبحث مايكروبايثون عن convert.py أو convert.mpy في قائمة من المجلدات، بالترتيب، ويأخذ أول ما يجده. هذه القائمة هي sys.path. والمُخرَج يتعلق بالبيئة، لذلك نكتفي بعرض الأمر هنا دون تشغيله في بناء الكتاب:
import sys
print(sys.path)
sys.path.append('/my_libs')حسب التوثيق، يوجد فيها عادة العنصر الفارغ ''، أي المجلد الجاري، وكذلك .frozen للوحدات المبنية داخل نظام اللوحة، ومجلد ينتهي بـ/lib. ولذلك نضع المكتبات في المجلد lib والبرنامج في الجذر. هكذا يبقى الجذر نظيفا، وتبقى المكتبة منفصلة عن البرنامج.
import convert print(convert.__name__) print(convert.to_percent(32768))
convert 50
فإن لم يجد الملف في كل القائمة، فهذه هي الرسالة:
import convert_v2 print(convert_v2.to_percent(32768))
ImportError: no module named 'convert_v2'
max7219.py في الجذر يسبق الملف الذي في lib، فيستورد البرنامج نفسه أو نسخة قديمة، وتظهر رسائل غريبة.11.2 تثبيت المكتبات – התקנת ספריות (Installing Libraries)
أوامر التثبيت في هذا القسم لم تُشغَّل في بناء الكتاب، لأنها تحتاج إلى شبكة حقيقية ولوحة حقيقية. أما المكتبات التي يطبع الكتاب مُخرَجها فتصل إلى المُحاكاة مباشرة من حزمة الملفات. توجد ثلاث طرق:
1. نسخ الملف بأنفسنا:
المكتبة التي كتبناها أو نزّلناها ننسخها إلى اللوحة. الأداة mpremote تعمل من الحاسوب عبر USB:
mpremote mkdir :lib mpremote cp max7219.py :lib/ mpremote ls :lib
وفي Thonny نفعل الشيء نفسه من نافذة الملفات: نختار الملف ونرفعه إلى المجلد lib في اللوحة.
2. الأداة mip على اللوحة:
اللوحة المتصلة بالشبكة تنزّل بنفسها. الفهرس الافتراضي هو micropython-lib، ويختار mip أول مجلد في sys.path ينتهي بـ/lib. ونستطيع تغييره بـtarget، بشرط أن يكون في sys.path.
import time
import network
import mip
from secrets import WIFI_SSID, WIFI_PASS
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.disconnect()
wlan.connect(WIFI_SSID, WIFI_PASS)
while not wlan.isconnected():
time.sleep(0.5)
mip.install('umqtt.simple')
mip.install('umqtt.simple', mpy=False)
mip.install('github:org/repo/path/foo.py', version='v1.2')| الأمر | عمله |
|---|---|
| mip.install('name') | من الفهرس، بملفات .mpy جاهزة. |
| mip.install('name', mpy=False) | بملفات .py: نقدر على قراءتها. |
| mip.install('name', version='x.y') | إصدار محدد. |
| mip.install('name', target='/x') | مجلد آخر، على أن يكون في sys.path. |
| mip.install('github:org/repo/foo.py', version='tag') | ملف من مستودع، بفرع أو وسم محدد. |
3. من الحاسوب:
mpremote mip install umqtt.simple mpremote mip install --target=/my_libs umqtt.simple mpremote mip install github:org/repo@v1.2
وفي Thonny مدير حزم في القائمة Tools، نبحث فيه عن الاسم ونثبّت. الأسماء في القوائم قد تتغيّر من إصدار إلى آخر. وبعد التثبيت نتأكد دائما: ننظر إلى المجلد lib، ونجرّب import مباشرة.
pip في بيئة افتراضية، كما رأينا في الفصل 6.11.3 نفحص قبل أن نستعمل – בודקים לפני שמשתמשים (Vetting a Library)
المكتبة شيفرة ستعمل داخل جهازنا، وقد كتبها غريب. لذلك نعتاد فحصها قبل أن نبني عليها، ونسأل أربعة أسئلة:
| السؤال | أين نجد الجواب | علامة خطر |
|---|---|---|
| الرخصة: هل يُسمح لي؟ | الملف LICENSE ورأس كل ملف | لا توجد رخصة أصلا |
| المصدر: من كتبها؟ | المستودع الرسمي وصاحبه | ملف منسوخ من موقع بلا اسم |
| الإصدار: هل هي حيّة؟ | تاريخ التعديلات، والوسوم، والأعطال المفتوحة | لم تُجرَّب على لوحتنا |
| الشيفرة: ماذا تفعل؟ | نقرأها: تثبيت mpy=False يعطي المصدر | شبكة أو ملفات لا علاقة لها بعملها |
الرخصة ليست تفصيلا. فهي التي تقول ما يحق لنا:
| الرخصة | ماذا تعني باختصار |
|---|---|
| MIT، BSD | استعمال وتعديل وبيع، بشرط إبقاء نص الرخصة واسم الكاتب. |
| Apache 2.0 | مثلها، مع شروط إضافية عن التعديلات وبراءات الاختراع. |
| GPL | إذا وزّعنا برنامجا مبنيا عليها، فعلينا توزيعه بالرخصة نفسها مع مصدره. |
| CC BY-NC-SA | لا استعمال تجاريا، والمشتق يكون بالرخصة نفسها. |
| لا توجد رخصة | لا إذن: الحقوق محفوظة لصاحبها حتى لو كان الملف مكشوفا على الإنترنت. |
ونقرأ الشيفرة بأسئلة محددة: ما الذي تستورده؟ هل تثبّت أرقام أطراف داخلها؟ هل تستعمل وحدات خاصة بلوحة أخرى؟ هل تحجب البرنامج طويلا بـsleep؟ هل تتصل بعنوان شبكة؟ هل تطبع كلمة سر أو تطلبها؟
11.4 نكتب مكتبة: الفصل بين الطبقات – כותבים ספרייה: הפרדה בין שכבות (Writing a Library: Separation)
لننظر أولا إلى ما لا نريده. هذا صنف يعمل، ويضيء عددا من صفوف المصفوفة حسب نسبة مئوية، لكنه يخلط كل شيء معا. لا يُشغَّل في بناء الكتاب، لأنه مثال سيئ:
from machine import Pin, SPI
class Matrix:
def __init__(self):
self.spi = SPI(2, 1000000, sck=Pin(18), mosi=Pin(23))
self.cs = Pin(5, Pin.OUT, value=1)
def bar(self, percent):
lit = (percent * 8 + 50) // 100
print('lit rows:', lit)
for i in range(8):
value = 0xFF if 7 - i < lit else 0
self.cs.value(0)
self.spi.write(bytes([i + 1, value]))
self.cs.value(1)- أرقام الأطراف مثبّتة داخله: لا يعمل على Pico W.
- ينشئ الناقل بنفسه: لا يمكن وضع شريحة ثانية على الناقل، ولا يمكن تجربته دون لوحة.
- يخلط الحساب مع البايتات: لنختبر معادلة بسيطة نحتاج إلى عتاد كامل.
- يطبع من داخله: البرنامج هو الذي يقرّر ماذا يظهر.
الحل ثلاث طبقات، ولكل طبقة سبب واحد لتغييرها:
machine، ولذلك نختبرها في أي مكان.MAX7219(spi, cs). هكذا عمل ADS1115(i2c) في الفصل 6، وهكذا يعمل كل تعريف في الكتاب.import# ledbar.py - turns a percentage into rows for an 8x8 matrix. No hardware.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
def bar_rows(percent):
if not 0 <= percent <= 100:
raise ValueError('percent is 0 to 100')
lit = (percent * 8 + 50) // 100
rows = bytearray(8)
for i in range(lit):
rows[7 - i] = 0xFF
return rowsوالبرنامج هو المكان الوحيد الذي يعرف اللوحة. مقياس الجهد موصول كما في الفصل 6، والمصفوفة كما في الفصل 10:
import time
import board
from machine import Pin
from convert import to_percent
from ledbar import bar_rows
from max7219 import MAX7219
pot = board.analog_in(board.POT)
spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
matrix = MAX7219(spi, cs)
while True:
percent = to_percent(pot.read_u16())
rows = bar_rows(percent)
matrix.show(rows)
print(percent, 'percent:', sum(1 for r in rows if r), 'rows lit')
time.sleep(1)0 percent: 0 rows lit 25 percent: 2 rows lit 50 percent: 4 rows lit 100 percent: 8 rows lit ...
قواعد لمن يكتب مكتبة، ونرى أغلبها في ads1115.py وmax7219.py:
- سطر في الرأس يقول: ما هذا الملف، ولأي شيء، ومن كتبه، وما رخصته.
- الاستيراد لا يفعل شيئا بالعتاد: كل العمل في الباني والدوال.
- لا طباعة داخل المكتبة. عند الخطأ نرفع استثناء:
raise ValueError('...'). - أسماء قليلة وواضحة للاستعمال، وشرطة سفلية لما هو داخلي:
_send. - لا نستعمل ما هو خاص بلوحة إذا أمكن تجنّبه.
from machine import Pin في max7219.py؟11.5 ملفات .mpy – קובצי mpy (Precompiled Files)
عند استيراد .py يترجم المتحكّم النص نفسه إلى شيفرة وسيطة (קוד ביניים)، ويحتاج في ذلك إلى ذاكرة. الأداة mpy-cross تفعل هذا مسبقا على الحاسوب، وتعطي ملف .mpy يُستورد بالاسم نفسه:
pip install mpy-cross mpy-cross max7219.py mpremote cp max7219.mpy :lib/
| متى نستعمله | متى لا نستعمله |
|---|---|
مكتبة كبيرة يفشل استيرادها بـMemoryError | ونحن ما زلنا نعدّل المكتبة كل دقيقة |
| مكتبة مستقرة نوزعها على لوحات كثيرة، بإصدار معروف | حين نريد أن نقرأ الشيفرة على اللوحة |
.mpy ما يدعمه مايكروبايثون على اللوحة. وإلا نرى ValueError: incompatible .mpy file. نعرف الرقم على اللوحة من الحقل التالي، ونقارنه بما يقوله mpy-cross --version. وملف .mpy ليس وسيلة لإخفاء السر: فكلمة السر تبقى داخله مقروءة..mpy. لا يُشغَّل في بناء الكتاب.import sys print(sys.implementation._mpy & 0xFF)
11.6 اختبار الوحدة بكائن بديل – בדיקת יחידה עם אובייקט מדומה (Unit Tests with a Fake)
طبقة المنطق تُختبر مباشرة بسطر assert. وهذا هو الاختبار كله:
from ledbar import bar_rows
assert bar_rows(0) == bytes(8)
assert bar_rows(100) == b'\xff' * 8
assert bar_rows(50) == bytes(4) + b'\xff' * 4
print('ledbar: all tests passed')ledbar: all tests passed
لتعريف الشريحة نحتاج إلى ناقل بديل. وبفضل تمرير الناقل عبر الباني، يكفي صنف من بضعة أسطر. نجمع البدائل في ملف واحد، ولا نستورد في الاختبار board ولا machine:
# fakes.py - stand-ins for the hardware, for unit tests. Runs anywhere.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
class FakeSPI:
def __init__(self):
self.sent = []
def write(self, data):
self.sent.append(bytes(data))
class FakePin:
def __init__(self, level=1):
self.level = level
self.lows = 0
def value(self, x=None):
if x is None:
return self.level
self.level = x
if x == 0:
self.lows += 1
class FakeClock:
def __init__(self):
self.t = 0
def __call__(self):
return self.tfrom max7219 import MAX7219
from fakes import FakeSPI, FakePin
spi = FakeSPI()
cs = FakePin()
m = MAX7219(spi, cs)
assert spi.sent[0] == bytes([0x0F, 0]), 'test mode must go off first'
assert spi.sent[-1] == bytes([0x0C, 1]), 'turn on must be last'
assert cs.lows == len(spi.sent), 'one CS window for each packet'
assert cs.level == 1, 'CS must stay high'
spi.sent = []
m.show((1, 2, 3, 4, 5, 6, 7, 8))
assert len(spi.sent) == 8
assert spi.sent[7] == bytes([0x08, 8])
try:
m.intensity(16)
assert False, 'no error for intensity 16'
except ValueError:
pass
print('max7219: all tests passed')max7219: all tests passed
اختبار ينجح دائما لا قيمة له. فنتأكد من أنه يكشف الخطأ حقا: نعدّل _send في صنف مشتق بحيث يبدّل البايتين، وهو خطأ لا يعطي أي رسالة على اللوحة:
from max7219 import MAX7219
from fakes import FakeSPI, FakePin
class Swapped(MAX7219):
def _send(self, reg, value):
self.spi.write(bytes([value, reg]))
spi = FakeSPI()
m = Swapped(spi, FakePin())
assert spi.sent[0] == bytes([0x0F, 0]), 'test mode must go off first'
print('never printed')AssertionError: test mode must go off first
assert يكفي لمكتبة صغيرة. وفي micropython-lib حزمة unittest لمن يريد أكثر، ولم نستعملها في الكتاب.11.7 مشروع: الصنف Button – פרויקט: המחלקה Button (Project: A Button Class)
التلامس المعدني لا ينغلق دفعة واحدة، بل يرتد عدة مرات خلال وقت قصير، وهذا هو ارتداد التلامس (קפיצות מגע). لو عدّدنا الحواف، لعدّت ضغطة واحدة عدة ضغطات. والحل دون حجب البرنامج: لا نصدّق تغيّر الحالة إلا بعد أن يبقى المستوى ثابتا مدة كافية.
ms.نكتب الصنف حسب ما تعلمناه: الطرف يأتي من الخارج، والساعة أيضا، حتى نستطيع في الاختبار أن نحرّك الزمن بأيدينا:
# button.py - a push button with debouncing, without blocking.
# pin: an input Pin, or any object with value(). clock: returns ms.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import time
class Button:
def __init__(self, pin, ms=30, active=0, clock=time.ticks_ms):
self.pin = pin
self.ms = ms
self.active = active
self.clock = clock
self.state = pin.value()
self.last = self.state
self.since = clock()
def pressed(self):
now = self.clock()
level = self.pin.value()
if level != self.last:
self.last = level
self.since = now
return False
if level == self.state:
return False
if time.ticks_diff(now, self.since) < self.ms:
return False
self.state = level
return level == self.activeالقيمة الافتراضية 30 مللي ثانية هي خيارنا نحن، وليست معيارا: لكل مفتاح سلوكه، ونزيدها إذا ظهرت ضغطات زائدة. ونكتب الاختبار قبل أن نقترب من اللوحة:
from button import Button
from fakes import FakePin, FakeClock
def count_presses(levels):
pin = FakePin()
clock = FakeClock()
btn = Button(pin, ms=30, clock=clock)
presses = 0
for level in levels:
pin.level = level
clock.t += 5
if btn.pressed():
presses += 1
return presses
STEADY = [0] * 12
BOUNCY = [0, 1, 0, 1, 0, 1] + [0] * 10
SPIKE = [0, 0, 1, 1, 1, 1]
RELEASE = [0] * 12 + [1, 0, 1, 0] + [1] * 12
assert count_presses(STEADY) == 1, 'a clean press'
assert count_presses(BOUNCY) == 1, 'bounces are one press'
assert count_presses(SPIKE) == 0, 'too short to be a press'
assert count_presses(RELEASE) == 1, 'release is not a press'
print('button: all tests passed')button: all tests passed
والآن على اللوحة، بالدائرة نفسها من الفصل 3: مفتاح إلى الأرضي، وثنائي باعث للضوء على LED_EXT. نبرمج في المُحاكاة ضغطة مرتدة ثم ضغطة نظيفة، ونعدّ الحواف بجانب الضغطات للمقارنة:
import time
import board
from machine import Pin
from button import Button
pin = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
led = Pin(board.LED_EXT, Pin.OUT)
btn = Button(pin)
presses = 0
edges = 0
last = pin.value()
while True:
level = pin.value()
if last == 1 and level == 0:
edges += 1
last = level
if btn.pressed():
presses += 1
led.value(presses % 2)
print('presses', presses, ' falling edges', edges)
time.sleep(0.005)presses 1 falling edges 3 presses 2 falling edges 5 ...
time.sleep(0.03) بعد كل قراءة للطرف، وهو أبسط؟11.8 شيفرة كتبها برنامج ذكاء اصطناعي – קוד שכתב כלי AI (Reviewing AI-Written Code)
برامج الذكاء الاصطناعي (כלי בינה מלאכותית) تكتب شيفرة تبدو صحيحة بسرعة. لكنها قد تخلط بين اللوحات، أو تخترع أمرا أو رقم مسجّل يبدو معقولا. لذلك نعامل ما تكتبه مثل مكتبة كتبها غريب، ونفحصه الفحص نفسه. وأسهل الأخطاء ما يوقف البرنامج. هذا يصلح لـPico W ويفشل على ESP32:
from machine import Timer
tick = Timer(-1)
tick.init(period=1000, callback=lambda t: print('tick'))ValueError: invalid Timer number
الأخطر ما لا يعطي أي رسالة. هذا برنامج مكتوب بثقة، باسم معقول لمسجّل لا وجود له في جدول MAX7219:
import board
from machine import Pin
BRIGHTNESS_REG = 0x0D
spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
cs.value(0)
spi.write(bytes([BRIGHTNESS_REG, 8]))
cs.value(1)
print('brightness set to 8')brightness set to 8 [spi] 0d 08
لذلك نتبع ترتيبا ثابتا:
| ما نفحصه | كيف |
|---|---|
| هل يترجم ويعمل؟ | نشغّله. هذا أدنى حد، وليس دليلا على الصحة. |
| هل الأوامر موجودة؟ | كل دالة غير مألوفة نبحث عنها في docs.micropython.org، للوحة الصحيحة. |
| هل الأرقام صحيحة؟ | كل عنوان ومسجّل وتوقيت نقارنه بورقة بيانات الشريحة. |
| هل الأطراف صحيحة؟ | نقارنها بملف اللوحة: مدخل فقط، أو طرف محجوز، أو تعارض كما في الفصل 10. |
| هل الجهود آمنة؟ | الجهود من ورقة البيانات، قبل أن نصل أي سلك. |
| أين الأسرار؟ | لا كلمة سر في الشيفرة ولا في المحادثة مع البرنامج. |
| من أين جاءت؟ | مكتبة يقترحها نفحصها كما في 11.3، ونتأكد أنها موجودة أصلا. |
| هل نفهمها؟ | نقدر على شرح كل سطر، ونكتب له اختبارا بأنفسنا. |
أهمّ أفكار الفصل:
importيبحث فيsys.pathبالترتيب، والمكتبات مكانهاlib.- نثبّت بـ
mipأوmpremoteأو Thonny أو بالنسخ، ونحدّد الإصدار عند الحاجة. - قبل الاستعمال: الرخصة والمصدر والإصدار وقراءة الشيفرة. لا رخصة تعني لا إذن.
- التعريف يعرف الشريحة، والمنطق يحسب، والبرنامج وحده يعرف اللوحة.
- الناقل يأتي عبر الباني، فنختبر بكائن بديل دون عتاد.
.mpyيوفّر الذاكرة عند الاستيراد، ويجب أن يطابق إصدار اللوحة.- الاختبار يفحص افتراضاتنا، وورقة البيانات تفحص الافتراضات نفسها.
أوامر الفصل:
| الأمر | عمله |
|---|---|
| sys.path | قائمة المجلدات التي يبحث فيها import. |
| mip.install('name') | تثبيت من اللوحة. |
| mpremote mip install name | تثبيت من الحاسوب. |
| mpremote cp f.py :lib/ | نسخ ملف إلى اللوحة. |
| mpy-cross f.py | ترجمة إلى .mpy. |
| assert cond, 'text' | يوقف البرنامج إذا كان الشرط خاطئا. |
| raise ValueError('text') | تبليغ البرنامج عن قيمة خاطئة. |
| Button(pin, ms=30) | مفتاح مع معالجة الارتداد؛ pressed(). |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| مكتبة | ספרייה | Library |
| حزمة | חבילה | Package |
| رخصة | רישיון | License |
| إصدار | גרסה | Version |
| طبقة التعريف | דרייבר | Driver |
| طبقة المنطق | לוגיקה | Logic Layer |
| تمرير عبر الباني | הזרקה דרך הבנאי | Constructor Injection |
| الباني | בנאי | Constructor |
| شيفرة وسيطة | קוד ביניים | Bytecode |
| اختبار الوحدة | בדיקת יחידה | Unit Test |
| كائن بديل | אובייקט מדומה | Fake Object |
| ارتداد التلامس | קפיצות מגע | Contact Bounce |
value. وهكذا يعمل الصنف مع كائن بديل في الاختبار.pressed() يعود فورا في كل مرة.أسئلة للتمرين:
lib، لأنه في sys.path، ولأن الجذر يبقى للبرنامج.mip.install('x') وmip.install('x', mpy=False)؟.mpy، والثاني ملفات المصدر .py التي نقرؤها.ADS1115 بناقل I2C بديل: يتأكد من القيمة السالبة، ومن اختيار القناة 2.from ads1115 import ADS1115
class FakeI2C:
def __init__(self, value):
self.value = value
self.reg = 0
self.config = 0
def writeto(self, addr, data):
self.reg = data[0]
if len(data) == 3:
self.config = (data[1] << 8) | data[2]
def readfrom(self, addr, n):
v = 0x8000 if self.reg == 1 else self.value
return bytes([v >> 8, v & 0xFF])
bus = FakeI2C(0xFFFF)
adc = ADS1115(bus)
assert adc.read_raw(2) == -1, 'negative values'
assert (bus.config >> 12) & 7 == 6, 'channel 2'
print('ads1115: all tests passed')Button حالة ضغطتين نظيفتين متتاليتين. ماذا نتوقع؟[0] * 12 + [1] * 12 + [0] * 12. نتوقع 2.(أ) تحويل القراءة الخام إلى درجة حرارة.
(ب) إرسال أمر تشغيل الشريحة.
(ج) تحديد إذا كان الجهاز يحتاج إلى تبريد.
Button كشف الضغط الطويل، بحيث نختبره بالكائن FakeClock؟ticks_diff بالمدة المطلوبة وتعطي True مرة واحدة. وفي الاختبار نزيد clock.t بأيدينا ولا ننتظر.spi.write_read(tx). كيف نتحقق؟machine.SPI: الأوامر الموثقة هي write وread وreadinto وwrite_readinto، وهذا الأمر ليس بينها.