com.xrlab.labframe_brainbit 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -2
- package/Scripts/BrainBitConfig.cs +16 -0
- package/Scripts/BrainBitData.cs +69 -0
- package/Scripts/BrainBitManager.cs +391 -40
- package/Scripts/EmotionalMathConfig.cs +115 -0
- package/Scripts/EmotionalMathConfig.cs.meta +11 -0
- package/Scripts/EmotionsController.cs +133 -0
- package/Scripts/EmotionsController.cs.meta +11 -0
- package/Scripts/LabFrame2023_Brainbit.asmdef +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
此套件為 LabFrame 2023 專用的 BrainBit 設備插件,用於連接與管理 BrainBit 腦波儀。
|
|
4
4
|
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> 請再記得多安裝此套件 https://github.com/BrainbitLLC/unity_em_st_artifacts.git#a04238a934b3da0494dd9120a489005277063a1f
|
|
7
|
+
> 開發當下此套件最新版(1.0.3)在android平台會有問題
|
|
8
|
+
|
|
5
9
|
## 支援功能
|
|
6
10
|
1. **設備連線管理:** 自動搜尋並手動觸發連接藍牙 BrainBit 設備。
|
|
7
11
|
2. **EEG 腦波數據收集:** 自動收集四個通道 (T3, T4, O1, O2) 的腦波數據。
|
|
8
12
|
3. **即時阻抗檢查:** 確認電極與頭皮的接觸阻抗值是否過高 (> 200,000Ω)。
|
|
9
13
|
4. **多階段資料分流儲存:** 收集期間可動態切換儲存 Tag(依照遊戲階段無縫寫入不同檔案)。
|
|
14
|
+
5. **情緒與光譜分析:** 透過 NeuroSDK `EegEmotionalMath` 即時運算專注 / 放鬆(MindData)與 δ/θ/α/β/γ 五頻段光譜百分比。
|
|
10
15
|
|
|
11
16
|
---
|
|
12
17
|
|
|
@@ -103,7 +108,95 @@ void CheckImpedanceStatus()
|
|
|
103
108
|
|
|
104
109
|
---
|
|
105
110
|
|
|
106
|
-
### 4.
|
|
111
|
+
### 4. 情緒與光譜分析 (MindData / SpectralData)
|
|
112
|
+
|
|
113
|
+
整合 NeuroSDK `EegEmotionalMath`,即時取得受測者的**專注度 / 放鬆度**以及**五頻段光譜百分比**。
|
|
114
|
+
|
|
115
|
+
> 情緒處理需要先完成約 6 秒的**校正** (請受測者安靜配戴),校正完成後才會開始輸出有效的 MindData / SpectralData。
|
|
116
|
+
|
|
117
|
+
#### 👉 開始 / 停止情緒處理
|
|
118
|
+
|
|
119
|
+
```csharp
|
|
120
|
+
// 啟動情緒處理(若 EEG 尚未啟動,會自動開啟;停止時會一併停止該次自動啟動的 EEG)
|
|
121
|
+
BrainBitManager.Instance.StartEmotionsProcessing(
|
|
122
|
+
autoWriteToLabData: true,
|
|
123
|
+
mindTag: "Gameplay_Mind",
|
|
124
|
+
spectralTag: "Gameplay_Spectral");
|
|
125
|
+
|
|
126
|
+
// 停止情緒處理
|
|
127
|
+
BrainBitManager.Instance.StopEmotionsProcessing();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### 👉 等待校正完成
|
|
131
|
+
|
|
132
|
+
```csharp
|
|
133
|
+
BrainBitManager.Instance.OnCalibrationProgress += pct => Debug.Log($"校正中 {pct}%");
|
|
134
|
+
BrainBitManager.Instance.OnCalibrationFinished += () => Debug.Log("校正完成!");
|
|
135
|
+
|
|
136
|
+
// 或在輪詢中判斷:
|
|
137
|
+
if (BrainBitManager.Instance.IsEmotionsCalibrated)
|
|
138
|
+
{
|
|
139
|
+
// 此時才能讀到有效的 MindData / SpectralData
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
若遊戲中需要重新校正(換受測者、中途摘下又戴回):
|
|
144
|
+
```csharp
|
|
145
|
+
BrainBitManager.Instance.RestartCalibration();
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### 👉 取得最新的專注 / 放鬆值
|
|
149
|
+
|
|
150
|
+
```csharp
|
|
151
|
+
void Update()
|
|
152
|
+
{
|
|
153
|
+
if (!BrainBitManager.Instance.IsEmotionsCalibrated) return;
|
|
154
|
+
|
|
155
|
+
var mind = BrainBitManager.Instance.GetLatestMindData();
|
|
156
|
+
if (mind == null) return;
|
|
157
|
+
|
|
158
|
+
Debug.Log($"專注: {mind.Attention:F1} / 放鬆: {mind.Relaxation:F1}");
|
|
159
|
+
// mind.InstAttention / mind.InstRelaxation 為瞬時值(抖動大,僅進階使用)
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### 👉 取得最新的五頻段光譜百分比
|
|
164
|
+
|
|
165
|
+
```csharp
|
|
166
|
+
var spec = BrainBitManager.Instance.GetLatestSpectralData();
|
|
167
|
+
if (spec != null)
|
|
168
|
+
{
|
|
169
|
+
Debug.Log($"δ:{spec.Delta:F1} θ:{spec.Theta:F1} α:{spec.Alpha:F1} β:{spec.Beta:F1} γ:{spec.Gamma:F1}");
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### 👉 事件訂閱(event-driven 寫法)
|
|
174
|
+
|
|
175
|
+
```csharp
|
|
176
|
+
BrainBitManager.Instance.OnMindDataReceived += mind => { /* 更新 UI */ };
|
|
177
|
+
BrainBitManager.Instance.OnSpectralDataReceived += spec => { /* 更新 UI */ };
|
|
178
|
+
BrainBitManager.Instance.OnEmotionsArtifact += hasArtifact => { /* 顯示雜訊警告 */ };
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### 👉 動態無縫切換儲存階段 (Tag)
|
|
182
|
+
|
|
183
|
+
```csharp
|
|
184
|
+
// 不用停情緒處理,下一筆資料就會被寫進新 tag
|
|
185
|
+
BrainBitManager.Instance.SetMindTag("Boss_Phase_Mind");
|
|
186
|
+
BrainBitManager.Instance.SetSpectralTag("Boss_Phase_Spectral");
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### 👉 可調設定(`BrainBitConfig`)
|
|
190
|
+
|
|
191
|
+
| 欄位 | 預設 | 說明 |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `EmotionsCalibrationLength` | `6` | 校正時間(秒)。越長越穩,但受測者需要等更久 |
|
|
194
|
+
| `EmotionsMentalEstimation` | `false` | 啟用 Mental Estimation(依實驗類型決定) |
|
|
195
|
+
| `EmotionsPrioritySide` | `SideType.NONE` | 優先分析腦側:`NONE` / `LEFT` / `RIGHT` |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### 5. 設備搜尋與多設備選擇機制
|
|
107
200
|
|
|
108
201
|
預設情況下,`BrainBitManager` 會自動過濾周遭的藍牙設備,只尋找型號為 `SensorLEBrainBit` 的腦波儀。
|
|
109
202
|
|
|
@@ -115,7 +208,7 @@ void CheckImpedanceStatus()
|
|
|
115
208
|
|
|
116
209
|
---
|
|
117
210
|
|
|
118
|
-
###
|
|
211
|
+
### 6. 實用除錯工具:獲取設備詳細參數
|
|
119
212
|
|
|
120
213
|
如果需要查看設備底層的詳細資訊(例如:電量、硬體版本、韌體版本、取樣頻率等),可使用內建的解析器 `SensorInfoProvider.cs` 將複雜的底層參數結構化。
|
|
121
214
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
using UnityEngine;
|
|
2
2
|
using System.Collections;
|
|
3
3
|
using System.Collections.Generic;
|
|
4
|
+
using SignalMath;
|
|
4
5
|
|
|
5
6
|
/// <summary>
|
|
6
7
|
/// BrainBit 設備配置
|
|
@@ -48,4 +49,19 @@ public class BrainBitConfig
|
|
|
48
49
|
/// 停止掃描後等待一段時間再建立連接
|
|
49
50
|
/// </summary>
|
|
50
51
|
public float ConnectDelaySeconds = 1.0f;
|
|
52
|
+
|
|
53
|
+
/// <summary>
|
|
54
|
+
/// 情緒處理校正時間(秒)。較長 → 較穩定,但受測者要等更久。
|
|
55
|
+
/// </summary>
|
|
56
|
+
public int EmotionsCalibrationLength = 6;
|
|
57
|
+
|
|
58
|
+
/// <summary>
|
|
59
|
+
/// 啟用 Mental Estimation(依實驗類型決定是否開啟)。
|
|
60
|
+
/// </summary>
|
|
61
|
+
public bool EmotionsMentalEstimation = false;
|
|
62
|
+
|
|
63
|
+
/// <summary>
|
|
64
|
+
/// 情緒分析優先腦側:NONE / LEFT / RIGHT,預設 NONE(雙側平均)。
|
|
65
|
+
/// </summary>
|
|
66
|
+
public SideType EmotionsPrioritySide = SideType.NONE;
|
|
51
67
|
}
|
package/Scripts/BrainBitData.cs
CHANGED
|
@@ -3,6 +3,7 @@ using System.Collections;
|
|
|
3
3
|
using System.Collections.Generic;
|
|
4
4
|
using LabFrame2023;
|
|
5
5
|
using System;
|
|
6
|
+
using SignalMath;
|
|
6
7
|
|
|
7
8
|
/// <summary>
|
|
8
9
|
/// BrainBit EEG 數據
|
|
@@ -184,4 +185,72 @@ public class BrainBit_ConnectionData : LabDataBase
|
|
|
184
185
|
{
|
|
185
186
|
return $"{EventType}: {DeviceName} ({DeviceAddress}) - Connected: {IsConnected}";
|
|
186
187
|
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// <summary>
|
|
191
|
+
/// BrainBit 情緒/心智狀態數據(專注、放鬆)
|
|
192
|
+
/// </summary>
|
|
193
|
+
[Serializable]
|
|
194
|
+
public class BrainBit_MindData : LabDataBase
|
|
195
|
+
{
|
|
196
|
+
/// <summary>相對專注度(平滑值,0-100,建議使用)</summary>
|
|
197
|
+
public double Attention;
|
|
198
|
+
|
|
199
|
+
/// <summary>相對放鬆度(平滑值,0-100,建議使用)</summary>
|
|
200
|
+
public double Relaxation;
|
|
201
|
+
|
|
202
|
+
/// <summary>瞬時專注度(抖動大,進階用)</summary>
|
|
203
|
+
public double InstAttention;
|
|
204
|
+
|
|
205
|
+
/// <summary>瞬時放鬆度(抖動大,進階用)</summary>
|
|
206
|
+
public double InstRelaxation;
|
|
207
|
+
|
|
208
|
+
public BrainBit_MindData() : base() { }
|
|
209
|
+
|
|
210
|
+
public BrainBit_MindData(MindData raw) : base()
|
|
211
|
+
{
|
|
212
|
+
Attention = raw.RelAttention;
|
|
213
|
+
Relaxation = raw.RelRelaxation;
|
|
214
|
+
InstAttention = raw.InstAttention;
|
|
215
|
+
InstRelaxation = raw.InstRelaxation;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public override string ToString()
|
|
219
|
+
=> $"Attention: {Attention:F1} | Relaxation: {Relaxation:F1}";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// <summary>
|
|
223
|
+
/// BrainBit 五頻段光譜百分比
|
|
224
|
+
/// </summary>
|
|
225
|
+
[Serializable]
|
|
226
|
+
public class BrainBit_SpectralData : LabDataBase
|
|
227
|
+
{
|
|
228
|
+
/// <summary>δ 波(深度放鬆 / 睡眠)</summary>
|
|
229
|
+
public double Delta;
|
|
230
|
+
|
|
231
|
+
/// <summary>θ 波(冥想 / 創意)</summary>
|
|
232
|
+
public double Theta;
|
|
233
|
+
|
|
234
|
+
/// <summary>α 波(放鬆清醒)</summary>
|
|
235
|
+
public double Alpha;
|
|
236
|
+
|
|
237
|
+
/// <summary>β 波(專注思考)</summary>
|
|
238
|
+
public double Beta;
|
|
239
|
+
|
|
240
|
+
/// <summary>γ 波(高階認知)</summary>
|
|
241
|
+
public double Gamma;
|
|
242
|
+
|
|
243
|
+
public BrainBit_SpectralData() : base() { }
|
|
244
|
+
|
|
245
|
+
public BrainBit_SpectralData(SpectralDataPercents raw) : base()
|
|
246
|
+
{
|
|
247
|
+
Delta = raw.Delta;
|
|
248
|
+
Theta = raw.Theta;
|
|
249
|
+
Alpha = raw.Alpha;
|
|
250
|
+
Beta = raw.Beta;
|
|
251
|
+
Gamma = raw.Gamma;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public override string ToString()
|
|
255
|
+
=> $"δ:{Delta:F1} θ:{Theta:F1} α:{Alpha:F1} β:{Beta:F1} γ:{Gamma:F1}";
|
|
187
256
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
using UnityEngine;
|
|
2
2
|
using System.Collections;
|
|
3
3
|
using System.Collections.Generic;
|
|
4
|
+
using System.Collections.Concurrent;
|
|
4
5
|
using LabFrame2023;
|
|
5
6
|
using UnityEngine.Events;
|
|
6
7
|
using NeuroSDK;
|
|
8
|
+
using SignalMath;
|
|
7
9
|
using System;
|
|
8
10
|
|
|
9
11
|
/// <summary>
|
|
@@ -42,6 +44,21 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
42
44
|
/// 是否正在掃描設備
|
|
43
45
|
/// </summary>
|
|
44
46
|
public bool IsScanning { get; private set; } = false;
|
|
47
|
+
|
|
48
|
+
/// <summary>
|
|
49
|
+
/// 正在跑情緒處理?
|
|
50
|
+
/// </summary>
|
|
51
|
+
public bool IsProcessingEmotions { get; private set; } = false;
|
|
52
|
+
|
|
53
|
+
/// <summary>
|
|
54
|
+
/// 情緒校正是否已完成?校正完成前 Mind / Spectral 回傳 null
|
|
55
|
+
/// </summary>
|
|
56
|
+
public bool IsEmotionsCalibrated { get; private set; } = false;
|
|
57
|
+
|
|
58
|
+
/// <summary>
|
|
59
|
+
/// 情緒校正進度 0-100
|
|
60
|
+
/// </summary>
|
|
61
|
+
public int CalibrationProgress { get; private set; } = 0;
|
|
45
62
|
#endregion
|
|
46
63
|
|
|
47
64
|
#region Events
|
|
@@ -69,6 +86,31 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
69
86
|
/// 錯誤事件
|
|
70
87
|
/// </summary>
|
|
71
88
|
public event UnityAction<string> OnError;
|
|
89
|
+
|
|
90
|
+
/// <summary>
|
|
91
|
+
/// 情緒/心智資料更新事件
|
|
92
|
+
/// </summary>
|
|
93
|
+
public event UnityAction<BrainBit_MindData> OnMindDataReceived;
|
|
94
|
+
|
|
95
|
+
/// <summary>
|
|
96
|
+
/// 五頻段光譜資料更新事件
|
|
97
|
+
/// </summary>
|
|
98
|
+
public event UnityAction<BrainBit_SpectralData> OnSpectralDataReceived;
|
|
99
|
+
|
|
100
|
+
/// <summary>
|
|
101
|
+
/// 情緒校正進度 0-100
|
|
102
|
+
/// </summary>
|
|
103
|
+
public event UnityAction<int> OnCalibrationProgress;
|
|
104
|
+
|
|
105
|
+
/// <summary>
|
|
106
|
+
/// 情緒校正完成
|
|
107
|
+
/// </summary>
|
|
108
|
+
public event UnityAction OnCalibrationFinished;
|
|
109
|
+
|
|
110
|
+
/// <summary>
|
|
111
|
+
/// 偵測到情緒處理期間的雜訊(sequence 或雙側)
|
|
112
|
+
/// </summary>
|
|
113
|
+
public event UnityAction<bool> OnEmotionsArtifact;
|
|
72
114
|
#endregion
|
|
73
115
|
|
|
74
116
|
#region Private Fields
|
|
@@ -91,6 +133,19 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
91
133
|
private string _currentEEGTag = "eeg";
|
|
92
134
|
// 用於記錄目前阻抗寫入資料的標籤
|
|
93
135
|
private string _currentImpedanceTag = "impedance";
|
|
136
|
+
|
|
137
|
+
// 主執行緒派發佇列,用於將背景執行緒的回呼安全地轉移到主執行緒執行
|
|
138
|
+
private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();
|
|
139
|
+
|
|
140
|
+
// === Emotions ===
|
|
141
|
+
private EmotionsController _emotionsController;
|
|
142
|
+
private bool _autoWriteEmotionData = false;
|
|
143
|
+
private string _currentMindTag = "mind";
|
|
144
|
+
private string _currentSpectralTag = "spectral";
|
|
145
|
+
private BrainBit_MindData _lastMindData;
|
|
146
|
+
private BrainBit_SpectralData _lastSpectralData;
|
|
147
|
+
// 若為 true,代表 EEG 串流是被情緒處理自動啟動的 — StopEmotionsProcessing 時要一起停
|
|
148
|
+
private bool _emotionsStartedEEG = false;
|
|
94
149
|
#endregion
|
|
95
150
|
|
|
96
151
|
#region IManager Implementation
|
|
@@ -119,11 +174,30 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
119
174
|
LabTools.Log($"[BrainBit] Config - AutoSelectBestSignal: {_config.AutoSelectBestSignal}, ConnectDelay: {_config.ConnectDelaySeconds}s");
|
|
120
175
|
}
|
|
121
176
|
|
|
177
|
+
/// <summary>
|
|
178
|
+
/// 每幀執行主執行緒佇列中的 Action(處理從背景執行緒派發過來的回呼)
|
|
179
|
+
/// </summary>
|
|
180
|
+
private void Update()
|
|
181
|
+
{
|
|
182
|
+
while (_mainThreadActions.TryDequeue(out var action))
|
|
183
|
+
{
|
|
184
|
+
try
|
|
185
|
+
{
|
|
186
|
+
action?.Invoke();
|
|
187
|
+
}
|
|
188
|
+
catch (Exception e)
|
|
189
|
+
{
|
|
190
|
+
LabTools.LogError($"[BrainBit] Error executing main thread action: {e.Message}");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
122
195
|
public IEnumerator ManagerDispose()
|
|
123
196
|
{
|
|
124
197
|
LabTools.Log("[BrainBit] Disposing BrainBit Manager...");
|
|
125
198
|
|
|
126
199
|
// 停止所有數據流
|
|
200
|
+
StopEmotionsProcessing();
|
|
127
201
|
StopEEGStream();
|
|
128
202
|
StopImpedanceStream();
|
|
129
203
|
|
|
@@ -174,8 +248,11 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
174
248
|
{
|
|
175
249
|
LabTools.Log("[BrainBit] Starting device scan...");
|
|
176
250
|
|
|
177
|
-
//
|
|
178
|
-
_scanner
|
|
251
|
+
// 重用 Scanner,避免重複建立造成 native 資源洩漏
|
|
252
|
+
if (_scanner == null)
|
|
253
|
+
{
|
|
254
|
+
_scanner = new Scanner(SensorFamily.SensorLEBrainBit);
|
|
255
|
+
}
|
|
179
256
|
_scanner.EventSensorsChanged += OnSensorsFound;
|
|
180
257
|
|
|
181
258
|
// 開始掃描
|
|
@@ -520,37 +597,46 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
520
597
|
|
|
521
598
|
private void OnSensorsFound(IScanner scanner, IReadOnlyList<SensorInfo> sensors)
|
|
522
599
|
{
|
|
600
|
+
// 此回呼由 NeuroSDK 在背景執行緒觸發!
|
|
601
|
+
// 必須將所有 Unity API 呼叫(StartCoroutine、StopCoroutine 等)派發到主執行緒
|
|
523
602
|
try
|
|
524
603
|
{
|
|
525
|
-
LabTools.Log($"[BrainBit] Found {sensors.Count} device(s)");
|
|
526
|
-
|
|
527
|
-
// 觸發設備發現事件
|
|
528
|
-
OnDeviceFound?.Invoke(new List<SensorInfo>(sensors));
|
|
604
|
+
LabTools.Log($"[BrainBit] Found {sensors.Count} device(s) (background thread)");
|
|
529
605
|
|
|
530
606
|
if (sensors.Count > 0)
|
|
531
607
|
{
|
|
532
|
-
//
|
|
533
|
-
StopScan();
|
|
534
|
-
|
|
535
|
-
// 選擇要連接的設備
|
|
608
|
+
// 選擇要連接的設備(純邏輯,不涉及 Unity API,可以在背景執行緒執行)
|
|
536
609
|
SensorInfo targetSensor = SelectBestDevice(sensors);
|
|
537
|
-
|
|
538
610
|
LabTools.Log($"[BrainBit] Selected device: {targetSensor.Name} (RSSI: {targetSensor.RSSI})");
|
|
539
|
-
|
|
611
|
+
|
|
612
|
+
// 先在背景緒停止 Scanner(SDK 層級,非 Unity API)
|
|
613
|
+
_scanner?.Stop();
|
|
614
|
+
_scanner.EventSensorsChanged -= OnSensorsFound;
|
|
615
|
+
|
|
616
|
+
// 將剩下的操作排入主執行緒執行
|
|
617
|
+
_mainThreadActions.Enqueue(() =>
|
|
540
618
|
{
|
|
541
|
-
|
|
542
|
-
_scanTimeoutCoroutine = null;
|
|
543
|
-
LabTools.Log("[BrainBit] Scan timeout coroutine stopped");
|
|
544
|
-
}
|
|
619
|
+
IsScanning = false;
|
|
545
620
|
|
|
546
|
-
|
|
547
|
-
|
|
621
|
+
// 觸發設備發現事件
|
|
622
|
+
OnDeviceFound?.Invoke(new List<SensorInfo>(sensors));
|
|
623
|
+
|
|
624
|
+
if (_scanTimeoutCoroutine != null)
|
|
625
|
+
{
|
|
626
|
+
StopCoroutine(_scanTimeoutCoroutine);
|
|
627
|
+
_scanTimeoutCoroutine = null;
|
|
628
|
+
LabTools.Log("[BrainBit] Scan timeout coroutine stopped");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 等待一段時間後建立連接(確保掃描完全停止)
|
|
632
|
+
StartCoroutine(DelayedConnect(targetSensor));
|
|
633
|
+
});
|
|
548
634
|
}
|
|
549
635
|
}
|
|
550
636
|
catch (Exception e)
|
|
551
637
|
{
|
|
552
638
|
LabTools.LogError($"[BrainBit] Error in OnSensorsFound: {e.Message}");
|
|
553
|
-
OnError?.Invoke($"Device discovery error: {e.Message}");
|
|
639
|
+
_mainThreadActions.Enqueue(() => OnError?.Invoke($"Device discovery error: {e.Message}"));
|
|
554
640
|
}
|
|
555
641
|
}
|
|
556
642
|
|
|
@@ -612,6 +698,13 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
612
698
|
{
|
|
613
699
|
LabTools.Log($"[BrainBit] Connecting to device: {sensorInfo.Name} ({sensorInfo.Address})");
|
|
614
700
|
|
|
701
|
+
// 清理舊的 Sensor(如果有的話),避免 native 資源洩漏
|
|
702
|
+
if (_currentSensor != null)
|
|
703
|
+
{
|
|
704
|
+
try { _currentSensor.Dispose(); } catch (Exception) { }
|
|
705
|
+
_currentSensor = null;
|
|
706
|
+
}
|
|
707
|
+
|
|
615
708
|
// 創建 Sensor(此時掃描已停止)
|
|
616
709
|
_currentSensor = _scanner.CreateSensor(sensorInfo) as BrainBitSensor;
|
|
617
710
|
|
|
@@ -657,6 +750,7 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
657
750
|
if (IsConnected)
|
|
658
751
|
{
|
|
659
752
|
// 停止所有數據流
|
|
753
|
+
StopEmotionsProcessing();
|
|
660
754
|
StopEEGStream();
|
|
661
755
|
StopImpedanceStream();
|
|
662
756
|
|
|
@@ -716,20 +810,32 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
716
810
|
|
|
717
811
|
private void OnSignalDataReceived(ISensor sensor, BrainBitSignalData[] data)
|
|
718
812
|
{
|
|
813
|
+
// 此回呼由 NeuroSDK 在背景執行緒觸發!
|
|
719
814
|
try
|
|
720
815
|
{
|
|
721
816
|
foreach (var packet in data)
|
|
722
817
|
{
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
// 觸發事件
|
|
726
|
-
OnEEGDataReceived?.Invoke(_lastEEGData);
|
|
818
|
+
var eegData = new BrainBit_EEGData(packet.T3, packet.T4, packet.O1, packet.O2);
|
|
819
|
+
_lastEEGData = eegData;
|
|
727
820
|
|
|
728
|
-
//
|
|
729
|
-
|
|
821
|
+
// 將事件觸發和資料寫入排入主執行緒
|
|
822
|
+
_mainThreadActions.Enqueue(() =>
|
|
730
823
|
{
|
|
731
|
-
|
|
732
|
-
|
|
824
|
+
// 觸發事件(訂閱者可能更新 UI)
|
|
825
|
+
OnEEGDataReceived?.Invoke(eegData);
|
|
826
|
+
|
|
827
|
+
// 自動保存到 LabDataManager
|
|
828
|
+
if (_autoWriteEEGData && LabDataManager.Instance.IsInited)
|
|
829
|
+
{
|
|
830
|
+
LabDataManager.Instance.WriteData(eegData, _currentEEGTag);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// 情緒處理(寄生在同一條 NeuroSDK 背景緒,不開額外 thread)
|
|
836
|
+
if (IsProcessingEmotions)
|
|
837
|
+
{
|
|
838
|
+
_emotionsController?.ProcessData(data);
|
|
733
839
|
}
|
|
734
840
|
}
|
|
735
841
|
catch (Exception e)
|
|
@@ -740,24 +846,30 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
740
846
|
|
|
741
847
|
private void OnResistanceDataReceived(ISensor sensor, BrainBitResistData data)
|
|
742
848
|
{
|
|
849
|
+
// 此回呼由 NeuroSDK 在背景執行緒觸發!
|
|
743
850
|
try
|
|
744
851
|
{
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
// 觸發事件
|
|
748
|
-
OnImpedanceDataReceived?.Invoke(_lastImpedanceData);
|
|
852
|
+
var impedanceData = new BrainBit_ImpedanceData(data.T3, data.T4, data.O1, data.O2);
|
|
853
|
+
_lastImpedanceData = impedanceData;
|
|
749
854
|
|
|
750
|
-
//
|
|
751
|
-
|
|
855
|
+
// 將事件觸發和資料寫入排入主執行緒
|
|
856
|
+
_mainThreadActions.Enqueue(() =>
|
|
752
857
|
{
|
|
753
|
-
|
|
754
|
-
|
|
858
|
+
// 觸發事件(訂閱者可能更新 UI)
|
|
859
|
+
OnImpedanceDataReceived?.Invoke(impedanceData);
|
|
755
860
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
861
|
+
// 檢查阻抗警告: 當有任一通道阻抗超過 200,000 時
|
|
862
|
+
if (!impedanceData.IsImpedanceGood)
|
|
863
|
+
{
|
|
864
|
+
LabTools.LogError($"[BrainBit] High impedance detected: {impedanceData.GetImpedanceStatus()}");
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// 自動保存到 LabDataManager
|
|
868
|
+
if (_autoWriteImpedanceData && LabDataManager.Instance.IsInited)
|
|
869
|
+
{
|
|
870
|
+
LabDataManager.Instance.WriteData(impedanceData, _currentImpedanceTag);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
761
873
|
}
|
|
762
874
|
catch (Exception e)
|
|
763
875
|
{
|
|
@@ -785,6 +897,16 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
785
897
|
_currentSensor = null;
|
|
786
898
|
}
|
|
787
899
|
|
|
900
|
+
// 清理情緒控制器
|
|
901
|
+
if (_emotionsController != null)
|
|
902
|
+
{
|
|
903
|
+
UnwireEmotionsCallbacks();
|
|
904
|
+
_emotionsController.Dispose();
|
|
905
|
+
_emotionsController = null;
|
|
906
|
+
}
|
|
907
|
+
IsProcessingEmotions = false;
|
|
908
|
+
IsEmotionsCalibrated = false;
|
|
909
|
+
|
|
788
910
|
// 重置狀態
|
|
789
911
|
IsConnected = false;
|
|
790
912
|
IsStreamingEEG = false;
|
|
@@ -799,4 +921,233 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
799
921
|
}
|
|
800
922
|
}
|
|
801
923
|
#endregion
|
|
924
|
+
|
|
925
|
+
#region Emotions Processing
|
|
926
|
+
|
|
927
|
+
/// <summary>
|
|
928
|
+
/// 啟動情緒處理(MindData / SpectralData / 校正進度)。
|
|
929
|
+
/// 若 EEG 串流未啟動,會自動啟動;呼叫 StopEmotionsProcessing 時會同步停止該 EEG 串流。
|
|
930
|
+
/// </summary>
|
|
931
|
+
public void StartEmotionsProcessing(bool autoWriteToLabData = true,
|
|
932
|
+
string mindTag = "mind",
|
|
933
|
+
string spectralTag = "spectral")
|
|
934
|
+
{
|
|
935
|
+
if (!IsConnected)
|
|
936
|
+
{
|
|
937
|
+
LabTools.LogError("[BrainBit] Device not connected");
|
|
938
|
+
OnError?.Invoke("Device not connected");
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (IsProcessingEmotions)
|
|
943
|
+
{
|
|
944
|
+
LabTools.LogError("[BrainBit] Emotions processing already active");
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
try
|
|
949
|
+
{
|
|
950
|
+
_autoWriteEmotionData = autoWriteToLabData;
|
|
951
|
+
_currentMindTag = string.IsNullOrEmpty(mindTag) ? "mind" : mindTag;
|
|
952
|
+
_currentSpectralTag = string.IsNullOrEmpty(spectralTag) ? "spectral" : spectralTag;
|
|
953
|
+
|
|
954
|
+
// 若 EEG 未啟動,自動啟動並記錄
|
|
955
|
+
if (!IsStreamingEEG)
|
|
956
|
+
{
|
|
957
|
+
StartEEGStream(autoWriteToLabData: false);
|
|
958
|
+
_emotionsStartedEEG = true;
|
|
959
|
+
}
|
|
960
|
+
else
|
|
961
|
+
{
|
|
962
|
+
_emotionsStartedEEG = false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// 建立 / 重建 controller(套用目前 BrainBitConfig)
|
|
966
|
+
_emotionsController?.Dispose();
|
|
967
|
+
_emotionsController = new EmotionsController(_config);
|
|
968
|
+
WireEmotionsCallbacks();
|
|
969
|
+
|
|
970
|
+
// 校正狀態重置
|
|
971
|
+
IsEmotionsCalibrated = false;
|
|
972
|
+
CalibrationProgress = 0;
|
|
973
|
+
_lastMindData = null;
|
|
974
|
+
_lastSpectralData = null;
|
|
975
|
+
|
|
976
|
+
_emotionsController.StartCalibration();
|
|
977
|
+
IsProcessingEmotions = true;
|
|
978
|
+
|
|
979
|
+
LabTools.Log($"[BrainBit] Emotions processing started (mindTag: {_currentMindTag}, spectralTag: {_currentSpectralTag})");
|
|
980
|
+
}
|
|
981
|
+
catch (Exception e)
|
|
982
|
+
{
|
|
983
|
+
if (_emotionsStartedEEG)
|
|
984
|
+
{
|
|
985
|
+
StopEEGStream();
|
|
986
|
+
_emotionsStartedEEG = false;
|
|
987
|
+
}
|
|
988
|
+
LabTools.LogError($"[BrainBit] Failed to start emotions processing: {e.Message}");
|
|
989
|
+
OnError?.Invoke($"Emotions processing failed: {e.Message}");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/// <summary>
|
|
994
|
+
/// 停止情緒處理。若本次 EEG 串流是由情緒處理自動啟動的,同步停止該 EEG 串流;
|
|
995
|
+
/// 否則保留 EEG 串流(避免誤停使用者正在錄的 EEG)。
|
|
996
|
+
/// </summary>
|
|
997
|
+
public void StopEmotionsProcessing()
|
|
998
|
+
{
|
|
999
|
+
if (!IsProcessingEmotions) return;
|
|
1000
|
+
|
|
1001
|
+
try
|
|
1002
|
+
{
|
|
1003
|
+
IsProcessingEmotions = false;
|
|
1004
|
+
|
|
1005
|
+
if (_emotionsController != null)
|
|
1006
|
+
{
|
|
1007
|
+
UnwireEmotionsCallbacks();
|
|
1008
|
+
_emotionsController.Dispose();
|
|
1009
|
+
_emotionsController = null;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
_autoWriteEmotionData = false;
|
|
1013
|
+
IsEmotionsCalibrated = false;
|
|
1014
|
+
CalibrationProgress = 0;
|
|
1015
|
+
|
|
1016
|
+
if (_emotionsStartedEEG && IsStreamingEEG)
|
|
1017
|
+
{
|
|
1018
|
+
StopEEGStream();
|
|
1019
|
+
}
|
|
1020
|
+
_emotionsStartedEEG = false;
|
|
1021
|
+
|
|
1022
|
+
LabTools.Log("[BrainBit] Emotions processing stopped");
|
|
1023
|
+
}
|
|
1024
|
+
catch (Exception e)
|
|
1025
|
+
{
|
|
1026
|
+
LabTools.LogError($"[BrainBit] Error stopping emotions processing: {e.Message}");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/// <summary>
|
|
1031
|
+
/// 重新校正(例如換受測者、中途摘下又戴回)。
|
|
1032
|
+
/// </summary>
|
|
1033
|
+
public void RestartCalibration()
|
|
1034
|
+
{
|
|
1035
|
+
if (!IsProcessingEmotions || _emotionsController == null)
|
|
1036
|
+
{
|
|
1037
|
+
LabTools.LogError("[BrainBit] Cannot restart calibration - emotions processing not active");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
IsEmotionsCalibrated = false;
|
|
1042
|
+
CalibrationProgress = 0;
|
|
1043
|
+
_emotionsController.StartCalibration();
|
|
1044
|
+
|
|
1045
|
+
LabTools.Log("[BrainBit] Emotion calibration restarted");
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/// <summary>
|
|
1049
|
+
/// 動態切換 MindData 寫入 Tag
|
|
1050
|
+
/// </summary>
|
|
1051
|
+
public void SetMindTag(string tag)
|
|
1052
|
+
{
|
|
1053
|
+
_currentMindTag = string.IsNullOrEmpty(tag) ? "mind" : tag;
|
|
1054
|
+
LabTools.Log($"[BrainBit] Mind data tag dynamically changed to: {_currentMindTag}");
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/// <summary>
|
|
1058
|
+
/// 動態切換 SpectralData 寫入 Tag
|
|
1059
|
+
/// </summary>
|
|
1060
|
+
public void SetSpectralTag(string tag)
|
|
1061
|
+
{
|
|
1062
|
+
_currentSpectralTag = string.IsNullOrEmpty(tag) ? "spectral" : tag;
|
|
1063
|
+
LabTools.Log($"[BrainBit] Spectral data tag dynamically changed to: {_currentSpectralTag}");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/// <summary>
|
|
1067
|
+
/// 取得最新的情緒/心智資料。校正完成前回傳 null。
|
|
1068
|
+
/// </summary>
|
|
1069
|
+
public BrainBit_MindData GetLatestMindData() => _lastMindData;
|
|
1070
|
+
|
|
1071
|
+
/// <summary>
|
|
1072
|
+
/// 取得最新的五頻段光譜資料。校正完成前回傳 null。
|
|
1073
|
+
/// </summary>
|
|
1074
|
+
public BrainBit_SpectralData GetLatestSpectralData() => _lastSpectralData;
|
|
1075
|
+
|
|
1076
|
+
private void WireEmotionsCallbacks()
|
|
1077
|
+
{
|
|
1078
|
+
if (_emotionsController == null) return;
|
|
1079
|
+
|
|
1080
|
+
_emotionsController.progressCalibrationCallback = OnEmotionsCalibrationProgress;
|
|
1081
|
+
_emotionsController.lastMindDataCallback = OnRawMindDataReceived;
|
|
1082
|
+
_emotionsController.lastSpectralDataCallback = OnRawSpectralDataReceived;
|
|
1083
|
+
_emotionsController.isArtefactedSequenceCallback = OnEmotionsArtifactDetected;
|
|
1084
|
+
_emotionsController.isBothSidesArtifactedCallback = OnEmotionsArtifactDetected;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private void UnwireEmotionsCallbacks()
|
|
1088
|
+
{
|
|
1089
|
+
if (_emotionsController == null) return;
|
|
1090
|
+
|
|
1091
|
+
_emotionsController.progressCalibrationCallback = null;
|
|
1092
|
+
_emotionsController.lastMindDataCallback = null;
|
|
1093
|
+
_emotionsController.lastSpectralDataCallback = null;
|
|
1094
|
+
_emotionsController.isArtefactedSequenceCallback = null;
|
|
1095
|
+
_emotionsController.isBothSidesArtifactedCallback = null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private void OnEmotionsCalibrationProgress(int progress)
|
|
1099
|
+
{
|
|
1100
|
+
// 這些 callback 由 NeuroSDK/EmotionsController 在背景緒觸發
|
|
1101
|
+
_mainThreadActions.Enqueue(() =>
|
|
1102
|
+
{
|
|
1103
|
+
CalibrationProgress = progress;
|
|
1104
|
+
OnCalibrationProgress?.Invoke(progress);
|
|
1105
|
+
|
|
1106
|
+
if (progress >= 100 && !IsEmotionsCalibrated)
|
|
1107
|
+
{
|
|
1108
|
+
IsEmotionsCalibrated = true;
|
|
1109
|
+
OnCalibrationFinished?.Invoke();
|
|
1110
|
+
LabTools.Log("[BrainBit] Emotion calibration finished");
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private void OnRawMindDataReceived(MindData raw)
|
|
1116
|
+
{
|
|
1117
|
+
var wrapped = new BrainBit_MindData(raw);
|
|
1118
|
+
|
|
1119
|
+
_mainThreadActions.Enqueue(() =>
|
|
1120
|
+
{
|
|
1121
|
+
_lastMindData = wrapped;
|
|
1122
|
+
OnMindDataReceived?.Invoke(wrapped);
|
|
1123
|
+
|
|
1124
|
+
if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
|
|
1125
|
+
{
|
|
1126
|
+
LabDataManager.Instance.WriteData(wrapped, _currentMindTag);
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private void OnRawSpectralDataReceived(SpectralDataPercents raw)
|
|
1132
|
+
{
|
|
1133
|
+
var wrapped = new BrainBit_SpectralData(raw);
|
|
1134
|
+
|
|
1135
|
+
_mainThreadActions.Enqueue(() =>
|
|
1136
|
+
{
|
|
1137
|
+
_lastSpectralData = wrapped;
|
|
1138
|
+
OnSpectralDataReceived?.Invoke(wrapped);
|
|
1139
|
+
|
|
1140
|
+
if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
|
|
1141
|
+
{
|
|
1142
|
+
LabDataManager.Instance.WriteData(wrapped, _currentSpectralTag);
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
private void OnEmotionsArtifactDetected(bool hasArtifact)
|
|
1148
|
+
{
|
|
1149
|
+
_mainThreadActions.Enqueue(() => OnEmotionsArtifact?.Invoke(hasArtifact));
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
#endregion
|
|
802
1153
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
using SignalMath;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
public class EmotionalMathConfig
|
|
5
|
+
{
|
|
6
|
+
public static EmotionalMathConfig GetDefault(bool isBipolar, BrainBitConfig labConfig = null)
|
|
7
|
+
{
|
|
8
|
+
int samplingFrequencyHz = 250;
|
|
9
|
+
|
|
10
|
+
var mathLib = new MathLibSetting
|
|
11
|
+
{
|
|
12
|
+
sampling_rate = (uint)samplingFrequencyHz,
|
|
13
|
+
process_win_freq = 25,
|
|
14
|
+
fft_window = (uint)samplingFrequencyHz * 2,
|
|
15
|
+
n_first_sec_skipped = 4,
|
|
16
|
+
bipolar_mode = isBipolar,
|
|
17
|
+
squared_spectrum = true,
|
|
18
|
+
channels_number = (uint)1,
|
|
19
|
+
channel_for_analysis = 0
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
var artsDetect = new ArtifactDetectSetting
|
|
23
|
+
{
|
|
24
|
+
art_bord = 110,
|
|
25
|
+
allowed_percent_artpoints = 70,
|
|
26
|
+
raw_betap_limit = 800_000,
|
|
27
|
+
total_pow_border = (uint)(8 * 1e7),
|
|
28
|
+
global_artwin_sec = 4,
|
|
29
|
+
spect_art_by_totalp = false,
|
|
30
|
+
hanning_win_spectrum = false,
|
|
31
|
+
hamming_win_spectrum = true,
|
|
32
|
+
num_wins_for_quality_avg = 100
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
var shortArtsDetect = new ShortArtifactDetectSetting
|
|
36
|
+
{
|
|
37
|
+
ampl_art_detect_win_size = 200,
|
|
38
|
+
ampl_art_zerod_area = 200,
|
|
39
|
+
ampl_art_extremum_border = 25
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
var mentalAndSpectralSettings = new MentalAndSpectralSetting
|
|
43
|
+
{
|
|
44
|
+
n_sec_for_instant_estimation = 2,
|
|
45
|
+
n_sec_for_averaging = 4
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
var emConfigs = new EmotionalMathConfig(samplingFrequencyHz, mathLib, artsDetect, shortArtsDetect, mentalAndSpectralSettings);
|
|
49
|
+
|
|
50
|
+
if (labConfig != null)
|
|
51
|
+
{
|
|
52
|
+
emConfigs.CallibrationLength = labConfig.EmotionsCalibrationLength;
|
|
53
|
+
emConfigs.MentalEstimation = labConfig.EmotionsMentalEstimation;
|
|
54
|
+
emConfigs.PrioritySide = labConfig.EmotionsPrioritySide;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return emConfigs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public int SamplingFrequencyHz
|
|
61
|
+
{
|
|
62
|
+
get; set;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public MathLibSetting MathLib;
|
|
66
|
+
|
|
67
|
+
public ArtifactDetectSetting ArtifactDetect;
|
|
68
|
+
|
|
69
|
+
public ShortArtifactDetectSetting ShortArtifactDetect;
|
|
70
|
+
|
|
71
|
+
public MentalAndSpectralSetting MentalAndSpectral;
|
|
72
|
+
|
|
73
|
+
public bool MentalEstimation { get; set; } = false;
|
|
74
|
+
|
|
75
|
+
public SideType PrioritySide { get; set; } = SideType.NONE;
|
|
76
|
+
|
|
77
|
+
public int CallibrationLength { get; set; } = 6;
|
|
78
|
+
|
|
79
|
+
public int SkipWinsAfterArtifact { get; set; } = 5;
|
|
80
|
+
|
|
81
|
+
// ZeroSpectWaves
|
|
82
|
+
public bool Active { get; set; } = true;
|
|
83
|
+
public int alpha { get; set; } = 1;
|
|
84
|
+
public int beta { get; set; } = 1;
|
|
85
|
+
public int theta { get; set; } = 1;
|
|
86
|
+
public int delta { get; set; } = 0;
|
|
87
|
+
public int gamma { get; set; } = 0;
|
|
88
|
+
|
|
89
|
+
// WeightsForSpectra
|
|
90
|
+
public double delta_c { get; set; } = 1;
|
|
91
|
+
public double theta_c { get; set; } = 1;
|
|
92
|
+
public double alpha_c { get; set; } = 1;
|
|
93
|
+
public double beta_c { get; set; } = 1;
|
|
94
|
+
public double gamma_c { get; set; } = 1;
|
|
95
|
+
|
|
96
|
+
public bool SpectNormalizationByBandsWidth { get; set; }
|
|
97
|
+
|
|
98
|
+
public bool SpectNormalizationByCoeffs { get; set; }
|
|
99
|
+
|
|
100
|
+
public EmotionalMathConfig(
|
|
101
|
+
int samplingFrequencyHz,
|
|
102
|
+
MathLibSetting mathLib,
|
|
103
|
+
ArtifactDetectSetting artifactDetect,
|
|
104
|
+
ShortArtifactDetectSetting shortArtifactDetect,
|
|
105
|
+
MentalAndSpectralSetting mentalAndSpectral
|
|
106
|
+
)
|
|
107
|
+
{
|
|
108
|
+
SamplingFrequencyHz = samplingFrequencyHz;
|
|
109
|
+
MathLib = mathLib;
|
|
110
|
+
ArtifactDetect = artifactDetect;
|
|
111
|
+
ShortArtifactDetect = shortArtifactDetect;
|
|
112
|
+
MentalAndSpectral = mentalAndSpectral;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
using NeuroSDK;
|
|
2
|
+
using SignalMath;
|
|
3
|
+
using System;
|
|
4
|
+
using System.Linq;
|
|
5
|
+
|
|
6
|
+
public class EmotionsController
|
|
7
|
+
{
|
|
8
|
+
private readonly EegEmotionalMath _math;
|
|
9
|
+
|
|
10
|
+
public Action<int> progressCalibrationCallback = null;
|
|
11
|
+
public Action<bool> isArtefactedSequenceCallback = null;
|
|
12
|
+
public Action<bool> isBothSidesArtifactedCallback = null;
|
|
13
|
+
public Action<SpectralDataPercents> lastSpectralDataCallback = null;
|
|
14
|
+
public Action<RawSpectVals> rawSpectralDataCallback = null;
|
|
15
|
+
public Action<MindData> lastMindDataCallback = null;
|
|
16
|
+
|
|
17
|
+
private bool isCalibrated = false;
|
|
18
|
+
|
|
19
|
+
public EmotionsController(BrainBitConfig labConfig = null)
|
|
20
|
+
{
|
|
21
|
+
var config = EmotionalMathConfig.GetDefault(true, labConfig);
|
|
22
|
+
|
|
23
|
+
_math = new EegEmotionalMath(
|
|
24
|
+
config.MathLib,
|
|
25
|
+
config.ArtifactDetect,
|
|
26
|
+
config.ShortArtifactDetect,
|
|
27
|
+
config.MentalAndSpectral);
|
|
28
|
+
|
|
29
|
+
_math?.SetZeroSpectWaves(config.Active, config.delta, config.theta, config.alpha, config.beta, config.gamma);
|
|
30
|
+
_math?.SetWeightsForSpectra(config.delta_c, config.theta_c, config.alpha_c, config.beta_c, config.gamma_c);
|
|
31
|
+
_math?.SetCallibrationLength(config.CallibrationLength);
|
|
32
|
+
_math?.SetMentalEstimationMode(config.MentalEstimation);
|
|
33
|
+
_math?.SetPrioritySide(config.PrioritySide);
|
|
34
|
+
_math?.SetSkipWinsAfterArtifact(config.SkipWinsAfterArtifact);
|
|
35
|
+
_math?.SetSpectNormalizationByBandsWidth(config.SpectNormalizationByBandsWidth);
|
|
36
|
+
_math?.SetSpectNormalizationByCoeffs(config.SpectNormalizationByCoeffs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public void Dispose() { _math.Dispose(); }
|
|
40
|
+
|
|
41
|
+
public void StartCalibration()
|
|
42
|
+
{
|
|
43
|
+
isCalibrated = false;
|
|
44
|
+
_math.StartCalibration();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public void ProcessData(BrainBitSignalData[] samples)
|
|
48
|
+
{
|
|
49
|
+
var bipolarSamples = new RawChannels[samples.Length];
|
|
50
|
+
|
|
51
|
+
for (var i = 0; i < samples.Length; i++)
|
|
52
|
+
{
|
|
53
|
+
bipolarSamples[i].LeftBipolar = samples[i].T3 - samples[i].O1;
|
|
54
|
+
bipolarSamples[i].RightBipolar = samples[i].T4 - samples[i].O2;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try
|
|
58
|
+
{
|
|
59
|
+
_math.PushData(bipolarSamples);
|
|
60
|
+
_math.ProcessDataArr();
|
|
61
|
+
|
|
62
|
+
resolveArtefacted();
|
|
63
|
+
|
|
64
|
+
if (!isCalibrated)
|
|
65
|
+
{
|
|
66
|
+
processCalibration();
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
{
|
|
70
|
+
resolveSpectralData();
|
|
71
|
+
resolveRawSpectralData();
|
|
72
|
+
resolveMindData();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (Exception ex)
|
|
76
|
+
{
|
|
77
|
+
Console.WriteLine(ex.ToString());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private void resolveArtefacted()
|
|
82
|
+
{
|
|
83
|
+
// sequence artifacts
|
|
84
|
+
bool isArtifactedSequence = _math.IsArtifactedSequence();
|
|
85
|
+
isArtefactedSequenceCallback?.Invoke(isArtifactedSequence);
|
|
86
|
+
|
|
87
|
+
// both sides artifacts
|
|
88
|
+
bool isBothSideArtifacted = _math.IsBothSidesArtifacted();
|
|
89
|
+
isBothSidesArtifactedCallback?.Invoke(isBothSideArtifacted);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private void processCalibration()
|
|
93
|
+
{
|
|
94
|
+
bool wasCalibrated = isCalibrated;
|
|
95
|
+
isCalibrated = _math.CalibrationFinished();
|
|
96
|
+
|
|
97
|
+
if (!isCalibrated)
|
|
98
|
+
{
|
|
99
|
+
int progress = _math.GetCallibrationPercents();
|
|
100
|
+
progressCalibrationCallback?.Invoke(progress);
|
|
101
|
+
}
|
|
102
|
+
else if (!wasCalibrated)
|
|
103
|
+
{
|
|
104
|
+
// Transition: just finished calibrating — emit 100% once
|
|
105
|
+
progressCalibrationCallback?.Invoke(100);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private void resolveSpectralData()
|
|
110
|
+
{
|
|
111
|
+
var spectralValues = _math?.ReadSpectralDataPercentsArr();
|
|
112
|
+
if (spectralValues.Length > 0)
|
|
113
|
+
{
|
|
114
|
+
var spectralVal = spectralValues.Last();
|
|
115
|
+
//if(spectralVal.Delta > 0)
|
|
116
|
+
lastSpectralDataCallback?.Invoke(spectralValues.Last());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
private void resolveRawSpectralData()
|
|
120
|
+
{
|
|
121
|
+
var rawSpectralValues = _math.ReadRawSpectralVals();
|
|
122
|
+
rawSpectralDataCallback?.Invoke(rawSpectralValues);
|
|
123
|
+
}
|
|
124
|
+
private void resolveMindData()
|
|
125
|
+
{
|
|
126
|
+
var mentalValues = _math.ReadMentalDataArr();
|
|
127
|
+
if (mentalValues.Length > 0)
|
|
128
|
+
{
|
|
129
|
+
lastMindDataCallback?.Invoke(mentalValues.Last());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"references": [
|
|
5
5
|
"GUID:446fbb70571d4224d98108c0517d6b29",
|
|
6
6
|
"GUID:a5a806c53ddbe413484b9d0109675c29",
|
|
7
|
-
"GUID:6055be8ebefd69e48b49212b09b47b2f"
|
|
7
|
+
"GUID:6055be8ebefd69e48b49212b09b47b2f",
|
|
8
|
+
"GUID:920348d3c44c9b44493d16cb002928b8"
|
|
8
9
|
],
|
|
9
10
|
"includePlatforms": [],
|
|
10
11
|
"excludePlatforms": [],
|
package/package.json
CHANGED