347 lines
14 KiB
Python
347 lines
14 KiB
Python
import sys
|
|
import serial
|
|
import serial.tools.list_ports
|
|
import threading
|
|
import time
|
|
import random
|
|
from collections import deque
|
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QComboBox, QLabel, QFileDialog, QProgressBar, QGridLayout, QGroupBox, QCheckBox, QLineEdit
|
|
from PyQt5.QtCore import QTimer, Qt
|
|
import pyqtgraph as pg
|
|
import pandas as pd
|
|
from datetime import datetime
|
|
import pyqtgraph.exporters
|
|
|
|
class VoltageReaderApp(QMainWindow):
|
|
NUM_CHANNELS = 2 # 通道数量
|
|
PACKET_SIZE = 7 # 数据包大小
|
|
PACKET_HEADER = [0xFE, 0xEE] # 数据包头
|
|
PACKET_FOOTER = 0xAA # 数据包尾
|
|
BAUD_RATES = ['9600', '19200', '38400', '57600', '115200'] # 波特率选项
|
|
PLOT_UPDATE_INTERVAL = 100 # 图表更新间隔(毫秒)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.initUI()
|
|
self.serial_port = None
|
|
self.data = [deque() for _ in range(self.NUM_CHANNELS)]
|
|
self.raw_data = [deque() for _ in range(self.NUM_CHANNELS)]
|
|
self.timestamps = deque()
|
|
self.reading_event = threading.Event()
|
|
self.start_time = None
|
|
self.buffer = bytearray()
|
|
self.timer = QTimer()
|
|
self.timer.timeout.connect(self.update_plot)
|
|
self.auto_follow = False
|
|
self.show_raw = False
|
|
self.test_mode = False # 添加测试模式标志
|
|
|
|
def initUI(self):
|
|
self.setWindowTitle("Voltage Monitor")
|
|
self.setGeometry(100, 100, 1600, 700)
|
|
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
main_layout = QHBoxLayout(central_widget)
|
|
|
|
control_layout = QVBoxLayout()
|
|
control_widget = QWidget()
|
|
control_widget.setLayout(control_layout)
|
|
control_widget.setFixedWidth(250)
|
|
main_layout.addWidget(control_widget)
|
|
|
|
self.setup_port_group(control_layout)
|
|
self.setup_action_group(control_layout)
|
|
self.setup_channel_group(control_layout)
|
|
self.setup_plot_widget(main_layout)
|
|
|
|
self.update_ports()
|
|
|
|
def setup_port_group(self, layout):
|
|
port_group = QGroupBox("串口设置")
|
|
port_layout = QVBoxLayout()
|
|
port_group.setLayout(port_layout)
|
|
layout.addWidget(port_group)
|
|
|
|
self.port_label = QLabel('选择串口:')
|
|
port_layout.addWidget(self.port_label)
|
|
|
|
self.port_combo = QComboBox()
|
|
port_layout.addWidget(self.port_combo)
|
|
|
|
self.refresh_button = QPushButton('刷新串口')
|
|
self.refresh_button.clicked.connect(self.update_ports)
|
|
port_layout.addWidget(self.refresh_button)
|
|
|
|
self.baud_label = QLabel('选择波特率:')
|
|
port_layout.addWidget(self.baud_label)
|
|
|
|
self.baud_combo = QComboBox()
|
|
self.baud_combo.addItems(self.BAUD_RATES)
|
|
self.baud_combo.setCurrentText('115200')
|
|
port_layout.addWidget(self.baud_combo)
|
|
|
|
self.connect_button = QPushButton('连接')
|
|
self.connect_button.clicked.connect(self.connect_serial)
|
|
port_layout.addWidget(self.connect_button)
|
|
|
|
def setup_action_group(self, layout):
|
|
action_group = QGroupBox("操作")
|
|
action_layout = QVBoxLayout()
|
|
action_group.setLayout(action_layout)
|
|
layout.addWidget(action_group)
|
|
|
|
self.start_button = QPushButton('开始读取')
|
|
self.start_button.clicked.connect(self.start_reading)
|
|
self.start_button.setEnabled(False)
|
|
action_layout.addWidget(self.start_button)
|
|
|
|
self.stop_button = QPushButton('停止读取')
|
|
self.stop_button.clicked.connect(self.stop_reading)
|
|
self.stop_button.setEnabled(False)
|
|
action_layout.addWidget(self.stop_button)
|
|
|
|
self.auto_button = QPushButton('自动跟随')
|
|
self.auto_button.setCheckable(True)
|
|
self.auto_button.clicked.connect(self.toggle_auto_follow)
|
|
action_layout.addWidget(self.auto_button)
|
|
|
|
self.show_raw_button = QPushButton('显示原始值')
|
|
self.show_raw_button.setCheckable(True)
|
|
self.show_raw_button.clicked.connect(self.toggle_show_raw)
|
|
action_layout.addWidget(self.show_raw_button)
|
|
|
|
self.save_button = QPushButton('保存数据')
|
|
self.save_button.clicked.connect(self.save_data)
|
|
action_layout.addWidget(self.save_button)
|
|
|
|
self.load_button = QPushButton('加载数据')
|
|
self.load_button.clicked.connect(self.load_data)
|
|
action_layout.addWidget(self.load_button)
|
|
|
|
self.test_button = QPushButton('测试模式')
|
|
self.test_button.setCheckable(True)
|
|
self.test_button.clicked.connect(self.toggle_test_mode)
|
|
action_layout.addWidget(self.test_button)
|
|
|
|
self.remark_label = QLabel('备注(学生姓名):')
|
|
action_layout.addWidget(self.remark_label)
|
|
|
|
self.remark_input = QLineEdit()
|
|
action_layout.addWidget(self.remark_input)
|
|
|
|
self.message_label = QLabel()
|
|
self.message_label.setWordWrap(True)
|
|
layout.addWidget(self.message_label)
|
|
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setAlignment(Qt.AlignCenter)
|
|
self.progress_bar.setVisible(False)
|
|
layout.addWidget(self.progress_bar)
|
|
|
|
def setup_channel_group(self, layout):
|
|
channel_group = QGroupBox("通道选择")
|
|
channel_layout = QVBoxLayout()
|
|
channel_group.setLayout(channel_layout)
|
|
layout.addWidget(channel_group)
|
|
|
|
self.checkboxes = []
|
|
for i in range(self.NUM_CHANNELS):
|
|
color = (i*12, 255-i*12, 150)
|
|
checkbox = QCheckBox(f'adc{i+1}')
|
|
checkbox.setChecked(True)
|
|
checkbox.setStyleSheet(f'color: black; background-color: rgb({color[0]}, {color[1]}, {color[2]})')
|
|
checkbox.stateChanged.connect(lambda state, idx=i: self.toggle_curve_visibility(state, idx))
|
|
channel_layout.addWidget(checkbox)
|
|
self.checkboxes.append(checkbox)
|
|
|
|
def setup_plot_widget(self, layout):
|
|
self.plot_widget = pg.PlotWidget()
|
|
self.plot_widget.showGrid(x=True, y=True, alpha=0.3)
|
|
layout.addWidget(self.plot_widget)
|
|
|
|
self.plot_data = [self.plot_widget.plot([], [], pen=pg.mkPen(color=(i*12, 255-i*12, 150))) for i in range(self.NUM_CHANNELS)]
|
|
|
|
def update_ports(self):
|
|
self.port_combo.clear()
|
|
ports = [port.device for port in serial.tools.list_ports.comports()]
|
|
self.port_combo.addItems(ports)
|
|
self.message_label.setText("串口列表已更新")
|
|
|
|
def connect_serial(self):
|
|
port = self.port_combo.currentText()
|
|
baudrate = self.baud_combo.currentText()
|
|
if port and baudrate:
|
|
try:
|
|
self.serial_port = serial.Serial(port, int(baudrate), timeout=1)
|
|
self.start_button.setEnabled(True)
|
|
self.stop_button.setEnabled(False)
|
|
self.message_label.setText(f"已连接到 {port},波特率 {baudrate}")
|
|
except serial.SerialException as e:
|
|
self.message_label.setText(f"连接失败: {e}")
|
|
|
|
def start_reading(self):
|
|
if self.serial_port or self.test_mode:
|
|
self.reading_event.set()
|
|
self.start_button.setEnabled(False)
|
|
self.stop_button.setEnabled(True)
|
|
if not self.start_time:
|
|
self.start_time = time.time()
|
|
threading.Thread(target=self.read_data, daemon=True).start()
|
|
self.timer.start(self.PLOT_UPDATE_INTERVAL)
|
|
self.message_label.setText("开始读取数据")
|
|
else:
|
|
self.message_label.setText("错误: 串口未打开,无法读取数据")
|
|
|
|
def stop_reading(self):
|
|
self.reading_event.clear()
|
|
self.start_button.setEnabled(True)
|
|
self.stop_button.setEnabled(False)
|
|
self.timer.stop()
|
|
self.message_label.setText("停止读取数据")
|
|
|
|
def read_data(self):
|
|
if self.test_mode:
|
|
self.generate_random_waveform()
|
|
else:
|
|
while self.reading_event.is_set() and self.serial_port:
|
|
try:
|
|
while self.serial_port.in_waiting > 0:
|
|
self.buffer.extend(self.serial_port.read(self.serial_port.in_waiting))
|
|
while len(self.buffer) >= self.PACKET_SIZE:
|
|
if self.buffer[:2] == bytearray(self.PACKET_HEADER) and self.buffer[self.PACKET_SIZE-1] == self.PACKET_FOOTER:
|
|
packet = self.buffer[:self.PACKET_SIZE]
|
|
self.buffer = self.buffer[self.PACKET_SIZE:]
|
|
raw_values = [packet[2 + i*2] | (packet[3 + i*2] << 8) for i in range(self.NUM_CHANNELS)]
|
|
voltages = [(raw / 4096.0) * 3.3 for raw in raw_values]
|
|
for i in range(self.NUM_CHANNELS):
|
|
self.raw_data[i].append(raw_values[i])
|
|
self.data[i].append(voltages[i])
|
|
if self.start_time is not None:
|
|
elapsed_time = time.time() - self.start_time
|
|
self.timestamps.append(elapsed_time)
|
|
else:
|
|
self.buffer.pop(0)
|
|
time.sleep(0.01)
|
|
except serial.SerialException as e:
|
|
self.message_label.setText(f"读取数据失败: {e}")
|
|
self.stop_reading()
|
|
except Exception as e:
|
|
self.message_label.setText(f"未知错误: {e}")
|
|
self.stop_reading()
|
|
|
|
def generate_random_waveform(self):
|
|
while self.reading_event.is_set():
|
|
elapsed_time = time.time() - self.start_time
|
|
self.timestamps.append(elapsed_time)
|
|
for i in range(self.NUM_CHANNELS):
|
|
raw_value = random.randint(0, 4095)
|
|
voltage = (raw_value / 4096.0) * 3.3
|
|
self.raw_data[i].append(raw_value)
|
|
self.data[i].append(voltage)
|
|
time.sleep(0.1)
|
|
|
|
def update_plot(self):
|
|
if self.timestamps:
|
|
for i in range(self.NUM_CHANNELS):
|
|
if self.checkboxes[i].isChecked():
|
|
if self.show_raw:
|
|
self.plot_data[i].setData(list(self.timestamps), list(self.raw_data[i]), clear=True)
|
|
else:
|
|
self.plot_data[i].setData(list(self.timestamps), list(self.data[i]), clear=True)
|
|
else:
|
|
self.plot_data[i].clear()
|
|
if self.auto_follow:
|
|
self.plot_widget.setXRange(self.timestamps[-1] - 5, self.timestamps[-1], padding=0)
|
|
|
|
def toggle_curve_visibility(self, state, index):
|
|
if state == 0:
|
|
self.plot_data[index].clear()
|
|
else:
|
|
if self.show_raw:
|
|
self.plot_data[index].setData(list(self.timestamps), list(self.raw_data[index]))
|
|
else:
|
|
self.plot_data[index].setData(list(self.timestamps), list(self.data[index]))
|
|
|
|
def toggle_auto_follow(self):
|
|
self.auto_follow = not self.auto_follow
|
|
self.auto_button.setText('取消自动跟随' if self.auto_follow else '自动跟随')
|
|
|
|
def toggle_show_raw(self):
|
|
self.show_raw = not self.show_raw
|
|
self.show_raw_button.setText('显示计算值' if self.show_raw else '显示原始值')
|
|
self.update_plot()
|
|
|
|
def toggle_test_mode(self):
|
|
self.test_mode = not self.test_mode
|
|
self.test_button.setText('退出测试模式' if self.test_mode else '测试模式')
|
|
if self.test_mode:
|
|
self.start_reading()
|
|
else:
|
|
self.stop_reading()
|
|
|
|
def save_data(self):
|
|
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
default_file_name = f"data_{current_time}.xlsx"
|
|
|
|
options = QFileDialog.Options()
|
|
options |= QFileDialog.DontUseNativeDialog
|
|
file_name, _ = QFileDialog.getSaveFileName(self, "保存数据文件", default_file_name, "Excel Files (*.xlsx);;All Files (*)", options=options)
|
|
|
|
if file_name:
|
|
if not file_name.endswith('.xlsx'):
|
|
file_name += '.xlsx'
|
|
|
|
student_name = self.remark_input.text()
|
|
|
|
data_dict = {"Timestamp": list(self.timestamps)}
|
|
for i in range(self.NUM_CHANNELS):
|
|
data_dict[f"Voltage_{i+1}"] = list(self.data[i])
|
|
data_dict[f"Raw_{i+1}"] = list(self.raw_data[i])
|
|
df = pd.DataFrame(data_dict)
|
|
|
|
self.progress_bar.setVisible(True)
|
|
self.progress_bar.setValue(0)
|
|
QApplication.processEvents()
|
|
|
|
try:
|
|
with pd.ExcelWriter(file_name, engine='openpyxl') as writer:
|
|
df.to_excel(writer, index=False, startrow=1)
|
|
worksheet = writer.sheets['Sheet1']
|
|
worksheet.merge_cells('A1:E1')
|
|
worksheet['A1'] = f"备注: {student_name}"
|
|
|
|
self.progress_bar.setValue(50)
|
|
QApplication.processEvents()
|
|
|
|
image_filename = file_name.replace('.xlsx', '.png')
|
|
exporter = pg.exporters.ImageExporter(self.plot_widget.plotItem)
|
|
exporter.parameters()['width'] = 1600
|
|
exporter.export(image_filename)
|
|
|
|
self.progress_bar.setValue(100)
|
|
QApplication.processEvents()
|
|
|
|
self.message_label.setText(f"数据已保存为 {file_name} 和 {image_filename}")
|
|
except Exception as e:
|
|
self.message_label.setText(f"保存数据失败: {e}")
|
|
finally:
|
|
self.progress_bar.setVisible(False)
|
|
|
|
def load_data(self):
|
|
options = QFileDialog.Options()
|
|
file_name, _ = QFileDialog.getOpenFileName(self, "加载数据文件", "", "Excel Files (*.xlsx);;All Files (*)", options=options)
|
|
if file_name:
|
|
df = pd.read_excel(file_name)
|
|
self.timestamps = deque(df["Timestamp"].tolist())
|
|
for i in range(self.NUM_CHANNELS):
|
|
self.data[i] = deque(df[f"Voltage_{i+1}"].tolist())
|
|
self.raw_data[i] = deque(df[f"Raw_{i+1}"].tolist())
|
|
self.update_plot()
|
|
self.message_label.setText(f"数据已加载自 {file_name}")
|
|
|
|
if __name__ == '__main__':
|
|
app = QApplication(sys.argv)
|
|
window = VoltageReaderApp()
|
|
window.show()
|
|
sys.exit(app.exec_()) |