上上篇:TGAM开发手册翻译
上一篇:神念科技TGAM模块组装测试
将TGAM模块与蓝牙模块组装好,并测试可以从蓝牙或者串口接收数据,然后就是本文的重点:MindViewer,用于将接收到的数据解析并绘制折线图。
本文所涉及的所有关键词和概念等等都在前两篇文章中。
需求工具网上常用的类似工具,收集如下需求。
1.功能需求从串口读取、解析、保存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数据 解析 使用状态机循环解析QByteArrayEEG包 包括原始数据、α、β、γ、δ等8种数据 信息包 包括电量、信号强度、眨眼强度等 人机交互模块主界面 使用QWidget开发 EEG包 使用QWT显示,每种数据一条线 信息包 使用QWT和控件显示,信号强度、眨眼强度等使用文字表示,冥想值、注意力用仪表盘显示 类调用图为
flowchart LR
B[解析类] <--> A[界面类]
C[仪表类] --> A
D[折线类] --> A
E[串口类] --> B
F[模拟类] --> B
G[本地文件类] --> B
注意:TGAM的数据为大端,一般的Intel CPU为小端
图片来自:蓝牙解析数据及文档解析说明 博客园
原始数据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),可以看出有效值为三个字节( − 2 24 -2^{24} − 2 2 4 至 2 24 − 1 2^{24}-1 2 2 4 − 1 )。
官方文档中说明:一个包最小四个字节,最大179个字节。
整理一下:
名称 范围 attention 0-100 meditation 0-100 8个EEG数据 − 2 24 -2^{24} − 2 2 4 至 2 24 − 1 2^{24}-1 2 2 4 − 1 signal 0-200 power 0-128 3v 16位raw value -32768~32767 blink 1-255 走神程度 0-10
这些值没有单位,只有多个值一起比较时才有意义。EEG数据单独显示,电池等信息以文字形式显示,冥想值、注意力用仪表盘显示。
使用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) { raw=false ; eeg=false ; if (ba.isEmpty ()) return -1 ; int size=ba.size (); if (size>179 ) return -1 ; 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){ state=PARSER_STATE_SYNC_CHECK; } ++i; break ; case PARSER_STATE_SYNC_CHECK: if ((uchar)ba[i]==PARSER_SYNC_BYTE){ 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 ; state=PARSER_STATE_CHKSUM; } ++i; break ; case PARSER_STATE_CHKSUM: { uchar z=i; for (int j=0 ;j<payloadLength;j++){ payloadSum+=(uchar)ba[z+j]; } payloadSum &= 0xff ; payloadSum = ~payloadSum & 0xff ; z+=payloadLength; if (payloadSum!=(uchar)ba[z]){ return -1 ; } state=PARSER_STATE_PAYLOAD; break ; } case PARSER_STATE_PAYLOAD: if ((uchar)ba[i]==0x02 ){ 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 ){ eeg=true ; pkt.attention=(uchar)ba[i+1 ]; state=PARSER_STATE_PAYLOAD; i+=2 ; }else if ((uchar)ba[i]==0x05 ){ eeg=true ; pkt.meditation=(uchar)ba[i+1 ]; state=PARSER_STATE_PAYLOAD; i+=2 ; }else if ((uchar)ba[i]==0x06 ){ }else if ((uchar)ba[i]==0x07 ){ }else if ((uchar)ba[i]==0x80 ){ 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=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 ; }else if ((uchar)ba[i]==0x86 ){ break ; } break ; case PARSER_STATE_NULL: break ; default : break ; } } return 0 ; }
软件绘图效果:
软件源码:MindViewer中
软件操作视频
参考资料