import sys import serial.tools.list_ports from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QPushButton, QLabel, QLineEdit, QMessageBox) from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QIntValidator from serial_recorder import SerialRecorder import os # 定义命令常量 CMD_ON = b'\x01\x00\x01\x02' CMD_OFF = b'\x01\x00\x00\x01' # CMD_DATA_ON = b'\xAA\xAA\xAA\xAA\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\xAD' # CMD_DATA_OFF = b'\xAA\xAA\xAA\xAA\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\xAE' # 智己项目 - 开关 9字节 CMD_DATA_ON = b'\x0A\x01\x77\x3B\x00\x00\x80\x3F\x7C' CMD_DATA_OFF = b'\x0A\x01\x77\x3B\x00\x00\x00\x00\xBD' class SerialUI(QMainWindow): def __init__(self): super().__init__() # self.setWindowTitle("Signal Collector V1.1.1 BUILD202503261405") self.resize(600, 400) # 设置无边框窗口 self.setWindowFlags(Qt.FramelessWindowHint) # 设置应用样式 self.setStyleSheet(""" QMainWindow { background-color: #2B2B2B; border: none; } QWidget { background-color: #2B2B2B; color: #FFFFFF; } QLabel { color: #CCCCCC; font-size: 14px; } QComboBox { background-color: #3C3F41; border: 1px solid #646464; border-radius: 3px; color: #FFFFFF; padding: 5px; min-width: 100px; } QComboBox:hover { border: 1px solid #FF8C00; } QComboBox::drop-down { border: none; padding-right: 8px; } QPushButton { background-color: #3C3F41; border: 1px solid #646464; border-radius: 3px; color: #FFFFFF; padding: 5px 15px; min-height: 25px; } QPushButton:hover { background-color: #434749; border: 1px solid #FF8C00; } QPushButton:pressed { background-color: #333637; } QPushButton:checked { background-color: #FF8C00; border: 1px solid #FF8C00; } QLineEdit { background-color: #3C3F41; border: 1px solid #646464; border-radius: 3px; color: #FFFFFF; padding: 5px; min-width: 100px; } QLineEdit:focus { border: 1px solid #FF8C00; } QLineEdit:disabled { background-color: #2B2B2B; color: #808080; } QStatusBar { background-color: #3C3F41; color: #CCCCCC; } QMenuBar { background-color: #2B2B2B; color: #FFFFFF; } QMenuBar::item { background-color: #2B2B2B; color: #FFFFFF; } QMenuBar::item:selected { background-color: #3C3F41; } QMenu { background-color: #2B2B2B; color: #FFFFFF; } QMenu::item:selected { background-color: #3C3F41; } #record_btn { background-color: #FF8C00; border: none; font-weight: bold; } #record_btn:hover { background-color: #FFA500; } #record_btn:checked { background-color: #CD3333; } #titleBar { background-color: #2B2B2B; border-bottom: 1px solid #3C3F41; min-height: 30px; } #titleLabel { color: #FFFFFF; font-size: 14px; padding-left: 10px; } #closeButton { background-color: transparent; border: none; color: #FFFFFF; padding: 5px 10px; } #closeButton:hover { background-color: #CD3333; } """) # 创建中心部件和布局 central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) layout.setContentsMargins(0, 0, 0, 0) # 移除边距 # 创建自定义标题栏 title_bar = QWidget() title_bar.setObjectName("titleBar") title_bar_layout = QHBoxLayout(title_bar) title_bar_layout.setContentsMargins(0, 0, 0, 0) # 标题标签 title_label = QLabel("Signal Collector V1.1.2 BUILD202511141419") title_label.setObjectName("titleLabel") # 关闭按钮 close_button = QPushButton("×") close_button.setObjectName("closeButton") close_button.setFixedSize(30, 30) close_button.clicked.connect(self.close) title_bar_layout.addWidget(title_label) title_bar_layout.addStretch() title_bar_layout.addWidget(close_button) # 添加标题栏到主布局 layout.addWidget(title_bar) # 创建内容区域 content_widget = QWidget() content_layout = QVBoxLayout(content_widget) content_layout.setContentsMargins(20, 20, 20, 20) # 串口选择区域 port_layout = QHBoxLayout() port_label = QLabel("串口:") self.port_combo = QComboBox() self.refresh_btn = QPushButton("刷新") self.refresh_btn.clicked.connect(self.refresh_ports) port_layout.addWidget(port_label) port_layout.addWidget(self.port_combo) port_layout.addWidget(self.refresh_btn) content_layout.addLayout(port_layout) # 波特率选择区域 baud_layout = QHBoxLayout() baud_label = QLabel("波特率:") self.baud_input = QLineEdit() self.baud_input.setText("4000000") self.baud_input.setValidator(QIntValidator(1200, 4000000)) self.baud_input.setPlaceholderText("请输入波特率") self.connect_btn = QPushButton("连接端口") self.connect_btn.setCheckable(True) self.connect_btn.clicked.connect(self.toggle_connection) baud_layout.addWidget(baud_label) baud_layout.addWidget(self.baud_input) baud_layout.addWidget(self.connect_btn) content_layout.addLayout(baud_layout) # 通道数设置区域 channel_layout = QHBoxLayout() channel_label = QLabel("通道数:") self.channel_input = QLineEdit() self.channel_input.setText("8") self.channel_input.setValidator(QIntValidator(1, 32)) self.channel_input.setPlaceholderText("请输入通道数(1-32)") self.apply_channel_btn = QPushButton("应用设置") self.apply_channel_btn.clicked.connect(self.apply_channel_settings) channel_layout.addWidget(channel_label) channel_layout.addWidget(self.channel_input) channel_layout.addWidget(self.apply_channel_btn) content_layout.addLayout(channel_layout) # 数据开关命令区域 data_cmd_layout = QHBoxLayout() data_cmd_label = QLabel("数据开关:") self.data_cmd_btn_on = QPushButton("ON") self.data_cmd_btn_on.clicked.connect(self.on_data_cmd_btn_on) self.data_cmd_btn_off = QPushButton("OFF") self.data_cmd_btn_off.clicked.connect(self.on_data_cmd_btn_off) data_cmd_layout.addWidget(data_cmd_label) data_cmd_layout.addWidget(self.data_cmd_btn_on) data_cmd_layout.addWidget(self.data_cmd_btn_off) content_layout.addLayout(data_cmd_layout) # 开关按钮区域 cmd_layout = QHBoxLayout() cmd_label = QLabel("开关命令:") self.cmd_btn_on = QPushButton("ON") self.cmd_btn_on.clicked.connect(self.on_cmd_btn_on) self.cmd_btn_off = QPushButton("OFF") self.cmd_btn_off.clicked.connect(self.on_cmd_btn_off) cmd_layout.addWidget(cmd_label) cmd_layout.addWidget(self.cmd_btn_on) cmd_layout.addWidget(self.cmd_btn_off) content_layout.addLayout(cmd_layout) # 录制控制区域 record_layout = QHBoxLayout() self.record_btn = QPushButton("开始录制") self.record_btn.setCheckable(True) self.record_btn.clicked.connect(self.toggle_recording) record_layout.addWidget(self.record_btn) content_layout.addLayout(record_layout) # 标签输入区域 tag_layout = QHBoxLayout() self.tag_input = QLineEdit() self.tag_input.setPlaceholderText("输入标签内容") self.add_label_btn = QPushButton("增加标签") self.add_label_btn.clicked.connect(self.add_tag) tag_layout.addWidget(self.tag_input) tag_layout.addWidget(self.add_label_btn) content_layout.addLayout(tag_layout) # 数据统计显示区域 stats_layout = QHBoxLayout() self.stats_label = QLabel("总帧数: 0 | 有效帧: 0 | 损坏帧: 0") self.stats_label.setAlignment(Qt.AlignCenter) stats_layout.addWidget(self.stats_label) content_layout.addLayout(stats_layout) # 添加内容区域到主布局 layout.addWidget(content_widget) # 添加状态栏 self.statusBar().showMessage("就绪") # 初始化串口列表 self.refresh_ports() # 初始化录制状态 self.is_recording = False self.serial_recorder = None # 设置对象名称以便样式表识别 self.record_btn.setObjectName("record_btn") # 增加组件间距 content_layout.setSpacing(10) # 为所有水平布局添加间距 for layout_item in [port_layout, baud_layout, channel_layout, data_cmd_layout, cmd_layout, record_layout, tag_layout]: layout_item.setSpacing(10) # 设置窗口最小尺寸 self.setMinimumSize(450, 250) # 初始化时禁用录制按钮和命令按钮 self.record_btn.setEnabled(False) self.cmd_btn_on.setEnabled(False) self.cmd_btn_off.setEnabled(False) self.data_cmd_btn_on.setEnabled(False) self.data_cmd_btn_off.setEnabled(False) # 创建记录保存目录 self.records_save_path = './records' if not os.path.exists(self.records_save_path): os.makedirs(self.records_save_path) # 创建定时器用于更新统计信息 self.stats_timer = QTimer() self.stats_timer.timeout.connect(self.update_stats_display) self.stats_timer.start(1000) # 每秒更新一次 # 添加退出时的确认标志 self.exit_confirmed = False # 添加鼠标事件处理 self.title_bar = title_bar self.title_bar.mousePressEvent = self.mousePressEvent self.title_bar.mouseMoveEvent = self.mouseMoveEvent self.title_bar.mouseReleaseEvent = self.mouseReleaseEvent # 窗口拖动相关变量 self.dragging = False self.drag_position = None def mousePressEvent(self, event): """鼠标按下事件""" if event.button() == Qt.LeftButton and self.title_bar.rect().contains(event.position().toPoint()): self.dragging = True self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): """鼠标移动事件""" if self.dragging: self.move(event.globalPosition().toPoint() - self.drag_position) event.accept() def mouseReleaseEvent(self, event): """鼠标释放事件""" if event.button() == Qt.LeftButton: self.dragging = False event.accept() def on_data_cmd_btn_on(self): """发送数据ON命令""" if self.serial_recorder and self.serial_recorder.serial: try: self.serial_recorder.serial.write(CMD_DATA_ON) self.statusBar().showMessage("已发送数据ON命令") except serial.serialutil.SerialTimeoutException: QMessageBox.warning(self, "警告", "发送数据ON命令超时,请检查串口连接") self.statusBar().showMessage("发送数据ON命令失败") except Exception as e: QMessageBox.warning(self, "错误", f"发送数据ON命令失败: {str(e)}") self.statusBar().showMessage("发送数据ON命令失败") def on_data_cmd_btn_off(self): """发送数据OFF命令""" if self.serial_recorder and self.serial_recorder.serial: try: self.serial_recorder.serial.write(CMD_DATA_OFF) self.statusBar().showMessage("已发送数据OFF命令") except serial.serialutil.SerialTimeoutException: QMessageBox.warning(self, "警告", "发送数据OFF命令超时,请检查串口连接") self.statusBar().showMessage("发送数据OFF命令失败") except Exception as e: QMessageBox.warning(self, "错误", f"发送数据OFF命令失败: {str(e)}") self.statusBar().showMessage("发送数据OFF命令失败") def on_cmd_btn_on(self): """发送ON命令""" if self.serial_recorder and self.serial_recorder.serial: try: self.serial_recorder.serial.write(CMD_ON) self.statusBar().showMessage("已发送ON命令") except serial.serialutil.SerialTimeoutException: QMessageBox.warning(self, "警告", "发送ON命令超时,请检查串口连接") self.statusBar().showMessage("发送ON命令失败") except Exception as e: QMessageBox.warning(self, "错误", f"发送ON命令失败: {str(e)}") self.statusBar().showMessage("发送ON命令失败") def on_cmd_btn_off(self): """发送OFF命令""" if self.serial_recorder and self.serial_recorder.serial: try: self.serial_recorder.serial.write(CMD_OFF) self.statusBar().showMessage("已发送OFF命令") except serial.serialutil.SerialTimeoutException: QMessageBox.warning(self, "警告", "发送OFF命令超时,请检查串口连接") self.statusBar().showMessage("发送OFF命令失败") except Exception as e: QMessageBox.warning(self, "错误", f"发送OFF命令失败: {str(e)}") self.statusBar().showMessage("发送OFF命令失败") def update_stats_display(self): """更新统计信息显示""" if self.serial_recorder: stats = self.serial_recorder.frame_processor.get_statistics() self.stats_label.setText( f"总帧数: {stats['total_frames']} | " f"有效帧: {stats['valid_frames']} | " f"损坏帧: {stats['corrupt_frames']}" ) def refresh_ports(self): """刷新可用串口列表""" self.port_combo.clear() ports = serial.tools.list_ports.comports() if ports: for port in ports: # 格式化串口信息:端口 - 设备名称 (设备ID) port_info = f"{port.device} - {port.description}" if port.serial_number: port_info += f" ({port.serial_number})" self.port_combo.addItem(port_info, port.device) else: self.port_combo.addItem("未找到可用串口") def toggle_connection(self): """切换串口连接状态""" if not self.connect_btn.isChecked(): # 断开连接 if self.serial_recorder: self.serial_recorder.stop() self.serial_recorder.disconnect() self.statusBar().showMessage("已断开连接") self.record_btn.setEnabled(False) self.cmd_btn_on.setEnabled(False) self.cmd_btn_off.setEnabled(False) self.data_cmd_btn_on.setEnabled(False) self.data_cmd_btn_off.setEnabled(False) self.connect_btn.setText("连接") # 更新按钮文字为"连接" else: # 建立连接 port = self.port_combo.currentData() if not port: QMessageBox.warning(self, "警告", "请选择串口") self.connect_btn.setChecked(False) return try: baudrate = int(self.baud_input.text()) num_channels = int(self.channel_input.text().strip()) if not 1 <= num_channels <= 32: raise ValueError("通道数必须在1-32之间") except ValueError as e: QMessageBox.warning(self, "警告", str(e)) self.connect_btn.setChecked(False) return # 创建串口记录器实例 self.serial_recorder = SerialRecorder( output_dir=self.records_save_path, num_channels=num_channels ) # 设置串口参数 self.serial_recorder.set_port(port, baudrate) # 测试连接 if self.serial_recorder.test_connection(): self.statusBar().showMessage("已连接") self.record_btn.setEnabled(True) self.cmd_btn_on.setEnabled(True) self.cmd_btn_off.setEnabled(True) self.data_cmd_btn_on.setEnabled(True) self.data_cmd_btn_off.setEnabled(True) self.connect_btn.setText("断开") # 更新按钮文字为"断开" else: self.connect_btn.setChecked(False) QMessageBox.warning(self, "错误", "连接失败") def toggle_recording(self): """切换录制状态""" if not self.record_btn.isChecked(): # 停止录制 if self.serial_recorder: self.serial_recorder.stop() self.is_recording = False self.record_btn.setText("开始录制") self.statusBar().showMessage("录制已停止") else: # 开始录制 if self.serial_recorder: if self.serial_recorder.start(): self.is_recording = True self.record_btn.setText("停止录制") self.statusBar().showMessage("正在录制") else: self.record_btn.setChecked(False) QMessageBox.warning(self, "错误", "启动录制失败") def add_tag(self): """添加标签""" tag = self.tag_input.text().strip() if tag: # TODO: 实现标签添加逻辑 self.tag_input.clear() self.statusBar().showMessage(f"已添加标签: {tag}") def apply_channel_settings(self): """应用通道数设置""" try: num_channels = int(self.channel_input.text().strip()) if not 1 <= num_channels <= 32: raise ValueError("通道数必须在1-32之间") if self.serial_recorder: self.serial_recorder.set_channel_nums(num_channels) self.statusBar().showMessage(f"已设置通道数为: {num_channels}") else: self.statusBar().showMessage("请先连接串口") except ValueError as e: QMessageBox.warning(self, "警告", str(e)) self.statusBar().showMessage("设置通道数失败") except Exception as e: QMessageBox.warning(self, "错误", f"设置通道数失败: {str(e)}") self.statusBar().showMessage("设置通道数失败") def closeEvent(self, event): """关闭事件处理""" if not self.exit_confirmed: reply = QMessageBox.question( self, '确认退出', '确定要退出程序吗?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.exit_confirmed = True self.cleanup_resources() event.accept() else: event.ignore() else: self.cleanup_resources() event.accept() def cleanup_resources(self): """清理资源""" if self.serial_recorder: if self.is_recording: self.serial_recorder.stop() # 确保停止录制并保存数据 self.serial_recorder.disconnect() self.stats_timer.stop() if __name__ == '__main__': app = QApplication(sys.argv) window = SerialUI() window.show() sys.exit(app.exec())