561 lines
21 KiB
Python
561 lines
21 KiB
Python
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())
|