البرمجة بلغة بايثون · الفصل 10 — الاتصال التسلسلي وأسس البرمجة النافذيةתקשורת טורית ויסודות בתכנות חלונאי · יסודות התכנות בשפת Python⌂📚🏠
الفصل 10

الاتصال التسلسلي وأسس البرمجة النافذية

תקשורת טורית ויסודות בתכנות חלונאי
· 15 ساعات تعليمية

عاشت برامجنا حتى الآن داخل الحاسوب: تقرأ من لوحة المفاتيح وتكتب على الشاشة، حتى السطر الذي فككناه في الفصل 5 كتبناه بأيدينا. في هذا الفصل يخرج البرنامج إلى العالم: يفتح خطا إلى لوحة أردوينو، يرسل إليها أوامر تشغّل ثنائيات باعثة للضوء، ويسألها عن مفتاح وعن حسّاسين. ثم نستبدل بالشاشة السوداء نافذة حقيقية فيها لافتات وأزرار ومربّعات نص.

في هذا الفصل:
  • نشرح كيف ينتقل بايت واحد على خط تسلسلي: معدّل الاتصال، خانة البداية، خانة النهاية وفحص الزوجية.
  • نفتح اتصالا بواسطة الفئة Serial، ونعرف الأخطاء التي تظهر عند فتحه.
  • نكتب ونقرأ بالعمليّات write وreadline وread وread_until وغيرها، ونميّز بين str وbytes.
  • نشغّل ثنائيات باعثة للضوء ونقرأ مفتاحا عبر الاتصال، وفق بروتوكول سؤال وجواب.
  • نبني نافذة بالمكتبة tkinter: عنوان، مقاسات، مكان على الشاشة.
  • نضيف إلى النافذة لافتة وزرّا ومربّع نص، ونكتب عمليّة تُنفَّذ عند حدث الضغط.

10.1 مبادئ الاتصال التسلسلي غير المتزامن – עקרונות בתקשורת טורית אסינכרונית

الاتصال التسلسلي (תקשורת טורית): إرسال المعطيات خانة بعد خانة على سلك واحد لكل اتجاه، لا عدة خانات معا على عدة أسلاك.

تكفي ثلاثة أسلاك لوصل الحاسوب بالمتحكّم: خط إرسال TX، خط استقبال RX، وأرضي مشترك GND تُقاس الجهود بالنسبة إليه. وفي لوحة أردوينو تصل هذه الخطوط إلى شريحة على اللوحة تنقل معطياتها عبر كبل USB، ويراها الحاسوب كمنفذ له اسم مثل COM3.

ComputerPython + pyserialArduinoSerial.begin(9600)TXRXwrite()RXTXreadline()GNDGND
خط الإرسال عند طرف هو خط الاستقبال عند الطرف الآخر، ولذلك يستطيع الطرفان أن يرسلا في اللحظة نفسها
غير متزامن (אסינכרונית): لا يوجد سلك ساعة بين الطرفين. يتفقان مسبقا على مدة الخانة، وكل طرف يعدّ الزمن وحده، ابتداء من لحظة يعلنها المرسل في أول كل بايت.
معدّل الاتصال (קצב תקשורת): عدد الخانات التي تمر على الخط في الثانية، ويُسمى أيضا baud. المعدّل الشائع مع أردوينو هو 9600 خانة في الثانية، ومدة الخانة هي مقلوبه.

البايت ليس ثماني خانات فقط، فهناك خانتان على الأقل تحيطان به، كما سنرى حالا. ولذلك نحسب عشر خانات لكل بايت:

baud = 9600
bit_time = 1 / baud
bits_per_byte = 1 + 8 + 1
byte_time = bits_per_byte * bit_time
print(round(bit_time * 1000000, 1), "us per bit")
print(round(byte_time * 1000, 3), "ms per byte")
print(baud // bits_per_byte, "bytes per second")
المُخرَج
104.2 us per bit
1.042 ms per byte
960 bytes per second

إطار البايت:

البايت الواحد ينتقل داخل إطار (מסגרת) له أجزاء ثابتة. نتبع في الشكل التالي الحرف "L"، وقيمته بالنظام الثنائي 01001100:

Start0D00D10D21D31D40D50D61D70P1Stop110idleidle010420831241752162572983393810421146us
إطار الحرف "L" على محور الزمن بمعدّل 9600، وفيه خانة زوجية زوجية (8E1): القيمة على الخط في كل خانة، وتحت المحور الزمن بالميكروثانية

الاختصار 8N1 هو الضبط الأكثر شيوعا: ثماني خانات معطيات، بلا زوجية N، وخانة نهاية واحدة، أي عشر خانات لكل بايت. وبهذا الضبط تعمل اللوحة والمكتبة ما لم نطلب غيره.

فحص الزوجية (בדיקת זוגיות): خانة يضيفها المرسل لكي يكون عدد الخانات 1، في المعطيات وخانة الزوجية معا، زوجيا (أو فرديا، إذا اتفق الطرفان على ذلك). يعدّ المستقبِل الخانات ويقارن.
مثال 10.1
نبني إطار الشكل السابق ببرنامج. كل ما نحتاج إليه من الفصل 5: القصّ بخطوة سالبة، التكرار، وcount.
الحل:
ch = "L"
bits = bin(ord(ch))[2:]
bits = "0" * (8 - len(bits)) + bits
print("Byte:", bits)
data = bits[::-1]
print("Sent first to last:", data)
parity = bits.count("1") % 2
print("Even parity bit:", parity)
frame = "0" + data + str(parity) + "1"
print("Frame:", frame, "=", len(frame), "bits")
المُخرَج
Byte: 01001100
Sent first to last: 00110010
Even parity bit: 1
Frame: 00011001011 = 11 bits
def parity_ok(data, parity):
    return (data + parity).count("1") % 2 == 0

print(parity_ok("00110010", "1"))
print(parity_ok("10110010", "1"))
print(parity_ok("10110011", "1"))
المُخرَج
True
False
True

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

خطأ شائع: على الطرفين أن يضبطا المعدّل نفسه بالضبط. عندما يختلفان لا تظهر رسالة خطأ: يأخذ المستقبِل عيّنة من وسط كل خانة حسب ساعته هو، فيقع في الخانة الخطأ ويقرأ حرفا آخر. في الشكل يرسل المرسل L، فيقرؤه مستقبِل بمعدّل 19200 على أنه à، حسب محاكاة صغيرة كتبناها لهذه العيّنات. هذا هو سبب الرموز الغريبة، وهذا أول ما نفحصه عندما تظهر.
Start0D00D10D21D31D40D50D61D70Stop110idleidle01042083124175216257298339381042us00000111
المرسل بمعدّل 9600 والمستقبِل بمعدّل 19200: النقاط الحمراء لحظات أخذ العيّنات، وهي أقرب بعضها إلى بعض من الخانات، فتقع أحيانا عيّنتان في الخانة نفسها
سؤال 10.1: ماذا يطبع البرنامج التالي؟ ما معنى 1 + 7 + 1 + 2؟
baud = 2400
bits = 1 + 7 + 1 + 2
print(bits)
print(baud // bits)

10.2 استعمال الفئة serial لإنشاء الاتصال – שימוש במחלקה serial לצורך יצירת תקשורת

ملاحظة: كل المُخرَجات في بنود الاتصال كتبها برنامج شغّلناه فعلا، والطرف الآخر من الخط في هذا التشغيل كان محاكاة لأردوينو، وهي تعرف المنفذ COM3 فقط. ثم شغّلنا البرامج نفسها مع لوحة Uno حقيقية موصولة بكبل USB من دون قطع إضافية، فجاءت الأجوبة حسب البروتوكول، ولكن بقيم اللوحة. ولكي نفعل ذلك نثبّت المكتبة بالأمر pip install pyserial، ونجد اسم المنفذ عندنا.

اسم الحزمة التي نثبّتها pyserial، لكن اسم الوحدة في الشيفرة import serial، وفيها الفئة serial.Serial بحرف كبير. المنهاج يقول «الفئة serial»، ويقصد هذه. وقبل أن نفتح الاتصال نسأل عن المنافذ الموجودة، ونجدها أيضا في «إدارة الأجهزة» في ويندوز، أو في بيئة أردوينو تحت القائمة Tools ثم Port:

from serial.tools import list_ports

for info in list_ports.comports():
    print(info)
    print(info.device)
المُخرَج
COM3 - Arduino Uno (COM3)
COM3

إنشاء كائن من الفئة Serial يفتح المنفذ (יציאה) فورا:

import serial
import time

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
print(port.port)
print(port.baudrate)
print(port.timeout)
print(port.is_open)
port.close()
print(port.is_open)
المُخرَج
COM3
9600
1
True
False

وللفئة وسائط أخرى تصف الإطار نفسه من البند 10.1:

الأمرعمله
portاسم المنفذ، مثل "COM3" في ويندوز
baudrateمعدّل الاتصال. الافتراضي 9600
bytesizeعدد خانات المعطيات: FIVEBITS حتى EIGHTBITS. الافتراضي 8
parityالزوجية: PARITY_NONE أو PARITY_EVEN أو PARITY_ODD. الافتراضي بلا زوجية
stopbitsخانات النهاية: STOPBITS_ONE أو STOPBITS_TWO. الافتراضي خانة واحدة
timeoutمهلة القراءة بالثواني. الافتراضي None، أي بلا حد
مثال 10.2
جهاز قياس في المختبر يعمل بزوجية زوجية: 8E1. نفتح الاتصال كما يطلب، بوسيط باسمه كما تعلمنا في الفصل 7.
الحل:
import serial
port = serial.Serial("COM3", 9600, timeout=1,
                     parity=serial.PARITY_EVEN)
print(port.bytesize, port.parity, port.stopbits)
port.close()
المُخرَج
8 E 1

يجب أن يوافق الطرف الآخر على ذلك. لو كان الطرف الآخر لوحة أردوينو لكتبنا فيها Serial.begin(9600, SERIAL_8E1).

خطأ شائع: إذا لم يوجد المنفذ توقف البرنامج عند الإنشاء. وكذلك إذا كان مشغولا: برنامج واحد فقط يستطيع أن يفتحه، فإذا تركنا نافذة المراقبة Serial Monitor مفتوحة في بيئة أردوينو لم يستطع برنامجنا أن يفتحه. سنتعلم في الفصل 11 كيف نلتقط مثل هذه الأخطاء.
import serial
port = serial.Serial("COM7", 9600, timeout=1)
print("Opened")
المُخرَج
serial.serialutil.SerialException: could not open port 'COM7':
FileNotFoundError(2, 
'The system cannot find the file specified.', None, 2)
خطأ شائع: المصيدة المشهورة: الأمر pip install serial يثبّت حزمة أخرى لا علاقة لها بموضوعنا، وعندها لا توجد الفئة Serial. والمصيدة الثانية: إذا سمّينا ملف برنامجنا serial.py فإن import serial يستورد برنامجنا نفسه بدل المكتبة. والمصيدة الثالثة: من دون timeout تنتظر القراءة بلا نهاية إذا لم يصل شيء، ويبدو البرنامج عالقا.
سؤال 10.2: ماذا يطبع البرنامج التالي؟
import serial
port = serial.Serial("COM3", 19200, timeout=2)
print(port.port, port.baudrate)
print(port.is_open)
port.close()
print(port.is_open)

10.3 استعمال العمليّات write وwritelines وopen وclose وread وreadline وread_until وget_settings – שימוש בפעולות write, writelines, open, close, read, readline, read_until, get_settings

الجمل النصية والبايتات:

البرنامج يتعامل مع الأحرف، أما الخط فينقل بايتات. ولذلك نحتاج إلى نوع جديد قبل أن نكتب شيئا.

سلسلة البايتات (רצף בתים): النوع bytes: تسلسل أعداد من 0 إلى 255، نكتبه كجملة نصية قبلها الحرف b، مثل b"PING".
text = "PING\n"
data = text.encode()
print(data)
print(type(data))
print(len(data))
print(data[0])
print(data.decode() == text)
print(b"OK\r\n".strip())
المُخرَج
b'PING\n'
<class 'bytes'>
5
80
True
b'OK'

العمليّة encode تحوّل الجملة النصية إلى بايتات، والعمليّة decode تعيدها جملة نصية. والعنصر الواحد من bytes عدد لا حرف: 80 هو رمز الحرف P، كما رأينا مع ord في الفصل 2. أما strip فتعمل هنا كما تعمل في الجمل النصية.

خطأ شائع: العمليّة write لا تقبل جملة نصية. وهذا أكثر خطأ نلقاه في الاتصال التسلسلي:
import serial
port = serial.Serial("COM3", 9600, timeout=1)
port.write("PING\n")
المُخرَج
TypeError: unicode strings are not supported, please encode to bytes: 'PING\n'

الكتابة والقراءة: write وreadline:

import serial
import time
port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
count = port.write(b"PING\n")
print(count)
raw = port.readline()
print(raw)
answer = raw.decode().strip()
print(answer)
print(answer == "PONG")
port.close()
المُخرَج
5
b'PONG\r\n'
PONG
True

العمليّة write تعيد عدد البايتات التي أُرسلت. وما تعيده readline نوعه bytes، وفي آخره الرمزان \r و\n، لأن اللوحة تنهي كل جواب بهما. لذلك نقرأ بخطوتين: decode ثم strip، ويمكن أن نكتبهما في تعبير واحد: port.readline().decode().strip().

خطأ شائع: من دون strip، لا يساوي الجواب ما نتوقعه أبدا، مع أنه يبدو على الشاشة صحيحا: طول "PONG\r\n" هو 6 حروف لا أربعة.
خطأ شائع: الأمر ينتهي بـ \n، واللوحة لا تبدأ بالتنفيذ قبل أن يصل. إذا نسيناه لم يصل جواب، وعادت readline بعد انتهاء المهلة بقيمة فارغة b''، بلا أي رسالة خطأ:
import serial
import time
port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
port.write(b"PING")
print(port.readline())
port.write(b"\n")
print(port.readline())
port.close()
المُخرَج
b''
b'PONG\r\n'

قراءة أجزاء: read وread_until:

العمليّة read(n) تعيد حتى n من البايتات، وبايتا واحدا إذا لم نحدد. إذا وصل أقل من ذلك عادت بما وصل عند انتهاء المهلة، وإذا لم يصل شيء عادت بقيمة فارغة. والعمليّة read_until تقرأ حتى تصل إلى البايتات التي نحددها، وتُبقيها ضمن النتيجة، ومن دون وسيط تتوقف عند \n، مثل readline:

import serial
import time
port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
port.write(b"PING\n")
print(port.read())
print(port.read(2))
print(port.read(100))
print(port.read(100))
port.write(b"READ\n")
print(port.read_until(b";"))
print(port.read_until())
port.close()
المُخرَج
b'P'
b'ON'
b'G\r\n'
b''
b'BAT=12.12;'
b'TMP=30.8\r\n'

كتابة عدة أوامر: writelines:

العمليّة writelines تكتب قائمة من bytes واحدا بعد الآخر. وهي لا تضيف \n من عندها، على الرغم من اسمها. أرسلنا ثلاثة أوامر، فوصلت ثلاثة أجوبة بالترتيب، أما القراءة الرابعة فلم تجد شيئا:

import serial
import time
port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
commands = [b"LED 1 ON\n", b"LED 2 ON\n", b"PING\n"]
port.writelines(commands)
print(port.readline())
print(port.readline())
print(port.readline())
print(port.readline())
port.close()
المُخرَج
b'OK\r\n'
b'OK\r\n'
b'PONG\r\n'
b''

الفتح والإغلاق: open وclose:

إذا أنشأنا الكائن بلا اسم منفذ لم يُفتح شيء، فنضبط صفاته أولا، ثم نفتحه بالعمليّة open. هذا مفيد عندما نختار المنفذ بعد أن يبدأ البرنامج. أما الكتابة بعد close فتوقف البرنامج، كما في آخر المُخرَج:

import serial
port = serial.Serial()
print(port.is_open)
port.port = "COM3"
port.baudrate = 9600
port.timeout = 1
port.open()
print(port.is_open)
port.close()
print(port.is_open)
port.write(b"PING\n")
المُخرَج
False
True
False
serial.serialutil.PortNotOpenError: Attempting to use a port that is not open
خطأ شائع: open لمنفذ مفتوح خطأ أيضا. وserial.Serial("COM3") يفتح المنفذ من تلقاء نفسه عندما نعطيه اسما، فلا نكتب بعده open.

قراءة الضبط: get_settings:

العمليّة get_settings تعيد قيم ضبط المنفذ، وهي مفيدة لكي نتأكد منه قبل أن نبحث عن خطأ في مكان آخر. النتيجة قاموس (מילון)، وهو بنية معطيات سنتوسع فيها في الفصل 11. يكفينا الآن أن قيمة كل اسم تُقرأ بين قوسين مربعين:

import serial
port = serial.Serial("COM3", 9600, timeout=1)
settings = port.get_settings()
print(settings["baudrate"])
print(settings["bytesize"], settings["parity"])
print(settings["stopbits"])
print(settings["timeout"])
port.close()
المُخرَج
9600
8 N
1
1

عمليّة تجمع كل شيء:

الخطوات تتكرر في كل أمر: نضيف \n، نحوّل، نكتب، نقرأ، نحوّل وننظف. نضعها في عمليّة كما في الفصل 7، ونسميها ask. وستخدمنا في بقية الفصل، ونكرر كتابتها هي وأوامر الاستيراد في رأس كل برنامج يستعملها، لكي يعمل كل برنامج وحده:

import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
print(ask(port, "PING"))
print(ask(port, "READ"))
print(ask(port, "HELLO"))
port.close()
المُخرَج
PONG
BAT=12.12;TMP=30.8
ERR
سؤال 10.3: ماذا يطبع البرنامج التالي؟ جواب اللوحة الأول هنا هو b'SW=0\r\n'.
import serial
import time
port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
port.write(b"SW?\n")
print(port.read(3))
print(port.readline().strip())
print(len(port.readline()))
port.close()

10.4 كتابة شيفرة بلغة بايثون تشغّل ثنائيات باعثة للضوء وتقرأ مفاتيح عبر الاتصال التسلسلي – כתיבת קוד בשפת python המפעיל נוריות וקולט מפסקים דרך התקשורת הטורית

البروتوكول (פרוטוקול): اتفاق بين طرفي الخط: ما الذي يُرسل، بأي صيغة، وما الذي يُجاب عليه. يجب أن يعرفه الطرفان.

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

الأمرالجوابالمعنى
PINGb'PONG\r\n'هل أنت موجودة؟
LED 1 ONb'OK\r\n'تشغيل ثنائي (נורית)، رقمه من 1 إلى 3. والأمر LED 1 OFF يطفئه
SW?b'SW=0\r\n'حالة المفتاح (מתג): 1 مضغوط، و0 غير مضغوط
READb'BAT=12.12;TMP=30.8\r\n'جهد البطارية والحرارة، بالصيغة التي فككناها في الفصل 5
HELLOb'ERR\r\n'أي سطر آخر
ComputerArduinob"LED 1 ON\n"b'OK\r\n'b"READ\n"b'BAT=12.12;TMP=30.8\r\n'
كل أمر يذهب في اتجاه، وجوابه يعود في الاتجاه الآخر، والحاسوب لا يرسل التالي قبل أن يقرأ الجواب

الشيفرة التي تعمل في اللوحة:

هذا هو الطرف الآخر من الخط، وهو يُكتب بلغة أردوينو لا بلغة بايثون. نجعل الثنائيات على الأطراف 8 و9 و10، كل ثنائي عبر مقاومة تحدّ تياره، والمفتاح بين الطرف 2 والأرضي، ومقسّم جهد البطارية على A0، وحسّاس الحرارة على A1، كما حسبناهما في الفصل 2. رفعنا هذه الشيفرة إلى لوحة Uno وفحصنا أجوبتها:

const int LED_PINS[] = {8, 9, 10};
const int SWITCH_PIN = 2;
const int BATTERY_PIN = A0;
const int TEMP_PIN = A1;

String line = "";

void setup() {
  Serial.begin(9600);
  for (int i = 0; i < 3; i++) {
    pinMode(LED_PINS[i], OUTPUT);
  }
  pinMode(SWITCH_PIN, INPUT_PULLUP);
}

void loop() {
  while (Serial.available() > 0) {
    char c = Serial.read();
    if (c == '\n') {
      line.trim();
      answer(line);
      line = "";
    } else {
      line += c;
    }
  }
}

العمليّة loop()، التي تنفّذها اللوحة مرة بعد مرة، تجمع الحروف في الجملة line، ولا تجيب إلا عند وصول '\n'. أي أن سطرا ناقصا يبقى منتظرا، وهذا ما رأيناه في البند 10.3. لم نستعمل هنا Serial.readStringUntil مع Serial.setTimeout، لأن تلك العمليّة تتوقف عن الانتظار بعد انقضاء المهلة وتعدّ ما وصل أمرا كاملا. بعد ذلك تأتي الإجابة:

void answer(String cmd) {
  if (cmd == "PING") {
    Serial.println("PONG");
  } else if (cmd == "SW?") {
    Serial.print("SW=");
    Serial.println(digitalRead(SWITCH_PIN) == LOW ? 1 : 0);
  } else if (cmd == "READ") {
    float bat = analogRead(BATTERY_PIN) * 5.0 / 1023 * 4;
    float tmp = analogRead(TEMP_PIN) * 5.0 / 1023 * 100;
    Serial.print("BAT=");
    Serial.print(bat, 2);
    Serial.print(";TMP=");
    Serial.println(tmp, 1);
  } else if (setLed(cmd)) {
    Serial.println("OK");
  } else {
    Serial.println("ERR");
  }
}

bool setLed(String cmd) {
  for (int i = 0; i < 3; i++) {
    String name = "LED " + String(i + 1);
    if (cmd == name + " ON") {
      digitalWrite(LED_PINS[i], HIGH);
      return true;
    }
    if (cmd == name + " OFF") {
      digitalWrite(LED_PINS[i], LOW);
      return true;
    }
  }
  return false;
}

تشغيل الثنائيات:

نبدأ بالأمر PING لكي نتأكد أن اللوحة موجودة، ثم نرسل الأوامر. الأمران الأخيران يبيّنان أن اللوحة ترفض رقما غير موجود، وأن الأحرف الكبيرة والصغيرة تختلف:

import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
if ask(port, "PING") == "PONG":
    print("LED 1 ON:", ask(port, "LED 1 ON"))
    print("LED 3 ON:", ask(port, "LED 3 ON"))
    print("LED 3 OFF:", ask(port, "LED 3 OFF"))
    print("LED 9 ON:", ask(port, "LED 9 ON"))
    print("led 1 on:", ask(port, "led 1 on"))
else:
    print("No answer from the board")
port.close()
المُخرَج
LED 1 ON: OK
LED 3 ON: OK
LED 3 OFF: OK
LED 9 ON: ERR
led 1 on: ERR
خطأ شائع: إذا أرسلنا أمرا ولم نقرأ جوابه، بقي الجواب منتظرا على الخط، وتأخذه القراءة التالية. عندها يصبح كل جواب جواب الأمر السابق: طلبنا هنا قراءة الحسّاسين، فوصل جواب تشغيل الثنائي. ولو مررناه إلى float لتوقف البرنامج. والقراءة الثانية تأتي بما طلبناه، متأخرة سطرا:
import serial
import time
port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
port.write(b"LED 1 ON\n")
port.write(b"READ\n")
print(port.readline())
print(port.readline())
port.close()
المُخرَج
b'OK\r\n'
b'BAT=12.12;TMP=30.8\r\n'

قراءة المفتاح:

الجواب سطر SW=1، فنفككه بالعمليّة split من الفصل 5. وفي المحاكاة تتغيّر حالة المفتاح وحدها، أما مع اللوحة فتتغيّر عندما نضغط بأيدينا:

import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
for i in range(5):
    answer = ask(port, "SW?")
    state = int(answer.split("=")[1])
    print(answer, state, state == 1)
port.close()
المُخرَج
SW=0 0 False
SW=1 1 True
SW=1 1 True
SW=0 0 False
SW=1 1 True
مثال 10.3
نربط المفتاح بالثنائي الأول: طالما المفتاح مضغوط يبقى الثنائي مضاء. اللوحة لا تعرف هذه العلاقة، فالقرار كله في برنامج بايثون، ولكي نغيّره لا نحتاج إلى برمجة اللوحة من جديد.
الحل:
import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
for i in range(5):
    answer = ask(port, "SW?")
    if answer == "SW=1":
        command = "LED 1 ON"
    else:
        command = "LED 1 OFF"
    print(answer, "->", command, ask(port, command))
    time.sleep(0.1)
port.close()
المُخرَج
SW=0 -> LED 1 OFF OK
SW=1 -> LED 1 ON OK
SW=1 -> LED 1 ON OK
SW=0 -> LED 1 OFF OK
SW=1 -> LED 1 ON OK
سؤال 10.4: ماذا يطبع البرنامج التالي؟
import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
replies = 0
for led in [1, 2, 3, 4]:
    if ask(port, "LED " + str(led) + " ON") == "OK":
        replies += 1
print(replies)
port.close()
▶تجربة حية — مختبر الاتصال التسلسليفي المختبر نرى الخط يعمل: خانات تتحرك بين الحاسوب والمتحكّم، ومنزلقان لمعدّلي الطرفين، فإذا اختلفا وصلت رموز مشوشة. يعمل فيه كائن جاهز باسم port من دون import، ونشغّل فيه ثمانية برامج جاهزة خطوة بعد خطوة، ونعدّلها، ونضغط أزرار نافذة مرسومة ترسل أوامر إلى المتحكّم. لكن المختبر مبسّط: write فيه تأخذ جملة نصية وreadline تعيد جملة نصية، أي من دون bytes، والعمليّة available خاصة به، وأوامره تختلف قليلا عن بروتوكول هذا الفصل، ولا يوجد فيه tkinter. ولذلك تُجرَّب برامج الكتاب نفسها على الحاسوب.
⚠
المختبر غير متاح الآن. المادة في هذا الفصل كاملة من دونه — نواصل القراءة، ونعود إلى التجربة لاحقا أو نفتحها مباشرة من الرابط الذي تحت الإطار.
https://fahed-lab.com/electronics/python_basics#serial
نفتح المختبر في صفحة مستقلة ↗ fahed-lab.com/electronics/python_basics#serial

10.5 إنشاء نافذة رسومية بالمكتبة tkinter: العنوان والمقاسات والمكان على الشاشة – יצירת חלון גרפי בספרייה Tkinter: כותרת, מידות ומיקום החלון על המסך

ننتقل إلى الوجه الثاني للفصل. المكتبة tkinter تأتي مع بايثون في ويندوز، فلا نثبّت شيئا. ونستوردها بالصيغة import tkinter as tk: الاسم tk اسم مختصر نختاره، ونكتبه قبل كل ما يأتي من المكتبة.

النافذة (חלון): مساحة على الشاشة لها إطار وعنوان وأزرار إغلاق وتصغير. في tkinter هي كائن من الفئة Tk.
import tkinter as tk

root = tk.Tk()
root.title("Station")
root.geometry("320x180+200+100")
root.resizable(False, False)
root.configure(bg="#e0f2fe")
root.mainloop()
tk_window
النافذة التي يفتحها البرنامج
ملاحظة: صور النوافذ في هذا الفصل ملتقطة من نوافذ حقيقية شغّلنا فيها البرامج نفسها، وضغطنا الأزرار وملأنا المربّعات من برنامج فحص ثم صوّرناها. شكل الإطار يختلف من حاسوب إلى آخر.

صيغة المكان هي "WIDTHxHEIGHT+X+Y"، بالبكسل، والحرف بين العرض والارتفاع هو x الصغير:

screentitleX = 200Y = 100width = 320height = 180(0, 0)
معنى الأعداد في "320x180+200+100": نقطة الأصل في الزاوية اليسرى العليا من الشاشة، والمحور Y يتجه إلى الأسفل
مثال 10.4
نريد نافذة في وسط شاشة مقاسها 1920 في 1080. نطرح مقاس النافذة من مقاس الشاشة ونقسم الفرق على اثنين، ونبني الجملة كما في الفصل 5.
الحل:
screen_w, screen_h = 1920, 1080
width, height = 320, 180
x = (screen_w - width) // 2
y = (screen_h - height) // 2
text = str(width) + "x" + str(height)
print(text + "+" + str(x) + "+" + str(y))
المُخرَج
320x180+800+450

البرنامج الحقيقي يأخذ مقاس الشاشة نفسها من root.winfo_screenwidth() وroot.winfo_screenheight()، بدل الأعداد الثابتة.

خطأ شائع: البرنامج يتوقف إذا كتبنا "320*180" أو "320 x 180"، فالصيغة دقيقة ولا تتسامح. وإذا نسينا mainloop انتهى البرنامج فورا، واختفت النافذة قبل أن نراها.
سؤال 10.5: نافذة ضبطناها بالأمر root.geometry("400x300+50+20"). ما إحداثيا زاويتها اليسرى العليا؟ وماذا يطبع البرنامج التالي عن حافتيها؟
x, y = 50, 20
width, height = 400, 300
print(x + width, y + height)

10.6 إنشاء لافتة لعرض نص: التصميم واللون والخط والمكان – יצירת תווית (Label) לצורך הצגת טקסט: עיצוב, צבע, גופן ומיקום על המסך

اللافتة (תווית): مكوّن (רכיב) يعرض نصا في النافذة، من الفئة Label. لا يكتب فيه المستخدم شيئا.

لكل مكوّن خطوتان: الإنشاء نحدد فيه النافذة التي يعيش فيها وخياراته، ووضعه في مكانه.

import tkinter as tk

root = tk.Tk()
root.title("Labels")
root.geometry("340x170+200+100")

head = tk.Label(root, text="Solar station")
head.config(font=("Arial", 16, "bold"))
head.config(fg="white", bg="#0f766e")
head.place(x=20, y=15)

volts = tk.Label(root, text="Battery: 12.12 V")
volts.config(font=("Consolas", 13), fg="#b91c1c")
volts.place(x=20, y=70)

temp = tk.Label(root, text="Temp: 30.8 C", width=20)
temp.config(font=("Arial", 11), bg="#fef3c7")
temp.place(x=20, y=110)

root.mainloop()
tk_labels
ثلاث لافتات، كل واحدة في المكان الذي حددناه لها
الأمرعمله
text="..."النص المعروض
font=("Arial", 16, "bold")الخط: الاسم، الحجم، والنمط "bold" أو "italic" إذا أردنا
fg="white" bg="#0f766e"لون الأحرف (foreground)، ولون الخلفية (background)
width=20العرض بعدد الأحرف، لا بالبكسل
place(x=20, y=70)الوضع بالبكسل، من الزاوية اليسرى العليا لداخل النافذة

الخيارات تُعطى عند الإنشاء أو بعده بالعمليّة config، وهي الاسم المختصر للعمليّة configure التي رأيناها في البند 10.5، والنتيجة واحدة. وهنا الفرق عن print: النص المعروض يبقى قابلا للتغيير طوال عمل البرنامج. config تغيّره، وcget تقرؤه.

الطريقة place تعطينا تحكما كاملا، لكننا نحسب كل شيء بأنفسنا. أما pack فترصّ المكوّنات واحدا تحت الآخر، في وسط النافذة، والخيار fill="x" يمدّ المكوّن على كل العرض، وpady يترك فراغا فوقه وتحته، وسنراها في البند التالي. والطريقة الثالثة grid تضع المكوّنات في صفوف وأعمدة، وسنستعملها في البند 10.8.

خطأ شائع: لافتة أنشأناها ولم نضعها لا تظهر، ولا تصلنا أي رسالة. ثم إن pack وgrid وplace تعيد None، ولذلك فإن info = tk.Label(root).pack() يترك في المتغيّر None، وعند أول info.config يتوقف البرنامج. نكتب الإنشاء في سطر والوضع في سطر، إلا إذا لم نحتج إلى المكوّن بعد ذلك. وفي النافذة الواحدة لا نخلط بين pack وgrid، لأن ذلك يوقف البرنامج أيضا.
سؤال 10.6: ماذا تعرض اللافتة عندما تظهر النافذة؟
import tkinter as tk

root = tk.Tk()
info = tk.Label(root, text="A")
info.pack()
info.config(text="B")
info.config(fg="red")
info.config(text=info.cget("text") + "C")
root.mainloop()

10.7 إنشاء زرّ وكتابة حدث لتنفيذ شيفرة عند الضغط عليه – יצירת לחצן (Button) וכתיבת אירוע לצורך ביצוע קוד בעקבות לחיצה

الزرّ (לחצן): مكوّن من الفئة Button يضغطه المستخدم بالفأرة.
الحدث (אירוע): شيء يحدث في أثناء عمل البرنامج ولا نعرف متى: ضغطة زرّ، كتابة حرف، إغلاق نافذة.

حتى الآن كانت البرامج تعمل من أول سطر إلى آخر سطر. برنامج النافذة يعمل بطريقة أخرى: يبني كل شيء، ثم يصل إلى mainloop وينتظر، وعند كل حدث يستدعي العمليّة التي ربطناها به، ويعود لينتظر. هذه هي حلقة الأحداث (לולאת אירועים).

root.mainloop()wait for an eventclick on Buttonwindow closedon_press()mainloop returns
ما يفعله mainloop: ينتظر، ويستدعي عمليّتنا عند الضغط، ولا يعود إلى البرنامج إلا عند إغلاق النافذة
مثال 10.5
نبني عدّادا للضغطات: زرّ، ولافتة تعرض كم مرة ضُغط.
الحل:
import tkinter as tk

count = 0

def on_press():
    global count
    count += 1
    shown.config(text="Presses: " + str(count))

root = tk.Tk()
root.title("Counter")
root.geometry("300x120+200+100")

shown = tk.Label(root, text="Presses: 0")
shown.config(font=("Arial", 14))
shown.pack(pady=12)
button = tk.Button(root, text="Press me", command=on_press)
button.pack()

root.mainloop()
tk_count0
عند الفتح
tk_count3
بعد ثلاث ضغطات
خطأ شائع: command=on_press() يستدعي العمليّة مرة واحدة عند بناء الزرّ، ويضع ما أعادته، وهو None، مكان معالج الحدث. فلا يحدث شيء عند الضغط.
خطأ شائع: طالما كان معالج الحدث يعمل، لا تستجيب النافذة لشيء. لذلك لا نضع فيه time.sleep طويلا ولا حلقة طويلة. وأي سطر بعد mainloop لا يُنفَّذ إلا بعد إغلاق النافذة.
سؤال 10.7: في البرنامج التالي يُضغط الزرّ مرتين بواسطة العمليّة invoke، وهي تفعل ما تفعله ضغطة الفأرة. ما قيمة calls في النهاية؟
import tkinter as tk

calls = 0

def on_press():
    global calls
    calls += 1

root = tk.Tk()
button = tk.Button(root, text="Go", command=on_press())
button.pack()
button.invoke()
button.invoke()
root.mainloop()

10.8 إنشاء مربّع نص لاستقبال معطيات من المستخدم – יצירת תיבת טקסט (TextBox) לצורך קליטת נתונים מהמשתמש

مربّع النص (תיבת טקסט): مكوّن يكتب فيه المستخدم سطرا واحدا. في tkinter اسم الفئة Entry، وهو ما يسميه المنهاج TextBox.

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

الأمرعمله
box.get()النص الموجود في المربّع، دائما من النوع str
box.insert(0, "12")يضع نصا ابتداء من الموقع 0
box.delete(0, tk.END)يمسح كل ما في المربّع

نكتب حاسبة لقانون أوم. فيها مربّعا نص وعنوان لكل مربّع، ولذلك نستعمل grid: كل مكوّن في صف وعمود، وcolumnspan يمدّ المكوّن على عمودين:

import tkinter as tk

def calculate():
    volts = float(volts_box.get())
    ohms = float(ohms_box.get())
    milli = round(volts / ohms * 1000, 2)
    result.config(text="I = " + str(milli) + " mA")

root = tk.Tk()
root.title("Ohm's law")
root.geometry("300x170+200+100")

tk.Label(root, text="Volts:").grid(row=0, column=0, pady=8)
volts_box = tk.Entry(root, width=12)
volts_box.grid(row=0, column=1)

tk.Label(root, text="Ohms:").grid(row=1, column=0)
ohms_box = tk.Entry(root, width=12)
ohms_box.grid(row=1, column=1)

go = tk.Button(root, text="Calculate", command=calculate)
go.grid(row=2, column=0, columnspan=2, pady=10)

result = tk.Label(root, text="I = ?", font=("Arial", 13))
result.grid(row=3, column=0, columnspan=2)

root.mainloop()
tk_ohm
بعد كتابة 12 و470 والضغط على الزرّ
خطأ شائع: ما يعود من get جملة نصية، تماما مثل input، ولذلك نحوّله قبل الحساب. وإذا قرأناه خارج معالج الحدث، عند بناء النافذة، حصلنا على جملة فارغة، لأن المستخدم لم يكتب شيئا بعد.
مثال 10.6
في البرنامج السابق، إذا كتب المستخدم abc أو ترك المربّع فارغا لم تتغيّر اللافتة، وظهرت رسالة الخطأ في الشاشة السوداء فقط، وبقيت النافذة مفتوحة: الخطأ في معالج الحدث لا يوقف حلقة الأحداث. نفحص الجملة النصية قبل التحويل بالفحص الذي بنيناه في الفصل 5، ونرفض المقاومة صفر، لأن القسمة عليه تعطي خطأ كهذا. يتغيّر رأس البرنامج فقط، أما النافذة فتبقى كما هي:
الحل:
import tkinter as tk

def is_number(text):
    digits = text.replace(".", "")
    return digits.isnumeric() and text.count(".") <= 1

def calculate():
    volts = volts_box.get().strip()
    ohms = ohms_box.get().strip()
    if is_number(volts) and is_number(ohms) and float(ohms) > 0:
        milli = round(float(volts) / float(ohms) * 1000, 2)
        result.config(text="I = " + str(milli) + " mA", fg="black")
    else:
        result.config(text="Bad value", fg="#b91c1c")

root = tk.Tk()
root.title("Ohm's law")
root.geometry("300x170+200+100")

tk.Label(root, text="Volts:").grid(row=0, column=0, pady=8)
volts_box = tk.Entry(root, width=12)
volts_box.grid(row=0, column=1)

tk.Label(root, text="Ohms:").grid(row=1, column=0)
ohms_box = tk.Entry(root, width=12)
ohms_box.grid(row=1, column=1)

go = tk.Button(root, text="Calculate", command=calculate)
go.grid(row=2, column=0, columnspan=2, pady=10)

result = tk.Label(root, text="I = ?", font=("Arial", 13))
result.grid(row=3, column=0, columnspan=2)

root.mainloop()
tk_check_bad
بعد كتابة 4.7k والضغط على الزرّ
سؤال 10.8: ماذا يطبع البرنامج التالي؟ ولماذا نحتاج إلى strip عند قراءة مربّع نص؟
def is_number(text):
    digits = text.replace(".", "")
    return digits.isnumeric() and text.count(".") <= 1

print(is_number("12"))
print(is_number("1.5.2"))
print(is_number(" 12"))
print(is_number(" 12".strip()))
مشروع الكتاب: محطة القياس · المرحلة 10 — المحطة تتكلّم

حتى الآن كان السطر BAT=12.42;TMP=31.5 يصل إلى البرنامج من المستخدم. الآن يصل من الخط. نرسل READ ست مرات، ونفكّ الجواب كما في الفصل 5، ونقرّر كما في الفصل 3. لكننا لا ننتظر هنا حتى تصل المحطة إلى حدّي الفصل 3 (البطارية 11.8 V والحرارة 40 درجة)، بل نشغّل ثنائيا تحذيريا قبل ذلك: عندما تهبط البطارية عن 12 V، أو تزيد الحرارة على 33 درجة.

def parse_line(line):
    battery, temp = -1.0, -1.0
    for item in line.split(";"):
        pos = item.find("=")
        name = item[:pos]
        value = float(item[pos + 1:])
        if name == "BAT":
            battery = value
        elif name == "TMP":
            temp = value
    return battery, temp
import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

def parse_line(line):
    battery, temp = -1.0, -1.0
    for item in line.split(";"):
        pos = item.find("=")
        name = item[:pos]
        value = float(item[pos + 1:])
        if name == "BAT":
            battery = value
        elif name == "TMP":
            temp = value
    return battery, temp

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
led_on = False
for i in range(6):
    line = ask(port, "READ")
    if line == "":
        print("No answer")
    else:
        battery, temp = parse_line(line)
        warn = battery < 12.0 or temp > 33.0
        print(line, "warn:", warn)
        if warn and not led_on:
            print("LED 1 ON:", ask(port, "LED 1 ON"))
            led_on = True
        elif led_on and not warn:
            print("LED 1 OFF:", ask(port, "LED 1 OFF"))
            led_on = False
    time.sleep(1)
port.close()
المُخرَج
BAT=12.12;TMP=30.8 warn: False
BAT=12.08;TMP=31.3 warn: False
BAT=12.02;TMP=32.3 warn: False
BAT=11.96;TMP=33.7 warn: True
LED 1 ON: OK
BAT=11.93;TMP=34.7 warn: True
BAT=11.89;TMP=36.2 warn: True

نرسل الأمر عند التغيّر فقط، لا في كل دورة، ولذلك نحفظ الحالة في led_on. والجواب الفارغ يعني أن المهلة انتهت، فلا نحاول أن نفككه. ولم نكرر هنا فحص الأرقام من الفصل 5، بل اكتفينا بالدالة float، لأن السطر يأتي من شيفرة كتبناها بأنفسنا، وصيغته معروفة. وهذا ضروري أيضا: ذلك الفحص يرفض الحرارة السالبة، أما float فيقبلها: "-3.5".replace(".", "").isnumeric() يعطي False، وfloat("-3.5") يعطي -3.5.

ثم نضع المحطة في نافذة. الزرّ Read يعرض القراءتين، والزرّ LED 1 يبدّل حالة الثنائي الأول، ولا يبدّلها في البرنامج إلا إذا أجابت اللوحة OK. العمليّة ask هنا تعرف port بلا وسيط، لأن معالج الحدث لا يأخذ وسائط. وبعد mainloop نغلق المنفذ، وهذا السطر يُنفَّذ عند إغلاق النافذة.

import serial
import time
import tkinter as tk

def ask(command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

def on_read():
    line = ask("READ")
    if line != "":
        parts = line.split(";")
        bat_label.config(text=parts[0] + " V")
        tmp_label.config(text=parts[1] + " C")

def on_led():
    global led_on
    if led_on:
        command = "LED 1 OFF"
    else:
        command = "LED 1 ON"
    if ask(command) == "OK":
        led_on = not led_on
        state.config(text=command)

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
led_on = False

root = tk.Tk()
root.title("Station")
root.geometry("300x190+200+100")

big = ("Arial", 14, "bold")
bat_label = tk.Label(root, text="BAT=?", font=big)
bat_label.pack(pady=4)
tmp_label = tk.Label(root, text="TMP=?", font=big)
tmp_label.pack()
read_btn = tk.Button(root, text="Read", command=on_read)
read_btn.pack(pady=6)
led_btn = tk.Button(root, text="LED 1", command=on_led)
led_btn.pack()
state = tk.Label(root, text="LED 1 OFF")
state.pack(pady=4)

root.mainloop()
port.close()
tk_station
بعد الضغط على Read ثم على LED 1

جاءت القراءة في الصورة من محاكاة الأردوينو، وهي القراءة الأولى نفسها في مُخرَج البرنامج أعلاه. نجرّب بأنفسنا: كيف نضيف زرّا يعرض حالة المفتاح؟ وكيف نلوّن لافتة البطارية بالأحمر تحت 12 V؟

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

ملخّص أوامر الفصل:

الأمرعمله
serial.Serial("COM3", 9600, timeout=1)إنشاء الكائن وفتح المنفذ. التثبيت: pip install pyserial
port.open() port.close()فتح المنفذ وإغلاقه
port.write(b"...")كتابة بايتات، وتعيد عددها
port.writelines([b"...", b"..."])كتابة قائمة، بلا إضافة نهاية سطر
port.read(n)قراءة حتى n بايتات
port.readline()قراءة سطر حتى \n
port.read_until(b";")قراءة حتى الفاصل، وهو ضمن النتيجة
port.get_settings()قاموس بضبط المنفذ
s.encode() b.decode()من str إلى bytes، ومن bytes إلى str
root = tk.Tk() root.title("...")إنشاء النافذة وعنوانها
root.geometry("WxH+X+Y")المقاسات والمكان على الشاشة
root.mainloop()حلقة الأحداث، حتى إغلاق النافذة
tk.Label(root, text="...")لافتة
tk.Button(root, text="...", command=f)زرّ، و f تُستدعى عند الضغط
tk.Entry(root, width=12)مربّع نص
w.pack() w.place(x=, y=) w.grid(row=, column=)وضع المكوّن في النافذة
w.config(text="...") w.cget("text")تغيير خيار، وقراءته
box.get() box.insert(0, s) box.delete(0, tk.END)قراءة مربّع النص، الكتابة فيه، ومسحه

قاموس المصطلحات – מילון מונחים:

بالعربيةبالعبرية (كما تظهر في الامتحان)English
الاتصال التسلسليתקשורת טוריתSerial communication
غير متزامنאסינכרוניתAsynchronous
المنفذיציאהPort
معدّل الاتصالקצב תקשורתBaud rate
الإطارמסגרתFrame
خانة البدايةסיבית התחלהStart bit
خانة النهايةסיבית סיוםStop bit
فحص الزوجيةבדיקת זוגיותParity check
مهلة الانتظارפסק זמןTimeout
سلسلة البايتاتרצף בתיםBytes
البروتوكولפרוטוקולProtocol
ثنائي باعث للضوءנוריתLED
مفتاح كهربائيמתגSwitch
النافذةחלוןWindow
مكوّنרכיבWidget
اللافتةתוויתLabel
الزرّלחצןButton
مربّع النصתיבת טקסטText box (Entry)
الحدثאירועEvent
معالج الحدثמטפל באירועEvent handler
حلقة الأحداثלולאת אירועיםEvent loop
الإجابات النهائية لأسئلة الفصل
10.1 11 , 218 ، خانة بداية، سبع خانات معطيات، خانة زوجية وخانتا نهاية.
10.2 COM3 19200 , True , False
10.3 b'SW=' , b'0' , 0
10.4 3
10.5 50 , 20 ، ثم الحافتان: 450 320
10.6 BC
10.7 1 : القوسان بعد on_press استدعيا العمليّة عند البناء، ولم يبقَ للزرّ معالج.
10.8 True , False , False , True ، لأن مسافة زائدة ليست رقما، والمستخدم لا يراها.

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

المستوى الأول — أساسي
1.
ماذا يطبع البرنامج؟
data = "SW?\n".encode()
print(len(data))
print(data[0])
print(b"SW=1\r\n".decode().strip())
print(b"SW=1\r\n".strip().decode()[-1])
الإجابات: 4 , 83 , SW=1 , 1
2.
نضبط الخط على سبع خانات معطيات، زوجية وخانة نهاية واحدة، بمعدّل 4800. نكتب برنامجا يحسب عدد الخانات في الإطار، وزمن إرسال 100 بايت.
الإجابات:
baud = 4800
bits = 1 + 7 + 1 + 1
print(bits, "bits per byte")
print(round(100 * bits / baud * 1000, 1), "ms for 100 bytes")
يطبع: 10 bits per byte , 208.3 ms for 100 bytes
3.
نكتب برنامجا يقرأ حرفا، ويطبع خانة الزوجية له في الزوجية الزوجية وفي الزوجية الفردية.
الإجابات:
ch = input("Character: ")
ones = bin(ord(ch)).count("1")
print("Even parity bit:", ones % 2)
print("Odd parity bit:", 1 - ones % 2)
للمُدخَل L يطبع: Even parity bit: 1 , Odd parity bit: 0
4.
يوجد في البرنامج التالي أربعة أخطاء، أحدها يوقف البرنامج. نجدها ونصلحها.
import serial
import time
port = serial.Serial("COM3", 9600)
time.sleep(2)
port.write("PING")
if port.readline() == "PONG":
    print("Board found")
الإجابات: لا يوجد timeout، وتُرسل جملة نصية بدل bytes، ولا يوجد \n في آخر الأمر، والجواب يُقارَن بجملة نصية قبل التحويل:
import serial
import time
port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
port.write(b"PING\n")
answer = port.readline().decode().strip()
if answer == "PONG":
    print("Board found")
port.close()
يطبع: Board found
5.
نكتب برنامجا يفتح نافذة عنوانها Panel A، مقاسها 300 في 100، في المكان (150, 60)، وفيها لافتة بأحرف بيضاء على خلفية خضراء، تمتد على كل العرض.
الإجابات:
import tkinter as tk

root = tk.Tk()
root.title("Panel A")
root.geometry("300x100+150+60")

alarm = tk.Label(root, text="ALARM OFF")
alarm.config(font=("Arial", 18, "bold"))
alarm.config(fg="white", bg="#15803d")
alarm.pack(fill="x", pady=20)

root.mainloop()
المستوى الثاني — متوسط
6.
نقرأ المفتاح عشر مرات، ونعدّ مرات الضغط. لا نعدّ كل قراءة فيها 1، فالضغطة الواحدة تستمر قراءات كثيرة، بل نعدّ الانتقال من 0 إلى 1.
الإجابات:
import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
presses = 0
last = "SW=0"
for i in range(10):
    now = ask(port, "SW?")
    if now == "SW=1" and last == "SW=0":
        presses += 1
    last = now
print("Presses:", presses)
port.close()
في المحاكاة يطبع: Presses: 4. ومع مفتاح حقيقي نضيف تأخيرا قصيرا بين القراءتين، لأن تلامس المفتاح يهتزّ لحظة عند الضغط.
7.
نبني نافذة تقرأ قيمة محوّل بين 0 و1023 من مربّع نص، وتعرض الجهد عند مرجع 5 V، كما في الفصل 2.
الإجابات:
import tkinter as tk

def convert():
    adc = int(box.get())
    volts = round(adc * 5 / 1023, 2)
    shown.config(text=str(volts) + " V")

root = tk.Tk()
root.title("ADC to volts")
root.geometry("300x130+150+60")

box = tk.Entry(root, width=10)
box.pack(pady=8)
tk.Button(root, text="Convert", command=convert).pack()
shown = tk.Label(root, text="? V", font=("Arial", 14))
shown.pack(pady=8)

root.mainloop()
للقيمة 620 تعرض اللافتة 3.03 V.
المستوى الثالث — تحدٍّ
8.
نقرأ READ ست مرات، ونجمع جهود البطارية في قائمة. نطبع المتوسط، أدنى قيمة، ومقدار الهبوط بين القراءة الأولى والأخيرة.
الإجابات:
import serial
import time

def ask(port, command):
    port.write((command + "\n").encode())
    return port.readline().decode().strip()

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)
values = []
for i in range(6):
    line = ask(port, "READ")
    first = line.split(";")[0]
    values.append(float(first.split("=")[1]))
print("Average:", round(sum(values) / len(values), 2))
print("Lowest:", min(values))
print("Drop:", round(values[0] - values[-1], 2))
port.close()
في المحاكاة يطبع: Average: 12.0 , Lowest: 11.89 , Drop: 0.23
9.
نكتب عمليّة تتلقى إطارا من 11 خانة، بالصيغة 8E1 كجملة نصية، وتعيد الحرف، أو نوع الخطأ: الطول، خانتا البداية والنهاية، أو الزوجية. ونفحصها بالإطار الذي بنيناه في البند 10.1، وبنسخ مشوهة منه.
الإجابات:
def decode(frame):
    if len(frame) != 11:
        return "LENGTH ERROR"
    if frame[0] != "0" or frame[10] != "1":
        return "FRAME ERROR"
    if frame[1:10].count("1") % 2 != 0:
        return "PARITY ERROR"
    return chr(int(frame[1:9][::-1], 2))

print(decode("00011001011"))
print(decode("00011001111"))
print(decode("10011001011"))
print(decode("0001100101"))
يطبع: L , PARITY ERROR , FRAME ERROR , LENGTH ERROR
10.
نبني نافذة «طرفية»: يكتب المستخدم أمرا في مربّع نص، بأحرف كبيرة أو صغيرة. يرسله الزرّ Send إلى اللوحة، وتعرض لافتة الأمر والجواب، ويُمسح المربّع. ونفحصها بالأمر led 2 on.
الإجابات:
import serial
import time
import tkinter as tk

def send():
    text = box.get().strip().upper()
    port.write((text + "\n").encode())
    answer = port.readline().decode().strip()
    reply.config(text=text + " -> " + answer)
    box.delete(0, tk.END)

port = serial.Serial("COM3", 9600, timeout=1)
time.sleep(2)

root = tk.Tk()
root.title("Terminal")
root.geometry("300x130+150+60")

box = tk.Entry(root, width=26)
box.pack(pady=8)
tk.Button(root, text="Send", command=send).pack()
reply = tk.Label(root, text="", font=("Consolas", 11))
reply.pack(pady=8)

root.mainloop()
port.close()
tk_e_term
بعد كتابة الأمر والضغط على الزرّ. الجواب من المحاكاة