PyQT5 Serial Monitor

🔧 How to Use

  1. Install dependencies:
bashCopyEditpip install pyqt5 pyserial
  1. Save this script as serial_monitor_gui.py and run it.

🐍 Full Python Script

pythonCopyEditimport sys
import serial
import serial.tools.list_ports
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QTextEdit, QLineEdit, QPushButton,
    QHBoxLayout, QLabel, QComboBox, QCheckBox
)
from PyQt5.QtCore import QThread, pyqtSignal, QTimer
from datetime import datetime

class SerialReader(QThread):
    data_received = pyqtSignal(str)

    def __init__(self, serial_port):
        super().__init__()
        self.ser = serial_port
        self.running = True

    def run(self):
        while self.running and self.ser.is_open:
            try:
                if self.ser.in_waiting:
                    line = self.ser.readline().decode('utf-8', errors='ignore').strip()
                    self.data_received.emit(line)
            except Exception as e:
                self.data_received.emit(f"[Error] {e}")
                break

    def stop(self):
        self.running = False
        if self.ser and self.ser.is_open:
            self.ser.close()

class SerialMonitor(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt5 Serial Monitor")
        self.resize(700, 500)

        self.ser = None
        self.serial_thread = None
        self.log_file = open("serial_log.txt", "a")

        self.init_ui()
        self.auto_reconnect_timer = QTimer()
        self.auto_reconnect_timer.timeout.connect(self.try_reconnect)
        self.auto_reconnect_timer.start(5000)

    def init_ui(self):
        layout = QVBoxLayout()

        # Port and baud rate
        port_layout = QHBoxLayout()
        self.port_combo = QComboBox()
        self.refresh_ports()

        self.baud_combo = QComboBox()
        self.baud_combo.addItems(["9600", "19200", "38400", "57600", "115200"])
        self.baud_combo.setCurrentText("9600")

        self.connect_btn = QPushButton("Connect")
        self.connect_btn.clicked.connect(self.toggle_connection)

        self.auto_scroll_cb = QCheckBox("Auto-scroll")
        self.auto_scroll_cb.setChecked(True)

        port_layout.addWidget(QLabel("Port:"))
        port_layout.addWidget(self.port_combo)
        port_layout.addWidget(QLabel("Baud:"))
        port_layout.addWidget(self.baud_combo)
        port_layout.addWidget(self.connect_btn)
        port_layout.addWidget(self.auto_scroll_cb)
        layout.addLayout(port_layout)

        # Serial output area
        self.output_area = QTextEdit()
        self.output_area.setReadOnly(True)
        layout.addWidget(self.output_area)

        # Input field
        self.input_field = QLineEdit()
        self.input_field.setPlaceholderText("Enter command and press Enter")
        self.input_field.returnPressed.connect(self.send_data)
        layout.addWidget(self.input_field)

        self.setLayout(layout)

    def refresh_ports(self):
        self.port_combo.clear()
        ports = serial.tools.list_ports.comports()
        for port in ports:
            self.port_combo.addItem(port.device)

    def toggle_connection(self):
        if self.ser and self.ser.is_open:
            self.disconnect_serial()
        else:
            self.connect_serial()

    def connect_serial(self):
        port = self.port_combo.currentText()
        baud = int(self.baud_combo.currentText())
        try:
            self.ser = serial.Serial(port, baud, timeout=1)
            self.output_area.append(f"[Connected] to {port} at {baud} baud")

            self.serial_thread = SerialReader(self.ser)
            self.serial_thread.data_received.connect(self.display_data)
            self.serial_thread.start()

            self.connect_btn.setText("Disconnect")
        except serial.SerialException as e:
            self.output_area.append(f"[Error] {e}")

    def disconnect_serial(self):
        if self.serial_thread:
            self.serial_thread.stop()
            self.serial_thread.wait()
        if self.ser and self.ser.is_open:
            self.ser.close()
        self.ser = None
        self.connect_btn.setText("Connect")
        self.output_area.append("[Disconnected]")

    def try_reconnect(self):
        if not self.ser or not self.ser.is_open:
            self.refresh_ports()
            self.connect_serial()

    def send_data(self):
        if self.ser and self.ser.is_open:
            data = self.input_field.text()
            if data:
                self.ser.write((data + "\n").encode('utf-8'))
                self.display_data(f"> {data}")
                self.input_field.clear()

    def display_data(self, line):
        timestamped_line = f"[{datetime.now().strftime('%H:%M:%S')}] {line}"
        self.output_area.append(timestamped_line)
        self.log_file.write(timestamped_line + "\n")
        self.log_file.flush()

        if self.auto_scroll_cb.isChecked():
            self.output_area.moveCursor(self.output_area.textCursor().End)

    def closeEvent(self, event):
        self.disconnect_serial()
        self.log_file.close()
        event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = SerialMonitor()
    window.show()
    sys.exit(app.exec_())