import pyfirmata
import time
import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QGroupBox,
QComboBox, QStatusBar, QGridLayout, QTabWidget,
QTableWidget, QTableWidgetItem, QHeaderView, QDoubleSpinBox,
QProgressBar, QMessageBox, QCheckBox, QSizePolicy)
from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFont, QColor, QIcon
from PyQt5.QtCore import QSize
# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Zen Hei'] # 中文字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
class ConnectionThread(QThread):
progress = pyqtSignal(int)
success = pyqtSignal(object)
error = pyqtSignal(str)
def __init__(self, port):
super().__init__()
self.port = port
def run(self):
try:
# 模拟连接进度
for i in range(1, 101):
time.sleep(0.03) # 更快的进度更新
self.progress.emit(i)
if i == 50:
# 在实际连接位置暂停
try:
self.board = pyfirmata.Arduino(self.port)
time.sleep(1.5) # 等待连接稳定
except Exception as e:
self.error.emit(str(e))
return
# 初始化引脚模式
digital_pins = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
for pin in digital_pins:
self.board.digital[pin].mode = pyfirmata.OUTPUT
self.board.digital[pin].write(0) # 初始化为低电平
# 设置模拟输入
self.it = pyfirmata.util.Iterator(self.board)
self.it.start()
# 初始化所有模拟引脚
for i in range(6):
self.board.analog[i].enable_reporting()
self.success.emit(self.board)
except Exception as e:
self.error.emit(f"连接失败: {str(e)}")
class ArduinoControlApp(QMainWindow):
def __init__(self):
super().__init__()
self.board = None
self.it = None
self.is_connected = False
self.led_state = False
self.voltage_data = []
self.time_data = []
self.start_time = time.time()
self.max_data_points = 500
self.analog_pin = 0 # 默认使用 A0 引脚读取电压
self.digital_pin_states = {}
self.analog_pin_values = {}
# 数字引脚监控相关变量
self.digital_monitor_pin = 2 # 默认监控D2引脚
self.digital_state_data = [] # 存储数字引脚状态数据
self.digital_time_data = [] # 存储数字引脚状态变化时间
self.digital_monitor_start_time = time.time()
self.last_digital_state = None
self.rise_count = 0
self.fall_count = 0
self.init_ui()
self.init_plot()
# 确保窗口大小适合屏幕
self.adjustSize()
self.center_window()
def center_window(self):
"""将窗口置于屏幕中央"""
screen = QApplication.primaryScreen().geometry()
window_size = self.geometry()
self.move(
(screen.width() - window_size.width()) // 2,
(screen.height() - window_size.height()) // 2
)
def init_ui(self):
self.setWindowTitle("Arduino 控制器 v2.2")
self.setMinimumSize(1200, 800) # 设置最小尺寸
self.setWindowIcon(QIcon("cpu.png")) # 如果有图标文件
# 设置全局字体
app_font = QFont("Microsoft YaHei", 9)
QApplication.setFont(app_font)
# 主布局
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout(main_widget)
main_layout.setSpacing(10)
main_layout.setContentsMargins(10, 10, 10, 10)
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("准备就绪")
# 状态指示灯
self.status_indicator = QLabel()
self.status_indicator.setFixedSize(16, 16)
self.status_indicator.setStyleSheet("background-color: gray; border-radius: 8px;")
self.status_bar.addPermanentWidget(self.status_indicator)
# 当前时间显示
self.time_label = QLabel()
self.time_label.setStyleSheet("color: #616161; font-weight: bold; padding: 0 10px;")
self.status_bar.addPermanentWidget(self.time_label)
self.update_time() # 初始更新时间显示
# 时间更新定时器
self.time_timer = QTimer()
self.time_timer.timeout.connect(self.update_time)
self.time_timer.start(1000) # 每秒更新一次
# 进度条(初始时隐藏)
self.progress_bar = QProgressBar()
self.progress_bar.setFixedWidth(200)
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setVisible(False)
self.status_bar.addPermanentWidget(self.progress_bar)
# 连接状态标签
self.status_label = QLabel("未连接")
self.status_label.setStyleSheet("color: #9E9E9E; font-weight: bold; padding: 0 10px;")
self.status_bar.addPermanentWidget(self.status_label)
# 创建选项卡
self.tab_widget = QTabWidget()
main_layout.addWidget(self.tab_widget, 1)
# 控制选项卡
control_tab = QWidget()
self.tab_widget.addTab(control_tab, "控制面板")
control_layout = QVBoxLayout(control_tab)
control_layout.setSpacing(10)
control_layout.setContentsMargins(10, 10, 10, 10)
# 连接控制组 - 使用卡片式设计
conn_group = QGroupBox("Arduino 连接设置")
conn_group.setStyleSheet("QGroupBox { font-weight: bold; }")
conn_layout = QGridLayout(conn_group)
conn_layout.setSpacing(8)
conn_layout.setContentsMargins(10, 15, 10, 15)
conn_layout.addWidget(QLabel("<b>端口:</b>"), 0, 0)
self.port_combo = QComboBox()
self.port_combo.addItems(["COM3", "COM4", "COM5", "COM6", "COM7", "/dev/ttyACM0", "/dev/ttyUSB0"])
self.port_combo.setCurrentText("COM5")
self.port_combo.setMinimumWidth(120)
conn_layout.addWidget(self.port_combo, 0, 1)
self.refresh_btn = QPushButton("刷新端口")
self.refresh_btn.setToolTip("重新扫描可用的串口")
self.refresh_btn.setFixedHeight(30)
self.refresh_btn.setStyleSheet("background-color: #2196F3; color: white; border-radius: 4px;")
self.refresh_btn.clicked.connect(self.refresh_ports)
conn_layout.addWidget(self.refresh_btn, 0, 2)
self.connect_btn = QPushButton("连接 Arduino")
self.connect_btn.setToolTip("连接到选定的Arduino设备")
self.connect_btn.setFixedHeight(30)
self.connect_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
font-weight: bold;
padding: 6px;
border-radius: 4px;
}
QPushButton:disabled {
background-color: #A5D6A7;
}
""")
self.connect_btn.clicked.connect(self.connect_arduino)
conn_layout.addWidget(self.connect_btn, 0, 3)
self.disconnect_btn = QPushButton("断开连接")
self.disconnect_btn.setToolTip("断开当前Arduino连接")
self.disconnect_btn.setFixedHeight(30)
self.disconnect_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
padding: 6px;
border-radius: 4px;
}
QPushButton:disabled {
background-color: #EF9A9A;
}
""")
self.disconnect_btn.clicked.connect(self.disconnect_arduino)
self.disconnect_btn.setEnabled(False)
conn_layout.addWidget(self.disconnect_btn, 0, 4)
# 扩展信息区域
self.conn_info_label = QLabel("<b>连接信息:</b> 请连接Arduino设备")
self.conn_info_label.setStyleSheet("color: #616161; padding: 5px 0;")
conn_layout.addWidget(self.conn_info_label, 1, 0, 1, 5)
# 引脚控制组
pin_group = QGroupBox("引脚控制")
pin_group.setStyleSheet("QGroupBox { font-weight: bold; }")
pin_layout = QGridLayout(pin_group)
pin_layout.setSpacing(6)
pin_layout.setContentsMargins(8, 12, 8, 12)
# 数字引脚控制表
pin_layout.addWidget(QLabel("<b style='font-size: 10.5pt; color: #1565C0;'>数字引脚控制</b>"), 0, 0, 1, 6)
# 表头
headers = ["引脚", "名称", "模式", "当前状态", "操作"]
for col, header in enumerate(headers):
label = QLabel(f"<b>{header}</b>")
label.setStyleSheet("color: #263238;")
pin_layout.addWidget(label, 1, col)
# 数字引脚列表 (简化到常用引脚)
self.digital_pins = [
{'pin': 2, 'name': '数字端口2', 'mode': '输出', 'color': '#E3F2FD'},
{'pin': 3, 'name': '数字端口3', 'mode': '输出', 'color': '#E3F2FD'},
{'pin': 4, 'name': '数字端口4', 'mode': '输出', 'color': '#E3F2FD'},
{'pin': 5, 'name': '数字端口5', 'mode': '输出', 'color': '#E3F2FD'},
{'pin': 6, 'name': 'PWM端口6', 'mode': '输出', 'color': '#FFF3E0'},
{'pin': 7, 'name': '数字端口7', 'mode': '输出', 'color': '#E3F2FD'},
{'pin': 8, 'name': '数字端口8', 'mode': '输出', 'color': '#E3F2FD'},
{'pin': 9, 'name': 'PWM端口9', 'mode': '输出', 'color': '#FFF3E0'},
{'pin': 10, 'name': 'PWM端口10', 'mode': '输出', 'color': '#FFF3E0'},
{'pin': 11, 'name': 'PWM端口11', 'mode': '输出', 'color': '#FFF3E0'},
{'pin': 12, 'name': '数字端口12', 'mode': '输出', 'color': '#E3F2FD'},
{'pin': 13, 'name': '板载LED', 'mode': '输出', 'color': '#FFEBEE'}
]
# 创建引脚控制行
self.pin_controls = []
for i, pin_info in enumerate(self.digital_pins):
row = i + 2
pin_num = pin_info['pin']
# 设置行样式
widget = QWidget()
widget.setStyleSheet(f"background-color: {pin_info['color']};")
# 引脚标签
pin_label = QLabel(f"<b>D{pin_num}</b>")
pin_label.setAlignment(Qt.AlignCenter)
pin_layout.addWidget(pin_label, row, 0)
# 名称标签
name_label = QLabel(pin_info['name'])
name_label.setAlignment(Qt.AlignCenter)
pin_layout.addWidget(name_label, row, 1)
# 模式标签
mode_label = QLabel(pin_info['mode'])
mode_label.setAlignment(Qt.AlignCenter)
pin_layout.addWidget(mode_label, row, 2)
# 状态标签
state_label = QLabel("低电平")
state_label.setAlignment(Qt.AlignCenter)
state_label.setStyleSheet("color: #d32f2f; font-weight: bold;")
pin_layout.addWidget(state_label, row, 3)
self.digital_pin_states[pin_num] = state_label
# 操作按钮
btn_layout = QHBoxLayout()
btn_layout.setAlignment(Qt.AlignCenter)
btn_layout.setSpacing(4)
high_btn = QPushButton("高电平")
high_btn.setFixedSize(70, 26)
high_btn.setStyleSheet("background-color: #388E3C; color: white; border-radius: 3px;")
high_btn.clicked.connect(lambda checked, p=pin_num: self.set_pin_high(p))
low_btn = QPushButton("低电平")
low_btn.setFixedSize(70, 26)
low_btn.setStyleSheet("background-color: #D32F2F; color: white; border-radius: 3px;")
low_btn.clicked.connect(lambda checked, p=pin_num: self.set_pin_low(p))
toggle_btn = QPushButton("切换")
toggle_btn.setFixedSize(60, 26)
toggle_btn.setStyleSheet("background-color: #1976D2; color: white; border-radius: 3px;")
toggle_btn.clicked.connect(lambda checked, p=pin_num: self.toggle_pin(p))
btn_layout.addWidget(high_btn)
btn_layout.addWidget(low_btn)
btn_layout.addWidget(toggle_btn)
# 将按钮布局添加到单元格
container = QWidget()
container.setLayout(btn_layout)
pin_layout.addWidget(container, row, 4)
# 状态指示器
status_indicator = QLabel()
status_indicator.setFixedSize(16, 16)
status_indicator.setStyleSheet("background-color: #d32f2f; border-radius: 8px;")
pin_layout.addWidget(status_indicator, row, 5)
self.digital_pin_states[f"{pin_num}_indicator"] = status_indicator
# 模拟引脚监控
pin_layout.addWidget(QLabel("<b style='font-size: 10.5pt; color: #1565C0;'>模拟引脚监控</b>"),
len(self.digital_pins) + 2, 0, 1, 6)
# 模拟引脚表头
analog_headers = ["引脚", "名称", "电压值"]
for col, header in enumerate(analog_headers):
label = QLabel(f"<b>{header}</b>")
label.setStyleSheet("color: #263238;")
pin_layout.addWidget(label, len(self.digital_pins) + 3, col)
# 模拟引脚列表
self.analog_pins = [
{'pin': 0, 'name': '模拟输入A0', 'color': '#F1F8E9'},
{'pin': 1, 'name': '模拟输入A1', 'color': '#F1F8E9'},
{'pin': 2, 'name': '模拟输入A2', 'color': '#F1F8E9'},
{'pin': 3, 'name': '模拟输入A3', 'color': '#F1F8E9'},
{'pin': 4, 'name': 'I2C SDA', 'color': '#E0F7FA'},
{'pin': 5, 'name': 'I2C SCL', 'color': '#E0F7FA'}
]
# 创建模拟引脚监控行
self.analog_value_labels = {}
for i, pin_info in enumerate(self.analog_pins):
row = len(self.digital_pins) + 4 + i
pin_num = pin_info['pin']
# 设置行样式
widget = QWidget()
widget.setStyleSheet(f"background-color: {pin_info['color']};")
# 引脚标签
pin_label = QLabel(f"<b>A{pin_num}</b>")
pin_label.setAlignment(Qt.AlignCenter)
pin_layout.addWidget(pin_label, row, 0)
# 名称标签
name_label = QLabel(pin_info['name'])
name_label.setAlignment(Qt.AlignCenter)
pin_layout.addWidget(name_label, row, 1)
# 电压值标签
value_label = QLabel("0.00 V")
value_label.setAlignment(Qt.AlignCenter)
value_label.setStyleSheet("color: #1976D2; font-weight: bold;")
pin_layout.addWidget(value_label, row, 2)
self.analog_pin_values[pin_num] = value_label
# 控制按钮组
button_group = QGroupBox("快速控制")
button_layout = QHBoxLayout(button_group)
self.led_btn = QPushButton("启动 LED 闪烁")
self.led_btn.setToolTip("启动或停止板载LED闪烁")
self.led_btn.setFixedHeight(36)
self.led_btn.setStyleSheet("""
QPushButton {
background-color: #FF9800;
color: white;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:disabled {
background-color: #FFCC80;
}
""")
self.led_btn.clicked.connect(self.toggle_led_blink)
self.led_btn.setEnabled(False)
self.reset_btn = QPushButton("重置所有引脚")
self.reset_btn.setToolTip("将所有引脚重置为低电平")
self.reset_btn.setFixedHeight(36)
self.reset_btn.setStyleSheet("""
QPushButton {
background-color: #607D8B;
color: white;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:disabled {
background-color: #B0BEC5;
}
""")
self.reset_btn.clicked.connect(self.reset_all_pins)
self.reset_btn.setEnabled(False)
button_layout.addWidget(self.led_btn, 1)
button_layout.addWidget(self.reset_btn, 1)
button_layout.addStretch(2)
# 添加到控制选项卡
control_layout.addWidget(conn_group)
control_layout.addWidget(pin_group, 1) # 添加伸缩因子
control_layout.addWidget(button_group)
# 监控选项卡
monitor_tab = QWidget()
self.tab_widget.addTab(monitor_tab, "数据监控")
monitor_layout = QVBoxLayout(monitor_tab)
monitor_layout.setSpacing(10)
monitor_layout.setContentsMargins(10, 10, 10, 10)
# 模拟电压监控部分
sim_group = QGroupBox("模拟电压监控")
sim_layout = QVBoxLayout(sim_group)
# 绘图区域
plot_group = QGroupBox("电压波形监控")
plot_layout = QVBoxLayout(plot_group)
plot_layout.setSpacing(8)
self.figure = Figure(figsize=(10, 4), dpi=100)
self.canvas = FigureCanvas(self.figure)
self.canvas.setMinimumHeight(300)
plot_layout.addWidget(self.canvas, 1)
# 绘图控制
plot_controls = QHBoxLayout()
plot_controls.setSpacing(8)
self.clear_plot_btn = QPushButton("清除图表")
self.clear_plot_btn.setToolTip("清除当前图表数据")
self.clear_plot_btn.setFixedHeight(30)
self.clear_plot_btn.setStyleSheet("background-color: #9C27B0; color: white; padding: 6px; border-radius: 4px;")
self.clear_plot_btn.clicked.connect(self.clear_plot)
self.pause_plot_btn = QPushButton("暂停监控")
self.pause_plot_btn.setToolTip("暂停或恢复监控")
self.pause_plot_btn.setFixedHeight(30)
self.pause_plot_btn.setStyleSheet("background-color: #E91E63; color: white; padding: 6px; border-radius: 4px;")
self.pause_plot_btn.clicked.connect(self.toggle_plot_monitoring)
self.save_data_btn = QPushButton("保存数据")
self.save_data_btn.setToolTip("将数据保存为CSV文件")
self.save_data_btn.setFixedHeight(30)
self.save_data_btn.setStyleSheet("background-color: #009688; color: white; padding: 6px; border-radius: 4px;")
self.save_data_btn.clicked.connect(self.save_voltage_data)
self.autoscale_check = QCheckBox("自动缩放")
self.autoscale_check.setToolTip("自动调整图表缩放")
self.autoscale_check.setFixedHeight(30)
self.autoscale_check.setChecked(True)
self.autoscale_check.stateChanged.connect(self.toggle_autoscale)
plot_controls.addWidget(self.clear_plot_btn)
plot_controls.addWidget(self.pause_plot_btn)
plot_controls.addWidget(self.save_data_btn)
plot_controls.addStretch(1)
plot_controls.addWidget(self.autoscale_check)
# 监控设置
monitor_settings = QHBoxLayout()
monitor_settings.setSpacing(8)
monitor_settings.addWidget(QLabel("监控引脚:"))
self.monitor_pin_combo = QComboBox()
self.monitor_pin_combo.setFixedHeight(28)
self.monitor_pin_combo.addItems(["A0", "A1", "A2", "A3", "A4", "A5"])
self.monitor_pin_combo.currentIndexChanged.connect(self.change_analog_pin)
monitor_settings.addWidget(self.monitor_pin_combo)
monitor_settings.addWidget(QLabel("采样间隔(ms):"))
self.sampling_interval = QDoubleSpinBox()
self.sampling_interval.setFixedHeight(28)
self.sampling_interval.setRange(10, 5000)
self.sampling_interval.setValue(100)
self.sampling_interval.setSuffix(" ms")
self.sampling_interval.valueChanged.connect(self.update_sampling_interval)
self.sampling_interval.setSingleStep(10)
monitor_settings.addWidget(self.sampling_interval)
monitor_settings.addStretch(1)
plot_layout.addLayout(plot_controls)
plot_layout.addLayout(monitor_settings)
# 电压统计
self.voltage_stats = QLabel("电压统计: 等待数据...")
self.voltage_stats.setStyleSheet("color: #1976D2; font-weight: bold; font-size: 12px; padding: 5px;")
plot_layout.addWidget(self.voltage_stats)
sim_layout.addWidget(plot_group, 1) # 添加伸缩因子
monitor_layout.addWidget(sim_group, 60) # 60%高度
# 数字引脚状态监控部分
digi_group = QGroupBox("数字引脚状态监控")
digi_layout = QVBoxLayout(digi_group)
# 数字引脚监控图表
digi_plot_group = QGroupBox("数字引脚状态变化")
digi_plot_layout = QVBoxLayout(digi_plot_group)
self.digital_figure = Figure(figsize=(10, 3), dpi=100)
self.digital_canvas = FigureCanvas(self.digital_figure)
self.digital_canvas.setMinimumHeight(250)
digi_plot_layout.addWidget(self.digital_canvas, 1)
# 数字引脚监控控制
digi_controls = QHBoxLayout()
digi_controls.setSpacing(8)
self.clear_digital_btn = QPushButton("清除图表")
self.clear_digital_btn.setFixedHeight(30)
self.clear_digital_btn.setStyleSheet(
"background-color: #9C27B0; color: white; padding: 6px; border-radius: 4px;")
self.clear_digital_btn.clicked.connect(self.clear_digital_plot)
self.save_digital_btn = QPushButton("保存数据")
self.save_digital_btn.setFixedHeight(30)
self.save_digital_btn.setStyleSheet(
"background-color: #009688; color: white; padding: 6px; border-radius: 4px;")
self.save_digital_btn.clicked.connect(self.save_digital_data)
self.highlight_check = QCheckBox("高亮变化点")
self.highlight_check.setToolTip("在状态变化点显示标记")
self.highlight_check.setFixedHeight(30)
self.highlight_check.setChecked(True)
self.highlight_check.stateChanged.connect(self.update_digital_plot)
digi_controls.addWidget(self.clear_digital_btn)
digi_controls.addWidget(self.save_digital_btn)
digi_controls.addStretch(1)
digi_controls.addWidget(self.highlight_check)
# 数字引脚监控设置
digi_settings = QHBoxLayout()
digi_settings.setSpacing(8)
digi_settings.addWidget(QLabel("监控引脚:"))
self.digital_monitor_combo = QComboBox()
self.digital_monitor_combo.setFixedHeight(28)
for pin_info in self.digital_pins:
pin_num = pin_info['pin']
self.digital_monitor_combo.addItem(f"D{pin_num} - {pin_info['name']}", pin_num)
self.digital_monitor_combo.currentIndexChanged.connect(self.change_digital_monitor_pin)
digi_settings.addWidget(self.digital_monitor_combo)
self.digital_stats = QLabel("状态统计: 等待数据...")
self.digital_stats.setStyleSheet("color: #1976D2; font-weight: bold; font-size: 12px; padding: 5px;")
digi_settings.addWidget(self.digital_stats, 1)
digi_settings.addStretch(1)
digi_plot_layout.addLayout(digi_controls)
digi_plot_layout.addLayout(digi_settings)
digi_layout.addWidget(digi_plot_group, 1) # 添加伸缩因子
monitor_layout.addWidget(digi_group, 40) # 40%高度
# LED 闪烁定时器
self.led_timer = QTimer()
self.led_timer.timeout.connect(self.blink_led)
# 电压读取定时器
self.voltage_timer = QTimer()
self.voltage_timer.timeout.connect(self.read_voltage)
self.voltage_timer.setInterval(100)
# 监控是否正在运行
self.plot_monitoring = True
def update_time(self):
"""更新状态栏中的当前时间"""
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
self.time_label.setText(f"当前时间: {current_time}")
def init_plot(self):
# 初始化模拟电压图表
self.ax = self.figure.add_subplot(111)
self.ax.set_title("实时电压波形", fontsize=12)
self.ax.set_xlabel("时间 (秒)", fontsize=10)
self.ax.set_ylabel("电压 (V)", fontsize=10)
self.ax.set_ylim(0, 5.1)
self.ax.grid(True, linestyle='--', alpha=0.7)
# 添加图例
self.line, = self.ax.plot([], [], 'b-', linewidth=1.5, label="电压值")
self.avg_line, = self.ax.plot([], [], 'r--', linewidth=1.2, label="平均电压")
self.ax.legend(loc='upper right')
# 初始化数字引脚状态图表
self.digital_ax = self.digital_figure.add_subplot(111)
self.digital_ax.set_title("数字引脚状态变化", fontsize=12)
self.digital_ax.set_xlabel("时间 (秒)", fontsize=10)
self.digital_ax.set_ylabel("状态", fontsize=10)
self.digital_ax.set_ylim(-0.1, 1.1)
self.digital_ax.set_yticks([0, 1])
self.digital_ax.set_yticklabels(['低电平', '高电平'])
self.digital_ax.grid(True, linestyle='--', alpha=0.7)
# 创建阶梯图和散点图
self.digital_line, = self.digital_ax.step([], [], 'g-', where='post', linewidth=1.5)
self.digital_scatter = self.digital_ax.scatter([], [], s=30, c='red', marker='o', zorder=10, label='状态变化点')
self.digital_ax.legend(loc='upper right')
self.canvas.draw()
self.digital_canvas.draw()
def update_plot(self):
if not self.voltage_data:
return
# 计算统计值
avg_voltage = np.mean(self.voltage_data) if self.voltage_data else 0
min_voltage = min(self.voltage_data) if self.voltage_data else 0
max_voltage = max(self.voltage_data) if self.voltage_data else 0
# 更新曲线数据
self.line.set_data(self.time_data, self.voltage_data)
# 更新平均线
if self.time_data:
avg_data = [avg_voltage] * len(self.time_data)
self.avg_line.set_data(self.time_data, avg_data)
# 自动调整坐标轴
if self.autoscale_check.isChecked():
if self.time_data:
max_time = max(self.time_data)
self.ax.set_xlim(0, max(10, max_time * 1.05))
if self.voltage_data:
min_v = min(self.voltage_data)
max_v = max(self.voltage_data)
y_margin = max(0.5, (max_v - min_v) * 0.2)
self.ax.set_ylim(max(0, min_v - y_margin), min(5.1, max_v + y_margin))
# 更新图表
self.canvas.draw()
# 更新电压统计
self.voltage_stats.setText(
f"电压统计: 当前 {self.voltage_data[-1]:.3f}V | 最小 {min_voltage:.3f}V | 最大 {max_voltage:.3f}V | 平均 {avg_voltage:.3f}V"
)
def update_digital_plot(self):
if not self.digital_time_data:
return
# 更新数字引脚状态图表
self.digital_line.set_data(self.digital_time_data, self.digital_state_data)
# 自动调整X轴范围
if self.digital_time_data:
max_time = max(self.digital_time_data)
self.digital_ax.set_xlim(0, max(10, max_time * 1.05))
# 仅当数据点超过100个时才限制显示范围
if len(self.digital_time_data) > 100:
max_time = self.digital_time_data[-1]
min_time = max(0, max_time - 30) # 显示最后30秒的数据
self.digital_ax.set_xlim(min_time, max_time)
else:
self.digital_ax.set_xlim(0, max(10, max_time))
# 高亮状态变化点
if self.highlight_check.isChecked():
# 查找所有状态变化点
change_points = []
change_times = []
for i in range(1, len(self.digital_state_data)):
if self.digital_state_data[i] != self.digital_state_data[i - 1]:
change_points.append(self.digital_state_data[i])
change_times.append(self.digital_time_data[i])
if change_times:
self.digital_scatter.set_offsets(np.column_stack([change_times, change_points]))
self.digital_scatter.set_visible(True)
else:
self.digital_scatter.set_visible(False)
else:
self.digital_scatter.set_visible(False)
# 更新图表
self.digital_canvas.draw()
# 更新状态统计
if self.digital_state_data:
last_state = self.digital_state_data[-1]
state_text = "高电平" if last_state == 1 else "低电平"
changes = len(self.digital_state_data)
self.digital_stats.setText(
f"当前状态: {state_text} | 状态变化: {changes}次 | 上升沿: {self.rise_count} | 下降沿: {self.fall_count}"
)
def refresh_ports(self):
# 模拟刷新端口列表
self.status_bar.showMessage("刷新可用端口...")
current_port = self.port_combo.currentText()
self.port_combo.clear()
self.port_combo.addItems(["COM3", "COM4", "COM5", "COM6", "COM7", "/dev/ttyACM0", "/dev/ttyUSB0"])
if current_port in [self.port_combo.itemText(i) for i in range(self.port_combo.count())]:
self.port_combo.setCurrentText(current_port)
else:
self.port_combo.setCurrentIndex(0)
self.status_bar.showMessage("端口列表已刷新")
def connect_arduino(self):
port = self.port_combo.currentText()
if not port:
QMessageBox.warning(self, "端口错误", "请选择有效的串口")
return
self.status_bar.showMessage(f"正在连接 Arduino ({port})...")
self.connect_btn.setEnabled(False)
self.disconnect_btn.setEnabled(False)
self.refresh_btn.setEnabled(False)
# 显示进度条
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_indicator.setStyleSheet("background-color: #FFC107; border-radius: 8px;")
self.status_label.setText("连接中...")
# 创建并启动连接线程
self.conn_thread = ConnectionThread(port)
self.conn_thread.progress.connect(self.progress_bar.setValue)
self.conn_thread.success.connect(self.handle_connection_success)
self.conn_thread.error.connect(self.handle_connection_error)
self.conn_thread.start()
def handle_connection_success(self, board):
self.board = board
self.it = self.conn_thread.it
self.is_connected = True
# 更新UI状态
self.progress_bar.setVisible(False)
self.status_indicator.setStyleSheet("background-color: #4CAF50; border-radius: 8px;")
self.status_label.setText("已连接")
self.conn_info_label.setText(f"<b>连接信息:</b> Arduino 已成功连接到 {self.port_combo.currentText()}")
self.status_bar.showMessage(f"成功连接到 Arduino ({self.port_combo.currentText()})")
# 更新按钮状态
self.connect_btn.setEnabled(False)
self.disconnect_btn.setEnabled(True)
self.refresh_btn.setEnabled(True)
self.led_btn.setEnabled(True)
self.reset_btn.setEnabled(True)
self.clear_plot_btn.setEnabled(True)
self.pause_plot_btn.setEnabled(True)
self.save_data_btn.setEnabled(True)
self.clear_digital_btn.setEnabled(True)
self.save_digital_btn.setEnabled(True)
# 启动电压读取
self.update_sampling_interval()
# 设置初始引脚状态
for pin_info in self.digital_pins:
pin = pin_info['pin']
self.digital_pin_states[pin].setText("低电平")
self.digital_pin_states[pin].setStyleSheet("color: #d32f2f; font-weight: bold;")
indicator = self.digital_pin_states.get(f"{pin}_indicator")
if indicator:
indicator.setStyleSheet("background-color: #d32f2f; border-radius: 8px;")
def handle_connection_error(self, message):
self.board = None
self.is_connected = False
# 更新UI状态
self.progress_bar.setVisible(False)
self.status_indicator.setStyleSheet("background-color: #f44336; border-radius: 8px;")
self.status_label.setText("连接失败")
self.conn_info_label.setText(f"<b>连接信息:</b> 连接失败: {message}")
QMessageBox.critical(self, "连接错误", f"连接失败:\n{message}")
self.status_bar.showMessage(f"连接失败: {message}")
# 更新按钮状态
self.connect_btn.setEnabled(True)
self.disconnect_btn.setEnabled(False)
self.refresh_btn.setEnabled(True)
self.led_btn.setEnabled(False)
self.reset_btn.setEnabled(False)
def disconnect_arduino(self):
if self.board:
# 停止所有定时器
self.led_timer.stop()
self.voltage_timer.stop()
# 重置所有引脚
self.reset_all_pins()
if hasattr(self, 'it'):
self.it.stop()
self.board.exit()
self.board = None
self.is_connected = False
self.progress_bar.setVisible(False)
self.status_indicator.setStyleSheet("background-color: #9E9E9E; border-radius: 8px;")
self.status_label.setText("未连接")
self.conn_info_label.setText("<b>连接信息:</b> Arduino 已断开连接")
self.status_bar.showMessage("已断开连接")
# 更新按钮状态
self.connect_btn.setEnabled(True)
self.disconnect_btn.setEnabled(False)
self.led_btn.setEnabled(False)
self.led_btn.setText("启动 LED 闪烁")
self.refresh_btn.setEnabled(True)
def set_pin_high(self, pin):
if not self.is_connected:
QMessageBox.warning(self, "未连接", "请先连接到Arduino")
return
try:
self.board.digital[pin].write(1)
self.digital_pin_states[pin].setText("高电平")
self.digital_pin_states[pin].setStyleSheet("color: #388E3C; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"{pin}_indicator")
if indicator:
indicator.setStyleSheet("background-color: #388E3C; border-radius: 8px;")
self.status_bar.showMessage(f"引脚 D{pin} 设置为高电平")
# 记录状态变化
if pin == self.digital_monitor_pin:
current_time = time.time() - self.digital_monitor_start_time
self.digital_state_data.append(1)
self.digital_time_data.append(current_time)
# 统计边缘变化
if self.last_digital_state == 0:
self.rise_count += 1
self.last_digital_state = 1
# 限制数据点数
if len(self.digital_time_data) > self.max_data_points:
self.digital_time_data.pop(0)
self.digital_state_data.pop(0)
self.update_digital_plot()
except Exception as e:
self.status_bar.showMessage(f"设置引脚失败: {str(e)}")
def set_pin_low(self, pin):
if not self.is_connected:
QMessageBox.warning(self, "未连接", "请先连接到Arduino")
return
try:
self.board.digital[pin].write(0)
self.digital_pin_states[pin].setText("低电平")
self.digital_pin_states[pin].setStyleSheet("color: #d32f2f; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"{pin}_indicator")
if indicator:
indicator.setStyleSheet("background-color: #d32f2f; border-radius: 8px;")
self.status_bar.showMessage(f"引脚 D{pin} 设置为低电平")
# 记录状态变化
if pin == self.digital_monitor_pin:
current_time = time.time() - self.digital_monitor_start_time
self.digital_state_data.append(0)
self.digital_time_data.append(current_time)
# 统计边缘变化
if self.last_digital_state == 1:
self.fall_count += 1
self.last_digital_state = 0
# 限制数据点数
if len(self.digital_time_data) > self.max_data_points:
self.digital_time_data.pop(0)
self.digital_state_data.pop(0)
self.update_digital_plot()
except Exception as e:
self.status_bar.showMessage(f"设置引脚失败: {str(e)}")
def toggle_pin(self, pin):
if not self.is_connected:
QMessageBox.warning(self, "未连接", "请先连接到Arduino")
return
try:
current_state = self.board.digital[pin].read()
new_state = 0 if current_state else 1
self.board.digital[pin].write(new_state)
if new_state:
self.digital_pin_states[pin].setText("高电平")
self.digital_pin_states[pin].setStyleSheet("color: #388E3C; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"{pin}_indicator")
if indicator:
indicator.setStyleSheet("background-color: #388E3C; border-radius: 8px;")
self.status_bar.showMessage(f"引脚 D{pin} 切换为高电平")
else:
self.digital_pin_states[pin].setText("低电平")
self.digital_pin_states[pin].setStyleSheet("color: #d32f2f; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"{pin}_indicator")
if indicator:
indicator.setStyleSheet("background-color: #d32f2f; border-radius: 8px;")
self.status_bar.showMessage(f"引脚 D{pin} 切换为低电平")
# 记录状态变化
if pin == self.digital_monitor_pin:
current_time = time.time() - self.digital_monitor_start_time
self.digital_state_data.append(new_state)
self.digital_time_data.append(current_time)
# 统计边缘变化
if self.last_digital_state is not None:
if self.last_digital_state == 0 and new_state == 1:
self.rise_count += 1
elif self.last_digital_state == 1 and new_state == 0:
self.fall_count += 1
self.last_digital_state = new_state
# 限制数据点数
if len(self.digital_time_data) > self.max_data_points:
self.digital_time_data.pop(0)
self.digital_state_data.pop(0)
self.update_digital_plot()
except Exception as e:
self.status_bar.showMessage(f"切换引脚失败: {str(e)}")
def reset_all_pins(self):
if not self.is_connected:
QMessageBox.warning(self, "未连接", "请先连接到Arduino")
return
try:
# 重置所有数字引脚
for pin_info in self.digital_pins:
pin = pin_info['pin']
self.board.digital[pin].write(0)
self.digital_pin_states[pin].setText("低电平")
self.digital_pin_states[pin].setStyleSheet("color: #d32f2f; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"{pin}_indicator")
if indicator:
indicator.setStyleSheet("background-color: #d32f2f; border-radius: 8px;")
# 记录状态变化
if self.digital_monitor_pin in [p['pin'] for p in self.digital_pins]:
current_time = time.time() - self.digital_monitor_start_time
self.digital_state_data.append(0)
self.digital_time_data.append(current_time)
# 重置最后状态
self.last_digital_state = 0
# 限制数据点数
if len(self.digital_time_data) > self.max_data_points:
self.digital_time_data.pop(0)
self.digital_state_data.pop(0)
self.update_digital_plot()
# 停止LED闪烁
if self.led_timer.isActive():
self.led_timer.stop()
self.led_btn.setText("启动 LED 闪烁")
self.status_bar.showMessage("所有引脚已重置")
except Exception as e:
self.status_bar.showMessage(f"重置引脚失败: {str(e)}")
def toggle_led_blink(self):
if not self.is_connected:
QMessageBox.warning(self, "未连接", "请先连接到Arduino")
return
if self.led_timer.isActive():
self.led_timer.stop()
self.board.digital[13].write(0) # 关闭LED
self.digital_pin_states[13].setText("低电平")
self.digital_pin_states[13].setStyleSheet("color: #d32f2f; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"13_indicator")
if indicator:
indicator.setStyleSheet("background-color: #d32f2f; border-radius: 8px;")
self.led_btn.setText("启动 LED 闪烁")
self.status_bar.showMessage("LED 闪烁已停止")
else:
self.led_timer.start(500) # 500ms间隔
self.led_btn.setText("停止 LED 闪烁")
self.status_bar.showMessage("LED 闪烁已启动")
def blink_led(self):
if not self.is_connected:
return
self.led_state = not self.led_state
self.board.digital[13].write(1 if self.led_state else 0)
if self.led_state:
self.digital_pin_states[13].setText("高电平")
self.digital_pin_states[13].setStyleSheet("color: #388E3C; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"13_indicator")
if indicator:
indicator.setStyleSheet("background-color: #388E3C; border-radius: 8px;")
else:
self.digital_pin_states[13].setText("低电平")
self.digital_pin_states[13].setStyleSheet("color: #d32f2f; font-weight: bold;")
# 更新指示灯
indicator = self.digital_pin_states.get(f"13_indicator")
if indicator:
indicator.setStyleSheet("background-color: #d32f2f; border-radius: 8px;")
# 记录状态变化
if 13 == self.digital_monitor_pin:
current_time = time.time() - self.digital_monitor_start_time
self.digital_state_data.append(1 if self.led_state else 0)
self.digital_time_data.append(current_time)
# 统计边缘变化
if self.last_digital_state is not None:
if self.last_digital_state == 0 and self.led_state == 1:
self.rise_count += 1
elif self.last_digital_state == 1 and self.led_state == 0:
self.fall_count += 1
self.last_digital_state = 1 if self.led_state else 0
# 限制数据点数
if len(self.digital_time_data) > self.max_data_points:
self.digital_time_data.pop(0)
self.digital_state_data.pop(0)
self.update_digital_plot()
def read_voltage(self):
if not self.is_connected or not self.plot_monitoring:
return
try:
# 读取所有模拟引脚值
for i in range(6):
analog_value = self.board.analog[i].read()
if analog_value is not None:
# 转换为电压值 (0-5V)
voltage = analog_value * 5.0
# 更新模拟引脚显示
self.analog_pin_values[i].setText(f"{voltage:.3f} V")
# 如果是当前监控的引脚,则添加到图表
if i == self.analog_pin:
current_time = time.time() - self.start_time
# 添加到数据列表
self.voltage_data.append(voltage)
self.time_data.append(current_time)
# 限制数据点数
if len(self.voltage_data) > self.max_data_points:
self.voltage_data.pop(0)
self.time_data.pop(0)
# 更新图表
self.update_plot()
except Exception as e:
print(f"读取电压失败: {str(e)}")
def change_analog_pin(self, index):
if not self.is_connected:
return
try:
# 更新当前监控引脚
self.analog_pin = index
# 清空数据
self.clear_plot()
self.status_bar.showMessage(f"已切换到监控模拟输入 A{index}")
except Exception as e:
self.status_bar.showMessage(f"切换引脚失败: {str(e)}")
def change_digital_monitor_pin(self, index):
pin = self.digital_monitor_combo.currentData()
if pin is not None:
self.digital_monitor_pin = pin
self.clear_digital_plot()
self.last_digital_state = None
self.rise_count = 0
self.fall_count = 0
# 更新图表标题
for pin_info in self.digital_pins:
if pin_info['pin'] == pin:
pin_name = pin_info['name']
break
else:
pin_name = f"D{pin}"
self.digital_ax.set_title(f"数字引脚状态变化 - {pin_name}", fontsize=12)
self.digital_canvas.draw()
self.status_bar.showMessage(f"已切换到监控数字引脚 D{pin}")
def clear_plot(self):
self.voltage_data = []
self.time_data = []
self.start_time = time.time()
self.update_plot()
self.voltage_stats.setText("电压统计: 等待数据...")
self.status_bar.showMessage("模拟电压图表已清除")
def clear_digital_plot(self):
self.digital_state_data = []
self.digital_time_data = []
self.digital_monitor_start_time = time.time()
self.last_digital_state = None
self.rise_count = 0
self.fall_count = 0
self.update_digital_plot()
self.digital_stats.setText("状态统计: 等待数据...")
self.status_bar.showMessage("数字引脚状态图表已清除")
def toggle_plot_monitoring(self):
self.plot_monitoring = not self.plot_monitoring
if self.plot_monitoring:
self.pause_plot_btn.setText("暂停监控")
self.pause_plot_btn.setStyleSheet("background-color: #E91E63; color: white;")
self.status_bar.showMessage("电压监控已恢复")
else:
self.pause_plot_btn.setText("恢复监控")
self.pause_plot_btn.setStyleSheet("background-color: #4CAF50; color: white;")
self.status_bar.showMessage("电压监控已暂停")
def toggle_autoscale(self):
if self.autoscale_check.isChecked():
self.status_bar.showMessage("自动缩放已启用")
else:
self.status_bar.showMessage("自动缩放已禁用")
self.update_plot()
def update_sampling_interval(self):
interval = int(self.sampling_interval.value())
self.voltage_timer.setInterval(interval)
if self.is_connected:
self.voltage_timer.start()
self.status_bar.showMessage(f"采样间隔设置为 {interval}ms")
else:
self.status_bar.showMessage("连接后采样间隔将生效")
def save_voltage_data(self):
if not self.voltage_data:
self.status_bar.showMessage("无数据可保存")
return
try:
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"电压数据_A{self.analog_pin}_{timestamp}.csv"
with open(filename, 'w') as f:
f.write("时间(秒),电压(V)\n")
for t, v in zip(self.time_data, self.voltage_data):
f.write(f"{t:.3f},{v:.4f}\n")
self.status_bar.showMessage(f"模拟电压数据已保存到 {filename}")
QMessageBox.information(self, "保存成功", f"数据已保存到文件:\n{filename}")
except Exception as e:
self.status_bar.showMessage(f"保存失败: {str(e)}")
QMessageBox.critical(self, "保存失败", f"保存数据时出错:\n{str(e)}")
def save_digital_data(self):
if not self.digital_time_data:
self.status_bar.showMessage("无数据可保存")
return
try:
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"数字引脚_D{self.digital_monitor_pin}_数据_{timestamp}.csv"
with open(filename, 'w') as f:
f.write("时间(秒),状态,电压(V),变化类型\n")
last_state = None
for t, s in zip(self.digital_time_data, self.digital_state_data):
voltage = 5.0 if s == 1 else 0.0
# 检测变化类型
change_type = ""
if last_state is not None:
if last_state == 0 and s == 1:
change_type = "上升沿"
elif last_state == 1 and s == 0:
change_type = "下降沿"
last_state = s
f.write(f"{t:.3f},{s},{voltage:.1f},{change_type}\n")
self.status_bar.showMessage(f"数字引脚数据已保存到 {filename}")
QMessageBox.information(self, "保存成功", f"数据已保存到文件:\n{filename}")
except Exception as e:
self.status_bar.showMessage(f"保存失败: {str(e)}")
QMessageBox.critical(self, "保存失败", f"保存数据时出错:\n{str(e)}")
def closeEvent(self, event):
if self.is_connected:
self.disconnect_arduino()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
# 设置应用全局字体
font = QFont("Microsoft YaHei", 9)
app.setFont(font)
window = ArduinoControlApp()
window.show()
sys.exit(app.exec_())删除数字引脚部分引脚控制,保留2,3,4,8,9,10,13的端口控制,其余端口控制删除,在此基础上优化界面显示