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

المكتبات: تثبيتها وكتابتها

ספריות: התקנה וכתיבה – Libraries: Installing and Writing
3 ساعات تعليمية

منذ الفصل الأول ونحن نكتب 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))
ESP32 + Pico W · MicroPython 1.29 · simulated board
convert
50

فإن لم يجد الملف في كل القائمة، فهذه هي الرسالة:

خطأ يتكرر كثيرا: الملف غير موجود على اللوحة، أو الاسم مختلف.
import convert_v2

print(convert_v2.to_percent(32768))
ESP32 + Pico W · MicroPython 1.29 · simulated board
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 مباشرة.

ملاحظة: هذا كله عن لوحتي مايكروبايثون. أما Raspberry Pi 4/5 فهو حاسوب، وتُثبَّت مكتباته بـpip في بيئة افتراضية، كما رأينا في الفصل 6.

11.3 نفحص قبل أن نستعمل – בודקים לפני שמשתמשים (Vetting a Library)

المكتبة شيفرة ستعمل داخل جهازنا، وقد كتبها غريب. لذلك نعتاد فحصها قبل أن نبني عليها، ونسأل أربعة أسئلة:

السؤالأين نجد الجوابعلامة خطر
الرخصة: هل يُسمح لي؟الملف LICENSE ورأس كل ملفلا توجد رخصة أصلا
المصدر: من كتبها؟المستودع الرسمي وصاحبهملف منسوخ من موقع بلا اسم
الإصدار: هل هي حيّة؟تاريخ التعديلات، والوسوم، والأعطال المفتوحةلم تُجرَّب على لوحتنا
الشيفرة: ماذا تفعل؟نقرأها: تثبيت mpy=False يعطي المصدرشبكة أو ملفات لا علاقة لها بعملها

الرخصة ليست تفصيلا. فهي التي تقول ما يحق لنا:

الرخصةماذا تعني باختصار
MIT، BSDاستعمال وتعديل وبيع، بشرط إبقاء نص الرخصة واسم الكاتب.
Apache 2.0مثلها، مع شروط إضافية عن التعديلات وبراءات الاختراع.
GPLإذا وزّعنا برنامجا مبنيا عليها، فعلينا توزيعه بالرخصة نفسها مع مصدره.
CC BY-NC-SAلا استعمال تجاريا، والمشتق يكون بالرخصة نفسها.
لا توجد رخصةلا إذن: الحقوق محفوظة لصاحبها حتى لو كان الملف مكشوفا على الإنترنت.

ونقرأ الشيفرة بأسئلة محددة: ما الذي تستورده؟ هل تثبّت أرقام أطراف داخلها؟ هل تستعمل وحدات خاصة بلوحة أخرى؟ هل تحجب البرنامج طويلا بـsleep؟ هل تتصل بعنوان شبكة؟ هل تطبع كلمة سر أو تطلبها؟

نشاط: طبّقوا الأسئلة الأربعة على مكتبة micropython-mfrc522 من الفصل السابق. الرخصة MIT والمصدر معروف. لكن وصفها يذكر ESP8266 وWiPy فقط. ماذا يعني ذلك لنا؟ وماذا نفحص في أي نسخة معدّلة؟

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)

الحل ثلاث طبقات، ولكل طبقة سبب واحد لتغييرها:

طبقة التعريف (דרייבר): مكتبة تعرف شريحة واحدة: مسجّلاتها وبايتاتها وتسلسل تشغيلها. لا تعرف ماذا نرسم، ولا على أي لوحة نعمل.
طبقة المنطق (לוגיקה): حساب محض: تدخل أعداد وتخرج أعداد. لا تستورد machine، ولذلك نختبرها في أي مكان.
تمرير الناقل عبر الباني (הזרקת האפיק דרך הבנאי): لا ينشئ الصنف الناقل، وإنما يأخذه من الخارج كمعامل: MAX7219(spi, cs). هكذا عمل ADS1115(i2c) في الفصل 6، وهكذا يعمل كل تعريف في الكتاب.
bar_main.pyprogram: connects everythingledbar.pylogic: no hardwaremax7219.pydriver: bytes and registersmachine.SPIthe real busFakeSPIin the unit test
البرنامج يربط الطبقات. والتعريف لا يعرف من هو الناقل: حقيقي أو بديل في الاختبار.
الملف ledbar.py، طبقة المنطق: ليس فيه 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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
0 percent: 0 rows lit
25 percent: 2 rows lit
50 percent: 4 rows lit
100 percent: 8 rows lit
...

قواعد لمن يكتب مكتبة، ونرى أغلبها في ads1115.py وmax7219.py:

سؤال 11.1: لماذا لا يوجد 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')
ESP32 + Pico W · MicroPython 1.29 · simulated board
ledbar: all tests passed

لتعريف الشريحة نحتاج إلى ناقل بديل. وبفضل تمرير الناقل عبر الباني، يكفي صنف من بضعة أسطر. نجمع البدائل في ملف واحد، ولا نستورد في الاختبار board ولا machine:

الملف fakes.py: البدائل، ولا تعمل مع أي عتاد
# 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.t
نختبر ترتيب التشغيل، ونافذة CS، والصفوف، والحد الأعلى لشدة الإضاءة.
from 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')
ESP32 + Pico W · MicroPython 1.29 · simulated board
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')
ESP32 + Pico W · MicroPython 1.29 · simulated board
AssertionError: test mode must go off first
حدود الاختبار: الاختبار يقول إن الشيفرة تفعل ما افترضناه، ولا يقول إن افتراضنا صحيح. فإذا أخطأنا في رقم مسجّل، أخطأنا فيه في الاختبار أيضا، وسينجح. الأرقام نقارنها بورقة البيانات، والشريحة الحقيقية تبقى الحكم الأخير.
ملاحظة: الأمر assert يكفي لمكتبة صغيرة. وفي micropython-lib حزمة unittest لمن يريد أكثر، ولم نستعملها في الكتاب.

11.7 مشروع: الصنف Button – פרויקט: המחלקה Button (Project: A Button Class)

التلامس المعدني لا ينغلق دفعة واحدة، بل يرتد عدة مرات خلال وقت قصير، وهذا هو ارتداد التلامس (קפיצות מגע). لو عدّدنا الحواف، لعدّت ضغطة واحدة عدة ضغطات. والحل دون حجب البرنامج: لا نصدّق تغيّر الحالة إلا بعد أن يبقى المستوى ثابتا مدة كافية.

pinstatemspressed() = True
الخط الأعلى هو ما يقرؤه الطرف. والخط الأسفل هو الحالة التي نصدّقها، بعد ثبات مدته ms.

نكتب الصنف حسب ما تعلمناه: الطرف يأتي من الخارج، والساعة أيضا، حتى نستطيع في الاختبار أن نحرّك الزمن بأيدينا:

الملف button.py، على لوحتي مايكروبايثون
# 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 مللي ثانية هي خيارنا نحن، وليست معيارا: لكل مفتاح سلوكه، ونزيدها إذا ظهرت ضغطات زائدة. ونكتب الاختبار قبل أن نقترب من اللوحة:

كل سطر قائمة تصف ما يقرؤه الطرف، بخطوة 5 مللي ثانية.
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')
ESP32 + Pico W · MicroPython 1.29 · simulated board
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
presses 1  falling edges 3
presses 2  falling edges 5
...
سؤال 11.2: لماذا لا نستعمل 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'))
ESP32 · MicroPython 1.29 · simulated board
ValueError: invalid Timer number

الأخطر ما لا يعطي أي رسالة. هذا برنامج مكتوب بثقة، باسم معقول لمسجّل لا وجود له في جدول MAX7219:

كل شيء نجح في الظاهر: البرنامج طبع جملته، والبايتات خرجت. ولكن الشدة لم تتغيّر، لأن مسجّلها 0x0A.
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')
ESP32 + Pico W · MicroPython 1.29 · simulated board
brightness set to 8
[spi]
0d 08

لذلك نتبع ترتيبا ثابتا:

ما نفحصهكيف
هل يترجم ويعمل؟نشغّله. هذا أدنى حد، وليس دليلا على الصحة.
هل الأوامر موجودة؟كل دالة غير مألوفة نبحث عنها في docs.micropython.org، للوحة الصحيحة.
هل الأرقام صحيحة؟كل عنوان ومسجّل وتوقيت نقارنه بورقة بيانات الشريحة.
هل الأطراف صحيحة؟نقارنها بملف اللوحة: مدخل فقط، أو طرف محجوز، أو تعارض كما في الفصل 10.
هل الجهود آمنة؟الجهود من ورقة البيانات، قبل أن نصل أي سلك.
أين الأسرار؟لا كلمة سر في الشيفرة ولا في المحادثة مع البرنامج.
من أين جاءت؟مكتبة يقترحها نفحصها كما في 11.3، ونتأكد أنها موجودة أصلا.
هل نفهمها؟نقدر على شرح كل سطر، ونكتب له اختبارا بأنفسنا.
قاعدة: لا نقبل رقما لأنه جاء في جواب مرتّب. إذا قال البرنامج إن زمن التحويل كذا أو الجهد المسموح كذا، فهذا ادعاء حتى نجده في المصدر. وإذا لم نجده نكتب: «غير مؤكد»، ولا نضع مكانه رقما يبدو جيدا. والقياس لا يُخترع: ما لم نقسه لا نكتبه كنتيجة في تقرير.

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

أوامر الفصل:

الأمرعمله
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
الإجابات النهائية لأسئلة الفصل
11.1 لأن الطرف يصل جاهزا من البرنامج. يكفي أن يكون له value. وهكذا يعمل الصنف مع كائن بديل في الاختبار.
11.2 لأنه يحجب البرنامج: لا نستطيع خلال هذا الوقت قراءة حسّاس أو تحديث شاشة. pressed() يعود فورا في كل مرة.

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

المستوى الأول — أساسي
1.
أين نضع المكتبة على اللوحة؟ ولماذا؟
الإجابات: في المجلد lib، لأنه في sys.path، ولأن الجذر يبقى للبرنامج.
2.
ما الفرق بين mip.install('x') وmip.install('x', mpy=False)؟
الإجابات: الأول يثبّت ملفات .mpy، والثاني ملفات المصدر .py التي نقرؤها.
3.
وجدنا ملفا على الإنترنت بلا رخصة. هل نستعمله في مشروع نوزعه؟
الإجابات: لا إذن لنا. نطلب إذن صاحبه أو نبحث عن مكتبة برخصة، أو نكتب بأنفسنا من ورقة البيانات.
المستوى الثاني — تطبيق
4.
اكتبوا اختبارا لـ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')
يطبع: ads1115: all tests passed
5.
أضيفوا إلى اختبار Button حالة ضغطتين نظيفتين متتاليتين. ماذا نتوقع؟
الإجابات: مثلا: [0] * 12 + [1] * 12 + [0] * 12. نتوقع 2.
6.
صنّفوا إلى تعريف أو منطق:
(أ) تحويل القراءة الخام إلى درجة حرارة.
(ب) إرسال أمر تشغيل الشريحة.
(ج) تحديد إذا كان الجهاز يحتاج إلى تبريد.
الإجابات: (أ) و(ج) منطق. (ب) تعريف.
المستوى الثالث — تعمّق
7.
كيف نضيف إلى Button كشف الضغط الطويل، بحيث نختبره بالكائن FakeClock؟
الإجابات: نخزّن زمن بداية الضغط عند تصديق الحالة الجديدة، ونضيف دالة تقارن ticks_diff بالمدة المطلوبة وتعطي True مرة واحدة. وفي الاختبار نزيد clock.t بأيدينا ولا ننتظر.
8.
اقترح علينا برنامج ذكاء اصطناعي الأمر spi.write_read(tx). كيف نتحقق؟
الإجابات: نبحث في توثيق machine.SPI: الأوامر الموثقة هي write وread وreadinto وwrite_readinto، وهذا الأمر ليس بينها.
9.
لماذا لا يكشف اختبارنا في 11.6 خطأ في رقم مسجّل كتبناه خطأ في المكتبة والاختبار معا؟ وما الذي يكشفه؟
الإجابات: لأن الاختبار يقارن الشيفرة بافتراض كاتبها. تكشفه المقارنة بورقة البيانات، والتجربة على شريحة حقيقية.