לאוטומציה, בית חכם תוצרת בית, איסוף נתונים, משחקים ועוד: כל מה שצריך לדעת כדי ליצור תוכנה בסיסית לתקשורת סריאלית בין המחשב ללוחות ארדואינו ובכלל, בשפת התכנות Python.
ביולי 2012, פחות משנה אחרי שהתחלתי לשחק עם ארדואינו, כתבתי פוסט על יצירת תקשורת סריאלית מצד המחשב באמצעות שפת התיכנות Object Pascal ואחת מספריות הקוד שנכתבו בה. זה עבד נהדר, וכשנתיים לאחר מכן ביססתי על אותה שיטה את ה-Serial Monitor Deluxe, תוכנת תקשורת שהייתה מצד אחד משוכללת יותר מהמוניטור הבסיסי של ארדואינו, ומצד שני מותאמת יותר לצורכי המייקר המודרני מאשר תוכנות טרמינל גנריות. היא עבדה מצוין לצרכים שלי באותה תקופה, אך עם הזמן עלה צורך בתוכנה עוד יותר משוכללת, שתיכתב בשפה קצת יותר פופולרית ומוכרת.
ייקח עוד זמן עד שהתוכנה המלאה שלי תהיה מוכנה, אך בינתיים – בשבילכם ובשביל התיעוד – הנה הצעדים הבסיסיים שצריך לעשות בשפת פייתון כדי להגדיר ולבצע תקשורת סריאלית (UART) על גבי USB בין המחשב לארדואינו ובכלל.
הכנות והתקנות (חד-פעמיות)
לפני הכול, צריך לוודא שיש פייתון על המחשב. הנה הקישור להורדה הרשמית.
השלב הבא הוא להתקין ספרייה בשם pySerial, שהיא לא חלק מההורדה הרשמית הנ"ל. אפשר למצוא מידע והוראות כאן, חשוב רק לזכור שזה לא קובץ התקנה רגיל אלא פקודה שצריך לכתוב בחלון שורת פקודה (כמו CMD ב-Windows):
pip install pyserial
אם זה יפעל כמו שצריך, ולא תהיה בעיית רשת, תראו משהו כזה:
מעכשיו, בתחילת כל תוכנית פייתון שרוצה "לדבר" עם פורט סריאלי, צריך לכלול את הספרייה הזו (שימו לב שכאן כותבים רק "serial", לא "pySerial"!)
import serial
רגע, איזה פורט?
יש לנו כעת את התשתית לכתוב ולקרוא מפורט סריאלי (מה שב-Windows נקרא "COM PORT"), אבל לאיזה פורט סריאלי? פורטים מופיעים או נעלמים בכל פעם שאנחנו מחברים או מנתקים מהמחשב התקן עם תקשורת סריאלית, ואנחנו לא יכולים לדעת בוודאות מראש איזה מספר פורט יקבל ההתקן שמעניין אותנו. כל תוכנת תקשורת ראויה לשמה תצטרך לזהות, לפחות ברגע ההפעלה, את הפורטים הקיימים ולתת למשתמש אפשרות לבחור מתוכם.
לשם כך – ואין לי מושג למה אבל ככה זה – צריך לכלול עוד ספרייה אחת בקוד:
import serial.tools.list_ports
לאחר מכן, פקודה כמו:
ports = serial.tools.list_ports.comports()
תחזיר לנו במשתנה ports רשימת אובייקטים שמייצגים את כל הפורטים הפעילים כרגע במערכת (זה אמור לעבוד בכל מערכות ההפעלה הנפוצות). בהמשך נראה איך משלבים את זה עם התחברות לפורט הרצוי, בינתיים תזכרו את שתי פקודות ה-import החיוניות האלה.
התחברות, הגדרות והתנתקות
בספרייה serial מוגדרת מחלקה בשם Serial (מקורי, אה?) ואנחנו יכולים ליצור מופעים (instance) של המחלקה הזו ולהתחבר דרכם לפורטים. לדוגמה, אם ידוע מראש שהפורט הרלוונטי נקרא COM21 וקצב השידור הרצוי הוא 115200 באוד, אפשר ליצור מופע בשם my_port בעזרת הפקודה הראשונה בקטע הקוד הבא. הפקודה השנייה תציג את שם הפורט כפי שמערכת ההפעלה מזהה אותו – וזה יעבוד רק אם החיבור הצליח, אחרת תקפוץ חריגה (exception).
my_port = serial.Serial("COM21", baudrate=115200)
print(my_port.name)
כבר בשלב יצירת המופע אפשר לשלוח פרמטרים להגדרת התכונות של הפורט, אך בעבודה עם UART מודרני בכלל וארדואינו בפרט, הגדרות ברירת המחדל של התכונות האלה כמעט תמיד מספיקות. למעשה, ברירת המחדל של baudrate היא הערך הנפוץ 9600, כך שאפילו עליו נוכל לוותר לפעמים. בכל מקרה, אם רוצים לשנות פרמטר בהמשך, תוך כדי ריצה, זה אפשרי:
my_port.baudrate = 19200
פרמטר אחד נוסף שעשוי לעניין אותנו ביומיום נקרא timeout, והוא אומר לאובייקט הסריאל כמה זמן לחכות לנתונים נוספים במהלך ביצוע של פקודת קריאה. ערך ברירת המחדל שלו, None, הוא מסוכן למדי – אם הנתונים הדרושים מתעכבים, התוכנה פשוט "תיתקע" בהמתנה להם. אפשר לבחור בערך 0, שאז אין המתנה כלל – גם כן מסוכן, בהתאם למגבלות והיכולות של הצד המשדר – או בערך ביניים (ביחידות של שניות, עם נקודה עשרונית). מבחינה פונקציונלית זה דומה להגדרת Serial.setTimeout בארדואינו, למי שמכיר.
אם רוצים לשחרר או להתחבר מחדש לפורט תוך כדי ריצה (למשל כדי שנוכל להתחבר אליו זמנית מהסריאל מוניטור של ארדואינו או תוכנה אחרת) משתמשים במתודות close ו-open:
my_port.close()
my_port.open()
קריאה וכתיבה
כדי לבדוק כמה בייטים הגיעו (אם בכלל) מההתקן החיצוני למחשב ועדיין מחכים שנקרא אותם, ניעזר במשתנה in_waiting של הפורט. שימו לב, זהו משתנה פשוט ולא פונקציה:
bytes_pending_read = my_port.in_waiting
את הקריאה עצמה נבצע באמצעות המתודה read. אפשר לשלוח לה כפרמטר את מספר הבייטים המבוקש (ואז timeout שהזכרתי למעלה עשוי להיכנס לתמונה), ואם לא שולחים כלום, היא מנסה לקרוא רק בייט אחד. בכל מקרה, הערך החוזר מהפונקציה הזו הוא אובייקט מהמחלקה bytes, לא מספר או רשימה רגילים, וצריך להתייחס אליו בהתאם.
# Careful, return type is bytes!
one_byte_received = my_port.read()
עד כמה שהצלחתי לראות, אין ב-pySerial דרך לקרוא טיפוסים ספציפיים יותר, כגון מספרים או מחרוזות. את אלה תצטרכו לבודד ולהמיר לבד מתוך ה-bytes שהגיעו.
אם יודעים כמה בייטים צפויים להגיע, או יודעים שרצף הבייטים אמור להיגמר בתת-רצף ספציפי (כגון הערך 0), אפשר להיעזר במתודה read_until, שמקבלת שני פרמטרים אופציונליים: terminator (שגם הוא bytes, עם ברירת מחדל LF – כלומר תו ASCII שמספרו 10), ו-size שקובע את מספר הבייטים המקסימלי שייקרא אם ה-terminator לא יופיע. גם המתודה הזו מוגבלת על ידי timeout. אגב, אם הקלט יסתיים ב-terminator שהגדרנו, הערך החוזר מהמתודה יכלול גם את ה-terminator עצמו.
# Read a null-terminated string
incoming_bytes = my_port.read_until(terminator=b'0')
כדי לכתוב לפורט, נשתמש במתודה write, שמקבלת כפרמטר bytes לשליחה ומחזירה את מספר הבייטים שנשלח בפועל. כל דבר שנרצה לשלוח שאינו bytes יצטרך כמובן לעבור המרה לפני כן. הנה מספר דוגמאות.
# Write strings
my_port.write(b"Hello string constant world!\n")
my_str = "Hello string variable world!\n"
my_port.write(my_str.encode())
# Write raw bytes or character
n = 65
ch = "B"
my_port.write(bytes([n, ord(ch)]))
# Write a number as plain text
x = 12345.6789
my_port.write(str(x).encode())
תוכנית לדוגמה
לסיכום, ועבור מי שהתעצלו לקרוא את הפרטים עד כה, הכנתי תוכנת פייתון שלמה ומאוד בסיסית, שמאפשרת לבחור פורט, מתחברת אליו ב-115200 באוד, ובלולאה אינסופית מאזינה לפורט ומחזירה כל בייט שהיא מקבלת כשהוא מוגדל ב-1 (למשל, תשלחו את התו "A" ותקבלו "B"). למען הקיצור אין בתוכנית שום תחכום, שום בדיקות לתקינות הקלט או בעיות פוטנציאליות אחרות, והיא גם לא סוגרת את הפורט בצורה מסודרת. הנה קוד התוכנה ב-pastebin.
כדי שתוכלו לבדוק את התוכנה הזו בקלות, הנה קוד משלים לארדואינו (אונו). אחרי צריבת הקוד, סגרו את סביבת הפיתוח של ארדואינו (כדי "לשחרר" את הפורט), והתחברו אליו באמצעות התוכנית בפייתון. אם הכול תקין, לוח הארדואינו יתחיל להבהב לאט כמו בקוד בלינק פשוט. אחרת, הוא יהבהב מהר.
איך כותבים לתצוגה של LCD
איזו תצוגה?
מבקש עזרה כתיבה לתצוגה הודעה פשוטה
כרגע צריך שיעבוד רק בתוכנה
LCD כמו בדוגמא שמצורפת תודה לעוזרים
מבקש גם את התוכנית וגם איך לחבר תודה
ספרייה נוספת (ומובנית בפייתון) שאפשר להשתמש בה, היא struct. ספרייה זו יכולה להפוך מספרים (או אפילו מבנים שלמים, כמו שמוגדרים בC) לאובייקטי bytes.
לדוגמא, כדי להפוך מספר 8 ביט עושים:
struct.pack("b", <number>)
מספר 16 ביט (ויותר גדולים) צריך לזכור את הendianess, סדר הבתים שממירים. ברירת המחדל היא little endia, כלומר נשלחים ה-least significat bytes קודם
מגניב, לא הכרתי את זה. תודה!
serial.tools.list_ports היא לא ספריה שונה. כאשר עושים בפייתון import a הוא מייבא רק את הפונקציות והמחלקות שנמצאות ישירות מתחת ל-a בהיררכיה של הספריה. לעומת זאת, פונקציות שנמצאות תחת ההיררכיה a.b לא מיובאות. לכן, מה שאתה בעצם עושה ב-serial.tools.list_ports זה לייבא את התקייה serial/tools/list_ports מהחבילה. אגב, בשביל הנוחות אתה יכול ליבא במפורש את הפונקציה שאתה משתמש בה: from serial.tools.list_ports import comports ואז תוכל לקרוא ישירות ל-comports (פשוט ports = comports()). אופציה נוספת היא ע"י נתינת שם משלך ל-serial.tools.list_ports בצורה הבאה: import serial.tools.list_ports as list_port ואז קריאה לפונקציה בצורה הבאה: ports = list_port.comports() כאשר השם שאתה נותן לא חייב להיות השם של… לקרוא עוד »
אוקיי, תודה. זה עדיין מטופש שלא כל ההיררכיה עוברת וצריך לכתוב import נפרד 🙂
יש לזה כמה סיבות טובות.
למשל קריאות – ברגע שאתה מציין במפורש מה אתה מיבא מאוד ברור מאיפה כל פונקציה שאתה משתמש בה בקוד מגיעה ובתחילת הקובץ אני יודע מראש בדיוק מה אתה מייבא.
סיבה שנייה היא יעילות – כל מה שאתה צריך מ-serial.tools.list_ports זאת פונקציה אחת ויש עוד מחלקה אחת מ-serial שאתה משתמש בה. חבל שהאינטרפרטר ייבא את כל הספרייה רק בשביל זה. בדרך שבה פייתון עובד הוא יכול לייבא את מינימום הדברים שצריך ולחסוך במשאבים.
לא יודע, לי נראה די ברור ש-serial.tools אמור להיות שייך ל-serial, ולא רק מבחינת השם. הגיוני שאם אני מתעסק עם פורטים, ארצה לדעת אילו פורטים זמינים במערכת שלי, ולהיפך… אז אין שום טעם בהפרדה. ונניח שזה חוסך עבודה לאינטרפרטר (מה שאני בהחלט מקבל ברמה העקרונית, וסיבה טובה לא לאהוב אינטרפרטרים 🙂 ), אז למה לא לייבא כל פונקציה וכל מחלקה בנפרד? תכל'ס ברור לי שיש איפשהו סיבה שכתבו את הספריות האלה בצורה הזאת ולא אחרת, אני פשוט אומר שהסיבה הזאת כנראה לא הייתה קוהרנטיות של השפה 😉