Terminal Example

The Terminal Example shows how to create a terminal for a simple serial interface by using Qt Serial Port.

It demonstrates the main features of the QSerialPort class, like configuration, I/O implementation and so forth. Also, the class QSerialPortInfo is invoked to display information about the serial ports available in the system.

Download this example

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys

from PySide6.QtWidgets import QApplication

from mainwindow import MainWindow

"""PySide6 port of the serialport/terminal example from Qt v6.x"""


if __name__ == "__main__":
    a = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(a.exec())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtGui import QPalette
from PySide6.QtWidgets import QPlainTextEdit


UNHANDLED_KEYS = [Qt.Key.Key_Backspace, Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up,
                  Qt.Key.Key_Down]


class Console(QPlainTextEdit):

    get_data = Signal(bytearray)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_localEchoEnabled = False
        self.document().setMaximumBlockCount(100)
        p = self.palette()
        p.setColor(QPalette.Base, Qt.black)
        p.setColor(QPalette.Text, Qt.green)
        self.setPalette(p)

    @Slot(bytearray)
    def put_data(self, data):
        self.insertPlainText(data.decode("utf8"))
        bar = self.verticalScrollBar()
        bar.setValue(bar.maximum())

    def set_local_echo_enabled(self, e):
        self.m_localEchoEnabled = e

    def keyPressEvent(self, e):
        key = e.key()
        if key not in UNHANDLED_KEYS:
            if self.m_localEchoEnabled:
                super().keyPressEvent(e)
            self.get_data.emit(e.text().encode())

    def mousePressEvent(self, e):
        self.setFocus()

    def mouseDoubleClickEvent(self, e):
        pass

    def contextMenuEvent(self, e):
        pass
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import QIODeviceBase, Slot
from PySide6.QtWidgets import QLabel, QMainWindow, QMessageBox
from PySide6.QtSerialPort import QSerialPort

from ui_mainwindow import Ui_MainWindow
from console import Console
from settingsdialog import SettingsDialog


HELP = """The <b>Simple Terminal</b> example demonstrates how to
 use the Qt Serial Port module in modern GUI applications
 using Qt, with a menu bar, toolbars, and a status bar."""


def description(s):
    return (f"Connected to {s.name} : {s.string_baud_rate}, "
            f"{s.string_data_bits}, {s.string_parity}, {s.string_stop_bits}, "
            f"{s.string_flow_control}")


class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_ui = Ui_MainWindow()
        self.m_status = QLabel()
        self.m_console = Console()
        self.m_settings = SettingsDialog(self)
        self.m_serial = QSerialPort(self)
        self.m_ui.setupUi(self)
        self.m_console.setEnabled(False)
        self.setCentralWidget(self.m_console)

        self.m_ui.actionConnect.setEnabled(True)
        self.m_ui.actionDisconnect.setEnabled(False)
        self.m_ui.actionQuit.setEnabled(True)
        self.m_ui.actionConfigure.setEnabled(True)

        self.m_ui.statusBar.addWidget(self.m_status)

        self.m_ui.actionConnect.triggered.connect(self.open_serial_port)
        self.m_ui.actionDisconnect.triggered.connect(self.close_serial_port)
        self.m_ui.actionQuit.triggered.connect(self.close)
        self.m_ui.actionConfigure.triggered.connect(self.m_settings.show)
        self.m_ui.actionClear.triggered.connect(self.m_console.clear)
        self.m_ui.actionAbout.triggered.connect(self.about)
        self.m_ui.actionAboutQt.triggered.connect(qApp.aboutQt)  # noqa: F821

        self.m_serial.errorOccurred.connect(self.handle_error)
        self.m_serial.readyRead.connect(self.read_data)
        self.m_console.get_data.connect(self.write_data)

    @Slot()
    def open_serial_port(self):
        s = self.m_settings.settings()
        self.m_serial.setPortName(s.name)
        self.m_serial.setBaudRate(s.baud_rate)
        self.m_serial.setDataBits(s.data_bits)
        self.m_serial.setParity(s.parity)
        self.m_serial.setStopBits(s.stop_bits)
        self.m_serial.setFlowControl(s.flow_control)
        if self.m_serial.open(QIODeviceBase.ReadWrite):
            self.m_console.setEnabled(True)
            self.m_console.set_local_echo_enabled(s.local_echo_enabled)
            self.m_ui.actionConnect.setEnabled(False)
            self.m_ui.actionDisconnect.setEnabled(True)
            self.m_ui.actionConfigure.setEnabled(False)
            self.show_status_message(description(s))
        else:
            QMessageBox.critical(self, "Error", self.m_serial.errorString())
            self.show_status_message("Open error")

    @Slot()
    def close_serial_port(self):
        if self.m_serial.isOpen():
            self.m_serial.close()
        self.m_console.setEnabled(False)
        self.m_ui.actionConnect.setEnabled(True)
        self.m_ui.actionDisconnect.setEnabled(False)
        self.m_ui.actionConfigure.setEnabled(True)
        self.show_status_message("Disconnected")

    @Slot()
    def about(self):
        QMessageBox.about(self, "About Simple Terminal", HELP)

    @Slot(bytearray)
    def write_data(self, data):
        self.m_serial.write(data)

    @Slot()
    def read_data(self):
        data = self.m_serial.readAll()
        self.m_console.put_data(data.data())

    @Slot(QSerialPort.SerialPortError)
    def handle_error(self, error):
        if error == QSerialPort.ResourceError:
            QMessageBox.critical(self, "Critical Error",
                                 self.m_serial.errorString())
            self.close_serial_port()

    @Slot(str)
    def show_status_message(self, message):
        self.m_status.setText(message)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Simple Terminal</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout"/>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>400</width>
     <height>26</height>
    </rect>
   </property>
   <widget class="QMenu" name="menuCalls">
    <property name="title">
     <string>Calls</string>
    </property>
    <addaction name="actionConnect"/>
    <addaction name="actionDisconnect"/>
    <addaction name="separator"/>
    <addaction name="actionQuit"/>
   </widget>
   <widget class="QMenu" name="menuTools">
    <property name="title">
     <string>Tools</string>
    </property>
    <addaction name="actionConfigure"/>
    <addaction name="actionClear"/>
   </widget>
   <widget class="QMenu" name="menuHelp">
    <property name="title">
     <string>Help</string>
    </property>
    <addaction name="actionAbout"/>
    <addaction name="actionAboutQt"/>
   </widget>
   <addaction name="menuCalls"/>
   <addaction name="menuTools"/>
   <addaction name="menuHelp"/>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
   <addaction name="actionConnect"/>
   <addaction name="actionDisconnect"/>
   <addaction name="actionConfigure"/>
   <addaction name="actionClear"/>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
  <action name="actionAbout">
   <property name="text">
    <string>&amp;About</string>
   </property>
   <property name="toolTip">
    <string>About program</string>
   </property>
   <property name="shortcut">
    <string>Alt+A</string>
   </property>
  </action>
  <action name="actionAboutQt">
   <property name="text">
    <string>About Qt</string>
   </property>
  </action>
  <action name="actionConnect">
   <property name="icon">
    <iconset resource="terminal.qrc">
     <normaloff>:/images/connect.png</normaloff>:/images/connect.png</iconset>
   </property>
   <property name="text">
    <string>C&amp;onnect</string>
   </property>
   <property name="toolTip">
    <string>Connect to serial port</string>
   </property>
   <property name="shortcut">
    <string>Ctrl+O</string>
   </property>
  </action>
  <action name="actionDisconnect">
   <property name="icon">
    <iconset resource="terminal.qrc">
     <normaloff>:/images/disconnect.png</normaloff>:/images/disconnect.png</iconset>
   </property>
   <property name="text">
    <string>&amp;Disconnect</string>
   </property>
   <property name="toolTip">
    <string>Disconnect from serial port</string>
   </property>
   <property name="shortcut">
    <string>Ctrl+D</string>
   </property>
  </action>
  <action name="actionConfigure">
   <property name="icon">
    <iconset resource="terminal.qrc">
     <normaloff>:/images/settings.png</normaloff>:/images/settings.png</iconset>
   </property>
   <property name="text">
    <string>&amp;Configure</string>
   </property>
   <property name="toolTip">
    <string>Configure serial port</string>
   </property>
   <property name="shortcut">
    <string>Alt+C</string>
   </property>
  </action>
  <action name="actionClear">
   <property name="icon">
    <iconset resource="terminal.qrc">
     <normaloff>:/images/clear.png</normaloff>:/images/clear.png</iconset>
   </property>
   <property name="text">
    <string>C&amp;lear</string>
   </property>
   <property name="toolTip">
    <string>Clear data</string>
   </property>
   <property name="shortcut">
    <string>Alt+L</string>
   </property>
  </action>
  <action name="actionQuit">
   <property name="icon">
    <iconset resource="terminal.qrc">
     <normaloff>:/images/application-exit.png</normaloff>:/images/application-exit.png</iconset>
   </property>
   <property name="text">
    <string>&amp;Quit</string>
   </property>
   <property name="shortcut">
    <string>Ctrl+Q</string>
   </property>
  </action>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources>
  <include location="terminal.qrc"/>
 </resources>
 <connections/>
</ui>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys

from PySide6.QtCore import Slot
from PySide6.QtGui import QIntValidator
from PySide6.QtWidgets import QComboBox
from PySide6.QtSerialPort import QSerialPort, QSerialPortInfo

from PySide6.QtWidgets import QDialog

from ui_settingsdialog import Ui_SettingsDialog


BLANK_STRING = "N/A"


CUSTOM_BAUDRATE_INDEX = 4


class Settings():

    def __init__(self):
        self.name = ""
        self.baud_rate = 0
        self.string_baud_rate = ""
        self.data_bits = QSerialPort.Data8
        self.string_data_bits = ""
        self.parity = QSerialPort.NoParity
        self.string_parity = ""
        self.stop_bits = QSerialPort.OneStop
        self.string_stop_bits = ""
        self.flow_control = QSerialPort.SoftwareControl
        self.string_flow_control = ""
        self.local_echo_enabled = False


class SettingsDialog(QDialog):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.m_ui = Ui_SettingsDialog()
        self._custom_port_index = -1
        self.m_ui.setupUi(self)
        self.m_currentSettings = Settings()
        self.m_intValidator = QIntValidator(0, 4000000, self)

        self.m_ui.baudRateBox.setInsertPolicy(QComboBox.NoInsert)

        self.m_ui.applyButton.clicked.connect(self.apply)
        self.m_ui.serialPortInfoListBox.currentIndexChanged.connect(self.show_port_info)
        self.m_ui.baudRateBox.currentIndexChanged.connect(self.check_custom_baud_rate_policy)
        self.m_ui.serialPortInfoListBox.currentIndexChanged.connect(
            self.check_custom_device_path_policy)

        self.fill_ports_parameters()
        self.fill_ports_info()

        self.update_settings()

    def settings(self):
        return self.m_currentSettings

    @Slot(int)
    def show_port_info(self, idx):
        if idx == -1:
            return

        list = self.m_ui.serialPortInfoListBox.itemData(idx)
        count = len(list) if list else 0
        description = list[1] if count > 1 else BLANK_STRING
        self.m_ui.descriptionLabel.setText(f"Description: {description}")
        manufacturer = list[2] if count > 2 else BLANK_STRING
        self.m_ui.manufacturerLabel.setText(f"Manufacturer: {manufacturer}")
        serialno = list[3] if count > 3 else BLANK_STRING
        self.m_ui.serialNumberLabel.setText(f"Serial number: {serialno}")
        location = list[4] if count > 4 else BLANK_STRING
        self.m_ui.locationLabel.setText(f"Location: {location}")
        vendor = list[5] if count > 5 else BLANK_STRING
        self.m_ui.vidLabel.setText(f"Vendor Identifier: {vendor}")
        id = list[6] if count > 6 else BLANK_STRING
        self.m_ui.pidLabel.setText(f"Product Identifier: {id}")

    @Slot()
    def apply(self):
        self.update_settings()
        self.hide()

    @Slot(int)
    def check_custom_baud_rate_policy(self, idx):
        is_custom_baud_rate = idx == CUSTOM_BAUDRATE_INDEX
        self.m_ui.baudRateBox.setEditable(is_custom_baud_rate)
        if is_custom_baud_rate:
            self.m_ui.baudRateBox.clearEditText()
            edit = self.m_ui.baudRateBox.lineEdit()
            edit.setValidator(self.m_intValidator)

    @Slot(int)
    def check_custom_device_path_policy(self, idx):
        is_custom_path = idx == self._custom_port_index
        self.m_ui.serialPortInfoListBox.setEditable(is_custom_path)
        if is_custom_path:
            self.m_ui.serialPortInfoListBox.clearEditText()

    def fill_ports_parameters(self):
        self.m_ui.baudRateBox.addItem("9600", QSerialPort.Baud9600)
        self.m_ui.baudRateBox.addItem("19200", QSerialPort.Baud19200)
        self.m_ui.baudRateBox.addItem("38400", QSerialPort.Baud38400)
        self.m_ui.baudRateBox.addItem("115200", QSerialPort.Baud115200)
        self.m_ui.baudRateBox.addItem("Custom")

        self.m_ui.dataBitsBox.addItem("5", QSerialPort.Data5)
        self.m_ui.dataBitsBox.addItem("6", QSerialPort.Data6)
        self.m_ui.dataBitsBox.addItem("7", QSerialPort.Data7)
        self.m_ui.dataBitsBox.addItem("8", QSerialPort.Data8)
        self.m_ui.dataBitsBox.setCurrentIndex(3)

        self.m_ui.parityBox.addItem("None", QSerialPort.NoParity)
        self.m_ui.parityBox.addItem("Even", QSerialPort.EvenParity)
        self.m_ui.parityBox.addItem("Odd", QSerialPort.OddParity)
        self.m_ui.parityBox.addItem("Mark", QSerialPort.MarkParity)
        self.m_ui.parityBox.addItem("Space", QSerialPort.SpaceParity)

        self.m_ui.stopBitsBox.addItem("1", QSerialPort.OneStop)
        if sys.platform == "win32":
            self.m_ui.stopBitsBox.addItem("1.5", QSerialPort.OneAndHalfStop)

        self.m_ui.stopBitsBox.addItem("2", QSerialPort.TwoStop)

        self.m_ui.flowControlBox.addItem("None", QSerialPort.NoFlowControl)
        self.m_ui.flowControlBox.addItem("RTS/CTS", QSerialPort.HardwareControl)
        self.m_ui.flowControlBox.addItem("XON/XOFF", QSerialPort.SoftwareControl)

    def fill_ports_info(self):
        self.m_ui.serialPortInfoListBox.clear()
        for info in QSerialPortInfo.availablePorts():
            list = []
            description = info.description()
            manufacturer = info.manufacturer()
            serial_number = info.serialNumber()
            list.append(info.portName())
            list.append(description if description else BLANK_STRING)
            list.append(manufacturer if manufacturer else BLANK_STRING)
            list.append(serial_number if serial_number else BLANK_STRING)
            list.append(info.systemLocation())
            vid = info.vendorIdentifier()
            list.append(f"{vid:x}" if vid else BLANK_STRING)
            pid = info.productIdentifier()
            list.append(f"{pid:x}" if pid else BLANK_STRING)
            self.m_ui.serialPortInfoListBox.addItem(list[0], list)

        self._custom_port_index = self.m_ui.serialPortInfoListBox.count()
        self.m_ui.serialPortInfoListBox.addItem("Custom")

    def update_settings(self):
        self.m_currentSettings.name = self.m_ui.serialPortInfoListBox.currentText()

        baud_index = self.m_ui.baudRateBox.currentIndex()
        if baud_index == CUSTOM_BAUDRATE_INDEX:
            text = self.m_ui.baudRateBox.currentText()
            self.m_currentSettings.baud_rate = int(text)
        else:
            self.m_currentSettings.baud_rate = self.m_ui.baudRateBox.currentData()
        self.m_currentSettings.string_baud_rate = f"{self.m_currentSettings.baud_rate}"

        self.m_currentSettings.data_bits = self.m_ui.dataBitsBox.currentData()
        self.m_currentSettings.string_data_bits = self.m_ui.dataBitsBox.currentText()

        self.m_currentSettings.parity = self.m_ui.parityBox.currentData()
        self.m_currentSettings.string_parity = self.m_ui.parityBox.currentText()

        self.m_currentSettings.stop_bits = self.m_ui.stopBitsBox.currentData()
        self.m_currentSettings.string_stop_bits = self.m_ui.stopBitsBox.currentText()

        self.m_currentSettings.flow_control = self.m_ui.flowControlBox.currentData()
        self.m_currentSettings.string_flow_control = self.m_ui.flowControlBox.currentText()

        self.m_currentSettings.local_echo_enabled = self.m_ui.localEchoCheckBox.isChecked()
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SettingsDialog</class>
 <widget class="QDialog" name="SettingsDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>392</width>
    <height>386</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Settings</string>
  </property>
  <layout class="QGridLayout" name="gridLayout_3">
   <item row="0" column="1">
    <widget class="QGroupBox" name="parametersBox">
     <property name="title">
      <string>Select Parameters</string>
     </property>
     <layout class="QGridLayout" name="gridLayout_2">
      <item row="0" column="0">
       <widget class="QLabel" name="baudRateLabel">
        <property name="text">
         <string>BaudRate:</string>
        </property>
       </widget>
      </item>
      <item row="0" column="1">
       <widget class="QComboBox" name="baudRateBox"/>
      </item>
      <item row="1" column="0">
       <widget class="QLabel" name="dataBitsLabel">
        <property name="text">
         <string>Data bits:</string>
        </property>
       </widget>
      </item>
      <item row="1" column="1">
       <widget class="QComboBox" name="dataBitsBox"/>
      </item>
      <item row="2" column="0">
       <widget class="QLabel" name="parityLabel">
        <property name="text">
         <string>Parity:</string>
        </property>
       </widget>
      </item>
      <item row="2" column="1">
       <widget class="QComboBox" name="parityBox"/>
      </item>
      <item row="3" column="0">
       <widget class="QLabel" name="stopBitsLabel">
        <property name="text">
         <string>Stop bits:</string>
        </property>
       </widget>
      </item>
      <item row="3" column="1">
       <widget class="QComboBox" name="stopBitsBox"/>
      </item>
      <item row="4" column="0">
       <widget class="QLabel" name="flowControlLabel">
        <property name="text">
         <string>Flow control:</string>
        </property>
       </widget>
      </item>
      <item row="4" column="1">
       <widget class="QComboBox" name="flowControlBox"/>
      </item>
     </layout>
    </widget>
   </item>
   <item row="0" column="0">
    <widget class="QGroupBox" name="selectBox">
     <property name="title">
      <string>Select Serial Port</string>
     </property>
     <layout class="QGridLayout" name="gridLayout">
      <item row="0" column="0">
       <widget class="QComboBox" name="serialPortInfoListBox"/>
      </item>
      <item row="1" column="0">
       <widget class="QLabel" name="descriptionLabel">
        <property name="text">
         <string>Description:</string>
        </property>
       </widget>
      </item>
      <item row="2" column="0">
       <widget class="QLabel" name="manufacturerLabel">
        <property name="text">
         <string>Manufacturer:</string>
        </property>
       </widget>
      </item>
      <item row="3" column="0">
       <widget class="QLabel" name="serialNumberLabel">
        <property name="text">
         <string>Serial number:</string>
        </property>
       </widget>
      </item>
      <item row="4" column="0">
       <widget class="QLabel" name="locationLabel">
        <property name="text">
         <string>Location:</string>
        </property>
       </widget>
      </item>
      <item row="5" column="0">
       <widget class="QLabel" name="vidLabel">
        <property name="text">
         <string>Vendor ID:</string>
        </property>
       </widget>
      </item>
      <item row="6" column="0">
       <widget class="QLabel" name="pidLabel">
        <property name="text">
         <string>Product ID:</string>
        </property>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
   <item row="2" column="0" colspan="2">
    <layout class="QHBoxLayout" name="horizontalLayout">
     <item>
      <spacer name="horizontalSpacer">
       <property name="orientation">
        <enum>Qt::Orientation::Horizontal</enum>
       </property>
       <property name="sizeHint" stdset="0">
        <size>
         <width>96</width>
         <height>20</height>
        </size>
       </property>
      </spacer>
     </item>
     <item>
      <widget class="QPushButton" name="applyButton">
       <property name="text">
        <string>Apply</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
   <item row="1" column="0" colspan="2">
    <widget class="QGroupBox" name="additionalOptionsGroupBox">
     <property name="title">
      <string>Additional options</string>
     </property>
     <layout class="QVBoxLayout" name="verticalLayout">
      <item>
       <widget class="QCheckBox" name="localEchoCheckBox">
        <property name="text">
         <string>Local echo</string>
        </property>
        <property name="checked">
         <bool>true</bool>
        </property>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>
<RCC>
    <qresource prefix="/">
        <file>images/connect.png</file>
        <file>images/disconnect.png</file>
        <file>images/application-exit.png</file>
        <file>images/settings.png</file>
        <file>images/clear.png</file>
    </qresource>
</RCC>