الاتصال التسلسلي وأسس البرمجة النافذية
عاشت برامجنا حتى الآن داخل الحاسوب: تقرأ من لوحة المفاتيح وتكتب على الشاشة، حتى السطر الذي فككناه في الفصل 5 كتبناه بأيدينا. في هذا الفصل يخرج البرنامج إلى العالم: يفتح خطا إلى لوحة أردوينو، يرسل إليها أوامر تشغّل ثنائيات باعثة للضوء، ويسألها عن مفتاح وعن حسّاسين. ثم نستبدل بالشاشة السوداء نافذة حقيقية فيها لافتات وأزرار ومربّعات نص.
- نشرح كيف ينتقل بايت واحد على خط تسلسلي: معدّل الاتصال، خانة البداية، خانة النهاية وفحص الزوجية.
- نفتح اتصالا بواسطة الفئة
Serial، ونعرف الأخطاء التي تظهر عند فتحه. - نكتب ونقرأ بالعمليّات
writeوreadlineوreadوread_untilوغيرها، ونميّز بينstrوbytes. - نشغّل ثنائيات باعثة للضوء ونقرأ مفتاحا عبر الاتصال، وفق بروتوكول سؤال وجواب.
- نبني نافذة بالمكتبة
tkinter: عنوان، مقاسات، مكان على الشاشة. - نضيف إلى النافذة لافتة وزرّا ومربّع نص، ونكتب عمليّة تُنفَّذ عند حدث الضغط.
10.1 مبادئ الاتصال التسلسلي غير المتزامن – עקרונות בתקשורת טורית אסינכרונית
تكفي ثلاثة أسلاك لوصل الحاسوب بالمتحكّم: خط إرسال TX، خط استقبال RX، وأرضي مشترك GND تُقاس الجهود بالنسبة إليه. وفي لوحة أردوينو تصل هذه الخطوط إلى شريحة على اللوحة تنقل معطياتها عبر كبل USB، ويراها الحاسوب كمنفذ له اسم مثل COM3.
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:
"L" على محور الزمن بمعدّل 9600، وفيه خانة زوجية زوجية (8E1): القيمة على الخط في كل خانة، وتحت المحور الزمن بالميكروثانية- الخط في حالة الراحة يبقى عند القيمة
1. لذلك يختلف الخط الساكن عن خط مقطوع. - خانة البداية (סיבית התחלה) قيمتها
0دائما. الهبوط من1إلى0يقول للمستقبِل أن يبدأ العدّ الآن. - خانات المعطيات، وعددها عادة ثمان، تُرسل من الخانة الأقل وزنا
D0إلى الأعلى وزناD7. ولهذا يظهر البايت على الخط مقلوبا. - خانة الزوجية
Pاختيارية، وسنشرحها بعد قليل. - خانة النهاية (סיבית סיום) قيمتها
1دائما، وهي تعيد الخط إلى الراحة، فيكون الهبوط التالي واضحا.
الاختصار 8N1 هو الضبط الأكثر شيوعا: ثماني خانات معطيات، بلا زوجية N، وخانة نهاية واحدة، أي عشر خانات لكل بايت. وبهذا الضبط تعمل اللوحة والمكتبة ما لم نطلب غيره.
1، في المعطيات وخانة الزوجية معا، زوجيا (أو فرديا، إذا اتفق الطرفان على ذلك). يعدّ المستقبِل الخانات ويقارن.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 على أنه à، حسب محاكاة صغيرة كتبناها لهذه العيّنات. هذا هو سبب الرموز الغريبة، وهذا أول ما نفحصه عندما تظهر.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
- الوسيط الأول اسم المنفذ، والثاني معدّل الاتصال، ويجب أن يساوي ما كتبناه في اللوحة.
- الوسيط
timeoutهو مهلة الانتظار (פסק זמן) بالثواني: كم تنتظر القراءة قبل أن تعود بما وصل. - الأمر
time.sleep(2)ليس زينة: تعيد لوحة أردوينو تشغيل نفسها عند فتح المنفذ، وما نرسله في أثناء ذلك يضيع. في مُخرَجات الكتاب لا ينتظر هذا الأمر، أما مع اللوحة فهو ضروري: جرّبنا على لوحة Uno من دونه، فعادت القراءة الأولى فارغة بعد ثانية. - الصفة
is_openتجيب هل المنفذ مفتوح، وclose()تغلقه لكي تستطيع برامج أخرى أن تستعمله.
وللفئة وسائط أخرى تصف الإطار نفسه من البند 10.1:
| الأمر | عمله |
|---|---|
| port | اسم المنفذ، مثل "COM3" في ويندوز |
| baudrate | معدّل الاتصال. الافتراضي 9600 |
| bytesize | عدد خانات المعطيات: FIVEBITS حتى EIGHTBITS. الافتراضي 8 |
| parity | الزوجية: PARITY_NONE أو PARITY_EVEN أو PARITY_ODD. الافتراضي بلا زوجية |
| stopbits | خانات النهاية: STOPBITS_ONE أو STOPBITS_TWO. الافتراضي خانة واحدة |
| timeout | مهلة القراءة بالثواني. الافتراضي None، أي بلا حد |
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).
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 تنتظر القراءة بلا نهاية إذا لم يصل شيء، ويبدو البرنامج عالقا.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
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 המפעיל נוריות וקולט מפסקים דרך התקשורת הטורית
أبسط البروتوكولات هو سؤال وجواب: الحاسوب يرسل أمرا في سطر، واللوحة تنفذ وتجيب بسطر واحد. وحتى الأمر غير المعروف يحظى بجواب، وإلا انتظر البرنامج شيئا لن يصل. هذا هو بروتوكول محطتنا، والأجوبة في الجدول خرجت من الأوامر نفسها:
| الأمر | الجواب | المعنى |
|---|---|---|
PING | b'PONG\r\n' | هل أنت موجودة؟ |
LED 1 ON | b'OK\r\n' | تشغيل ثنائي (נורית)، رقمه من 1 إلى 3. والأمر LED 1 OFF يطفئه |
SW? | b'SW=0\r\n' | حالة المفتاح (מתג): 1 مضغوط، و0 غير مضغوط |
READ | b'BAT=12.12;TMP=30.8\r\n' | جهد البطارية والحرارة، بالصيغة التي فككناها في الفصل 5 |
HELLO | b'ERR\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;
}Serial.printlnيضيف إلى السطر الرمزين\rو\n. هذا هو مصدر النهاية التي نزيلها بالعمليّةstrip.Serial.print(bat, 2)تطبع عددا بمنزلتين عشريتين. بالقراءتين620و63نحصل على الجوابBAT=12.12;TMP=30.8.- مع
INPUT_PULLUPيكون الطرف في حالةHIGHعند ترك المفتاح، وLOWعند الضغط، ولذلك نعكس القيمة قبل الإرسال.
تشغيل الثنائيات:
نبدأ بالأمر 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
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
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()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.Tk()ينشئ النافذة، وtitleيكتب العنوان في الشريط العلوي.geometryيحدد المقاسات والمكان معا، وسنفكك صيغته حالا.resizable(False, False)يمنع تغيير المقاسات بالفأرة، في الاتجاهين.configure(bg=...)يغيّر لون الخلفية، باسمه بالإنجليزية أو بقيمته.mainloop()يبقي النافذة حيّة، ولا يعود حتى تُغلق. سنعود إليه في البند 10.7.
صيغة المكان هي "WIDTHxHEIGHT+X+Y"، بالبكسل، والحرف بين العرض والارتفاع هو x الصغير:
"320x180+200+100": نقطة الأصل في الزاوية اليسرى العليا من الشاشة، والمحور Y يتجه إلى الأسفل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 انتهى البرنامج فورا، واختفت النافذة قبل أن نراها.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()| الأمر | عمله |
|---|---|
| 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، لأن ذلك يوقف البرنامج أيضا.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 وينتظر، وعند كل حدث يستدعي العمليّة التي ربطناها به، ويعود لينتظر. هذه هي حلقة الأحداث (לולאת אירועים).
mainloop: ينتظر، ويستدعي عمليّتنا عند الضغط، ولا يعود إلى البرنامج إلا عند إغلاق النافذة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()- الوسيط
command=on_pressيربط الزرّ بالعمليّةon_press. هي معالج الحدث (מטפל באירוע)، ونكتب اسمها فقط، بلا قوسين. - معالج الحدث لا يأخذ وسائط، ولا أحد يستعمل ما يعيده. لذلك يغيّر اللافتة بالعمليّة
config، ويغيّر العدّاد بالأمرglobalكما تعلمنا في الفصل 7. - العمليّة كُتبت قبل أن تظهر
shownفي البرنامج، وهذا لا يضرّ: هي تبحث عنها عند الضغط، أي بعد أن تكون قد أُنشئت.
command=on_press() يستدعي العمليّة مرة واحدة عند بناء الزرّ، ويضع ما أعادته، وهو None، مكان معالج الحدث. فلا يحدث شيء عند الضغط.time.sleep طويلا ولا حلقة طويلة. وأي سطر بعد mainloop لا يُنفَّذ إلا بعد إغلاق النافذة.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()12 و470 والضغط على الزرّget جملة نصية، تماما مثل input، ولذلك نحوّله قبل الحساب. وإذا قرأناه خارج معالج الحدث، عند بناء النافذة، حصلنا على جملة فارغة، لأن المستخدم لم يكتب شيئا بعد.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()4.7k والضغط على الزرّ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()))حتى الآن كان السطر 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, tempimport 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()Read ثم على LED 1جاءت القراءة في الصورة من محاكاة الأردوينو، وهي القراءة الأولى نفسها في مُخرَج البرنامج أعلاه. نجرّب بأنفسنا: كيف نضيف زرّا يعرض حالة المفتاح؟ وكيف نلوّن لافتة البطارية بالأحمر تحت 12 V؟
أهمّ أفكار الفصل:
- البايت ينتقل في إطار: خانة بداية
0، معطيات من الأقل وزنا، زوجية اختيارية، وخانة نهاية1. - لا توجد ساعة مشتركة، ولذلك يجب أن يتساوى المعدّل في الطرفين، وإلا وصلت رموز غريبة بلا رسالة خطأ.
- الخط ينقل
bytes: نكتب بواسطةencode، ونقرأ بواسطةdecodeثمstrip. - نعطي
timeoutدائما، وننتظر بعد فتح المنفذ حتى تنتهي اللوحة من إعادة التشغيل. - البروتوكول اتفاق: كل أمر ينتهي بـ
\n، ولكل أمر جواب، ونقرأ الجواب قبل الأمر التالي. - برنامج النافذة يبني المكوّنات ثم ينتظر في
mainloop، ولا يعمل إلا عند الأحداث. - كل مكوّن ينشأ ثم يوضع، و
commandنكتب فيه اسم العمليّة بلا قوسين. - ما يعود من
getجملة نصية: نفحصها، ثم نحوّلها.
ملخّص أوامر الفصل:
| الأمر | عمله |
|---|---|
| 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 |
11 , 218 ، خانة بداية، سبع خانات معطيات، خانة زوجية وخانتا نهاية.COM3 19200 , True , Falseb'SW=' , b'0' , 0350 , 20 ، ثم الحافتان: 450 320BC1 : القوسان بعد on_press استدعيا العمليّة عند البناء، ولم يبقَ للزرّ معالج.True , False , False , True ، لأن مسافة زائدة ليست رقما، والمستخدم لا يراها.أسئلة للتمرين:
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 , 1baud = 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 bytesch = 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: 0import 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 foundPanel 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()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. ومع مفتاح حقيقي نضيف تأخيرا قصيرا بين القراءتين، لأن تلامس المفتاح يهتزّ لحظة عند الضغط.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.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.238E1 كجملة نصية، وتعيد الحرف، أو نوع الخطأ: الطول، خانتا البداية والنهاية، أو الزوجية. ونفحصها بالإطار الذي بنيناه في البند 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 ERRORSend إلى اللوحة، وتعرض لافتة الأمر والجواب، ويُمسح المربّع. ونفحصها بالأمر 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()