# 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_())