demo/python/serial/serial_assistant.py
2025-06-17 14:34:48 +08:00

587 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# pip install pyserial PyQt5
import sys
import re
import serial
import serial.tools.list_ports
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QComboBox, QPushButton, QTextEdit, QLineEdit,
QSpinBox, QCheckBox, QMessageBox, QGroupBox, QSplitter,
QScrollArea, QSizePolicy, QFrame)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDateTime, QTimer
from PyQt5.QtGui import QTextCursor, QColor, QTextCharFormat, QFont
class SerialThread(QThread):
data_received = pyqtSignal(str, str) # data, timestamp
def __init__(self, serial_port):
super().__init__()
self.serial_port = serial_port
self.running = True
def run(self):
while self.running and self.serial_port.is_open:
try:
if self.serial_port.in_waiting:
data = self.serial_port.read(self.serial_port.in_waiting).decode('utf-8', errors='ignore')
timestamp = QDateTime.currentDateTime().toString("hh:mm:ss.zzz")
self.data_received.emit(data, timestamp)
except Exception as e:
print(f"Serial read error: {e}")
break
def stop(self):
self.running = False
class SerialDebugger(QMainWindow):
def __init__(self):
super().__init__()
self.serial = None
self.serial_thread = None
self.auto_send_timer = None
self.send_count = 0
self.receive_count = 0
self.init_ui()
self.refresh_ports()
def init_ui(self):
self.setWindowTitle('高级串口调试助手')
self.setGeometry(100, 100, 1200, 800)
# 主窗口部件
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QHBoxLayout(main_widget)
main_layout.setContentsMargins(5, 5, 5, 5)
# 左侧控制面板
control_panel = QWidget()
control_panel.setMaximumWidth(300)
control_layout = QVBoxLayout(control_panel)
control_layout.setContentsMargins(0, 0, 0, 0)
# 串口设置区域
port_group = QGroupBox("串口设置")
port_layout = QVBoxLayout()
# 串口选择
port_select_layout = QHBoxLayout()
self.port_label = QLabel('串口:')
self.port_combo = QComboBox()
self.refresh_btn = QPushButton('刷新')
self.refresh_btn.clicked.connect(self.refresh_ports)
port_select_layout.addWidget(self.port_label)
port_select_layout.addWidget(self.port_combo)
port_select_layout.addWidget(self.refresh_btn)
port_layout.addLayout(port_select_layout)
# 波特率选择
baud_layout = QHBoxLayout()
self.baud_label = QLabel('波特率:')
self.baud_combo = QComboBox()
common_baudrates = ['9600', '19200', '38400', '57600', '115200',
'230400', '460800', '500000', '576000', '750000',
'921600', '1000000', '1152000', '1500000', '2000000']
self.baud_combo.addItems(common_baudrates)
self.baud_combo.setCurrentText('2000000')
self.baud_combo.setEditable(True)
baud_layout.addWidget(self.baud_label)
baud_layout.addWidget(self.baud_combo)
port_layout.addLayout(baud_layout)
# 其他串口参数
param_layout = QHBoxLayout()
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.stop_bits_combo.setCurrentText('1')
param_layout.addWidget(self.data_bits_label)
param_layout.addWidget(self.data_bits_combo)
param_layout.addWidget(self.stop_bits_label)
param_layout.addWidget(self.stop_bits_combo)
port_layout.addLayout(param_layout)
# 校验位
parity_layout = QHBoxLayout()
self.parity_label = QLabel('校验位:')
self.parity_combo = QComboBox()
self.parity_combo.addItems(['', '奇校验', '偶校验', '标记', '空格'])
self.parity_combo.setCurrentText('')
parity_layout.addWidget(self.parity_label)
parity_layout.addWidget(self.parity_combo)
port_layout.addLayout(parity_layout)
# 控制按钮
ctrl_btn_layout = QHBoxLayout()
self.open_btn = QPushButton('打开串口')
self.open_btn.clicked.connect(self.toggle_serial)
self.dtr_check = QCheckBox('DTR')
self.dtr_check.stateChanged.connect(self.set_dtr)
self.rts_check = QCheckBox('RTS')
self.rts_check.stateChanged.connect(self.set_rts)
ctrl_btn_layout.addWidget(self.open_btn)
ctrl_btn_layout.addWidget(self.dtr_check)
ctrl_btn_layout.addWidget(self.rts_check)
port_layout.addLayout(ctrl_btn_layout)
port_group.setLayout(port_layout)
control_layout.addWidget(port_group)
# 接收选项
rx_group = QGroupBox("接收设置")
rx_layout = QVBoxLayout()
self.hex_display = QCheckBox('十六进制显示')
self.hex_display.stateChanged.connect(self.update_receive_display)
self.timestamp_check = QCheckBox('显示时间戳')
# self.timestamp_check.setChecked(True)
self.terminal_mode = QCheckBox('自动滚动')
self.terminal_mode.setChecked(True)
self.terminal_mode.stateChanged.connect(self.toggle_terminal_mode)
self.clear_rx_btn = QPushButton('清空接收区')
self.clear_rx_btn.clicked.connect(self.clear_receive)
rx_layout.addWidget(self.hex_display)
rx_layout.addWidget(self.timestamp_check)
rx_layout.addWidget(self.terminal_mode)
rx_layout.addWidget(self.clear_rx_btn)
rx_group.setLayout(rx_layout)
control_layout.addWidget(rx_group)
# 发送选项
tx_group = QGroupBox("发送设置")
tx_layout = QVBoxLayout()
self.hex_send = QCheckBox('十六进制发送')
self.auto_send_check = QCheckBox('自动发送')
self.auto_send_check.stateChanged.connect(self.toggle_auto_send)
self.auto_send_interval = QSpinBox()
self.auto_send_interval.setRange(100, 10000)
self.auto_send_interval.setValue(1000)
self.auto_send_interval.setSuffix('ms')
auto_send_layout = QHBoxLayout()
auto_send_layout.addWidget(self.auto_send_check)
auto_send_layout.addWidget(self.auto_send_interval)
self.clear_tx_btn = QPushButton('清空发送区')
self.clear_tx_btn.clicked.connect(lambda: self.send_text.clear())
tx_layout.addWidget(self.hex_send)
tx_layout.addLayout(auto_send_layout)
tx_layout.addWidget(self.clear_tx_btn)
tx_group.setLayout(tx_layout)
control_layout.addWidget(tx_group)
# 自定义命令区域(可折叠)
self.cmd_group = QGroupBox("自定义命令 ▼")
self.cmd_group.setCheckable(True)
self.cmd_group.setChecked(True)
self.cmd_group.toggled.connect(self.toggle_cmd_panel)
cmd_layout = QVBoxLayout()
# 命令列表
self.cmd_scroll = QScrollArea()
self.cmd_scroll.setWidgetResizable(True)
self.cmd_scroll_widget = QWidget()
self.cmd_scroll_layout = QVBoxLayout(self.cmd_scroll_widget)
self.cmd_scroll_layout.setContentsMargins(2, 2, 2, 2)
self.cmd_scroll.setWidget(self.cmd_scroll_widget)
# 添加命令控件
add_cmd_layout = QHBoxLayout()
self.new_cmd_input = QLineEdit()
self.new_cmd_input.setPlaceholderText("输入新命令")
self.add_cmd_btn = QPushButton('添加')
self.add_cmd_btn.clicked.connect(self.add_custom_command)
add_cmd_layout.addWidget(self.new_cmd_input)
add_cmd_layout.addWidget(self.add_cmd_btn)
cmd_layout.addWidget(self.cmd_scroll)
cmd_layout.addLayout(add_cmd_layout)
self.cmd_group.setLayout(cmd_layout)
control_layout.addWidget(self.cmd_group)
# 添加一些示例命令
self.add_example_commands()
control_layout.addStretch()
main_layout.addWidget(control_panel)
# 右侧通信区域
comm_panel = QWidget()
comm_layout = QVBoxLayout(comm_panel)
comm_layout.setContentsMargins(5, 0, 0, 0)
# 接收区
rx_frame = QFrame()
rx_frame.setFrameShape(QFrame.StyledPanel)
rx_frame_layout = QVBoxLayout(rx_frame)
rx_frame_layout.setContentsMargins(0, 0, 0, 0)
self.receive_text = QTextEdit()
self.receive_text.setReadOnly(True)
self.receive_text.setLineWrapMode(QTextEdit.NoWrap)
font = QFont("Courier New", 10)
self.receive_text.setFont(font)
rx_frame_layout.addWidget(self.receive_text)
comm_layout.addWidget(rx_frame)
# 发送区
tx_frame = QFrame()
tx_frame.setFrameShape(QFrame.StyledPanel)
tx_frame_layout = QVBoxLayout(tx_frame)
tx_frame_layout.setContentsMargins(0, 0, 0, 0)
self.send_text = QTextEdit()
self.send_text.setFont(font)
send_btn_layout = QHBoxLayout()
self.send_btn = QPushButton('发送')
self.send_btn.clicked.connect(self.send_data)
send_btn_layout.addStretch()
send_btn_layout.addWidget(self.send_btn)
tx_frame_layout.addWidget(self.send_text)
tx_frame_layout.addLayout(send_btn_layout)
comm_layout.addWidget(tx_frame)
main_layout.addWidget(comm_panel, stretch=1)
# 状态栏
self.status_bar = self.statusBar()
self.status_label = QLabel("发送: 0 字节 | 接收: 0 字节")
self.status_bar.addPermanentWidget(self.status_label)
self.status_bar.showMessage('就绪')
# 颜色格式
self.default_format = QTextCharFormat()
self.default_format.setForeground(QColor('black'))
self.error_format = QTextCharFormat()
self.error_format.setForeground(QColor('red'))
self.warning_format = QTextCharFormat()
self.warning_format.setForeground(QColor('orange'))
self.success_format = QTextCharFormat()
self.success_format.setForeground(QColor('green'))
self.info_format = QTextCharFormat()
self.info_format.setForeground(QColor('blue'))
def add_example_commands(self):
"""添加一些示例命令"""
example_commands = [
("55 55 55 55 55 55", True),
("BOUFFALOLAB5555DTR1", False),
("BOUFFALOLAB5555DTR0", False),
("BOUFFALOLAB5555RTS0", False),
("BOUFFALOLAB5555RTS1", False)
]
for cmd, is_hex in example_commands:
self.add_command_to_panel(cmd, is_hex)
def add_command_to_panel(self, cmd, is_hex=False):
"""添加一个命令到命令面板"""
cmd_widget = QWidget()
cmd_layout = QHBoxLayout(cmd_widget)
cmd_layout.setContentsMargins(0, 0, 0, 0)
cmd_label = QLabel(cmd)
cmd_label.setWordWrap(True)
hex_check = QCheckBox('HEX')
hex_check.setChecked(is_hex)
send_btn = QPushButton('发送')
send_btn.setMaximumWidth(60)
send_btn.clicked.connect(lambda _, c=cmd, h=hex_check: self.send_custom_command(c, h.isChecked()))
del_btn = QPushButton('×')
del_btn.setMaximumWidth(30)
del_btn.clicked.connect(lambda _, w=cmd_widget: self.remove_command(w))
cmd_layout.addWidget(cmd_label, stretch=1)
cmd_layout.addWidget(hex_check)
cmd_layout.addWidget(send_btn)
cmd_layout.addWidget(del_btn)
self.cmd_scroll_layout.addWidget(cmd_widget)
def remove_command(self, widget):
"""从命令面板移除一个命令"""
self.cmd_scroll_layout.removeWidget(widget)
widget.deleteLater()
def toggle_cmd_panel(self, checked):
"""切换命令面板的展开/折叠状态"""
if checked:
self.cmd_group.setTitle("自定义命令 ▼")
else:
self.cmd_group.setTitle("自定义命令 ►")
def refresh_ports(self):
self.port_combo.clear()
ports = serial.tools.list_ports.comports()
for port in ports:
self.port_combo.addItem(port.device)
if not ports:
self.port_combo.addItem('无可用串口')
def toggle_serial(self):
if self.serial and self.serial.is_open:
self.close_serial()
self.open_btn.setText('打开串口')
self.status_bar.showMessage('串口已关闭')
else:
if self.open_serial():
self.open_btn.setText('关闭串口')
self.status_bar.showMessage('串口已打开')
def open_serial(self):
port = self.port_combo.currentText()
if not port or port == '无可用串口':
QMessageBox.warning(self, '警告', '没有可用的串口!')
return False
try:
baudrate = int(self.baud_combo.currentText())
except ValueError:
QMessageBox.warning(self, '警告', '请输入有效的波特率!')
return False
# 获取其他串口参数
bytesize = {
'5': serial.FIVEBITS,
'6': serial.SIXBITS,
'7': serial.SEVENBITS,
'8': serial.EIGHTBITS
}.get(self.data_bits_combo.currentText(), serial.EIGHTBITS)
stopbits = {
'1': serial.STOPBITS_ONE,
'1.5': serial.STOPBITS_ONE_POINT_FIVE,
'2': serial.STOPBITS_TWO
}.get(self.stop_bits_combo.currentText(), serial.STOPBITS_ONE)
parity = {
'': serial.PARITY_NONE,
'奇校验': serial.PARITY_ODD,
'偶校验': serial.PARITY_EVEN,
'标记': serial.PARITY_MARK,
'空格': serial.PARITY_SPACE
}.get(self.parity_combo.currentText(), serial.PARITY_NONE)
try:
self.serial = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
timeout=1
)
# 设置初始DTR/RTS状态
self.serial.dtr = self.dtr_check.isChecked()
self.serial.rts = self.rts_check.isChecked()
self.serial_thread = SerialThread(self.serial)
self.serial_thread.data_received.connect(self.receive_data)
self.serial_thread.start()
return True
except Exception as e:
QMessageBox.critical(self, '错误', f'无法打开串口: {e}')
return False
def close_serial(self):
self.toggle_auto_send(False)
if self.serial_thread:
self.serial_thread.stop()
self.serial_thread.wait()
self.serial_thread = None
if self.serial and self.serial.is_open:
self.serial.close()
self.serial = None
def receive_data(self, data, timestamp):
self.receive_count += len(data.encode('utf-8'))
self.update_status()
cursor = self.receive_text.textCursor()
cursor.movePosition(QTextCursor.End)
if self.timestamp_check.isChecked():
timestamp_str = f"[{timestamp}] "
cursor.insertText(timestamp_str, self.default_format)
# 智能颜色判断
fmt = self.determine_text_format(data)
if self.hex_display.isChecked():
hex_data = ' '.join([f'{ord(c):02X}' for c in data])
cursor.insertText(hex_data, fmt)
else:
cursor.insertText(data, fmt)
if self.terminal_mode.isChecked():
cursor.movePosition(QTextCursor.End)
self.receive_text.setTextCursor(cursor)
def determine_text_format(self, text):
# 根据内容判断使用哪种颜色格式
text_lower = text.lower()
'''
if re.search(r'error|fail|failed|err|非法|错误', text_lower):
return self.error_format
elif re.search(r'warn|alert|警告|注意', text_lower):
return self.warning_format
elif re.search(r'success|ok|完成|成功', text_lower):
return self.success_format
elif re.search(r'info|version|信息|版本', text_lower):
return self.info_format
else:
return self.default_format
'''
return self.default_format
def send_data(self):
if not self.serial or not self.serial.is_open:
QMessageBox.warning(self, '警告', '请先打开串口!')
return
text = self.send_text.toPlainText()
if not text:
return
try:
if self.hex_send.isChecked():
# 处理十六进制发送
text = text.strip()
hex_bytes = []
for part in text.split():
try:
hex_bytes.append(int(part, 16))
except ValueError:
QMessageBox.warning(self, '警告', f'无效的十六进制数据: {part}')
return
data = bytes(hex_bytes)
else:
data = text.encode('utf-8')
self.serial.write(data)
self.send_count += len(data)
self.update_status()
self.status_bar.showMessage(f'已发送 {len(data)} 字节', 2000)
except Exception as e:
QMessageBox.critical(self, '错误', f'发送失败: {e}')
def update_receive_display(self):
# 当切换十六进制显示时,更新显示内容
if self.serial and self.serial.is_open:
current_text = self.receive_text.toPlainText()
if current_text:
if self.hex_display.isChecked():
hex_data = ' '.join([f'{ord(c):02X}' for c in current_text if not c.isspace()])
self.receive_text.setPlainText(hex_data)
else:
try:
# 尝试将十六进制数据转换回字符
bytes_data = bytes.fromhex(current_text.replace(' ', ''))
text = bytes_data.decode('utf-8', errors='ignore')
self.receive_text.setPlainText(text)
except:
# 如果不是有效的十六进制数据,保持原样
pass
def toggle_terminal_mode(self, state):
if state:
self.receive_text.setLineWrapMode(QTextEdit.NoWrap)
cursor = self.receive_text.textCursor()
cursor.movePosition(QTextCursor.End)
self.receive_text.setTextCursor(cursor)
else:
self.receive_text.setLineWrapMode(QTextEdit.WidgetWidth)
def set_dtr(self, state):
if self.serial and self.serial.is_open:
self.serial.dtr = state == Qt.Checked
def set_rts(self, state):
if self.serial and self.serial.is_open:
self.serial.rts = state == Qt.Checked
def toggle_auto_send(self, state):
if state:
interval = self.auto_send_interval.value()
self.auto_send_timer = QTimer()
self.auto_send_timer.timeout.connect(self.send_data)
self.auto_send_timer.start(interval)
else:
if self.auto_send_timer:
self.auto_send_timer.stop()
self.auto_send_timer = None
def add_custom_command(self):
cmd = self.new_cmd_input.text().strip()
if cmd:
self.add_command_to_panel(cmd)
self.new_cmd_input.clear()
def send_custom_command(self, cmd, is_hex):
if not self.serial or not self.serial.is_open:
QMessageBox.warning(self, '警告', '请先打开串口!')
return
# 保存当前状态
current_hex = self.hex_send.isChecked()
# 使用命令的HEX设置
self.hex_send.setChecked(is_hex)
# 设置发送文本并发送
self.send_text.setPlainText(cmd)
self.send_data()
# 恢复原状态
self.hex_send.setChecked(current_hex)
def clear_receive(self):
self.receive_text.clear()
self.receive_count = 0
self.update_status()
def update_status(self):
self.status_label.setText(f"发送: {self.send_count} 字节 | 接收: {self.receive_count} 字节")
def closeEvent(self, event):
self.close_serial()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
debugger = SerialDebugger()
debugger.show()
sys.exit(app.exec_())