MindViewer-TGAM模块数据图形化软件

上上篇:TGAM开发手册翻译

上一篇:神念科技TGAM模块组装测试

将TGAM模块与蓝牙模块组装好,并测试可以从蓝牙或者串口接收数据,然后就是本文的重点:MindViewer,用于将接收到的数据解析并绘制折线图。

本文所涉及的所有关键词和概念等等都在前两篇文章中。

需求

工具网上常用的类似工具,收集如下需求。

1.功能需求

  • 从串口读取、解析、保存EEG数据
    • 支持从串口读取EEG数据
    • 支持解析串口EEG数据
    • 支持将串口EEG数据保存至本地
  • 从本地文件读取、解析EEG数据
    • 支持从文件读取EEG数据
    • 支持解析本地EEG数据
  • 从模拟源读取、解析EEG数据
    • 支持生成模拟EEG数据
    • 支持读取模拟EEG数据
    • 支持解析模拟EEG数据
  • 支持数据解析
    • 支持将串口、本地、模拟统一处理为数据包
  • 支持人机交互界面显示
    • 支持以16进制查看数据
    • 支持以图形查看数据
    • 支持串口、本地、模拟数据动态解析展示
    • 支持EEG数据动态交互显示

2.其他需求

  • 使用自动打包发布功能
  • CPU占用不超过200%
  • 内存占用不超过500M
  • 交互响应时间不超过1s

架构设计

本软件只是一个独立程序,并无其他依赖,所以不需要进行架构设计。但是硬要是往上靠的话,会涉及类的层级调用,算是层次架构。

系统设计

概要设计

将需求分配给模块,形成模块结构图。

  • 激励模块 包含模拟数据、本地数据、COM数据
  • 解析模块 能够解析模拟数据、本地数据、COM数据
  • 人机交互模块 能够提供人机交互界面、数据显示交互
flowchart TB

A[MindViewer] --> B[激励模块]
A --> C[解析模块]
A --> D[人机交互模块]
B --> E[COM]
B --> F[文件]
B --> G[模拟]
D --> H[界面]
D --> I[EEG图]
D --> J[信息图]

详细设计

  • 软件使用Qt 6.8.0 MSVC2022开发,跨平台移植以Ubuntu24.04LTS为基准。
  • 激励模块
    • COM数据读取使用QSerialPort
    • 本地数据读取使用QFile
    • 模拟数据按照官方格式使用随机数生成组包,生成的数据保存至QByteArray中
  • 解析模块
    • 适配器 将COM数据、本地数据、模拟数据全部处理为统一的QByteArray数据
    • 解析 使用状态机循环解析QByteArray
      • EEG包 包括原始数据、α、β、γ、δ等8种数据
      • 信息包 包括电量、信号强度、眨眼强度等
  • 人机交互模块
    • 主界面 使用QWidget开发
    • EEG包 使用QWT显示,每种数据一条线
    • 信息包 使用QWT和控件显示,信号强度、眨眼强度等使用文字表示,冥想值、注意力用仪表盘显示

类调用图为

flowchart LR

B[解析类] <--> A[界面类]
C[仪表类] --> A 
D[折线类] --> A 
E[串口类] --> B 
F[模拟类] --> B 
G[本地文件类] --> B

注意:TGAM的数据为大端,一般的Intel CPU为小端

图片来自:蓝牙解析数据及文档解析说明 博客园

data

原始数据

TGAM芯片每秒钟会通过蓝牙发送513包数据,其中有512包的原始数据包和一个包含EEG数据的大包。

512个数据包为:

1
2
AA AA 04 80 02 high low checksum
AA AA 04 80 02 FA 03 80

AA AA通用的包头

接下来的04表示在最后一位(即校验值)之前有四个字节的数据

接下来的80 02,80为表示原始数据,02表示原始数据有两个字节

high low两个字节组成了原始数据,计算方法根据官方的手册为:

1
short raw = (Value[0]<<8) | Value[1];

其中Value[0]是高阶字节,Value[1]是低阶字节。

而最后一位是校验位,将04(不包括)后面和校验值前面的数据加起来进行处理

1
2
checksum &= 0xFF;
checksum = ~checksum & 0xFF;

如果值和校验值一样就是有效数据,否则就丢弃。

EEG数据包

此数据包包括两部分,一部分是EEG数据,另一部分是其他数据(冥想值、注意力、眨眼强度、信号强度等等),有可能不同时出现,也有可能只出现一部分。顺序也不一定

典型数据包如下(官方说明文档里面的):

1
2
3
4
5
6
7
8
9
10
11
12
13
byte: value // Explanation
[ 0]: 0xAA // [SYNC]
[ 1]: 0xAA // [SYNC]
[ 2]: 0x08 // [PLENGTH] (payload length) of 8 bytes
[ 3]: 0x02 // [CODE] POOR_SIGNAL Quality
[ 4]: 0x20 // Some poor signal detected (32/255)
[ 5]: 0x01 // [CODE] BATTERY Level
[ 6]: 0x7E // Almost full 3V of battery (126/127)
[ 7]: 0x04 // [CODE] ATTENTION eSense
[ 8]: 0x12 // eSense Attention level of 18%
[ 9]: 0x05 // [CODE] MEDITATION eSense
[10]: 0x60 // eSense Meditation level of 96%
[11]: 0xE3 // [CHKSUM] (1's comp inverse of 8-bit Payload sum of 0x1C)

还有一种可能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
AA 同步
AA 同步
20 是十进制的32,即有32个字节的payload,除掉20本身+两个AA同步+最后校验和
02 代表信号值Signal
C8 信号的值
83 代表EEG Power开始了
18 是十进制的24,说明EEG Power是由24个字节组成的,以下每三个字节为一组
18 Delta 1/3
D4 Delta 2/3
8B Delta 3/3
13 Theta 1/3
D1 Theta 2/3
69 Theta 3/3
02 LowAlpha 1/3
58 LowAlpha 2/3
C1 LowAlpha 3/3
17 HighAlpha 1/3
3B HighAlpha 2/3
DC HighAlpha 3/3
02 LowBeta 1/3
50 LowBeta 2/3
00 LowBeta 3/3
03 HighBeta 1/3
CB HighBeta 2/3
9D HighBeta 3/3
03 LowGamma 1/3
6D LowGamma 2/3
3B LowGamma 3/3
03 MiddleGamma 1/3
7E MiddleGamma 2/3
89 MiddleGamma 3/3
04 代表专注度Attention
00 Attention的值(0到100之间)
05 代表放松度Meditation
00 Meditation的值(0到100之间)
D5 校验和

以结构体表示为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
typedef struct type1
{
int ASYN1;
int ASYN2;
int PAYLOAD;
int Signal;
int SignalValue;
int Power;
int PowerValue;
int Delta1;
int Delta2;
int Delta3;
int Theta1;
int Theta2;
int Theta3;
int LowAlpha1;
int LowAlpha2;
int LowAlpha3;
int HighAlpha1;
int HightAlpha2;
int HightAlpha3;
int LowBeta1;
int LowBeta2;
int LowBeta3;
int HighBeta1;
int HighBeta2;
int HighBeta3;
int LowGamma1;
int LowGamma2;
int LowGamma3;
int MiddleGamma1;
int MiddleGamma2;
int MiddleGamma3;
int Attention;
int AttentionValue;
int Meditation;
int MeditationValue;
int checksum;
} type1;

其他的好理解,EEG数据由三个字节,每个字节八位,结果计算:

1
delta=(payload[i]<<16) | (payload[(i+1)]<<8)  | (payload[(i+2)])

得到一个有符号整数(signed int),可以看出有效值为三个字节( 224-2^{24}22412^{24}-1 )。

官方文档中说明:一个包最小四个字节,最大179个字节。

整理一下:

名称范围
attention0-100
meditation0-100
8个EEG数据224-2^{24}22412^{24}-1
signal0-200
power0-128 3v
16位raw value-32768~32767
blink1-255
走神程度0-10

这些值没有单位,只有多个值一起比较时才有意义。EEG数据单独显示,电池等信息以文字形式显示[6],冥想值、注意力用仪表盘显示。

使用Qt作为界面,qwt来绘制折线图。

数据包解析部分(参考官方提供的实例代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
int MainWindow::parserData(QByteArray ba, bool &raw, short &rawValue, bool &eeg, struct _eegPkt &pkt)
{
//qDebug()<<"Parsering data";
raw=false;
eeg=false;

if(ba.isEmpty()) return -1;
int size=ba.size();
if(size>179) return -1;//官方说明最多179个字节

int i=0;
uchar state=PARSER_STATE_SYNC;
uchar payloadLength;
uchar payloadSum;
while(i<size-1){
switch(state){
case PARSER_STATE_SYNC://第一个同步字节
if((uchar)ba[i]==PARSER_SYNC_BYTE){
//qDebug()<<"parser first aa "<<i<<(uchar)ba[i];
state=PARSER_STATE_SYNC_CHECK;
}
++i;
break;
case PARSER_STATE_SYNC_CHECK:
if((uchar)ba[i]==PARSER_SYNC_BYTE){
//qDebug()<<"parser second aa "<<i<<(uchar)ba[i];
state=PARSER_STATE_PAYLOAD_LENGTH;//准备解析负载长度
}else{
state=PARSER_STATE_SYNC;
}
++i;
break;
case PARSER_STATE_PAYLOAD_LENGTH:
payloadLength=(uchar)ba[i];
if(payloadLength>170){
state=PARSER_STATE_SYNC;
return -3;
}else if(payloadLength==170){
return -4;
}else{
payloadSum=0;
//qDebug()<<"parser payload length "<<i<<(uchar)ba[i];
state=PARSER_STATE_CHKSUM;//准备解析有效数据
}
++i;
break;
case PARSER_STATE_CHKSUM:
{
uchar z=i;
//首先校验数据是否有效
for(int j=0;j<payloadLength;j++){
//qDebug()<<(uchar)ba[z+j];
payloadSum+=(uchar)ba[z+j];
}
payloadSum &= 0xff;
payloadSum = ~payloadSum & 0xff;
z+=payloadLength;

if(payloadSum!=(uchar)ba[z]){
//如果与校验值不同就丢弃此包数据
return -1;
}/*else{
qDebug()<<"match";
}*/
//qDebug()<<"get data check sum is: "<<z<<(uchar)ba[z];
//qDebug()<<"parser check sum "<<i;
state=PARSER_STATE_PAYLOAD;
break;
}
case PARSER_STATE_PAYLOAD://解析数据
if((uchar)ba[i]==0x02){//数据信号强度值
//qDebug()<<"signal value "<<i<<(uchar)ba[i]<<(uchar)ba[i+1]<<(uint)ba[i+1];
eeg=true;
pkt.signal=(uchar)ba[i+1];
state=PARSER_STATE_PAYLOAD;
i+=2;
}else if((uchar)ba[i]==0x03){
}else if((uchar)ba[i]==0x04){//注意力值
//qDebug()<<"attention value "<<i<<(uchar)ba[i+1];
eeg=true;
pkt.attention=(uchar)ba[i+1];
state=PARSER_STATE_PAYLOAD;
i+=2;
}else if((uchar)ba[i]==0x05){//冥想值
//qDebug()<<"meditation value "<<i<<(uchar)ba[i+1];
eeg=true;
pkt.meditation=(uchar)ba[i+1];
state=PARSER_STATE_PAYLOAD;
i+=2;
//qDebug()<<"current i "<<i<<size;
}else if((uchar)ba[i]==0x06){//8bit raw value
}else if((uchar)ba[i]==0x07){
}else if((uchar)ba[i]==0x80){//16位原始数据
//qDebug()<<"parser raw value "<<i;
//qDebug()<<"parser a is: "<<(uchar)ba[i+2];
//qDebug()<<"parser b is: "<<(uchar)ba[i+3];
raw=true;
rawValue=((uchar)ba[i+3]<<8)|(uchar)ba[i+2];
return 0;
}else if((uchar)ba[i]==0x81){
}else if((uchar)ba[i]==0x83){//eeg数据部分
//0x83标志eeg部分开始,下一位表示为eeg部分程度默认为0x18
//qDebug()<<"parser eeg data "<<i<<ba[i];
//qDebug()<<"parser eeg length "<<(uchar)ba[i+1];
eeg=true;
pkt.delta =((uint)ba[i+4]<<16)|((uint)ba[i+3]<<8)|((uint)ba[i+2]);
pkt.theta =((uint)ba[i+7]<<16)|((uint)ba[i+6]<<8)|((uint)ba[i+5]);
pkt.lowAlpha =((uint)ba[i+10]<<16)|((uint)ba[i+9]<<8)|((uint)ba[i+8]);
pkt.highAlpha =((uint)ba[i+13]<<16)|((uint)ba[i+12]<<8)|((uint)ba[i+11]);
pkt.lowBeta =((uint)ba[i+16]<<16)|((uint)ba[i+15]<<8)|((uint)ba[i+14]);
pkt.highBeta =((uint)ba[i+19]<<16)|((uint)ba[i+18]<<8)|((uint)ba[i+17]);
pkt.lowGamma =((uint)ba[i+22]<<16)|((uint)ba[i+21]<<8)|((uint)ba[i+20]);
pkt.midGamma =((uint)ba[i+25]<<16)|((uint)ba[i+24]<<8)|((uint)ba[i+23]);
state=PARSER_STATE_PAYLOAD;
i+=26;
//qDebug()<<(uchar)ba[i]<<i;
}else if((uchar)ba[i]==0x86){
break;
}
//ui->textHex->appendPlainText(QString::number((uchar)ba[z],16));
break;
case PARSER_STATE_NULL:
break;
default:
break;
}
}

//qDebug()<<"all data has been parsered";

return 0;
}

软件

绘图效果:result

软件源码:MindViewer中

软件操作视频

参考资料


MindViewer-TGAM模块数据图形化软件
https://blog.jackeylea.com/qt/mindviewer-tgam-module-graphic-application/
作者
JackeyLea
发布于
2020年12月28日
许可协议