Q1:好了,现在我想让EEG这份代码加一个虚拟数据让代码跑起来,并且看到滤波与不滤波的效果我可以怎么做呢

好!让 EEG 代码跑起来,看到滤波 vs 不滤波效果 🎯


方案:添加虚拟数据模式

修改 SerialThread,让它在没有硬件时生成带噪声的正弦波


第一步:修改 SerialThread__init__

class SerialThread(QtCore.QThread):
    data_received = QtCore.pyqtSignal(str)
    
    def __init__(self, com_port: str, log_queue: queue.Queue, virtual_mode=False):
        super().__init__()
        self.com_port = com_port
        self.log_queue = log_queue
        self.virtual_mode = virtual_mode  # ← 新增:虚拟模式标志
        
        # 虚拟数据用
        self.virtual_t = 0
        self.virtual_noise = 50  # 噪声幅度(μV)
        
        # 原有代码...
        self.buffer = ChannelBuffer(capacity=DATA_LENGTH)
        # ...

第二步:修改 run 方法

def run(self):
    self.running = True
    
    if self.virtual_mode:
        # ===== 虚拟模式:生成正弦波 + 噪声 =====
        self._run_virtual()
    else:
        # ===== 真实串口模式(原有代码)=====
        self._run_serial()

第三步:添加虚拟数据生成

def _run_virtual(self):
    """虚拟模式:生成8通道正弦波,带50Hz干扰"""
    import numpy as np
    
    while self.running:
        # 生成8通道数据
        values = []
        for ch in range(8):
            # 基础信号:10Hz正弦波(模拟脑电α波)
            base = np.sin(2 * np.pi * 10 * self.virtual_t + ch) * 100
            
            # 添加50Hz工频干扰(模拟电源噪声)
            interference = np.sin(2 * np.pi * 50 * self.virtual_t) * self.virtual_noise
            
            # 添加随机噪声
            noise = np.random.randn() * 20
            
            value = base + interference + noise
            values.append(value)
        
        # 格式化字符串(模拟串口输出)
        raw = f"通道数据(μV): {','.join(f'{v:.2f}' for v in values)}"
        
        # 走原有处理流程
        self._process_data(raw)
        
        # 模拟500Hz采样率(2ms间隔)
        self.virtual_t += 0.002
        time.sleep(0.002)

def _process_data(self, raw: str):
    """共用数据处理(日志 + 解析 + 滤波)"""
    # 日志
    try:
        self.log_queue.put_nowait(f"{datetime.now()},{raw}")
    except queue.Full:
        pass
    
    # 解析并处理
    self._process_serial_data(raw)
    
    # 状态栏更新
    self.data_received.emit(raw)

第四步:修改主程序启动

# ADCPlotter.__init__ 中
class ADCPlotter(QtWidgets.QMainWindow):
    def __init__(self, parent=None, virtual_mode=True):  # ← 默认虚拟模式
        # ...
        
        # 检测串口或强制虚拟
        if virtual_mode:
            com_port = "VIRTUAL"
            InfoBar.info(title='模式', content='虚拟数据模式(带50Hz干扰)', ...)
        else:
            com_port = self._detect_com_port()
        
        self.serial_thread = SerialThread(com_port, self.log_queue, virtual_mode=virtual_mode)

第五步:添加滤波开关对比

在控制栏添加按钮,实时切换:

# _setup_ui 中添加
self.virtual_noise_btn = PushButton("噪声:开")
self.virtual_noise_btn.clicked.connect(self._toggle_noise)

def _toggle_noise(self):
    """切换虚拟噪声"""
    if self.serial_thread.virtual_noise > 0:
        self.serial_thread.virtual_noise = 0  # 关噪声
        self.virtual_noise_btn.setText("噪声:关")
        self._info("已关闭50Hz干扰")
    else:
        self.serial_thread.virtual_noise = 50  # 开噪声
        self.virtual_noise_btn.setText("噪声:开")
        self._info("已开启50Hz干扰")

运行效果

操作 效果
启动 8通道正弦波,带50Hz干扰
勾选"滤波开启" 50Hz干扰被陷波滤波去除,波形变干净
点击"噪声:关" 关闭干扰源,看原始信号
点击"噪声:开" 开启干扰,看滤波效果

快速验证滤波效果

# 在 _process_serial_data 中添加打印
def _process_serial_data(self, raw: str):
    values = self._parse_line(raw)
    if values:
        ch0_raw = values[0]
        
        if self.filter_enabled:
            # 滤波后
            ch0_filtered = self._apply_filter(ch0_raw, channel=0)
            print(f"CH0 原始:{ch0_raw:8.2f} 滤波后:{ch0_filtered:8.2f}")
        else:
            print(f"CH0 原始:{ch0_raw:8.2f} (未滤波)")

你现在要做的

  1. 备份原文件 FIFO_plot.py
  2. 修改 SerialThread 添加虚拟模式
  3. 运行看效果:对比勾选/取消"滤波开启"的波形

需要我给出完整修改后的代码文件,还是你先试着改,遇到问题问我?💪

Q2
我按照您的提示修改了代码,但是运行结果跟源代码是一样的,是我哪里修改的不对吗

我看到问题了!你修改了代码,但**主程序入口没有启用虚拟模式**,而且`ADCPlotter.__init__`里有**重复的串口初始化代码**覆盖了虚拟模式。

问题定位

问题1:主程序入口没传参数

# 你的代码第900行
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = ADCPlotter()           # ← 没传 virtual_mode=True!
    window.resize(1200, 800)
    window.show()
    sys.exit(app.exec_())

问题2:__init__里有重复的串口代码

# 第410-430行:你添加了虚拟模式判断
if virtual_mode:
    com_port = "VIRTUAL"
    InfoBar.info(...)
else:
    com_port = self._detect_com_port()

self.serial_thread = SerialThread(com_port, self.log_queue, virtual_mode=virtual_mode)

# 第432-450行:原来的代码还在,又创建了一次!
com_port = self._detect_com_port()  # ← 这里覆盖了!
...
self.serial_thread = SerialThread(com_port, self.log_queue)  # ← 又创建一次,没传virtual_mode

结果:第二次创建覆盖了第一次,始终走真实串口模式。


修复方案

修复1:删除重复的串口代码

# 找到这段(约432-450行),全部删除!
###############################################################################
com_port = self._detect_com_port()
if not com_port:
    available_ports = [p.device for p in serial.tools.list_ports.comports()]
    if not available_ports:
        QtWidgets.QMessageBox.critical(self, "错误", "未检测到任何串口")
        self.log_thread.stop()
        sys.exit(1)

    com_port, ok = QtWidgets.QInputDialog.getItem(self, "选择串口", "请选择串口设备:", available_ports, 0,
                                                  False)
    if not ok or not com_port:
        sys.exit(1)

self.serial_thread = SerialThread(com_port, self.log_queue)
self.serial_thread.data_received.connect(self._on_serial_line)
self.serial_thread.start()
###############################################################################

# 保留你添加的这段(约410-430行):
if virtual_mode:
    com_port = "VIRTUAL"
    InfoBar.info(title='模式', content='虚拟数据模式(带50Hz干扰)')
else:
    com_port = self._detect_com_port()

self.serial_thread = SerialThread(com_port, self.log_queue, virtual_mode=virtual_mode)
self.serial_thread.data_received.connect(self._on_serial_line)  # 加上这行信号连接
self.serial_thread.start()  # 加上启动

修复2:主程序启用虚拟模式

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = ADCPlotter(virtual_mode=True)  # ← 关键:传参数!
    window.resize(1200, 800)
    window.show()
    sys.exit(app.exec_())

快速验证

修改后运行,应该看到:

1. 弹出提示:"虚拟数据模式(带50Hz干扰)"
2. 波形显示8通道正弦波
3. 点击"滤波开启"取消勾选,50Hz干扰出现(波形变粗/抖动)
4. 勾选"滤波开启",干扰被去除(波形变平滑)

或者:简化方案,强制虚拟模式

如果不想改主程序,在__init__里强制:

def __init__(self, parent=None, virtual_mode=True):  # ← 默认True
    ...
    # 删除所有真实串口代码,只保留:
    com_port = "VIRTUAL"
    InfoBar.info(title='模式', content='虚拟数据模式(带50Hz干扰)')
    self.serial_thread = SerialThread(com_port, self.log_queue, virtual_mode=True)
    self.serial_thread.data_received.connect(self._on_serial_line)
    self.serial_thread.start()

你现在要做的

  1. 删除 ### 包围的重复串口代码(约432-450行)
  2. 确认 只保留一段SerialThread创建代码
  3. 修改 主程序 ADCPlotter(virtual_mode=True)
  4. 运行,看是否有"虚拟数据模式"提示

改完运行,告诉我结果! 💪

成功显示图像时刻!Amazing!

真的非常非常感谢AI感谢kimi
在这里插入图片描述
Q3:天啊,运行成功了!但是画面中的数据十分杂乱。可以修改成只显示单通道吗,然后把画面的更新频率改低一些

太好了!运行成功!🎉 画面杂乱是因为8通道叠加+高频率更新,我来帮你优化。


修改目标

问题 解决方案
8通道太乱 只显示单通道(CH1)
更新太快 降低频率或降低采样率
波形太密 减少PLOT_LENGTH显示点数

快速修改(3处)

修改1:只显示CH1(约第480行)

def _update_time_plot(self):
    x, data = self.serial_thread.get_plot_data(PLOT_LENGTH)
    if data.size == 0:
        return

    # 只显示CH1,忽略勾选框
    # active = [i for i, cb in enumerate(self.ch_boxes) if cb.isChecked()]
    # if not active:
    #     return
    # for i in active:
    #     self.time_curves[i].setData(x, data[i])
    
    # 只画CH1
    self.time_curves[0].setData(x, data[0])
    
    # 隐藏其他通道
    for i in range(1, 8):
        self.time_curves[i].setData([], [])

修改2:降低更新频率(第20行)

UPDATE_INTERVAL = 200  # 60 → 200,每200ms刷新一次(5fps,更慢更省CPU)

修改3:减少显示点数(第23行)

PLOT_LENGTH = 500  # 1000 → 500,显示500个点(画面更宽松)

额外优化:降低虚拟数据采样率

虚拟模式生成数据太快,也降一点(第280行):

# 模拟200Hz采样率(5ms间隔),而不是500Hz
self.virtual_t += 0.005  # 0.002 → 0.005
time.sleep(0.005)        # 0.002 → 0.005

修改后效果

修改前 修改后
8通道叠加,混乱 单通道清晰
60ms刷新(16fps) 200ms刷新(5fps),更慢更稳
显示1000点,密集 显示500点,宽松
500Hz虚拟采样 200Hz虚拟采样,波形更平滑

完整替换 _update_time_plot 函数

def _update_time_plot(self):
    x, data = self.serial_thread.get_plot_data(PLOT_LENGTH)
    if data.size == 0:
        return

    # 只显示CH1,单通道清晰
    self.time_curves[0].setData(x, data[0])
    
    # 清空其他通道
    for i in range(1, 8):
        self.time_curves[i].setData([], [])

    # 自动滚动X轴
    if len(x) > 0:
        self.time_plot.setXRange(x[0], x[-1])

    # 自动Y轴
    if self.auto_y_box.isChecked():
        ch0_data = data[0]
        mask = ~np.isnan(ch0_data)
        if np.any(mask):
            valid_data = ch0_data[mask]
            y_min, y_max = np.min(valid_data), np.max(valid_data)
            if y_min != y_max:
                margin = (y_max - y_min) * 0.1 or 1.0
                self.time_plot.setYRange(y_min - margin, y_max + margin)
            else:
                self.time_plot.setYRange(y_min - 1, y_max + 1)

现在修改,运行看效果!

改完应该看到:一条蓝色正弦波,缓慢滚动,清晰平滑

成功了吗?💪

Q4:为什么我把界面上面的CH2.3.4.5取消掉了,但是上位机界面并没有把这几个波形取消掉呢

因为你只改了界面勾选框的状态,但代码还在画所有勾选的通道


问题所在

# 你的代码(约第480行)
active = [i for i, cb in enumerate(self.ch_boxes) if cb.isChecked()]
# 这里根据勾选框状态决定画哪些通道

for i in active:
    self.time_curves[i].setData(x, data[i])
# 所以取消勾选 → active列表里没有 → 不画

但你说取消后还在画,说明可能是:

可能原因 检查
改完代码没保存 Ctrl+S 保存
改完没重启程序 关掉重新运行
改错地方了 确认改的是_update_time_plot
有缓存 删掉__pycache__文件夹

快速验证修改是否生效

_update_time_plot里加打印:

def _update_time_plot(self):
    x, data = self.serial_thread.get_plot_data(PLOT_LENGTH)
    
    # 加这行!看控制台输出
    active = [i for i, cb in enumerate(self.ch_boxes) if cb.isChecked()]
    print(f"当前勾选的通道: {active}")  # ← 加这行
    
    if not active:
        return
    # ...

运行后看控制台

  • 只勾CH1 → 应该显示 [0]
  • 勾CH1+CH2 → 应该显示 [0, 1]

如果显示不对,说明界面和代码没连上


彻底解决方案:强制只画CH1(不管勾选)

def _update_time_plot(self):
    x, data = self.serial_thread.get_plot_data(PLOT_LENGTH)
    if data.size == 0:
        return

    # ===== 强制只画CH1 =====
    self.time_curves[0].setData(x, data[0])
    
    # 其他通道强制清空
    for i in range(1, 8):
        self.time_curves[i].setData([], [])
    
    # 自动Y轴只看CH1
    if self.auto_y_box.isChecked():
        ch0 = data[0]
        mask = ~np.isnan(ch0)
        if np.any(mask):
            y_min, y_max = np.min(ch0[mask]), np.max(ch0[mask])
            margin = (y_max - y_min) * 0.1 or 1.0
            self.time_plot.setYRange(y_min - margin, y_max + margin)

检查步骤

  1. 保存代码(Ctrl+S)
  2. 完全关闭程序(点X)
  3. 重新运行
  4. 看是否只有蓝色一条线

还是不行的话,把现在的_update_time_plot完整代码发给我,我帮你检查!💪

在这里插入图片描述
Q5:现在可以看到单独1条线了,我竟然有点不知道下一步应该做什么
太好了!单通道成功!🎉 你现在处于关键转折点——基础功能都通了,接下来要选方向深入。


你现在掌握的技能

技能 成果
PyQt5窗口 创建、控件、布局
信号槽 跨线程通信、定时刷新
QThread 后台数据生成、暂停/继续
pyqtgraph 实时曲线、滚动显示
虚拟数据 模拟硬件、测试滤波

下一步选择(3个方向)

方向A:深入理解滤波器(推荐,最核心)

目标:真正看懂iirnotchfirwin的作用

任务

  1. 在虚拟数据里单独控制50Hz干扰的幅度
  2. 对比勾选/取消"滤波开启"的波形差异
  3. 修改NOTCH_FREQ = 50为其他值(如30),看滤波效果变化

代码修改(第265行):

# 添加滑块控制干扰幅度
self.interference_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.interference_slider.setRange(0, 100)
self.interference_slider.setValue(50)
self.interference_slider.valueChanged.connect(self._change_interference)

def _change_interference(self, value):
    self.serial_thread.virtual_noise = value  # 实时改噪声幅度

方向B:完整还原8通道EEG

目标:理解原代码的8通道架构

任务

  1. 恢复8通道显示,但分层排列(不重叠)
  2. 每个通道Y轴偏移,像真实脑电图那样

效果

CH1: ~~~~~~  (Y轴中心 +300)
CH2: ~~~~~~  (Y轴中心 +200)
CH3: ~~~~~~  (Y轴中心 +100)
...

方向C:数据保存与分析

目标:把虚拟数据存下来,用Python离线分析

任务

  1. 点击"保存"按钮,把当前1秒数据存为CSV
  2. matplotlib画静态图
  3. 计算FFT频谱,对比滤波前后

我的建议

选A!理由

  • 滤波是这份代码的核心技术点
  • 你现在能看到效果,是学习的最佳时机
  • 搞懂后,你能向他人解释"为什么用50Hz陷波"

立即开始(A方向)

第一步:添加噪声控制滑块

_setup_ui里添加(第395行附近):

# 噪声幅度滑块
self.noise_label = QtWidgets.QLabel("50Hz干扰:50")
ctrl.addWidget(self.noise_label)

self.noise_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.noise_slider.setRange(0, 100)
self.noise_slider.setValue(50)
self.noise_slider.valueChanged.connect(self._update_noise)
ctrl.addWidget(self.noise_slider)

添加回调函数:

def _update_noise(self, value):
    self.serial_thread.virtual_noise = value
    self.noise_label.setText(f"50Hz干扰:{value}")

运行后:拖动滑块,实时改变干扰强度,观察滤波效果!


选哪个方向?

或者你有其他想法(比如加FFT频谱显示改颜色主题)?

告诉我,帮你规划!💪

至此,我真的觉得AI帮助了我完成过去靠自己根本不可能完成的艰难任务。
全球科技通史的作者说:技术的进步可以叠加的。
我又想起博弈论中的正和游戏,技术的进步就很像正和游戏。太神奇了

更多推荐