diff --git a/python/serial/serial_assistant.py b/python/serial/serial_assistant.py new file mode 100644 index 0000000..ac8de2f --- /dev/null +++ b/python/serial/serial_assistant.py @@ -0,0 +1,586 @@ +# 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_())