com.xrlab.labframe_brainbit 1.0.9 → 1.1.1
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 +424 -48
- 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
|
}
|
|
@@ -5,6 +5,7 @@ using System.Collections.Concurrent;
|
|
|
5
5
|
using LabFrame2023;
|
|
6
6
|
using UnityEngine.Events;
|
|
7
7
|
using NeuroSDK;
|
|
8
|
+
using SignalMath;
|
|
8
9
|
using System;
|
|
9
10
|
|
|
10
11
|
/// <summary>
|
|
@@ -43,6 +44,21 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
43
44
|
/// 是否正在掃描設備
|
|
44
45
|
/// </summary>
|
|
45
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;
|
|
46
62
|
#endregion
|
|
47
63
|
|
|
48
64
|
#region Events
|
|
@@ -70,6 +86,31 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
70
86
|
/// 錯誤事件
|
|
71
87
|
/// </summary>
|
|
72
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;
|
|
73
114
|
#endregion
|
|
74
115
|
|
|
75
116
|
#region Private Fields
|
|
@@ -83,10 +124,12 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
83
124
|
private bool _autoWriteEEGData = false;
|
|
84
125
|
private bool _autoWriteImpedanceData = false;
|
|
85
126
|
|
|
86
|
-
private Coroutine _connectionMonitorCoroutine;
|
|
87
|
-
private Coroutine _scanTimeoutCoroutine;
|
|
88
|
-
|
|
89
|
-
private int _reconnectAttempts = 0;
|
|
127
|
+
private Coroutine _connectionMonitorCoroutine;
|
|
128
|
+
private Coroutine _scanTimeoutCoroutine;
|
|
129
|
+
|
|
130
|
+
private int _reconnectAttempts = 0;
|
|
131
|
+
private bool _isInitialized = false;
|
|
132
|
+
private bool _autoConnectScheduled = false;
|
|
90
133
|
|
|
91
134
|
// 用於記錄目前 EEG 寫入資料的標籤 (對應不同的儲存檔案)
|
|
92
135
|
private string _currentEEGTag = "eeg";
|
|
@@ -95,6 +138,16 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
95
138
|
|
|
96
139
|
// 主執行緒派發佇列,用於將背景執行緒的回呼安全地轉移到主執行緒執行
|
|
97
140
|
private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();
|
|
141
|
+
|
|
142
|
+
// === Emotions ===
|
|
143
|
+
private EmotionsController _emotionsController;
|
|
144
|
+
private bool _autoWriteEmotionData = false;
|
|
145
|
+
private string _currentMindTag = "mind";
|
|
146
|
+
private string _currentSpectralTag = "spectral";
|
|
147
|
+
private BrainBit_MindData _lastMindData;
|
|
148
|
+
private BrainBit_SpectralData _lastSpectralData;
|
|
149
|
+
// 若為 true,代表 EEG 串流是被情緒處理自動啟動的 — StopEmotionsProcessing 時要一起停
|
|
150
|
+
private bool _emotionsStartedEEG = false;
|
|
98
151
|
#endregion
|
|
99
152
|
|
|
100
153
|
#region IManager Implementation
|
|
@@ -103,20 +156,21 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
103
156
|
LabTools.Log("[BrainBit] Initializing BrainBit Manager...");
|
|
104
157
|
|
|
105
158
|
// 載入配置
|
|
106
|
-
|
|
159
|
+
if (!EnsureInitialized(nameof(ManagerInit)))
|
|
160
|
+
{
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
107
163
|
|
|
108
164
|
// 初始化數據對象
|
|
109
|
-
_lastEEGData = new BrainBit_EEGData();
|
|
110
|
-
_lastImpedanceData = new BrainBit_ImpedanceData();
|
|
111
165
|
|
|
112
166
|
// 開始連接監控
|
|
113
|
-
_connectionMonitorCoroutine = StartCoroutine(MonitorConnection());
|
|
114
167
|
|
|
115
168
|
// 自動連接
|
|
116
|
-
if (_config.AutoConnectOnInit)
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
169
|
+
if (_config.AutoConnectOnInit && !_autoConnectScheduled && !IsConnected && !IsScanning)
|
|
170
|
+
{
|
|
171
|
+
_autoConnectScheduled = true;
|
|
172
|
+
StartCoroutine(DelayedAutoConnect());
|
|
173
|
+
}
|
|
120
174
|
|
|
121
175
|
LabTools.Log("[BrainBit] Manager initialized successfully");
|
|
122
176
|
LabTools.Log($"[BrainBit] Config - AutoConnect: {_config.AutoConnectOnInit}, ScanTimeout: {_config.ScanTimeoutSeconds}s");
|
|
@@ -146,6 +200,7 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
146
200
|
LabTools.Log("[BrainBit] Disposing BrainBit Manager...");
|
|
147
201
|
|
|
148
202
|
// 停止所有數據流
|
|
203
|
+
StopEmotionsProcessing();
|
|
149
204
|
StopEEGStream();
|
|
150
205
|
StopImpedanceStream();
|
|
151
206
|
|
|
@@ -180,17 +235,59 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
180
235
|
}
|
|
181
236
|
#endregion
|
|
182
237
|
|
|
183
|
-
#region Public Methods
|
|
184
|
-
|
|
238
|
+
#region Public Methods
|
|
239
|
+
private bool EnsureInitialized(string callerName)
|
|
240
|
+
{
|
|
241
|
+
if (_isInitialized && _config != null)
|
|
242
|
+
{
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try
|
|
247
|
+
{
|
|
248
|
+
_config ??= LabTools.GetConfig<BrainBitConfig>(true);
|
|
249
|
+
if (_config == null)
|
|
250
|
+
{
|
|
251
|
+
LabTools.LogError($"[BrainBit] {callerName} failed - BrainBitConfig is missing");
|
|
252
|
+
OnError?.Invoke("BrainBit config is missing");
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_lastEEGData ??= new BrainBit_EEGData();
|
|
257
|
+
_lastImpedanceData ??= new BrainBit_ImpedanceData();
|
|
258
|
+
|
|
259
|
+
if (_connectionMonitorCoroutine == null)
|
|
260
|
+
{
|
|
261
|
+
_connectionMonitorCoroutine = StartCoroutine(MonitorConnection());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_isInitialized = true;
|
|
265
|
+
LabTools.Log($"[BrainBit] Initialization ready for {callerName}");
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
catch (Exception e)
|
|
269
|
+
{
|
|
270
|
+
LabTools.LogError($"[BrainBit] {callerName} failed during initialization: {e.Message}");
|
|
271
|
+
OnError?.Invoke($"Initialization failed: {e.Message}");
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// <summary>
|
|
185
277
|
/// 開始掃描 BrainBit 設備
|
|
186
278
|
/// </summary>
|
|
187
|
-
public void StartScan()
|
|
188
|
-
{
|
|
189
|
-
if (
|
|
190
|
-
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
279
|
+
public void StartScan()
|
|
280
|
+
{
|
|
281
|
+
if (!EnsureInitialized(nameof(StartScan)))
|
|
282
|
+
{
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (IsScanning)
|
|
287
|
+
{
|
|
288
|
+
LabTools.LogError("[BrainBit] Already scanning for devices");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
194
291
|
|
|
195
292
|
try
|
|
196
293
|
{
|
|
@@ -525,10 +622,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
525
622
|
}
|
|
526
623
|
}
|
|
527
624
|
|
|
528
|
-
private IEnumerator ScanTimeout()
|
|
529
|
-
{
|
|
530
|
-
|
|
531
|
-
|
|
625
|
+
private IEnumerator ScanTimeout()
|
|
626
|
+
{
|
|
627
|
+
if (!EnsureInitialized(nameof(ScanTimeout)))
|
|
628
|
+
{
|
|
629
|
+
IsScanning = false;
|
|
630
|
+
_scanTimeoutCoroutine = null;
|
|
631
|
+
yield break;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
float timeoutSeconds = _config.ScanTimeoutSeconds;
|
|
635
|
+
LabTools.Log($"[BrainBit] Scan timeout set for {timeoutSeconds} seconds");
|
|
636
|
+
yield return new WaitForSeconds(timeoutSeconds);
|
|
532
637
|
|
|
533
638
|
if (_scanTimeoutCoroutine != null && _currentSensor == null)
|
|
534
639
|
{
|
|
@@ -629,12 +734,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
629
734
|
/// 延遲連接設備
|
|
630
735
|
/// </summary>
|
|
631
736
|
/// <param name="sensorInfo">設備信息</param>
|
|
632
|
-
private IEnumerator DelayedConnect(SensorInfo sensorInfo)
|
|
633
|
-
{
|
|
634
|
-
|
|
737
|
+
private IEnumerator DelayedConnect(SensorInfo sensorInfo)
|
|
738
|
+
{
|
|
739
|
+
if (!EnsureInitialized(nameof(DelayedConnect)))
|
|
740
|
+
{
|
|
741
|
+
yield break;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
float connectDelaySeconds = _config.ConnectDelaySeconds;
|
|
745
|
+
LabTools.Log($"[BrainBit] Waiting {connectDelaySeconds}s before connecting...");
|
|
635
746
|
|
|
636
747
|
// 等待指定時間,確保掃描完全停止
|
|
637
|
-
yield return new WaitForSeconds(
|
|
748
|
+
yield return new WaitForSeconds(connectDelaySeconds);
|
|
638
749
|
|
|
639
750
|
// 建立連接
|
|
640
751
|
ConnectToDevice(sensorInfo);
|
|
@@ -698,6 +809,7 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
698
809
|
if (IsConnected)
|
|
699
810
|
{
|
|
700
811
|
// 停止所有數據流
|
|
812
|
+
StopEmotionsProcessing();
|
|
701
813
|
StopEEGStream();
|
|
702
814
|
StopImpedanceStream();
|
|
703
815
|
|
|
@@ -724,12 +836,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
724
836
|
}
|
|
725
837
|
}
|
|
726
838
|
|
|
727
|
-
private void HandleDisconnection()
|
|
728
|
-
{
|
|
729
|
-
LabTools.LogError($"[BrainBit] Device disconnected: {ConnectedDeviceName}");
|
|
730
|
-
|
|
731
|
-
if (
|
|
732
|
-
{
|
|
839
|
+
private void HandleDisconnection()
|
|
840
|
+
{
|
|
841
|
+
LabTools.LogError($"[BrainBit] Device disconnected: {ConnectedDeviceName}");
|
|
842
|
+
|
|
843
|
+
if (!EnsureInitialized(nameof(HandleDisconnection)))
|
|
844
|
+
{
|
|
845
|
+
Disconnect();
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (_config.DisconnectNotification)
|
|
850
|
+
{
|
|
733
851
|
LabPromptBox.Show($"BrainBit 設備已斷線!\nDevice {ConnectedDeviceName} disconnected!");
|
|
734
852
|
}
|
|
735
853
|
|
|
@@ -742,12 +860,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
742
860
|
Disconnect();
|
|
743
861
|
}
|
|
744
862
|
|
|
745
|
-
private IEnumerator AttemptReconnect()
|
|
746
|
-
{
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
863
|
+
private IEnumerator AttemptReconnect()
|
|
864
|
+
{
|
|
865
|
+
if (!EnsureInitialized(nameof(AttemptReconnect)))
|
|
866
|
+
{
|
|
867
|
+
yield break;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
_reconnectAttempts++;
|
|
871
|
+
LabTools.Log($"[BrainBit] Attempting reconnection ({_reconnectAttempts}/{_config.AutoReconnectAttempts})...");
|
|
872
|
+
|
|
873
|
+
float reconnectIntervalSeconds = _config.ReconnectIntervalSeconds;
|
|
874
|
+
yield return new WaitForSeconds(reconnectIntervalSeconds);
|
|
751
875
|
|
|
752
876
|
if (!IsConnected)
|
|
753
877
|
{
|
|
@@ -778,6 +902,12 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
778
902
|
}
|
|
779
903
|
});
|
|
780
904
|
}
|
|
905
|
+
|
|
906
|
+
// 情緒處理(寄生在同一條 NeuroSDK 背景緒,不開額外 thread)
|
|
907
|
+
if (IsProcessingEmotions)
|
|
908
|
+
{
|
|
909
|
+
_emotionsController?.ProcessData(data);
|
|
910
|
+
}
|
|
781
911
|
}
|
|
782
912
|
catch (Exception e)
|
|
783
913
|
{
|
|
@@ -838,13 +968,25 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
838
968
|
_currentSensor = null;
|
|
839
969
|
}
|
|
840
970
|
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
971
|
+
// 清理情緒控制器
|
|
972
|
+
if (_emotionsController != null)
|
|
973
|
+
{
|
|
974
|
+
UnwireEmotionsCallbacks();
|
|
975
|
+
_emotionsController.Dispose();
|
|
976
|
+
_emotionsController = null;
|
|
977
|
+
}
|
|
978
|
+
IsProcessingEmotions = false;
|
|
979
|
+
IsEmotionsCalibrated = false;
|
|
846
980
|
|
|
847
|
-
|
|
981
|
+
// 重置狀態
|
|
982
|
+
IsConnected = false;
|
|
983
|
+
IsStreamingEEG = false;
|
|
984
|
+
IsStreamingImpedance = false;
|
|
985
|
+
IsScanning = false;
|
|
986
|
+
_isInitialized = false;
|
|
987
|
+
_autoConnectScheduled = false;
|
|
988
|
+
|
|
989
|
+
LabTools.Log("[BrainBit] Resources cleaned up");
|
|
848
990
|
}
|
|
849
991
|
catch (Exception e)
|
|
850
992
|
{
|
|
@@ -852,4 +994,238 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
|
|
|
852
994
|
}
|
|
853
995
|
}
|
|
854
996
|
#endregion
|
|
855
|
-
|
|
997
|
+
|
|
998
|
+
#region Emotions Processing
|
|
999
|
+
|
|
1000
|
+
/// <summary>
|
|
1001
|
+
/// 啟動情緒處理(MindData / SpectralData / 校正進度)。
|
|
1002
|
+
/// 若 EEG 串流未啟動,會自動啟動;呼叫 StopEmotionsProcessing 時會同步停止該 EEG 串流。
|
|
1003
|
+
/// </summary>
|
|
1004
|
+
public void StartEmotionsProcessing(bool autoWriteToLabData = true,
|
|
1005
|
+
string mindTag = "mind",
|
|
1006
|
+
string spectralTag = "spectral")
|
|
1007
|
+
{
|
|
1008
|
+
if (!EnsureInitialized(nameof(StartEmotionsProcessing)))
|
|
1009
|
+
{
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (!IsConnected)
|
|
1014
|
+
{
|
|
1015
|
+
LabTools.LogError("[BrainBit] Device not connected");
|
|
1016
|
+
OnError?.Invoke("Device not connected");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (IsProcessingEmotions)
|
|
1021
|
+
{
|
|
1022
|
+
LabTools.LogError("[BrainBit] Emotions processing already active");
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
try
|
|
1027
|
+
{
|
|
1028
|
+
_autoWriteEmotionData = autoWriteToLabData;
|
|
1029
|
+
_currentMindTag = string.IsNullOrEmpty(mindTag) ? "mind" : mindTag;
|
|
1030
|
+
_currentSpectralTag = string.IsNullOrEmpty(spectralTag) ? "spectral" : spectralTag;
|
|
1031
|
+
|
|
1032
|
+
// 若 EEG 未啟動,自動啟動並記錄
|
|
1033
|
+
if (!IsStreamingEEG)
|
|
1034
|
+
{
|
|
1035
|
+
StartEEGStream(autoWriteToLabData: false);
|
|
1036
|
+
_emotionsStartedEEG = true;
|
|
1037
|
+
}
|
|
1038
|
+
else
|
|
1039
|
+
{
|
|
1040
|
+
_emotionsStartedEEG = false;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// 建立 / 重建 controller(套用目前 BrainBitConfig)
|
|
1044
|
+
_emotionsController?.Dispose();
|
|
1045
|
+
_emotionsController = new EmotionsController(_config);
|
|
1046
|
+
WireEmotionsCallbacks();
|
|
1047
|
+
|
|
1048
|
+
// 校正狀態重置
|
|
1049
|
+
IsEmotionsCalibrated = false;
|
|
1050
|
+
CalibrationProgress = 0;
|
|
1051
|
+
_lastMindData = null;
|
|
1052
|
+
_lastSpectralData = null;
|
|
1053
|
+
|
|
1054
|
+
_emotionsController.StartCalibration();
|
|
1055
|
+
IsProcessingEmotions = true;
|
|
1056
|
+
|
|
1057
|
+
LabTools.Log($"[BrainBit] Emotions processing started (mindTag: {_currentMindTag}, spectralTag: {_currentSpectralTag})");
|
|
1058
|
+
}
|
|
1059
|
+
catch (Exception e)
|
|
1060
|
+
{
|
|
1061
|
+
if (_emotionsStartedEEG)
|
|
1062
|
+
{
|
|
1063
|
+
StopEEGStream();
|
|
1064
|
+
_emotionsStartedEEG = false;
|
|
1065
|
+
}
|
|
1066
|
+
LabTools.LogError($"[BrainBit] Failed to start emotions processing: {e.Message}");
|
|
1067
|
+
OnError?.Invoke($"Emotions processing failed: {e.Message}");
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/// <summary>
|
|
1072
|
+
/// 停止情緒處理。若本次 EEG 串流是由情緒處理自動啟動的,同步停止該 EEG 串流;
|
|
1073
|
+
/// 否則保留 EEG 串流(避免誤停使用者正在錄的 EEG)。
|
|
1074
|
+
/// </summary>
|
|
1075
|
+
public void StopEmotionsProcessing()
|
|
1076
|
+
{
|
|
1077
|
+
if (!IsProcessingEmotions) return;
|
|
1078
|
+
|
|
1079
|
+
try
|
|
1080
|
+
{
|
|
1081
|
+
IsProcessingEmotions = false;
|
|
1082
|
+
|
|
1083
|
+
if (_emotionsController != null)
|
|
1084
|
+
{
|
|
1085
|
+
UnwireEmotionsCallbacks();
|
|
1086
|
+
_emotionsController.Dispose();
|
|
1087
|
+
_emotionsController = null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
_autoWriteEmotionData = false;
|
|
1091
|
+
IsEmotionsCalibrated = false;
|
|
1092
|
+
CalibrationProgress = 0;
|
|
1093
|
+
|
|
1094
|
+
if (_emotionsStartedEEG && IsStreamingEEG)
|
|
1095
|
+
{
|
|
1096
|
+
StopEEGStream();
|
|
1097
|
+
}
|
|
1098
|
+
_emotionsStartedEEG = false;
|
|
1099
|
+
|
|
1100
|
+
LabTools.Log("[BrainBit] Emotions processing stopped");
|
|
1101
|
+
}
|
|
1102
|
+
catch (Exception e)
|
|
1103
|
+
{
|
|
1104
|
+
LabTools.LogError($"[BrainBit] Error stopping emotions processing: {e.Message}");
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/// <summary>
|
|
1109
|
+
/// 重新校正(例如換受測者、中途摘下又戴回)。
|
|
1110
|
+
/// </summary>
|
|
1111
|
+
public void RestartCalibration()
|
|
1112
|
+
{
|
|
1113
|
+
if (!IsProcessingEmotions || _emotionsController == null)
|
|
1114
|
+
{
|
|
1115
|
+
LabTools.LogError("[BrainBit] Cannot restart calibration - emotions processing not active");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
IsEmotionsCalibrated = false;
|
|
1120
|
+
CalibrationProgress = 0;
|
|
1121
|
+
_emotionsController.StartCalibration();
|
|
1122
|
+
|
|
1123
|
+
LabTools.Log("[BrainBit] Emotion calibration restarted");
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/// <summary>
|
|
1127
|
+
/// 動態切換 MindData 寫入 Tag
|
|
1128
|
+
/// </summary>
|
|
1129
|
+
public void SetMindTag(string tag)
|
|
1130
|
+
{
|
|
1131
|
+
_currentMindTag = string.IsNullOrEmpty(tag) ? "mind" : tag;
|
|
1132
|
+
LabTools.Log($"[BrainBit] Mind data tag dynamically changed to: {_currentMindTag}");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/// <summary>
|
|
1136
|
+
/// 動態切換 SpectralData 寫入 Tag
|
|
1137
|
+
/// </summary>
|
|
1138
|
+
public void SetSpectralTag(string tag)
|
|
1139
|
+
{
|
|
1140
|
+
_currentSpectralTag = string.IsNullOrEmpty(tag) ? "spectral" : tag;
|
|
1141
|
+
LabTools.Log($"[BrainBit] Spectral data tag dynamically changed to: {_currentSpectralTag}");
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/// <summary>
|
|
1145
|
+
/// 取得最新的情緒/心智資料。校正完成前回傳 null。
|
|
1146
|
+
/// </summary>
|
|
1147
|
+
public BrainBit_MindData GetLatestMindData() => _lastMindData;
|
|
1148
|
+
|
|
1149
|
+
/// <summary>
|
|
1150
|
+
/// 取得最新的五頻段光譜資料。校正完成前回傳 null。
|
|
1151
|
+
/// </summary>
|
|
1152
|
+
public BrainBit_SpectralData GetLatestSpectralData() => _lastSpectralData;
|
|
1153
|
+
|
|
1154
|
+
private void WireEmotionsCallbacks()
|
|
1155
|
+
{
|
|
1156
|
+
if (_emotionsController == null) return;
|
|
1157
|
+
|
|
1158
|
+
_emotionsController.progressCalibrationCallback = OnEmotionsCalibrationProgress;
|
|
1159
|
+
_emotionsController.lastMindDataCallback = OnRawMindDataReceived;
|
|
1160
|
+
_emotionsController.lastSpectralDataCallback = OnRawSpectralDataReceived;
|
|
1161
|
+
_emotionsController.isArtefactedSequenceCallback = OnEmotionsArtifactDetected;
|
|
1162
|
+
_emotionsController.isBothSidesArtifactedCallback = OnEmotionsArtifactDetected;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
private void UnwireEmotionsCallbacks()
|
|
1166
|
+
{
|
|
1167
|
+
if (_emotionsController == null) return;
|
|
1168
|
+
|
|
1169
|
+
_emotionsController.progressCalibrationCallback = null;
|
|
1170
|
+
_emotionsController.lastMindDataCallback = null;
|
|
1171
|
+
_emotionsController.lastSpectralDataCallback = null;
|
|
1172
|
+
_emotionsController.isArtefactedSequenceCallback = null;
|
|
1173
|
+
_emotionsController.isBothSidesArtifactedCallback = null;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private void OnEmotionsCalibrationProgress(int progress)
|
|
1177
|
+
{
|
|
1178
|
+
// 這些 callback 由 NeuroSDK/EmotionsController 在背景緒觸發
|
|
1179
|
+
_mainThreadActions.Enqueue(() =>
|
|
1180
|
+
{
|
|
1181
|
+
CalibrationProgress = progress;
|
|
1182
|
+
OnCalibrationProgress?.Invoke(progress);
|
|
1183
|
+
|
|
1184
|
+
if (progress >= 100 && !IsEmotionsCalibrated)
|
|
1185
|
+
{
|
|
1186
|
+
IsEmotionsCalibrated = true;
|
|
1187
|
+
OnCalibrationFinished?.Invoke();
|
|
1188
|
+
LabTools.Log("[BrainBit] Emotion calibration finished");
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
private void OnRawMindDataReceived(MindData raw)
|
|
1194
|
+
{
|
|
1195
|
+
var wrapped = new BrainBit_MindData(raw);
|
|
1196
|
+
|
|
1197
|
+
_mainThreadActions.Enqueue(() =>
|
|
1198
|
+
{
|
|
1199
|
+
_lastMindData = wrapped;
|
|
1200
|
+
OnMindDataReceived?.Invoke(wrapped);
|
|
1201
|
+
|
|
1202
|
+
if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
|
|
1203
|
+
{
|
|
1204
|
+
LabDataManager.Instance.WriteData(wrapped, _currentMindTag);
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private void OnRawSpectralDataReceived(SpectralDataPercents raw)
|
|
1210
|
+
{
|
|
1211
|
+
var wrapped = new BrainBit_SpectralData(raw);
|
|
1212
|
+
|
|
1213
|
+
_mainThreadActions.Enqueue(() =>
|
|
1214
|
+
{
|
|
1215
|
+
_lastSpectralData = wrapped;
|
|
1216
|
+
OnSpectralDataReceived?.Invoke(wrapped);
|
|
1217
|
+
|
|
1218
|
+
if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
|
|
1219
|
+
{
|
|
1220
|
+
LabDataManager.Instance.WriteData(wrapped, _currentSpectralTag);
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private void OnEmotionsArtifactDetected(bool hasArtifact)
|
|
1226
|
+
{
|
|
1227
|
+
_mainThreadActions.Enqueue(() => OnEmotionsArtifact?.Invoke(hasArtifact));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
#endregion
|
|
1231
|
+
}
|
|
@@ -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