יסודות התכנות בשפת Python · פרק 10 — תקשורת טורית ויסודות בתכנות חלונאי⌂📚🏠
פרק 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 לצורך יצירת תקשורת

הערה: כל הפלטים בסעיפי התקשורת נכתבו בידי תוכנית שהרצנו בפועל, והצד השני של הקו בהרצה הזו היה הדמיה של ארדואינו, שמכירה רק את היציאה 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

למחלקה יש פרמטרים נוספים, שמתארים את אותה מסגרת מסעיף 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, כלומר בלי הגבלה
דוגמה 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

מחרוזות ובתים:

התוכנית עובדת עם תווים, אבל הקו מעביר בתים. לכן נזדקק לטיפוס חדש לפני שנכתוב משהו.

רצף בתים: הטיפוס 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
שאלה 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 מגיעה עם פייתון ב-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_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
אחרי כתיבת הפקודה ולחיצה על הלחצן. התשובה מההדמיה