python串口读取数据并显示(带区域选择)
采用 PySide6 + Matplotlib 开发,支持数据采集、实时绘图、区域选择分析等功能。
一、核心功能
串口通信与数据解析
自动识别可用串口,支持自定义波特率(9600/19200/38400/57600/115200)。
采用正则表达式解析串口数据,支持多单位(mmH2O/mmHg/mbar/psi/kPa/Pa),格式为 P + 数值 + 单位(如 P 123.45 kPa)。
多线程处理:串口接收在子线程中运行,避免阻塞 UI,通过信号槽机制向主线程传递数据 / 错误信息。
实时数据可视化
动态绘制压力 - 时间折线图,支持限制最大数据点(默认 1000 个,避免性能下降)。
坐标轴自适应:Y 轴根据数据范围动态调整(支持所有值相同时的特殊处理),X 轴显示相对时间(从首次采集开始计时,单位:秒)。
关键信息展示:当前值、采集时间、数据点总数,实时更新在图表上。
区域选择与分析
手动区域选择:点击 X 轴两次确定区域边界,支持拖动三角形调整范围,点击 “删除区域” 按钮移除。
自动区域分析:数据点超过 30 个时,自动将数据分为 3 段,计算每段的最小值、最大值,并高亮显示区域。
统计信息展示:选中区域的最小值、最大值实时显示在图表上。
交互控制
支持 “开始 / 暂停 / 停止” 采集,“清空” 图表数据。
集成 Matplotlib 导航工具栏,支持放大、缩小、平移、重置视图等操作。
环境依赖
安装必要库:pip install pyside6 matplotlib pyserial numpy。
支持 Windows/macOS/Linux,注意:Windows 需确保串口驱动正常,Linux 需添加用户到dialout组(避免权限问题)。
操作步骤
运行代码:直接执行脚本,自动识别可用串口。
选择串口与波特率:从下拉框选择可用串口,默认波特率 9600(根据设备手册调整)。
开始采集:点击 “开始接收”,图表实时显示压力数据。
区域分析:
手动选择:点击 X 轴两次,创建区域,拖动三角形调整范围,点击 “删除区域” 移除。
自动分析:数据点超过 30 个时,自动分为 3 段,每段显示最小值、最大值。
控制操作:点击 “暂停接收” 暂停采集(可恢复),“停止接收” 终止采集,“清空” 清除所有数据。
数据格式要求
串口发送的数据需包含 P + 数值 + 单位 格式(如 P 98.76 mmHg、P -12.34 Pa),支持数值前的正负号、数值与单位间的空格。
import sys
import os
import serial
import serial.tools.list_ports
import re
import time
from datetime import datetime
# === 指定使用 PySide6 ===
os.environ["QT_API"] = "pyside6"
# === 关键:提前设置 Matplotlib 中文字体,避免警告和乱码 ===
import matplotlib
matplotlib.use("QtAgg")
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'KaiTi', 'DejaVu Sans']
matplotlib.rcParams['axes.unicode_minus'] = False # 正常显示负号
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.dates import date2num
from matplotlib.lines import Line2D
from matplotlib.patches import Rectangle
from PySide6.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QWidget, QSizePolicy,
QPushButton, QHBoxLayout, QComboBox, QLabel, QMessageBox, QFrame
)
from PySide6.QtCore import QThread, Signal, Slot
# ========== 串口数据解析相关 ==========
# 支持的所有单位(顺序不重要,但建议长的放前面,避免前缀冲突,比如 'kPa' 在 'Pa' 前)
VALID_UNITS = ['mmH2O', 'mmHg', 'mbar', 'psi', 'kPa', 'Pa']
# 构建正则:匹配 P + 数值 + 任一单位
# 使用非捕获组 (?:...) 提高效率
unit_pattern = '|'.join(re.escape(u) for u in VALID_UNITS)
pattern = re.compile(rf'P\s*([\d.+-]+)\s*({unit_pattern})', re.IGNORECASE)
class SerialReceiver(QThread):
# 自定义信号:用于在主线程中更新UI
data_received = Signal(float, datetime) # 发送解析后的数值和时间戳
error_received = Signal(str)
def __init__(self, port: str, baudrate: int = 9600):
super().__init__()
self.port = port
self.baudrate = baudrate
self.ser = None
self.running = False
self.paused = False
self.buffer = ""
def run(self):
try:
self.ser = serial.Serial(
port=self.port,
baudrate=self.baudrate,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=0.1
)
self.running = True
print(f"已打开串口:{self.ser.name},解析多单位压力值(按Ctrl+C退出)...\n")
while self.running:
if self.paused:
time.sleep(0.1)
continue
if self.ser.in_waiting > 0:
raw = self.ser.read(self.ser.in_waiting).decode('ascii', errors='ignore')
self.buffer += raw
# 查找所有匹配项
matches = list(pattern.finditer(self.buffer))
if matches:
# 处理所有完整匹配(避免漏掉)
for match in matches:
value_str = match.group(1)
unit = match.group(2).strip()
try:
value = float(value_str) # 验证是否为有效数字
current_time = datetime.now()
self.data_received.emit(value, current_time)
except ValueError:
continue # 跳过无效数字
# 更新缓冲区:保留最后一个匹配之后的内容
self.buffer = self.buffer[matches[-1].end():]
else:
# 防止缓冲区无限增长
if len(self.buffer) > 200:
self.buffer = self.buffer[-100:] # 保留最后100字符
time.sleep(0.01)
except serial.SerialException as e:
self.error_received.emit(f"串口错误:{e}")
except Exception as e:
self.error_received.emit(f"未知错误:{e}")
finally:
if self.ser and self.ser.is_open:
self.ser.close()
print("串口已关闭")
def pause(self):
self.paused = True
def resume(self):
self.paused = False
def stop(self):
self.running = False
self.quit()
self.wait()
# ========== 交互选择器类(性能优化版) ==========
class XAxisRegionSelector:
def __init__(self, canvas, ax):
self.canvas = canvas
self.ax = ax
self.start_line = None
self.end_line = None
self.start_triangle = None
self.end_triangle = None
self.dragging_start = False
self.dragging_end = False
self.current_region_idx = None
self.current_line = None
self.region_count = 0
self.fill_area = None
# 存储所有区域信息
self.regions = []
self.colors = ['red', 'blue', 'green', 'orange', 'purple', 'brown', 'pink', 'gray', 'olive', 'cyan']
self.color_alpha = 0.2
self.line_alpha = 0.7
# 信息文本对象
self.info_text = None
self.instruction_text = None
self.total_points_text = None
# 连接事件
self.cid_press = self.canvas.mpl_connect('button_press_event', self.on_press)
self.cid_release = self.canvas.mpl_connect('button_release_event', self.on_release)
self.cid_motion = self.canvas.mpl_connect('motion_notify_event', self.on_motion)
def set_info_text(self, info_text, instruction_text, total_points_text):
self.info_text = info_text
self.instruction_text = instruction_text
self.total_points_text = total_points_text
def on_press(self, event):
if event.inaxes != self.ax:
return
# 检查是否点击了删除按钮
for i, (start_line, end_line, fill_area, delete_btn, info_text_obj, start_tri, end_tri, region_data) in enumerate(self.regions):
# 使用更精确的点击检测
bbox = delete_btn.get_window_extent(self.canvas.figure.canvas.renderer)
bbox_data = bbox.transformed(self.ax.transData.inverted())
if bbox_data.contains(event.xdata, event.ydata):
# 删除区域
start_line.remove()
end_line.remove()
if fill_area:
fill_area.remove()
delete_btn.remove()
info_text_obj.remove()
start_tri.remove()
end_tri.remove()
# 从列表中移除
del self.regions[i]
# 更新区域编号
for j, (sl, el, fa, db, it, st, et, rd) in enumerate(self.regions):
db.set_text(f'删除\n区域{j+1}')
self.canvas.draw_idle()
return
# 检查是否拖动三角形
for i, (start_line, end_line, fill_area, delete_btn, info_text_obj, start_tri, end_tri, region_data) in enumerate(self.regions):
# 获取三角形的位置
start_tri_x = start_tri.get_xdata()[0]
end_tri_x = end_tri.get_xdata()[0]
# 检查是否点击了开始三角形
if abs(event.xdata - start_tri_x) < 0.02 * (max(self.ax.get_xlim()) - min(self.ax.get_xlim())):
self.dragging_start = True
self.current_region_idx = i
self.current_line = 'start'
return
# 检查是否点击了结束三角形
if abs(event.xdata - end_tri_x) < 0.02 * (max(self.ax.get_xlim()) - min(self.ax.get_xlim())):
self.dragging_end = True
self.current_region_idx = i
self.current_line = 'end'
return
# 开始新的区域选择
if self.start_line is None:
self.start_line = self.ax.axvline(x=event.xdata, color=self.colors[self.region_count % len(self.colors)],
linestyle='--', linewidth=0.5, alpha=self.line_alpha)
# 添加开始三角形
y_triangle = max(self.ax.get_ylim()) + 0.05 * (max(self.ax.get_ylim()) - min(self.ax.get_ylim()))
self.start_triangle = self.ax.plot([event.xdata], [y_triangle],
marker='^', markersize=10,
color=self.colors[self.region_count % len(self.colors)],
clip_on=False)[0]
elif self.end_line is None:
self.end_line = self.ax.axvline(x=event.xdata, color=self.colors[self.region_count % len(self.colors)],
linestyle='--', linewidth=0.5, alpha=self.line_alpha)
# 添加结束三角形
y_triangle = max(self.ax.get_ylim()) + 0.05 * (max(self.ax.get_ylim()) - min(self.ax.get_ylim()))
self.end_triangle = self.ax.plot([event.xdata], [y_triangle],
marker='^', markersize=10,
color=self.colors[self.region_count % len(self.colors)],
clip_on=False)[0]
# 确保x0 < x1
x0, x1 = self.start_line.get_xdata()[0], self.end_line.get_xdata()[0]
if x0 > x1:
x0, x1 = x1, x0
# 创建填充区域
self.fill_area = self.ax.axvspan(x0, x1,
color=self.colors[self.region_count % len(self.colors)], alpha=0.2)
# 添加删除按钮
ylim = self.ax.get_ylim()
button_y = ylim[1] - 0.05 * (ylim[1] - ylim[0])
button_x = (x0 + x1) / 2
btn_text = self.ax.text(button_x, button_y, f'删除\n区域{self.region_count+1}',
ha='center', va='center',
bbox=dict(boxstyle='round,pad=0.3', facecolor=self.colors[self.region_count % len(self.colors)], alpha=0.7),
fontsize=8)
# 添加区域信息文本(暂时为空)
info_y = button_y - 0.03 * (ylim[1] - ylim[0])
info_text_obj = self.ax.text(button_x, info_y, '等待数据...',
ha='center', va='center',
bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.6),
fontsize=7)
# 存储区域信息
region_data = {
'x0': x0,
'x1': x1,
'min_val': None,
'max_val': None,
'avg_val': None,
'count': 0,
'selected_indices': []
}
self.regions.append((self.start_line, self.end_line, self.fill_area, btn_text, info_text_obj,
self.start_triangle, self.end_triangle, region_data))
self.region_count += 1
# 重置
self.start_line = None
self.end_line = None
self.start_triangle = None
self.end_triangle = None
self.fill_area = None
def on_motion(self, event):
if event.inaxes != self.ax or event.xdata is None:
return
if self.dragging_start and self.current_region_idx is not None:
# 更新当前区域的开始线和开始三角形
region = self.regions[self.current_region_idx]
start_line, end_line, fill_area, delete_btn, info_text_obj, start_tri, end_tri, region_data = region
start_line.set_xdata([event.xdata]) # 更新开始线
start_tri.set_xdata([event.xdata]) # 更新开始三角形
start_tri.set_ydata([max(self.ax.get_ylim()) + 0.05 * (max(self.ax.get_ylim()) - min(self.ax.get_ylim()))]) # 保持三角形在上方
# 更新填充区域
start_x = event.xdata
end_x = end_line.get_xdata()[0] # 获取结束线的x值
if start_x > end_x:
start_x, end_x = end_x, start_x
# 移除旧的填充区域
fill_area.remove()
# 创建新的填充区域
new_fill = self.ax.axvspan(start_x, end_x,
color=self.colors[self.current_region_idx % len(self.colors)], alpha=0.2)
# 更新区域信息
region_data['x0'] = start_x
region_data['x1'] = end_x
self.regions[self.current_region_idx] = (start_line, end_line, new_fill, delete_btn, info_text_obj,
start_tri, end_tri, region_data)
elif self.dragging_end and self.current_region_idx is not None:
# 更新当前区域的结束线和结束三角形
region = self.regions[self.current_region_idx]
start_line, end_line, fill_area, delete_btn, info_text_obj, start_tri, end_tri, region_data = region
end_line.set_xdata([event.xdata]) # 更新结束线
end_tri.set_xdata([event.xdata]) # 更新结束三角形
end_tri.set_ydata([max(self.ax.get_ylim()) + 0.05 * (max(self.ax.get_ylim()) - min(self.ax.get_ylim()))]) # 保持三角形在上方
# 更新填充区域
start_x = start_line.get_xdata()[0] # 获取开始线的x值
end_x = event.xdata
if start_x > end_x:
start_x, end_x = end_x, start_x
# 移除旧的填充区域
fill_area.remove()
# 创建新的填充区域
new_fill = self.ax.axvspan(start_x, end_x,
color=self.colors[self.current_region_idx % len(self.colors)], alpha=0.2)
# 更新区域信息
region_data['x0'] = start_x
region_data['x1'] = end_x
self.regions[self.current_region_idx] = (start_line, end_line, new_fill, delete_btn, info_text_obj,
start_tri, end_tri, region_data)
self.canvas.draw_idle()
def on_release(self, event):
self.dragging_start = False
self.dragging_end = False
self.current_region_idx = None
self.current_line = None
def disconnect(self):
if self.cid_press:
self.canvas.mpl_disconnect(self.cid_press)
if self.cid_release:
self.canvas.mpl_disconnect(self.cid_release)
if self.cid_motion:
self.canvas.mpl_disconnect(self.cid_motion)
def update_regions(self, time_nums, pressures):
"""更新所有区域的统计信息"""
if not self.regions:
return
for i, (start_line, end_line, fill_area, delete_btn, info_text_obj, start_tri, end_tri, region_data) in enumerate(self.regions):
x0 = region_data['x0']
x1 = region_data['x1']
# 找到选中范围内的数据点
selected_indices = []
for idx, x in enumerate(time_nums):
if x0 <= x <= x1:
selected_indices.append(idx)
if selected_indices:
selected_values = [pressures[idx] for idx in selected_indices]
if selected_values: # 确保有选中的数据
min_val = min(selected_values)
max_val = max(selected_values)
avg_val = sum(selected_values) / len(selected_values)
count = len(selected_values)
# 更新信息文本
info_text_obj.set_text(f'最小值: {min_val:.4f}\n最大值: {max_val:.4f}')
# 更新区域数据
region_data['min_val'] = min_val
region_data['max_val'] = max_val
region_data['avg_val'] = avg_val
region_data['count'] = count
region_data['selected_indices'] = selected_indices
# ========== 自定义画布类(性能优化版) ==========
class MplCanvas(FigureCanvas):
def __init__(self, parent=None, width=16, height=10, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
super().__init__(self.fig)
self.setParent(parent)
self.selector = None
# 实时数据存储
self.times = []
self.pressures = []
self.time_nums = []
self.relative_times = [] # 相对时间,从0开始
# 预先创建数据线对象,避免重复创建
self.data_line = None
# 三个自动分段区域
self.auto_regions = [] # 存储区域对象的引用
self.auto_texts = [] # 存储文本对象的引用
self.auto_colors = ['red', 'blue', 'green']
self.plot()
def plot(self):
# 清空图形
self.fig.clear()
# 使用 subplot 代替 add_axes,自动处理标签空间
ax = self.fig.add_subplot(111)
# 预先创建数据线对象
self.data_line, = ax.plot([], [], 'b-', linewidth=1.5, label='压力值', alpha=0.8)
ax.set_title('实时压力数据折线图 - 使用X轴边界选择区域', fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('相对时间 (秒)', fontsize=12)
ax.set_ylabel('压力值', fontsize=12)
ax.grid(True, linestyle='--', alpha=0.6)
ax.legend()
# 底部信息文本
info_text = self.fig.text(0.08, 0.02, '', fontsize=10,
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9, edgecolor='gray'))
# 顶部说明
instruction_text = self.fig.text(0.02, 0.95,
"使用说明: 点击两次X轴确定区域边界,或拖动边界三角形调整。点击删除按钮可删除区域",
fontsize=11, style='italic', color='blue',
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8))
total_points_text = self.fig.text(0.02, 0.91,
f"数据点总数: 0",
fontsize=10, style='italic', color='green',
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8))
# 初始化选择器
self.selector = XAxisRegionSelector(self, ax)
self.selector.set_info_text(info_text, instruction_text, total_points_text)
# 使用 tight_layout 自动调整布局,为标签留出空间
self.fig.tight_layout(rect=[0, 0.05, 1, 0.95]) # 为底部 info_text 留出空间
# 刷新画布
self.draw()
def update_data(self, pressure, current_time):
# 添加新数据点
self.times.append(current_time)
self.pressures.append(pressure)
self.time_nums.append(date2num(current_time))
# 计算相对时间(从第一次数据开始)
if len(self.time_nums) == 1:
self.start_time_num = self.time_nums[0] # 记录开始时间
relative_time = (date2num(current_time) - self.start_time_num) * 24 * 3600 # 转换为秒
self.relative_times.append(relative_time)
# 限制数据点数量,保持性能
if len(self.times) > 1000: # 保留最多1000个点
self.times = self.times[-1000:]
self.pressures = self.pressures[-1000:]
self.time_nums = self.time_nums[-1000:]
self.relative_times = self.relative_times[-1000:]
# 更新数据线,使用相对时间
ax = self.fig.get_axes()[0]
self.data_line.set_data(self.relative_times, self.pressures)
# 重新计算坐标轴范围以适应新数据
ax.relim()
ax.autoscale_view()
# 动态设置Y轴范围,处理所有值相同的情况
if self.pressures:
y_min = min(self.pressures)
y_max = max(self.pressures)
if y_min == y_max:
# 当所有值相同时,设置一个固定范围
y_center = y_min
y_min = y_center - 1
y_max = y_center + 1
else:
y_min = y_min - 0.1 * (y_max - y_min)
y_max = y_max + 0.1 * (y_max - y_min)
ax.set_ylim(y_min, y_max)
# 设置X轴范围以显示所有数据(移除滑动窗口)
ax.set_xlim(min(self.relative_times), max(self.relative_times) + 0.1)
# 更新底部信息文本
info_text = self.fig.texts[2] # 第三个文本对象是info_text
info_text.set_text(f"当前值: {pressure:.4f} | 时间: {current_time.strftime('%H:%M:%S.%f')[:-3]}")
# 更新顶部总数文本
total_points_text = self.fig.texts[1] # 第二个文本对象是total_points_text
total_points_text.set_text(f"数据点总数: {len(self.pressures)}")
# 当数据超过30个点时,自动创建3个分段区域
if len(self.pressures) >= 30:
self.update_auto_regions(ax)
# 更新手动选择的区域统计信息
if self.selector:
self.selector.update_regions(self.relative_times, self.pressures)
# 使用 draw_idle 而不是 draw,提高性能
self.draw_idle()
def update_auto_regions(self, ax):
"""当数据超过30个点后,自动创建3个分段区域"""
n = len(self.pressures)
if n < 30:
return
# 清除旧的自动区域和文本
for region in self.auto_regions:
try:
region.remove()
except:
pass # 如果已经移除,则忽略
for text in self.auto_texts:
try:
text.remove()
except:
pass # 如果已经移除,则忽略
self.auto_regions = []
self.auto_texts = []
# 计算三个段的范围
seg1 = n // 3
seg2 = 2 * n // 3
for i in range(3):
if i == 0:
start_idx, end_idx = 0, seg1
elif i == 1:
start_idx, end_idx = seg1, seg2
else:
start_idx, end_idx = seg2, n-1
# 获取该区域的数据
selected_indices = list(range(start_idx, end_idx + 1))
selected_values = [self.pressures[j] for j in selected_indices]
selected_relative_times = [self.relative_times[j] for j in selected_indices]
# 计算统计信息
min_val = min(selected_values)
max_val = max(selected_values)
avg_val = sum(selected_values) / len(selected_values)
count = len(selected_values)
# 创建填充区域
x_fill = self.relative_times[start_idx:end_idx+1]
y_fill = self.pressures[start_idx:end_idx+1]
fill_area = ax.fill_between(x_fill, y_fill,
color=self.auto_colors[i], alpha=0.15)
self.auto_regions.append(fill_area)
# 添加区域信息文本
if i == 0:
pos_x = self.relative_times[start_idx]
pos_y = max(self.pressures) - 0.1 * (max(self.pressures) - min(self.pressures))
elif i == 1:
pos_x = self.relative_times[(start_idx + end_idx) // 2]
pos_y = max(self.pressures) - 0.15 * (max(self.pressures) - min(self.pressures))
else:
pos_x = self.relative_times[end_idx]
pos_y = max(self.pressures) - 0.2 * (max(self.pressures) - min(self.pressures))
info_text = ax.text(pos_x, pos_y, f'段{i+1}: 最小值: {min_val:.4f}\n最大值: {max_val:.4f}',
ha='center', va='center',
bbox=dict(boxstyle='round,pad=0.3', facecolor=self.auto_colors[i], alpha=0.6),
fontsize=8)
self.auto_texts.append(info_text)
# ========== 主窗口 ==========
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("串口实时压力数据可视化系统 - PySide6")
self.resize(1200, 800)
# 串口相关
self.serial_thread = None
self.is_receiving = False
# UI 组件
self.init_ui()
def init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
central_widget.setLayout(layout)
# 串口设置区域
control_frame = QFrame()
control_layout = QHBoxLayout()
control_frame.setLayout(control_layout)
control_layout.addWidget(QLabel("串口:"))
self.port_combo = QComboBox()
control_layout.addWidget(self.port_combo)
control_layout.addWidget(QLabel("波特率:"))
self.baudrate_combo = QComboBox()
self.baudrate_combo.addItems(['9600', '19200', '38400', '57600', '115200'])
self.baudrate_combo.setCurrentText('9600')
control_layout.addWidget(self.baudrate_combo)
self.refresh_btn = QPushButton("刷新端口")
self.refresh_btn.clicked.connect(self.refresh_ports)
control_layout.addWidget(self.refresh_btn)
layout.addWidget(control_frame)
# 按钮区域
btn_layout = QHBoxLayout()
self.start_btn = QPushButton("开始接收")
self.pause_btn = QPushButton("暂停接收")
self.stop_btn = QPushButton("停止接收")
self.clear_btn = QPushButton("清空")
self.start_btn.clicked.connect(self.start_receive)
self.pause_btn.clicked.connect(self.pause_receive)
self.stop_btn.clicked.connect(self.stop_receive)
self.clear_btn.clicked.connect(self.clear_display)
self.pause_btn.setEnabled(False)
self.stop_btn.setEnabled(False)
btn_layout.addWidget(self.start_btn)
btn_layout.addWidget(self.pause_btn)
btn_layout.addWidget(self.stop_btn)
btn_layout.addWidget(self.clear_btn)
layout.addLayout(btn_layout)
# 创建并添加画布
self.canvas = MplCanvas(self, width=16, height=10, dpi=100)
layout.addWidget(self.canvas)
# 添加导航工具栏
self.toolbar = NavigationToolbar(self.canvas, self)
layout.addWidget(self.toolbar)
# 确保画布随窗口缩放
self.canvas.setSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Expanding
)
self.refresh_ports()
def refresh_ports(self):
self.port_combo.clear()
ports = [port.device for port in serial.tools.list_ports.comports()]
if not ports:
self.port_combo.addItem("无可用串口")
else:
self.port_combo.addItems(ports)
def start_receive(self):
port = self.port_combo.currentText()
if port == "无可用串口" or not port:
QMessageBox.warning(self, "错误", "请选择有效串口!")
return
baudrate = int(self.baudrate_combo.currentText())
if self.serial_thread and self.serial_thread.isRunning():
return
self.serial_thread = SerialReceiver(port, baudrate=baudrate)
self.serial_thread.data_received.connect(self.on_data_received)
self.serial_thread.error_received.connect(self.on_error_received)
self.serial_thread.start()
self.is_receiving = True
self.start_btn.setEnabled(False)
self.pause_btn.setEnabled(True)
self.stop_btn.setEnabled(True)
def on_data_received(self, pressure, timestamp):
"""接收到数据时更新图表"""
self.canvas.update_data(pressure, timestamp)
def on_error_received(self, error_msg):
"""接收到错误时显示"""
QMessageBox.critical(self, "串口错误", error_msg)
def pause_receive(self):
if not self.serial_thread:
return
if self.pause_btn.text() == "暂停接收":
self.serial_thread.pause()
self.pause_btn.setText("继续接收")
else:
self.serial_thread.resume()
self.pause_btn.setText("暂停接收")
def stop_receive(self):
if self.serial_thread:
self.serial_thread.stop()
self.serial_thread = None
self.is_receiving = False
self.start_btn.setEnabled(True)
self.pause_btn.setEnabled(False)
self.stop_btn.setEnabled(False)
self.pause_btn.setText("暂停接收")
def clear_display(self):
# 清空图表数据
self.canvas.times = []
self.canvas.pressures = []
self.canvas.time_nums = []
self.canvas.relative_times = []
# 清除自动区域和文本
for region in self.canvas.auto_regions:
try:
region.remove()
except:
pass
for text in self.canvas.auto_texts:
try:
text.remove()
except:
pass
self.canvas.auto_regions = []
self.canvas.auto_texts = []
# 重置数据线
ax = self.canvas.fig.get_axes()[0]
self.canvas.data_line.set_data([], [])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
# 清空文本信息
info_text = self.canvas.fig.texts[2]
info_text.set_text("")
total_points_text = self.canvas.fig.texts[1]
total_points_text.set_text(f"数据点总数: 0")
# 刷新画布
self.canvas.draw_idle()
def closeEvent(self, event):
if self.serial_thread:
self.serial_thread.stop()
if self.canvas.selector:
self.canvas.selector.disconnect()
event.accept()
# ========== 启动应用 ==========
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())


被折叠的 条评论
为什么被折叠?



