mirror of
https://github.com/YuTaoV5/YuEEG.git
synced 2025-11-04 14:50:03 +08:00
Part One
This commit is contained in:
commit
e60b90d2cf
BIN
Hardware/ADS1299.zip
Normal file
BIN
Hardware/ADS1299.zip
Normal file
Binary file not shown.
277
Hardware/arduino/sketch_jun15a.ino
Normal file
277
Hardware/arduino/sketch_jun15a.ino
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
#include <SPI.h>
|
||||||
|
#include "esp_timer.h"
|
||||||
|
|
||||||
|
// 定义引脚
|
||||||
|
#define CS_PIN 10
|
||||||
|
#define SCLK_PIN 12
|
||||||
|
#define MOSI_PIN 11
|
||||||
|
#define MISO_PIN 13
|
||||||
|
#define DRDY_PIN 15
|
||||||
|
#define CLKSEL_PIN 16
|
||||||
|
#define START_PIN 18
|
||||||
|
#define RESET_PIN 8
|
||||||
|
#define PWDN_PIN 3
|
||||||
|
|
||||||
|
// 定义命令
|
||||||
|
#define WAKEUP 0x02
|
||||||
|
#define STANDBY 0x04
|
||||||
|
#define RESET 0x06
|
||||||
|
#define START 0x08
|
||||||
|
#define STOP 0x0A
|
||||||
|
#define RDATAC 0x10
|
||||||
|
#define SDATAC 0x11
|
||||||
|
#define RDATA 0x12
|
||||||
|
#define RREG 0x20
|
||||||
|
#define WREG 0x40
|
||||||
|
|
||||||
|
volatile boolean startRead = false;
|
||||||
|
volatile boolean readLeadOff = false;
|
||||||
|
volatile boolean continuousReadMode = true;
|
||||||
|
|
||||||
|
esp_timer_handle_t dataTimer;
|
||||||
|
esp_timer_handle_t impedanceTimer;
|
||||||
|
|
||||||
|
void IRAM_ATTR onDataTimer(void* arg) {
|
||||||
|
if (digitalRead(DRDY_PIN) == LOW) {
|
||||||
|
startRead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR onImpedanceTimer(void* arg) {
|
||||||
|
readLeadOffStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// 初始化串口
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
// 初始化引脚
|
||||||
|
pinMode(CS_PIN, OUTPUT);
|
||||||
|
pinMode(SCLK_PIN, OUTPUT);
|
||||||
|
pinMode(MOSI_PIN, OUTPUT);
|
||||||
|
pinMode(MISO_PIN, INPUT);
|
||||||
|
pinMode(DRDY_PIN, INPUT);
|
||||||
|
pinMode(CLKSEL_PIN, OUTPUT);
|
||||||
|
pinMode(START_PIN, OUTPUT);
|
||||||
|
pinMode(RESET_PIN, OUTPUT);
|
||||||
|
pinMode(PWDN_PIN, OUTPUT);
|
||||||
|
|
||||||
|
// 初始化SPI
|
||||||
|
SPI.begin(SCLK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);
|
||||||
|
SPI.setBitOrder(MSBFIRST);
|
||||||
|
SPI.setDataMode(SPI_MODE1);
|
||||||
|
SPI.setClockDivider(SPI_CLOCK_DIV4);
|
||||||
|
|
||||||
|
// 初始化ADS1299
|
||||||
|
initADS1299();
|
||||||
|
getDeviceID();
|
||||||
|
Serial.println("ADS1299 初始化完成");
|
||||||
|
|
||||||
|
// 配置ESP32定时器
|
||||||
|
const esp_timer_create_args_t dataTimer_args = {
|
||||||
|
.callback = &onDataTimer,
|
||||||
|
.name = "ADS1299 Data Timer"
|
||||||
|
};
|
||||||
|
|
||||||
|
const esp_timer_create_args_t impedanceTimer_args = {
|
||||||
|
.callback = &onImpedanceTimer,
|
||||||
|
.name = "ADS1299 Impedance Timer"
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_timer_create(&dataTimer_args, &dataTimer);
|
||||||
|
esp_timer_create(&impedanceTimer_args, &impedanceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (Serial.available()) {
|
||||||
|
char cmd = Serial.read();
|
||||||
|
if (cmd == '1') {
|
||||||
|
startContinuousReadMode();
|
||||||
|
} else if (cmd == '2') {
|
||||||
|
startLeadOffDetectionMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startRead) {
|
||||||
|
startRead = false;
|
||||||
|
readData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void startContinuousReadMode() {
|
||||||
|
continuousReadMode = true;
|
||||||
|
readLeadOff = false;
|
||||||
|
// 复位芯片
|
||||||
|
sendCommand(RESET);
|
||||||
|
delay(100);
|
||||||
|
sendCommand(SDATAC); // 停止数据连续读取
|
||||||
|
// 配置寄存器
|
||||||
|
writeRegister(0x01, 0x96); // CONFIG1 设置数据速率为1kSPS
|
||||||
|
writeRegister(0x02, 0xD0); // CONFIG2 内部参考电压和偏置电流
|
||||||
|
writeRegister(0x03, 0xE0); // CONFIG3 启用偏置驱动器
|
||||||
|
for (int i = 0x05; i <= 0x0C; i++) {
|
||||||
|
writeRegister(i, 0x60); // CHxSET 设置PGA增益和输入类型
|
||||||
|
}
|
||||||
|
writeRegister(0x0D, 0xFF); // BIAS_SENSP
|
||||||
|
writeRegister(0x0E, 0xFF); // BIAS_SENSN
|
||||||
|
// 启动连续数据读取模式
|
||||||
|
sendCommand(START);
|
||||||
|
sendCommand(RDATAC); // 启动数据连续读取模式
|
||||||
|
esp_timer_start_periodic(dataTimer, 1000); // 1000微秒 = 1毫秒
|
||||||
|
esp_timer_stop(impedanceTimer); // 停止阻抗读取定时器
|
||||||
|
}
|
||||||
|
|
||||||
|
void startLeadOffDetectionMode() {
|
||||||
|
continuousReadMode = false;
|
||||||
|
readLeadOff = true;
|
||||||
|
// 复位芯片
|
||||||
|
sendCommand(RESET);
|
||||||
|
delay(100);
|
||||||
|
sendCommand(SDATAC); // 停止数据连续读取
|
||||||
|
// 配置导联脱落检测
|
||||||
|
writeRegister(0x0F, 0x02); // LOFF寄存器,设置为6nA
|
||||||
|
writeRegister(0x18, 0xFF); // 启用所有通道的正极导联检测
|
||||||
|
writeRegister(0x19, 0xFF); // 启用所有通道的负极导联检测
|
||||||
|
esp_timer_start_periodic(impedanceTimer, 100000); // 100000微秒 = 100毫秒
|
||||||
|
esp_timer_stop(dataTimer); // 停止数据读取定时器
|
||||||
|
}
|
||||||
|
|
||||||
|
void initADS1299() {
|
||||||
|
// 启动时序
|
||||||
|
digitalWrite(CLKSEL_PIN, HIGH);
|
||||||
|
digitalWrite(CS_PIN, LOW);
|
||||||
|
digitalWrite(START_PIN, LOW);
|
||||||
|
digitalWrite(RESET_PIN, HIGH);
|
||||||
|
digitalWrite(PWDN_PIN, HIGH);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
// 复位芯片
|
||||||
|
sendCommand(RESET);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
// 停止连续读取模式
|
||||||
|
sendCommand(SDATAC);
|
||||||
|
|
||||||
|
// 配置寄存器
|
||||||
|
writeRegister(0x01, 0x96); // CONFIG1 设置数据速率为1kSPS
|
||||||
|
writeRegister(0x02, 0xD0); // CONFIG2 内部参考电压和偏置电流
|
||||||
|
writeRegister(0x03, 0xE0); // CONFIG3 启用偏置驱动器
|
||||||
|
for (int i = 0x05; i <= 0x0C; i++) {
|
||||||
|
writeRegister(i, 0x60); // CHxSET 设置PGA增益和输入类型
|
||||||
|
}
|
||||||
|
writeRegister(0x0D, 0xFF); // BIAS_SENSP
|
||||||
|
writeRegister(0x0E, 0xFF); // BIAS_SENSN
|
||||||
|
// 启动连续数据读取模式
|
||||||
|
sendCommand(START);
|
||||||
|
sendCommand(RDATAC);
|
||||||
|
}
|
||||||
|
|
||||||
|
void readLeadOffStatus() {
|
||||||
|
byte statP = readRegister(0x1C); // 读取LOFF_STATP寄存器
|
||||||
|
byte statN = readRegister(0x1D); // 读取LOFF_STATN寄存器
|
||||||
|
|
||||||
|
Serial.print("Lead-Off Status:");
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
bool pStatus = statP & (1 << i);
|
||||||
|
bool nStatus = statN & (1 << i);
|
||||||
|
Serial.print(pStatus ? "Off" : "On");
|
||||||
|
if (i != 7) {
|
||||||
|
Serial.print(",");
|
||||||
|
} else {
|
||||||
|
Serial.println("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 读取导联检测结果
|
||||||
|
// readLeadOffImpedance();
|
||||||
|
}
|
||||||
|
|
||||||
|
void readLeadOffImpedance() {
|
||||||
|
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("Lead-Off Impedance:");
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
Serial.print(channelData[i], 6);
|
||||||
|
if (i != 7) {
|
||||||
|
Serial.print(",");
|
||||||
|
} else {
|
||||||
|
Serial.println("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void getDeviceID() {
|
||||||
|
digitalWrite(CS_PIN, LOW); // 低电平以进行通信
|
||||||
|
SPI.transfer(SDATAC); // 停止连续读取数据模式
|
||||||
|
SPI.transfer(RREG | 0x00); // 读取寄存器命令
|
||||||
|
SPI.transfer(0x00); // 请求一个字节
|
||||||
|
byte data = SPI.transfer(0x00); // 读取字节
|
||||||
|
digitalWrite(CS_PIN, HIGH); // 高电平以结束通信
|
||||||
|
Serial.print("Device ID: ");
|
||||||
|
Serial.println(data, BIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendCommand(byte cmd) {
|
||||||
|
digitalWrite(CS_PIN, LOW);
|
||||||
|
SPI.transfer(cmd);
|
||||||
|
digitalWrite(CS_PIN, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeRegister(byte reg, byte value) {
|
||||||
|
digitalWrite(CS_PIN, LOW);
|
||||||
|
SPI.transfer(WREG | reg);
|
||||||
|
SPI.transfer(0x00); // 写一个寄存器
|
||||||
|
SPI.transfer(value);
|
||||||
|
digitalWrite(CS_PIN, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte readRegister(byte reg) {
|
||||||
|
digitalWrite(CS_PIN, LOW);
|
||||||
|
SPI.transfer(RREG | reg);
|
||||||
|
SPI.transfer(0x00); // 读取一个寄存器
|
||||||
|
byte value = SPI.transfer(0x00);
|
||||||
|
digitalWrite(CS_PIN, HIGH);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void readData() {
|
||||||
|
digitalWrite(CS_PIN, LOW);
|
||||||
|
byte data[27];
|
||||||
|
for (int i = 0; i < 27; i++) {
|
||||||
|
data[i] = SPI.transfer(0x00);
|
||||||
|
}
|
||||||
|
digitalWrite(CS_PIN, HIGH);
|
||||||
|
|
||||||
|
// 转换为9通道的double数据
|
||||||
|
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("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertData(byte *data, double *channelData) {
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
long value = ((long)data[3 * i + 3] << 16) | ((long)data[3 * i + 4] << 8) | data[3 * i + 5];
|
||||||
|
if (value & 0x800000) { // 如果最高位为1,则为负数
|
||||||
|
value |= 0xFF000000; // 扩展符号位
|
||||||
|
}
|
||||||
|
channelData[i] = (double)value * 4.5 / (double)0x7FFFFF; // 将值转换为电压
|
||||||
|
}
|
||||||
|
}
|
||||||
213
Interface/main.py
Normal file
213
Interface/main.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
# coding:utf-8
|
||||||
|
import sys
|
||||||
|
from PyQt5.QtCore import Qt, QRect, QUrl
|
||||||
|
from PyQt5.QtGui import QIcon, QPainter, QImage, QBrush, QColor, QFont, QDesktopServices
|
||||||
|
from PyQt5.QtWidgets import QApplication, QFrame, QStackedWidget, QHBoxLayout, QLabel
|
||||||
|
|
||||||
|
from qfluentwidgets import (NavigationInterface, NavigationItemPosition, NavigationWidget, MessageBox,
|
||||||
|
isDarkTheme, setTheme, Theme, setThemeColor, qrouter, FluentWindow, NavigationAvatarWidget)
|
||||||
|
from qfluentwidgets import FluentIcon as FIF
|
||||||
|
from qframelesswindow import FramelessWindow, StandardTitleBar
|
||||||
|
from plot_card import *
|
||||||
|
|
||||||
|
class Widget(QFrame):
|
||||||
|
|
||||||
|
def __init__(self, text: str, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self.label = QLabel(text, self)
|
||||||
|
self.label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.hBoxLayout = QHBoxLayout(self)
|
||||||
|
self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter)
|
||||||
|
self.setObjectName(text.replace(' ', '-'))
|
||||||
|
|
||||||
|
|
||||||
|
class Window(FramelessWindow):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setTitleBar(StandardTitleBar(self))
|
||||||
|
|
||||||
|
# use dark theme mode
|
||||||
|
setTheme(Theme.DARK)
|
||||||
|
|
||||||
|
# change the theme color
|
||||||
|
# setThemeColor('#0078d4')
|
||||||
|
self.hBoxLayout = QHBoxLayout(self)
|
||||||
|
self.navigationInterface = NavigationInterface(self, showMenuButton=True)
|
||||||
|
self.stackWidget = QStackedWidget(self)
|
||||||
|
# channels_to_display = [0, 1, 2, 3, 4, 5, 6, 7] # Example control parameter list
|
||||||
|
channels_to_display = [2]
|
||||||
|
pen_colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k', 'w', 'orange']
|
||||||
|
pen_widths = [2, 2, 2, 2, 2, 2, 2, 2, 2]
|
||||||
|
plot_num = 1000
|
||||||
|
# create sub interface
|
||||||
|
self.tfplot = TFwindow(channels_to_display, pen_colors, pen_widths, plot_num)
|
||||||
|
self.time_plot = TimeDomainPlot2(channels_to_display, pen_colors, pen_widths, plot_num)
|
||||||
|
self.leadoff = StatusGrid()
|
||||||
|
self.ser_frame = SerialCommunication()
|
||||||
|
self.vtk_demo = VTKWidget()
|
||||||
|
|
||||||
|
self.folderInterface = Widget('Folder Interface', self)
|
||||||
|
self.settingInterface = Widget('Setting Interface', self)
|
||||||
|
self.albumInterface = Widget('Album Interface', self)
|
||||||
|
self.albumInterface1 = Widget('Album Interface 1', self)
|
||||||
|
self.albumInterface2 = Widget('Album Interface 2', self)
|
||||||
|
self.albumInterface1_1 = Widget('Album Interface 1-1', self)
|
||||||
|
|
||||||
|
# initialize layout
|
||||||
|
self.initLayout()
|
||||||
|
|
||||||
|
# add items to navigation interface
|
||||||
|
self.initNavigation()
|
||||||
|
|
||||||
|
self.initWindow()
|
||||||
|
|
||||||
|
self.ser_frame.open_button.clicked.connect(self.open_port)
|
||||||
|
|
||||||
|
def open_port(self):
|
||||||
|
if self.ser_frame.serial_port and self.ser_frame.serial_port.is_open:
|
||||||
|
self.ser_frame.serial_port.close()
|
||||||
|
self.ser_frame.serial_port = None
|
||||||
|
self.ser_frame.open_button.setText("Open Port")
|
||||||
|
self.ser_frame.send_button.setEnabled(False)
|
||||||
|
else:
|
||||||
|
port = self.ser_frame.port_combobox.currentText()
|
||||||
|
baud_rate = int(self.ser_frame.baud_combobox.currentText())
|
||||||
|
try:
|
||||||
|
self.ser_frame.serial_port = serial.Serial(port, baud_rate, timeout=1)
|
||||||
|
self.ser_frame.open_button.setText("Close Port")
|
||||||
|
self.ser_frame.send_button.setEnabled(True)
|
||||||
|
self.read_thread = SerialReader(self.ser_frame.serial_port)
|
||||||
|
self.read_thread.data_received.connect(self.ser_frame.receive_data)
|
||||||
|
self.read_thread.status_received.connect(self.ser_frame.receive_data)
|
||||||
|
self.read_thread.data_received.connect(self.tfplot.update_plots)
|
||||||
|
self.read_thread.data_received.connect(self.time_plot.update_plot)
|
||||||
|
self.read_thread.status_received.connect(self.leadoff.update_status)
|
||||||
|
self.read_thread.start()
|
||||||
|
except serial.SerialException as e:
|
||||||
|
self.ser_frame.receive_text.append(f"Error opening port: {e}")
|
||||||
|
|
||||||
|
def initLayout(self):
|
||||||
|
self.hBoxLayout.setSpacing(0)
|
||||||
|
self.hBoxLayout.setContentsMargins(0, self.titleBar.height(), 0, 0)
|
||||||
|
self.hBoxLayout.addWidget(self.navigationInterface)
|
||||||
|
self.hBoxLayout.addWidget(self.stackWidget)
|
||||||
|
self.hBoxLayout.setStretchFactor(self.stackWidget, 1)
|
||||||
|
|
||||||
|
def initNavigation(self):
|
||||||
|
# enable acrylic effect
|
||||||
|
# self.navigationInterface.setAcrylicEnabled(True)
|
||||||
|
|
||||||
|
self.addSubInterface(self.vtk_demo, FIF.PHOTO, 'PCB')
|
||||||
|
self.addSubInterface(self.tfplot, FIF.MARKET, 'Data')
|
||||||
|
self.addSubInterface(self.leadoff, FIF.TILES, 'Impedance')
|
||||||
|
self.addSubInterface(self.ser_frame, FIF.CHAT, 'Chat')
|
||||||
|
self.addSubInterface(self.time_plot, FIF.ALIGNMENT, 'Data2')
|
||||||
|
self.navigationInterface.addSeparator()
|
||||||
|
|
||||||
|
self.addSubInterface(self.albumInterface, FIF.ALBUM, 'Albums', NavigationItemPosition.SCROLL)
|
||||||
|
self.addSubInterface(self.albumInterface1, FIF.ALBUM, 'Album 1', parent=self.albumInterface)
|
||||||
|
self.addSubInterface(self.albumInterface1_1, FIF.ALBUM, 'Album 1.1', parent=self.albumInterface1)
|
||||||
|
self.addSubInterface(self.albumInterface2, FIF.ALBUM, 'Album 2', parent=self.albumInterface)
|
||||||
|
|
||||||
|
# add navigation items to scroll area
|
||||||
|
self.addSubInterface(self.folderInterface, FIF.FOLDER, 'Folder library', NavigationItemPosition.SCROLL)
|
||||||
|
|
||||||
|
# add custom widget to bottom
|
||||||
|
self.navigationInterface.addWidget(
|
||||||
|
routeKey='avatar',
|
||||||
|
widget=NavigationAvatarWidget('YuTaoV5', './resource/my_logo.jpg'),
|
||||||
|
onClick=self.showMessageBox,
|
||||||
|
position=NavigationItemPosition.BOTTOM,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.addSubInterface(self.settingInterface, FIF.SETTING, 'Settings', NavigationItemPosition.BOTTOM)
|
||||||
|
|
||||||
|
# !IMPORTANT: don't forget to set the default route key if you enable the return button
|
||||||
|
# qrouter.setDefaultRouteKey(self.stackWidget, self.musicInterface.objectName())
|
||||||
|
|
||||||
|
# set the maximum width
|
||||||
|
# self.navigationInterface.setExpandWidth(300)
|
||||||
|
|
||||||
|
self.stackWidget.currentChanged.connect(self.onCurrentInterfaceChanged)
|
||||||
|
self.stackWidget.setCurrentIndex(0)
|
||||||
|
|
||||||
|
# always expand
|
||||||
|
# self.navigationInterface.setCollapsible(False)
|
||||||
|
|
||||||
|
|
||||||
|
def initWindow(self):
|
||||||
|
self.resize(900, 700)
|
||||||
|
self.setWindowIcon(QIcon('./resource/school_logo.ico'))
|
||||||
|
self.setWindowTitle('ADS1299上位机')
|
||||||
|
self.titleBar.setAttribute(Qt.WA_StyledBackground)
|
||||||
|
|
||||||
|
desktop = QApplication.desktop().availableGeometry()
|
||||||
|
w, h = desktop.width(), desktop.height()
|
||||||
|
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
|
||||||
|
|
||||||
|
# NOTE: set the minimum window width that allows the navigation panel to be expanded
|
||||||
|
# self.navigationInterface.setMinimumExpandWidth(900)
|
||||||
|
# self.navigationInterface.expand(useAni=False)
|
||||||
|
|
||||||
|
self.setQss()
|
||||||
|
|
||||||
|
def addSubInterface(self, interface, icon, text: str, position=NavigationItemPosition.TOP, parent=None):
|
||||||
|
""" add sub interface """
|
||||||
|
self.stackWidget.addWidget(interface)
|
||||||
|
self.navigationInterface.addItem(
|
||||||
|
routeKey=interface.objectName(),
|
||||||
|
icon=icon,
|
||||||
|
text=text,
|
||||||
|
onClick=lambda: self.switchTo(interface),
|
||||||
|
position=position,
|
||||||
|
tooltip=text,
|
||||||
|
parentRouteKey=parent.objectName() if parent else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def setQss(self):
|
||||||
|
color = 'dark' if isDarkTheme() else 'light'
|
||||||
|
with open(f'resource/{color}/demo.qss', encoding='utf-8') as f:
|
||||||
|
self.setStyleSheet(f.read())
|
||||||
|
|
||||||
|
def switchTo(self, widget):
|
||||||
|
self.stackWidget.setCurrentWidget(widget)
|
||||||
|
|
||||||
|
def onCurrentInterfaceChanged(self, index):
|
||||||
|
widget = self.stackWidget.widget(index)
|
||||||
|
self.navigationInterface.setCurrentItem(widget.objectName())
|
||||||
|
print(widget.objectName())
|
||||||
|
if self.ser_frame.serial_port and self.ser_frame.serial_port.is_open:
|
||||||
|
if index == 1:
|
||||||
|
data = "1"
|
||||||
|
self.ser_frame.serial_port.write(data.encode('utf-8'))
|
||||||
|
if index == 2:
|
||||||
|
data = "2"
|
||||||
|
self.ser_frame.serial_port.write(data.encode('utf-8'))
|
||||||
|
|
||||||
|
# !IMPORTANT: This line of code needs to be uncommented if the return button is enabled
|
||||||
|
# qrouter.push(self.stackWidget, widget.objectName())
|
||||||
|
|
||||||
|
def showMessageBox(self):
|
||||||
|
w = MessageBox(
|
||||||
|
'ADS1299上位机',
|
||||||
|
'所有硬件资料以及软件即将全部开源🚀',
|
||||||
|
self
|
||||||
|
)
|
||||||
|
w.yesButton.setText('给个Star😘')
|
||||||
|
w.cancelButton.setText('残忍拒绝OrZ')
|
||||||
|
|
||||||
|
if w.exec():
|
||||||
|
QDesktopServices.openUrl(QUrl("https://github.com/YuTaoV5/ADS1299_EEG"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||||
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||||
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
|
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
w = Window()
|
||||||
|
w.show()
|
||||||
|
app.exec_()
|
||||||
490
Interface/plot_card.py
Normal file
490
Interface/plot_card.py
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
import sys
|
||||||
|
import serial
|
||||||
|
import threading
|
||||||
|
import numpy as np
|
||||||
|
from PyQt5.QtGui import QPalette, QColor, QFont
|
||||||
|
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QFrame, QGridLayout, QLabel, QHBoxLayout
|
||||||
|
from PyQt5.QtCore import pyqtSignal, QThread, QTimer, Qt
|
||||||
|
from pyqtgraph import PlotWidget, mkPen, ViewBox, TextItem
|
||||||
|
from qfluentwidgets import TransparentPushButton, ComboBox, LineEdit, TextEdit, PushButton
|
||||||
|
import serial.tools.list_ports
|
||||||
|
from scipy.signal import find_peaks
|
||||||
|
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
|
||||||
|
import vtkmodules.all as vtk
|
||||||
|
from scipy.signal import iirnotch, lfilter
|
||||||
|
|
||||||
|
class SerialReader(QThread):
|
||||||
|
data_received = pyqtSignal(list)
|
||||||
|
status_received = pyqtSignal(list)
|
||||||
|
|
||||||
|
def __init__(self, serial_port):
|
||||||
|
super().__init__()
|
||||||
|
self.serial_port = serial_port
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self.running:
|
||||||
|
line = self.serial_port.readline().decode('utf-8').strip()
|
||||||
|
if line.startswith("Channel:"):
|
||||||
|
if self.is_valid_data(line):
|
||||||
|
data = list(map(float, line.split(":")[1].split(",")))
|
||||||
|
self.data_received.emit(data)
|
||||||
|
elif line.startswith("Lead-Off Status:"):
|
||||||
|
status = line.split(":")[1].split(",")
|
||||||
|
self.status_received.emit(status)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
self.serial_port.close()
|
||||||
|
|
||||||
|
def is_valid_data(self, line):
|
||||||
|
if not line.startswith("Channel:"):
|
||||||
|
return False
|
||||||
|
parts = line.split(":")[1].split(",")
|
||||||
|
return len(parts) == 9
|
||||||
|
|
||||||
|
class SerialCommunication(QFrame):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.initUI()
|
||||||
|
self.serial_port = None
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
self.setObjectName("Chat")
|
||||||
|
|
||||||
|
port_layout = QHBoxLayout()
|
||||||
|
port_label = TransparentPushButton("Port:", self)
|
||||||
|
self.port_combobox = ComboBox()
|
||||||
|
self.refresh_ports()
|
||||||
|
port_layout.addWidget(port_label)
|
||||||
|
port_layout.addWidget(self.port_combobox)
|
||||||
|
|
||||||
|
baud_layout = QHBoxLayout()
|
||||||
|
baud_label = TransparentPushButton("Baud Rate:", self)
|
||||||
|
self.baud_combobox = ComboBox()
|
||||||
|
self.baud_combobox.addItems(["115200", "9600", "250000", "500000", "1000000"])
|
||||||
|
baud_layout.addWidget(baud_label)
|
||||||
|
baud_layout.addWidget(self.baud_combobox)
|
||||||
|
|
||||||
|
self.send_text = LineEdit()
|
||||||
|
self.receive_text = TextEdit()
|
||||||
|
self.receive_text.setReadOnly(True)
|
||||||
|
|
||||||
|
self.open_button = PushButton("Open Port")
|
||||||
|
self.send_button = PushButton("Send")
|
||||||
|
self.send_button.clicked.connect(self.send_data)
|
||||||
|
self.send_button.setEnabled(False)
|
||||||
|
|
||||||
|
sendV_layout = QVBoxLayout()
|
||||||
|
sendV_layout.addWidget(self.send_text)
|
||||||
|
sendV_layout.addWidget(self.send_button)
|
||||||
|
send_layout = QHBoxLayout()
|
||||||
|
send_layout.addWidget(TransparentPushButton("Send:", self))
|
||||||
|
send_layout.addLayout(sendV_layout)
|
||||||
|
|
||||||
|
recV_layout = QVBoxLayout()
|
||||||
|
recV_layout.addWidget(self.receive_text)
|
||||||
|
recV_layout.addWidget(self.open_button)
|
||||||
|
rec_layout = QHBoxLayout()
|
||||||
|
rec_layout.addWidget(TransparentPushButton("Receive:", self))
|
||||||
|
rec_layout.addLayout(recV_layout)
|
||||||
|
|
||||||
|
layout.addLayout(port_layout)
|
||||||
|
layout.addLayout(baud_layout)
|
||||||
|
layout.addLayout(send_layout)
|
||||||
|
layout.addLayout(rec_layout)
|
||||||
|
layout.setContentsMargins(30, 50, 20, 20)
|
||||||
|
layout.setSpacing(10)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def refresh_ports(self):
|
||||||
|
ports = serial.tools.list_ports.comports()
|
||||||
|
self.port_combobox.clear()
|
||||||
|
for port in ports:
|
||||||
|
self.port_combobox.addItem(port.device)
|
||||||
|
|
||||||
|
def open_port(self):
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
self.serial_port.close()
|
||||||
|
self.serial_port = None
|
||||||
|
self.open_button.setText("Open Port")
|
||||||
|
self.send_button.setEnabled(False)
|
||||||
|
else:
|
||||||
|
port = self.port_combobox.currentText()
|
||||||
|
baud_rate = int(self.baud_combobox.currentText())
|
||||||
|
try:
|
||||||
|
self.serial_port = serial.Serial(port, baud_rate, timeout=1)
|
||||||
|
self.open_button.setText("Close Port")
|
||||||
|
self.send_button.setEnabled(True)
|
||||||
|
self.read_thread = SerialReader(self.serial_port)
|
||||||
|
self.read_thread.data_received.connect(self.receive_data)
|
||||||
|
self.read_thread.status_received.connect(self.receive_data)
|
||||||
|
self.read_thread.start()
|
||||||
|
except serial.SerialException as e:
|
||||||
|
self.receive_text.append(f"Error opening port: {e}")
|
||||||
|
|
||||||
|
def send_data(self):
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
data = self.send_text.text()
|
||||||
|
self.serial_port.write(data.encode('utf-8'))
|
||||||
|
|
||||||
|
def receive_data(self, data):
|
||||||
|
self.receive_text.append(str(data))
|
||||||
|
|
||||||
|
class CustomViewBox(ViewBox):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.setMouseMode(self.RectMode)
|
||||||
|
|
||||||
|
def wheelEvent(self, ev, axis=None):
|
||||||
|
if axis is None:
|
||||||
|
axis = [0, 1]
|
||||||
|
ev.accept()
|
||||||
|
if ev.delta() > 0:
|
||||||
|
scale_factor = 0.9
|
||||||
|
else:
|
||||||
|
scale_factor = 1.1
|
||||||
|
self.scaleBy((scale_factor, 1), center=(0, 0))
|
||||||
|
|
||||||
|
class StatusGrid(QFrame):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.initUI()
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
self.layout = QGridLayout()
|
||||||
|
self.squares = []
|
||||||
|
self.setObjectName("Impedance")
|
||||||
|
for i in range(2):
|
||||||
|
row = []
|
||||||
|
for j in range(4):
|
||||||
|
label = QLabel(self)
|
||||||
|
label.setText(f"通道{i * 4 + j + 1}")
|
||||||
|
label.setAutoFillBackground(True)
|
||||||
|
palette = label.palette()
|
||||||
|
palette.setColor(QPalette.Window, QColor('red'))
|
||||||
|
label.setPalette(palette)
|
||||||
|
# Using QFont to set bold and font size
|
||||||
|
font = QFont()
|
||||||
|
font.setBold(True)
|
||||||
|
font.setPointSize(16) # Set the font size to 16
|
||||||
|
label.setFont(font)
|
||||||
|
label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.layout.addWidget(label, i, j)
|
||||||
|
row.append(label)
|
||||||
|
self.squares.append(row)
|
||||||
|
self.layout.setContentsMargins(30, 50, 20, 20)
|
||||||
|
self.layout.setSpacing(50)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def update_status(self, status_list):
|
||||||
|
for i in range(2):
|
||||||
|
for j in range(4):
|
||||||
|
palette = self.squares[i][j].palette()
|
||||||
|
color = QColor('blue') if status_list[i * 4 + j] == 'On' else QColor('red')
|
||||||
|
palette.setColor(QPalette.Window, color)
|
||||||
|
self.squares[i][j].setPalette(palette)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeDomainPlot(QFrame):
|
||||||
|
def __init__(self, channels_to_display, pen_colors, pen_widths, num_plots):
|
||||||
|
super().__init__()
|
||||||
|
self.channels_to_display = channels_to_display
|
||||||
|
self.pen_colors = pen_colors
|
||||||
|
self.pen_widths = pen_widths
|
||||||
|
self.initUI()
|
||||||
|
self.data_buffer = np.zeros((9, num_plots)) # Buffer for data points for 9 channels
|
||||||
|
|
||||||
|
self.update_timer = QTimer()
|
||||||
|
self.update_timer.timeout.connect(self.refresh_plot)
|
||||||
|
self.update_timer.start(20) # 50Hz refresh rate
|
||||||
|
|
||||||
|
# Design notch filters for 49.5Hz, 50Hz, and 50.5Hz
|
||||||
|
fs = 250 # Sampling frequency
|
||||||
|
f0_1 = 49.5 # First frequency to be removed from signal
|
||||||
|
f0_2 = 50.0 # Second frequency to be removed from signal
|
||||||
|
f0_3 = 50.5 # Third frequency to be removed from signal
|
||||||
|
Q = 30 # Quality factor
|
||||||
|
w0_1 = f0_1 / (fs / 2) # Normalized Frequency for 49.5Hz
|
||||||
|
w0_2 = f0_2 / (fs / 2) # Normalized Frequency for 50Hz
|
||||||
|
w0_3 = f0_3 / (fs / 2) # Normalized Frequency for 50.5Hz
|
||||||
|
self.b1, self.a1 = iirnotch(w0_1, Q)
|
||||||
|
self.b2, self.a2 = iirnotch(w0_2, Q)
|
||||||
|
self.b3, self.a3 = iirnotch(w0_3, Q)
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
self.plot_widget = PlotWidget()
|
||||||
|
self.layout.addWidget(self.plot_widget)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.setObjectName("TimeDomainPlot")
|
||||||
|
self.plots = [self.plot_widget.plot(pen=mkPen(color=self.pen_colors[i], width=self.pen_widths[i])) for i in range(9)]
|
||||||
|
self.plot_widget.setYRange(-10, 10) # Initial Y range
|
||||||
|
|
||||||
|
def update_plot(self, data):
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
self.data_buffer[i] = np.roll(self.data_buffer[i], -1)
|
||||||
|
self.data_buffer[i][-1] = data[i]
|
||||||
|
|
||||||
|
# Apply notch filters to the entire data_buffer for each channel
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
filtered_data = lfilter(self.b1, self.a1, self.data_buffer[i])
|
||||||
|
filtered_data = lfilter(self.b2, self.a2, filtered_data)
|
||||||
|
filtered_data = lfilter(self.b3, self.a3, filtered_data)
|
||||||
|
self.data_buffer[i] = filtered_data
|
||||||
|
|
||||||
|
def refresh_plot(self):
|
||||||
|
half_buffer_length = self.data_buffer.shape[1] // 2
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
self.plots[i].setData(self.data_buffer[i, -half_buffer_length:])
|
||||||
|
|
||||||
|
max_y = np.max(self.data_buffer[self.channels_to_display, -half_buffer_length:])
|
||||||
|
min_y = np.min(self.data_buffer[self.channels_to_display, -half_buffer_length:])
|
||||||
|
self.plot_widget.setYRange(min_y, max_y)
|
||||||
|
|
||||||
|
class TimeDomainPlot2(QFrame):
|
||||||
|
def __init__(self, channels_to_display, pen_colors, pen_widths, num_plots):
|
||||||
|
super().__init__()
|
||||||
|
self.channels_to_display = channels_to_display
|
||||||
|
self.pen_colors = pen_colors
|
||||||
|
self.pen_widths = pen_widths
|
||||||
|
self.initUI()
|
||||||
|
self.data_buffer = np.zeros((9, num_plots)) # Buffer for data points for 9 channels
|
||||||
|
|
||||||
|
self.update_timer = QTimer()
|
||||||
|
self.update_timer.timeout.connect(self.refresh_plot)
|
||||||
|
self.update_timer.start(20) # 50Hz refresh rate
|
||||||
|
|
||||||
|
# Design notch filters for 49.5Hz, 50Hz, and 50.5Hz
|
||||||
|
fs = 250 # Sampling frequency
|
||||||
|
f0_1 = 49.5 # First frequency to be removed from signal
|
||||||
|
f0_2 = 50.0 # Second frequency to be removed from signal
|
||||||
|
f0_3 = 50.5 # Third frequency to be removed from signal
|
||||||
|
Q = 30 # Quality factor
|
||||||
|
w0_1 = f0_1 / (fs / 2) # Normalized Frequency for 49.5Hz
|
||||||
|
w0_2 = f0_2 / (fs / 2) # Normalized Frequency for 50Hz
|
||||||
|
w0_3 = f0_3 / (fs / 2) # Normalized Frequency for 50.5Hz
|
||||||
|
self.b1, self.a1 = iirnotch(w0_1, Q)
|
||||||
|
self.b2, self.a2 = iirnotch(w0_2, Q)
|
||||||
|
self.b3, self.a3 = iirnotch(w0_3, Q)
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
self.plot_widgets = []
|
||||||
|
self.plots = []
|
||||||
|
for i in range(9):
|
||||||
|
plot_widget = PlotWidget()
|
||||||
|
plot_widget.setYRange(-10, 10) # Initial Y range
|
||||||
|
self.layout.addWidget(plot_widget)
|
||||||
|
self.plot_widgets.append(plot_widget)
|
||||||
|
plot = plot_widget.plot(pen=mkPen(color=self.pen_colors[i], width=self.pen_widths[i]))
|
||||||
|
self.plots.append(plot)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.setObjectName("TimeDomainPlot2")
|
||||||
|
|
||||||
|
def update_plot(self, data):
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
self.data_buffer[i] = np.roll(self.data_buffer[i], -1)
|
||||||
|
self.data_buffer[i][-1] = data[i]
|
||||||
|
|
||||||
|
# Apply notch filters to the entire data_buffer for each channel
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
filtered_data = lfilter(self.b1, self.a1, self.data_buffer[i])
|
||||||
|
filtered_data = lfilter(self.b2, self.a2, filtered_data)
|
||||||
|
filtered_data = lfilter(self.b3, self.a3, filtered_data)
|
||||||
|
self.data_buffer[i] = filtered_data
|
||||||
|
|
||||||
|
def refresh_plot(self):
|
||||||
|
half_buffer_length = self.data_buffer.shape[1] // 2
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
self.plots[i].setData(self.data_buffer[i, -half_buffer_length:])
|
||||||
|
|
||||||
|
for i in range(9):
|
||||||
|
max_y = np.max(self.data_buffer[i, -half_buffer_length:])
|
||||||
|
min_y = np.min(self.data_buffer[i, -half_buffer_length:])
|
||||||
|
self.plot_widgets[i].setYRange(min_y, max_y)
|
||||||
|
|
||||||
|
class FrequencyDomainPlot(QFrame):
|
||||||
|
def __init__(self, channels_to_display, pen_colors, pen_widths, num_plots):
|
||||||
|
super().__init__()
|
||||||
|
self.channels_to_display = channels_to_display
|
||||||
|
self.pen_colors = pen_colors
|
||||||
|
self.pen_widths = pen_widths
|
||||||
|
self.initUI()
|
||||||
|
self.data_buffer = np.zeros((9, num_plots)) # Buffer for 100 data points for 9 channels
|
||||||
|
|
||||||
|
self.update_timer = QTimer()
|
||||||
|
self.update_timer.timeout.connect(self.refresh_plot)
|
||||||
|
self.update_timer.start(20) # 50Hz refresh rate
|
||||||
|
|
||||||
|
# Design notch filters for 49.5Hz, 50Hz, and 50.5Hz
|
||||||
|
fs = 250 # Sampling frequency
|
||||||
|
f0_1 = 49.0 # First frequency to be removed from signal
|
||||||
|
f0_2 = 50.0 # Second frequency to be removed from signal
|
||||||
|
f0_3 = 51.0 # Third frequency to be removed from signal
|
||||||
|
Q = 30 # Quality factor
|
||||||
|
w0_1 = f0_1 / (fs / 2) # Normalized Frequency for 48Hz
|
||||||
|
w0_2 = f0_2 / (fs / 2) # Normalized Frequency for 49Hz
|
||||||
|
w0_3 = f0_3 / (fs / 2) # Normalized Frequency for 50Hz
|
||||||
|
self.b1, self.a1 = iirnotch(w0_1, Q)
|
||||||
|
self.b2, self.a2 = iirnotch(w0_2, Q)
|
||||||
|
self.b3, self.a3 = iirnotch(w0_3, Q)
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
self.plot_widget = PlotWidget(viewBox=CustomViewBox())
|
||||||
|
self.layout.addWidget(self.plot_widget)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.setObjectName("FrequencyDomainPlot")
|
||||||
|
self.plots = [self.plot_widget.plot(pen=mkPen(color=self.pen_colors[i], width=self.pen_widths[i])) for i in range(9)]
|
||||||
|
self.peak_texts = [TextItem("", color=self.pen_colors[i]) for i in range(9)]
|
||||||
|
for text in self.peak_texts:
|
||||||
|
self.plot_widget.addItem(text)
|
||||||
|
self.plot_widget.setYRange(0, 10) # Initial Y range
|
||||||
|
|
||||||
|
def update_plot(self, data):
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
self.data_buffer[i] = np.roll(self.data_buffer[i], -1)
|
||||||
|
self.data_buffer[i][-1] = data[i]
|
||||||
|
|
||||||
|
# Apply notch filters to the entire data_buffer for each channel
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
filtered_data = lfilter(self.b1, self.a1, self.data_buffer[i])
|
||||||
|
filtered_data = lfilter(self.b2, self.a2, filtered_data)
|
||||||
|
filtered_data = lfilter(self.b3, self.a3, filtered_data)
|
||||||
|
self.data_buffer[i] = filtered_data
|
||||||
|
|
||||||
|
def refresh_plot(self):
|
||||||
|
freq_data = np.abs(np.fft.rfft(self.data_buffer, axis=1))
|
||||||
|
freqs = np.fft.rfftfreq(self.data_buffer.shape[1], d=1 / 250.0) # Assuming 250Hz sampling rate
|
||||||
|
|
||||||
|
# Filter frequency data to only include 3-40Hz
|
||||||
|
mask = (freqs >= 3) & (freqs <= 100)
|
||||||
|
filtered_freqs = freqs[mask]
|
||||||
|
filtered_freq_data = freq_data[:, mask]
|
||||||
|
|
||||||
|
for i in self.channels_to_display:
|
||||||
|
self.plots[i].setData(filtered_freqs, filtered_freq_data[i])
|
||||||
|
|
||||||
|
peaks, _ = find_peaks(filtered_freq_data[i])
|
||||||
|
if len(peaks) > 0:
|
||||||
|
peak_freq = filtered_freqs[peaks]
|
||||||
|
peak_value = filtered_freq_data[i][peaks]
|
||||||
|
max_peak_index = np.argmax(peak_value)
|
||||||
|
self.peak_texts[i].setPos(peak_freq[max_peak_index], peak_value[max_peak_index])
|
||||||
|
self.peak_texts[i].setText(f"{peak_freq[max_peak_index]:.1f} Hz")
|
||||||
|
else:
|
||||||
|
self.peak_texts[i].setText("")
|
||||||
|
|
||||||
|
max_y = np.max(filtered_freq_data[self.channels_to_display])
|
||||||
|
self.plot_widget.setYRange(0, max_y)
|
||||||
|
|
||||||
|
class TFwindow(QWidget):
|
||||||
|
def __init__(self, channels_to_display, pen_colors, pen_widths, plot_num):
|
||||||
|
super().__init__()
|
||||||
|
self.time_domain_plot = TimeDomainPlot(channels_to_display, pen_colors, pen_widths, plot_num)
|
||||||
|
self.frequency_domain_plot = FrequencyDomainPlot(channels_to_display, pen_colors, pen_widths, plot_num)
|
||||||
|
self.initUI()
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
self.layout.addWidget(self.time_domain_plot)
|
||||||
|
self.layout.addWidget(self.frequency_domain_plot)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.setObjectName("TFplot")
|
||||||
|
self.setWindowTitle('Real-time Serial Data Plotter')
|
||||||
|
self.layout.setContentsMargins(30, 50, 20, 20)
|
||||||
|
|
||||||
|
def update_plots(self, data):
|
||||||
|
self.time_domain_plot.update_plot(data)
|
||||||
|
self.frequency_domain_plot.update_plot(data)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self.time_domain_plot.update_timer.stop()
|
||||||
|
self.frequency_domain_plot.update_timer.stop()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class VTKWidget(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(VTKWidget, self).__init__(parent)
|
||||||
|
self.vl = QVBoxLayout()
|
||||||
|
|
||||||
|
# VTK Renderer
|
||||||
|
self.vtkWidget = QVTKRenderWindowInteractor(self)
|
||||||
|
self.vl.addWidget(self.vtkWidget)
|
||||||
|
|
||||||
|
self.ren = vtk.vtkRenderer()
|
||||||
|
self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
|
||||||
|
self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
|
||||||
|
|
||||||
|
# Load OBJ and MTL files
|
||||||
|
self.load_obj_file("resource/PCB.obj", "resource/PCB.mtl")
|
||||||
|
|
||||||
|
# Add a light to the renderer
|
||||||
|
self.add_light()
|
||||||
|
|
||||||
|
self.setLayout(self.vl)
|
||||||
|
self.iren.Initialize()
|
||||||
|
|
||||||
|
def load_obj_file(self, obj_file_path, mtl_file_path):
|
||||||
|
# Create an OBJ importer
|
||||||
|
importer = vtk.vtkOBJImporter()
|
||||||
|
importer.SetFileName(obj_file_path)
|
||||||
|
importer.SetFileNameMTL(mtl_file_path)
|
||||||
|
importer.SetTexturePath("resource")
|
||||||
|
importer.SetRenderWindow(self.vtkWidget.GetRenderWindow())
|
||||||
|
importer.Update()
|
||||||
|
|
||||||
|
self.ren.ResetCamera()
|
||||||
|
|
||||||
|
def add_light(self):
|
||||||
|
# Create a light
|
||||||
|
light = vtk.vtkLight()
|
||||||
|
light.SetFocalPoint(0, 0, 0)
|
||||||
|
light.SetPosition(1, 1, 1)
|
||||||
|
light.SetIntensity(0.3) # Adjust intensity to make the scene darker
|
||||||
|
|
||||||
|
self.ren.AddLight(light)
|
||||||
|
# Set a darker background color
|
||||||
|
colors = vtk.vtkNamedColors()
|
||||||
|
self.ren.SetBackground(colors.GetColor3d("DarkSlateGray"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
channels_to_display = [0] # Example control parameter list
|
||||||
|
pen_colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k', 'w', 'orange']
|
||||||
|
pen_widths = [2, 2, 2, 2, 2, 2, 2, 2, 2]
|
||||||
|
tfplot = TFwindow(channels_to_display, pen_colors, pen_widths, 100)
|
||||||
|
leadoff = StatusGrid()
|
||||||
|
ser_frame = SerialCommunication()
|
||||||
|
vtk_demo = VTKWidget()
|
||||||
|
def open_port():
|
||||||
|
if ser_frame.serial_port and ser_frame.serial_port.is_open:
|
||||||
|
ser_frame.serial_port.close()
|
||||||
|
ser_frame.serial_port = None
|
||||||
|
ser_frame.open_button.setText("Open Port")
|
||||||
|
ser_frame.send_button.setEnabled(False)
|
||||||
|
else:
|
||||||
|
port = ser_frame.port_combobox.currentText()
|
||||||
|
baud_rate = int(ser_frame.baud_combobox.currentText())
|
||||||
|
try:
|
||||||
|
ser_frame.serial_port = serial.Serial(port, baud_rate, timeout=1)
|
||||||
|
ser_frame.open_button.setText("Close Port")
|
||||||
|
ser_frame.send_button.setEnabled(True)
|
||||||
|
read_thread = SerialReader(ser_frame.serial_port)
|
||||||
|
read_thread.data_received.connect(ser_frame.receive_data)
|
||||||
|
read_thread.status_received.connect(leadoff.update_status)
|
||||||
|
read_thread.data_received.connect(tfplot.update_plots)
|
||||||
|
read_thread.start()
|
||||||
|
except serial.SerialException as e:
|
||||||
|
ser_frame.receive_text.append(f"Error opening port: {e}")
|
||||||
|
|
||||||
|
ser_frame.open_button.clicked.connect(open_port)
|
||||||
|
tfplot.show()
|
||||||
|
ser_frame.show()
|
||||||
|
leadoff.show()
|
||||||
|
vtk_demo.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
136
Interface/resource/PCB.mtl
Normal file
136
Interface/resource/PCB.mtl
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# Designed by EasyEDA Pro
|
||||||
|
newmtl mtl1
|
||||||
|
Ka 0.25 0.25 0.25
|
||||||
|
Kd 0.25 0.25 0.25
|
||||||
|
Ks 0.07 0.07 0.07
|
||||||
|
endmtl
|
||||||
|
newmtl mtl2
|
||||||
|
Ka 0.64 0.62 0.60
|
||||||
|
Kd 0.45 0.43 0.42
|
||||||
|
Ks 0.03 0.03 0.03
|
||||||
|
endmtl
|
||||||
|
newmtl mtl3
|
||||||
|
Ka 1.00 1.00 1.00
|
||||||
|
Kd 1.00 1.00 1.00
|
||||||
|
Ks 0.88 0.88 0.88
|
||||||
|
endmtl
|
||||||
|
newmtl mtl4
|
||||||
|
Ka 0.77 0.77 0.77
|
||||||
|
Kd 0.77 0.77 0.77
|
||||||
|
Ks 0.62 0.62 0.62
|
||||||
|
endmtl
|
||||||
|
newmtl mtl5
|
||||||
|
Ka 0.59 0.46 0.00
|
||||||
|
Kd 0.59 0.46 0.00
|
||||||
|
Ks 0.29 0.23 0.00
|
||||||
|
endmtl
|
||||||
|
newmtl mtl6
|
||||||
|
Ka 0.85 0.85 0.85
|
||||||
|
Kd 0.85 0.85 0.85
|
||||||
|
Ks 0.43 0.43 0.43
|
||||||
|
endmtl
|
||||||
|
newmtl mtl7
|
||||||
|
Ka 0.25 0.25 0.25
|
||||||
|
Kd 0.25 0.25 0.25
|
||||||
|
Ks 0.13 0.13 0.13
|
||||||
|
endmtl
|
||||||
|
newmtl mtl8
|
||||||
|
Ka 1.00 1.00 1.00
|
||||||
|
Kd 1.00 1.00 1.00
|
||||||
|
Ks 0.50 0.50 0.50
|
||||||
|
endmtl
|
||||||
|
newmtl mtl9
|
||||||
|
Ka 0.00 0.00 0.00
|
||||||
|
Kd 0.00 0.00 0.00
|
||||||
|
Ks 0.00 0.00 0.00
|
||||||
|
endmtl
|
||||||
|
newmtl mtl10
|
||||||
|
Ka 0.90 0.78 0.69
|
||||||
|
Kd 0.90 0.78 0.69
|
||||||
|
Ks 0.45 0.39 0.34
|
||||||
|
endmtl
|
||||||
|
newmtl mtl11
|
||||||
|
Ka 0.51 0.51 0.63
|
||||||
|
Kd 0.51 0.51 0.63
|
||||||
|
Ks 0.25 0.25 0.31
|
||||||
|
endmtl
|
||||||
|
newmtl mtl12
|
||||||
|
Ka 0.85 0.85 0.85
|
||||||
|
Kd 0.85 0.85 0.85
|
||||||
|
Ks 0.42 0.42 0.42
|
||||||
|
endmtl
|
||||||
|
newmtl mtl13
|
||||||
|
Ka 0.50 0.25 0.00
|
||||||
|
Kd 0.50 0.25 0.00
|
||||||
|
Ks 0.44 0.22 0.00
|
||||||
|
endmtl
|
||||||
|
newmtl mtl14
|
||||||
|
Ka 0.75 0.75 0.75
|
||||||
|
Kd 0.75 0.75 0.75
|
||||||
|
Ks 0.38 0.38 0.38
|
||||||
|
endmtl
|
||||||
|
newmtl mtl15
|
||||||
|
Ka 0.95 0.76 0.18
|
||||||
|
Kd 0.95 0.76 0.18
|
||||||
|
Ks 0.76 0.61 0.15
|
||||||
|
endmtl
|
||||||
|
newmtl mtl16
|
||||||
|
Ka 0.25 0.25 0.25
|
||||||
|
Kd 0.18 0.18 0.18
|
||||||
|
Ks 0.05 0.05 0.05
|
||||||
|
endmtl
|
||||||
|
newmtl mtl17
|
||||||
|
Ka 0.00 1.00 1.00
|
||||||
|
Kd 0.00 1.00 1.00
|
||||||
|
Ks 0.00 1.00 1.00
|
||||||
|
endmtl
|
||||||
|
newmtl mtl18
|
||||||
|
Ka 0.15 0.15 0.15
|
||||||
|
Kd 0.15 0.15 0.15
|
||||||
|
Ks 0.07 0.07 0.07
|
||||||
|
endmtl
|
||||||
|
newmtl mtl19
|
||||||
|
Ka 0.43 0.35 0.37
|
||||||
|
Kd 0.43 0.35 0.37
|
||||||
|
Ks 0.30 0.25 0.26
|
||||||
|
endmtl
|
||||||
|
newmtl mtl20
|
||||||
|
Ka 1.00 1.00 1.00
|
||||||
|
Kd 1.00 1.00 1.00
|
||||||
|
Ks 0.30 0.30 0.30
|
||||||
|
endmtl
|
||||||
|
newmtl mtl21
|
||||||
|
Ka 0.11 0.11 0.11
|
||||||
|
Kd 0.11 0.11 0.11
|
||||||
|
Ks 0.08 0.08 0.08
|
||||||
|
endmtl
|
||||||
|
newmtl mtl22
|
||||||
|
Ka 0.78 0.76 0.74
|
||||||
|
Kd 0.78 0.76 0.74
|
||||||
|
Ks 0.39 0.38 0.37
|
||||||
|
endmtl
|
||||||
|
newmtl mtl23
|
||||||
|
Ka 0.44 0.44 0.44
|
||||||
|
Kd 0.44 0.44 0.44
|
||||||
|
Ks 0.22 0.22 0.22
|
||||||
|
endmtl
|
||||||
|
newmtl mtl24
|
||||||
|
Ka 1.00 1.00 1.00
|
||||||
|
Kd 1.00 1.00 1.00
|
||||||
|
Ks 1.00 1.00 1.00
|
||||||
|
endmtl
|
||||||
|
newmtl mtl25
|
||||||
|
Ka 0.00 0.33 0.65
|
||||||
|
Kd 0.00 0.33 0.65
|
||||||
|
Ks 0.00 0.33 0.65
|
||||||
|
endmtl
|
||||||
|
newmtl mtl26
|
||||||
|
Ka 0.62 0.62 0.36
|
||||||
|
Kd 0.62 0.62 0.36
|
||||||
|
Ks 0.62 0.62 0.36
|
||||||
|
endmtl
|
||||||
|
newmtl mtl27
|
||||||
|
Ka 0.00 0.15 0.36
|
||||||
|
Kd 0.00 0.15 0.36
|
||||||
|
Ks 0.00 0.15 0.36
|
||||||
|
endmtl
|
||||||
201874
Interface/resource/PCB.obj
Normal file
201874
Interface/resource/PCB.obj
Normal file
File diff suppressed because it is too large
Load Diff
692632
Interface/resource/PCB.step
Normal file
692632
Interface/resource/PCB.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Interface/resource/PCB.zip
Normal file
BIN
Interface/resource/PCB.zip
Normal file
Binary file not shown.
50
Interface/resource/dark/demo.qss
Normal file
50
Interface/resource/dark/demo.qss
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
Widget > QLabel {
|
||||||
|
font: 24px 'Segoe UI', 'Microsoft YaHei';
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget {
|
||||||
|
border: 1px solid rgb(29, 29, 29);
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
background-color: rgb(39, 39, 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
Window {
|
||||||
|
background-color: rgb(32, 32, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
StandardTitleBar {
|
||||||
|
background-color: rgb(32, 32, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
StandardTitleBar > QLabel,
|
||||||
|
Widget > QLabel {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MinimizeButton {
|
||||||
|
qproperty-normalColor: white;
|
||||||
|
qproperty-normalBackgroundColor: transparent;
|
||||||
|
qproperty-hoverColor: white;
|
||||||
|
qproperty-hoverBackgroundColor: rgba(255, 255, 255, 26);
|
||||||
|
qproperty-pressedColor: white;
|
||||||
|
qproperty-pressedBackgroundColor: rgba(255, 255, 255, 51)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MaximizeButton {
|
||||||
|
qproperty-normalColor: white;
|
||||||
|
qproperty-normalBackgroundColor: transparent;
|
||||||
|
qproperty-hoverColor: white;
|
||||||
|
qproperty-hoverBackgroundColor: rgba(255, 255, 255, 26);
|
||||||
|
qproperty-pressedColor: white;
|
||||||
|
qproperty-pressedBackgroundColor: rgba(255, 255, 255, 51)
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseButton {
|
||||||
|
qproperty-normalColor: white;
|
||||||
|
qproperty-normalBackgroundColor: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
16
Interface/resource/light/demo.qss
Normal file
16
Interface/resource/light/demo.qss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
Widget > QLabel {
|
||||||
|
font: 24px 'Segoe UI', 'Microsoft YaHei';
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget {
|
||||||
|
border: 1px solid rgb(229, 229, 229);
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
background-color: rgb(249, 249, 249);
|
||||||
|
}
|
||||||
|
|
||||||
|
Window {
|
||||||
|
background-color: rgb(243, 243, 243);
|
||||||
|
}
|
||||||
|
|
||||||
BIN
Interface/resource/logo.png
Normal file
BIN
Interface/resource/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
BIN
Interface/resource/my_logo.jpg
Normal file
BIN
Interface/resource/my_logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
BIN
Interface/resource/school_logo.ico
Normal file
BIN
Interface/resource/school_logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
Interface/resource/school_logo.png
Normal file
BIN
Interface/resource/school_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
59
Interface/vtk_test.py
Normal file
59
Interface/vtk_test.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import sys
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
||||||
|
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
|
||||||
|
import vtkmodules.all as vtk
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(MainWindow, self).__init__(parent)
|
||||||
|
self.setWindowTitle("VTK with PyQt Example")
|
||||||
|
self.frame = QWidget()
|
||||||
|
self.vl = QVBoxLayout()
|
||||||
|
|
||||||
|
# VTK Renderer
|
||||||
|
self.vtkWidget = QVTKRenderWindowInteractor(self.frame)
|
||||||
|
self.vl.addWidget(self.vtkWidget)
|
||||||
|
|
||||||
|
self.ren = vtk.vtkRenderer()
|
||||||
|
self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
|
||||||
|
self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
|
||||||
|
|
||||||
|
# Load OBJ and MTL files
|
||||||
|
self.load_obj_file("resource/PCB.obj", "resource/PCB.mtl")
|
||||||
|
|
||||||
|
# Add a light to the renderer
|
||||||
|
self.add_light()
|
||||||
|
|
||||||
|
self.frame.setLayout(self.vl)
|
||||||
|
self.setCentralWidget(self.frame)
|
||||||
|
|
||||||
|
self.show()
|
||||||
|
self.iren.Initialize()
|
||||||
|
|
||||||
|
def load_obj_file(self, obj_file_path, mtl_file_path):
|
||||||
|
# Create an OBJ importer
|
||||||
|
importer = vtk.vtkOBJImporter()
|
||||||
|
importer.SetFileName(obj_file_path)
|
||||||
|
importer.SetFileNameMTL(mtl_file_path)
|
||||||
|
importer.SetTexturePath("resource")
|
||||||
|
importer.SetRenderWindow(self.vtkWidget.GetRenderWindow())
|
||||||
|
importer.Update()
|
||||||
|
|
||||||
|
self.ren.ResetCamera()
|
||||||
|
|
||||||
|
def add_light(self):
|
||||||
|
# Create a light
|
||||||
|
light = vtk.vtkLight()
|
||||||
|
light.SetFocalPoint(0, 0, 0)
|
||||||
|
light.SetPosition(1, 1, 1)
|
||||||
|
light.SetIntensity(0.3) # Adjust intensity to make the scene darker
|
||||||
|
|
||||||
|
self.ren.AddLight(light)
|
||||||
|
# Set a darker background color
|
||||||
|
colors = vtk.vtkNamedColors()
|
||||||
|
self.ren.SetBackground(colors.GetColor3d("DarkSlateGray"))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = MainWindow()
|
||||||
|
sys.exit(app.exec_())
|
||||||
Loading…
Reference in New Issue
Block a user