normal update

This commit is contained in:
yutaov5 2025-08-24 20:27:54 +08:00
parent fc7f542f3b
commit 19170299bc
38 changed files with 305430 additions and 401 deletions

View File

@ -1,45 +1,29 @@
#include <SPI.h> #include <SPI.h>
#include "esp_timer.h" #include "esp_timer.h"
// 定义宏,简化寄存器配置 // ================== 宏定义区 ==================
#define CONFIG_1 0xD5 // ---------- 采样率配置 ----------
#define CONFIG_2 0xC0 #define CONFIG1_REG 0x95 // 单片模式500 SPS
#define CONFIG_3 0xEC #define CONFIG2_REG 0xC0 // 常规采样,无测试信号
#define CHnSET 0x60 #define CONFIG3_REG 0xEC // 内部参考、BIAS 缓冲开启
#define ENABLE_SRB1 0x20
#define BIAS_SENSP 0xFF
#define BIAS_SENSN 0xFF
#define LEAD_OFF_CURRENT 0x00
#define ENABLE_POSITIVE_LEAD_OFF 0x00
#define ENABLE_NEGATIVE_LEAD_OFF 0x00
// 定义模式常量 // ---------- 通道配置 ----------
#define MODE_CONTINUOUS_READ 1 #define CHnSET_REG 0x60 // 增益 24x普通电极输入
#define ENABLE_SRB1 0x20 // MISC1: 启用 SRB1
#define BIAS_SENSP 0xFF // BIAS 正端全开
#define BIAS_SENSN 0xFF // BIAS 负端全开
// ---------- 阻抗测量配置 ----------
#define LOFF_CONFIG 0x06 // 选择 6nAFLEAD_OFF=10(31.2Hz)
#define LOFF_SENSP 0xFF // 开启所有通道正端注入
#define LOFF_SENSN 0xFF // 开启所有通道负端注入
// ---------- 模式选择 ----------
#define MODE_CONTINUOUS_READ 1
#define MODE_IMPEDANCE_MEASURE 2 #define MODE_IMPEDANCE_MEASURE 2
#define MODE_SELF_TEST 3 #define MODE_SELF_TEST 3
int currentMode = MODE_CONTINUOUS_READ; // 当前模式 // ================== 引脚定义 ==================
volatile bool dataReady = false; // 标志位
double channelDataBuffer[9]; // 缓冲区,用于保存读取的数据
// 函数声明
void IRAM_ATTR onDRDYInterrupt(); // DRDY引脚中断服务函数
void setup();
void loop();
void startContinuousReadMode();
void startImpedanceMeasurementMode();
void startSelfTestMode();
void initADS1299();
void measureImpedance();
void getDeviceID();
void sendCommand(byte cmd);
void writeRegister(byte reg, byte value);
byte readRegister(byte reg);
void readData();
void convertData(byte *data, double *channelData);
// 定义引脚
#define CS_PIN A3 #define CS_PIN A3
#define SCLK_PIN SCK #define SCLK_PIN SCK
#define MOSI_PIN MOSI #define MOSI_PIN MOSI
@ -47,9 +31,10 @@ void convertData(byte *data, double *channelData);
#define DRDY_PIN A0 #define DRDY_PIN A0
#define START_PIN A2 #define START_PIN A2
#define RESET_PIN A1 #define RESET_PIN A1
// 定义命令
// ================== 命令定义 ==================
#define WAKEUP 0x02 #define WAKEUP 0x02
#define STANDBY 0x04 #define STANDBY 0x04
#define RESET 0x06 #define RESET 0x06
#define START 0x08 #define START 0x08
#define STOP 0x0A #define STOP 0x0A
@ -59,8 +44,26 @@ void convertData(byte *data, double *channelData);
#define RREG 0x20 #define RREG 0x20
#define WREG 0x40 #define WREG 0x40
// ================== 全局变量 ==================
volatile bool dataReady = false; // DRDY 中断标志
int currentMode = MODE_CONTINUOUS_READ;
double channelDataBuffer[9]; // 0=STATUS其余 8 个通道
// ================== 函数声明 ==================
void IRAM_ATTR onDRDYInterrupt();
void initADS1299();
void startContinuousReadMode();
void startImpedanceMeasurementMode();
void startSelfTestMode();
void readData();
void convertData(byte *data, double *channelData);
void sendCommand(byte cmd);
void writeRegister(byte reg, byte value);
byte readRegister(byte reg);
void getDeviceID();
// ================== setup ==================
void setup() { void setup() {
// 初始化串口
Serial.begin(115200); Serial.begin(115200);
// 初始化引脚 // 初始化引脚
@ -72,33 +75,31 @@ void setup() {
pinMode(START_PIN, OUTPUT); pinMode(START_PIN, OUTPUT);
pinMode(RESET_PIN, OUTPUT); pinMode(RESET_PIN, OUTPUT);
digitalWrite(CS_PIN, LOW); digitalWrite(CS_PIN, HIGH); // CS 默认拉高
digitalWrite(START_PIN, LOW); digitalWrite(START_PIN, LOW);
digitalWrite(RESET_PIN, HIGH); digitalWrite(RESET_PIN, HIGH);
delay(100); delay(100);
// 初始化SPI // 初始化 SPI
SPI.begin(SCLK_PIN, MISO_PIN, MOSI_PIN, CS_PIN); SPI.begin(SCLK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);
SPI.setBitOrder(MSBFIRST); SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE1));
SPI.setDataMode(SPI_MODE1);
SPI.setClockDivider(SPI_CLOCK_DIV8); // 约 5 MHz
// 初始化ADS1299 // 初始化 ADS1299
initADS1299(); initADS1299();
getDeviceID(); getDeviceID();
Serial.println("ADS1299 initialized"); Serial.println("ADS1299 初始化完成");
// 配置外部中断DRDY_PIN为低时触发中断 // 配置外部中断DRDY 下降沿触发
attachInterrupt(digitalPinToInterrupt(DRDY_PIN), onDRDYInterrupt, FALLING); attachInterrupt(digitalPinToInterrupt(DRDY_PIN), onDRDYInterrupt, FALLING);
currentMode = MODE_CONTINUOUS_READ; currentMode = MODE_CONTINUOUS_READ;
} }
// ================== loop ==================
void loop() { void loop() {
// 处理串口输入切换ADS1299功能 // 串口命令切换模式
if (Serial.available()) { if (Serial.available()) {
char cmd = Serial.read(); char cmd = Serial.read();
Serial.print("收到:");
Serial.println(cmd);
if (cmd == '1') { if (cmd == '1') {
currentMode = MODE_CONTINUOUS_READ; currentMode = MODE_CONTINUOUS_READ;
startContinuousReadMode(); startContinuousReadMode();
@ -109,55 +110,37 @@ void loop() {
currentMode = MODE_SELF_TEST; currentMode = MODE_SELF_TEST;
startSelfTestMode(); startSelfTestMode();
} }
} }
// 检查是否有数据准备好
// 有数据时读取
if (dataReady) { if (dataReady) {
// 清除标志位
dataReady = false; dataReady = false;
Serial.print("channel:");
// 打印数据 for (int i = 1; i <= 8; i++) {
Serial.print("Channel:"); Serial.print(channelDataBuffer[i], 6); // V 输出
for (int i = 0; i < 9; i++) { if (i < 8) Serial.print(",");
Serial.print(channelDataBuffer[i], 6);
if (i != 8) {
Serial.print(",");
} else {
Serial.println("");
}
} }
Serial.println();
} }
} }
// DRDY引脚的中断服务函数 // ================== 中断服务函数 ==================
void IRAM_ATTR onDRDYInterrupt() { void IRAM_ATTR onDRDYInterrupt() {
if (currentMode == MODE_CONTINUOUS_READ) { readData(); // 读取数据
readData(); // 读取数据
} else if (currentMode == MODE_IMPEDANCE_MEASURE) {
measureImpedance(); // 测量阻抗
} else if (currentMode == MODE_SELF_TEST) {
readData(); // 读取数据
}
} }
// ================== 模式配置 ==================
void startContinuousReadMode() { void startContinuousReadMode() {
sendCommand(RESET); sendCommand(RESET);
delay(100); delay(100);
sendCommand(SDATAC); sendCommand(SDATAC);
// 使用宏配置寄存器 writeRegister(0x01, CONFIG1_REG);
writeRegister(0x01, CONFIG_1); writeRegister(0x02, CONFIG2_REG);
writeRegister(0x02, CONFIG_2); writeRegister(0x03, CONFIG3_REG);
writeRegister(0x03, CONFIG_3); for (int i = 0x05; i <= 0x0C; i++) writeRegister(i, CHnSET_REG);
writeRegister(0x04, 0x00);
for (int i = 0x05; i <= 0x0C; i++) {
writeRegister(i, CHnSET);
}
writeRegister(0x0D, BIAS_SENSP); writeRegister(0x0D, BIAS_SENSP);
writeRegister(0x0E, BIAS_SENSN); writeRegister(0x0E, BIAS_SENSN);
writeRegister(0x15, ENABLE_SRB1); // 启用SRB1 writeRegister(0x15, ENABLE_SRB1);
// 启动数据连续读取
sendCommand(START); sendCommand(START);
sendCommand(RDATAC); sendCommand(RDATAC);
} }
@ -166,156 +149,108 @@ void startImpedanceMeasurementMode() {
sendCommand(RESET); sendCommand(RESET);
delay(100); delay(100);
sendCommand(SDATAC); sendCommand(SDATAC);
writeRegister(0x0F, LEAD_OFF_CURRENT); // 设置导联电流 writeRegister(0x04, LOFF_CONFIG);
writeRegister(0x18, ENABLE_POSITIVE_LEAD_OFF); // 启用正极导联检测 writeRegister(0x0F, LOFF_SENSP);
writeRegister(0x19, ENABLE_NEGATIVE_LEAD_OFF); // 启用负极导联检测 writeRegister(0x10, LOFF_SENSN);
for (int i = 0x05; i <= 0x0C; i++) writeRegister(i, CHnSET_REG);
sendCommand(START);
sendCommand(RDATAC);
Serial.println("阻抗测量模式已启用(注意需解调导联频率信号)");
} }
void startSelfTestMode() { void startSelfTestMode() {
sendCommand(RESET); sendCommand(RESET);
delay(100); delay(100);
sendCommand(SDATAC); sendCommand(SDATAC);
// 配置CONFIG2寄存器启用测试信号 1af 2hz writeRegister(0x01, 0x95); // 500SPS
writeRegister(0x01, 0xD5); writeRegister(0x02, 0xD1); // 开启测试信号
writeRegister(0x02, 0xD1); writeRegister(0x03, CONFIG3_REG);
writeRegister(0x03, 0xEC); for (int i = 0x05; i <= 0x0C; i++) writeRegister(i, 0x65); // 测试信号输入
writeRegister(0x04, 0x00); // MISC1寄存器启用SRB1 sendCommand(START);
// 配置所有通道输入为内部测试信号 sendCommand(RDATAC);
for (int i = 0x05; i <= 0x0C; i++) { Serial.println("自检模式已启用");
writeRegister(i, 0x65); // 设置每个通道为测试信号 }
}
// 启动数据连续读取 // ================== 初始化 ==================
void initADS1299() {
sendCommand(RESET);
delay(100);
sendCommand(SDATAC);
writeRegister(0x01, CONFIG1_REG);
writeRegister(0x02, CONFIG2_REG);
writeRegister(0x03, CONFIG3_REG);
for (int i = 0x05; i <= 0x0C; i++) writeRegister(i, CHnSET_REG);
writeRegister(0x0D, BIAS_SENSP);
writeRegister(0x0E, BIAS_SENSN);
writeRegister(0x15, ENABLE_SRB1);
// 默认关闭导联检测
writeRegister(0x04, 0x00);
writeRegister(0x0F, 0x00);
writeRegister(0x10, 0x00);
sendCommand(START); sendCommand(START);
sendCommand(RDATAC); sendCommand(RDATAC);
} }
void initADS1299() { // ================== 数据读取 ==================
sendCommand(RESET);
delay(100);
sendCommand(65);
// 使用宏定义配置寄存器
writeRegister(0x01, CONFIG_1);
writeRegister(0x02, CONFIG_2);
writeRegister(0x03, CONFIG_3);
writeRegister(0x04, 0x00); // MISC1寄存器
for (int i = 0x05; i <= 0x0C; i++) {
writeRegister(i, CHnSET); // 设置PGA增益和输入类型
}
writeRegister(0x0D, BIAS_SENSP);
writeRegister(0x0E, BIAS_SENSN);
writeRegister(0x15, ENABLE_SRB1); // 启用SRB1
sendCommand(START);
sendCommand(RDATAC); // 启动数据连续读取模式
}
void readData() { void readData() {
byte data[27]; byte data[27];
digitalWrite(CS_PIN, LOW); digitalWrite(CS_PIN, LOW);
delayMicroseconds(1); // 确保 tSDSU for (int i = 0; i < 27; i++) data[i] = SPI.transfer(0x00);
for (int i = 0; i < 27; i++) {
data[i] = SPI.transfer(0x00);
}
digitalWrite(CS_PIN, HIGH); digitalWrite(CS_PIN, HIGH);
delayMicroseconds(1); // 确保 tDSHD
// 转换数据,并将结果保存在全局缓冲区
convertData(data, channelDataBuffer); convertData(data, channelDataBuffer);
// 设置标志,表示数据已经准备好 // 设置标志,表示数据已经准备好
dataReady = true; dataReady = true;
} }
void measureImpedance() {
byte data[27];
digitalWrite(CS_PIN, LOW);
for (int i = 0; i < 27; i++) {
data[i] = SPI.transfer(0x00);
}
digitalWrite(CS_PIN, HIGH);
double channelData[9];
convertData(data, channelData);
Serial.print("Channel:");
for (int i = 0; i < 9; i++) {
Serial.print(channelData[i], 6);
if (i != 8) {
Serial.print(",");
} else {
Serial.println("");
}
}
Serial.print("Impedance Measurement:");
for (int i = 0; i < 8; i++) {
double voltage = channelData[i];
double current = 0.000006; // 假设使用6nA的导联电流
double impedance = voltage / current;
Serial.print(impedance, 2); // 打印阻抗值
if (i != 7) {
Serial.print(",");
} else {
Serial.println("");
}
}
}
void convertData(byte *data, double *channelData) { void convertData(byte *data, double *channelData) {
// 解析STATUS寄存器 // 解析 STATUS
long statusValue = ((long)data[0] << 16) | ((long)data[1] << 8) | data[2]; long statusValue = ((long)data[0] << 16) | ((long)data[1] << 8) | data[2];
channelData[0] = (double)statusValue; channelData[0] = (double)statusValue;
// 转换通道数据 // 转换通道
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
long value = ((long)data[3*i+3] << 16) | ((long)data[3*i+4] << 8) | data[3*i+5]; long raw = ((long)data[3*i+3] << 16) | ((long)data[3*i+4] << 8) | data[3*i+5];
if (raw & 0x800000) raw |= 0xFF000000; // 符号扩展
// 24位符号扩展 double vPerLSB = 4.5 / (24.0 * 8388608.0); // 单位 V
if (value & 0x800000) { channelData[i+1] = (double)raw * vPerLSB; // 输出电压 (V)
value |= 0xFF000000; // 负数扩展
}
// 电压计算 (考虑增益)
double vPerLSB = 4.5 / (24 * 8388608.0); // 1 LSB对应电压
channelData[i+1] = (double)value * vPerLSB;
} }
} }
// ================== 底层 SPI 操作 ==================
void sendCommand(byte cmd) { void sendCommand(byte cmd) {
digitalWrite(CS_PIN, LOW); digitalWrite(CS_PIN, LOW);
delayMicroseconds(1); // 确保 tSDSU
SPI.transfer(cmd); SPI.transfer(cmd);
digitalWrite(CS_PIN, HIGH); digitalWrite(CS_PIN, HIGH);
delayMicroseconds(1); // 确保 tSDSU
} }
void writeRegister(byte reg, byte value) { void writeRegister(byte reg, byte value) {
digitalWrite(CS_PIN, LOW); digitalWrite(CS_PIN, LOW);
delayMicroseconds(1); // 确保 tSDSU
SPI.transfer(WREG | reg); SPI.transfer(WREG | reg);
SPI.transfer(0x00); SPI.transfer(0x00);
SPI.transfer(value); SPI.transfer(value);
digitalWrite(CS_PIN, HIGH); digitalWrite(CS_PIN, HIGH);
delayMicroseconds(1); // 确保 tSDSU
} }
byte readRegister(byte reg) { byte readRegister(byte reg) {
digitalWrite(CS_PIN, LOW); digitalWrite(CS_PIN, LOW);
delayMicroseconds(1); // 确保 tSDSU
SPI.transfer(RREG | reg); SPI.transfer(RREG | reg);
SPI.transfer(0x00); SPI.transfer(0x00);
byte value = SPI.transfer(0x00); byte val = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH); digitalWrite(CS_PIN, HIGH);
delayMicroseconds(1); // 确保 tSDSU return val;
return value;
} }
void getDeviceID() { void getDeviceID() {
digitalWrite(CS_PIN, LOW); digitalWrite(CS_PIN, LOW);
delayMicroseconds(1); // 确保 tSDSU
SPI.transfer(SDATAC); SPI.transfer(SDATAC);
SPI.transfer(RREG | 0x00); SPI.transfer(RREG | 0x00);
SPI.transfer(0x00); SPI.transfer(0x00);
byte data = SPI.transfer(0x00); byte id = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH); digitalWrite(CS_PIN, HIGH);
delayMicroseconds(1); // 确保 tSDSU Serial.print("Device ID: 0b");
Serial.print("Device ID: "); Serial.println(id, BIN);
Serial.println(data, BIN); }
}

View File

@ -1,247 +1,571 @@
import sys import sys
import os
import re
import time import time
import queue
from collections import deque from collections import deque
from datetime import datetime, timedelta
import numpy as np import numpy as np
import re
from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5 import QtWidgets, QtCore, QtGui
import pyqtgraph as pg import pyqtgraph as pg
import serial import serial
import serial.tools.list_ports import serial.tools.list_ports
from qfluentwidgets import * from PyQt5.QtWidgets import QFrame
from qfluentwidgets import CheckBox, ComboBox, PushButton, LineEdit, SpinBox, InfoBar, InfoBarPosition
from qfluentwidgets import FluentIcon as FIF from qfluentwidgets import FluentIcon as FIF
from scipy.signal import butter, lfilter, lfilter_zi, iirnotch
# 配置参数 from scipy.signal import butter, filtfilt, lfilter, lfilter_zi, iirnotch, firwin, get_window
DATA_LENGTH = 2000 # 数据缓存长度 from scipy.signal import medfilt
PLOT_LENGTH = 1000 # 显示数据长度 # ================== 配置参数(可按需修改) ==================
FFT_LENGTH = 500 # FFT分析长度 SAMPLE_RATE = 500 # 采样率(与固件保持一致)
UPDATE_INTERVAL = 60 # 界面刷新间隔(ms) UPDATE_INTERVAL = 60 # UI刷新间隔 ms
SAMPLE_RATE = 500 # 采样率 DATA_LENGTH = 20000 # 环形缓存长度(每通道,增大以适应分时数据)
PLOT_LENGTH = 1000 # 时域显示长度
FFT_LENGTH = 500 # FFT长度点数
# 修改滤波器参数 # 滤波参数
NOTCH_FREQ = 50.0 # 陷波频率 NOTCH_FREQ = 50.0 # 保持50Hz陷波工频干扰
QUALITY_FACTOR = 10 # 品质因数 QUALITY_FACTOR = 30 # 提高品质因数更精准去除50Hz
LOW_CUTOFF = 2.0 # 带通滤波低频截止 LOW_CUTOFF = 0.5 # 降低低频截止,保留更慢的变化
HIGH_CUTOFF = 40.0 # 带通滤波高频截止 HIGH_CUTOFF = 80.0 # 提高高频截止,保留方波边沿
FIR_ORDER = 101 # FIR滤波器阶数 FIR_ORDER = 61 # 适当降低FIR阶数减少延迟
MEDIAN_WINDOW = 800 # 中值滤波窗口大小
# 中值滤波窗口(针对方波信号优化)
MEDIAN_WINDOW_SIZE = 5 # 5-7点方波信号专用
# 显示参数
DEFAULT_Y_LIM_UV = 100.0 # 固定Y轴半幅±μV
# ================== 日志配置 ==================
LOG_DIR = "logs" # 日志保存目录
LOG_MAX_SIZE_MB = 100 # 单个日志文件最大大小MB
LOG_MAX_AGE_HOURS = 24 # 日志最大时长(小时)
LOG_FLUSH_SEC = 1.0 # 日志刷新间隔(秒)
LOG_BATCH_LINES = 1000 # 每批写入的行数
LOG_QUEUE_MAX = 10000 # 日志队列最大长度(防内存溢出)
class ChannelBuffer:
"""
修复满缓冲后波形消失问题
核心思路
1. x_axis 从每个通道实际最晚时间戳减去 n保证不会超出 buffer
2. np.interp 时限制插值范围在已有数据区间
"""
def __init__(self, capacity=20000):
self.capacity = capacity
self.data = [np.zeros(capacity, dtype=np.float32) for _ in range(8)]
self.timestamps = [np.zeros(capacity, dtype=np.int64) for _ in range(8)]
self.indices = [0]*8
self.is_full = [False]*8
def append(self, channel_id, values: list, start_index=None):
if channel_id < 0 or channel_id >= 8 or len(values) == 0:
return
n = len(values)
idx = self.indices[channel_id]
ts = np.arange(start_index, start_index + n) if start_index is not None else np.arange(idx, idx + n)
if n > self.capacity:
values = values[-self.capacity:]
ts = ts[-self.capacity:]
n = self.capacity
for i in range(n):
self.data[channel_id][idx] = values[i]
self.timestamps[channel_id][idx] = ts[i]
idx = (idx + 1) % self.capacity
if idx == 0:
self.is_full[channel_id] = True
self.indices[channel_id] = idx
def get_recent(self, n=PLOT_LENGTH):
"""
获取最近 n 个点的数据
修复20000点满后波形消失问题
1. 不依赖全局索引计算x轴
2. 始终返回连续数组
"""
y_data = np.full((8, n), np.nan, dtype=np.float32)
x_axis = np.arange(n) # 绘图直接用 0~n-1
for ch in range(8):
idx = self.indices[ch]
if self.is_full[ch]:
# 已满取环形缓冲最后n个点
data = np.concatenate((self.data[ch][idx:], self.data[ch][:idx]))
else:
# 未满,直接取已有数据
data = self.data[ch][:idx]
# 截取最后n个点
y_data[ch, -len(data):] = data[-n:]
return x_axis, y_data
# ================== 异步日志写入线程 ==================
class LogWriterThread(QtCore.QThread):
"""
异步日志写入线程解决以下问题
1. 避免UI线程因写日志而卡顿
2. 支持日志自动分片按大小/时间
3. 防止日志队列无限增长导致内存溢出
4. 高效批量写入减少I/O操作次数
"""
log_rotated = QtCore.pyqtSignal(str) # 发出新日志文件路径
def __init__(self, base_dir: str, log_queue: queue.Queue):
super().__init__()
self.base_dir = base_dir
self.log_queue = log_queue
self.running = False
self._f = None
self._current_path = None
self._start_time = None
self._line_count = 0
self._file_size = 0 # 当前文件字节数
def _new_log_file(self):
"""创建新日志文件,添加头信息"""
now = datetime.now()
filename = f"EEG_log_{now.strftime('%Y%m%d_%H%M%S')}.csv"
self._current_path = os.path.join(self.base_dir, filename)
os.makedirs(self.base_dir, exist_ok=True)
# 使用大缓冲区减少I/O次数
self._f = open(self._current_path, "w", encoding="utf-8", buffering=1024 * 1024)
# 写入头信息
self._f.write(f"# YuEEG 日志 - {now.strftime('%Y-%m-%d %H:%M:%S')}\n")
self._f.write("# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8\n")
self._f.flush()
self._start_time = now
self._line_count = 0
self._file_size = 0
self.log_rotated.emit(self._current_path) # 通知UI
def _should_rotate(self):
"""判断是否需要切换到新日志文件"""
if self._f is None:
return True
# 按大小
if self._file_size > LOG_MAX_SIZE_MB * 1024 * 1024:
return True
# 按时间
if datetime.now() - self._start_time > timedelta(hours=LOG_MAX_AGE_HOURS):
return True
return False
def run(self):
"""主线程:从队列取数据,批量写入磁盘"""
self.running = True
self._new_log_file() # 初始化第一个文件
batch = [] # 批量缓存
last_flush = time.monotonic()
while self.running:
try:
item = self.log_queue.get(timeout=0.2) # 最多等0.2秒
except queue.Empty:
item = None
if item is None:
# 超时或收到停止信号
if batch:
content = "\n".join(batch) + "\n"
self._f.write(content)
self._file_size += len(content.encode('utf-8'))
batch.clear()
self._f.flush()
if not self.running:
break
else:
# 检查是否需要切分日志
if self._should_rotate():
if batch:
self._f.write("\n".join(batch) + "\n")
batch.clear()
self._f.flush()
self._f.close()
self._new_log_file() # 创建新文件
batch.append(item)
self._line_count += 1
# 批量写入
if len(batch) >= LOG_BATCH_LINES:
content = "\n".join(batch) + "\n"
self._f.write(content)
self._file_size += len(content.encode('utf-8'))
batch.clear()
# 定期 flush
now = time.monotonic()
if now - last_flush >= LOG_FLUSH_SEC:
self._f.flush()
last_flush = now
# 结束前最后 flush
if batch:
self._f.write("\n".join(batch) + "\n")
self._f.flush()
self._f.close()
def stop(self):
"""安全停止线程"""
self.running = False
try:
self.log_queue.put_nowait(None) # 唤醒阻塞的 get()
except Exception:
pass
self.wait(2000)
# ================== 串口线程(修复分时数据问题) ==================
class SerialThread(QtCore.QThread): class SerialThread(QtCore.QThread):
data_received = QtCore.pyqtSignal(str) data_received = QtCore.pyqtSignal(str) # 原始数据行(用于状态栏)
def __init__(self, com_port): def __init__(self, com_port: str, log_queue: queue.Queue):
super().__init__() super().__init__()
self.com_port = com_port self.com_port = com_port
self.filter_flag = True
self.serial_port = None self.serial_port = None
self.running = False self.running = False
self.data_buffer = [[] for _ in range(9)] self.log_queue = log_queue # 日志队列
self.timestamps = []
self.write_index = 0
self.max_data_length = 1_000_000
# 初始化滤波器 # 使用通道专用缓冲区(修复核心问题)
self.buffer = ChannelBuffer(capacity=DATA_LENGTH)
self.global_index = 0 # 全局时间索引
# 过滤开关
self.filter_enabled = True
self.median_enabled = False # 中值滤波开关UI 控制)
# 滤波器初始化
self._init_filters() self._init_filters()
# -------- 滤波器设计与状态 --------
def _init_filters(self): def _init_filters(self):
# 设计50Hz陷波滤波器
nyquist = 0.5 * SAMPLE_RATE nyquist = 0.5 * SAMPLE_RATE
freq = NOTCH_FREQ / nyquist
self.b_notch, self.a_notch = iirnotch(freq, QUALITY_FACTOR)
self.zi_notch = [lfilter_zi(self.b_notch, self.a_notch) for _ in range(9)]
# 设计FIR带通滤波器 # 1) 50Hz 陷波提高Q值
from scipy.signal import firwin w0 = NOTCH_FREQ / nyquist
self.fir_coeff = firwin(FIR_ORDER, self.b_notch, self.a_notch = iirnotch(w0, QUALITY_FACTOR)
[LOW_CUTOFF/nyquist, HIGH_CUTOFF/nyquist], self.zi_notch = [lfilter_zi(self.b_notch, self.a_notch) for _ in range(8)]
pass_zero=False)
self.zi_fir = [np.zeros(FIR_ORDER - 1) for _ in range(9)]
# 中值滤波缓冲区
self.median_buffers = [deque(maxlen=MEDIAN_WINDOW) for _ in range(9)]
# 2) 方波优化带通(更宽通带)
self.fir_coeff = firwin(FIR_ORDER, [LOW_CUTOFF / nyquist, HIGH_CUTOFF / nyquist], pass_zero=False)
self.zi_fir = [np.zeros(FIR_ORDER - 1) for _ in range(8)]
# 3) 中值滤波缓冲(针对方波优化)
self.median_buffers = [deque(maxlen=MEDIAN_WINDOW_SIZE) for _ in range(8)]
# -------- 主循环:读串口,解析,入缓冲 --------
def run(self): def run(self):
self.running = True self.running = True
try: try:
self.serial_port = serial.Serial(self.com_port, baudrate=115200, timeout=1) self.serial_port = serial.Serial("COM4", baudrate=115200, timeout=1)
self.serial_port.write(b'1') # 启动数据传输 # 默认进入连续采样模式
try:
self.serial_port.write(b'1')
except Exception:
pass
while self.running: while self.running:
if self.serial_port.in_waiting: chunk = self.serial_port.readline()
try: if not chunk:
raw_data = self.serial_port.readline().decode('utf-8', errors='ignore').strip() continue
self.data_received.emit(raw_data) try:
# 使用正则表达式验证数据格式 raw = chunk.decode('utf-8', errors='ignore').strip()
if re.match(r'^Channel:(-?\d+\.?\d*,){8}-?\d+\.?\d*$', raw_data): # print("RAW:", raw)
values = list(map(float, raw_data.split('Channel:')[1].split(','))) except Exception:
self._update_buffer(values) continue
except Exception as e:
print(f"Error processing data: {e}") if not raw:
continue
# 1. 原始数据送入日志队列(异步写盘)
try:
# 尝试解析为8通道数据成功则格式化为CSV
values = self._parse_line(raw)
if values and len(values) == 8:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
log_line = f"{timestamp},{','.join(map(str, values))}"
self.log_queue.put_nowait(log_line)
else:
# 无法解析的行,原样记录
self.log_queue.put_nowait(f"# {raw}")
except queue.Full:
# 队列满则丢弃,防止阻塞采集
pass
# 2. 限频更新状态栏避免UI卡顿
self.data_received.emit(raw)
# 3. 尝试解析并处理数据
self._process_serial_data(raw)
finally: finally:
if self.serial_port and self.serial_port.is_open: if self.serial_port and self.serial_port.is_open:
self.serial_port.close() self.serial_port.close()
def _update_buffer(self, values): # -------- 处理串口数据一次发8通道--------
filtered_values = [] def _process_serial_data(self, text: str):
for i in range(9): """处理串口数据一次性8通道针对方波信号优化"""
# 1. 50Hz陷波滤波 values = self._parse_line(text)
notch_filtered, self.zi_notch[i] = lfilter( if values is None or len(values) != 8:
self.b_notch, return
self.a_notch,
[values[i]],
zi=self.zi_notch[i]
)
# 2. FIR带通滤波 # 遍历8个通道逐一滤波并写入buffer
fir_filtered, self.zi_fir[i] = lfilter( for ch in range(8):
self.fir_coeff, val = values[ch]
[1.0],
notch_filtered,
zi=self.zi_fir[i]
)
# 3. 中值滤波去基线 if self.filter_enabled:
self.median_buffers[i].append(fir_filtered[0]) # ===== 方波信号专用优化滤波 =====
# 当缓冲区满时计算中值 # 1) 中值滤波优先(去除脉冲噪声,保留方波边沿)
if len(self.median_buffers[i]) >= MEDIAN_WINDOW: if self.median_enabled:
median = np.median(self.median_buffers[i]) # 添加当前值到中值缓冲区
filtered_values.append(fir_filtered[0] - median) self.median_buffers[ch].append(val)
else:
filtered_values.append(fir_filtered[0])
# 更新数据缓冲区 # 当缓冲区满时计算中值窗口大小建议5-7点
while len(self.timestamps) >= self.max_data_length: if len(self.median_buffers[ch]) >= self.median_buffers[ch].maxlen:
for ch in self.data_buffer: # 使用中值滤波去除脉冲噪声
ch.pop(0) median_val = float(np.median(self.median_buffers[ch]))
self.timestamps.pop(0) val = median_val # 直接使用中值(去除了脉冲)
for i in range(9): # 2) 带通滤波(针对方波优化)
if self.filter_flag: # 注意这里保留IIR滤波用于实时处理但调整了参数
self.data_buffer[i].append(filtered_values[i]) y1, self.zi_notch[ch] = lfilter(
else: self.b_notch, self.a_notch, [val], zi=self.zi_notch[ch]
self.data_buffer[i].append(values[i]) )
self.timestamps.append(self.write_index)
self.write_index += 1
# 方波信号带通优化:更宽的通带,保留更多谐波成分
y2, self.zi_fir[ch] = lfilter(
self.fir_coeff, [1.0], y1, zi=self.zi_fir[ch]
)
val = float(y2[0])
# 写入对应通道的缓冲区一次1个点
self.buffer.append(ch, [val], self.global_index)
# 每行数据只增加一次全局时间索引
self.global_index += 1
# -------- 行解析:从任意前缀中抽取浮点数 --------
def _parse_line(self, text: str):
"""
兼容形如
"通道数据(μV): v1,v2,...,v8"
或任意含冒号/空格后跟 8 个逗号分隔浮点数的行
提取前 8 个数并返回μV
"""
if ":" in text or "" in text:
part = re.split(r'[:]', text, maxsplit=1)[1]
else:
part = text
nums = re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', part)
if len(nums) < 8:
return None
# 转换为浮点数
try:
return list(map(float, nums))
except ValueError:
return None
# -------- 提供绘图数据 --------
def get_plot_data(self, length=PLOT_LENGTH): def get_plot_data(self, length=PLOT_LENGTH):
valid_length = min(len(self.timestamps), length) """
start = max(0, len(self.timestamps) - valid_length) 获取最近 length 个点的数据
x_axis = np.arange(start, start + valid_length) 修复20000点满后波形消失问题
data = [np.array(ch[start:start + valid_length]) for ch in self.data_buffer] 1. 使用ChannelBuffer安全x轴
return x_axis, np.array(data) 2. np.interp插值在已有数据范围内
"""
return self.buffer.get_recent(length)
def get_fft_data(self): def get_fft_data(self, length=FFT_LENGTH):
return self.get_plot_data(FFT_LENGTH) """
获取FFT数据直接用安全的get_plot_data
"""
return self.get_plot_data(length)
def get_last_data(self, num): # -------- 外部控制 --------
"""获取最新的num个点数据返回形状为(9, num)的ndarray""" def set_filter_enabled(self, on: bool):
data = [] self.filter_enabled = on
for ch in self.data_buffer:
if len(ch) >= num: def set_median_enabled(self, on: bool):
data.append(np.array(ch[-num:])) self.median_enabled = on
else:
padding = np.full(num - len(ch), np.nan) def send_mode(self, key: str):
data.append(np.concatenate([padding, ch])) """向固件发送模式:'1'连续、'2'阻抗、'3'自检"""
return np.array(data) try:
if self.serial_port and self.serial_port.is_open:
self.serial_port.write(key.encode('ascii'))
except Exception:
pass
def stop(self): def stop(self):
self.running = False self.running = False
self.wait(2000) self.wait(2000)
# ================== 主窗口(微调) ==================
class ADCPlotter(QtWidgets.QMainWindow): class ADCPlotter(QtWidgets.QMainWindow):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("YuEEG Data Viewer") self.setWindowTitle("YuEEG Data Viewer")
self.setWindowIcon(QtGui.QIcon('./school_logo.ico')) self.setWindowIcon(QtGui.QIcon('./school_logo.ico'))
# 全局绘图配置
pg.setConfigOptions(antialias=True, useOpenGL=True)
# ========= 1. 创建日志系统 =========
self.log_queue = queue.Queue(maxsize=LOG_QUEUE_MAX)
self.log_thread = LogWriterThread(LOG_DIR, self.log_queue)
self.log_thread.log_rotated.connect(self._on_log_rotated)
self.log_thread.start()
self._setup_ui() self._setup_ui()
if not (com_port := self._detect_com_port()):
com_port = self._detect_com_port()
if not com_port:
QtWidgets.QMessageBox.critical(self, "错误", "未检测到可用串口") QtWidgets.QMessageBox.critical(self, "错误", "未检测到可用串口")
# 停止日志线程
self.log_thread.stop()
sys.exit(1) sys.exit(1)
self.serial_thread = SerialThread(com_port)
self.serial_thread.data_received.connect(self._append_serial_data) self.serial_thread = SerialThread(com_port, self.log_queue)
self.serial_thread.data_received.connect(self._on_serial_line)
self.serial_thread.start() self.serial_thread.start()
self._start_timers() self._start_timers()
# 显示当前日志文件
self._on_log_rotated(self.log_thread._current_path)
# 默认启用自动Y轴更适合稀疏信号
self.auto_y_box.setChecked(True)
self._on_auto_y_changed()
# -------- UI 构建 --------
def _setup_ui(self): def _setup_ui(self):
self.central_widget = QtWidgets.QWidget() cw = QtWidgets.QWidget()
self.setCentralWidget(self.central_widget) self.setCentralWidget(cw)
main_layout = QtWidgets.QVBoxLayout(self.central_widget) main = QtWidgets.QVBoxLayout(cw)
main.setContentsMargins(8, 8, 8, 8)
main.setSpacing(8)
# 创建绘图区域 # 绘图区
self.plot_container = QtWidgets.QWidget()
self.plot_layout = QtWidgets.QVBoxLayout(self.plot_container)
self.plot_layout.setContentsMargins(0, 0, 0, 0)
self.plot_layout.setSpacing(0)
# 时域图表配置
self.time_plot = pg.PlotWidget() self.time_plot = pg.PlotWidget()
self.time_plot.setAntialiasing(True)
self.time_plot.useOpenGL(True)
self.time_plot.setLabel('bottom', 'Samples')
self.time_plot.setLabel('left', 'Amplitude')
self.time_plot.showGrid(x=True, y=True, alpha=0.3)
# 频域图表配置
self.fft_plot = pg.PlotWidget() self.fft_plot = pg.PlotWidget()
self.fft_plot.setAntialiasing(True) for p in (self.time_plot, self.fft_plot):
self.fft_plot.useOpenGL(True) p.showGrid(x=True, y=True, alpha=0.3)
self.time_plot.setLabel('bottom', 'Samples')
self.time_plot.setLabel('left', 'Amplitude (μV)')
self.fft_plot.setLabel('bottom', 'Frequency (Hz)') self.fft_plot.setLabel('bottom', 'Frequency (Hz)')
self.fft_plot.setLabel('left', 'Amplitude') self.fft_plot.setLabel('left', 'Amplitude (a.u.)')
self.fft_plot.showGrid(x=True, y=True, alpha=0.3)
self.plot_layout.addWidget(self.time_plot) main.addWidget(self.time_plot)
self.plot_layout.addWidget(self.fft_plot) main.addWidget(self.fft_plot)
main_layout.addWidget(self.plot_container)
# 初始化曲线 # 曲线8条
colors = ['#FF0000', '#00FF00', '#0000FF', '#00FFFF', palette = ['#FF3B30', '#34C759', '#007AFF', '#5AC8FA',
'#FF00FF', '#FFFF00', '#FFFFFF', '#A0A0A0', '#FFA500'] '#AF52DE', '#FFCC00', '#FF9500', '#8E8E93']
self.time_curves = [self.time_plot.plot(pen=color) for color in colors] self.time_curves = [self.time_plot.plot(pen=pg.mkPen(color=c, width=1.5)) for c in palette]
self.fft_curves = [self.fft_plot.plot(pen=color) for color in colors] self.fft_curves = [self.fft_plot.plot(pen=pg.mkPen(color=c, width=1.2)) for c in palette]
# 控制面板 # 控制栏
control_layout = QtWidgets.QHBoxLayout() ctrl = QtWidgets.QHBoxLayout()
self.checkboxes = [CheckBox(f"CH{i + 1}") for i in range(9)] main.addLayout(ctrl)
for cb in self.checkboxes: # 我想在这里加一条分割线作为ctrl和串口数据的分割线但写的好像不对
# 创建一条水平分割线
line = QFrame()
line.setFrameShape(QFrame.HLine) # 水平线
line.setFrameShadow(QFrame.Sunken) # 阴影效果(看起来是“凹下去”的)
line.setStyleSheet("color: #cccccc;") # 设置颜色(可选)
main.addWidget(line)
# 通道勾选
self.ch_boxes = [CheckBox(f"CH{i + 1}") for i in range(8)]
for cb in self.ch_boxes:
cb.setChecked(True) cb.setChecked(True)
control_layout.addWidget(cb) ctrl.addWidget(cb)
# 显示模式选择 # 显示模式
self.mode_selector = ComboBox() self.mode_combo = ComboBox()
self.mode_selector.addItems(["波形", "频谱", "并列"]) self.mode_combo.addItems(["波形", "频谱", "并列"])
self.mode_selector.currentIndexChanged.connect(self.update_display_mode) self.mode_combo.currentIndexChanged.connect(self._update_display_mode)
control_layout.addWidget(self.mode_selector) ctrl.addWidget(self.mode_combo)
# 功能模式切换按钮 # 滤波开关
self.fun_selector = PushButton("标准") self.filter_box = CheckBox("滤波开启")
self.fun_selector.clicked.connect(self.fun_change) self.filter_box.setChecked(True)
control_layout.addWidget(self.fun_selector) self.filter_box.stateChanged.connect(
# 暂停按钮 lambda s: self.serial_thread.set_filter_enabled(self.filter_box.isChecked()))
self.pause_button = PushButton("暂停") ctrl.addWidget(self.filter_box)
self.pause_button.clicked.connect(self.toggle_pause)
control_layout.addWidget(self.pause_button)
main_layout.addLayout(control_layout) # 中值滤波开关
self.checkboxes[0].setChecked(False) self.median_box = CheckBox("中值滤波(去基线)")
self.update_display_mode() self.median_box.setChecked(False)
self.median_box.stateChanged.connect(
lambda s: self.serial_thread.set_median_enabled(self.median_box.isChecked()))
ctrl.addWidget(self.median_box)
# Y轴控制
self.auto_y_box = CheckBox("自动Y轴")
self.auto_y_box.setChecked(False) # 注意初始化后会在__init__中设为True
self.auto_y_box.stateChanged.connect(self._on_auto_y_changed)
ctrl.addWidget(self.auto_y_box)
self.y_spin = SpinBox()
self.y_spin.setRange(10, 1000)
self.y_spin.setValue(int(DEFAULT_Y_LIM_UV))
self.y_spin.valueChanged.connect(self._apply_y_range)
ctrl.addWidget(self.y_spin)
# 模式切换按钮(标准/测试)
self.fun_btn = PushButton("标准")
self.fun_btn.clicked.connect(self._toggle_fun_mode)
ctrl.addWidget(self.fun_btn)
# 暂停
self.pause_btn = PushButton("暂停")
self.pause_btn.clicked.connect(self._toggle_pause)
ctrl.addWidget(self.pause_btn)
# 初始显示
self._update_display_mode()
self._apply_y_range()
def _start_timers(self): def _start_timers(self):
self.plot_timer = QtCore.QTimer() self.plot_timer = QtCore.QTimer(self)
self.plot_timer.timeout.connect(self.update_plots) self.plot_timer.timeout.connect(self._refresh_plots)
self.plot_timer.start(UPDATE_INTERVAL) self.plot_timer.start(UPDATE_INTERVAL)
def fun_change(self): # -------- 事件处理 --------
if self.fun_selector.text() == "测试": def _toggle_fun_mode(self):
self.serial_thread.serial_port.write(b'1') # 与固件保持一致:'1' 连续、'3' 自检
self.serial_thread.filter_flag = True if self.fun_btn.text() == "标准":
self.fun_selector.setText("标准") self.serial_thread.send_mode('3') # 切到自检
elif self.fun_selector.text() == "标准": self.serial_thread.set_filter_enabled(False)
self.serial_thread.serial_port.write(b'3') self.fun_btn.setText("测试")
self.serial_thread.filter_flag = False self._info("已切换到自检信号(测试)")
self.fun_selector.setText("测试") else:
self.serial_thread.send_mode('1') # 回到连续采样
self.serial_thread.set_filter_enabled(True)
self.fun_btn.setText("标准")
self._info("已切换到标准采样")
def update_display_mode(self): def _toggle_pause(self):
mode = self.mode_selector.currentText() if self.plot_timer.isActive():
self.plot_timer.stop()
self.pause_btn.setText("继续")
else:
self.plot_timer.start()
self.pause_btn.setText("暂停")
def _update_display_mode(self):
mode = self.mode_combo.currentText()
if mode == "波形": if mode == "波形":
self.time_plot.show() self.time_plot.show()
self.fft_plot.hide() self.fft_plot.hide()
@ -252,89 +576,150 @@ class ADCPlotter(QtWidgets.QMainWindow):
self.time_plot.show() self.time_plot.show()
self.fft_plot.show() self.fft_plot.show()
def update_plots(self): def _on_auto_y_changed(self):
mode = self.mode_selector.currentText() if self.auto_y_box.isChecked():
if mode in ["波形", "并列"]: self.time_plot.enableAutoRange(axis='y', enable=True)
self.update_time_plot() else:
if mode in ["频谱", "并列"]: self.time_plot.enableAutoRange(axis='y', enable=False)
self.update_fft_plot() self._apply_y_range()
def update_time_plot(self): def _apply_y_range(self):
x_axis, data = self.serial_thread.get_plot_data() if not self.auto_y_box.isChecked():
lim = float(self.y_spin.value())
self.time_plot.setYRange(-lim, +lim)
def _refresh_plots(self):
mode = self.mode_combo.currentText()
if mode in ("波形", "并列"):
self._update_time_plot()
if mode in ("频谱", "并列"):
self._update_fft_plot()
def _update_time_plot(self):
x, data = self.serial_thread.get_plot_data(PLOT_LENGTH)
if data.size == 0: if data.size == 0:
return return
active = [i for i, cb in enumerate(self.checkboxes) if cb.isChecked()]
active = [i for i, cb in enumerate(self.ch_boxes) if cb.isChecked()]
if not active: if not active:
return return
# 更新时域曲线
for i in active: for i in active:
self.time_curves[i].setData(x_axis, data[i]) self.time_curves[i].setData(x, data[i])
# 自动滚动X轴 # 自动滚动X轴
self.time_plot.setXRange(x_axis[-1] - PLOT_LENGTH, x_axis[-1]) if len(x) > 0:
self.time_plot.setXRange(x[0], x[-1])
# 自动调整Y轴范围 # 自动Y轴范围若开启
valid_data = data[active] if self.auto_y_box.isChecked():
y_min = np.nanmin(valid_data) # 只考虑有数据的区域
y_max = np.nanmax(valid_data) mask = ~np.isnan(data)
if not np.isnan(y_min) and not np.isnan(y_max): if np.any(mask):
margin = (y_max - y_min) * 0.1 or 1.0 valid_data = data[mask]
self.time_plot.setYRange(y_min - margin, y_max + margin) 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)
def update_fft_plot(self): def _update_fft_plot(self):
x_axis, data = self.serial_thread.get_fft_data() x, data = self.serial_thread.get_fft_data(FFT_LENGTH)
if data.size == 0: if data.size == 0:
return return
fs = SAMPLE_RATE fs = SAMPLE_RATE
max_amp = 0 win = get_window('hann', data.shape[1])
for i in range(9): freq = np.fft.rfftfreq(len(win), 1.0 / fs)
if not self.checkboxes[i].isChecked():
max_amp = 0.0
for i in range(8):
if not self.ch_boxes[i].isChecked():
self.fft_curves[i].setData([], [])
continue continue
# 计算FFT
signal = data[i, :] sig = data[i].astype(float)
fft = np.abs(np.fft.rfft(signal)) # 跳过全NaN的情况
freq = np.fft.rfftfreq(len(signal), 1 / fs) if np.all(np.isnan(sig)) or len(sig) < 8:
# 限制频率范围 self.fft_curves[i].setData([], [])
mask = (freq >= 3) & (freq <= 50) continue
self.fft_curves[i].setData(freq[mask], fft[mask])
current_max = np.max(fft[mask]) # 处理NaN值
if current_max > max_amp: sig = np.nan_to_num(sig)
max_amp = current_max sig = sig - np.mean(sig)
# 调整Y轴范围 sig = sig * win
amp = np.abs(np.fft.rfft(sig))
# 仅展示 3~50 Hz
mask = (freq >= 3.0) & (freq <= 50.0)
self.fft_curves[i].setData(freq[mask], amp[mask])
if amp[mask].size > 0:
max_amp = max(max_amp, float(np.max(amp[mask])))
if max_amp > 0: if max_amp > 0:
self.fft_plot.setYRange(0, max_amp * 1.1) self.fft_plot.setYRange(0, max_amp * 1.1)
else:
self.fft_plot.enableAutoRange('y')
def _append_serial_data(self, text): def _on_serial_line(self, text: str):
# 状态栏里看最后200字符
try: try:
self.statusBar().showMessage(text[-200:], 2000) self.statusBar().showMessage(text[-200:], 1500)
except: except Exception:
pass pass
def _on_log_rotated(self, path: str):
"""日志切片通知,更新状态栏"""
filename = os.path.basename(path)
self.statusBar().showMessage(f"日志: {filename}", 3000)
# -------- 串口发现 --------
def _detect_com_port(self): def _detect_com_port(self):
ports = serial.tools.list_ports.comports() ports = serial.tools.list_ports.comports()
# 优先匹配常见关键字
for p in ports: for p in ports:
if 'USB' in p.description: desc = (p.description or '').lower()
if 'usb' in desc or 'ch340' in desc or 'cp210' in desc or 'silabs' in desc or 'uart' in desc or 'serial' in desc:
return p.device return p.device
return None # 兜底返回第一个
return ports[0].device if ports else None
def toggle_pause(self): def _info(self, msg: str):
if self.plot_timer.isActive(): try:
self.plot_timer.stop() InfoBar.success(
self.pause_button.setText("继续") title='提示',
else: content=msg,
self.plot_timer.start() orient=QtCore.Qt.Horizontal,
self.pause_button.setText("暂停") isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=1500,
parent=self
)
except Exception:
# 若 qfluentwidgets 不可用则忽略
pass
def closeEvent(self, event): def closeEvent(self, event):
self.serial_thread.stop() """窗口关闭时清理资源"""
# 先停止串口线程
try:
self.serial_thread.stop()
except Exception:
pass
# 再停止日志线程(确保数据完整写入)
try:
self.log_thread.stop()
except Exception:
pass
event.accept() event.accept()
# ================== 程序入口 ==================
if __name__ == '__main__': if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
window = ADCPlotter() window = ADCPlotter()
window.resize(1200, 800)
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 18:46:40
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 18:46:40
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 18:49:54
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 18:49:54
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 18:51:09
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 18:51:09
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 18:51:42
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 18:51:42
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 18:53:10
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 18:53:10
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 18:58:55
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 18:58:55
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 19:05:18
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 19:05:18
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 19:10:20
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 19:10:20
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 19:26:23
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 19:26:23
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 19:52:40
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 19:52:40
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 20:06:53
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 20:06:53
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 20:12:33
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 20:12:33
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# YuEEG 日志 - 2025-08-24 20:15:49
# 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8
1 # YuEEG 日志 - 2025-08-24 20:15:49
2 # 时间戳,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8

File diff suppressed because it is too large Load Diff