Non-GUI (Command-Line Interface) และ GUI (Graphical User Interface) โดยทั้งสองส่วนนี้ใช้คลาส UARTHandler
เป็นแกนกลางในการจัดการการสื่อสารผ่านพอร์ตอนุกรม (UART) อย่างไรก็ตาม การใช้งานในสองรูปแบบนี้มีความแตกต่างกันในแง่ของการออกแบบและการทำงานของโปรแกรม มาดูรายละเอียดและวิเคราะห์แต่ละประเด็นที่คุณสงสัยกันครับ
1. การใช้งาน self
ในคลาส UARTHandler
บทบาทของ self
-
Attributes :
self.port
,self.baudrate
,self.buffer
,self.data_queue
, และอื่น ๆ เป็น attributes ของคลาสUARTHandler
- Attributes เหล่านี้ถูกกำหนดในคอนสตรักเตอร์ (
__init__
) และสามารถเรียกใช้งานได้ใน methods อื่น ๆ เช่น_read_loop
,send_data
,get_received_line
, และclose
-
Methods :
- การใช้
self.method_name()
เช่นself.send_data(data)
ช่วยให้ methods ภายในคลาสสามารถเข้าถึง attributes และ methods อื่น ๆ ได้ - การใช้
self
ยังช่วยให้ threads (เช่น_read_loop
) และ callbacks สามารถทำงานร่วมกับ attributes ของคลาสได้อย่างปลอดภัย
- การใช้
ตัวอย่างที่สำคัญ :
python
def _read_loop(self):
while not self.stop_thread.is_set():
if self.uart.is_open and self.uart.in_waiting:
data = self.uart.read(self.uart.in_waiting).decode('utf-8', errors='ignore')
self.buffer += data
self.uart
คือ attribute ที่ถูกกำหนดใน__init__
และถูกใช้งานใน_read_loop
self.buffer
เป็นตัวแปรที่เก็บข้อมูลที่อ่านมาจาก UART และสามารถถูกเรียกใช้งานใน methods อื่น ๆ
2. ความแตกต่างระหว่าง GUI และ Non-GUI
Non-GUI (serial_monitor.py
)
-
การทำงาน :
- โปรแกรมทำงานบน command-line interface (CLI) โดยไม่มีหน้าจอกราฟิก
- ใช้
input()
เพื่อรับข้อมูลจากผู้ใช้ และแสดงผลทาง terminal - การรับข้อมูลจาก UART ทำผ่าน thread (
receive_loop
) เพื่อให้สามารถรับข้อมูลแบบ real-time โดยไม่บล็อกการทำงานของ main loop
-
การใช้
UARTHandler
:UARTHandler
ถูกสร้างเป็นออบเจ็กต์เพื่อจัดการการสื่อสาร UART- Thread (
receive_loop
) ใช้ methodget_received_line
จากUARTHandler
เพื่อรับข้อมูลจาก queue (data_queue
) และแสดงผลใน terminal
GUI (serial_monitor_gui.py
)
-
การทำงาน :
- โปรแกรมแสดงผลผ่าน graphical user interface (GUI) โดยใช้ไลบรารี
ttkbootstrap
- มี widget เช่น dropdown menu, text box, และ button เพื่อให้ผู้ใช้สามารถเลือกพอร์ต, baud rate, ส่งข้อมูล, และดูข้อความที่รับ-ส่งได้
- การรับข้อมูลจาก UART ทำผ่าน thread (
receive_loop
) เช่นเดียวกับ Non-GUI แต่ข้อมูลจะถูกแสดงผลใน widget (output_box
) แทนที่จะเป็น terminal
- โปรแกรมแสดงผลผ่าน graphical user interface (GUI) โดยใช้ไลบรารี
-
การใช้
UARTHandler
:UARTHandler
ถูกสร้างเป็นออบเจ็กต์เมื่อผู้ใช้กดปุ่ม "Connect"- Thread (
receive_loop
) ใช้ methodget_received_line
จากUARTHandler
เพื่อรับข้อมูลจาก queue และแสดงผลในoutput_box
3. ประเด็นที่กล่าวถึง
(1) การใช้ with
เพื่อเปิด/ปิดพอร์ตอย่างปลอดภัย
-
Non-GUI :
- ใน
serial_monitor.py
การปิดพอร์ตทำผ่าน methodclose()
ของUARTHandler
- การใช้
with
อาจไม่จำเป็นในกรณีนี้ เพราะโปรแกรมทำงานแบบ CLI และมีการจัดการทรัพยากรผ่านtry-finally
หรือfinally
block
- ใน
-
GUI :
- ใน
serial_monitor_gui.py
การปิดพอร์ตทำผ่าน methoddisconnect()
ซึ่งเรียกclose()
ของUARTHandler
- การใช้
with
อาจเหมาะสำหรับการจัดการทรัพยากรในบางกรณี เช่น เมื่อต้องการเปิด/ปิดพอร์ตเฉพาะในขอบเขตบางส่วนของโปรแกรม
- ใน
(2) การจัดการ Logging
-
Non-GUI :
- Logging ถูกแสดงผลใน terminal และบันทึกลงไฟล์
uart_debug.log
- การใช้
logging.basicConfig
กำหนด format และ handler สำหรับ logging
- Logging ถูกแสดงผลใน terminal และบันทึกลงไฟล์
-
GUI :
- Logging ถูกแสดงผลใน widget (
output_box
) และบันทึกลงไฟล์uart_debug.log
- การแสดงผลใน GUI ทำผ่าน method
append_output
ซึ่งอัปเดตข้อความในoutput_box
- Logging ถูกแสดงผลใน widget (
(3) การใช้ Queue
-
Non-GUI :
data_queue
ถูกใช้เพื่อรับข้อมูลจาก thread (_read_loop
) และส่งข้อมูลกลับไปยัง main loop- Main loop ใช้
get_received_line
เพื่อรับข้อมูลจาก queue และแสดงผลใน terminal
-
GUI :
data_queue
ถูกใช้ในลักษณะเดียวกัน แต่ข้อมูลจะถูกแสดงผลในoutput_box
แทนที่จะเป็น terminal
(4) การรองรับ ASCII และ Hex
-
Non-GUI :
- Method
send_data
ตรวจสอบว่าข้อมูลที่ส่งเป็น ASCII หรือ Hex (0x...
) - หากเป็น Hex จะแปลงข้อมูลด้วย
bytes.fromhex
ก่อนส่งออก
- Method
-
GUI :
- การรองรับ ASCII และ Hex ทำในลักษณะเดียวกัน โดยใช้ method
send_data
ของUARTHandler
- การรองรับ ASCII และ Hex ทำในลักษณะเดียวกัน โดยใช้ method
(5) การใช้งานในโปรเจกต์ใหญ่
- Modular Design :
UARTHandler
ถูกออกแบบให้เป็นโมดูลแยกต่างหาก เพื่อให้สามารถนำกลับมาใช้ใหม่ได้ในโปรเจกต์อื่น- การใช้งานใน Non-GUI และ GUI แสดงให้เห็นถึงความยืดหยุ่นของคลาสนี้
สรุปความแตกต่างระหว่าง GUI และ Non-GUI
Non GUI
# serial_monitor.py
import threading
import time
from uart_handler import UARTHandler
def receive_loop(uart: UARTHandler):
"""
รับข้อมูลจาก UART แบบ Real-time แล้วแสดงออกหน้าจอ
"""
try:
while True:
line = uart.get_received_line(timeout=0.1)
if line:
print(f"\033[92m<<< {line}\033[0m") # สีเขียวสำหรับข้อความขาเข้า
except Exception as e:
print(f"Receive thread error: {e}")
def main():
port = 'COM11' # เปลี่ยนตามพอร์ตของคุณ
baudrate = 115200
buffer_size = 2048
try:
uart = UARTHandler(port, baudrate, buffer_size)
except Exception as e:
print(f"Failed to initialize UART: {e}")
return
print(f"=== Serial Monitor (baud: {baudrate}) ===")
print("Type and press Enter to send. Press Ctrl+C to exit.\n")
# Start receiving thread
receiver = threading.Thread(target=receive_loop, args=(uart,), daemon=True)
receiver.start()
try:
while True:
user_input = input(">>> ") # สีขาวสำหรับข้อความขาออก
if user_input.lower() == "exit":
break
uart.send_data(user_input)
except KeyboardInterrupt:
print("\nExiting...")
finally:
uart.close()
if __name__ == "__main__":
main()
Python GUI
# serial_monitor_gui.py
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
import serial.tools.list_ports
import threading
from uart_handler import UARTHandler
class SerialMonitorApp:
def __init__(self, root):
self.root = root
self.root.title("Python Serial Monitor")
self.uart = None
self.receiver_thread = None
self.running = False
self.port_var = ttk.StringVar()
self.baud_var = ttk.IntVar(value=115200)
self.input_var = ttk.StringVar()
self.setup_widgets()
def setup_widgets(self):
frame_top = ttk.Frame(self.root, padding=10)
frame_top.pack(fill=X)
# Serial port dropdown
ttk.Label(frame_top, text="Port:").pack(side=LEFT, padx=(0, 5))
self.port_combo = ttk.Combobox(frame_top, textvariable=self.port_var, width=15)
self.port_combo.pack(side=LEFT, padx=5)
self.refresh_ports()
# Baud rate
ttk.Label(frame_top, text="Baudrate:").pack(side=LEFT, padx=(10, 5))
self.baud_entry = ttk.Entry(frame_top, textvariable=self.baud_var, width=10)
self.baud_entry.pack(side=LEFT, padx=5)
# Connect button
self.connect_btn = ttk.Button(frame_top, text="Connect", command=self.toggle_connection, bootstyle=SUCCESS)
self.connect_btn.pack(side=LEFT, padx=10)
# Output box
self.output_box = ttk.ScrolledText(self.root, height=20, wrap='word')
self.output_box.pack(fill=BOTH, expand=True, padx=10, pady=(5, 0))
self.output_box.configure(state='disabled')
# Send box
frame_bottom = ttk.Frame(self.root, padding=10)
frame_bottom.pack(fill=X)
self.input_entry = ttk.Entry(frame_bottom, textvariable=self.input_var)
self.input_entry.pack(side=LEFT, fill=X, expand=True, padx=(0, 5))
self.input_entry.bind("<Return>", self.send_data)
self.send_btn = ttk.Button(frame_bottom, text="Send", command=self.send_data)
self.send_btn.pack(side=LEFT)
def refresh_ports(self):
ports = serial.tools.list_ports.comports()
port_list = [port.device for port in ports]
self.port_combo['values'] = port_list
if port_list:
self.port_combo.current(0)
def toggle_connection(self):
if self.uart:
self.disconnect()
else:
self.connect()
def connect(self):
port = self.port_var.get()
baud = self.baud_var.get()
if not port:
self.show_message("No COM port selected!", "error")
return
try:
self.uart = UARTHandler(port, baud)
self.running = True
self.connect_btn.config(text="Disconnect", bootstyle=DANGER)
self.start_receiver()
except Exception as e:
self.show_message(f"Connection failed: {e}", "error")
def disconnect(self):
self.running = False
if self.uart:
self.uart.close()
self.uart = None
self.connect_btn.config(text="Connect", bootstyle=SUCCESS)
self.show_message("Disconnected", "info")
def start_receiver(self):
self.receiver_thread = threading.Thread(target=self.receive_loop, daemon=True)
self.receiver_thread.start()
def receive_loop(self):
while self.running and self.uart:
line = self.uart.get_received_line(timeout=0.1)
if line:
self.append_output(f"[RX] {line}")
def send_data(self, event=None):
msg = self.input_var.get().strip()
if self.uart and msg:
self.uart.send_data(msg)
self.append_output(f"[TX] {msg}")
self.input_var.set("")
def append_output(self, text):
self.output_box.configure(state='normal')
self.output_box.insert('end', text + '\n')
self.output_box.see('end')
self.output_box.configure(state='disabled')
def show_message(self, text, level="info"):
if level == "error":
ttk.messagebox.showerror("Error", text)
else:
ttk.messagebox.showinfo("Info", text)
def main():
app = ttk.Window(themename="cyborg", title="Serial Monitor", size=(600, 500))
SerialMonitorApp(app)
app.mainloop()
if __name__ == "__main__":
main()