update readme, change gui

This commit is contained in:
ProtoDrive000 2024-11-11 12:59:14 +08:00
parent 0cbd15211b
commit d2bc1612cb
33 changed files with 693 additions and 895515 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

8
.idea/YuEEG.iml generated Normal file
View 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>

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

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

@ -1,17 +1,23 @@
<<<<<<< HEAD
<h1 align = "center">🌟YuEEG 8通道脑机接口设备🌟</h1> <h1 align = "center">🌟YuEEG 8通道脑机接口设备🌟</h1>
=======
<h1 align = "center">🌟ADS1299 8通道脑机接口设备🌟</h1>
>>>>>>> 90881ebe4698cae25b6a6c3273afd805ba9cf01a
![Img](https://img.shields.io/badge/Arduino-passing-green) <p align = "center">
![Img](https://img.shields.io/badge/PlatformIO-passing-green) <img src="https://img.shields.io/badge/Arduino-passing-green" />
![Img](https://img.shields.io/badge/YuEEG-V1\.6-grey) <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] > [!CAUTION]
> 项目基于自定义的MIT协议除本项目拥有者以外该项目不允许用来参加任何商业比赛。 > 项目基于自定义的MIT协议除本项目拥有者以外该项目不允许用来参加任何商业比赛。
> [!NOTE]
> 项目尚未完全更新【SSVEP范式】建议Star后会添加更多内容。
## 概述 🧠🔌 ## 概述 🧠🔌
本项目旨在利用TI的ADS1299芯片构建一个8通道的脑机接口BCI设备。项目涵盖硬件电路设计、芯片驱动开发以及数据接收和显示的上位机程序。 本项目旨在利用TI的ADS1299芯片构建一个8通道的脑机接口BCI设备。项目涵盖硬件电路设计、芯片驱动开发以及数据接收和显示的上位机程序。
@ -51,19 +57,130 @@
>嘉立创链接:[https://oshwhub.com/protodrive000/1299_pro](https://a360.co/3AnxQdK%20) >嘉立创链接:[https://oshwhub.com/protodrive000/1299_pro](https://a360.co/3AnxQdK%20)
访问密码yutaov5 访问密码yutaov5
||| |正面|反面|
|-|-| |-|-|
|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221805.png#pic_center =400x)|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221809.png#pic_center =400x)| |![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221805.png#pic_center =410x)|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221809.png#pic_center =400x)|
### 外壳模型设计图: ### 外壳模型设计图:
>3D外壳文件https://a360.co/3AnxQdK >3D外壳文件https://a360.co/3AnxQdK
访问密码yutaov5 访问密码yutaov5
|||| |单外壳|外壳+脑电帽|仰视图|
|-|-|-| |-|-|-|
|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221759.png#pic_center =400x)|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221635.PNG#pic_center =400x)|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221644.png#pic_center =400x)| |![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221759.png)|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221635.PNG)|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110221644.png)|
## 上位机程序 📊
> 目前仅提供串口通讯的 plot_only.py 界面程序
### 功能:
- **数据接收**:通过串口接收来自硬件设备的数据。
- **数据展示**实时显示EEG信号。
- **数据存储**:保存数据以便后续分析。
### 使用技术:
- **编程语言**Python
- **图形库**pyqt + fluent
### 界面截图:
|正常采集模式|测试信号模式|
|-|-|
|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241111123715.png#pic_center%20=400x)|![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241111123732.png#pic_center%20=400x)|
## 使用指南 📚
### 硬件连接:
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 @@ SRBSignal Reference Buffer引脚在ADS1299中用于信号参考电极的
- **SRB1**当SRB1引脚用于参考时所有通道的负输入可以连接到SRB1。设置`MISC1`寄存器的SRB1位可以实现这种配置。 - **SRB1**当SRB1引脚用于参考时所有通道的负输入可以连接到SRB1。设置`MISC1`寄存器的SRB1位可以实现这种配置。
- **SRB2**SRB2引脚可以选择单独的电极作为参考电极并通过设置`CHnSET`寄存器的SRB2位将该电极的电位作为其他通道的参考。 - **SRB2**SRB2引脚可以选择单独的电极作为参考电极并通过设置`CHnSET`寄存器的SRB2位将该电极的电位作为其他通道的参考。
#### 上位机程序 📊
### 功能:
- **数据接收**:通过串口接收来自硬件设备的数据。
- **数据展示**实时显示EEG信号。
- **数据存储**:保存数据以便后续分析。
### 使用技术:
- **编程语言**Python
- **图形库**pyqt + fluent
### 界面截图:
![Img](https://imgpool.protodrive.xyz/img/yank-note-picgo-img-20241110222325.png#pic_center%20=400x)
<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以报告问题。 欢迎大家对本项目进行贡献请提交pull request或创建issue以报告问题。

3
Software/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

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

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

Binary file not shown.

271
Software/plot_only.py Normal file
View 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_())

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -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_()

View File

@ -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_())

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

View File

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