This commit is contained in:
ProtoDrive000 2024-06-19 21:52:11 +08:00
commit e60b90d2cf
15 changed files with 895747 additions and 0 deletions

BIN
Hardware/ADS1299.zip Normal file

Binary file not shown.

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

692632
Interface/resource/PCB.step Normal file

File diff suppressed because it is too large Load Diff

BIN
Interface/resource/PCB.zip Normal file

Binary file not shown.

View 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;
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

59
Interface/vtk_test.py Normal file
View 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_())