ในบทความนี้ใช้ความรู้จากบทความการอ่านรายชื่อพอร์ตอนุกรมที่ถูกเชื่อมต่อมาปรับปรุงให้เป็นการส่งข้อมูลส่งไปให้บอร์ด Arduino Uno ที่มีหลอดแอลอีดี (LED) เชื่อมต่ออยู่ที่ขา 2,3,4,5,6,7,8 และ 9 ทำให้ผู้ใช้งานสามารถสั่งเปิดหรือปิดหลอดดังกล่าวด้วยการสั่งงานผ่าน GUI (Graphics User Interface) ของ PyQt5 ดังภาพที่ 1 และส่งข้อมูลไปให้บอร์ด Uno ทางพอร์ตสื่อสารอนุกรมด้วย pySerial โดยในตัวอย่างครั้งนี้ การทำงานรองรับทั้งระบบปฏิบัติการ Windows, macOS และ Linux ซึ่งจะทำให้เห็นว่า PyQt5 และ pySerial สามารถรองรการทำงานกับทั้ง 3 ระบบได้
บทความนี้ประกอบด้วย 3 ตอน คือ
- List the serial ports connected to the RPi with pySerial.
- List the serial ports connected to the RPi with pySerial and PyQt5.
- LEDs on/off via PyQt5 and serial communication.
ปรับปรุงโค้ด
จากโค้ดในครั้งก่อนได้อ่านรายการพอร์ตทั้งหมดของระบบปฏิบัติการแบบ Linux โดยพิจารณาชื่ออุปกรณ์ที่ขึ้นต้นด้วย /dev/tty ในครั้งนี้ได้ปรับเปลี่ยนให้รองรับกับ 3 ระบบปฏิบัติการ โดยอ่านรายชื่อระบบปฏิบัติการจากคุณสมบัติ platform ของไลบรารี sys
- ถ้าขึ้นต้นด้วย ‘win’ ให้ใช้ค่าพอร์ตเป็น ‘COM1’, ‘COM2’, .., ‘COM127’
- ถ้าขึ้นต้นด้วย ‘linux’ ให้ใช้ค่าพอร์ตเป็นรายการที่ขึ้นต้นด้วย /dev/tty
- ถ้าคำขึ้นต้นเป็น ‘darwin’ ซึ่งหมายถึงระบบปฏบัติการ macOS ให้ใช้ค่าพอร์ตขึ้นต้นด้วย /dev/cu.
- กรณีอื่น ๆ ให้รายงานว่าไม่รู้จักระบบปฏิบัติการ
โค้ดโปรแกรมตัวอย่างเป็นดังนี้
if sys.platform.startswith('win'):
ports = ['COM%s' % (i + 1) for i in range(128)]
elif sys.platform.startswith('linux'):
ports = glob.glob('/dev/tty[AU]*')
elif sys.platform.startswith('darwin'):
ports = glob.glob('/dev/cu.*')
else:
raise EnvironmentError('ไม่รองรับการทำงานของระบบปฏิบัติการนี้!!!')
glob
คลาส glob ใช้สำหรับค้นหาชื่อไฟล์บทระบบปฏิบัติการยูนิกซ์ (Unix) ตามรูปแบบที่กำหนด โดยการใช้งานให้นำเข้าไลบรารี glob และเรียกใช้ผ่านทางวัตถุ glob
ตัวอย่างการค้นหาไฟล์ที่ชื่อ /dev/abc และ /dev/ttyUSB0 สามารถเขียนโค้ดได้ดังนี้ ซึ่งได้ตัวอย่างผลลัพธ์ดังภาพที่ 2
import glob
result = glob.glob("/dev/abc")
print(result)
result = glob.glob("/dev/ttyUSB")
print(result)
นอกจากระบุชื่อไฟล์ตรง ๆ แล้ว ผู้เขียนโปรแกรมสามารถใส่ wildcard ได้แก่ * และ ? เข้าไปได้ ซึ่ง
- เครื่องหมาย * แทนตัวอักษรหรือตัวเลขใด ๆ จำนวนเท่าใดก็ได้
- เครื่องหมาย ? แทนตัวอักษรหรือตัวเลขใด ๆ จำนวน 1 ตัว
ตัวอย่างในภาพที่ 3 เป็นการสั่งค้นหา /dev/tty* กับ /dev/tty? ซึ่งในแบบแรกจะรายงานทุกไฟล์ที่ขึ้นต้นด้วย /dev/tty และในแบบที่ 2 จะค้นหาเฉพาะ /dev/tty ที่มีตัวเลขหรือตัวอักษรตามมา 1 หลัก
และแบบสุดท้ายคือระบุคำค้นเป็น /dev/tty[A]* และ /dev/tty[AU]* จะได้ผลลัพธ์ดังตัวอย่างในภาพที่ 4 ซึ่งคำสั่งแรกหมายถึงค้นหา /dev/tty ที่มี A ตามมา และที่เหลือเป็นอะไรก็ได้ ส่วนแบบที่ 2 เป็นการหา /dev/tty ที่ตามมาด้วย A หรือ U และที่เหลือเป็นอะไรก็ได้ และนอกจากนี้สามารถกำหนดเป็นช่วงได้ เช่น /dev/tty[A-U]* สำหรับหาที่ขึ้นต้นด้วย /dev/tty ตามด้วยตัวอักษร A ถึง U พิมพ์ใหญ่ และตามด้วยอะไรก็ได้ และ /dev/tty[A-Ua-u]* สำหรับหาที่ขึ้นต้นด้วย /dev/tty ตามด้วย A ถึง u หรือ a ถึง u และตามด้วยตัวอักษรหรือตัวเลขใดก็ได้
PyQt5
วิดเจ็ตที่นำมาใช้เพิ่มเติมจากบทความก่อนหน้านี้ที่เกี่ยวกับ PyQt5 ที่ได้เคยกล่าวถึงไปแล้ว คือ คล่า QCheckBox ซึ่งเป็นส่วนติดต่อกับผู้ใช้สำหรับให้ผู้เลือกหรือไม่เลือกรายการนั้น และรองรับการเลือกพร้อมกันหลายรายการ
QCheckBox
วิดเจ็ต QCheckBox ใช้สำหรับสร้างวิดเจ็ตนำเข้าข้อมูลจากผู้ใช้โดยให้ผู้ใช้เลือกรายการที่สร้างขึ้น โดยการสร้างวัตถุประเภท เช็คบ็อกซ์จะต้องนำเข้าคลาส QCheckBox ที่สืบทอดมาจาก QtWidgets.QAbstractButton และสืบทอดมาจาก QtWidgets.QtWidget อีกทอดหนึ่ง ซึ่งการสร้างวัตถุประเภทนี้มีรูปแบบการสร้างดังนี้ ส่วนพารามิเตอร์ที่ต้องส่งให้นั้นประกอบด้วย ข้อความที่แสดงด้านหลังปุ่มรายการเลือก และค่าตำแหน่งของหน้าต่างหลักที่ต้องการให้เลเบลนี้ไปแสดงบนหน้าต่าง โดยส่วนของข้อความสามารถกำหนดเป็นค่าใด ๆ และสามารถเปลี่ยนได้ในภายหลัง ส่วนหน้าต่างหลักในตัวอย่างโปรแกรมจะใช้เป็น self เพื่อระบุว่า เลเบลอยู่ภายใต้หน้าต่างที่ถูกสร้างขึ้น
วัตถุ = QCheckBox( ข้อความ, หน้าต่างหลัก )
การกำหนดข้อความให้กับวัตถุประเภท QCheckBox ใช้เมธอดตามรูปแบบต่อไปนี้
วัตถุ.setText( ข้อความ )
กรณีที่ต้องการให้ปรับขนาดของ QCheckBox ใหม่ให้เหมาะกับข้อความที่กำหนดให้เรียกใช้ฟังก์ชันต่อไปนี้
วัตถุ.adjustSize()
สำหรับการระบุตำแหน่งแสดงวิดเจ็ตกำหนดได้คำสั่ง move() ดังรูปแบบของการใช้งานต่อไปนี้
วัตถุ.move( x, y)
การตรวจสอบว่ารายการถูกเลือกหรือไม่ใช้คำสั่งดังนี้
สถานะการเลือก = วัตถุ.isChecked()
สำหรับการกำหนดให้รายการนั้นถูกเลือกทำได้โดยการส่งค่า True หรือ False เป็นพารามิเตอร์ของคำสั่ง setCheckState() ดังนี้
วัตถุ.setCheckState( สถานะ )
ตัวอย่างโปรแกรม
โปรแกรมประกอบด้วย 2 ส่วน คือ ส่วนของ Arduino Uno และส่วนของ GUI
โปรแกรมสำหรับ Arduino Uno
บอร์ด Arduino Uno ได้ต่อหลอดแอลอีดีไว้ที่ขา 2,3,4,5,6,7,8 และ 9 ดังภาพที่ 5 ซึ่งหลักการทำงานของโปรแกรมเป็นดังต่อไปนี้
- เปิดการสื่อสารพอร์ตอนุกรมด้วยความเร็ว 115200bps หรือที่ตรงกันกับโปรแกรมฝั่ง GUI
- ปรับให้ทุกขาทำให้หลอดดับ
- ส่วนของ loop
- ถ้ามีข้อมูลนำเข้าทางพอร์ตสื่อสารอนุกรมของ Arduino Uno
- อ่านข้อความมาเก็บใน s
- แปลง s เป็นตัวเลข x
- ตรวจสอบแต่ละบิตของ x ถ้าเป็น 1 ให้ทำดังนี้
- เปิดหลอดแอลอีดีของขา 2
- ถ้าไม่ใช่ให้ปิด
- ถ้ามีข้อมูลนำเข้าทางพอร์ตสื่อสารอนุกรมของ Arduino Uno
โค้ดโปรแกรมคือ
#include <Arduino.h>
uint8_t leds[] = {2, 3, 4, 5, 6, 7, 8, 9};
void setup() {
Serial.begin( 115200 );
for (int i = 0; i < 8; i++) {
pinMode( leds[i], OUTPUT );
digitalWrite( leds[i], LOW );
}
}
void loop() {
if (Serial.available()) {
// มีข้อมูล
String s = Serial.readString();
// Serial.print(s);
uint8_t x = s.toInt();
if (x & 0b00000001) {
digitalWrite( leds[0], HIGH );
} else {
digitalWrite( leds[0], LOW );
}
if (x & 0b00000010) {
digitalWrite( leds[1], HIGH );
} else {
digitalWrite( leds[1], LOW );
}
if (x & 0b00000100) {
digitalWrite( leds[2], HIGH );
} else {
digitalWrite( leds[2], LOW );
}
if (x & 0b00001000) {
digitalWrite( leds[3], HIGH );
} else {
digitalWrite( leds[3], LOW );
}
if (x & 0b00010000) {
digitalWrite( leds[4], HIGH );
} else {
digitalWrite( leds[4], LOW );
}
if (x & 0b00100000) {
digitalWrite( leds[5], HIGH );
} else {
digitalWrite( leds[5], LOW );
}
if (x & 0b01000000) {
digitalWrite( leds[6], HIGH );
} else {
digitalWrite( leds[6], LOW );
}
if (x & 0b10000000) {
digitalWrite( leds[7], HIGH );
} else {
digitalWrite( leds[7], LOW );
}
}
}
โปรแกรมของฝั่ง GUI
โปรแกรมของฝั่ง GUI ยังคงใช้ PyQt5 เป็นไลบรารีสำหรับทำงาน โดยเพิ่มส่วนของการรองรับระบบปฏิบัติการในการตรวจสอบชื่อของอุปกรณ์ที่เชื่อมต่อสื่อสารกับพอร์ตอนุกรมดังที่ได้กล่าวไว้ก่อนหน้านี้แล้ว และการทำงานหลักของโปรแกรมเป็นดังนี้
- สร้างหน้าต่าง และวิดเจ็ต
- ตรวจสอบรายชื่อพอร์ตสื่อสารอนุกรมที่เชื่อมต่อ
- ถ้าผู้ใช้กดปุ่ม ‘ส่งข้อมูล’
- ตรวจสอบว่ามีพอร์ตที่เชื่อมต่อ
- แปลงข้อมูลจากรายการ check box ให้เป็นตัวเลข
- แปลงตัวเลขเป็นข้อความ
- แปลงข้อความเป็น bytearray
- เปิดพอร์ตสื่อสาร
- ส่ง bytearray
- ปิดพอร์ตสื่อสาร
โค้ดโปรแกรมเป็นดังต่อไปนี้ โดยมีหน้าจอแสดงผลดังภาพที่ 1 และสิ่งที่ต้องตะหนักอยู่เสมอ คือ การรับ/ส่งข้อมูลนั้น ตัวข้อมูลอยู่ในรูปแบบ bytearray ดังนั้น ต้องทำการแปลงข้อมูลไปกลับด้วย ตัวแปรสตริง.decode() และ ตัวแปรสตริง.encode()
# -*- coding: UTF-8 -*-
import sys
import glob
import serial
from PyQt5.QtWidgets import QApplication, QCheckBox, QLabel, QComboBox, QWidget, QMainWindow, QPushButton
from PyQt5.QtGui import QIcon, QColor
class MyApp(QMainWindow):
def __init__(self, title, w=640, h=480):
super().__init__()
self.setWindowTitle(title)
self.setGeometry(0, 0, w, h)
self.status = False
self.lbl = QLabel("รายการพอร์ตอนุกรม", self)
self.lbl.move( 100, 20 )
self.lbl.adjustSize()
self.lbl2 = QLabel("สถานะ", self)
self.lbl2.move( 100, 70)
self.lbl2.adjustSize()
self.cbb = QComboBox(self)
self.cbb.move( 220, 20 )
## serial port
if sys.platform.startswith('win'):
ports = ['COM%s' % (i + 1) for i in range(128)]
elif sys.platform.startswith('linux'):
ports = glob.glob('/dev/tty[A-Za-z]*')
elif sys.platform.startswith('darwin'):
ports = glob.glob('/dev/cu.*')
else:
raise EnvironmentError('ไม่รองรับการทำงานของระบบปฏิบัติการนี้!!!')
for port in ports:
try:
s = serial.Serial(port)
s.close()
self.cbb.addItem(port)
except (OSError, serial.SerialException):
pass
self.cbb.adjustSize()
### Check box
self.chkLeds = []
for i in range(8):
self.chkLeds.append(QCheckBox("LED{}".format(i),self))
self.chkLeds[i].move(100+(i*64),110)
### send button
self.btnSend = QPushButton("ส่งข้อมูล", self)
self.btnSend.setToolTip("ส่งข้อมูลไปที่พอร์ตอนุกรม")
self.btnSend.move( 200, 68 )
self.btnSend.clicked[bool].connect(self.on_clicked_btnSend)
## status bar
if self.cbb.count() > 0:
self.statusBar().showMessage('status: พร้อมทำงาน')
else:
self.statusBar().showMessage('status: ไม่พบพอร์ตอนุกรม')
self.show()
def on_clicked_btnSend(self):
source = self.sender()
#print(self.cbb.count(), self.cbb.currentText())
if (self.cbb.count() == 0):
return
### แปลงข้อมูล
data = 0
if (self.chkLeds[0].isChecked()):
data += 1
if (self.chkLeds[1].isChecked()):
data += 2
if (self.chkLeds[2].isChecked()):
data += 4
if (self.chkLeds[3].isChecked()):
data += 8
if (self.chkLeds[4].isChecked()):
data += 16
if (self.chkLeds[5].isChecked()):
data += 32
if (self.chkLeds[6].isChecked()):
data += 64
if (self.chkLeds[7].isChecked()):
data += 128
data = "{}\n".format(data)
encoded_string = data.encode()
byte_array = bytearray(encoded_string)
print(byte_array)
port = self.cbb.currentText()
try:
scon = serial.Serial(port=port,baudrate=115200)
scon.write(byte_array)
scon.flushOutput()
scon.close()
except:
self.statusBar().showMessage("{} เปิดใช้ไม่ได้!!!".format(port))
if __name__ == '__main__':
app = QApplication(sys.argv)
my_app = MyApp("PyQt5 serial 3",640, 320)
sys.exit(app.exec_())
ตัวอย่างภาพที่ 6 กำหนดให้ LED0,LED2,LED4 และ LED6 ติด ได้ผลลัพธ์ดังภาพที่ 7 และจากภาพที่ 8 กำหนดให้ LED4, LED5,LED6 และ LED7 ดังภาพที่ 9
สรุป
จากบทความนี้จะพบว่า เมื่อเราใช้ PyQt5 และ pySerial ทำให้เราสามารถเขียนโปรแกรมสื่อสารกับอุปกรณ์ที่เป็นบอร์ดไมโครคอนโทรลเลอร์ผ่านทางพอร์ตสื่อสารอนุกรมได้ และสามารถนำไปประยุกต์ได้มากมายขึ้น แต่ผู้เขียนโปรแกรมจะต้องออกแบบรูปแบบของการสื่อสารหรือโพรโทคอล (protocol) ให้เหมาะสมกับระบบที่พัฒนาขึ้น สุดท้ายนี้ หวังว่าบทความนี้คงมีประโยชน์บ้างไม่มากก็น้อย และขอให้สนุกกับการเขียนโปรแกรมครับ
แหล่งอ้างอิง
(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-10-28