mirror of
https://github.com/YuTaoV5/YuEEG.git
synced 2025-09-26 23:09:24 +08:00
update readme, change gui
This commit is contained in:
parent
0cbd15211b
commit
d2bc1612cb
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
8
.idea/YuEEG.iml
generated
Normal file
8
.idea/YuEEG.iml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="SSVEP" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
112
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
112
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,112 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="91">
|
||||
<item index="0" class="java.lang.String" itemvalue="six" />
|
||||
<item index="1" class="java.lang.String" itemvalue="wheel" />
|
||||
<item index="2" class="java.lang.String" itemvalue="python-dateutil" />
|
||||
<item index="3" class="java.lang.String" itemvalue="tzdata" />
|
||||
<item index="4" class="java.lang.String" itemvalue="PyQt5-sip" />
|
||||
<item index="5" class="java.lang.String" itemvalue="pyserial" />
|
||||
<item index="6" class="java.lang.String" itemvalue="setuptools" />
|
||||
<item index="7" class="java.lang.String" itemvalue="numpy" />
|
||||
<item index="8" class="java.lang.String" itemvalue="Pinyin2Hanzi" />
|
||||
<item index="9" class="java.lang.String" itemvalue="pandas" />
|
||||
<item index="10" class="java.lang.String" itemvalue="PyQt5-Qt5" />
|
||||
<item index="11" class="java.lang.String" itemvalue="pip" />
|
||||
<item index="12" class="java.lang.String" itemvalue="PyQt5" />
|
||||
<item index="13" class="java.lang.String" itemvalue="PyAudio" />
|
||||
<item index="14" class="java.lang.String" itemvalue="pytz" />
|
||||
<item index="15" class="java.lang.String" itemvalue="scipy" />
|
||||
<item index="16" class="java.lang.String" itemvalue="joblib" />
|
||||
<item index="17" class="java.lang.String" itemvalue="threadpoolctl" />
|
||||
<item index="18" class="java.lang.String" itemvalue="scikit-learn" />
|
||||
<item index="19" class="java.lang.String" itemvalue="dlib" />
|
||||
<item index="20" class="java.lang.String" itemvalue="opencv_python" />
|
||||
<item index="21" class="java.lang.String" itemvalue="onnxruntime-gpu" />
|
||||
<item index="22" class="java.lang.String" itemvalue="protobuf" />
|
||||
<item index="23" class="java.lang.String" itemvalue="nvidia-cuda-cupti-cu11" />
|
||||
<item index="24" class="java.lang.String" itemvalue="MarkupSafe" />
|
||||
<item index="25" class="java.lang.String" itemvalue="requests" />
|
||||
<item index="26" class="java.lang.String" itemvalue="torchvision" />
|
||||
<item index="27" class="java.lang.String" itemvalue="nvidia-cublas-cu11" />
|
||||
<item index="28" class="java.lang.String" itemvalue="opencv-python-headless" />
|
||||
<item index="29" class="java.lang.String" itemvalue="nvidia-cusparse-cu11" />
|
||||
<item index="30" class="java.lang.String" itemvalue="nvidia-cusolver-cu11" />
|
||||
<item index="31" class="java.lang.String" itemvalue="nvidia-curand-cu11" />
|
||||
<item index="32" class="java.lang.String" itemvalue="nvidia-nccl-cu11" />
|
||||
<item index="33" class="java.lang.String" itemvalue="gdown" />
|
||||
<item index="34" class="java.lang.String" itemvalue="filelock" />
|
||||
<item index="35" class="java.lang.String" itemvalue="nvidia-cudnn-cu11" />
|
||||
<item index="36" class="java.lang.String" itemvalue="lit" />
|
||||
<item index="37" class="java.lang.String" itemvalue="certifi" />
|
||||
<item index="38" class="java.lang.String" itemvalue="nvidia-cufft-cu11" />
|
||||
<item index="39" class="java.lang.String" itemvalue="soupsieve" />
|
||||
<item index="40" class="java.lang.String" itemvalue="urllib3" />
|
||||
<item index="41" class="java.lang.String" itemvalue="nvidia-cuda-runtime-cu11" />
|
||||
<item index="42" class="java.lang.String" itemvalue="beautifulsoup4" />
|
||||
<item index="43" class="java.lang.String" itemvalue="triton" />
|
||||
<item index="44" class="java.lang.String" itemvalue="nvidia-cuda-nvrtc-cu11" />
|
||||
<item index="45" class="java.lang.String" itemvalue="packaging" />
|
||||
<item index="46" class="java.lang.String" itemvalue="torch" />
|
||||
<item index="47" class="java.lang.String" itemvalue="tqdm" />
|
||||
<item index="48" class="java.lang.String" itemvalue="flatbuffers" />
|
||||
<item index="49" class="java.lang.String" itemvalue="nvidia-nvtx-cu11" />
|
||||
<item index="50" class="java.lang.String" itemvalue="cmake" />
|
||||
<item index="51" class="java.lang.String" itemvalue="typing_extensions" />
|
||||
<item index="52" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||
<item index="53" class="java.lang.String" itemvalue="Pillow" />
|
||||
<item index="54" class="java.lang.String" itemvalue="transformers" />
|
||||
<item index="55" class="java.lang.String" itemvalue="altgraph" />
|
||||
<item index="56" class="java.lang.String" itemvalue="Bottleneck" />
|
||||
<item index="57" class="java.lang.String" itemvalue="brotlipy" />
|
||||
<item index="58" class="java.lang.String" itemvalue="greenlet" />
|
||||
<item index="59" class="java.lang.String" itemvalue="qt-material" />
|
||||
<item index="60" class="java.lang.String" itemvalue="cffi" />
|
||||
<item index="61" class="java.lang.String" itemvalue="cloudpickle" />
|
||||
<item index="62" class="java.lang.String" itemvalue="QtPy" />
|
||||
<item index="63" class="java.lang.String" itemvalue="mkl-random" />
|
||||
<item index="64" class="java.lang.String" itemvalue="mss" />
|
||||
<item index="65" class="java.lang.String" itemvalue="importlib-metadata" />
|
||||
<item index="66" class="java.lang.String" itemvalue="mkl-fft" />
|
||||
<item index="67" class="java.lang.String" itemvalue="gym-notices" />
|
||||
<item index="68" class="java.lang.String" itemvalue="pyOpenSSL" />
|
||||
<item index="69" class="java.lang.String" itemvalue="zipp" />
|
||||
<item index="70" class="java.lang.String" itemvalue="pywin32-ctypes" />
|
||||
<item index="71" class="java.lang.String" itemvalue="gevent" />
|
||||
<item index="72" class="java.lang.String" itemvalue="pyparsing" />
|
||||
<item index="73" class="java.lang.String" itemvalue="zope.event" />
|
||||
<item index="74" class="java.lang.String" itemvalue="auto-py-to-exe" />
|
||||
<item index="75" class="java.lang.String" itemvalue="QScintilla" />
|
||||
<item index="76" class="java.lang.String" itemvalue="pyinstaller" />
|
||||
<item index="77" class="java.lang.String" itemvalue="wincertstore" />
|
||||
<item index="78" class="java.lang.String" itemvalue="nes-py" />
|
||||
<item index="79" class="java.lang.String" itemvalue="opencv-python" />
|
||||
<item index="80" class="java.lang.String" itemvalue="pyglet" />
|
||||
<item index="81" class="java.lang.String" itemvalue="cryptography" />
|
||||
<item index="82" class="java.lang.String" itemvalue="zope.interface" />
|
||||
<item index="83" class="java.lang.String" itemvalue="QDarkStyle" />
|
||||
<item index="84" class="java.lang.String" itemvalue="pyinstaller-hooks-contrib" />
|
||||
<item index="85" class="java.lang.String" itemvalue="win-inet-pton" />
|
||||
<item index="86" class="java.lang.String" itemvalue="PySocks" />
|
||||
<item index="87" class="java.lang.String" itemvalue="gym" />
|
||||
<item index="88" class="java.lang.String" itemvalue="pypiwin32" />
|
||||
<item index="89" class="java.lang.String" itemvalue="mkl-service" />
|
||||
<item index="90" class="java.lang.String" itemvalue="numexpr" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N801" />
|
||||
<option value="N802" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="SSVEP" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/YuEEG.iml" filepath="$PROJECT_DIR$/.idea/YuEEG.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
232
README.md
232
README.md
@ -1,17 +1,23 @@
|
||||
|
||||
<<<<<<< HEAD
|
||||
<h1 align = "center">🌟YuEEG 8通道脑机接口设备🌟</h1>
|
||||
=======
|
||||
<h1 align = "center">🌟ADS1299 8通道脑机接口设备🌟</h1>
|
||||
>>>>>>> 90881ebe4698cae25b6a6c3273afd805ba9cf01a
|
||||
|
||||

|
||||

|
||||

|
||||
<p align = "center">
|
||||
<img src="https://img.shields.io/badge/Arduino-passing-green" />
|
||||
<img src="https://img.shields.io/badge/PlatformIO-passing-green" />
|
||||
<img src="https://img.shields.io/badge/ESP32S3%20-项目-blue" />
|
||||
<img src="https://img.shields.io/badge/ADS1299%20-项目-red" />
|
||||
<img src="https://img.shields.io/badge/YuEEG-V1.6-grey" />
|
||||
</p>
|
||||
<p align = "center">
|
||||
<img src="https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110230401.jpg" width="400" />
|
||||
</p>
|
||||
|
||||
> [!CAUTION]
|
||||
> 项目基于自定义的MIT协议,除本项目拥有者以外该项目不允许用来参加任何商业比赛。
|
||||
|
||||
> [!NOTE]
|
||||
> 项目尚未完全更新【SSVEP范式】,建议Star后,会添加更多内容。
|
||||
|
||||
## 概述 🧠🔌
|
||||
|
||||
本项目旨在利用TI的ADS1299芯片构建一个8通道的脑机接口(BCI)设备。项目涵盖硬件电路设计、芯片驱动开发以及数据接收和显示的上位机程序。
|
||||
@ -51,19 +57,130 @@
|
||||
>嘉立创链接:[https://oshwhub.com/protodrive000/1299_pro](https://a360.co/3AnxQdK%20)
|
||||
访问密码:yutaov5
|
||||
|
||||
|||
|
||||
|正面|反面|
|
||||
|-|-|
|
||||
|||
|
||||
|||
|
||||
### 外壳模型设计图:
|
||||
|
||||
>3D外壳文件:https://a360.co/3AnxQdK
|
||||
访问密码:yutaov5
|
||||
|
||||
||||
|
||||
|单外壳|外壳+脑电帽|仰视图|
|
||||
|-|-|-|
|
||||
||||
|
||||
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 上位机程序 📊
|
||||
> 目前仅提供串口通讯的 plot_only.py 界面程序
|
||||
### 功能:
|
||||
|
||||
- **数据接收**:通过串口接收来自硬件设备的数据。
|
||||
- **数据展示**:实时显示EEG信号。
|
||||
- **数据存储**:保存数据以便后续分析。
|
||||
|
||||
### 使用技术:
|
||||
|
||||
- **编程语言**:Python
|
||||
- **图形库**:pyqt + fluent
|
||||
|
||||
### 界面截图:
|
||||
|正常采集模式|测试信号模式|
|
||||
|-|-|
|
||||
|||
|
||||
|
||||
|
||||
## 使用指南 📚
|
||||
|
||||
### 硬件连接:
|
||||
|
||||
1. 本项目支持两种供电模式:
|
||||
- USB接入XiaoESP32S3直接供电使用,同时串口发送数据
|
||||
- 背面焊接TPS63070锂电池转5V模块,然后电源接口接入3.7V锂电池即可
|
||||
1. 将EEG电极正确连接到被测试者。
|
||||
2. 使用USB线连接设备和电脑。
|
||||
|
||||
### 引脚连接
|
||||
```c++
|
||||
// 定义引脚
|
||||
#define CS_PIN A3
|
||||
#define SCLK_PIN SCK
|
||||
#define MOSI_PIN MOSI
|
||||
#define MISO_PIN MISO
|
||||
#define DRDY_PIN A0
|
||||
#define START_PIN A2
|
||||
#define RESET_PIN A1
|
||||
```
|
||||
|
||||
|
||||
### 硬件设置
|
||||
|
||||
驱动代码是为支持ESP32的Arduino IDE编写的,代码通过SPI与ADS1299进行数据采集。
|
||||
|
||||
### 先决条件
|
||||
|
||||
- **Arduino IDE**:从[Arduino官方网站](https://www.arduino.cc/en/software)下载最新版本。
|
||||
- **ESP32开发板包**:在Arduino IDE中通过开发板管理器添加ESP32开发板支持。
|
||||
### 加载代码:
|
||||
|
||||
打开Arduino IDE,选择 `文件` > `打开` 并选择 `ino 文件`。
|
||||
在 `工具` > `开发板` 下选择你的ESP32开发板。
|
||||
在 `工具` > `端口` 下设置正确的COM端口。
|
||||
### 上传代码:
|
||||
>如果是第一次下载,上电前按住boot再上电后松开,然后正常下载
|
||||
|
||||
编译并将代码上传到ESP32开发板。
|
||||
### 上位机代码使用方法
|
||||
上传完成后,ESP32开发板将开始与ADS1299芯片进行通信,并将EEG数据输出到串口监视器。
|
||||
|
||||
- 模式切换:使用以下串口命令或者左下方按钮切换不同模式:
|
||||
- 1:连续读取模式
|
||||
- 3:自检模式
|
||||
|
||||
>示例用法
|
||||
上传完成后,在Arduino IDE中打开串口监视器,波特率设为115200。你将看到来自每个通道的实时EEG数据。
|
||||
``` C
|
||||
// 数据格式如下
|
||||
Channel 1: 0.123456, Channel 2: 0.654321, ..., Channel 8: 0.345678
|
||||
```
|
||||
### 上位机安装步骤
|
||||
|
||||
1. **克隆此仓库**:
|
||||
```bash
|
||||
git clone https://github.com/YuTaoV5/YuEEG.git
|
||||
cd YuEEG
|
||||
```
|
||||
1. 安装所需库:
|
||||
```bash
|
||||
pip install pyserial pyqt5
|
||||
pip install pyqtgraph PyQt-Fluent-Widgets
|
||||
pip install scikit-learn
|
||||
```
|
||||
1. 软件运行:
|
||||
```bash
|
||||
python plot_only.py
|
||||
```
|
||||
## 贡献指南
|
||||
欢迎社区贡献!请随时提交问题、功能请求或拉取请求。
|
||||
|
||||
## 未来改进
|
||||
- 增加对不同微控制器的支持。
|
||||
- 扩展脑电帽设计,以便调整电极位置。
|
||||
|
||||
## 致谢
|
||||
特别感谢德州仪器提供ADS1299芯片,并感谢开源社区的启发与支持。
|
||||
## 项目结构 🗂️
|
||||
```
|
||||
├── hardware
|
||||
│ ├── schematics
|
||||
│ └── pcb
|
||||
├── firmware
|
||||
│ └── ads1299_driver.ino
|
||||
├── software
|
||||
│ └── bci_gui.py
|
||||
└── README.md
|
||||
```
|
||||
## 软件开发 💻
|
||||
|
||||
### 芯片驱动:
|
||||
@ -195,99 +312,6 @@ SRB(Signal Reference Buffer)引脚在ADS1299中用于信号参考电极的
|
||||
- **SRB1**:当SRB1引脚用于参考时,所有通道的负输入可以连接到SRB1。设置`MISC1`寄存器的SRB1位可以实现这种配置。
|
||||
- **SRB2**:SRB2引脚可以选择单独的电极作为参考电极,并通过设置`CHnSET`寄存器的SRB2位,将该电极的电位作为其他通道的参考。
|
||||
|
||||
#### 上位机程序 📊
|
||||
|
||||
### 功能:
|
||||
|
||||
- **数据接收**:通过串口接收来自硬件设备的数据。
|
||||
- **数据展示**:实时显示EEG信号。
|
||||
- **数据存储**:保存数据以便后续分析。
|
||||
|
||||
### 使用技术:
|
||||
|
||||
- **编程语言**:Python
|
||||
- **图形库**:pyqt + fluent
|
||||
|
||||
### 界面截图:
|
||||

|
||||
|
||||
<video width="640" height="360" controls>
|
||||
<source src="https://imgpool.protodrive.xyz/img/8c357eebb239c2a005d27d91e568f3a7.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## 使用指南 📚
|
||||
|
||||
### 硬件连接:
|
||||
|
||||
1. 连接电源。
|
||||
2. 将EEG电极正确连接到被测试者。
|
||||
3. 使用USB线连接设备和电脑。
|
||||
|
||||
### 软件设置
|
||||
|
||||
驱动代码是为支持ESP32的Arduino IDE编写的,代码通过SPI与ADS1299进行数据采集。
|
||||
|
||||
### 先决条件
|
||||
|
||||
- **Arduino IDE**:从[Arduino官方网站](https://www.arduino.cc/en/software)下载最新版本。
|
||||
- **ESP32开发板包**:在Arduino IDE中通过开发板管理器添加ESP32开发板支持。
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆此仓库**:
|
||||
```bash
|
||||
git clone https://github.com/YuTaoV5/YuEEG.git
|
||||
cd YuEEG
|
||||
### 软件运行:
|
||||
|
||||
2. 安装所需库:
|
||||
```bash
|
||||
pip install pyserial pyqt5
|
||||
pip install pyqtgraph PyQt-Fluent-Widgets
|
||||
pip install vtk scikit-learn
|
||||
```
|
||||
|
||||
### 加载代码:
|
||||
|
||||
打开Arduino IDE,选择 文件 > 打开 并选择 ino 文件。
|
||||
在 工具 > 开发板 下选择你的ESP32开发板。
|
||||
在 工具 > 端口 下设置正确的COM端口。
|
||||
### 上传代码:
|
||||
|
||||
编译并将代码上传到ESP32开发板。
|
||||
## 代码使用方法
|
||||
上传完成后,ESP32开发板将开始与ADS1299芯片进行通信,并将EEG数据输出到串口监视器。
|
||||
|
||||
- 模式切换:使用以下串口命令切换不同模式:
|
||||
- 1:连续读取模式
|
||||
- 2:阻抗测量模式
|
||||
- 3:自检模式
|
||||
|
||||
>示例用法
|
||||
上传完成后,在Arduino IDE中打开串口监视器,波特率设为115200。你将看到来自每个通道的实时EEG数据。
|
||||
``` C
|
||||
Channel 1: 0.123456, Channel 2: 0.654321, ..., Channel 8: 0.345678
|
||||
```
|
||||
## 贡献指南
|
||||
欢迎社区贡献!请随时提交问题、功能请求或拉取请求。
|
||||
|
||||
## 未来改进
|
||||
- 增加对不同微控制器的支持。
|
||||
- 扩展脑电帽设计,以便调整电极位置。
|
||||
|
||||
## 致谢
|
||||
特别感谢德州仪器提供ADS1299芯片,并感谢开源社区的启发与支持。
|
||||
## 项目结构 🗂️
|
||||
```
|
||||
├── hardware
|
||||
│ ├── schematics
|
||||
│ └── pcb
|
||||
├── firmware
|
||||
│ └── ads1299_driver.ino
|
||||
├── software
|
||||
│ └── bci_gui.py
|
||||
└── README.md
|
||||
```
|
||||
## 贡献 🤝
|
||||
欢迎大家对本项目进行贡献!请提交pull request或创建issue以报告问题。
|
||||
|
||||
|
3
Software/.idea/.gitignore
generated
vendored
Normal file
3
Software/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
112
Software/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
112
Software/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,112 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="91">
|
||||
<item index="0" class="java.lang.String" itemvalue="six" />
|
||||
<item index="1" class="java.lang.String" itemvalue="wheel" />
|
||||
<item index="2" class="java.lang.String" itemvalue="python-dateutil" />
|
||||
<item index="3" class="java.lang.String" itemvalue="tzdata" />
|
||||
<item index="4" class="java.lang.String" itemvalue="PyQt5-sip" />
|
||||
<item index="5" class="java.lang.String" itemvalue="pyserial" />
|
||||
<item index="6" class="java.lang.String" itemvalue="setuptools" />
|
||||
<item index="7" class="java.lang.String" itemvalue="numpy" />
|
||||
<item index="8" class="java.lang.String" itemvalue="Pinyin2Hanzi" />
|
||||
<item index="9" class="java.lang.String" itemvalue="pandas" />
|
||||
<item index="10" class="java.lang.String" itemvalue="PyQt5-Qt5" />
|
||||
<item index="11" class="java.lang.String" itemvalue="pip" />
|
||||
<item index="12" class="java.lang.String" itemvalue="PyQt5" />
|
||||
<item index="13" class="java.lang.String" itemvalue="PyAudio" />
|
||||
<item index="14" class="java.lang.String" itemvalue="pytz" />
|
||||
<item index="15" class="java.lang.String" itemvalue="scipy" />
|
||||
<item index="16" class="java.lang.String" itemvalue="joblib" />
|
||||
<item index="17" class="java.lang.String" itemvalue="threadpoolctl" />
|
||||
<item index="18" class="java.lang.String" itemvalue="scikit-learn" />
|
||||
<item index="19" class="java.lang.String" itemvalue="dlib" />
|
||||
<item index="20" class="java.lang.String" itemvalue="opencv_python" />
|
||||
<item index="21" class="java.lang.String" itemvalue="onnxruntime-gpu" />
|
||||
<item index="22" class="java.lang.String" itemvalue="protobuf" />
|
||||
<item index="23" class="java.lang.String" itemvalue="nvidia-cuda-cupti-cu11" />
|
||||
<item index="24" class="java.lang.String" itemvalue="MarkupSafe" />
|
||||
<item index="25" class="java.lang.String" itemvalue="requests" />
|
||||
<item index="26" class="java.lang.String" itemvalue="torchvision" />
|
||||
<item index="27" class="java.lang.String" itemvalue="nvidia-cublas-cu11" />
|
||||
<item index="28" class="java.lang.String" itemvalue="opencv-python-headless" />
|
||||
<item index="29" class="java.lang.String" itemvalue="nvidia-cusparse-cu11" />
|
||||
<item index="30" class="java.lang.String" itemvalue="nvidia-cusolver-cu11" />
|
||||
<item index="31" class="java.lang.String" itemvalue="nvidia-curand-cu11" />
|
||||
<item index="32" class="java.lang.String" itemvalue="nvidia-nccl-cu11" />
|
||||
<item index="33" class="java.lang.String" itemvalue="gdown" />
|
||||
<item index="34" class="java.lang.String" itemvalue="filelock" />
|
||||
<item index="35" class="java.lang.String" itemvalue="nvidia-cudnn-cu11" />
|
||||
<item index="36" class="java.lang.String" itemvalue="lit" />
|
||||
<item index="37" class="java.lang.String" itemvalue="certifi" />
|
||||
<item index="38" class="java.lang.String" itemvalue="nvidia-cufft-cu11" />
|
||||
<item index="39" class="java.lang.String" itemvalue="soupsieve" />
|
||||
<item index="40" class="java.lang.String" itemvalue="urllib3" />
|
||||
<item index="41" class="java.lang.String" itemvalue="nvidia-cuda-runtime-cu11" />
|
||||
<item index="42" class="java.lang.String" itemvalue="beautifulsoup4" />
|
||||
<item index="43" class="java.lang.String" itemvalue="triton" />
|
||||
<item index="44" class="java.lang.String" itemvalue="nvidia-cuda-nvrtc-cu11" />
|
||||
<item index="45" class="java.lang.String" itemvalue="packaging" />
|
||||
<item index="46" class="java.lang.String" itemvalue="torch" />
|
||||
<item index="47" class="java.lang.String" itemvalue="tqdm" />
|
||||
<item index="48" class="java.lang.String" itemvalue="flatbuffers" />
|
||||
<item index="49" class="java.lang.String" itemvalue="nvidia-nvtx-cu11" />
|
||||
<item index="50" class="java.lang.String" itemvalue="cmake" />
|
||||
<item index="51" class="java.lang.String" itemvalue="typing_extensions" />
|
||||
<item index="52" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||
<item index="53" class="java.lang.String" itemvalue="Pillow" />
|
||||
<item index="54" class="java.lang.String" itemvalue="transformers" />
|
||||
<item index="55" class="java.lang.String" itemvalue="altgraph" />
|
||||
<item index="56" class="java.lang.String" itemvalue="Bottleneck" />
|
||||
<item index="57" class="java.lang.String" itemvalue="brotlipy" />
|
||||
<item index="58" class="java.lang.String" itemvalue="greenlet" />
|
||||
<item index="59" class="java.lang.String" itemvalue="qt-material" />
|
||||
<item index="60" class="java.lang.String" itemvalue="cffi" />
|
||||
<item index="61" class="java.lang.String" itemvalue="cloudpickle" />
|
||||
<item index="62" class="java.lang.String" itemvalue="QtPy" />
|
||||
<item index="63" class="java.lang.String" itemvalue="mkl-random" />
|
||||
<item index="64" class="java.lang.String" itemvalue="mss" />
|
||||
<item index="65" class="java.lang.String" itemvalue="importlib-metadata" />
|
||||
<item index="66" class="java.lang.String" itemvalue="mkl-fft" />
|
||||
<item index="67" class="java.lang.String" itemvalue="gym-notices" />
|
||||
<item index="68" class="java.lang.String" itemvalue="pyOpenSSL" />
|
||||
<item index="69" class="java.lang.String" itemvalue="zipp" />
|
||||
<item index="70" class="java.lang.String" itemvalue="pywin32-ctypes" />
|
||||
<item index="71" class="java.lang.String" itemvalue="gevent" />
|
||||
<item index="72" class="java.lang.String" itemvalue="pyparsing" />
|
||||
<item index="73" class="java.lang.String" itemvalue="zope.event" />
|
||||
<item index="74" class="java.lang.String" itemvalue="auto-py-to-exe" />
|
||||
<item index="75" class="java.lang.String" itemvalue="QScintilla" />
|
||||
<item index="76" class="java.lang.String" itemvalue="pyinstaller" />
|
||||
<item index="77" class="java.lang.String" itemvalue="wincertstore" />
|
||||
<item index="78" class="java.lang.String" itemvalue="nes-py" />
|
||||
<item index="79" class="java.lang.String" itemvalue="opencv-python" />
|
||||
<item index="80" class="java.lang.String" itemvalue="pyglet" />
|
||||
<item index="81" class="java.lang.String" itemvalue="cryptography" />
|
||||
<item index="82" class="java.lang.String" itemvalue="zope.interface" />
|
||||
<item index="83" class="java.lang.String" itemvalue="QDarkStyle" />
|
||||
<item index="84" class="java.lang.String" itemvalue="pyinstaller-hooks-contrib" />
|
||||
<item index="85" class="java.lang.String" itemvalue="win-inet-pton" />
|
||||
<item index="86" class="java.lang.String" itemvalue="PySocks" />
|
||||
<item index="87" class="java.lang.String" itemvalue="gym" />
|
||||
<item index="88" class="java.lang.String" itemvalue="pypiwin32" />
|
||||
<item index="89" class="java.lang.String" itemvalue="mkl-service" />
|
||||
<item index="90" class="java.lang.String" itemvalue="numexpr" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N801" />
|
||||
<option value="N802" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
Software/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
Software/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
4
Software/.idea/misc.xml
generated
Normal file
4
Software/.idea/misc.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="SSVEP" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
Software/.idea/modules.xml
generated
Normal file
8
Software/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/software.iml" filepath="$PROJECT_DIR$/.idea/software.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
8
Software/.idea/software.iml
generated
Normal file
8
Software/.idea/software.iml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="SSVEP" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
Software/.idea/vcs.xml
generated
Normal file
6
Software/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
BIN
Software/__pycache__/plot_card.cpython-38.pyc
Normal file
BIN
Software/__pycache__/plot_card.cpython-38.pyc
Normal file
Binary file not shown.
271
Software/plot_only.py
Normal file
271
Software/plot_only.py
Normal file
@ -0,0 +1,271 @@
|
||||
import sys
|
||||
import time
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from collections import deque
|
||||
import pyqtgraph as pg
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import re
|
||||
import numpy as np
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout
|
||||
from pyqtgraph import LegendItem
|
||||
from qfluentwidgets import LineEdit, PushButton, TextEdit, CheckBox
|
||||
from scipy.signal import welch, butter, filtfilt
|
||||
|
||||
# Set the length of the data pool to handle 750 samples (3 seconds of data at 250Hz)
|
||||
data_len = 2000
|
||||
|
||||
|
||||
class SerialThread(QtCore.QThread):
|
||||
data_received = QtCore.pyqtSignal(str) # Signal to send data back to the main thread
|
||||
|
||||
def __init__(self, com_port):
|
||||
super().__init__()
|
||||
self.com_port = com_port
|
||||
self.serial_port = serial.Serial(self.com_port, baudrate=115200, timeout=1)
|
||||
self.data_pool = [deque(maxlen=data_len) for _ in range(9)] # Data pool for each channel
|
||||
self.running = True
|
||||
|
||||
def run(self):
|
||||
self.serial_port.write(b'1') # Send '1' to start data transmission
|
||||
while self.running:
|
||||
if self.serial_port.in_waiting:
|
||||
try:
|
||||
raw_data = self.serial_port.readline().decode('utf-8').strip()
|
||||
if raw_data:
|
||||
# Emit the raw data to the main thread for display
|
||||
self.data_received.emit(raw_data)
|
||||
# print(f"{raw_data}")
|
||||
# Integrity check (matches 'Channel:' followed by 9 float values)
|
||||
match = re.match(r'Channel:([\d\.\-]+,){8}[\d\.\-]+', raw_data)
|
||||
if match:
|
||||
values = [float(x) for x in raw_data.split('Channel:')[1].split(',')]
|
||||
# Add data to the pool for each channel
|
||||
for i in range(9):
|
||||
self.data_pool[i].append(values[i])
|
||||
except:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
self.serial_port.close()
|
||||
|
||||
def send_data(self, data):
|
||||
self.serial_port.write(data.encode('utf-8') + b'\r\n') # 发送数据到串口
|
||||
|
||||
def get_latest_data(self, size):
|
||||
"""Return the latest 'size' data from the data pool for all channels."""
|
||||
if size > len(self.data_pool[0]):
|
||||
raise ValueError("Requested size is larger than the current data pool size.")
|
||||
|
||||
# Convert the latest 'size' elements from deque to numpy array for each channel
|
||||
return np.array([list(self.data_pool[i])[-size:] for i in range(9)])
|
||||
|
||||
|
||||
class ADCPlotter(QtWidgets.QMainWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setWindowTitle("YuEEG Data Viewer")
|
||||
self.setWindowIcon(QIcon('./school_logo.ico'))
|
||||
self.setGeometry(100, 100, 800, 750)
|
||||
# Detect available COM ports and connect to the first available one
|
||||
com_port = self.detect_com_port()
|
||||
print(f"Connected to: {com_port}")
|
||||
if not com_port:
|
||||
raise Exception("No available COM ports detected.")
|
||||
|
||||
# Main layout
|
||||
self.central_widget = QtWidgets.QWidget()
|
||||
self.setCentralWidget(self.central_widget)
|
||||
self.layout = QtWidgets.QVBoxLayout(self.central_widget)
|
||||
|
||||
# Set up PyQtGraph plot
|
||||
self.plot_widget = pg.PlotWidget()
|
||||
self.layout.addWidget(self.plot_widget)
|
||||
# Create a legend
|
||||
self.legend = LegendItem(offset=(30, -30)) # Position the legend in the plot area
|
||||
self.legend.setParentItem(self.plot_widget.graphicsItem())
|
||||
# Create colored plots with thicker lines for each channel
|
||||
self.colors = ['r', 'g', 'b', 'c', 'm', 'y', 'w', 'grey', 'orange']
|
||||
self.plot_data = []
|
||||
for i in range(9):
|
||||
plot_item = self.plot_widget.plot(pen=pg.mkPen(color=self.colors[i], width=2))
|
||||
self.plot_data.append(plot_item)
|
||||
self.legend.addItem(plot_item, f"Channel {i + 1}") # Add each plot to the legend with a label
|
||||
|
||||
# Add a QTextEdit widget to display the raw data received
|
||||
self.text_box = TextEdit(self)
|
||||
self.text_box.setReadOnly(True)
|
||||
|
||||
self.send_text = LineEdit()
|
||||
self.send_button = PushButton("Send")
|
||||
self.adc_button = PushButton("Normal")
|
||||
self.adc_button.clicked.connect(self.mod_switch)
|
||||
self.send_button.clicked.connect(self.send_data)
|
||||
sendV_layout = QHBoxLayout()
|
||||
sendV_layout.addWidget(self.adc_button)
|
||||
sendV_layout.addWidget(self.send_text)
|
||||
sendV_layout.addWidget(self.send_button)
|
||||
|
||||
self.layout.addWidget(self.text_box)
|
||||
self.layout.addLayout(sendV_layout)
|
||||
|
||||
# Add checkboxes for selecting channels to display
|
||||
self.checkboxes = []
|
||||
self.checkbox_layout = QHBoxLayout()
|
||||
for i in range(9):
|
||||
checkbox = CheckBox(f"通道{i + 1}")
|
||||
checkbox.setChecked(True) # All channels checked by default
|
||||
self.checkboxes.append(checkbox)
|
||||
self.checkbox_layout.addWidget(checkbox)
|
||||
|
||||
self.layout.addLayout(self.checkbox_layout)
|
||||
|
||||
# Create a thread to handle serial data reception
|
||||
self.serial_thread = SerialThread(com_port)
|
||||
self.serial_thread.data_received.connect(self.handle_serial_data)
|
||||
self.serial_thread.start()
|
||||
|
||||
# Timer to refresh plot every 20ms
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.update_plot)
|
||||
self.timer.start(100)
|
||||
self.beg = time.time()
|
||||
self.previous_psd = None
|
||||
|
||||
def send_data(self):
|
||||
data = self.send_text.text()
|
||||
self.serial_thread.send_data(data)
|
||||
|
||||
def mod_switch(self):
|
||||
if self.adc_button.text() == "Normal":
|
||||
self.serial_thread.send_data("3")
|
||||
self.adc_button.setText("Test")
|
||||
elif self.adc_button.text() == "Test":
|
||||
self.serial_thread.send_data("1")
|
||||
self.adc_button.setText("Normal")
|
||||
def detect_com_port(self):
|
||||
"""Automatically detect available COM ports, excluding virtual ports."""
|
||||
ports = list(serial.tools.list_ports.comports())
|
||||
for port in ports:
|
||||
if 'Bluetooth' not in port.description and 'Virtual' not in port.description:
|
||||
return port.device
|
||||
return None
|
||||
|
||||
def append_text_box(self, text):
|
||||
"""Append text to the text box, keeping only the last 10 lines."""
|
||||
current_text = self.text_box.toPlainText()
|
||||
lines = current_text.split('\n')
|
||||
|
||||
# Append new text
|
||||
lines.append(text)
|
||||
|
||||
# Keep only the last 10 lines
|
||||
if len(lines) > 10:
|
||||
lines = lines[-10:]
|
||||
|
||||
# Update the text box
|
||||
self.text_box.setPlainText('\n'.join(lines))
|
||||
# Move the cursor to the end
|
||||
# self.text_box.moveCursor(QtCore.QTextCursor.End)
|
||||
def check_psd(self, data, fs):
|
||||
threshold = 50
|
||||
if data is not None:
|
||||
psd_values = self.compute_psd(data, fs)
|
||||
current_psd_sum = np.sum(psd_values)
|
||||
if self.previous_psd is not None:
|
||||
# 比较当前PSD总和与上次总和的变化是否超过阈值
|
||||
if threshold < current_psd_sum - self.previous_psd < 500:
|
||||
print(f"发现向上突变,目前差值为:{(current_psd_sum - self.previous_psd)}")
|
||||
return True
|
||||
else:
|
||||
print(f"差值为:{current_psd_sum - self.previous_psd}")
|
||||
self.previous_psd = current_psd_sum
|
||||
def compute_psd(self, data, fs):
|
||||
# 定义感兴趣的频率范围
|
||||
low_freq = 3
|
||||
high_freq = 40
|
||||
|
||||
# 存储每个通道在3-40Hz的PSD值
|
||||
psd_values = []
|
||||
|
||||
for i in range(data.shape[0]):
|
||||
# 计算功率谱密度
|
||||
f, psd = welch(data[i, :], fs, nperseg=500)
|
||||
|
||||
# 只保留3到40Hz的频段
|
||||
freq_mask = (f >= low_freq) & (f <= high_freq)
|
||||
psd_in_band = psd[freq_mask]
|
||||
|
||||
# 计算3到40Hz频段内的PSD值(通过对频段内的PSD进行积分)
|
||||
psd_band_value = np.trapz(psd_in_band, f[freq_mask])
|
||||
|
||||
# 将结果存储起来
|
||||
psd_values.append(psd_band_value)
|
||||
return psd_values
|
||||
def handle_serial_data(self, raw_data):
|
||||
"""Handle data received from the serial thread."""
|
||||
self.append_text_box(raw_data)
|
||||
|
||||
# 定义 Buterworth 滤波器函数
|
||||
def apply_lowpass_filter(self, signal, cutoff_freq=50, sample_rate=500, order=5):
|
||||
nyquist_freq = 0.5 * sample_rate
|
||||
normalized_cutoff_freq = cutoff_freq / nyquist_freq
|
||||
b, a = butter(order, normalized_cutoff_freq, btype='low', analog=False)
|
||||
filtered_signal = filtfilt(b, a, signal)
|
||||
return filtered_signal
|
||||
|
||||
def update_plot(self):
|
||||
"""Update the plot based on the current data in the data pool."""
|
||||
try:
|
||||
# Fetch the latest 750 samples from the serial thread data pool
|
||||
latest_data = self.serial_thread.get_latest_data(data_len)
|
||||
|
||||
|
||||
# Update the plot for each channel
|
||||
for i in range(9):
|
||||
if self.checkboxes[i].isChecked(): # Only update if the channel is checked
|
||||
waveform = latest_data[i]
|
||||
filtered_waveform = self.apply_lowpass_filter(waveform)
|
||||
self.plot_data[i].setData(filtered_waveform)
|
||||
else:
|
||||
self.plot_data[i].setData([]) # Hide the waveform by clearing the data
|
||||
# self.plot_data[7].setData(latest_data[7])
|
||||
# Collect data from only the selected channels
|
||||
selected_data = [latest_data[i] for i in range(9) if self.checkboxes[i].isChecked()]
|
||||
|
||||
# If there is any selected data, calculate global min and max
|
||||
if selected_data:
|
||||
global_min = np.min([np.min(data) for data in selected_data])
|
||||
global_max = np.max([np.max(data) for data in selected_data])
|
||||
# Set Y-axis range based on global min/max values
|
||||
self.plot_widget.setYRange(global_min, global_max)
|
||||
else:
|
||||
global_min = -4.5
|
||||
global_max = 4.5
|
||||
# Set Y-axis range based on global min/max values
|
||||
self.plot_widget.setYRange(global_min, global_max)
|
||||
eeg_data = latest_data
|
||||
self.channel = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
eeg_data = np.array([eeg_data[i - 1] for i in self.channel])
|
||||
if time.time() - self.beg > 1:
|
||||
psd_values = self.compute_psd(eeg_data, 500)
|
||||
current_psd_sum = np.sum(psd_values)
|
||||
print(f"psd:{current_psd_sum}")
|
||||
self.beg = time.time()
|
||||
except ValueError:
|
||||
pass # Skip plotting if not enough data is available
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Ensure the thread is stopped when the application is closed."""
|
||||
self.serial_thread.stop()
|
||||
self.serial_thread.wait()
|
||||
event.accept()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = ADCPlotter()
|
||||
main.show()
|
||||
sys.exit(app.exec_())
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@ -1,213 +0,0 @@
|
||||
# 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_()
|
@ -1,490 +0,0 @@
|
||||
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_())
|
@ -1,136 +0,0 @@
|
||||
# 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
software/resource/PCB.obj
201874
software/resource/PCB.obj
File diff suppressed because it is too large
Load Diff
692632
software/resource/PCB.step
692632
software/resource/PCB.step
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,50 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 180 KiB |
Binary file not shown.
Before Width: | Height: | Size: 110 KiB |
Loading…
Reference in New Issue
Block a user