תקשורת טורית ויסודות בתכנות חלונאי
עד עכשיו התוכניות שלנו חיו בתוך המחשב: קראו מהמקלדת וכתבו למסך, ואפילו את השורה שפענחנו בפרק 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 לצורך יצירת תקשורת
COM3. אחר כך הרצנו את אותן תוכניות מול לוח Uno אמיתי, מחובר בכבל USB בלי רכיבים נוספים, והתשובות הגיעו לפי הפרוטוקול, אבל עם הערכים של הלוח. כדי לעשות זאת מתקינים את הספרייה בפקודה pip install pyserial, ומוצאים את שם היציאה אצלנו.שם החבילה שמתקינים הוא pyserial, אבל שם המודול בקוד הוא import serial, ובו המחלקה serial.Serial באות גדולה. תכנית הלימודים אומרת "המחלקה serial", ולזו היא מתכוונת. לפני שפותחים את התקשורת שואלים אילו יציאות קיימות. אפשר למצוא אותן גם ב"מנהל ההתקנים" של Windows, או בסביבת הארדואינו בתפריט 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" ב-Windows |
| 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
מחרוזות ובתים:
התוכנית עובדת עם תווים, אבל הקו מעביר בתים. לכן נזדקק לטיפוס חדש לפני שנכתוב משהו.
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 מחזירה את הגדרות היציאה, וכדאי לבדוק אותן לפני שמחפשים שגיאה במקום אחר. התוצאה היא מילון (Dictionary), מבנה נתונים שנרחיב עליו בפרק 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 מגיעה עם פייתון ב-Windows, ואין מה להתקין. מייבאים אותה בצורה 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()