import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QComboBox, QPushButton, QTextEdit, QLineEdit, QSpinBox)
from PyQt5.QtCore import QIODevice, Qt, QTimer
from PyQt5.QtSerialPort import QSerialPort, QSerialPortInfo
class SerialTool(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQt5 串口工具")
self.resize(800, 600)
# 串口对象
self.serial = QSerialPort()
# 创建UI
self.init_ui()
# 定时刷新可用串口
self.timer = QTimer(self)
self.timer.timeout.connect(self.refresh_ports)
self.timer.start(1000) # 1秒刷新一次
# 连接信号槽
self.connect_slots()
# 初始化可用串口
self.refresh_ports()
def init_ui(self):
# 主窗口布局
main_widget = QWidget()
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
# 串口配置区域
config_layout = QHBoxLayout()
# 串口选择
self.port_label = QLabel("串口:")
self.port_combo = QComboBox()
# 波特率选择
self.baud_label = QLabel("波特率:")
self.baud_combo = QComboBox()
self.baud_combo.addItems(["9600", "19200", "38400", "57600", "115200"])
self.baud_combo.setCurrentText("115200")
# 数据位
self.data_bits_label = QLabel("数据位:")
self.data_bits_combo = QComboBox()
self.data_bits_combo.addItems(["5", "6", "7", "8"])
self.data_bits_combo.setCurrentText("8")
# 停止位
self.stop_bits_label = QLabel("停止位:")
self.stop_bits_combo = QComboBox()
self.stop_bits_combo.addItems(["1", "1.5", "2"])
# 校验位
self.parity_label = QLabel("校验位:")
self.parity_combo = QComboBox()
self.parity_combo.addItems(["无", "奇校验", "偶校验", "标记", "空格"])
# 打开/关闭按钮
self.open_btn = QPushButton("打开串口")
# 添加到配置布局
config_layout.addWidget(self.port_label)
config_layout.addWidget(self.port_combo)
config_layout.addWidget(self.baud_label)
config_layout.addWidget(self.baud_combo)
config_layout.addWidget(self.data_bits_label)
config_layout.addWidget(self.data_bits_combo)
config_layout.addWidget(self.stop_bits_label)
config_layout.addWidget(self.stop_bits_combo)
config_layout.addWidget(self.parity_label)
config_layout.addWidget(self.parity_combo)
config_layout.addWidget(self.open_btn)
# 数据收发区域
data_layout = QHBoxLayout()
# 接收区
receive_group = QVBoxLayout()
self.receive_label = QLabel("接收数据:")
self.receive_text = QTextEdit()
self.receive_text.setReadOnly(True)
self.clear_receive_btn = QPushButton("清空接收")
receive_group.addWidget(self.receive_label)
receive_group.addWidget(self.receive_text)
receive_group.addWidget(self.clear_receive_btn)
# 发送区
send_group = QVBoxLayout()
self.send_label = QLabel("发送数据:")
self.send_text = QTextEdit()
self.send_btn = QPushButton("发送")
self.clear_send_btn = QPushButton("清空发送")
send_group.addWidget(self.send_label)
send_group.addWidget(self.send_text)
send_group.addWidget(self.send_btn)
send_group.addWidget(self.clear_send_btn)
data_layout.addLayout(receive_group, stretch=1)
data_layout.addLayout(send_group, stretch=1)
# 状态栏
self.status_bar = self.statusBar()
self.status_label = QLabel("就绪")
self.status_bar.addWidget(self.status_label)
# 添加到主布局
main_layout.addLayout(config_layout)
main_layout.addLayout(data_layout)
def connect_slots(self):
"""连接信号槽"""
self.open_btn.clicked.connect(self.toggle_serial)
self.send_btn.clicked.connect(self.send_data)
self.clear_receive_btn.clicked.connect(self.receive_text.clear)
self.clear_send_btn.clicked.connect(self.send_text.clear)
self.serial.readyRead.connect(self.read_data)
def refresh_ports(self):
"""刷新可用串口列表"""
current_port = self.port_combo.currentText()
self.port_combo.clear()
ports = QSerialPortInfo.availablePorts()
for port in ports:
self.port_combo.addItem(port.portName())
# 保持之前选择的串口
if current_port in [self.port_combo.itemText(i) for i in range(self.port_combo.count())]:
self.port_combo.setCurrentText(current_port)
def toggle_serial(self):
"""打开/关闭串口"""
if self.serial.isOpen():
self.close_serial()
else:
self.open_serial()
def open_serial(self):
"""打开串口"""
port_name = self.port_combo.currentText()
if not port_name:
self.status_label.setText("错误: 请选择串口")
return
# 配置串口参数
self.serial.setPortName(port_name)
self.serial.setBaudRate(int(self.baud_combo.currentText()))
# 数据位
data_bits = {
"5": QSerialPort.Data5,
"6": QSerialPort.Data6,
"7": QSerialPort.Data7,
"8": QSerialPort.Data8
}
self.serial.setDataBits(data_bits[self.data_bits_combo.currentText()])
# 停止位
stop_bits = {
"1": QSerialPort.OneStop,
"1.5": QSerialPort.OneAndHalfStop,
"2": QSerialPort.TwoStop
}
self.serial.setStopBits(stop_bits[self.stop_bits_combo.currentText()])
# 校验位
parity = {
"无": QSerialPort.NoParity,
"奇校验": QSerialPort.OddParity,
"偶校验": QSerialPort.EvenParity,
"标记": QSerialPort.MarkParity,
"空格": QSerialPort.SpaceParity
}
self.serial.setParity(parity[self.parity_combo.currentText()])
# 打开串口
if self.serial.open(QIODevice.ReadWrite):
self.open_btn.setText("关闭串口")
self.status_label.setText(f"已连接 {port_name}")
self.port_combo.setEnabled(False)
self.baud_combo.setEnabled(False)
self.data_bits_combo.setEnabled(False)
self.stop_bits_combo.setEnabled(False)
self.parity_combo.setEnabled(False)
else:
self.status_label.setText(f"错误: 无法打开串口 {port_name}")
def close_serial(self):
"""关闭串口"""
if self.serial.isOpen():
self.serial.close()
self.open_btn.setText("打开串口")
self.status_label.setText("串口已关闭")
self.port_combo.setEnabled(True)
self.baud_combo.setEnabled(True)
self.data_bits_combo.setEnabled(True)
self.stop_bits_combo.setEnabled(True)
self.parity_combo.setEnabled(True)
def read_data(self):
"""读取串口数据"""
if self.serial.bytesAvailable():
data = self.serial.readAll()
try:
text = data.data().decode('utf-8')
self.receive_text.moveCursor(Qt.TextCursor.End)
self.receive_text.insertPlainText(text)
except UnicodeDecodeError:
# 如果不是UTF-8文本,显示16进制
hex_data = data.toHex().data().decode()
self.receive_text.moveCursor(Qt.TextCursor.End)
self.receive_text.insertPlainText(f"[HEX] {hex_data} ")
def send_data(self):
"""发送数据"""
if not self.serial.isOpen():
self.status_label.setText("错误: 串口未打开")
return
text = self.send_text.toPlainText()
if not text:
return
# 发送文本数据
if self.serial.write(text.encode('utf-8')) == -1:
self.status_label.setText("错误: 发送失败")
else:
self.status_label.setText("数据已发送")
def closeEvent(self, event):
"""关闭窗口时确保串口关闭"""
if self.serial.isOpen():
self.serial.close()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SerialTool()
window.show()
sys.exit(app.exec_())